From 618d43b7e80d1bd63e41cd24f368e6c481d295cb Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 27 May 2021 11:10:10 +0200 Subject: [PATCH 01/68] version bump --- appveyor.yml | 2 +- doc/source/doxygen-docs/changelog.md | 3 +++ package.xml | 2 +- version_prefix.txt | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 602bc43b95..c548d86868 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ # version format -version: 2.3.1-{branch}-build{build} +version: 2.3.2-{branch}-build{build} os: Visual Studio 2019 diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index aa6e49a1b7..ff5eb1a2b4 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -1,5 +1,8 @@ \page changelog Change Log +# Version 2.3.2: UNRELEASED +- (None) + # Version 2.3.1: Released May 26th, 2021 - General cmake scripts: - `find_package(mrpt-xxx)` is now much faster. diff --git a/package.xml b/package.xml index 83e2502f21..2714c75f26 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ --> mrpt2 - 2.3.1 + 2.3.2 Mobile Robot Programming Toolkit (MRPT) version 2.x Jose-Luis Blanco-Claraco diff --git a/version_prefix.txt b/version_prefix.txt index 6d4abbe8b0..47ef73593f 100644 --- a/version_prefix.txt +++ b/version_prefix.txt @@ -1,4 +1,4 @@ -2.3.1 +2.3.2 # IMPORTANT: This file is parsed by CMake, don't add any comment to # the first line. # This file is used in both Windows and Linux scripts to automatically From 0a5a432664f88b938217b3a0783892cf2732192a Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Tue, 25 May 2021 11:31:23 +0200 Subject: [PATCH 02/68] more unit tests --- .../CGasConcentrationGridMap2D_unittest.cpp | 76 +++++++++++++++++++ libs/maps/src/maps/CRandomFieldGridMap2D.cpp | 1 + 2 files changed, 77 insertions(+) create mode 100644 libs/maps/src/maps/CGasConcentrationGridMap2D_unittest.cpp diff --git a/libs/maps/src/maps/CGasConcentrationGridMap2D_unittest.cpp b/libs/maps/src/maps/CGasConcentrationGridMap2D_unittest.cpp new file mode 100644 index 0000000000..d33af6737f --- /dev/null +++ b/libs/maps/src/maps/CGasConcentrationGridMap2D_unittest.cpp @@ -0,0 +1,76 @@ +/* +------------------------------------------------------------------------+ + | Mobile Robot Programming Toolkit (MRPT) | + | https://www.mrpt.org/ | + | | + | Copyright (c) 2005-2021, Individual contributors, see AUTHORS file | + | See: https://www.mrpt.org/Authors - All rights reserved. | + | Released under BSD License. See: https://www.mrpt.org/License | + +------------------------------------------------------------------------+ */ + +#include +#include +#include + +const double xMin = -4.0, xMax = 4.0, yMin = -4.0, yMax = 4.0; +const double val = 0.2, sigma = 1.0; +const bool ti = true; // time invariant +const bool um = false; // update map. Manual call to updateMapEstimation() + +static void test_CGasConcentrationGridMap2D_insertAndRead( + mrpt::maps::CRandomFieldGridMap2D::TMapRepresentation mapType, + const double resolution, + const std::function& + cellToValue) +{ + mrpt::maps::CGasConcentrationGridMap2D grid( + mapType, xMin, xMax, yMin, yMax, resolution); + + // Inside: + grid.insertIndividualReading(1.0 * val, {2.0, 3.0}, um, ti, sigma); + grid.insertIndividualReading(2.0 * val, {1.0, 0.5}, um, ti, sigma); + grid.insertIndividualReading(3.0 * val, {-1.0, -2.0}, um, ti, sigma); + + grid.insertionOptions.GMRF_skip_variance = true; + grid.updateMapEstimation(); + + for (int i = 0; i < 2; i++) + { + { + const double map_value = cellToValue(*grid.cellByPos(2.0, 3.0)); + EXPECT_NEAR(map_value, 1.0 * val, 1e-2) << "mapType: " << mapType; + } + { + const double map_value = cellToValue(*grid.cellByPos(1.0, 0.5)); + EXPECT_NEAR(map_value, 2.0 * val, 1e-2) << "mapType: " << mapType; + } + { + const double map_value = cellToValue(*grid.cellByPos(-1.0, -2.0)); + EXPECT_NEAR(map_value, 3.0 * val, 1e-2) << "mapType: " << mapType; + + // Test after map enlarge: + grid.resize(-5.0, 5.0, -5.0, 5.0, {}, .0); + } + } +} + +TEST(CGasConcentrationGridMap2D, insertAndRead) +{ + test_CGasConcentrationGridMap2D_insertAndRead( + mrpt::maps::CRandomFieldGridMap2D::mrKalmanFilter, 1.0, + [](const mrpt::maps::TRandomFieldCell& c) { return c.kf_mean(); }); + test_CGasConcentrationGridMap2D_insertAndRead( + mrpt::maps::CRandomFieldGridMap2D::mrKalmanApproximate, 1.0, + [](const mrpt::maps::TRandomFieldCell& c) { return c.kf_mean(); }); + test_CGasConcentrationGridMap2D_insertAndRead( + mrpt::maps::CRandomFieldGridMap2D::mrGMRF_SD, 0.5, + [](const mrpt::maps::TRandomFieldCell& c) { return c.gmrf_mean(); }); + +#if 0 + test_CGasConcentrationGridMap2D_insertAndRead( + mrpt::maps::CRandomFieldGridMap2D::mrKernelDM, 0.1, + [](const mrpt::maps::TRandomFieldCell& c) { return c.dm_mean(); }); + test_CGasConcentrationGridMap2D_insertAndRead( + mrpt::maps::CRandomFieldGridMap2D::mrKernelDMV, 0.1, + [](const mrpt::maps::TRandomFieldCell& c) { return c.dm_mean(); }); +#endif +} diff --git a/libs/maps/src/maps/CRandomFieldGridMap2D.cpp b/libs/maps/src/maps/CRandomFieldGridMap2D.cpp index da8aae229d..87f1d4309b 100644 --- a/libs/maps/src/maps/CRandomFieldGridMap2D.cpp +++ b/libs/maps/src/maps/CRandomFieldGridMap2D.cpp @@ -591,6 +591,7 @@ void CRandomFieldGridMap2D::insertObservation_KernelDM_DMV( // Compute the "parzen Gaussian" once only: // ------------------------------------------------- + ASSERT_LT_(m_resolution, 0.5 * m_insertOptions_common->cutoffRadius); int Ac_cutoff = round(m_insertOptions_common->cutoffRadius / m_resolution); unsigned Ac_all = 1 + 2 * Ac_cutoff; double minWinValueAtCutOff = exp(-square( From c3fdb4ae5fc1f1f73b7021ac59d1027c449b68cb Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Tue, 25 May 2021 11:37:36 +0200 Subject: [PATCH 03/68] clean up dead code --- .../src/maps/CGasConcentrationGridMap2D.cpp | 116 ------------------ 1 file changed, 116 deletions(-) diff --git a/libs/maps/src/maps/CGasConcentrationGridMap2D.cpp b/libs/maps/src/maps/CGasConcentrationGridMap2D.cpp index b2fe6e184e..246bd98ba8 100644 --- a/libs/maps/src/maps/CGasConcentrationGridMap2D.cpp +++ b/libs/maps/src/maps/CGasConcentrationGridMap2D.cpp @@ -144,19 +144,6 @@ void CGasConcentrationGridMap2D::internal_clear() windGrid_module.fill(insertionOptions.default_wind_speed); windGrid_direction.fill(insertionOptions.default_wind_direction); - /*float S = windGrid_direction.getSizeX() * -windGrid_direction.getSizeY(); - - for( unsigned int y=windGrid_direction.getSizeY()/2; -y memory_retention = - // exp(-time/memory_relative_strenght) - // memory_retention = exp(- mrpt::system::timeDifference(m_map[cx + - // cy*m_size_x].last_updated, now()) / memory_relative_strenght); - // //Update Uncertainty (STD) - // m_map[cx + cy*m_size_x].kf_std = 1 - ( (1-m_map[cx + - // cy*m_size_x].updated_std) * memory_retention ); - // } - // } } /*--------------------------------------------------------------- @@ -1199,95 +1172,6 @@ that models the propagation of the gas comming from cell_i. } } - // OLD WAY - - /* --------------------------------------------------------- - Estimate the volume of the Gaussian on each affected cell - //-----------------------------------------------------------*/ - // for(int cx=min_cx; cx<=max_cx; cx++) - //{ - // for(int cy=min_cy; cy<=max_cy; cy++) - // { - // // Coordinates of affected cell (center of the cell) - // float cell_a_x = (cx+0.5f)*LUT.resolution; - // float cell_a_y = (cy+0.5f)*LUT.resolution; - // float w_cell_a = 0.0; //initial Gaussian value of - // cell afected - - // // Estimate volume of the Gaussian under cell (a) - // // Partition each cell into (p x p) subcells and - // evaluate the gaussian. - // int p = 40; - // float subcell_pres = LUT.resolution/p; - // float cell_a_x_min = cell_a_x - LUT.resolution/2.0; - // float cell_a_y_min = cell_a_y - LUT.resolution/2.0; - - // - // for(int scy=0; scy Date: Sat, 29 May 2021 02:34:40 +0200 Subject: [PATCH 04/68] BUGFIX: CImage::empty() didn't work for external (lazy-load) images --- doc/source/doxygen-docs/changelog.md | 3 ++- libs/img/include/mrpt/img/CImage.h | 5 ++++- libs/img/src/CImage.cpp | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index ff5eb1a2b4..094de499b7 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -1,7 +1,8 @@ \page changelog Change Log # Version 2.3.2: UNRELEASED -- (None) +- BUG FIXES: + - mrpt::img::CImage::isEmpty() should return false for delay-load images. # Version 2.3.1: Released May 26th, 2021 - General cmake scripts: diff --git a/libs/img/include/mrpt/img/CImage.h b/libs/img/include/mrpt/img/CImage.h index 751319a914..a834c2553a 100644 --- a/libs/img/include/mrpt/img/CImage.h +++ b/libs/img/include/mrpt/img/CImage.h @@ -671,7 +671,10 @@ class CImage : public mrpt::serialization::CSerializable, public CCanvas /** Returns true if the image is RGB or RGBA, false if it is grayscale */ bool isColor() const; - /** Returns true if the object is in the state after default constructor */ + /** Returns true if the object is in the state after default constructor. + * Returns false for delay-loaded images, disregarding whether the image is + * actually on disk or memory. + */ bool isEmpty() const; /** Returns true (as of MRPT v2.0.0, it's fixed) */ diff --git a/libs/img/src/CImage.cpp b/libs/img/src/CImage.cpp index 4f98394871..8bce27874f 100644 --- a/libs/img/src/CImage.cpp +++ b/libs/img/src/CImage.cpp @@ -875,7 +875,7 @@ int CImage::channelCount() const bool CImage::isEmpty() const { #if MRPT_HAS_OPENCV - return m_imgIsExternalStorage || m_impl->img.empty(); + return !m_imgIsExternalStorage && m_impl->img.empty(); #else THROW_EXCEPTION("MRPT built without OpenCV support"); #endif From 3418567774f2dc70113925a0a0125746dab6b66b Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 31 May 2021 02:43:06 +0200 Subject: [PATCH 05/68] Fix FTBFS with gcc-8 --- doc/source/doxygen-docs/changelog.md | 1 + libs/containers/include/mrpt/containers/yaml.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 094de499b7..169f4529f4 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -3,6 +3,7 @@ # Version 2.3.2: UNRELEASED - BUG FIXES: - mrpt::img::CImage::isEmpty() should return false for delay-load images. + - Fix build error with GCC 8 in `mrpt/containers/yaml.h`. # Version 2.3.1: Released May 26th, 2021 - General cmake scripts: diff --git a/libs/containers/include/mrpt/containers/yaml.h b/libs/containers/include/mrpt/containers/yaml.h index 2da3035ca8..e1fa1a18ca 100644 --- a/libs/containers/include/mrpt/containers/yaml.h +++ b/libs/containers/include/mrpt/containers/yaml.h @@ -89,7 +89,7 @@ class yaml struct node_t; using scalar_t = std::any; using sequence_t = std::vector; - using map_t = std::map /*transparent comp*/>; + using map_t = std::map; using comments_t = std::array< std::optional, static_cast(CommentPosition::MAX)>; From bdd76dd3df69e2a51cfbb566ae780f9dbfc65361 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 31 May 2021 11:11:34 +0200 Subject: [PATCH 06/68] docs improvements --- doc/source/compiling.rst | 34 ++++-- doc/source/supported-sensors.rst | 109 ++++++++---------- .../include/mrpt/hwdrivers/CImpinjRFID.h | 1 + 3 files changed, 69 insertions(+), 75 deletions(-) diff --git a/doc/source/compiling.rst b/doc/source/compiling.rst index cde9ec43a5..12c3c1dbc7 100644 --- a/doc/source/compiling.rst +++ b/doc/source/compiling.rst @@ -31,27 +31,39 @@ Minimum compiler requisites: .. dropdown:: Debian/Ubuntu :open: - **Minimum** recommended requisites: + **Minimum recommended** requisites: .. code-block:: bash sudo apt install build-essential pkg-config cmake libwxgtk3.0-dev \ - libwxgtk3.0-gtk3-dev libopencv-dev libeigen3-dev libgtest-dev + libwxgtk3.0-gtk3-dev libopencv-dev libeigen3-dev zlib1g-dev \ + libsuitesparse-dev libjpeg-dev - **Recommended additional** packages to enable most MRPT features - (except ROS bridges): + **Recommended additional** packages to enable most MRPT features: .. code-block:: bash - sudo apt install libftdi-dev freeglut3-dev zlib1g-dev \ - libusb-1.0-0-dev libudev-dev libfreenect-dev libdc1394-22-dev \ - libavformat-dev libswscale-dev libassimp-dev libjpeg-dev \ - libsuitesparse-dev libpcap-dev liboctomap-dev libglfw3-dev \ - binutils-dev + # Build OpenGL graphics, Qt and nanogui GUIs: + sudo apt install freeglut3-dev libassimp-dev libglfw3-dev \ + libglu1-mesa-dev libqt5opengl5-dev qtbase5-dev \ + libxrandr-dev libxxf86vm-dev + # Support most common sensors: + sudo apt install libftdi-dev libusb-1.0-0-dev libudev-dev libfreenect-dev \ + libdc1394-22-dev libavformat-dev libswscale-dev libpcap-dev \ + liboctomap-dev libopenni2-dev - Install additional dependencies for ros1bridge using official Ubuntu - repositories. If you already have a ROS distribution installed, + # Support showing debug information in call stacks upon exceptions: + sudo apt install binutils-dev libiberty-dev + + # Support using system SimpleINI library (only Ubuntu >=20.04 focal) + sudo apt install libicu-dev libsimpleini-dev + + If your Ubuntu distribution is old and does not have any of the packages + above, do not worry and ignore it, MRPT CMake scripts will handle it. + + **ROS1 support:** Install additional dependencies for ros1bridge using + official Ubuntu repositories. If you already have a ROS distribution installed, doing ``source /opt/ros/xxx/setup.bash`` is enough, no further packages must be installed. Do not install these packages if you do not need the `mrpt::ros1bridge `_ module. diff --git a/doc/source/supported-sensors.rst b/doc/source/supported-sensors.rst index 1ece1b3084..f5fbcffd44 100644 --- a/doc/source/supported-sensors.rst +++ b/doc/source/supported-sensors.rst @@ -11,9 +11,9 @@ All hardware and sensor-related classes can be found in the \ `mrpt-hwdrivers library `__, which contains -the \ `mrpt::hwdrivers `__ namespace. -See also -the \ `rawlog-grabber `__ application. +the \ `mrpt::hwdrivers `__ namespace. +See also: :ref:`doxid-app_rawlog-grabber`. + Unless specifically noted, all devices are supported under \ **both Windows and Linux**, with only some (for now) supported on MacOS. @@ -32,13 +32,13 @@ Any 3D camera supported by OpenNI2 :align: right The C++ interface is implemented in the -class \ `mrpt::hwdrivers::OpenNI2Sensor `__. +class \ `mrpt::hwdrivers::OpenNI2Sensor `__. Xbox Kinect (RGB+D camera) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The C++ interface is implemented in the -class \ `mrpt::hwdrivers::CKinect `__. +class \ `mrpt::hwdrivers::CKinect `__. See \ `this page `__ for more information, demo videos and example code. @@ -50,9 +50,8 @@ SwissRanger SR3000/4000 (legacy) :align: right The C++ interface is implemented in the -class \ `mrpt::hwdrivers::CSwissRanger3DCamera `__. -See the \ `page of the demo -application `__ for +class \ `mrpt::hwdrivers::CSwissRanger3DCamera `__. +See the \ `page of the demo application `__ for more information and a demo video. Manufacturer: mesa-imaging @@ -68,7 +67,7 @@ SICK LMS 2XX :align: right SICK LMS2XX laser scanners are supported by the -class \ `mrpt::hwdrivers::CSickLaserSerial `__, +class \ `mrpt::hwdrivers::CSickLaserSerial `__, which supports a wide variety of configurations (different aperture angles, 0.25/0.50/1.0 degrees separation between rays,…) and operating baud rates, including 500Kbauds for the USB2SERIAL converters that allow @@ -81,7 +80,7 @@ SICK LMS 100 (Ethernet) :align: right SICK LMS 100 laser scanners with Ethernet interface are supported by the -class \ `mrpt::hwdrivers::CLMS100Eth `__. +class \ `mrpt::hwdrivers::CLMS100Eth `__. SICK TiM 55x/56x (Ethernet) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -90,7 +89,7 @@ SICK TiM 55x/56x (Ethernet) :align: right SICK TiM laser scanners with Ethernet interface are supported by the -class mrpt::hwdrivers::CSICKTim561Eth. +class `mrpt::hwdrivers::CSICKTim561Eth `_. Note: Since MRPT 1.9.9. @@ -101,7 +100,7 @@ Hokuyo URG/UTM/UXM :align: right A wide range of Hokuyo laser scanners are supported by one single class, -the `mrpt::hwdrivers::CHokuyoURG `__. +the `mrpt::hwdrivers::CHokuyoURG `__. See also the example named `HOKUYO_laser_test `__. @@ -114,7 +113,7 @@ Ibeo Automotive Laser Scanners :align: right Ibeo LUX laser scanner, Ethernet-interfaced, is supported since MRPT -0.9.4 through the class \ `mrpt::hwdrivers::CIbeoLuxETH `__. +0.9.4 through the class \ `mrpt::hwdrivers::CIbeoLuxETH `__. See the manufacturer website: \ http://www.ibeo-as.com/  @@ -126,7 +125,7 @@ RoboPeak RP-LIDAR laser Scanners The low-cost RP-LIDAR sensor, USB-interfaced, is supported since MRPT 1.2.2 through the -class \ `mrpt::hwdrivers::CRobotPeakLidar `__. +class \ `mrpt::hwdrivers::CRoboPeakLidar `__. See the manufacturer website: \ http://rplidar.robopeak.com/ @@ -142,24 +141,21 @@ Velodyne `Velodyne 3D LIDARs `__ are supported by means of: -- `mrpt::hwdrivers::CVelodyneScanner `__ -- `mrpt::obs::CObservationVelodyne `__ -- The new - application \ `velodyne-view `__ -- Visualization of datasets - in \ `RawLogViewer `__ -- Grab of LiDAR scans, simultaneously to other sensors, is possible - via \ `rawlog-grabber `__ - +- `mrpt::hwdrivers::CVelodyneScanner `__ +- `mrpt::obs::CObservationVelodyne `__ +- :ref:`doxid-app_velodyne-view` +- Visualization of datasets: :ref:`app_RawLogViewer` +- Grabbing LiDAR scans, simultaneously to other sensors, is possible + via: :ref:`doxid-app_rawlog-grabber` 4. Cameras ---------- -**Important: **\ MRPT provides a universal class capable of managing all +**Important:** MRPT provides a universal class capable of managing all the following cameras with a common interface, deciding which camera to open at runtime and converting the images from all the cameras to one single format, the OpenCV IplImage format. See the -class \ `mrpt::hwdrivers::CCameraSensor `__. +class \ `mrpt::hwdrivers::CCameraSensor `__. Monocular and stereo cameras ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -171,17 +167,17 @@ Apart from the generic CCameraSensor class, MRPT offers this implementation-specific classes: - All \ **cameras supported by OpenCV** are accessible by means of the - class \ `mrpt::hwdrivers::CImageGrabber_OpenCV `__. + class \ `mrpt::hwdrivers::CImageGrabber_OpenCV `__. - **Firewire cameras** are specifically supported in GNU/Linux by means of the libdc1394 library and the - class \ `mrpt::hwdrivers::CImageGrabber_dc1394 `__. + class \ `mrpt::hwdrivers::CImageGrabber_dc1394 `__. - All \ **Point Grey Research (PGR) cameras** supported by \ `FlyCapture2 `__ can be read with the - class \ `mrpt::hwdrivers::CImageGrabber_FlyCapture2 `__ (Requires: + class \ `mrpt::hwdrivers::CImageGrabber_FlyCapture2 `__ (Requires: MRPT 1.0.3). \ **Stereo pairs** built from two independent PGR cameras are supported - via \ `mrpt::hwdrivers::CCameraSensor `__. + via \ `mrpt::hwdrivers::CCameraSensor `__. Bumblebee/Bumblebee2 Stereo cameras ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -190,7 +186,7 @@ Bumblebee stereo cameras are supported in MRPT in both Windows & GNU/Linux by means of the manufacturer API and the libdc1394 libraries, respectively. The C++ interface is the same in any case, and it’s implemented in the -class\ `mrpt::hwdrivers::CStereoGrabber_Bumblebee_libdc1394 `__. +class\ `mrpt::hwdrivers::CStereoGrabber_Bumblebee_libdc1394 `__. Videre Stereo cameras ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -198,7 +194,7 @@ Videre Stereo cameras Videre stereo cameras are supported in MRPT (since MRPT 0.9.1) for GNU/Linux only for now, by means of the manufacturer SVS API. The C++ interface is implemented in the -class \ `mrpt::hwdrivers::CStereoGrabber_SVS `__. +class \ `mrpt::hwdrivers::CStereoGrabber_SVS `__. IP cameras and video files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -209,32 +205,23 @@ IP cameras and video files Both IP cameras and offline video files (in many common video formats and codecs) are supported by means of the ffmpeg libraries, within the MRPT -class \ `mrpt::hwdrivers::CFFMPEG_InputStream `__. +class \ `mrpt::hwdrivers::CFFMPEG_InputStream `__. 5. Inertial Sensors (IMUs) ---------------------------- -5.1. xSens MTi Inertial Unit (IMU) – 3rd generation devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. image:: images/mti-150x150.jpg - :align: right - -The interface to this sensor is implemented in the -class \ `mrpt::hwdrivers::CIMUXSens `__. - -5.2. xSens MTi Inertial Units (IMUs) – 4th generation devices +5.1. xSens MTi Inertial Units (IMUs) – 4th generation devices ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. image:: images/MTi_10-series-150x150.jpg :align: right The interface to this sensor is implemented in the -class \ `mrpt::hwdrivers::CIMUXSens_MT4 `__. +class \ `mrpt::hwdrivers::CIMUXSens_MT4 `__. Required: MRPT 1.0.3 -KVH DSP3000 (Fiber Optic Gyro) +5.2. KVH DSP3000 (Fiber Optic Gyro) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. image:: images/KVH_dsp3000_IMU-150x150.jpg @@ -242,7 +229,7 @@ KVH DSP3000 (Fiber Optic Gyro) A precise fiber optic gyro. The interface to this sensor is implemented in the -class \ `mrpt::hwdrivers::CGyroKVHDSP3000 `__. +class \ `mrpt::hwdrivers::CGyroKVHDSP3000 `__. See the manufacturer website: \ `http://www.kvh.com/…/Fiber-Optic-Gyros/DSP-3000.aspx `__  @@ -259,7 +246,7 @@ Parser of standard NMEA commands and Novatel binary frames An implementation of a parser of NMEA commands from a wide range of GPS devices, also capable of receiving Novatel frames (this latter feature, only available in MRPT 1.3.3 or newer), can be found in the -class \ `mrpt::hwdrivers::CGPSInterface `__. +class \ `mrpt::hwdrivers::CGPSInterface `__. RTK corrections via NTRIP ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -270,7 +257,7 @@ RTK corrections via NTRIP You can use advanced RTK GPS receivers with MRPT. For that, MRPT includes a class that receives NTRIP RTK corrections from an Internet server and sends them to a serial port connected to the GPS receiver. -See \ `mrpt::hwdrivers::CNTRIPEmitter `__. +See \ `mrpt::hwdrivers::CNTRIPEmitter `__. 7. Activemedia robotic bases (All ARIA-compatible bases) -------------------------------------------------------- @@ -278,14 +265,6 @@ See \ `mrpt::hwdrivers::CNTRIPEmitter `__, -which implements basic mobility functions as well as sonars and other -sensors. Since MRPT carries its own embedded version of ARIA, you won’t -need any software or library previously installed in your system to use -this class. - Note: Support for these robots was dropped in MRPT 1.5.3. Use older versions if you need it with MRPT. Ten years ago, it might make sense to integrate ARIA into MRPT, but nowadays it’s probably more practical to @@ -296,7 +275,9 @@ use ARIA ROS packages to access robots instead of directly using MRPT. An interface to this mobile robot, equipped with an IP camera, is implemented in the C++ -class \ `mrpt::hwdrivers::CRovio `__. +class ``mrpt::hwdrivers::CRovio``. + +Note: Deprecated and removed in MRPT 2.1.0. See the manufacturer web: \ http://www.wowwee.com/en/products/tech/telepresence/rovio/rovio  @@ -309,7 +290,7 @@ web: \ http://www.wowwee.com/en/products/tech/telepresence/rovio/rovio  A cross-platform and very simple interface to joysticks is provided via the -class \ `mrpt::hwdrivers::CJoystick `__. +class \ `mrpt::hwdrivers::CJoystick `__. 10. Pan and Tilt Units ---------------------- @@ -318,14 +299,14 @@ Direct Perception Pan-Tilt-Unit (PTU) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ An interface to this PTU model is implemented in the C++ -class \ `mrpt::hwdrivers::CPtuDPerception `__. +class \ `mrpt::hwdrivers::CPtuDPerception `__. Micos Tilt-Unit ~~~~~~~~~~~~~~~~~~~~~ An interface to the precision “rotation stage DT-80”, by MICOS. See the C++ -class \ `mrpt::hwdrivers::CTuMicos `__. +class \ `mrpt::hwdrivers::CTuMicos `__. See the manufacturer website: http://www.micos-online.com/web2/en/1,5,120,dt80.html @@ -337,7 +318,7 @@ Impinj’s RFID Speedway Revolution Reader ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This sensor is supported by the MRPT -class \ `mrpt::hwdrivers::CImpinfRFID `__. +class \ `mrpt::hwdrivers::CImpinjRFID `__. Note however that this sensor requires an external program outside of MRPT for communications (refer to the Doxygen documentation of the C++ class). @@ -355,7 +336,7 @@ Phidgets boards :align: right Phidgets Inc.’s board “PhidgetInterfaceKit 8/8/8” is supported by -class \ `mrpt::hwdrivers::CPhidgetInterfaceKitProximitySensors `__. +class \ `mrpt::hwdrivers::CPhidgetInterfaceKitProximitySensors `__. Manufacturer web: \ http://www.phidgets.com/products.php?product_id=1018 @@ -363,7 +344,7 @@ National Instruments boards compatible with DAQmx Base ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | See - class: \ `mrpt::hwdrivers::CNationalInstrumentsDAQ `__ + class: \ `mrpt::hwdrivers::CNationalInstrumentsDAQ `__ | Read \ `this blog entry `__ about the limitations of NI PCI/USB DAQ boards in 64bit Linux distributions. @@ -378,7 +359,7 @@ MiniRAE Lite photoionization detector (PID) This sensor for fast detection of volatile organic compounds (VOC) is supported by the software driver C++ -class \ `mrpt::hwdrivers::CRaePID `__ +class \ `mrpt::hwdrivers::CRaePID `__ Manufacturer web: \ http://www.raesystems.com/products/minirae-lite @@ -390,7 +371,7 @@ Gill WindSonic Wind sensor Speed and direction ultrasonic wind sensor. Supported by the software driver C++ -class \ `mrpt::hwdrivers::CGillAnemometer `__  +class \ `mrpt::hwdrivers::CGillAnemometer `__  Manufacturer web: \ http://gillinstruments.com/products/anemometer/windsonic.htm diff --git a/libs/hwdrivers/include/mrpt/hwdrivers/CImpinjRFID.h b/libs/hwdrivers/include/mrpt/hwdrivers/CImpinjRFID.h index 850f3e0ae2..a58bf30454 100644 --- a/libs/hwdrivers/include/mrpt/hwdrivers/CImpinjRFID.h +++ b/libs/hwdrivers/include/mrpt/hwdrivers/CImpinjRFID.h @@ -22,6 +22,7 @@ namespace mrpt::hwdrivers * connects to a program that does the actual communication with the receiver. * This is done because the manufacturer only provides libraries for C# and * Java. The program that runs the device must be started after this object + * \ingroup mrpt_hwdrivers_grp */ class CImpinjRFID : public mrpt::hwdrivers::CGenericSensor { From 40fe522208fe18200d396258182abfb748a66c1a Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Wed, 2 Jun 2021 10:45:28 +0200 Subject: [PATCH 07/68] CTimeLogger: Include custom `name` in COutputLogger --- doc/source/doxygen-docs/changelog.md | 3 +++ libs/system/include/mrpt/system/CTimeLogger.h | 2 +- libs/system/src/CTimeLogger.cpp | 15 ++++++++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 169f4529f4..909de35414 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -1,6 +1,9 @@ \page changelog Change Log # Version 2.3.2: UNRELEASED +- Changes in libraries: + - \ref mrpt_system_grp + - mrpt::system::CTimeLogger: Include custom `name` in underlying mrpt::system::COutputLogger name. - BUG FIXES: - mrpt::img::CImage::isEmpty() should return false for delay-load images. - Fix build error with GCC 8 in `mrpt/containers/yaml.h`. diff --git a/libs/system/include/mrpt/system/CTimeLogger.h b/libs/system/include/mrpt/system/CTimeLogger.h index 9676c3f2cb..a8793fed3f 100644 --- a/libs/system/include/mrpt/system/CTimeLogger.h +++ b/libs/system/include/mrpt/system/CTimeLogger.h @@ -135,7 +135,7 @@ class CTimeLogger : public mrpt::system::COutputLogger const bool is_time = false) noexcept; const std::string& getName() const noexcept { return m_name; } - void setName(const std::string& name) noexcept { m_name = name; } + void setName(const std::string& name) noexcept; /** Start of a named section \sa enter */ inline void enter(const std::string_view& func_name) noexcept diff --git a/libs/system/src/CTimeLogger.cpp b/libs/system/src/CTimeLogger.cpp index e0627cffeb..5cd2ce8a39 100644 --- a/libs/system/src/CTimeLogger.cpp +++ b/libs/system/src/CTimeLogger.cpp @@ -64,15 +64,20 @@ void global_profiler_leave(const char* func_name) noexcept CTimeLogger::CTimeLogger( bool enabled, const std::string& name, const bool keep_whole_history) - : COutputLogger("CTimeLogger"), - m_tictac(), - m_enabled(enabled), - m_name(name), - m_keep_whole_history(keep_whole_history) + : m_enabled(enabled), m_keep_whole_history(keep_whole_history) { + setName(name); m_tictac.Tic(); } +void CTimeLogger::setName(const std::string& name) noexcept +{ + m_name = name; + COutputLogger::setLoggerName( + name.empty() ? std::string("CTimeLogger") + : (name + std::string(" timeLogger"))); +} + CTimeLogger::~CTimeLogger() { // Dump all stats: From 2a06291fae3965d923592e43419fbbc016a95de7 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Wed, 2 Jun 2021 18:03:33 +0200 Subject: [PATCH 08/68] CDisplayWindowGUI: improved API to allow multiple callback handlers --- apps/navlog-viewer/navlog-viewer-ui.cpp | 4 +- doc/source/doxygen-docs/changelog.md | 2 + libs/gui/include/mrpt/gui/CDisplayWindowGUI.h | 110 ++++++++++++++---- libs/gui/src/CDisplayWindowGUI.cpp | 46 +++++++- samples/hwdrivers_mynteye_icp/test.cpp | 2 +- 5 files changed, 132 insertions(+), 32 deletions(-) diff --git a/apps/navlog-viewer/navlog-viewer-ui.cpp b/apps/navlog-viewer/navlog-viewer-ui.cpp index 7bdf82d9ce..d56e2cebb9 100644 --- a/apps/navlog-viewer/navlog-viewer-ui.cpp +++ b/apps/navlog-viewer/navlog-viewer-ui.cpp @@ -283,8 +283,8 @@ NavlogViewerApp::NavlogViewerApp() // Setup idle loop code: // ----------------------------- - m_win->setLoopCallback([this]() { OnMainIdleLoop(); }); - m_win->setKeyboardCallback( + m_win->addLoopCallback([this]() { OnMainIdleLoop(); }); + m_win->addKeyboardCallback( [this](int key, int scancode, int action, int modifiers) { return OnKeyboardCallback(key, scancode, action, modifiers); }); diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 909de35414..2aae5d39da 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -2,6 +2,8 @@ # Version 2.3.2: UNRELEASED - Changes in libraries: + - \ref mrpt_gui_grp + - mrpt::gui::CDisplayWindowGUI: improved API to allow multiple callback handlers, and to report exceptions in them. - \ref mrpt_system_grp - mrpt::system::CTimeLogger: Include custom `name` in underlying mrpt::system::COutputLogger name. - BUG FIXES: diff --git a/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h b/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h index 3d0a358328..3306ef3abc 100644 --- a/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h +++ b/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h @@ -76,6 +76,16 @@ struct CDisplayWindowGUI_Params class CDisplayWindowGUI : public nanogui::Screen { public: + /** @name Callback functor types + * @{ */ + + using loop_callback_t = std::function; + using drop_files_callback_t = std::function& /* filenames */)>; + using keyboard_callback_t = std::function; + /** @} */ + /** @name Ctor and basic window set up * @{ */ using Ptr = std::shared_ptr; @@ -105,29 +115,88 @@ class CDisplayWindowGUI : public nanogui::Screen void setWindowTitle(const std::string& str); /** Every time the window is about to be repainted, an optional callback can - * be called, if provided via this method. */ - void setLoopCallback(const std::function& callback) + * be called, if provided via this method. This method can be safely called + * multiple times to register multiple callbacks. + * + * \note (New in MRPT 2.3.2) + */ + void addLoopCallback(const loop_callback_t& callback) + { + m_loopCallbacks.push_back(callback); + } + const auto& loopCallbacks() const { return m_loopCallbacks; } + + /** Handles drag-and drop events of files into the window. + * This method can be safely called multiple times to register multiple + * callbacks. + * + * \note (New in MRPT 2.3.2) + */ + void addDropFilesCallback(const drop_files_callback_t& callback) + { + m_dropFilesCallbacks.push_back(callback); + } + const auto& dropFilesCallbacks() const { return m_dropFilesCallbacks; } + + /** Handles keyboard events. + * This method can be safely called multiple times to register multiple + * callbacks. + * + * \note (New in MRPT 2.3.2) + */ + void addKeyboardCallback(const keyboard_callback_t& callback) + { + m_keyboardCallbacks.push_back(callback); + } + const auto& keyboardCallbacks() const { return m_keyboardCallbacks; } + + /** \note This call replaces *all* existing loop callbacks. See + * addLoopCallback(). + * \deprecated In MRPT 2.3.2, replaced by addLoopCallback() + */ + void setLoopCallback(const loop_callback_t& callback) + { + m_loopCallbacks.clear(); + m_loopCallbacks.push_back(callback); + } + /** \deprecated In MRPT 2.3.2, replaced by loopCallbacks() */ + loop_callback_t loopCallback() const { - m_loopCallback = callback; + return m_loopCallbacks.empty() ? loop_callback_t() + : m_loopCallbacks.at(0); } - const auto& loopCallback() const { return m_loopCallback; } - /** Sets a handle for file drop events */ - void setDropFilesCallback( - const std::function< - bool(const std::vector& /* filenames */)>& callback) + /** \note This call replaces *all* existing drop file callbacks. See + * addDropFilesCallback(). + * \deprecated In MRPT 2.3.2, replaced by addDropFilesCallback() + */ + void setDropFilesCallback(const drop_files_callback_t& callback) + { + m_dropFilesCallbacks.clear(); + m_dropFilesCallbacks.push_back(callback); + } + /** \deprecated In MRPT 2.3.2, replaced by dropFilesCallbacks() */ + drop_files_callback_t dropFilesCallback() const { - m_dropFilesCallback = callback; + return m_dropFilesCallbacks.empty() ? drop_files_callback_t() + : m_dropFilesCallbacks.at(0); } - const auto& dropFilesCallback() const { return m_dropFilesCallback; } - void setKeyboardCallback(const std::function& callback) + /** \note This call replaces *all* existing keyboard callbacks. See + * addKeyboardCallback(). + * \deprecated In MRPT 2.3.2, replaced by addKeyboardCallback() + */ + void setKeyboardCallback(const keyboard_callback_t& callback) + { + m_keyboardCallbacks.clear(); + m_keyboardCallbacks.push_back(callback); + } + /** \deprecated In MRPT 2.3.2, replaced by keyboardCallbacks() */ + keyboard_callback_t keyboardCallback() const { - m_keyboardCallback = callback; + return m_keyboardCallbacks.empty() ? keyboard_callback_t() + : m_keyboardCallbacks.at(0); } - const auto& keyboardCallback() const { return m_keyboardCallback; } /** @} */ @@ -228,14 +297,9 @@ class CDisplayWindowGUI : public nanogui::Screen /** Used to keep track of mouse events on the camera */ internal::NanoGUICanvasHeadless m_background_canvas; - std::function m_loopCallback; - // Returns true if handled - std::function& /* filenames */)> - m_dropFilesCallback; - // Returns true if handled - std::function - m_keyboardCallback; + std::vector m_loopCallbacks; + std::vector m_dropFilesCallbacks; + std::vector m_keyboardCallbacks; void createSubWindowsControlUI(); diff --git a/libs/gui/src/CDisplayWindowGUI.cpp b/libs/gui/src/CDisplayWindowGUI.cpp index 28393d464c..73336dc056 100644 --- a/libs/gui/src/CDisplayWindowGUI.cpp +++ b/libs/gui/src/CDisplayWindowGUI.cpp @@ -47,7 +47,18 @@ CDisplayWindowGUI::~CDisplayWindowGUI() void CDisplayWindowGUI::drawContents() { // If provided, call the user loop code: - if (m_loopCallback) m_loopCallback(); + for (const auto& callback : m_loopCallbacks) + { + try + { + callback(); + } + catch (const std::exception& e) + { + std::cerr << "[CDisplayWindowGUI] Exception in loop callback:\n" + << e.what() << std::endl; + } + } // Optional: render background scene. std::lock_guard lck(background_scene_mtx); @@ -120,16 +131,39 @@ bool CDisplayWindowGUI::scrollEvent( bool CDisplayWindowGUI::dropEvent(const std::vector& filenames) { - if (m_dropFilesCallback) return m_dropFilesCallback(filenames); - else - return false; + for (const auto& callback : m_dropFilesCallbacks) + { + try + { + if (callback(filenames)) return true; + } + catch (const std::exception& e) + { + std::cerr << "[CDisplayWindowGUI] Exception in drop file event " + "callback:\n" + << e.what() << std::endl; + } + } + + return false; } bool CDisplayWindowGUI::keyboardEvent( int key, int scancode, int action, int modifiers) { - if (m_keyboardCallback) - if (m_keyboardCallback(key, scancode, action, modifiers)) return true; + for (const auto& callback : m_keyboardCallbacks) + { + try + { + if (callback(key, scancode, action, modifiers)) return true; + } + catch (const std::exception& e) + { + std::cerr + << "[CDisplayWindowGUI] Exception in keyboard event callback:\n" + << e.what() << std::endl; + } + } if (Screen::keyboardEvent(key, scancode, action, modifiers)) return true; diff --git a/samples/hwdrivers_mynteye_icp/test.cpp b/samples/hwdrivers_mynteye_icp/test.cpp index cbe6ce9d49..88a1ae6ac8 100644 --- a/samples/hwdrivers_mynteye_icp/test.cpp +++ b/samples/hwdrivers_mynteye_icp/test.cpp @@ -523,7 +523,7 @@ void Test_3DCamICP() win.performLayout(); // Set loop hook to update text messages: - win.setLoopCallback([&lbStatuses, &ui_data]() { + win.addLoopCallback([&lbStatuses, &ui_data]() { ui_data.strStatuses_mtx.lock(); for (unsigned int i = 0; i < lbStatuses.size(); i++) lbStatuses[i]->setValue(ui_data.strStatuses[i]); From 05ed0b591056310e5efad0a1ee1d045cd08863f5 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Sat, 5 Jun 2021 08:40:10 +0200 Subject: [PATCH 09/68] Fix exception rendering empty point clouds due to invalid bounding box. --- doc/source/doxygen-docs/changelog.md | 1 + libs/opengl/include/mrpt/opengl/CPointCloud.h | 46 +++++++------- .../include/mrpt/opengl/CPointCloudColoured.h | 61 ++++++++++--------- 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 2aae5d39da..e86667a302 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -9,6 +9,7 @@ - BUG FIXES: - mrpt::img::CImage::isEmpty() should return false for delay-load images. - Fix build error with GCC 8 in `mrpt/containers/yaml.h`. + - Fix exception rendering empty point clouds due to invalid bounding box. # Version 2.3.1: Released May 26th, 2021 - General cmake scripts: diff --git a/libs/opengl/include/mrpt/opengl/CPointCloud.h b/libs/opengl/include/mrpt/opengl/CPointCloud.h index 4523ed78ef..0af6a51622 100644 --- a/libs/opengl/include/mrpt/opengl/CPointCloud.h +++ b/libs/opengl/include/mrpt/opengl/CPointCloud.h @@ -97,6 +97,7 @@ class CPointCloud : public CRenderizableShaderPoints, * in the coordinate frame of the object parent. */ mrpt::math::TBoundingBox getBoundingBox() const override { + if (empty()) return {}; if (auto bb = this->octree_getBoundingBox(); bb) return *bb; else return {}; @@ -105,9 +106,9 @@ class CPointCloud : public CRenderizableShaderPoints, /** @name Read/Write of the list of points to render @{ */ - inline size_t size() const { return m_points.size(); } + size_t size() const { return m_points.size(); } /** Set the number of points (with contents undefined) */ - inline void resize(size_t N) + void resize(size_t N) { m_points.resize(N); m_minmax_valid = false; @@ -115,7 +116,7 @@ class CPointCloud : public CRenderizableShaderPoints, } /** Like STL std::vector's reserve */ - inline void reserve(size_t N) { m_points.reserve(N); } + void reserve(size_t N) { m_points.reserve(N); } /** Set the list of (X,Y,Z) point coordinates, all at once, from three * vectors with their coordinates */ @@ -149,7 +150,7 @@ class CPointCloud : public CRenderizableShaderPoints, } /** Get a const reference to the internal array of points */ - inline const std::vector& getArrayPoints() const + const std::vector& getArrayPoints() const { return m_points; } @@ -157,6 +158,8 @@ class CPointCloud : public CRenderizableShaderPoints, /** Empty the list of points. */ void clear(); + bool empty() const { return m_points.empty(); } + /** Adds a new point to the cloud */ void insertPoint(float x, float y, float z); @@ -171,7 +174,7 @@ class CPointCloud : public CRenderizableShaderPoints, /** Read access to each individual point (checks for "i" in the valid * range only in Debug). */ - inline const mrpt::math::TPoint3Df& operator[](size_t i) const + const mrpt::math::TPoint3Df& operator[](size_t i) const { #ifdef _DEBUG ASSERT_LT_(i, size()); @@ -179,7 +182,7 @@ class CPointCloud : public CRenderizableShaderPoints, return m_points[i]; } - inline const mrpt::math::TPoint3Df& getPoint3Df(size_t i) const + const mrpt::math::TPoint3Df& getPoint3Df(size_t i) const { return m_points[i]; } @@ -190,8 +193,7 @@ class CPointCloud : public CRenderizableShaderPoints, /** Write an individual point (without checking validity of the index). */ - inline void setPoint_fast( - size_t i, const float x, const float y, const float z) + void setPoint_fast(size_t i, const float x, const float y, const float z) { m_points[i] = {x, y, z}; m_minmax_valid = false; @@ -231,29 +233,29 @@ class CPointCloud : public CRenderizableShaderPoints, /** @name Modify the appearance of the rendered points @{ */ - inline void enableColorFromX(bool v = true) + void enableColorFromX(bool v = true) { m_colorFromDepth = v ? CPointCloud::colX : CPointCloud::colNone; CRenderizable::notifyChange(); } - inline void enableColorFromY(bool v = true) + void enableColorFromY(bool v = true) { m_colorFromDepth = v ? CPointCloud::colY : CPointCloud::colNone; CRenderizable::notifyChange(); } - inline void enableColorFromZ(bool v = true) + void enableColorFromZ(bool v = true) { m_colorFromDepth = v ? CPointCloud::colZ : CPointCloud::colNone; CRenderizable::notifyChange(); } - inline void enablePointSmooth(bool enable = true) + void enablePointSmooth(bool enable = true) { m_pointSmooth = enable; CRenderizable::notifyChange(); } - inline void disablePointSmooth() { m_pointSmooth = false; } - inline bool isPointSmoothEnabled() const { return m_pointSmooth; } + void disablePointSmooth() { m_pointSmooth = false; } + bool isPointSmoothEnabled() const { return m_pointSmooth; } /** Sets the colors used as extremes when colorFromDepth is enabled. */ void setGradientColors( const mrpt::img::TColorf& colorMin, const mrpt::img::TColorf& colorMax); @@ -285,7 +287,7 @@ class CPointCloud : public CRenderizableShaderPoints, mrpt::img::TColorf m_colorFromDepth_min = {0, 0, 0}, m_colorFromDepth_max = {0, 0, 1}; - inline void internal_render_one_point(size_t i) const; + void internal_render_one_point(size_t i) const; }; /** Specialization mrpt::opengl::PointCloudAdapter @@ -307,19 +309,19 @@ class PointCloudAdapter static constexpr bool HAS_RGBu8 = false; /** Constructor (accept a const ref for convenience) */ - inline PointCloudAdapter(const mrpt::opengl::CPointCloud& obj) + PointCloudAdapter(const mrpt::opengl::CPointCloud& obj) : m_obj(*const_cast(&obj)) { } /** Get number of points */ - inline size_t size() const { return m_obj.size(); } + size_t size() const { return m_obj.size(); } /** Set number of points (to uninitialized values) */ - inline void resize(const size_t N) { m_obj.resize(N); } + void resize(const size_t N) { m_obj.resize(N); } /** Does nothing as of now */ - inline void setDimensions(size_t height, size_t width) {} + void setDimensions(size_t height, size_t width) {} /** Get XYZ coordinates of i'th point */ template - inline void getPointXYZ(const size_t idx, T& x, T& y, T& z) const + void getPointXYZ(const size_t idx, T& x, T& y, T& z) const { const auto& pt = m_obj[idx]; x = pt.x; @@ -327,14 +329,14 @@ class PointCloudAdapter z = pt.z; } /** Set XYZ coordinates of i'th point */ - inline void setPointXYZ( + void setPointXYZ( const size_t idx, const coords_t x, const coords_t y, const coords_t z) { m_obj.setPoint_fast(idx, x, y, z); } /** Set XYZ coordinates of i'th point */ - inline void setInvalidPoint(const size_t idx) + void setInvalidPoint(const size_t idx) { m_obj.setPoint_fast(idx, 0, 0, 0); } diff --git a/libs/opengl/include/mrpt/opengl/CPointCloudColoured.h b/libs/opengl/include/mrpt/opengl/CPointCloudColoured.h index 92b1ee76f1..bcd6d701d8 100644 --- a/libs/opengl/include/mrpt/opengl/CPointCloudColoured.h +++ b/libs/opengl/include/mrpt/opengl/CPointCloudColoured.h @@ -62,6 +62,7 @@ class CPointCloudColoured : public CRenderizableShaderPoints, * in the coordinate frame of the object parent. */ mrpt::math::TBoundingBox getBoundingBox() const override { + if (empty()) return {}; if (auto bb = this->octree_getBoundingBox(); bb) return *bb; else return {}; @@ -78,7 +79,7 @@ class CPointCloudColoured : public CRenderizableShaderPoints, void insertPoint(const mrpt::math::TPointXYZfRGBAu8& p); /** Set the number of points, with undefined contents */ - inline void resize(size_t N) + void resize(size_t N) { m_points.resize(N); m_point_colors.resize(N); @@ -87,13 +88,13 @@ class CPointCloudColoured : public CRenderizableShaderPoints, } /** Like STL std::vector's reserve */ - inline void reserve(size_t N) + void reserve(size_t N) { m_points.reserve(N); m_point_colors.reserve(N); } - inline const mrpt::math::TPoint3Df& getPoint3Df(size_t i) const + const mrpt::math::TPoint3Df& getPoint3Df(size_t i) const { return m_points[i]; } @@ -103,8 +104,7 @@ class CPointCloudColoured : public CRenderizableShaderPoints, void setPoint(size_t i, const mrpt::math::TPointXYZfRGBAu8& p); /** Like \a setPoint() but does not check for index out of bounds */ - inline void setPoint_fast( - const size_t i, const mrpt::math::TPointXYZfRGBAu8& p) + void setPoint_fast(const size_t i, const mrpt::math::TPointXYZfRGBAu8& p) { m_points[i] = p.pt; m_point_colors[i] = mrpt::img::TColor(p.r, p.g, p.b, p.a); @@ -112,7 +112,7 @@ class CPointCloudColoured : public CRenderizableShaderPoints, } /** Like \a setPoint() but does not check for index out of bounds */ - inline void setPoint_fast( + void setPoint_fast( const size_t i, const float x, const float y, const float z) { m_points[i] = {x, y, z}; @@ -120,7 +120,7 @@ class CPointCloudColoured : public CRenderizableShaderPoints, } /** Like \c setPointColor but without checking for out-of-index erors */ - inline void setPointColor_fast( + void setPointColor_fast( size_t index, float R, float G, float B, float A = 1) { m_point_colors[index].R = f2u8(R); @@ -128,7 +128,7 @@ class CPointCloudColoured : public CRenderizableShaderPoints, m_point_colors[index].B = f2u8(B); m_point_colors[index].A = f2u8(A); } - inline void setPointColor_u8_fast( + void setPointColor_u8_fast( size_t index, uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xff) { m_point_colors[index].R = r; @@ -137,29 +137,31 @@ class CPointCloudColoured : public CRenderizableShaderPoints, m_point_colors[index].A = a; } /** Like \c getPointColor but without checking for out-of-index erors */ - inline void getPointColor_fast( - size_t index, float& R, float& G, float& B) const + void getPointColor_fast(size_t index, float& R, float& G, float& B) const { R = u8tof(m_point_colors[index].R); G = u8tof(m_point_colors[index].G); B = u8tof(m_point_colors[index].B); } - inline void getPointColor_fast( + void getPointColor_fast( size_t index, uint8_t& r, uint8_t& g, uint8_t& b) const { r = m_point_colors[index].R; g = m_point_colors[index].B; b = m_point_colors[index].B; } - inline mrpt::img::TColor getPointColor(size_t index) const + mrpt::img::TColor getPointColor(size_t index) const { return m_point_colors[index]; } /** Return the number of points */ - inline size_t size() const { return m_points.size(); } + size_t size() const { return m_points.size(); } + + bool empty() const { return m_points.empty(); } + /** Erase all the points */ - inline void clear() + void clear() { m_points.clear(); m_point_colors.clear(); @@ -243,19 +245,19 @@ class PointCloudAdapter static constexpr bool HAS_RGBu8 = false; /** Constructor (accept a const ref for convenience) */ - inline PointCloudAdapter(const mrpt::opengl::CPointCloudColoured& obj) + PointCloudAdapter(const mrpt::opengl::CPointCloudColoured& obj) : m_obj(*const_cast(&obj)) { } /** Get number of points */ - inline size_t size() const { return m_obj.size(); } + size_t size() const { return m_obj.size(); } /** Set number of points (to uninitialized values) */ - inline void resize(const size_t N) { m_obj.resize(N); } + void resize(const size_t N) { m_obj.resize(N); } /** Does nothing as of now */ - inline void setDimensions(size_t height, size_t width) {} + void setDimensions(size_t height, size_t width) {} /** Get XYZ coordinates of i'th point */ template - inline void getPointXYZ(const size_t idx, T& x, T& y, T& z) const + void getPointXYZ(const size_t idx, T& x, T& y, T& z) const { const auto& p = m_obj.getPoint3Df(idx); x = p.x; @@ -263,20 +265,20 @@ class PointCloudAdapter z = p.z; } /** Set XYZ coordinates of i'th point */ - inline void setPointXYZ( + void setPointXYZ( const size_t idx, const coords_t x, const coords_t y, const coords_t z) { m_obj.setPoint_fast(idx, x, y, z); } - inline void setInvalidPoint(const size_t idx) + void setInvalidPoint(const size_t idx) { m_obj.setPoint_fast(idx, 0, 0, 0); } /** Get XYZ_RGBf coordinates of i'th point */ template - inline void getPointXYZ_RGBAf( + void getPointXYZ_RGBAf( const size_t idx, T& x, T& y, T& z, float& Rf, float& Gf, float& Bf, float& Af) const { @@ -291,7 +293,7 @@ class PointCloudAdapter Af = u8tof(col.A); } /** Set XYZ_RGBf coordinates of i'th point */ - inline void setPointXYZ_RGBAf( + void setPointXYZ_RGBAf( const size_t idx, const coords_t x, const coords_t y, const coords_t z, const float Rf, const float Gf, const float Bf, const float Af) { @@ -303,7 +305,7 @@ class PointCloudAdapter /** Get XYZ_RGBu8 coordinates of i'th point */ template - inline void getPointXYZ_RGBu8( + void getPointXYZ_RGBu8( const size_t idx, T& x, T& y, T& z, uint8_t& r, uint8_t& g, uint8_t& b) const { @@ -317,7 +319,7 @@ class PointCloudAdapter b = col.B; } /** Set XYZ_RGBu8 coordinates of i'th point */ - inline void setPointXYZ_RGBu8( + void setPointXYZ_RGBu8( const size_t idx, const coords_t x, const coords_t y, const coords_t z, const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a = 0xff) @@ -327,26 +329,25 @@ class PointCloudAdapter } /** Get RGBf color of i'th point */ - inline void getPointRGBf( - const size_t idx, float& r, float& g, float& b) const + void getPointRGBf(const size_t idx, float& r, float& g, float& b) const { m_obj.getPointColor_fast(idx, r, g, b); } /** Set XYZ_RGBf coordinates of i'th point */ - inline void setPointRGBf( + void setPointRGBf( const size_t idx, const float r, const float g, const float b) { m_obj.setPointColor_fast(idx, r, g, b); } /** Get RGBu8 color of i'th point */ - inline void getPointRGBu8( + void getPointRGBu8( const size_t idx, uint8_t& r, uint8_t& g, uint8_t& b) const { m_obj.getPointColor_fast(idx, r, g, b); } /** Set RGBu8 coordinates of i'th point */ - inline void setPointRGBu8( + void setPointRGBu8( const size_t idx, const uint8_t r, const uint8_t g, const uint8_t b) { m_obj.setPointColor_u8_fast(idx, r, g, b); From 96b4acadb4f66a4fc72a320cdff40978e508933e Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Sat, 5 Jun 2021 09:06:05 +0200 Subject: [PATCH 10/68] More icons in RawLogViewer tree view --- apps/RawLogViewer/CRawlogTreeView.cpp | 9 ++ apps/RawLogViewer/imgs/tree_icon10.xpm | 162 +++++++++++++++++++++++ apps/RawLogViewer/imgs/tree_icon11.xpm | 162 +++++++++++++++++++++++ apps/RawLogViewer/imgs/tree_icon12.xpm | 110 ++++++++++++++++ apps/RawLogViewer/imgs/tree_icon7.xpm | 164 ++++++++++++++++++++---- apps/RawLogViewer/xRawLogViewerMain.cpp | 6 + doc/source/doxygen-docs/changelog.md | 3 + 7 files changed, 594 insertions(+), 22 deletions(-) create mode 100644 apps/RawLogViewer/imgs/tree_icon10.xpm create mode 100644 apps/RawLogViewer/imgs/tree_icon11.xpm create mode 100644 apps/RawLogViewer/imgs/tree_icon12.xpm diff --git a/apps/RawLogViewer/CRawlogTreeView.cpp b/apps/RawLogViewer/CRawlogTreeView.cpp index b00135228b..61b4519e11 100644 --- a/apps/RawLogViewer/CRawlogTreeView.cpp +++ b/apps/RawLogViewer/CRawlogTreeView.cpp @@ -413,6 +413,15 @@ int CRawlogTreeView::iconIndexFromClass(const TRuntimeClassId* class_ID) iconIndex = 8; else if (class_ID == CLASS_ID(CObservationRFID)) iconIndex = 8; + else if (class_ID == CLASS_ID(CObservationOdometry)) + iconIndex = 9; + else if ( + class_ID == CLASS_ID(CObservation3DRangeScan) || + class_ID == CLASS_ID(CObservationVelodyneScan) || + class_ID == CLASS_ID(CObservationPointCloud)) + iconIndex = 10; + else if (class_ID == CLASS_ID(CObservationIMU)) + iconIndex = 11; else if (class_ID->derivedFrom(CLASS_ID(CObservation))) iconIndex = 2; // Default observation else if (class_ID == CLASS_ID(CActionCollection)) diff --git a/apps/RawLogViewer/imgs/tree_icon10.xpm b/apps/RawLogViewer/imgs/tree_icon10.xpm new file mode 100644 index 0000000000..4e8502edff --- /dev/null +++ b/apps/RawLogViewer/imgs/tree_icon10.xpm @@ -0,0 +1,162 @@ +/* XPM */ +static const char * tree_icon10_xpm[] = { +"16 16 143 2", +" c None", +". c #6A6C6E", +"+ c #676A69", +"@ c #494A49", +"# c #424442", +"$ c #454647", +"% c #3A3C3A", +"& c #373939", +"* c #363735", +"= c #3D3E3D", +"- c #181816", +"; c #3A3B3A", +"> c #131414", +", c #4B4E50", +"' c #4A4E51", +") c #4E5253", +"! c #646769", +"~ c #5F615E", +"{ c #31322F", +"] c #2F2F2D", +"^ c #40423D", +"/ c #353632", +"( c #2D2D2A", +"_ c #3D3F3E", +": c #181919", +"< c #363A3C", +"[ c #696F71", +"} c #797E82", +"| c #4F4F4C", +"1 c #30312D", +"2 c #30302D", +"3 c #9D9E99", +"4 c #C9CAC6", +"5 c #979895", +"6 c #434341", +"7 c #2F302D", +"8 c #2F312F", +"9 c #7A8285", +"0 c #4C4F51", +"a c #62676A", +"b c #4A4B48", +"c c #363733", +"d c #141411", +"e c #B4B5B1", +"f c #E7E9E5", +"g c #F5F4F2", +"h c #FBFBF9", +"i c #D2D4D1", +"j c #33342F", +"k c #2A2B28", +"l c #515657", +"m c #3F4243", +"n c #414445", +"o c #5C5F61", +"p c #3A3B35", +"q c #222322", +"r c #BCBDB7", +"s c #E6E7E4", +"t c #F3F3F1", +"u c #F3F4F3", +"v c #F0F2EF", +"w c #AEB0AC", +"x c #2B2B29", +"y c #181917", +"z c #5A5F61", +"A c #444747", +"B c #515455", +"C c #363732", +"D c #171815", +"E c #868682", +"F c #E7E8E5", +"G c #F0F0EE", +"H c #F9F9F8", +"I c #F4F6F4", +"J c #E9EBE8", +"K c #3B3B39", +"L c #292925", +"M c #343435", +"N c #303130", +"O c #232423", +"P c #7A7A79", +"Q c #41423D", +"R c #0C0C0A", +"S c #C3C3C1", +"T c #BEBFBA", +"U c #ECEDE8", +"V c #F6F6F5", +"W c #FAFAFA", +"X c #EDF0ED", +"Y c #8E8E8C", +"Z c #272623", +"` c #1B1B1A", +" . c #505150", +".. c #555857", +"+. c #646460", +"@. c #4D4E49", +"#. c #1B1C17", +"$. c #222321", +"%. c #82837C", +"&. c #D8D9D5", +"*. c #F1F1EF", +"=. c #F5F5F4", +"-. c #F1F1ED", +";. c #ABACA9", +">. c #2D2D29", +",. c #31312F", +"'. c #353533", +"). c #3A3A39", +"!. c #33342E", +"~. c #10100D", +"{. c #B0B1AF", +"]. c #8E908B", +"^. c #EDEEEB", +"/. c #F6F7F5", +"(. c #E8E9E5", +"_. c #6F706B", +":. c #292A25", +"<. c #22221E", +"[. c #585957", +"}. c #747575", +"|. c #40413C", +"1. c #2A2B26", +"2. c #030300", +"3. c #50514E", +"4. c #8E8F8A", +"5. c #BBBBB8", +"6. c #848580", +"7. c #4B4C49", +"8. c #565756", +"9. c #42443F", +"0. c #252620", +"a. c #181814", +"b. c #11120E", +"c. c #363734", +"d. c #1A1B15", +"e. c #2A2B27", +"f. c #565956", +"g. c #31322C", +"h. c #34352F", +"i. c #282A25", +"j. c #2E2F29", +"k. c #31322E", +"l. c #6D706B", +" . ", +" + @ # $ % & ", +" * = - ; > , ' ) ! ", +" ~ { ] ^ / / ( _ : < [ } ", +" | 1 2 3 4 5 6 7 8 9 0 a b ", +" c d e f g h i j k l m n o ", +" p q r s t u v w x y z A B ", +" C D E F G H I J K L M N O P ", +" Q R S T U V W X Y Z ` ...+.", +" @.#.$.%.&.*.=.-.;.>.,.'.). ", +" !.~.{.].^./.(._.:.<.[.}. ", +" |.1.2.3.4.5.6.L :.7.8. ", +" 9.0.a.b.c.d.e.e.f. ", +" g.h.i.j.k.l. ", +" ", +" "}; diff --git a/apps/RawLogViewer/imgs/tree_icon11.xpm b/apps/RawLogViewer/imgs/tree_icon11.xpm new file mode 100644 index 0000000000..b18163ed28 --- /dev/null +++ b/apps/RawLogViewer/imgs/tree_icon11.xpm @@ -0,0 +1,162 @@ +/* XPM */ +static const char * tree_icon11_xpm[] = { +"16 16 143 2", +" c None", +". c #929292", +"+ c #909090", +"@ c #8E8E8E", +"# c #8D8D8D", +"$ c #8C8C8C", +"% c #8B8B8B", +"& c #8A8A8A", +"* c #898989", +"= c #878787", +"- c #868686", +"; c #838383", +"> c #848484", +", c #858585", +"' c #828282", +") c #818181", +"! c #808080", +"~ c #7A7A7A", +"{ c #7B7B7B", +"] c #7C7C7C", +"^ c #797979", +"/ c #6E6E6E", +"( c #6F6F6F", +"_ c #707070", +": c #727272", +"< c #737373", +"[ c #717171", +"} c #ACACAC", +"| c #A4A4A4", +"1 c #787878", +"2 c #696969", +"3 c #666666", +"4 c #676767", +"5 c #686868", +"6 c #6C6C6C", +"7 c #747474", +"8 c #6D726E", +"9 c #ADADAD", +"0 c #C4C4C4", +"a c #DDDDDD", +"b c #B4B4B4", +"c c #949494", +"d c #C2C2C2", +"e c #AFAFAF", +"f c #959595", +"g c #3E423D", +"h c #3C463D", +"i c #4C543F", +"j c #A9A9A7", +"k c #B7B7B7", +"l c #AEAEAE", +"m c #A1A1A1", +"n c #9C9C9C", +"o c #9B9B9B", +"p c #A2A2A2", +"q c #AAAAAA", +"r c #ABABAB", +"s c #959797", +"t c #656965", +"u c #252718", +"v c #2A2F29", +"w c #3D463E", +"x c #3E4630", +"y c #2B2E19", +"z c #000101", +"A c #1F1F1F", +"B c #4A4A4A", +"C c #5A5A59", +"D c #5C5D5C", +"E c #515050", +"F c #5A857D", +"G c #75B8A6", +"H c #21524A", +"I c #172C23", +"J c #272D1D", +"K c #2A2F28", +"L c #3A453A", +"M c #3D452E", +"N c #33371E", +"O c #060707", +"P c #070707", +"Q c #090908", +"R c #090A07", +"S c #090907", +"T c #3C756B", +"U c #71B8A5", +"V c #24584E", +"W c #1B2E25", +"X c #292D1E", +"Y c #2B302A", +"Z c #474D43", +"` c #3A442D", +" . c #353A20", +".. c #060708", +"+. c #080808", +"@. c #080908", +"#. c #0A0B07", +"$. c #090808", +"%. c #367166", +"&. c #6FB7A4", +"*. c #26594F", +"=. c #1E2F24", +"-. c #2A2D1E", +";. c #2F342D", +">. c #999A99", +",. c #363D25", +"'. c #3B3C1C", +"). c #070708", +"!. c #080909", +"~. c #0A0A0A", +"{. c #346E63", +"]. c #76B9A4", +"^. c #1C4942", +"/. c #223023", +"(. c #292E1D", +"_. c #555754", +":. c #A4A4A3", +"<. c #61624D", +"[. c #22210E", +"}. c #090909", +"|. c #0A0B08", +"1. c #33685E", +"2. c #5EA694", +"3. c #092A25", +"4. c #3B453B", +"5. c #767675", +"6. c #646464", +"7. c #A7A7A7", +"8. c #B2B2B2", +"9. c #9D9D9D", +"0. c #6B6B6B", +"a. c #4C4C4C", +"b. c #424241", +"c. c #4D4D4C", +"d. c #7C8C8A", +"e. c #A1A7A6", +"f. c #888888", +"g. c #6A6A6A", +"h. c #757575", +"i. c #7F7F7F", +"j. c #A0A0A0", +"k. c #9E9E9E", +"l. c #9F9F9F", +" . . . + + @ @ # ", +" @ $ % % % $ % & * = - = ", +"# ; ; > > , , > ; ; ' ' ) ) ! ", +"! ~ ~ ~ ~ { ] { ] ~ { ^ ^ ~ ~ ", +"@ / ( _ : _ < : _ [ [ [ _ ( ^ ", +"} | 1 2 3 4 2 5 4 4 3 6 7 * ' ", +"8 9 0 a b c = ! % | d e f ; g ", +"h i j k l m n o p q r s t u v ", +"w x y z A B C D E F G H I J K ", +"L M N O P Q R R S T U V W X Y ", +"Z ` ...+.@.S #.$.%.&.*.=.-.;. ", +">.,.'.).+.!.~.#.S {.].^./.(._.$ ", +"r :.<.[.!.}.Q |.Q 1.2.3.4.5.6.- ", +" 7.8.:.9.0.a.b.c.d.e.% f.7 g. ", +" 8.$ h.i.f.^ * # ^ < , ] ", +" q j.! c n k.l. "}; diff --git a/apps/RawLogViewer/imgs/tree_icon12.xpm b/apps/RawLogViewer/imgs/tree_icon12.xpm new file mode 100644 index 0000000000..7f6ab6b57d --- /dev/null +++ b/apps/RawLogViewer/imgs/tree_icon12.xpm @@ -0,0 +1,110 @@ +/* XPM */ +static const char * tree_icon12_xpm[] = { +"16 16 91 1", +" c None", +". c #878C90", +"+ c #737577", +"@ c #75777A", +"# c #7D8184", +"$ c #82868A", +"% c #6C6F70", +"& c #808487", +"* c #707173", +"= c #888889", +"- c #949190", +"; c #9EA2A7", +"> c #828689", +", c #757778", +"' c #868C91", +") c #8F8987", +"! c #949696", +"~ c #808386", +"{ c #797F85", +"] c #8A8A88", +"^ c #868A8F", +"/ c #7D848C", +"( c #777C84", +"_ c #8B807B", +": c #797A7A", +"< c #66696E", +"[ c #898D91", +"} c #858B90", +"| c #9FA4A5", +"1 c #B8AD92", +"2 c #948D7D", +"3 c #A0977E", +"4 c #C7AD7A", +"5 c #B79F73", +"6 c #BEA774", +"7 c #BBA473", +"8 c #BCA778", +"9 c #938D7A", +"0 c #707375", +"a c #878B8E", +"b c #74797F", +"c c #8A8C8C", +"d c #A6997B", +"e c #8B826D", +"f c #A59775", +"g c #D0B16D", +"h c #CFB06E", +"i c #D7B672", +"j c #DBBD79", +"k c #D8BD7F", +"l c #B7AB8D", +"m c #94999C", +"n c #92979D", +"o c #81878B", +"p c #787C81", +"q c #76797E", +"r c #707172", +"s c #666A6B", +"t c #676968", +"u c #6E6E6A", +"v c #727370", +"w c #7B7C78", +"x c #848583", +"y c #90928F", +"z c #989D9F", +"A c #A1A7AD", +"B c #8A8F92", +"C c #8A8D8F", +"D c #95999D", +"E c #777A7E", +"F c #7D8187", +"G c #8D8884", +"H c #8F9293", +"I c #83878B", +"J c #878B8D", +"K c #767B81", +"L c #928981", +"M c #9A9C9A", +"N c #7C7F83", +"O c #888C8E", +"P c #7F858A", +"Q c #908A86", +"R c #878B8F", +"S c #888C90", +"T c #989CA1", +"U c #9A9EA3", +"V c #9FA3A9", +"W c #7A7D82", +"X c #828687", +"Y c #7A7E83", +"Z c #8B8E92", +" . ", +" +@ #$ ", +" % & ", +" * =-; > ", +" , ') ! > ", +" ~ { ] ", +" ^ /( _:< [ ", +" }|1234567890 a ", +" bcdefghijklmno ", +" pqrstuvwxyzA B ", +" CD EF GH I ", +" J K LM N ", +" O P Q R ", +" STUV W ", +" XS YY ", +" Z "}; diff --git a/apps/RawLogViewer/imgs/tree_icon7.xpm b/apps/RawLogViewer/imgs/tree_icon7.xpm index ab581a323a..2901073ed6 100644 --- a/apps/RawLogViewer/imgs/tree_icon7.xpm +++ b/apps/RawLogViewer/imgs/tree_icon7.xpm @@ -1,24 +1,144 @@ /* XPM */ static const char * tree_icon7_xpm[] = { -"16 16 5 1", -" c None", -". c #6F4DEC", -"+ c #1D0869", -"@ c #3C1EAC", -"# c #0C0525", -"........... ", -"+........... ", -"++........... ", -"+++........... ", -"+++@@@@@@@@@@@ ", -"+++@@@@@@@@@@@ ", -"+++@@@@@@@@@@@ ", -"+++@@@@@@@@@@@ ", -"+++@@@@@@@@@@@ ", -"+++@+++++++++@ ", -"++++#########@ ", -"+++#########+@ ", -"+++#########+@ ", -" ++#########+@ ", -" +@#########@ ", -" @@@@@@@@@@@ "}; +"16 16 125 2", +" c None", +". c #4A494A", +"+ c #454546", +"@ c #413F42", +"# c #3C3D3F", +"$ c #444242", +"% c #3F3D3E", +"& c #413E40", +"* c #434140", +"= c #433F41", +"- c #434344", +"; c #353639", +"> c #494949", +", c #494645", +"' c #414042", +") c #403F40", +"! c #454243", +"~ c #454342", +"{ c #454343", +"] c #3D3D40", +"^ c #414953", +"/ c #36373A", +"( c #444140", +"_ c #464342", +": c #434143", +"< c #43464A", +"[ c #333337", +"} c #3A3A3E", +"| c #586470", +"1 c #3F434B", +"2 c #312F32", +"3 c #28272B", +"4 c #313233", +"5 c #2E2E33", +"6 c #32353B", +"7 c #2F3946", +"8 c #38475C", +"9 c #58585F", +"0 c #263648", +"a c #131E29", +"b c #212C38", +"c c #222F3E", +"d c #393F4D", +"e c #5B6169", +"f c #5C5655", +"g c #213141", +"h c #102F46", +"i c #1E3245", +"j c #2D2B2D", +"k c #474442", +"l c #908781", +"m c #515659", +"n c #47494C", +"o c #4C4846", +"p c #1D1E20", +"q c #1A2125", +"r c #1C2329", +"s c #343132", +"t c #484645", +"u c #666462", +"v c #656765", +"w c #5A636C", +"x c #464443", +"y c #1E2023", +"z c #171E21", +"A c #20252A", +"B c #312C2C", +"C c #3C3A3B", +"D c #282C33", +"E c #414347", +"F c #625F5C", +"G c #5D5E5D", +"H c #4C535A", +"I c #1F2226", +"J c #171C20", +"K c #1C2124", +"L c #454A55", +"M c #5B6779", +"N c #8A9397", +"O c #4C5056", +"P c #464647", +"Q c #5A5854", +"R c #525353", +"S c #313439", +"T c #27282D", +"U c #4C5561", +"V c #8E8C72", +"W c #C0A85B", +"X c #9E833B", +"Y c #3A3C3F", +"Z c #434546", +"` c #3F4142", +" . c #585653", +".. c #303336", +"+. c #242930", +"@. c #58523D", +"#. c #A78837", +"$. c #78693E", +"%. c #3E3F3B", +"&. c #32363B", +"*. c #3D4245", +"=. c #3E4040", +"-. c #414242", +";. c #2C3033", +">. c #292E32", +",. c #3A3D3D", +"'. c #30353A", +"). c #222B36", +"!. c #2C3237", +"~. c #2F3235", +"{. c #414447", +"]. c #3A3B3C", +"^. c #282D30", +"/. c #262C2F", +"(. c #2F3337", +"_. c #2A2E33", +":. c #2F3336", +"<. c #363A3D", +"[. c #48494B", +"}. c #323536", +"|. c #2E3134", +"1. c #303338", +"2. c #34363A", +"3. c #3A3E40", +"4. c #3B3F41", +" . + @ ", +" # $ % & * = - ", +" ; > , ' ) ! ~ { ] ", +" ^ / - ( _ : < [ } ", +" | 1 2 3 4 5 6 7 8 ", +" 9 0 a b c d e ", +" f g h i j k l ", +" m n o p q r s t u ", +" v w x y z A B C D E ", +" F G H I J K L M N O ", +" P Q R S T U V W X Y ", +" Z ` ...+.@.#.$.%.&.*. ", +" =.-.;.>.,.'.).!.~.{. ", +" ].^./.(._.:.<. ", +" [.}.|.1.2. ", +" 3.4. "}; diff --git a/apps/RawLogViewer/xRawLogViewerMain.cpp b/apps/RawLogViewer/xRawLogViewerMain.cpp index 93d9f137d0..4820f2bc33 100644 --- a/apps/RawLogViewer/xRawLogViewerMain.cpp +++ b/apps/RawLogViewer/xRawLogViewerMain.cpp @@ -84,6 +84,9 @@ #include #include "imgs/tree_icon1.xpm" +#include "imgs/tree_icon10.xpm" +#include "imgs/tree_icon11.xpm" +#include "imgs/tree_icon12.xpm" #include "imgs/tree_icon2.xpm" #include "imgs/tree_icon3.xpm" #include "imgs/tree_icon4.xpm" @@ -1655,6 +1658,9 @@ xRawLogViewerFrame::xRawLogViewerFrame(wxWindow* parent, wxWindowID id) imgList->Add(wxIcon(tree_icon7_xpm)); imgList->Add(wxIcon(tree_icon8_xpm)); imgList->Add(wxIcon(tree_icon9_xpm)); + imgList->Add(wxIcon(tree_icon10_xpm)); + imgList->Add(wxIcon(tree_icon11_xpm)); + imgList->Add(wxIcon(tree_icon12_xpm)); tree_view->AssignImageList(imgList); diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index e86667a302..f026e8f3f4 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -1,6 +1,9 @@ \page changelog Change Log # Version 2.3.2: UNRELEASED +- Changes in applications: + - RawLogViewer: + - More tree view icons. - Changes in libraries: - \ref mrpt_gui_grp - mrpt::gui::CDisplayWindowGUI: improved API to allow multiple callback handlers, and to report exceptions in them. From 5b44809d487113ce9a28c6110056835fbdc31043 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Sat, 5 Jun 2021 10:38:44 +0200 Subject: [PATCH 11/68] missing include --- apps/RawLogViewer/CRawlogTreeView.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/RawLogViewer/CRawlogTreeView.cpp b/apps/RawLogViewer/CRawlogTreeView.cpp index 61b4519e11..bb0af2b7ac 100644 --- a/apps/RawLogViewer/CRawlogTreeView.cpp +++ b/apps/RawLogViewer/CRawlogTreeView.cpp @@ -36,6 +36,7 @@ std::atomic_bool CRawlogTreeView::RAWLOG_UNDERGOING_CHANGES{false}; #define MRPT_NO_WARN_BIG_HDR // It's ok here #include #include +#include // this one is in mrpt-maps using namespace mrpt; using namespace mrpt::system; From ca65a4dfaf7a4e95130b1ba0c69d6fabcd0b3dc1 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Sat, 5 Jun 2021 12:04:25 +0200 Subject: [PATCH 12/68] new export-to-txt API in CObservation --- apps/rawlog-edit/CMakeLists.txt | 1 + apps/rawlog-edit/rawlog-edit_2d-scans.cpp | 112 +-------------- apps/rawlog-edit/rawlog-edit_anemometer.cpp | 117 +-------------- apps/rawlog-edit/rawlog-edit_export_txt.cpp | 135 ++++++++++++++++++ apps/rawlog-edit/rawlog-edit_imu.cpp | 130 +---------------- apps/rawlog-edit/rawlog-edit_main.cpp | 24 +++- apps/rawlog-edit/rawlog-edit_odometry.cpp | 130 +---------------- doc/man-pages/pod/rawlog-edit.pod | 23 ++- doc/source/doxygen-docs/app_rawlog-edit.md | 16 +++ doc/source/doxygen-docs/changelog.md | 4 + libs/obs/include/mrpt/obs/CObservation.h | 40 ++++-- .../mrpt/obs/CObservation2DRangeScan.h | 7 + .../mrpt/obs/CObservationBatteryState.h | 5 + .../mrpt/obs/CObservationBeaconRanges.h | 5 + libs/obs/include/mrpt/obs/CObservationIMU.h | 5 + .../include/mrpt/obs/CObservationOdometry.h | 5 + .../mrpt/obs/CObservationReflectivity.h | 5 + .../include/mrpt/obs/CObservationRobotPose.h | 5 + .../include/mrpt/obs/CObservationWindSensor.h | 15 +- .../mrpt/obs/CObservationWirelessPower.h | 5 + libs/obs/src/CObservation2DRangeScan.cpp | 27 ++++ libs/obs/src/CObservationBatteryState.cpp | 18 +++ libs/obs/src/CObservationBeaconRanges.cpp | 25 +++- libs/obs/src/CObservationIMU.cpp | 30 ++++ libs/obs/src/CObservationOdometry.cpp | 25 ++++ libs/obs/src/CObservationReflectivity.cpp | 10 ++ libs/obs/src/CObservationRobotPose.cpp | 45 ++++++ libs/obs/src/CObservationWindSensor.cpp | 16 +++ libs/obs/src/CObservationWirelessPower.cpp | 9 ++ 29 files changed, 493 insertions(+), 501 deletions(-) create mode 100644 apps/rawlog-edit/rawlog-edit_export_txt.cpp diff --git a/apps/rawlog-edit/CMakeLists.txt b/apps/rawlog-edit/CMakeLists.txt index cd69a341e1..63cdf83713 100644 --- a/apps/rawlog-edit/CMakeLists.txt +++ b/apps/rawlog-edit/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(${PROJECT_NAME} rawlog-edit_gps_gas.cpp rawlog-edit_info.cpp rawlog-edit_externalize.cpp + rawlog-edit_export_txt.cpp rawlog-edit_deexternalize.cpp rawlog-edit_filters.cpp rawlog-edit_cuts.cpp diff --git a/apps/rawlog-edit/rawlog-edit_2d-scans.cpp b/apps/rawlog-edit/rawlog-edit_2d-scans.cpp index ded96292f1..887bd5f6d8 100644 --- a/apps/rawlog-edit/rawlog-edit_2d-scans.cpp +++ b/apps/rawlog-edit/rawlog-edit_2d-scans.cpp @@ -18,117 +18,13 @@ using namespace mrpt::rawlogtools; using namespace mrpt::io; using namespace std; +DECLARE_OP_FUNCTION(op_export_txt); + // ====================================================================== // op_export_2d_scans_txt // ====================================================================== DECLARE_OP_FUNCTION(op_export_2d_scans_txt) { - // A class to do this operation: - class CRawlogProcessor_Export2DSCANS_TXT - : public CRawlogProcessorOnEachObservation - { - protected: - string m_inFile; - - map lstFiles, lstFilesTimes; - string m_filPrefix; - - public: - size_t m_entriesSaved; - - CRawlogProcessor_Export2DSCANS_TXT( - CFileGZInputStream& in_rawlog, TCLAP::CmdLine& cmdline, - bool Verbose) - : CRawlogProcessorOnEachObservation(in_rawlog, cmdline, Verbose), - m_entriesSaved(0) - { - getArgValue(cmdline, "input", m_inFile); - - m_filPrefix = - extractFileDirectory(m_inFile) + extractFileName(m_inFile); - } - - // return false on any error. - bool processOneObservation(CObservation::Ptr& o) override - { - if (!IS_CLASS(*o, CObservation2DRangeScan)) return true; - - const CObservation2DRangeScan* obs = - dynamic_cast(o.get()); - - auto it = lstFiles.find(obs->sensorLabel); - - FILE *f_this, *f_this_times; - - if (it == lstFiles.end()) // A new file for this sensorlabel?? - { - const std::string fileName = m_filPrefix + string("_") + - fileNameStripInvalidChars(obs->sensorLabel) + - string(".txt"); - - const std::string fileNameTimes = m_filPrefix + string("_") + - fileNameStripInvalidChars(obs->sensorLabel) + - string("_times.txt"); - - VERBOSE_COUT << "Writing LASER TXT file: " << fileName << endl; - - f_this = lstFiles[obs->sensorLabel] = - os::fopen(fileName.c_str(), "wt"); - f_this_times = lstFilesTimes[obs->sensorLabel] = - os::fopen(fileNameTimes.c_str(), "wt"); - if (!f_this || !f_this_times) - THROW_EXCEPTION_FMT( - "Cannot open output file for write: %s", - fileName.c_str()); - } - else - { - f_this = it->second; - f_this_times = lstFilesTimes.find(obs->sensorLabel)->second; - } - - // Time: - const double sampleTime = timestampTotime_t(obs->timestamp); - ::fprintf(f_this_times, "%14.4f\n", sampleTime); - - // Ranges: - for (size_t j = 0; j < obs->getScanSize(); j++) - ::fprintf( - f_this, "%.6f ", - obs->getScanRangeValidity(j) ? obs->getScanRange(j) : 0); - ::fprintf(f_this, "\n"); - - m_entriesSaved++; - - return true; // All ok - } - - // Destructor: close files and generate summary files: - virtual ~CRawlogProcessor_Export2DSCANS_TXT() - { - for (auto it = lstFiles.begin(); it != lstFiles.end(); ++it) - { - os::fclose(it->second); - } - - // Save the joint file: - // ------------------------- - VERBOSE_COUT << "Number of different 2D-SCAN sensorLabels : " - << lstFiles.size() << endl; - - lstFiles.clear(); - } // end of destructor - }; - - // Process - // --------------------------------- - CRawlogProcessor_Export2DSCANS_TXT proc(in_rawlog, cmdline, verbose); - proc.doProcessRawlog(); - - // Dump statistics: - // --------------------------------- - VERBOSE_COUT << "Time to process file (sec) : " << proc.m_timToParse - << "\n"; - VERBOSE_COUT << "Number of records saved : " - << proc.m_entriesSaved << "\n"; + // Forward: + op_export_txt(in_rawlog, cmdline, verbose); } diff --git a/apps/rawlog-edit/rawlog-edit_anemometer.cpp b/apps/rawlog-edit/rawlog-edit_anemometer.cpp index 639d66fc91..6b91cfe39f 100644 --- a/apps/rawlog-edit/rawlog-edit_anemometer.cpp +++ b/apps/rawlog-edit/rawlog-edit_anemometer.cpp @@ -18,122 +18,13 @@ using namespace mrpt::rawlogtools; using namespace mrpt::io; using namespace std; +DECLARE_OP_FUNCTION(op_export_txt); + // ====================================================================== // op_export_anemometer_txt // ====================================================================== DECLARE_OP_FUNCTION(op_export_anemometer_txt) { - // A class to do this operation: - class CRawlogProcessor_ExportANEMOMETER_TXT - : public CRawlogProcessorOnEachObservation - { - protected: - string m_inFile; - - map lstFiles; - string m_filPrefix; - - public: - size_t m_entriesSaved; - - CRawlogProcessor_ExportANEMOMETER_TXT( - CFileGZInputStream& in_rawlog, TCLAP::CmdLine& cmdline, - bool Verbose) - : CRawlogProcessorOnEachObservation(in_rawlog, cmdline, Verbose), - m_entriesSaved(0) - { - getArgValue(cmdline, "input", m_inFile); - - m_filPrefix = - extractFileDirectory(m_inFile) + extractFileName(m_inFile); - } - - // return false on any error. - bool processOneObservation(CObservation::Ptr& o) override - { - if (!IS_CLASS(*o, CObservationWindSensor)) return true; - - const CObservationWindSensor* obs = - dynamic_cast(o.get()); - - auto it = lstFiles.find(obs->sensorLabel); - - FILE* f_this; - - if (it == lstFiles.end()) // A new file for this sensorlabel?? - { - const std::string fileName = m_filPrefix + string("_") + - fileNameStripInvalidChars(obs->sensorLabel.empty() - ? string("ANEMOMETER") - : obs->sensorLabel) + - string(".txt"); - - VERBOSE_COUT << "Writing anemometer TXT file: " << fileName - << endl; - - f_this = lstFiles[obs->sensorLabel] = - os::fopen(fileName.c_str(), "wt"); - if (!f_this) - THROW_EXCEPTION_FMT( - "Cannot open output file for write: %s", - fileName.c_str()); - - // The first line is a description of the columns: - ::fprintf( - f_this, - "%% " - "%14s " // TIMESTAMP - "%18s %18s " // WIND (mod, direction) - "\n", - "Time", "WIND_MODULE(m/s)", "WIND_DIRECTION (deg)"); - } - else - f_this = it->second; - - // For each entry in this sequence: Compute the timestamp and save - // values: - ASSERT_(obs->timestamp != INVALID_TIMESTAMP); - TTimeStamp t = obs->timestamp; - - double sampleTime = timestampTotime_t(t); - - // Time: - ::fprintf( - f_this, - "%14.4f " // TIMESTAMP - "%3.4f %3.2f" // WIND (mod, direction) - "\n", - sampleTime, obs->speed, obs->direction); - m_entriesSaved++; - return true; // All ok - } - - // Destructor: close files and generate summary files: - ~CRawlogProcessor_ExportANEMOMETER_TXT() - { - for (auto it = lstFiles.begin(); it != lstFiles.end(); ++it) - { - os::fclose(it->second); - } - - // Save the joint file: - // ------------------------- - VERBOSE_COUT << "Number of different anemometer sensorLabels : " - << lstFiles.size() << endl; - - lstFiles.clear(); - } // end of destructor - }; - - // Process - // --------------------------------- - CRawlogProcessor_ExportANEMOMETER_TXT proc(in_rawlog, cmdline, verbose); - proc.doProcessRawlog(); - - // Dump statistics: - // --------------------------------- - VERBOSE_COUT << "Time to process file (sec) : " << proc.m_timToParse - << "\n"; - VERBOSE_COUT << "Number of records saved : " - << proc.m_entriesSaved << "\n"; + // Forward: + op_export_txt(in_rawlog, cmdline, verbose); } diff --git a/apps/rawlog-edit/rawlog-edit_export_txt.cpp b/apps/rawlog-edit/rawlog-edit_export_txt.cpp new file mode 100644 index 0000000000..fb7657fca6 --- /dev/null +++ b/apps/rawlog-edit/rawlog-edit_export_txt.cpp @@ -0,0 +1,135 @@ +/* +------------------------------------------------------------------------+ + | Mobile Robot Programming Toolkit (MRPT) | + | https://www.mrpt.org/ | + | | + | Copyright (c) 2005-2021, Individual contributors, see AUTHORS file | + | See: https://www.mrpt.org/Authors - All rights reserved. | + | Released under BSD License. See: https://www.mrpt.org/License | + +------------------------------------------------------------------------+ */ + +#include + +#include "rawlog-edit-declarations.h" + +using namespace mrpt; +using namespace mrpt::obs; +using namespace mrpt::system; +using namespace mrpt::rawlogtools; +using namespace std; +using namespace mrpt::io; + +// ====================================================================== +// op_export_txt +// ====================================================================== +DECLARE_OP_FUNCTION(op_export_txt) +{ + // A class to do this operation: + class CRawlogProcessor_Export_TXT : public CRawlogProcessorOnEachObservation + { + protected: + string m_inFile; + + map lstFiles; + string m_filPrefix; + + public: + size_t m_entriesSaved; + + CRawlogProcessor_Export_TXT( + CFileGZInputStream& in_rawlog, TCLAP::CmdLine& cmdline, + bool Verbose) + : CRawlogProcessorOnEachObservation(in_rawlog, cmdline, Verbose), + m_entriesSaved(0) + { + getArgValue(cmdline, "input", m_inFile); + + m_filPrefix = + extractFileDirectory(m_inFile) + extractFileName(m_inFile); + } + + // return false on any error. + bool processOneObservation(CObservation::Ptr& obs) override + { + if (!obs->exportTxtSupported()) return true; + + ASSERTMSG_( + !obs->sensorLabel.empty(), + mrpt::format( + "Exporting as TXT requires all observations having " + "non-empty sensorLabels, but empty label found for " + "observation of type '%s'", + obs->GetRuntimeClass()->className)); + + auto it = lstFiles.find(obs->sensorLabel); + + FILE* f_this = nullptr; + + if (it == lstFiles.end()) // A new file for this sensorlabel?? + { + const std::string fileName = m_filPrefix + string("_") + + fileNameStripInvalidChars(obs->sensorLabel.empty() + ? string("ODOMETRY") + : obs->sensorLabel) + + string(".txt"); + + VERBOSE_COUT << "Writing TXT/CSV file: " << fileName << endl; + + f_this = lstFiles[obs->sensorLabel] = + os::fopen(fileName.c_str(), "wt"); + if (!f_this) + THROW_EXCEPTION_FMT( + "Cannot open output file for write: %s", + fileName.c_str()); + + // The first line is a description of the columns: + ::fprintf( + f_this, + "%% " + "%16s " // TIMESTAMP + "%s\n", + "Time", obs->exportTxtHeader().c_str()); + } + else + f_this = it->second; + + ASSERT_(obs->timestamp != INVALID_TIMESTAMP); + double sampleTime = mrpt::Clock::toDouble(obs->timestamp); + + // Time: + ::fprintf( + f_this, + "%16.6f " // TIMESTAMP + "%s\n", + sampleTime, obs->exportTxtDataRow().c_str()); + m_entriesSaved++; + return true; // All ok + } + + // Destructor: close files and generate summary files: + ~CRawlogProcessor_Export_TXT() + { + for (auto it = lstFiles.begin(); it != lstFiles.end(); ++it) + if (it->second) os::fclose(it->second); + + // Save the joint file: + // ------------------------- + VERBOSE_COUT + << "Number of different sensorLabels exported to TXT/CSV: " + << lstFiles.size() << endl; + + lstFiles.clear(); + } // end of destructor + }; + + // Process + // --------------------------------- + CRawlogProcessor_Export_TXT proc(in_rawlog, cmdline, verbose); + proc.doProcessRawlog(); + + // Dump statistics: + // --------------------------------- + VERBOSE_COUT << "Time to process file (sec) : " << proc.m_timToParse + << "\n"; + VERBOSE_COUT << "Number of records saved : " + << proc.m_entriesSaved << "\n"; +} diff --git a/apps/rawlog-edit/rawlog-edit_imu.cpp b/apps/rawlog-edit/rawlog-edit_imu.cpp index 79dacff8e2..5802f840e6 100644 --- a/apps/rawlog-edit/rawlog-edit_imu.cpp +++ b/apps/rawlog-edit/rawlog-edit_imu.cpp @@ -18,135 +18,13 @@ using namespace mrpt::rawlogtools; using namespace std; using namespace mrpt::io; +DECLARE_OP_FUNCTION(op_export_txt); + // ====================================================================== // op_export_imu_txt // ====================================================================== DECLARE_OP_FUNCTION(op_export_imu_txt) { - // A class to do this operation: - class CRawlogProcessor_ExportIMU_TXT - : public CRawlogProcessorOnEachObservation - { - protected: - string m_inFile; - - map lstFiles; - string m_filPrefix; - - public: - size_t m_entriesSaved; - - CRawlogProcessor_ExportIMU_TXT( - CFileGZInputStream& in_rawlog, TCLAP::CmdLine& cmdline, - bool Verbose) - : CRawlogProcessorOnEachObservation(in_rawlog, cmdline, Verbose), - m_entriesSaved(0) - { - getArgValue(cmdline, "input", m_inFile); - - m_filPrefix = - extractFileDirectory(m_inFile) + extractFileName(m_inFile); - } - - // return false on any error. - bool processOneObservation(CObservation::Ptr& o) override - { - if (!IS_CLASS(*o, CObservationIMU)) return true; - - const CObservationIMU* obs = - dynamic_cast(o.get()); - - auto it = lstFiles.find(obs->sensorLabel); - - FILE* f_this; - - if (it == lstFiles.end()) // A new file for this sensorlabel?? - { - const std::string fileName = m_filPrefix + string("_") + - fileNameStripInvalidChars(obs->sensorLabel) + - string(".txt"); - - VERBOSE_COUT << "Writing IMU TXT file: " << fileName << endl; - - f_this = lstFiles[obs->sensorLabel] = - os::fopen(fileName.c_str(), "wt"); - if (!f_this) - THROW_EXCEPTION_FMT( - "Cannot open output file for write: %s", - fileName.c_str()); - - // The first line is a description of the columns: - ::fprintf( - f_this, - "%% " - "%14s " // TIMESTAMP - "%22s %22s %22s " // IMU_{X,Y,Z}_ACC - "%22s %22s %22s " // IMU_YAW_VEL... - "%22s %22s %22s " // IMU_X_VEL... - "%22s %22s %22s " // IMU_YAW... - "%22s %22s %22s " // IMU_X... - "%22s %22s %22s " // MAG_X MAG_Y MAG_Z - "%22s %22s %22s " // PRESS ALTIT TEMP - "\n", - "Time", "IMU_X_ACC", "IMU_Y_ACC", "IMU_Z_ACC", - "IMU_YAW_VEL", "IMU_PITCH_VEL", "IMU_ROLL_VEL", "IMU_X_VEL", - "IMU_Y_VEL", "IMU_Z_VEL", "IMU_YAW", "IMU_PITCH", - "IMU_ROLL", "IMU_X", "IMU_Y", "IMU_Z", "MAG_X", "MAG_Y", - "MAG_Z", "PRESS", "ALTITUDE", "TEMPERATURE"); - } - else - f_this = it->second; - - ASSERT_(obs->dataIsPresent.size() == obs->rawMeasurements.size()); - size_t nValuesPerRow = obs->dataIsPresent.size(); - - // For each entry in this sequence: Compute the timestamp and save - // all 15 values: - ASSERT_(obs->timestamp != INVALID_TIMESTAMP); - TTimeStamp t = obs->timestamp; - - double sampleTime = timestampTotime_t(t); - - // Time: - ::fprintf(f_this, "%14.4f ", sampleTime); - ASSERT_(obs->rawMeasurements.size() == obs->rawMeasurements.size()); - for (size_t idx = 0; idx < nValuesPerRow; idx++) - ::fprintf( - f_this, "%23.16f ", - obs->dataIsPresent[idx] ? obs->rawMeasurements[idx] : 0); - ::fprintf(f_this, "\n"); - - m_entriesSaved++; - - return true; // All ok - } - - // Destructor: close files and generate summary files: - ~CRawlogProcessor_ExportIMU_TXT() - { - for (auto it = lstFiles.begin(); it != lstFiles.end(); ++it) - { - os::fclose(it->second); - } - - // Save the joint file: - // ------------------------- - VERBOSE_COUT << "Number of different IMU sensorLabels : " - << lstFiles.size() << endl; - - lstFiles.clear(); - } // end of destructor - }; - - // Process - // --------------------------------- - CRawlogProcessor_ExportIMU_TXT proc(in_rawlog, cmdline, verbose); - proc.doProcessRawlog(); - - // Dump statistics: - // --------------------------------- - VERBOSE_COUT << "Time to process file (sec) : " << proc.m_timToParse - << "\n"; - VERBOSE_COUT << "Number of records saved : " - << proc.m_entriesSaved << "\n"; + // Forward: + op_export_txt(in_rawlog, cmdline, verbose); } diff --git a/apps/rawlog-edit/rawlog-edit_main.cpp b/apps/rawlog-edit/rawlog-edit_main.cpp index 5f5001a6f8..1d8013ebe4 100644 --- a/apps/rawlog-edit/rawlog-edit_main.cpp +++ b/apps/rawlog-edit/rawlog-edit_main.cpp @@ -46,16 +46,22 @@ using namespace mrpt::io; DECLARE_OP_FUNCTION(op_camera_params); DECLARE_OP_FUNCTION(op_cut); DECLARE_OP_FUNCTION(op_deexternalize); -DECLARE_OP_FUNCTION(op_export_2d_scans_txt); -DECLARE_OP_FUNCTION(op_export_anemometer_txt); -DECLARE_OP_FUNCTION(op_export_enose_txt); + DECLARE_OP_FUNCTION(op_export_gps_all); DECLARE_OP_FUNCTION(op_export_gps_gas_kml); DECLARE_OP_FUNCTION(op_export_gps_kml); +DECLARE_OP_FUNCTION(op_export_enose_txt); DECLARE_OP_FUNCTION(op_export_gps_txt); +DECLARE_OP_FUNCTION(op_export_rawdaq_txt); + +DECLARE_OP_FUNCTION(op_export_txt); +// op_export_txt is a generic replacement of all these: +DECLARE_OP_FUNCTION(op_export_2d_scans_txt); +DECLARE_OP_FUNCTION(op_export_anemometer_txt); DECLARE_OP_FUNCTION(op_export_imu_txt); DECLARE_OP_FUNCTION(op_export_odometry_txt); -DECLARE_OP_FUNCTION(op_export_rawdaq_txt); +// ^^^ + DECLARE_OP_FUNCTION(op_externalize); DECLARE_OP_FUNCTION(op_generate_3d_pointclouds); DECLARE_OP_FUNCTION(op_info); @@ -358,6 +364,16 @@ int main(int argc, char** argv) cmd, false)); ops_functors["export-rawdaq-txt"] = &op_export_rawdaq_txt; + arg_ops.push_back(std::make_unique( + "", "export-txt", + "Op: Generic export observations to TXT/CSV files.\n" + "Generates one .txt file for each different sensor label of " + "all observation classes that supports the export-to-txt API.\n" + "The generated .txt files will be saved in the same path than " + "the input rawlog, as `_.txt`.", + cmd, false)); + ops_functors["export-txt"] = &op_export_txt; + arg_ops.push_back(std::make_unique( "", "export-2d-scans-txt", "Op: Export 2D scans to TXT files.\n" diff --git a/apps/rawlog-edit/rawlog-edit_odometry.cpp b/apps/rawlog-edit/rawlog-edit_odometry.cpp index cb816f6204..55a195e820 100644 --- a/apps/rawlog-edit/rawlog-edit_odometry.cpp +++ b/apps/rawlog-edit/rawlog-edit_odometry.cpp @@ -18,137 +18,15 @@ using namespace mrpt::rawlogtools; using namespace std; using namespace mrpt::io; +DECLARE_OP_FUNCTION(op_export_txt); + // ====================================================================== // op_export_odometry_txt // ====================================================================== DECLARE_OP_FUNCTION(op_export_odometry_txt) { - // A class to do this operation: - class CRawlogProcessor_ExportODO_TXT - : public CRawlogProcessorOnEachObservation - { - protected: - string m_inFile; - - map lstFiles; - string m_filPrefix; - - public: - size_t m_entriesSaved; - - CRawlogProcessor_ExportODO_TXT( - CFileGZInputStream& in_rawlog, TCLAP::CmdLine& cmdline, - bool Verbose) - : CRawlogProcessorOnEachObservation(in_rawlog, cmdline, Verbose), - m_entriesSaved(0) - { - getArgValue(cmdline, "input", m_inFile); - - m_filPrefix = - extractFileDirectory(m_inFile) + extractFileName(m_inFile); - } - - // return false on any error. - bool processOneObservation(CObservation::Ptr& o) override - { - if (!IS_CLASS(*o, CObservationOdometry)) return true; - - const CObservationOdometry* obs = - dynamic_cast(o.get()); - - auto it = lstFiles.find(obs->sensorLabel); - - FILE* f_this; - - if (it == lstFiles.end()) // A new file for this sensorlabel?? - { - const std::string fileName = m_filPrefix + string("_") + - fileNameStripInvalidChars(obs->sensorLabel.empty() - ? string("ODOMETRY") - : obs->sensorLabel) + - string(".txt"); - - VERBOSE_COUT << "Writing odometry TXT file: " << fileName - << endl; - - f_this = lstFiles[obs->sensorLabel] = - os::fopen(fileName.c_str(), "wt"); - if (!f_this) - THROW_EXCEPTION_FMT( - "Cannot open output file for write: %s", - fileName.c_str()); - - // The first line is a description of the columns: - ::fprintf( - f_this, - "%% " - "%14s " // TIMESTAMP - "%18s %18s %18s " // GLOBAL_ODO_{x,y,phi} - "%18s %18s %18s " // HAS, TICKS_L/R - "%18s %18s %18s " // HAS, V,W - "\n", - "Time", "GLOBAL_ODO_X", "GLOBAL_ODO_Y", - "GLOBAL_ODO_PHI_RAD", "HAS_ENCODERS", "LEFT_ENC_INCR_TICKS", - "RIGHT_ENC_INCR_TICKS", "HAS_VELOCITIES", - "LIN_SPEED_MetPerSec", "ANG_SPEED_RadPerSec"); - } - else - f_this = it->second; - - // For each entry in this sequence: Compute the timestamp and save - // all 15 values: - ASSERT_(obs->timestamp != INVALID_TIMESTAMP); - TTimeStamp t = obs->timestamp; - - double sampleTime = timestampTotime_t(t); - - // Time: - ::fprintf( - f_this, - "%14.4f " // TIMESTAMP - "%18.5f %18.5f %18.5f " // GLOBAL_ODO_{x,y,phi} - "%18i %18i %18i " // HAS, TICKS_L/R - "%18i %18.5f %18.5f" // HAS, V,W - "\n", - sampleTime, obs->odometry.x(), obs->odometry.y(), - obs->odometry.phi(), - static_cast(obs->hasEncodersInfo ? 1 : 0), - static_cast(obs->encoderLeftTicks), - static_cast(obs->encoderRightTicks), - static_cast(obs->hasVelocities ? 1 : 0), - obs->velocityLocal.vx, obs->velocityLocal.omega); - m_entriesSaved++; - return true; // All ok - } - - // Destructor: close files and generate summary files: - ~CRawlogProcessor_ExportODO_TXT() - { - for (auto it = lstFiles.begin(); it != lstFiles.end(); ++it) - { - os::fclose(it->second); - } - - // Save the joint file: - // ------------------------- - VERBOSE_COUT << "Number of different odometry sensorLabels : " - << lstFiles.size() << endl; - - lstFiles.clear(); - } // end of destructor - }; - - // Process - // --------------------------------- - CRawlogProcessor_ExportODO_TXT proc(in_rawlog, cmdline, verbose); - proc.doProcessRawlog(); - - // Dump statistics: - // --------------------------------- - VERBOSE_COUT << "Time to process file (sec) : " << proc.m_timToParse - << "\n"; - VERBOSE_COUT << "Number of records saved : " - << proc.m_entriesSaved << "\n"; + // Forward: + op_export_txt(in_rawlog, cmdline, verbose); } // ====================================================================== diff --git a/doc/man-pages/pod/rawlog-edit.pod b/doc/man-pages/pod/rawlog-edit.pod index d955fbfaa0..6498cca4ae 100644 --- a/doc/man-pages/pod/rawlog-edit.pod +++ b/doc/man-pages/pod/rawlog-edit.pod @@ -8,7 +8,7 @@ rawlog-edit - Command-line robotic datasets (rawlogs) manipulation tool ] [--camera-params ] [--sensors-pose ] [--generate-3d-pointclouds] [--cut] [--export-2d-scans-txt] - [--export-rawdaq-txt] [--recalc-odometry] + [--export-txt] [--export-rawdaq-txt] [--recalc-odometry] [--export-anemometer-txt] [--export-enose-txt] [--export-odometry-txt] [--export-imu-txt] [--export-gps-all] [--export-gps-txt] @@ -22,8 +22,8 @@ rawlog-edit - Command-line robotic datasets (rawlogs) manipulation tool [--text-file-output ] [--rectify-centers-coincide] [--image-size ] [--txt-externals] [--image-format ] [--out-dir <.>] [-p - ] [-o ] -i - [--] [--version] [-h] + ] [-o ] -i + [--] [--version] [-h] =head1 USAGE EXAMPLES @@ -44,6 +44,10 @@ B rawlog-edit --cut --to-time 1281619819 \ -i I -o I +B + +rawlog-edit --export-txt -i I + B @@ -94,10 +98,10 @@ These are the supported arguments and operations: Requires: -o (or --output) - Optional: --image-format to set image format (default=jpg), + Optional: --image-format to set image format (default=jpg), --image-size to resize output images (example: --image-size 640x480) - + --camera-params @@ -148,6 +152,15 @@ These are the supported arguments and operations: The generated .txt files will be saved in the same path than the input rawlog, with the same filename + each sensorLabel. + --export-txt + Op: Generic export observations to TXT/CSV files. + + Generates one .txt file for each different sensor label of all + observation classes that supports the export-to-txt API. + + The generated .txt files will be saved in the same path than the input + rawlog, as `_.txt`. + --export-rawdaq-txt Op: Export raw DAQ readings to TXT files. diff --git a/doc/source/doxygen-docs/app_rawlog-edit.md b/doc/source/doxygen-docs/app_rawlog-edit.md index 70c0bbc3bb..810d70147e 100644 --- a/doc/source/doxygen-docs/app_rawlog-edit.md +++ b/doc/source/doxygen-docs/app_rawlog-edit.md @@ -45,6 +45,11 @@ rawlog-edit - Command-line robotic datasets (rawlogs) manipulation tool rawlog-edit --cut --to-time 1281619819 -i in.rawlog -o out.rawlog +**Export all suitable observations to TXT/CSV files:** + + rawlog-edit --export-txt -i in.rawlog + + **Generate a Google Earth KML file with the GPS data in a dataset:** rawlog-edit --export-gps-kml -i in.rawlog @@ -143,6 +148,17 @@ These are the supported arguments and operations: The generated .txt files will be saved in the same path than the input rawlog, with the same filename + each sensorLabel. + + --export-txt + Op: Generic export observations to TXT/CSV files. + + Generates one .txt file for each different sensor label of all + observation classes that supports the export-to-txt API. + + The generated .txt files will be saved in the same path than the input + rawlog, as `_.txt`. + + --export-rawdaq-txt Op: Export raw DAQ readings to TXT files. diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index f026e8f3f4..cfe081ffb4 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -4,9 +4,13 @@ - Changes in applications: - RawLogViewer: - More tree view icons. + - rawlog-edit: + - New operation `--export-txt` exploiting the new export-to-txt API in mrpt::obs::CObservation - Changes in libraries: - \ref mrpt_gui_grp - mrpt::gui::CDisplayWindowGUI: improved API to allow multiple callback handlers, and to report exceptions in them. + - \ref mrpt_obs_grp + - mrpt::obs::CObservation now has a common API to export datasets to TXT/CSV files, see methods exportTxtSupported(), exportTxtHeader(), exportTxtDataRow(). It has been implemented in all suitable observation classes. - \ref mrpt_system_grp - mrpt::system::CTimeLogger: Include custom `name` in underlying mrpt::system::COutputLogger name. - BUG FIXES: diff --git a/libs/obs/include/mrpt/obs/CObservation.h b/libs/obs/include/mrpt/obs/CObservation.h index 7bddad9c8e..13b114ca44 100644 --- a/libs/obs/include/mrpt/obs/CObservation.h +++ b/libs/obs/include/mrpt/obs/CObservation.h @@ -26,18 +26,21 @@ namespace obs * \ingroup mrpt_obs_grp */ #define INVALID_LANDMARK_ID (-1) -/** Declares a class that represents any robot's observation. - * This is a base class for many types of sensor observations. +/** Generic sensor observation. + * + * This is a base virtual class for all types of sensor observations. * Users can add new observation types creating a new class deriving from this - * one. + * one, or reuse those provided in MRPT modules. Most observations are defined + * in \def mrpt_obs_grp. + * + * Observations do not include any information about the robot localization, + * but just raw sensory data and, where aplicable, information about the + * sensor position and orientation in the **local frame** (vehicle frame). * - * IMPORTANT: Observations don't include any information about the robot - * pose, - * just raw sensory data and, where aplicable, information about the sensor - * position and - * orientation in the local frame of the robot. + * Datasets with large number of observations can be managed with + * mrpt::obs::CRawLog. * - * \sa CSensoryFrame, CMetricMap + * \sa CSensoryFrame, CMetricMap, mrpt::obs::CRawLog * \ingroup mrpt_obs_grp */ class CObservation : public mrpt::serialization::CSerializable @@ -153,6 +156,25 @@ class CObservation : public mrpt::serialization::CSerializable /** Return by value version of getDescriptionAsText(std::ostream&) */ std::string getDescriptionAsTextValue() const; + /** @name Export to TXT/CSV API (see the rawlog-edit app) + @{ */ + /** Must return true if the class is exportable to TXT/CSV files, in which + * case the other virtual methods in this group must be redefined too. + */ + virtual bool exportTxtSupported() const { return false; } + + /** Returns the description of the data columns. Timestamp is automatically + * included as the first column, do not list it. See example implementations + * if interested in enabling this in custom CObservation classes. + * Do not include newlines.*/ + virtual std::string exportTxtHeader() const { return {}; } + + /** Returns one row of data with the data stored in this particular object. + * Do not include newlines. */ + virtual std::string exportTxtDataRow() const { return {}; } + + /** @} */ + /** @name Delayed-load manual control methods. @{ */ diff --git a/libs/obs/include/mrpt/obs/CObservation2DRangeScan.h b/libs/obs/include/mrpt/obs/CObservation2DRangeScan.h index b5e4adc794..8155fae58d 100644 --- a/libs/obs/include/mrpt/obs/CObservation2DRangeScan.h +++ b/libs/obs/include/mrpt/obs/CObservation2DRangeScan.h @@ -263,6 +263,13 @@ class CObservation2DRangeScan : public CObservation void filterByExclusionAngles( const std::vector>& angles); + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + + /** @} */ + }; // End of class def. } // namespace obs diff --git a/libs/obs/include/mrpt/obs/CObservationBatteryState.h b/libs/obs/include/mrpt/obs/CObservationBatteryState.h index 0b05d8d967..67a220c8a0 100644 --- a/libs/obs/include/mrpt/obs/CObservationBatteryState.h +++ b/libs/obs/include/mrpt/obs/CObservationBatteryState.h @@ -67,6 +67,11 @@ class CObservationBatteryState : public CObservation void getDescriptionAsText( std::ostream& o) const override; // See base class docs + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/include/mrpt/obs/CObservationBeaconRanges.h b/libs/obs/include/mrpt/obs/CObservationBeaconRanges.h index ce268e197f..b83bee8a51 100644 --- a/libs/obs/include/mrpt/obs/CObservationBeaconRanges.h +++ b/libs/obs/include/mrpt/obs/CObservationBeaconRanges.h @@ -73,6 +73,11 @@ class CObservationBeaconRanges : public CObservation * beacon, or 0 if the beacon is not observed */ float getSensedRangeByBeaconID(int32_t beaconID); + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/include/mrpt/obs/CObservationIMU.h b/libs/obs/include/mrpt/obs/CObservationIMU.h index 1ee8f9f041..e7eb2fb9f1 100644 --- a/libs/obs/include/mrpt/obs/CObservationIMU.h +++ b/libs/obs/include/mrpt/obs/CObservationIMU.h @@ -160,6 +160,11 @@ class CObservationIMU : public CObservation } void getDescriptionAsText(std::ostream& o) const override; + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/include/mrpt/obs/CObservationOdometry.h b/libs/obs/include/mrpt/obs/CObservationOdometry.h index f6557b7d7a..926f71bba6 100644 --- a/libs/obs/include/mrpt/obs/CObservationOdometry.h +++ b/libs/obs/include/mrpt/obs/CObservationOdometry.h @@ -58,6 +58,11 @@ class CObservationOdometry : public CObservation void setSensorPose(const mrpt::poses::CPose3D&) override {} void getDescriptionAsText(std::ostream& o) const override; + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/include/mrpt/obs/CObservationReflectivity.h b/libs/obs/include/mrpt/obs/CObservationReflectivity.h index 0aa8f9558d..e4c6c2e6dd 100644 --- a/libs/obs/include/mrpt/obs/CObservationReflectivity.h +++ b/libs/obs/include/mrpt/obs/CObservationReflectivity.h @@ -56,6 +56,11 @@ class CObservationReflectivity : public CObservation } void getDescriptionAsText(std::ostream& o) const override; + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/include/mrpt/obs/CObservationRobotPose.h b/libs/obs/include/mrpt/obs/CObservationRobotPose.h index 2abcfc3ae3..cb801041f9 100644 --- a/libs/obs/include/mrpt/obs/CObservationRobotPose.h +++ b/libs/obs/include/mrpt/obs/CObservationRobotPose.h @@ -35,6 +35,11 @@ class CObservationRobotPose : public CObservation void getDescriptionAsText( std::ostream& o) const override; // See base class docs + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/include/mrpt/obs/CObservationWindSensor.h b/libs/obs/include/mrpt/obs/CObservationWindSensor.h index a020d5d340..2341be5190 100644 --- a/libs/obs/include/mrpt/obs/CObservationWindSensor.h +++ b/libs/obs/include/mrpt/obs/CObservationWindSensor.h @@ -33,12 +33,10 @@ class CObservationWindSensor : public CObservation /** @name The data members * @{ */ - /** The wind speed in m/s */ - double speed{.0}; - /** The wind flow direction in deg */ - double direction{.0}; - /** The location of the sensing anemometer on the robot coordinate framework - */ + double speed = 0; //!< Wind speed [m/s] + double direction = 0; //!< Wind flow direction [degrees] + + /** The location of the sensing anemometer on the robot frame */ mrpt::poses::CPose3D sensorPoseOnRobot; /** @} */ @@ -48,6 +46,11 @@ class CObservationWindSensor : public CObservation void setSensorPose(const mrpt::poses::CPose3D& newSensorPose) override; void getDescriptionAsText(std::ostream& o) const override; + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/include/mrpt/obs/CObservationWirelessPower.h b/libs/obs/include/mrpt/obs/CObservationWirelessPower.h index dec9168087..32627bf51e 100644 --- a/libs/obs/include/mrpt/obs/CObservationWirelessPower.h +++ b/libs/obs/include/mrpt/obs/CObservationWirelessPower.h @@ -46,6 +46,11 @@ class CObservationWirelessPower : public CObservation void getDescriptionAsText( std::ostream& o) const override; // See base class docs + // See base class docs: + bool exportTxtSupported() const override { return true; } + std::string exportTxtHeader() const override; + std::string exportTxtDataRow() const override; + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/src/CObservation2DRangeScan.cpp b/libs/obs/src/CObservation2DRangeScan.cpp index 9b10e108e3..65791097c1 100644 --- a/libs/obs/src/CObservation2DRangeScan.cpp +++ b/libs/obs/src/CObservation2DRangeScan.cpp @@ -577,3 +577,30 @@ void CObservation2DRangeScan::loadFromVectors( m_validRange[i] = scanValidity[i]; } } + +// See base class docs: +std::string CObservation2DRangeScan::exportTxtHeader() const +{ + std::string ret = "RANGES[i] ... VALID[i]"; + if (hasIntensity()) ret += " ... INTENSITY[i]"; + return ret; +} + +std::string CObservation2DRangeScan::exportTxtDataRow() const +{ + std::stringstream o; + for (size_t i = 0; i < m_scan.size(); i++) + o << format("%.03f ", m_scan[i]); + o << " "; + + for (size_t i = 0; i < m_validRange.size(); i++) + o << format("%u ", m_validRange[i] ? 1 : 0); + o << " "; + + if (hasIntensity()) + { + for (size_t i = 0; i < m_intensity.size(); i++) + o << format("%d ", m_intensity[i]); + } + return o.str(); +} diff --git a/libs/obs/src/CObservationBatteryState.cpp b/libs/obs/src/CObservationBatteryState.cpp index cad3ae4d5b..b6aeb29f39 100644 --- a/libs/obs/src/CObservationBatteryState.cpp +++ b/libs/obs/src/CObservationBatteryState.cpp @@ -99,3 +99,21 @@ void CObservationBatteryState::getDescriptionAsText(std::ostream& o) const (voltageOtherBatteriesValid[i] == true) ? "True" : "False"); } } + +std::string CObservationBatteryState::exportTxtHeader() const +{ + return "VoltageMainRobotBattery " + "VoltageMainRobotComputer " + "[other voltages...]"; +} +std::string CObservationBatteryState::exportTxtDataRow() const +{ + std::string s; + s += mrpt::format("%18.5f ", voltageMainRobotBattery); + s += mrpt::format("%18.5f ", voltageMainRobotComputer); + + for (CVectorDouble::Index i = 0; i < voltageOtherBatteries.size(); i++) + s += mrpt::format("%18.5f ", voltageOtherBatteries[i]); + + return s; +} diff --git a/libs/obs/src/CObservationBeaconRanges.cpp b/libs/obs/src/CObservationBeaconRanges.cpp index ade2ede1a3..8cfe9509d6 100644 --- a/libs/obs/src/CObservationBeaconRanges.cpp +++ b/libs/obs/src/CObservationBeaconRanges.cpp @@ -152,11 +152,28 @@ void CObservationBeaconRanges::getDescriptionAsText(std::ostream& o) const o << " BEACON RANGE SENSOR POSITION ON ROBOT \n"; o << "------------------------------------------------\n"; - for (const auto& it : sensedData) + for (const auto& d : sensedData) { o << format( - " %i %.04f (%.03f,%.03f,%.03f)\n", (int)it.beaconID, - it.sensedDistance, it.sensorLocationOnRobot.x(), - it.sensorLocationOnRobot.y(), it.sensorLocationOnRobot.z()); + " %i %.04f (%.03f,%.03f,%.03f)\n", (int)d.beaconID, + d.sensedDistance, d.sensorLocationOnRobot.x(), + d.sensorLocationOnRobot.y(), d.sensorLocationOnRobot.z()); } } + +std::string CObservationBeaconRanges::exportTxtHeader() const +{ + return "[BEACON_ID RANGE SENSOR_LOCATION_ON_ROBOT] x N \n"; +} +std::string CObservationBeaconRanges::exportTxtDataRow() const +{ + std::stringstream o; + for (const auto& d : sensedData) + { + o << format( + " %i %.04f %.03f %.03f %.03f", (int)d.beaconID, + d.sensedDistance, d.sensorLocationOnRobot.x(), + d.sensorLocationOnRobot.y(), d.sensorLocationOnRobot.z()); + } + return o.str(); +} diff --git a/libs/obs/src/CObservationIMU.cpp b/libs/obs/src/CObservationIMU.cpp index 03249e9d9f..7c8b4af5b2 100644 --- a/libs/obs/src/CObservationIMU.cpp +++ b/libs/obs/src/CObservationIMU.cpp @@ -200,3 +200,33 @@ void CObservationIMU::getDescriptionAsText(std::ostream& o) const DUMP_IMU_DATA(IMU_Y_ACC_GLOBAL) DUMP_IMU_DATA(IMU_Z_ACC_GLOBAL) } + +std::string CObservationIMU::exportTxtHeader() const +{ + return mrpt::format( + "%16s %16s %16s " // IMU_{X,Y,Z}_ACC + "%16s %16s %16s " // IMU_YAW_VEL... + "%16s %16s %16s " // IMU_X_VEL... + "%16s %16s %16s " // IMU_YAW... + "%16s %16s %16s " // IMU_X... + "%16s %16s %16s " // MAG_X MAG_Y MAG_Z + "%16s %16s %16s " // PRESS ALTIT TEMP + "%16s %16s %16s %16s " // ORI_QUAT + "%16s %16s %16s " // YAW_VEL_GLOBAL + "%16s %16s %16s " // X Y Z ACC GLOBAL + , + "IMU_X_ACC", "IMU_Y_ACC", "IMU_Z_ACC", "IMU_WZ", "IMU_WY", "IMU_WX", + "IMU_X_VEL", "IMU_Y_VEL", "IMU_Z_VEL", "IMU_YAW", "IMU_PITCH", + "IMU_ROLL", "IMU_X", "IMU_Y", "IMU_Z", "MAG_X", "MAG_Y", "MAG_Z", + "PRESS", "ALTITUDE", "TEMPERATURE", "ORI_QUAT_X", "ORI_QUAT_Y", + "ORI_QUAT_Z", "ORI_QUAT_W", "YAW_VEL_GLOBAL", "PITCH_VEL_GLOBAL", + "ROLL_VEL_GLOBAL", "X_ACC_GLOBAL", "Y_ACC_GLOBAL", "Z_ACC_GLOBAL"); +} +std::string CObservationIMU::exportTxtDataRow() const +{ + std::string s; + for (size_t idx = 0; idx < rawMeasurements.size(); idx++) + s += mrpt::format( + "%16.8f ", dataIsPresent[idx] ? rawMeasurements[idx] : 0); + return s; +} diff --git a/libs/obs/src/CObservationOdometry.cpp b/libs/obs/src/CObservationOdometry.cpp index 19ab61503f..b6186353ec 100644 --- a/libs/obs/src/CObservationOdometry.cpp +++ b/libs/obs/src/CObservationOdometry.cpp @@ -105,3 +105,28 @@ void CObservationOdometry::getDescriptionAsText(std::ostream& o) const else o << "Velocity info: Not available!\n"; } + +std::string CObservationOdometry::exportTxtHeader() const +{ + return mrpt::format( + "%18s %18s %18s " // GLOBAL_ODO_{x,y,phi} + "%18s %18s %18s " // HAS, TICKS_L/R + "%18s %18s %18s %18s " // HAS, VX, VY,W + , + "GLOBAL_ODO_X", "GLOBAL_ODO_Y", "GLOBAL_ODO_PHI_RAD", "HAS_ENCODERS", + "LEFT_ENC_INCR_TICKS", "RIGHT_ENC_INCR_TICKS", "HAS_VELOCITIES", + "VEL_VX_MetPerSec", "VEL_VY_MetPerSec", "ANG_SPEED_RadPerSec"); +} +std::string CObservationOdometry::exportTxtDataRow() const +{ + return mrpt::format( + "%18.5f %18.5f %18.5f " // GLOBAL_ODO_{x,y,phi} + "%18i %18i %18i " // HAS, TICKS_L/R + "%18i %18.5f %18.5f %18.5f" // HAS, Vx, Vy,W + , + odometry.x(), odometry.y(), odometry.phi(), + static_cast(hasEncodersInfo ? 1 : 0), + static_cast(encoderLeftTicks), static_cast(encoderRightTicks), + static_cast(hasVelocities ? 1 : 0), velocityLocal.vx, + velocityLocal.vy, velocityLocal.omega); +} diff --git a/libs/obs/src/CObservationReflectivity.cpp b/libs/obs/src/CObservationReflectivity.cpp index b839af3364..a494d83e30 100644 --- a/libs/obs/src/CObservationReflectivity.cpp +++ b/libs/obs/src/CObservationReflectivity.cpp @@ -51,3 +51,13 @@ void CObservationReflectivity::getDescriptionAsText(std::ostream& o) const o << "reflectivityLevel=" << reflectivityLevel << std::endl; o << "channel=" << channel << " (-1=any)" << std::endl; } + +std::string CObservationReflectivity::exportTxtHeader() const +{ + return "reflectivityLevel channel"; +} + +std::string CObservationReflectivity::exportTxtDataRow() const +{ + return mrpt::format("%18.5f %5d", reflectivityLevel, channel); +} diff --git a/libs/obs/src/CObservationRobotPose.cpp b/libs/obs/src/CObservationRobotPose.cpp index 1648cd8999..e00a3b6f7c 100644 --- a/libs/obs/src/CObservationRobotPose.cpp +++ b/libs/obs/src/CObservationRobotPose.cpp @@ -63,3 +63,48 @@ void CObservationRobotPose::getDescriptionAsText(std::ostream& o) const o << "Sensor pose: " << sensorPose << endl; o << "Pose: " << pose.asString() << endl; } + +// See base class docs: +std::string CObservationRobotPose::exportTxtHeader() const +{ + return mrpt::format( + "%18s %18s %18s %18s %18s %18s" // POSE + // COV + "%18s %18s %18s %18s %18s %18s" + "%18s %18s %18s %18s %18s" + "%18s %18s %18s %18s" + "%18s %18s %18s" + "%18s %18s" + "%18s", + "X", "Y", "Z", "YAW_RAD", "PITCH_RAD", "ROLL_RAD", // pose + // cov + "var_x", "std_x_y", "std_x_z", "std_x_yaw", "std_x_pitch", + "std_x_roll", // + "var_y", "std_y_z", "std_y_yaw", "std_y_pitch", "std_y_roll", // + "var_z", "std_z_yaw", "std_z_pitch", "std_z_roll", // + "var_yaw", "std_yaw_pitch", "std_yaw_roll", // + "var_pitch", "std_yaw_roll", // + "var_roll"); +} +std::string CObservationRobotPose::exportTxtDataRow() const +{ + const auto& C = pose.cov; + return mrpt::format( + "%18.5f %18.5f %18.5f %18.5f %18.5f %18.5f " // POSE + // cov + "%18.5f %18.5f %18.5f %18.5f %18.5f %18.5f " + "%18.5f %18.5f %18.5f %18.5f %18.5f " + "%18.5f %18.5f %18.5f %18.5f " + "%18.5f %18.5f %18.5f " + "%18.5f %18.5f " + "%18.5f ", + pose.mean.x(), pose.mean.y(), pose.mean.z(), pose.mean.yaw(), + pose.mean.pitch(), pose.mean.roll(), + // cov + C(0, 0), C(0, 1), C(0, 2), C(0, 3), C(0, 4), C(0, 5), // + C(1, 1), C(1, 2), C(1, 3), C(1, 4), C(1, 5), // + C(2, 2), C(2, 3), C(2, 4), C(2, 5), // + C(3, 3), C(3, 4), C(3, 5), // + C(4, 4), C(4, 5), // + C(5, 5)); +} diff --git a/libs/obs/src/CObservationWindSensor.cpp b/libs/obs/src/CObservationWindSensor.cpp index 4d55230490..dc88530509 100644 --- a/libs/obs/src/CObservationWindSensor.cpp +++ b/libs/obs/src/CObservationWindSensor.cpp @@ -67,3 +67,19 @@ void CObservationWindSensor::getDescriptionAsText(std::ostream& o) const { CObservation::getDescriptionAsText(o); } + +std::string CObservationWindSensor::exportTxtHeader() const +{ + return mrpt::format( + "%18s %18s" // WIND (mod, direction) + , + "WIND_MODULE(m/s)", "WIND_DIRECTION (deg)"); +} + +std::string CObservationWindSensor::exportTxtDataRow() const +{ + return mrpt::format( + "%18.5f %18.3f" // WIND (mod, direction) + , + speed, direction); +} diff --git a/libs/obs/src/CObservationWirelessPower.cpp b/libs/obs/src/CObservationWirelessPower.cpp index ffd774dc21..3735444e93 100644 --- a/libs/obs/src/CObservationWirelessPower.cpp +++ b/libs/obs/src/CObservationWirelessPower.cpp @@ -68,3 +68,12 @@ void CObservationWirelessPower::getDescriptionAsText(std::ostream& o) const CObservation::getDescriptionAsText(o); o << format("Measured Power: %.02f/100\n", power); } + +std::string CObservationWirelessPower::exportTxtHeader() const +{ + return mrpt::format("%18s", "POWER[0,100]"); +} +std::string CObservationWirelessPower::exportTxtDataRow() const +{ + return mrpt::format("%18.5f ", power); +} From fd295ab8a8dab7f8d3f1d276780e3a017a37ff07 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 6 Jun 2021 08:41:35 +0200 Subject: [PATCH 13/68] aesthetic minor edit: aligned columns --- libs/obs/src/CObservationIMU.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/obs/src/CObservationIMU.cpp b/libs/obs/src/CObservationIMU.cpp index 7c8b4af5b2..fc3da7e00e 100644 --- a/libs/obs/src/CObservationIMU.cpp +++ b/libs/obs/src/CObservationIMU.cpp @@ -162,7 +162,7 @@ void CObservationIMU::getDescriptionAsText(std::ostream& o) const }; #define DUMP_IMU_DATA(x) \ - o << format("%15s = ", #x); \ + o << format("%20s = ", #x); \ if (dataIsPresent[x]) \ o << format("%10f %s\n", rawMeasurements[x], imu_units[x]); \ else \ From e05d0a2c0d266990498a5f82cdfda160d2edb62c Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 6 Jun 2021 09:36:58 +0200 Subject: [PATCH 14/68] rawlogviewer: show images timestamp --- apps/RawLogViewer/CFormPlayVideo.cpp | 82 ++++++++++++++++++---------- apps/RawLogViewer/CFormPlayVideo.h | 6 +- doc/source/doxygen-docs/changelog.md | 1 + 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/apps/RawLogViewer/CFormPlayVideo.cpp b/apps/RawLogViewer/CFormPlayVideo.cpp index 1f504cc71b..d53f1c0f47 100644 --- a/apps/RawLogViewer/CFormPlayVideo.cpp +++ b/apps/RawLogViewer/CFormPlayVideo.cpp @@ -513,6 +513,7 @@ void CFormPlayVideo::OnbtnPlayClick(wxCommandEvent& event) { wxTheApp->Yield(); CSerializable::Ptr obj; + mrpt::Clock::time_point imgTimestamp = INVALID_TIMESTAMP; if (fil) { archiveFrom(*fil) >> obj; } else @@ -523,25 +524,36 @@ void CFormPlayVideo::OnbtnPlayClick(wxCommandEvent& event) bool doDelay = false; - if (IS_CLASS(*obj, CSensoryFrame)) - { doDelay = showSensoryFrame(obj.get(), nImgs); } + if (auto sf = std::dynamic_pointer_cast(obj); sf) + { doDelay = showSensoryFrame(*sf, nImgs, imgTimestamp); } else if (IS_DERIVED(*obj, CObservation)) { - CSensoryFrame sf; - sf.insert(std::dynamic_pointer_cast(obj)); - doDelay = showSensoryFrame(&sf, nImgs); + CSensoryFrame sf2; + sf2.insert(std::dynamic_pointer_cast(obj)); + doDelay = showSensoryFrame(sf2, nImgs, imgTimestamp); } // Free the loaded object! if (fil) obj.reset(); // Update UI - if ((count++) % 100 == 0) + count++; + // if ((count) % 10 == 0) { progressBar->SetValue( fil ? (int)fil->getPosition() : (int)count); wxString str; - str.sprintf(_("Processed: %d images"), nImgs); + if (imgTimestamp != INVALID_TIMESTAMP) + { + str.sprintf( + _("Processed: %d images. Timestamp: %f [%s UTC]"), + nImgs, mrpt::Clock::toDouble(imgTimestamp), + mrpt::system::dateTimeToString(imgTimestamp).c_str()); + } + else + { + str.sprintf(_("Processed: %d images."), nImgs); + } lbProgress->SetLabel(str); if (!doDelay) wxTheApp->Yield(); } @@ -652,12 +664,12 @@ void CFormPlayVideo::drawHorzRules(mrpt::img::CImage& img) img.line(0, y, w - 1, y, mrpt::img::TColor::white()); } -bool CFormPlayVideo::showSensoryFrame(void* SF, size_t& nImgs) +bool CFormPlayVideo::showSensoryFrame( + mrpt::obs::CSensoryFrame& SF, size_t& nImgs, Clock::time_point& timestamp) { WX_START_TRY - ASSERT_(SF); - auto* sf = (CSensoryFrame*)SF; + auto* sf = &SF; bool doDelay = false; bool doReduceLargeImgs = cbReduceLarge->GetValue(); @@ -685,6 +697,8 @@ bool CFormPlayVideo::showSensoryFrame(void* SF, size_t& nImgs) sf->getObservationByClass(img_idx_sf); if (!obsImg) break; // No more images, go on... + timestamp = obsImg->timestamp; + // Onto which panel to draw?? if (!orderByYaw && !orderByY) { @@ -810,8 +824,10 @@ bool CFormPlayVideo::showSensoryFrame(void* SF, size_t& nImgs) { CObservationStereoImages::Ptr obsImg2 = sf->getObservationByClass(); + if (obsImg2) { + timestamp = obsImg2->timestamp; nImgs++; // Left: @@ -944,8 +960,10 @@ bool CFormPlayVideo::showSensoryFrame(void* SF, size_t& nImgs) { CObservation3DRangeScan::Ptr obs3D = sf->getObservationByClass(); + if (obs3D && obs3D->hasIntensityImage) { + timestamp = obs3D->timestamp; nImgs++; // Intensity channel @@ -997,32 +1015,45 @@ bool CFormPlayVideo::showSensoryFrame(void* SF, size_t& nImgs) return false; } -void CFormPlayVideo::OnprogressBarCmdScrollChanged(wxScrollEvent& event) +void CFormPlayVideo::OnprogressBarCmdScrollChanged(wxScrollEvent&) { int idx = progressBar->GetValue(); m_idxInRawlog = idx; + mrpt::Clock::time_point imgTimestamp; + size_t nImgs = 0; + if (idx > 0 && idx < (int)rawlog.size()) { if (rawlog.getType(idx) == CRawlog::etSensoryFrame) { - size_t dummy = 0; - CSensoryFrame::Ptr sf = rawlog.getAsObservations(idx); - showSensoryFrame(sf.get(), dummy); + showSensoryFrame(*sf.get(), nImgs, imgTimestamp); edIndex->SetValue(idx); } else if (rawlog.getType(idx) == CRawlog::etObservation) { - size_t dummy = 0; - CObservation::Ptr o = rawlog.getAsObservation(idx); CSensoryFrame sf; sf.insert(o); - showSensoryFrame(&sf, dummy); + showSensoryFrame(sf, nImgs, imgTimestamp); edIndex->SetValue(idx); } + + wxString str; + if (imgTimestamp != INVALID_TIMESTAMP) + { + str.sprintf( + _("Processed: %d images. Timestamp: %f [%s UTC]"), nImgs, + mrpt::Clock::toDouble(imgTimestamp), + mrpt::system::dateTimeToString(imgTimestamp).c_str()); + } + else + { + str.sprintf(_("Processed: %d images."), nImgs); + } + lbProgress->SetLabel(str); } } @@ -1103,20 +1134,11 @@ void CFormPlayVideo::saveCamImage(int n) WX_END_TRY } -void CFormPlayVideo::OnbtnSaveCam1Click(wxCommandEvent& event) -{ - saveCamImage(0); -} -void CFormPlayVideo::OnbtnSaveCam2Click(wxCommandEvent& event) -{ - saveCamImage(1); -} -void CFormPlayVideo::OnbtnSaveCam3Click(wxCommandEvent& event) -{ - saveCamImage(2); -} +void CFormPlayVideo::OnbtnSaveCam1Click(wxCommandEvent&) { saveCamImage(0); } +void CFormPlayVideo::OnbtnSaveCam2Click(wxCommandEvent&) { saveCamImage(1); } +void CFormPlayVideo::OnbtnSaveCam3Click(wxCommandEvent&) { saveCamImage(2); } -void CFormPlayVideo::OncbImageDirsSelect(wxCommandEvent& event) +void CFormPlayVideo::OncbImageDirsSelect(wxCommandEvent&) { wxString dir = cbImageDirs->GetValue(); string dirc = string(dir.mb_str()); diff --git a/apps/RawLogViewer/CFormPlayVideo.h b/apps/RawLogViewer/CFormPlayVideo.h index 8ecb975e29..c05c84ac36 100644 --- a/apps/RawLogViewer/CFormPlayVideo.h +++ b/apps/RawLogViewer/CFormPlayVideo.h @@ -24,7 +24,9 @@ #include //*) +#include #include +#include class CFormPlayVideo : public wxDialog { @@ -87,7 +89,9 @@ class CFormPlayVideo : public wxDialog void OncbImageDirsSelect(wxCommandEvent& event); //*) - bool showSensoryFrame(void* SF, size_t& nImgs); + bool showSensoryFrame( + mrpt::obs::CSensoryFrame& SF, size_t& nImgs, + mrpt::Clock::time_point& timestamp); void drawHorzRules(mrpt::img::CImage& img); diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index cfe081ffb4..698f80fd31 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -4,6 +4,7 @@ - Changes in applications: - RawLogViewer: - More tree view icons. + - "Play video" window now also shows timestamps. - rawlog-edit: - New operation `--export-txt` exploiting the new export-to-txt API in mrpt::obs::CObservation - Changes in libraries: From 3005f565fa22b880da28ce22cb087dda07a19bab Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Tue, 8 Jun 2021 00:05:33 +0200 Subject: [PATCH 15/68] docs: slam basics --- ...tutorial-slam-for-beginners-the-basics.rst | 68 +++++++++++++++++++ doc/source/tutorials.rst | 1 + 2 files changed, 69 insertions(+) create mode 100644 doc/source/tutorial-slam-for-beginners-the-basics.rst diff --git a/doc/source/tutorial-slam-for-beginners-the-basics.rst b/doc/source/tutorial-slam-for-beginners-the-basics.rst new file mode 100644 index 0000000000..54ea204560 --- /dev/null +++ b/doc/source/tutorial-slam-for-beginners-the-basics.rst @@ -0,0 +1,68 @@ +.. tutorial-slam-for-beginners-the-basics: + +=========================================================================== +Simultaneous Localization and Mapping (SLAM) for beginners: the basics +=========================================================================== + +.. contents:: :local: + +Videos +==================== + +For those who are new into mobile robotics and want a gentle introduction, I +recommend the following online resources: + +- The `"5 minutes with Cyrill" series `_ (2020-2021). + Here you can see the chapter for SLAM: + +.. raw:: html + +
+ +



+ + +- These `taped seminars `_ by Cyrill Stachniss (2012-2013): + +.. raw:: html + +
+ +



+ +- This course by Dr. Jürgen Sturm (TUM, 2013): + +.. raw:: html + +
+ +



+ +- My 2020 course `"A brief introduction to factor graphs" `_ (5 videos). + Here it is the fifth video, an introduction to the GTSAM library: + +.. raw:: html + +
+ +

+ + +Papers and books +==================== + +- The current must-read SLAM survey paper (2020-2021): + Cadena, C., Carlone, L., Carrillo, H., Latif, Y., Scaramuzza, D., Neira, J., ... & Leonard, J. J. (2016). + Past, present, and future of simultaneous localization and mapping: Toward the robust-perception age. + IEEE Transactions on robotics, 32(6), 1309-1332. (`PDF `_) + +- Our SLAM book, for those who want a rigorous treatment of all probabilistic equations in modern mobile robotics (~2012): + `“Simultaneous Localization and Mapping for Mobile Robots: Introduction and Methods” `_ (Fernández-Madrigal, J.A. and Blanco, J.L., 2012). + +- About EKF-based SLAM with landmark maps (the trend between 1999 and ~2010, outdated nowadays): + + - Hugh Durrant-Whyte & Tim Bailey. “Simultaneous Localisation and Mapping (SLAM): Part I The Essential Algorithms“, (2006) (`PDF `_) + + - Tim Bailey & Hugh Durrant-Whyte and . “Simultaneous Localisation and Mapping (SLAM): Part II State of the Art“, (2006) (`PDF `_) + +- An excellent introductory/advanced book (for everything up to ~2005): `Probabilistic Robotics `_, by Sebastian Thrun, Wolfram Burgard and Dieter Fox. diff --git a/doc/source/tutorials.rst b/doc/source/tutorials.rst index 63c03ea162..5b382f5618 100644 --- a/doc/source/tutorials.rst +++ b/doc/source/tutorials.rst @@ -23,6 +23,7 @@ Note: This page is in the process of being imported from https://www.mrpt.org/tu :maxdepth: 2 :caption: Robotics, SLAM + tutorial-slam-for-beginners-the-basics tutorial-mrpt-maps-model range_only_localization_mapping tutorial-icp-alignment From ae50d86e69cfa4f672b37473d5b50c137d5312ad Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Wed, 9 Jun 2021 23:04:32 +0200 Subject: [PATCH 16/68] clarify docs --- libs/containers/include/mrpt/containers/bimap.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/containers/include/mrpt/containers/bimap.h b/libs/containers/include/mrpt/containers/bimap.h index 7fe1108416..b0be62f793 100644 --- a/libs/containers/include/mrpt/containers/bimap.h +++ b/libs/containers/include/mrpt/containers/bimap.h @@ -101,8 +101,9 @@ class bimap return m_v2k.find(v) != m_v2k.end(); } - /** Get the value associated the given key, KEY->VALUE, raising an - * exception if not present. + /** Get the value associated the given key, KEY->VALUE, raising an + * exception if not present (equivalent to `directMap.at()`). + * * \sa inverse, hasKey, hasValue * \exception std::exception On key not present in the bi-map. */ @@ -114,7 +115,7 @@ class bimap } /** Get the key associated the given value, VALUE->KEY, returning false if - * not present. + * not present (equivalent to `inverseMap.at()`). * \sa direct, hasKey, hasValue * \return false on value not found. */ From 9321d0cf635e7b5cd53bf9c3341f1e63599b10d8 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 10 Jun 2021 00:41:14 +0200 Subject: [PATCH 17/68] docs update --- .../mrpt/obs/CObservation3DRangeScan.h | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/libs/obs/include/mrpt/obs/CObservation3DRangeScan.h b/libs/obs/include/mrpt/obs/CObservation3DRangeScan.h index a63724cfe2..d95346f7cd 100644 --- a/libs/obs/include/mrpt/obs/CObservation3DRangeScan.h +++ b/libs/obs/include/mrpt/obs/CObservation3DRangeScan.h @@ -51,9 +51,7 @@ void unprojectInto( *"rangeImage(ROW,COLUMN)" contains a distance or a depth, depending *on \a range_is_depth. Ranges are stored as uint16_t for efficiency. The units *of ranges are stored separately in rangeUnits. - * - 2D intensity (grayscale or RGB) image (as a mrpt::img::CImage): For - *SwissRanger cameras, a logarithmic A-law compression is used to convert the - *original 16bit intensity to a more standard 8bit graylevel. + * - 2D intensity (grayscale or RGB) image (as a mrpt::img::CImage). * - 2D confidence image (as a mrpt::img::CImage): For each pixel, a 0x00 *and a 0xFF mean the lowest and highest confidence levels, respectively. * - Semantic labels: Stored as a matrix of bitfields, each bit having a @@ -62,18 +60,15 @@ void unprojectInto( *range images are available in the map \a rangeImageOtherLayers. * * The coordinates of the 3D point cloud are in meters with respect to the - *depth camera origin of coordinates - * (in SwissRanger, the front face of the camera: a small offset ~1cm in - *front of the physical focal point), + *depth camera origin of coordinates, * with the +X axis pointing forward, +Y pointing left-hand and +Z pointing *up. By convention, a 3D point with its coordinates set to (0,0,0), will be *considered as invalid. * The field CObservation3DRangeScan::relativePoseIntensityWRTDepth describes - *the change of coordinates from - * the depth camera to the intensity (RGB or grayscale) camera. In a - *SwissRanger camera both cameras coincide, - * so this pose is just a rotation (0,0,0,-90deg,0,-90deg). But in - * Microsoft Kinect there is also an offset, as shown in this figure: + * the change of coordinates from the depth camera to the intensity + * (RGB or grayscale) camera. In some cameras both cameras coincide, + * so this pose would be just a rotation (0,0,0,-90deg,0,-90deg). + * In Microsoft Kinect there is also an offset, as shown in this figure: * *
* @@ -525,8 +520,9 @@ class CObservation3DRangeScan : public CObservation /** Relative pose of the intensity camera wrt the depth camera (which is the * coordinates origin for this observation). - * In a SwissRanger camera, this will be (0,0,0,-90deg,0,-90deg) since - * both cameras coincide. + * In a SwissRanger camera, this will be (0,0,0,-90deg,0,-90deg) since the + * location of both cameras coincide and there is only a rotation due to the + * different axes conventions (see picture above). * In a Kinect, this will include a small lateral displacement and a * rotation, according to the drawing on the top of this page. * \sa doDepthAndIntensityCamerasCoincide @@ -582,7 +578,7 @@ class CObservation3DRangeScan : public CObservation * parameters of a 3D camera given a range (depth) image and the * corresponding 3D point cloud. * \param camera_offset The offset (in meters) in the +X direction of the - * point cloud. It's 1cm for SwissRanger SR4000. + * point cloud. * \return The final average reprojection error per pixel (typ <0.05 px) */ static double recoverCameraCalibrationParameters( From 514d13410e6b3c3e18b3e2776c7bc78895d25b54 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sat, 12 Jun 2021 19:05:29 +0200 Subject: [PATCH 18/68] New 3D navigation key binding --- doc/source/doxygen-docs/changelog.md | 1 + .../tutorial-3d-navigation-cheatsheet.rst | 3 ++- libs/gui/include/mrpt/gui/CGlCanvasBase.h | 3 ++- .../mrpt/gui/internal/NanoGUICanvasHeadless.h | 3 +++ libs/gui/src/CGlCanvasBase.cpp | 2 -- libs/gui/src/CQtGlCanvasBase.cpp | 13 ++++++++++++- libs/gui/src/CWxGLCanvasBase.cpp | 12 +++++++++++- libs/gui/src/NanoGUICanvasHeadless.cpp | 17 ++++++++++++++++- 8 files changed, 47 insertions(+), 7 deletions(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 698f80fd31..992c8bddd5 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -10,6 +10,7 @@ - Changes in libraries: - \ref mrpt_gui_grp - mrpt::gui::CDisplayWindowGUI: improved API to allow multiple callback handlers, and to report exceptions in them. + - New 3D navigation key binding: SHIFT+scroll wheel, for fast up/down pure vertical motion of the camera point. - \ref mrpt_obs_grp - mrpt::obs::CObservation now has a common API to export datasets to TXT/CSV files, see methods exportTxtSupported(), exportTxtHeader(), exportTxtDataRow(). It has been implemented in all suitable observation classes. - \ref mrpt_system_grp diff --git a/doc/source/tutorial-3d-navigation-cheatsheet.rst b/doc/source/tutorial-3d-navigation-cheatsheet.rst index 895e917efd..9c449733d7 100644 --- a/doc/source/tutorial-3d-navigation-cheatsheet.rst +++ b/doc/source/tutorial-3d-navigation-cheatsheet.rst @@ -16,7 +16,8 @@ All MRPT applications use the following convention: - **Pan** (XY plane): Right-button pressed + mouse move. -- **Move camera along Z axis**: SHIFT+Left-button pressed + mouse move left/right. +- **Move camera along Z axis**: SHIFT+Left-button pressed + mouse move left/right, +or (starting in MRPT 2.3.2) SHIFT+scroll wheel for faster up/down vertical motion. The implementation of the features above by handling mouse and keyboard events diff --git a/libs/gui/include/mrpt/gui/CGlCanvasBase.h b/libs/gui/include/mrpt/gui/CGlCanvasBase.h index 04ae131c51..ee3e86d9b1 100644 --- a/libs/gui/include/mrpt/gui/CGlCanvasBase.h +++ b/libs/gui/include/mrpt/gui/CGlCanvasBase.h @@ -42,7 +42,8 @@ class CGlCanvasBase float cameraFOV = 30.f; }; - CGlCanvasBase(); + CGlCanvasBase() = default; + virtual ~CGlCanvasBase(); /** Sets the minimum of the zoom * See also setMaximumZoom(float) */ diff --git a/libs/gui/include/mrpt/gui/internal/NanoGUICanvasHeadless.h b/libs/gui/include/mrpt/gui/internal/NanoGUICanvasHeadless.h index 454bed2c21..aafd3570f9 100644 --- a/libs/gui/include/mrpt/gui/internal/NanoGUICanvasHeadless.h +++ b/libs/gui/include/mrpt/gui/internal/NanoGUICanvasHeadless.h @@ -31,6 +31,9 @@ class NanoGUICanvasHeadless : public mrpt::gui::CGlCanvasBaseHeadless void mouseButtonEvent( const nanogui::Vector2i& p, int button, bool down, int modifiers); void scrollEvent(const nanogui::Vector2i& p, const nanogui::Vector2f& rel); + + private: + int m_lastModifiers = 0; }; } // namespace mrpt::gui::internal #endif // MRPT_HAS_NANOGUI diff --git a/libs/gui/src/CGlCanvasBase.cpp b/libs/gui/src/CGlCanvasBase.cpp index 295675298a..1fd7d9b727 100644 --- a/libs/gui/src/CGlCanvasBase.cpp +++ b/libs/gui/src/CGlCanvasBase.cpp @@ -44,8 +44,6 @@ using mrpt::system::CTicTac; float CGlCanvasBase::SENSIBILITY_DEG_PER_PIXEL = 0.1f; -CGlCanvasBase::CGlCanvasBase() {} - CGlCanvasBase::~CGlCanvasBase() { // Ensure all OpenGL resources are freed before the opengl context is gone: diff --git a/libs/gui/src/CQtGlCanvasBase.cpp b/libs/gui/src/CQtGlCanvasBase.cpp index a0de06fc6c..3e686e6da6 100644 --- a/libs/gui/src/CQtGlCanvasBase.cpp +++ b/libs/gui/src/CQtGlCanvasBase.cpp @@ -118,7 +118,18 @@ void CQtGlCanvasBase::mouseReleaseEvent(QMouseEvent* event) void CQtGlCanvasBase::wheelEvent(QWheelEvent* event) { CamaraParams params = cameraParams(); - updateZoom(params, event->delta()); + if (event->modifiers() != Qt::ShiftModifier) + { + // regular zoom: + updateZoom(params, event->delta()); + } + else + { + // Move vertically +-Z: + params.cameraPointingZ += + event->delta() * params.cameraZoomDistance * 1e-4; + } + setCameraParams(params); updateCamerasParams(); diff --git a/libs/gui/src/CWxGLCanvasBase.cpp b/libs/gui/src/CWxGLCanvasBase.cpp index e8b25afb11..333a65f513 100644 --- a/libs/gui/src/CWxGLCanvasBase.cpp +++ b/libs/gui/src/CWxGLCanvasBase.cpp @@ -123,7 +123,17 @@ void CWxGLCanvasBase::OnMouseMove(wxMouseEvent& event) void CWxGLCanvasBase::OnMouseWheel(wxMouseEvent& event) { CamaraParams params = cameraParams(); - updateZoom(params, event.GetWheelRotation()); + if (!event.ShiftDown()) + { + // regular zoom: + updateZoom(params, event.GetWheelRotation()); + } + else + { + // Move vertically +-Z: + params.cameraPointingZ += + event.GetWheelRotation() * params.cameraZoomDistance * 1e-4; + } setCameraParams(params); Refresh(false); diff --git a/libs/gui/src/NanoGUICanvasHeadless.cpp b/libs/gui/src/NanoGUICanvasHeadless.cpp index 9ab2404335..9292f54f83 100644 --- a/libs/gui/src/NanoGUICanvasHeadless.cpp +++ b/libs/gui/src/NanoGUICanvasHeadless.cpp @@ -21,6 +21,8 @@ void NanoGUICanvasHeadless::mouseMotionEvent( const nanogui::Vector2i& p, const nanogui::Vector2i& rel, int button, int modifiers) { + m_lastModifiers = modifiers; + const bool leftIsDown = button & (1 << GLFW_MOUSE_BUTTON_LEFT); const bool rightIsDown = button & (1 << GLFW_MOUSE_BUTTON_RIGHT); @@ -54,6 +56,8 @@ void NanoGUICanvasHeadless::mouseMotionEvent( void NanoGUICanvasHeadless::mouseButtonEvent( const nanogui::Vector2i& p, int button, bool down, int modifiers) { + m_lastModifiers = modifiers; + setMousePos(p.x(), p.y()); setMouseClicked(down); } @@ -61,7 +65,18 @@ void NanoGUICanvasHeadless::scrollEvent( const nanogui::Vector2i& p, const nanogui::Vector2f& rel) { CamaraParams params = cameraParams(); - updateZoom(params, 125 * rel.y()); + + if (!(m_lastModifiers & GLFW_MOD_SHIFT)) + { + // regular zoom: + updateZoom(params, 125 * rel.y()); + } + else + { + // Move vertically +-Z: + params.cameraPointingZ += + 125 * rel.y() * params.cameraZoomDistance * 1e-4; + } setCameraParams(params); } From 4b58aacff9f14498bf73dfae1c4c2908d5907118 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 14 Jun 2021 00:21:28 +0200 Subject: [PATCH 19/68] PPA packages: Xenial EOL; added Impish --- packaging/prepare_ubuntu_pkgs_for_ppa.sh | 15 ++++----------- packaging/upload_all_mrpt_ppa.sh | 2 -- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packaging/prepare_ubuntu_pkgs_for_ppa.sh b/packaging/prepare_ubuntu_pkgs_for_ppa.sh index 40a6c469b5..3750ce8866 100755 --- a/packaging/prepare_ubuntu_pkgs_for_ppa.sh +++ b/packaging/prepare_ubuntu_pkgs_for_ppa.sh @@ -14,13 +14,13 @@ set -e # List of distributions to create PPA packages for: -# - Xenial LTS EOL: Apr 2021 # - Groovy EOL: Jul 2021 # - Hirsute EOL: Jan 2022 # - Bionic LTS EOL: Apr 2023 # - Focal LTS EOL: Apr 2025 +# - Impish EOL: Jul 2022 if [ -z ${LST_DISTROS+x} ]; then - LST_DISTROS=(xenial bionic focal groovy hirsute) + LST_DISTROS=(bionic focal groovy hirsute impish) fi count=${#LST_DISTROS[@]} @@ -28,15 +28,8 @@ echo "=========================================================================" echo " Ubuntu PPA script for $count distros: ${LST_DISTROS[@]}" echo "=========================================================================" -# Special case for Xenial: enforce g++7 -export MRPT_PKG_CUSTOM_CMAKE_PARAMS_xenial="-DCMAKE_C_COMPILER=/usr/bin/gcc-7 -DCMAKE_CXX_COMPILER=/usr/bin/g++-7" - # Special case for Bionic: use embedded version of simpleini # (Remove these lines after xenial and bionic EOL) -export MRPT_PKG_EXPORTED_SUBMODULES_xenial="simpleini nanoflann" -export DEB_EXTRA_BUILD_DEPS_xenial="cmake" # dummy package (it cannot be blank) -export DEB_NANOFLANN_DEP_xenial="libmrpt-common-dev" # dummy package (it cannot be blank) - export MRPT_PKG_EXPORTED_SUBMODULES_bionic="simpleini nanoflann" export DEB_EXTRA_BUILD_DEPS_bionic="cmake" # dummy package (it cannot be blank) export DEB_NANOFLANN_DEP_bionic="libmrpt-common-dev" # dummy package (it cannot be blank) @@ -88,8 +81,8 @@ rm -fr $MRPT_UBUNTU_OUT_DIR/ # And now create the custom packages for each Ubuntu distribution: # ------------------------------------------------------------------- # Xenial:armhf does not have any version of liboctomap-dev: -export MRPT_RELEASE_EXTRA_OTHERLIBS_URL="https://github.com/OctoMap/octomap/archive/v1.9.1.zip" -export MRPT_RELEASE_EXTRA_OTHERLIBS_PATH="3rdparty/octomap.zip" +#export MRPT_RELEASE_EXTRA_OTHERLIBS_URL="https://github.com/OctoMap/octomap/archive/v1.9.1.zip" +#export MRPT_RELEASE_EXTRA_OTHERLIBS_PATH="3rdparty/octomap.zip" IDXS=$(seq 0 $(expr $count - 1)) diff --git a/packaging/upload_all_mrpt_ppa.sh b/packaging/upload_all_mrpt_ppa.sh index 8116d197b0..dc4a286d0e 100755 --- a/packaging/upload_all_mrpt_ppa.sh +++ b/packaging/upload_all_mrpt_ppa.sh @@ -1,5 +1,3 @@ #!/bin/bash -find xenial -name '*.changes' | xargs -I FIL dput ppa:joseluisblancoc/mrpt-unstable-xenial FIL -rm -fr xenial || true find . -name '*.changes' | xargs -I FIL dput ppa:joseluisblancoc/mrpt FIL From 728810ecc47143a0c2346ce7ea5a026c15575a18 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Tue, 15 Jun 2021 18:11:38 +0200 Subject: [PATCH 20/68] Emit warning on ignoring return --- libs/core/include/mrpt/core/WorkerThreadsPool.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/core/include/mrpt/core/WorkerThreadsPool.h b/libs/core/include/mrpt/core/WorkerThreadsPool.h index bf4f576a61..d3d3350211 100644 --- a/libs/core/include/mrpt/core/WorkerThreadsPool.h +++ b/libs/core/include/mrpt/core/WorkerThreadsPool.h @@ -65,7 +65,7 @@ class WorkerThreadsPool /** Enqueue one new working item, to be executed by threads when any is * available. */ template - auto enqueue(F&& f, Args&&... args) + [[nodiscard]] auto enqueue(F&& f, Args&&... args) -> std::future::type>; /** Returns the number of enqueued tasks, currently waiting for a free From 30b6d71e56d9cb625f7188de0ac907b6c470b632 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Wed, 16 Jun 2021 07:31:43 +0200 Subject: [PATCH 21/68] add docs --- libs/gui/include/mrpt/gui/CDisplayWindowGUI.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h b/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h index 3306ef3abc..28957f77bc 100644 --- a/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h +++ b/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h @@ -56,6 +56,8 @@ struct CDisplayWindowGUI_Params * mrpt::gui::CDisplayWindowGUI win; * // Populate win adding UI controls, etc. * // ... + * win.performLayout(); + * * win.drawAll(); * win.setVisible(true); * nanogui::mainloop(); From d78a4af66a71f8ff478853e490356647f8abce9d Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 17 Jun 2021 18:08:41 +0200 Subject: [PATCH 22/68] YAML macros can handle enums --- doc/source/doxygen-docs/changelog.md | 2 + .../containers/include/mrpt/containers/yaml.h | 62 ++++++++++++------- libs/containers/src/yaml_unittest.cpp | 33 ++++++++++ 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 992c8bddd5..a6f317d447 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -8,6 +8,8 @@ - rawlog-edit: - New operation `--export-txt` exploiting the new export-to-txt API in mrpt::obs::CObservation - Changes in libraries: + - \ref mrpt_containers_grp + - YAML macros `MCP_LOAD_OPT()`, `MCP_LOAD_REQ()`, and `MCP_SAVE()` now also support reading and writing enums directly as YAML, transparently converting numerical values to/from their symbolic names. - \ref mrpt_gui_grp - mrpt::gui::CDisplayWindowGUI: improved API to allow multiple callback handlers, and to report exceptions in them. - New 3D navigation key binding: SHIFT+scroll wheel, for fast up/down pure vertical motion of the camera point. diff --git a/libs/containers/include/mrpt/containers/yaml.h b/libs/containers/include/mrpt/containers/yaml.h index e1fa1a18ca..0424e94c83 100644 --- a/libs/containers/include/mrpt/containers/yaml.h +++ b/libs/containers/include/mrpt/containers/yaml.h @@ -768,14 +768,20 @@ std::ostream& operator<<(std::ostream& o, const yaml& p); * * MCP_LOAD_REQ(p, K); * \endcode + * + * Since MRPT 2.3.2, this also works for enums, converting to textual names of + * values. Note that this requires enums to implement mrpt::typemeta::TEnumType. */ -#define MCP_LOAD_REQ(paramsVariable__, keyproxiedMapEntryName__) \ - if (!paramsVariable__.has(#keyproxiedMapEntryName__)) \ +#define MCP_LOAD_REQ(Yaml__, Var__) \ + if (!Yaml__.has(#Var__)) \ throw std::invalid_argument(mrpt::format( \ "Required parameter `%s` not an existing key in dictionary.", \ - #keyproxiedMapEntryName__)); \ - keyproxiedMapEntryName__ = paramsVariable__[#keyproxiedMapEntryName__] \ - .as() + #Var__)); \ + if constexpr (std::is_enum_v) \ + Var__ = mrpt::typemeta::TEnumType>:: \ + name2value(Yaml__[#Var__].as()); \ + else \ + Var__ = Yaml__[#Var__].as() /** Macro to load a variable from a mrpt::containers::yaml (initials MCP) * dictionary, leaving it with its former value if not found (OPTional). @@ -787,23 +793,32 @@ std::ostream& operator<<(std::ostream& o, const yaml& p); * * MCP_LOAD_OPT(p, K); * \endcode + * + * Since MRPT 2.3.2, this also works for enums, converting to textual names of + * values. Note that this requires enums to implement mrpt::typemeta::TEnumType. */ -#define MCP_LOAD_OPT(paramsVariable__, keyproxiedMapEntryName__) \ - keyproxiedMapEntryName__ = paramsVariable__.getOrDefault( \ - #keyproxiedMapEntryName__, keyproxiedMapEntryName__) +#define MCP_LOAD_OPT(Yaml__, Var__) \ + if constexpr (std::is_enum_v) \ + { \ + if (Yaml__.has(#Var__)) \ + Var__ = mrpt::typemeta::TEnumType>::name2value(Yaml__[#Var__].as()); \ + } \ + else if (Yaml__.has(#Var__)) \ + Var__ = Yaml__[#Var__].as() /** Just like MCP_LOAD_REQ(), but converts the read number from degrees to * radians */ -#define MCP_LOAD_REQ_DEG(paramsVariable__, keyproxiedMapEntryName__) \ - MCP_LOAD_REQ(paramsVariable__, keyproxiedMapEntryName__); \ - keyproxiedMapEntryName__ = mrpt::DEG2RAD(keyproxiedMapEntryName__) +#define MCP_LOAD_REQ_DEG(Yaml__, Var__) \ + MCP_LOAD_REQ(Yaml__, Var__); \ + Var__ = mrpt::DEG2RAD(Var__) /** Just like MCP_LOAD_OPT(), but converts the read number from degrees to * radians */ -#define MCP_LOAD_OPT_DEG(paramsVariable__, keyproxiedMapEntryName__) \ - keyproxiedMapEntryName__ = mrpt::RAD2DEG(keyproxiedMapEntryName__); \ - MCP_LOAD_OPT(paramsVariable__, keyproxiedMapEntryName__); \ - keyproxiedMapEntryName__ = mrpt::DEG2RAD(keyproxiedMapEntryName__) +#define MCP_LOAD_OPT_DEG(Yaml__, Var__) \ + Var__ = mrpt::RAD2DEG(Var__); \ + MCP_LOAD_OPT(Yaml__, Var__); \ + Var__ = mrpt::DEG2RAD(Var__) /** Macro to store a variable into a mrpt::containers::yaml (initials MCP) * dictionary, using as "key" the name of the variable. @@ -819,13 +834,18 @@ std::ostream& operator<<(std::ostream& o, const yaml& p); * // loaded in memory: * MCP_SAVE_DEG(p,K); * \endcode + * + * Since MRPT 2.3.2, this also works for enums, converting to textual names of + * values. Note that this requires enums to implement mrpt::typemeta::TEnumType. */ -#define MCP_SAVE(paramsVariable__, keyproxiedMapEntryName__) \ - paramsVariable__[#keyproxiedMapEntryName__] = keyproxiedMapEntryName__; - -#define MCP_SAVE_DEG(paramsVariable__, keyproxiedMapEntryName__) \ - paramsVariable__[#keyproxiedMapEntryName__] = \ - mrpt::RAD2DEG(keyproxiedMapEntryName__); +#define MCP_SAVE(Yaml__, Var__) \ + if constexpr (std::is_enum_v) \ + Yaml__[#Var__] = mrpt::typemeta::TEnumType< \ + std::remove_cv_t>::value2name(Var__); \ + else \ + Yaml__[#Var__] = Var__; + +#define MCP_SAVE_DEG(Yaml__, Var__) Yaml__[#Var__] = mrpt::RAD2DEG(Var__); } // namespace mrpt::containers diff --git a/libs/containers/src/yaml_unittest.cpp b/libs/containers/src/yaml_unittest.cpp index 12b568d589..9630bd2683 100644 --- a/libs/containers/src/yaml_unittest.cpp +++ b/libs/containers/src/yaml_unittest.cpp @@ -11,6 +11,7 @@ #include #include #include +#include // for enum type tests #include #include // count() @@ -443,6 +444,38 @@ MRPT_TEST(yaml, macros) EXPECT_EQ(Foo, 9.0); EXPECT_THROW(MCP_LOAD_REQ(p, Bar), std::exception); + + { + mrpt::containers::yaml p2; + int i = 10; + MCP_SAVE(p2, i); + + EXPECT_EQ(p2["i"].as(), 10); + + { + mrpt::system::VerbosityLevel vl = mrpt::system::LVL_WARN; + MCP_SAVE(p2, vl); + } + + EXPECT_EQ(p2["vl"].as(), "WARN"); + + { + mrpt::system::VerbosityLevel vl; + MCP_LOAD_REQ(p2, vl); + EXPECT_EQ(vl, mrpt::system::LVL_WARN); + } + { + mrpt::system::VerbosityLevel vl = mrpt::system::LVL_ERROR; + MCP_LOAD_OPT(p2, vl); + EXPECT_EQ(vl, mrpt::system::LVL_WARN); + } + { + auto p3 = p2; + mrpt::system::VerbosityLevel vl = mrpt::system::LVL_ERROR; + p3["vl"] = "FakeEnumValue"; + EXPECT_THROW(MCP_LOAD_OPT(p3, vl), std::exception); + } + } } MRPT_TEST_END() From ab1f878adc0d8ed5222b09cb16d6f8bbd0f9cbcc Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 17 Jun 2021 18:10:12 +0200 Subject: [PATCH 23/68] fix missing header --- libs/containers/include/mrpt/containers/yaml.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/containers/include/mrpt/containers/yaml.h b/libs/containers/include/mrpt/containers/yaml.h index 0424e94c83..11a8c765ff 100644 --- a/libs/containers/include/mrpt/containers/yaml.h +++ b/libs/containers/include/mrpt/containers/yaml.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include From cc52f3b3b1d27d51e6d48fed286004dec4980adf Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 17 Jun 2021 18:25:23 +0200 Subject: [PATCH 24/68] fix: containers now must depend on typemeta headers --- doc/source/images/graph_mrpt_libs.dot | 1 + doc/source/images/graph_mrpt_libs.map | 42 ++-- doc/source/images/graph_mrpt_libs.png | Bin 122828 -> 123255 bytes doc/source/images/graph_mrpt_libs.svg | 266 +++++++++++++------------- libs/containers/CMakeLists.txt | 1 + 5 files changed, 159 insertions(+), 151 deletions(-) diff --git a/doc/source/images/graph_mrpt_libs.dot b/doc/source/images/graph_mrpt_libs.dot index bac402b87c..70773dee0d 100644 --- a/doc/source/images/graph_mrpt_libs.dot +++ b/doc/source/images/graph_mrpt_libs.dot @@ -22,6 +22,7 @@ digraph MRPT_LIBS { containers [label="mrpt-containers",URL="group_mrpt_containers_grp.html"]; containers -> core; + containers -> typemeta; { rank = sink; core [label="mrpt-core",URL="group_mrpt_core_grp.html"]; diff --git a/doc/source/images/graph_mrpt_libs.map b/doc/source/images/graph_mrpt_libs.map index 94bf23e2b7..3ea065e692 100644 --- a/doc/source/images/graph_mrpt_libs.map +++ b/doc/source/images/graph_mrpt_libs.map @@ -1,34 +1,34 @@ - + - - - - - - + + + + + + - - + + - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/doc/source/images/graph_mrpt_libs.png b/doc/source/images/graph_mrpt_libs.png index 5457ce8eac05b16e9a3de71c05ff7e59faf547d9..0ed5e94a611bcdf05ea4442ee98db6b114ec740f 100644 GIT binary patch literal 123255 zcmb@uc{tY5`ZfMYMM4Nkh71v*XfkAoM}|tqsAOzFgCTPzWF8Y5MCObcl7!5mR76Ti zW)TUM42j>`&iVd^>wSO!y`Ae^=Q`5!d_H^M`@Yw`*1C7Fu8sy112+SOLSfRB zv_=%ls!jB(@sozDA-(vY)m8^JsFWr0pQMV6XbNRBMT@%M&^`Y97Z20$xuvD4VcVV5 zM=_GGEWXF&vLDW+sF$Z?SXjxQR*hB`UKJ|&lh!0_ zxcoELSeig_LGmX><}DlfTf=TCvu8np>-a;ObWTr399&cwGbXM z?a$AjKOJT#EuWT`TX#KmeCqX2{y-R)&;IMH1?1(qgM)+bC|#AG|NV{j#EBDK&pg&s zbPWytwi)MHe0p@c;K`E%c6Q<-I`Kh1v~>LobH7YVownpYd>D2Ae#G6KXKQO~SGc;m z%6j}1ru6pKTz~2|!PnN-Hu>&|az)8bF|jp8XWsbc=WqYok{G#Wy-06!f&#^RtRuh5 zt)guo9pz&8v*1GOMyqda=_ci_lJYL2eqCK%KNl~E3TE%vrMPe3KB@-dA~xg34VJ;- zVe4vdnsm>~2MO=0a>%%RHsRacrYJtq^ zjS;+#SgeIVpY3<-+QzfiOP|9 z0#7duGar3;+^FC4OPPzBtLqj=OPwHe@@8Z9|Bi$7W zcmvbyo|ef;hbuo{iaIzrY<>SLILMIq$dZttV6kYF=Gy{GHHx>756$a0Z)hk10Rfsi zIt@=92PhQD6HllJTvmSmb(FUsJ{$;S=G85^cv4tcxEjIk>FGK0;o&9u~8( zooM2f6_gh*UQ|Ci{i@*7unyOzP1kfYu3x9M>&RoGynXxDyRuS=jg2iSIoZdo!ZZ2e zm$EH+8+dpaw{OpPQyO+LIC_**X6H`q6muAl99wg9^V`-|u~+ueta|-rjX(Wo2Z)Mp{#+eh$4VSu|r|ZS7e;VPZG1 zD3m0f1o#eC8>qubh z=;+X|UAyfIohCIc;6ERwd*+M?CF%0a56Yra({xMf;R%^*i&r$$^?!f=w4S0}e6F{u zW80fcUUNemxHfLI9Pcci{Q1+@P~1N;(L9SgIH;XpeJ9(r|q z$)j7DF0Lao_OV*Ga^fadRv&CX*&T|-sZ)NBAMe`dwJ=)|o|Tp5KL1+`G2}4#BC7u# zi?y|N&r_et@(=jZ6|}UW=H=Ix7Bi0*i!(AY^?cT#rsa|6j_Q2%;>A`aw&?iyu!Fay z`ugib5ge;2vvYF-J9n-xcO6&N)@GnsjD2|MzA!7j=lnYKH3WZayStlPBlz zzGX&o+2=kD!QF1jxmcjir;|LFT1Az@+a#d9pIV(a;`{y&-3>E}Fm&dt1Hw!-@< z_kx1jmn%Su)2(~=)+#Gk^}i8)R#D;jZ{8(&Osp(8NHbxQj(sjY6?p0aBdYrbmr-o&U%pB_$OU8oE9~jVf%sTrV=aroX+T z%Q^b6cJ9}3Yu-tPJNNG4Em!87Rj?%-pc?*I4m`cmcWrIHMMXQUtgRh?bjh{noAV24 zL{LLHC40xm-x$yaXR7us7b2(A7cWM9DR&z{8Y(C$3A}xK1NYvGugP!JE>{;09WSrS zk(HjIU*FuTd-ZDVnl)=o9^0_$#LN2p8UI|@*~u+>xjm^dX& zyk}?KO)IZ#l)3voLD}q?JB@;Z!pO+T=B-;tDq4&O*ImAR`Qzu$YbR!hBa@Og+nhUB zhk)CTU!3@*d@5-LW&Qf~D_5=RL(Oq;c7Bd%Yklfy{j{{S4++6(eiLAX((-B2W>~vc z?cSaXy`!Tc0%`%hSm`@E&oV2`4Mxu_b`B1n`i&jU&`GGpzmspYOqdB2A|gL=MMQV+XXfw8#$p`SB$bVP4bP%34@h=yWVPC^Ppz-&g9;x8n8d z*E^g?be+dOFqPQ%G|W$rSz1`=W}EY`Ur)Pp=gxiC7I`*EnX2?uUL7(ubtLI%!*(;O zmzPqR+eAcW=GM}O=@}Vxd-v|`DtBAAxVRV;7Dh!O*x^2D5ifV?z{N2()~k#Jq@=O& zDSG5lZUfzWvLEdFPspD9M=kCh+(QUGFa(!=;)w(FjjK8O+Ny!c4 zSLS18W}clJP8_OyKo#^s_6ELiSX}UE&pozYEr3z1l5^~RjVRf0?nA0ja42bQJ-IM6L%0(pCQbp|CJnw{G&+uL0LDLn;RkuEkoBV0>^S zkr}J!&%1lHHI)wE!@|qUg!Fsp&JG$B*yjOEn>NR4bF`^Ye;*+STl1S~xhoSVCnuN8 zDSCOgA7S_Q+Sk_Ar5fJTU3iycoZs8uzv9S|Bj>xGvhwlq{aka@`r^flv(p{sQ?IsH zSuD&>G4R^SJCArHQlB?A*0r^<^<4SIr1bau&e{2S!R_0RSn^V-%U@)>4=@fjcBHFx zCj$cm@Q)A9f2v;U0se*KC^WO9Ir#-w|GM(XI#AY&>cpr?(_EWibRT&u;1>{J^|8S6 z#hW)qBlBbn5mk=E&AcB!ezf?}Rch0gwgzbv_4J_s6P75?ev)8nIXE~3wr-`r(D}F~ zSuJo9PumNaH_}-=rcu%TYq-Vo^l5*C44uB&sR6Ei?hbMr-c?Oc^ze)7#8(4-wU*eM zr)6YpdzP#fEiyShU7w)fHu>Jz+_JCM-=_1i7``tmIy!ZdPf(wK`Ry_ZNITb&MYcD5HU^OSH2REI^J2}p_+*fyUl97;O+L z=D)tZ%NXg%=SQ~ixis9I>@xOY$mCFZ>x09a7N#2cUoVa^Fudg=_e*K!_HqouArN!E50I=NOXIJaD z#^kAk!12d6H0I{!fW2!k2&IJhkRC+r_4HUrYmV`Da(aHXC~KTQFTumhD-fkgzUBVD z{p6Vw6C;%uXK4?|?Gj70n72VreO`KLcry?s_DWb(G*m|wIHnJa=I6IEj8Bmp?++|3 z`IJL}HFhI8RyWJz<9QAxD)HjT0_>=tIwkDu>#K{RCR6oi%qJUAIpav;f$OW8nv(A{ zsR8jij<$>0*x99+S9xhmo6vYCCks4#_RQh$p9_q<^6c*(9IiW?coj!#&!k8#^|#(~ z8Z_lq^km1AsOjpme8@W)x^{z9b$k0`Q!|AvTUJ$Ep1~2WLyIA+E0zoxEx2zV55>aP zHn=g8f34K~68E0-^jIF8CDIkle!XY^w({y_z{)!`73gIsuU3#v~CfeuS1FPIpOPX-S_Gh%l<4la zC>y!vReXCdc6k>TO2@~?zs)ib<&kruI&-sT(UT-=_(N4y^=-D%HlBU%ENC|g&g@9l zEG;c1>7wo3JICK6dhJ;T8C_=g22+8tNyj83BeUbw3l@N>V4e9$bPtxp%?Uvf5eK40 z4qIHfz;y87L8`iXu#S-6mPYbEO^1Q%0@m?I+kR1abEr;iBG=9{ zj8;}w4#)!}wVRqJ4mRSLpfWXtZ`#}2+neHspVi4xM7MCq>i_;LC3^DlgUB70?a z2cWor@On{Ik?3F@alMj@UmX5?KgOf%#dB?OPV{)mMWziKHth5GNtR1&{+y>w6|{Eays_v%NO3N#rbWn-#^k^x^$^DcmL0hhbQ)~q@zRQ#d}Bj zTIERm%%ew-YKzZx^bQXPUZ-1^0>CgiIoZqP7o5pOqIZ6)%w0xg`5n^I`*n1f1%-qP zoxkZD=9wOo8hd!GXtk1((#K~WI|G<`#c%J{bX>iTFU;GUB5|^e-OI}>-L1A%VG3Cp@!0nE_Wp!_Xwg{KufL8KpvmP8 zjymc?l4V^0JtLzZRWNGIGtHeXcqIUlDiV@~we|JfT!~gK{xqxvJrh$slF5yG_d)@y z>gwxPp@m6#7o_P!j&$o~UOA_gA|fJBorV|)07bS!x;4%_*K|q-DN6wPosOADC05*M zu#7L*pw#K~_fJK1-@bhVK%?sEMUAAbI|I0jOkmNGcT(2#_g0bx?d|Q^+1Z0|4puEA z-Yrc{O}?R_Ye7S_7M(pp>hiz&X@eJ?YdIyVK~u2s@Pr}Ph@U#lmVv5Co*98#;67#5 zD#x!b7)AOL5}BYHdi(kaN*;I-B}Cq$dHF3)s2?~`+uqJj4vLM~46n50yo?szcK)P3v%!NyHECMWJ1lEg z?(_Vu>8x{i*SQajHI=H@pE->)yO!Lczm>XU3BvlwyU=nC%bUz5_k%m8E|( z1ib^1ClySGhJ@_!T5xaAF)o zTmlXwrIxYd`0?X}e?T&~ID0k_1W?k6l8aBBzuiR4i_;N2&>oLw8bB>KH}`GDWjC^y zik`o9-5Zh73N30@xO;jwO&_RD<34owaG^t=DtRqHUG@Zni(q?B40 z!J)gpKnlp1DKa-+xPQMJq>2eADFw z^rU#>D6N%Z&A|r!`#Yn7v!0TYBHs069vMZ!^?NniAA#tX1$OVn!Gq{KbFyAtNz$2} z-O}Q}J3A~_jCSNlo-B7w#(!&%lc(wJwRrygc@mFRugH7f2Zio1tL z#njW}nW4^9Nu0snDA4KqTME?BR<&mAqJq zo0p<~u3=_wK*&3O|0w-?qQ~4hqRX_@X%&**EI@_ycxEJCdGYlaIhED4MP?Gtw&-K5>zDc zsMy%)4~h9lA9905jrrQ569Xhddc@s!?bTBQjq1Z&UjvhqlL4})2J9<19yNeB>RHxS z<;)plY49!*+qSJGrNVdBTEc+>x|rE^#{>ohkdD=)(8|AaogpRZ;X@`my15HG z3}+QOu=P!1f+jaNH|f6|`=77z^Y;&K*5kT*^(q>ae&lIFV#>RIr|0MA#|EOP{J!R% z77~=XA|y1l?uGzhoUS)cdI}h+l;{u+cJ?H!??_wvT5t?f@*1`zg&c#AK;9v1vL&SI`uqD69vDnUAUbDDfax75842oXe`d$`Yb?eq@9NJZ!V)=kP0-~aHXrnk11jEoOgGtIWuZrj_K3DhpH6sD^gcRB( zSX>7#EXa@~*_@nhdoFeb0XJ^MY7~#8l{CG4xgSsyovUx6*SwZy; z8SqF}*JJLKmn*DTu>#0MH`$)vlpCDTDk zjO1h*KI{A#F+)(QYauh`CrN9hU|x+&I#-#X}48^OV|;KLE1v7;SGv?rR4 zn$AH*pm+mzt&w7Ftf@&hY&oc@SxtJL>S`50H|=Jl>@A3Mq#=@G2_QXmZx0h&Fx8n; z&;L2eu=S*np`}sNcd|Ui_T7Vz#pjH(t}*p2WR8?|^fBbPU5mt-WVl%gPU9ui~XgE+heg@yY;L9(#1rNk&K{5t{M z3I>XBD`0<8+pBa0qu8rh4fmpnq@m!HX*dM;^8&%62VYD|9-R)4#=j1U;8kcU)FG(X z0^k;kf{Ka?C4h0GpgiF#N$wW!2dTrg%Z3GtiQn6|y0KiD%Sr^W1OG1Fta6ZpnL{em zh4X*LL$NED>O$Oz8j_NlDs9)kl{AZ3BKN6&{}G84hUVNfVPS) z-K>n9aQV;o8a$?YhVH7pz)0smKMB8Hs{oL{5xl_1a<}cwyz+j4r~^0>WF?mCv^I$V zE~IqRlP6iZxadJ+wrZt6v8-E-e|UFq!IsiXPEM+3X7`WVJLn&;3t&=ncHRY)V0q@u zY63zzID!%Cwj@4cJ|bXyzkd(IQM9ZN;SewOr~?Z`^6jB`SyrQLL)HW}4pUSYi*x5f z|C4&wkX9T$;12uleXkRh`O&;IyvsPEj$|M~HcH#>*Vi`=uC5K}--7@tLz?F-2nN;H zXNylrP|vs#NB$C|3o9?Lq`dkTG~E4x{9e@)RZ9_|O7Gmc(~D5X+ZwVFw7=!?=~tYG z?(f?OHb4k;>Sp=jR8n>)duwLD7qw9Fc_Z&K){%1z(gvwf?97=nr2WKBTbw?<3MKcM z=d`vnXOuRfqSH=2IseHQERQ8(U0gx;AL<05KP%_HZfX@vJ0Q%q2B13t7&%x+PmTx% z02x?)LNwse32oV;j*n_02tJJpZA03&O_;2LnVA`Ib6jE~KT>7(t(iN5g^=?cXMUUn zV8BypkzNdh>CKxrEv&43!90fQ29~_6IO;G;N>$du=_MZ0=c^n%aUSX*!|e{9XW`2-P-5R zS4Krer9=nCEN`KYlzC+skjOIEqoU&7lqj_UmQX-1qwHvNtKr(QLliof}6$M0NQ9N z&+F^gNM+pt)e-*Ug{Je5u8jaf4f$1z;_|_oTglmG&^Ixmhknsjd3BRU1ke5o&uJ>Y z427QX`i7sG#3Ati>>eiiD)_*9Y_VSnr8*UAlT&oI>;aFa~8!u1Y+2YR~yr`X%K10S9!A(FRv)s}@oPl%hywXyvV zGMm~{B+wqUgtN1sdLG0odv%_$R5Xw}up}oCrK(vS#>$8z5NQQy^Z+`o0I5?hHJvmu z;UZuNc)^khqCH`py1smozSt#8aDvc*u;xO5}p9m|qwzh=A z(f;xmHMGBDlhPG;$CgGfo(a2Bbixf0yrJm_Z(AV-NI&Ux>C(aT2SHwgFC(k~1VC$$ zxv&Q4-2F?!tOLLTG8;178F?wuOAH3|ht9NK~3y?ZC7KFq)w0*QJfQ75gf2O>vZz!*~n$qCiP zfBOy6mHrI4-v$SE>dYA{u#jAGPQs|iJsy&=|Eq;!@Pp_PG&7#V6s@0nS^8Vfqv@7B z{))?uiLBiF1VZYDpKA)Fu^o>OPf)*=5)kG`+Nh9qZYiOFk8k>>y~`PkVRqT{jc z( zsu&mM2LTAp!=^oxmCzV5G3+Z>uTF6qY696b=$o;`${JgQz_M)5)YCk8unuXF0OeLK zry@W!Jo?d*$f+JiMn($71i6azRm%8o=)kkDsRuv-H({ ziigG!pA$TrwH8GV-SMsqTsHs#s^IZskS*CG!h)p)M;JDP!gKgJsP(<4s%q9SD+EyT zGUz2XgsPCddNQ&G4Fw1Qq2M+cJ7n|X^YQ zQVMUx$1{SC?$g*2JV=BXJ-MHdx&UqpQ8^h(T;{7TpyOG+dNo=`wgh->T3Ya(iZy1J z6)hGn75R3w{2eZsoG-lTTK{+W63DT)(3w7d`b0ws3k$n(>sA23M^%Y5TDs~U4PCQW z&COocX&$S7f!`u{S4xQBNvt?~xxLR3QFe_5wN&8htG@bG$&3r+1AHj(_w zQi~>9xu_My81nZ0`(=rC{dysQbW>9J#6<~e5ab znUKgzoGDT{pMp1?QVdyGSh#WbZZKMU4*S9im$9R8uyAeKbAGE=H&R(oS!pfO6!{Gh zZ~~H&OrVd&D|IeHn;}nNYHAvM6(kxsqWTP_hbPN9A&n5S5nd|qs>rR19zO?Dl_EMc zTDm{nynUMvn!x!_k5&(5=li300+=l{o}QVo6a6_x><^x7iEpz6#PB0#|}JYq|>jjhlvLM&5LeMxzsDA%gNDCD< z>a&Wv;Zbnn&;<8m6{#6-tKfRDAo>T4Z3n`+{gD^do!Rv61}mSv9;E4y-d9#uRz*XD z4qcJQ@V{%~40jI&fWWkd+L&T6_+#2d2g}{XaJ>Fm<(Uis7vF;;7l7AqZ8whqB<;Ot zUs(fOP54z*cYczb^ftbZ+r1VGD0^*jpWTOCF?kIExYzs;Ib!1#SxsseFG``aH+

;BCX+5piMo31L)2rMo}E?iq;Ih*-&>z|{Z|dqi94A0wz;sSQ0XF_n;%0vwle ziCqL>3mvu(d=ewGDK-bs}!P>90@ojhwXv=Cbsr@@Os!JC=jhqojPR!en`8J zhPgGccQ+swe;@EIk(HM*k-U6JvtA}7K8xAOKDa-%g`;0O^wq8+q>wX1?9K3SCNu?w zZWCrGYpX##0dy+IhB_7&W=}x$lTHiE3Ns#2A1FN#P^TQTxid1I`gF zKp1}a@AHAo6O#AOqyk{T(xc=#^FvbJzbTWLKhPci=&lHbB=a+fLu@y}!4pw{h<$Z| zsb2r)(IQZ32o8es8uVF)A>@cdL`*RV(nh!lnLcx8MW}Nms19W!OA~4q5A*8H zHceZyJ)VKUD;)dx@BjGZ!X|W{;_@1ILN{y|1b2#)gI35e1=P*R*cddN;|Q1{kV$=L zQiv>wRnaHr2}556w;vXrk8SVBU3$=2rKJE-7v`)m@Ry-F0-oD&3dxrs#?{lcW6}Qb zJhpq!fP-8I7H$n3aS->>%q~ldBa-0Gq0)Nedmu6yI7c{OFZXGyYmWnZl5Yh~xLw|V zFcU9Dziyod1PyEkO_m{g40p@fkoA!Gn)Od1** z&^If@DoVGw{ySV{6vbU>WJFkrWE?M8sc-j)*fM zbr(1;fmrR<(o1Js#q2WRIG}g~R5BY)kSG5={&@qbtqH0t4g>n5@4(_6K&K{cQB7&p zN;?B(GOF`3iU!yStWV2?!{R}N08;ZyCWVziwzL%BQwAwLGbB;c&I0L!wO}J!1%hxD z>1-4)b7HaB6R386< zSKcpcSn(Mcjx_qDu5fc-QBb?~?!$)$XwVimHuUGto!fW$&mm{FtTl_TcZH*66KLqBB>J6l*sO+OHZ|r4SqxS9(WBM}ro3MNSz%U+6(jrE7g zikOTc1j1e`egHiza?IPdwp!?kdb+xs<%7k!$mViFQ)Pg|gixL4SEl^&mTpUnS1BqW zdsp4gEWwig(tN*kSHVpRcDmkg-^Aqo4GoFz2bTTL9j>QkWg*SU3<0PtM((KDP*YVP zF&18#b83|n7r+n$Td<$P(i%A37dasng2g4Bb*53ytq~r5OQ6c-H^Q#1T)8p`pF~uC zBX_us8c7lWR7Q9mc8<`7uT2$ifgD;7 zzJfZeyQ=2P5{6ZXjsQMk2c)275ERiwWMrT=VR^N2WUzu$FWI`vT-Kr+^QQ_D()l@1 zKkLSg!8i&7cz38=4@QLb9f^3taSOIcI45A93gY*g-H(TYS#>{xT>WgOqD$4>1Z4NV zZ{LE!YB;@P7dM6;Na`a?h~-OC-24;=ZS9KgSRAVo_YM5 z{5w$<+AP4)a4j8c76%Xrn00;CTFxL4$cU-TljToF7>hITffPHplSXjsprI)^^M>2( zvxd{m7q^}oL@KBx71Cbv=<5NfX4Za!O(=q2IRf^e`+sJl?kitozIyd4c|qR5Y|@Ze zH-!0s0{{=SO+K)lWJaU~()J@+M0FUqY7pX_LmL#!ndbOlbOrF>;#_OPEC|g@9Yg^& zBeMdVM5~*atSugE1*Pw8M@J*@G|&Qf7IFx>ssLn8YXlBz)+!w96ow(lkO5HJ;d+&j zS3f;8Gz3dYkXkI8`_(k$E>73zte zKPx-CUqQhR#BCDk?JpUA00g3VhrNPQ1#3PvU2jB|Kpf{)?E zimk#s^&ePmi^0XT#ZG1G2)Q`PzY7a z?Y@D4L#DnI4o-f7S}asn0!@f74X(J9OF`YeaB$Sts=khwGjL|eqL7T$Mv4a_LE^-d zL(ba%Ab!^LPK-a)dKTiDW{1@B@u{N_G3GiT@M9L1a*m=LH%QB z&xl-pG|!ay>RaVF8pcj8frVUkn?+YTFdL`L;?MU_7^Cq4KQO|R!uAP9V6;Q%$o9Ls zK0%yYVVyO%pn-&ForDy^rQn)763;L+tvd~uFj<~w$)jKj>J|RIAp$OQ>*asYK%i}J zsa#sTj7TWZI4a##XQI=C)KpsRDPNX88;5OXnd87}NIKH6yCahwaAwxa zYR@~l2|lA*6t}7W9NjJTNOzi9Z|*B@A}$F?r$-k|R8?2(Q&c2whsWnX>06wvHv&`v z%pqfc-~fnA7t9q2JAksy7=1Z*>=^kW4hzN^a!+8~n(H3;^?8H}C@?A7m#`Cw$ z_s-(AkiJz99b&ZKab!s#s-pWV#zgCIG-P>!oXRTv?nOCc{_Q;$+g8 z6sg()4k00u4i{nbBiJd-(8S=w=Ksqj?7g23pbf>=%g^fTIN!XTGIn zi>PQS2!0%$)xM&@~ZD^@6y@qZRlq=D7{_7?r zzK;QwEgBGL4tRQ&A-BClyF7DIa&h$iHi~y~ak1b{ZAl0*U%!5Rie1?hsqFD{CE6oG zAT#qTz4i$)Jc`~EUNsspgFrq=pIZn89rFRnKPi2B=2#QH7<{k^9Q%Z;#k#D?%lU|z zv2Y9yAq(1~^O;cGtz&M^4*~J0ZtGtJ5+RYnsE&Lt*$%~yLIIM+DIqRm5=PDp%XZMT$2c+z4NDt1IA|!?Oh(x7-QC?cqnV~2_czYa;Y~|R17bOd6Nksw z4_3~H>t}n_f__$>Rwr^aq$k+2ZeS!4{NDgfNkrwsZV>2vYgD)uz9BatXr|bP{}sIw|RN&&l^KmSW3jat0p9=RsdL><{Gnc z@O_}SU&-;WBZOmHx5s?6p?E#w-{EUy z5Es&CBq1<6+Y22FWI)Ua4;#TAV(HkJ%Bho{_**EVmXP4VOQ=m99TVm!ZYU__?#Dw_ zh|9;((Zjkf1SzWymQ9QIOT*v3%{BXsy11cZDuC&qSX}gMsSLlMj1$ThRQqtN$!;Q+ z#WbS_n{FuH2VIoNJ}}49pc(Z!J(>MZx{XBLTin#ip#{o zm}1c0i8w~vi$JA;<*ii?>{J~vjV?m_N{Bi5%>uLngiD4G0P~wZz1o8H#~+z`+!d>HV!wBRQ|#2 z)LqxAz|2p++1q1*bU@t6=rcJCHJVff2aH^(Lrqx!VIzZJaJtKdCbfO^!^2RF8w`|B zR2qzwE$mBKC(zQe?ex=t@aN*=J^5h8lVRf_GyzQI`Q9@6A^_yVbl{pAA>dHYW~N~X zSqPpcP{6DS`aeLFP(`ooFfe%6|4y1CJW*q*2Sg(Lf&SRMHRHsH3z+K*v_d~VE2@6Q zZ0^7Ow0U4mOW?x4Y-}PSQBkzWw4FNg*O1A9rhpD|s#-fb@^8?_W+U~~p=ranE8=AJ z(Q-`A6dN?{(q$F9XJ7S?6T&#AK3Tx>ESul8Pr;k5vHza|&Hs1-BKgoBX*n~nMz!S_ z<#3YOTaIl;ut7F#I7K1Y6_bXd^ri|VqCq+WtCpuFC)%s7 zT_Y0vI=+2)Y|Oq9cZ+3RKut>vxGOONQw2Rm(eA>D-w#&;n6V(k0J!Ifi5aIFd1u*P zJCXoVoLDG8?wgYLYa-GUazi1^LjU(KRS=UCFy*CC3&9k9_(O;X82a5yrfu!ZhI`L^ ze7C^1ZJ`+yjN^I3Ww}{VG2!07Ou|Y~f15FJ?@~d+BrG)eV5;~4bgDs?^^!Wc~vTAYxe4nc?N~_^~(#dVUOH4b1dh196F^AvQ4R7O);; zY2=pYywZO{$FBYO@y>r91<;aAP~}$w;s#G3ICR=d$jJzuv1xTeEPk!)syTv zV#RHEANuw;=y4d=ALmcc^L0u7z?t);K3qvWsn{^>>3AJm{R-dAr!3U2ty>RnPJ}t~ zg>g(2rBA2!Us;}Mv4GrgLj2#bBh7L%U+6FgX~F9sEo5fs$YcBv%^0!Hp|^=0y8`~? zIaf(Q*mMUm+x$E8N}ATZ~v!o@eXH9pL)oG&9iotQ8Ip;VE3cryk}vR=C0 zj!pog>mL|6*4R53w84m{qgv5x;b~_Fk!>(cc)->+%e2VG#)i0e(MRehu93#A0Ekuc zB^i*fM?Mw`MexYg%7>^%ol&%U_++dQb1j@wrY4P9Psh+V*-5j^W1CpIa#N-*>73+nH; z`k=?|`*@7?oJ%3!IUAdHJ?^}`_YZiR%Dy}tUl_d<7m|W?p?G6XKt`wb<=XApvUxs4 zX(9~=d?p1EwPJT+HSI->TwvQVIUQucIQ@lrNLty*{5I|x5MT?w5fxP<&mbylmrLtl zs%DI#T)dVR>`r3_>& z6UVU{HQ!;XUmf!+s|g8&E)f3dWt%U06s{c>6tp%0;L}w=9>7jXWbL%3zX(h846C6K z?~swH1*u4wGZ0khN$5sLM?aRiY$H>ZK)3=D5{$6Co1k_e;Riy~=mnr?0C_GAi4@*F z;tYc@8=_EV zUSSb_l71j$o35wQ`XE)2E5Y+6x7mIj8nQ%nWMS~%Dkar`uduYWT?6|cWXG*<_MA9* zk{7iS{)7D$E%$Qx7Y=bWNsPp&7S z5LR{6mw|$^v8-buCh)!R`fjK6Mjuh~0jZ@8cme1^GCNvI4X}Tf@C!l)r=gG#A#^L~ z6_(I?@_`Ur2<=X~6^!uTJlDMKZx}cWU--HKIeqbXkWXkS@OhAN2#PlvXyHpxJ)~{k zZU&xRL4n6+9o$kF6&L`Z#L__n5`pd3=-UENI$`CALWz@28cA^W1n}Ie&wmV#B7iij zC<-p@l%^uG`BrG|3F%C{&S1c3D41sTLq{(Dxu?VY8VqR{EifxVrj{Y6`foXSO9iJ) zR5YVAcOv_|;_A(p6!ImEFWd@!q#xjF^2N2}(CN=FG3=r}Z2bN*Rz(_qQ*n3D`THZ$?HdP%dG$RwR>a#LSHW9SRX+h}{Yk z#>wgFt9mcEy&%>yGI$K#pkwO<&b0swy#=HjVEh53t(#SNR%RSPj8xX)Gq%ui0Vru; z5FQ+;AEJT~rxbF(4e=|$4twJe8FWY6i|@(*v-9Zha{~ah1t_t^#|^r5H3i)rI|i(1 zD5e#j9K;I&o>tl29_I>Trop!bv`0il0EuG-QxUAyHU%?xu5yfu71bji9~ZA;qDJhW51Rh(daD`(Fu z^vwlTTKz%fzbB2I#L_i@mugZpesZa;qhtHb zGu>I&X{paNwBD(yLKH)f!zH1iujnE}zS5B!W`?|`iy*H7v29VTBZE`v>FFdRKs=^U zh(8wIZWl;_&xbeZi*_T_O z<>4%ehzxz58@g?-ig>8C#s6I~JNXw^Q_xagZZtXl=}b;mmQnddho;cwdQ8UbROS*o z|1K{4YFWqk2#lBnVYXm_LMfpmBZe@mO*Zmh>&4ze7xN#i?!`5FZb(n;&vT9|{%mbijx(wyRv`E}8?Pmt!$M z=#3g(_>hY3xc2ksO=w`rR8FDoI}w}@jch&%1ATqs*t>K0t|h!qXzBG5PF+pR9e*ZL z`m{60Rw3q0`HAT9Q!W!87>M%D4y~Sv^hm#KmE`yO1N^(>wN{WxlqVlVvIuhzsWu|2$4pzhfkdN%usYI{It=@ z@}pP*Xfz&r>9y{9I50`*3MozFHchS)=zI8|{rGbI)}vwZa-F|lIv|?Y!Uo`UwG93t z^x6SP8bO8}84wm7&YQtvWE&q%;aYH3 zzEGS1MlkfG^YftHr~K&Hs|}AP9!PvHCu2Yh3s;CnDHx^s^BGPk>OKYl*MXn|{;Mo*T1P8hi7bPJN=lv-+I1OOgNJg|l-G0!Eu~O`We;sL_Osn8q!7K^j z7MpAd2y(2x_w~VdAIaW zI+e;*quswSc0#_q(Ih%j~wDaXzVtRtl24w$< zp_vDopTFO{$wp+}2Sp9>PONBHXA78X9J2=@=nzU2rdRfrE$OfjTD?Md&B5(=L=3g#utfwicF

_{+-)%gPXcX5 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®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 jR_LUosyYH47{vHPttnpD1~?0iaU&C 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 diff --git a/doc/source/images/graph_mrpt_libs.svg b/doc/source/images/graph_mrpt_libs.svg index 8ffb188a3a..ba87dc7a91 100644 --- a/doc/source/images/graph_mrpt_libs.svg +++ b/doc/source/images/graph_mrpt_libs.svg @@ -13,8 +13,8 @@ apps - -mrpt-apps + +mrpt-apps @@ -30,8 +30,8 @@ apps->gui - - + + @@ -45,104 +45,104 @@ apps->slam - - + + hwdrivers - -mrpt-hwdrivers + +mrpt-hwdrivers apps->hwdrivers - - + + graphslam - -mrpt-graphslam + +mrpt-graphslam apps->graphslam - - + + hmtslam - -mrpt-hmtslam + +mrpt-hmtslam apps->hmtslam - - + + - + opengl - + mrpt-opengl - + gui->opengl - + maps - + mrpt-maps - + slam->maps - + vision - + mrpt-vision - + slam->vision - + hwdrivers->gui - - + + @@ -154,40 +154,40 @@ - + hwdrivers->comms - + - + hwdrivers->maps - + - + hwdrivers->vision - + - + graphslam->gui - - + + - + graphslam->slam - - + + - + hmtslam->graphslam - - + + @@ -238,40 +238,40 @@ - + math->system - + serialization - + mrpt-serialization - + math->serialization - + random - - -mrpt-random + + +mrpt-random - + math->random - - + + @@ -310,13 +310,13 @@ - + io->system - + expr->system @@ -325,253 +325,259 @@ containers - -mrpt-containers + +mrpt-containers - + system->containers - - + + - + typemeta - - -mrpt-typemeta (.h) + + +mrpt-typemeta (.h) - + system->typemeta - - + + core - -mrpt-core + +mrpt-core containers->core - - + + + + + +containers->typemeta + + - + detectors - - -mrpt-detectors + + +mrpt-detectors - + detectors->gui - - + + - + detectors->slam - - + + - + graphs - + mrpt-graphs - + graphs->opengl - + img - + mrpt-img - + opengl->img - + poses - + mrpt-poses - + opengl->poses - + maps->graphs - + obs - + mrpt-obs - + maps->obs - + vision->obs - + img->math - + img->config - + img->io - + kinematics - + mrpt-kinematics - + kinematics->opengl - + obs->opengl - + tfest - + mrpt-tfest - + obs->tfest - + rtti - - -mrpt-rtti + + +mrpt-rtti - + serialization->rtti - - + + - + nav - + mrpt-nav - + nav->maps - + nav->kinematics - + tfest->poses - + poses->bayes - + rtti->core - - + + - + rtti->typemeta - - + + @@ -583,7 +589,7 @@ - + ros1bridge->maps @@ -598,7 +604,7 @@ - + topography->obs @@ -613,7 +619,7 @@ - + visionlgpl->vision diff --git a/libs/containers/CMakeLists.txt b/libs/containers/CMakeLists.txt index fd07cdae49..5251aa84ed 100644 --- a/libs/containers/CMakeLists.txt +++ b/libs/containers/CMakeLists.txt @@ -6,6 +6,7 @@ define_mrpt_lib( containers # Dependencies mrpt-core + mrpt-typemeta ) # extra dependencies required by unit tests in this module: From a91cb4befb91ee3e49753ca1eb6f9c721efa1129 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Sat, 19 Jun 2021 01:22:59 +0200 Subject: [PATCH 25/68] fix docs --- doc/source/tutorial-3d-navigation-cheatsheet.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/tutorial-3d-navigation-cheatsheet.rst b/doc/source/tutorial-3d-navigation-cheatsheet.rst index 9c449733d7..4f86bf0e95 100644 --- a/doc/source/tutorial-3d-navigation-cheatsheet.rst +++ b/doc/source/tutorial-3d-navigation-cheatsheet.rst @@ -17,7 +17,7 @@ All MRPT applications use the following convention: - **Pan** (XY plane): Right-button pressed + mouse move. - **Move camera along Z axis**: SHIFT+Left-button pressed + mouse move left/right, -or (starting in MRPT 2.3.2) SHIFT+scroll wheel for faster up/down vertical motion. + or (starting in MRPT 2.3.2) SHIFT+scroll wheel for faster up/down vertical motion. The implementation of the features above by handling mouse and keyboard events From c24ce26da9f9c624f83a742a987c938919e9f89b Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 20 Jun 2021 16:59:07 +0200 Subject: [PATCH 26/68] docs: remove EOL Xenial. --- doc/source/download-mrpt.rst | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/doc/source/download-mrpt.rst b/doc/source/download-mrpt.rst index adf804588a..501647d751 100644 --- a/doc/source/download-mrpt.rst +++ b/doc/source/download-mrpt.rst @@ -52,31 +52,6 @@ Last **stable release** (`mrpt-stable PPA status =7. - See `instructions here `_. - -Last **stable release** (`mrpt-stable-xenial PPA status `_), for Ubuntu 16.04 LTS Xenial (EOL: April 2021): - -.. code-block:: bash - - # Install pre-requisites (** ONLY FOR Ubuntu 16.04 Xenial **) - sudo add-apt-repository ppa:ubuntu-toolchain-r/test # gcc-7 Backport - sudo add-apt-repository ppa:joseluisblancoc/mrpt-stable-xenial - sudo apt-get update - sudo apt-get install libmrpt-dev mrpt-apps - -**Nightly builds** (`mrpt-nightly-xenial PPA status `_), for Ubuntu 16.04 LTS Xenial (EOL: April 2021): - -.. code-block:: bash - - # Install pre-requisites (** ONLY FOR Ubuntu 16.04 Xenial **) - sudo add-apt-repository ppa:ubuntu-toolchain-r/test # gcc-7 Backport - sudo add-apt-repository ppa:joseluisblancoc/mrpt-unstable-xenial - sudo apt-get update - sudo apt-get install libmrpt-dev mrpt-apps - Windows installers -------------------- From fadece424d18cb024114c48c2b9697b011da8734 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 20 Jun 2021 18:21:07 +0200 Subject: [PATCH 27/68] add rotated() methods --- doc/source/doxygen-docs/changelog.md | 2 ++ libs/math/include/mrpt/math/TTwist2D.h | 10 ++++++++++ libs/math/include/mrpt/math/TTwist3D.h | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index a6f317d447..f780265499 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -15,6 +15,8 @@ - New 3D navigation key binding: SHIFT+scroll wheel, for fast up/down pure vertical motion of the camera point. - \ref mrpt_obs_grp - mrpt::obs::CObservation now has a common API to export datasets to TXT/CSV files, see methods exportTxtSupported(), exportTxtHeader(), exportTxtDataRow(). It has been implemented in all suitable observation classes. + - \ref mrpt_poses_grp + - New methods mrpt::math::TTwist2D::rotated() and mrpt::math::TTwist3D::rotated() - \ref mrpt_system_grp - mrpt::system::CTimeLogger: Include custom `name` in underlying mrpt::system::COutputLogger name. - BUG FIXES: diff --git a/libs/math/include/mrpt/math/TTwist2D.h b/libs/math/include/mrpt/math/TTwist2D.h index a90a291a55..7d2bf59d71 100644 --- a/libs/math/include/mrpt/math/TTwist2D.h +++ b/libs/math/include/mrpt/math/TTwist2D.h @@ -95,6 +95,16 @@ struct TTwist2D : public internal::ProvideStaticResize /** Transform the (vx,vy) components for a counterclockwise rotation of * `ang` radians. */ void rotate(const double ang); + + /** Like rotate(), but returning a copy of the rotated twist. + * \note New in MRPT 2.3.2 */ + [[nodiscard]] TTwist2D rotated(const double ang) const + { + TTwist2D r = *this; + r.rotate(ang); + return r; + } + bool operator==(const TTwist2D& o) const; bool operator!=(const TTwist2D& o) const; /** Returns the pose increment of multiplying each twist component times diff --git a/libs/math/include/mrpt/math/TTwist3D.h b/libs/math/include/mrpt/math/TTwist3D.h index 0e26e79eb1..696991ef32 100644 --- a/libs/math/include/mrpt/math/TTwist3D.h +++ b/libs/math/include/mrpt/math/TTwist3D.h @@ -151,6 +151,15 @@ struct TTwist3D : public internal::ProvideStaticResize * The translational part of the pose is ignored */ void rotate(const mrpt::math::TPose3D& rot); + /** Like rotate(), but returning a copy of the rotated twist. + * \note New in MRPT 2.3.2 */ + [[nodiscard]] TTwist3D rotated(const mrpt::math::TPose3D& rot) const + { + TTwist3D r = *this; + r.rotate(rot); + return r; + } + /** Set the current object value from a string generated by 'asString' (eg: * "[vx vy vz wx wy wz]" ) * \sa asString From c45cd40dddd888d2dcaad96b58287fa9995ba610 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Mon, 21 Jun 2021 17:20:40 +0200 Subject: [PATCH 28/68] CObservationImage::unload() now does actually unload lazy-load images --- doc/source/doxygen-docs/changelog.md | 1 + libs/obs/include/mrpt/obs/CObservationImage.h | 13 +++++++++++++ libs/obs/src/CObservationImage.cpp | 1 + 3 files changed, 15 insertions(+) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index f780265499..1623defc7d 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -15,6 +15,7 @@ - New 3D navigation key binding: SHIFT+scroll wheel, for fast up/down pure vertical motion of the camera point. - \ref mrpt_obs_grp - mrpt::obs::CObservation now has a common API to export datasets to TXT/CSV files, see methods exportTxtSupported(), exportTxtHeader(), exportTxtDataRow(). It has been implemented in all suitable observation classes. + - mrpt::obs::CObservationImage::unload() defaulted to doing nothing. It now correctly unloads lazy-load images. - \ref mrpt_poses_grp - New methods mrpt::math::TTwist2D::rotated() and mrpt::math::TTwist3D::rotated() - \ref mrpt_system_grp diff --git a/libs/obs/include/mrpt/obs/CObservationImage.h b/libs/obs/include/mrpt/obs/CObservationImage.h index d65e3c3748..b4f63689c4 100644 --- a/libs/obs/include/mrpt/obs/CObservationImage.h +++ b/libs/obs/include/mrpt/obs/CObservationImage.h @@ -72,8 +72,21 @@ class CObservationImage : public CObservation } void getDescriptionAsText(std::ostream& o) const override; + /** @name Delayed-load (lazy-load) manual control methods. + @{ */ + + /** Makes sure the image, which may be externally stored, are loaded in + * memory. \sa unload + */ void load() const override; + /** Unload image, for the case of it being stored in lazy-load mode + * (othewise, the method has no effect). + * \sa load + */ + void unload() override; + /** @} */ + }; // End of class def. } // namespace mrpt::obs diff --git a/libs/obs/src/CObservationImage.cpp b/libs/obs/src/CObservationImage.cpp index db4501a7c9..a45d2ecdb4 100644 --- a/libs/obs/src/CObservationImage.cpp +++ b/libs/obs/src/CObservationImage.cpp @@ -160,3 +160,4 @@ void CObservationImage::getDescriptionAsText(std::ostream& o) const } void CObservationImage::load() const { image.forceLoad(); } +void CObservationImage::unload() { image.unload(); } From 7b314d2d1cbc950fb31c5202684ac87f51350f7f Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 21 Jun 2021 22:43:15 +0200 Subject: [PATCH 29/68] new string utils functions; nanogui: fit error messages to fit on screen --- apps/navlog-viewer/navlog-viewer-ui.cpp | 11 +++------ doc/source/doxygen-docs/changelog.md | 1 + libs/gui/include/mrpt/gui/CDisplayWindowGUI.h | 6 ++++- .../system/include/mrpt/system/string_utils.h | 12 ++++++++++ libs/system/src/string_utils.cpp | 24 +++++++++++++++++++ libs/system/src/string_utils_unittest.cpp | 21 ++++++++++++++++ 6 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 libs/system/src/string_utils_unittest.cpp diff --git a/apps/navlog-viewer/navlog-viewer-ui.cpp b/apps/navlog-viewer/navlog-viewer-ui.cpp index d56e2cebb9..c553f4e3c1 100644 --- a/apps/navlog-viewer/navlog-viewer-ui.cpp +++ b/apps/navlog-viewer/navlog-viewer-ui.cpp @@ -388,19 +388,14 @@ void NavlogViewerApp::loadLogfile(const std::string& fileName) { // EOF in the middle of an object... It may be usual if the logger // is shut down not cleanly. - // auto dlg = - new nanogui::MessageDialog( - m_win.get(), nanogui::MessageDialog::Type::Warning, - "Loading ended with an exception", mrpt::exception_to_str(e)); - - break; + // Show the error message (catch below) + throw; } } + NANOGUI_END_TRY(*m_win) // Update stats, etc... updateInfoFromLoadedLog(); - - NANOGUI_END_TRY(*m_win) } void NavlogViewerApp::OnMainIdleLoop() diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 1623defc7d..b1c56b0bec 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -20,6 +20,7 @@ - New methods mrpt::math::TTwist2D::rotated() and mrpt::math::TTwist3D::rotated() - \ref mrpt_system_grp - mrpt::system::CTimeLogger: Include custom `name` in underlying mrpt::system::COutputLogger name. + - New functions mrpt::system::firstNLines() and mrpt::system::nthOccurrence() - BUG FIXES: - mrpt::img::CImage::isEmpty() should return false for delay-load images. - Fix build error with GCC 8 in `mrpt/containers/yaml.h`. diff --git a/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h b/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h index 28957f77bc..757d52e24d 100644 --- a/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h +++ b/libs/gui/include/mrpt/gui/CDisplayWindowGUI.h @@ -12,6 +12,7 @@ #include #include #include +#include // firstNLines() #include #include @@ -356,10 +357,13 @@ class CDisplayWindowGUI : public nanogui::Screen } \ catch (const std::exception& e) \ { \ - const auto sErr = mrpt::exception_to_str(e); \ + const size_t maxLines = 7; \ + const std::string sErr = \ + mrpt::system::firstNLines(mrpt::exception_to_str(e), maxLines); \ auto dlg = new nanogui::MessageDialog( \ &_parentWindowRef_, nanogui::MessageDialog::Type::Warning, \ "Exception", sErr); \ + dlg->requestFocus(); \ dlg->setCallback([](int /*result*/) {}); \ } diff --git a/libs/system/include/mrpt/system/string_utils.h b/libs/system/include/mrpt/system/string_utils.h index 408725c2c9..bfc37ccec5 100644 --- a/libs/system/include/mrpt/system/string_utils.h +++ b/libs/system/include/mrpt/system/string_utils.h @@ -145,6 +145,18 @@ void stringListAsString( const std::deque& lst, std::string& out, const std::string& newline = "\r\n"); +/** Finds the position of the n-th occurence of the given substring, or + * std::string::npos if it does not happen. + * \note New in MRPT 2.3.2 + */ +size_t nthOccurrence( + const std::string& str, const std::string& strToFind, size_t nth); + +/** Returns the first `n` lines (splitted by '\n' chars) of the given text. + * \note New in MRPT 2.3.2 + */ +std::string firstNLines(const std::string& str, size_t n); + /** Original code snippet found in http://stackoverflow.com/a/30357710 */ /**\{*/ diff --git a/libs/system/src/string_utils.cpp b/libs/system/src/string_utils.cpp index b5bfadc3c8..1ba054b81a 100644 --- a/libs/system/src/string_utils.cpp +++ b/libs/system/src/string_utils.cpp @@ -358,3 +358,27 @@ void mrpt::system::stringListAsString( { impl_stringListAsString(lst, outText, newLine); } + +size_t mrpt::system::nthOccurrence( + const std::string& str, const std::string& strToFind, size_t nth) +{ + size_t pos = 0; + size_t cnt = 0; + + while (cnt != nth) + { + pos++; + pos = str.find(strToFind, pos); + if (pos == std::string::npos) return std::string::npos; + cnt++; + } + return pos; +} + +std::string mrpt::system::firstNLines(const std::string& str, size_t n) +{ + const auto i = nthOccurrence(str, "\n", n); + if (i == std::string::npos) return str; + else + return str.substr(0, i); +} diff --git a/libs/system/src/string_utils_unittest.cpp b/libs/system/src/string_utils_unittest.cpp new file mode 100644 index 0000000000..cc6414e67b --- /dev/null +++ b/libs/system/src/string_utils_unittest.cpp @@ -0,0 +1,21 @@ +/* +------------------------------------------------------------------------+ + | Mobile Robot Programming Toolkit (MRPT) | + | https://www.mrpt.org/ | + | | + | Copyright (c) 2005-2021, Individual contributors, see AUTHORS file | + | See: https://www.mrpt.org/Authors - All rights reserved. | + | Released under BSD License. See: https://www.mrpt.org/License | + +------------------------------------------------------------------------+ */ + +#include +#include + +TEST(string_utils, firstNLines) +{ + const std::string s = "1\n2\n3\n4\n"; + + EXPECT_EQ(mrpt::system::firstNLines(s, 1), "1"); + EXPECT_EQ(mrpt::system::firstNLines(s, 2), "1\n2"); + EXPECT_EQ(mrpt::system::firstNLines(s, 3), "1\n2\n3"); + EXPECT_EQ(mrpt::system::firstNLines(s, 10), s); +} From 0869e7d8d1fd5ca28ecabed60f914cf2f5e9c56a Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 24 Jun 2021 00:57:05 +0200 Subject: [PATCH 30/68] fix wx warnings --- apps/RawLogViewer/CScanAnimation.cpp | 6 ++-- apps/RawLogViewer/ViewOptions3DPoints.cpp | 43 ++++++----------------- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/apps/RawLogViewer/CScanAnimation.cpp b/apps/RawLogViewer/CScanAnimation.cpp index 71f02aaaee..9aae547280 100644 --- a/apps/RawLogViewer/CScanAnimation.cpp +++ b/apps/RawLogViewer/CScanAnimation.cpp @@ -512,7 +512,9 @@ bool CScanAnimation::update_opengl_viz(const CSensoryFrame& sf) (tim_last != INVALID_TIMESTAMP && fabs(mrpt::system::timeDifference(ro.timestamp, tim_last)) > largest_period)) - { lst_to_delete.push_back(o.first); } + { + lst_to_delete.push_back(o.first); + } } // Remove too old observations: @@ -634,7 +636,7 @@ void CScanAnimation::OnbtnVizOptions(wxCommandEvent&) wxDefaultSize, wxDEFAULT_DIALOG_STYLE, _T("wxID_ANY")); dlgViz = &dlg; - auto sizer1 = new wxFlexGridSizer(2, 1, 0, 0); + auto sizer1 = new wxFlexGridSizer(0, 1, 0, 0); sizer1->AddGrowableCol(0); auto panel = new ViewOptions3DPoints(&dlg); diff --git a/apps/RawLogViewer/ViewOptions3DPoints.cpp b/apps/RawLogViewer/ViewOptions3DPoints.cpp index 4c5365e68d..499245cdc4 100644 --- a/apps/RawLogViewer/ViewOptions3DPoints.cpp +++ b/apps/RawLogViewer/ViewOptions3DPoints.cpp @@ -170,7 +170,7 @@ ViewOptions3DPoints::ViewOptions3DPoints(wxWindow* parent, wxWindowID id) FlexGridSizer3->Add(FlexGridSizer4, 1, wxALL | wxEXPAND, 0); StaticBoxSizer2->Add(FlexGridSizer3, 1, wxALL | wxEXPAND, 5); FlexGridSizer1->Add(StaticBoxSizer2, 1, wxALL | wxEXPAND, 5); - FlexGridSizer5 = new wxFlexGridSizer(2, 1, 0, 0); + FlexGridSizer5 = new wxFlexGridSizer(0, 1, 0, 0); StaticBoxSizer3 = new wxStaticBoxSizer(wxHORIZONTAL, this, _("Sensor pose")); FlexGridSizer6 = new wxFlexGridSizer(0, 1, 0, 0); @@ -228,40 +228,17 @@ ViewOptions3DPoints::ViewOptions3DPoints(wxWindow* parent, wxWindowID id) SetSizer(FlexGridSizer1); FlexGridSizer1->Fit(this); FlexGridSizer1->SetSizeHints(this); - - Connect( - ID_BUTTON1, wxEVT_COMMAND_BUTTON_CLICKED, - (wxObjectEventFunction)&ViewOptions3DPoints::OnbtnApplyClick); - Connect( - ID_CHECKBOX1, wxEVT_COMMAND_CHECKBOX_CLICKED, - (wxObjectEventFunction)&ViewOptions3DPoints::OnbtnApplyClick); - Connect( - ID_RADIOBOX1, wxEVT_COMMAND_RADIOBOX_SELECTED, - (wxObjectEventFunction)&ViewOptions3DPoints::OnbtnApplyClick); - Connect( - ID_RADIOBOX2, wxEVT_COMMAND_RADIOBOX_SELECTED, - (wxObjectEventFunction)&ViewOptions3DPoints::OnbtnApplyClick); - Connect( - ID_CHECKBOX2, wxEVT_COMMAND_CHECKBOX_CLICKED, - (wxObjectEventFunction)&ViewOptions3DPoints::OnbtnApplyClick); //*) - Bind(wxEVT_BUTTON, &ViewOptions3DPoints::OnbtnApplyClick, this, ID_BUTTON1); - Bind( - wxEVT_RADIOBOX, &ViewOptions3DPoints::OnbtnApplyClick, this, - ID_RADIOBOX1); - Bind( - wxEVT_RADIOBOX, &ViewOptions3DPoints::OnbtnApplyClick, this, - ID_RADIOBOX2); - Bind( - wxEVT_CHECKBOX, &ViewOptions3DPoints::OnbtnApplyClick, this, - ID_CHECKBOX2); - Bind( - wxEVT_SPINCTRL, &ViewOptions3DPoints::OnbtnApplyClick, this, - ID_SPINCTRL1); - Bind( - wxEVT_CHECKBOX, &ViewOptions3DPoints::OnbtnApplyClick, this, - ID_CHECKBOX3); + using Me = ViewOptions3DPoints; + + Bind(wxEVT_BUTTON, &Me::OnbtnApplyClick, this, ID_BUTTON1); + Bind(wxEVT_CHECKBOX, &Me::OnbtnApplyClick, this, ID_CHECKBOX1); + Bind(wxEVT_CHECKBOX, &Me::OnbtnApplyClick, this, ID_CHECKBOX2); + Bind(wxEVT_RADIOBOX, &Me::OnbtnApplyClick, this, ID_RADIOBOX1); + Bind(wxEVT_RADIOBOX, &Me::OnbtnApplyClick, this, ID_RADIOBOX2); + Bind(wxEVT_CHECKBOX, &Me::OnbtnApplyClick, this, ID_CHECKBOX3); + Bind(wxEVT_SPINCTRL, &Me::OnbtnApplyClick, this, ID_SPINCTRL1); m_params.load_from_ini_file(); m_params.to_UI(*this); From 157be011edb1100f3843ce007d7b8c4a893d91c1 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 24 Jun 2021 02:23:31 +0200 Subject: [PATCH 31/68] BUGFIX: fix broken rendering of wx 2D plots on some systems --- apps/2d-slam-demo/slamdemoMain.cpp | 15 +- apps/RawLogViewer/CFormRawMap.cpp | 7 +- apps/RawLogViewer/xRawLogViewerMain.cpp | 18 +- doc/source/doxygen-docs/changelog.md | 1 + .../include/mrpt/3rdparty/mathplot/mathplot.h | 25 +-- libs/gui/src/CDisplayWindowPlots.cpp | 5 +- libs/gui/src/mathplots/mathplot.cpp | 157 ++++++------------ 7 files changed, 78 insertions(+), 150 deletions(-) diff --git a/apps/2d-slam-demo/slamdemoMain.cpp b/apps/2d-slam-demo/slamdemoMain.cpp index 58e34a4eef..6ca678ce4d 100644 --- a/apps/2d-slam-demo/slamdemoMain.cpp +++ b/apps/2d-slam-demo/slamdemoMain.cpp @@ -847,7 +847,6 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) plotGT->AddLayer(m_lyGTvisibleRange); plotGT->LockAspect(); - plotGT->EnableDoubleBuffer(true); // Map plot ------------ m_lyMapRobot = new mpPolygon(); @@ -857,7 +856,6 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) m_lyMapRobot->SetCoordinateBase(0, 0, 0); plotMap->AddLayer(m_lyMapRobot); plotMap->LockAspect(); - plotMap->EnableDoubleBuffer(true); // Observations plot ------------ m_lyObsRobot = new mpPolygon(); @@ -874,7 +872,6 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) plotObs->AddLayer(m_lyObsvisibleRange); plotObs->LockAspect(); - plotObs->EnableDoubleBuffer(true); // IC plot ------------ m_lyICvisibleRange = new mpPolygon(); @@ -884,7 +881,6 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) plotIndivCompat->AddLayer(m_lyICvisibleRange); plotIndivCompat->LockAspect(false); - plotIndivCompat->EnableDoubleBuffer(true); // X ERROR plot ------------ m_lyERRX_err = new mpFXYVector(); @@ -903,7 +899,6 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) plotErrorX->AddLayer(m_lyERRX_boundDown); plotErrorX->LockAspect(false); - plotErrorX->EnableDoubleBuffer(true); // Y ERROR plot ------------ m_lyERRY_err = new mpFXYVector(); @@ -922,7 +917,6 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) plotErrorY->AddLayer(m_lyERRY_boundDown); plotErrorY->LockAspect(false); - plotErrorY->EnableDoubleBuffer(true); // Phi ERROR plot ------------ m_lyERRPHI_err = new mpFXYVector(); @@ -941,7 +935,6 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) plotErrorPhi->AddLayer(m_lyERRPHI_boundDown); plotErrorPhi->LockAspect(false); - plotErrorPhi->EnableDoubleBuffer(true); // Stats Time plot ------------ m_lyStatTimes = new mpFXYVector(); @@ -950,7 +943,6 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) plotStatTime->AddLayer(m_lyStatTimes); plotStatTime->LockAspect(false); - plotStatTime->EnableDoubleBuffer(true); // DA Stats plots ------------ #define INIT_DA_PLOT(CODE) \ @@ -958,8 +950,7 @@ slamdemoFrame::slamdemoFrame(wxWindow* parent, wxWindowID id) m_lyDa##CODE->SetPen(wxPen(wxColour(0, 0, 0), 5)); \ m_lyDa##CODE->SetContinuity(false); \ plotDa##CODE->AddLayer(m_lyDa##CODE); \ - plotDa##CODE->LockAspect(false); \ - plotDa##CODE->EnableDoubleBuffer(true); + plotDa##CODE->LockAspect(false); INIT_DA_PLOT(FP); INIT_DA_PLOT(FN); @@ -1949,7 +1940,9 @@ void slamdemoFrame::executeOneStep() // Save dataset to file? if (m_rawlog_out_file.fileOpenCorrectly()) - { archiveFrom(m_rawlog_out_file) << act << sf; } + { + archiveFrom(m_rawlog_out_file) << act << sf; + } } // For the case of doing D.A., save the correspondences REAL_MAP <-> diff --git a/apps/RawLogViewer/CFormRawMap.cpp b/apps/RawLogViewer/CFormRawMap.cpp index a5adbdb430..8aaf2a6150 100644 --- a/apps/RawLogViewer/CFormRawMap.cpp +++ b/apps/RawLogViewer/CFormRawMap.cpp @@ -462,7 +462,9 @@ void loadMapInto3DScene(COpenGLScene& scene) auto this_t = it.first; if (distanceBetweenPoints(x0, y0, z0, p.x, p.y, p.z) < 5.5) - { obj->appendLine(x0, y0, z0, p.x, p.y, p.z); } + { + obj->appendLine(x0, y0, z0, p.x, p.y, p.z); + } else if (last_t) { // We have a gap without GT: @@ -715,7 +717,6 @@ void CFormRawMap::OnbtnGenerateClick(wxCommandEvent&) plotMap->AddLayer(lyPoints); plotMap->AddLayer(lyPath); - plotMap->EnableDoubleBuffer(true); lyPath->SetData(pathX, pathY); @@ -976,7 +977,6 @@ void CFormRawMap::OnbtnGeneratePathsClick(wxCommandEvent&) plotMap->AddLayer(lyCov, false); // - plotMap->EnableDoubleBuffer(true); plotMap->Fit(); // Update the window to show the new data fitted. plotMap->LockAspect(true); @@ -1121,7 +1121,6 @@ void CFormRawMap::OnGenerateFromRTK(wxCommandEvent&) plotMap->AddLayer(lyPoints); plotMap->AddLayer(lyPath); - plotMap->EnableDoubleBuffer(true); lyPath->SetData(pathX, pathY); diff --git a/apps/RawLogViewer/xRawLogViewerMain.cpp b/apps/RawLogViewer/xRawLogViewerMain.cpp index 4820f2bc33..6f9427b968 100644 --- a/apps/RawLogViewer/xRawLogViewerMain.cpp +++ b/apps/RawLogViewer/xRawLogViewerMain.cpp @@ -1704,12 +1704,6 @@ xRawLogViewerFrame::xRawLogViewerFrame(wxWindow* parent, wxWindowID id) plotRangeBearing->AddLayer(new mpScaleY()); plotRangeBearing->AddLayer(lyRangeBearingLandmarks); - plotScan2D->EnableDoubleBuffer(true); - plotAct2D_XY->EnableDoubleBuffer(true); - plotAct2D_PHI->EnableDoubleBuffer(true); - plotRangeBearing->EnableDoubleBuffer(true); - plotRawlogSensorTimes->EnableDoubleBuffer(true); - Maximize(); // Maximize the main window // Set sliders: @@ -3168,7 +3162,9 @@ void xRawLogViewerFrame::OnFileCountEntries(wxCommandEvent&) if (newObj->GetRuntimeClass() == CLASS_ID(CSensoryFrame) || newObj->GetRuntimeClass() == CLASS_ID(CActionCollection) || newObj->GetRuntimeClass() == CLASS_ID(CPose2D)) - { entryIndex++; } + { + entryIndex++; + } else { // Unknown class: @@ -4010,7 +4006,9 @@ void xRawLogViewerFrame::OnRemoveSpecificRangeMeas(wxCommandEvent&) obs_2->sensedData[q].sensedDistance); if (filter) - { obs_2->sensedData[q].sensedDistance = 0; } + { + obs_2->sensedData[q].sensedDistance = 0; + } nFilt++; } } @@ -4431,7 +4429,9 @@ void xRawLogViewerFrame::OnRecalculateActionsICP(wxCommandEvent&) { // Check type: if (rawlog.getType(countLoop) == CRawlog::etActionCollection) - { act_between = rawlog.getAsAction(countLoop); } + { + act_between = rawlog.getAsAction(countLoop); + } else if (rawlog.getType(countLoop) == CRawlog::etSensoryFrame) { // This is a SF: diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index b1c56b0bec..04cdadfbfd 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -25,6 +25,7 @@ - mrpt::img::CImage::isEmpty() should return false for delay-load images. - Fix build error with GCC 8 in `mrpt/containers/yaml.h`. - Fix exception rendering empty point clouds due to invalid bounding box. + - Fix broken 2D plots rendering in Ubuntu 20.04 (and probably other systems), via an update in mpWindow to properly use wxAutoBufferedPaintDC. # Version 2.3.1: Released May 26th, 2021 - General cmake scripts: diff --git a/libs/gui/include/mrpt/3rdparty/mathplot/mathplot.h b/libs/gui/include/mrpt/3rdparty/mathplot/mathplot.h index 7eed81af84..98cb00c2a1 100644 --- a/libs/gui/include/mrpt/3rdparty/mathplot/mathplot.h +++ b/libs/gui/include/mrpt/3rdparty/mathplot/mathplot.h @@ -20,6 +20,8 @@ #pragma once +#include + // JL: This is VERY ugly, but ask MS why we cannot export a DLL class with STL // members !! #if defined(_MSC_VER) @@ -113,8 +115,8 @@ #define DEBUG_NEW new (_NORMAL_BLOCK, __FILE__, __LINE__) #else #define DEBUG_NEW new -#endif // _DEBUG -#endif // _WINDOWS +#endif // _DEBUG +#endif // _WINDOWS // Separation for axes when set close to border #define X_BORDER_SEPARATION 40 @@ -925,7 +927,6 @@ typedef std::deque wxLayerList; Since wxMathPlot version 0.03, the mpWindow incorporates the following features: - - DoubleBuffering (Default=disabled): Can be set with EnableDoubleBuffer - Mouse based pan/zoom (Default=enabled): Can be set with EnableMousePanZoom. @@ -1007,7 +1008,7 @@ class WXDLLIMPEXP_MATHPLOT mpWindow : public wxWindow double GetScaleX() const { return m_scaleX; - }; // Schaling's method: maybe another method esists with the same name + }; // Schaling's method: maybe another method esists with the same name /** Get current view's Y scale. See @ref mpLayer::Plot "rules for coordinate transformation" @@ -1147,11 +1148,6 @@ class WXDLLIMPEXP_MATHPLOT mpWindow : public wxWindow // ( (m_posY-y) * m_scaleY); } inline wxCoord y2p(double y) { return (wxCoord)((m_posY - y) * m_scaleY); } - /** Enable/disable the double-buffering of the window, eliminating the - * flicker (default=disabled). - */ - void EnableDoubleBuffer(bool enabled) { m_enableDoubleBuffer = enabled; } - /** Enable/disable the feature of pan/zoom with the mouse (default=enabled) */ void EnableMousePanZoom(bool enabled) { m_enableMouseNavigation = enabled; } @@ -1478,14 +1474,6 @@ class WXDLLIMPEXP_MATHPLOT mpWindow : public wxWindow int m_marginTop, m_marginRight, m_marginBottom, m_marginLeft; - /** For double buffering */ - int m_last_lx, m_last_ly; - /** For double buffering */ - wxMemoryDC m_buff_dc; - /** For double buffering */ - wxBitmap* m_buff_bmp; - /** For double buffering */ - bool m_enableDoubleBuffer; /** For pan/zoom with the mouse. */ bool m_enableMouseNavigation; bool m_mouseMovedAfterRightClick; @@ -1498,6 +1486,9 @@ class WXDLLIMPEXP_MATHPLOT mpWindow : public wxWindow /** For moving info layers over the window area */ mpInfoLayer* m_movingInfoLayer; + bool m_drawDottedSelectedWindow = false; + wxPoint m_dottedWindowSize{0, 0}; + DECLARE_DYNAMIC_CLASS(mpWindow) DECLARE_EVENT_TABLE() }; diff --git a/libs/gui/src/CDisplayWindowPlots.cpp b/libs/gui/src/CDisplayWindowPlots.cpp index 5ab7d126c6..093a036534 100644 --- a/libs/gui/src/CDisplayWindowPlots.cpp +++ b/libs/gui/src/CDisplayWindowPlots.cpp @@ -57,7 +57,6 @@ CWindowDialogPlots::CWindowDialogPlots( m_plot->AddLayer(new mpScaleX()); m_plot->AddLayer(new mpScaleY()); m_plot->LockAspect(false); - m_plot->EnableDoubleBuffer(true); m_plot->Fit(-10, 10, -10, 10); @@ -691,8 +690,8 @@ void CDisplayWindowPlots::setPos([[maybe_unused]] int x, [[maybe_unused]] int y) /*--------------------------------------------------------------- setWindowTitle ---------------------------------------------------------------*/ -void CDisplayWindowPlots::setWindowTitle([ - [maybe_unused]] const std::string& str) +void CDisplayWindowPlots::setWindowTitle( + [[maybe_unused]] const std::string& str) { #if MRPT_HAS_WXWIDGETS if (!isOpen()) diff --git a/libs/gui/src/mathplots/mathplot.cpp b/libs/gui/src/mathplots/mathplot.cpp index 1fd3f9c14f..d7f7ebb0ec 100644 --- a/libs/gui/src/mathplots/mathplot.cpp +++ b/libs/gui/src/mathplots/mathplot.cpp @@ -49,6 +49,7 @@ const int INVALID_CLICK_COORDS = -99999; #include #include +#include #include #include #include @@ -888,7 +889,9 @@ void mpScaleX::Plot(wxDC& dc, mpWindow& w) { // Date and/or time axis representation if (m_labelType == mpX_DATETIME) - { fmt = (wxT("%04.0f-%02.0f-%02.0fT%02.0f:%02.0f:%02.0f")); } + { + fmt = (wxT("%04.0f-%02.0f-%02.0fT%02.0f:%02.0f:%02.0f")); + } else if (m_labelType == mpX_DATE) { fmt = (wxT("%04.0f-%02.0f-%02.0f")); @@ -952,11 +955,15 @@ void mpScaleX::Plot(wxDC& dc, mpWindow& w) #endif dc.SetPen(m_pen); if ((m_flags == mpALIGN_BOTTOM) && !m_drawOutsideMargins) - { dc.DrawLine(p, orgy + 4, p, minYpx); } + { + dc.DrawLine(p, orgy + 4, p, minYpx); + } else { if ((m_flags == mpALIGN_TOP) && !m_drawOutsideMargins) - { dc.DrawLine(p, orgy - 4, p, maxYpx); } + { + dc.DrawLine(p, orgy - 4, p, maxYpx); + } else { dc.DrawLine(p, 0 /*-w.GetScrY()*/, p, w.GetScrY()); @@ -1080,7 +1087,9 @@ void mpScaleX::Plot(wxDC& dc, mpWindow& w) dc.GetTextExtent(s, &tx, &ty); if ((m_flags == mpALIGN_BORDER_BOTTOM) || (m_flags == mpALIGN_TOP)) - { dc.DrawText(s, p - tx / 2, orgy - 4 - ty); } + { + dc.DrawText(s, p - tx / 2, orgy - 4 - ty); + } else { dc.DrawText(s, p - tx / 2, orgy + 4); @@ -1258,7 +1267,9 @@ void mpScaleY::Plot(wxDC& dc, mpWindow& w) if (m_ticks) { // Draw axis ticks if (m_flags == mpALIGN_BORDER_LEFT) - { dc.DrawLine(orgx, p, orgx + 4, p); } + { + dc.DrawLine(orgx, p, orgx + 4, p); + } else { dc.DrawLine( @@ -1274,11 +1285,15 @@ void mpScaleY::Plot(wxDC& dc, mpWindow& w) #endif dc.SetPen(m_pen); if ((m_flags == mpALIGN_LEFT) && !m_drawOutsideMargins) - { dc.DrawLine(orgx - 4, p, endPx, p); } + { + dc.DrawLine(orgx - 4, p, endPx, p); + } else { if ((m_flags == mpALIGN_RIGHT) && !m_drawOutsideMargins) - { dc.DrawLine(minYpx, p, orgx + 4, p); } + { + dc.DrawLine(minYpx, p, orgx + 4, p); + } else { dc.DrawLine(0 /*-w.GetScrX()*/, p, w.GetScrX(), p); @@ -1418,9 +1433,6 @@ mpWindow::mpWindow( m_scrX = m_scrY = 64; // Fixed from m_scrX = m_scrX = 64; m_minX = m_minY = 0; m_maxX = m_maxY = 0; - m_last_lx = m_last_ly = 0; - m_buff_bmp = nullptr; - m_enableDoubleBuffer = FALSE; m_enableMouseNavigation = TRUE; m_mouseMovedAfterRightClick = FALSE; m_movingInfoLayer = nullptr; @@ -1457,8 +1469,7 @@ mpWindow::mpWindow( m_mouseLClick_X = INVALID_CLICK_COORDS; m_mouseLClick_Y = INVALID_CLICK_COORDS; - // J.L.Blanco: Eliminates the "flick" with the double buffer. - SetBackgroundStyle(wxBG_STYLE_CUSTOM); + SetBackgroundStyle(wxBG_STYLE_PAINT); UpdateAll(); } @@ -1467,12 +1478,6 @@ mpWindow::~mpWindow() { // Free all the layers: DelAllLayers(true, false); - - if (m_buff_bmp) - { - delete m_buff_bmp; - m_buff_bmp = nullptr; - } } // Mouse handler, for detecting when the user drag with the right button or just @@ -1535,6 +1540,8 @@ void mpWindow::OnMouseWheel(wxMouseEvent& event) // JLB void mpWindow::OnMouseMove(wxMouseEvent& event) { + m_drawDottedSelectedWindow = false; + if (!m_enableMouseNavigation) { event.Skip(); @@ -1578,14 +1585,10 @@ void mpWindow::OnMouseMove(wxMouseEvent& event) { if (m_movingInfoLayer == nullptr) { - wxClientDC dc(this); - wxPen pen(*wxBLACK, 1, wxPENSTYLE_DOT); - dc.SetPen(pen); - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRectangle( - m_mouseLClick_X, m_mouseLClick_Y, + m_drawDottedSelectedWindow = true; + m_dottedWindowSize = { event.GetX() - m_mouseLClick_X, - event.GetY() - m_mouseLClick_Y); + event.GetY() - m_mouseLClick_Y}; } else { @@ -1609,16 +1612,6 @@ void mpWindow::OnMouseMove(wxMouseEvent& event) RefreshRect(tmpLyr->GetRectangle()); } } - /* if (m_coordTooltip) { - wxString toolTipContent; - toolTipContent.Printf(_("X = %f\nY = %f"), p2x(event.GetX()), - p2y(event.GetY())); - wxTipWindow** ptr = nullptr; - wxRect rectBounds(event.GetX(), event.GetY(), 5, 5); - wxTipWindow* tip = new wxTipWindow(this, toolTipContent, 100, - ptr, &rectBounds); - - } */ } } event.Skip(); @@ -1634,7 +1627,7 @@ void mpWindow::OnMouseLeftDown(wxMouseEvent& event) event.GetY()); /*m_mouseLClick_X, m_mouseLClick_Y);*/ #endif wxPoint pointClicked = event.GetPosition(); - m_movingInfoLayer = IsInsideInfoLayer(pointClicked); + ´ m_movingInfoLayer = IsInsideInfoLayer(pointClicked); if (m_movingInfoLayer != nullptr) { #ifdef MATHPLOT_DO_LOGGING @@ -2096,16 +2089,10 @@ void mpWindow::DelAllLayers(bool alsoDeleteObject, bool refreshDisplay) if (refreshDisplay) UpdateAll(); } -// void mpWindow::DoPrepareDC(wxDC& dc) -// { -// dc.SetDeviceOrigin(x2p(m_minX), y2p(m_maxY)); -// } - -void mpWindow::OnPaint(wxPaintEvent& WXUNUSED(event)) +void mpWindow::OnPaint(wxPaintEvent&) { - wxPaintDC dc(this); - dc.GetSize(&m_scrX, &m_scrY); // This is the size of the visible area only! - // DoPrepareDC(dc); + wxAutoBufferedPaintDC dc(this); + dc.GetSize(&m_scrX, &m_scrY); #ifdef MATHPLOT_DO_LOGGING { @@ -2118,74 +2105,30 @@ void mpWindow::OnPaint(wxPaintEvent& WXUNUSED(event)) #endif // Selects direct or buffered draw: - wxDC* trgDc; - - // J.L.Blanco @ Aug 2007: Added double buffer support - if (m_enableDoubleBuffer) - { - if (m_last_lx != m_scrX || m_last_ly != m_scrY) - { - if (m_buff_bmp) delete m_buff_bmp; - m_buff_bmp = new wxBitmap(m_scrX, m_scrY); - m_buff_dc.SelectObject(*m_buff_bmp); - m_last_lx = m_scrX; - m_last_ly = m_scrY; - } - trgDc = &m_buff_dc; - } - else - { - trgDc = &dc; - } // Draw background: - // trgDc->SetDeviceOrigin(0,0); - trgDc->SetPen(*wxTRANSPARENT_PEN); + dc.SetPen(*wxTRANSPARENT_PEN); wxBrush brush(GetBackgroundColour()); - trgDc->SetBrush(brush); - trgDc->SetTextForeground(m_fgColour); - trgDc->DrawRectangle(0, 0, m_scrX, m_scrY); + dc.SetBrush(brush); + dc.SetTextForeground(m_fgColour); + dc.DrawRectangle(0, 0, m_scrX, m_scrY); // Draw all the layers: - // trgDc->SetDeviceOrigin( m_scrX>>1, m_scrY>>1); // Origin at the center - wxLayerList::iterator li; - for (li = m_layers.begin(); li != m_layers.end(); ++li) - { - (*li)->Plot(*trgDc, *this); - }; + for (const auto& ly : m_layers) + ly->Plot(dc, *this); - // If doublebuffer, draw now to the window: - if (m_enableDoubleBuffer) + if (m_drawDottedSelectedWindow) { - // trgDc->SetDeviceOrigin(0,0); - // dc.SetDeviceOrigin(0,0); // Origin at the center - dc.Blit(0, 0, m_scrX, m_scrY, trgDc, 0, 0); + static wxPen pen(*wxBLACK, 1, wxPENSTYLE_DOT); + dc.SetPen(pen); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle( + m_mouseLClick_X, m_mouseLClick_Y, m_dottedWindowSize.x, + m_dottedWindowSize.y); } - /* if (m_coordTooltip) { - wxString toolTipContent; - wxPoint mousePoint = wxGetMousePosition(); - toolTipContent.Printf(_("X = %f\nY = %f"), p2x(mousePoint.x), - p2y(mousePoint.y)); - SetToolTip(toolTipContent); - }*/ // If scrollbars are enabled, refresh them - if (m_enableScrollBars) - { - /* m_scrollX = (int) floor((m_posX - m_minX)*m_scaleX); - m_scrollY = (int) floor((m_maxY - m_posY )*m_scaleY); - Scroll(m_scrollX, m_scrollY);*/ - // Scroll(x2p(m_posX), y2p(m_posY)); - // SetVirtualSize((int) ((m_maxX - m_minX)*m_scaleX), (int) - // ((m_maxY - m_minY)*m_scaleY)); - // int centerX = (m_scrX - m_marginLeft - m_marginRight)/2; // + - // m_marginLeft; // c.x = m_scrX/2; - // int centerY = (m_scrY - m_marginTop - m_marginBottom)/2; // - - // m_marginTop; // c.y = m_scrY/2; - /*SetScrollbars(1, 1, (int) ((m_maxX - m_minX)*m_scaleX), (int) ((m_maxY - * - m_minY)*m_scaleY));*/ //, x2p(m_posX + centerX/m_scaleX), - // y2p(m_posY - centerY/m_scaleY), true); - } + if (m_enableScrollBars) {} } // void mpWindow::OnScroll2(wxScrollWinEvent &event) @@ -2290,7 +2233,7 @@ void mpWindow::SetMPScrollbars(bool status) // // SetVirtualSize((int) (m_maxX - m_minX), (int) (m_maxY - // m_minY)); // } - // Refresh(false);*/ + // Refresh(true);*/ } bool mpWindow::UpdateBBox() @@ -2426,7 +2369,7 @@ void mpWindow::UpdateAll() } } - Refresh(FALSE); + Refresh(true); } void mpWindow::DoScrollCalc(const int position, const int orientation) @@ -3399,7 +3342,9 @@ void mpBitmapLayer::SetBitmap( const wxImage& inBmp, double x, double y, double lx, double ly) { if (!inBmp.Ok()) - { wxLogError(wxT("[mpBitmapLayer] Assigned bitmap is not Ok()!")); } + { + wxLogError(wxT("[mpBitmapLayer] Assigned bitmap is not Ok()!")); + } else { if (lx < 0) From 977e29fed01e64367b8520af5f855596c53ded9b Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Fri, 25 Jun 2021 08:06:04 +0200 Subject: [PATCH 32/68] fix typo; fix clang-format --- apps/2d-slam-demo/slamdemoMain.cpp | 4 +-- apps/RawLogViewer/CFormRawMap.cpp | 4 +-- apps/RawLogViewer/CScanAnimation.cpp | 4 +-- apps/RawLogViewer/xRawLogViewerMain.cpp | 12 +++------ libs/gui/src/CDisplayWindowPlots.cpp | 4 +-- libs/gui/src/mathplots/mathplot.cpp | 34 +++++++------------------ 6 files changed, 17 insertions(+), 45 deletions(-) diff --git a/apps/2d-slam-demo/slamdemoMain.cpp b/apps/2d-slam-demo/slamdemoMain.cpp index 6ca678ce4d..a950ae72f2 100644 --- a/apps/2d-slam-demo/slamdemoMain.cpp +++ b/apps/2d-slam-demo/slamdemoMain.cpp @@ -1940,9 +1940,7 @@ void slamdemoFrame::executeOneStep() // Save dataset to file? if (m_rawlog_out_file.fileOpenCorrectly()) - { - archiveFrom(m_rawlog_out_file) << act << sf; - } + { archiveFrom(m_rawlog_out_file) << act << sf; } } // For the case of doing D.A., save the correspondences REAL_MAP <-> diff --git a/apps/RawLogViewer/CFormRawMap.cpp b/apps/RawLogViewer/CFormRawMap.cpp index 8aaf2a6150..8f13610dc9 100644 --- a/apps/RawLogViewer/CFormRawMap.cpp +++ b/apps/RawLogViewer/CFormRawMap.cpp @@ -462,9 +462,7 @@ void loadMapInto3DScene(COpenGLScene& scene) auto this_t = it.first; if (distanceBetweenPoints(x0, y0, z0, p.x, p.y, p.z) < 5.5) - { - obj->appendLine(x0, y0, z0, p.x, p.y, p.z); - } + { obj->appendLine(x0, y0, z0, p.x, p.y, p.z); } else if (last_t) { // We have a gap without GT: diff --git a/apps/RawLogViewer/CScanAnimation.cpp b/apps/RawLogViewer/CScanAnimation.cpp index 9aae547280..b5080558db 100644 --- a/apps/RawLogViewer/CScanAnimation.cpp +++ b/apps/RawLogViewer/CScanAnimation.cpp @@ -512,9 +512,7 @@ bool CScanAnimation::update_opengl_viz(const CSensoryFrame& sf) (tim_last != INVALID_TIMESTAMP && fabs(mrpt::system::timeDifference(ro.timestamp, tim_last)) > largest_period)) - { - lst_to_delete.push_back(o.first); - } + { lst_to_delete.push_back(o.first); } } // Remove too old observations: diff --git a/apps/RawLogViewer/xRawLogViewerMain.cpp b/apps/RawLogViewer/xRawLogViewerMain.cpp index 6f9427b968..6310a8fd5a 100644 --- a/apps/RawLogViewer/xRawLogViewerMain.cpp +++ b/apps/RawLogViewer/xRawLogViewerMain.cpp @@ -3162,9 +3162,7 @@ void xRawLogViewerFrame::OnFileCountEntries(wxCommandEvent&) if (newObj->GetRuntimeClass() == CLASS_ID(CSensoryFrame) || newObj->GetRuntimeClass() == CLASS_ID(CActionCollection) || newObj->GetRuntimeClass() == CLASS_ID(CPose2D)) - { - entryIndex++; - } + { entryIndex++; } else { // Unknown class: @@ -4006,9 +4004,7 @@ void xRawLogViewerFrame::OnRemoveSpecificRangeMeas(wxCommandEvent&) obs_2->sensedData[q].sensedDistance); if (filter) - { - obs_2->sensedData[q].sensedDistance = 0; - } + { obs_2->sensedData[q].sensedDistance = 0; } nFilt++; } } @@ -4429,9 +4425,7 @@ void xRawLogViewerFrame::OnRecalculateActionsICP(wxCommandEvent&) { // Check type: if (rawlog.getType(countLoop) == CRawlog::etActionCollection) - { - act_between = rawlog.getAsAction(countLoop); - } + { act_between = rawlog.getAsAction(countLoop); } else if (rawlog.getType(countLoop) == CRawlog::etSensoryFrame) { // This is a SF: diff --git a/libs/gui/src/CDisplayWindowPlots.cpp b/libs/gui/src/CDisplayWindowPlots.cpp index 093a036534..fcdf1051f9 100644 --- a/libs/gui/src/CDisplayWindowPlots.cpp +++ b/libs/gui/src/CDisplayWindowPlots.cpp @@ -690,8 +690,8 @@ void CDisplayWindowPlots::setPos([[maybe_unused]] int x, [[maybe_unused]] int y) /*--------------------------------------------------------------- setWindowTitle ---------------------------------------------------------------*/ -void CDisplayWindowPlots::setWindowTitle( - [[maybe_unused]] const std::string& str) +void CDisplayWindowPlots::setWindowTitle([ + [maybe_unused]] const std::string& str) { #if MRPT_HAS_WXWIDGETS if (!isOpen()) diff --git a/libs/gui/src/mathplots/mathplot.cpp b/libs/gui/src/mathplots/mathplot.cpp index d7f7ebb0ec..6019eb8b97 100644 --- a/libs/gui/src/mathplots/mathplot.cpp +++ b/libs/gui/src/mathplots/mathplot.cpp @@ -889,9 +889,7 @@ void mpScaleX::Plot(wxDC& dc, mpWindow& w) { // Date and/or time axis representation if (m_labelType == mpX_DATETIME) - { - fmt = (wxT("%04.0f-%02.0f-%02.0fT%02.0f:%02.0f:%02.0f")); - } + { fmt = (wxT("%04.0f-%02.0f-%02.0fT%02.0f:%02.0f:%02.0f")); } else if (m_labelType == mpX_DATE) { fmt = (wxT("%04.0f-%02.0f-%02.0f")); @@ -955,15 +953,11 @@ void mpScaleX::Plot(wxDC& dc, mpWindow& w) #endif dc.SetPen(m_pen); if ((m_flags == mpALIGN_BOTTOM) && !m_drawOutsideMargins) - { - dc.DrawLine(p, orgy + 4, p, minYpx); - } + { dc.DrawLine(p, orgy + 4, p, minYpx); } else { if ((m_flags == mpALIGN_TOP) && !m_drawOutsideMargins) - { - dc.DrawLine(p, orgy - 4, p, maxYpx); - } + { dc.DrawLine(p, orgy - 4, p, maxYpx); } else { dc.DrawLine(p, 0 /*-w.GetScrY()*/, p, w.GetScrY()); @@ -1087,9 +1081,7 @@ void mpScaleX::Plot(wxDC& dc, mpWindow& w) dc.GetTextExtent(s, &tx, &ty); if ((m_flags == mpALIGN_BORDER_BOTTOM) || (m_flags == mpALIGN_TOP)) - { - dc.DrawText(s, p - tx / 2, orgy - 4 - ty); - } + { dc.DrawText(s, p - tx / 2, orgy - 4 - ty); } else { dc.DrawText(s, p - tx / 2, orgy + 4); @@ -1267,9 +1259,7 @@ void mpScaleY::Plot(wxDC& dc, mpWindow& w) if (m_ticks) { // Draw axis ticks if (m_flags == mpALIGN_BORDER_LEFT) - { - dc.DrawLine(orgx, p, orgx + 4, p); - } + { dc.DrawLine(orgx, p, orgx + 4, p); } else { dc.DrawLine( @@ -1285,15 +1275,11 @@ void mpScaleY::Plot(wxDC& dc, mpWindow& w) #endif dc.SetPen(m_pen); if ((m_flags == mpALIGN_LEFT) && !m_drawOutsideMargins) - { - dc.DrawLine(orgx - 4, p, endPx, p); - } + { dc.DrawLine(orgx - 4, p, endPx, p); } else { if ((m_flags == mpALIGN_RIGHT) && !m_drawOutsideMargins) - { - dc.DrawLine(minYpx, p, orgx + 4, p); - } + { dc.DrawLine(minYpx, p, orgx + 4, p); } else { dc.DrawLine(0 /*-w.GetScrX()*/, p, w.GetScrX(), p); @@ -1627,7 +1613,7 @@ void mpWindow::OnMouseLeftDown(wxMouseEvent& event) event.GetY()); /*m_mouseLClick_X, m_mouseLClick_Y);*/ #endif wxPoint pointClicked = event.GetPosition(); - ´ m_movingInfoLayer = IsInsideInfoLayer(pointClicked); + m_movingInfoLayer = IsInsideInfoLayer(pointClicked); if (m_movingInfoLayer != nullptr) { #ifdef MATHPLOT_DO_LOGGING @@ -3342,9 +3328,7 @@ void mpBitmapLayer::SetBitmap( const wxImage& inBmp, double x, double y, double lx, double ly) { if (!inBmp.Ok()) - { - wxLogError(wxT("[mpBitmapLayer] Assigned bitmap is not Ok()!")); - } + { wxLogError(wxT("[mpBitmapLayer] Assigned bitmap is not Ok()!")); } else { if (lx < 0) From 707ef29778fabd2a1ceaa939fe5a0a87616402e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Blanco=20Claraco?= Date: Fri, 25 Jun 2021 13:12:16 +0200 Subject: [PATCH 33/68] fix missing PPA export variables --- packaging/prepare_ubuntu_pkgs_for_ppa.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packaging/prepare_ubuntu_pkgs_for_ppa.sh b/packaging/prepare_ubuntu_pkgs_for_ppa.sh index 3750ce8866..3d7d696bea 100755 --- a/packaging/prepare_ubuntu_pkgs_for_ppa.sh +++ b/packaging/prepare_ubuntu_pkgs_for_ppa.sh @@ -46,6 +46,10 @@ export MRPT_PKG_EXPORTED_SUBMODULES_hirsute="" export DEB_EXTRA_BUILD_DEPS_hirsute="libsimpleini-dev, libnanoflann-dev" export DEB_NANOFLANN_DEP_hirsute="libnanoflann-dev" # make mrpt-math-dev to depend on nanoflann headers +export MRPT_PKG_EXPORTED_SUBMODULES_impish="" +export DEB_EXTRA_BUILD_DEPS_impish="libsimpleini-dev, libnanoflann-dev" +export DEB_NANOFLANN_DEP_impish="libnanoflann-dev" # make mrpt-math-dev to depend on nanoflann headers + # Checks # -------------------------------- From 54a98abb058322e85be363ee2ee9ad62228ce08e Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Sun, 27 Jun 2021 02:03:31 +0200 Subject: [PATCH 34/68] BUGFIX: CImage::getPixelDepth() should force loading lazy load images --- doc/source/doxygen-docs/changelog.md | 1 + libs/img/src/CImage.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 04cdadfbfd..ff3ed9aafb 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -26,6 +26,7 @@ - Fix build error with GCC 8 in `mrpt/containers/yaml.h`. - Fix exception rendering empty point clouds due to invalid bounding box. - Fix broken 2D plots rendering in Ubuntu 20.04 (and probably other systems), via an update in mpWindow to properly use wxAutoBufferedPaintDC. + - mrpt::img::CImage::getPixelDepth() should force loading lazy load images. # Version 2.3.1: Released May 26th, 2021 - General cmake scripts: diff --git a/libs/img/src/CImage.cpp b/libs/img/src/CImage.cpp index 8bce27874f..9f54f28028 100644 --- a/libs/img/src/CImage.cpp +++ b/libs/img/src/CImage.cpp @@ -290,6 +290,7 @@ PixelDepth CImage::getPixelDepth() const { MRPT_START #if MRPT_HAS_OPENCV + makeSureImageIsLoaded(); // For delayed loaded images stored externally return cvDepth2PixelDepth(m_impl->img.depth()); #else THROW_EXCEPTION("The MRPT has been compiled with MRPT_HAS_OPENCV=0 !"); From 02a2951b2655c781c0a7719183619633984911ab Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Sun, 27 Jun 2021 02:09:01 +0200 Subject: [PATCH 35/68] BUGFIX: wrong rendering of different textures within the same opengl shader program --- doc/source/doxygen-docs/changelog.md | 1 + .../CRenderizableShaderTexturedTriangles.h | 6 +- .../CRenderizableShaderTexturedTriangles.cpp | 68 +++++++++++++++---- libs/opengl/src/RenderQueue.cpp | 4 -- 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index ff3ed9aafb..495d137968 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -27,6 +27,7 @@ - Fix exception rendering empty point clouds due to invalid bounding box. - Fix broken 2D plots rendering in Ubuntu 20.04 (and probably other systems), via an update in mpWindow to properly use wxAutoBufferedPaintDC. - mrpt::img::CImage::getPixelDepth() should force loading lazy load images. + - Fixed wrong rendering of different textures within the same opengl shader program. # Version 2.3.1: Released May 26th, 2021 - General cmake scripts: diff --git a/libs/opengl/include/mrpt/opengl/CRenderizableShaderTexturedTriangles.h b/libs/opengl/include/mrpt/opengl/CRenderizableShaderTexturedTriangles.h index 7d15518da2..aa8cf262af 100644 --- a/libs/opengl/include/mrpt/opengl/CRenderizableShaderTexturedTriangles.h +++ b/libs/opengl/include/mrpt/opengl/CRenderizableShaderTexturedTriangles.h @@ -114,6 +114,7 @@ class CRenderizableShaderTexturedTriangles : public virtual CRenderizable bool m_enableLight = false; mutable unsigned int m_glTextureName{0}; + mutable std::optional m_glTextureUnit{0}; //!< the "i" in GL_TEXTUREi mutable bool m_texture_is_loaded{false}; mutable bool m_texture_is_pending_destruction{false}; bool m_textureImageAssigned = false; @@ -127,8 +128,9 @@ class CRenderizableShaderTexturedTriangles : public virtual CRenderizable void unloadTexture(); - static unsigned int getNewTextureNumber(); - static void releaseTextureName(unsigned int i); + /// Returns: [texture name, texture unit] + static std::pair getNewTextureNumber(); + static void releaseTextureName(unsigned int texName, unsigned int texUnit); mutable COpenGLBuffer m_vertexBuffer; mutable COpenGLVertexArrayObject m_vao; diff --git a/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp b/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp index 3b61e6fa2b..5724c0b244 100644 --- a/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp +++ b/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp @@ -18,6 +18,7 @@ #include #include // std::align +#include #include using namespace mrpt; @@ -62,12 +63,13 @@ void CRenderizableShaderTexturedTriangles::render(const RenderContext& rc) const // This will load and/or select our texture, only once: initializeTextures(); + ASSERT_(m_glTextureUnit.has_value()); // Set the texture uniform: { const Program& s = *rc.shader; - // bound to GL_TEXTURE0: - glUniform1i(s.uniformId("textureSampler"), 0); + // bound to GL_TEXTURE0 + "i": + glUniform1i(s.uniformId("textureSampler"), *m_glTextureUnit); } // Enable/disable lights: @@ -281,7 +283,8 @@ void CRenderizableShaderTexturedTriangles::initializeTextures() const if (m_texture_is_pending_destruction) { m_texture_is_pending_destruction = false; - releaseTextureName(m_glTextureName); + ASSERT_(m_glTextureUnit.has_value()); + releaseTextureName(m_glTextureName, *m_glTextureUnit); m_glTextureName = 0; } @@ -293,20 +296,26 @@ void CRenderizableShaderTexturedTriangles::initializeTextures() const if (m_texture_is_loaded) { // activate the texture unit first before binding texture - glActiveTexture(GL_TEXTURE0); + ASSERT_(m_glTextureUnit.has_value()); + glActiveTexture(GL_TEXTURE0 + *m_glTextureUnit); glBindTexture(GL_TEXTURE_2D, m_glTextureName); CHECK_OPENGL_ERROR(); return; } // Reserve the new one -------------------------- + m_textureImage.forceLoad(); // just in case they are lazy-load imgs + m_textureImageAlpha.forceLoad(); + ASSERT_(m_textureImage.getPixelDepth() == mrpt::img::PixelDepth::D8U); // allocate texture names: - m_glTextureName = getNewTextureNumber(); + const auto p = getNewTextureNumber(); + m_glTextureName = p.first; + m_glTextureUnit = p.second; // activate the texture unit first before binding texture - glActiveTexture(GL_TEXTURE0); + glActiveTexture(GL_TEXTURE0 + *m_glTextureUnit); // select our current texture glBindTexture(GL_TEXTURE_2D, m_glTextureName); CHECK_OPENGL_ERROR(); @@ -603,7 +612,8 @@ class TextureResourceHandler return o; } - unsigned int generateTextureID() + /// Return [textureName, textureUnit] + std::pair generateTextureID() { #if MRPT_HAS_OPENGL_GLUT auto lck = mrpt::lockHelper(m_texturesMtx); @@ -616,24 +626,50 @@ class TextureResourceHandler CHECK_OPENGL_ERROR(); m_textureReservedFrom[textureID] = std::this_thread::get_id(); - return textureID; + int foundUnit = -1; + for (int i = 0; i < m_maxTextureUnits; i++) + if (!m_occupiedTextureUnits.count(i)) + { + foundUnit = i; + break; + } + if (foundUnit < 0) + { + foundUnit = 0; + std::cerr + << "[mrpt TextureResourceHandler] **WARNING**: Apparently " + "your program reached the maximum number of allowed " + "simultaneous OpenGL textures (" + << m_maxTextureUnits << ")" << std::endl; + } + else + { + m_occupiedTextureUnits.insert(foundUnit); + } + + return {textureID, foundUnit}; #else THROW_EXCEPTION("This function needs OpenGL"); #endif } - void releaseTextureID(unsigned int id) + void releaseTextureID(unsigned int texName, unsigned int texUnit) { #if MRPT_HAS_OPENGL_GLUT auto lck = mrpt::lockHelper(m_texturesMtx); - m_destroyQueue[std::this_thread::get_id()].push_back(id); + m_destroyQueue[std::this_thread::get_id()].push_back(texName); processDestroyQueue(); + m_occupiedTextureUnits.erase(texUnit); #endif } private: - TextureResourceHandler() = default; + TextureResourceHandler() + { + glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &m_maxTextureUnits); + // std::cout << "max texture units: " << m_maxTextureUnits << std::endl; + } void processDestroyQueue() { @@ -653,17 +689,21 @@ class TextureResourceHandler std::mutex m_texturesMtx; std::map m_textureReservedFrom; std::map> m_destroyQueue; + std::set m_occupiedTextureUnits; + GLint m_maxTextureUnits; #endif }; -unsigned int CRenderizableShaderTexturedTriangles::getNewTextureNumber() +std::pair + CRenderizableShaderTexturedTriangles::getNewTextureNumber() { return TextureResourceHandler::Instance().generateTextureID(); } -void CRenderizableShaderTexturedTriangles::releaseTextureName(unsigned int i) +void CRenderizableShaderTexturedTriangles::releaseTextureName( + unsigned int texName, unsigned int texUnit) { - TextureResourceHandler::Instance().releaseTextureID(i); + TextureResourceHandler::Instance().releaseTextureID(texName, texUnit); } const mrpt::math::TBoundingBox diff --git a/libs/opengl/src/RenderQueue.cpp b/libs/opengl/src/RenderQueue.cpp index 260abeb1b0..caa515fade 100644 --- a/libs/opengl/src/RenderQueue.cpp +++ b/libs/opengl/src/RenderQueue.cpp @@ -162,10 +162,6 @@ void mrpt::opengl::processRenderQueue( shader.uniformId("pmv_matrix"), 1, IS_TRANSPOSED, rqe.renderState.pmv_matrix.data()); - // Use Texture0: - if (shader.hasUniform("textureSampler")) - glUniform1i(shader.uniformId("textureSampler"), 0); - CRenderizable::RenderContext rc; rc.shader = &shader; rc.shader_id = rqSet.first; From 8c8281688f9336f74d46a3988c8214087eec3b45 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 27 Jun 2021 09:25:17 +0200 Subject: [PATCH 36/68] RawLogViewer: fix weird mono font --- apps/RawLogViewer/xRawLogViewerMain.cpp | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/apps/RawLogViewer/xRawLogViewerMain.cpp b/apps/RawLogViewer/xRawLogViewerMain.cpp index 6310a8fd5a..907e185495 100644 --- a/apps/RawLogViewer/xRawLogViewerMain.cpp +++ b/apps/RawLogViewer/xRawLogViewerMain.cpp @@ -389,6 +389,10 @@ xRawLogViewerFrame::xRawLogViewerFrame(wxWindow* parent, wxWindowID id) // Load my custom icons: wxArtProvider::Push(new MyArtProvider); + const wxFont monoFont( + 8, wxFontFamily::wxFONTFAMILY_TELETYPE, wxFontStyle::wxFONTSTYLE_NORMAL, + wxFontWeight::wxFONTWEIGHT_NORMAL); + //(*Initialize(xRawLogViewerFrame) wxMenu* Menu39; wxBoxSizer* BoxSizer6; @@ -677,10 +681,7 @@ xRawLogViewerFrame::xRawLogViewerFrame(wxWindow* parent, wxWindowID id) wxTE_MULTILINE | wxTE_READONLY | wxTE_WORDWRAP | wxNO_BORDER | wxVSCROLL, wxDefaultValidator, _T("ID_TEXTCTRL1")); - wxFont memoFont( - wxSize(10, 10), wxFontFamily::wxFONTFAMILY_TELETYPE, - wxFontStyle::wxFONTSTYLE_NORMAL, wxFontWeight::wxFONTWEIGHT_NORMAL); - memo->SetFont(memoFont); + memo->SetFont(monoFont); BoxSizer2->Add(memo, 1, wxALL | wxEXPAND | wxALIGN_LEFT | wxALIGN_TOP, 0); Panel3->SetSizer(BoxSizer2); BoxSizer2->Fit(Panel3); @@ -720,10 +721,7 @@ xRawLogViewerFrame::xRawLogViewerFrame(wxWindow* parent, wxWindowID id) wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY | wxVSCROLL, wxDefaultValidator, _T("ID_TEXTCTRL2")); memStats->SetMinSize(wxSize(-1, 150)); - wxFont memStatsFont( - 10, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxNORMAL, false, - _T("Monospace"), wxFONTENCODING_DEFAULT); - memStats->SetFont(memStatsFont); + memStats->SetFont(monoFont); memStats->SetToolTip(_("Statistics of the rawlog load")); Panel11 = new wxPanel( SplitterWindow2, ID_PANEL25, wxDefaultPosition, wxDefaultSize, @@ -742,10 +740,7 @@ xRawLogViewerFrame::xRawLogViewerFrame(wxWindow* parent, wxWindowID id) Notebook3, ID_TEXTCTRL3, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY, wxDefaultValidator, _T("ID_TEXTCTRL3")); - wxFont txtExceptionFont( - 10, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxNORMAL, false, - _T("Monospace"), wxFONTENCODING_DEFAULT); - txtException->SetFont(txtExceptionFont); + txtException->SetFont(monoFont); Notebook3->AddPage(SplitterWindow2, _("Dataset statistics && info"), false); Notebook3->AddPage(txtException, _("End of load message"), false); FlexGridSizer6->Add( From d7b0daaa39357a9c21a17fe354584fd8f49b8b12 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 27 Jun 2021 10:08:49 +0200 Subject: [PATCH 37/68] get_env(): fix clang-tidy warning if using return as global static --- libs/core/include/mrpt/core/get_env.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/core/include/mrpt/core/get_env.h b/libs/core/include/mrpt/core/get_env.h index 938348be78..d0874afcf5 100644 --- a/libs/core/include/mrpt/core/get_env.h +++ b/libs/core/include/mrpt/core/get_env.h @@ -12,6 +12,7 @@ #include #include +#include namespace mrpt { @@ -19,18 +20,20 @@ namespace mrpt * \ingroup mrpt_core_grp */ template -inline T get_env(const std::string& varname, const T& defValue = T()) +inline T get_env(const std::string_view& varname, const T& defValue = T()) { - auto s = ::getenv(varname.c_str()); + const std::string v(varname.data(), varname.size()); + auto s = ::getenv(v.c_str()); if (!s) return defValue; return mrpt::from_string(s, defValue, false /*dont throw*/); } /** Specialization for bool: understands "true", "True", number!=0 as `true` */ template <> -inline bool get_env(const std::string& varname, const bool& defValue) +inline bool get_env(const std::string_view& varname, const bool& defValue) { - auto s = ::getenv(varname.c_str()); + const std::string v(varname.data(), varname.size()); + auto s = ::getenv(v.c_str()); if (!s) return defValue; const std::string str(s); if (str == "true" || str == "TRUE" || str == "True") return true; From 17e4a7ec8b367826d473cb489fd98b0788facb78 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 27 Jun 2021 10:09:06 +0200 Subject: [PATCH 38/68] Fix more CImage copy errors if using lazy load. --- libs/img/src/CImage.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/img/src/CImage.cpp b/libs/img/src/CImage.cpp index 9f54f28028..80346df8e9 100644 --- a/libs/img/src/CImage.cpp +++ b/libs/img/src/CImage.cpp @@ -201,6 +201,9 @@ CImage::CImage(const CImage& img, copy_type_t copy_type) CImage() #endif { + // Also, copy our custom fields: + m_imgIsExternalStorage = img.m_imgIsExternalStorage; + m_externalFile = img.m_externalFile; } CImage CImage::makeDeepCopy() const @@ -217,6 +220,7 @@ CImage CImage::makeDeepCopy() const void CImage::asCvMat(cv::Mat& out_img, copy_type_t copy_type) const { #if MRPT_HAS_OPENCV + makeSureImageIsLoaded(); if (copy_type == DEEP_COPY) out_img = m_impl->img.clone(); else out_img = m_impl->img; From 7c5943cb5cc12d05c62a95dcc965dc2769c17a83 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 27 Jun 2021 10:09:47 +0200 Subject: [PATCH 39/68] Fix opengl textures not being released correctly; optional debug traces --- .../src/CRenderizableShaderTexturedTriangles.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp b/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp index 5724c0b244..ed440ac532 100644 --- a/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp +++ b/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp @@ -9,6 +9,7 @@ #include "opengl-precomp.h" // Precompiled header // +#include #include #include #include @@ -31,6 +32,9 @@ using mrpt::img::CImage; IMPLEMENTS_VIRTUAL_SERIALIZABLE( CRenderizableShaderTexturedTriangles, CRenderizable, mrpt::opengl) +const bool MRPT_OPENGL_VERBOSE = + mrpt::get_env("MRPT_OPENGL_VERBOSE", false); + // Whether to profile memory allocations: //#define TEXTUREOBJ_PROFILE_MEM_ALLOC @@ -552,6 +556,9 @@ void CRenderizableShaderTexturedTriangles::unloadTexture() { m_texture_is_loaded = false; m_texture_is_pending_destruction = true; + ASSERT_(m_glTextureUnit.has_value()); + releaseTextureName(m_glTextureName, *m_glTextureUnit); + m_glTextureName = 0; } } @@ -647,6 +654,10 @@ class TextureResourceHandler m_occupiedTextureUnits.insert(foundUnit); } + if (MRPT_OPENGL_VERBOSE) + std::cout << "[mrpt generateTextureID] textureName:" << textureID + << " unit: " << foundUnit << std::endl; + return {textureID, foundUnit}; #else THROW_EXCEPTION("This function needs OpenGL"); @@ -658,6 +669,10 @@ class TextureResourceHandler #if MRPT_HAS_OPENGL_GLUT auto lck = mrpt::lockHelper(m_texturesMtx); + if (MRPT_OPENGL_VERBOSE) + std::cout << "[mrpt releaseTextureID] textureName: " << texName + << " unit: " << texUnit << std::endl; + m_destroyQueue[std::this_thread::get_id()].push_back(texName); processDestroyQueue(); m_occupiedTextureUnits.erase(texUnit); From dd7f2f748c36a8f545c06223fd35da0945370c51 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco-Claraco Date: Sun, 27 Jun 2021 11:26:49 +0200 Subject: [PATCH 40/68] fix compile w/o opengl --- libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp b/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp index ed440ac532..0ec13bda8e 100644 --- a/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp +++ b/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp @@ -682,8 +682,12 @@ class TextureResourceHandler private: TextureResourceHandler() { +#if MRPT_HAS_OPENGL_GLUT glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &m_maxTextureUnits); - // std::cout << "max texture units: " << m_maxTextureUnits << std::endl; + if (MRPT_OPENGL_VERBOSE) + std::cout << "[mrpt TextureResourceHandler] maxTextureUnits:" + << m_maxTextureUnits << std::endl; +#endif } void processDestroyQueue() From f4161392c3ed5ddd7eedaf6258cf165816a9ed99 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 28 Jun 2021 09:29:29 +0200 Subject: [PATCH 41/68] Fix the too-large error message windows in other places too. --- apps/SceneViewer3D/_DSceneViewerMain.cpp | 45 ++++++++++++++---------- libs/gui/include/mrpt/gui/WxUtils.h | 7 ++-- libs/gui/src/CDisplayWindow3D.cpp | 4 +-- libs/gui/src/error_box.cpp | 8 +++-- 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/apps/SceneViewer3D/_DSceneViewerMain.cpp b/apps/SceneViewer3D/_DSceneViewerMain.cpp index 1d143cbc62..5b76c232fe 100644 --- a/apps/SceneViewer3D/_DSceneViewerMain.cpp +++ b/apps/SceneViewer3D/_DSceneViewerMain.cpp @@ -9,6 +9,7 @@ #include "_DSceneViewerMain.h" +#include // firstNLines() #include #include "CDlgCamTracking.h" @@ -168,22 +169,32 @@ wxLogWindow* logWin = nullptr; void saveLastUsedDirectoryToCfgFile(const std::string& fil) { - try - { - iniFile->write(iniFileSect, "LastDir", extractFileDirectory(fil)); - } - catch (const std::exception& e) - { - wxMessageBox(mrpt::exception_to_str(e), _("Exception"), wxOK); - } + WX_START_TRY + + iniFile->write(iniFileSect, "LastDir", extractFileDirectory(fil)); + + WX_END_TRY } void CMyGLCanvas::OnRenderError(const wxString& str) { - if (!logWin) logWin = new wxLogWindow(this, wxT("Log window"), false); + const size_t maxLines = 7; + const std::string sErr = + mrpt::system::firstNLines(str.ToStdString(), maxLines); + + if (!logWin) + { + logWin = new wxLogWindow(this, wxT("Log window"), false); + logWin->Show(); + } - wxLogError(str); - logWin->Show(); + static double lastErrMsg = 0; + double tNow = mrpt::Clock::nowDouble(); + if (tNow - lastErrMsg > 10.0) + { + lastErrMsg = tNow; + wxLogError(wxString(sErr)); + } } void CMyGLCanvas::OnPreRender() {} @@ -866,6 +877,7 @@ void _DSceneViewerFrame::OnOpenFile(wxCommandEvent& event) void _DSceneViewerFrame::loadFromFile( const std::string& fil, bool isInASequence) { + WX_START_TRY try { // Save the path @@ -963,17 +975,12 @@ void _DSceneViewerFrame::loadFromFile( Refresh(false); } - catch (const std::exception& e) - { - std::cerr << mrpt::exception_to_str(e) << std::endl; - btnAutoplay->SetValue(false); - wxMessageBox(mrpt::exception_to_str(e), _("Exception"), wxOK, this); - } - catch (...) + catch (const std::exception&) { btnAutoplay->SetValue(false); - wxMessageBox(_("Runtime error!"), _("Exception"), wxOK, this); + throw; } + WX_END_TRY } void _DSceneViewerFrame::updateTitle() diff --git a/libs/gui/include/mrpt/gui/WxUtils.h b/libs/gui/include/mrpt/gui/WxUtils.h index 2c3124ae74..ca47347d16 100644 --- a/libs/gui/include/mrpt/gui/WxUtils.h +++ b/libs/gui/include/mrpt/gui/WxUtils.h @@ -12,6 +12,7 @@ #include #include #include +#include // firstNLines() #if MRPT_HAS_WXWIDGETS @@ -76,8 +77,10 @@ namespace gui } \ catch (std::exception & e) \ { \ - wxMessageBox( \ - mrpt::exception_to_str(e), wxT("Exception"), wxOK, nullptr); \ + const size_t maxLines = 7; \ + const std::string sErr = \ + mrpt::system::firstNLines(mrpt::exception_to_str(e), maxLines); \ + wxMessageBox(sErr, wxT("Exception"), wxOK, nullptr); \ } \ catch (...) \ { \ diff --git a/libs/gui/src/CDisplayWindow3D.cpp b/libs/gui/src/CDisplayWindow3D.cpp index 345eb711fa..1674dd9c55 100644 --- a/libs/gui/src/CDisplayWindow3D.cpp +++ b/libs/gui/src/CDisplayWindow3D.cpp @@ -328,9 +328,9 @@ void C3DWindowDialog::OnClose(wxCloseEvent& event) } // Menu: Close -void C3DWindowDialog::OnMenuClose(wxCommandEvent& event) { Close(); } +void C3DWindowDialog::OnMenuClose(wxCommandEvent&) { Close(); } // Menu: About -void C3DWindowDialog::OnMenuAbout(wxCommandEvent& event) +void C3DWindowDialog::OnMenuAbout(wxCommandEvent&) { ::wxMessageBox( _("3D Scene viewer\n Class gui::CDisplayWindow3D\n MRPT C++ library"), diff --git a/libs/gui/src/error_box.cpp b/libs/gui/src/error_box.cpp index a52bfe3116..6c973b8916 100644 --- a/libs/gui/src/error_box.cpp +++ b/libs/gui/src/error_box.cpp @@ -10,6 +10,7 @@ #include "gui-precomp.h" // Precompiled headers // #include +#include // firstNLines() #if MRPT_HAS_Qt5 #include @@ -40,12 +41,15 @@ void mrpt::gui::tryCatch( void mrpt::gui::showErrorMessage(const std::string& str) { + const size_t maxLines = 7; + const std::string sErr = mrpt::system::firstNLines(str, maxLines); + #if MRPT_HAS_Qt5 QErrorMessage msg; - msg.showMessage(QString::fromStdString(str)); + msg.showMessage(QString::fromStdString(sErr)); msg.exec(); #elif MRPT_HAS_WXWIDGETS - wxMessageBox(str.c_str(), _("Exception")); + wxMessageBox(sErr.c_str(), _("Exception")); #else std::cerr << str << std::endl; #endif // MRPT_HAS_Qt5 From b3cdd7badd941dfbdb6fa8f01938e8078e5d6d09 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 28 Jun 2021 09:29:45 +0200 Subject: [PATCH 42/68] remove useless declaration --- libs/opengl/include/mrpt/opengl/CTexturedPlane.h | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/opengl/include/mrpt/opengl/CTexturedPlane.h b/libs/opengl/include/mrpt/opengl/CTexturedPlane.h index ccfbc9640d..c5a964172b 100644 --- a/libs/opengl/include/mrpt/opengl/CTexturedPlane.h +++ b/libs/opengl/include/mrpt/opengl/CTexturedPlane.h @@ -29,7 +29,6 @@ class CTexturedPlane : public CRenderizableShaderTexturedTriangles /** Used for ray-tracing */ mutable std::vector tmpPoly; void updatePoly() const; - void unloadTexture(); public: /** @name Renderizable shader API virtual methods From c1a4ccc13f5b285ec645060f139cbd7b815f89af Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 28 Jun 2021 10:28:30 +0200 Subject: [PATCH 43/68] gl texture house keeping: simplify and fix corner cases --- .../CRenderizableShaderTexturedTriangles.h | 26 +++++-- .../CRenderizableShaderTexturedTriangles.cpp | 78 +++++++++---------- libs/opengl/src/CTexturedPlane.cpp | 13 ---- 3 files changed, 59 insertions(+), 58 deletions(-) diff --git a/libs/opengl/include/mrpt/opengl/CRenderizableShaderTexturedTriangles.h b/libs/opengl/include/mrpt/opengl/CRenderizableShaderTexturedTriangles.h index aa8cf262af..40a8f8ca03 100644 --- a/libs/opengl/include/mrpt/opengl/CRenderizableShaderTexturedTriangles.h +++ b/libs/opengl/include/mrpt/opengl/CRenderizableShaderTexturedTriangles.h @@ -110,13 +110,27 @@ class CRenderizableShaderTexturedTriangles : public virtual CRenderizable void writeToStreamTexturedObject(mrpt::serialization::CArchive& out) const; void readFromStreamTexturedObject(mrpt::serialization::CArchive& in); + using texture_name_t = unsigned int; + /// the "i" in GL_TEXTUREi + using texture_unit_t = int; + + struct texture_name_unit_t + { + texture_name_unit_t() = default; + texture_name_unit_t(texture_name_t Name, texture_unit_t Unit) + : name(Name), unit(Unit) + { + } + + texture_name_t name = 0; + /// the "i" in GL_TEXTUREi + texture_unit_t unit = 0; + }; + private: bool m_enableLight = false; - mutable unsigned int m_glTextureName{0}; - mutable std::optional m_glTextureUnit{0}; //!< the "i" in GL_TEXTUREi - mutable bool m_texture_is_loaded{false}; - mutable bool m_texture_is_pending_destruction{false}; + mutable std::optional m_glTexture; bool m_textureImageAssigned = false; mutable mrpt::img::CImage m_textureImage{4, 4}; mutable mrpt::img::CImage m_textureImageAlpha; @@ -129,8 +143,8 @@ class CRenderizableShaderTexturedTriangles : public virtual CRenderizable void unloadTexture(); /// Returns: [texture name, texture unit] - static std::pair getNewTextureNumber(); - static void releaseTextureName(unsigned int texName, unsigned int texUnit); + static texture_name_unit_t getNewTextureNumber(); + static void releaseTextureName(const texture_name_unit_t& t); mutable COpenGLBuffer m_vertexBuffer; mutable COpenGLVertexArrayObject m_vao; diff --git a/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp b/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp index 0ec13bda8e..3295c03599 100644 --- a/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp +++ b/libs/opengl/src/CRenderizableShaderTexturedTriangles.cpp @@ -67,13 +67,13 @@ void CRenderizableShaderTexturedTriangles::render(const RenderContext& rc) const // This will load and/or select our texture, only once: initializeTextures(); - ASSERT_(m_glTextureUnit.has_value()); + ASSERT_(m_glTexture.has_value()); // Set the texture uniform: { const Program& s = *rc.shader; // bound to GL_TEXTURE0 + "i": - glUniform1i(s.uniformId("textureSampler"), *m_glTextureUnit); + glUniform1i(s.uniformId("textureSampler"), m_glTexture->unit); } // Enable/disable lights: @@ -281,28 +281,27 @@ void CRenderizableShaderTexturedTriangles::initializeTextures() const static mrpt::system::CTimeLogger tim; #endif - // Destroy a former texture, if it was freed: - // It's important we do this from the very same thread from which it was - // reserved: - if (m_texture_is_pending_destruction) + // Note: if we are rendering and the user assigned us no texture image, + // let's create a dummy one with the uniform CRenderizable's color: + if (!textureImageHasBeenAssigned() || m_textureImage.isEmpty()) { - m_texture_is_pending_destruction = false; - ASSERT_(m_glTextureUnit.has_value()); - releaseTextureName(m_glTextureName, *m_glTextureUnit); - m_glTextureName = 0; + mrpt::img::CImage im_rgb(4, 4, mrpt::img::CH_RGB), + im_a(4, 4, mrpt::img::CH_GRAY); + im_rgb.filledRectangle(0, 0, 3, 3, m_color); + im_a.filledRectangle( + 0, 0, 3, 3, + mrpt::img::TColor(m_color.A, m_color.A, m_color.A, m_color.A)); + const_cast(this)->assignImage( + std::move(im_rgb), std::move(im_a)); } - // Do nothing until we are assigned an image. - if (m_textureImage.isEmpty()) return; - try { - if (m_texture_is_loaded) + if (m_glTexture.has_value()) { // activate the texture unit first before binding texture - ASSERT_(m_glTextureUnit.has_value()); - glActiveTexture(GL_TEXTURE0 + *m_glTextureUnit); - glBindTexture(GL_TEXTURE_2D, m_glTextureName); + glActiveTexture(GL_TEXTURE0 + m_glTexture->unit); + glBindTexture(GL_TEXTURE_2D, m_glTexture->name); CHECK_OPENGL_ERROR(); return; } @@ -314,14 +313,12 @@ void CRenderizableShaderTexturedTriangles::initializeTextures() const ASSERT_(m_textureImage.getPixelDepth() == mrpt::img::PixelDepth::D8U); // allocate texture names: - const auto p = getNewTextureNumber(); - m_glTextureName = p.first; - m_glTextureUnit = p.second; + m_glTexture = getNewTextureNumber(); // activate the texture unit first before binding texture - glActiveTexture(GL_TEXTURE0 + *m_glTextureUnit); + glActiveTexture(GL_TEXTURE0 + m_glTexture->unit); // select our current texture - glBindTexture(GL_TEXTURE_2D, m_glTextureName); + glBindTexture(GL_TEXTURE_2D, m_glTexture->name); CHECK_OPENGL_ERROR(); // when texture area is small, linear interpolation. Default is @@ -494,7 +491,9 @@ void CRenderizableShaderTexturedTriangles::initializeTextures() const } // End of color texture WITHOUT trans. - m_texture_is_loaded = true; + // Was: m_texture_is_loaded = true; + // Now this situation is represented by the optional m_glTexture having + // a valid value. #ifdef TEXTUREOBJ_PROFILE_MEM_ALLOC { @@ -527,8 +526,9 @@ void CRenderizableShaderTexturedTriangles::initializeTextures() const } catch (exception& e) { - THROW_EXCEPTION( - format("m_glTextureName=%i\n%s", m_glTextureName, e.what())); + THROW_EXCEPTION(format( + "m_glTextureName=%i\n%s", m_glTexture ? m_glTexture->name : 0, + e.what())); } catch (...) { @@ -552,14 +552,11 @@ CRenderizableShaderTexturedTriangles::~CRenderizableShaderTexturedTriangles() } void CRenderizableShaderTexturedTriangles::unloadTexture() { - if (m_texture_is_loaded) - { - m_texture_is_loaded = false; - m_texture_is_pending_destruction = true; - ASSERT_(m_glTextureUnit.has_value()); - releaseTextureName(m_glTextureName, *m_glTextureUnit); - m_glTextureName = 0; - } + if (!m_glTexture.has_value()) return; + + releaseTextureName(*m_glTexture); + + m_glTexture.reset(); } void CRenderizableShaderTexturedTriangles::writeToStreamTexturedObject( @@ -580,8 +577,6 @@ void CRenderizableShaderTexturedTriangles::readFromStreamTexturedObject( uint8_t version; in >> version; - CRenderizable::notifyChange(); - switch (version) { case 0: @@ -605,6 +600,7 @@ void CRenderizableShaderTexturedTriangles::readFromStreamTexturedObject( break; default: MRPT_THROW_UNKNOWN_SERIALIZATION_VERSION(version); }; + CRenderizable::notifyChange(); } @@ -713,16 +709,20 @@ class TextureResourceHandler #endif }; -std::pair +CRenderizableShaderTexturedTriangles::texture_name_unit_t CRenderizableShaderTexturedTriangles::getNewTextureNumber() { - return TextureResourceHandler::Instance().generateTextureID(); + CRenderizableShaderTexturedTriangles::texture_name_unit_t ret; + const auto r = TextureResourceHandler::Instance().generateTextureID(); + ret.name = r.first; + ret.unit = r.second; + return ret; } void CRenderizableShaderTexturedTriangles::releaseTextureName( - unsigned int texName, unsigned int texUnit) + const texture_name_unit_t& t) { - TextureResourceHandler::Instance().releaseTextureID(texName, texUnit); + TextureResourceHandler::Instance().releaseTextureID(t.name, t.unit); } const mrpt::math::TBoundingBox diff --git a/libs/opengl/src/CTexturedPlane.cpp b/libs/opengl/src/CTexturedPlane.cpp index 8ba69012be..e35aab9f32 100644 --- a/libs/opengl/src/CTexturedPlane.cpp +++ b/libs/opengl/src/CTexturedPlane.cpp @@ -33,19 +33,6 @@ void CTexturedPlane::onUpdateBuffers_TexturedTriangles() using P2f = mrpt::math::TPoint2Df; using P3f = mrpt::math::TPoint3Df; - // Note: if we are rendering and the user assigned us no texture image, - // let's create a dummy one with the uniform CRenderizable's color: - if (!textureImageHasBeenAssigned()) - { - mrpt::img::CImage im_rgb(4, 4, mrpt::img::CH_RGB), - im_a(4, 4, mrpt::img::CH_GRAY); - im_rgb.filledRectangle(0, 0, 3, 3, m_color); - im_a.filledRectangle( - 0, 0, 3, 3, - mrpt::img::TColor(m_color.A, m_color.A, m_color.A, m_color.A)); - this->assignImage(std::move(im_rgb), std::move(im_a)); - } - auto& tris = CRenderizableShaderTexturedTriangles::m_triangles; tris.clear(); From 27764a7f3f13434ca40be2bf83c6cbb29455263d Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 28 Jun 2021 10:44:20 +0200 Subject: [PATCH 44/68] clarify and fix deep copy of lazy-load image --- libs/img/include/mrpt/img/CImage.h | 4 ++-- libs/img/src/CImage.cpp | 38 +++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/libs/img/include/mrpt/img/CImage.h b/libs/img/include/mrpt/img/CImage.h index a834c2553a..f8df0c37db 100644 --- a/libs/img/include/mrpt/img/CImage.h +++ b/libs/img/include/mrpt/img/CImage.h @@ -202,11 +202,11 @@ class CImage : public mrpt::serialization::CSerializable, public CCanvas } } - /** Constructor from a cv::Mat image, making or not a deep copy of the data + /** Constructor from a cv::Mat image, making or not a deep copy of the data. */ CImage(const cv::Mat& img, copy_type_t copy_type); - /** Constructor from another CImage, making or not a deep copy of the data + /** Constructor from another CImage, making or not a deep copy of the data. */ CImage(const CImage& img, copy_type_t copy_type); diff --git a/libs/img/src/CImage.cpp b/libs/img/src/CImage.cpp index 80346df8e9..083e959c03 100644 --- a/libs/img/src/CImage.cpp +++ b/libs/img/src/CImage.cpp @@ -193,23 +193,35 @@ CImage::CImage(const cv::Mat& img, copy_type_t copy_type) : CImage() #endif } -CImage::CImage(const CImage& img, copy_type_t copy_type) - : -#if MRPT_HAS_OPENCV - CImage(img.m_impl->img, copy_type) -#else - CImage() -#endif +CImage::CImage(const CImage& img, copy_type_t copy_type) : CImage() { - // Also, copy our custom fields: +#if MRPT_HAS_OPENCV + MRPT_START + + // Also, copy our custom fields, only if making a shallow copy! m_imgIsExternalStorage = img.m_imgIsExternalStorage; m_externalFile = img.m_externalFile; + + // this new image is *not* lazy-load. + if (copy_type == DEEP_COPY && !img.asCvMatRef().empty()) + { + // deep copy + m_impl->img = img.asCvMatRef().clone(); + } + else + { + // shallow copy + m_impl->img = img.m_impl->img; + } + MRPT_END +#endif } CImage CImage::makeDeepCopy() const { #if MRPT_HAS_OPENCV CImage ret(*this); + ret.makeSureImageIsLoaded(); ret.m_impl->img = m_impl->img.clone(); return ret; #else @@ -307,7 +319,6 @@ bool CImage::loadFromFile(const std::string& fileName, int isColor) MRPT_START #if MRPT_HAS_OPENCV - m_imgIsExternalStorage = false; #ifdef HAVE_OPENCV_IMGCODECS MRPT_TODO("Port to cv::imdecode()?"); MRPT_TODO("add flag to reuse current img buffer"); @@ -318,6 +329,10 @@ bool CImage::loadFromFile(const std::string& fileName, int isColor) if (!newImg) return false; m_impl->img = cv::cvarrToMat(newImg); #endif + + m_imgIsExternalStorage = false; + m_externalFile.clear(); + if (m_impl->img.empty()) return false; return true; @@ -371,6 +386,7 @@ void CImage::loadFromMemoryBuffer( #if MRPT_HAS_OPENCV resize(width, height, color ? CH_RGB : CH_GRAY); m_imgIsExternalStorage = false; + m_externalFile.clear(); auto* imgData = m_impl->img.data; const auto imgWidthStep = m_impl->img.step[0]; @@ -813,7 +829,7 @@ void CImage::getSize(TImageSize& s) const size_t CImage::getWidth() const { #if MRPT_HAS_OPENCV - if (m_imgIsExternalStorage) makeSureImageIsLoaded(); + makeSureImageIsLoaded(); return m_impl->img.cols; #else return 0; @@ -850,7 +866,7 @@ size_t CImage::getRowStride() const size_t CImage::getHeight() const { #if MRPT_HAS_OPENCV - if (m_imgIsExternalStorage) makeSureImageIsLoaded(); + makeSureImageIsLoaded(); return m_impl->img.rows; #else return 0; From 65202c76a98eabf0ec1a056cbab13011b6f649b1 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 28 Jun 2021 16:13:39 +0200 Subject: [PATCH 45/68] Fix valgrind/helgrind warning on missing lock. --- libs/system/include/mrpt/system/WorkerThreadsPool.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/system/include/mrpt/system/WorkerThreadsPool.h b/libs/system/include/mrpt/system/WorkerThreadsPool.h index fd890a404f..ce7586d783 100644 --- a/libs/system/include/mrpt/system/WorkerThreadsPool.h +++ b/libs/system/include/mrpt/system/WorkerThreadsPool.h @@ -93,8 +93,8 @@ auto WorkerThreadsPool::enqueue(F&& f, Args&&... args) } // Enqeue the new task: tasks_.emplace([task]() { (*task)(); }); + condition_.notify_one(); } - condition_.notify_one(); return res; } } // namespace mrpt::system From 49c9cf69d98ef5e99553fbaba597ea2b2f221278 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 28 Jun 2021 16:39:13 +0200 Subject: [PATCH 46/68] BUGFIX: potential crash inside libbfd --- doc/source/doxygen-docs/changelog.md | 1 + libs/core/src/backtrace.cpp | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 495d137968..dba384dece 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -28,6 +28,7 @@ - Fix broken 2D plots rendering in Ubuntu 20.04 (and probably other systems), via an update in mpWindow to properly use wxAutoBufferedPaintDC. - mrpt::img::CImage::getPixelDepth() should force loading lazy load images. - Fixed wrong rendering of different textures within the same opengl shader program. + - Fixed potential crashes inside BFD if using BFD and calling mrpt::callStackBackTrace() from several parallel threads. # Version 2.3.1: Released May 26th, 2021 - General cmake scripts: diff --git a/libs/core/src/backtrace.cpp b/libs/core/src/backtrace.cpp index 012c5e1e2a..4141f66d8d 100644 --- a/libs/core/src/backtrace.cpp +++ b/libs/core/src/backtrace.cpp @@ -17,6 +17,7 @@ #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -404,7 +405,14 @@ void mrpt::callStackBackTrace( addrs.push_back(callstack[i]); #if MRPT_HAS_BFD - out_bt.backtrace_levels = backtraceSymbols(addrs.data(), addrs.size()); + // It seems BFD crashes if it is invoked from several threads in + // parallel! + static std::mutex bfdMtx; + { + std::lock_guard lck(bfdMtx); + out_bt.backtrace_levels = + backtraceSymbols(addrs.data(), addrs.size()); + } #else std::cerr << "[mrpt::callStackBackTrace] Should never reach here!!\n"; #endif From a77ca34f082adcbca8505ecd3bfb68a422c661b7 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 28 Jun 2021 20:47:41 +0200 Subject: [PATCH 47/68] fix bug in image.scale() if target.size==org.size --- libs/img/src/CImage.cpp | 3 ++- libs/img/src/CImage_unittest.cpp | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/libs/img/src/CImage.cpp b/libs/img/src/CImage.cpp index 083e959c03..bb6654b936 100644 --- a/libs/img/src/CImage.cpp +++ b/libs/img/src/CImage.cpp @@ -1773,7 +1773,8 @@ void CImage::scaleImage( if (out_img.m_impl->img.data == srcImg.data) srcImg = srcImg.clone(); // Already done? - if (out_img.getWidth() == width && out_img.getHeight() == height) + if (srcImg.cols == static_cast(width) && + srcImg.rows == static_cast(height)) { out_img.m_impl->img = srcImg; return; diff --git a/libs/img/src/CImage_unittest.cpp b/libs/img/src/CImage_unittest.cpp index 1068412f04..806abe8047 100644 --- a/libs/img/src/CImage_unittest.cpp +++ b/libs/img/src/CImage_unittest.cpp @@ -64,6 +64,9 @@ TEST(CImage, CtorDefault) { mrpt::img::CImage img; EXPECT_THROW(img.isColor(), std::exception); + EXPECT_THROW(img.getWidth(), std::exception); + EXPECT_THROW(img.getHeight(), std::exception); + EXPECT_THROW(img.getPixelDepth(), std::exception); } #if MRPT_HAS_OPENCV @@ -353,6 +356,14 @@ TEST(CImage, ScaleImage) bool load_ok = a.loadFromFile(tstImgFileColor); EXPECT_TRUE(load_ok); + { + CImage b(600, 400); + a.scaleImage(b, 600, 400); + EXPECT_EQ(b.getWidth(), 600U); + EXPECT_EQ(b.getHeight(), 400U); + EXPECT_EQ(a.getWidth(), 320U); + EXPECT_EQ(a.getHeight(), 240U); + } { CImage b; a.scaleImage(b, 600, 400); From 0894ad93b321a77dace63340deb58e43c32ca02a Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Mon, 28 Jun 2021 23:43:47 +0200 Subject: [PATCH 48/68] more consistent exceptions in with/without opencv builds --- libs/img/src/CImage.cpp | 6 +++--- libs/img/src/CImage_unittest.cpp | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/img/src/CImage.cpp b/libs/img/src/CImage.cpp index bb6654b936..e2db6c7d81 100644 --- a/libs/img/src/CImage.cpp +++ b/libs/img/src/CImage.cpp @@ -832,7 +832,7 @@ size_t CImage::getWidth() const makeSureImageIsLoaded(); return m_impl->img.cols; #else - return 0; + THROW_EXCEPTION("MRPT built without OpenCV support"); #endif } @@ -869,7 +869,7 @@ size_t CImage::getHeight() const makeSureImageIsLoaded(); return m_impl->img.rows; #else - return 0; + THROW_EXCEPTION("MRPT built without OpenCV support"); #endif } @@ -898,7 +898,7 @@ bool CImage::isEmpty() const #if MRPT_HAS_OPENCV return !m_imgIsExternalStorage && m_impl->img.empty(); #else - THROW_EXCEPTION("MRPT built without OpenCV support"); + return true; #endif } diff --git a/libs/img/src/CImage_unittest.cpp b/libs/img/src/CImage_unittest.cpp index 806abe8047..95488c8d1b 100644 --- a/libs/img/src/CImage_unittest.cpp +++ b/libs/img/src/CImage_unittest.cpp @@ -63,6 +63,7 @@ static bool expect_identical( TEST(CImage, CtorDefault) { mrpt::img::CImage img; + EXPECT_TRUE(img.isEmpty()); EXPECT_THROW(img.isColor(), std::exception); EXPECT_THROW(img.getWidth(), std::exception); EXPECT_THROW(img.getHeight(), std::exception); From c6e8c98ef6b40b4a1b737479aacc28ed94c14ab6 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 1 Jul 2021 02:41:21 +0200 Subject: [PATCH 49/68] Add C++14 helper templates --- doc/source/doxygen-docs/changelog.md | 2 ++ libs/core/include/mrpt/core/integer_select.h | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index dba384dece..8f099ae20c 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -10,6 +10,8 @@ - Changes in libraries: - \ref mrpt_containers_grp - YAML macros `MCP_LOAD_OPT()`, `MCP_LOAD_REQ()`, and `MCP_SAVE()` now also support reading and writing enums directly as YAML, transparently converting numerical values to/from their symbolic names. + - \ref mrpt_core_grp + - Added C++14 helper templates mrpt::uint_select_by_bytecount_t and mrpt::int_select_by_bytecount_t - \ref mrpt_gui_grp - mrpt::gui::CDisplayWindowGUI: improved API to allow multiple callback handlers, and to report exceptions in them. - New 3D navigation key binding: SHIFT+scroll wheel, for fast up/down pure vertical motion of the camera point. diff --git a/libs/core/include/mrpt/core/integer_select.h b/libs/core/include/mrpt/core/integer_select.h index b39a88166f..b8411263f1 100644 --- a/libs/core/include/mrpt/core/integer_select.h +++ b/libs/core/include/mrpt/core/integer_select.h @@ -48,6 +48,12 @@ struct int_select_by_bytecount<8> using type = int64_t; }; +/** Usage: `int_select_by_bytecount_t var;`. + * C++14 helper type for int_select_by_bytecount<> */ +template +using int_select_by_bytecount_t = + typename int_select_by_bytecount::type; + /** Usage: `uint_select_by_bytecount::type var;` allows defining var as a * unsigned integer with, at least, N bytes. */ template @@ -78,5 +84,11 @@ struct uint_select_by_bytecount<8> using type = uint64_t; }; +/** Usage: `uint_select_by_bytecount_t var;`. + * C++14 helper type for uint_select_by_bytecount<> */ +template +using uint_select_by_bytecount_t = + typename uint_select_by_bytecount::type; + /** @} */ } // namespace mrpt From 4cbd1ae7d550b5f3341c2256bda8dae0993fe6bb Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 1 Jul 2021 02:41:53 +0200 Subject: [PATCH 50/68] CTimeLogger made helgrind clean. --- doc/source/doxygen-docs/changelog.md | 4 +- .../include/mrpt/containers/ts_hash_map.h | 79 ++++++++++++++++--- libs/system/include/mrpt/system/CTimeLogger.h | 45 ++++++++++- libs/system/src/CTimeLogger.cpp | 25 +++++- libs/system/src/CTimeLogger_unittest.cpp | 28 +++++++ 5 files changed, 164 insertions(+), 17 deletions(-) diff --git a/doc/source/doxygen-docs/changelog.md b/doc/source/doxygen-docs/changelog.md index 8f099ae20c..324690a766 100644 --- a/doc/source/doxygen-docs/changelog.md +++ b/doc/source/doxygen-docs/changelog.md @@ -21,7 +21,9 @@ - \ref mrpt_poses_grp - New methods mrpt::math::TTwist2D::rotated() and mrpt::math::TTwist3D::rotated() - \ref mrpt_system_grp - - mrpt::system::CTimeLogger: Include custom `name` in underlying mrpt::system::COutputLogger name. + - mrpt::system::CTimeLogger: + - Include custom `name` in underlying mrpt::system::COutputLogger name. + - Fix all valgrind/helgrind warning messages. - New functions mrpt::system::firstNLines() and mrpt::system::nthOccurrence() - BUG FIXES: - mrpt::img::CImage::isEmpty() should return false for delay-load images. diff --git a/libs/containers/include/mrpt/containers/ts_hash_map.h b/libs/containers/include/mrpt/containers/ts_hash_map.h index 3160e54188..5c9b60c520 100644 --- a/libs/containers/include/mrpt/containers/ts_hash_map.h +++ b/libs/containers/include/mrpt/containers/ts_hash_map.h @@ -10,8 +10,10 @@ #include // remove MSVC warnings #include +#include #include +#include #include #include @@ -23,7 +25,24 @@ struct ts_map_entry bool used{false}; KEY first; VALUE second; + ts_map_entry() = default; + ts_map_entry(const ts_map_entry& e) { *this = e; } + ts_map_entry& operator=(const ts_map_entry& e) + { + used = !!e.used; + first = e.first; + second = e.second; + return *this; + } + ts_map_entry(ts_map_entry&& e) { *this = std::move(e); } + ts_map_entry& operator=(ts_map_entry&& e) + { + used = e.used; + first = e.first; + second = e.second; + return *this; + } }; /** hash function used by ts_hash_map. Uses dbj2 method */ @@ -175,29 +194,58 @@ class ts_hash_map /** Number of elements accessed with write access so far */ size_t m_size{0}; + std::recursive_mutex m_mtx; //!< for m_vec and m_size + public: /** @name Constructors, read/write access and other operations @{ */ //!< Default constructor */ ts_hash_map() = default; + + ts_hash_map(const ts_hash_map& o) { *this = o; } + ts_hash_map(ts_hash_map&& o) { *this = std::move(o); } + + ts_hash_map& operator=(const ts_hash_map& o) + { + auto lck1 = mrpt::lockHelper(m_mtx); + auto lck2 = mrpt::lockHelper(o.m_mtx); + m_vec = o.m_vec; + m_size = o.m_size; + return *this; + } + ts_hash_map& operator=(ts_hash_map&& o) + { + auto lck1 = mrpt::lockHelper(m_mtx); + auto lck2 = mrpt::lockHelper(o.m_mtx); + m_vec = std::move(o.m_vec); + m_size = o.m_size; + return *this; + } + /** Clear the contents of this container */ void clear() { + auto lck = mrpt::lockHelper(m_mtx); m_size = 0; for (size_t oi = 0; oi < m_vec.size(); oi++) for (size_t ii = 0; ii < NUM_HAS_TABLE_COLLISIONS_ALLOWED; ii++) m_vec[oi][ii] = value_type(); } - bool empty() const { return m_size == 0; } + bool empty() const + { + auto lck = mrpt::lockHelper(m_mtx); + return m_size == 0; + } + /** noexcept version of operator[], returns nullptr upon failure */ VALUE* find_or_alloc(const KEY& key) noexcept { - typename mrpt::uint_select_by_bytecount::type - hash; + auto lck = mrpt::lockHelper(m_mtx); + + mrpt::uint_select_by_bytecount_t hash; reduced_hash(key, hash); - std::array, NUM_HAS_TABLE_COLLISIONS_ALLOWED>& - match_arr = m_vec[hash]; + auto& match_arr = m_vec[hash]; for (unsigned int i = 0; i < NUM_HAS_TABLE_COLLISIONS_ALLOWED; i++) { if (!match_arr[i].used) @@ -216,6 +264,8 @@ class ts_hash_map * already. */ VALUE& operator[](const KEY& key) { + auto lck = mrpt::lockHelper(m_mtx); + VALUE* v = find_or_alloc(key); if (!v) throw std::runtime_error("ts_hash_map: too many hash collisions!"); @@ -224,12 +274,11 @@ class ts_hash_map const_iterator find(const KEY& key) const { - typename mrpt::uint_select_by_bytecount::type - hash; + auto lck = mrpt::lockHelper(m_mtx); + + mrpt::uint_select_by_bytecount_t hash; reduced_hash(key, hash); - const std::array< - ts_map_entry, NUM_HAS_TABLE_COLLISIONS_ALLOWED>& - match_arr = m_vec[hash]; + auto& match_arr = m_vec[hash]; for (unsigned int i = 0; i < NUM_HAS_TABLE_COLLISIONS_ALLOWED; i++) { if (match_arr[i].used && match_arr[i].first == key) @@ -240,21 +289,29 @@ class ts_hash_map const_iterator begin() const { + auto lck = mrpt::lockHelper(m_mtx); + const_iterator it(m_vec, *this, 0, -1); ++it; return it; } const_iterator end() const { + auto lck = mrpt::lockHelper(m_mtx); return const_iterator(m_vec, *this, m_vec.size(), 0); } iterator begin() { + auto lck = mrpt::lockHelper(m_mtx); iterator it(m_vec, *this, 0, -1); ++it; return it; } - iterator end() { return iterator(m_vec, *this, m_vec.size(), 0); } + iterator end() + { + auto lck = mrpt::lockHelper(m_mtx); + return iterator(m_vec, *this, m_vec.size(), 0); + } /** @} */ }; // end class ts_hash_map diff --git a/libs/system/include/mrpt/system/CTimeLogger.h b/libs/system/include/mrpt/system/CTimeLogger.h index a8793fed3f..b3d398082a 100644 --- a/libs/system/include/mrpt/system/CTimeLogger.h +++ b/libs/system/include/mrpt/system/CTimeLogger.h @@ -59,13 +59,44 @@ class CTimeLogger : public mrpt::system::COutputLogger //! Data of all the calls: struct TCallData { - TCallData(); - size_t n_calls{0}; double min_t{0}, max_t{0}, mean_t{0}, last_t{0}; std::stack> open_calls; bool has_time_units{true}; std::optional> whole_history{}; + + // Each instance holds its own mutex, even after = operations. + std::mutex mtx; + + TCallData() = default; + + TCallData(const TCallData& d) { *this = d; } + TCallData(TCallData&& d) { *this = std::move(d); } + + TCallData& operator=(const TCallData& d) + { + n_calls = d.n_calls; + min_t = d.min_t; + max_t = d.max_t; + mean_t = d.mean_t; + last_t = d.last_t; + open_calls = d.open_calls; + has_time_units = d.has_time_units; + whole_history = d.whole_history; + return *this; + } + TCallData& operator=(TCallData&& d) + { + n_calls = d.n_calls; + min_t = d.min_t; + max_t = d.max_t; + mean_t = d.mean_t; + last_t = d.last_t; + open_calls = std::move(d.open_calls); + has_time_units = d.has_time_units; + whole_history = std::move(d.whole_history); + return *this; + } }; protected: @@ -111,10 +142,18 @@ class CTimeLogger : public mrpt::system::COutputLogger /** Dump all stats through the COutputLogger interface. \sa getStatsAsText, * saveToCVSFile */ void dumpAllStats(const size_t column_width = 80) const; + /** Resets all stats. By default (deep_clear=false), all section names are * remembered (not freed) so the cost of creating upon the first next call - * is avoided. */ + * is avoided. + * + * \note By design, calling this method is the only one which is not thread + * safe. It's not made thread-safe to save the performance cost. Please, + * ensure that you call `clear()` only while there are no other threads + * registering annotations in the object. + */ void clear(bool deep_clear = false); + void enable(bool enabled = true) { m_enabled = enabled; } void disable() { m_enabled = false; } bool isEnabled() const { return m_enabled; } diff --git a/libs/system/src/CTimeLogger.cpp b/libs/system/src/CTimeLogger.cpp index 5cd2ce8a39..0345801bc0 100644 --- a/libs/system/src/CTimeLogger.cpp +++ b/libs/system/src/CTimeLogger.cpp @@ -10,6 +10,7 @@ #include "system-precomp.h" // Precompiled headers // #include +#include #include #include #include @@ -91,7 +92,11 @@ void CTimeLogger::clear(bool deep_clear) else { for (auto& e : m_data) + { + e.second.mtx.lock(); + e.second.mtx.unlock(); e.second = TCallData(); + } } } @@ -112,6 +117,8 @@ void CTimeLogger::getStats(std::map& out_stats) const out_stats.clear(); for (const auto& e : m_data) { + auto lck = mrpt::lockHelper(e.second.mtx); + TCallStats& cs = out_stats[std::string(e.first)]; cs.min_t = e.second.min_t; cs.max_t = e.second.max_t; @@ -175,6 +182,8 @@ std::string CTimeLogger::getStatsAsText(const size_t column_width) const std::string last_parent; for (const auto& i : stat_strs) { + auto lck = mrpt::lockHelper(i.second.mtx); + string line = string(i.first); // make a copy const auto dot_pos = line.find("."); @@ -216,6 +225,8 @@ void CTimeLogger::saveToCSVFile(const std::string& csv_file) const "WHOLE_HISTORY]\n"; for (const auto& i : m_data) { + auto lck = mrpt::lockHelper(i.second.mtx); + s += format( "\"%.*s\",%7u,%e,%e,%e,%e,%e", static_cast(i.first.size()), i.first.data(), static_cast(i.second.n_calls), @@ -256,6 +267,8 @@ void CTimeLogger::saveToMFile(const std::string& file) const for (const auto& i : m_data) { + auto lck = mrpt::lockHelper(i.second.mtx); + s_names += "'"s + i.first + "',"s; s_counts += std::to_string(i.second.n_calls) + ","s; s_mins += mrpt::format("%e,", i.second.min_t); @@ -309,6 +322,7 @@ void CTimeLogger::do_enter(const std::string_view& func_name) noexcept return; } auto& d = *d_ptr; + auto lck = mrpt::lockHelper(d.mtx); d.n_calls++; d.open_calls.push(0); // Dummy value, it'll be written below d.open_calls.top() = m_tictac.Tac(); // to avoid possible delays. @@ -326,6 +340,7 @@ double CTimeLogger::do_leave(const std::string_view& func_name) noexcept return .0; } auto& d = *d_ptr; + auto lck = mrpt::lockHelper(d.mtx); if (!d.open_calls.empty()) { @@ -371,6 +386,7 @@ void CTimeLogger::registerUserMeasure( return; } auto& d = *d_ptr; + auto lck = mrpt::lockHelper(d.mtx); d.has_time_units = is_time; d.last_t = value; @@ -395,21 +411,26 @@ void CTimeLogger::registerUserMeasure( } } -CTimeLogger::TCallData::TCallData() = default; - double CTimeLogger::getMeanTime(const std::string& name) const { TDataMap::const_iterator it = m_data.find(name); if (it == m_data.end()) return 0; else + { + auto lck = mrpt::lockHelper(it->second.mtx); + return it->second.n_calls ? it->second.mean_t / it->second.n_calls : 0; + } } double CTimeLogger::getLastTime(const std::string& name) const { TDataMap::const_iterator it = m_data.find(name); if (it == m_data.end()) return 0; else + { + auto lck = mrpt::lockHelper(it->second.mtx); return it->second.last_t; + } } CTimeLoggerEntry::CTimeLoggerEntry( diff --git a/libs/system/src/CTimeLogger_unittest.cpp b/libs/system/src/CTimeLogger_unittest.cpp index cb870f49ee..48d5af2ce0 100644 --- a/libs/system/src/CTimeLogger_unittest.cpp +++ b/libs/system/src/CTimeLogger_unittest.cpp @@ -11,6 +11,7 @@ #include #include +#include #include static void doTimLogEntry( @@ -95,3 +96,30 @@ TEST(CTimeLogger, printStatsFaulty) tl.clear(true); EXPECT_EQ(std::count(s.begin(), s.end(), '\n'), 14U); } + +TEST(CTimeLogger, multithread) +{ + mrpt::system::CTimeLogger tl; + std::vector ths; + std::mutex mtx; + + mtx.lock(); + + for (int i = 0; i < 20; i++) + { + ths.push_back(std::thread([i, &mtx, &tl]() { + mtx.lock(); + mtx.unlock(); + doTimLogEntry(tl, mrpt::format("foo%i", i % 5).c_str(), 10); + })); + } + + mtx.unlock(); // now, all threads will run + + for (auto& t : ths) + if (t.joinable()) t.join(); + + const std::string s = tl.getStatsAsText(); + tl.clear(true); // to silent console output upon dtor + EXPECT_EQ(std::count(s.begin(), s.end(), '\n'), 9U); +} From f51fabff2c2328420ff8f3b0ff925d936cede134 Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 1 Jul 2021 02:46:52 +0200 Subject: [PATCH 51/68] add test_helgrind target --- tests/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 510c8881bc..3dec7b05be 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,7 @@ set_target_properties(tests_build_all PROPERTIES FOLDER "unit tests") if (UNIX) add_custom_target(test_gdb) add_custom_target(test_valgrind) + add_custom_target(test_helgrind) endif() @@ -116,6 +117,13 @@ foreach(_TSTLIB ${LST_LIB_TESTS}) ) add_dependencies(run_tests_${_TSTLIB}_valgrind test_${_TSTLIB}) add_dependencies(test_valgrind run_tests_${_TSTLIB}_valgrind) + + # test_helgrind + add_custom_target(run_tests_${_TSTLIB}_helgrind + COMMAND "valgrind" "--tool=helgrind" "--error-exitcode=1" "${GENERATED_EXE}" + ) + add_dependencies(run_tests_${_TSTLIB}_helgrind test_${_TSTLIB}) + add_dependencies(test_helgrind run_tests_${_TSTLIB}_helgrind) endif() set_target_properties(test_${_TSTLIB} PROPERTIES FOLDER "unit tests") From b8082f21fec9236c4023aa2f6d053e2cd7ecdf6a Mon Sep 17 00:00:00 2001 From: Jose Luis Blanco Claraco Date: Thu, 1 Jul 2021 10:22:24 +0200 Subject: [PATCH 52/68] fix app name --- AUTHORS | 2 +- .../SceneViewer3D/CodeBlock_project_Linux/3DSceneViewer.cbp | 4 ++-- .../CodeBlock_project_Windows/3DSceneViewer.cbp | 6 +++--- apps/SceneViewer3D/_DSceneViewerMain.cpp | 6 +++--- apps/SceneViewer3D/wxsmith/_DSceneViewerframe.wxs | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/AUTHORS b/AUTHORS index ce782c9f9e..44b20024a8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -113,7 +113,7 @@ the GitHub contributors page (https://github.com/MRPT/mrpt/graphs/contributors). * Vicente Arevalo Espejo, University of Malaga. - The application camera-calib. - Some classes: mrpt::opengl::CSetOfTexturedTriangles, etc.. - - Some minor changes in: 3DSceneViewer and RawlogViewer applications. + - Some minor changes in: SceneViewer3D and RawlogViewer applications. - Some image-related functions. * Adrien Barral - Robopec (France). diff --git a/apps/SceneViewer3D/CodeBlock_project_Linux/3DSceneViewer.cbp b/apps/SceneViewer3D/CodeBlock_project_Linux/3DSceneViewer.cbp index 85384719d3..61c1b632b1 100644 --- a/apps/SceneViewer3D/CodeBlock_project_Linux/3DSceneViewer.cbp +++ b/apps/SceneViewer3D/CodeBlock_project_Linux/3DSceneViewer.cbp @@ -2,12 +2,12 @@ -