Files
rtabmap/app/ios/RTABMapApp/ViewController.swift
matlabbe 28dbf1c98a iOS feb 2025 various updates (#1451)
* Using shadows on all texts and icons to see them better when background is white. Buffering database previews to show faster the Library. Updated how RAM usage is computed and now show max memory. Added warnings when RAM usage is low.

* Added measuring tool

* fixed map not shown when closing visualization

* Added measure text size setting, changed look and feel of point to point

* Fixed measuring point not showing

* Added fix for #1401

* Addressing #1407

* Export: Added colored OBJ options

* fixed index_t not existing on focal

* Fixed Settings not applied after restoring to all default settings witohut restarting the app (Marker detection not working issue #1455 )

* Marker detection: Fixing wrong depth used when rgb and depth image sizes cannot be divided exactly one from the the other #1455

* Added Marker Max Range option (default 1m)

* Added OBJ texture policy option (keep color on textureless polygons)

* Added texture/color blending option directly in the app. Added LAZ export option.

* Hiding measuring button if not mesh, dont zip if exporting to laz, updating Export XXX button based on the current context, fixed always blending texture/color on not visualization mode

* updated default marker max range to 2m

* bump ios app version

* fixing pcl 1.8.1 build

* fixed android build
2025-03-15 10:58:03 -07:00

2764 lines
122 KiB
Swift

//
// 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<Element>.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)
{
print("Show HUD")
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)
{
print("Hide HUD")
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()
print("enableSetNeedsDisplay")
}
else
{
view?.enableSetNeedsDisplay = false
self.isPaused = false
print("disableSetNeedsDisplay")
}
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
print("measuringMode = \(measuringMode)")
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<UITouch>,
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<UITouch>, 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<UITouch>, 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<UITouch>, 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!.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()
})
}))
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)
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)
}
}
}