Skip to content

Save GPX

In this guide you will learn how to save map matched improved positions obtained during a real or simulated navigation, in GPX format.

Save GPX - cpp example screenshot

Setup

First, get an API key token, see the Getting Started guide.

Prerequisites

It is required that you complete the Environment Setup - CPP Examples guide before starting this guide.

Setting your API key token

To set your API key token, define it like this:

#define API_TOKEN "YOUR_API_KEY_TOKEN"

replacing YOUR_API_KEY_TOKEN with your actual API key token text, within the quotes.

This can be done in the main() function, before the following line:

#if defined(API_TOKEN)

or outside the main() function, further up, or in a header file.

Although it is also possible to define your API key token as the text value of the GEM_TOKEN environment variable, this is not recommended, as it is not secure.

Build and run

In Visual Studio, right-click on the SaveGPX project, and select Set as Startup Project then press F5 to run.

How it works

 1int main( int argc, char** argv )
 2{
 3   std::string projectApiToken = "";
 4#define API_TOKEN "YOUR_API_KEY_TOKEN"
 5#if defined(API_TOKEN)
 6   projectApiToken = std::string( API_TOKEN );
 7#endif
 8   // Sdk objects can be created & used below this line
 9   Environment::SdkSession session(projectApiToken, { argc > 1 ? argv[1] : "" });
10
11   // Create an interactive map view
12   CTouchEventListener pTouchEventListener;
13   gem::StrongPointer<gem::MapView> mapView = gem::MapView::produce(session.produceOpenGLContext(
14      Environment::WindowFrameworks::ImGUI, "SaveGPX", &pTouchEventListener, getUiRender()));
15   if ( !mapView )
16   {
17      GEM_LOGE( "Error creating gem::MapView: %d", GEM_GET_API_ERROR() );
18   }
19   WAIT_UNTIL_WINDOW_CLOSE();
20   return 0;
21}

First, the API key token is set.

Next, the SDK environment is initialized, passing in the API key token string, and the SDK API debug logging path.
Environment::SdkSession session(projectApiToken, { argc > 1 ? argv[1] : "" });
The debug logging path is set from the first command line argument, if there is one, otherwise it can be given as a hardcoded string.
A touch event listener is instantiated, to support touch input for the interactive map, such as pan and zoom.
CTouchEventListener pTouchEventListener;
The MapView interactive map instance is created, using an OpenGL context for rendering, and selecting ImGui for the graphic control interface.
gem::StrongPointer<gem::MapView> mapView = gem::MapView::produce(session.produceOpenGLContext(Environment::WindowFrameworks::ImGUI,
The window title is specified, and the touch event listener instanced is passed in.
"SaveGPX", &pTouchEventListener, getUiRender()));

Finally, the getUiRender() function is passed in, which uses ImGui to render GUI elements, capture user control input, and call the appropriate SDK functions.

A custom recorder listener derived from gem::IProgressListener is instantiated to receive GPX export events, such as started, and completed:

 1class RecorderListener : public gem::IProgressListener
 2{
 3public:
 4 RecorderListener( const gem::StringRef dstGPXTracksPath )
 5 {
 6     gpxRecorderBookmarks = gem::RecorderBookmarks::produce( dstGPXTracksPath );
 7 }
 8 void notifyStart( bool hasProgress ) override
 9 {
10 }
11 void notifyComplete( int reason, gem::String recordPath ) override
12 {
13     GEM_LOGI( "Recorder stopped with code = %d", reason );
14     if( reason == gem::KNoError )
15     {
16         if( auto error = gpxRecorderBookmarks->exportLog( recordPath, gem::EFileType::Gpx ) == gem::KNoError )
17         {
18             GEM_LOGI( "Successfully exported log %s to gpx.", recordPath);
19         }
20         else
21         {
22             GEM_LOGE( "Could not export log %s to gpx. Error code = %d", recordPath, error );
23         }
24     }
25 }
26private:
27 gem::StrongPointer<gem::RecorderBookmarks> gpxRecorderBookmarks;
28};

