├── .gitattributes ├── Visualization ├── step.txt ├── plot_inertial_frame.m ├── gps.txt ├── r2q.m ├── q2r.m ├── rotmtx2angle.m ├── angle2rotmtx.m ├── plot_google_map.m └── main_script.m ├── screenshot.png ├── data_visualization.png ├── CoreLocationMotion-Data-Logger ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 40.png │ │ ├── 50.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 1024.png │ │ └── Contents.json │ └── body_frame_definition.imageset │ │ ├── body_frame_definition.png │ │ └── Contents.json ├── UsefulFunctions.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── AppDelegate.swift └── ViewController.swift ├── CoreLocationMotion-Data-Logger.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── project.pbxproj ├── LICENSE ├── .gitignore └── README.markdown /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.swift linguist-vendored=false 3 | -------------------------------------------------------------------------------- /Visualization/step.txt: -------------------------------------------------------------------------------- 1 | # Created at 2019-06-11 17:53:34 in KST in Seoul South Korea 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/screenshot.png -------------------------------------------------------------------------------- /data_visualization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/data_visualization.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/body_frame_definition.imageset/body_frame_definition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyojinKim/CoreLocationMotion-Data-Logger/HEAD/CoreLocationMotion-Data-Logger/Assets.xcassets/body_frame_definition.imageset/body_frame_definition.png -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/body_frame_definition.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "body_frame_definition.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Visualization/plot_inertial_frame.m: -------------------------------------------------------------------------------- 1 | function plot_inertial_frame(magnitude) 2 | 3 | 4 | % center point of inertial frame 5 | X = 0; 6 | Y = 0; 7 | Z = 0; 8 | 9 | 10 | % [X Y Z] axis end points of inertial frame 11 | X_axis = [magnitude;0;0]; 12 | Y_axis = [0;magnitude;0]; 13 | Z_axis = [0;0;magnitude]; 14 | 15 | 16 | % draw inertial frame 17 | line([X_axis(1) X],[X_axis(2) Y],[X_axis(3) Z],'Color','r','LineWidth',4) 18 | line([Y_axis(1) X],[Y_axis(2) Y],[Y_axis(3) Z],'Color','g','LineWidth',4) 19 | line([Z_axis(1) X],[Z_axis(2) Y],[Z_axis(3) Z],'Color','b','LineWidth',4) 20 | 21 | 22 | end -------------------------------------------------------------------------------- /Visualization/gps.txt: -------------------------------------------------------------------------------- 1 | # Created at 2019-06-11 17:53:34 in KST in Seoul South Korea 2 | 1559524370805430784 37.577586 127.000719 65.000000 49.917660 nan 10.000000 3 | 1559524377537879040 37.577602 127.000680 65.000000 49.658478 nan 10.000000 4 | 1559524378753008896 37.577602 127.000747 65.000000 49.971073 nan 10.000000 5 | 1559524385161470976 37.577589 127.000724 65.000000 49.705128 nan 10.000000 6 | 1559524391571050240 37.577607 127.000595 65.000000 50.232967 nan 10.000000 7 | 1559524392924622080 37.577598 127.000759 65.000000 50.363014 nan 10.000000 8 | 1559524399328719104 37.577573 127.000740 65.000000 50.266418 nan 10.000000 9 | 1559524405752304128 37.577577 127.000713 65.000000 50.352654 nan 10.000000 -------------------------------------------------------------------------------- /Visualization/r2q.m: -------------------------------------------------------------------------------- 1 | function [q] = r2q( R ) 2 | % Project: Patch-based illumination-variant DVO 3 | % Function: r2q 4 | % 5 | % Description: 6 | % This function convert from rotation matrix to unit orientation quaternion 7 | % 8 | % Example: 9 | % OUTPUT: 10 | % quatVector: quaternion vector composed of [qw qx qy qz] 11 | % 12 | % INPUT: 13 | % R = rotation matrix (3x3) defined as [inertial frame] = R * [body frame] (R = R_gb) 14 | % 15 | % NOTE: 16 | % 17 | % Author: Pyojin Kim 18 | % Email: pjinkim1215@gmail.com 19 | % Website: 20 | % 21 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 22 | % log: 23 | % 2015-02-06: Complete 24 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 25 | % 26 | 27 | q1 = 0.5 * sqrt(1+R(1,1)+R(2,2)+R(3,3)); 28 | q2 = (1/(4*q1))*(R(3,2)-R(2,3)); 29 | q3 = (1/(4*q1))*(R(1,3)-R(3,1)); 30 | q4 = (1/(4*q1))*(R(2,1)-R(1,2)); 31 | 32 | q = [q1;q2;q3;q4]; 33 | 34 | end 35 | 36 | -------------------------------------------------------------------------------- /Visualization/q2r.m: -------------------------------------------------------------------------------- 1 | function R = q2r( q ) 2 | % Project: Patch-based illumination-variant DVO 3 | % Function: q2r 4 | % 5 | % Description: 6 | % This function convert from unit orientation quaternion to rotation matrix 7 | % 8 | % Example: 9 | % OUTPUT: 10 | % R = rotation matrix (3x3) defined as [inertial frame] = R * [body frame] (R = R_gb) 11 | % 12 | % INPUT: 13 | % quatVector: quaternion vector composed of [qw qx qy qz] 14 | % 15 | % NOTE: 16 | % 17 | % Author: Pyojin Kim 18 | % Email: pjinkim1215@gmail.com 19 | % Website: 20 | % 21 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 22 | % log: 23 | % 2015-02-06: Complete 24 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 25 | % 26 | 27 | % quaternion to dcm 28 | q = q/norm(q); 29 | 30 | a = q(1); 31 | b = q(2); 32 | c = q(3); 33 | d = q(4); 34 | R=[ a*a+b*b-c*c-d*d, 2*(b*c-a*d), 2*(b*d+a*c); 35 | 2*(b*c+a*d), a*a-b*b+c*c-d*d, 2*(c*d-a*b); 36 | 2*(b*d-a*c), 2*(c*d+a*b), a*a-b*b-c*c+d*d; ]; 37 | 38 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pyojin Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/UsefulFunctions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsefulFunctions.swift 3 | // CoreLocationMotion-Data-Logger 4 | // 5 | // Created by kimpyojin on 30/05/2019. 6 | // Copyright © 2019 Pyojin Kim. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | func interfaceIntTime(second: Int64) -> String { 13 | var input = second 14 | let hours: Int64 = input / 3600 15 | input = input % 3600 16 | let mins: Int64 = input / 60 17 | let secs: Int64 = input % 60 18 | 19 | guard second >= 0 else { 20 | fatalError("Second can not be negative: \(second)"); 21 | } 22 | 23 | return String(format: "%02d:%02d:%02d", hours, mins, secs) 24 | } 25 | 26 | func timeToString() -> String { 27 | let date = Date() 28 | let calendar = Calendar.current 29 | let year = calendar.component(.year, from: date) 30 | let month = calendar.component(.month, from: date) 31 | let day = calendar.component(.day, from: date) 32 | let hour = calendar.component(.hour, from: date) 33 | let minute = calendar.component(.minute, from: date) 34 | let sec = calendar.component(.second, from: date) 35 | return String(format:"%04d-%02d-%02d %02d:%02d:%02d in PDT", year, month, day, hour, minute, sec) 36 | } 37 | -------------------------------------------------------------------------------- /Visualization/rotmtx2angle.m: -------------------------------------------------------------------------------- 1 | function [eulerAngle]= rotmtx2angle( R ) 2 | % Project: Patch-based Illumination invariant Visual Odometry (PIVO) 3 | % Function: rotmtx2angle 4 | % 5 | % Description: 6 | % This function return the euler angle along x,y and z direction 7 | % from rotation matrix to [phi;theta;psi] angle defined as ZYX sequence 8 | % 9 | % Example: 10 | % OUTPUT: 11 | % eulerAngle: angle vector composed of [phi;theta;psi] 12 | % phi = Rotation angle along x direction in radians 13 | % theta = Rotation angle along y direction in radians 14 | % psi = Rotation angle along z direction in radians 15 | % 16 | % INPUT: 17 | % R = rotation matrix (3x3) defined as [body frame] = R * [inertial frame] (R = R_bg) 18 | % 19 | % NOTE: 20 | % 21 | % Author: Pyojin Kim 22 | % Email: pjinkim1215@gmail.com 23 | % Website: 24 | % 25 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 26 | % log: 27 | % 2017-02-06: Complete 28 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 29 | % 30 | 31 | % change rotMtxBody / [Inertial frame] = rotMtxBody * [Body frame] 32 | rotMtxBody = R.'; 33 | 34 | phi=atan2( rotMtxBody(3,2) , rotMtxBody(3,3) ); 35 | theta=asin( -rotMtxBody(3,1) ); 36 | psi=atan2( rotMtxBody(2,1) , rotMtxBody(1,1) ); 37 | 38 | eulerAngle = [phi;theta;psi]; 39 | 40 | end 41 | 42 | 43 | -------------------------------------------------------------------------------- /Visualization/angle2rotmtx.m: -------------------------------------------------------------------------------- 1 | function [ R ] = angle2rotmtx(eulerAngle) 2 | % Project: Patch-based Illumination invariant Visual Odometry (PIVO) 3 | % Function: angle2rotmtx 4 | % 5 | % Description: 6 | % This function return the rotation matrix rotMtx 7 | % [Body frame] = rotMtx * [Inertial frame] 8 | % from [phi;theta;psi] angle defined as ZYX sequence to rotation matrix 9 | % 10 | % Example: 11 | % OUTPUT: 12 | % R = rotation matrix (3x3) defined as [body frame] = R * [inertial frame] (R = R_bg) 13 | % 14 | % INPUT: 15 | % eulerAngle: angle vector composed of [phi;theta;psi] 16 | % phi = Rotation angle along x direction in radians 17 | % theta = Rotation angle along y direction in radians 18 | % psi = Rotation angle along z direction in radians 19 | % 20 | % NOTE: 21 | % 22 | % Author: Pyojin Kim 23 | % Email: pjinkim1215@gmail.com 24 | % Website: 25 | % 26 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 27 | % log: 28 | % 2016-08-20: 29 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 30 | % 31 | 32 | % assign roll, pitch, yaw values 33 | phi = eulerAngle(1); 34 | theta = eulerAngle(2); 35 | psi = eulerAngle(3); 36 | 37 | R_x = [1 0 0;0 cos(phi) -sin(phi);0 sin(phi) cos(phi)]; 38 | 39 | R_y = [cos(theta) 0 sin(theta);0 1 0;-sin(theta) 0 cos(theta)]; 40 | 41 | R_z = [cos(psi) -sin(psi) 0; sin(psi) cos(psi) 0; 0 0 1]; 42 | 43 | rotMtxBody = [R_z * R_y * R_x]; % [Inertial frame] = rotMtxBody * [Body frame] 44 | 45 | R = rotMtxBody.'; % [Body frame] = rotMtx * [Inertial frame] 46 | 47 | end 48 | 49 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | Visualization/api_key.mat 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots/**/*.png 69 | fastlane/test_output 70 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSLocationAlwaysUsageDescription 6 | We need your location for anytime, anywhere localization. 7 | NSLocationUsageDescription 8 | We need your location for anytime, anywhere localization. 9 | NSLocationAlwaysAndWhenInUseUsageDescription 10 | We need your location for anytime, anywhere localization. 11 | NSLocationWhenInUseUsageDescription 12 | We need your location for anytime, anywhere localization. 13 | NSMotionUsageDescription 14 | We need your location for anytime, anywhere localization. 15 | CFBundleDevelopmentRegion 16 | $(DEVELOPMENT_LANGUAGE) 17 | CFBundleExecutable 18 | $(EXECUTABLE_NAME) 19 | CFBundleIdentifier 20 | $(PRODUCT_BUNDLE_IDENTIFIER) 21 | CFBundleInfoDictionaryVersion 22 | 6.0 23 | CFBundleName 24 | $(PRODUCT_NAME) 25 | CFBundlePackageType 26 | APPL 27 | CFBundleShortVersionString 28 | 1.0 29 | CFBundleVersion 30 | 1 31 | LSRequiresIPhoneOS 32 | 33 | UILaunchStoryboardName 34 | LaunchScreen 35 | UIMainStoryboardFile 36 | Main 37 | UIRequiredDeviceCapabilities 38 | 39 | armv7 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CoreLocationMotion-Data-Logger 4 | // 5 | // Created by kimpyojin on 29/05/2019. 6 | // Copyright © 2019 Pyojin Kim. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Core Location Motion Data Logger # 2 | 3 | This is a simple application to allow the easy capture of GPS/IMU data on iOS devices for offline use. 4 | I wanted to play around with data from GPS and IMU with Core Location and Core Motion frameworks in Swift 5.0 for iPhone Xs. 5 | 6 | ![Core Location Motion Data Logger](https://github.com/PyojinKim/CoreLocationMotion-Data-Logger/blob/master/screenshot.png) 7 | 8 | The aspects of the IMU that you can log follow directly from the Core Motion documentation provided by Apple. 9 | For more details, see the Core Motion documentation [here](https://developer.apple.com/documentation/coremotion). 10 | 11 | 12 | ## Usage Notes ## 13 | 14 | The txt files are produced automatically after pressing Stop button. 15 | This Xcode project is written under Xcode Version 10.2.1 (10E1001) for iOS 12.2. 16 | It doesn't currently check for sensor availability before logging. 17 | 18 | 19 | ## Reference Frames and Device Attitude ## 20 | 21 | In the global (inertial or reference) frame in Core Motion, +Z axis is vertical (up) and the X axis points toward magnetic north: [here](https://developer.apple.com/documentation/coremotion/getting_processed_device-motion_data/understanding_reference_frames_and_device_attitude). 22 | Y axis is determined based on the Z- and X-axes using the right hand rule. 23 | The device frame is attached as shown in the above figure: [here](https://developer.apple.com/documentation/coremotion/getting_raw_gyroscope_events). 24 | 25 | 26 | ## Output Format ## 27 | 28 | I have chosen the following output formats, but they are easy to modify if you find something else more convenient. 29 | 30 | * CLLocation (gps.txt): `timestamp, latitude, longitude, horizontalAccuracy, altitude, buildingFloor, verticalAccuracy \n` 31 | * CMDeviceMotion (game_rv.txt): `timestamp, quaternion_x, quaternion_y, quaternion_z, quaternion_w \n` 32 | * CMDeviceMotion (gyro.txt): `timestamp, gyro_x, gyro_y, gyro_z \n` 33 | * CMDeviceMotion (gravity.txt): `timestamp, gravity_x, gravity_y, gravity_z \n` 34 | * CMDeviceMotion (linacce.txt): `timestamp, user_acceleration_x, user_acceleration_y, user_acceleration_z \n` 35 | * CMDeviceMotion (magnet.txt): `timestamp, magnetic_x, magnetic_y, magnetic_z \n` 36 | * CMDeviceMotion (heading.txt): `timestamp, heading_angle \n` 37 | * CMAccelerometerData (acce.txt): `timestamp, acceleration_x, acceleration_y, acceleration_z \n` 38 | * CMGyroData (gyro_uncalib.txt): `timestamp, gyro_x, gyro_y, gyro_z \n` 39 | * CMMagnetometerData (magnet_uncalib.txt): `timestamp, magnetic_x, magnetic_y, magnetic_z \n` 40 | * CMPedometerData (step.txt): `timestamp, step count, distance \n` 41 | * CMAltitudeData (height.txt): `timestamp, relative_altitude \n` 42 | * CMAltitudeData (pressure.txt): `timestamp, pressure \n` 43 | * UIDevice (battery.txt): `timestamp, battery_level \n` 44 | 45 | There are alternative representations of the attitude (roll/pitch/yaw, quaternions, rotation matrix). 46 | You will have to modify the source code if you prefer logging one of those instead of quaternion format. 47 | 48 | 49 | ## Offline Matlab Visualization ## 50 | 51 | The ability to experiment with different algorithms to process the IMU data is the reason that I created this project in the first place. 52 | I have included an example script that you can use to parse and visualize the data that comes from Core Location & Core Motion Data Logger. 53 | Look under the Visualization directory to check it out. 54 | You can run the script by typing the following in your terminal: 55 | 56 | run main_script.m 57 | 58 | Here's one of the figures produced by the Matlab script: 59 | 60 | ![Data visualization](https://github.com/PyojinKim/CoreLocationMotion-Data-Logger/blob/master/data_visualization.png) 61 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2E9DDE1B229F7805001F5B2C /* UsefulFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9DDE1A229F7805001F5B2C /* UsefulFunctions.swift */; }; 11 | 2EB6F604229E752D009915A4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB6F603229E752D009915A4 /* AppDelegate.swift */; }; 12 | 2EB6F606229E752D009915A4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB6F605229E752D009915A4 /* ViewController.swift */; }; 13 | 2EB6F609229E752D009915A4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2EB6F607229E752D009915A4 /* Main.storyboard */; }; 14 | 2EB6F60B229E752E009915A4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2EB6F60A229E752E009915A4 /* Assets.xcassets */; }; 15 | 2EB6F60E229E752E009915A4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2EB6F60C229E752E009915A4 /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 2E9DDE1A229F7805001F5B2C /* UsefulFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsefulFunctions.swift; sourceTree = ""; }; 20 | 2EB6F600229E752D009915A4 /* CoreLocationMotion-Data-Logger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CoreLocationMotion-Data-Logger.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 2EB6F603229E752D009915A4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 2EB6F605229E752D009915A4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 2EB6F608229E752D009915A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 2EB6F60A229E752E009915A4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 2EB6F60D229E752E009915A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 2EB6F60F229E752E009915A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 2EB6F5FD229E752D009915A4 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 2EB6F5F7229E752D009915A4 = { 41 | isa = PBXGroup; 42 | children = ( 43 | 2EB6F602229E752D009915A4 /* CoreLocationMotion-Data-Logger */, 44 | 2EB6F601229E752D009915A4 /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 2EB6F601229E752D009915A4 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 2EB6F600229E752D009915A4 /* CoreLocationMotion-Data-Logger.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 2EB6F602229E752D009915A4 /* CoreLocationMotion-Data-Logger */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 2EB6F603229E752D009915A4 /* AppDelegate.swift */, 60 | 2EB6F605229E752D009915A4 /* ViewController.swift */, 61 | 2EB6F607229E752D009915A4 /* Main.storyboard */, 62 | 2EB6F60A229E752E009915A4 /* Assets.xcassets */, 63 | 2EB6F60C229E752E009915A4 /* LaunchScreen.storyboard */, 64 | 2EB6F60F229E752E009915A4 /* Info.plist */, 65 | 2E9DDE1A229F7805001F5B2C /* UsefulFunctions.swift */, 66 | ); 67 | path = "CoreLocationMotion-Data-Logger"; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | 2EB6F5FF229E752D009915A4 /* CoreLocationMotion-Data-Logger */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = 2EB6F612229E752E009915A4 /* Build configuration list for PBXNativeTarget "CoreLocationMotion-Data-Logger" */; 76 | buildPhases = ( 77 | 2EB6F5FC229E752D009915A4 /* Sources */, 78 | 2EB6F5FD229E752D009915A4 /* Frameworks */, 79 | 2EB6F5FE229E752D009915A4 /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = "CoreLocationMotion-Data-Logger"; 86 | productName = "CoreLocationMotion-Data-Logger"; 87 | productReference = 2EB6F600229E752D009915A4 /* CoreLocationMotion-Data-Logger.app */; 88 | productType = "com.apple.product-type.application"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 2EB6F5F8229E752D009915A4 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastSwiftUpdateCheck = 1020; 97 | LastUpgradeCheck = 1020; 98 | ORGANIZATIONNAME = "Pyojin Kim"; 99 | TargetAttributes = { 100 | 2EB6F5FF229E752D009915A4 = { 101 | CreatedOnToolsVersion = 10.2.1; 102 | }; 103 | }; 104 | }; 105 | buildConfigurationList = 2EB6F5FB229E752D009915A4 /* Build configuration list for PBXProject "CoreLocationMotion-Data-Logger" */; 106 | compatibilityVersion = "Xcode 9.3"; 107 | developmentRegion = en; 108 | hasScannedForEncodings = 0; 109 | knownRegions = ( 110 | en, 111 | Base, 112 | ); 113 | mainGroup = 2EB6F5F7229E752D009915A4; 114 | productRefGroup = 2EB6F601229E752D009915A4 /* Products */; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | 2EB6F5FF229E752D009915A4 /* CoreLocationMotion-Data-Logger */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXResourcesBuildPhase section */ 124 | 2EB6F5FE229E752D009915A4 /* Resources */ = { 125 | isa = PBXResourcesBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | 2EB6F60E229E752E009915A4 /* LaunchScreen.storyboard in Resources */, 129 | 2EB6F60B229E752E009915A4 /* Assets.xcassets in Resources */, 130 | 2EB6F609229E752D009915A4 /* Main.storyboard in Resources */, 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXResourcesBuildPhase section */ 135 | 136 | /* Begin PBXSourcesBuildPhase section */ 137 | 2EB6F5FC229E752D009915A4 /* Sources */ = { 138 | isa = PBXSourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | 2EB6F606229E752D009915A4 /* ViewController.swift in Sources */, 142 | 2E9DDE1B229F7805001F5B2C /* UsefulFunctions.swift in Sources */, 143 | 2EB6F604229E752D009915A4 /* AppDelegate.swift in Sources */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | /* End PBXSourcesBuildPhase section */ 148 | 149 | /* Begin PBXVariantGroup section */ 150 | 2EB6F607229E752D009915A4 /* Main.storyboard */ = { 151 | isa = PBXVariantGroup; 152 | children = ( 153 | 2EB6F608229E752D009915A4 /* Base */, 154 | ); 155 | name = Main.storyboard; 156 | sourceTree = ""; 157 | }; 158 | 2EB6F60C229E752E009915A4 /* LaunchScreen.storyboard */ = { 159 | isa = PBXVariantGroup; 160 | children = ( 161 | 2EB6F60D229E752E009915A4 /* Base */, 162 | ); 163 | name = LaunchScreen.storyboard; 164 | sourceTree = ""; 165 | }; 166 | /* End PBXVariantGroup section */ 167 | 168 | /* Begin XCBuildConfiguration section */ 169 | 2EB6F610229E752E009915A4 /* Debug */ = { 170 | isa = XCBuildConfiguration; 171 | buildSettings = { 172 | ALWAYS_SEARCH_USER_PATHS = NO; 173 | CLANG_ANALYZER_NONNULL = YES; 174 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 175 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 176 | CLANG_CXX_LIBRARY = "libc++"; 177 | CLANG_ENABLE_MODULES = YES; 178 | CLANG_ENABLE_OBJC_ARC = YES; 179 | CLANG_ENABLE_OBJC_WEAK = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 185 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 186 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INFINITE_RECURSION = YES; 190 | CLANG_WARN_INT_CONVERSION = YES; 191 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 193 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 195 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 196 | CLANG_WARN_STRICT_PROTOTYPES = YES; 197 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 198 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 199 | CLANG_WARN_UNREACHABLE_CODE = YES; 200 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 201 | CODE_SIGN_IDENTITY = "iPhone Developer"; 202 | COPY_PHASE_STRIP = NO; 203 | DEBUG_INFORMATION_FORMAT = dwarf; 204 | ENABLE_STRICT_OBJC_MSGSEND = YES; 205 | ENABLE_TESTABILITY = YES; 206 | GCC_C_LANGUAGE_STANDARD = gnu11; 207 | GCC_DYNAMIC_NO_PIC = NO; 208 | GCC_NO_COMMON_BLOCKS = YES; 209 | GCC_OPTIMIZATION_LEVEL = 0; 210 | GCC_PREPROCESSOR_DEFINITIONS = ( 211 | "DEBUG=1", 212 | "$(inherited)", 213 | ); 214 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 215 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 216 | GCC_WARN_UNDECLARED_SELECTOR = YES; 217 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 218 | GCC_WARN_UNUSED_FUNCTION = YES; 219 | GCC_WARN_UNUSED_VARIABLE = YES; 220 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 221 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 222 | MTL_FAST_MATH = YES; 223 | ONLY_ACTIVE_ARCH = YES; 224 | SDKROOT = iphoneos; 225 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 226 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 227 | }; 228 | name = Debug; 229 | }; 230 | 2EB6F611229E752E009915A4 /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | CLANG_ANALYZER_NONNULL = YES; 235 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 237 | CLANG_CXX_LIBRARY = "libc++"; 238 | CLANG_ENABLE_MODULES = YES; 239 | CLANG_ENABLE_OBJC_ARC = YES; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_COMMA = YES; 244 | CLANG_WARN_CONSTANT_CONVERSION = YES; 245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 257 | CLANG_WARN_STRICT_PROTOTYPES = YES; 258 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 259 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | CODE_SIGN_IDENTITY = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu11; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | MTL_FAST_MATH = YES; 278 | SDKROOT = iphoneos; 279 | SWIFT_COMPILATION_MODE = wholemodule; 280 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 281 | VALIDATE_PRODUCT = YES; 282 | }; 283 | name = Release; 284 | }; 285 | 2EB6F613229E752E009915A4 /* Debug */ = { 286 | isa = XCBuildConfiguration; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CODE_SIGN_STYLE = Automatic; 290 | DEVELOPMENT_TEAM = RVXJ8FM9Z4; 291 | INFOPLIST_FILE = "CoreLocationMotion-Data-Logger/Info.plist"; 292 | LD_RUNPATH_SEARCH_PATHS = ( 293 | "$(inherited)", 294 | "@executable_path/Frameworks", 295 | ); 296 | PRODUCT_BUNDLE_IDENTIFIER = "com.pjinkim.CoreLocationMotion-Data-Logger"; 297 | PRODUCT_NAME = "$(TARGET_NAME)"; 298 | SWIFT_VERSION = 5.0; 299 | TARGETED_DEVICE_FAMILY = "1,2"; 300 | }; 301 | name = Debug; 302 | }; 303 | 2EB6F614229E752E009915A4 /* Release */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 307 | CODE_SIGN_STYLE = Automatic; 308 | DEVELOPMENT_TEAM = RVXJ8FM9Z4; 309 | INFOPLIST_FILE = "CoreLocationMotion-Data-Logger/Info.plist"; 310 | LD_RUNPATH_SEARCH_PATHS = ( 311 | "$(inherited)", 312 | "@executable_path/Frameworks", 313 | ); 314 | PRODUCT_BUNDLE_IDENTIFIER = "com.pjinkim.CoreLocationMotion-Data-Logger"; 315 | PRODUCT_NAME = "$(TARGET_NAME)"; 316 | SWIFT_VERSION = 5.0; 317 | TARGETED_DEVICE_FAMILY = "1,2"; 318 | }; 319 | name = Release; 320 | }; 321 | /* End XCBuildConfiguration section */ 322 | 323 | /* Begin XCConfigurationList section */ 324 | 2EB6F5FB229E752D009915A4 /* Build configuration list for PBXProject "CoreLocationMotion-Data-Logger" */ = { 325 | isa = XCConfigurationList; 326 | buildConfigurations = ( 327 | 2EB6F610229E752E009915A4 /* Debug */, 328 | 2EB6F611229E752E009915A4 /* Release */, 329 | ); 330 | defaultConfigurationIsVisible = 0; 331 | defaultConfigurationName = Release; 332 | }; 333 | 2EB6F612229E752E009915A4 /* Build configuration list for PBXNativeTarget "CoreLocationMotion-Data-Logger" */ = { 334 | isa = XCConfigurationList; 335 | buildConfigurations = ( 336 | 2EB6F613229E752E009915A4 /* Debug */, 337 | 2EB6F614229E752E009915A4 /* Release */, 338 | ); 339 | defaultConfigurationIsVisible = 0; 340 | defaultConfigurationName = Release; 341 | }; 342 | /* End XCConfigurationList section */ 343 | }; 344 | rootObject = 2EB6F5F8229E752D009915A4 /* Project object */; 345 | } 346 | -------------------------------------------------------------------------------- /Visualization/plot_google_map.m: -------------------------------------------------------------------------------- 1 | function varargout = plot_google_map(varargin) 2 | % function h = plot_google_map(varargin) 3 | % Plots a google map on the current axes using the Google Static Maps API 4 | % 5 | % USAGE: 6 | % h = plot_google_map(Property, Value,...) 7 | % Plots the map on the given axes. Used also if no output is specified 8 | % 9 | % Or: 10 | % [lonVec latVec imag] = plot_google_map(Property, Value,...) 11 | % Returns the map without plotting it 12 | % 13 | % PROPERTIES: 14 | % Axis - Axis handle. If not given, gca is used. 15 | % Height (640) - Height of the image in pixels (max 640) 16 | % Width (640) - Width of the image in pixels (max 640) 17 | % Scale (2) - (1/2) Resolution scale factor. Using Scale=2 will 18 | % double the resulotion of the downloaded image (up 19 | % to 1280x1280) and will result in finer rendering, 20 | % but processing time will be longer. 21 | % Resize (1) - (recommended 1-2) Resolution upsampling factor. 22 | % Increases image resolution using imresize(). This results 23 | % in a finer image but it needs the image processing 24 | % toolbox and processing time will be longer. 25 | % MapType - ('roadmap') Type of map to return. Any of [roadmap, 26 | % satellite, terrain, hybrid]. See the Google Maps API for 27 | % more information. 28 | % Alpha (1) - (0-1) Transparency level of the map (0 is fully 29 | % transparent). While the map is always moved to the 30 | % bottom of the plot (i.e. will not hide previously 31 | % drawn items), this can be useful in order to increase 32 | % readability if many colors are plotted 33 | % (using SCATTER for example). 34 | % ShowLabels (1) - (0/1) Controls whether to display city/street textual labels on the map 35 | % Style - (string) A style configuration string. See: 36 | % https://developers.google.com/maps/documentation/static-maps/?csw=1#StyledMaps 37 | % http://instrument.github.io/styled-maps-wizard/ 38 | % Language - (string) A 2 letter ISO 639-1 language code for displaying labels in a 39 | % local language instead of English (where available). 40 | % For example, for Chinese use: 41 | % plot_google_map('language','zh') 42 | % For the list of codes, see: 43 | % http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 44 | % Marker - The marker argument is a text string with fields 45 | % conforming to the Google Maps API. The 46 | % following are valid examples: 47 | % '43.0738740,-70.713993' (default midsize orange marker) 48 | % '43.0738740,-70.713993,blue' (midsize blue marker) 49 | % '43.0738740,-70.713993,yellowa' (midsize yellow 50 | % marker with label "A") 51 | % '43.0738740,-70.713993,tinyredb' (tiny red marker 52 | % with label "B") 53 | % Refresh (1) - (0/1) defines whether to automatically refresh the 54 | % map upon zoom/pan action on the figure. 55 | % AutoAxis (1) - (0/1) defines whether to automatically adjust the axis 56 | % of the plot to avoid the map being stretched. 57 | % This will adjust the span to be correct 58 | % according to the shape of the map axes. 59 | % MapScale (0) - (0/1) defines wheteher to add a scale indicator to 60 | % the map. 61 | % ScaleWidth (0.25) - (0.1-0.9) defines the max width of the scale 62 | % indicator relative to the map width. 63 | % ScaleLocation (sw) - (ne, n, se, s, sw, nw) defines the location of 64 | % scale indicator on the map. 65 | % ScaleUnits (si) - (si/imp) changes the scale indicator units between 66 | % SI and imperial units. 67 | % FigureResizeUpdate (1) - (0/1) defines whether to automatically refresh the 68 | % map upon resizing the figure. This will ensure map 69 | % isn't stretched after figure resize. 70 | % APIKey - (string) set your own API key which you obtained from Google: 71 | % http://developers.google.com/maps/documentation/staticmaps/#api_key 72 | % This will enable up to 25,000 map requests per day, 73 | % compared to a few hundred requests without a key. 74 | % To set the key, use: 75 | % plot_google_map('APIKey','SomeLongStringObtaindFromGoogle') 76 | % You need to do this only once to set the key. 77 | % To disable the use of a key, use: 78 | % plot_google_map('APIKey','') 79 | % 80 | % OUTPUT: 81 | % h - Handle to the plotted map 82 | % 83 | % lonVect - Vector of Longidute coordinates (WGS84) of the image 84 | % latVect - Vector of Latidute coordinates (WGS84) of the image 85 | % imag - Image matrix (height,width,3) of the map 86 | % 87 | % EXAMPLE - plot a map showing some capitals in Europe: 88 | % lat = [48.8708 51.5188 41.9260 40.4312 52.523 37.982]; 89 | % lon = [2.4131 -0.1300 12.4951 -3.6788 13.415 23.715]; 90 | % plot(lon, lat, '.r', 'MarkerSize', 20) 91 | % plot_google_map('MapScale', 1) 92 | % 93 | % References: 94 | % http://www.mathworks.com/matlabcentral/fileexchange/24113 95 | % http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/ 96 | % http://developers.google.com/maps/documentation/staticmaps/ 97 | % https://www.mathworks.com/matlabcentral/fileexchange/33545-automatic-map-scale-generation 98 | % 99 | % Acknowledgements: 100 | % Val Schmidt for the submission of get_google_map.m 101 | % Jonathan Sullivan for the submission of makescale.m 102 | % 103 | % Author: 104 | % Zohar Bar-Yehuda 105 | % 106 | % Version 2.0 - 08/04/2018 107 | % - Add an option to show a map scale 108 | % - Several bugfixes 109 | % Version 1.8 - 25/04/2016 - By Hannes Diethelm 110 | % - Add resize parameter to resize image using imresize() 111 | % - Fix scale parameter 112 | % Version 1.7 - 14/04/2016 113 | % - Add custom style support 114 | % Version 1.6 - 12/11/2015 115 | % - Use system temp folder for writing image files (with fallback to current dir if missing write permissions) 116 | % Version 1.5 - 20/11/2014 117 | % - Support for MATLAB R2014b 118 | % - several fixes for complex layouts: several maps in one figure, 119 | % map inside a panel, specifying axis handle as input (thanks to Luke Plausin) 120 | % Version 1.4 - 25/03/2014 121 | % - Added the language parameter for showing labels in a local language 122 | % - Display the URL on error to allow easier debugging of API errors 123 | % Version 1.3 - 06/10/2013 124 | % - Improved functionality of AutoAxis, which now handles any shape of map axes. 125 | % Now also updates the extent of the map if the figure is resized. 126 | % - Added the showLabels parameter which allows hiding the textual labels on the map. 127 | % Version 1.2 - 16/06/2012 128 | % - Support use of the "scale=2" parameter by default for finer rendering (set scale=1 if too slow). 129 | % - Auto-adjust axis extent so the map isn't stretched. 130 | % - Set and use an API key which enables a much higher usage volume per day. 131 | % Version 1.1 - 25/08/2011 132 | 133 | persistent apiKey useTemp 134 | if isnumeric(apiKey) 135 | % first run, check if API key file exists 136 | if exist('api_key.mat','file') 137 | load api_key 138 | else 139 | apiKey = ''; 140 | end 141 | end 142 | 143 | if isempty(useTemp) 144 | % first run, check if we have wrtie access to the temp folder 145 | try 146 | tempfilename = tempname; 147 | fid = fopen(tempfilename, 'w'); 148 | if fid > 0 149 | fclose(fid); 150 | useTemp = true; 151 | delete(tempfilename); 152 | else 153 | % Don't have write access to temp folder or it doesn't exist, fallback to current dir 154 | useTemp = false; 155 | end 156 | catch 157 | % in case tempname fails for some reason 158 | useTemp = false; 159 | end 160 | end 161 | 162 | hold on 163 | 164 | % Default parametrs 165 | axHandle = gca; 166 | set(axHandle, 'Layer','top'); % Put axis on top of image, so it doesn't hide the axis lines and ticks 167 | height = 640; 168 | width = 640; 169 | scale = 2; 170 | resize = 1; 171 | maptype = 'roadmap'; 172 | alphaData = 1; 173 | autoRefresh = 1; 174 | figureResizeUpdate = 1; 175 | autoAxis = 1; 176 | showLabels = 1; 177 | language = ''; 178 | markeridx = 1; 179 | markerlist = {}; 180 | style = ''; 181 | mapScale = 0; 182 | scaleWidth = 0.25; 183 | scaleLocation = 'se'; 184 | scaleUnits = 'si'; 185 | 186 | % Handle input arguments 187 | if nargin >= 2 188 | for idx = 1:2:length(varargin) 189 | switch lower(varargin{idx}) 190 | case 'axis' 191 | axHandle = varargin{idx+1}; 192 | case 'height' 193 | height = varargin{idx+1}; 194 | case 'width' 195 | width = varargin{idx+1}; 196 | case 'scale' 197 | scale = round(varargin{idx+1}); 198 | if scale < 1 || scale > 2 199 | error('Scale must be 1 or 2'); 200 | end 201 | case 'resize' 202 | resize = varargin{idx+1}; 203 | case 'maptype' 204 | maptype = varargin{idx+1}; 205 | case 'alpha' 206 | alphaData = varargin{idx+1}; 207 | case 'refresh' 208 | autoRefresh = varargin{idx+1}; 209 | case 'showlabels' 210 | showLabels = varargin{idx+1}; 211 | case 'figureresizeupdate' 212 | figureResizeUpdate = varargin{idx+1}; 213 | case 'language' 214 | language = varargin{idx+1}; 215 | case 'marker' 216 | markerlist{markeridx} = varargin{idx+1}; 217 | markeridx = markeridx + 1; 218 | case 'autoaxis' 219 | autoAxis = varargin{idx+1}; 220 | case 'apikey' 221 | apiKey = varargin{idx+1}; % set new key 222 | % save key to file 223 | funcFile = which('plot_google_map.m'); 224 | pth = fileparts(funcFile); 225 | keyFile = fullfile(pth,'api_key.mat'); 226 | save(keyFile,'apiKey') 227 | case 'style' 228 | style = varargin{idx+1}; 229 | case 'mapscale' 230 | mapScale = varargin{idx+1}; 231 | case 'scalewidth' 232 | scaleWidth = varargin{idx+1}; 233 | case 'scalelocation' 234 | scaleLocation = varargin{idx+1}; 235 | case 'scaleunits' 236 | scaleUnits = varargin{idx+1}; 237 | otherwise 238 | error(['Unrecognized variable: ' varargin{idx}]) 239 | end 240 | end 241 | end 242 | if height > 640 243 | height = 640; 244 | end 245 | if width > 640 246 | width = 640; 247 | end 248 | 249 | % Store paramters in axis handle (for auto refresh callbacks) 250 | ud = get(axHandle, 'UserData'); 251 | if isempty(ud) 252 | % explicitly set as struct to avoid warnings 253 | ud = struct; 254 | end 255 | ud.gmap_params = varargin; 256 | set(axHandle, 'UserData', ud); 257 | 258 | curAxis = axis(axHandle); 259 | if max(abs(curAxis)) > 500 || curAxis(3) > 90 || curAxis(4) < -90 260 | warning('Axis limits are not reasonable for WGS1984, ignoring. Please make sure your plotted data in WGS1984 coordinates,') 261 | return; 262 | end 263 | 264 | % Enforce Latitude constraints of EPSG:900913 265 | if curAxis(3) < -85 266 | curAxis(3) = -85; 267 | end 268 | if curAxis(4) > 85 269 | curAxis(4) = 85; 270 | end 271 | % Enforce longitude constrains 272 | if curAxis(1) < -180 273 | curAxis(1) = -180; 274 | end 275 | if curAxis(1) > 180 276 | curAxis(1) = 0; 277 | end 278 | if curAxis(2) > 180 279 | curAxis(2) = 180; 280 | end 281 | if curAxis(2) < -180 282 | curAxis(2) = 0; 283 | end 284 | 285 | if isequal(curAxis,[0 1 0 1]) % probably an empty figure 286 | % display world map 287 | curAxis = [-200 200 -85 85]; 288 | axis(curAxis) 289 | end 290 | 291 | 292 | if autoAxis 293 | % adjust current axis limit to avoid strectched maps 294 | [xExtent,yExtent] = latLonToMeters(curAxis(3:4), curAxis(1:2) ); 295 | xExtent = diff(xExtent); % just the size of the span 296 | yExtent = diff(yExtent); 297 | % get axes aspect ratio 298 | drawnow 299 | org_units = get(axHandle,'Units'); 300 | set(axHandle,'Units','Pixels') 301 | ax_position = get(axHandle,'position'); 302 | set(axHandle,'Units',org_units) 303 | aspect_ratio = ax_position(4) / ax_position(3); 304 | 305 | if xExtent*aspect_ratio > yExtent 306 | centerX = mean(curAxis(1:2)); 307 | centerY = mean(curAxis(3:4)); 308 | spanX = (curAxis(2)-curAxis(1))/2; 309 | spanY = (curAxis(4)-curAxis(3))/2; 310 | 311 | % enlarge the Y extent 312 | spanY = spanY*xExtent*aspect_ratio/yExtent; % new span 313 | if spanY > 85 314 | spanX = spanX * 85 / spanY; 315 | spanY = spanY * 85 / spanY; 316 | end 317 | curAxis(1) = centerX-spanX; 318 | curAxis(2) = centerX+spanX; 319 | curAxis(3) = centerY-spanY; 320 | curAxis(4) = centerY+spanY; 321 | elseif yExtent > xExtent*aspect_ratio 322 | 323 | centerX = mean(curAxis(1:2)); 324 | centerY = mean(curAxis(3:4)); 325 | spanX = (curAxis(2)-curAxis(1))/2; 326 | spanY = (curAxis(4)-curAxis(3))/2; 327 | % enlarge the X extent 328 | spanX = spanX*yExtent/(xExtent*aspect_ratio); % new span 329 | if spanX > 180 330 | spanY = spanY * 180 / spanX; 331 | spanX = spanX * 180 / spanX; 332 | end 333 | 334 | curAxis(1) = centerX-spanX; 335 | curAxis(2) = centerX+spanX; 336 | curAxis(3) = centerY-spanY; 337 | curAxis(4) = centerY+spanY; 338 | end 339 | % Enforce Latitude constraints of EPSG:900913 340 | if curAxis(3) < -85 341 | curAxis(3:4) = curAxis(3:4) + (-85 - curAxis(3)); 342 | end 343 | if curAxis(4) > 85 344 | curAxis(3:4) = curAxis(3:4) + (85 - curAxis(4)); 345 | end 346 | axis(axHandle, curAxis); % update axis as quickly as possible, before downloading new image 347 | drawnow 348 | end 349 | 350 | % Delete previous map from plot (if exists) 351 | if nargout <= 1 % only if in plotting mode 352 | curChildren = get(axHandle,'children'); 353 | map_objs = findobj(curChildren,'tag','gmap'); 354 | bd_callback = []; 355 | for idx = 1:length(map_objs) 356 | if ~isempty(get(map_objs(idx),'ButtonDownFcn')) 357 | % copy callback properties from current map 358 | bd_callback = get(map_objs(idx),'ButtonDownFcn'); 359 | end 360 | end 361 | ud = get(axHandle, 'UserData'); 362 | delete(map_objs); 363 | delete(findobj(curChildren,'tag','MapScale')); 364 | % Recover userdata of axis (cleared in cleanup function) 365 | set(axHandle, 'UserData', ud); 366 | end 367 | 368 | % Calculate zoom level for current axis limits 369 | [xExtent,yExtent] = latLonToMeters(curAxis(3:4), curAxis(1:2) ); 370 | minResX = diff(xExtent) / width; 371 | minResY = diff(yExtent) / height; 372 | minRes = max([minResX minResY]); 373 | tileSize = 256; 374 | initialResolution = 2 * pi * 6378137 / tileSize; % 156543.03392804062 for tileSize 256 pixels 375 | zoomlevel = floor(log2(initialResolution/minRes)); 376 | 377 | % Enforce valid zoom levels 378 | if zoomlevel < 0 379 | zoomlevel = 0; 380 | end 381 | if zoomlevel > 19 382 | zoomlevel = 19; 383 | end 384 | 385 | % Calculate center coordinate in WGS1984 386 | lat = (curAxis(3)+curAxis(4))/2; 387 | lon = (curAxis(1)+curAxis(2))/2; 388 | 389 | % Construct query URL 390 | preamble = 'http://maps.googleapis.com/maps/api/staticmap'; 391 | location = ['?center=' num2str(lat,10) ',' num2str(lon,10)]; 392 | zoomStr = ['&zoom=' num2str(zoomlevel)]; 393 | sizeStr = ['&scale=' num2str(scale) '&size=' num2str(width) 'x' num2str(height)]; 394 | maptypeStr = ['&maptype=' maptype ]; 395 | if ~isempty(apiKey) 396 | keyStr = ['&key=' apiKey]; 397 | else 398 | keyStr = ''; 399 | end 400 | markers = '&markers='; 401 | for idx = 1:length(markerlist) 402 | if idx < length(markerlist) 403 | markers = [markers markerlist{idx} '%7C']; 404 | else 405 | markers = [markers markerlist{idx}]; 406 | end 407 | end 408 | 409 | if showLabels == 0 410 | if ~isempty(style) 411 | style = [style '&style=']; 412 | end 413 | style = [style 'feature:all|element:labels|visibility:off']; 414 | end 415 | 416 | if ~isempty(language) 417 | languageStr = ['&language=' language]; 418 | else 419 | languageStr = ''; 420 | end 421 | 422 | if ismember(maptype,{'satellite','hybrid'}) 423 | filename = 'tmp.jpg'; 424 | format = '&format=jpg'; 425 | convertNeeded = 0; 426 | else 427 | filename = 'tmp.png'; 428 | format = '&format=png'; 429 | convertNeeded = 1; 430 | end 431 | sensor = '&sensor=false'; 432 | 433 | if ~isempty(style) 434 | styleStr = ['&style=' style]; 435 | else 436 | styleStr = ''; 437 | end 438 | 439 | url = [preamble location zoomStr sizeStr maptypeStr format markers languageStr sensor keyStr styleStr]; 440 | 441 | % Get the image 442 | if useTemp 443 | filepath = fullfile(tempdir, filename); 444 | else 445 | filepath = filename; 446 | end 447 | 448 | try 449 | urlwrite(url,filepath); 450 | catch % error downloading map 451 | warning(['Unable to download map form Google Servers.\n' ... 452 | 'Matlab error was: %s\n\n' ... 453 | 'Possible reasons: missing write permissions, no network connection, quota exceeded, or some other error.\n' ... 454 | 'Consider using an API key if quota problems persist.\n\n' ... 455 | 'To debug, try pasting the following URL in your browser, which may result in a more informative error:\n%s'], lasterr, url); 456 | varargout{1} = []; 457 | varargout{2} = []; 458 | varargout{3} = []; 459 | return 460 | end 461 | 462 | [M, Mcolor] = imread(filepath); 463 | Mcolor = uint8(Mcolor * 255); 464 | %M = cast(M,'double'); 465 | delete(filepath); % delete temp file 466 | width = size(M,2); 467 | height = size(M,1); 468 | 469 | % We now want to convert the image from a colormap image with an uneven 470 | % mesh grid, into an RGB truecolor image with a uniform grid. 471 | % This would enable displaying it with IMAGE, instead of PCOLOR. 472 | % Advantages are: 473 | % 1) faster rendering 474 | % 2) makes it possible to display together with other colormap annotations (PCOLOR, SCATTER etc.) 475 | 476 | % Convert image from colormap type to RGB truecolor (if PNG is used) 477 | if convertNeeded 478 | imag = zeros(height,width,3, 'uint8'); 479 | for idx = 1:3 480 | cur_map = Mcolor(:,idx); 481 | imag(:,:,idx) = reshape(cur_map(M+1),height,width); 482 | end 483 | else 484 | imag = M; 485 | end 486 | % Resize if needed 487 | if resize ~= 1 488 | imag = imresize(imag, resize, 'bilinear'); 489 | end 490 | 491 | % Calculate a meshgrid of pixel coordinates in EPSG:900913 492 | width = size(imag,2); 493 | height = size(imag,1); 494 | centerPixelY = round(height/2); 495 | centerPixelX = round(width/2); 496 | [centerX,centerY] = latLonToMeters(lat, lon ); % center coordinates in EPSG:900913 497 | curResolution = initialResolution / 2^zoomlevel / scale / resize; % meters/pixel (EPSG:900913) 498 | xVec = centerX + ((1:width)-centerPixelX) * curResolution; % x vector 499 | yVec = centerY + ((height:-1:1)-centerPixelY) * curResolution; % y vector 500 | [xMesh,yMesh] = meshgrid(xVec,yVec); % construct meshgrid 501 | 502 | % convert meshgrid to WGS1984 503 | [lonMesh,latMesh] = metersToLatLon(xMesh,yMesh); 504 | 505 | % Next, project the data into a uniform WGS1984 grid 506 | uniHeight = round(height*resize); 507 | uniWidth = round(width*resize); 508 | latVect = linspace(latMesh(1,1),latMesh(end,1),uniHeight); 509 | lonVect = linspace(lonMesh(1,1),lonMesh(1,end),uniWidth); 510 | [uniLonMesh,uniLatMesh] = meshgrid(lonVect,latVect); 511 | uniImag = zeros(uniHeight,uniWidth,3); 512 | 513 | % Fast Interpolation to uniform grid 514 | uniImag = myTurboInterp2(lonMesh,latMesh,imag,uniLonMesh,uniLatMesh); 515 | 516 | if nargout <= 1 % plot map 517 | % display image 518 | hold(axHandle, 'on'); 519 | cax = caxis; 520 | h = image(lonVect,latVect,uniImag, 'Parent', axHandle); 521 | caxis(cax); % Preserve caxis that is sometimes changed by the call to image() 522 | set(axHandle,'YDir','Normal') 523 | set(h,'tag','gmap') 524 | set(h,'AlphaData',alphaData) 525 | 526 | % add a dummy image to allow pan/zoom out to x2 of the image extent 527 | h_tmp = image(lonVect([1 end]),latVect([1 end]),zeros(2),'Visible','off', 'Parent', axHandle, 'CDataMapping', 'scaled'); 528 | set(h_tmp,'tag','gmap') 529 | 530 | uistack(h,'bottom') % move map to bottom (so it doesn't hide previously drawn annotations) 531 | axis(axHandle, curAxis) % restore original zoom 532 | if nargout == 1 533 | varargout{1} = h; 534 | end 535 | set(h, 'UserData', onCleanup(@() cleanupFunc(axHandle))); 536 | 537 | % if auto-refresh mode - override zoom callback to allow autumatic 538 | % refresh of map upon zoom actions. 539 | figHandle = axHandle; 540 | while ~strcmpi(get(figHandle, 'Type'), 'figure') 541 | % Recursively search for parent figure in case axes are in a panel 542 | figHandle = get(figHandle, 'Parent'); 543 | end 544 | 545 | zoomHandle = zoom(axHandle); 546 | panHandle = pan(figHandle); % This isn't ideal, doesn't work for contained axis 547 | if autoRefresh 548 | set(zoomHandle,'ActionPostCallback',@update_google_map); 549 | set(panHandle, 'ActionPostCallback', @update_google_map); 550 | else % disable zoom override 551 | set(zoomHandle,'ActionPostCallback',[]); 552 | set(panHandle, 'ActionPostCallback',[]); 553 | end 554 | 555 | % set callback for figure resize function, to update extents if figure 556 | % is streched. 557 | if figureResizeUpdate &&isempty(get(figHandle, 'ResizeFcn')) 558 | % set only if not already set by someone else 559 | set(figHandle, 'ResizeFcn', @update_google_map_fig); 560 | end 561 | 562 | % set callback properties 563 | set(h,'ButtonDownFcn',bd_callback); 564 | 565 | if mapScale 566 | makescale(axHandle, 'set_callbacks', 0, 'units', scaleUnits, ... 567 | 'location', scaleLocation, 'width', scaleWidth); 568 | end 569 | else % don't plot, only return map 570 | varargout{1} = lonVect; 571 | varargout{2} = latVect; 572 | varargout{3} = uniImag; 573 | end 574 | 575 | 576 | % Coordinate transformation functions 577 | 578 | function [lon,lat] = metersToLatLon(x,y) 579 | % Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum 580 | originShift = 2 * pi * 6378137 / 2.0; % 20037508.342789244 581 | lon = (x ./ originShift) * 180; 582 | lat = (y ./ originShift) * 180; 583 | lat = 180 / pi * (2 * atan( exp( lat * pi / 180)) - pi / 2); 584 | 585 | function [x,y] = latLonToMeters(lat, lon ) 586 | % Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913" 587 | originShift = 2 * pi * 6378137 / 2.0; % 20037508.342789244 588 | x = lon * originShift / 180; 589 | y = log(tan((90 + lat) * pi / 360 )) / (pi / 180); 590 | y = y * originShift / 180; 591 | 592 | 593 | function ZI = myTurboInterp2(X,Y,Z,XI,YI) 594 | % An extremely fast nearest neighbour 2D interpolation, assuming both input 595 | % and output grids consist only of squares, meaning: 596 | % - uniform X for each column 597 | % - uniform Y for each row 598 | XI = XI(1,:); 599 | X = X(1,:); 600 | YI = YI(:,1); 601 | Y = Y(:,1); 602 | 603 | xiPos = nan*ones(size(XI)); 604 | xLen = length(X); 605 | yiPos = nan*ones(size(YI)); 606 | yLen = length(Y); 607 | % find x conversion 608 | xPos = 1; 609 | for idx = 1:length(xiPos) 610 | if XI(idx) >= X(1) && XI(idx) <= X(end) 611 | while xPos < xLen && X(xPos+1)= Y(end) 626 | while yPos < yLen && Y(yPos+1)>YI(idx) 627 | yPos = yPos + 1; 628 | end 629 | diffs = abs(Y(yPos:yPos+1)-YI(idx)); 630 | if diffs(1) < diffs(2) 631 | yiPos(idx) = yPos; 632 | else 633 | yiPos(idx) = yPos + 1; 634 | end 635 | end 636 | end 637 | ZI = Z(yiPos,xiPos,:); 638 | 639 | 640 | function update_google_map(obj,evd) 641 | % callback function for auto-refresh 642 | drawnow; 643 | try 644 | axHandle = evd.Axes; 645 | catch ex 646 | % Event doesn't contain the correct axes. Panic! 647 | axHandle = gca; 648 | end 649 | ud = get(axHandle, 'UserData'); 650 | if isfield(ud, 'gmap_params') 651 | params = ud.gmap_params; 652 | plot_google_map(params{:}); 653 | end 654 | 655 | 656 | function update_google_map_fig(obj,evd) 657 | % callback function for auto-refresh 658 | drawnow; 659 | axes_objs = findobj(get(gcf,'children'),'type','axes'); 660 | for idx = 1:length(axes_objs) 661 | if ~isempty(findobj(get(axes_objs(idx),'children'),'tag','gmap')); 662 | ud = get(axes_objs(idx), 'UserData'); 663 | if isfield(ud, 'gmap_params') 664 | params = ud.gmap_params; 665 | else 666 | params = {}; 667 | end 668 | 669 | % Add axes to inputs if needed 670 | if ~sum(strcmpi(params, 'Axis')) 671 | params = [params, {'Axis', axes_objs(idx)}]; 672 | end 673 | plot_google_map(params{:}); 674 | end 675 | end 676 | 677 | function cleanupFunc(h) 678 | ud = get(h, 'UserData'); 679 | if isstruct(ud) && isfield(ud, 'gmap_params') 680 | ud = rmfield(ud, 'gmap_params'); 681 | set(h, 'UserData', ud); 682 | end 683 | 684 | -------------------------------------------------------------------------------- /Visualization/main_script.m: -------------------------------------------------------------------------------- 1 | clc; 2 | close all; 3 | clear variables; %clear classes; 4 | rand('state',0); % rand('state',sum(100*clock)); 5 | dbstop if error; 6 | 7 | 8 | %% common setting to read text files 9 | 10 | delimiter = ' '; 11 | headerlinesIn = 1; 12 | nanoSecondToSecond = 1000000000; 13 | 14 | 15 | %% 1) GPS location 16 | 17 | % parsing GPS location text 18 | textFileDir = 'gps.txt'; 19 | textGPSLocationData = importdata(textFileDir, delimiter, headerlinesIn); 20 | deviceLatitude = textGPSLocationData.data(:,2); 21 | deviceLongitude = textGPSLocationData.data(:,3); 22 | 23 | % plot trajectory on Google map 24 | figure; 25 | plot(deviceLongitude, deviceLatitude,'.r','MarkerSize',20); hold on; 26 | plot_google_map('maptype', 'roadmap', 'APIKey', 'AIzaSyB_uD1rGjX6MJkoQgSDyjHkbdu-b-_5Bjg'); 27 | legend('CLLocation'); 28 | xlabel('Longitude'); ylabel('Latitude'); hold off; 29 | 30 | 31 | %% 2) device orientation 32 | 33 | % parsing device orientation text 34 | textFileDir = 'game_rv.txt'; 35 | textOrientationData = importdata(textFileDir, delimiter, headerlinesIn); 36 | deviceOrientationTime = textOrientationData.data(:,1).'; 37 | deviceOrientationTime = (deviceOrientationTime - deviceOrientationTime(1)) ./ nanoSecondToSecond; 38 | deviceOrientationData = textOrientationData.data(:,[5 2 3 4]).'; 39 | numData = size(deviceOrientationData,2); 40 | 41 | % convert from unit quaternion to rotation matrix & roll/pitch/yaw 42 | R_gb = zeros(3,3,numData); 43 | rpy_gb = zeros(3,numData); 44 | for k = 1:numData 45 | R_gb(:,:,k) = q2r(deviceOrientationData(:,k)); 46 | rpy_gb(:,k) = rotmtx2angle(inv(R_gb(:,:,k))); 47 | end 48 | 49 | % play 3-DoF device orientation 50 | figure(10); 51 | L = 1; % coordinate axis length 52 | A = [0 0 0 1; L 0 0 1; 0 0 0 1; 0 L 0 1; 0 0 0 1; 0 0 L 1].'; 53 | for k = 1:5:numData 54 | cla; 55 | figure(10); 56 | plot_inertial_frame(0.5); hold on; grid on; axis equal; 57 | T_gb = [R_gb(:,:,k), ones(3,1); 58 | zeros(1,3), 1]; 59 | B = T_gb * A; 60 | plot3(B(1,1:2),B(2,1:2),B(3,1:2),'-r','LineWidth',1); % x: red 61 | plot3(B(1,3:4),B(2,3:4),B(3,3:4),'-g','LineWidth',1); % y: green 62 | plot3(B(1,5:6),B(2,5:6),B(3,5:6),'-b','LineWidth',1); % z: blue 63 | refresh; pause(0.01); 64 | k 65 | end 66 | 67 | % plot roll/pitch/yaw of device orientation 68 | figure; 69 | subplot(3,1,1); 70 | plot(deviceOrientationTime, rpy_gb(1,:), 'm'); hold on; grid on; axis tight; 71 | set(gcf,'color','w'); hold off; 72 | axis([min(deviceOrientationTime) max(deviceOrientationTime) min(rpy_gb(1,:)) max(rpy_gb(1,:))]); 73 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 74 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 75 | ylabel('Roll [rad]','FontName','Times New Roman','FontSize',17); 76 | subplot(3,1,2); 77 | plot(deviceOrientationTime, rpy_gb(2,:), 'm'); hold on; grid on; axis tight; 78 | set(gcf,'color','w'); hold off; 79 | axis([min(deviceOrientationTime) max(deviceOrientationTime) min(rpy_gb(2,:)) max(rpy_gb(2,:))]); 80 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 81 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 82 | ylabel('Pitch [rad]','FontName','Times New Roman','FontSize',17); 83 | subplot(3,1,3); 84 | plot(deviceOrientationTime, rpy_gb(3,:), 'm'); hold on; grid on; axis tight; 85 | set(gcf,'color','w'); hold off; 86 | axis([min(deviceOrientationTime) max(deviceOrientationTime) min(rpy_gb(3,:)) max(rpy_gb(3,:))]); 87 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 88 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 89 | ylabel('Yaw [rad]','FontName','Times New Roman','FontSize',17); 90 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 91 | 92 | % plot update rate of device orientation 93 | timeDifference = diff(deviceOrientationTime); 94 | meanUpdateRate = (1/mean(timeDifference)); 95 | figure; 96 | plot(deviceOrientationTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 97 | set(gcf,'color','w'); hold off; 98 | axis([min(deviceOrientationTime) max(deviceOrientationTime) min(timeDifference) max(timeDifference)]); 99 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 100 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 101 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 102 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 103 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 104 | 105 | 106 | %% 3) unbiased rotation rate 107 | 108 | % parsing unbiased rotation rate text 109 | textFileDir = 'gyro.txt'; 110 | textUnbiasedGyroData = importdata(textFileDir, delimiter, headerlinesIn); 111 | unbiasedGyroTime = textUnbiasedGyroData.data(:,1).'; 112 | unbiasedGyroTime = (unbiasedGyroTime - unbiasedGyroTime(1)) ./ nanoSecondToSecond; 113 | unbiasedGyroData = textUnbiasedGyroData.data(:,[2 3 4]).'; 114 | 115 | % plot unbiased rotation rate X-Y-Z 116 | figure; 117 | subplot(3,1,1); 118 | plot(unbiasedGyroTime, unbiasedGyroData(1,:), 'm'); hold on; grid on; axis tight; 119 | set(gcf,'color','w'); hold off; 120 | axis([min(unbiasedGyroTime) max(unbiasedGyroTime) min(unbiasedGyroData(1,:)) max(unbiasedGyroData(1,:))]); 121 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 122 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 123 | ylabel('X [rad/s]','FontName','Times New Roman','FontSize',17); 124 | subplot(3,1,2); 125 | plot(unbiasedGyroTime, unbiasedGyroData(2,:), 'm'); hold on; grid on; axis tight; 126 | set(gcf,'color','w'); hold off; 127 | axis([min(unbiasedGyroTime) max(unbiasedGyroTime) min(unbiasedGyroData(2,:)) max(unbiasedGyroData(2,:))]); 128 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 129 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 130 | ylabel('Y [rad/s]','FontName','Times New Roman','FontSize',17); 131 | subplot(3,1,3); 132 | plot(unbiasedGyroTime, unbiasedGyroData(3,:), 'm'); hold on; grid on; axis tight; 133 | set(gcf,'color','w'); hold off; 134 | axis([min(unbiasedGyroTime) max(unbiasedGyroTime) min(unbiasedGyroData(3,:)) max(unbiasedGyroData(3,:))]); 135 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 136 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 137 | ylabel('Z [rad/s]','FontName','Times New Roman','FontSize',17); 138 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 139 | 140 | % plot unbiased rotation rate update rate 141 | timeDifference = diff(unbiasedGyroTime); 142 | meanUpdateRate = (1/mean(timeDifference)); 143 | figure; 144 | plot(unbiasedGyroTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 145 | set(gcf,'color','w'); hold off; 146 | axis([min(unbiasedGyroTime) max(unbiasedGyroTime) min(timeDifference) max(timeDifference)]); 147 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 148 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 149 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 150 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 151 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 152 | 153 | 154 | %% 4) gravity vector 155 | 156 | % parsing gravity text 157 | textFileDir = 'gravity.txt'; 158 | textGravityData = importdata(textFileDir, delimiter, headerlinesIn); 159 | gravityTime = textGravityData.data(:,1).'; 160 | gravityTime = (gravityTime - gravityTime(1)) ./ nanoSecondToSecond; 161 | gravityData = textGravityData.data(:,[2 3 4]).'; 162 | 163 | % plot gravity vector X-Y-Z 164 | figure; 165 | subplot(3,1,1); 166 | plot(gravityTime, gravityData(1,:), 'm'); hold on; grid on; axis tight; 167 | set(gcf,'color','w'); hold off; 168 | axis([min(gravityTime) max(gravityTime) min(gravityData(1,:)) max(gravityData(1,:))]); 169 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 170 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 171 | ylabel('X [m/s-2]','FontName','Times New Roman','FontSize',17); 172 | subplot(3,1,2); 173 | plot(gravityTime, gravityData(2,:), 'm'); hold on; grid on; axis tight; 174 | set(gcf,'color','w'); hold off; 175 | axis([min(gravityTime) max(gravityTime) min(gravityData(2,:)) max(gravityData(2,:))]); 176 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 177 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 178 | ylabel('Y [m/s-2]','FontName','Times New Roman','FontSize',17); 179 | subplot(3,1,3); 180 | plot(gravityTime, gravityData(3,:), 'm'); hold on; grid on; axis tight; 181 | set(gcf,'color','w'); hold off; 182 | axis([min(gravityTime) max(gravityTime) min(gravityData(3,:)) max(gravityData(3,:))]); 183 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 184 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 185 | ylabel('Z [m/s-2]','FontName','Times New Roman','FontSize',17); 186 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 187 | 188 | % plot gravity vector update rate 189 | timeDifference = diff(gravityTime); 190 | meanUpdateRate = (1/mean(timeDifference)); 191 | figure; 192 | plot(gravityTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 193 | set(gcf,'color','w'); hold off; 194 | axis([min(gravityTime) max(gravityTime) min(timeDifference) max(timeDifference)]); 195 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 196 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 197 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 198 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 199 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 200 | 201 | 202 | %% 5) user-generated acceleration vector 203 | 204 | % parsing user-generated acceleration text 205 | textFileDir = 'linacce.txt'; 206 | textUserAccelerationData = importdata(textFileDir, delimiter, headerlinesIn); 207 | userAccelTime = textUserAccelerationData.data(:,1).'; 208 | userAccelTime = (userAccelTime - userAccelTime(1)) ./ nanoSecondToSecond; 209 | userAccelData = textUserAccelerationData.data(:,[2 3 4]).'; 210 | 211 | % plot user-generated acceleration vector X-Y-Z 212 | figure; 213 | subplot(3,1,1); 214 | plot(userAccelTime, userAccelData(1,:), 'm'); hold on; grid on; axis tight; 215 | set(gcf,'color','w'); hold off; 216 | axis([min(userAccelTime) max(userAccelTime) min(userAccelData(1,:)) max(userAccelData(1,:))]); 217 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 218 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 219 | ylabel('X [m/s-2]','FontName','Times New Roman','FontSize',17); 220 | subplot(3,1,2); 221 | plot(userAccelTime, userAccelData(2,:), 'm'); hold on; grid on; axis tight; 222 | set(gcf,'color','w'); hold off; 223 | axis([min(userAccelTime) max(userAccelTime) min(userAccelData(2,:)) max(userAccelData(2,:))]); 224 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 225 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 226 | ylabel('Y [m/s-2]','FontName','Times New Roman','FontSize',17); 227 | subplot(3,1,3); 228 | plot(userAccelTime, userAccelData(3,:), 'm'); hold on; grid on; axis tight; 229 | set(gcf,'color','w'); hold off; 230 | axis([min(userAccelTime) max(userAccelTime) min(userAccelData(3,:)) max(userAccelData(3,:))]); 231 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 232 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 233 | ylabel('Z [m/s-2]','FontName','Times New Roman','FontSize',17); 234 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 235 | 236 | % plot user-generated acceleration vector update rate 237 | timeDifference = diff(userAccelTime); 238 | meanUpdateRate = (1/mean(timeDifference)); 239 | figure; 240 | plot(userAccelTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 241 | set(gcf,'color','w'); hold off; 242 | axis([min(userAccelTime) max(userAccelTime) min(timeDifference) max(timeDifference)]); 243 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 244 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 245 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 246 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 247 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 248 | 249 | 250 | %% 6) calibrated magnetic field 251 | 252 | % parsing calibrated magnetic field text 253 | textFileDir = 'magnet.txt'; 254 | textMagnetData = importdata(textFileDir, delimiter, headerlinesIn); 255 | magnetTime = textMagnetData.data(:,1).'; 256 | magnetTime = (magnetTime - magnetTime(1)) ./ nanoSecondToSecond; 257 | magnetData = textMagnetData.data(:,[2 3 4]).'; 258 | 259 | % plot calibrated magnetic field X-Y-Z 260 | figure; 261 | subplot(3,1,1); 262 | plot(magnetTime, magnetData(1,:), 'm'); hold on; grid on; axis tight; 263 | set(gcf,'color','w'); hold off; 264 | axis([min(magnetTime) max(magnetTime) min(magnetData(1,:)) max(magnetData(1,:))]); 265 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 266 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 267 | ylabel('X [microT]','FontName','Times New Roman','FontSize',17); 268 | subplot(3,1,2); 269 | plot(magnetTime, magnetData(2,:), 'm'); hold on; grid on; axis tight; 270 | set(gcf,'color','w'); hold off; 271 | axis([min(magnetTime) max(magnetTime) min(magnetData(2,:)) max(magnetData(2,:))]); 272 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 273 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 274 | ylabel('Y [microT]','FontName','Times New Roman','FontSize',17); 275 | subplot(3,1,3); 276 | plot(magnetTime, magnetData(3,:), 'm'); hold on; grid on; axis tight; 277 | set(gcf,'color','w'); hold off; 278 | axis([min(magnetTime) max(magnetTime) min(magnetData(3,:)) max(magnetData(3,:))]); 279 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 280 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 281 | ylabel('Z [microT]','FontName','Times New Roman','FontSize',17); 282 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 283 | 284 | % plot calibrated magnetic field update rate 285 | timeDifference = diff(magnetTime); 286 | meanUpdateRate = (1/mean(timeDifference)); 287 | figure; 288 | plot(magnetTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 289 | set(gcf,'color','w'); hold off; 290 | axis([min(magnetTime) max(magnetTime) min(timeDifference) max(timeDifference)]); 291 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 292 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 293 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 294 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 295 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 296 | 297 | 298 | %% 7) heading angle (measured in degrees) 299 | 300 | % parsing heading angle text 301 | textFileDir = 'heading.txt'; 302 | textHeadingData = importdata(textFileDir, delimiter, headerlinesIn); 303 | headingTime = textHeadingData.data(:,1).'; 304 | headingTime = (headingTime - headingTime(1)) ./ nanoSecondToSecond; 305 | headingData = textHeadingData.data(:,2).'; 306 | 307 | % plot heading angle relative to the current reference frame 308 | figure; 309 | plot(headingTime, headingData, 'm'); hold on; grid on; axis tight; 310 | set(gcf,'color','w'); hold off; 311 | axis([min(headingTime) max(headingTime) min(headingData) max(headingData)]); 312 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 313 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 314 | ylabel('Heading [deg]','FontName','Times New Roman','FontSize',17); 315 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 316 | 317 | % plot heading angle update rate 318 | timeDifference = diff(headingTime); 319 | meanUpdateRate = (1/mean(timeDifference)); 320 | figure; 321 | plot(headingTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 322 | set(gcf,'color','w'); hold off; 323 | axis([min(headingTime) max(headingTime) min(timeDifference) max(timeDifference)]); 324 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 325 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 326 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 327 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 328 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 329 | 330 | 331 | %% 8) raw acceleration 332 | 333 | % parsing raw acceleration text 334 | textFileDir = 'acce.txt'; 335 | textRawAccelerationData = importdata(textFileDir, delimiter, headerlinesIn); 336 | rawAccelTime = textRawAccelerationData.data(:,1).'; 337 | rawAccelTime = (rawAccelTime - rawAccelTime(1)) ./ nanoSecondToSecond; 338 | rawAccelData = textRawAccelerationData.data(:,[2 3 4]).'; 339 | 340 | % plot raw acceleration X-Y-Z 341 | figure; 342 | subplot(3,1,1); 343 | plot(rawAccelTime, rawAccelData(1,:), 'm'); hold on; grid on; axis tight; 344 | set(gcf,'color','w'); hold off; 345 | axis([min(rawAccelTime) max(rawAccelTime) min(rawAccelData(1,:)) max(rawAccelData(1,:))]); 346 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 347 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 348 | ylabel('X [m/s-2]','FontName','Times New Roman','FontSize',17); 349 | subplot(3,1,2); 350 | plot(rawAccelTime, rawAccelData(2,:), 'm'); hold on; grid on; axis tight; 351 | set(gcf,'color','w'); hold off; 352 | axis([min(rawAccelTime) max(rawAccelTime) min(rawAccelData(2,:)) max(rawAccelData(2,:))]); 353 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 354 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 355 | ylabel('Y [m/s-2]','FontName','Times New Roman','FontSize',17); 356 | subplot(3,1,3); 357 | plot(rawAccelTime, rawAccelData(3,:), 'm'); hold on; grid on; axis tight; 358 | set(gcf,'color','w'); hold off; 359 | axis([min(rawAccelTime) max(rawAccelTime) min(rawAccelData(3,:)) max(rawAccelData(3,:))]); 360 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 361 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 362 | ylabel('Z [m/s-2]','FontName','Times New Roman','FontSize',17); 363 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 364 | 365 | % plot raw acceleration update rate 366 | timeDifference = diff(rawAccelTime); 367 | meanUpdateRate = (1/mean(timeDifference)); 368 | figure; 369 | plot(rawAccelTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 370 | set(gcf,'color','w'); hold off; 371 | axis([min(rawAccelTime) max(rawAccelTime) min(timeDifference) max(timeDifference)]); 372 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 373 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 374 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 375 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 376 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 377 | 378 | 379 | %% 9) raw rotation rate 380 | 381 | % parsing raw rotation rate text 382 | textFileDir = 'gyro_uncalib.txt'; 383 | textRawGyroData = importdata(textFileDir, delimiter, headerlinesIn); 384 | rawGyroTime = textRawGyroData.data(:,1).'; 385 | rawGyroTime = (rawGyroTime - rawGyroTime(1)) ./ nanoSecondToSecond; 386 | rawGyroData = textRawGyroData.data(:,[2 3 4]).'; 387 | 388 | % plot raw rotation rate X-Y-Z 389 | figure; 390 | subplot(3,1,1); 391 | plot(rawGyroTime, rawGyroData(1,:), 'm'); hold on; grid on; axis tight; 392 | set(gcf,'color','w'); hold off; 393 | axis([min(rawGyroTime) max(rawGyroTime) min(rawGyroData(1,:)) max(rawGyroData(1,:))]); 394 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 395 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 396 | ylabel('X [rad/s]','FontName','Times New Roman','FontSize',17); 397 | subplot(3,1,2); 398 | plot(rawGyroTime, rawGyroData(2,:), 'm'); hold on; grid on; axis tight; 399 | set(gcf,'color','w'); hold off; 400 | axis([min(rawGyroTime) max(rawGyroTime) min(rawGyroData(2,:)) max(rawGyroData(2,:))]); 401 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 402 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 403 | ylabel('Y [rad/s]','FontName','Times New Roman','FontSize',17); 404 | subplot(3,1,3); 405 | plot(rawGyroTime, rawGyroData(3,:), 'm'); hold on; grid on; axis tight; 406 | set(gcf,'color','w'); hold off; 407 | axis([min(rawGyroTime) max(rawGyroTime) min(rawGyroData(3,:)) max(rawGyroData(3,:))]); 408 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 409 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 410 | ylabel('Z [rad/s]','FontName','Times New Roman','FontSize',17); 411 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 412 | 413 | % plot raw rotation rate update rate 414 | timeDifference = diff(rawGyroTime); 415 | meanUpdateRate = (1/mean(timeDifference)); 416 | figure; 417 | plot(rawGyroTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 418 | set(gcf,'color','w'); hold off; 419 | axis([min(rawGyroTime) max(rawGyroTime) min(timeDifference) max(timeDifference)]); 420 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 421 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 422 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 423 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 424 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 425 | 426 | 427 | %% 10) raw magnetic field 428 | 429 | % parsing raw magnetic field text 430 | textFileDir = 'magnet_uncalib.txt'; 431 | textRawMagnetData = importdata(textFileDir, delimiter, headerlinesIn); 432 | rawMagnetTime = textRawMagnetData.data(:,1).'; 433 | rawMagnetTime = (rawMagnetTime - rawMagnetTime(1)) ./ nanoSecondToSecond; 434 | rawMagnetData = textRawMagnetData.data(:,[2 3 4]).'; 435 | 436 | % plot raw magnetic field X-Y-Z 437 | figure; 438 | subplot(3,1,1); 439 | plot(rawMagnetTime, rawMagnetData(1,:), 'm'); hold on; grid on; axis tight; 440 | set(gcf,'color','w'); hold off; 441 | axis([min(rawMagnetTime) max(rawMagnetTime) min(rawMagnetData(1,:)) max(rawMagnetData(1,:))]); 442 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 443 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 444 | ylabel('X [microT]','FontName','Times New Roman','FontSize',17); 445 | subplot(3,1,2); 446 | plot(rawMagnetTime, rawMagnetData(2,:), 'm'); hold on; grid on; axis tight; 447 | set(gcf,'color','w'); hold off; 448 | axis([min(rawMagnetTime) max(rawMagnetTime) min(rawMagnetData(2,:)) max(rawMagnetData(2,:))]); 449 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 450 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 451 | ylabel('Y [microT]','FontName','Times New Roman','FontSize',17); 452 | subplot(3,1,3); 453 | plot(rawMagnetTime, rawMagnetData(3,:), 'm'); hold on; grid on; axis tight; 454 | set(gcf,'color','w'); hold off; 455 | axis([min(rawMagnetTime) max(rawMagnetTime) min(rawMagnetData(3,:)) max(rawMagnetData(3,:))]); 456 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 457 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 458 | ylabel('Z [microT]','FontName','Times New Roman','FontSize',17); 459 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 460 | 461 | % plot raw magnetic field update rate 462 | timeDifference = diff(rawMagnetTime); 463 | meanUpdateRate = (1/mean(timeDifference)); 464 | figure; 465 | plot(rawMagnetTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 466 | set(gcf,'color','w'); hold off; 467 | axis([min(rawMagnetTime) max(rawMagnetTime) min(timeDifference) max(timeDifference)]); 468 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 469 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 470 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 471 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 472 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 473 | 474 | 475 | %% 11) step count / distance 476 | 477 | % parsing step text 478 | textFileDir = 'step.txt'; 479 | textSteptData = importdata(textFileDir, delimiter, headerlinesIn); 480 | stepTime = textSteptData.data(:,1).'; 481 | stepTime = (stepTime - stepTime(1)) ./ nanoSecondToSecond; 482 | stepData = textSteptData.data(:,[2 3]).'; 483 | 484 | % plot step count 485 | figure; 486 | subplot(2,1,1); 487 | plot(stepTime, stepData(1,:), 'm'); hold on; grid on; axis tight; 488 | set(gcf,'color','w'); hold off; 489 | axis([min(stepTime) max(stepTime) min(stepData(1,:)) max(stepData(1,:))]); 490 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 491 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 492 | ylabel('Step Counting','FontName','Times New Roman','FontSize',17); 493 | subplot(2,1,2); 494 | plot(stepTime, stepData(2,:), 'm'); hold on; grid on; axis tight; 495 | set(gcf,'color','w'); hold off; 496 | axis([min(stepTime) max(stepTime) min(stepData(2,:)) max(stepData(2,:))]); 497 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 498 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 499 | ylabel('Traveled Distance [m]','FontName','Times New Roman','FontSize',17); 500 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 501 | 502 | % plot step count update rate 503 | timeDifference = diff(stepTime); 504 | meanUpdateRate = (1/mean(timeDifference)); 505 | figure; 506 | plot(stepTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 507 | set(gcf,'color','w'); hold off; 508 | axis([min(stepTime) max(stepTime) min(timeDifference) max(timeDifference)]); 509 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 510 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 511 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 512 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 513 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 514 | 515 | 516 | %% 12) height 517 | 518 | % parsing height text 519 | textFileDir = 'height.txt'; 520 | textHeightData = importdata(textFileDir, delimiter, headerlinesIn); 521 | heightTime = textHeightData.data(:,1).'; 522 | heightTime = (heightTime - heightTime(1)) ./ nanoSecondToSecond; 523 | heightData = textHeightData.data(:,2).'; 524 | 525 | % plot relative height 526 | figure; 527 | plot(heightTime, heightData, 'm'); hold on; grid on; axis tight; 528 | set(gcf,'color','w'); hold off; 529 | axis([min(heightTime) max(heightTime) min(heightData) max(heightData)]); 530 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 531 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 532 | ylabel('Relative Height [m]','FontName','Times New Roman','FontSize',17); 533 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 534 | 535 | % plot height update rate 536 | timeDifference = diff(heightTime); 537 | meanUpdateRate = (1/mean(timeDifference)); 538 | figure; 539 | plot(heightTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 540 | set(gcf,'color','w'); hold off; 541 | axis([min(heightTime) max(heightTime) min(timeDifference) max(timeDifference)]); 542 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 543 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 544 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 545 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 546 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 547 | 548 | 549 | %% 13) pressure 550 | 551 | % parsing pressure text 552 | textFileDir = 'pressure.txt'; 553 | textPressureData = importdata(textFileDir, delimiter, headerlinesIn); 554 | pressureTime = textPressureData.data(:,1).'; 555 | pressureTime = (pressureTime - pressureTime(1)) ./ nanoSecondToSecond; 556 | pressureData = textPressureData.data(:,2).'; 557 | 558 | % plot pressure 559 | figure; 560 | plot(pressureTime, pressureData, 'm'); hold on; grid on; axis tight; 561 | set(gcf,'color','w'); hold off; 562 | axis([min(pressureTime) max(pressureTime) min(pressureData) max(pressureData)]); 563 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 564 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 565 | ylabel('Pressure [kPa]','FontName','Times New Roman','FontSize',17); 566 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 567 | 568 | % plot pressure update rate 569 | timeDifference = diff(pressureTime); 570 | meanUpdateRate = (1/mean(timeDifference)); 571 | figure; 572 | plot(pressureTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 573 | set(gcf,'color','w'); hold off; 574 | axis([min(pressureTime) max(pressureTime) min(timeDifference) max(timeDifference)]); 575 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 576 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 577 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 578 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 579 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 580 | 581 | 582 | %% 14) battery 583 | 584 | % parsing battery text 585 | textFileDir = 'battery.txt'; 586 | textBatteryData = importdata(textFileDir, delimiter, headerlinesIn); 587 | batteryTime = textBatteryData.data(:,1).'; 588 | batteryTime = (batteryTime - batteryTime(1)) ./ nanoSecondToSecond; 589 | batteryData = textBatteryData.data(:,2).'; 590 | 591 | % plot battery 592 | figure; 593 | plot(batteryTime, batteryData, 'm'); hold on; grid on; axis tight; 594 | set(gcf,'color','w'); hold off; 595 | axis([min(batteryTime) max(batteryTime) min(batteryData) max(batteryData)]); 596 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 597 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 598 | ylabel('Battery [%]','FontName','Times New Roman','FontSize',17); 599 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 600 | 601 | % plot battery update rate 602 | timeDifference = diff(batteryTime); 603 | meanUpdateRate = (1/mean(timeDifference)); 604 | figure; 605 | plot(batteryTime(2:end), timeDifference, 'm'); hold on; grid on; axis tight; 606 | set(gcf,'color','w'); hold off; 607 | axis([min(batteryTime) max(batteryTime) min(timeDifference) max(timeDifference)]); 608 | set(get(gcf,'CurrentAxes'),'FontName','Times New Roman','FontSize',17); 609 | xlabel('Time [sec]','FontName','Times New Roman','FontSize',17); 610 | ylabel('Time Difference [sec]','FontName','Times New Roman','FontSize',17); 611 | title(['Mean Update Rate: ', num2str(meanUpdateRate), ' Hz'],'FontName','Times New Roman','FontSize',17); 612 | set(gcf,'Units','pixels','Position',[100 200 1800 900]); % modify figure 613 | 614 | 615 | 616 | 617 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CoreLocationMotion-Data-Logger 4 | // 5 | // Created by kimpyojin on 29/05/2019. 6 | // Copyright © 2019 Pyojin Kim. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreLocation 11 | import CoreMotion 12 | import os.log 13 | 14 | class ViewController: UIViewController, CLLocationManagerDelegate { 15 | 16 | // cellphone screen UI outlet objects 17 | @IBOutlet weak var startStopButton: UIButton! 18 | @IBOutlet weak var statusLabel: UILabel! 19 | 20 | @IBOutlet weak var latitudeLabel: UILabel! 21 | @IBOutlet weak var longitudeLabel: UILabel! 22 | @IBOutlet weak var horizontalAccuracyLabel: UILabel! 23 | @IBOutlet weak var altitudeLabel: UILabel! 24 | @IBOutlet weak var buildingFloorLabel: UILabel! 25 | @IBOutlet weak var verticalAccuracyLabel: UILabel! 26 | 27 | @IBOutlet weak var rxLabel: UILabel! 28 | @IBOutlet weak var ryLabel: UILabel! 29 | @IBOutlet weak var rzLabel: UILabel! 30 | @IBOutlet weak var mxLabel: UILabel! 31 | @IBOutlet weak var myLabel: UILabel! 32 | @IBOutlet weak var mzLabel: UILabel! 33 | 34 | @IBOutlet weak var axLabel: UILabel! 35 | @IBOutlet weak var ayLabel: UILabel! 36 | @IBOutlet weak var azLabel: UILabel! 37 | 38 | @IBOutlet weak var wxLabel: UILabel! 39 | @IBOutlet weak var wyLabel: UILabel! 40 | @IBOutlet weak var wzLabel: UILabel! 41 | 42 | @IBOutlet weak var stepCounterLabel: UILabel! 43 | @IBOutlet weak var distanceLabel: UILabel! 44 | 45 | 46 | // constants for collecting data 47 | let numSensor = 14 48 | let GYRO_TXT = 0 49 | let GYRO_UNCALIB_TXT = 1 50 | let ACCE_TXT = 2 51 | let LINACCE_TXT = 3 52 | let GRAVITY_TXT = 4 53 | let MAGNET_TXT = 5 54 | let MAGNET_UNCALIB_TXT = 6 55 | let GAME_RV_TXT = 7 56 | let GPS_TXT = 8 57 | let STEP_TXT = 9 58 | let HEADING_TXT = 10 59 | let HEIGHT_TXT = 11 60 | let PRESSURE_TXT = 12 61 | let BATTERY_TXT = 13 62 | 63 | let sampleFrequency: TimeInterval = 200 64 | let gravity: Double = 9.81 65 | let defaultValue: Double = 0.0 66 | var isRecording: Bool = false 67 | 68 | 69 | // various motion managers and queue instances 70 | let locationManager = CLLocationManager() 71 | let motionManager = CMMotionManager() 72 | let pedoMeter = CMPedometer() 73 | let altimeter = CMAltimeter() 74 | let customQueue: DispatchQueue = DispatchQueue(label: "pyojinkim.me") 75 | 76 | // variables for measuring time in iOS clock 77 | var recordingTimer: Timer = Timer() 78 | var batteryLevelTimer: Timer = Timer() 79 | var secondCounter: Int64 = 0 { 80 | didSet { 81 | statusLabel.text = interfaceIntTime(second: secondCounter) 82 | } 83 | } 84 | let mulSecondToNanoSecond: Double = 1000000000 85 | 86 | 87 | // text file input & output 88 | var fileHandlers = [FileHandle]() 89 | var fileURLs = [URL]() 90 | var fileNames: [String] = ["gyro.txt", "gyro_uncalib.txt", "acce.txt", "linacce.txt", "gravity.txt", "magnet.txt", "magnet_uncalib.txt", "game_rv.txt", "gps.txt", "step.txt", "heading.txt", "height.txt", "pressure.txt", "battery.txt"] 91 | 92 | 93 | override func viewDidLoad() { 94 | super.viewDidLoad() 95 | 96 | // default device setting 97 | statusLabel.text = "Ready" 98 | UIDevice.current.isBatteryMonitoringEnabled = true 99 | 100 | // define Core Location manager setting 101 | locationManager.delegate = self 102 | locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation 103 | locationManager.requestAlwaysAuthorization() 104 | locationManager.startUpdatingLocation() 105 | 106 | // define Core Motion manager setting 107 | customQueue.async { 108 | self.startIMUUpdate() 109 | self.startPedometerUpdate() 110 | self.startAltimeterUpdate() 111 | self.startBatteryLevelUpdate() 112 | } 113 | } 114 | 115 | 116 | override func viewWillDisappear(_ animated: Bool) { 117 | locationManager.stopUpdatingLocation() 118 | customQueue.sync { 119 | stopIMUUpdate() 120 | } 121 | pedoMeter.stopUpdates() 122 | altimeter.stopRelativeAltitudeUpdates() 123 | } 124 | 125 | 126 | // when the Start/Stop button is pressed 127 | @IBAction func startStopButtonPressed(_ sender: UIButton) { 128 | if (self.isRecording == false) { 129 | 130 | // start GPS/IMU data recording 131 | customQueue.async { 132 | if (self.createFiles()) { 133 | DispatchQueue.main.async { 134 | // reset timer 135 | self.secondCounter = 0 136 | self.recordingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (Timer) -> Void in 137 | self.secondCounter += 1 138 | }) 139 | 140 | // update UI 141 | self.startStopButton.setTitle("Stop", for: .normal) 142 | 143 | // make sure the screen won't lock 144 | UIApplication.shared.isIdleTimerDisabled = true 145 | } 146 | self.isRecording = true 147 | } else { 148 | self.errorMsg(msg: "Failed to create the file") 149 | return 150 | } 151 | } 152 | } else { 153 | 154 | // stop recording and share the recorded text file 155 | if (recordingTimer.isValid) { 156 | recordingTimer.invalidate() 157 | } 158 | if (batteryLevelTimer.isValid) { 159 | batteryLevelTimer.invalidate() 160 | } 161 | 162 | customQueue.async { 163 | self.isRecording = false 164 | if (self.fileHandlers.count == self.numSensor) { 165 | for handler in self.fileHandlers { 166 | handler.closeFile() 167 | } 168 | DispatchQueue.main.async { 169 | let activityVC = UIActivityViewController(activityItems: self.fileURLs, applicationActivities: nil) 170 | self.present(activityVC, animated: true, completion: nil) 171 | } 172 | } 173 | } 174 | 175 | // initialize UI on the screen 176 | self.latitudeLabel.text = String(format:"%.3f", self.defaultValue) 177 | self.longitudeLabel.text = String(format:"%.3f", self.defaultValue) 178 | self.horizontalAccuracyLabel.text = String(format:"%.3f", self.defaultValue) 179 | self.altitudeLabel.text = String(format:"%.2f", self.defaultValue) 180 | self.buildingFloorLabel.text = String(format:"%02df", self.defaultValue) 181 | self.verticalAccuracyLabel.text = String(format:"%.3f", self.defaultValue) 182 | 183 | self.rxLabel.text = String(format:"%.3f", self.defaultValue) 184 | self.ryLabel.text = String(format:"%.3f", self.defaultValue) 185 | self.rzLabel.text = String(format:"%.3f", self.defaultValue) 186 | self.mxLabel.text = String(format:"%.3f", self.defaultValue) 187 | self.myLabel.text = String(format:"%.3f", self.defaultValue) 188 | self.mzLabel.text = String(format:"%.3f", self.defaultValue) 189 | 190 | self.axLabel.text = String(format:"%.3f", self.defaultValue) 191 | self.ayLabel.text = String(format:"%.3f", self.defaultValue) 192 | self.azLabel.text = String(format:"%.3f", self.defaultValue) 193 | 194 | self.wxLabel.text = String(format:"%.3f", self.defaultValue) 195 | self.wyLabel.text = String(format:"%.3f", self.defaultValue) 196 | self.wzLabel.text = String(format:"%.3f", self.defaultValue) 197 | 198 | self.stepCounterLabel.text = String(format:"%04d", self.defaultValue) 199 | self.distanceLabel.text = String(format:"%.1f", self.defaultValue) 200 | 201 | self.startStopButton.setTitle("Start", for: .normal) 202 | self.statusLabel.text = "Ready" 203 | 204 | // resume screen lock 205 | UIApplication.shared.isIdleTimerDisabled = false 206 | } 207 | } 208 | 209 | 210 | // define startUpdatingLocation() function 211 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 212 | 213 | // optional binding for safety 214 | if let latestLocation = manager.location { 215 | let timestamp = latestLocation.timestamp.timeIntervalSince1970 * self.mulSecondToNanoSecond 216 | let latitude = latestLocation.coordinate.latitude 217 | let longitude = latestLocation.coordinate.longitude 218 | let horizontalAccuracy = latestLocation.horizontalAccuracy 219 | let altitude = latestLocation.altitude 220 | let verticalAccuracy = latestLocation.verticalAccuracy 221 | var buildingFloor = -9 222 | if let temp = latestLocation.floor { 223 | buildingFloor = temp.level 224 | } 225 | 226 | // dispatch queue to display UI 227 | DispatchQueue.main.async { 228 | self.latitudeLabel.text = String(format:"%.3f", latitude) 229 | self.longitudeLabel.text = String(format:"%.3f", longitude) 230 | self.horizontalAccuracyLabel.text = String(format:"%.3f", horizontalAccuracy) 231 | self.altitudeLabel.text = String(format:"%.2f", altitude) 232 | self.verticalAccuracyLabel.text = String(format:"%.3f", verticalAccuracy) 233 | self.buildingFloorLabel.text = String(format:"%02d", buildingFloor) 234 | } 235 | 236 | // custom queue to save GPS location data 237 | self.customQueue.async { 238 | if ((self.fileHandlers.count == self.numSensor) && self.isRecording) { 239 | let locationData = String(format: "%.0f %.6f %.6f %.6f %.6f %.6f %.6f \n", 240 | timestamp, 241 | latitude, 242 | longitude, 243 | horizontalAccuracy, 244 | altitude, 245 | verticalAccuracy, 246 | buildingFloor) 247 | if let locationDataToWrite = locationData.data(using: .utf8) { 248 | self.fileHandlers[self.GPS_TXT].write(locationDataToWrite) 249 | } else { 250 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 251 | } 252 | } 253 | } 254 | } 255 | } 256 | 257 | 258 | // define didFailWithError function 259 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 260 | print("GPS Error => \(error.localizedDescription)") 261 | } 262 | 263 | 264 | // define startIMUUpdate() function 265 | private func startIMUUpdate() { 266 | 267 | // define IMU update interval up to 200 Hz (in real, iOS can only support up to 100 Hz) 268 | motionManager.deviceMotionUpdateInterval = 1.0 / sampleFrequency 269 | motionManager.showsDeviceMovementDisplay = true 270 | motionManager.accelerometerUpdateInterval = 1.0 / sampleFrequency 271 | motionManager.gyroUpdateInterval = 1.0 / sampleFrequency 272 | motionManager.magnetometerUpdateInterval = 1.0 / sampleFrequency 273 | 274 | 275 | // 1) update device motion 276 | if (!motionManager.isDeviceMotionActive) { 277 | motionManager.startDeviceMotionUpdates(using: .xMagneticNorthZVertical, to: OperationQueue.main) { (motion: CMDeviceMotion?, error: Error?) in 278 | 279 | // optional binding for safety 280 | if let deviceMotion = motion { 281 | //let timestamp = Date().timeIntervalSince1970 * self.mulSecondToNanoSecond 282 | let timestamp = deviceMotion.timestamp * self.mulSecondToNanoSecond 283 | let deviceOrientationRx = deviceMotion.attitude.pitch 284 | let deviceOrientationRy = deviceMotion.attitude.roll 285 | let deviceOrientationRz = deviceMotion.attitude.yaw 286 | 287 | let deviceOrientationQx = deviceMotion.attitude.quaternion.x 288 | let deviceOrientationQy = deviceMotion.attitude.quaternion.y 289 | let deviceOrientationQz = deviceMotion.attitude.quaternion.z 290 | let deviceOrientationQw = deviceMotion.attitude.quaternion.w 291 | 292 | let processedGyroDataX = deviceMotion.rotationRate.x 293 | let processedGyroDataY = deviceMotion.rotationRate.y 294 | let processedGyroDataZ = deviceMotion.rotationRate.z 295 | 296 | let gravityGx = deviceMotion.gravity.x * self.gravity 297 | let gravityGy = deviceMotion.gravity.y * self.gravity 298 | let gravityGz = deviceMotion.gravity.z * self.gravity 299 | 300 | let userAccelDataX = deviceMotion.userAcceleration.x * self.gravity 301 | let userAccelDataY = deviceMotion.userAcceleration.y * self.gravity 302 | let userAccelDataZ = deviceMotion.userAcceleration.z * self.gravity 303 | 304 | let magneticFieldX = deviceMotion.magneticField.field.x 305 | let magneticFieldY = deviceMotion.magneticField.field.y 306 | let magneticFieldZ = deviceMotion.magneticField.field.z 307 | 308 | let deviceHeadingAngle = deviceMotion.heading 309 | 310 | // dispatch queue to display UI 311 | DispatchQueue.main.async { 312 | self.rxLabel.text = String(format:"%.3f", deviceOrientationRx) 313 | self.ryLabel.text = String(format:"%.3f", deviceOrientationRy) 314 | self.rzLabel.text = String(format:"%.3f", deviceOrientationRz) 315 | 316 | self.mxLabel.text = String(format:"%.3f", magneticFieldX) 317 | self.myLabel.text = String(format:"%.3f", magneticFieldY) 318 | self.mzLabel.text = String(format:"%.3f", magneticFieldZ) 319 | } 320 | 321 | // custom queue to save IMU text data 322 | self.customQueue.async { 323 | if ((self.fileHandlers.count == self.numSensor) && self.isRecording) { 324 | 325 | // the device orientation expressed in the quaternion format 326 | let attitudeData = String(format: "%.0f %.6f %.6f %.6f %.6f \n", 327 | timestamp, 328 | deviceOrientationQx, 329 | deviceOrientationQy, 330 | deviceOrientationQz, 331 | deviceOrientationQw) 332 | if let attitudeDataToWrite = attitudeData.data(using: .utf8) { 333 | self.fileHandlers[self.GAME_RV_TXT].write(attitudeDataToWrite) 334 | } else { 335 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 336 | } 337 | 338 | // the unbiased rotation rate 339 | let processedGyroData = String(format: "%.0f %.6f %.6f %.6f \n", 340 | timestamp, 341 | processedGyroDataX, 342 | processedGyroDataY, 343 | processedGyroDataZ) 344 | if let processedGyroDataToWrite = processedGyroData.data(using: .utf8) { 345 | self.fileHandlers[self.GYRO_TXT].write(processedGyroDataToWrite) 346 | } else { 347 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 348 | } 349 | 350 | // the current gravity vector 351 | let gravityData = String(format: "%.0f %.6f %.6f %.6f \n", 352 | timestamp, 353 | gravityGx, 354 | gravityGy, 355 | gravityGz) 356 | if let gravityDataToWrite = gravityData.data(using: .utf8) { 357 | self.fileHandlers[self.GRAVITY_TXT].write(gravityDataToWrite) 358 | } else { 359 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 360 | } 361 | 362 | // the user-generated acceleration vector (without gravity) 363 | let userAccelData = String(format: "%.0f %.6f %.6f %.6f \n", 364 | timestamp, 365 | userAccelDataX, 366 | userAccelDataY, 367 | userAccelDataZ) 368 | if let userAccelDataToWrite = userAccelData.data(using: .utf8) { 369 | self.fileHandlers[self.LINACCE_TXT].write(userAccelDataToWrite) 370 | } else { 371 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 372 | } 373 | 374 | // the current magnetic field vector 375 | let magneticData = String(format: "%.0f %.6f %.6f %.6f \n", 376 | timestamp, 377 | magneticFieldX, 378 | magneticFieldY, 379 | magneticFieldZ) 380 | if let magneticDataToWrite = magneticData.data(using: .utf8) { 381 | self.fileHandlers[self.MAGNET_TXT].write(magneticDataToWrite) 382 | } else { 383 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 384 | } 385 | 386 | // the heading angle (degrees) relative to the reference frame 387 | let headingAngleData = String(format: "%.0f %.6f \n", 388 | timestamp, 389 | deviceHeadingAngle) 390 | if let headingAngleDataToWrite = headingAngleData.data(using: .utf8) { 391 | self.fileHandlers[self.HEADING_TXT].write(headingAngleDataToWrite) 392 | } else { 393 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 394 | } 395 | } 396 | } 397 | } 398 | } 399 | } 400 | 401 | 402 | // 2) update raw acceleration value 403 | if (!motionManager.isAccelerometerActive) { 404 | motionManager.startAccelerometerUpdates(to: OperationQueue.main) { (motion: CMAccelerometerData?, error: Error?) in 405 | 406 | // optional binding for safety 407 | if let accelerometerData = motion { 408 | //let timestamp = Date().timeIntervalSince1970 * self.mulSecondToNanoSecond 409 | let timestamp = accelerometerData.timestamp * self.mulSecondToNanoSecond 410 | let rawAccelDataX = accelerometerData.acceleration.x * self.gravity 411 | let rawAccelDataY = accelerometerData.acceleration.y * self.gravity 412 | let rawAccelDataZ = accelerometerData.acceleration.z * self.gravity 413 | 414 | // dispatch queue to display UI 415 | DispatchQueue.main.async { 416 | self.axLabel.text = String(format:"%.3f", rawAccelDataX) 417 | self.ayLabel.text = String(format:"%.3f", rawAccelDataY) 418 | self.azLabel.text = String(format:"%.3f", rawAccelDataZ) 419 | } 420 | 421 | // custom queue to save IMU text data 422 | self.customQueue.async { 423 | if ((self.fileHandlers.count == self.numSensor) && self.isRecording) { 424 | let rawAccelData = String(format: "%.0f %.6f %.6f %.6f \n", 425 | timestamp, 426 | rawAccelDataX, 427 | rawAccelDataY, 428 | rawAccelDataZ) 429 | if let rawAccelDataToWrite = rawAccelData.data(using: .utf8) { 430 | self.fileHandlers[self.ACCE_TXT].write(rawAccelDataToWrite) 431 | } else { 432 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 433 | } 434 | } 435 | } 436 | } 437 | } 438 | } 439 | 440 | 441 | // 3) update raw gyroscope value 442 | if (!motionManager.isGyroActive) { 443 | motionManager.startGyroUpdates(to: OperationQueue.main) { (motion: CMGyroData?, error: Error?) in 444 | 445 | // optional binding for safety 446 | if let gyroData = motion { 447 | //let timestamp = Date().timeIntervalSince1970 * self.mulSecondToNanoSecond 448 | let timestamp = gyroData.timestamp * self.mulSecondToNanoSecond 449 | let rawGyroDataX = gyroData.rotationRate.x 450 | let rawGyroDataY = gyroData.rotationRate.y 451 | let rawGyroDataZ = gyroData.rotationRate.z 452 | 453 | // dispatch queue to display UI 454 | DispatchQueue.main.async { 455 | self.wxLabel.text = String(format:"%.3f", rawGyroDataX) 456 | self.wyLabel.text = String(format:"%.3f", rawGyroDataY) 457 | self.wzLabel.text = String(format:"%.3f", rawGyroDataZ) 458 | } 459 | 460 | // custom queue to save IMU text data 461 | self.customQueue.async { 462 | if ((self.fileHandlers.count == self.numSensor) && self.isRecording) { 463 | let rawGyroData = String(format: "%.0f %.6f %.6f %.6f \n", 464 | timestamp, 465 | rawGyroDataX, 466 | rawGyroDataY, 467 | rawGyroDataZ) 468 | if let rawGyroDataToWrite = rawGyroData.data(using: .utf8) { 469 | self.fileHandlers[self.GYRO_UNCALIB_TXT].write(rawGyroDataToWrite) 470 | } else { 471 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 472 | } 473 | } 474 | } 475 | } 476 | } 477 | } 478 | 479 | 480 | // 4) update raw magnetometer data 481 | if (!motionManager.isMagnetometerActive) { 482 | motionManager.startMagnetometerUpdates(to: OperationQueue.main) { (motion: CMMagnetometerData?, error: Error?) in 483 | 484 | // optional binding for safety 485 | if let magnetometerData = motion { 486 | //let timestamp = Date().timeIntervalSince1970 * self.mulSecondToNanoSecond 487 | let timestamp = magnetometerData.timestamp * self.mulSecondToNanoSecond 488 | let rawMagnetDataX = magnetometerData.magneticField.x 489 | let rawMagnetDataY = magnetometerData.magneticField.y 490 | let rawMagnetDataZ = magnetometerData.magneticField.z 491 | 492 | // custom queue to save IMU text data 493 | self.customQueue.async { 494 | if ((self.fileHandlers.count == self.numSensor) && self.isRecording) { 495 | let rawMagnetData = String(format: "%.0f %.6f %.6f %.6f \n", 496 | timestamp, 497 | rawMagnetDataX, 498 | rawMagnetDataY, 499 | rawMagnetDataZ) 500 | if let rawMagnetDataToWrite = rawMagnetData.data(using: .utf8) { 501 | self.fileHandlers[self.MAGNET_UNCALIB_TXT].write(rawMagnetDataToWrite) 502 | } else { 503 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 504 | } 505 | } 506 | } 507 | } 508 | } 509 | } 510 | } 511 | 512 | 513 | // define startPedometerUpdate() function 514 | private func startPedometerUpdate() { 515 | 516 | // check the step counter and distance are available 517 | if (CMPedometer.isStepCountingAvailable() && CMPedometer.isDistanceAvailable()) { 518 | pedoMeter.startUpdates(from: Date()) { (motion: CMPedometerData?, error: Error?) in 519 | 520 | // optional binding for safety 521 | if let pedometerData = motion { 522 | let timestamp = Date().timeIntervalSince1970 * self.mulSecondToNanoSecond 523 | let stepCounter = pedometerData.numberOfSteps.intValue 524 | var distance: Double = -100 525 | if let temp = pedometerData.distance { 526 | distance = temp.doubleValue 527 | } 528 | 529 | // dispatch queue to display UI 530 | DispatchQueue.main.async { 531 | self.stepCounterLabel.text = String(format:"%04d", stepCounter) 532 | self.distanceLabel.text = String(format:"%.1f", distance) 533 | } 534 | 535 | // custom queue to save pedometer data 536 | self.customQueue.async { 537 | if ((self.fileHandlers.count == self.numSensor) && self.isRecording) { 538 | let pedoData = String(format: "%.0f %04d %.3f \n", 539 | timestamp, 540 | stepCounter, 541 | distance) 542 | if let pedoDataToWrite = pedoData.data(using: .utf8) { 543 | self.fileHandlers[self.STEP_TXT].write(pedoDataToWrite) 544 | } else { 545 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 546 | } 547 | } 548 | } 549 | } 550 | } 551 | } 552 | } 553 | 554 | 555 | // define startAltimeterUpdate() function 556 | private func startAltimeterUpdate() { 557 | 558 | // check barometric sensor information are available 559 | if (CMAltimeter.isRelativeAltitudeAvailable()) { 560 | altimeter.startRelativeAltitudeUpdates(to: OperationQueue.main) { (motion: CMAltitudeData?, error: Error?) in 561 | 562 | // optional binding for safety 563 | if let barometerData = motion { 564 | //let timestamp = Date().timeIntervalSince1970 * self.mulSecondToNanoSecond 565 | let timestamp = barometerData.timestamp * self.mulSecondToNanoSecond 566 | let relativeAltitude = barometerData.relativeAltitude.doubleValue 567 | let pressure = barometerData.pressure.doubleValue 568 | 569 | // custom queue to save barometric text data 570 | self.customQueue.async { 571 | if ((self.fileHandlers.count == self.numSensor) && self.isRecording) { 572 | 573 | // the change in altitude (in meters) since the first reported event 574 | let relativeAltitudeData = String(format: "%.0f %.6f \n", 575 | timestamp, 576 | relativeAltitude) 577 | if let relativeAltitudeDataToWrite = relativeAltitudeData.data(using: .utf8) { 578 | self.fileHandlers[self.HEIGHT_TXT].write(relativeAltitudeDataToWrite) 579 | } else { 580 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 581 | } 582 | 583 | // the recorded pressure (in kilopascals) 584 | let pressureData = String(format: "%.0f %.6f \n", 585 | timestamp, 586 | pressure) 587 | if let pressureDataToWrite = pressureData.data(using: .utf8) { 588 | self.fileHandlers[self.PRESSURE_TXT].write(pressureDataToWrite) 589 | } else { 590 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 591 | } 592 | } 593 | } 594 | } 595 | } 596 | } 597 | } 598 | 599 | 600 | // define startBatteryLevelUpdate() function 601 | private func startBatteryLevelUpdate() { 602 | DispatchQueue.main.async { 603 | self.batteryLevelTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (Timer) -> Void in 604 | 605 | let timestamp = Date().timeIntervalSince1970 * self.mulSecondToNanoSecond 606 | let batteryLevel = UIDevice.current.batteryLevel 607 | 608 | // custom queue to save battery level text data 609 | self.customQueue.async { 610 | if ((self.fileHandlers.count == self.numSensor) && self.isRecording) { 611 | 612 | // the battery charge level for the device 613 | let batteryLevelData = String(format: "%.0f %.6f \n", 614 | timestamp, 615 | batteryLevel) 616 | if let batteryLevelDataToWrite = batteryLevelData.data(using: .utf8) { 617 | self.fileHandlers[self.BATTERY_TXT].write(batteryLevelDataToWrite) 618 | } else { 619 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 620 | } 621 | } 622 | } 623 | }) 624 | } 625 | } 626 | 627 | 628 | private func stopIMUUpdate() { 629 | if (motionManager.isDeviceMotionActive) { 630 | motionManager.stopDeviceMotionUpdates() 631 | } 632 | if (motionManager.isAccelerometerActive) { 633 | motionManager.stopAccelerometerUpdates() 634 | } 635 | if (motionManager.isGyroActive) { 636 | motionManager.stopGyroUpdates() 637 | } 638 | if (motionManager.isMagnetometerActive) { 639 | motionManager.stopMagnetometerUpdates() 640 | } 641 | } 642 | 643 | 644 | // some useful functions 645 | private func errorMsg(msg: String) { 646 | DispatchQueue.main.async { 647 | let fileAlert = UIAlertController(title: "CoreLocationMotion-Data-Logger", message: msg, preferredStyle: .alert) 648 | fileAlert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) 649 | self.present(fileAlert, animated: true, completion: nil) 650 | } 651 | } 652 | 653 | 654 | private func createFiles() -> Bool { 655 | 656 | // initialize file handlers 657 | self.fileHandlers.removeAll() 658 | self.fileURLs.removeAll() 659 | 660 | // create each GPS/IMU sensor text files 661 | let startHeader = "" 662 | for i in 0...(self.numSensor - 1) { 663 | var url = URL(fileURLWithPath: NSTemporaryDirectory()) 664 | url.appendPathComponent(fileNames[i]) 665 | self.fileURLs.append(url) 666 | 667 | // delete previous text files 668 | if (FileManager.default.fileExists(atPath: url.path)) { 669 | do { 670 | try FileManager.default.removeItem(at: url) 671 | } catch { 672 | os_log("cannot remove previous file", log:.default, type:.error) 673 | return false 674 | } 675 | } 676 | 677 | // create new text files 678 | if (!FileManager.default.createFile(atPath: url.path, contents: startHeader.data(using: String.Encoding.utf8), attributes: nil)) { 679 | self.errorMsg(msg: "cannot create file \(self.fileNames[i])") 680 | return false 681 | } 682 | 683 | // assign new file handlers 684 | let fileHandle: FileHandle? = FileHandle(forWritingAtPath: url.path) 685 | if let handle = fileHandle { 686 | self.fileHandlers.append(handle) 687 | } else { 688 | return false 689 | } 690 | } 691 | 692 | // write current recording time information 693 | let timeHeader = "# Created at \(timeToString()) in Burnaby Canada \n" 694 | for i in 0...(self.numSensor - 1) { 695 | if let timeHeaderToWrite = timeHeader.data(using: .utf8) { 696 | self.fileHandlers[i].write(timeHeaderToWrite) 697 | } else { 698 | os_log("Failed to write data record", log: OSLog.default, type: .fault) 699 | return false 700 | } 701 | } 702 | 703 | // return true if everything is alright 704 | return true 705 | } 706 | } 707 | -------------------------------------------------------------------------------- /CoreLocationMotion-Data-Logger/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 35 | 42 | 49 | 56 | 63 | 70 | 77 | 84 | 91 | 98 | 105 | 112 | 119 | 126 | 133 | 140 | 147 | 154 | 161 | 168 | 175 | 182 | 189 | 196 | 203 | 210 | 217 | 224 | 231 | 238 | 245 | 252 | 259 | 266 | 273 | 280 | 287 | 294 | 301 | 308 | 315 | 322 | 329 | 336 | 343 | 350 | 357 | 364 | 371 | 380 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | --------------------------------------------------------------------------------