diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 2cf5a56..3b599ff 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -77,6 +77,10 @@ 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */; }; 7A813DBE2506A57100CC93B9 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A813DBD2506A57100CC93B9 /* AuthenticationServices.framework */; }; 7A813DC12508C4D900CC93B9 /* ExceptionCatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */; }; + 7A813DC32508EE4F00CC93B9 /* EventCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A813DC22508EE4F00CC93B9 /* EventCell.swift */; }; + 7A813DC5250AAF3C00CC93B9 /* LocationEditController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A813DC4250AAF3C00CC93B9 /* LocationEditController.swift */; }; + 7A813DC9250B5C9700CC93B9 /* LocationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A813DC8250B5C9700CC93B9 /* LocationRow.swift */; }; + 7A813DCB250B5DC900CC93B9 /* LocationPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A813DCA250B5DC900CC93B9 /* LocationPickerController.swift */; }; 7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */; }; 7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */; }; 7A96AE2A246AFD6200297C33 /* Eureka in Frameworks */ = {isa = PBXBuildFile; productRef = 7A96AE29246AFD6200297C33 /* Eureka */; }; @@ -88,6 +92,7 @@ 7AB562BA249C9E9B00473D53 /* Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB562B9249C9E9B00473D53 /* Region.swift */; }; 7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; }; 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; }; + 7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C92250B954900F237B2 /* Navigation.swift */; }; 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */; }; 7AE26A3524F31B0700625033 /* EventsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3424F31B0700625033 /* EventsController.swift */; }; 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFE727240455E200910EB7 /* SettingsController.swift */; }; @@ -160,6 +165,10 @@ 7A7547DC2403180A004E8406 /* SectionHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SectionHeader.xib; sourceTree = ""; }; 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhotoCell.swift; sourceTree = ""; }; 7A813DBD2506A57100CC93B9 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; + 7A813DC22508EE4F00CC93B9 /* EventCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCell.swift; sourceTree = ""; }; + 7A813DC4250AAF3C00CC93B9 /* LocationEditController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditController.swift; sourceTree = ""; }; + 7A813DC8250B5C9700CC93B9 /* LocationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRow.swift; sourceTree = ""; }; + 7A813DCA250B5DC900CC93B9 /* LocationPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerController.swift; sourceTree = ""; }; 7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeImage.swift; sourceTree = ""; }; 7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleReportImage.swift; sourceTree = ""; }; 7A92D0AB240425B100EF3B77 /* ATGMediaBrowser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ATGMediaBrowser.framework; path = Carthage/Build/iOS/ATGMediaBrowser.framework; sourceTree = ""; }; @@ -171,6 +180,7 @@ 7AB562B9249C9E9B00473D53 /* Region.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Region.swift; sourceTree = ""; }; 7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = ""; }; 7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; + 7ADF6C92250B954900F237B2 /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = ""; }; 7AE26A3424F31B0700625033 /* EventsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsController.swift; sourceTree = ""; }; 7AEFE727240455E200910EB7 /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; @@ -248,6 +258,7 @@ 7A11471423FDEAF800B424AF /* Controllers */ = { isa = PBXGroup; children = ( + 7A813DC7250B5C6E00CC93B9 /* Location */, 7A11471523FDEB2A00B424AF /* MainSplitController.swift */, 7A11471723FDEBFA00B424AF /* ReportController.swift */, 7A11471923FE839000B424AF /* AuthController.swift */, @@ -259,7 +270,6 @@ 7A33381024990DAE00D878F1 /* FiltersController.swift */, 7A27ADC6249D43210035F39E /* RegionsController.swift */, 7A27ADF2249F8B650035F39E /* RecordsController.swift */, - 7AE26A3424F31B0700625033 /* EventsController.swift */, ); path = Controllers; sourceTree = ""; @@ -329,6 +339,7 @@ 7A659B5A24A3768A0043A0F2 /* Substrings.swift */, 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */, 7A21112924FC3D7E003BBF6F /* AudioEngine.swift */, + 7ADF6C92250B954900F237B2 /* Navigation.swift */, ); path = Extensions; sourceTree = ""; @@ -354,6 +365,7 @@ 7A7547DC2403180A004E8406 /* SectionHeader.xib */, 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */, 7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */, + 7A813DC22508EE4F00CC93B9 /* EventCell.swift */, ); path = Cells; sourceTree = ""; @@ -402,6 +414,17 @@ path = Fonts; sourceTree = ""; }; + 7A813DC7250B5C6E00CC93B9 /* Location */ = { + isa = PBXGroup; + children = ( + 7A813DC4250AAF3C00CC93B9 /* LocationEditController.swift */, + 7AE26A3424F31B0700625033 /* EventsController.swift */, + 7A813DC8250B5C9700CC93B9 /* LocationRow.swift */, + 7A813DCA250B5DC900CC93B9 /* LocationPickerController.swift */, + ); + path = Location; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -512,7 +535,9 @@ 7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */, 7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */, 7A27ADF924A09CAD0035F39E /* CocoaError.swift in Sources */, + 7A813DC9250B5C9700CC93B9 /* LocationRow.swift in Sources */, 7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */, + 7A813DC32508EE4F00CC93B9 /* EventCell.swift in Sources */, 7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */, 7AB562BA249C9E9B00473D53 /* Region.swift in Sources */, 7A659B5924A2B1BA0043A0F2 /* AudioRecord.swift in Sources */, @@ -549,12 +574,14 @@ 7A11474723FF2AA500B424AF /* User.swift in Sources */, 7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */, 7AF58D3124029E1000CE01A0 /* VehicleHeaderCell.swift in Sources */, + 7A813DC5250AAF3C00CC93B9 /* LocationEditController.swift in Sources */, 7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */, 7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */, 7A488C3F24A74B990054D0B2 /* RealmBindObserver.swift in Sources */, 7AAE6AD324CDDF950023860B /* VehicleEvent.swift in Sources */, 7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */, 7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */, + 7A813DCB250B5DC900CC93B9 /* LocationPickerController.swift in Sources */, 7A11474423FF06CA00B424AF /* Api.swift in Sources */, 7A488C3D24A74B990054D0B2 /* RxCollectionViewRealmDataSource.swift in Sources */, 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */, @@ -565,6 +592,7 @@ 7A05161A2414FF0900FC55AC /* DateSection.swift in Sources */, 7A333814249A532400D878F1 /* Filter.swift in Sources */, 7A11474B23FF368B00B424AF /* Settings.swift in Sources */, + 7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */, 7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */, 7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */, 7A64AE812469E16100ABE48E /* ProgressAnimatedView.swift in Sources */, @@ -715,7 +743,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -737,7 +765,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; diff --git a/AutoCat/AppDelegate.swift b/AutoCat/AppDelegate.swift index dd113f2..e3a5230 100644 --- a/AutoCat/AppDelegate.swift +++ b/AutoCat/AppDelegate.swift @@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let config = Realm.Configuration( - schemaVersion: 15, + schemaVersion: 16, migrationBlock: { migration, oldSchemaVersion in if oldSchemaVersion <= 3 { var numbers: [String] = [] diff --git a/AutoCat/Assets.xcassets/MapPin.imageset/Contents.json b/AutoCat/Assets.xcassets/MapPin.imageset/Contents.json new file mode 100644 index 0000000..0596fc9 --- /dev/null +++ b/AutoCat/Assets.xcassets/MapPin.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "map_pin@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_pin@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AutoCat/Assets.xcassets/MapPin.imageset/map_pin@2x.png b/AutoCat/Assets.xcassets/MapPin.imageset/map_pin@2x.png new file mode 100644 index 0000000..feaae74 Binary files /dev/null and b/AutoCat/Assets.xcassets/MapPin.imageset/map_pin@2x.png differ diff --git a/AutoCat/Assets.xcassets/MapPin.imageset/map_pin@3x.png b/AutoCat/Assets.xcassets/MapPin.imageset/map_pin@3x.png new file mode 100644 index 0000000..feaae74 Binary files /dev/null and b/AutoCat/Assets.xcassets/MapPin.imageset/map_pin@3x.png differ diff --git a/AutoCat/Base.lproj/Main.storyboard b/AutoCat/Base.lproj/Main.storyboard index 5547eb9..4ed0dc7 100644 --- a/AutoCat/Base.lproj/Main.storyboard +++ b/AutoCat/Base.lproj/Main.storyboard @@ -227,27 +227,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + @@ -261,14 +328,14 @@ - + - + - + diff --git a/AutoCat/Cells/EventCell.swift b/AutoCat/Cells/EventCell.swift new file mode 100644 index 0000000..2d2e63a --- /dev/null +++ b/AutoCat/Cells/EventCell.swift @@ -0,0 +1,26 @@ +import UIKit + +class EventCell: UITableViewCell { + @IBOutlet weak var address: UILabel! + @IBOutlet weak var date: UILabel! + + let dateFormatter = DateFormatter() + + override func awakeFromNib() { + super.awakeFromNib() + + self.dateFormatter.dateStyle = .short + self.dateFormatter.timeStyle = .short + } + + func configure(with event: VehicleEvent) { + if let addressString = event.address { + self.address.text = addressString + } else { + self.address.text = "Lat: \(event.latitude), Lon: \(event.longitude)" + } + + let date = Date(timeIntervalSince1970: event.date) + self.date.text = self.dateFormatter.string(from: date) + } +} diff --git a/AutoCat/Controllers/EventsController.swift b/AutoCat/Controllers/EventsController.swift deleted file mode 100644 index 23dfdfd..0000000 --- a/AutoCat/Controllers/EventsController.swift +++ /dev/null @@ -1,68 +0,0 @@ -import UIKit -import MapKit - -class EventPin: NSObject, MKAnnotation { - var coordinate: CLLocationCoordinate2D - var title: String? - var subtitle: String? - - init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String) { - self.coordinate = coordinate - self.title = title - self.subtitle = subtitle - } -} - -class EventsController: UIViewController { - - @IBOutlet weak var map: MKMapView! - - public var events: [VehicleEvent] = [] { - didSet { - self.pins = self.events.map { event in - let coordinate = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude) - let subtitle = event.address ?? "\(event.latitude), \(event.longitude)" - let date = Date(timeIntervalSince1970: event.date) - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .short - let title = formatter.string(from: date) - return EventPin(coordinate: coordinate, title: title, subtitle: subtitle) - } - } - } - - private var pins: [EventPin] = [] - - override func viewDidLoad() { - super.viewDidLoad() - - #if targetEnvironment(macCatalyst) - self.map.showsZoomControls = true - #endif - - self.map.addAnnotations(self.pins) - self.centerMap() - } - - func centerMap() { - var minLat = 0.0, maxLat = 0.0, minLon = 0.0, maxLon = 0.0 - for event in self.events { - if event.latitude < minLat || minLat == 0 { minLat = event.latitude } - if event.latitude > maxLat || maxLat == 0 { maxLat = event.latitude } - if event.longitude < minLon || minLon == 0 { minLon = event.longitude } - if event.longitude > maxLon || maxLon == 0 { maxLon = event.longitude } - } - - let center = CLLocationCoordinate2D(latitude: (minLat + maxLat)/2, longitude: (minLon + maxLon)/2) - let leftTop = CLLocation(latitude: minLat, longitude: minLon) - let rightBottom = CLLocation(latitude: maxLat, longitude: maxLon) - var diagonal = leftTop.distance(from: rightBottom) - if diagonal < 1000 { - diagonal = 1000 - } - - let region = MKCoordinateRegion(center: center, latitudinalMeters: diagonal, longitudinalMeters: diagonal) - self.map.setRegion(region, animated: true) - } -} diff --git a/AutoCat/Controllers/Location/EventsController.swift b/AutoCat/Controllers/Location/EventsController.swift new file mode 100644 index 0000000..23134ff --- /dev/null +++ b/AutoCat/Controllers/Location/EventsController.swift @@ -0,0 +1,251 @@ +import UIKit +import MapKit +import RxSwift +import Realm +import RealmSwift + +class EventPin: NSObject, MKAnnotation { + var coordinate: CLLocationCoordinate2D + var title: String? + var subtitle: String? + + init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String) { + self.coordinate = coordinate + self.title = title + self.subtitle = subtitle + } +} + +enum EventsMode { + case map + case list +} + +class EventsController: UIViewController, UITableViewDataSource, UITableViewDelegate { + + @IBOutlet weak var map: MKMapView! + @IBOutlet weak var tableView: UITableView! + + let bag = DisposeBag() + var modeButton: UIBarButtonItem! + var addButton: UIBarButtonItem! + var mode: EventsMode = .map + + public var vehicle: Vehicle? { + didSet { + if let vehicle = self.vehicle { + self.pins = vehicle.events.map { event in + let coordinate = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude) + let subtitle = event.address ?? "\(event.latitude), \(event.longitude)" + let date = Date(timeIntervalSince1970: event.date) + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + let title = formatter.string(from: date) + return EventPin(coordinate: coordinate, title: title, subtitle: subtitle) + } + } else { + + } + + if self.isViewLoaded { + self.updateInterface() + } + } + } + + private var pins: [EventPin] = [] + + override func viewDidLoad() { + super.viewDidLoad() + + self.title = self.vehicle?.number ?? "Events" + + #if targetEnvironment(macCatalyst) + self.map.showsZoomControls = true + #endif + + self.modeButton = UIBarButtonItem(image: UIImage(systemName: "list.bullet"), style: .plain, target: self, action: #selector(switchMode(_:))) + self.addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addEvent(_:))) + self.navigationItem.rightBarButtonItems = [self.modeButton, self.addButton] + + self.updateInterface() + } + + func updateInterface() { + self.map.removeAnnotations(self.map.annotations) + self.map.addAnnotations(self.pins) + self.centerMap() + self.tableView.reloadData() + } + + func centerMap() { + guard let vehicle = self.vehicle else { return } + + var minLat = 0.0, maxLat = 0.0, minLon = 0.0, maxLon = 0.0 + for event in vehicle.events { + if event.latitude < minLat || minLat == 0 { minLat = event.latitude } + if event.latitude > maxLat || maxLat == 0 { maxLat = event.latitude } + if event.longitude < minLon || minLon == 0 { minLon = event.longitude } + if event.longitude > maxLon || maxLon == 0 { maxLon = event.longitude } + } + + let center = CLLocationCoordinate2D(latitude: (minLat + maxLat)/2, longitude: (minLon + maxLon)/2) + let leftTop = CLLocation(latitude: minLat, longitude: minLon) + let rightBottom = CLLocation(latitude: maxLat, longitude: maxLon) + var diagonal = leftTop.distance(from: rightBottom)*1.2 + if diagonal < 1000 { + diagonal = 1000 + } + + let region = MKCoordinateRegion(center: center, latitudinalMeters: diagonal, longitudinalMeters: diagonal) + self.map.setRegion(region, animated: true) + } + + @objc func switchMode(_ sender: UIBarButtonItem) { + switch self.mode { + case .map: + self.mode = .list + self.tableView.reloadData() + self.tableView.isHidden = false + self.map.isHidden = true + self.modeButton.image = UIImage(systemName: "map") + case .list: + self.mode = .map + self.tableView.isHidden = true + self.map.isHidden = false + self.modeButton.image = UIImage(systemName: "list.bullet") + } + } + + // MARK: - UITableViewDataSource + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.vehicle?.events.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "EventCell", for: indexPath) as? EventCell else { + return UITableViewCell() + } + + if let event = self.vehicle?.events[indexPath.row] { + cell.configure(with: event) + } + + return cell + } + + // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let delete = UIContextualAction(style: .destructive, title: "Delete") { action, view, completion in + self.deleteEvent(index: indexPath.row, completion: completion) + } + delete.image = UIImage(systemName: "trash") + + let edit = UIContextualAction(style: .normal, title: "Edit") { action, view, completion in + self.editEvent(index: indexPath.row) + completion(true) + } + edit.image = UIImage(systemName: "pencil") + edit.backgroundColor = .systemBlue + + let configuration = UISwipeActionsConfiguration(actions: [delete, edit]) + configuration.performsFirstActionWithFullSwipe = false + return configuration + } + + // MARK: - Event actions + + func deleteEvent(index: Int, completion: @escaping (Bool) -> Void) { + guard let vehicle = self.vehicle else { + IHProgressHUD.showError(withStatus: "Unknown vehicle") + return + } + + let event = vehicle.events[index] + if let eventId = event.id { + IHProgressHUD.show() + Api.remove(event: eventId).observeOn(MainScheduler.instance).subscribe(onSuccess: { vehicle in + completion(self.update(vehicle: vehicle)) + }, onError: { error in + completion(false) + IHProgressHUD.show(error: error) + print(error) + }).disposed(by: self.bag) + } else { + self.showAlert(title: "Error", message: "Event ID is not found. Please try to update vehicle record, containing this event.") + } + } + + func editEvent(index: Int) { + guard let vehicle = self.vehicle else { + IHProgressHUD.showError(withStatus: "Unknown vehicle") + return + } + + let event = vehicle.events[index] + let sb = UIStoryboard(name: "Main", bundle: nil) + let controller = sb.instantiateViewController(identifier: "LocationEditController") as LocationEditController + controller.title = "Edit event" + controller.date = Date(timeIntervalSince1970: event.date) + controller.placemark = Placemark(latitude: event.latitude, longitude: event.longitude, address: event.address) + controller.onDone = { newEvent in + newEvent.id = event.id + self.navigationController?.popViewController(animated: true, completion: { + IHProgressHUD.show() + Api.edit(event: newEvent) + .observeOn(MainScheduler.instance) + .subscribe(onSuccess: { self.update(vehicle: $0) }, onError: + { error in + IHProgressHUD.show(error: error) + }) + .disposed(by: self.bag) + }) + } + self.navigationController?.pushViewController(controller, animated: true) + } + + @objc func addEvent(_ sender: UIBarButtonItem) { + guard let vehicle = self.vehicle else { + IHProgressHUD.showError(withStatus: "Unknown vehicle") + return + } + + let sb = UIStoryboard(name: "Main", bundle: nil) + let controller = sb.instantiateViewController(identifier: "LocationEditController") as LocationEditController + controller.title = "Add new event" + controller.onDone = { newEvent in + self.navigationController?.popViewController(animated: true, completion: { + IHProgressHUD.show() + Api.add(event: newEvent, to: vehicle.number) + .observeOn(MainScheduler.instance) + .subscribe(onSuccess: { self.update(vehicle: $0) }, onError: + { error in + IHProgressHUD.show(error: error) + }) + .disposed(by: self.bag) + }) + } + self.navigationController?.pushViewController(controller, animated: true) + } + + @discardableResult + func update(vehicle: Vehicle) -> Bool { + do { + if let realm = self.vehicle?.realm { + try realm.write { + realm.add(vehicle, update: .all) + } + } + self.vehicle = vehicle + IHProgressHUD.dismiss() + return true + } catch { + IHProgressHUD.show(error: error) + print(error) + return false + } + } +} diff --git a/AutoCat/Controllers/Location/LocationEditController.swift b/AutoCat/Controllers/Location/LocationEditController.swift new file mode 100644 index 0000000..ce0632e --- /dev/null +++ b/AutoCat/Controllers/Location/LocationEditController.swift @@ -0,0 +1,55 @@ +import UIKit +import Eureka +import RxSwift +import CoreLocation + +class LocationEditController: FormViewController { + + private let bag = DisposeBag() + private var doneButton: UIBarButtonItem! + + var date = Date() + var placemark: Placemark? = nil + + var onDone: ((VehicleEvent) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + + self.doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:))) + self.navigationItem.rightBarButtonItem = self.doneButton + self.doneButton.isEnabled = false + + form +++ Section("") + <<< DateTimeInlineRow(){ + $0.title = "Date and time" + $0.value = self.date + }.onChange { row in + if let newDate = row.value { + self.date = newDate + } + } + + <<< LocationRow() { row in + row.title = "Location" + row.value = self.placemark + }.onChange { row in + if let newPlacemark = row.value { + self.placemark = newPlacemark + self.doneButton.isEnabled = true + } else { + self.doneButton.isEnabled = false + } + } + } + + @objc func doneTapped(_ sender: UIBarButtonItem) { + guard let placemark = self.placemark else { return } + let event = VehicleEvent(lat: placemark.latitude, lon: placemark.longitude, speed: -1, dir: -1) + event.date = self.date.timeIntervalSince1970 + if let address = placemark.address { + event.address = address + } + self.onDone?(event) + } +} diff --git a/AutoCat/Controllers/Location/LocationPickerController.swift b/AutoCat/Controllers/Location/LocationPickerController.swift new file mode 100644 index 0000000..5c0150f --- /dev/null +++ b/AutoCat/Controllers/Location/LocationPickerController.swift @@ -0,0 +1,156 @@ +import Foundation +import MapKit +import Eureka +import RxSwift +import Intents + +public struct Placemark: Equatable { + var latitude: Double + var longitude: Double + var address: String? +} + +public class LocationPickerController : UIViewController, TypedRowControllerType, MKMapViewDelegate { + + public var row: RowOf! + public var onDismissCallback: ((UIViewController) -> ())? + + private let bag = DisposeBag() + private var geocodingDisposable: Disposable? + private var address: String? + + lazy var mapView : MKMapView = { [unowned self] in + let v = MKMapView(frame: self.view.bounds) + v.autoresizingMask = [.flexibleWidth, .flexibleHeight] + return v + }() + + lazy var pinView: UIImageView = { [unowned self] in + let v = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + v.image = UIImage(named: "MapPin", in: Bundle(for: LocationPickerController.self), compatibleWith: nil) + v.image = v.image?.withRenderingMode(.alwaysTemplate) + v.tintColor = self.view.tintColor + v.backgroundColor = .clear + v.clipsToBounds = true + v.contentMode = .scaleAspectFit + v.isUserInteractionEnabled = false + return v + }() + + let width: CGFloat = 10.0 + let height: CGFloat = 5.0 + + lazy var ellipse: UIBezierPath = { [unowned self] in + let ellipse = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: self.width, height: self.height)) + return ellipse + }() + + + lazy var ellipsisLayer: CAShapeLayer = { [unowned self] in + let layer = CAShapeLayer() + layer.bounds = CGRect(x: 0, y: 0, width: self.width, height: self.height) + layer.path = self.ellipse.cgPath + layer.fillColor = UIColor.gray.cgColor + layer.fillRule = .nonZero + layer.lineCap = .butt + layer.lineDashPattern = nil + layer.lineDashPhase = 0.0 + layer.lineJoin = .miter + layer.lineWidth = 1.0 + layer.miterLimit = 10.0 + layer.strokeColor = UIColor.gray.cgColor + return layer + }() + + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nil, bundle: nil) + } + + convenience public init(_ callback: ((UIViewController) -> ())?){ + self.init(nibName: nil, bundle: nil) + onDismissCallback = callback + } + + public override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(mapView) + + mapView.delegate = self + mapView.addSubview(pinView) + mapView.layer.insertSublayer(ellipsisLayer, below: pinView.layer) + + let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(LocationPickerController.tappedDone(_:))) + button.title = "Done" + navigationItem.rightBarButtonItem = button + + if let value = row.value { + let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: value.latitude, longitude: value.longitude), latitudinalMeters: 1000, longitudinalMeters: 1000) + mapView.setRegion(region, animated: true) + } + else{ + mapView.showsUserLocation = true + } + updateTitle() + + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + let center = mapView.convert(mapView.centerCoordinate, toPointTo: pinView) + pinView.center = CGPoint(x: center.x, y: center.y - (pinView.bounds.height/2)) + ellipsisLayer.position = center + } + + + @objc func tappedDone(_ sender: UIBarButtonItem){ + let target = mapView.convert(ellipsisLayer.position, toCoordinateFrom: mapView) + row.value = Placemark(latitude: target.latitude, longitude: target.longitude, address: self.address) + onDismissCallback?(self) + } + + func updateTitle(){ + let fmt = NumberFormatter() + fmt.maximumFractionDigits = 4 + fmt.minimumFractionDigits = 4 + let latitude = fmt.string(from: NSNumber(value: mapView.centerCoordinate.latitude))! + let longitude = fmt.string(from: NSNumber(value: mapView.centerCoordinate.longitude))! + title = "\(latitude), \(longitude)" + + self.address = nil + self.geocodingDisposable?.dispose() + self.geocodingDisposable = LocationManager + .getAddressForLocation(latitude: mapView.centerCoordinate.latitude, longitude: mapView.centerCoordinate.longitude) + .observeOn(MainScheduler.instance) + .subscribe(onSuccess: { address in + self.title = address + self.address = address + }) + } + + public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + ellipsisLayer.transform = CATransform3DMakeScale(0.5, 0.5, 1) + UIView.animate(withDuration: 0.2, animations: { [weak self] in + self?.pinView.center = CGPoint(x: self!.pinView.center.x, y: self!.pinView.center.y - 10) + }) + } + + public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + ellipsisLayer.transform = CATransform3DIdentity + UIView.animate(withDuration: 0.2, animations: { [weak self] in + self?.pinView.center = CGPoint(x: self!.pinView.center.x, y: self!.pinView.center.y + 10) + }) + updateTitle() + } + + public func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { + mapView.showsUserLocation = false + let region = MKCoordinateRegion(center: userLocation.coordinate, latitudinalMeters: 1000, longitudinalMeters: 1000) + mapView.setRegion(region, animated: true) + } +} diff --git a/AutoCat/Controllers/Location/LocationRow.swift b/AutoCat/Controllers/Location/LocationRow.swift new file mode 100644 index 0000000..0364e26 --- /dev/null +++ b/AutoCat/Controllers/Location/LocationRow.swift @@ -0,0 +1,57 @@ +import UIKit +import Eureka +import CoreLocation + +public final class LocationRow: OptionsRow>, PresenterRowType, RowType { + + public typealias PresenterRow = LocationPickerController + + /// Defines how the view controller will be presented, pushed, etc. + public var presentationMode: PresentationMode? + + /// Will be called before the presentation occurs. + public var onPresentCallback: ((FormViewController, PresenterRow) -> Void)? + + public required init(tag: String?) { + super.init(tag: tag) + presentationMode = .show(controllerProvider: ControllerProvider.callback { return LocationPickerController(){ _ in } }, onDismiss: { vc in _ = vc.navigationController?.popViewController(animated: true) }) + + displayValueFor = { + guard let placemark = $0 else { return "" } + let fmt = NumberFormatter() + fmt.maximumFractionDigits = 4 + fmt.minimumFractionDigits = 4 + let latitude = fmt.string(from: NSNumber(value: placemark.latitude))! + let longitude = fmt.string(from: NSNumber(value: placemark.longitude))! + return "\(latitude), \(longitude)" + } + } + + /** + Extends `didSelect` method + */ + public override func customDidSelect() { + super.customDidSelect() + guard let presentationMode = presentationMode, !isDisabled else { return } + if let controller = presentationMode.makeController() { + controller.row = self + controller.title = selectorTitle ?? controller.title + onPresentCallback?(cell.formViewController()!, controller) + presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) + } else { + presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) + } + } + + /** + Prepares the pushed row setting its title and completion callback. + */ + public override func prepare(for segue: UIStoryboardSegue) { + super.prepare(for: segue) + guard let rowVC = segue.destination as? PresenterRow else { return } + rowVC.title = selectorTitle ?? rowVC.title + rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback + onPresentCallback?(cell.formViewController()!, rowVC) + rowVC.row = self + } +} diff --git a/AutoCat/Controllers/RecordsController.swift b/AutoCat/Controllers/RecordsController.swift index b16e4fd..34a387e 100644 --- a/AutoCat/Controllers/RecordsController.swift +++ b/AutoCat/Controllers/RecordsController.swift @@ -159,7 +159,6 @@ class RecordsController: UIViewController, UITableViewDelegate { return AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: text), raw: text, duration: duration, event: event) } .subscribe(onSuccess: { record in - print(record) let realm = try? Realm() try? realm?.write { realm?.add(record) diff --git a/AutoCat/Controllers/ReportController.swift b/AutoCat/Controllers/ReportController.swift index 2625f09..952e3eb 100644 --- a/AutoCat/Controllers/ReportController.swift +++ b/AutoCat/Controllers/ReportController.swift @@ -274,8 +274,7 @@ class ReportController: UIViewController, UICollectionViewDataSource, UICollecti } else if let events = self.vehicle?.events, indexPath.row == ReportGeneralSection.events.rawValue && events.count > 0 { let controller = sb.instantiateViewController(identifier: "EventsController") as EventsController - controller.events = Array(events) - controller.title = self.vehicle?.number ?? "Events" + controller.vehicle = self.vehicle self.navigationController?.pushViewController(controller, animated: true) } } diff --git a/AutoCat/Extensions/Navigation.swift b/AutoCat/Extensions/Navigation.swift new file mode 100644 index 0000000..35f9eb0 --- /dev/null +++ b/AutoCat/Extensions/Navigation.swift @@ -0,0 +1,32 @@ +import UIKit + +extension UINavigationController { + public func pushViewController( + _ viewController: UIViewController, + animated: Bool, + completion: @escaping () -> Void) + { + pushViewController(viewController, animated: animated) + + guard animated, let coordinator = transitionCoordinator else { + DispatchQueue.main.async { completion() } + return + } + + coordinator.animate(alongsideTransition: nil) { _ in completion() } + } + + func popViewController( + animated: Bool, + completion: @escaping () -> Void) + { + popViewController(animated: animated) + + guard animated, let coordinator = transitionCoordinator else { + DispatchQueue.main.async { completion() } + return + } + + coordinator.animate(alongsideTransition: nil) { _ in completion() } + } +} diff --git a/AutoCat/Models/VehicleEvent.swift b/AutoCat/Models/VehicleEvent.swift index 30a4d7a..669f7f6 100644 --- a/AutoCat/Models/VehicleEvent.swift +++ b/AutoCat/Models/VehicleEvent.swift @@ -1,8 +1,10 @@ import Foundation import RealmSwift import RxSwift +import CoreLocation -class VehicleEvent: Object, Codable { +public class VehicleEvent: Object, Codable { + @objc dynamic var id: String? @objc dynamic var date: TimeInterval = Date().timeIntervalSince1970 @objc dynamic var latitude: Double = 0 @objc dynamic var longitude: Double = 0 @@ -10,6 +12,10 @@ class VehicleEvent: Object, Codable { @objc dynamic var direction: Double = 0 @objc dynamic var address: String? = nil + var coordinate: CLLocationCoordinate2D { + return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude) + } + init(lat: Double, lon: Double, speed: Double, dir: Double) { self.latitude = lat self.longitude = lon diff --git a/AutoCat/Utils/Api.swift b/AutoCat/Utils/Api.swift index adeef35..c925416 100644 --- a/AutoCat/Utils/Api.swift +++ b/AutoCat/Utils/Api.swift @@ -40,14 +40,14 @@ class Api { } return URLSession.shared.rx.data(request: request).asSingle().map { data in - let str = String(data: data, encoding: .utf8) - print("================================") - if let string = str?.replacingOccurrences(of: "\\\"", with: "\"") - .replacingOccurrences(of: "\\'", with: "'") - .replacingOccurrences(of: "\\n", with: "") { - print(string) - } - print("================================") +// let str = String(data: data, encoding: .utf8) +// print("================================") +// if let string = str?.replacingOccurrences(of: "\\\"", with: "\"") +// .replacingOccurrences(of: "\\'", with: "'") +// .replacingOccurrences(of: "\\n", with: "") { +// print(string) +// } +// print("================================") let resp = try JSONDecoder().decode(Response.self, from: data) if resp.success { return resp.data! @@ -231,4 +231,14 @@ class Api { return vehicle } } + + public static func remove(event id: String) -> Single { + let body = ["eventId": id] + return self.makeBodyRequest(api: "events", body: body, method: "DELETE") + } + + public static func edit(event: VehicleEvent) -> Single { + let body = ["event": event] + return self.makeBodyRequest(api: "events", body: body, method: "PUT") + } } diff --git a/AutoCat/Utils/Location.swift b/AutoCat/Utils/Location.swift index fc55dd9..9684b1c 100644 --- a/AutoCat/Utils/Location.swift +++ b/AutoCat/Utils/Location.swift @@ -106,12 +106,7 @@ class LocationManager { } static func requestCurrentLocation() -> Single { - return self.checkPermissions().flatMap(self.requestLocation).do(onSuccess: { event in - print("Get location success") - }, onSubscribed: { - print("Get location subscribed") - }, onDispose: { - print("Get location dispose") + return self.checkPermissions().flatMap(self.requestLocation).do(onDispose: { self.manager.stopUpdatingLocation() }) } diff --git a/AutoCat/Utils/Recorder.swift b/AutoCat/Utils/Recorder.swift index ea12b8e..88ea5fa 100644 --- a/AutoCat/Utils/Recorder.swift +++ b/AutoCat/Utils/Recorder.swift @@ -105,7 +105,7 @@ class Recorder { if let transcription = result?.bestTranscription { self.result = transcription.formattedString self.endRecognitionTimer?.invalidate() - self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { timer in + self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in self.finishRecording() observer(.success(self.result)) }