diff --git a/Gemfile.lock b/Gemfile.lock index c8762fa..24683d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.1) + CFPropertyList (3.0.3) activesupport (4.2.11.1) i18n (~> 0.7) minitest (~> 5.1) @@ -12,8 +12,25 @@ GEM algoliasearch (1.27.1) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + artifactory (3.0.15) atomos (0.1.3) - babosa (1.0.3) + aws-eventstream (1.1.1) + aws-partitions (1.484.0) + aws-sdk-core (3.119.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.46.0) + aws-sdk-core (~> 3, >= 3.119.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.98.0) + aws-sdk-core (~> 3, >= 3.119.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.4) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) claide (1.0.3) cocoapods (1.8.4) activesupport (>= 4.0.2, < 5) @@ -52,93 +69,125 @@ GEM cocoapods-try (1.1.0) colored (1.2) colored2 (3.1.2) - commander-fastlane (4.4.6) - highline (~> 1.7.2) + commander (4.6.0) + highline (~> 2.0.0) concurrent-ruby (1.1.5) - declarative (0.0.10) - declarative-option (0.1.0) - digest-crc (0.4.1) + declarative (0.0.20) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.5) - emoji_regex (1.0.1) + dotenv (2.7.6) + emoji_regex (3.2.2) escape (0.0.4) - excon (0.71.0) - faraday (0.17.0) + excon (0.85.0) + faraday (1.6.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0.1) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) multipart-post (>= 1.2, < 3) - faraday-cookie_jar (0.0.6) - faraday (>= 0.7.4) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) http-cookie (~> 1.0.0) - faraday_middleware (0.13.1) - faraday (>= 0.7.4, < 1.0) - fastimage (2.1.7) - fastlane (2.136.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday_middleware (1.1.0) + faraday (~> 1.0) + fastimage (2.2.4) + fastlane (2.191.0) CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.3, < 3.0.0) - babosa (>= 1.0.2, < 2.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) colored - commander-fastlane (>= 4.4.6, < 5.0.0) + commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 2.0) - excon (>= 0.45.0, < 1.0.0) - faraday (~> 0.17) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 0.13.1) + faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.21.2, < 0.24.0) - google-cloud-storage (>= 1.15.0, < 2.0.0) - highline (>= 1.7.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) json (< 3.0.0) - jwt (~> 2.1.0) + jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multi_xml (~> 0.5) multipart-post (~> 2.0.0) + naturally (~> 2.2) plist (>= 3.1.0, < 4.0.0) - public_suffix (~> 2.0.0) - rubyzip (>= 1.3.0, < 2.0.0) + rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) simctl (~> 1.6.3) - slack-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (>= 1.4.5, < 2.0.0) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) - xcodeproj (>= 1.8.1, < 2.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) ffi (1.11.2) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-api-client (0.23.9) + google-apis-androidpublisher_v3 (0.10.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.5, < 0.7.0) - httpclient (>= 2.8.1, < 3.0) - mime-types (~> 3.0) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) representable (~> 3.0) - retriable (>= 2.0, < 4.0) - signet (~> 0.9) - google-cloud-core (1.4.1) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.6.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-playcustomapp_v1 (0.5.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-storage_v1 (0.6.0) + google-apis-core (>= 0.4, < 2.a) + google-cloud-core (1.6.0) google-cloud-env (~> 1.0) - google-cloud-env (1.3.0) - faraday (~> 0.11) - google-cloud-storage (1.16.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.5.0) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.1.0) + google-cloud-storage (1.34.1) + addressable (~> 2.5) digest-crc (~> 0.4) - google-api-client (~> 0.23) - google-cloud-core (~> 1.2) - googleauth (>= 0.6.2, < 0.10.0) - googleauth (0.6.7) - faraday (~> 0.12) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (0.17.0) + faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.7) - highline (1.7.10) - http-cookie (1.0.3) + signet (~> 0.14) + highline (2.0.3) + http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) i18n (0.9.5) @@ -152,77 +201,80 @@ GEM sassc (~> 2.1) sqlite3 (~> 1.3) xcinvoke (~> 0.3.0) - json (2.3.1) - jwt (2.1.0) + jmespath (1.4.0) + json (2.5.1) + jwt (2.2.3) liferaft (0.0.6) - memoist (0.16.1) - mime-types (3.3) - mime-types-data (~> 3.2015) - mime-types-data (3.2019.1009) - mini_magick (4.9.5) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.1.0) minitest (5.13.0) molinillo (0.6.6) - multi_json (1.14.1) - multi_xml (0.6.0) + multi_json (1.15.0) multipart-post (2.0.0) mustache (1.1.0) - nanaimo (0.2.6) + nanaimo (0.3.0) nap (1.1.0) - naturally (2.2.0) + naturally (2.2.1) netrc (0.11.0) open4 (1.3.4) - os (1.0.1) - plist (3.5.0) - public_suffix (2.0.5) + os (1.1.1) + plist (3.6.0) + public_suffix (4.0.6) + rake (13.0.6) redcarpet (3.5.1) - representable (3.0.4) + representable (3.1.1) declarative (< 0.1.0) - declarative-option (< 0.2.0) + trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) + rexml (3.2.5) rouge (2.0.7) ruby-macho (1.4.0) - rubyzip (1.3.0) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) sassc (2.2.1) ffi (~> 1.9) security (0.1.3) - signet (0.12.0) + signet (0.15.0) addressable (~> 2.3) - faraday (~> 0.9) + faraday (>= 0.17.3, < 2.0) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.6) + simctl (1.6.8) CFPropertyList naturally - slack-notifier (2.3.2) sqlite3 (1.4.1) terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) thread_safe (0.3.6) - tty-cursor (0.7.0) - tty-screen (0.7.0) - tty-spinner (0.9.1) + trailblazer-option (0.1.1) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) tty-cursor (~> 0.7) tzinfo (1.2.5) thread_safe (~> 0.1) uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.6) - unicode-display_width (1.6.0) + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + webrick (1.7.0) word_wrap (1.0.0) xcinvoke (0.3.0) liferaft (~> 0.0.6) - xcodeproj (1.13.0) + xcodeproj (1.20.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.2.6) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) xcpretty (0.3.0) rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.0) + xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) PLATFORMS @@ -235,4 +287,4 @@ DEPENDENCIES mini_magick (>= 4.9.4) BUNDLED WITH - 2.1.4 + 2.2.25 diff --git a/GeoTrackKit/Core/Extension/CLLocationExtension.swift b/GeoTrackKit/Core/Extension/CLLocationExtension.swift index 52b8acd..e1eb591 100644 --- a/GeoTrackKit/Core/Extension/CLLocationExtension.swift +++ b/GeoTrackKit/Core/Extension/CLLocationExtension.swift @@ -17,6 +17,54 @@ public extension CLLocation { } +public extension CLLocationDegrees { + + /// Gets you the longitude (in degrees) from this `Mercator X` value. + var lonFromMercatorX: CLLocationDegrees { + let degrees = (self / Constants.earthRadius).radiansToDegrees + guard degrees <= 180 else { + return 180 + } + guard degrees >= -180 else { + return -180 + } + return degrees + } + + /// Gets you the latitude (in degrees) from this `Mercator Y` value. + var latFromMercatorY: CLLocationDegrees { + let degrees = ((2.0 * atan(exp(self/Constants.earthRadius))) - .pi/2).radiansToDegrees + guard degrees <= 90 else { + return 90 + } + guard degrees >= -90 else { + return -90 + } + return degrees + } + + /// Gets you the Mercator Y from these latitudinal degrees + var yFromLatitude: CLLocationDegrees { + guard self > -90 else { + return -89.99999999.yFromLatitude + } + guard self < 90 else { + return 89.99999999.yFromLatitude + } + return log(tan(.pi/4 + radians / 2)) * Constants.earthRadius + } + + /// Gets you the Mercator X from this longitudinal degrees + var xFromLongitude: CLLocationDegrees { + return radians * Constants.earthRadius + } + + private struct Constants { + static let earthRadius = 6378137.0 + } + +} + // MARK: - x / y public extension CLLocationCoordinate2D { diff --git a/GeoTrackKit/Core/GeoTrackAnalyzer.swift b/GeoTrackKit/Core/GeoTrackAnalyzer.swift index 2e3acea..d9d9c74 100644 --- a/GeoTrackKit/Core/GeoTrackAnalyzer.swift +++ b/GeoTrackKit/Core/GeoTrackAnalyzer.swift @@ -203,9 +203,9 @@ extension CLLocation { /// Tells you if this point is above, below or at the same altitude as another point /// /// - Parameter point: The point to compare this point with - /// - Returns: `unknown` if the altitude is the same (very, very unlikely), `down` if the provided point is below - /// this point and `up` if the provided point is above this point. - func compare(to point: CLLocation) -> Direction { + /// - Returns: `unknown` if the altitude is the same (very, very unlikely), `down` + /// if the provided point is below this point and `up` if the provided point is above this point. + func compare(to point: CLLocation) -> GeoTrackKit.Direction { if altitude == point.altitude { return .unknown } else if altitude > point.altitude { diff --git a/GeoTrackKit/Core/GeoTrackManager.swift b/GeoTrackKit/Core/GeoTrackManager.swift index 7ac2ee0..5b4b125 100644 --- a/GeoTrackKit/Core/GeoTrackManager.swift +++ b/GeoTrackKit/Core/GeoTrackManager.swift @@ -216,10 +216,12 @@ extension GeoTrackManager { var recentLocations = [CLLocation]() // Ensure that the first point is recent (not old points which we often get when tracking begins): - if let oldPointTimeThreshold = GeoTrackManager.oldPointTimeThreshold, lastPoint == nil { - locations.forEach { (location) in - guard abs(location.timestamp.timeIntervalSinceNow) < oldPointTimeThreshold else { - return + if lastPoint == nil { + locations.forEach { location in + if let oldPointTimeThreshold = GeoTrackManager.oldPointTimeThreshold { + guard abs(location.timestamp.timeIntervalSinceNow) < oldPointTimeThreshold else { + return GTDebug(message: "skipping point: \(location)") + } } recentLocations.append(location) } diff --git a/GeoTrackKit/Core/Map/GeoTrackMap.swift b/GeoTrackKit/Core/Map/GeoTrackMap.swift index a4b01f3..314e8c6 100644 --- a/GeoTrackKit/Core/Map/GeoTrackMap.swift +++ b/GeoTrackKit/Core/Map/GeoTrackMap.swift @@ -59,6 +59,18 @@ public extension GeoTrackMap { return } removeOverlays(overlays) + print("Removed overlays") + + if let legs = model?.legs { + legs.forEach { leg in + guard let polyline = model?.expandedPolyline(forLeg: leg, size: 100) else { + return + } + addOverlay(polyline) + print("Added expanded polyline overlay") + } + } + guard let polylines = model?.polylines else { return } @@ -87,6 +99,12 @@ extension GeoTrackMap: MKMapViewDelegate { /// - overlay: The overlay to create a renderer for /// - Returns: The result MKOverlay renderer public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + if let polygon = overlay as? MKPolygon { + let renderer = MKPolygonRenderer(polygon: polygon) + renderer.fillColor = unknownColor + return renderer + } + guard let polyline = overlay as? MKPolyline else { return MKPolylineRenderer(overlay: overlay) } @@ -105,7 +123,7 @@ extension GeoTrackMap: MKMapViewDelegate { case .upward: renderer.strokeColor = ascentColor default: - break + renderer.strokeColor = unknownColor } return renderer diff --git a/GeoTrackKit/Core/Map/UIModels/UIGeoTrack.swift b/GeoTrackKit/Core/Map/UIModels/UIGeoTrack.swift index 1500c0f..c7a4912 100644 --- a/GeoTrackKit/Core/Map/UIModels/UIGeoTrack.swift +++ b/GeoTrackKit/Core/Map/UIModels/UIGeoTrack.swift @@ -65,6 +65,31 @@ public extension UIGeoTrack { return polys } + /// Gets you the points for the provided leg. + /// - Parameter leg: The leg that you want the points for. + func points(for leg: Leg) -> [CLLocation]? { + guard leg.index >= 0, leg.index < track.points.count, leg.endIndex < track.points.count else { + return nil + } + + return Array(track.points[leg.index...leg.endIndex]) + } + + func expandedPolyline(forLeg leg: Leg, size meters: CLLocationDistance) -> MKPolygon? { + guard let points = points(for: leg), + let coordinates = track.toPolygonPointArray(points: points, size: meters) else { return nil } + + return MKPolygon(coordinates: coordinates.map({ $0.coordinate }), count: coordinates.count) + } + + /// Gets you an expanded polyline of the entire track. + /// - Parameter meters: The distance outward from the line (in meters). + func expandedPolyline(size meters: CLLocationDistance) -> MKPolygon? { + guard let coordinates = track.toPolygonPointArray(size: meters) else { return nil } + + return MKPolygon(coordinates: coordinates.map({ $0.coordinate }), count: coordinates.count) + } + /// Toggles the visibility of all cells /// /// - Parameter visible: Whether they should all be visible or not. diff --git a/GeoTrackKit/Core/Models/Analyze/Leg.swift b/GeoTrackKit/Core/Models/Analyze/Leg.swift index 287c597..d44b842 100644 --- a/GeoTrackKit/Core/Models/Analyze/Leg.swift +++ b/GeoTrackKit/Core/Models/Analyze/Leg.swift @@ -239,7 +239,7 @@ open class Leg { /// - Parameter point: The point to compare with this relative point. /// - Returns: the direction func compare(to anotherPoint: CLLocation) -> Direction { - return point.compare(to: anotherPoint) + return point.compare(to: anotherPoint) } /// Tells you if this leg is moving in the same direction as another leg diff --git a/GeoTrackKit/Core/Models/GeoTrack+Utilities.swift b/GeoTrackKit/Core/Models/GeoTrack+Utilities.swift index 65dc9bd..ee6a4c8 100644 --- a/GeoTrackKit/Core/Models/GeoTrack+Utilities.swift +++ b/GeoTrackKit/Core/Models/GeoTrack+Utilities.swift @@ -6,7 +6,9 @@ // import CoreLocation -import Foundation +import MapKit + +// MARK: - GeoTrack public extension GeoTrack { @@ -63,6 +65,105 @@ public extension GeoTrack { return false } + func toPolygonPointArray(size meters: CLLocationDistance) -> [CLLocation]? { + return toPolygonPointArray(points: points, size: meters) + } + + /// Creates a polygon from the track that expands the line by the `size` meters you provide. + /// - Parameter points: The points to convert to a polygon array + /// - Parameter meters: The distance to expand outwards. + func toPolygonPointArray(points: [CLLocation], size meters: CLLocationDistance) -> [CLLocation]? { + // swiftlint:disable:previous cyclomatic_complexity + guard points.count > 2 else { + return nil + } + + var result = [CLLocation]() + var bottomResult = [CLLocation]() + + for idx in 1.. Horizontal { + if second.x < first.x { + return .west + } else if second.x > first.x { + return .east + } else { + return .none + } + } + } + + enum Vertical { + case north + case south + case none + + /// Tells you what direction the second point is from the first in the north/west direction. + /// - Parameters: + /// - first: The first point. + /// - second: The second point. + static func direction(from first: CLLocation, to second: CLLocation) -> Vertical { + if second.y < first.y { + return .south + } else if second.y > first.y { + return .north + } else { + return .none + } + } + } + + struct Direction { + let horizontal: Horizontal + let vertical: Vertical + } + + // swiftlint:disable:next identifier_name + convenience init(x: CLLocationDegrees, y: CLLocationDegrees) { + self.init(latitude: y.latFromMercatorY, longitude: x.lonFromMercatorX) + } + + /// Tells you what direction the provided point is from the provided point. + /// - Parameter point: The point you want to know the direction from. + func direction(from point: CLLocation) -> Direction { + return Direction(horizontal: .direction(from: self, to: point), + vertical: .direction(from: self, to: point)) + } +} + +// MARK: - MKPolygon Extension + +extension MKPolygon { + + func contains(point: CLLocation) -> Bool { + let polygonRenderer = MKPolygonRenderer(polygon: self) + let currentMapPoint = MKMapPoint(point.coordinate) + let polygonViewPoint = polygonRenderer.point(for: currentMapPoint) + + return polygonRenderer.path.contains(polygonViewPoint) + } +} diff --git a/GeoTrackKit/Core/Models/GeoTrack.swift b/GeoTrackKit/Core/Models/GeoTrack.swift index a70ac16..27cd59b 100644 --- a/GeoTrackKit/Core/Models/GeoTrack.swift +++ b/GeoTrackKit/Core/Models/GeoTrack.swift @@ -114,6 +114,32 @@ public extension GeoTrack { return result } + // swiftlint:disable line_length + var standardGpx: String { + var result = """ + + + + + +""" + for point in points { + result += """ + \(point.altitude) + +""" + } + // swiftlint:enable line_length + + result += """ + + + +""" + + return result + } + } // MARK: - API(Events) diff --git a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj index f0ca055..2b0326f 100644 --- a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj +++ b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 0E1E09A3899897CDAE589A5B /* Pods_GeoTrackKitExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A51639AB7B6B4BAEF27C313 /* Pods_GeoTrackKitExample.framework */; }; 1B2451A1B46FD1E813C13A25 /* Pods_GeoTrackKitExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6E72B23B9554498B66030FE /* Pods_GeoTrackKitExampleTests.framework */; }; 36302DB9210E17B400834A1D /* GeoTrackKitErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36302DB8210E17B400834A1D /* GeoTrackKitErrorTests.swift */; }; + 363296A32363CF2500D43680 /* LatLonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363296A22363CF2500D43680 /* LatLonTests.swift */; }; 3661C3662381C80B009DE76C /* GeoTrackManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3661C3652381C80B009DE76C /* GeoTrackManagerTests.swift */; }; 3661C3682381D335009DE76C /* PointFilterOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3661C3672381D335009DE76C /* PointFilterOptionsTests.swift */; }; 3661C36B2382253E009DE76C /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 3661C3692382253D009DE76C /* README.md */; }; @@ -68,6 +69,7 @@ /* Begin PBXFileReference section */ 36302DB8210E17B400834A1D /* GeoTrackKitErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoTrackKitErrorTests.swift; sourceTree = ""; }; + 363296A22363CF2500D43680 /* LatLonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatLonTests.swift; sourceTree = ""; }; 3661C3652381C80B009DE76C /* GeoTrackManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoTrackManagerTests.swift; sourceTree = ""; }; 3661C3672381D335009DE76C /* PointFilterOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointFilterOptionsTests.swift; sourceTree = ""; }; 3661C3692382253D009DE76C /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; @@ -200,6 +202,7 @@ isa = PBXGroup; children = ( 368A87B01E63332E003D115A /* TrackReader.swift */, + 363296A22363CF2500D43680 /* LatLonTests.swift */, ); path = Utilities; sourceTree = ""; @@ -609,6 +612,7 @@ buildActionMask = 2147483647; files = ( 366CE43C1DFCAC360090BD42 /* GeoTrackSerializationTests.swift in Sources */, + 363296A32363CF2500D43680 /* LatLonTests.swift in Sources */, 36302DB9210E17B400834A1D /* GeoTrackKitErrorTests.swift in Sources */, 36E1E3461DEB3D0D00CFC7BC /* GeoTrackLocationEventTests.swift in Sources */, 368A87B11E63332E003D115A /* TrackReader.swift in Sources */, diff --git a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/xcuserdata/einternicola.xcuserdatad/xcschemes/xcschememanagement.plist b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/xcuserdata/einternicola.xcuserdatad/xcschemes/xcschememanagement.plist index 161ac9e..6508cf6 100644 --- a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/xcuserdata/einternicola.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/xcuserdata/einternicola.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ GeoTrackKitExample.xcscheme_^#shared#^_ orderHint - 0 + 1 SuppressBuildableAutocreation diff --git a/GeoTrackKitExample/GeoTrackKitExample/Extensions/CoreLocationExtension.swift b/GeoTrackKitExample/GeoTrackKitExample/Extensions/CoreLocationExtension.swift index b571fc9..9dcbde6 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Extensions/CoreLocationExtension.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Extensions/CoreLocationExtension.swift @@ -30,4 +30,9 @@ extension CLLocationDistance { return self * 3.28084 } + /// Converts meters to miles. + var metersToMiles: Double { + return self * 0.000621371 + } + } diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/LegSwitchCell.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/LegSwitchCell.swift index e763842..bfa284e 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/LegSwitchCell.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/LegSwitchCell.swift @@ -17,6 +17,11 @@ class LegSwitchCell: UITableViewCell { var indexPath: IndexPath? weak var model: UIGeoTrack? + override func prepareForReuse() { + super.prepareForReuse() + setSelected(false, animated: false) + } + @IBAction func didToggleSwitch(_ sender: UISwitch) { guard let indexPath = indexPath else { return assertionFailure("IndexPath not set on cell") diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackMapViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackMapViewController.swift index 7c7f021..1c4f4d6 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackMapViewController.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackMapViewController.swift @@ -129,8 +129,8 @@ private extension TrackMapViewController { return nil } - /// Writes the track to a GPX file and gives you back the URL - var trackWrittenToGpxFile: URL? { + /// Writes the track to an Xcode GPX file and gives you back the URL + var trackWrittenXcodeToGpxFile: URL? { guard let model = model else { return nil } @@ -163,6 +163,39 @@ private extension TrackMapViewController { return nil } + var trackWrittenToGpxFile: URL? { + guard let model = model else { + return nil + } + + do { + let documentsFolder = try FileManager.default.url( + for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + let fileName = model.track.name.trackNameToFileSystemName + let fileUrl = documentsFolder.appendingPathComponent("\(fileName).gpx") + + let gpxString = model.track.standardGpx + guard let data = gpxString.data(using: .utf8) else { + return nil + } + + do { + try data.write(to: fileUrl, options: []) + } catch { + print(error) + assertionFailure(error.localizedDescription) + return nil + } + return fileUrl + } catch { + print("ERROR trying to serialize to JSON: \(error.localizedDescription)") + print("\(error)") + assertionFailure(error.localizedDescription) + } + + return nil + } + /// Shows a action sheet with a set of sharing options. func showShareOptions() { let dialog = UIAlertController(title: "Share", message: "How would you like to share?", @@ -172,8 +205,12 @@ private extension TrackMapViewController { self?.shareJsonFile() dialog.dismiss(animated: true) }) + dialog.addAction(UIAlertAction(title: "Xcode GPX", style: .default) { [weak self] _ in + self?.shareXcodeGPX() + dialog.dismiss(animated: true) + }) dialog.addAction(UIAlertAction(title: "GPX", style: .default) { [weak self] _ in - self?.shareGPX() + self?.shareGpx() dialog.dismiss(animated: true) }) dialog.addAction(UIAlertAction(title: "Cancel", style: .cancel)) @@ -181,8 +218,17 @@ private extension TrackMapViewController { present(dialog, animated: true) } - /// Shares the track as a GPX file - func shareGPX() { + /// Shares the track as an Xcode GPX file + func shareXcodeGPX() { + guard let trackWrittenToGpxFile = trackWrittenXcodeToGpxFile else { + return + } + let activityVC = UIActivityViewController(activityItems: [trackWrittenToGpxFile], applicationActivities: nil) + present(activityVC, animated: true, completion: nil) + } + + /// Shares the track written to a standard GPX file + func shareGpx() { guard let trackWrittenToGpxFile = trackWrittenToGpxFile else { return } diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewCell.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewCell.swift index aac0648..f1f49a7 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewCell.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewCell.swift @@ -31,14 +31,18 @@ class TrackOverviewCell: UITableViewCell { extension TrackOverviewCell { func updateContents() { - guard let analyzer = analyzer else { + chromeView.backgroundColor = .black + overviewLabel.textColor = .white + + guard let analyzer = analyzer, let stats = analyzer.stats else { return } let numRuns = analyzer.legs.filter({ $0.direction == .downward }).count - let vertical = abs(analyzer.stats?.verticalDescent ?? 0).metersToFeet + let vertical = abs(stats.verticalDescent.metersToFeet) + let distance = String(format: "%.2f mi distance", stats.totalDistance.metersToMiles) - overviewLabel.text = "\(numRuns) runs\n\(Int(vertical))ft vertical descent" + overviewLabel.text = "\(numRuns) runs\n\(Int(vertical))ft vertical descent\n\(distance)" } } diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewTableViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewTableViewController.swift index 94e325c..4bbffcd 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewTableViewController.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewTableViewController.swift @@ -6,6 +6,7 @@ // Copyright © 2017 Eric Internicola. All rights reserved. // +import CoreLocation import GeoTrackKit import UIKit @@ -48,7 +49,8 @@ extension TrackOverviewTableViewController { } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.section == 0 { + switch indexPath.section { + case 0: let cell = tableView.dequeueReusableCell(withIdentifier: "TrackOverviewCell", for: indexPath) guard let overviewCell = cell as? TrackOverviewCell else { return cell @@ -56,19 +58,39 @@ extension TrackOverviewTableViewController { overviewCell.analyzer = analyzer return overviewCell - } - let cell = tableView.dequeueReusableCell(withIdentifier: "LegSwitchCell", for: indexPath) + default: + let cell = tableView.dequeueReusableCell(withIdentifier: "LegSwitchCell", for: indexPath) + + guard let model = model, let legCell = cell as? LegSwitchCell else { + return cell + } + + legCell.toggleSwitch.isOn = model.isVisible(at: indexPath.row) + legCell.label.text = analyzer?.legs[indexPath.row].string + legCell.indexPath = indexPath + legCell.model = model - guard let model = model, let legCell = cell as? LegSwitchCell else { return cell } + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.section != 0, let analyzer = analyzer, indexPath.row < analyzer.legs.count else { + return + } + let leg = analyzer.legs[indexPath.row] + + guard let points = leg.points(from: analyzer.track) else { + return + } - legCell.toggleSwitch.isOn = model.isVisible(at: indexPath.row) - legCell.label.text = analyzer?.legs[indexPath.row].string - legCell.indexPath = indexPath - legCell.model = model + let name = "\(leg.isAscent ? "Ascent" : "Descent") #\(indexPath.row / 2 + 1)" + let track = GeoTrack(points: points, name: name, description: "") - return cell + let trackMapVC = TrackMapViewController.loadFromStoryboard() + trackMapVC.model = UIGeoTrack(with: track) + navigationController?.pushViewController(trackMapVC, animated: true) + tableView.cellForRow(at: indexPath)?.setSelected(false, animated: true) } } @@ -77,6 +99,14 @@ extension TrackOverviewTableViewController { extension Leg { + var isAscent: Bool { + return direction == .upward + } + + var isDescent: Bool { + return direction == .downward + } + var string: String { var result = String(index) + " - " result += String(endIndex) + ", " @@ -87,4 +117,14 @@ extension Leg { return result } + /// Gets you the points for this leg from the track. + /// - Parameter track: The track that you want the points for. + func points(from track: GeoTrack) -> [CLLocation]? { + guard index < track.points.count, endIndex < track.points.count, index < endIndex else { + return nil + } + + return Array(track.points[index...endIndex]) + } + } diff --git a/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackKit/Core/GeoTrackManagerTests.swift b/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackKit/Core/GeoTrackManagerTests.swift index 47a219c..b824137 100644 --- a/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackKit/Core/GeoTrackManagerTests.swift +++ b/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackKit/Core/GeoTrackManagerTests.swift @@ -12,18 +12,18 @@ import XCTest class GeoTrackManagerTests: XCTestCase { let mockManager = MockLocationManager() - var manager: GeoTrackManager? + var subject: GeoTrackManager? var oldPointTimeThreshold: TimeInterval? = GeoTrackManager.oldPointTimeThreshold override func setUp() { super.setUp() GeoTrackManager.shared.reset() GeoTrackManager.shared.locationManager = mockManager - manager = GeoTrackManager.shared as? GeoTrackManager + subject = GeoTrackManager.shared as? GeoTrackManager GeoTrackManager.shared.shouldStorePoints = true oldPointTimeThreshold = GeoTrackManager.oldPointTimeThreshold - XCTAssertNotNil(manager) + XCTAssertNotNil(subject) } override func tearDown() { @@ -35,7 +35,7 @@ class GeoTrackManagerTests: XCTestCase { } func testFliteringAllPoints() { - guard let manager = manager else { + guard let manager = subject else { return XCTFail("cannot locate the manager") } GeoTrackManager.shared.pointFilter = .filterAllPoints @@ -57,7 +57,7 @@ class GeoTrackManagerTests: XCTestCase { } func testFilteringDefaults() { - guard let manager = manager else { + guard let manager = subject else { return XCTFail("cannot locate the manager") } GeoTrackManager.shared.pointFilter = .defaultFilterOptions @@ -75,6 +75,7 @@ class GeoTrackManagerTests: XCTestCase { manager.locationManager(locationServicing: mockManager, didChangeAuthorization: .authorizedWhenInUse) manager.locationManager(locationServicing: mockManager, didUpdateLocations: points) + XCTAssertNotNil(manager.track) XCTAssertNotEqual(0, manager.track?.points.count ?? 0) XCTAssertTrue((manager.track?.points.count ?? points.count) < points.count) } diff --git a/GeoTrackKitExample/GeoTrackKitExampleTests/Utilities/LatLonTests.swift b/GeoTrackKitExample/GeoTrackKitExampleTests/Utilities/LatLonTests.swift new file mode 100644 index 0000000..b9fe128 --- /dev/null +++ b/GeoTrackKitExample/GeoTrackKitExampleTests/Utilities/LatLonTests.swift @@ -0,0 +1,39 @@ +// +// LatLonTests.swift +// GeoTrackKitExampleTests +// +// Created by Eric Internicola on 10/25/19. +// Copyright © 2019 Eric Internicola. All rights reserved. +// + +import CoreLocation +import GeoTrackKit +import XCTest + +class LatLonTests: XCTestCase { + + func testDegreesToRadians() { + XCTAssertEqual(CLLocationDegrees.pi, 180.0.degreesToRadians) + } + + func testRadiansToDegrees() { + XCTAssertEqual(180.0, CLLocationDegrees.pi.radiansToDegrees) + } + + func testLatToMercatorY() { + XCTAssertEqual(4485399.4, 37.33138973.yFromLatitude, accuracy: 0.1) + } + + func testLonToMercatorX() { + XCTAssertEqual(-13584391.4, -122.03066431.xFromLongitude, accuracy: 0.1) + } + + func testMercatorXToLon() { + XCTAssertEqual(-122.03066060755671, -13584391.4.lonFromMercatorX, accuracy: 0.1) + } + + func testMercatorYToLat() { + XCTAssertEqual(37.33138647537632, 4485399.4.latFromMercatorY, accuracy: 0.1) + } + +}