From 075d8bb633b624b5127c4cb39d12630b6cf0bfdd Mon Sep 17 00:00:00 2001 From: kazu Date: Fri, 16 Sep 2016 19:30:04 +0200 Subject: [PATCH] a lot!!! of changes added main menu added debug display many debug widgets for plotting live data worked on android live sensors added offline-data sensor feeding some dummy data sensors worked on the map display added ui debug for grid-points, particles and weights added a cool dude to display the estimation added real filtering based on the Indoor components c++11 fixes for android compilation online and offline filtering support new resampling technique for testing map loading via dialog --- Config.h | 29 ++ Controller.cpp | 135 +++++++ Controller.h | 50 +++ Settings.h | 29 ++ main.cpp | 224 +---------- map/MapView.cpp | 132 ------- map/MapView.h | 76 ---- map/Renderable.h | 40 -- misc/fixc11.h | 12 +- nav/Filter.h | 149 ++++++++ nav/NavController.h | 299 +++++++++++++++ nav/Node.h | 29 ++ nav/RegionalResampling.h | 68 ++++ nav/State.h | 77 ++++ qml.qrc | 21 +- res/gl/fragmentColorPoint.glsl | 25 ++ res/gl/fragmentTex.glsl | 7 +- res/gl/tex/empty_normals.jpg | Bin 0 -> 998 bytes res/gl/tex/wall3.jpg | Bin 0 -> 26594 bytes res/gl/tex/wall3_normal.jpg | Bin 0 -> 57750 bytes res/gl/vertex1.glsl | 5 +- res/icons/bug.svg | 57 +++ res/icons/camera.svg | 8 + res/icons/cube.svg | 44 +++ res/icons/load.svg | 43 +++ res/icons/route.svg | 91 +++++ res/icons/run.svg | 51 +++ sensors/AccelerometerSensor.h | 13 +- sensors/BarometerSensor.h | 11 + sensors/GyroscopeSensor.h | 11 + sensors/Sensor.h | 17 +- sensors/SensorFactory.h | 63 +++- sensors/StepSensor.h | 58 +-- sensors/TurnSensor.h | 53 +++ sensors/WiFiSensor.h | 43 +-- sensors/android/AccelerometerSensorAndroid.h | 2 +- sensors/android/BarometerSensorAndroid.h | 55 +++ sensors/android/GyroscopeSensorAndroid.h | 59 +++ sensors/android/SensorFactoryAndroid.h | 41 ++ sensors/android/WiFiSensorAndroid.cpp | 20 + sensors/android/WiFiSensorAndroid.h | 17 +- sensors/dummy/AccelerometerSensorDummy.h | 32 +- sensors/dummy/BarometerSensorDummy.h | 43 +++ sensors/dummy/GyroscopeSensorDummy.h | 48 +++ sensors/dummy/RandomSensor.h | 56 +++ sensors/dummy/SensorFactoryDummy.h | 36 ++ sensors/dummy/WiFiSensorDummy.h | 10 +- sensors/linux/WiFiSensorLinux.h | 71 +++- sensors/linux/WiFiSensorLinuxC.c | 376 +++++++++++++++++++ sensors/linux/WiFiSensorLinuxC.h | 49 +++ sensors/offline/AllInOneSensor.h | 110 ++++++ sensors/offline/SensorFactoryOffline.h | 43 +++ ui/Icons.h | 90 +++++ ui/MainWindow.cpp | 35 ++ ui/MainWindow.h | 43 +++ ui/debug/PlotTurns.cpp | 49 +++ ui/debug/PlotTurns.h | 30 ++ ui/debug/PlotWiFiScan.cpp | 53 +++ ui/debug/PlotWiFiScan.h | 30 ++ ui/debug/SensorDataWidget.cpp | 217 +++++++++++ ui/debug/SensorDataWidget.h | 53 +++ ui/debug/plot/Axes.h | 66 ++++ ui/debug/plot/Data.h | 80 ++++ ui/debug/plot/Plot.h | 215 +++++++++++ ui/debug/plot/PlottWidget.cpp | 37 ++ ui/debug/plot/PlottWidget.h | 30 ++ ui/debug/plot/Range.h | 48 +++ ui/dialog/LoadSetupDialog.cpp | 73 ++++ ui/dialog/LoadSetupDialog.h | 25 ++ {map => ui/map}/FloorRenderer.h | 0 ui/map/MapView.cpp | 266 +++++++++++++ ui/map/MapView.h | 152 ++++++++ ui/map/Renderable.h | 76 ++++ ui/map/elements/ColorPoints.h | 114 ++++++ {map => ui/map}/elements/Doors.h | 0 {map => ui/map}/elements/Ground.h | 47 ++- {map => ui/map}/elements/Handrails.h | 0 ui/map/elements/Object.h | 84 +++++ {map => ui/map}/elements/Path.h | 1 - {map => ui/map}/elements/Stairs.h | 6 +- {map => ui/map}/elements/Walls.h | 50 ++- {map => ui/map}/gl/GL.h | 12 + {map => ui/map}/gl/GLHelper.h | 0 {map => ui/map}/gl/GLLines.h | 0 ui/map/gl/GLPoints.h | 111 ++++++ {map => ui/map}/gl/GLTriangles.h | 5 + ui/map/gl/Shader.h | 31 ++ ui/menu/MainMenu.cpp | 55 +++ ui/menu/MainMenu.h | 36 ++ yasmin.pro | 101 ++++- 90 files changed, 4735 insertions(+), 624 deletions(-) create mode 100644 Config.h create mode 100644 Controller.cpp create mode 100644 Controller.h create mode 100644 Settings.h delete mode 100644 map/MapView.cpp delete mode 100644 map/MapView.h delete mode 100644 map/Renderable.h create mode 100644 nav/Filter.h create mode 100644 nav/NavController.h create mode 100644 nav/Node.h create mode 100644 nav/RegionalResampling.h create mode 100644 nav/State.h create mode 100644 res/gl/fragmentColorPoint.glsl create mode 100644 res/gl/tex/empty_normals.jpg create mode 100755 res/gl/tex/wall3.jpg create mode 100755 res/gl/tex/wall3_normal.jpg create mode 100644 res/icons/bug.svg create mode 100644 res/icons/camera.svg create mode 100644 res/icons/cube.svg create mode 100644 res/icons/load.svg create mode 100644 res/icons/route.svg create mode 100644 res/icons/run.svg create mode 100644 sensors/BarometerSensor.h create mode 100644 sensors/GyroscopeSensor.h create mode 100644 sensors/TurnSensor.h create mode 100644 sensors/android/BarometerSensorAndroid.h create mode 100644 sensors/android/GyroscopeSensorAndroid.h create mode 100644 sensors/android/SensorFactoryAndroid.h create mode 100644 sensors/android/WiFiSensorAndroid.cpp create mode 100644 sensors/dummy/BarometerSensorDummy.h create mode 100644 sensors/dummy/GyroscopeSensorDummy.h create mode 100644 sensors/dummy/RandomSensor.h create mode 100644 sensors/dummy/SensorFactoryDummy.h create mode 100644 sensors/linux/WiFiSensorLinuxC.c create mode 100644 sensors/linux/WiFiSensorLinuxC.h create mode 100644 sensors/offline/AllInOneSensor.h create mode 100644 sensors/offline/SensorFactoryOffline.h create mode 100644 ui/Icons.h create mode 100644 ui/MainWindow.cpp create mode 100644 ui/MainWindow.h create mode 100644 ui/debug/PlotTurns.cpp create mode 100644 ui/debug/PlotTurns.h create mode 100644 ui/debug/PlotWiFiScan.cpp create mode 100644 ui/debug/PlotWiFiScan.h create mode 100644 ui/debug/SensorDataWidget.cpp create mode 100644 ui/debug/SensorDataWidget.h create mode 100644 ui/debug/plot/Axes.h create mode 100644 ui/debug/plot/Data.h create mode 100644 ui/debug/plot/Plot.h create mode 100644 ui/debug/plot/PlottWidget.cpp create mode 100644 ui/debug/plot/PlottWidget.h create mode 100644 ui/debug/plot/Range.h create mode 100644 ui/dialog/LoadSetupDialog.cpp create mode 100644 ui/dialog/LoadSetupDialog.h rename {map => ui/map}/FloorRenderer.h (100%) create mode 100644 ui/map/MapView.cpp create mode 100644 ui/map/MapView.h create mode 100644 ui/map/Renderable.h create mode 100644 ui/map/elements/ColorPoints.h rename {map => ui/map}/elements/Doors.h (100%) rename {map => ui/map}/elements/Ground.h (67%) rename {map => ui/map}/elements/Handrails.h (100%) create mode 100644 ui/map/elements/Object.h rename {map => ui/map}/elements/Path.h (99%) rename {map => ui/map}/elements/Stairs.h (94%) rename {map => ui/map}/elements/Walls.h (69%) rename {map => ui/map}/gl/GL.h (80%) rename {map => ui/map}/gl/GLHelper.h (100%) rename {map => ui/map}/gl/GLLines.h (100%) create mode 100644 ui/map/gl/GLPoints.h rename {map => ui/map}/gl/GLTriangles.h (97%) create mode 100644 ui/map/gl/Shader.h create mode 100644 ui/menu/MainMenu.cpp create mode 100644 ui/menu/MainMenu.h diff --git a/Config.h b/Config.h new file mode 100644 index 0000000..82a65ec --- /dev/null +++ b/Config.h @@ -0,0 +1,29 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include + +class Config { + +public: + + // notes + // copy: scp -P 2222 /tmp/grid.dat kazu@192.168.24.11:/storage/sdcard1/YASMIN/maps/car/ + // all: scp -P 2222 -r /apps/android/workspace/YASMIN_DATA/* kazu@192.168.24.11:/storage/sdcard1/YASMIN/ + + /** get the directory where maps are stored */ + static inline std::string getMapDir() { +#ifdef ANDROID +// const std::string folder = getenv("EXTERNAL_STORAGE") + std::string("/YASMIN/maps/"); // this is NOT the sdcard?! +// qDebug(folder.c_str()); +// return folder; + return "/storage/sdcard1/YASMIN/maps/"; +#else + return "/apps/android/workspace/YASMIN_DATA/maps/"; +#endif + } + +}; + +#endif // CONFIG_H diff --git a/Controller.cpp b/Controller.cpp new file mode 100644 index 0000000..cbf52b1 --- /dev/null +++ b/Controller.cpp @@ -0,0 +1,135 @@ +#include "Controller.h" + +#include "ui/map/MapView.h" +#include "ui/menu/MainMenu.h" +#include "ui/MainWindow.h" +#include "ui/dialog/LoadSetupDialog.h" +#include "ui/debug/SensorDataWidget.h" + +#include +#include + +#include +#include +#include + +#include "sensors/dummy/SensorFactoryDummy.h" +#include "sensors/android/SensorFactoryAndroid.h" +#include "sensors/offline/SensorFactoryOffline.h" + +#include "nav/NavController.h" + +Controller::Controller() { + + // OpenGL setup + // MUST happen before anything gets visible (= gets initialized) + QSurfaceFormat format; + format.setDepthBufferSize(16); + QSurfaceFormat::setDefaultFormat(format); + + // configure the to-be-used sensor factory + //SensorFactory::set(new SensorFactoryDummy()); + //SensorFactory::set(new SensorFactoryOffline("/apps/android/workspace/YASMIN_DATA/offline/gyroacctestingfrank/nexus6/kleinerKreis_216steps_6runden_telefongerade.csv")); + //SensorFactory::set(new SensorFactoryOffline("/apps/android/workspace/YASMIN_DATA/offline/gyroacctestingfrank/s3mini/kleinerKreis_225steps_6runden_telefongerade.csv")); + //SensorFactory::set(new SensorFactoryOffline("/apps/android/workspace/YASMIN_DATA/offline/gyroacctestingfrank/s4/kleinerKreis_220steps_6runden_telefongeneigt.csv")); + SensorFactory::set(new SensorFactoryOffline("/apps/android/workspace/YASMIN_DATA/offline/bergwerk/path4/nexus/vor/1454776525797.csv")); + + + + + mainWindow = new MainWindow(); + + + Assert::isTrue(connect(mainWindow->getMainMenu(), &MainMenu::onLoadButton, this, &Controller::onLoadButton), "connect() failed"); + Assert::isTrue(connect(mainWindow->getMainMenu(), &MainMenu::onDebugButton, this, &Controller::onDebugButton), "connect() failed"); + Assert::isTrue(connect(mainWindow->getMainMenu(), &MainMenu::onStartButton, this, &Controller::onStartButton), "connect() failed"); + Assert::isTrue(connect(mainWindow->getMainMenu(), &MainMenu::onTransparentButton, this, &Controller::onTransparentButton), "connect() failed"); + Assert::isTrue(connect(mainWindow->getMainMenu(), &MainMenu::onCameraButton, this, &Controller::onCameraButton), "connect() failed"); + + // order is important! otherwise OpenGL fails! + mainWindow->show(); + + + + // start all sensors + SensorFactory::get().getAccelerometer().start(); + SensorFactory::get().getGyroscope().start(); + SensorFactory::get().getBarometer().start(); + SensorFactory::get().getWiFi().start(); + +} + +MapView* Controller::getMapView() const { + return mainWindow->getMapView(); +} + + +void buildGridOnce(Grid* grid, Floorplan::IndoorMap* map, const std::string& saveFile) { + GridFactory gf(*grid); + gf.build(map); + Importance::addImportance(*grid); + std::ofstream out(saveFile, std::ofstream::binary); + grid->write(out); + out.close(); +} + +void Controller::onLoadButton() { + + // pick a map to load + QDir dir = LoadSetupDialog::pickSetupFolder(); + + // cancelled? + if (dir.path() == ".") { return; } + + QFile fMap(dir.path() + "/map.xml"); + QFile fGrid(dir.path() + "/grid.dat"); + + Assert::isTrue(fMap.exists(), "map.xml missing"); + //Assert::isTrue(fGrid.exists(), "grid.dat missing"); + + fMap.open(QIODevice::ReadOnly); + QString str = QString(fMap.readAll()); + im = Floorplan::Reader::readFromString(str.toStdString()); + + + + const std::string sGrid = fGrid.fileName().toStdString(); + std::ifstream inp(sGrid, std::ifstream::binary); + //Assert::isTrue(inp.good(), "failed to open grid.dat"); + + // create a new, empty grid + if (grid) {delete grid; grid = nullptr;} + grid = new Grid(20); + + // grid.dat empty? -> build one and save it + if (!inp.good() || (inp.peek()&&0) || inp.eof()) { + buildGridOnce(grid, im, sGrid); + } else { + grid->read(inp); + } + + // create a new navigator + if (nav) {delete nav; nav = nullptr;} + nav = new NavController(this, grid, im); + + getMapView()->setMap(im); + getMapView()->showGridImportance(grid); + + +} + +void Controller::onDebugButton() { + mainWindow->getSensorDataWidget()->setVisible( !mainWindow->getSensorDataWidget()->isVisible() ); +} + +void Controller::onStartButton() { + nav->start(); +} + +void Controller::onTransparentButton() { + mainWindow->getMapView()->toggleRenderMode(); +} + +void Controller::onCameraButton() { + nav->toggleCamera(); +} diff --git a/Controller.h b/Controller.h new file mode 100644 index 0000000..cbd3a77 --- /dev/null +++ b/Controller.h @@ -0,0 +1,50 @@ +#ifndef CONTROLLER_H +#define CONTROLLER_H + +class MainWindow; +class MainMenu; +class MapView; + +class NavController; + +#include +template class Grid; +class MyGridNode; + +namespace Floorplan { + class IndoorMap; +} + +class Controller : public QObject { + + Q_OBJECT + +public: + + /** ctor */ + explicit Controller(); + +public: + + MapView* getMapView() const; + +private slots: + + void onLoadButton(); + void onDebugButton(); + void onStartButton(); + + void onTransparentButton(); + void onCameraButton(); + +private: + + MainWindow* mainWindow; + + Grid* grid = nullptr; + NavController* nav = nullptr; + Floorplan::IndoorMap* im = nullptr; + +}; + +#endif // CONTROLLER_H diff --git a/Settings.h b/Settings.h new file mode 100644 index 0000000..b116807 --- /dev/null +++ b/Settings.h @@ -0,0 +1,29 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include + +namespace Settings { + + const int numParticles = 3000; + + const float turnSigma = 3.5; + + const float stepLength = 0.80; + const float stepSigma = 0.1; + + const float smartphoneAboveGround = 1.3; + + const float offlineSensorSpeedup = 2.5; + + + const GridPoint destination = GridPoint(70*100, 35*100, 0*100); + + const float wifiSigma = 9.0; + const float wifiTXP = -48; + const float wifiEXP = 2.5; + const float wifiWAF = -9.0; + +} + +#endif // SETTINGS_H diff --git a/main.cpp b/main.cpp index 7f94a52..f5e02fa 100644 --- a/main.cpp +++ b/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -14,7 +15,8 @@ #include #include "sensors/SensorFactory.h" -#include "map/MapView.h" +#include "Controller.h" +#include "ui/map/MapView.h" #include @@ -24,29 +26,17 @@ #include #include -class LeListener : public SensorListener, public SensorListener, public SensorListener { -public: - void onSensorData(const WiFiSensorData& data) override { - const std::string str = "\n" + data.asString(); - qDebug(str.c_str()); - } - void onSensorData(const AccelerometerData& data) override { - const std::string str = data.asString(); - qDebug(str.c_str()); - } - void onSensorData(const StepData& data) override { - qDebug("STEP!"); - } -}; struct MyNode : public GridPoint, public GridNode, public WiFiGridNode<10> { - float imp; + float navImportance; MyNode() {;} MyNode(const float x_cm, const float y_cm, const float z_cm) : GridPoint(x_cm, y_cm, z_cm) {;} +public: + static void staticSerialize(std::ostream& out) { WiFiGridNode::staticSerialize(out); } @@ -57,208 +47,30 @@ struct MyNode : public GridPoint, public GridNode, public WiFiGridNode<10> { }; + int main(int argc, char *argv[]) { - - int sizeOfNode = sizeof(MyNode); - - std::vector points = { - Point3(1140,530,1060), Point3(1140,2100,1060), Point3(1140,2880,1060), Point3(1140,4442.21,1060), Point3(1190,4840,1060), Point3(1660,4840,890), Point3(1660,5040,890), Point3(1180,5040,720), Point3(1180,4840,720), Point3(1180,4460,720), Point3(2350,4460,720), Point3(4440,4440,720), Point3(5183.26,4280,720), Point3(5800,4280,550), Point3(6110,4280,380), Point3(7680,4280,380), Point3(7680,3860,190), Point3(7680,3400,0), Point3(7400,3400,0), Point3(7400,4030,0) - }; - std::vector path; - - Interpolator interpol; - int ts = 0; - Point3 last = points[0]; - for (Point3 p : points) { - const float dist = p.getDistance(last); - const float diffMS = (dist / 100.0f) / 1.5f * 1000; - ts += diffMS; - interpol.add(ts, p); - last = p; - path.push_back(p / 100.0f); - } - - - - //QGuiApplication app(argc, argv); QApplication app(argc, argv); - // OpenGL Setup - QSurfaceFormat format; - format.setDepthBufferSize(16); - QSurfaceFormat::setDefaultFormat(format); + Controller ctrl; + Point3 eye(40,42,1.8); + ctrl.getMapView()->setLookEye(eye); + ctrl.getMapView()->setLookDir(Point3(-1, 0, -0.1)); - Floorplan::IndoorMap* im = Floorplan::Reader::readFromFile("/mnt/data/workspaces/IndoorMap/maps/SHL21.xml"); - - Grid grid(20); - GridFactory gf(grid); - - // build - //gf.build(im); - //Importance::addImportance(grid, 0); - //std::ofstream out("/tmp/grid.dat"); - //grid.write(out); out.close(); - - // read - std::ifstream inp("/tmp/grid.dat"); - grid.reset(); - grid.read(inp); - - // estimate WiFi signal strengths - WiFiModelLogDist mdlLogDist(-40.0f, 2.25f); - WiFiGridEstimator::estimate(grid, mdlLogDist, im); - - - - MapView* map = new MapView(); - map->setMinimumHeight(200); - map->setMinimumWidth(200); - map->setMap(im); - //map->setPath(path); - map->show(); - - - struct DijkstraMapper { - Grid& grid; - DijkstraMapper(Grid& grid) : grid(grid) {;} - int getNumNeighbors(const MyNode& n) const {return grid.getNumNeighbors(n);} - const MyNode* getNeighbor(const MyNode& n, const int idx) const {return &grid.getNeighbor(n, idx);} - float getWeightBetween(const MyNode& n1, const MyNode& n2) const {return n1.getDistanceInCM(n2) / n2.imp;} - - }; - - - - struct LeListener : public APIListener { - - Grid& grid; - Dijkstra& d; - const DijkstraNode* dnEnd; - MapView* map; - - Point3 nextPos; - Point3 pos1; - Point3 pos2; - - LeListener(Grid& grid, Dijkstra& d, const DijkstraNode* dnEnd, MapView* map) : grid(grid), d(d), dnEnd(dnEnd), map(map) { - - - std::thread t(&LeListener::update, this); - t.detach(); - - } - - /** the currently estimated path to the target has changed */ - virtual void onPathUpdate(const std::vector& curPath) override { - - } - - /** the currently estimated position has changed */ - virtual void onPositionUpdate(const Point3 pos) override { - - nextPos = pos; - - // update the path to the target - const GridPoint xxx(pos.x, pos.y, pos.z); - const DijkstraNode* dnStart = d.getNode( grid.getNearestNode(xxx) ); - const DijkstraPath dp(dnStart, dnEnd); - map->setPath(dp); - - } - - void update() { - - - std::this_thread::sleep_for(std::chrono::milliseconds(700)); - - const int transMS = 500; - const int updateMS = 75; - const float factor1 = ((float) updateMS / (float) transMS) * 0.7; - const float factor2 = factor1 * 0.4f; - const Point3 human(0, 0, 180); - - while(true) { - - - std::this_thread::sleep_for(std::chrono::milliseconds(updateMS)); - -// Point3 diff = (nextPos - pos1); -// if (diff.length() > 30) {diff = diff.normalized() * 30;} else {diff *= factor1;} -// pos1 += diff; -// pos2 += diff*0.5; - - - pos1 = pos1 * (1-factor1) + nextPos * (factor1); // fast update - pos2 = pos2 * (1-factor2) + nextPos * (factor2); // slow update - const Point3 dir = pos2 - pos1; - - map->setLookAt((pos1+human) / 100.0f, (dir) / 100.0f); - - } - - - } - - }; - - - DummyAPI api("/mnt/data/workspaces/navindoor"); - api.setTarget(4); - - - Dijkstra d; - //GridPoint end(points.front().x, points.front().y, points.front().z); - GridPoint end(api.getDst().x, api.getDst().y, api.getDst().z); - d.build(&grid.getNearestNode(end), DijkstraMapper(grid)); - const DijkstraNode* dnEnd = d.getNode(grid.getNearestNode(end)); - - api.addListener(new LeListener(grid, d, dnEnd, map)); - api.setSpeed(2); - api.startNavigation(); // auto run = [&] () { -// //int ts = 0; -// int ts = 97000; -// while(ts < interpol.getMaxKey() && ts >= 0) { -// std::this_thread::sleep_for(std::chrono::milliseconds(50)); -// //ts += 50; -// ts -= 150; -// const Point3 human(0, 0, 180); -//// const Point3 pos0 = interpol.get(ts-500); -//// const Point3 pos1 = interpol.get(ts+500); -// const Point3 pos = interpol.get(ts); -//// //const Point3 dir = Point3(-50000, -50000, 50000);//pos0 - pos; -//// Point3 dir = pos1 - pos0; dir.z /= 2; // only slight down/up looking -//// map->setLookAt((pos+human) / 100.0f, (dir) / 100.0f); - -// GridPoint cur(pos.x, pos.y, pos.z); -// const DijkstraNode* dnStart = d.getNode( grid.getNearestNode(cur) ); -// DijkstraPath dp(dnStart, dnEnd); -// map->setPath(dp); - - -//// Point3 next = -//// dp.getFromStart(2).element->inCentimeter() + -//// dp.getFromStart(4).element->inCentimeter() + -//// dp.getFromStart(6).element->inCentimeter(); -//// next /= 3; -//// const Point3 dir = pos - next ; -// static Point3 lastPos; -// lastPos = lastPos * 0.85 + pos * 0.15; - -// const Point3 dir = lastPos - pos ; -// map->setLookAt((pos+human) / 100.0f, (dir) / 100.0f); - +// static float r = 0; +// while(true) { +// r += 0.01; +// eye += Point3(-0.01, 0, 0); +// ctrl.getMapView()->setLookEye(eye); +// usleep(1000*50); // } // }; + // std::thread t(run); - -// QQmlApplicationEngine engine; -// engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); - return app.exec(); } diff --git a/map/MapView.cpp b/map/MapView.cpp deleted file mode 100644 index b230f11..0000000 --- a/map/MapView.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include "MapView.h" - -#include - -#include "elements/Walls.h" -#include "elements/Ground.h" -#include "elements/Handrails.h" -#include "elements/Stairs.h" -#include "elements/Doors.h" -#include "elements/Path.h" - -// http://doc.qt.io/qt-5/qtopengl-cube-example.html - -MapView::MapView(QWidget* parent) : QOpenGLWidget(parent) { - - -}; - -void MapView::setMap(Floorplan::IndoorMap* map) { - - for (Floorplan::Floor* floor : map->floors) { - elements.push_back(new Ground(floor)); - elements.push_back(new Walls(floor)); - elements.push_back(new Handrails(floor)); - elements.push_back(new Stairs(floor)); - elements.push_back(new Doors(floor)); - } - - this->path = new Path(); - elements.push_back(this->path); - -} - -void MapView::setPath(const std::vector& path) { - this->path->set(path); -} - - -void MapView::timerEvent(QTimerEvent *) { - update(); -} - -void MapView::initializeGL() { - - initializeOpenGLFunctions(); - - glEnable(GL_DEPTH_TEST); - glEnable(GL_CULL_FACE); - - - - for (Renderable* r : elements) { - r->initGL(); - } - - timer.start(60, this); - -} - -void MapView::paintGL() { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - draw(); -} - -void MapView::resizeGL(int w, int h) { - - // Calculate aspect ratio - qreal aspect = qreal(w) / qreal(h ? h : 1); - - // viewing frustrum [0:50] meter - const qreal zNear = 0.02, zFar = 50, fov = 50.0; - - // Reset projection - matProject.setToIdentity(); - matProject.scale(-1, 1, 1); - glCullFace(GL_FRONT); - //matProject.scale(0.05, 0.05, 0.05); - matProject.perspective(fov, aspect, zNear, zFar); - //matProject.scale(-0.01, 0.01, 0.01); - -} - -void MapView::setLookAt(const Point3 pos_m, const Point3 dir) { - QVector3D qDir(dir.x, dir.z, dir.y); - QVector3D lookAt = QVector3D(pos_m.x, pos_m.z, pos_m.y); - QVector3D eye = lookAt + qDir * 0.1; - QVector3D up = QVector3D(0,1,0); - matView.setToIdentity(); - //matView.scale(0.01, 0.01, 0.01); - matView.lookAt(eye, lookAt, up); - //matView.scale(0.99, 1, 1); - //matView.translate(0.7, 0, 0); - lightPos = eye + QVector3D(0.0, 4.0, 0.0); - eyePos = eye; -} - -void MapView::draw() { - - // clear everything - glClearColor(0,0,0,1); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - //static float angularSpeed = 0; - //angularSpeed += 1.5; - - //texture->bind(); - //QVector3D rotationAxis(1,1,1); - //QQuaternion rotation = QQuaternion::fromAxisAndAngle(rotationAxis, angularSpeed); - - // Calculate model view transformation - QMatrix4x4 matModel; - //matModel.setToIdentity(); - //matModel.translate(0.0, 0.0, 0.0); - //matModel.rotate(rotation); - - for (Renderable* r : elements) { - - QOpenGLShaderProgram& program = r->getProgram(); - program.bind(); - - // set the matrices - program.setUniformValue("m_matrix", matModel); - program.setUniformValue("mv_matrix", matView * matModel); - program.setUniformValue("mvp_matrix", matProject * matView * matModel); - program.setUniformValue("lightWorldPos", lightPos); - program.setUniformValue("eyeWorldPos", eyePos); - - r->render(); - - } - -} diff --git a/map/MapView.h b/map/MapView.h deleted file mode 100644 index c966da5..0000000 --- a/map/MapView.h +++ /dev/null @@ -1,76 +0,0 @@ -#ifndef MAPVIEW_H -#define MAPVIEW_H - -#include -#include -#include -#include - -#include -#include - -#include "elements/Path.h" - -namespace Floorplan { - class IndoorMap; -} - -class Renderable; -class Path; - - -class MapView : public QOpenGLWidget, protected QOpenGLFunctions { - -private: - - QMatrix4x4 matProject; - QMatrix4x4 matView; - - QVector3D lightPos; - QVector3D eyePos; - - - QBasicTimer timer; - - std::vector elements; - Path* path; - - - -public: - - MapView(QWidget* parent = 0); - - /** set the map to display */ - void setMap(Floorplan::IndoorMap* map); - - /** the position to look at */ - void setLookAt(const Point3 pos, const Point3 dir = Point3(-1, -1, 0.1)); - - - /** set the path to disply */ - void setPath(const std::vector& path); - - /** set the path to disply */ - template void setPath(const DijkstraPath& path) { - this->path->set(path); - } - - -protected: - - void timerEvent(QTimerEvent *e) Q_DECL_OVERRIDE; - - void initializeGL(); - - void paintGL(); - - void resizeGL(int width, int height); - -private: - - void draw(); - -}; - -#endif // MAPVIEW_H diff --git a/map/Renderable.h b/map/Renderable.h deleted file mode 100644 index 24aacc8..0000000 --- a/map/Renderable.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef RENDERABLE_H -#define RENDERABLE_H - -#include - -class Renderable { - -protected: - - QOpenGLShaderProgram program; - -public: - - /** get the renderable's shader */ - QOpenGLShaderProgram& getProgram() {return program;} - - /** render the renderable */ - void render() { - program.bind(); - _render(); - } - - - virtual void initGL() = 0; - - virtual void _render() = 0; - -protected: - - /** helper method to build the shader */ - void loadShader(const QString& vertex, const QString& fragment) { - if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, vertex)) {throw "1";} - if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, fragment)) {throw "2";} - if (!program.link()) {throw "3";} - if (!program.bind()) {throw "4";} - } - -}; - -#endif // RENDERABLE_H diff --git a/misc/fixc11.h b/misc/fixc11.h index 9c6b36a..bd7561c 100644 --- a/misc/fixc11.h +++ b/misc/fixc11.h @@ -13,36 +13,36 @@ namespace std { //} - template string to_string(const T val) { + template inline string to_string(const T val) { stringstream ss; ss << val; return ss.str(); } - template T round(const T val) { + template inline T round(const T val) { return ::round(val); } // http://stackoverflow.com/questions/19478687/no-member-named-stoi-in-namespace-std - int stoi(const std::string& str) { + inline int stoi(const std::string& str) { std::istringstream is(str); int val; is >> val; return val; } // analog zu oben - float stof(const std::string& str) { + inline float stof(const std::string& str) { std::istringstream is(str); float val; is >> val; return val; } // analog zu oben - double stod(const std::string& str) { + inline double stod(const std::string& str) { std::istringstream is(str); double val; is >> val; return val; } // analog zu oben - uint64_t stol(const std::string& str) { + inline uint64_t stol(const std::string& str) { std::istringstream is(str); uint64_t val; is >> val; return val; } diff --git a/nav/Filter.h b/nav/Filter.h new file mode 100644 index 0000000..9e3ee35 --- /dev/null +++ b/nav/Filter.h @@ -0,0 +1,149 @@ +#ifndef FILTER_H +#define FILTER_H + +#include + +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "State.h" +#include "Node.h" + +#include "../Settings.h" + +class PFInit : public K::ParticleFilterInitializer { + +private: + + Grid* grid; + +public: + + PFInit(Grid* grid) : grid(grid) { + + } + + virtual void initialize(std::vector>& particles) override { + + std::minstd_rand gen; + std::uniform_int_distribution distIdx(0, grid->getNumNodes()-1); + std::uniform_real_distribution distHead(0, 2*M_PI); + + for (K::Particle& p : particles) { + const int idx = distIdx(gen); + const MyGridNode& node = (*grid)[idx]; + p.state.position = node; // random position + p.state.heading.direction = Heading(distHead(gen)); // random heading + } + +// // fix position + heading +// for (K::Particle& p : particles) { +// const int idx = 9000; +// const MyGridNode& node = (*grid)[idx]; +// p.state.position = node; +// p.state.heading.direction = Heading(0); +// } + + } + +}; + +class PFTrans : public K::ParticleFilterTransition { + +public: + + Grid* grid; + GridWalker walker; + + WalkModuleFavorZ modFavorZ; + WalkModuleHeadingControl modHeading; + WalkModuleNodeImportance modImportance; + WalkModuleButterActivity modBarometer; + WalkModuleFollowDestination modDestination; + + std::minstd_rand gen; + +public: + + PFTrans(Grid* grid, MyControl* ctrl) : grid(grid), modHeading(ctrl, Settings::turnSigma), modDestination(*grid) { + + walker.addModule(&modFavorZ); + walker.addModule(&modHeading); + walker.addModule(&modImportance); + walker.addModule(&modBarometer); + walker.addModule(&modDestination); + + if (Settings::destination != GridPoint(0,0,0)) { + modDestination.setDestination(grid->getNodeFor(Settings::destination)); + } + + } + + + + void transition(std::vector>& particles, const MyControl* control) override { + + std::normal_distribution noise(0, Settings::stepSigma); + + for (K::Particle& p : particles) { + const float dist_m = std::abs(control->numStepsSinceLastTransition * Settings::stepLength + noise(gen)); + p.state = walker.getDestination(*grid, p.state, dist_m); + } + + ((MyControl*)control)->resetAfterTransition(); + + } + +}; + +class PFEval : public K::ParticleFilterEvaluation { + + WiFiModelLogDistCeiling& wifiModel; + WiFiObserverFree wiFiProbability; + +public: + + PFEval(WiFiModelLogDistCeiling& wifiModel) : wifiModel(wifiModel), wiFiProbability(Settings::wifiSigma, wifiModel) { + + } + + double evaluation(std::vector>& particles, const MyObservation& _observation) override { + + double sum = 0; + + // smartphone is 1.3 meter above ground + const Point3 person(0,0,Settings::smartphoneAboveGround); + + // local copy!! observation might be changed async outside!! (will really produces crashes!) + const MyObservation observation = _observation; + + for (K::Particle& p : particles) { + const double pWiFi = wiFiProbability.getProbability(p.state.position.inMeter()+person, observation.currentTime, observation.wifi); + const double pGPS = 1; + const double prob = pWiFi * pGPS; + p.weight = prob; + sum += prob; + } + + return sum; + + } + +}; + + + +#endif // FILTER_H diff --git a/nav/NavController.h b/nav/NavController.h new file mode 100644 index 0000000..e6d963d --- /dev/null +++ b/nav/NavController.h @@ -0,0 +1,299 @@ +#ifndef NAVCONTROLLER_H +#define NAVCONTROLLER_H + +#include "../sensors/AccelerometerSensor.h" +#include "../sensors/GyroscopeSensor.h" +#include "../sensors/BarometerSensor.h" +#include "../sensors/WiFiSensor.h" +#include "../sensors/SensorFactory.h" +#include "../sensors/StepSensor.h" +#include "../sensors/TurnSensor.h" + +#include "../ui/debug/SensorDataWidget.h" +#include "../ui/map/MapView.h" + +#include +#include + +#include "State.h" +#include "Filter.h" +#include "Controller.h" + +#include +#include +#include +#include + +#include "Settings.h" +#include "RegionalResampling.h" + +Q_DECLARE_METATYPE(const void*) + + + + + + + + +class NavController : + public SensorListener, + public SensorListener, + public SensorListener, + public SensorListener, + public SensorListener, + public SensorListener, + public SensorListener { + +private: + + Controller* mainController; + Grid* grid; + WiFiModelLogDistCeiling wifiModel; + Floorplan::IndoorMap* im; + + MyObservation curObs; + MyControl curCtrl; + + bool running = false; + std::thread tUpdate; + std::thread tDisplay; + + std::unique_ptr> pf; + +public: + + virtual ~NavController() { + if (running) {stop();} + } + + NavController(Controller* mainController, Grid* grid, Floorplan::IndoorMap* im) : mainController(mainController), grid(grid), wifiModel(im), im(im) { + + wifiModel.loadAPs(im, Settings::wifiTXP, Settings::wifiEXP, Settings::wifiWAF); + + SensorFactory::get().getAccelerometer().addListener(this); + SensorFactory::get().getGyroscope().addListener(this); + SensorFactory::get().getBarometer().addListener(this); + SensorFactory::get().getWiFi().addListener(this); + SensorFactory::get().getSteps().addListener(this); + SensorFactory::get().getTurns().addListener(this); + + std::unique_ptr> init(new PFInit(grid)); + + //std::unique_ptr> estimation(new K::ParticleFilterEstimationWeightedAverage()); + std::unique_ptr> estimation(new K::ParticleFilterEstimationOrderedWeightedAverage(0.1)); + + //std::unique_ptr> resample(new K::ParticleFilterResamplingSimple()); + //std::unique_ptr> resample(new K::ParticleFilterResamplingPercent(0.10)); + std::unique_ptr resample(new RegionalResampling()); + + + std::unique_ptr> eval(new PFEval(wifiModel)); + std::unique_ptr> transition(new PFTrans(grid, &curCtrl)); + + pf = std::unique_ptr>(new K::ParticleFilter(Settings::numParticles, std::move(init))); + pf->setTransition(std::move(transition)); + pf->setEvaluation(std::move(eval)); + pf->setEstimation(std::move(estimation)); + pf->setResampling(std::move(resample)); + + pf->setNEffThreshold(1.0); + + } + + void start() { + Assert::isFalse(running, "already started!"); + running = true; + tUpdate = std::thread(&NavController::update, this); + tDisplay = std::thread(&NavController::display, this); + } + + void stop() { + Assert::isTrue(running, "not started!"); + running = false; + tUpdate.join(); + tDisplay.join(); + } + + void onSensorData(Sensor* sensor, const Timestamp ts, const AccelerometerData& data) override { + (void) sensor; + curObs.currentTime = ts; + } + + void onSensorData(Sensor* sensor, const Timestamp ts, const GyroscopeData& data) override { + (void) sensor; + curObs.currentTime = ts; + } + + void onSensorData(Sensor* sensor, const Timestamp ts, const BarometerData& data) override { + (void) sensor; + curObs.currentTime = ts; + } + + void onSensorData(Sensor* sensor, const Timestamp ts, const WiFiMeasurements& data) override { + (void) sensor; + (void) ts; + curObs.currentTime = ts; + curObs.wifi = data; + } + + void onSensorData(Sensor* sensor, const Timestamp ts, const GPSData& data) override { + (void) sensor; + (void) ts; + curObs.currentTime = ts; + curObs.gps = data; + } + + void onSensorData(Sensor* sensor, const Timestamp ts, const StepData& data) override { + (void) sensor; + (void) ts; + curObs.currentTime = ts; + curCtrl.numStepsSinceLastTransition += data.stepsSinceLastEvent; // set to zero after each transition + } + + void onSensorData(Sensor* sensor, const Timestamp ts, const TurnData& data) override { + (void) sensor; + (void) ts; + curObs.currentTime = ts; + curCtrl.turnSinceLastTransition_rad += data.radSinceLastEvent; // set to zero after each transition + } + + int cameraMode = 0; + void toggleCamera() { + cameraMode = (cameraMode + 1) % 3; + } + +private: + + /** particle-filter update loop */ + void update() { + + Timestamp lastTransition; + + while(running) { + +// // fixed update rate based on the systems time -> LIVE! even for offline data +// const Timestamp ts1 = Timestamp::fromUnixTime(); +// doUpdate(); +// const Timestamp ts2 = Timestamp::fromUnixTime(); +// const Timestamp needed = ts2-ts1; +// const Timestamp sleep = Timestamp::fromMS(500) - needed; +// std::this_thread::sleep_for(std::chrono::milliseconds(sleep.ms())); + + // fixed update rate based on incoming sensor data + // allows working with live data and faster for offline data + const Timestamp diff = curObs.currentTime - lastTransition; + if (diff > Timestamp::fromMS(500)) { + doUpdate(); + lastTransition = curObs.currentTime; + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + } + } + + + MyState curEst; + //MyState lastEst; + + void doUpdate() { + + //lastEst = curEst; + curEst = pf->update(&curCtrl, curObs); + + // hacky.. but we need to call this one from the main thread! + //mainController->getMapView()->showParticles(pf->getParticles()); + qRegisterMetaType(); + Assert::isTrue(QMetaObject::invokeMethod(mainController->getMapView(), "showParticles", Qt::QueuedConnection, Q_ARG(const void*, &pf->getParticles())), "call failed"); + + + PFTrans* trans = (PFTrans*)pf->getTransition(); + const MyGridNode* node = grid->getNodePtrFor(curEst.position); + if (node) { + const DijkstraPath path = trans->modDestination.getShortestPath(*node); + // mainController->getMapView()->showGridImportance(); + Assert::isTrue(QMetaObject::invokeMethod(mainController->getMapView(), "setPath", Qt::QueuedConnection, Q_ARG(const void*, &path)), "call failed"); + } + + /* + static K::Gnuplot gp; + K::GnuplotSplot plot; + K::GnuplotSplotElementLines lines; plot.add(&lines); + K::GnuplotSplotElementPoints points; plot.add(&points); + K::GnuplotSplotElementPoints best; plot.add(&best); best.setPointSize(2); best.setColorHex("#0000ff"); + + for (const K::Particle& p : pf->getParticles()) { + const Point3 pos = p.state.position.inMeter(); + points.add(K::GnuplotPoint3(pos.x, pos.y, pos.z)); + } + + for (const Floorplan::Floor* f : im->floors) { + for (const Floorplan::FloorOutlinePolygon* polygon : f->outline) { + for (int i = 0; i < polygon->poly.points.size(); ++i) { + const Point2 p1 = polygon->poly.points[i]; + const Point2 p2 = polygon->poly.points[(i+1)%polygon->poly.points.size()]; + K::GnuplotPoint3 gp1(p1.x, p1.y, f->atHeight); + K::GnuplotPoint3 gp2(p2.x, p2.y, f->atHeight); + lines.addSegment(gp1, gp2); + } + } + } + + K::GnuplotPoint3 gpBest(curEst.position.x_cm/100.0f, curEst.position.y_cm/100.0f, curEst.position.z_cm/100.0f); + best.add(gpBest); + + gp.draw(plot); + gp.flush(); + */ + + + } + + const int display_ms = 50; + + /** UI update loop */ + void display() { + while(running) { + doDisplay(); + std::this_thread::sleep_for(std::chrono::milliseconds(display_ms)); + } + } + + Point3 curPosFast; + Point3 curPosSlow; + + + + void doDisplay() { + + const float kappa1 = display_ms / 1000.0f; + const float kappa2 = kappa1 * 0.7; + + const float myHeight_m = 1.80; + + curPosFast = curPosFast * (1-kappa1) + curEst.position.inMeter() * (kappa1); + curPosSlow = curPosSlow * (1-kappa2) + curEst.position.inMeter() * (kappa2); + + const Point3 dir = (curPosFast - curPosSlow).normalized(); + const Point3 dir2 = Point3(dir.x, dir.y, -0.2).normalized(); + + if (cameraMode == 0) { + mainController->getMapView()->setLookAt(curPosFast + Point3(0,0,myHeight_m), dir); + } else if (cameraMode == 1) { + mainController->getMapView()->setLookAt(curPosFast + Point3(0,0,myHeight_m) - dir2*4, dir2); + } else if (cameraMode == 2) { + const Point3 spectator = curPosFast + Point3(0,0,20) - dir*15; + const Point3 spectatorDir = (curPosFast - spectator).normalized(); + mainController->getMapView()->setLookEye(spectator); + mainController->getMapView()->setLookDir(spectatorDir); + } + + mainController->getMapView()->setCurrentEstimation(curPosFast, dir); + + + } + +}; + +#endif // NAVCONTROLLER_H diff --git a/nav/Node.h b/nav/Node.h new file mode 100644 index 0000000..b2be7c0 --- /dev/null +++ b/nav/Node.h @@ -0,0 +1,29 @@ +#ifndef NODE_H +#define NODE_H + +#include +#include + +struct MyGridNode : public GridNode, public GridPoint {//, public WiFiGridNode<10> { + + float navImportance; + float getNavImportance() const { return navImportance; } + + /** empty ctor */ + MyGridNode() : GridPoint(-1, -1, -1) {;} + + /** ctor */ + MyGridNode(const int x_cm, const int y_cm, const int z_cm) : GridPoint(x_cm, y_cm, z_cm) {;} + + + static void staticDeserialize(std::istream& inp) { + //WiFiGridNode::staticDeserialize(inp); + } + + static void staticSerialize(std::ostream& out) { + //WiFiGridNode::staticSerialize(out); + } + +}; + +#endif // NODE_H diff --git a/nav/RegionalResampling.h b/nav/RegionalResampling.h new file mode 100644 index 0000000..7646006 --- /dev/null +++ b/nav/RegionalResampling.h @@ -0,0 +1,68 @@ +#ifndef REGIONALRESAMPLING_H +#define REGIONALRESAMPLING_H + +#include +#include "State.h" + +class RegionalResampling : public K::ParticleFilterResampling { + +public: + + float maxDist = 12.5; + + RegionalResampling() {;} + + void resample(std::vector>& particles) override { + + Point3 sum; + for (const K::Particle& p : particles) { + sum += p.state.position.inMeter(); + } + const Point3 avg = sum / particles.size(); + + std::vector> next; + for (const K::Particle& p : particles) { + const float dist = p.state.position.inMeter().getDistance(avg); + if (rand() % 6 != 0) {continue;} + if (dist < maxDist) {next.push_back(p);} + } + + // cumulate + std::vector> copy = particles; + double cumWeight = 0; + for ( K::Particle& p : copy) { + cumWeight += p.weight; + p.weight = cumWeight; + } + + // draw missing particles + const int missing = particles.size() - next.size(); + for (int i = 0; i < missing; ++i) { + next.push_back(draw(copy, cumWeight)); + } + + std::swap(next, particles); + + } + + std::minstd_rand gen; + + /** draw one particle according to its weight from the copy vector */ + const K::Particle& draw(std::vector>& copy, const double cumWeight) { + + // generate random values between [0:cumWeight] + std::uniform_real_distribution dist(0, cumWeight); + + // draw a random value between [0:cumWeight] + const float rand = dist(gen); + + // search comparator (cumWeight is ordered -> use binary search) + auto comp = [] (const K::Particle& s, const float d) {return s.weight < d;}; + auto it = std::lower_bound(copy.begin(), copy.end(), rand, comp); + return *it; + + } + +}; + +#endif // REGIONALRESAMPLING_H diff --git a/nav/State.h b/nav/State.h new file mode 100644 index 0000000..002a30a --- /dev/null +++ b/nav/State.h @@ -0,0 +1,77 @@ +#ifndef STATE_H +#define STATE_H + +#include +#include +#include +#include +#include + +#include +#include + +struct MyState : public WalkState, public WalkStateFavorZ, public WalkStateHeading, public WalkStateBarometerActivity { + + + /** ctor */ + MyState(const int x_cm, const int y_cm, const int z_cm) : WalkState(GridPoint(x_cm, y_cm, z_cm)), WalkStateHeading(Heading(0), 0) { + ; + } + + MyState() : WalkState(GridPoint()), WalkStateHeading(Heading(0), 0) { + ; + } + + MyState& operator += (const MyState& o) { + position += o.position; + return *this; + } + + MyState& operator /= (const float val) { + position /= val; + return *this; + } + + MyState operator * (const float val) const { + MyState copy = *this; + copy.position = copy.position * val; + return copy; + } + + + + +}; + +/** observed sensor data */ +struct MyObservation { + + /** wifi measurements */ + WiFiMeasurements wifi; + + /** gps measurements */ + GPSData gps; + + /** time of evaluation */ + Timestamp currentTime; + +}; + +/** (observed) control data */ +struct MyControl { + + /** turn angle (in radians) since the last transition */ + float turnSinceLastTransition_rad = 0; + + /** number of steps since the last transition */ + int numStepsSinceLastTransition = 0; + + /** reset the control-data after each transition */ + void resetAfterTransition() { + turnSinceLastTransition_rad = 0; + numStepsSinceLastTransition = 0; + } + +}; + +#endif // STATE_H diff --git a/qml.qrc b/qml.qrc index e8bac0b..07cfb98 100644 --- a/qml.qrc +++ b/qml.qrc @@ -1,8 +1,23 @@ - res/gl/fragment1.glsl + res/gl/fragmentLine.glsl + res/gl/fragmentTexSimple.glsl res/gl/vertex1.glsl - res/gl/tex/floor1.jpg - res/gl/tex/wall1.jpg + res/gl/fragmentTex.glsl + res/gl/tex/arrows.png + res/gl/tex/door2.jpg + res/gl/tex/door2_normal.jpg + res/gl/tex/floor4.jpg + res/gl/tex/floor4_normal.jpg + res/icons/load.svg + res/icons/run.svg + res/icons/route.svg + res/icons/bug.svg + res/gl/fragmentColorPoint.glsl + res/gl/tex/wall3_normal.jpg + res/gl/tex/wall3.jpg + res/gl/tex/empty_normals.jpg + res/icons/cube.svg + res/icons/camera.svg diff --git a/res/gl/fragmentColorPoint.glsl b/res/gl/fragmentColorPoint.glsl new file mode 100644 index 0000000..e7fed43 --- /dev/null +++ b/res/gl/fragmentColorPoint.glsl @@ -0,0 +1,25 @@ +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +// interpolated values +//varying vec3 v_WorldPos; +//varying vec3 v_normal; +//varying vec2 v_texcoord; +varying vec3 v_color; + +void main() { + + // set point color + gl_FragColor = vec4(v_color, 1.0); + + #ifdef GL_ES + + // set point size + gl_PointSize = 3.0; + + #endif + +} diff --git a/res/gl/fragmentTex.glsl b/res/gl/fragmentTex.glsl index cb57729..a609dc3 100644 --- a/res/gl/fragmentTex.glsl +++ b/res/gl/fragmentTex.glsl @@ -22,7 +22,7 @@ void main() { // diffuse fragment color vec4 ambient = texture2D(texDiffuse, v_texcoord); - vec4 diffuse = ambient * vec4(0.8, 0.6, 0.35, 1.0); + vec4 diffuse = ambient * vec4(0.8, 0.8, 0.8, 1.0); vec4 specular = vec4(1,1,1,1); // get the normal from the normal map @@ -46,12 +46,12 @@ void main() { // vec3 h = normalize(lightDir + eyeDir); // float intSpec = max(dot(h, normal), 0.0); // float specularIntensity = pow(intSpec, 2.0); - float specularIntensity = pow(max(0.0, dot(eyeDir, reflect(-lightDir, normal))), 16); + float specularIntensity = pow(max(0.0, dot(eyeDir, reflect(-lightDir, normal))), 16.0); // distance between fragment and light (in meter) float distToLight_m = length(lightWorldPos - v_WorldPos); - float lightInt = clamp(1.0 / (distToLight_m / 10.0), 0.0, 1.0); + float lightInt = 1.0;//clamp(1.0 / (distToLight_m / 10.0), 0.0, 1.0); // Set fragment color from texture vec4 finalColor = @@ -60,6 +60,7 @@ void main() { clamp(specular * specularIntensity, 0.0, 1.0) * 1.0; gl_FragColor = clamp(finalColor, 0.0, 1.0); + gl_FragColor.a = 0.4; // FOG //float mixing = pow((1.0 - v_CamPos.z * 3.0), 2); diff --git a/res/gl/tex/empty_normals.jpg b/res/gl/tex/empty_normals.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f559e3b85abece62805af2b5276ba4cb790677fc GIT binary patch literal 998 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 literal 0 HcmV?d00001 diff --git a/res/gl/vertex1.glsl b/res/gl/vertex1.glsl index 2543183..9adb082 100644 --- a/res/gl/vertex1.glsl +++ b/res/gl/vertex1.glsl @@ -12,6 +12,7 @@ attribute vec3 a_position; attribute vec3 a_normal; attribute vec2 a_texcoord; attribute vec3 a_tangent; +attribute vec3 a_color; varying vec3 v_WorldPos; varying vec3 v_CamPos; @@ -20,6 +21,8 @@ varying vec3 v_normal; varying vec2 v_texcoord; varying mat3 normalMat; +varying vec3 v_color; + void main() { @@ -29,7 +32,7 @@ void main() { v_CamPos = vec3(mv_matrix * vec4(a_position, 1.0)); v_normal = a_normal;//normalize(vec3(mv_matrix * vec4(a_normal, 0.0))); v_texcoord = a_texcoord; - + v_color = a_color; // http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/ diff --git a/res/icons/bug.svg b/res/icons/bug.svg new file mode 100644 index 0000000..efc456b --- /dev/null +++ b/res/icons/bug.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icons/camera.svg b/res/icons/camera.svg new file mode 100644 index 0000000..d281655 --- /dev/null +++ b/res/icons/camera.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/icons/cube.svg b/res/icons/cube.svg new file mode 100644 index 0000000..baf0637 --- /dev/null +++ b/res/icons/cube.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icons/load.svg b/res/icons/load.svg new file mode 100644 index 0000000..6319f45 --- /dev/null +++ b/res/icons/load.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icons/route.svg b/res/icons/route.svg new file mode 100644 index 0000000..2953033 --- /dev/null +++ b/res/icons/route.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icons/run.svg b/res/icons/run.svg new file mode 100644 index 0000000..e8b0847 --- /dev/null +++ b/res/icons/run.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensors/AccelerometerSensor.h b/sensors/AccelerometerSensor.h index b4ed6c0..e6e65cf 100644 --- a/sensors/AccelerometerSensor.h +++ b/sensors/AccelerometerSensor.h @@ -2,18 +2,7 @@ #define ACCELEROMETERSENSOR_H #include "Sensor.h" - -struct AccelerometerData { - float x; - float y; - float z; - AccelerometerData(const float x, const float y, const float z) : x(x), y(y), z(z) {;} - std::string asString() const { - std::stringstream ss; - ss << "(" << x << "," << y << "," << z << ")"; - return ss.str(); - } -}; +#include class AccelerometerSensor : public Sensor { diff --git a/sensors/BarometerSensor.h b/sensors/BarometerSensor.h new file mode 100644 index 0000000..cbcdf94 --- /dev/null +++ b/sensors/BarometerSensor.h @@ -0,0 +1,11 @@ +#ifndef BAROMETERSENSOR_H +#define BAROMETERSENSOR_H + +#include "Sensor.h" +#include + +class BarometerSensor : public Sensor { + +}; + +#endif // BAROMETERSENSOR_H diff --git a/sensors/GyroscopeSensor.h b/sensors/GyroscopeSensor.h new file mode 100644 index 0000000..94207d9 --- /dev/null +++ b/sensors/GyroscopeSensor.h @@ -0,0 +1,11 @@ +#ifndef GYROSCOPESENSOR_H +#define GYROSCOPESENSOR_H + +#include "Sensor.h" +#include + +class GyroscopeSensor : public Sensor { + +}; + +#endif // GYROSCOPESENSOR_H diff --git a/sensors/Sensor.h b/sensors/Sensor.h index 8c37d9d..eee245c 100644 --- a/sensors/Sensor.h +++ b/sensors/Sensor.h @@ -3,6 +3,9 @@ #include #include +#include + +template class Sensor; /** listen for sensor events */ template class SensorListener { @@ -10,7 +13,7 @@ template class SensorListener { public: /** incoming sensor data */ - virtual void onSensorData(const T& data) = 0; + virtual void onSensorData(Sensor* sensor, const Timestamp ts, const T& data) = 0; }; @@ -38,9 +41,17 @@ public: protected: /** inform all attached listeners */ - void informListeners(const T& sensorData) const { + void informListeners(const T& sensorData) { + const Timestamp now = Timestamp::fromRunningTime(); for (SensorListener* l : listeners) { - l->onSensorData(sensorData); + l->onSensorData(this, now, sensorData); + } + } + + /** inform all attached listeners. call this if you know the timestamp */ + void informListeners(const Timestamp ts, const T& sensorData) { + for (SensorListener* l : listeners) { + l->onSensorData(this, ts, sensorData); } } diff --git a/sensors/SensorFactory.h b/sensors/SensorFactory.h index eeea752..769c68f 100644 --- a/sensors/SensorFactory.h +++ b/sensors/SensorFactory.h @@ -12,36 +12,69 @@ #include "dummy/AccelerometerSensorDummy.h" #include "android/AccelerometerSensorAndroid.h" +#include "GyroscopeSensor.h" +#include "android/GyroscopeSensorAndroid.h" +#include "dummy/GyroscopeSensorDummy.h" + +#include "BarometerSensor.h" +#include "android/BarometerSensorAndroid.h" +#include "dummy/BarometerSensorDummy.h" + #include "StepSensor.h" +#include "TurnSensor.h" + + class SensorFactory { +private: + + /** this one is a dirty hack, as static class member variables do not work header-only */ + static SensorFactory** getPtr() { + static SensorFactory* ptr = nullptr; + return &ptr; + } + +public: + + /** set the to-be-used sensor-fatory */ + static void set(SensorFactory* fac) { + Assert::isNull(*getPtr(), "set() was already called. currentely this is not intended"); + *getPtr() = fac; + } + + /** get the currently configured sensory factory */ + static SensorFactory& get() { + Assert::isNotNull(*getPtr(), "call set() first to set an actual factory instance!"); + return **getPtr(); + } + public: /** get the WiFi sensor */ - static WiFiSensor& getWiFi() { -#ifdef ANDROID - return WiFiSensorAndroid::get(); -#else - return WiFiSensorDummy::get(); -#endif - } + virtual WiFiSensor& getWiFi() = 0; /** get the Accelerometer sensor */ - static AccelerometerSensor& getAccelerometer() { - #ifdef ANDROID - return AccelerometerSensorAndroid::get(); -#else - return AccelerometerSensorDummy::get(); -#endif - } + virtual AccelerometerSensor& getAccelerometer() = 0; + + /** get the Gyroscope sensor */ + virtual GyroscopeSensor& getGyroscope() = 0; + + /** get the Barometer sensor */ + virtual BarometerSensor& getBarometer() = 0; /** get the Step sensor */ - static StepSensor& getSteps() { + StepSensor& getSteps() { static StepSensor steps(getAccelerometer()); return steps; } + /** get the Turn sensor */ + TurnSensor& getTurns() { + static TurnSensor turns(getAccelerometer(), getGyroscope()); + return turns; + } + }; #endif // SENSORFACTORY_H diff --git a/sensors/StepSensor.h b/sensors/StepSensor.h index ecdc42b..4078c02 100644 --- a/sensors/StepSensor.h +++ b/sensors/StepSensor.h @@ -1,66 +1,46 @@ #ifndef STEPSENSOR_H #define STEPSENSOR_H - -#include "../misc/fixc11.h" +#include #include "AccelerometerSensor.h" -#include "Sensor.h" + struct StepData { - ; + const int stepsSinceLastEvent = 0; + StepData(const int stepsSinceLastEvent) : stepsSinceLastEvent(stepsSinceLastEvent) {;} }; -class StepSensor : public Sensor, public SensorListener { +/** + * step-sensor detects steps from the accelerometer + */ +class StepSensor : public SensorListener, public Sensor { private: - AccelerometerSensor& acc; + StepDetection sd; public: - /** hidden ctor. use singleton */ - StepSensor(AccelerometerSensor& acc) : acc(acc) { - ; - } - - void start() override { + StepSensor(AccelerometerSensor& acc) { acc.addListener(this); - acc.start(); } - void stop() override { - throw "todo"; + virtual void start() override { + // } - virtual void onSensorData(const AccelerometerData& data) override { - parse(data); + virtual void stop() override { + // } - -protected: - - const float threshold = 11.0; - const int blockTime = 25; - int block = 0; - - void parse(const AccelerometerData& data) { - - const float x = data.x; - const float y = data.y; - const float z = data.z; - - const float mag = std::sqrt( (x*x) + (y*y) + (z*z) ); - - if (block > 0) { - --block; - } else if (mag > threshold) { - informListeners(StepData()); - block = blockTime; + virtual void onSensorData(Sensor* sensor, const Timestamp ts, const AccelerometerData& data) override { + (void) sensor; + const bool step = sd.add(ts, data); + if (step) { + informListeners(ts, StepData(1)); } - } }; - #endif // STEPSENSOR_H diff --git a/sensors/TurnSensor.h b/sensors/TurnSensor.h new file mode 100644 index 0000000..3da9102 --- /dev/null +++ b/sensors/TurnSensor.h @@ -0,0 +1,53 @@ +#ifndef TURNSENSOR_H +#define TURNSENSOR_H + +#include +#include "AccelerometerSensor.h" +#include "GyroscopeSensor.h" + +struct TurnData { + float radSinceLastEvent; + float radSinceStart; + TurnData() : radSinceLastEvent(0), radSinceStart(0) {;} +}; + +class TurnSensor : public SensorListener, public SensorListener, public Sensor { + +private: + + TurnDetection turn; + TurnData data; + +public: + + /** ctor */ + TurnSensor(AccelerometerSensor& acc, GyroscopeSensor& gyro) { + acc.addListener(this); + gyro.addListener(this); + } + + void start() override { + // + } + + void stop() override { + // + } + + virtual void onSensorData(Sensor* sensor, const Timestamp ts, const AccelerometerData& data) override { + (void) sensor; + turn.addAccelerometer(ts, data); + } + + virtual void onSensorData(Sensor* sensor, const Timestamp ts, const GyroscopeData& data) override { + (void) sensor; + const float rad = turn.addGyroscope(ts, data); + this->data.radSinceLastEvent = rad; + this->data.radSinceStart += rad; + informListeners(ts, this->data); + } + +}; + + +#endif // TURNSENSOR_H diff --git a/sensors/WiFiSensor.h b/sensors/WiFiSensor.h index 8da04b4..b30576d 100644 --- a/sensors/WiFiSensor.h +++ b/sensors/WiFiSensor.h @@ -1,35 +1,36 @@ #ifndef WIFISENSOR_H #define WIFISENSOR_H +#include "../misc/fixc11.h" #include #include #include "Sensor.h" +#include + +//struct WiFiSensorDataEntry { +// std::string bssid; +// float rssi; +// WiFiSensorDataEntry(const std::string& bssid, const float rssi) : bssid(bssid), rssi(rssi) {;} +// std::string asString() const { +// std::stringstream ss; +// ss << bssid << '\t' << (int)rssi; +// return ss.str(); +// } +//}; -struct WiFiSensorDataEntry { - std::string bssid; - float rssi; - WiFiSensorDataEntry(const std::string& bssid, const float rssi) : bssid(bssid), rssi(rssi) {;} - std::string asString() const { - std::stringstream ss; - ss << bssid << '\t' << (int)rssi; - return ss.str(); - } -}; - - -struct WiFiSensorData { - std::vector entries; - std::string asString() const { - std::stringstream ss; - for(const WiFiSensorDataEntry& e : entries) {ss << e.asString() << '\n';} - return ss.str(); - } -}; +//struct WiFiSensorData { +// std::vector entries; +// std::string asString() const { +// std::stringstream ss; +// for(const WiFiSensorDataEntry& e : entries) {ss << e.asString() << '\n';} +// return ss.str(); +// } +//}; /** interface for all wifi sensors */ -class WiFiSensor : public Sensor { +class WiFiSensor : public Sensor { protected: diff --git a/sensors/android/AccelerometerSensorAndroid.h b/sensors/android/AccelerometerSensorAndroid.h index 8d97c8b..1744860 100644 --- a/sensors/android/AccelerometerSensorAndroid.h +++ b/sensors/android/AccelerometerSensorAndroid.h @@ -51,6 +51,6 @@ public: }; -#endif ANDROID +#endif // ANDROID #endif // ACCELEROMETERSENSORANDROID_H diff --git a/sensors/android/BarometerSensorAndroid.h b/sensors/android/BarometerSensorAndroid.h new file mode 100644 index 0000000..8befd68 --- /dev/null +++ b/sensors/android/BarometerSensorAndroid.h @@ -0,0 +1,55 @@ +#ifndef BAROMETERSENSORANDROID_H +#define BAROMETERSENSORANDROID_H + +#ifdef ANDROID + +#include + +#include "../BarometerSensor.h" + +#include + +#include "../AccelerometerSensor.h" + +class BarometerSensorAndroid : public BarometerSensor { + +private: + + QPressureSensor baro; + + /** hidden ctor. use singleton */ + BarometerSensorAndroid() { + ; + } + +public: + + /** singleton access */ + static BarometerSensorAndroid& get() { + static BarometerSensorAndroid baro; + return baro; + } + + void start() override { + + auto onSensorData = [&] () { + BarometerData data(baro.reading()->pressure()); + informListeners(data); + }; + + baro.connect(&baro, &QPressureSensor::readingChanged, onSensorData); + baro.start(); + + } + + void stop() override { + throw "TODO"; + } + + + +}; + +#endif + +#endif // BAROMETERSENSORANDROID_H diff --git a/sensors/android/GyroscopeSensorAndroid.h b/sensors/android/GyroscopeSensorAndroid.h new file mode 100644 index 0000000..b42f747 --- /dev/null +++ b/sensors/android/GyroscopeSensorAndroid.h @@ -0,0 +1,59 @@ +#ifndef GYROSCOPESENSORANDROID_H +#define GYROSCOPESENSORANDROID_H + +#ifdef ANDROID + +#include + +#include "../GyroscopeSensor.h" + +#include + +#include "../AccelerometerSensor.h" + +class GyroscopeSensorAndroid : public GyroscopeSensor { + +private: + + QGyroscope gyro; + + /** hidden ctor. use singleton */ + GyroscopeSensorAndroid() { + ; + } + +public: + + /** singleton access */ + static GyroscopeSensorAndroid& get() { + static GyroscopeSensorAndroid gyro; + return gyro; + } + + float degToRad(const float deg) { + return deg / 180.0f * M_PI; + } + + void start() override { + + auto onSensorData = [&] () { + GyroscopeData data(degToRad(gyro.reading()->x()), degToRad(gyro.reading()->y()), degToRad(gyro.reading()->z())); + informListeners(data); + }; + + gyro.connect(&gyro, &QGyroscope::readingChanged, onSensorData); + gyro.start(); + + } + + void stop() override { + throw "TODO"; + } + + + +}; + +#endif // ANDROID + +#endif // GYROSCOPESENSORANDROID_H diff --git a/sensors/android/SensorFactoryAndroid.h b/sensors/android/SensorFactoryAndroid.h new file mode 100644 index 0000000..454e71a --- /dev/null +++ b/sensors/android/SensorFactoryAndroid.h @@ -0,0 +1,41 @@ +#ifndef SENSORFACTORYANDROID_H +#define SENSORFACTORYANDROID_H + +#ifdef ANDROID + +#include "../SensorFactory.h" + +#include "WiFiSensorAndroid.h" +#include "AccelerometerSensorAndroid.h" +#include "GyroscopeSensorAndroid.h" +#include "BarometerSensorAndroid.h" + +/** + * sensor factory that provides real hardware sensors from + * an android smartphone that fire real data values + */ +class SensorFactoryAndroid : public SensorFactory { + +public: + + WiFiSensor& getWiFi() override { + return WiFiSensorAndroid::get(); + } + + AccelerometerSensor& getAccelerometer() override { + return AccelerometerSensorAndroid::get(); + } + + GyroscopeSensor& getGyroscope() override { + return GyroscopeSensorAndroid::get(); + } + + BarometerSensor& getBarometer() override { + return BarometerSensorAndroid::get(); + } + +}; + +#endif + +#endif // SENSORFACTORYANDROID_H diff --git a/sensors/android/WiFiSensorAndroid.cpp b/sensors/android/WiFiSensorAndroid.cpp new file mode 100644 index 0000000..7001439 --- /dev/null +++ b/sensors/android/WiFiSensorAndroid.cpp @@ -0,0 +1,20 @@ +#ifdef ANDROID + +#include "WiFiSensorAndroid.h" + +extern "C" { + + /** called after each successful WiFi scan */ + JNIEXPORT void JNICALL Java_indoor_java_WiFi_onScanComplete(JNIEnv* env, jobject jobj, jbyteArray arrayID) { + (void) env; (void) jobj; + jsize length = env->GetArrayLength(arrayID); + jboolean isCopy; + jbyte* data = env->GetByteArrayElements(arrayID, &isCopy); + std::string str((char*)data, length); + env->ReleaseByteArrayElements(arrayID, data, JNI_ABORT); + WiFiSensorAndroid::get().handle(str); + } + +} + +#endif diff --git a/sensors/android/WiFiSensorAndroid.h b/sensors/android/WiFiSensorAndroid.h index a2e0a96..96b2f03 100644 --- a/sensors/android/WiFiSensorAndroid.h +++ b/sensors/android/WiFiSensorAndroid.h @@ -40,7 +40,7 @@ public: void handle(const std::string& data) { // to-be-constructed sensor data - WiFiSensorData sensorData; + WiFiMeasurements sensorData; // parse each mac->rssi entry for (int i = 0; i < (int)data.length(); i += 17+1+2) { @@ -49,7 +49,7 @@ public: const int8_t pad1 = data[i+18]; const int8_t pad2 = data[i+19]; if (pad1 != 0 || pad2 != 0) {Debug::error("padding error within WiFi scan result");} - sensorData.entries.push_back(WiFiSensorDataEntry(bssid, rssi)); + sensorData.entries.push_back(WiFiMeasurement(AccessPoint(bssid), rssi)); } // call listeners @@ -60,20 +60,7 @@ public: }; -extern "C" { - /** called after each successful WiFi scan */ - JNIEXPORT void JNICALL Java_indoor_java_WiFi_onScanComplete(JNIEnv* env, jobject jobj, jbyteArray arrayID) { - (void) env; (void) jobj; - jsize length = env->GetArrayLength(arrayID); - jboolean isCopy; - jbyte* data = env->GetByteArrayElements(arrayID, &isCopy); - std::string str((char*)data, length); - env->ReleaseByteArrayElements(arrayID, data, JNI_ABORT); - WiFiSensorAndroid::get().handle(str); - } - -} #endif diff --git a/sensors/dummy/AccelerometerSensorDummy.h b/sensors/dummy/AccelerometerSensorDummy.h index 4af499c..14abd62 100644 --- a/sensors/dummy/AccelerometerSensorDummy.h +++ b/sensors/dummy/AccelerometerSensorDummy.h @@ -2,9 +2,17 @@ #define ACCELEROMETERSENSORDUMMY_H #include "../AccelerometerSensor.h" +#include "RandomSensor.h" +#include -class AccelerometerSensorDummy : public AccelerometerSensor { +class AccelerometerSensorDummy : public RandomSensor { +private: + + /** hidden ctor */ + AccelerometerSensorDummy() : RandomSensor(Timestamp::fromMS(10)) { + ; + } public: @@ -14,13 +22,25 @@ public: return acc; } - void start() override { - //throw "todo"; +protected: + + std::minstd_rand gen; + std::uniform_real_distribution distNoise = std::uniform_real_distribution(-0.5, +0.5); + + AccelerometerData getRandomEntry() override { + + const Timestamp ts = Timestamp::fromRunningTime(); + const float Hz = 1.6; + const float intensity = 2.0; + + const float x = distNoise(gen); + const float y = distNoise(gen); + const float z = 9.81 + std::sin(ts.sec()*2*M_PI*Hz) * intensity + distNoise(gen); + + return AccelerometerData(x,y,z); + } - void stop() override { - throw "todo"; - } }; diff --git a/sensors/dummy/BarometerSensorDummy.h b/sensors/dummy/BarometerSensorDummy.h new file mode 100644 index 0000000..d0a5eab --- /dev/null +++ b/sensors/dummy/BarometerSensorDummy.h @@ -0,0 +1,43 @@ +#ifndef BAROMETERSENSORDUMMY_H +#define BAROMETERSENSORDUMMY_H + +#include "../BarometerSensor.h" +#include "RandomSensor.h" +#include + +class BarometerSensorDummy : public RandomSensor { + +private: + + std::thread thread; + + /** hidden ctor */ + BarometerSensorDummy() : RandomSensor(Timestamp::fromMS(100)) { + ; + } + +public: + + /** singleton access */ + static BarometerSensorDummy& get() { + static BarometerSensorDummy baro; + return baro; + } + +protected: + + std::minstd_rand gen; + std::uniform_real_distribution distNoise = std::uniform_real_distribution(-0.09, +0.09); + + BarometerData getRandomEntry() override { + + const Timestamp ts = Timestamp::fromRunningTime(); + + const float hPa = 930 + std::sin(ts.sec()) * 0.5 + distNoise(gen); + return BarometerData(hPa); + + } + +}; + +#endif // BAROMETERSENSORDUMMY_H diff --git a/sensors/dummy/GyroscopeSensorDummy.h b/sensors/dummy/GyroscopeSensorDummy.h new file mode 100644 index 0000000..927c4b8 --- /dev/null +++ b/sensors/dummy/GyroscopeSensorDummy.h @@ -0,0 +1,48 @@ +#ifndef GYROSCOPESENSORDUMMY_H +#define GYROSCOPESENSORDUMMY_H + +#include "../GyroscopeSensor.h" +#include "RandomSensor.h" +#include + +class GyroscopeSensorDummy : public RandomSensor { + +private: + + /** hidden ctor */ + GyroscopeSensorDummy() : RandomSensor(Timestamp::fromMS(10)) { + ; + } + +public: + + /** singleton access */ + static GyroscopeSensorDummy& get() { + static GyroscopeSensorDummy gyro; + return gyro; + } + + +protected: + + std::minstd_rand gen; + std::uniform_real_distribution distNoise = std::uniform_real_distribution(-0.1, +0.1); + + GyroscopeData getRandomEntry() override { + + const Timestamp ts = Timestamp::fromRunningTime(); + + const float Hz = 0.1; + const float intensity = 0.35; + + const float x = distNoise(gen); + const float y = distNoise(gen); + const float z = std::sin(ts.sec()*2*M_PI*Hz) * intensity + distNoise(gen); + + return GyroscopeData(x,y,z); + + } + +}; + +#endif // GYROSCOPESENSORDUMMY_H diff --git a/sensors/dummy/RandomSensor.h b/sensors/dummy/RandomSensor.h new file mode 100644 index 0000000..e368946 --- /dev/null +++ b/sensors/dummy/RandomSensor.h @@ -0,0 +1,56 @@ +#ifndef RANDOMSENSOR_H +#define RANDOMSENSOR_H + +#include +#include +#include + +template class RandomSensor : public BaseClass { + +private: + + std::thread thread; + bool running = false; + Timestamp interval; + +public: + + RandomSensor(const Timestamp interval) : interval(interval) { + ; + } + + void start() override { + Assert::isFalse(running, "sensor allready running!"); + running = true; + thread = std::thread(&RandomSensor::run, this); + } + + void stop() override { + Assert::isTrue(running, "sensor not yet running!"); + running = false; + thread.join(); + } + +protected: + + /** subclasses must provide a random entry here */ + virtual Element getRandomEntry() = 0; + +private: + + void run() { + + while(running) { + + const Element rnd = getRandomEntry(); + Sensor::informListeners(rnd); + + std::this_thread::sleep_for(std::chrono::milliseconds(interval.ms())); + + } + + } + +}; + +#endif // RANDOMSENSOR_H diff --git a/sensors/dummy/SensorFactoryDummy.h b/sensors/dummy/SensorFactoryDummy.h new file mode 100644 index 0000000..2af30f1 --- /dev/null +++ b/sensors/dummy/SensorFactoryDummy.h @@ -0,0 +1,36 @@ +#ifndef SENSORFACTORYDUMMY_H +#define SENSORFACTORYDUMMY_H + +#include "../SensorFactory.h" + +#include "WiFiSensorDummy.h" +#include "AccelerometerSensorDummy.h" +#include "GyroscopeSensorDummy.h" +#include "BarometerSensorDummy.h" + +/** + * sensor factory that provides sensors that fire dummy data + */ +class SensorFactoryDummy : public SensorFactory { + +public: + + WiFiSensor& getWiFi() override { + return WiFiSensorDummy::get(); + } + + AccelerometerSensor& getAccelerometer() override { + return AccelerometerSensorDummy::get(); + } + + GyroscopeSensor& getGyroscope() override { + return GyroscopeSensorDummy::get(); + } + + BarometerSensor& getBarometer() override { + return BarometerSensorDummy::get(); + } + +}; + +#endif // SENSORFACTORYDUMMY_H diff --git a/sensors/dummy/WiFiSensorDummy.h b/sensors/dummy/WiFiSensorDummy.h index 5f94310..7e09b8b 100644 --- a/sensors/dummy/WiFiSensorDummy.h +++ b/sensors/dummy/WiFiSensorDummy.h @@ -54,6 +54,12 @@ private: aps.push_back(DummyAP("00:00:00:00:00:01", Point2(0, 0))); aps.push_back(DummyAP("00:00:00:00:00:02", Point2(20, 0))); aps.push_back(DummyAP("00:00:00:00:00:03", Point2(10, 20))); + aps.push_back(DummyAP("00:00:00:00:00:04", Point2(10, 30))); + aps.push_back(DummyAP("00:00:00:00:00:05", Point2(10, 40))); + aps.push_back(DummyAP("00:00:00:00:00:06", Point2(10, 50))); + aps.push_back(DummyAP("00:00:00:00:00:07", Point2(10, 60))); + aps.push_back(DummyAP("00:00:00:00:00:08", Point2(10, 70))); + aps.push_back(DummyAP("00:00:00:00:00:09", Point2(10, 80))); float deg = 0; @@ -73,11 +79,11 @@ private: const float y = cy + std::cos(deg) * rad; // construct scan data - WiFiSensorData scan; + WiFiMeasurements scan; for (DummyAP& ap : aps) { const float dist = ap.pos.getDistance(Point2(x, y)); const float rssi = LogDistanceModel::distanceToRssi(-40, 1.5, dist); - scan.entries.push_back(WiFiSensorDataEntry(ap.mac, rssi)); + scan.entries.push_back(WiFiMeasurement(AccessPoint(ap.mac), rssi)); } // call diff --git a/sensors/linux/WiFiSensorLinux.h b/sensors/linux/WiFiSensorLinux.h index 17f4b56..5b1d8b1 100644 --- a/sensors/linux/WiFiSensorLinux.h +++ b/sensors/linux/WiFiSensorLinux.h @@ -1,15 +1,38 @@ #ifndef WIFISENSORLINUX_H #define WIFISENSORLINUX_H +#ifdef LINUX_DESKTOP + #include "../WiFiSensor.h" +#include + +extern "C" { + #include "WiFiSensorLinuxC.h" +} class WiFiSensorLinux : public WiFiSensor { +private: + + std::vector freqs = {2412, 2417, 2422, 2427, 2432, 2437, 2442, 2447, 2452, 2457, 2462, 2467, 2472}; + bool running; + std::thread thread; + + int ifIdx; + wifiState state; private: WiFiSensorLinux() { + ifIdx = wifiGetInterfaceIndex("wlp3s0"); + if (ifIdx == 0) {throw Exception("wifi interface not found!");} + + int ret = wifiGetDriver(&state); + if (ret != 0) {throw Exception("wifi driver not found!");} + + std::cout << "if: " << ifIdx << std::endl; + } public: @@ -21,13 +44,59 @@ public: } void start() override { - + running = true; + thread = std::thread(&WiFiSensorLinux::run, this); } void stop() override { + running = false; + thread.join(); + } + +private: + + void run() { + + wifiScanResult result; + wifiChannels channels; + channels.frequencies = freqs.data(); + channels.numUsed = freqs.size(); + + + + while(running) { + + int ret; + + // trigger a scan + ret = wifiTriggerScan(&state, ifIdx, &channels); + if (ret != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + // fetch scan result (blocks) + ret = wifiGetScanResult(&state, ifIdx, &result); + + // convert to our format + const Timestamp ts = Timestamp::fromUnixTime(); + WiFiMeasurements data; + + for (int i = 0; i < result.numUsed; ++i) { + const std::string mac = result.entries[i].mac; + const int rssi = result.entries[i].rssi; + data.entries.push_back(WiFiMeasurement(AccessPoint(mac), rssi)); + } + + // and call the listeners + informListeners(ts, data); + + } } }; +#endif + #endif // WIFISENSORLINUX_H diff --git a/sensors/linux/WiFiSensorLinuxC.c b/sensors/linux/WiFiSensorLinuxC.c new file mode 100644 index 0000000..83abf8e --- /dev/null +++ b/sensors/linux/WiFiSensorLinuxC.c @@ -0,0 +1,376 @@ +#ifdef LINUX_DESKTOP + +#include "WiFiSensorLinuxC.h" + +struct trigger_results { + int done; + int aborted; +}; + + +struct handler_args { // For family_handler() and nl_get_multicast_id(). + const char *group; + int id; +}; + + +static int error_handler(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg) { + // Callback for errors. + printf("error_handler() called.\n"); + int *ret = (int*)arg; + *ret = err->error; + return NL_STOP; +} + + +static int finish_handler(struct nl_msg *msg, void *arg) { + // Callback for NL_CB_FINISH. + int *ret = (int*)arg; + *ret = 0; + return NL_SKIP; +} + + +static int ack_handler(struct nl_msg *msg, void *arg) { + // Callback for NL_CB_ACK. + int *ret = (int*)arg; + *ret = 0; + return NL_STOP; +} + + +static int no_seq_check(struct nl_msg *msg, void *arg) { + // Callback for NL_CB_SEQ_CHECK. + return NL_OK; +} + + +static int family_handler(struct nl_msg *msg, void *arg) { + // Callback for NL_CB_VALID within nl_get_multicast_id(). From http://sourcecodebrowser.com/iw/0.9.14/genl_8c.html. + struct handler_args *grp = arg; + struct nlattr *tb[CTRL_ATTR_MAX + 1]; + struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); + struct nlattr *mcgrp; + int rem_mcgrp; + + nla_parse(tb, CTRL_ATTR_MAX, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), NULL); + + if (!tb[CTRL_ATTR_MCAST_GROUPS]) return NL_SKIP; + + nla_for_each_nested(mcgrp, tb[CTRL_ATTR_MCAST_GROUPS], rem_mcgrp) { // This is a loop. + struct nlattr *tb_mcgrp[CTRL_ATTR_MCAST_GRP_MAX + 1]; + + nla_parse(tb_mcgrp, CTRL_ATTR_MCAST_GRP_MAX, nla_data(mcgrp), nla_len(mcgrp), NULL); + + if (!tb_mcgrp[CTRL_ATTR_MCAST_GRP_NAME] || !tb_mcgrp[CTRL_ATTR_MCAST_GRP_ID]) continue; + if (strncmp((const char*)nla_data(tb_mcgrp[CTRL_ATTR_MCAST_GRP_NAME]), grp->group, + nla_len(tb_mcgrp[CTRL_ATTR_MCAST_GRP_NAME]))) { + continue; + } + + grp->id = nla_get_u32(tb_mcgrp[CTRL_ATTR_MCAST_GRP_ID]); + break; + } + + return NL_SKIP; +} + + +int nl_get_multicast_id(struct nl_sock *sock, const char *family, const char *group) { + // From http://sourcecodebrowser.com/iw/0.9.14/genl_8c.html. + struct nl_msg *msg; + struct nl_cb *cb; + int ret, ctrlid; + struct handler_args grp = { .group = group, .id = -ENOENT, }; + + msg = nlmsg_alloc(); + if (!msg) return -ENOMEM; + + cb = nl_cb_alloc(NL_CB_DEFAULT); + if (!cb) { + ret = -ENOMEM; + goto out_fail_cb; + } + + ctrlid = genl_ctrl_resolve(sock, "nlctrl"); + + genlmsg_put(msg, 0, 0, ctrlid, 0, 0, CTRL_CMD_GETFAMILY, 0); + + ret = -ENOBUFS; + NLA_PUT_STRING(msg, CTRL_ATTR_FAMILY_NAME, family); + + ret = nl_send_auto_complete(sock, msg); + if (ret < 0) goto out; + + ret = 1; + + nl_cb_err(cb, NL_CB_CUSTOM, error_handler, &ret); + nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, ack_handler, &ret); + nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, family_handler, &grp); + + while (ret > 0) nl_recvmsgs(sock, cb); + + if (ret == 0) ret = grp.id; + + nla_put_failure: + out: + nl_cb_put(cb); + out_fail_cb: + nlmsg_free(msg); + return ret; +} + + +void mac_addr_n2a(char *mac_addr, unsigned char *arg) { + // From http://git.kernel.org/cgit/linux/kernel/git/jberg/iw.git/tree/util.c. + int i, l; + + l = 0; + for (i = 0; i < 6; i++) { + if (i == 0) { + sprintf(mac_addr+l, "%02x", arg[i]); + l += 2; + } else { + sprintf(mac_addr+l, ":%02x", arg[i]); + l += 3; + } + } +} + + +void print_ssid(unsigned char *ie, int ielen) { + uint8_t len; + uint8_t *data; + int i; + + while (ielen >= 2 && ielen >= ie[1]) { + if (ie[0] == 0 && ie[1] >= 0 && ie[1] <= 32) { + len = ie[1]; + data = ie + 2; + for (i = 0; i < len; i++) { + if (isprint(data[i]) && data[i] != ' ' && data[i] != '\\') printf("%c", data[i]); + else if (data[i] == ' ' && (i != 0 && i != len -1)) printf(" "); + else printf("\\x%.2x", data[i]); + } + break; + } + ielen -= ie[1] + 2; + ie += ie[1] + 2; + } +} + + +static int callback_trigger(struct nl_msg *msg, void *arg) { + // Called by the kernel when the scan is done or has been aborted. + struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); + struct trigger_results *results = arg; + + //printf("Got something.\n"); + //printf("%d\n", arg); + //nl_msg_dump(msg, stdout); + + if (gnlh->cmd == NL80211_CMD_SCAN_ABORTED) { + printf("Got NL80211_CMD_SCAN_ABORTED.\n"); + results->done = 1; + results->aborted = 1; + } else if (gnlh->cmd == NL80211_CMD_NEW_SCAN_RESULTS) { + printf("Got NL80211_CMD_NEW_SCAN_RESULTS.\n"); + results->done = 1; + results->aborted = 0; + } // else probably an uninteresting multicast message. + + return NL_SKIP; +} + +// called by the kernel for each detected AP +static int callback_dump(struct nl_msg *msg, void *arg) { + + // user data + struct wifiScanResult* res = arg; + struct wifiScanResultEntry* entry = &(res->entries[res->numUsed]); + ++res->numUsed; + + struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); + char mac_addr[20]; + struct nlattr *tb[NL80211_ATTR_MAX + 1]; + struct nlattr *bss[NL80211_BSS_MAX + 1]; + static struct nla_policy bss_policy[NL80211_BSS_MAX + 1] = { + [NL80211_BSS_TSF] = { .type = NLA_U64 }, + [NL80211_BSS_FREQUENCY] = { .type = NLA_U32 }, + [NL80211_BSS_BSSID] = { }, + [NL80211_BSS_BEACON_INTERVAL] = { .type = NLA_U16 }, + [NL80211_BSS_CAPABILITY] = { .type = NLA_U16 }, + [NL80211_BSS_INFORMATION_ELEMENTS] = { }, + [NL80211_BSS_SIGNAL_MBM] = { .type = NLA_U32 }, + [NL80211_BSS_SIGNAL_UNSPEC] = { .type = NLA_U8 }, + [NL80211_BSS_STATUS] = { .type = NLA_U32 }, + [NL80211_BSS_SEEN_MS_AGO] = { .type = NLA_U32 }, + [NL80211_BSS_BEACON_IES] = { }, + }; + + // Parse and error check. + nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), NULL); + if (!tb[NL80211_ATTR_BSS]) { + printf("bss info missing!\n"); + return NL_SKIP; + } + if (nla_parse_nested(bss, NL80211_BSS_MAX, tb[NL80211_ATTR_BSS], bss_policy)) { + printf("failed to parse nested attributes!\n"); + return NL_SKIP; + } + if (!bss[NL80211_BSS_BSSID]) return NL_SKIP; + if (!bss[NL80211_BSS_INFORMATION_ELEMENTS]) return NL_SKIP; + + // signal-strength + int mBm = nla_get_s32(bss[NL80211_BSS_SIGNAL_MBM]); + entry->rssi = mBm / 100; + + // mac-address + mac_addr_n2a(entry->mac, (unsigned char*)nla_data(bss[NL80211_BSS_BSSID])); + + // Start printing. +// mac_addr_n2a(mac_addr, (unsigned char*)nla_data(bss[NL80211_BSS_BSSID])); +// printf("%s, ", mac_addr); +// printf("%d MHz, ", nla_get_u32(bss[NL80211_BSS_FREQUENCY])); +// print_ssid((unsigned char*)nla_data(bss[NL80211_BSS_INFORMATION_ELEMENTS]), nla_len(bss[NL80211_BSS_INFORMATION_ELEMENTS])); +// printf("%f dB", mBm/100.0f); +// printf("\n"); + + return NL_SKIP; +} + + + +int wifiGetDriver(struct wifiState* state) { + + // open nl80211 socket to kernel. + state->socket = nl_socket_alloc(); + genl_connect(state->socket); + + state->mcid = nl_get_multicast_id(state->socket, "nl80211", "scan"); + nl_socket_add_membership(state->socket, state->mcid); // Without this, callback_trigger() won't be called. + + state->driverID = genl_ctrl_resolve(state->socket, "nl80211"); + + return 0; + +} + +int wifiCleanup(struct wifiState* state) { + + nl_socket_drop_membership(state->socket, state->mcid); // No longer need this. + nl_socket_free(state->socket); + +} + +int wifiGetScanResult(struct wifiState* state, int interfaceIndex, struct wifiScanResult* res) { + + // reset the result. very important! + res->numUsed = 0; + + // new message + struct nl_msg *msg = nlmsg_alloc(); + + // configure message + genlmsg_put(msg, 0, 0, state->driverID, 0, NLM_F_DUMP, NL80211_CMD_GET_SCAN, 0); + nla_put_u32(msg, NL80211_ATTR_IFINDEX, interfaceIndex); + + // add the to-be-called function for each detected AP + nl_socket_modify_cb(state->socket, NL_CB_VALID, NL_CB_CUSTOM, callback_dump, res); + + // send + int ret = nl_send_auto(state->socket, msg); + printf("NL80211_CMD_GET_SCAN sent %d bytes to the kernel.\n", ret); + + // get answer (potential error-code) + ret = nl_recvmsgs_default(state->socket); + nlmsg_free(msg); + if (ret < 0) { printf("ERROR: nl_recvmsgs_default() returned %d (%s).\n", ret, nl_geterror(-ret)); return ret; } + +} + +int wifiTriggerScan(struct wifiState* state, int interfaceIndex, struct wifiChannels* channels) { + + // Starts the scan and waits for it to finish. Does not return until the scan is done or has been aborted. + struct trigger_results results = { .done = 0, .aborted = 0 }; + struct nl_msg *msg; + struct nl_cb *cb; + struct nl_msg *freqs_to_scan; + int err; + int ret; + + // Allocate the messages and callback handler. + msg = nlmsg_alloc(); + if (!msg) { + printf("ERROR: Failed to allocate netlink message for msg.\n"); + return -ENOMEM; + } + freqs_to_scan = nlmsg_alloc(); + if (!freqs_to_scan) { + printf("ERROR: Failed to allocate netlink message for ssids_to_scan.\n"); + nlmsg_free(msg); + return -ENOMEM; + } + cb = nl_cb_alloc(NL_CB_DEFAULT); + if (!cb) { + printf("ERROR: Failed to allocate netlink callbacks.\n"); + nlmsg_free(msg); + nlmsg_free(freqs_to_scan); + return -ENOMEM; + } + + // Setup the messages and callback handler. + genlmsg_put(msg, 0, 0, state->driverID, 0, 0, NL80211_CMD_TRIGGER_SCAN, 0); // Setup which command to run. + nla_put_u32(msg, NL80211_ATTR_IFINDEX, interfaceIndex); // Add message attribute, which interface to use. + + + // limit to-be-scanned channels? + if (channels->numUsed > 0) { + for (int i = 0; i < channels->numUsed; ++i) { nla_put_u32(freqs_to_scan, 0, channels->frequencies[i]); } + nla_put_nested(msg, NL80211_ATTR_SCAN_FREQUENCIES, freqs_to_scan); + } + nlmsg_free(freqs_to_scan); + + + + nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, callback_trigger, &results); // Add the callback. + nl_cb_err(cb, NL_CB_CUSTOM, error_handler, &err); + nl_cb_set(cb, NL_CB_FINISH, NL_CB_CUSTOM, finish_handler, &err); + nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, ack_handler, &err); + nl_cb_set(cb, NL_CB_SEQ_CHECK, NL_CB_CUSTOM, no_seq_check, NULL); // No sequence checking for multicast messages. + + // Send NL80211_CMD_TRIGGER_SCAN to start the scan. The kernel may reply with NL80211_CMD_NEW_SCAN_RESULTS on + // success or NL80211_CMD_SCAN_ABORTED if another scan was started by another process. + err = 1; + ret = nl_send_auto(state->socket, msg); // Send the message. + printf("NL80211_CMD_TRIGGER_SCAN sent %d bytes to the kernel.\n", ret); + printf("Waiting for scan to complete...\n"); + while (err > 0) ret = nl_recvmsgs(state->socket, cb); // First wait for ack_handler(). This helps with basic errors. + if (err < 0) { + printf("WARNING: err has a value of %d.\n", err); + } + if (ret < 0) { + printf("ERROR: nl_recvmsgs() returned %d (%s).\n", ret, nl_geterror(-ret)); + return ret; + } + while (!results.done) nl_recvmsgs(state->socket, cb); // Now wait until the scan is done or aborted. + if (results.aborted) { + printf("ERROR: Kernel aborted scan.\n"); + return 1; + } + printf("Scan is done.\n"); + + // Cleanup. + nlmsg_free(msg); + nl_cb_put(cb); + + return 0; + +} + +int wifiGetInterfaceIndex(const char *name) { + return if_nametoindex(name); +} + + +#endif diff --git a/sensors/linux/WiFiSensorLinuxC.h b/sensors/linux/WiFiSensorLinuxC.h new file mode 100644 index 0000000..c21d3d2 --- /dev/null +++ b/sensors/linux/WiFiSensorLinuxC.h @@ -0,0 +1,49 @@ +#ifndef WIFISENSORLINUXC_H +#define WIFISENSORLINUXC_H + +#ifdef LINUX_DESKTOP + +#include +#include +#include +#include +#include +#include +#include + +struct wifiChannels { + uint32_t* frequencies; // array of frequencies + uint32_t numUsed; // number of array elements +}; + +struct wifiScanResultEntry { + char mac[17]; + int rssi; +}; + +struct wifiScanResult { + struct wifiScanResultEntry entries[128]; + int numUsed; +}; + +struct wifiState { + struct nl_sock* socket; + int driverID; + int mcid; +}; + +/** get the driver used for scanning */ +int wifiGetDriver(struct wifiState* state); + +/** convert interface name to index number. 0 if interface is not present */ +int wifiGetInterfaceIndex(const char* name); + +/** trigger a scan on the given channels / the provided interface */ +int wifiTriggerScan(struct wifiState* state, int interfaceIndex, struct wifiChannels* channels); + +/** blocking get the result of a triggered scan */ +int wifiGetScanResult(struct wifiState* state, int interfaceIndex, struct wifiScanResult* res); + +#ifdef LINUX_DESKTOP + +#endif // WIFISENSORLINUXC_H diff --git a/sensors/offline/AllInOneSensor.h b/sensors/offline/AllInOneSensor.h new file mode 100644 index 0000000..542bfd6 --- /dev/null +++ b/sensors/offline/AllInOneSensor.h @@ -0,0 +1,110 @@ +#ifndef ALLINONESENSOR_H +#define ALLINONESENSOR_H + +#include "Settings.h" +#include +#include "../WiFiSensor.h" +#include "../AccelerometerSensor.h" +#include "../GyroscopeSensor.h" +#include "../BarometerSensor.h" +#include + +class AllInOneSensor : + public WiFiSensor, public AccelerometerSensor, public GyroscopeSensor, public BarometerSensor, + public OfflineAndroidListener { + +private: + + std::string file; + bool running = false; + std::thread thread; + +public: + + AllInOneSensor(const std::string& file) : file(file) { + ; + } + + void start() { + if (running) {return;} + running = true; + thread = std::thread(&AllInOneSensor::run, this); + } + + void stop() { + if (!running) {return;} + running = false; + thread.join(); + } + +protected: + + virtual void onGyroscope(const Timestamp _ts, const GyroscopeData data) override { + const Timestamp ts = relativeTS(_ts); + handbrake(ts); + GyroscopeSensor::informListeners(ts, data); + } + + virtual void onAccelerometer(const Timestamp _ts, const AccelerometerData data) override { + const Timestamp ts = relativeTS(_ts); + handbrake(ts); + AccelerometerSensor::informListeners(ts, data); + } + + virtual void onGravity(const Timestamp ts, const AccelerometerData data) override { + (void) ts; + (void) data; + } + + virtual void onWiFi(const Timestamp _ts, const WiFiMeasurements data) override { + const Timestamp ts = relativeTS(_ts); + handbrake(ts); + WiFiMeasurements copy = data; + for (WiFiMeasurement& m : copy.entries) {m.ts = ts;} // make each timestmap also relative + WiFiSensor::informListeners(ts, copy); + } + + virtual void onBarometer(const Timestamp _ts, const BarometerData data) override { + const Timestamp ts = relativeTS(_ts); + handbrake(ts); + BarometerSensor::informListeners(ts, data); + } + +private: + + Timestamp baseTS; + Timestamp relativeTS(const Timestamp ts) { + if (baseTS.isZero()) {baseTS = ts;} + return ts - baseTS; + } + + /** handbrake for the offline-parser to ensure realtime events */ + Timestamp startSensorTS; + Timestamp startSystemTS; + + void handbrake(const Timestamp ts) { + if (startSensorTS.isZero()) {startSensorTS = ts;} + if (startSystemTS.isZero()) {startSystemTS = Timestamp::fromUnixTime();} + const Timestamp runtimeOfflineData = ts - startSensorTS;; + const Timestamp runtimeSystemTime = (Timestamp::fromUnixTime() - startSystemTS) * Settings::offlineSensorSpeedup; + const Timestamp diff = (runtimeOfflineData - runtimeSystemTime); + if (diff > Timestamp::fromMS(0)) { + std::this_thread::sleep_for(std::chrono::milliseconds(diff.ms())); + } + } + +private: + + /** + * file-parsing runs in a background thread + * the parsing is manually slowed down via handbrake() + * which blocks during the event callbacks + */ + void run() { + OfflineAndroid parser; + parser.parse(file, this); + } + +}; + +#endif // ALLINONESENSOR_H diff --git a/sensors/offline/SensorFactoryOffline.h b/sensors/offline/SensorFactoryOffline.h new file mode 100644 index 0000000..bbde418 --- /dev/null +++ b/sensors/offline/SensorFactoryOffline.h @@ -0,0 +1,43 @@ +#ifndef SENSORFACTORYOFFLINE_H +#define SENSORFACTORYOFFLINE_H + +#include + +#include "../SensorFactory.h" +#include "AllInOneSensor.h" +#include + +/** + * factory class that provides sensors that fire events from offline data + */ +class SensorFactoryOffline : public SensorFactory { + +private: + + AllInOneSensor allInOne; + +public: + + SensorFactoryOffline(const std::string& file) : allInOne(file) { + ; + } + + WiFiSensor& getWiFi() override { + return allInOne; + } + + AccelerometerSensor& getAccelerometer() override { + return allInOne; + } + + GyroscopeSensor& getGyroscope() override { + return allInOne; + } + + BarometerSensor& getBarometer() override { + return allInOne; + } + +}; + +#endif // SENSORFACTORYOFFLINE_H diff --git a/ui/Icons.h b/ui/Icons.h new file mode 100644 index 0000000..3da7e46 --- /dev/null +++ b/ui/Icons.h @@ -0,0 +1,90 @@ +#ifndef ICONS_H +#define ICONS_H + +#include "../misc/fixc11.h" + +#include +#include +#include +#include + +#include + +#include + +class Icons { + +public: + + + static const QPixmap& getPixmap(const std::string& name, const int size = 32) { + + // caching + static std::unordered_map cache; + + // try to get the image from the cache + const std::string cacheKey = std::to_string(size) + name; + auto it = cache.find(cacheKey); + + // not in cache? + if (it == cache.end()) { + + // build + const QColor fill = Qt::transparent; + const std::string file = "://res/icons/" + name + ".svg"; + QSvgRenderer renderer(QString(file.c_str())); + QPixmap pm(size, size); + pm.fill(fill); + QPainter painter(&pm); + renderer.render(&painter, pm.rect()); + + // add to cache + cache[cacheKey] = pm; + + } + + // done + return cache[cacheKey]; + + } + + static const QPixmap& getPixmapColored(const std::string& name, const QColor color, const int size = 32) { + + // caching + static std::unordered_map cache; + + // try to get the image from the cache + const QString hex = color.name(); + const std::string cacheKey = hex.toStdString() + "_" + std::to_string(size) + "_" + name; + auto it = cache.find(cacheKey); + + // not in cache? + if (it == cache.end()) { + + // copy + QPixmap colored = getPixmap(name, size); + QPainter painter(&colored); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(colored.rect(), color); + painter.end(); + + // add to cache + cache[cacheKey] = colored; + + } + + // done + return cache[cacheKey]; + + } + + static QIcon getIcon(const std::string& name, const int size = 32) { + + return QIcon(getPixmap(name, size)); + + } + +}; + + +#endif // ICONS_H diff --git a/ui/MainWindow.cpp b/ui/MainWindow.cpp new file mode 100644 index 0000000..2cf887f --- /dev/null +++ b/ui/MainWindow.cpp @@ -0,0 +1,35 @@ +#include "MainWindow.h" + +#include + +#include "map/MapView.h" +#include "menu/MainMenu.h" +#include "debug/SensorDataWidget.h" + + +MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { + + setMinimumHeight(500); + setMinimumWidth(500); + + mapView = new MapView(this); + mainMenu = new MainMenu(this); + sensorWidget = new SensorDataWidget(this); + + //sensorWidget->setVisible(false); + showMaximized(); + +} + +void MainWindow::resizeEvent(QResizeEvent* event) { + + const int w = event->size().width(); + const int h = event->size().height(); + + mapView->setGeometry(0,0,w,h); + mainMenu->setGeometry(0,0,w,64); + sensorWidget->setGeometry(0,64,w,h-64); + +} + + diff --git a/ui/MainWindow.h b/ui/MainWindow.h new file mode 100644 index 0000000..d816bc1 --- /dev/null +++ b/ui/MainWindow.h @@ -0,0 +1,43 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +class MapView; +class MainMenu; +class SensorDataWidget; + +class MainWindow : public QWidget { + Q_OBJECT + +public: + + /** ctor */ + explicit MainWindow(QWidget *parent = 0); + +private: + + MapView* mapView = nullptr; + MainMenu* mainMenu = nullptr; + SensorDataWidget* sensorWidget = nullptr; + +public: + + MapView* getMapView() const {return mapView;} + MainMenu* getMainMenu() const {return mainMenu;} + SensorDataWidget* getSensorDataWidget() const {return sensorWidget;} + + +// void setMapView(QWidget* widget) {mapView = widget; mapView->setParent(this);} +// void setMainMenu(QWidget* widget) {mainMenu = widget; mainMenu->setParent(this);} +// void setSensorWidget(QWidget* widget) {sensorWidget = widget; sensorWidget->setParent(this);} + +signals: + +public slots: + + void resizeEvent(QResizeEvent* event); + +}; + +#endif // MAINWINDOW_H diff --git a/ui/debug/PlotTurns.cpp b/ui/debug/PlotTurns.cpp new file mode 100644 index 0000000..0f69dbd --- /dev/null +++ b/ui/debug/PlotTurns.cpp @@ -0,0 +1,49 @@ +#include "PlotTurns.h" +#include + +PlotTurns::PlotTurns(QWidget *parent) : QWidget(parent) { + + setMinimumWidth(96); + setMinimumHeight(96); + + resize(96, 96); + +// setMaximumWidth(64); +// setMaximumHeight(64); + +} + +void PlotTurns::add(const Timestamp ts, const TurnData& data) { + (void) ts; + this->data = data; + static int i = 0; + if (++i % 4 == 0) { + QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); + } +} + +void PlotTurns::paintEvent(QPaintEvent* evt) { + + (void) evt; + QPainter p(this); + + const float s = std::min(width(), height()); + const float s1 = s / 1.9; + + const float cx = width() / 2; + const float cy = height() / 2; + + const float x1 = cx + std::cos(data.radSinceStart-M_PI_2) * s1; + const float y1 = cy + std::sin(data.radSinceStart-M_PI_2) * s1; + + p.fillRect(0,0,width(),height(),QColor(255,255,255,192)); + p.setPen(Qt::black); + p.drawRect(0,0,width()-1,height()-1); + + const QPen pen(Qt::black, 2); + p.setPen(pen); + p.drawLine(cx, cy, x1, y1); + + p.end(); + +} diff --git a/ui/debug/PlotTurns.h b/ui/debug/PlotTurns.h new file mode 100644 index 0000000..d84fd99 --- /dev/null +++ b/ui/debug/PlotTurns.h @@ -0,0 +1,30 @@ +#ifndef PLOTTURNS_H +#define PLOTTURNS_H + +#include +#include "../sensors/TurnSensor.h" +#include + +class PlotTurns : public QWidget { + + Q_OBJECT + +private: + + TurnData data; + +public: + + explicit PlotTurns(QWidget *parent = 0); + + void add(const Timestamp ts, const TurnData& data); + +signals: + +public slots: + + void paintEvent(QPaintEvent*); + +}; + +#endif // PLOTTURNS_H diff --git a/ui/debug/PlotWiFiScan.cpp b/ui/debug/PlotWiFiScan.cpp new file mode 100644 index 0000000..1c6462c --- /dev/null +++ b/ui/debug/PlotWiFiScan.cpp @@ -0,0 +1,53 @@ +#include "../misc/fixc11.h" +#include "PlotWiFiScan.h" + +#include +#include + +PlotWiFiScan::PlotWiFiScan(QWidget *parent) : QWidget(parent) { + + setMinimumWidth(96); + setMinimumHeight(96); + + //setAutoFillBackground(false); + +} + +void PlotWiFiScan::add(const Timestamp ts, const WiFiMeasurements& data) { + (void) ts; + this->data = data; + QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); +} + +void PlotWiFiScan::paintEvent(QPaintEvent* evt) { + + (void) evt; + QPainter p(this); + + const int x0 = 4; const int xw = 150; + const int y0 = 3; + const int lh = 13; + + int x = x0; + int y = y0; + + + p.fillRect(0,0,width(),height(),QColor(255,255,255,192)); + p.setPen(Qt::black); + p.drawRect(0,0,width()-1,height()-1); + + const QFont font("Arial", 9); + p.setFont(font); + p.setPen(Qt::black); + + for (const WiFiMeasurement& m : data.entries) { + const std::string& mac = m.getAP().getMAC().asString(); + std::string str = mac + ": " + std::to_string((int)m.getRSSI()); + p.drawStaticText(x, y, QStaticText(str.c_str())); + y += lh; + if (y > 90) {y = y0; x += xw;} + } + + p.end(); + +} diff --git a/ui/debug/PlotWiFiScan.h b/ui/debug/PlotWiFiScan.h new file mode 100644 index 0000000..cb3a2af --- /dev/null +++ b/ui/debug/PlotWiFiScan.h @@ -0,0 +1,30 @@ +#ifndef PLOTWIFISCAN_H +#define PLOTWIFISCAN_H + +#include +#include "../sensors/WiFiSensor.h" + +class PlotWiFiScan : public QWidget { + + Q_OBJECT + +private: + + WiFiMeasurements data; + +public: + + /** ctor */ + explicit PlotWiFiScan(QWidget *parent = 0); + + void add(const Timestamp ts, const WiFiMeasurements& data); + +signals: + +public slots: + + void paintEvent(QPaintEvent*); + +}; + +#endif // PLOTWIFISCAN_H diff --git a/ui/debug/SensorDataWidget.cpp b/ui/debug/SensorDataWidget.cpp new file mode 100644 index 0000000..43dd041 --- /dev/null +++ b/ui/debug/SensorDataWidget.cpp @@ -0,0 +1,217 @@ +#include "../misc/fixc11.h" +#include "SensorDataWidget.h" + +#include "plot/PlottWidget.h" +#include +#include + +#include "../sensors/SensorFactory.h" +#include "PlotTurns.h" +#include "PlotWiFiScan.h" + + +template void removeOld(Data& data, const Timestamp limit) { + if (data.size() < 2) {return;} + while ( (data.back().key - data.front().key) > limit.ms()) { + data.remove(0); + } +} + +template class PlotXLines : public PlotWidget { + +protected: + + QColor colors[4] = {QColor(255,0,0), QColor(0,192,0), QColor(0,0,255), QColor(0,0,0)}; + LinePlot line[num]; + +public: + + PlotXLines(QWidget* parent) : PlotWidget(parent) { + for (int i = 0; i < num; ++i) { + pc.addPlot(&line[i]); + line[i].setColor(colors[i]); + } + } + + void addLineNode(const Timestamp ts, const float y, const int idx) { + LinePlot& lp = line[idx]; + lp.getData().add(ts.ms(), y); + } + + Timestamp lastRefresh; + bool needsRefresh(const Timestamp ts) { + const Timestamp diff = ts - lastRefresh; + return (diff > Timestamp::fromMS(100)); + } + + + void refresh(const Timestamp ts) { + + // ensure event from main-thread using queued-connection + QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); + + lastRefresh = ts; + + } + +}; + +class PlotAcc : public PlotXLines<3> { + +protected: + + PointPlot steps; + +public: + + PlotAcc(QWidget* parent) : PlotXLines(parent) { + steps.setColor(colors[2]); + steps.setPointSize(8); + pc.addPlot(&steps); + const float s = 4.2; + const float ref = 9.81; + pc.setValRange(Range(ref-s, ref+s)); + } + + void addStep(const Timestamp ts) { + steps.getData().add(ts.ms(), 9.81); + } + + void add(const Timestamp ts, const AccelerometerData& data) { + addLineNode(ts, data.x, 0); + addLineNode(ts, data.y, 1); + addLineNode(ts, data.z, 2); + if (needsRefresh(ts)) { + limit(); + refresh(ts); + } + } + + void limit() { + const Timestamp limit = Timestamp::fromMS(3000); + removeOld(line[0].getData(), limit); + removeOld(line[1].getData(), limit); + removeOld(line[2].getData(), limit); + removeOld(steps.getData(), limit - Timestamp::fromMS(100)); // remove steps a little before. prevents errors + } + +}; + +class PlotGyro : public PlotXLines<3> { + +public: + + PlotGyro(QWidget* parent) : PlotXLines(parent) { + const float s = 1; + const float ref = 0; + pc.setValRange(Range(ref-s, ref+s)); + } + + void add(const Timestamp ts, const GyroscopeData& data) { + addLineNode(ts, data.x, 0); + addLineNode(ts, data.y, 1); + addLineNode(ts, data.z, 2); + if (needsRefresh(ts)) { + limit(); + refresh(ts); + } + } + + void limit() { + const Timestamp limit = Timestamp::fromMS(3000); + removeOld(line[0].getData(), limit); + removeOld(line[1].getData(), limit); + removeOld(line[2].getData(), limit); + } + +}; + +class PlotBaro : public PlotXLines<1> { + +public: + + PlotBaro(QWidget* parent) : PlotXLines(parent) { + + } + + void add(const Timestamp ts, const BarometerData& data) { + addLineNode(ts, data.hPa, 0); + if (needsRefresh(ts)) { + limit(); + refresh(ts); + } + const float s = 0.5; + const float ref = line[0].getData().front().val; + pc.setValRange(Range(ref-s, ref+s)); + } + + void limit() { + removeOld(line[0].getData(), Timestamp::fromMS(8000)); + } + +}; + +class PlotTurn : public QWidget { + +}; + + + +SensorDataWidget::SensorDataWidget(QWidget* parent) : QWidget(parent) { + + QGridLayout* lay = new QGridLayout(this); + + plotGyro = new PlotGyro(this); + plotAcc = new PlotAcc(this); + plotBaro = new PlotBaro(this); + plotTurn = new PlotTurns(this); + plotWiFi = new PlotWiFiScan(this); + + lay->addWidget(plotGyro, 0, 0, 1, 4, Qt::AlignTop); + lay->addWidget(plotAcc, 1, 0, 1, 4, Qt::AlignTop); + lay->addWidget(plotBaro, 2, 0, 1, 4, Qt::AlignTop); + lay->addWidget(plotTurn, 3, 0, 1, 1, Qt::AlignTop); + lay->addWidget(plotWiFi, 3, 1, 1, 3, Qt::AlignTop); + + SensorFactory::get().getAccelerometer().addListener(this); + SensorFactory::get().getGyroscope().addListener(this); + SensorFactory::get().getBarometer().addListener(this); + SensorFactory::get().getSteps().addListener(this); + SensorFactory::get().getTurns().addListener(this); + SensorFactory::get().getWiFi().addListener(this); + + //setAutoFillBackground(false); + +} + +void SensorDataWidget::onSensorData(Sensor* sensor, const Timestamp ts, const AccelerometerData& data) { + (void) sensor; + ((PlotAcc*)plotAcc)->add(ts, data); +} + +void SensorDataWidget::onSensorData(Sensor* sensor, const Timestamp ts, const StepData& data) { + (void) sensor; + (void) data; + ((PlotAcc*)plotAcc)->addStep(ts); +} + +void SensorDataWidget::onSensorData(Sensor* sensor, const Timestamp ts, const GyroscopeData& data) { + (void) sensor; + ((PlotGyro*)plotGyro)->add(ts, data); +} + +void SensorDataWidget::onSensorData(Sensor* sensor, const Timestamp ts, const BarometerData& data) { + (void) sensor; + ((PlotBaro*)plotBaro)->add(ts, data); +} + +void SensorDataWidget::onSensorData(Sensor* sensor, const Timestamp ts, const TurnData& data) { + (void) sensor; + ((PlotTurns*)plotTurn)->add(ts, data); +} + +void SensorDataWidget::onSensorData(Sensor* sensor, const Timestamp ts, const WiFiMeasurements& data) { + (void) sensor; + ((PlotWiFiScan*)plotWiFi)->add(ts, data); +} + diff --git a/ui/debug/SensorDataWidget.h b/ui/debug/SensorDataWidget.h new file mode 100644 index 0000000..5c91942 --- /dev/null +++ b/ui/debug/SensorDataWidget.h @@ -0,0 +1,53 @@ +#ifndef SENSORDATAWIDGET_H +#define SENSORDATAWIDGET_H + +#include "../misc/fixc11.h" +#include "plot/PlottWidget.h" + +#include + +#include "../sensors/AccelerometerSensor.h" +#include "../sensors/GyroscopeSensor.h" +#include "../sensors/BarometerSensor.h" +#include "../sensors/StepSensor.h" +#include "../sensors/TurnSensor.h" +#include "../sensors/WiFiSensor.h" + +class PlotWidget; + +/** debug display for sensor data */ +class SensorDataWidget : + public QWidget, + public SensorListener, + public SensorListener, + public SensorListener, + public SensorListener, + public SensorListener, + public SensorListener { + + + Q_OBJECT + +public: + + SensorDataWidget(QWidget* parent); + + void onSensorData(Sensor* sensor, const Timestamp ts, const AccelerometerData& data) override; + void onSensorData(Sensor* sensor, const Timestamp ts, const GyroscopeData& data) override; + void onSensorData(Sensor* sensor, const Timestamp ts, const BarometerData& data) override; + void onSensorData(Sensor* sensor, const Timestamp ts, const StepData& data) override; + void onSensorData(Sensor* sensor, const Timestamp ts, const TurnData& data) override; + void onSensorData(Sensor* sensor, const Timestamp ts, const WiFiMeasurements& data) override; + +private: + + PlotWidget* plotGyro; + PlotWidget* plotAcc; + PlotWidget* plotBaro; + QWidget* plotTurn; + QWidget* plotWiFi; + + +}; + +#endif // SENSORDATAWIDGET_H diff --git a/ui/debug/plot/Axes.h b/ui/debug/plot/Axes.h new file mode 100644 index 0000000..35422d7 --- /dev/null +++ b/ui/debug/plot/Axes.h @@ -0,0 +1,66 @@ +#ifndef AXES_H +#define AXES_H + +#include "Range.h" + +class Axes { + + /** min/max value to display */ + Range range; + + /** number of available pixels for above range */ + int pixels; + + /** whether to invert the axes */ + bool invert = false; + +public: + + void setMin(const float min) {this->range.min = min;} + float getMin() const {return this->range.min;} + + void setMax(const float max) {this->range.max = max;} + float getMax() const {return this->range.max;} + + void setRange(const Range& range) {this->range = range;} + const Range& getRange() const {return this->range;} + + void setPixels(const int px) {this->pixels = px;} + int getPixels() const {return this->pixels;} + + void setInverted(const bool inverted) {this->invert = inverted;} + bool isInverted() const {return this->invert;} + + float convert(const float val) const { + float percent = (val - range.min) / (range.getSize()); + if (invert) {percent = 1-percent;} + return percent * pixels; + } + +}; + +class AxesX : public Axes { + +public: + + AxesX() { + setInverted(false); + } + + void setWidth(const int px) {setPixels(px);} + +}; + +class AxesY : public Axes { + +public: + + AxesY() { + setInverted(true); + } + + void setHeight(const int px) {setPixels(px);} + +}; + +#endif // AXES_H diff --git a/ui/debug/plot/Data.h b/ui/debug/plot/Data.h new file mode 100644 index 0000000..5487374 --- /dev/null +++ b/ui/debug/plot/Data.h @@ -0,0 +1,80 @@ +#ifndef PLOT_DATA_H +#define PLOT_DATA_H + +#include +#include "Range.h" +#include + +class Data { + + using Key = float; + using Value = float; + +public: + + struct KeyVal { + Key key; + Value val; + KeyVal(const Key& key, const Value& val) : key(key), val(val) {;} + }; + +private: + + /** contained data */ + std::vector data; + + + +public: + + /** add a new value */ + void add(const Key key, const Value val) { + data.push_back(KeyVal(key,val)); + } + + /** remove the given index */ + void remove(const int idx) { + data.erase(data.begin()+idx); + } + + Key getKey(const int idx) const { + return data[idx].key; + } + + Value getValue(const int idx) const { + return data[idx].val; + } + + const KeyVal& getKeyValue(const int idx) const { + return data[idx]; + } + + const KeyVal& operator [] (const int idx) const { + return data[idx]; + } + + const KeyVal& front() const {return data.front();} + const KeyVal& back() const {return data.back();} + + /** get the range (min/max) for the key-data (x-axes) */ + Range getKeyRange() const { + Range range(+INFINITY,-INFINITY); + for (const KeyVal& kv : data) {range.adjust(kv.key);} + return range; + } + + /** get the range (min/max) for the value-data (y-axes) */ + Range getValueRange() const { + Range range(+INFINITY,-INFINITY); + for (const KeyVal& kv : data) {range.adjust(kv.val);} + return range; + } + + /** get the number of entries */ + size_t size() const { + return data.size(); + } + +}; + +#endif // PLOT_DATA_H diff --git a/ui/debug/plot/Plot.h b/ui/debug/plot/Plot.h new file mode 100644 index 0000000..54a4a89 --- /dev/null +++ b/ui/debug/plot/Plot.h @@ -0,0 +1,215 @@ +#ifndef PLOT_H +#define PLOT_H + +#include + +#include "Axes.h" +#include "Data.h" + +#include + +/** describes a plot-setup */ +struct PlotParameters { + + AxesX xAxes; + AxesY yAxes; + + int w; + int h; + + /** helper method */ + Point2 getPoint(const typename Data::KeyVal& kv) const { + const float x1 = xAxes.convert(kv.key); + const float y1 = yAxes.convert(kv.val); + return Point2(x1, y1); + } + +}; + +/** interface for all plots */ +class Plot { + +protected: + + Data data; + + QColor bg = QColor(255,255,255,128); + +public: + + virtual ~Plot() {;} + + void render(QPainter& p, const PlotParameters& params) { + // maybe do something here? + renderSub(p, params); + } + + Data& getData() {return data;} + + Range getKeyRange() const {return data.getKeyRange();} + + Range getValueRange() const {return data.getValueRange();} + +protected: + + /** subclasses must render themselves here */ + virtual void renderSub(QPainter& p, const PlotParameters& params) = 0; + +}; + + + +/** combine several plots (several lines, points, ...) together into one plot */ +class PlotContainer { + +private: + + PlotParameters params; + + std::vector plots; + +public: + + /** ctor */ + PlotContainer() { + ; + } + + AxesX& getAxesX() {return params.xAxes;} + AxesY& getAxesY() {return params.yAxes;} + + Range overwriteRangeKey = Range(0,0); + Range overwriteRangeVal = Range(0,0); + + void setValRange(const Range& range) {overwriteRangeVal = range;} + void setKeyRange(const Range& range) {overwriteRangeKey = range;} + + + void resize(const int w, const int h) { + params.w = w; + params.h = h; + } + + void addPlot(Plot* plt) { + plots.push_back(plt); + } + + void render(QPainter& p) { + + setupAxes(); + + QColor bg(255,255,255,192); + p.fillRect(0,0,params.w,params.h,bg); + p.setPen(Qt::black); + p.drawRect(0,0,params.w-1,params.h-1); + + for (Plot* plt : plots) { + plt->render(p, params); + } + + } + + + + void setupAxes() { + + params.xAxes.setPixels(params.w); + params.yAxes.setPixels(params.h); + + Range keyRange(+INFINITY,-INFINITY); + Range valRange(+INFINITY,-INFINITY); + + if (overwriteRangeKey.isValid()) {keyRange.adjust(overwriteRangeKey);} + if (overwriteRangeVal.isValid()) {valRange.adjust(overwriteRangeVal);} + + /** calculate min/max for both x and y axis */ + for (Plot* plt : plots) { + if (!overwriteRangeKey.isValid()) {keyRange.adjust(plt->getKeyRange());} + if (!overwriteRangeVal.isValid()) {valRange.adjust(plt->getValueRange());} + } + + valRange.scale(1.1); + + params.xAxes.setRange(keyRange); + params.yAxes.setRange(valRange); + + } + +}; + + + +class LinePlot : public Plot { + +private: + + QColor lineColor = QColor(0,0,255); + +public: + + void setColor(const QColor c) { + this->lineColor = c; + } + +protected: + + void renderSub(QPainter& p , const PlotParameters& params) override { + + p.setPen(lineColor); + + for (int i = 0; i < (int) data.size()-1; ++i) { + + const typename Data::KeyVal kv1 = data[i+0]; + const typename Data::KeyVal kv2 = data[i+1]; + + const Point2 p1 = params.getPoint(kv1); + const Point2 p2 = params.getPoint(kv2); + + p.drawLine(p1.x, p1.y, p2.x, p2.y); + + } + + } + +}; + + +class PointPlot : public Plot { + +private: + + QColor pointColor = QColor(0,0,255); + float pointSize = 4; + +public: + + void setColor(const QColor c) { + this->pointColor = c; + } + + void setPointSize(const float size) { + this->pointSize = size; + } + +protected: + + void renderSub(QPainter& p , const PlotParameters& params) override { + + p.setPen(Qt::NoPen); + p.setBrush(pointColor); + + for (int i = 0; i < (int) data.size(); ++i) { + + const typename Data::KeyVal kv1 = data[i+0]; + + const Point2 p1 = params.getPoint(kv1); + + p.drawEllipse(p1.x, p1.y, pointSize, pointSize); + + } + + } + +}; + +#endif // PLOT_H diff --git a/ui/debug/plot/PlottWidget.cpp b/ui/debug/plot/PlottWidget.cpp new file mode 100644 index 0000000..1eaa569 --- /dev/null +++ b/ui/debug/plot/PlottWidget.cpp @@ -0,0 +1,37 @@ +#include "PlottWidget.h" + +#include +#include +#include + +PlotWidget::PlotWidget(QWidget *parent) : QWidget(parent) { + + setMinimumSize(100, 100); + +// LinePlot* lp = new LinePlot(); +// pc.addPlot(lp); + +// lp->getData().add(1, 1); +// lp->getData().add(2, 2); +// lp->getData().add(3, 3); +// lp->getData().add(4, 1); +// lp->getData().add(5, 2); +// lp->getData().add(6, 3); +// lp->getData().add(7, 1); +// lp->getData().add(8, 2); +// lp->getData().add(9, 3); + + +} + +void PlotWidget::resizeEvent(QResizeEvent* evt) { + (void) evt; + pc.resize(width(), height()); +} + +void PlotWidget::paintEvent(QPaintEvent* evt) { + (void) evt; + QPainter p(this); + pc.render(p); + p.end(); +} diff --git a/ui/debug/plot/PlottWidget.h b/ui/debug/plot/PlottWidget.h new file mode 100644 index 0000000..6c4d84a --- /dev/null +++ b/ui/debug/plot/PlottWidget.h @@ -0,0 +1,30 @@ +#ifndef PLOTTI_H +#define PLOTTI_H + +#include +#include "Plot.h" + +/** widget to render one plot */ +class PlotWidget : public QWidget { + + Q_OBJECT + +public: + + /** ctor */ + explicit PlotWidget(QWidget *parent = 0); + +protected: + + PlotContainer pc; + +signals: + +public slots: + + void paintEvent(QPaintEvent*); + void resizeEvent(QResizeEvent*); + +}; + +#endif // PLOTTI_H diff --git a/ui/debug/plot/Range.h b/ui/debug/plot/Range.h new file mode 100644 index 0000000..d27d8fa --- /dev/null +++ b/ui/debug/plot/Range.h @@ -0,0 +1,48 @@ +#ifndef PLOT_RANGE_H +#define PLOT_RANGE_H + +struct Range { + + float min; + float max; + + Range() : min(0), max(0) {;} + + Range(const float min, const float max) : min(min), max(max) { + ; + } + + float getSize() const { + return max-min; + } + + float getCenter() const { + return (max+min)/2; + } + + bool isValid() const { + return getSize() > 0; + } + + /** resize the region. 1.0 = keep-as-is */ + void scale(const float val) { + const float center = getCenter(); + const float size = getSize(); + min = center - size / 2 * val; + max = center + size / 2 * val; + } + + /** adjust (grow) the range */ + void adjust(const float val) { + if (val < min) {min = val;} + if (val > max) {max = val;} + } + + void adjust(const Range& o) { + if (o.min < min) {min = o.min;} + if (o.max > max) {max = o.max;} + } + +}; + +#endif // PLOT_RANGE_H diff --git a/ui/dialog/LoadSetupDialog.cpp b/ui/dialog/LoadSetupDialog.cpp new file mode 100644 index 0000000..b7ddc00 --- /dev/null +++ b/ui/dialog/LoadSetupDialog.cpp @@ -0,0 +1,73 @@ +#include "LoadSetupDialog.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "../Config.h" + +LoadSetupDialog::LoadSetupDialog() { + + // the folder all map-setups reside within + const std::string base = Config::getMapDir(); + QDir mapFolder(QString(base.c_str())); + + // sanity check. folder must exist + Assert::isTrue(mapFolder.exists(), "folder not found: " + base); + + // get all subfolders (each subfolder descibres one setup), skip the first two folders: "." and ".." + QStringList subfolders = mapFolder.entryList(QDir::Dirs); + subfolders.removeFirst(); + subfolders.removeFirst(); +// for (int i = 2; i < subfolders.size(); ++i) { +// const QString subfolder = subfolders[i]; +// std::cout << subfolder.toStdString() << std::endl; +// } + + int w = 350; + int h = 350; + + const QFont font("Arial", 20); + + QListView* lst = new QListView(this); + lst->setGeometry(5,5,w-5-5,h-5-5); + lst->setFont(font); + + QStringListModel* mdl = new QStringListModel(subfolders); + lst->setModel(mdl); + + // list item selected + connect(lst, &QListView::clicked, [this, base, subfolders] (const QModelIndex& idx) { + const int i = idx.row(); + selDir = base + subfolders[i].toStdString(); + this->close(); + }); + +// QPushButton* btnOK = new QPushButton(this); +// btnOK->setText("OK"); +// btnOK->setGeometry(5,h-32-5,w-5-5,32); + +// // OK button clicked +// btnOK->connect(btnOK, &QPushButton::clicked, [&] () { +// this->close(); +// }); + + this->resize(w,h); + +} + +QDir LoadSetupDialog::pickSetupFolder() { + + + LoadSetupDialog dlg; + dlg.exec(); + return QDir(QString(dlg.selDir.c_str())); + +} diff --git a/ui/dialog/LoadSetupDialog.h b/ui/dialog/LoadSetupDialog.h new file mode 100644 index 0000000..603b053 --- /dev/null +++ b/ui/dialog/LoadSetupDialog.h @@ -0,0 +1,25 @@ +#ifndef LOADSETUPDIALOG_H +#define LOADSETUPDIALOG_H + +#include +#include + +class LoadSetupDialog : public QDialog { + + Q_OBJECT + +private: + + /** hidden ctor */ + explicit LoadSetupDialog(); + + std::string selDir = ""; + +public: + + /** show a dialog to open a data-folder */ + static QDir pickSetupFolder(); + +}; + +#endif // LOADSETUPDIALOG_H diff --git a/map/FloorRenderer.h b/ui/map/FloorRenderer.h similarity index 100% rename from map/FloorRenderer.h rename to ui/map/FloorRenderer.h diff --git a/ui/map/MapView.cpp b/ui/map/MapView.cpp new file mode 100644 index 0000000..2ad4fd9 --- /dev/null +++ b/ui/map/MapView.cpp @@ -0,0 +1,266 @@ +#include "MapView.h" + +#include +#include + +#include "elements/Walls.h" +#include "elements/Ground.h" +#include "elements/Handrails.h" +#include "elements/Stairs.h" +#include "elements/Doors.h" +#include "elements/Path.h" +#include "elements/ColorPoints.h" +#include "elements/Object.h" + +#include +#include + +/** + * before adding elements to the MapView via setMap(), + * the MapViews openGL context must be initialized + * that means: the MapView must have been added to a window, + * which is already visible! + */ + +MapView::MapView(QWidget* parent) : QOpenGLWidget(parent) { + + + +}; + +void MapView::clear() { + + for (Renderable* r : elements) {delete r;} + elements.clear(); + +} + +void MapView::setMap(Floorplan::IndoorMap* map) { + + clear(); + + if (!isGLInitialized) {throw Exception("openGL is not yet initialized. add mapView to a visible window!");} + + // first to be rendered + this->colorPoints = new ColorPoints(); + elements.push_back(this->colorPoints); + + //leDude = new Object("/mnt/firma/tmp/3D/minion/minion.obj", "/mnt/firma/tmp/3D/minion/minion.png", "", 0.35); + leDude = new Object("/mnt/firma/tmp/3D/gnome/gnome.obj", "/mnt/firma/tmp/3D/gnome/gnome_diffuse.jpg", "/mnt/firma/tmp/3D/gnome/gnome_normal.jpg", 0.033); + //leDude = new Object("/mnt/firma/tmp/3D/squirrel/squirrel.obj", "/mnt/firma/tmp/3D/squirrel/squirrel.jpg", "/mnt/firma/tmp/3D/squirrel/squirrel_normal.jpg", 0.033); + elements.push_back(leDude); + + for (Floorplan::Floor* floor : map->floors) { + elements.push_back(new Ground(floor)); + elements.push_back(new Walls(floor)); + elements.push_back(new Handrails(floor)); + elements.push_back(new Stairs(floor)); + elements.push_back(new Doors(floor)); + } + + this->path = new Path(); + elements.push_back(this->path); + + + + // initialize the OpenGL context of all contained elements + for (Renderable* r : elements) { + r->initGL(); + } + + // i want the focus! needed for key-events + setFocusPolicy(Qt::StrongFocus); + +} + +void MapView::setPath(const std::vector& path) { + this->path->set(path); +} + + +void MapView::timerEvent(QTimerEvent *) { + update(); +} + +void MapView::initializeGL() { + + initializeOpenGLFunctions(); + + // basic config + glEnable(GL_DEPTH_TEST); + glEnable(GL_CULL_FACE); + + // start background update timer + const int fps = 25; + const int interval = 1000 / fps; + timer.start(interval, this); + + // OpenGL is now initialized + isGLInitialized = true; + +} + +void MapView::paintGL() { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + draw(); +} + +void MapView::resizeGL(int w, int h) { + + // Calculate aspect ratio + qreal aspect = qreal(w) / qreal(h ? h : 1); + + // viewing frustrum [0:50] meter + const qreal zNear = 0.02, zFar = 50, fov = 50.0; + + // Reset projection + matProject.setToIdentity(); + matProject.scale(-1, 1, 1); + glCullFace(GL_FRONT); + //matProject.scale(0.05, 0.05, 0.05); + matProject.perspective(fov, aspect, zNear, zFar); + //matProject.scale(-0.01, 0.01, 0.01); + +} + +void MapView::rebuildLookat() { +// QVector3D qDir(lookAt.dir.x, lookAt.dir.z, lookAt.dir.y); +// QVector3D at = QVector3D(lookAt.pos.x, lookAt.pos.z, lookAt.pos.y); +// QVector3D eye = at + qDir * 0.1; +// QVector3D up = QVector3D(0,1,0); +// matView.setToIdentity(); +// //matView.scale(0.01, 0.01, 0.01); +// matView.lookAt(eye, at, up); +// //matView.scale(0.99, 1, 1); +// //matView.translate(0.7, 0, 0); +// lightPos = eye + QVector3D(0.0, 4.0, 0.0); +// eyePos = eye; + + const Point3 dir = lookAt.getDir(); + + QVector3D qDir(dir.x, dir.z, dir.y); + QVector3D eye(lookAt.eye_m.x, lookAt.eye_m.z, lookAt.eye_m.y); + QVector3D at = eye + qDir * 0.5; + QVector3D up = QVector3D(0,1,0); + matView.setToIdentity(); + matView.lookAt(eye, at, up); + lightPos = eye + QVector3D(0.0, 0.5, 0.0) + qDir * 1.2; + eyePos = eye; + + + +} + +void MapView::setCurrentEstimation(const Point3 pos, const Point3 dir) { + const float angle = std::atan2(dir.y, dir.x) * 180 / M_PI; + if (leDude) { + leDude->setPosition(pos.x, pos.y, pos.z); + leDude->setRotation(0, 0, -angle + 90); + } +} + +void MapView::setLookAt(const Point3 pos_m, const Point3 dir) { + lookAt.eye_m = pos_m + dir * 0.1; + lookAt.dir = dir; + rebuildLookat(); +} + +void MapView::setLookDir(const Point3 dir) { + lookAt.dir = dir; + rebuildLookat(); +} + +void MapView::setLookEye(const Point3 eye_m) { + lookAt.eye_m = eye_m; + rebuildLookat(); +} + + + +void MapView::mousePressEvent(QMouseEvent* evt) { + mouseState.down = true; + mouseState.x = evt->x(); + mouseState.y = evt->y(); +} + +void MapView::mouseMoveEvent(QMouseEvent* evt) { + + const float dx = evt->x() - mouseState.x; + const float dy = evt->y() - mouseState.y; + + // PI*0.3 head movement left/right and up/down + const float yFac = (this->height() / 2) / (M_PI * 0.3); + const float xFac = (this->width() / 2) / (M_PI * 0.3); + + lookAt.dirOffset = Point3(0, std::sin(dx/xFac), std::sin(-dy/yFac)); + rebuildLookat(); + +} + +void MapView::mouseReleaseEvent(QMouseEvent* evt) { + mouseState.down = false; +} + + +void MapView::keyPressEvent(QKeyEvent* evt) { + + if (evt->key() == Qt::Key_W) {lookAt.eye_m += lookAt.getDir(); rebuildLookat();} + if (evt->key() == Qt::Key_S) {lookAt.eye_m -= lookAt.getDir(); rebuildLookat();} + + +} + +void MapView::toggleRenderMode() { + + renderMode = (RenderMode) (((int)renderMode + 1) % 3); + + for (Renderable* r : elements) { + if (renderMode == RenderMode::OUTLINE) { + r->setOutlineOnly(true); + } else { + r->setOutlineOnly(false); + } + } + +} + +void MapView::draw() { + + //const Timestamp ts1 = Timestamp::fromUnixTime(); + + // clear everything + glClearColor(0,0,0,1); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + if (renderMode == RenderMode::TRANSPARENT) { + glEnable(GL_BLEND); + } else { + glDisable(GL_BLEND); + } + + glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + + + + for (Renderable* r : elements) { + + QOpenGLShaderProgram& program = r->getProgram(); + program.bind(); + + // set the matrices + program.setUniformValue("m_matrix", r->modelMatrix.mat); + program.setUniformValue("mv_matrix", matView * r->modelMatrix.mat); + program.setUniformValue("mvp_matrix", matProject * matView * r->modelMatrix.mat); + program.setUniformValue("lightWorldPos", lightPos); + program.setUniformValue("eyeWorldPos", eyePos); + + r->render(); + + } + + //const Timestamp ts2 = Timestamp::fromUnixTime(); + //qDebug("%d ms", (ts2-ts1).ms()); + + +} diff --git a/ui/map/MapView.h b/ui/map/MapView.h new file mode 100644 index 0000000..6774e64 --- /dev/null +++ b/ui/map/MapView.h @@ -0,0 +1,152 @@ +#ifndef MAPVIEW_H +#define MAPVIEW_H + +#include <../misc/fixc11.h> + + +#include +#include +#include +#include + +#include +#include + +#include "elements/Path.h" +#include "elements/ColorPoints.h" +#include "elements/Object.h" + +#include "../nav/State.h" + +namespace Floorplan { + class IndoorMap; +} + +class Renderable; +class Path; + + +class MapView : public QOpenGLWidget, protected QOpenGLFunctions { + + Q_OBJECT + +private: + + QMatrix4x4 matProject; + QMatrix4x4 matView; + + QVector3D lightPos; + QVector3D eyePos; + + + QBasicTimer timer; + + std::vector elements; + Path* path = nullptr; + ColorPoints* colorPoints = nullptr; + Object* leDude = nullptr; + + struct LookAt { + Point3 eye_m = Point3(0,0,1); + Point3 dir = Point3(1,0,-0.1); + Point3 dirOffset = Point3(0,0,0); + Point3 getDir() const {return dir + dirOffset;} + } lookAt; + + struct MouseState { + float x = 0; + float y = 0; + bool down = false; + } mouseState; + + void rebuildLookat(); + + void clear(); + +public: + + MapView(QWidget* parent = 0); + + /** set the map to display */ + void setMap(Floorplan::IndoorMap* map); + + /** the position to look at + looking direction */ + void setLookAt(const Point3 pos, const Point3 dir = Point3(1, 0, -0.1)); + + /** set the eye's looking direction (looking from eye into this direction) */ + void setLookDir(const Point3 dir); + + /** set the eye's position (looking from here) */ + void setLookEye(const Point3 eye_m); + + + /** set the currently estimated position */ + void setCurrentEstimation(const Point3 pos, const Point3 dir); + + /** set the path to disply */ + void setPath(const std::vector& path); + + /** NOTE: must be called from Qt's main thread! */ + /** set the path to disply */ + Q_INVOKABLE void setPath(const void* path) { + setPath( (const DijkstraPath*) path); + } + + /** NOTE: must be called from Qt's main thread! */ + /** set the path to disply */ + template void setPath(const DijkstraPath* path) { + this->path->set(*path); + } + + /** NOTE: must be called from Qt's main thread! */ + void showGridImportance(Grid* grid) { + this->colorPoints->setFromGridImportance(grid); + } + + /** NOTE: must be called from Qt's main thread! */ + Q_INVOKABLE void showParticles(const void* particles) { + showParticles((const std::vector>*) particles); + } + + /** NOTE: must be called from Qt's main thread! */ + void showParticles(const std::vector>* particles) { + this->colorPoints->setFromParticles(*particles); + } + + enum RenderMode { + NORMAL, + TRANSPARENT, + OUTLINE, + }; + + RenderMode renderMode = RenderMode::NORMAL; + + void toggleRenderMode(); + +public slots: + + void mousePressEvent(QMouseEvent*); + void mouseMoveEvent(QMouseEvent*); + void mouseReleaseEvent(QMouseEvent*); + + void keyPressEvent(QKeyEvent*); + +protected: + + void timerEvent(QTimerEvent *e) Q_DECL_OVERRIDE; + + void initializeGL(); + + void paintGL(); + + void resizeGL(int width, int height); + +private: + + bool isGLInitialized = false; + + void draw(); + +}; + +#endif // MAPVIEW_H diff --git a/ui/map/Renderable.h b/ui/map/Renderable.h new file mode 100644 index 0000000..fb639a6 --- /dev/null +++ b/ui/map/Renderable.h @@ -0,0 +1,76 @@ +#ifndef RENDERABLE_H +#define RENDERABLE_H + +#include + +class Renderable { + +protected: + + QOpenGLShaderProgram program; + +public: + + /** dtor */ + virtual ~Renderable() {;} + + /** get the renderable's shader */ + QOpenGLShaderProgram& getProgram() {return program;} + + /** render the renderable */ + void render() { + program.bind(); + _render(); + } + + struct ModelMatrix { + QVector3D pos = QVector3D(0,0,0); + QVector3D rot = QVector3D(0,0,0); + QVector3D scale = QVector3D(1,1,1); + QMatrix4x4 mat; + ModelMatrix() {mat.setToIdentity();} + void update() { + const QVector3D _rot = rot.normalized(); + const float rotDeg = rot.length(); + mat.setToIdentity(); + mat.scale(scale.x(), scale.y(), scale.z()); + mat.translate(pos.x(), pos.y(), pos.z()); + mat.rotate(rotDeg, _rot.x(), _rot.y(), _rot.z()); + } + } modelMatrix; + + void setPosition(QVector3D vec) { + modelMatrix.pos = vec * 0.99; + modelMatrix.update(); + } + + void setPosition(const float x, const float y, const float z) { + setPosition(QVector3D(x,z,y)); + } + + /** in degrees! */ + void setRotation(const float x, const float y, const float z) { + modelMatrix.rot = QVector3D(x,z,y); + modelMatrix.update(); + } + + virtual void setOutlineOnly(const bool outline) {;} + + virtual void initGL() = 0; + + virtual void _render() = 0; + +protected: + + /** helper method to build the shader */ + void loadShader(const QString& vertex, const QString& fragment) { + program.removeAllShaders(); + if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, vertex)) {throw "1";} + if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, fragment)) {throw "2";} + if (!program.link()) {throw "3";} + if (!program.bind()) {throw "4";} + } + +}; + +#endif // RENDERABLE_H diff --git a/ui/map/elements/ColorPoints.h b/ui/map/elements/ColorPoints.h new file mode 100644 index 0000000..921cce3 --- /dev/null +++ b/ui/map/elements/ColorPoints.h @@ -0,0 +1,114 @@ +#ifndef GL_PARTICLES_H +#define GL_PARTICLES_H + + +#include +#include + +#include "../gl/GLHelper.h" +#include "../gl/GLPoints.h" +#include "../Renderable.h" + +#include "../../../nav/Node.h" + +class ColorPoints : public Renderable { + +private: + + GLPoints points; + float size = 3.0f; + +public: + + /** ctor */ + ColorPoints() { + + } + + /** NOTE: must be called from Qt's main thread! */ + void setFromGridImportance(Grid* grid) { + + points.clear(); + + for (const MyGridNode& n : *grid) { + const QVector3D pt(n.x_cm/100.0f, n.z_cm/100.0f + 0.1f, n.y_cm/100.0f); // swap z and y + const float f = n.getNavImportance(); + float h = 0.66 - (f*0.20); // 0.66 is blue on the HSV-scale + if (h < 0) {h = 0;} + if (h > 1) {h = 1;} + const QColor color = QColor::fromHsvF(h, 1, 1); + points.addPoint(pt, color); + } + + size = 3.0f; + points.rebuild(); + + } + + /** NOTE: must be called from Qt's main thread! */ + template void setFromParticles(const std::vector>& particles) { + + points.clear(); + + // group particles by grid-point + std::unordered_map weights; + for (const K::Particle& p : particles) { + const GridPoint gp = p.state.position; + weights[gp] += p.weight; + } + + // find min/max + float min = +INFINITY; + float max = -INFINITY; + for (auto it : weights) { + if (it.second > max) {max = it.second;} + if (it.second < min) {min = it.second;} + } + + // draw colored + for (auto it : weights) { + const GridPoint gp = it.first; + const float w = it.second; + const float p = (w-min) / (max-min); // [0:1] + const QVector3D pt(gp.x_cm/100.0f, gp.z_cm/100.0f + 0.1f, gp.y_cm/100.0f); // swap z and y + float h = 0.66 - (p*0.66); // 0.66 is blue on the HSV-scale + const QColor color = QColor::fromHsvF(h, 1, 1); + points.addPoint(pt, color); + } + + +// for (const K::Particle& p : particles) { +// const GridPoint gp = p.state.position; +// const QVector3D pt(gp.x_cm/100.0f, gp.z_cm/100.0f + 0.1f, gp.y_cm/100.0f); // swap z and y +// const QColor color = Qt::blue; +// points.addPoint(pt, color); +// } + + size = 6.0f; + points.rebuild(); + + } + + + void initGL() override { + loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentColorPoint.glsl"); + //program.setUniformValue("color", QVector4D(0.5, 0.5, 0.5, 1.0)); + points.initGL(); + } + + /** render the floor */ + void _render() override { + //glDisable(GL_DEPTH_TEST); + //glPointSize() +#ifndef ANDROID + glPointSize(size); +#endif + points.render(&program); + //glEnable(GL_DEPTH_TEST); + } + + +}; + + +#endif // GL_PARTICLES_H diff --git a/map/elements/Doors.h b/ui/map/elements/Doors.h similarity index 100% rename from map/elements/Doors.h rename to ui/map/elements/Doors.h diff --git a/map/elements/Ground.h b/ui/map/elements/Ground.h similarity index 67% rename from map/elements/Ground.h rename to ui/map/elements/Ground.h index 60f08fa..2d391e2 100644 --- a/map/elements/Ground.h +++ b/ui/map/elements/Ground.h @@ -4,9 +4,10 @@ #include #include "../gl/GLHelper.h" #include "../gl/GLTriangles.h" +#include "../gl/GLLines.h" #include "../Renderable.h" -#include "../../lib/gpc/Polygon.h" +#include "../../../lib/gpc/Polygon.h" class Ground : public Renderable { @@ -17,11 +18,14 @@ private: GLTriangles flooring; GLTriangles ceiling; + GLLines outline; + bool outlineOnly = false; + public: /** ctor */ - Ground(Floorplan::Floor* floor) : floor(floor) { - ; + Ground(Floorplan::Floor* floor) : floor(floor) { + setOutlineOnly(false); } @@ -37,17 +41,36 @@ public: flooring.build(); ceiling.build(); + outline.build(); - loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentTex.glsl"); - program.setUniformValue("texDiffuse", 0); - program.setUniformValue("texNormalMap", 1); + //loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentTex.glsl"); + //program.setUniformValue("texDiffuse", 0); + //program.setUniformValue("texNormalMap", 1); } /** render the floor */ - void _render() override { - flooring.render(&program); - ceiling.render(&program); + void _render() override { + if (outlineOnly) { + glLineWidth(5); + outline.render(&program); + } else { + flooring.render(&program); + ceiling.render(&program); + } + } + + /** render only the outline? */ + void setOutlineOnly(const bool outline) override { +// this->outlineOnly = outline; +// if (outlineOnly) { +// loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentLine.glsl"); +// program.setUniformValue("color", QVector4D(0.0, 0.0, 0.4, 1.0)); +// } else { + loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentTex.glsl"); + program.setUniformValue("texDiffuse", 0); + program.setUniformValue("texNormalMap", 1); +// } } @@ -80,7 +103,7 @@ private: const QVector3D normFloor(0, +1, 0); - const QVector3D normCeil(0, +1, 0); // why +1??? + const QVector3D normCeil(0, -1, 0); const QVector3D t(1,0,0); const float s = 0.6; @@ -108,6 +131,10 @@ private: ceiling.addFaceCW(vnt1, vnt2, vnt3); } + outline.addLine(vert1, vert2); + outline.addLine(vert2, vert3); + outline.addLine(vert3, vert1); + } } diff --git a/map/elements/Handrails.h b/ui/map/elements/Handrails.h similarity index 100% rename from map/elements/Handrails.h rename to ui/map/elements/Handrails.h diff --git a/ui/map/elements/Object.h b/ui/map/elements/Object.h new file mode 100644 index 0000000..af0687d --- /dev/null +++ b/ui/map/elements/Object.h @@ -0,0 +1,84 @@ +#ifndef OBJECT_H +#define OBJECT_H + + +#include +#include "../gl/GLHelper.h" +#include "../gl/GLTriangles.h" +#include "../Renderable.h" + +#include + + +class Object : public Renderable { + +private: + + GLTriangles triangles; + +public: + + /** ctor */ + Object(const std::string& file, const std::string& colorTexture, std::string normalsTexture, const float scale = 1.0) { + + K::ObjFileReader reader(file, false); + + + if (normalsTexture.empty()) {normalsTexture = ":/res/gl/tex/empty_normals.jpg";} + + triangles.setDiffuse(colorTexture.c_str()); + triangles.setNormalMap(normalsTexture.c_str()); + + for (const K::ObjFileReader::Face& face : reader.getData().faces) { + + const QVector3D vertex1(face.vnt[0].vertex.x, face.vnt[0].vertex.y, face.vnt[0].vertex.z); + const QVector3D vertex2(face.vnt[1].vertex.x, face.vnt[1].vertex.y, face.vnt[1].vertex.z); + const QVector3D vertex3(face.vnt[2].vertex.x, face.vnt[2].vertex.y, face.vnt[2].vertex.z); + + const QVector3D normal1(face.vnt[0].normal.x, face.vnt[0].normal.y, face.vnt[0].normal.z); + const QVector3D normal2(face.vnt[1].normal.x, face.vnt[1].normal.y, face.vnt[1].normal.z); + const QVector3D normal3(face.vnt[2].normal.x, face.vnt[2].normal.y, face.vnt[2].normal.z); + + const QVector2D texture1(face.vnt[0].texture.x, face.vnt[0].texture.y); + const QVector2D texture2(face.vnt[1].texture.x, face.vnt[1].texture.y); + const QVector2D texture3(face.vnt[2].texture.x, face.vnt[2].texture.y); + + const QVector3D o(0, 0.0, 0); + + const VertNormTex vnt1(vertex1*scale+o, normal1, texture1); + const VertNormTex vnt2(vertex2*scale+o, normal2, texture2); + const VertNormTex vnt3(vertex3*scale+o, normal3, texture3); + + triangles.addFace(vnt1, vnt2, vnt3); + + } + + } + + + + void initGL() override { + build(); + triangles.build(); + loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentTex.glsl"); + program.setUniformValue("texDiffuse", 0); + program.setUniformValue("texNormalMap", 1); + } + + /** render the floor */ + void _render() override { + triangles.render(&program); + } + + + +private: + + void build() { + triangles.build(); + } + +}; + + +#endif // OBJECT_H diff --git a/map/elements/Path.h b/ui/map/elements/Path.h similarity index 99% rename from map/elements/Path.h rename to ui/map/elements/Path.h index f925712..e674c0b 100644 --- a/map/elements/Path.h +++ b/ui/map/elements/Path.h @@ -9,7 +9,6 @@ #include "../gl/GLTriangles.h" #include "../Renderable.h" -#include "../../lib/gpc/Polygon.h" class Path : public Renderable { diff --git a/map/elements/Stairs.h b/ui/map/elements/Stairs.h similarity index 94% rename from map/elements/Stairs.h rename to ui/map/elements/Stairs.h index 7fbc529..cf126b2 100644 --- a/map/elements/Stairs.h +++ b/ui/map/elements/Stairs.h @@ -27,8 +27,10 @@ public: void initGL() override { build(); - parts.setDiffuse(":/res/gl/tex/granite1.jpg"); - parts.setNormalMap(":/res/gl/tex/granite1_normal.jpg"); +// parts.setDiffuse(":/res/gl/tex/granite1.jpg"); +// parts.setNormalMap(":/res/gl/tex/granite1_normal.jpg"); + parts.setDiffuse(":/res/gl/tex/floor4.jpg"); + parts.setNormalMap(":/res/gl/tex/floor4_normal.jpg"); parts.build(); loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentTex.glsl"); diff --git a/map/elements/Walls.h b/ui/map/elements/Walls.h similarity index 69% rename from map/elements/Walls.h rename to ui/map/elements/Walls.h index 6e1482c..93a14a8 100644 --- a/map/elements/Walls.h +++ b/ui/map/elements/Walls.h @@ -4,9 +4,10 @@ #include #include "../gl/GLHelper.h" #include "../gl/GLTriangles.h" +#include "../gl/GLLines.h" #include "../Renderable.h" +#include "../gl/Shader.h" -#include "../../lib/gpc/Polygon.h" class Walls : public Renderable { @@ -14,36 +15,54 @@ private: Floorplan::Floor* floor; - GLTriangles walls; + GLTriangles triangles; + GLLines outlines; + bool outlineOnly = false; public: /** ctor */ Walls(Floorplan::Floor* floor) : floor(floor) { - ; + + setOutlineOnly(false); + } void initGL() override { build(); - walls.setDiffuse(":/res/gl/tex/floor3.jpg"); - walls.setNormalMap(":/res/gl/tex/floor3_normal.jpg"); - walls.build(); + triangles.build(); + triangles.setDiffuse(":/res/gl/tex/wall3.jpg"); + triangles.setNormalMap(":/res/gl/tex/wall3_normal.jpg"); - loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentTex.glsl"); - program.setUniformValue("texDiffuse", 0); - program.setUniformValue("texNormalMap", 1); - //glEnable(GL_TEXTURE0 + 1); + outlines.build(); } /** render the floor */ void _render() override { - walls.render(&program); + if (outlineOnly) { + glLineWidth(1); + outlines.render(&program); + } else { + triangles.render(&program); + } } + /** render only the outline? */ + void setOutlineOnly(const bool outline) override { + this->outlineOnly = outline; + if (outlineOnly) { + loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentLine.glsl"); + program.setUniformValue("color", QVector4D(0.9, 0.9, 0.9, 1.0)); + } else { + loadShader(":/res/gl/vertex1.glsl", ":/res/gl/fragmentTex.glsl"); + program.setUniformValue("texDiffuse", 0); + program.setUniformValue("texNormalMap", 1); + } + } private: @@ -98,15 +117,20 @@ private: const VertNormTexTan vnt2(vert2, n1, tex2*s, tan); const VertNormTexTan vnt3(vert3, n1, tex3*s, tan); const VertNormTexTan vnt4(vert4, n1, tex4*s, tan); - walls.addQuadCCW(vnt1, vnt2, vnt3, vnt4); + triangles.addQuadCCW(vnt1, vnt2, vnt3, vnt4); } { const VertNormTexTan vnt1(vert1, n2, tex1*s, -tan); const VertNormTexTan vnt2(vert2, n2, tex2*s, -tan); const VertNormTexTan vnt3(vert3, n2, tex3*s, -tan); const VertNormTexTan vnt4(vert4, n2, tex4*s, -tan); - walls.addQuadCW(vnt1, vnt2, vnt3, vnt4); + triangles.addQuadCW(vnt1, vnt2, vnt3, vnt4); } + outlines.addLine(vert1, vert2); + outlines.addLine(vert2, vert3); + outlines.addLine(vert3, vert4); + outlines.addLine(vert4, vert1); + } //private: diff --git a/map/gl/GL.h b/ui/map/gl/GL.h similarity index 80% rename from map/gl/GL.h rename to ui/map/gl/GL.h index e294280..3ed907b 100644 --- a/map/gl/GL.h +++ b/ui/map/gl/GL.h @@ -13,6 +13,18 @@ struct Vert { bool operator == (const Vert& o) const {return (vert == o.vert);} }; +struct VertColor { + QVector3D vert; + QVector3D color; + VertColor(QVector3D vert, QVector3D color) : vert(vert), color(color) {;} + int getVertOffset() const {return 0;} + int getColorOffset() const {return sizeof(QVector3D);} + int getTanOffset() const {throw "error";} + bool operator == (const VertColor& o) const {return (vert == o.vert) && (color == o.color);} + static bool hasTangent() {return false;} + static bool hasColor() {return true;} +}; + struct VertNorm { QVector3D vert; QVector3D norm; diff --git a/map/gl/GLHelper.h b/ui/map/gl/GLHelper.h similarity index 100% rename from map/gl/GLHelper.h rename to ui/map/gl/GLHelper.h diff --git a/map/gl/GLLines.h b/ui/map/gl/GLLines.h similarity index 100% rename from map/gl/GLLines.h rename to ui/map/gl/GLLines.h diff --git a/ui/map/gl/GLPoints.h b/ui/map/gl/GLPoints.h new file mode 100644 index 0000000..7bc0c27 --- /dev/null +++ b/ui/map/gl/GLPoints.h @@ -0,0 +1,111 @@ +#ifndef GLPOINTS_H +#define GLPOINTS_H + + +#include +#include "GL.h" +#include "GLHelper.h" + +#include + +class GLPoints : protected QOpenGLFunctions { + +private: + + QOpenGLBuffer arrayBuf; + QOpenGLBuffer indexBuf; + + std::vector vertices; + std::vector indices; + + int mode = GL_POINTS; + bool initOnce = true; + +public: + + /** ctor */ + GLPoints() : arrayBuf(QOpenGLBuffer::VertexBuffer), indexBuf(QOpenGLBuffer::IndexBuffer) { + alloc(); + } + + /** dtor */ + ~GLPoints() { + destroy(); + } + + /** add a new face to this element */ + void addPoint(const QVector3D& pt, const QColor& color) { + indices.push_back(vertices.size()); + QVector3D c(color.redF(), color.greenF(), color.blueF()); + vertices.push_back(VertColor(pt, c)); + } + + + void alloc() { + if (!indexBuf.isCreated()) {indexBuf.create();} + if (!arrayBuf.isCreated()) {arrayBuf.create();} + } + + void destroy() { + if (indexBuf.isCreated()) {indexBuf.destroy();} + if (arrayBuf.isCreated()) {arrayBuf.destroy();} + } + + /** build the underlying buffers */ + void build() { + + // Transfer vertex data to VBO 0 + arrayBuf.bind(); + arrayBuf.allocate(vertices.data(), vertices.size() * sizeof(vertices[0])); + + // Transfer index data to VBO 1 + indexBuf.bind(); + indexBuf.allocate(indices.data(), indices.size() * sizeof(indices[0])); + + } + + void initGL() { + initializeOpenGLFunctions(); + } + + void rebuild() { + build(); + } + + void clear() { + indices.clear(); + vertices.clear(); + } + + void setMode(const int mode) { + this->mode = mode; + } + + /** render the element */ + void render(QOpenGLShaderProgram *program) { + + if (indices.empty()) {return;} + + // Tell OpenGL which VBOs to use + arrayBuf.bind(); + indexBuf.bind(); + + // vertices + int vertLoc = program->attributeLocation("a_position"); + program->enableAttributeArray(vertLoc); + program->setAttributeBuffer(vertLoc, GL_FLOAT, vertices[0].getVertOffset(), 3, sizeof(vertices[0])); + + // colors + int colorLoc = program->attributeLocation("a_color"); + program->enableAttributeArray(colorLoc); + program->setAttributeBuffer(colorLoc, GL_FLOAT, vertices[0].getColorOffset(), 3, sizeof(vertices[0])); + + // Draw cube geometry using indices from VBO 1 + glDrawElements(mode, indices.size(), GL_UNSIGNED_INT, 0); + + } + + +}; + +#endif // GLPOINTS_H diff --git a/map/gl/GLTriangles.h b/ui/map/gl/GLTriangles.h similarity index 97% rename from map/gl/GLTriangles.h rename to ui/map/gl/GLTriangles.h index 62268a8..f0427b3 100644 --- a/map/gl/GLTriangles.h +++ b/ui/map/gl/GLTriangles.h @@ -59,6 +59,11 @@ public: setTexture(1, textureFile); } + /** add a new face to this element */ + void addFace(const T& vnt1, const T& vnt2, const T& vnt3) { + addFace(vnt1, vnt2, vnt3, 0); + } + /** add a new face to this element */ void addFaceCCW(const T& vnt1, const T& vnt2, const T& vnt3) { addFace(vnt1, vnt2, vnt3, 1); diff --git a/ui/map/gl/Shader.h b/ui/map/gl/Shader.h new file mode 100644 index 0000000..9a65c58 --- /dev/null +++ b/ui/map/gl/Shader.h @@ -0,0 +1,31 @@ +#ifndef SHADER_H +#define SHADER_H + +#include + +/** + * just some helper methods + */ +class Shader { + +private: + + QOpenGLShaderProgram program; + +public: + + /** get the underlying program */ + QOpenGLShaderProgram* getProgram() {return &program;} + + /** helper method to build the shader */ + void loadShaderFromFile(const QString& vertex, const QString& fragment) { + if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, vertex)) {throw "1";} + if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, fragment)) {throw "2";} + if (!program.link()) {throw "3";} + if (!program.bind()) {throw "4";} + } + + +}; + +#endif // SHADER_H diff --git a/ui/menu/MainMenu.cpp b/ui/menu/MainMenu.cpp new file mode 100644 index 0000000..83ced25 --- /dev/null +++ b/ui/menu/MainMenu.cpp @@ -0,0 +1,55 @@ + +#include "MainMenu.h" +#include "../Icons.h" + +#include +#include + +#include + +MainMenu::MainMenu(QWidget* parent) : QWidget(parent) { + + setMinimumHeight(64); + + QGridLayout* lay = new QGridLayout(this); + int row = 0; + int col = 0; + + btnLoadMap = getButton("load"); + Assert::isTrue(connect(btnLoadMap, &QPushButton::clicked, this, &MainMenu::onLoadButton), "connect() failed"); + lay->addWidget(btnLoadMap, row, col, 1,1,Qt::AlignTop); ++col; + + btnDebug = getButton("bug"); + Assert::isTrue(connect(btnDebug, &QPushButton::clicked, this, &MainMenu::onDebugButton), "connect() failed"); + lay->addWidget(btnDebug, row, col, 1,1,Qt::AlignTop); ++col; + + btnCamera = getButton("camera"); + Assert::isTrue(connect(btnCamera, &QPushButton::clicked, this, &MainMenu::onCameraButton), "connect() failed"); + lay->addWidget(btnCamera, row, col, 1,1,Qt::AlignTop); ++col; + + btnTransparent = getButton("cube"); + Assert::isTrue(connect(btnTransparent, &QPushButton::clicked, this, &MainMenu::onTransparentButton), "connect() failed"); + lay->addWidget(btnTransparent, row, col, 1,1,Qt::AlignTop); ++col; + + btnStart = getButton("run"); + Assert::isTrue(connect(btnStart, &QPushButton::clicked, this, &MainMenu::onStartButton), "connect() failed"); + lay->addWidget(btnStart, row, col, 1,1,Qt::AlignTop); ++col; + + +} + +QPushButton* MainMenu::getButton(const std::string& icon) { + + const int size = 48; + const int border = 4; + + QPushButton* btn = new QPushButton(Icons::getIcon(icon, size), ""); + btn->setIconSize(QSize(size,size)); + btn->setMinimumHeight(size+border); + btn->setMaximumHeight(size+border); + btn->setMinimumWidth(size+border); + btn->setMaximumWidth(size+border); + + return btn; + +} diff --git a/ui/menu/MainMenu.h b/ui/menu/MainMenu.h new file mode 100644 index 0000000..43cdb75 --- /dev/null +++ b/ui/menu/MainMenu.h @@ -0,0 +1,36 @@ +#ifndef MAINMENU_H +#define MAINMENU_H + +#include + +class QPushButton; + +class MainMenu : public QWidget { + Q_OBJECT + +public: + + /** ctor */ + explicit MainMenu(QWidget* parent); + +signals: + + void onLoadButton(); + void onStartButton(); + void onDebugButton(); + void onCameraButton(); + void onTransparentButton(); + +private: + + QPushButton* getButton(const std::string& icon); + + QPushButton* btnLoadMap; + QPushButton* btnStart; + QPushButton* btnDebug; + QPushButton* btnCamera; + QPushButton* btnTransparent; + +}; + +#endif diff --git a/yasmin.pro b/yasmin.pro index 6fa1c3f..0427bdc 100644 --- a/yasmin.pro +++ b/yasmin.pro @@ -1,10 +1,19 @@ TEMPLATE = app -QT += qml opengl +QT += qml opengl svg # android? -#QT += androidextras sensors -#DEFINES += ANDROID + + +# CONFIG+=ANDROID DEFINES+=ANDROID +ANDROID { + QT += androidextras + QT += sensors +} + +# debug +DEFINES += WITH_DEBUG_LOG +DEFINES += WITH_ASSERTIONS CONFIG += c++11 @@ -12,7 +21,13 @@ CONFIG += c++11 ANDROID_PACKAGE_SOURCE_DIR = $$PWD/_android INCLUDEPATH += \ - ../ + ../ \ + ./lib/ + + +# linux desktop wifi +#INCLUDEPATH +=/usr/include/libnl3/ +#LIBS += -lnl-genl-3 -lnl-3 OTHER_FILES += \ _android/src/WiFi.java \ @@ -21,15 +36,24 @@ OTHER_FILES += \ SOURCES += \ main.cpp \ - map/MapView.cpp \ - map/Geometry.cpp \ - lib/gpc/gpc.cpp \ - ../Indoor/lib/tinyxml/tinyxml2.cpp + lib/gpc/gpc.cpp \ + ../Indoor/lib/tinyxml/tinyxml2.cpp \ + ui/map/MapView.cpp \ + ui/menu/MainMenu.cpp \ + ui/MainWindow.cpp \ + Controller.cpp \ + ui/dialog/LoadSetupDialog.cpp \ + ui/debug/SensorDataWidget.cpp \ + ui/debug/plot/PlottWidget.cpp \ + ui/debug/PlotTurns.cpp \ + ui/debug/PlotWiFiScan.cpp \ + sensors/android/WiFiSensorAndroid.cpp \ + sensors/linux/WiFiSensorLinuxC.c RESOURCES += qml.qrc # Additional import path used to resolve QML modules in Qt Creator's code model -QML_IMPORT_PATH = +#QML_IMPORT_PATH = # Default rules for deployment. include(deployment.pri) @@ -46,24 +70,69 @@ HEADERS += \ sensors/linux/WiFiSensorLinux.h \ sensors/android/WiFiSensorAndroid.h \ sensors/StepSensor.h \ + sensors/TurnSensor.h \ sensors/AccelerometerSensor.h \ + sensors/GyroscopeSensor.h \ + sensors/BarometerSensor.h \ sensors/android/AccelerometerSensorAndroid.h \ + sensors/android/GyroscopeSensorAndroid.h \ + sensors/android/BarometerSensorAndroid.h \ sensors/dummy/AccelerometerSensorDummy.h \ + sensors/dummy/GyroscopeSensorDummy.h \ + sensors/dummy/BarometerSensorDummy.h \ sensors/Sensor.h \ sensors/SensorFactory.h \ sensors/WiFiSensor.h \ misc/Debug.h \ misc/fixc11.h \ sensors/dummy/WiFiSensorDummy.h \ - map/MapView.h \ - map/Geometry.h \ - map/FloorRenderer.h \ - map/Ground.h \ lib/gpc/Polygon.h \ - map/GL.h \ - Stairs.h + Stairs.h \ + ui/map/MapView.h \ + ui/map/FloorRenderer.h \ + ui/map/gl/GL.h \ + ui/map/gl/GLHelper.h \ + ui/map/gl/GLLines.h \ + ui/map/gl/GLTriangles.h \ + ui/map/elements/Doors.h \ + ui/map/elements/Ground.h \ + ui/map/elements/Handrails.h \ + ui/map/elements/Path.h \ + ui/map/elements/Stairs.h \ + ui/map/elements/Walls.h \ + ui/Icons.h \ + ui/MainWindow.h \ + Controller.h \ + ui/menu/MainMenu.h \ + Config.h \ + ui/dialog/LoadSetupDialog.h \ + ui/debug/plot/Axes.h \ + ui/debug/plot/Plot.h \ + ui/debug/plot/Data.h \ + ui/debug/plot/Range.h \ + nav/NavController.h \ + sensors/dummy/RandomSensor.h \ + ui/debug/SensorDataWidget.h \ + ui/debug/plot/PlottWidget.h \ + ui/debug/PlotTurns.h \ + ui/debug/PlotWiFiScan.h \ + nav/State.h \ + nav/Filter.h \ + nav/Node.h \ + sensors/linux/WiFiSensorLinuxC.h \ + ui/map/gl/GLPoints.h \ + ui/map/elements/ColorPoints.h \ + sensors/offline/SensorFactoryOffline.h \ + sensors/dummy/SensorFactoryDummy.h \ + sensors/android/SensorFactoryAndroid.h \ + ui/map/gl/Shader.h \ + ui/map/elements/Object.h \ + Settings.h \ + nav/RegionalResampling.h \ + sensors/offline/AllInOneSensor.h DISTFILES += \ android-sources/src/MyActivity.java \ res/gl/vertex1.glsl \ - res/gl/fragment1.glsl + res/gl/fragment1.glsl \ + res/gl/tex/empty_normals.jpg