mirror of
https://github.com/introlab/rtabmap.git
synced 2026-09-02 17:40:23 +08:00
* 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
2764 lines
122 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|