/*
IGO8 Track Format
Copyright (C) 2008 Dustin Johnson, Dustin@Dustinj.us
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 2 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, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
July 26, 2008 - Dustin: Added tracknum, title, and description options
July 26, 2008 - Dustin: Validated the new code for char to Unicode conversion
*/
/*
iGo8 (*.trk) Format Overview
|------------------------------| <--\
| ID Block (20B) | |
|------------------------------| |
| | |
| |
| | H
| | e
| | a
| Description Block (256B) | d
| | e
| | r
| |
| | |
| | |
| | |
|------------------------------| <--/
| Information Block (12B) |
|------------------------------|
| Waypoint 1 |
|------------------------------|
| Waypoint 2 |
|------------------------------|
| Waypoint 3 |
|------------------------------|
| ... |
|------------------------------|
ID Block: defined by igo8_id_block
Description Block: Two null-terminated unicode 2 strings.
The first is the title of the track,
the second is the description.
Information Block: defined by igo8_information_block
Waypoint: defined by igo8_point
*/
#include <cstdio> // for SEEK_SET
#include <cstdint>
#include <cstdlib> // for atoi
#include <cstring> // for memset
#include <QChar> // for QChar
#include <QString> // for QString
#include <QVector> // for QVector
#include "defs.h"
#include "gbfile.h" // for gbfwrite, gbfclose, gbfseek, gbfgetint32, gbfread, gbfile, gbfopen_le
#include "src/core/datetime.h" // for DateTime
#define FLOAT_TO_INT(x) ((int)((x) + ((x)<0?-0.5:0.5)))
#define IGO8_HEADER_SIZE (sizeof(igo8_id_block) + 256)
#define MYNAME "IGO8"
struct igo8_id_block {
uint32_t unknown_1;
uint32_t unknown_2;
uint32_t unknown_3;
uint32_t track_number;
uint32_t unknown_4;
};
using p_igo8_id_block = igo8_id_block*;
struct igo8_information_block {
uint32_t start_time; // In Unix time
uint32_t zero; // Doesn't appear to serve a purpose
uint32_t total_file_size; // In bytes
};
using p_igo8_information_block = igo8_information_block*;
struct igo8_point {
uint32_t unix_time;
uint32_t lon;
uint32_t lat;
};
using p_igo8_point = igo8_point*;
// Files
static gbfile* igo8_file_in;
static gbfile* igo8_file_out;
// Options
static char* igo8_option_tracknum = nullptr;
static char* igo8_option_title = nullptr;
static char* igo8_option_description = nullptr;
// Internal state
static uint32_t invented_time;
static uint32_t point_count;
static int in_point_count;
// Exported options list
static QVector<arglist_t> igo8_options = {
{ "tracknum", &igo8_option_tracknum, "Track identification number", nullptr, ARGTYPE_INT, ARG_NOMINMAX, nullptr },
{ "title", &igo8_option_title, "Track title", nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr },
{ "description", &igo8_option_description, "Track description", nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr },
};
// Sanity check
static void igo8_check_type_sizes()
{
if constexpr(sizeof(igo8_point) != 12) {
fatal(MYNAME ": igo8_point is %ld bytes instead of the required 12.\n",
(long) sizeof(igo8_point));
}
if constexpr(sizeof(igo8_information_block) != 12) {
fatal(MYNAME ": igo8_information_block is %ld bytes instead of the required 12.\n",
(long) sizeof(igo8_information_block));
}
if constexpr(sizeof(igo8_id_block) != 20) {
fatal(MYNAME ": igo8_id_block is %ld bytes instead of the required 20.\n",
(long) sizeof(igo8_id_block));
}
}
// Reader initialization callback
static void igo8_read_init(const QString& fname)
{
igo8_file_in = gbfopen_le(fname, "rb", MYNAME);
// Make sure that we are in the environment we expect and require
igo8_check_type_sizes();
// Seek past the header and most of the Information Block. Read
// the last word for trackpoint count since latest igo8 seems to
// zero-pad the files.
gbfseek(igo8_file_in, IGO8_HEADER_SIZE + sizeof(igo8_information_block) - 4, SEEK_SET);
in_point_count = (gbfgetint32(igo8_file_in) - IGO8_HEADER_SIZE -
sizeof(igo8_information_block)) / sizeof(igo8_point);
}
// Reader callback
static void igo8_read()
{
igo8_point point;
auto* track_head = new route_head;
track_add_head(track_head);
while (in_point_count &&
gbfread(&point, sizeof(point), 1, igo8_file_in) > 0) {
in_point_count--;
auto* wpt_tmp = new Waypoint;
wpt_tmp->latitude = le_read32(&point.lat) / (double)0x800000;
wpt_tmp->longitude = le_read32(&point.lon) / (double)0x800000;
wpt_tmp->SetCreationTime(le_read32(&point.unix_time));
track_add_wpt(track_head, wpt_tmp);
}
}
// Reader close callback
static void igo8_read_deinit()
{
gbfclose(igo8_file_in);
}
// Writer initialize callback
static void igo8_write_init(const QString& fname)
{
igo8_file_out = gbfopen_le(fname, "wb", MYNAME);
igo8_check_type_sizes();
invented_time = 1;
point_count = 0;
}
// Writer close callback
static void igo8_write_deinit()
{
uint32_t normalized_file_size;
// Seek to the start of the third long in the Information Block, this is
// where we will write out the total size of the file.
gbfseek(igo8_file_out, IGO8_HEADER_SIZE + sizeof(uint32_t)*2, SEEK_SET);
// The total size of the file is the number of points written + Information block + Header
le_write32(&normalized_file_size, sizeof(igo8_point)*(point_count) + sizeof(igo8_information_block) + IGO8_HEADER_SIZE);
// Write the size
gbfwrite(&normalized_file_size, sizeof(normalized_file_size), 1, igo8_file_out);
gbfclose(igo8_file_out);
}
// Write point callback
static void write_igo8_track_point(const Waypoint* wpt)
{
igo8_point point;
memset(&point, 0, sizeof(point));
// iGo8 appears to expect a time, if one isn't provided
// then we shall make our own, where each point is one
// second apart.
if (wpt->creation_time.isValid()) {
le_write32(&point.unix_time, wpt->GetCreationTime().toTime_t());
} else {
le_write32(&point.unix_time, invented_time++);
}
// Write the first part of the Information Block, the start time
if (point_count == 0) {
gbfwrite(&point, sizeof(point), 1, igo8_file_out);
}
le_write32(&point.lon, FLOAT_TO_INT(wpt->longitude * 0x800000));
le_write32(&point.lat, FLOAT_TO_INT(wpt->latitude * 0x800000));
gbfwrite(&point, sizeof(point), 1, igo8_file_out);
// Count the number of point printed, we will use this at the end to
// finish filling out the Information Block.
point_count++;
}
// Write src unicode str to the dst cstring using unicode characters.
// dst_max_length is in bytes.
// I have no idea if iGo8 even supports real unicode 2, but is does look like
// it as every ascii character is a short with the ascii character as the
// least significant 7 bits.
static unsigned int print_unicode(char* dst, int dst_max_length, const QString& src)
{
int max_qchars = dst_max_length / 2;
if (max_qchars < 1) {
// We must have room for the terminator.
fatal(MYNAME ": igo8 header overflow.\n");
}
// Write as many characters from the source as possible
// while leaving space for a terminator.
int n_src_qchars = src.size() > (max_qchars - 1) ? max_qchars - 1 : src.size();
for (int i = 0; i < n_src_qchars; ++i) {
le_write16(dst, src.at(i).unicode());
dst += 2;
}
le_write16(dst, 0); // NULL (U+0000) terminator
return (n_src_qchars + 1) * 2;
}
static void write_header()
{
char header[IGO8_HEADER_SIZE] = {};
igo8_id_block tmp_id_block;
auto* id_block = (p_igo8_id_block)header;
uint32_t current_position = 0;
const char* title = "Title";
const char* description = "Description";
// These values seem to be constant for me, but I have no idea what they are.
tmp_id_block.unknown_1 = 0x0000029B;
tmp_id_block.unknown_2 = 0x000003E7;
tmp_id_block.unknown_3 = 0x00000003;
// This appears to be a unique number that IDs the track.
// It is mono-incrementing and offset by 2 above the track number.
// e.g. "Track 1" --> track_number = 3
// XXX - Dustin: My guess is that this number is used as the key for the track color, if
// XXX - Dustin: multiple tracks have the same color they will be colored the same, just
// XXX - Dustin: a guess though.
if (igo8_option_tracknum) {
tmp_id_block.track_number = atoi(igo8_option_tracknum);
} else {
tmp_id_block.track_number = 0x00000010;
}
tmp_id_block.unknown_4 = 0x00000001;
// Byte swap out to the header buffer.
le_write32(&id_block->unknown_1, tmp_id_block.unknown_1);
le_write32(&id_block->unknown_2, tmp_id_block.unknown_2);
le_write32(&id_block->unknown_3, tmp_id_block.unknown_3);
le_write32(&id_block->track_number, tmp_id_block.track_number);
le_write32(&id_block->unknown_4, tmp_id_block.unknown_4);
// Move past the ID block, we have just filled it.
current_position += sizeof(*id_block);
// Set the title of the track
// Note: we shorten the length of the potential title by 2 because we need to leave at
// least enough room to have a null for the description string that follows it.
if (igo8_option_title) {
title = igo8_option_title;
}
current_position += print_unicode((header+current_position), IGO8_HEADER_SIZE - current_position - 2, title);
// Set the description of the track
if (igo8_option_description) {
description = igo8_option_description;
}
current_position += print_unicode((header+current_position), IGO8_HEADER_SIZE - current_position, description);
gbfwrite(&header, IGO8_HEADER_SIZE, 1, igo8_file_out);
}
// Writer callback
static void igo8_write()
{
write_header();
track_disp_all(nullptr, nullptr, write_igo8_track_point);
}
// Callback definitions
ff_vecs_t igo8_vecs = {
ff_type_file,
{ ff_cap_none, (ff_cap)(ff_cap_read | ff_cap_write), ff_cap_none },
igo8_read_init,
igo8_write_init,
igo8_read_deinit,
igo8_write_deinit,
igo8_read,
igo8_write,
nullptr,
&igo8_options,
CET_CHARSET_UTF8,
1
, NULL_POS_OPS,
nullptr
};