Files
agv_pro_ros2/rtabmap/app/ios/RTABMapApp/ViewController.swift
T
2025-07-14 11:34:38 +08:00

2765 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)
{
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<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!.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)
}
}
}