The UI render function:

 1auto getUiRender()
 2{
 3 auto sdkExamplesPath = Environment::GetInstance().GetSDKExamplesPath();
 4 auto sdkCachePath = Environment::GetInstance().GetCachePath();
 5
 6 auto dstCacheResPath = gem::FileSystem().makePath(sdkCachePath.c_str(), u"Data", u"Res/");
 7 gem::FileSystem().createFolder(dstCacheResPath, true);
 8
 9 auto srcNMEAPath = gem::FileSystem().makePath(sdkExamplesPath.c_str(), u"Examples", u"Interactive", u"SaveGPX", u"strasbourg.nmea");
10 int ret;
11 if ((ret = gem::FileSystem().copyFile(srcNMEAPath, dstCacheResPath)) != gem::KNoError)
12 {
13     GEM_LOGE("Error copy NMEA resource (%d)", GEM_GET_API_ERROR());
14 }
15 srcNMEAPath = gem::FileSystem().makePath(dstCacheResPath, "strasbourg.nmea");
16
17 auto dstGPXTracksPath = gem::FileSystem().makePath(sdkCachePath, u"Data", u"Tracks/");
18 gem::FileSystem().createFolder(dstGPXTracksPath, true);
19
20 auto dstLogsPath = gem::FileSystem().makePath(dstGPXTracksPath, "GPSLogs/");
21 gem::FileSystem().createFolder(dstLogsPath, true);
22
23 std::pair<gem::sense::DataSourcePtr, int> dataSource = gem::sense::produceLogDataSource(srcNMEAPath);
24 gem::PositionService().setDataSource( dataSource.first );
25
26 gem::sense::DataTypeList datatypes;
27 datatypes.push_back(gem::sense::EDataType::Position);
28 datatypes.push_back(gem::sense::EDataType::ImprovedPosition);
29
30 gem::RecorderConfigurationPtr gpxRecorderConfigs = gem::StrongPointerFactory<gem::RecorderConfiguration>();
31 gpxRecorderConfigs->logsDir = dstLogsPath;
32 gpxRecorderConfigs->dataSource = dataSource.first;
33 gpxRecorderConfigs->recordedTypes = datatypes;
34 gpxRecorderConfigs->minDurationSeconds = 10;
35 gpxRecorderConfigs->chunkDurationSeconds = 60;
36 gpxRecorderConfigs->bContinuousRecording = true;
37 gpxRecorderConfigs->deleteOlderThanKeepMin = false;
38 gpxRecorderConfigs->keepMinSeconds = 3600;
39
40 gem::StrongPointer<RecorderListener> recorderListener = gem::StrongPointerFactory<RecorderListener>(dstGPXTracksPath);
41
42 gem::StrongPointer<gem::Recorder> gpxRecorder = gem::Recorder::produce(gpxRecorderConfigs);
43 if( gpxRecorder )
44 {
45     gpxRecorder->addListener( recorderListener );
46 }

The custom GUI function creates the SDK/Data/Tracks/ and SDK/Data/Tracks/GPSLogs/ directories, where SDK is the SDK install directory itself; the final output .gpx files are saved in the Tracks directory, and internal format .gm files, used to generate the .gpx files, are in the GPSLogs directory. The .gm files may be deleted after the .gpx file is saved. An NMEA data source is created from a pre-recorded navigation, included with this example, using gem::sense::produceLogDataSource() after the .nmea file is copied to the Data/Res/ directory within the SDK install directory.

The recorder is configured as follows:

A gem::sense::DataTypeList is instantiated, and the data types to be recorded are pushed into the list/vector.
Note that it is mandatory to add gem::sense::EDataType::Position because there can be no navigation without this data type. All other data types in the gem::sense::EDataType enum are optional.
A recorder configuration is instantiated, and the data types configured above are set, like this: gem::StrongPointerFactory<gem::RecorderConfiguration>()
Note that the minDurationSeconds parameter indicates, in this example, that at least 10 seconds of navigation must be recorded in order for the .gpx output file to be saved.

The lambda function within the UI render function:

 1 return std::bind([dataSource, gpxRecorder](gem::StrongPointer<gem::MapView> mapView)
 2 {
 3     ImGuiIO& io = ImGui::GetIO();
 4     const ImGuiViewport* main_viewport = ImGui::GetMainViewport();
 5     ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x + 0, main_viewport->WorkPos.y + 20), ImGuiCond_FirstUseEver);
 6     ImGui::Begin("panel", nullptr, ImGuiWindowFlags_NoMove
 7         | ImGuiWindowFlags_NoDecoration
 8         | ImGuiWindowFlags_AlwaysAutoResize
 9         | ImGuiWindowFlags_NoSavedSettings);
10     if (gpxRecorder)
11     {
12         auto recstat = gpxRecorder.get()->getStatus();
13         char* str = "UNDEFINED";
14         switch (recstat)
15         {
16         case gem::ERecorderStatus::Restarting:
17             str = "gem::ERecorderStatus::Restarting"; break;
18         case gem::ERecorderStatus::Starting:
19             str = "gem::ERecorderStatus::Starting"; break;
20         case gem::ERecorderStatus::Recording:
21             str = "gem::ERecorderStatus::Recording"; break;
22         case gem::ERecorderStatus::Stopped:
23             str = "gem::ERecorderStatus::Stopped"; break;
24         case gem::ERecorderStatus::Stopping:
25             str = "gem::ERecorderStatus::Stopping"; break;
26         }
27         GEM_LOGE("Recorder status: %s ( %d )", str, recstat);
28
29         bool recStopped = gem::ERecorderStatus::Stopped == gpxRecorder.get()->getStatus();
30         ImGui::Spacing();
31         ImGui::BeginDisabled(!recStopped);
32         if (ImGui::Button("Start recording"))
33         {
34             gpxRecorder.get()->startRecording();
35             mapView->startFollowingPosition();
36         }
37         ImGui::EndDisabled();
38
39         bool recRecording = gem::ERecorderStatus::Recording == gpxRecorder.get()->getStatus();
40         ImGui::Spacing();
41         ImGui::BeginDisabled(!recRecording);
42         if (ImGui::Button("Stop recording"))
43         {
44             gpxRecorder.get()->stopRecording();
45         }
46         ImGui::EndDisabled();
47     }
48     bool shouldFollowPosition = gem::PositionService().getDataSource() ? !mapView->isFollowingPosition() : false;
49     ImGui::Spacing();
50     ImGui::BeginDisabled(!shouldFollowPosition);
51     if (ImGui::Button("Follow position"))
52     {
53         mapView->startFollowingPosition();
54     }
55     ImGui::EndDisabled();
56     ImGui::End();
57 } , std::placeholders::_1); }

Outside the lambda function, the paths are defined, and the NMEA input file is read, and the recorder is configured. Inside the lambda function:

First, the main ImGui viewport is obtained, and the x,y position of the ImGui window, with the identifier “panel”, in pixels, is set within the SDK OpenGL viewport.

The ImGui graphical control elements are within the ImGui window/panel, rendered on top of the SDK OpenGL viewport, which contains the interactive map. The ImGui panel is created using the ImGui::Begin() function, and the ImGui elements are defined each cycle to build the UI in the block ending with the ImGui::End() function call.

There is a button to start recording, using gpxRecorder.get()->startRecording() a button to stop recording, using gpxRecorder.get()->stopRecording() and a button to resume following position, using mapView->startFollowingPosition() in case the map was panned, thus causing the camera to no longer follow the position indicator arrow. The status of the recorder, given as an gem::ERecorderStatus enum, is obtained using gpxRecorder.get()->getStatus() and printed in the console output debug log.

C++ Examples

Maps SDK for C++ Examples can be downloaded or cloned with Git