Added rtabmap::databaseRecovery() function, added rtabmap-recovery tool, added "Database recovery" menu action in DbViewer

This commit is contained in:
matlabbe
2017-10-11 19:36:00 -04:00
parent ab0aad87ec
commit d4248385f0
17 changed files with 848 additions and 97 deletions

View File

@@ -30,6 +30,7 @@ SET(headers_ui
./DepthCalibrationDialog.h
./3rdParty/QMultiComboBox.h
./TexturingState.h
./RecoveryState.h
./EditDepthArea.h
)

View File

@@ -69,6 +69,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "rtabmap/core/RegistrationIcp.h"
#include "rtabmap/core/OccupancyGrid.h"
#include "rtabmap/core/GeodeticCoords.h"
#include "rtabmap/core/Recovery.h"
#include "rtabmap/gui/DataRecorder.h"
#include "ExportCloudsDialog.h"
#include "EditDepthArea.h"
@@ -77,6 +78,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "ExportDialog.h"
#include "rtabmap/gui/ProgressDialog.h"
#include "ParametersToolBox.h"
#include "RecoveryState.h"
#include <pcl/io/pcd_io.h>
#include <pcl/io/ply_io.h>
#include <pcl/filters/voxel_grid.h>
@@ -216,10 +218,15 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) :
connect(ui_->actionQuit, SIGNAL(triggered()), this, SLOT(close()));
ui_->actionOpen_database->setEnabled(true);
ui_->actionClose_database->setEnabled(false);
// connect actions with custom slots
ui_->actionSave_config->setShortcut(QKeySequence::Save);
connect(ui_->actionSave_config, SIGNAL(triggered()), this, SLOT(writeSettings()));
connect(ui_->actionOpen_database, SIGNAL(triggered()), this, SLOT(openDatabase()));
connect(ui_->actionClose_database, SIGNAL(triggered()), this, SLOT(closeDatabase()));
connect(ui_->actionDatabase_recovery, SIGNAL(triggered()), this, SLOT(recoverDatabase()));
connect(ui_->actionExport, SIGNAL(triggered()), this, SLOT(exportDatabase()));
connect(ui_->actionExtract_images, SIGNAL(triggered()), this, SLOT(extractImages()));
connect(ui_->actionEdit_depth_image, SIGNAL(triggered()), this, SLOT(editDepthImage()));
@@ -692,51 +699,23 @@ bool DatabaseViewer::openDatabase(const QString & path)
UDEBUG("Open database \"%s\"", path.toStdString().c_str());
if(QFile::exists(path))
{
if(dbDriver_)
{
delete dbDriver_;
dbDriver_ = 0;
ids_.clear();
idToIndex_.clear();
neighborLinks_.clear();
loopLinks_.clear();
graphes_.clear();
graphLinks_.clear();
odomPoses_.clear();
groundTruthPoses_.clear();
gpsPoses_.clear();
gpsValues_.clear();
mapIds_.clear();
links_.clear();
linksAdded_.clear();
linksRefined_.clear();
linksRemoved_.clear();
localMaps_.clear();
localMapsInfo_.clear();
generatedLocalMaps_.clear();
generatedLocalMapsInfo_.clear();
ui_->graphViewer->clearAll();
occupancyGridViewer_->clear();
ui_->menuExport_poses->setEnabled(false);
ui_->menuExport_GPS->setEnabled(false);
ui_->actionPoses_KML->setEnabled(false);
ui_->checkBox_showOptimized->setEnabled(false);
ui_->toolBox_statistics->clear();
databaseFileName_.clear();
ui_->checkBox_alignPosesWithGroundTruth->setVisible(false);
ui_->label_alignPosesWithGroundTruth->setVisible(false);
}
std::string driverType = "sqlite3";
dbDriver_ = DBDriver::create();
if(!dbDriver_->openConnection(path.toStdString()))
{
ui_->actionClose_database->setEnabled(false);
ui_->actionOpen_database->setEnabled(true);
delete dbDriver_;
dbDriver_ = 0;
QMessageBox::warning(this, "Database error", tr("Can't open database \"%1\"").arg(path));
}
else
{
ui_->actionClose_database->setEnabled(true);
ui_->actionOpen_database->setEnabled(false);
pathDatabase_ = UDirectory::getDir(path.toStdString()).c_str();
databaseFileName_ = UFile::getName(path.toStdString());
ui_->graphViewer->setWorkingDirectory(pathDatabase_);
@@ -810,33 +789,8 @@ bool DatabaseViewer::openDatabase(const QString & path)
return false;
}
void DatabaseViewer::closeEvent(QCloseEvent* event)
bool DatabaseViewer::closeDatabase()
{
//write settings before quit?
bool save = false;
if(this->isWindowModified())
{
QMessageBox::Button b=QMessageBox::question(this,
tr("Database Viewer"),
tr("There are unsaved changed settings. Save them?"),
QMessageBox::Save | QMessageBox::Cancel | QMessageBox::Discard);
if(b == QMessageBox::Save)
{
save = true;
}
else if(b != QMessageBox::Discard)
{
event->ignore();
return;
}
}
if(save)
{
writeSettings();
}
event->accept();
if(dbDriver_)
{
if(linksAdded_.size() || linksRefined_.size() || linksRemoved_.size())
@@ -889,12 +843,11 @@ void DatabaseViewer::closeEvent(QCloseEvent* event)
if(button != QMessageBox::Yes && button != QMessageBox::No)
{
event->ignore();
return false;
}
}
if(event->isAccepted() &&
generatedLocalMaps_.size() &&
if( generatedLocalMaps_.size() &&
uStrNumCmp(dbDriver_->getDatabaseVersion(), "0.11.10") >= 0)
{
QMessageBox::StandardButton button = QMessageBox::question(this,
@@ -929,9 +882,161 @@ void DatabaseViewer::closeEvent(QCloseEvent* event)
if(button != QMessageBox::Yes && button != QMessageBox::No)
{
event->ignore();
return false;
}
}
delete dbDriver_;
dbDriver_ = 0;
ids_.clear();
idToIndex_.clear();
neighborLinks_.clear();
loopLinks_.clear();
graphes_.clear();
graphLinks_.clear();
odomPoses_.clear();
groundTruthPoses_.clear();
gpsPoses_.clear();
gpsValues_.clear();
mapIds_.clear();
links_.clear();
linksAdded_.clear();
linksRefined_.clear();
linksRemoved_.clear();
localMaps_.clear();
localMapsInfo_.clear();
generatedLocalMaps_.clear();
generatedLocalMapsInfo_.clear();
ui_->graphViewer->clearAll();
occupancyGridViewer_->clear();
ui_->menuExport_poses->setEnabled(false);
ui_->menuExport_GPS->setEnabled(false);
ui_->actionPoses_KML->setEnabled(false);
ui_->checkBox_showOptimized->setEnabled(false);
ui_->toolBox_statistics->clear();
databaseFileName_.clear();
ui_->checkBox_alignPosesWithGroundTruth->setVisible(false);
ui_->label_alignPosesWithGroundTruth->setVisible(false);
ui_->label_optimizeFrom->setText(tr("Optimize from"));
ui_->textEdit_info->clear();
ui_->pushButton_refine->setEnabled(false);
ui_->pushButton_add->setEnabled(false);
ui_->pushButton_reset->setEnabled(false);
ui_->pushButton_reject->setEnabled(false);
ui_->horizontalSlider_loops->setEnabled(false);
ui_->horizontalSlider_loops->setMaximum(0);
ui_->horizontalSlider_iterations->setEnabled(false);
ui_->horizontalSlider_iterations->setMaximum(0);
ui_->horizontalSlider_neighbors->setEnabled(false);
ui_->horizontalSlider_neighbors->setMaximum(0);
ui_->label_constraint->clear();
ui_->label_constraint_opt->clear();
ui_->label_variance->clear();
ui_->horizontalSlider_A->setEnabled(false);
ui_->horizontalSlider_A->setMaximum(0);
ui_->horizontalSlider_B->setEnabled(false);
ui_->horizontalSlider_B->setMaximum(0);
ui_->label_idA->setText("NaN");
ui_->label_idB->setText("NaN");
sliderAValueChanged(0);
sliderBValueChanged(0);
constraintsViewer_->clear();
constraintsViewer_->update();
cloudViewer_->clear();
cloudViewer_->update();
occupancyGridViewer_->clear();
occupancyGridViewer_->update();
ui_->graphViewer->clearAll();
ui_->label_loopClosures->clear();
ui_->label_timeOptimization->clear();
ui_->label_pathLength->clear();
ui_->label_poses->clear();
ui_->spinBox_optimizationsFrom->setEnabled(false);
ui_->graphicsView_A->clear();
ui_->graphicsView_B->clear();
ui_->graphicsView_stereo->clear();
stereoViewer_->clear();
stereoViewer_->update();
ui_->toolBox_statistics->clear();
}
ui_->actionClose_database->setEnabled(dbDriver_ != 0);
ui_->actionOpen_database->setEnabled(dbDriver_ == 0);
return dbDriver_ == 0;
}
void DatabaseViewer::recoverDatabase()
{
QString path = QFileDialog::getOpenFileName(this, tr("Select file"), pathDatabase_, tr("Databases (*.db)"));
if(!path.isEmpty())
{
if(path.compare(pathDatabase_+QDir::separator()+databaseFileName_.c_str()) == 0)
{
QMessageBox::information(this, "Database recovery", tr("The selected database is already opened, close it first."));
return;
}
std::string errorMsg;
rtabmap::ProgressDialog * progressDialog = new rtabmap::ProgressDialog(this);
progressDialog->setAttribute(Qt::WA_DeleteOnClose);
progressDialog->setMaximumSteps(100);
progressDialog->show();
progressDialog->setCancelButtonVisible(true);
RecoveryState state(progressDialog);
if(databaseRecovery(path.toStdString(), false, &errorMsg, &state))
{
QMessageBox::information(this, "Database recovery", tr("Database \"%1\" recovered! Try opening it again.").arg(path));
}
else
{
QMessageBox::warning(this, "Database recovery", tr("Database recovery failed: \"%1\".").arg(errorMsg.c_str()));
}
progressDialog->setValue(progressDialog->maximumSteps());
}
}
void DatabaseViewer::closeEvent(QCloseEvent* event)
{
//write settings before quit?
bool save = false;
if(this->isWindowModified())
{
QMessageBox::Button b=QMessageBox::question(this,
tr("Database Viewer"),
tr("There are unsaved changed settings. Save them?"),
QMessageBox::Save | QMessageBox::Cancel | QMessageBox::Discard);
if(b == QMessageBox::Save)
{
save = true;
}
else if(b != QMessageBox::Discard)
{
event->ignore();
return;
}
}
if(save)
{
writeSettings();
}
event->accept();
if(!closeDatabase())
{
event->ignore();
}
if(event->isAccepted())
@@ -3139,7 +3244,7 @@ void DatabaseViewer::update(int value,
labelMapId->setText(QString::number(mapId));
}
}
else
else if(value != 0)
{
ULOGGER_ERROR("Slider index out of range ?");
}
@@ -3537,12 +3642,18 @@ void DatabaseViewer::update3dView()
void DatabaseViewer::sliderNeighborValueChanged(int value)
{
this->updateConstraintView(neighborLinks_.at(value));
if(value < neighborLinks_.size())
{
this->updateConstraintView(neighborLinks_.at(value));
}
}
void DatabaseViewer::sliderLoopValueChanged(int value)
{
this->updateConstraintView(loopLinks_.at(value));
if(value < loopLinks_.size())
{
this->updateConstraintView(loopLinks_.at(value));
}
}
// only called when ui_->checkBox_showOptimized state changed

View File

@@ -42,6 +42,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "rtabmap/core/RegistrationVis.h"
#include "rtabmap/core/OccupancyGrid.h"
#include "rtabmap/core/GainCompensator.h"
#include "rtabmap/core/Recovery.h"
#include "rtabmap/gui/ImageView.h"
#include "rtabmap/gui/KeypointItem.h"
@@ -66,6 +67,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "AboutDialog.h"
#include "PostProcessingDialog.h"
#include "DepthCalibrationDialog.h"
#include "RecoveryState.h"
#include <QtGui/QCloseEvent>
#include <QtGui/QPixmap>
@@ -143,6 +145,7 @@ MainWindow::MainWindow(PreferencesDialog * prefDialog, QWidget * parent) :
_firstStamp(0.0f),
_processingStatistics(false),
_processingDownloadedMap(false),
_recovering(false),
_odometryReceived(false),
_newDatabasePath(""),
_newDatabasePathOutput(""),
@@ -776,8 +779,11 @@ bool MainWindow::handleEvent(UEvent* anEvent)
}
else if(anEvent->getClassName().compare("RtabmapEventInit") == 0)
{
RtabmapEventInit * rtabmapEventInit = (RtabmapEventInit*)anEvent;
emit rtabmapEventInitReceived((int)rtabmapEventInit->getStatus(), rtabmapEventInit->getInfo().c_str());
if(!_recovering)
{
RtabmapEventInit * rtabmapEventInit = (RtabmapEventInit*)anEvent;
emit rtabmapEventInitReceived((int)rtabmapEventInit->getStatus(), rtabmapEventInit->getInfo().c_str());
}
}
else if(anEvent->getClassName().compare("RtabmapEvent3DMap") == 0)
{
@@ -4224,10 +4230,10 @@ void MainWindow::newDatabase()
tr("Cannot create a new database because the temporary database \"%1\" already exists. "
"There may be another instance of RTAB-Map running with the same Working Directory or "
"the last time RTAB-Map was not closed correctly. "
"Do you want to continue (the database will be deleted to create the new one)?").arg(databasePath.c_str()),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
"Do you want to recover the database (click Ignore to delete it and create a new one)?").arg(databasePath.c_str()),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Ignore, QMessageBox::No);
if(r == QMessageBox::Yes)
if(r == QMessageBox::Ignore)
{
if(QFile::remove(databasePath.c_str()))
{
@@ -4239,6 +4245,44 @@ void MainWindow::newDatabase()
return;
}
}
else if(r == QMessageBox::Yes)
{
std::string errorMsg;
rtabmap::ProgressDialog * progressDialog = new rtabmap::ProgressDialog(this);
progressDialog->setAttribute(Qt::WA_DeleteOnClose);
progressDialog->setMaximumSteps(100);
progressDialog->show();
progressDialog->setCancelButtonVisible(true);
RecoveryState state(progressDialog);
_recovering = true;
if(databaseRecovery(databasePath, false, &errorMsg, &state))
{
_recovering = false;
progressDialog->setValue(progressDialog->maximumSteps());
QString newPath = QFileDialog::getSaveFileName(this, tr("Save recovered database"), _preferencesDialog->getWorkingDirectory()+QDir::separator()+QString("recovered.db"), tr("RTAB-Map database files (*.db)"));
if(newPath.isEmpty())
{
return;
}
if(QFileInfo(newPath).suffix() == "")
{
newPath += ".db";
}
if(QFile::exists(newPath))
{
QFile::remove(newPath);
}
QFile::rename(databasePath.c_str(), newPath);
return;
}
else
{
_recovering = false;
progressDialog->setValue(progressDialog->maximumSteps());
QMessageBox::warning(this, "Database recovery", tr("Database recovery failed: \"%1\".", errorMsg.c_str()));
return;
}
}
else
{
return;

103
guilib/src/RecoveryState.h Normal file
View File

@@ -0,0 +1,103 @@
/*
Copyright (c) 2010-2017, Mathieu Labbe - IntRoLab - Universite de Sherbrooke
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Universite de Sherbrooke nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef RECOVERYSTATE_H_
#define RECOVERYSTATE_H_
#include "rtabmap/gui/ProgressDialog.h"
#include "rtabmap/core/ProgressState.h"
#include "rtabmap/utilite/UStl.h"
#include "rtabmap/utilite/UConversion.h"
#include <QApplication>
namespace rtabmap {
class RecoveryState : public QObject, public ProgressState
{
Q_OBJECT
public:
RecoveryState(ProgressDialog * dialog): dialog_(dialog)
{
connect(dialog_, SIGNAL(canceled()), this, SLOT(cancel()));
}
virtual ~RecoveryState() {}
virtual bool callback(const std::string & msg) const
{
if(!msg.empty())
{
QString msgQt = msg.c_str();
if(msgQt.contains("Processed"))
{
dialog_->incrementStep();
dialog_->appendText(msg.c_str());
}
else if(msgQt.contains("Found"))
{
std::list<std::string> strSplit = uSplitNumChar(msg);
if(strSplit.size() == 3)
{
int nodes = uStr2Int(*(++strSplit.begin()));
if(nodes > 0)
{
dialog_->setMaximumSteps(nodes);
}
}
dialog_->appendText(msg.c_str());
}
else if(msgQt.contains("Skipping") ||
msgQt.contains("Failed processing"))
{
dialog_->appendText(msg.c_str(), Qt::darkYellow);
}
else
{
dialog_->appendText(msg.c_str());
}
}
QApplication::processEvents();
if(!isCanceled())
{
return ProgressState::callback(msg);
}
return false;
}
public slots:
void cancel()
{
setCanceled(true);
}
private:
ProgressDialog * dialog_;
};
}
#endif /* RECOVERYSTATE_H_ */

View File

@@ -81,6 +81,7 @@ void StatItem::clearCache()
{
_x.clear();
_y.clear();
_value->clear();
}
void StatItem::addValue(float y)

View File

@@ -21,7 +21,16 @@
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@@ -52,8 +61,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>393</width>
<height>256</height>
<width>388</width>
<height>242</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1">
@@ -244,8 +253,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>393</width>
<height>256</height>
<width>387</width>
<height>242</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2" columnstretch="0,1">
@@ -425,7 +434,16 @@
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="margin">
<property name="leftMargin">
<number>12</number>
</property>
<property name="topMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
@@ -481,7 +499,16 @@
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="margin">
<property name="leftMargin">
<number>12</number>
</property>
<property name="topMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
@@ -552,7 +579,7 @@
<x>0</x>
<y>0</y>
<width>1547</width>
<height>22</height>
<height>25</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@@ -578,6 +605,9 @@
<addaction name="actionGPS_KML"/>
</widget>
<addaction name="actionOpen_database"/>
<addaction name="actionClose_database"/>
<addaction name="separator"/>
<addaction name="actionDatabase_recovery"/>
<addaction name="separator"/>
<addaction name="actionSave_config"/>
<addaction name="separator"/>
@@ -1040,7 +1070,16 @@
</attribute>
<widget class="QWidget" name="dockWidgetContents_3">
<layout class="QVBoxLayout" name="verticalLayout_10">
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@@ -1094,8 +1133,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>324</width>
<height>188</height>
<width>340</width>
<height>186</height>
</rect>
</property>
<attribute name="label">
@@ -1230,8 +1269,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>280</width>
<height>666</height>
<width>282</width>
<height>626</height>
</rect>
</property>
<attribute name="label">
@@ -1593,8 +1632,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>201</width>
<height>126</height>
<width>205</width>
<height>117</height>
</rect>
</property>
<attribute name="label">
@@ -1693,8 +1732,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>186</width>
<height>496</height>
<width>185</width>
<height>487</height>
</rect>
</property>
<attribute name="label">
@@ -1796,7 +1835,16 @@
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@@ -1909,7 +1957,16 @@
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@@ -2040,7 +2097,16 @@
</attribute>
<widget class="QWidget" name="dockWidgetContents_7">
<layout class="QVBoxLayout" name="verticalLayout_13">
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@@ -2058,7 +2124,16 @@
</attribute>
<widget class="QWidget" name="dockWidgetContents_5">
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@@ -2103,7 +2178,16 @@
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@@ -2321,6 +2405,16 @@
<string>Google Earth (*.kml)</string>
</property>
</action>
<action name="actionDatabase_recovery">
<property name="text">
<string>Database recovery</string>
</property>
</action>
<action name="actionClose_database">
<property name="text">
<string>Close database</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>