/*
Support for Motorrad Routenplaner (Map&Guide) .bcr files.
Copyright (C) 2005-2007 Olaf Klein, o.b.klein@gpsbabel.org
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.
*/
/*
2006/01/22: reader simplified with inifile library
2007/01/30: new option prefer_shortnames
don't check global_opts.objective
2007/04&14: new handling of DESCRIPTION lines
*/
#include <cmath> // for M_PI, atan, exp, log, tan
#include <cstdio> // for printf, snprintf, sscanf
#include <cstdlib> // for atof, atoi
#include <QString> // for QString, operator+
#include <Qt> // for CaseInsensitive
#include <QtGlobal> // for foreach
#include "defs.h"
#include "csv_util.h" // for csv_stringclean
#include "garmin_tables.h" // for gt_find_desc_from_icon_number, gt_find_icon_number_from_desc, MAPSOURCE
#include "gbfile.h" // for gbfprintf, gbfclose, gbfopen, gbfile
#include "inifile.h" // for inifile_readstr, inifile_done, inifile_init, inifile_t
#define MYNAME "bcr"
#undef BCR_DEBUG
#define R_EARTH 6371000 /* radius of our big blue ball */
#define BCR_DEF_ICON "Standort"
#define BCR_DEF_MPS_ICON "Waypoint"
#define BCR_UNKNOWN /*(double) */ 999999999
/*
6371014 would be a better value when converting to f.e. to mapsoure,
but this seems to be used by Map&Guide when exporting to XML.
*/
static gbfile* fout;
static int curr_rte_num, target_rte_num;
static double radius;
static inifile_t* ini;
/* placeholders for options */
static char* rtenum_opt;
static char* rtename_opt;
static char* radius_opt;
static char* prefer_shortnames_opt;
static
QVector<arglist_t> bcr_args = {
{
"index", &rtenum_opt, "Index of route to write (if more than one in source)",
nullptr, ARGTYPE_INT, "1", nullptr, nullptr
},
{
"name", &rtename_opt, "New name for the route",
nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr
},
{
"radius", &radius_opt, "Radius of our big earth (default 6371000 meters)", "6371000",
ARGTYPE_FLOAT, ARG_NOMINMAX, nullptr
},
{
"prefer_shortnames", &prefer_shortnames_opt, "Use shortname instead of description",
nullptr, ARGTYPE_BOOL, ARG_NOMINMAX, nullptr
},
};
struct bcr_icon_mapping_t {
const char* bcr_name;
const char* mps_name;
const char* symbol_DE;
bool warned;
};
static
bcr_icon_mapping_t bcr_icon_mapping[] = {
{ BCR_DEF_ICON, BCR_DEF_MPS_ICON, BCR_DEF_ICON, false },
{ "", BCR_DEF_MPS_ICON, "Eigene Adressen", false },
{ "AdrMon alpen", "Summit", "Pass-Strassen", false },
{ "AdrMon bauern", nullptr, "Bauern- und Biohoefe", false },
{ "AdrMon cmpngs", "Campground", "Campingplaetzte", false },
{ "AdrMon p_aeu", "Scenic Area", "Sehenswertes", false },
{ "AdrMon p_beu", "Gas Station", "Tanken", false },
{ "AdrMon p_deu", "Parking Area", "Parken", false },
{ "AdrMon p_feu", "Restaurant", "Gastro", false },
{ "AdrMon p_geu", "Museum", "Freizeit", false },
{ "AdrMon p_heu", "Gas Station", "Tankstellen", false },
{ "AdrMon p_keu", nullptr, "Faehrverbindungen", false },
{ "AdrMon p_leu", nullptr, "Grenzuebergaenge", false },
{ "AdrMon p_teu", nullptr, "Wein- und Sektgueter", false },
{ "AdrMon RUINEN", "Ghost Town", "Burgen und Schloesser", false },
{ "AdrMon NFHAUS", "Residence", "Naturfreundehaeuser", false },
{ "AdrMon racing", "Bike Trail", "Rennstrecken", false },
{ "AdrMon TNKRST", "Bar", "Tankraststaetten", false },
{ "AdrMon tpclub", "Contact, Biker", "Motorrad-Clubs", false },
{ "AdrMon tpequ", nullptr, "Motorrad-Equipment", false },
{ "AdrMon tphot", "Hotel", "Motorrad-Hotels", false },
{ "AdrMon tpmh", nullptr, "Motorradhaendler", false },
{ "AdrMon tpss", "Restricted Area", "Sperrungen", false },
{ "AdrMon tpsw", "Scenic Area", "Sehenswertes", false },
{ "AdrMon tptref", nullptr, "Treffpunkte", false },
{ "AdrMon VORTE", "Information", "Ortsinformationen", false },
{ "AdrMon WEBCAM", nullptr, "WebCam-Standorte", false },
{ "AdrMon youthh", nullptr, "Jugendherbergen", false },
{ "Town", "City (Small)", "Orte", false },
{ nullptr, nullptr, nullptr, false }
};
static void
bcr_handle_icon_str(const char* str, Waypoint* wpt)
{
wpt->icon_descr = BCR_DEF_MPS_ICON;
for (bcr_icon_mapping_t* m = bcr_icon_mapping; (m->bcr_name); m++) {
if (case_ignore_strcmp(str, m->bcr_name) == 0) {
if (m->symbol_DE == nullptr) {
if (! m->warned) {
m->warned = true;
warning(MYNAME ": Unknown icon \"%s\" found. Please report.\n", str);
}
return;
}
wpt->description = m->symbol_DE;
if (m->mps_name != nullptr) {
int nr = gt_find_icon_number_from_desc(m->mps_name, MAPSOURCE);
wpt->icon_descr = gt_find_desc_from_icon_number(nr, MAPSOURCE);
}
return;
}
}
}
static const char*
get_bcr_icon_from_icon_descr(const QString& icon_descr)
{
const char* result = BCR_DEF_ICON;
if (!icon_descr.isNull()) {
for (bcr_icon_mapping_t* m = bcr_icon_mapping; (m->bcr_name); m++) {
if (! m->mps_name) {
continue;
}
if (icon_descr.compare(m->mps_name, Qt::CaseInsensitive) == 0) {
result = m->bcr_name;
break;
}
}
}
return result;
}
static void
bcr_init_radius()
{
if (radius_opt != nullptr) { /* preinitialize the earth radius */
radius = atof(radius_opt);
if (radius <= 0) {
fatal(MYNAME ": Sorry, the radius should be greater than zero!\n");
}
} else {
radius = (double)R_EARTH;
}
if (global_opts.verbose_status > 0) {
printf(MYNAME ": We calculate with radius %f meters.\n", radius);
}
}
static void
bcr_rd_init(const QString& fname)
{
ini = inifile_init(fname, MYNAME);
bcr_init_radius();
}
static void
bcr_rd_deinit()
{
inifile_done(ini);
}
/* ------------------------------------------------------------*/
static void
bcr_create_waypts_from_route(route_head* route)
{
foreach (const Waypoint* wpt, route->waypoint_list) {
waypt_add(new Waypoint(*wpt));
}
}
static void
bcr_wgs84_to_mercator(const double lat, const double lon, int* north, int* east)
{
double N = log(tan(lat * M_PI / 360 + M_PI / 4)) * radius;
double E = lon * radius * M_PI / 180.0;
if (lat > 0) {
N += 0.500000000001; /* we go from double to integer */
} else {
N -= 0.500000000001; /* it's time to round a little bit */
}
if (lon > 0) {
E += 0.500000000001;
} else {
E -= 0.500000000001;
}
*north = N;
*east = E;
}
static void
bcr_mercator_to_wgs84(const int north, const int east, double* lat, double* lon)
{
*lat = 2 * (atan(exp(north / radius)) - M_PI / 4) / M_PI * 180.0;
*lon = (double)east * 180.0 / (radius * M_PI);
}
/* ------------------------------------------------------------- */
static void
bcr_data_read()
{
auto* route = new route_head;
route_add_head(route);
QString routename = inifile_readstr(ini, "client", "routename");
if (!routename.isNull()) {
route->rte_name = routename;
}
for (int index = 1; ; index++) {
char station[32];
QString str;
int mlat, mlon; /* mercator data */
snprintf(station, sizeof(station), "STATION%d", index);
str = inifile_readstr(ini, "coordinates", station);
if (str.isNull()) {
break;
}
if (2 != sscanf(CSTR(str), "%d,%d", &mlon, &mlat)) {
fatal(MYNAME ": structure error at %s (Coordinates)!\n", station);
}
auto* wpt = new Waypoint;
wpt->shortname = station;
bcr_mercator_to_wgs84(mlat, mlon, &wpt->latitude, &wpt->longitude);
str = inifile_readstr(ini, "client", station);
if (!str.isNull()) {
int cx = str.indexOf(',');
if (cx < 0) {
fatal(MYNAME ": structure error at %s (Client)!\n", station);
}
bcr_handle_icon_str(CSTR(str.left(cx)), wpt);
}
str = inifile_readstr(ini, "description", station);
if (!str.isNull()) {
QString note = str.section(',', 0, 0);
if (!note.isEmpty()) {
wpt->notes = note;
}
QString shortname = str.section(',', 1, 1);
if (!shortname.isEmpty()) {
wpt->shortname = shortname;
}
}
route_add_wpt(route, wpt);
}
/* remove empty route */
if (route->rte_waypt_ct() == 0) {
route_del_head(route);
} else {
bcr_create_waypts_from_route(route);
}
}
/* %%% bcr write support %%% ----------------------------------- */
static void
bcr_wr_init(const QString& fname)
{
fout = gbfopen(fname, "wb", MYNAME);
bcr_init_radius();
}
static void
bcr_wr_deinit()
{
gbfclose(fout);
}
static void bcr_write_line(gbfile* fileout, const QString& key,
const int* index, const QString& value)
{
if (value.isEmpty()) { // Windows. Add CR/LF on output.
gbfprintf(fileout, "%s\r\n", CSTR(key));
} else {
char* tmp = (value != nullptr) ? xstrdup(value) : xstrdup("");
if (index != nullptr) {
gbfprintf(fileout, "%s%d=%s\r\n", CSTR(key), *index, tmp);
} else {
gbfprintf(fileout, "%s=%s\r\n", CSTR(key), tmp);
}
xfree(tmp);
}
}
static void
bcr_route_header(const route_head* route)
{
int north, east, nmax, emin;
curr_rte_num++;
if (curr_rte_num != target_rte_num) {
return;
}
bcr_write_line(fout, "[CLIENT]", nullptr, nullptr); /* client section */
bcr_write_line(fout, "REQUEST", nullptr, "TRUE");
QString sout = route->rte_name;
if (rtename_opt != nullptr) {
sout = rtename_opt;
}
if (sout != nullptr) {
bcr_write_line(fout, "ROUTENAME", nullptr, sout);
} else {
bcr_write_line(fout, "ROUTENAME", nullptr, "Route");
}
bcr_write_line(fout, "DESCRIPTIONLINES", nullptr, "0");
int i = 0;
foreach (const Waypoint* wpt, route->waypoint_list) {
i++;
const char* icon = get_bcr_icon_from_icon_descr(wpt->icon_descr);
sout = QString("%1,%2").arg(icon).arg(BCR_UNKNOWN,10);
bcr_write_line(fout, "STATION", &i, sout);
}
bcr_write_line(fout, "[COORDINATES]", nullptr, nullptr); /* coords section */
int nmin = emin = (1<<30);
int emax = nmax = -nmin;
i = 0;
foreach (const Waypoint* wpt, route->waypoint_list) {
i++;
bcr_wgs84_to_mercator(wpt->latitude, wpt->longitude, &north, &east);
if (north > nmax) {
nmax = north;
}
if (east > emax) {
emax = east;
}
if (north < nmin) {
nmin = north;
}
if (east < emin) {
emin = east;
}
sout = QString::number(east) + "," + QString::number(north);
bcr_write_line(fout, "STATION", &i, sout);
}
bcr_write_line(fout, "[DESCRIPTION]", nullptr, nullptr); /* descr. section */
i = 0;
foreach (const Waypoint* wpt, route->waypoint_list) {
QString s2;
i++;
QString s1 = wpt->notes;
if (s1.isEmpty()) {
s1 = wpt->description;
}
if (prefer_shortnames_opt || (s1.isEmpty())) {
s2 = s1;
s1 = wpt->shortname;
} else {
s2 = wpt->shortname;
}
if (s1.isEmpty()) {
s1 = QString();
} else {
s1 = csv_stringclean(s1, ",\t\r\n");
}
if (s2.isEmpty()) {
s2 = QString();
} else {
s2 = csv_stringclean(s2, ",\t\r\n");
}
if (sout.isEmpty()) {
sout = QString("%1,%2,@,0").arg(s1, s1);
} else {
sout = QString("%1,%2,@,0").arg(s1, s2);
}
bcr_write_line(fout, "STATION", &i, sout);
}
bcr_write_line(fout, "[ROUTE]", nullptr, nullptr); /* route section */
sout = QString::number(emin) + "," +
QString::number(nmax) + "," +
QString::number(emax) + "," +
QString::number(nmin);
bcr_write_line(fout, "ROUTERECT", nullptr, sout);
}
static void
bcr_data_write()
{
target_rte_num = 1;
if (rtenum_opt != nullptr) {
target_rte_num = atoi(rtenum_opt);
if (((unsigned)target_rte_num > route_count()) || (target_rte_num < 1))
fatal(MYNAME ": invalid route number %d (1..%d))!\n",
target_rte_num, route_count());
}
curr_rte_num = 0;
route_disp_all(bcr_route_header, nullptr, nullptr);
}
ff_vecs_t bcr_vecs = {
ff_type_file,
{ ff_cap_none, ff_cap_none, (ff_cap)(ff_cap_read | ff_cap_write)},
bcr_rd_init,
bcr_wr_init,
bcr_rd_deinit,
bcr_wr_deinit,
bcr_data_read,
bcr_data_write,
nullptr,
&bcr_args,
CET_CHARSET_MS_ANSI, 0, /* CET-REVIEW */
NULL_POS_OPS,
nullptr
};