// // ViewController.swift // GLKittutorial // // Created by Mathieu Labbe on 2020-12-28. // import GLKit import ARKit import Zip import StoreKit extension Array { func size() -> Int { return MemoryLayout.stride * self.count } } class ViewController: GLKViewController, ARSessionDelegate, RTABMapObserver, UIPickerViewDataSource, UIPickerViewDelegate, CLLocationManagerDelegate { private let session = ARSession() private var locationManager: CLLocationManager? private var mLastKnownLocation: CLLocation? private var mLastLightEstimate: CGFloat? private var context: EAGLContext? private var rtabmap: RTABMap? private var databases = [URL]() private var currentDatabaseIndex: Int = 0 private var openedDatabasePath: URL? private var progressDialog: UIAlertController? var progressView : UIProgressView? var maxPolygonsPickerView: UIPickerView! var maxPolygonsPickerData: [Int]! private var mTotalLoopClosures: Int = 0 private var mMapNodes: Int = 0 private var mTimeThr: Int = 0 private var mMaxFeatures: Int = 0 private var mLoopThr = 0.11 private var mDataRecording = false private var mReviewRequested = false private var mMaximumMemory: Int = 0 // UI states private enum State { case STATE_WELCOME, // Camera/Motion off - showing only buttons open and start new scan STATE_CAMERA, // Camera/Motion on - not mapping STATE_MAPPING, // Camera/Motion on - mapping STATE_IDLE, // Camera/Motion off STATE_PROCESSING, // Camera/Motion off - post processing STATE_VISUALIZING, // Camera/Motion off - Showing optimized mesh STATE_VISUALIZING_CAMERA, // Camera/Motion on - Showing optimized mesh and localizing STATE_VISUALIZING_WHILE_LOADING, // Camera/Motion off - Loading data while showing optimized mesh STATE_VISUALIZING_AND_MEASURING // Camera/Motion on - Showing optimized mesh without localizing and measuring tools enabled } private var mState: State = State.STATE_WELCOME; private func getStateString(state: State) -> String { switch state { case .STATE_WELCOME: return "Welcome" case .STATE_CAMERA: return "Camera Preview" case .STATE_MAPPING: return mDataRecording ? "Data Recording" : "Mapping" case .STATE_PROCESSING: return "Processing" case .STATE_VISUALIZING: return "Visualizing" case .STATE_VISUALIZING_CAMERA: return "Visualizing with Camera" case .STATE_VISUALIZING_WHILE_LOADING: return "Visualizing while Loading" case .STATE_VISUALIZING_AND_MEASURING: return "Measuring" default: // IDLE return "Idle" } } private var depthSupported: Bool = false private var viewMode: Int = 2 // 0=Cloud, 1=Mesh, 2=Textured Mesh private var cameraMode: Int = 1 private var statusShown: Bool = true private var debugShown: Bool = false private var mapShown: Bool = true private var odomShown: Bool = true private var graphShown: Bool = true private var gridShown: Bool = true private var optimizedGraphShown: Bool = true private var wireframeShown: Bool = false private var backfaceShown: Bool = false private var lightingShown: Bool = false private var textureColorSeamsShown: Bool = false private var mHudVisible: Bool = true private var mLastTimeHudShown: DispatchTime = .now() private var mMenuOpened: Bool = false private var lowMemoryWarningShown: Bool = false static var previewImages: [String: UIImage] = [:] private var measuringMode: Int = 0 private var visualizationType: Int = 0 // 0=Cloud, 1=Mesh, 2=Texture Mesh @IBOutlet weak var stopButton: UIButton! @IBOutlet weak var recordButton: UIButton! @IBOutlet weak var menuButton: UIButton! @IBOutlet weak var viewButton: UIButton! @IBOutlet weak var newScanButtonLarge: UIButton! @IBOutlet weak var libraryButton: UIButton! @IBOutlet weak var statusLabel: UILabel! @IBOutlet weak var closeVisualizationButton: UIButton! @IBOutlet weak var stopCameraButton: UIButton! @IBOutlet weak var stopMeasuringButton: UIButton! @IBOutlet weak var teleportButton: UIButton! @IBOutlet weak var addMeasureButton: UIButton! @IBOutlet weak var removeMeasureButton: UIButton! @IBOutlet weak var measuringModeButton: UIButton! @IBOutlet weak var exportOBJPLYButton: UIButton! @IBOutlet weak var orthoDistanceSlider: UISlider!{ didSet{ orthoDistanceSlider.transform = CGAffineTransform(rotationAngle: CGFloat(-Double.pi/2)) } } @IBOutlet weak var orthoGridSlider: UISlider! @IBOutlet weak var toastLabel: UILabel! let RTABMAP_TMP_DB = "rtabmap.tmp.db" let RTABMAP_RECOVERY_DB = "rtabmap.tmp.recovery.db" let RTABMAP_EXPORT_DIR = "Export" func getDocumentDirectory() -> URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] } func getTmpDirectory() -> URL { return URL(fileURLWithPath: NSTemporaryDirectory()) } @objc func defaultsChanged(){ print("defaultsChanged()") updateDisplayFromDefaults() } func showToast(message : String, seconds: Double){ if(!self.toastLabel.isHidden) { return; } self.toastLabel.text = message self.toastLabel.isHidden = false DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + seconds) { self.toastLabel.isHidden = true } } func resetNoTouchTimer(_ showHud: Bool = false) { if(showHud) { mMenuOpened = false mHudVisible = true setNeedsStatusBarAppearanceUpdate() updateState(state: self.mState) mLastTimeHudShown = DispatchTime.now() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { if(DispatchTime.now() <= self.mLastTimeHudShown + 4.9) { return } if(self.mState != .STATE_WELCOME && self.mState != .STATE_CAMERA && self.presentedViewController as? UIAlertController == nil && !self.mMenuOpened) { print("Hide HUD") self.mHudVisible = false self.setNeedsStatusBarAppearanceUpdate() self.updateState(state: self.mState) } } } else if(mState != .STATE_WELCOME && mState != .STATE_CAMERA && presentedViewController as? UIAlertController == nil && !mMenuOpened) { self.mHudVisible = false self.setNeedsStatusBarAppearanceUpdate() self.updateState(state: self.mState) } } func addShadow(_ view: UIView, _ offset: Int = 4) { view.layer.shadowColor = UIColor.black.cgColor view.layer.shadowRadius = 3.0 view.layer.shadowOpacity = 1.0 view.layer.shadowOffset = CGSize(width: offset, height: offset) view.layer.masksToBounds = false } override func viewDidLoad() { mMaximumMemory = getAvailableMemory() super.viewDidLoad() // Do any additional setup after loading the view. self.toastLabel.isHidden = true session.delegate = self addShadow(stopButton) addShadow(recordButton) addShadow(menuButton) addShadow(viewButton) addShadow(newScanButtonLarge) addShadow(libraryButton) addShadow(closeVisualizationButton) addShadow(stopCameraButton) addShadow(stopMeasuringButton) addShadow(teleportButton) addShadow(addMeasureButton) addShadow(removeMeasureButton) addShadow(measuringModeButton) addShadow(exportOBJPLYButton) addShadow(statusLabel, 0) addShadow(toastLabel, 0) addShadow(orthoDistanceSlider, 0) addShadow(orthoGridSlider, 0) depthSupported = ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) rtabmap = RTABMap() rtabmap?.setupCallbacksWithCPP() context = EAGLContext(api: .openGLES2) EAGLContext.setCurrent(context) if let view = self.view as? GLKView, let context = context { view.context = context delegate = self rtabmap?.initGlContent() } menuButton.showsMenuAsPrimaryAction = true viewButton.showsMenuAsPrimaryAction = true statusLabel.numberOfLines = 0 statusLabel.text = "" updateDatabases() let doubleTap = UITapGestureRecognizer(target: self, action: #selector(doubleTapped(_:))) doubleTap.numberOfTapsRequired = 2 view.addGestureRecognizer(doubleTap) let singleTap = UITapGestureRecognizer(target: self, action: #selector(singleTapped(_:))) singleTap.numberOfTapsRequired = 1 view.addGestureRecognizer(singleTap) let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(defaultsChanged), name: UserDefaults.didChangeNotification, object: nil) rtabmap!.addObserver(self) registerSettingsBundle() updateDisplayFromDefaults() maxPolygonsPickerView = UIPickerView(frame: CGRect(x: 10, y: 50, width: 250, height: 150)) maxPolygonsPickerView.delegate = self maxPolygonsPickerView.dataSource = self // This is where you can set your min/max values let minNum = 0 let maxNum = 9 maxPolygonsPickerData = Array(stride(from: minNum, to: maxNum + 1, by: 1)) orthoDistanceSlider.setValue(80, animated: false) orthoGridSlider.setValue(90, animated: false) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.updateState(state: self.mState) } } func progressStatusUpdate() { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { if self.mState == .STATE_PROCESSING && self.statusShown { let availableMem = self.getAvailableMemory() let usedMem = self.mMaximumMemory - availableMem; self.statusLabel.text = "Status: \(self.getStateString(state: self.mState))\n" + "RAM Usage (MB): \(usedMem) / \(self.mMaximumMemory)" self.progressStatusUpdate() } } } func progressUpdated(_ rtabmap: RTABMap, count: Int, max: Int) { DispatchQueue.main.async { self.progressView?.setProgress(Float(count)/Float(max), animated: true) } } func initEventReceived(_ rtabmap: RTABMap, status: Int, msg: String) { DispatchQueue.main.async { var optimizedMeshDetected = 0 if(msg == "Loading optimized cloud...done!") { optimizedMeshDetected = 1; } else if(msg == "Loading optimized mesh...done!") { optimizedMeshDetected = 2; } else if(msg == "Loading optimized texture mesh...done!") { optimizedMeshDetected = 3; } if(optimizedMeshDetected > 0) { if(optimizedMeshDetected==1) { self.visualizationType = 0; self.setMeshRendering(viewMode: 0) } else if(optimizedMeshDetected==2) { self.visualizationType = 1; self.setMeshRendering(viewMode: 1) } else // isOBJ { self.visualizationType = 2; self.setMeshRendering(viewMode: 2) } self.updateState(state: .STATE_VISUALIZING_WHILE_LOADING); self.setGLCamera(type: 2); self.dismiss(animated: true) self.showToast(message: "Optimized mesh detected in the database, it is shown while the database is loading...", seconds: 3) } let availableMem = self.getAvailableMemory() let usedMem = self.mMaximumMemory - availableMem; self.statusLabel.text = "Status: " + (status == 1 && msg.isEmpty ? self.mState == State.STATE_CAMERA ? "Camera Preview" : "Idle" : msg) + "\n" + "RAM Usage (MB): \(usedMem) / \(self.mMaximumMemory)" } } func statsUpdated(_ rtabmap: RTABMap, nodes: Int, words: Int, points: Int, polygons: Int, updateTime: Float, loopClosureId: Int, highestHypId: Int, databaseMemoryUsed: Int, inliers: Int, matches: Int, featuresExtracted: Int, hypothesis: Float, nodesDrawn: Int, fps: Float, rejected: Int, rehearsalValue: Float, optimizationMaxError: Float, optimizationMaxErrorRatio: Float, distanceTravelled: Float, fastMovement: Int, landmarkDetected: Int, x: Float, y: Float, z: Float, roll: Float, pitch: Float, yaw: Float) { let availableMem = self.getAvailableMemory() let usedMem = self.mMaximumMemory - availableMem; if(loopClosureId > 0) { mTotalLoopClosures += 1; } let previousNodes = mMapNodes mMapNodes = nodes; let formattedDate = Date().getFormattedDate(format: "HH:mm:ss.SSS") DispatchQueue.main.async { if(self.mMapNodes>0 && previousNodes==0 && self.mState != .STATE_MAPPING) { self.updateState(state: self.mState) // refesh menus and actions } self.statusLabel.text = "" if self.statusShown { self.statusLabel.text = self.statusLabel.text! + "Status: \(self.getStateString(state: self.mState))\n" + "RAM Usage (MB): \(usedMem) / \(self.mMaximumMemory)" } if self.debugShown { self.statusLabel.text = self.statusLabel.text! + "\n" var gpsString = "\n" if(UserDefaults.standard.bool(forKey: "SaveGPS")) { if(self.mLastKnownLocation != nil) { let secondsOld = (Date().timeIntervalSince1970 - self.mLastKnownLocation!.timestamp.timeIntervalSince1970) var bearing = 0.0 if(self.mLastKnownLocation!.course > 0.0) { bearing = self.mLastKnownLocation!.course } gpsString = String(format: "GPS: %.2f %.2f %.2fm %ddeg %.0fm [%d sec old]\n", self.mLastKnownLocation!.coordinate.longitude, self.mLastKnownLocation!.coordinate.latitude, self.mLastKnownLocation!.altitude, Int(bearing), self.mLastKnownLocation!.horizontalAccuracy, Int(secondsOld)); } else { gpsString = "GPS: [not yet available]\n"; } } var lightString = "\n" if(self.mLastLightEstimate != nil) { lightString = String("Light (lm): \(Int(self.mLastLightEstimate!))\n") } self.statusLabel.text = self.statusLabel.text! + gpsString + //gps lightString + //env sensors "Time: \(formattedDate)\n" + "Nodes (WM): \(nodes) (\(nodesDrawn) shown)\n" + "Words: \(words)\n" + "Database (MB): \(databaseMemoryUsed)\n" + "Number of points: \(points)\n" + "Polygons: \(polygons)\n" + "Update time (ms): \(Int(updateTime)) / \(self.mTimeThr==0 ? "No Limit" : String(self.mTimeThr))\n" + "Features: \(featuresExtracted) / \(self.mMaxFeatures==0 ? "No Limit" : (self.mMaxFeatures == -1 ? "Disabled" : String(self.mMaxFeatures)))\n" + "Rehearsal (%): \(Int(rehearsalValue*100))\n" + "Loop closures: \(self.mTotalLoopClosures)\n" + "Inliers: \(inliers)\n" + "Hypothesis (%): \(Int(hypothesis*100)) / \(Int(self.mLoopThr*100)) (\(loopClosureId>0 ? loopClosureId : highestHypId))\n" + String(format: "FPS (rendering): %.1f Hz\n", fps) + String(format: "Travelled distance: %.2f m\n", distanceTravelled) + String(format: "Pose (x,y,z): %.2f %.2f %.2f", x, y, z) } if(self.mState == .STATE_MAPPING || self.mState == .STATE_VISUALIZING_CAMERA) { if(loopClosureId > 0) { if(self.mState == .STATE_VISUALIZING_CAMERA) { self.showToast(message: "Localized!", seconds: 1); } else { self.showToast(message: "Loop closure detected!", seconds: 1); } } else if(rejected > 0) { if(inliers >= UserDefaults.standard.integer(forKey: "MinInliers")) { if(optimizationMaxError > 0.0) { self.showToast(message: String(format: "Loop closure rejected, too high graph optimization error (%.3fm: ratio=%.3f < factor=%.1fx).", optimizationMaxError, optimizationMaxErrorRatio, UserDefaults.standard.float(forKey: "MaxOptimizationError")), seconds: 1); } else { self.showToast(message: String(format: "Loop closure rejected, graph optimization failed! You may try a different Graph Optimizer (see Mapping options)."), seconds: 1); } } else { self.showToast(message: String(format: "Loop closure rejected, not enough inliers (%d/%d < %d).", inliers, matches, UserDefaults.standard.integer(forKey: "MinInliers")), seconds: 1); } } else if(landmarkDetected > 0) { self.showToast(message: "Landmark \(landmarkDetected) detected!", seconds: 1); } } if(self.mState == .STATE_MAPPING) { if(availableMem < 400) { let msg = "Scanning will be stopped because the free memory is too " + "low (\(availableMem) MB). You should be able to save the database but some post-processing and exporting options may fail. " + "\n\nNote that for large environments, you can save multiple databases and " + "merge them with RTAB-Map Desktop version." let alert = UIAlertController(title: "Memory is full!", message: msg, preferredStyle: .alert) let alertActionYes = UIAlertAction(title: "Ok", style: .default) { (UIAlertAction) -> Void in self.stopMapping(ignoreSaving: false, offerPostProcessing: false); } alert.addAction(alertActionYes) self.present(alert, animated: true, completion: nil) } else if(!self.lowMemoryWarningShown && usedMem*3 > availableMem && !self.mDataRecording) { self.lowMemoryWarningShown = true let msg = "Available memory (\(availableMem) MB) should be at least 3 times the " + "memory used (\(usedMem) MB) so that some post-processing and exporting options " + "have enough memory to work correctly. If you just want to save the database " + "after scanning, you can continue until the next warning.\n\n" + "Note that showing only point clouds and/or decrease density reduce memory needed for rendering." let alert = UIAlertController(title: "Memory is full!", message: msg, preferredStyle: .alert) let alertActionYes = UIAlertAction(title: "Stop Now", style: .default) { (UIAlertAction) -> Void in self.stopMapping(ignoreSaving: false); } alert.addAction(alertActionYes) let alertActionNo = UIAlertAction(title: "Continue", style: .cancel) { (UIAlertAction) -> Void in } alert.addAction(alertActionNo) self.present(alert, animated: true, completion: nil) } } } } func cameraInfoEventReceived(_ rtabmap: RTABMap, type: Int, key: String, value: String) { if(self.debugShown && key == "UpstreamRelocationFiltered") { DispatchQueue.main.async { self.dismiss(animated: true) self.showToast(message: "ARKit re-localization filtered because an acceleration of \(value) has been detected, which is over current threshold set in the settings.", seconds: 3) } } } func getAvailableMemory() -> Int { return os_proc_available_memory()/(1024*1024) } @objc func appMovedToBackground() { print("appMovedToBackground()") if(mState == .STATE_VISUALIZING_CAMERA || mState == .STATE_VISUALIZING_AND_MEASURING || mState == .STATE_MAPPING || mState == .STATE_CAMERA) { stopMapping(ignoreSaving: true) } } @objc func appMovedToForeground() { print("appMovedToForeground()") updateDisplayFromDefaults() updateState(state: mState) if(mMapNodes > 0 && self.openedDatabasePath == nil) { let msg = "RTAB-Map has been pushed to background while mapping. Do you want to save the map now?" let alert = UIAlertController(title: mDataRecording ? "Data Recording Stopped" : "Mapping Stopped!", message: msg, preferredStyle: .alert) let alertActionNo = UIAlertAction(title: "Ignore", style: .cancel) { (UIAlertAction) -> Void in } alert.addAction(alertActionNo) let alertActionYes = UIAlertAction(title: "Yes", style: .default) { (UIAlertAction) -> Void in self.save() } alert.addAction(alertActionYes) self.present(alert, animated: true, completion: nil) } } func setMeshRendering(viewMode: Int) { switch viewMode { case 0: self.rtabmap?.setMeshRendering(enabled: false, withTexture: false) case 1: self.rtabmap?.setMeshRendering(enabled: true, withTexture: false) default: self.rtabmap?.setMeshRendering(enabled: true, withTexture: true) } self.viewMode = viewMode updateState(state: mState) } func setGLCamera(type: Int) { cameraMode = type rtabmap!.setCamera(type: type); } func startCamera() { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: // The user has previously granted access to the camera. print("Start Camera") rtabmap!.startCamera() let configuration = ARWorldTrackingConfiguration() var message = "" if(mState != .STATE_VISUALIZING_AND_MEASURING) { if(!UserDefaults.standard.bool(forKey: "LidarMode")) { message = "LiDAR is disabled (Settings->Mapping->LiDAR Mode = OFF), only tracked features will be mapped." self.setMeshRendering(viewMode: 0) } else if !depthSupported { message = "The device does not have a LiDAR, only tracked features will be mapped. A LiDAR is required for accurate 3D reconstruction." self.setMeshRendering(viewMode: 0) } else { configuration.frameSemantics = .sceneDepth } } session.run(configuration, options: [.resetSceneReconstruction, .resetTracking, .removeExistingAnchors]) switch mState { case .STATE_VISUALIZING_AND_MEASURING, .STATE_VISUALIZING_CAMERA: break // State should be already set default: locationManager?.startUpdatingLocation() updateState(state: .STATE_CAMERA) } if(!message.isEmpty) { let alertController = UIAlertController(title: "Start Camera", message: message, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { (action) in } alertController.addAction(okAction) present(alertController, animated: true) } case .notDetermined: // The user has not yet been asked for camera access. AVCaptureDevice.requestAccess(for: .video) { granted in if granted { DispatchQueue.main.async { self.startCamera() } } } default: let alertController = UIAlertController(title: "Camera Disabled", message: "Camera permission is required to start the camera. You can enable it in Settings.", preferredStyle: .alert) let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } if UIApplication.shared.canOpenURL(settingsUrl) { UIApplication.shared.open(settingsUrl, completionHandler: { (success) in print("Settings opened: \(success)") // Prints true }) } } alertController.addAction(settingsAction) let okAction = UIAlertAction(title: "Ignore", style: .default) { (action) in } alertController.addAction(okAction) present(alertController, animated: true) } } private func updateState(state: State) { print("State: \(state)") if(mState != state) { mState = state; resetNoTouchTimer(true) return } mState = state; var actionNewScanEnabled: Bool var actionNewDataRecording: Bool var actionSaveEnabled: Bool var actionResumeEnabled: Bool var actionExportEnabled: Bool var actionOptimizeEnabled: Bool var actionSettingsEnabled: Bool var actionMeasuringEnabled: Bool switch mState { case .STATE_CAMERA: libraryButton.isEnabled = false libraryButton.isHidden = false menuButton.isHidden = false viewButton.isHidden = false newScanButtonLarge.isHidden = true // WELCOME button recordButton.isHidden = false stopButton.isHidden = true closeVisualizationButton.isHidden = true stopCameraButton.isHidden = false stopMeasuringButton.isHidden = true exportOBJPLYButton.isHidden = true orthoDistanceSlider.isHidden = cameraMode != 3 orthoGridSlider.isHidden = cameraMode != 3 teleportButton.isHidden = true addMeasureButton.isHidden = true removeMeasureButton.isHidden = true measuringModeButton.isHidden = true actionNewScanEnabled = !mDataRecording actionNewDataRecording = mDataRecording actionSaveEnabled = false actionResumeEnabled = false actionExportEnabled = false actionOptimizeEnabled = false actionSettingsEnabled = false actionMeasuringEnabled = false case .STATE_MAPPING: libraryButton.isEnabled = false libraryButton.isHidden = !mHudVisible menuButton.isHidden = !mHudVisible viewButton.isHidden = !mHudVisible newScanButtonLarge.isHidden = true // WELCOME button recordButton.isHidden = true stopButton.isHidden = false closeVisualizationButton.isHidden = true stopCameraButton.isHidden = true stopMeasuringButton.isHidden = true exportOBJPLYButton.isHidden = true orthoDistanceSlider.isHidden = cameraMode != 3 || !mHudVisible orthoGridSlider.isHidden = cameraMode != 3 || !mHudVisible teleportButton.isHidden = true addMeasureButton.isHidden = true removeMeasureButton.isHidden = true measuringModeButton.isHidden = true actionNewScanEnabled = !mDataRecording actionNewDataRecording = mDataRecording actionSaveEnabled = false actionResumeEnabled = false actionExportEnabled = false actionOptimizeEnabled = false actionSettingsEnabled = false actionMeasuringEnabled = false case .STATE_PROCESSING, .STATE_VISUALIZING_WHILE_LOADING, .STATE_VISUALIZING_CAMERA, .STATE_VISUALIZING_AND_MEASURING: libraryButton.isEnabled = false libraryButton.isHidden = !mHudVisible menuButton.isHidden = !mHudVisible viewButton.isHidden = !mHudVisible newScanButtonLarge.isHidden = true // WELCOME button recordButton.isHidden = true stopButton.isHidden = true closeVisualizationButton.isHidden = true stopCameraButton.isHidden = mState != .STATE_VISUALIZING_CAMERA stopMeasuringButton.isHidden = mState != .STATE_VISUALIZING_AND_MEASURING exportOBJPLYButton.isHidden = true orthoDistanceSlider.isHidden = cameraMode != 3 || mState == .STATE_PROCESSING orthoGridSlider.isHidden = cameraMode != 3 || mState == .STATE_PROCESSING teleportButton.isHidden = mState != .STATE_VISUALIZING_AND_MEASURING || cameraMode != 0 addMeasureButton.isHidden = mState != .STATE_VISUALIZING_AND_MEASURING || cameraMode == 3 removeMeasureButton.isHidden = mState != .STATE_VISUALIZING_AND_MEASURING || cameraMode == 3 measuringModeButton.isHidden = true actionNewScanEnabled = false actionNewDataRecording = false actionSaveEnabled = false actionResumeEnabled = false actionExportEnabled = false actionOptimizeEnabled = false actionSettingsEnabled = false actionMeasuringEnabled = mState == .STATE_VISUALIZING_AND_MEASURING case .STATE_VISUALIZING: libraryButton.isEnabled = !databases.isEmpty libraryButton.isHidden = !mHudVisible menuButton.isHidden = !mHudVisible viewButton.isHidden = !mHudVisible newScanButtonLarge.isHidden = true // WELCOME button recordButton.isHidden = true stopButton.isHidden = true closeVisualizationButton.isHidden = !mHudVisible stopCameraButton.isHidden = true stopMeasuringButton.isHidden = true exportOBJPLYButton.isHidden = !mHudVisible orthoDistanceSlider.isHidden = cameraMode != 3 || !mHudVisible orthoGridSlider.isHidden = cameraMode != 3 || !mHudVisible teleportButton.isHidden = true addMeasureButton.isHidden = true removeMeasureButton.isHidden = true measuringModeButton.isHidden = !mHudVisible || self.visualizationType==0 actionNewScanEnabled = true actionNewDataRecording = true actionSaveEnabled = mMapNodes>0 actionResumeEnabled = mMapNodes>0 actionExportEnabled = mMapNodes>0 actionOptimizeEnabled = mMapNodes>0 actionSettingsEnabled = true actionMeasuringEnabled = true default: // IDLE // WELCOME libraryButton.isEnabled = !databases.isEmpty libraryButton.isHidden = mState != .STATE_WELCOME && !mHudVisible menuButton.isHidden = mState != .STATE_WELCOME && !mHudVisible viewButton.isHidden = mState != .STATE_WELCOME && !mHudVisible newScanButtonLarge.isHidden = mState != .STATE_WELCOME recordButton.isHidden = true stopButton.isHidden = true closeVisualizationButton.isHidden = true stopCameraButton.isHidden = true stopMeasuringButton.isHidden = true exportOBJPLYButton.isHidden = true orthoDistanceSlider.isHidden = cameraMode != 3 || !mHudVisible orthoGridSlider.isHidden = cameraMode != 3 || !mHudVisible teleportButton.isHidden = true addMeasureButton.isHidden = true removeMeasureButton.isHidden = true measuringModeButton.isHidden = true actionNewScanEnabled = true actionNewDataRecording = true actionSaveEnabled = mState != .STATE_WELCOME && mMapNodes>0 actionResumeEnabled = mState != .STATE_WELCOME && mMapNodes>0 actionExportEnabled = mState != .STATE_WELCOME && mMapNodes>0 actionOptimizeEnabled = mState != .STATE_WELCOME && mMapNodes>0 actionSettingsEnabled = true actionMeasuringEnabled = false } let view = self.view as? GLKView if(mState != .STATE_MAPPING && mState != .STATE_CAMERA && mState != .STATE_VISUALIZING_CAMERA && mState != .STATE_VISUALIZING_AND_MEASURING) { self.isPaused = true view?.enableSetNeedsDisplay = true self.view.setNeedsDisplay() } else { view?.enableSetNeedsDisplay = false self.isPaused = false } if !self.isPaused { self.view.setNeedsDisplay() } // Update menus based on current state if(!exportOBJPLYButton.isHidden) { let format = UserDefaults.standard.string(forKey: "ExportPointCloudFormat")!; var title = "Export " if (self.visualizationType == 2) { title += "OBJ"; } else if (self.visualizationType == 1) { title += "PLY"; } else { title += format == "las" ? "LAS" : format == "laz" ? "LAZ" : "PLY"; } self.exportOBJPLYButton.setTitle(title, for: .normal) } // PointCloud menu let pointCloudMenu = UIMenu(title: "Point cloud...", children: [ UIAction(title: "Current Density", handler: { _ in self.export(isOBJ: false, meshing: false, regenerateCloud: false, optimized: false, optimizedMaxPolygons: 0, previousState: self.mState) }), UIAction(title: "Max Density", handler: { _ in self.export(isOBJ: false, meshing: false, regenerateCloud: true, optimized: false, optimizedMaxPolygons: 0, previousState: self.mState) }) ]) // Optimized Mesh menu let optimizedMeshMenu = UIMenu(title: "Optimized mesh...", children: [ UIAction(title: "Colored Mesh", handler: { _ in self.exportMesh(isOBJ: false) }), UIAction(title: "Textured Mesh", handler: { _ in self.exportMesh(isOBJ: true) }) ]) // Export menu let exportMenu = UIMenu(title: "Assemble...", children: [pointCloudMenu, optimizedMeshMenu]) // Optimized Mesh menu let optimizeAdvancedMenu = UIMenu(title: "Advanced...", children: [ UIAction(title: "Global Graph Optimization", handler: { _ in self.optimization(approach: 0) }), UIAction(title: "Detect More Loop Closures", handler: { _ in self.optimization(approach: 2) }), UIAction(title: "Adjust Colors (Fast)", handler: { _ in self.optimization(approach: 5) }), UIAction(title: "Adjust Colors (Full)", handler: { _ in self.optimization(approach: 6) }), UIAction(title: "Mesh Smoothing", handler: { _ in self.optimization(approach: 7) }), UIAction(title: "Bundle Adjustment", handler: { _ in self.optimization(approach: 1) }), UIAction(title: "Noise Filtering", handler: { _ in self.optimization(approach: 4) }) ]) // Optimize menu let optimizeMenu = UIMenu(title: "Optimize...", children: [ UIAction(title: "Standard Optimization", handler: { _ in self.optimization(approach: -1) }), optimizeAdvancedMenu]) // Advanced menu let advancedMenu = UIMenu(title: "Advanced...", children: [ UIAction(title: "New Data Recording", image: UIImage(systemName: "plus.app"), attributes: actionNewDataRecording ? [] : .disabled, state: .off, handler: { _ in self.newScan(dataRecordingMode: true) }) ]) // Measuring menu let measuringMenu = UIMenu(title: "Measuring...", image: UIImage(systemName: "ruler"), children: [ UIAction(title: "Plane to Plane Mode", image: measuringMode == 0 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), handler: { _ in self.measuringMode = 0 self.rtabmap!.setMeasuringMode(self.measuringMode) self.resetNoTouchTimer(true) }), UIAction(title: "Point to Point Mode", image: measuringMode == 2 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), handler: { _ in self.measuringMode = 2 self.rtabmap!.setMeasuringMode(self.measuringMode) self.resetNoTouchTimer(true) }), UIAction(title: "Clear All Measures", image: UIImage(systemName: "trash"), state: .off, handler: { _ in self.clearMeasures(); self.resetNoTouchTimer(true) }) ]) var fileMenuChildren: [UIMenuElement] = [] fileMenuChildren.append(UIAction(title: "New Mapping Session", image: UIImage(systemName: "plus.app"), attributes: actionNewScanEnabled ? [] : .disabled, state: .off, handler: { _ in self.newScan() })) if(actionOptimizeEnabled) { fileMenuChildren.append(optimizeMenu) } else { fileMenuChildren.append(UIAction(title: "Optimize...", attributes: .disabled, state: .off, handler: { _ in })) } if(actionExportEnabled) { fileMenuChildren.append(exportMenu) } else { fileMenuChildren.append(UIAction(title: "Assemble...", attributes: .disabled, state: .off, handler: { _ in })) } fileMenuChildren.append(UIAction(title: "Save", image: UIImage(systemName: "square.and.arrow.down"), attributes: actionSaveEnabled ? [] : .disabled, state: .off, handler: { _ in self.save() })) fileMenuChildren.append(UIAction(title: "Append Scan", image: UIImage(systemName: "play.fill"), attributes: actionResumeEnabled ? [] : .disabled, state: .off, handler: { _ in self.resumeScan() })) if(actionMeasuringEnabled) { fileMenuChildren.append(measuringMenu) } else { fileMenuChildren.append(UIAction(title: "Measuring...", image: UIImage(systemName: "ruler"), attributes: .disabled, state: .off, handler: { _ in })) } fileMenuChildren.append(advancedMenu) // File menu let fileMenu = UIMenu(title: "File", options: .displayInline, children: fileMenuChildren) // Visibility menu let visibilityMenu = UIMenu(title: "Visibility...", children: [ UIAction(title: "Status", image: statusShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: (self.mState != .STATE_WELCOME) ? [] : .disabled, handler: { _ in self.statusShown = !self.statusShown self.resetNoTouchTimer(true) }), UIAction(title: "Debug", image: debugShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: (self.mState != .STATE_WELCOME) ? [] : .disabled, handler: { _ in self.debugShown = !self.debugShown self.resetNoTouchTimer(true) }), UIAction(title: "Odom Visible", image: odomShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: (self.mState == .STATE_MAPPING || self.mState == .STATE_CAMERA || self.mState == .STATE_VISUALIZING_CAMERA || self.mState == .STATE_VISUALIZING_AND_MEASURING) ? [] : .disabled, handler: { _ in self.odomShown = !self.odomShown self.rtabmap!.setOdomCloudShown(shown: self.odomShown) self.resetNoTouchTimer(true) }), UIAction(title: "Graph Visibile", image: graphShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: (self.mState == .STATE_MAPPING || self.mState == .STATE_CAMERA || self.mState == .STATE_IDLE) ? [] : .disabled, handler: { _ in self.graphShown = !self.graphShown self.rtabmap!.setGraphVisible(visible: self.graphShown) self.resetNoTouchTimer(true) }), UIAction(title: "Grid Visible", image: gridShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), handler: { _ in self.gridShown = !self.gridShown self.rtabmap!.setGridVisible(visible: self.gridShown) self.resetNoTouchTimer(true) }), UIAction(title: "Optimized Graph", image: optimizedGraphShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: (self.mState == .STATE_IDLE) ? [] : .disabled, handler: { _ in self.optimizedGraphShown = !self.optimizedGraphShown self.rtabmap!.setGraphOptimization(enabled: self.optimizedGraphShown) self.resetNoTouchTimer(true) }) ]) let settingsMenu = UIMenu(title: "Settings", options: .displayInline, children: [visibilityMenu, UIAction(title: "Settings", image: UIImage(systemName: "gearshape.2"), attributes: actionSettingsEnabled ? [] : .disabled, state: .off, handler: { _ in guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } if UIApplication.shared.canOpenURL(settingsUrl) { UIApplication.shared.open(settingsUrl, completionHandler: { (success) in print("Settings opened: \(success)") // Prints true }) } }), UIAction(title: "Restore All Default Settings", attributes: actionSettingsEnabled ? [] : .disabled, state: .off, handler: { _ in let ac = UIAlertController(title: "Reset All Default Settings", message: "Do you want to reset all settings to default?", preferredStyle: .alert) ac.addAction(UIAlertAction(title: "Yes", style: .default, handler: { _ in let notificationCenter = NotificationCenter.default notificationCenter.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil) UserDefaults.standard.reset() self.registerSettingsBundle() self.updateDisplayFromDefaults(); notificationCenter.addObserver(self, selector: #selector(self.defaultsChanged), name: UserDefaults.didChangeNotification, object: nil) })) ac.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil)) self.present(ac, animated: true) }) ]) menuButton.menu = UIMenu(title: "", children: [fileMenu, settingsMenu]) menuButton.addTarget(self, action: #selector(ViewController.menuOpened(_:)), for: .menuActionTriggered) // Camera menu let renderingMenu = UIMenu(title: "Rendering", options: .displayInline, children: [ UIAction(title: "Texture/Color Blend", image: self.textureColorSeamsShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: self.mState == .STATE_VISUALIZING || self.mState == .STATE_VISUALIZING_CAMERA || self.mState == .STATE_VISUALIZING_AND_MEASURING || self.mState == .STATE_VISUALIZING_WHILE_LOADING ? [] : .disabled, handler: { _ in self.textureColorSeamsShown = !self.textureColorSeamsShown self.rtabmap!.setTextureColorSeamsHidden(hidden: !self.textureColorSeamsShown) self.resetNoTouchTimer(true) }), UIAction(title: "Wireframe", image: self.wireframeShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), handler: { _ in self.wireframeShown = !self.wireframeShown self.rtabmap!.setWireframe(enabled: self.wireframeShown) self.resetNoTouchTimer(true) }), UIAction(title: "Lighting", image: self.lightingShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: self.mState == .STATE_VISUALIZING || self.mState == .STATE_VISUALIZING_CAMERA || self.mState == .STATE_VISUALIZING_AND_MEASURING || self.mState == .STATE_VISUALIZING_WHILE_LOADING ? [] : .disabled, handler: { _ in self.lightingShown = !self.lightingShown self.rtabmap!.setLighting(enabled: self.lightingShown) self.resetNoTouchTimer(true) }), UIAction(title: "Backface", image: self.backfaceShown ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), handler: { _ in self.backfaceShown = !self.backfaceShown self.rtabmap!.setBackfaceCulling(enabled: !self.backfaceShown) self.resetNoTouchTimer(true) }) ]) let cameraMenu = UIMenu(title: "View", options: .displayInline, children: [ UIAction(title: "First-P. View", image: cameraMode == 0 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: (self.mState == .STATE_CAMERA || self.mState == .STATE_VISUALIZING || self.mState == .STATE_MAPPING || self.mState == .STATE_VISUALIZING_CAMERA || self.mState == .STATE_VISUALIZING_AND_MEASURING) ? [] : .disabled, handler: { _ in self.setGLCamera(type: 0) if(self.mState == .STATE_VISUALIZING) { self.rtabmap?.setLocalizationMode(enabled: true) self.rtabmap?.setPausedMapping(paused: false); self.updateState(state: .STATE_VISUALIZING_CAMERA) self.startCamera() } else { self.resetNoTouchTimer(true) } }), UIAction(title: "Third-P. View", image: cameraMode == 1 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: (self.mState != .STATE_VISUALIZING_AND_MEASURING) ? [] : .disabled, handler: { _ in self.setGLCamera(type: 1) self.resetNoTouchTimer(true) }), UIAction(title: "Top View", image: cameraMode == 2 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), handler: { _ in self.setGLCamera(type: 2) self.resetNoTouchTimer(true) }), UIAction(title: "Ortho View", image: cameraMode == 3 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), handler: { _ in self.setGLCamera(type: 3) self.resetNoTouchTimer(true) }) ]) let showCloudMeshActions = mState != .STATE_VISUALIZING && mState != .STATE_VISUALIZING_CAMERA && mState != .STATE_VISUALIZING_AND_MEASURING && mState != .STATE_PROCESSING && mState != .STATE_VISUALIZING_WHILE_LOADING let cloudMeshMenu = UIMenu(title: "CloudMesh", options: .displayInline, children: [ UIAction(title: "Point Cloud", image: viewMode == 0 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: showCloudMeshActions ? [] : .disabled, handler: { _ in self.setMeshRendering(viewMode: 0) self.resetNoTouchTimer(true) }), UIAction(title: "Mesh", image: viewMode == 1 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: showCloudMeshActions ? [] : .disabled, handler: { _ in self.setMeshRendering(viewMode: 1) self.resetNoTouchTimer(true) }), UIAction(title: "Texture Mesh", image: viewMode == 2 ? UIImage(systemName: "checkmark.circle") : UIImage(systemName: "circle"), attributes: showCloudMeshActions ? [] : .disabled, handler: { _ in self.setMeshRendering(viewMode: 2) self.resetNoTouchTimer(true) }) ]) var viewMenuChildren: [UIMenuElement] = [] viewMenuChildren.append(cameraMenu) viewMenuChildren.append(renderingMenu) viewMenuChildren.append(cloudMeshMenu) viewButton.menu = UIMenu(title: "", children: viewMenuChildren) viewButton.addTarget(self, action: #selector(ViewController.menuOpened(_:)), for: .menuActionTriggered) } @IBAction func menuOpened(_ sender:UIButton) { mMenuOpened = true; } func exportMesh(isOBJ: Bool) { let ac = UIAlertController(title: "Maximum Polygons", message: "\n\n\n\n\n\n\n\n\n\n", preferredStyle: .alert) ac.view.addSubview(maxPolygonsPickerView) maxPolygonsPickerView.selectRow(2, inComponent: 0, animated: false) ac.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in let pickerValue = self.maxPolygonsPickerData[self.maxPolygonsPickerView.selectedRow(inComponent: 0)] self.export(isOBJ: isOBJ, meshing: true, regenerateCloud: false, optimized: true, optimizedMaxPolygons: pickerValue*100000, previousState: self.mState); })) ac.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) present(ac, animated: true) } func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return maxPolygonsPickerData.count } func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { if(row == 0) { return "No Limit" } return "\(maxPolygonsPickerData[row])00 000" } // Auto-hide the home indicator to maximize immersion in AR experiences. override var prefersHomeIndicatorAutoHidden: Bool { return true } // Hide the status bar to maximize immersion in AR experiences. override var prefersStatusBarHidden: Bool { return !mHudVisible } //This is called when a new frame has been updated. func session(_ session: ARSession, didUpdate frame: ARFrame) { var status = "" var accept = false switch frame.camera.trackingState { case .normal: accept = true case .notAvailable: status = "Tracking not available" case .limited(.excessiveMotion): accept = true status = "Please Slow Your Movement" case .limited(.insufficientFeatures): accept = true status = "Avoid Featureless Surfaces" case .limited(.initializing): status = "Initializing" case .limited(.relocalizing): status = "Relocalizing" default: status = "Unknown tracking state" } mLastLightEstimate = frame.lightEstimate?.ambientIntensity if !status.isEmpty && mLastLightEstimate != nil && mLastLightEstimate! < 100 && accept { status = "Camera Is Occluded Or Lighting Is Too Dark" } if let rotation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation { rtabmap?.postOdometryEvent(frame: frame, orientation: rotation, viewport: self.view.frame.size) } if !status.isEmpty { DispatchQueue.main.async { self.showToast(message: status, seconds: 2) } } } // This is called when a session fails. func session(_ session: ARSession, didFailWithError error: Error) { // Present an error message to the user. guard error is ARError else { return } let errorWithInfo = error as NSError let messages = [ errorWithInfo.localizedDescription, errorWithInfo.localizedFailureReason, errorWithInfo.localizedRecoverySuggestion ] let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") DispatchQueue.main.async { // Present an alert informing about the error that has occurred. let alertController = UIAlertController(title: "The AR session failed.", message: errorMessage, preferredStyle: .alert) let restartAction = UIAlertAction(title: "Restart Session", style: .default) { _ in alertController.dismiss(animated: true, completion: nil) if let configuration = self.session.configuration { self.session.run(configuration, options: [.resetSceneReconstruction, .resetTracking, .removeExistingAnchors]) } } alertController.addAction(restartAction) self.present(alertController, animated: true, completion: nil) } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { mLastKnownLocation = locations.last! rtabmap?.setGPS(location: locations.last!); } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print(error) } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { print(status.rawValue) if(status == .notDetermined) { locationManager?.requestWhenInUseAuthorization() } if(status == .denied) { let alertController = UIAlertController(title: "GPS Disabled", message: "GPS option is enabled (Settings->Mapping...) but localization is denied for this App. To enable location for this App, go in Settings->Privacy->Location.", preferredStyle: .alert) let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in self.locationManager = nil self.mLastKnownLocation = nil guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } if UIApplication.shared.canOpenURL(settingsUrl) { UIApplication.shared.open(settingsUrl, completionHandler: { (success) in print("Settings opened: \(success)") // Prints true }) } } alertController.addAction(settingsAction) let okAction = UIAlertAction(title: "Turn Off GPS", style: .default) { (action) in UserDefaults.standard.setValue(false, forKey: "SaveGPS") self.updateDisplayFromDefaults() } alertController.addAction(okAction) present(alertController, animated: true) } else if(status == .authorizedWhenInUse) { if locationManager != nil { if(locationManager!.accuracyAuthorization == .reducedAccuracy) { let alertController = UIAlertController(title: "GPS Reduced Accuracy", message: "Your location settings for this App is set to reduced accuracy. We recommend to use high accuracy.", preferredStyle: .alert) let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } if UIApplication.shared.canOpenURL(settingsUrl) { UIApplication.shared.open(settingsUrl, completionHandler: { (success) in print("Settings opened: \(success)") // Prints true }) } } alertController.addAction(settingsAction) let okAction = UIAlertAction(title: "Ignore", style: .default) { (action) in } alertController.addAction(okAction) present(alertController, animated: true) } } } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // The screen shouldn't dim during AR experiences. UIApplication.shared.isIdleTimerDisabled = true } var statusBarOrientation: UIInterfaceOrientation? { get { guard let orientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation else { #if DEBUG fatalError("Could not obtain UIInterfaceOrientation from a valid windowScene") #else return nil #endif } return orientation } } deinit { EAGLContext.setCurrent(context) rtabmap = nil context = nil EAGLContext.setCurrent(nil) } var firstTouch: UITouch? var secondTouch: UITouch? override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) for touch in touches { if (firstTouch == nil) { firstTouch = touch let pose = touch.location(in: self.view) let normalizedX = pose.x / self.view.bounds.size.width; let normalizedY = pose.y / self.view.bounds.size.height; rtabmap?.onTouchEvent(touch_count: 1, event: 0, x0: Float(normalizedX), y0: Float(normalizedY), x1: 0.0, y1: 0.0); } else if (firstTouch != nil && secondTouch == nil) { secondTouch = touch if let pose0 = firstTouch?.location(in: self.view) { if let pose1 = secondTouch?.location(in: self.view) { let normalizedX0 = pose0.x / self.view.bounds.size.width; let normalizedY0 = pose0.y / self.view.bounds.size.height; let normalizedX1 = pose1.x / self.view.bounds.size.width; let normalizedY1 = pose1.y / self.view.bounds.size.height; rtabmap?.onTouchEvent(touch_count: 2, event: 5, x0: Float(normalizedX0), y0: Float(normalizedY0), x1: Float(normalizedX1), y1: Float(normalizedY1)); } } } } if self.isPaused { self.view.setNeedsDisplay() } } override func touchesMoved(_ touches: Set, with event: UIEvent?) { super.touchesMoved(touches, with: event) var firstTouchUsed = false var secondTouchUsed = false for touch in touches { if(touch == firstTouch) { firstTouchUsed = true } else if(touch == secondTouch) { secondTouchUsed = true } } if(secondTouch != nil) { if(firstTouchUsed || secondTouchUsed) { if let pose0 = firstTouch?.location(in: self.view) { if let pose1 = secondTouch?.location(in: self.view) { let normalizedX0 = pose0.x / self.view.bounds.size.width; let normalizedY0 = pose0.y / self.view.bounds.size.height; let normalizedX1 = pose1.x / self.view.bounds.size.width; let normalizedY1 = pose1.y / self.view.bounds.size.height; rtabmap?.onTouchEvent(touch_count: 2, event: 2, x0: Float(normalizedX0), y0: Float(normalizedY0), x1: Float(normalizedX1), y1: Float(normalizedY1)); } } } } else if(firstTouchUsed) { if let pose = firstTouch?.location(in: self.view) { let normalizedX = pose.x / self.view.bounds.size.width; let normalizedY = pose.y / self.view.bounds.size.height; rtabmap?.onTouchEvent(touch_count: 1, event: 2, x0: Float(normalizedX), y0: Float(normalizedY), x1: 0.0, y1: 0.0); } } if self.isPaused { self.view.setNeedsDisplay() } } override func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) for touch in touches { if(touch == firstTouch) { firstTouch = nil } else if(touch == secondTouch) { secondTouch = nil } } if (firstTouch == nil && secondTouch != nil) { firstTouch = secondTouch secondTouch = nil } if (firstTouch != nil && secondTouch == nil) { let pose = firstTouch!.location(in: self.view) let normalizedX = pose.x / self.view.bounds.size.width; let normalizedY = pose.y / self.view.bounds.size.height; rtabmap?.onTouchEvent(touch_count: 1, event: 0, x0: Float(normalizedX), y0: Float(normalizedY), x1: 0.0, y1: 0.0); } if self.isPaused { self.view.setNeedsDisplay() } } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { super.touchesCancelled(touches, with: event) for touch in touches { if(touch == firstTouch) { firstTouch = nil; } else if(touch == secondTouch) { secondTouch = nil; } } if self.isPaused { self.view.setNeedsDisplay() } } @IBAction func doubleTapped(_ gestureRecognizer: UITapGestureRecognizer) { if gestureRecognizer.state == UIGestureRecognizer.State.recognized { let pose = gestureRecognizer.location(in: gestureRecognizer.view) let normalizedX = pose.x / self.view.bounds.size.width; let normalizedY = pose.y / self.view.bounds.size.height; rtabmap?.onTouchEvent(touch_count: 3, event: 0, x0: Float(normalizedX), y0: Float(normalizedY), x1: 0.0, y1: 0.0); if self.isPaused { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.view.setNeedsDisplay() } } } } @IBAction func singleTapped(_ gestureRecognizer: UITapGestureRecognizer) { if gestureRecognizer.state == .recognized { resetNoTouchTimer(!mHudVisible) if self.isPaused { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.view.setNeedsDisplay() } } } } func registerSettingsBundle(){ let appDefaults = [String:AnyObject]() UserDefaults.standard.register(defaults: appDefaults) } func updateDisplayFromDefaults() { //Get the defaults let defaults = UserDefaults.standard //let appendMode = defaults.bool(forKey: "AppendMode") // update preference rtabmap!.setOnlineBlending(enabled: defaults.bool(forKey: "Blending")); rtabmap!.setNodesFiltering(enabled: defaults.bool(forKey: "NodesFiltering")); rtabmap!.setFullResolution(enabled: defaults.bool(forKey: "HDMode")); rtabmap!.setSmoothing(enabled: defaults.bool(forKey: "Smoothing")); rtabmap!.setDepthBleedingError(value: defaults.float(forKey: "DepthBleedingError")); rtabmap!.setAppendMode(enabled: defaults.bool(forKey: "AppendMode")); rtabmap!.setUpstreamRelocalizationAccThr(value: defaults.float(forKey: "UpstreamRelocalizationFilteringAccThr")); rtabmap!.setExportPointCloudFormat(format: defaults.string(forKey: "ExportPointCloudFormat")!); mTimeThr = (defaults.string(forKey: "TimeLimit")! as NSString).integerValue mMaxFeatures = (defaults.string(forKey: "MaxFeaturesExtractedLoopClosure")! as NSString).integerValue // Mapping parameters rtabmap!.setMappingParameter(key: "Rtabmap/DetectionRate", value: defaults.string(forKey: "UpdateRate")!); rtabmap!.setMappingParameter(key: "Rtabmap/TimeThr", value: defaults.string(forKey: "TimeLimit")!); rtabmap!.setMappingParameter(key: "Rtabmap/MemoryThr", value: defaults.string(forKey: "MemoryLimit")!); rtabmap!.setMappingParameter(key: "RGBD/LinearSpeedUpdate", value: defaults.string(forKey: "MaximumMotionSpeed")!); let motionSpeed = ((defaults.string(forKey: "MaximumMotionSpeed")!) as NSString).floatValue/2.0; rtabmap!.setMappingParameter(key: "RGBD/AngularSpeedUpdate", value: NSString(format: "%.2f", motionSpeed) as String); rtabmap!.setMappingParameter(key: "Rtabmap/LoopThr", value: defaults.string(forKey: "LoopClosureThreshold")!); rtabmap!.setMappingParameter(key: "Mem/RehearsalSimilarity", value: defaults.string(forKey: "SimilarityThreshold")!); rtabmap!.setMappingParameter(key: "Kp/MaxFeatures", value: defaults.string(forKey: "MaxFeaturesExtractedVocabulary")!); rtabmap!.setMappingParameter(key: "Vis/MaxFeatures", value: defaults.string(forKey: "MaxFeaturesExtractedLoopClosure")!); rtabmap!.setMappingParameter(key: "Vis/MinInliers", value: defaults.string(forKey: "MinInliers")!); rtabmap!.setMappingParameter(key: "RGBD/OptimizeMaxError", value: defaults.string(forKey: "MaxOptimizationError")!); rtabmap!.setMappingParameter(key: "Kp/DetectorStrategy", value: defaults.string(forKey: "FeatureType")!); rtabmap!.setMappingParameter(key: "Vis/FeatureType", value: defaults.string(forKey: "FeatureType")!); rtabmap!.setMappingParameter(key: "Mem/NotLinkedNodesKept", value: defaults.bool(forKey: "SaveAllFramesInDatabase") ? "true" : "false"); rtabmap!.setMappingParameter(key: "RGBD/OptimizeFromGraphEnd", value: defaults.bool(forKey: "OptimizationfromGraphEnd") ? "true" : "false"); rtabmap!.setMappingParameter(key: "RGBD/MaxOdomCacheSize", value: defaults.string(forKey: "MaximumOdometryCacheSize")!); rtabmap!.setMappingParameter(key: "Optimizer/Strategy", value: defaults.string(forKey: "GraphOptimizer")!); rtabmap!.setMappingParameter(key: "RGBD/ProximityBySpace", value: defaults.string(forKey: "ProximityDetection")!); let markerDetection = defaults.integer(forKey: "ArUcoMarkerDetection") if(markerDetection == -1) { rtabmap!.setMappingParameter(key: "RGBD/MarkerDetection", value: "false"); } else { rtabmap!.setMappingParameter(key: "RGBD/MarkerDetection", value: "true"); rtabmap!.setMappingParameter(key: "Marker/Dictionary", value: defaults.string(forKey: "ArUcoMarkerDetection")!); rtabmap!.setMappingParameter(key: "Marker/CornerRefinementMethod", value: (markerDetection > 16 ? "3":"0")); rtabmap!.setMappingParameter(key: "Marker/MaxDepthError", value: defaults.string(forKey: "MarkerDepthErrorEstimation")!); rtabmap!.setMappingParameter(key: "Marker/MaxRange", value: defaults.string(forKey: "MarkerMaxRange")!); if let val = NumberFormatter().number(from: defaults.string(forKey: "MarkerSize")!)?.doubleValue { rtabmap!.setMappingParameter(key: "Marker/Length", value: String(format: "%f", val/100.0)) } else{ rtabmap!.setMappingParameter(key: "Marker/Length", value: "0") } } // Rendering rtabmap!.setCloudDensityLevel(value: defaults.integer(forKey: "PointCloudDensity")); rtabmap!.setMaxCloudDepth(value: defaults.float(forKey: "MaxDepth")); rtabmap!.setMinCloudDepth(value: defaults.float(forKey: "MinDepth")); rtabmap!.setDepthConfidence(value: defaults.integer(forKey: "DepthConfidence")); rtabmap!.setPointSize(value: defaults.float(forKey: "PointSize")); rtabmap!.setMeshAngleTolerance(value: defaults.float(forKey: "MeshAngleTolerance")); rtabmap!.setMeshTriangleSize(value: defaults.integer(forKey: "MeshTriangleSize")); rtabmap!.setMeshDecimationFactor(value: defaults.float(forKey: "MeshDecimationFactor")); let bgColor = defaults.float(forKey: "BackgroundColor"); rtabmap!.setBackgroundColor(gray: bgColor); DispatchQueue.main.async { self.statusLabel.textColor = bgColor>=0.6 ? UIColor(white: 0.0, alpha: 1) : UIColor(white: 1.0, alpha: 1) } rtabmap!.setClusterRatio(value: defaults.float(forKey: "NoiseFilteringRatio")); rtabmap!.setMaxGainRadius(value: defaults.float(forKey: "ColorCorrectionRadius")); rtabmap!.setRenderingTextureDecimation(value: defaults.integer(forKey: "TextureResolution")); rtabmap!.setMetricSystem(defaults.integer(forKey: "MeasuringUnits") == 0); rtabmap!.setMeasuringTextSize(defaults.float(forKey: "MeasuringTextSize")); if(locationManager != nil && !defaults.bool(forKey: "SaveGPS")) { locationManager?.stopUpdatingLocation() locationManager = nil mLastKnownLocation = nil } else if(locationManager == nil && defaults.bool(forKey: "SaveGPS")) { locationManager = CLLocationManager() locationManager?.desiredAccuracy = kCLLocationAccuracyBestForNavigation locationManager?.delegate = self } } func resumeScan() { if(mState == State.STATE_VISUALIZING) { closeVisualization() rtabmap!.postExportation(visualize: false) } if(!mDataRecording) { let alertController = UIAlertController(title: "Append Mode", message: "The camera preview will not be aligned to map on start, move to a previously scanned area, then push Record. When a loop closure is detected, new scans will be appended to map.", preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { (action) in } alertController.addAction(okAction) present(alertController, animated: true) } setGLCamera(type: 0); startCamera(); } func newScan(dataRecordingMode: Bool = false) { print("databases.size() = \(databases.size())") if(databases.count >= 10 && !mReviewRequested && self.depthSupported) { SKStoreReviewController.requestReviewInCurrentScene() mReviewRequested = true } if(mState == State.STATE_VISUALIZING) { closeVisualization() } mMapNodes = 0; self.openedDatabasePath = nil let tmpDatabase = self.getDocumentDirectory().appendingPathComponent(self.RTABMAP_TMP_DB) if(!(self.mState == State.STATE_CAMERA || self.mState == State.STATE_MAPPING) && FileManager.default.fileExists(atPath: tmpDatabase.path) && tmpDatabase.fileSize > 1024*1024) // > 1MB { dismiss(animated: true, completion: { let msg = "The previous session (\(tmpDatabase.fileSizeString)) was not correctly saved, do you want to recover it?" let alert = UIAlertController(title: "Recovery", message: msg, preferredStyle: .alert) let alertActionNo = UIAlertAction(title: "Ignore", style: .destructive) { (UIAlertAction) -> Void in do { try FileManager.default.removeItem(at: tmpDatabase) } catch { print("Could not clear tmp database: \(error)") } self.newScan(dataRecordingMode: dataRecordingMode) } alert.addAction(alertActionNo) let alertActionCancel = UIAlertAction(title: "Cancel", style: .cancel) { (UIAlertAction) -> Void in // do nothing } alert.addAction(alertActionCancel) let alertActionYes = UIAlertAction(title: "Yes", style: .default) { (UIAlertAction2) -> Void in let fileName = Date().getFormattedDate(format: "yyMMdd-HHmmss") + ".db" let outputDbPath = self.getDocumentDirectory().appendingPathComponent(fileName).path var indicator: UIActivityIndicatorView? let alertView = UIAlertController(title: "Recovering", message: "Please wait while recovering data...", preferredStyle: .alert) let alertViewActionCancel = UIAlertAction(title: "Cancel", style: .cancel) { (UIAlertAction) -> Void in self.dismiss(animated: true, completion: { self.progressView = nil indicator = UIActivityIndicatorView(style: .large) indicator?.frame = CGRect(x: 0.0, y: 0.0, width: 60.0, height: 60.0) indicator?.center = self.view.center self.view.addSubview(indicator!) indicator?.bringSubviewToFront(self.view) indicator?.startAnimating() self.rtabmap!.cancelProcessing(); }) } alertView.addAction(alertViewActionCancel) let previousState = self.mState self.updateState(state: .STATE_PROCESSING); self.present(alertView, animated: true, completion: { // Add your progressbar after alert is shown (and measured) let margin:CGFloat = 8.0 let rect = CGRect(x: margin, y: 84.0, width: alertView.view.frame.width - margin * 2.0 , height: 2.0) self.progressView = UIProgressView(frame: rect) self.progressView!.progress = 0 self.progressView!.tintColor = self.view.tintColor alertView.view.addSubview(self.progressView!) var success : Bool = false DispatchQueue.background(background: { success = self.rtabmap!.recover(from: tmpDatabase.path, to: outputDbPath) }, completion:{ if(indicator != nil) { indicator!.stopAnimating() indicator!.removeFromSuperview() } if self.progressView != nil { self.dismiss(animated: self.openedDatabasePath == nil, completion: { if(success) { let alertSaved = UIAlertController(title: "Database saved!", message: String(format: "Database \"%@\" successfully recovered!", fileName), preferredStyle: .alert) let yes = UIAlertAction(title: "OK", style: .default) { (UIAlertAction) -> Void in self.openDatabase(fileUrl: URL(fileURLWithPath: outputDbPath)) } alertSaved.addAction(yes) self.present(alertSaved, animated: true, completion: nil) } else { self.updateState(state: previousState); self.showToast(message: "Recovery failed!", seconds: 4) } }) } else { self.showToast(message: "Recovery canceled", seconds: 2) self.updateState(state: previousState); } }) }) } alert.addAction(alertActionYes) self.present(alert, animated: true, completion: nil) }) } else { let inMemory = UserDefaults.standard.bool(forKey: "DatabaseInMemory") && !dataRecordingMode mDataRecording = dataRecordingMode self.rtabmap!.setDataRecorderMode(enabled: dataRecordingMode) self.optimizedGraphShown = true // Always reset to true when opening a database self.rtabmap!.openDatabase(databasePath: tmpDatabase.path, databaseInMemory: inMemory, optimize: false, clearDatabase: true) if(!(self.mState == State.STATE_CAMERA || self.mState == State.STATE_MAPPING)) { if(mDataRecording) { let alertController = UIAlertController(title: "Data Recording Mode", message: "This mode should be only used if you want to record raw ARKit data as long as possible without any feedback: loop closure detection and map rendering are disabled. The database size in Debug display shows how much data has been recorded so far.", preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { (action) in } alertController.addAction(okAction) present(alertController, animated: true) self.debugShown = true } self.setGLCamera(type: 0); self.startCamera(); } } } func save() { //Step : 1 let alert = UIAlertController(title: "Save Scan", message: "RTAB-Map Database Name (*.db):", preferredStyle: .alert ) //Step : 2 let save = UIAlertAction(title: "Save", style: .default) { (alertAction) in let textField = alert.textFields![0] as UITextField if textField.text != "" { //Read TextFields text data let fileName = textField.text!+".db" let filePath = self.getDocumentDirectory().appendingPathComponent(fileName).path if FileManager.default.fileExists(atPath: filePath) { let alert = UIAlertController(title: "File Already Exists", message: "Do you want to overwrite the existing file?", preferredStyle: .alert) let yes = UIAlertAction(title: "Yes", style: .default) { (UIAlertAction) -> Void in self.saveDatabase(fileName: fileName); } alert.addAction(yes) let no = UIAlertAction(title: "No", style: .cancel) { (UIAlertAction) -> Void in if(self.mDataRecording) { self.save() // We cannot skip saving after data recording } } alert.addAction(no) self.present(alert, animated: true, completion: nil) } else { self.saveDatabase(fileName: fileName); } } else { self.save() } } //Step : 3 var placeholder = Date().getFormattedDate(format: "yyMMdd-HHmmss") if(mDataRecording) { placeholder += "-recording" } if self.openedDatabasePath != nil && !self.openedDatabasePath!.path.isEmpty { var components = self.openedDatabasePath!.lastPathComponent.components(separatedBy: ".") if components.count > 1 { // If there is a file extension components.removeLast() placeholder = components.joined(separator: ".") } else { placeholder = self.openedDatabasePath!.lastPathComponent } } alert.addTextField { (textField) in textField.text = placeholder } //Step : 4 alert.addAction(save) //Cancel action if(!mDataRecording) { alert.addAction(UIAlertAction(title: "Cancel", style: .default) { (alertAction) in }) } self.present(alert, animated: true) { alert.textFields?.first?.selectAll(nil) } } func saveDatabase(fileName: String) { let filePath = self.getDocumentDirectory().appendingPathComponent(fileName).path let indicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .large) indicator.frame = CGRect(x: 0.0, y: 0.0, width: 60.0, height: 60.0) indicator.center = view.center view.addSubview(indicator) indicator.bringSubviewToFront(view) indicator.startAnimating() let previousState = mState; updateState(state: .STATE_PROCESSING); DispatchQueue.background(background: { self.rtabmap?.save(databasePath: filePath); // save }, completion:{ // main thread indicator.stopAnimating() indicator.removeFromSuperview() self.openedDatabasePath = URL(fileURLWithPath: filePath) let alert = UIAlertController(title: "Database saved!", message: String(format: "Database \"%@\" successfully saved!", fileName), preferredStyle: .alert) let yes = UIAlertAction(title: "OK", style: .default) { (UIAlertAction) -> Void in } alert.addAction(yes) self.present(alert, animated: true, completion: nil) do { let tmpDatabase = self.getDocumentDirectory().appendingPathComponent(self.RTABMAP_TMP_DB) try FileManager.default.removeItem(at: tmpDatabase) } catch { print("Could not clear tmp database: \(error)") } self.updateDatabases() self.updateState(state: self.mDataRecording ? .STATE_WELCOME : previousState) }) } private func export(isOBJ: Bool, meshing: Bool, regenerateCloud: Bool, optimized: Bool, optimizedMaxPolygons: Int, previousState: State) { let defaults = UserDefaults.standard let cloudVoxelSize = defaults.float(forKey: "VoxelSize") let textureSize = isOBJ ? defaults.integer(forKey: "TextureSize") : 0 let textureCount = defaults.integer(forKey: "MaximumOutputTextures") let normalK = defaults.integer(forKey: "NormalK") let maxTextureDistance = defaults.float(forKey: "MaxTextureDistance") let minTextureClusterSize = defaults.integer(forKey: "MinTextureClusterSize") let optimizedVoxelSize = cloudVoxelSize let optimizedDepth = defaults.integer(forKey: "ReconstructionDepth") let optimizedColorRadius = defaults.float(forKey: "ColorRadius") let optimizedCleanWhitePolygons = defaults.bool(forKey: "CleanMesh") let optimizedMinClusterSize = defaults.integer(forKey: "PolygonFiltering") let textureVertexColorPolicy = defaults.integer(forKey: "TextureVertexColorPolicy") let blockRendering = false var indicator: UIActivityIndicatorView? let alertView = UIAlertController(title: "Assembling", message: "Please wait while assembling data...", preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in self.dismiss(animated: true, completion: { self.progressView = nil indicator = UIActivityIndicatorView(style: .large) indicator?.frame = CGRect(x: 0.0, y: 0.0, width: 60.0, height: 60.0) indicator?.center = self.view.center self.view.addSubview(indicator!) indicator?.bringSubviewToFront(self.view) indicator?.startAnimating() self.rtabmap!.cancelProcessing() }) })) let previousState = mState updateState(state: .STATE_PROCESSING); present(alertView, animated: true, completion: { // Add your progressbar after alert is shown (and measured) let margin:CGFloat = 8.0 let rect = CGRect(x: margin, y: 84.0, width: alertView.view.frame.width - margin * 2.0 , height: 2.0) self.progressView = UIProgressView(frame: rect) self.progressView!.progress = 0 self.progressView!.tintColor = self.view.tintColor alertView.view.addSubview(self.progressView!) self.progressStatusUpdate() // This will update memory usage during post processing var success : Bool = false DispatchQueue.background(background: { success = self.rtabmap!.exportMesh( cloudVoxelSize: cloudVoxelSize, regenerateCloud: regenerateCloud, meshing: meshing, textureSize: textureSize, textureCount: textureCount, normalK: normalK, optimized: optimized, optimizedVoxelSize: optimizedVoxelSize, optimizedDepth: optimizedDepth, optimizedMaxPolygons: optimizedMaxPolygons, optimizedColorRadius: optimizedColorRadius, optimizedCleanWhitePolygons: optimizedCleanWhitePolygons, optimizedMinClusterSize: optimizedMinClusterSize, optimizedMaxTextureDistance: maxTextureDistance, optimizedMinTextureClusterSize: minTextureClusterSize, textureVertexColorPolicy: textureVertexColorPolicy, blockRendering: blockRendering) }, completion:{ if(indicator != nil) { indicator!.stopAnimating() indicator!.removeFromSuperview() } if self.progressView != nil { self.dismiss(animated: self.openedDatabasePath == nil, completion: { if(success) { if(!meshing && cloudVoxelSize>0.0) { self.showToast(message: "Cloud assembled and voxelized at \(cloudVoxelSize) m.", seconds: 2) } if(!meshing) { self.visualizationType = 0; self.setMeshRendering(viewMode: 0) } else if(!isOBJ) { self.visualizationType = 1; self.setMeshRendering(viewMode: 1) } else // isOBJ { self.visualizationType = 2; self.setMeshRendering(viewMode: 2) } self.updateState(state: .STATE_VISUALIZING) self.rtabmap!.postExportation(visualize: true) if previousState != .STATE_VISUALIZING { self.setGLCamera(type: 2) } if self.openedDatabasePath == nil { self.save(); } } else { self.updateState(state: previousState); self.showToast(message: "Exporting map failed!", seconds: 4) } }) } else { self.showToast(message: "Export canceled", seconds: 2) self.updateState(state: previousState); } }) }) } private func optimization(withStandardMeshExport: Bool = false, approach: Int) { if(mState == State.STATE_VISUALIZING) { closeVisualization() rtabmap!.postExportation(visualize: false) } let alertView = UIAlertController(title: "Post-Processing", message: "Please wait while optimizing...", preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in self.dismiss(animated: true) self.progressView = nil self.rtabmap!.cancelProcessing() })) let previousState = mState updateState(state: .STATE_PROCESSING) // Show it to your users present(alertView, animated: true, completion: { // Add your progressbar after alert is shown (and measured) let margin:CGFloat = 8.0 let rect = CGRect(x: margin, y: 72.0, width: alertView.view.frame.width - margin * 2.0 , height: 2.0) self.progressView = UIProgressView(frame: rect) self.progressView!.progress = 0 self.progressView!.tintColor = self.view.tintColor alertView.view.addSubview(self.progressView!) self.progressStatusUpdate() // This will update memory usage during post processing var loopDetected : Int = -1 DispatchQueue.background(background: { loopDetected = self.rtabmap!.postProcessing(approach: approach); }, completion:{ // main thread if self.progressView != nil { self.dismiss(animated: self.openedDatabasePath == nil, completion: { self.progressView = nil if(loopDetected >= 0) { if(approach == -1) { if(withStandardMeshExport) { self.export(isOBJ: true, meshing: true, regenerateCloud: false, optimized: true, optimizedMaxPolygons: 200000, previousState: previousState); } else { if self.openedDatabasePath == nil { self.save(); } } } } else if(loopDetected < 0) { self.showToast(message: "Optimization failed!", seconds: 4.0) } }) } else { self.showToast(message: "Optimization canceled", seconds: 4.0) } self.updateState(state: .STATE_IDLE); }) }) } func stopMapping(ignoreSaving: Bool, offerPostProcessing: Bool = true) { session.pause() locationManager?.stopUpdatingLocation() rtabmap?.setPausedMapping(paused: true) rtabmap?.stopCamera() if(mDataRecording) { // this will show the trajectory before saving self.rtabmap!.setGraphOptimization(enabled: false) } setGLCamera(type: 2) if(mState == .STATE_VISUALIZING_CAMERA) { self.rtabmap?.setLocalizationMode(enabled: false) } updateState(state: mState == .STATE_VISUALIZING_CAMERA || mState == .STATE_VISUALIZING_AND_MEASURING ? .STATE_VISUALIZING : .STATE_IDLE); if !ignoreSaving { if(mDataRecording || !offerPostProcessing) { // Go directly to save self.save() } else { dismiss(animated: true, completion: { var msg = "Do you want to do standard graph optimization and make a nice assembled mesh now? This can be also done later using \"Optimize\" and \"Assemble\" menus." let depthUsed = self.depthSupported && UserDefaults.standard.bool(forKey: "LidarMode") if !depthUsed { msg = "Do you want to do standard graph optimization now? This can be also done later using \"Optimize\" menu." } let alert = UIAlertController(title: "Mapping Stopped! Optimize Now?", message: msg, preferredStyle: .alert) if depthUsed { let alertActionOnlyGraph = UIAlertAction(title: "Only Optimize", style: .default) { (UIAlertAction) -> Void in self.optimization(withStandardMeshExport: false, approach: -1) } alert.addAction(alertActionOnlyGraph) } let alertActionNo = UIAlertAction(title: "Save First", style: .cancel) { (UIAlertAction) -> Void in self.save() } alert.addAction(alertActionNo) let alertActionYes = UIAlertAction(title: "Yes", style: .default) { (UIAlertAction) -> Void in self.optimization(withStandardMeshExport: depthUsed, approach: -1) } alert.addAction(alertActionYes) self.present(alert, animated: true, completion: nil) }) } } else if(mMapNodes == 0) { updateState(state: State.STATE_WELCOME); statusLabel.text = "" } } func shareFile(_ fileUrl: URL) { let fileURL = NSURL(fileURLWithPath: fileUrl.path) // Create the Array which includes the files you want to share var filesToShare = [Any]() // Add the path of the file to the Array filesToShare.append(fileURL) // Make the activityViewContoller which shows the share-view let activityViewController = UIActivityViewController(activityItems: filesToShare, applicationActivities: nil) if let popoverController = activityViewController.popoverPresentationController { popoverController.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 2, width: 0, height: 0) popoverController.sourceView = self.view popoverController.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0) } // Show the share-view self.present(activityViewController, animated: true, completion: nil) } func openDatabase(fileUrl: URL) { if(mState == .STATE_CAMERA) { stopMapping(ignoreSaving: true) } openedDatabasePath = fileUrl; let fileName: String = self.openedDatabasePath!.lastPathComponent var progressDialog = UIAlertController(title: "Loading", message: String(format: "Loading \"%@\". Please wait while point clouds and/or meshes are created...", fileName), preferredStyle: .alert) // Show it to your users self.present(progressDialog, animated: true) updateState(state: .STATE_PROCESSING); var status = 0 DispatchQueue.background(background: { self.optimizedGraphShown = true // Always reset to true when opening a database status = self.rtabmap!.openDatabase(databasePath: self.openedDatabasePath!.path, databaseInMemory: true, optimize: false, clearDatabase: false) }, completion:{ // main thread if(status == -1) { self.dismiss(animated: true) self.showToast(message: "The map is loaded but optimization of the map's graph has failed, so the map cannot be shown. Change the Graph Optimizer approach used or enable/disable if the graph is optimized from graph end in \"Settings -> Mapping...\" and try opening again.", seconds: 4) } else if(status == -2) { self.dismiss(animated: true) self.showToast(message: "Failed to open database: Out of memory! Try again after lowering Point Cloud Density in Settings.", seconds: 4) } else { if(status >= 1 && status<=3) { self.visualizationType = status-1; self.updateState(state: .STATE_VISUALIZING); self.resetNoTouchTimer(true); } else { self.setGLCamera(type: 2); self.updateState(state: .STATE_IDLE); self.dismiss(animated: true) self.showToast(message: "Database loaded!", seconds: 2) } } }) } func closeVisualization() { updateState(state: .STATE_IDLE); } func rename(fileURL: URL) { //Step : 1 let alert = UIAlertController(title: "Rename Scan", message: "RTAB-Map Database Name (*.db):", preferredStyle: .alert ) //Step : 2 let rename = UIAlertAction(title: "Rename", style: .default) { (alertAction) in let textField = alert.textFields![0] as UITextField if textField.text != "" { //Read TextFields text data let fileName = textField.text!+".db" let filePath = self.getDocumentDirectory().appendingPathComponent(fileName).path if FileManager.default.fileExists(atPath: filePath) { let alert = UIAlertController(title: "File Already Exists", message: "Do you want to overwrite the existing file?", preferredStyle: .alert) let yes = UIAlertAction(title: "Yes", style: .default) { (UIAlertAction) -> Void in do { try FileManager.default.moveItem(at: fileURL, to: URL(fileURLWithPath: filePath)) print("File \(fileURL) renamed to \(filePath)") } catch { print("Error renaming file \(fileURL) to \(filePath)") } self.openLibrary() } alert.addAction(yes) let no = UIAlertAction(title: "No", style: .cancel) { (UIAlertAction) -> Void in } alert.addAction(no) self.present(alert, animated: true, completion: nil) } else { do { try FileManager.default.moveItem(at: fileURL, to: URL(fileURLWithPath: filePath)) print("File \(fileURL) renamed to \(filePath)") } catch { print("Error renaming file \(fileURL) to \(filePath)") } self.openLibrary() } } } //Step : 3 alert.addTextField { (textField) in var components = fileURL.lastPathComponent.components(separatedBy: ".") if components.count > 1 { // If there is a file extension components.removeLast() textField.text = components.joined(separator: ".") } else { textField.text = fileURL.lastPathComponent } } //Step : 4 alert.addAction(rename) //Cancel action alert.addAction(UIAlertAction(title: "Cancel", style: .default) { (alertAction) in }) self.present(alert, animated: true) { alert.textFields?.first?.selectAll(nil) } } func exportOBJPLY() { //Step : 1 let alert = UIAlertController(title: "Export Scan", message: "Model Name:", preferredStyle: .alert ) //Step : 2 let save = UIAlertAction(title: "Ok", style: .default) { (alertAction) in let textField = alert.textFields![0] as UITextField if textField.text != "" { self.dismiss(animated: true) //Read TextFields text data let fileName = textField.text! + (self.exportOBJPLYButton.title(for: .normal)!.contains("LAZ") ? ".laz" : ".zip") let filePath = self.getDocumentDirectory().appendingPathComponent(fileName).path if FileManager.default.fileExists(atPath: filePath) { let alert = UIAlertController(title: "File Already Exists", message: "\(fileName) already exists, do you want to overwrite it?", preferredStyle: .alert) let yes = UIAlertAction(title: "Yes", style: .default) { (UIAlertAction) -> Void in self.writeExportedFiles(fileName: textField.text!); } alert.addAction(yes) let no = UIAlertAction(title: "No", style: .cancel) { (UIAlertAction) -> Void in } alert.addAction(no) self.present(alert, animated: true, completion: nil) } else { self.writeExportedFiles(fileName: textField.text!); } } } //Step : 3 alert.addTextField { (textField) in if self.openedDatabasePath != nil && !self.openedDatabasePath!.path.isEmpty { var components = self.openedDatabasePath!.lastPathComponent.components(separatedBy: ".") if components.count > 1 { // If there is a file extension components.removeLast() textField.text = components.joined(separator: ".") } else { textField.text = self.openedDatabasePath!.lastPathComponent } } else { textField.text = Date().getFormattedDate(format: "yyMMdd-HHmmss") } } //Step : 4 alert.addAction(save) //Cancel action alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { (alertAction) in }) self.present(alert, animated: true) { alert.textFields?.first?.selectAll(nil) } } func writeExportedFiles(fileName: String) { let isLAZ = self.visualizationType==0 && self.exportOBJPLYButton.title(for: .normal)!.contains("LAZ") let alertView = UIAlertController(title: "Exporting", message: "Please wait while exporting data to \(fileName+(isLAZ ? ".laz" : ".zip"))...", preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in self.dismiss(animated: true) self.progressView = nil self.rtabmap!.cancelProcessing() })) let previousState = mState; updateState(state: .STATE_PROCESSING); present(alertView, animated: true, completion: { // Add your progressbar after alert is shown (and measured) let margin:CGFloat = 8.0 let rect = CGRect(x: margin, y: 84.0, width: alertView.view.frame.width - margin * 2.0 , height: 2.0) self.progressView = UIProgressView(frame: rect) self.progressView!.progress = 0 self.progressView!.tintColor = self.view.tintColor alertView.view.addSubview(self.progressView!) let exportDir = self.getTmpDirectory().appendingPathComponent(self.RTABMAP_EXPORT_DIR) do { try FileManager.default.removeItem(at: exportDir) } catch {} do { try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true) } catch { print("Failed adding export directory \(exportDir)") return } var success : Bool = false var zipFileUrl : URL! DispatchQueue.background(background: { print("Exporting to directory \(exportDir.path) with name \(fileName)") if(self.rtabmap!.writeExportedMesh(directory: exportDir.path, name: fileName)) { do { let fileURLs = try FileManager.default.contentsOfDirectory(at: exportDir, includingPropertiesForKeys: nil) if(!fileURLs.isEmpty) { if(isLAZ) { zipFileUrl = self.getDocumentDirectory().appendingPathComponent(fileName+".laz") do { if FileManager.default.fileExists(atPath: zipFileUrl.path) { try FileManager.default.removeItem(at: zipFileUrl) } try FileManager.default.moveItem(at: fileURLs.first!, to: zipFileUrl) print("LAZ file \(zipFileUrl.path) created (size=\(zipFileUrl.fileSizeString)") success = true } catch { print("Failed moving \(fileURLs.first!) to \(zipFileUrl.path)") return } } else { do { zipFileUrl = try Zip.quickZipFiles(fileURLs, fileName: fileName) // Zip print("Zip file \(zipFileUrl.path) created (size=\(zipFileUrl.fileSizeString)") success = true } catch { print("Something went wrong while zipping") } } } } catch { print("No files exported to \(exportDir)") return } } }, completion:{ if self.progressView != nil { self.dismiss(animated: true) } if(success) { let alertShare = UIAlertController(title: "Mesh/Cloud Saved!", message: "\(fileName+(isLAZ ? ".laz" : ".zip")) (\(zipFileUrl.fileSizeString) successfully exported in Documents of RTAB-Map! Share it?", preferredStyle: .alert) let alertActionYes = UIAlertAction(title: "Yes", style: .default) { (UIAlertAction) -> Void in self.shareFile(zipFileUrl) } alertShare.addAction(alertActionYes) let alertActionNo = UIAlertAction(title: "No", style: .cancel) { (UIAlertAction) -> Void in } alertShare.addAction(alertActionNo) self.present(alertShare, animated: true, completion: nil) } else { self.showToast(message: "Exporting mesh/cloud canceled!", seconds: 2) } self.updateState(state: previousState); }) }) } func updateDatabases() { databases.removeAll() do { let fileURLs = try FileManager.default.contentsOfDirectory(at: getDocumentDirectory(), includingPropertiesForKeys: nil) // if you want to filter the directory contents you can do like this: let data = fileURLs.map { url in (url, (try? url.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? Date.distantPast) } .sorted(by: { $0.1 > $1.1 }) // sort descending modification dates .map { $0.0 } // extract file names databases = data.filter{ $0.pathExtension == "db" && $0.lastPathComponent != RTABMAP_TMP_DB && $0.lastPathComponent != RTABMAP_RECOVERY_DB } } catch { print("Error while enumerating files : \(error.localizedDescription)") return } } func openLibrary() { updateDatabases(); if databases.isEmpty { return } let alertController = UIAlertController(title: "Library", message: nil, preferredStyle: .alert) let customView = VerticalScrollerView() customView.dataSource = self customView.delegate = self customView.reload() alertController.view.addSubview(customView) customView.translatesAutoresizingMaskIntoConstraints = false customView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 60).isActive = true customView.rightAnchor.constraint(equalTo: alertController.view.rightAnchor, constant: -10).isActive = true customView.leftAnchor.constraint(equalTo: alertController.view.leftAnchor, constant: 10).isActive = true customView.bottomAnchor.constraint(equalTo: alertController.view.bottomAnchor, constant: -45).isActive = true alertController.view.translatesAutoresizingMaskIntoConstraints = false alertController.view.heightAnchor.constraint(equalToConstant: 600).isActive = true alertController.view.widthAnchor.constraint(equalToConstant: 400).isActive = true customView.backgroundColor = .darkGray let selectAction = UIAlertAction(title: "Select", style: .default) { (action) in self.openDatabase(fileUrl: self.databases[self.currentDatabaseIndex]) } let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(selectAction) alertController.addAction(cancelAction) self.present(alertController, animated: true, completion: nil) } //MARK: Actions @IBAction func stopAction(_ sender: UIButton) { stopMapping(ignoreSaving: false) } @IBAction func recordAction(_ sender: UIButton) { rtabmap?.setPausedMapping(paused: false); lowMemoryWarningShown = false updateState(state: .STATE_MAPPING) } @IBAction func newScanAction(_ sender: UIButton) { newScan() } @IBAction func closeVisualizationAction(_ sender: UIButton) { closeVisualization() rtabmap!.postExportation(visualize: false) } @IBAction func stopCameraAction(_ sender: UIButton) { appMovedToBackground(); } @IBAction func exportOBJPLYAction(_ sender: UIButton) { exportOBJPLY() } @IBAction func libraryAction(_ sender: UIButton) { openLibrary(); } @IBAction func rotateGridAction(_ sender: UISlider) { rtabmap!.setGridRotation((Float(sender.value)-90.0)/2.0) self.view.setNeedsDisplay() } @IBAction func clipDistanceAction(_ sender: UISlider) { rtabmap!.setOrthoCropFactor(Float(120-sender.value)/20.0 - 3.0) self.view.setNeedsDisplay() } @IBAction func removeMeasureAction(_ sender: UIButton) { self.rtabmap!.removeMeasure() } @IBAction func addMeasureAction(_ sender: UIButton) { self.rtabmap!.addMeasureButtonClicked() } @IBAction func teleportButtonAction(_ sender: UIButton) { self.rtabmap!.teleportButtonClicked() } func clearMeasures() { self.rtabmap!.clearMeasures() self.resetNoTouchTimer(true) } @IBAction func startMeasuring(_ sender: UIButton) { self.setGLCamera(type: 0) self.updateState(state: .STATE_VISUALIZING_AND_MEASURING) self.startCamera() } } func clearBackgroundColor(of view: UIView) { if let effectsView = view as? UIVisualEffectView { effectsView.removeFromSuperview() return } view.backgroundColor = .clear view.subviews.forEach { (subview) in clearBackgroundColor(of: subview) } } extension ViewController: GLKViewControllerDelegate { // OPENGL UPDATE func glkViewControllerUpdate(_ controller: GLKViewController) { } // OPENGL DRAW override func glkView(_ view: GLKView, drawIn rect: CGRect) { if let rotation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation { let viewportSize = CGSize(width: rect.size.width * view.contentScaleFactor, height: rect.size.height * view.contentScaleFactor) rtabmap?.setupGraphic(size: viewportSize, orientation: rotation) } let value = rtabmap?.render() DispatchQueue.main.async { if(value != 0 && self.progressView != nil) { print("Render dismissing") self.dismiss(animated: true) self.progressView = nil } if(value == -1) { self.showToast(message: "Out of Memory!", seconds: 2) } else if(value == -2) { self.showToast(message: "Rendering Error!", seconds: 2) } } } } extension Date { func getFormattedDate(format: String) -> String { let dateformat = DateFormatter() dateformat.dateFormat = format return dateformat.string(from: self) } var millisecondsSince1970:Int64 { Int64((self.timeIntervalSince1970 * 1000.0).rounded()) } init(milliseconds:Int64) { self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000) } } extension DispatchQueue { static func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) { DispatchQueue.global(qos: .userInitiated).async { background?() if let completion = completion { DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { completion() }) } } } } extension ViewController: VerticalScrollerViewDelegate { func verticalScrollerView(_ horizontalScrollerView: VerticalScrollerView, didSelectViewAt index: Int) { //1 let previousDatabaseView = horizontalScrollerView.view(at: currentDatabaseIndex) as! DatabaseView previousDatabaseView.highlightDatabase(false) //2 currentDatabaseIndex = index //3 let databaseView = horizontalScrollerView.view(at: currentDatabaseIndex) as! DatabaseView databaseView.highlightDatabase(true) //4 } } extension ViewController: VerticalViewDataSource { func numberOfViews(in horizontalScrollerView: VerticalScrollerView) -> Int { return databases.count } func getScrollerViewItem(_ horizontalScrollerView: VerticalScrollerView, viewAt index: Int) -> UIView { print(databases[index].path) let databaseView = DatabaseView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), databaseURL: databases[index]) databaseView.delegate = self if currentDatabaseIndex == index { databaseView.highlightDatabase(true) } else { databaseView.highlightDatabase(false) } return databaseView } } extension ViewController: DatabaseViewDelegate { func databaseShared(databaseURL: URL) { self.dismiss(animated: true) self.shareFile(databaseURL) } func databaseRenamed(databaseURL: URL) { self.dismiss(animated: true) if(openedDatabasePath?.lastPathComponent == databaseURL.lastPathComponent) { let alertController = UIAlertController(title: "Rename Database", message: "Database \(databaseURL.lastPathComponent) is already opened, cannot rename it.", preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { (action) in } alertController.addAction(okAction) present(alertController, animated: true) return } self.rename(fileURL: databaseURL) } func databaseDeleted(databaseURL: URL) { self.dismiss(animated: true) if(openedDatabasePath?.lastPathComponent == databaseURL.lastPathComponent) { let alertController = UIAlertController(title: "Delete Database", message: "Database \(databaseURL.lastPathComponent) is already opened, cannot delete it.", preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { (action) in } alertController.addAction(okAction) present(alertController, animated: true) return } do { try FileManager.default.removeItem(at: databaseURL) print("File \(databaseURL) deleted") } catch { print("Error deleting file \(databaseURL)") } self.updateDatabases() if(!databases.isEmpty) { self.openLibrary() } else { self.updateState(state: self.mState) } } } extension UserDefaults { func reset() { let defaults = UserDefaults.standard defaults.dictionaryRepresentation().keys.forEach(defaults.removeObject(forKey:)) setDefaultsFromSettingsBundle() } } extension SKStoreReviewController { public static func requestReviewInCurrentScene() { if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { requestReview(in: scene) } } }