Save GPX ¶
|
Setup ¶
Prerequisites ¶
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.
Environment::SdkSession
session(projectApiToken,
{
argc
>
1
?
argv[1]
:
""
});
CTouchEventListener
pTouchEventListener;
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,
"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:
gem::sense::DataTypeList
is instantiated, and the data types to be recorded are pushed into the list/vector.
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.
gem::StrongPointerFactory<gem::RecorderConfiguration>()
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.
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.