Panoramix - lightweight 3D view of your surroundings
This program shows an interactive 3D panorama of any place on Earth, with relevant points-of-interest (mountain peaks, etc.). It is designed to be lightweight and fast, thanks to optimized geometric algorithms implemented in C++.
Copyright (C) 2017 Guillaume Endignoux
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/gpl-3.0.txt
Configuration parameters are set in the file
The most important parameters are the following.
- API token: terrain data is downloaded from Mapbox and a token is needed to use this API.
This proof-of-concept does not include any token so it is your responsibility to acquire a token.
Once you have a token, put it in the
MAPBOX_TOKENfield of the configuration file.
- "Retina" display: for some reason, screens with "retina" display have a larger pixel resolution than coordinate resolution (size of windows, position to draw text, etc.).
This effect (observed on MacOS) must be accounted for when reading a pixel depth buffer to detect visible labels, and drawing those labels.
RETINA_FACTORof 1 on normal displays, and 2 on "retina" displays (or maybe more if you have a futuristic super-retina display). There may be a way to detect this factor at runtime, so if you find a portable API to do it your contribution is welcomed!
- Earth curvature: a simple model assumes a locally flat Earth, with Mercator coordinates simply scaled according to the origin's latitude. This approximation is already good but slightly inaccurate: an angle of 1 degree on the surface of the Earth represents ~110 kilometers. Common mountains can be seen at such a distance, and by simple geometry they should appear 0.5 degree closer to the horizon compared to the flat Earth model. You can enable a more precise model that accounts for the Earth curvature, assuming a spheric Earth (which is a good approximation of the Earth ellipsoid).
- You can also tune the Earth radius (the default is the equator's radius) and other parameters if you feel like it (but results are not guaranteed for absurd values).
Apart from the terrain data downloaded on the fly with Mapbox API, this project needs a local file containing labels (mountain peaks, mountain passes, etc.).
The program expects a file named
labels in the current directory, that follows the grammar defined in
Each label contains GPS coordinates, a name and optionally its elevation in meters (e.g. for mountain peaks), as well as a type (mountain peak, moutain pass, etc.).
labels file containing labels for Switzerland is provided in the
data/ folder (you need to decompress it with e.g.
The program expects it in the
data/labels path (relative to the current directory), so you may need to create a
data/ folder in your build directory and move the
labels file inside it.
Creating a more comprehensive
labels file that fits your need is left as an exercise.
You first need to install protocol buffers on your machine.
setup.sh to generate C++ code from
You need to have the (header-only) asio library on your machine and update the
INCLUDEPATH variable in
You also need development files for
zlib (to parse gzip-compressed HTTP streams).
The program needs to be linked with OpenSSL (or similar),
zlib shared libraries.
Instructions may vary depending on your OS, but in any case you have to modify the
LIBS variable in
Once you have installed the dependencies and updated the configuration file, you can simply compile and run the program with QtCreator.
A default view is loaded with GPS coordinates set in
You can use the Open menu item and enter GPS coordinates to create a new view.
To move in the 3D scene, use the following controls:
- zoom: mouse wheel, or keys I and O,
- turn camera: mouse drag and drop, or arrow left/right,
- move forward/backward (w.r.t. the ground): arrow up/down (small increments) or page up/down (large increments),
- move up/down: keys U and D (small increments) or T and B (large increments),
- go back to the ground: key G,
- move the sunlight: keys E (clockwise, i.e. towards the evening for the Northern hemisphere) and M (anti-clockwise, i.e. towards the morning).
Depending on the location, the panorama is limited to points located up to a few hundred kilometers away. Therefore, the panorama may be incomplete, as some high mountain peaks are visible from much further away.
Unfortunately for Arctic and Antarctic explorers, the view is limited to points located up to ~85 degrees of latitude (more precisely
atan(sinh(pi))), due to the Mercator projection used by the underlying API to obtain terrain data.
The user interface is implemented with Qt5 for the (very basic) windows.
The 3D scene is implemented with Qt5's frontend to OpenGL, which make it easier to manage shaders and vertex buffers, compared to the raw OpenGL API. There is a buffer for vertices, a buffer for normals and an index buffer for triangles.
Labels are drawn with Qt's 2D drawing API (more precisely with
QPainter), after reading OpenGL's depth buffer to detect visible points of interest.
At the geometric level, this program fetches terrain data from Mapbox vector tiles. More precisely, it obtains elevation data from contour lines, and converts them into a set of points with known altitude (discarding the lines). Then, a 2D Delaunay triangulation is created from these points to generate a 3D mesh of triangles. Points are then projected to the model view (either locally flat Earth, or spheric Earth), and the mesh is given to OpenGL for rendering.
To compute Delaunay triangulations, the divide-and-conquer algorithm is due to Guibas and Stolfi, with the triangle-based data structure promoted by Shewchuk.
To compute the altitude of any point on the map (in particular the current point of view), the program finds the point in the Delaunay triangulation and interpolates elevation in the triangle that contains it. A quadtree is used to speed up point lookup in the triangulation.
The program uses networking to request terrain data from Mapbox; this is implemented with the
All operations are asynchronous (
async_write, etc.), and are performed in a separate networking thread.
A very basic local cache (limited to
CACHE_LIMIT tiles) avoids redownloading the same tiles for views that overlap.
Additionally, another thread manages a queue of network requests to make sure that no more than
MAX_REQUESTS requests are sent concurrently to the terrain data server (where
MAX_REQUESTS is defined in the
Besides the UI and networking threads, a thread pool managed by asio's
io_service decodes terrain data and computes geometric structures to build the world model (Delaunay triangulation, quadtree, mesh, etc.) in the background.
More precisely, a task is spawned for each tile to fetch the terrain data from the network or from the local cache and to decode it into a set of points.
Tiles are then fed to a simple multi-producer-single-consumer queue (based on a mutex and condition variable).
A separate task flushes the queue whenever new tiles are available, and builds a new world model with the new set of tiles.
Data structures in the world model (written by the background thread pool and read by the UI thread) are guarded by mutexes, and use
std::shared_ptr to avoid contention on big data structures.
For example, the UI thread can make a copy of the shared pointer, release the mutex and start reading the world model.
In the meantime, the background thread can update the shared pointer with a new world model, that will be read by the UI thread later (e.g. at the next frame).
Note that the internal reference counting of
std::shared_ptr is thread safe.
In practice, each shared data structure is not accessed more than a few hundreds of times per second, so simple mutexes and condition variables do the job, there is no need for lock-free data structures at this stage!
This project is written in C++14, with automatic memory management via smart pointers (
std::shared_ptr) or containers (often
This itself does not remove all memory-related bugs; ASAN and UBSAN have been run on the project (and indeed uncovered some bugs, that are now fixed).
All data is downloaded via HTTPS.
TLS implementation relies on
asio::ssl::* (which is usually based on OpenSSL or similar library on your system).
In particular, certificate validation uses
This project does not roll its own crypto!
HTTP/1 parsing is implemented in a very simple way, discarding all headers except for
Content-Encoding: gzip, and reading data until the connection is closed.
GZIP parsing is a 50-line wrapper around
Other data formats are serialized with protocol buffers, and the associated parsers were generated with
Due to the amount of concurrency, data races are the most probable potential security issues. TSAN has also been run on the project (and uncovered some design issues), but it cannot detect all data races.
This project is a proof-of-concept and is not security-critical, so public disclosure of bugs is encouraged via GitHub issues.