1 /*
2 	Support for Google Earth & Keyhole "kml" format.
3 
4 	Copyright (C) 2005-2013 Robert Lipe, robertlipe+source@gpsbabel.org
5 	Updates by Andrew Kirmse, akirmse at google.com
6 
7 	This program is free software; you can redistribute it and/or modify
8 	it under the terms of the GNU General Public License as published by
9 	the Free Software Foundation; either version 2 of the License, or
10 	(at your option) any later version.
11 
12 	This program is distributed in the hope that it will be useful,
13 	but WITHOUT ANY WARRANTY; without even the implied warranty of
14 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 	GNU General Public License for more details.
16 
17 	You should have received a copy of the GNU General Public License
18 	along with this program; if not, write to the Free Software
19 	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20 
21  */
22 
23 #include <cctype>                       // for tolower, toupper
24 #include <cmath>                        // for fabs
25 #include <cstdio>                       // for sscanf, printf
26 #include <cstdlib>                      // for atoi, atol, atof
27 #include <cstring>                      // for strcmp
28 #include <tuple>                        // for tuple, make_tuple, tie
29 
30 #include <QtCore/QByteArray>            // for QByteArray
31 #include <QtCore/QChar>                 // for QChar
32 #include <QtCore/QDate>                 // for QDate
33 #include <QtCore/QDateTime>             // for QDateTime
34 #include <QtCore/QFile>                 // for QFile
35 #include <QtCore/QIODevice>             // for operator|, QIODevice, QIODevice::Text, QIODevice::WriteOnly
36 #include <QtCore/QList>                 // for QList
37 #include <QtCore/QStaticStringData>     // for QStaticStringData
38 #include <QtCore/QString>               // for QString, QStringLiteral, operator+, operator!=
39 #include <QtCore/QStringList>           // for QStringList
40 #include <QtCore/QVector>               // for QVector
41 #include <QtCore/QXmlStreamAttributes>  // for QXmlStreamAttributes
42 #include <QtCore/Qt>                    // for ISODate
43 #include <QtCore/QtGlobal>              // for foreach, qint64, qPrintable
44 
45 #include "defs.h"
46 #include "kml.h"
47 #include "formspec.h"                   // for FsChainFind, kFsGpx
48 #include "grtcirc.h"                    // for RAD, gcdist, radtometers
49 #include "src/core/datetime.h"          // for DateTime
50 #include "src/core/file.h"              // for File
51 #include "src/core/logging.h"           // for Warning, Fatal
52 #include "src/core/optional.h"          // for optional
53 #include "src/core/xmlstreamwriter.h"   // for XmlStreamWriter
54 #include "src/core/xmltag.h"            // for xml_findfirst, xml_tag, fs_xml, xml_attribute, xml_findnext
55 #include "units.h"                      // for fmt_setunits, fmt_speed, fmt_altitude, fmt_distance, units_aviation, units_metric, units_nautical, units_statute
56 #include "xmlgeneric.h"                 // for cb_cdata, cb_end, cb_start, xg_callback, xg_string, xg_cb_type, xml_deinit, xml_ignore_tags, xml_init, xml_read, xg_tag_mapping
57 
58 
59 //  Icons provided and hosted by Google.  Used with permission.
60 #define ICON_BASE "https://earth.google.com/images/kml-icons/"
61 #define ICON_NOSAT ICON_BASE "youarehere-warning.png"
62 #define ICON_WPT "https://maps.google.com/mapfiles/kml/pal4/icon61.png"
63 #define ICON_TRK ICON_BASE "track-directional/track-none.png"
64 #define ICON_RTE ICON_BASE "track-directional/track-none.png"
65 #define ICON_MULTI_TRK ICON_BASE "track-directional/track-0.png"
66 #define ICON_DIR ICON_BASE "track-directional/track-%1.png" // format string where next arg is rotational degrees.
67 
68 #define MYNAME "kml"
69 
kml_init_color_sequencer(unsigned int steps_per_rev)70 void KmlFormat::kml_init_color_sequencer(unsigned int steps_per_rev)
71 {
72   if (rotate_colors) {
73     float color_step = atof(opt_rotate_colors);
74     if (color_step > 0.0f) {
75       // step around circle by given number of degrees for each track(route)
76       kml_color_sequencer.step = ((float)kml_color_limit) * 6.0f * color_step / 360.0f;
77     } else {
78       // one cycle around circle for all the tracks(routes)
79       kml_color_sequencer.step = ((float)kml_color_limit) * 6.0f / ((float)steps_per_rev);
80     }
81     kml_color_sequencer.color.opacity=255;
82     kml_color_sequencer.seq = 0.0f;
83   }
84 }
85 
kml_step_color()86 void KmlFormat::kml_step_color()
87 {
88   // Map kml_color_sequencer.seq to an integer in the range [0, kml_color_limit*6).
89   // Note that color_seq may be outside this range if the cast from float to int fails.
90   int color_seq = ((int) kml_color_sequencer.seq) % (kml_color_limit * 6);
91   if (global_opts.debug_level >= 1) {
92     printf(MYNAME ": kml_color_sequencer seq %f %d, step %f\n",kml_color_sequencer.seq, color_seq, kml_color_sequencer.step);
93   }
94   if ((color_seq >= (0*kml_color_limit)) && (color_seq < (1*kml_color_limit))) {
95     kml_color_sequencer.color.bbggrr = (0)<<16 | (color_seq)<<8 | (kml_color_limit);
96   } else if ((color_seq >= (1*kml_color_limit)) && (color_seq < (2*kml_color_limit))) {
97     kml_color_sequencer.color.bbggrr = (0)<<16 | (kml_color_limit)<<8 | (2*kml_color_limit-color_seq);
98   } else if ((color_seq >= (2*kml_color_limit)) && (color_seq < (3*kml_color_limit))) {
99     kml_color_sequencer.color.bbggrr = (color_seq-2*kml_color_limit)<<16 | (kml_color_limit)<<8 | (0);
100   } else if ((color_seq >= (3*kml_color_limit)) && (color_seq < (4*kml_color_limit))) {
101     kml_color_sequencer.color.bbggrr = (kml_color_limit)<<16 | (4*kml_color_limit-color_seq)<<8 | (0);
102   } else if ((color_seq >= (4*kml_color_limit)) && (color_seq < (5*kml_color_limit))) {
103     kml_color_sequencer.color.bbggrr = (kml_color_limit)<<16 | (0)<<8 | (color_seq-4*kml_color_limit);
104   } else if ((color_seq >= (5*kml_color_limit)) && (color_seq < (6*kml_color_limit))) {
105     kml_color_sequencer.color.bbggrr = (6*kml_color_limit-color_seq)<<16 | (0)<<8 | (kml_color_limit);
106   } else { // should not occur, but to be safe generate a legal color.
107     warning(MYNAME ": Error in color conversion - using default color.\n");
108     kml_color_sequencer.color.bbggrr = (102)<<16 | (102)<<8 | (102);
109   }
110   // compute next color.
111   kml_color_sequencer.seq = kml_color_sequencer.seq + kml_color_sequencer.step;
112 }
113 
114 const char* KmlFormat::kml_tags_to_ignore[] = {
115   "kml",
116   "Document",
117   "Folder",
118   nullptr
119 };
120 
121 const char* KmlFormat::kml_tags_to_skip[] = {
122   "Camera",
123   "LookAt",
124   "styleUrl",
125   "snippet",
126   nullptr
127 };
128 
wpt_s(xg_string,const QXmlStreamAttributes *)129 void KmlFormat::wpt_s(xg_string /*args*/, const QXmlStreamAttributes* /*attrs*/)
130 {
131   if (wpt_tmp) {
132     fatal(MYNAME ": wpt_s: invalid kml file\n");
133   }
134   wpt_tmp = new Waypoint;
135   wpt_tmp_queued = false;
136 
137   /* Invalidate timespan elements for a beginning Placemark,
138    * so that each Placemark has its own (or no) TimeSpan. */
139   wpt_timespan_begin = gpsbabel::DateTime();
140   wpt_timespan_end = gpsbabel::DateTime();
141 }
142 
wpt_e(xg_string,const QXmlStreamAttributes *)143 void KmlFormat::wpt_e(xg_string /*args*/, const QXmlStreamAttributes* /*attrs*/)
144 {
145   if (!wpt_tmp) {
146     fatal(MYNAME ": wpt_e: invalid kml file\n");
147   }
148   if (wpt_tmp_queued) {
149     waypt_add(wpt_tmp);
150     wpt_tmp = nullptr;
151   } else {
152     delete wpt_tmp;
153     wpt_tmp = nullptr;
154   }
155   wpt_tmp_queued = false;
156 }
157 
wpt_name(xg_string args,const QXmlStreamAttributes *)158 void KmlFormat::wpt_name(xg_string args, const QXmlStreamAttributes* /*attrs*/)
159 {
160   if (!wpt_tmp) {
161     fatal(MYNAME ": wpt_name: invalid kml file\n");
162   }
163   wpt_tmp->shortname = args;
164 }
165 
wpt_desc(const QString & args,const QXmlStreamAttributes *)166 void KmlFormat::wpt_desc(const QString& args, const QXmlStreamAttributes* /*attrs*/)
167 {
168   if (!wpt_tmp) {
169     fatal(MYNAME ": wpt_desc: invalid kml file\n");
170   }
171   wpt_tmp->description += args.trimmed();
172 }
173 
wpt_time(xg_string args,const QXmlStreamAttributes *)174 void KmlFormat::wpt_time(xg_string args, const QXmlStreamAttributes* /*attrs*/)
175 {
176   if (!wpt_tmp) {
177     fatal(MYNAME ": wpt_time: invalid kml file\n");
178   }
179   wpt_tmp->SetCreationTime(xml_parse_time(args));
180 }
181 
wpt_ts_begin(xg_string args,const QXmlStreamAttributes *)182 void KmlFormat::wpt_ts_begin(xg_string args, const QXmlStreamAttributes* /*attrs*/)
183 {
184   wpt_timespan_begin = xml_parse_time(args);
185 }
186 
wpt_ts_end(xg_string args,const QXmlStreamAttributes *)187 void KmlFormat::wpt_ts_end(xg_string args, const QXmlStreamAttributes* /*attrs*/)
188 {
189   wpt_timespan_end = xml_parse_time(args);
190 }
191 
wpt_coord(const QString & args,const QXmlStreamAttributes *)192 void KmlFormat::wpt_coord(const QString& args, const QXmlStreamAttributes* /*attrs*/)
193 {
194   double lat, lon, alt;
195   if (! wpt_tmp) {
196     return;
197   }
198   // Alt is actually optional.
199   int n = sscanf(CSTRc(args), "%lf,%lf,%lf", &lon, &lat, &alt);
200   if (n >= 2) {
201     wpt_tmp->latitude = lat;
202     wpt_tmp->longitude = lon;
203   }
204   if (n == 3) {
205     wpt_tmp->altitude = alt;
206   }
207   wpt_tmp_queued = true;
208 }
209 
wpt_icon(xg_string args,const QXmlStreamAttributes *)210 void KmlFormat::wpt_icon(xg_string args, const QXmlStreamAttributes* /*attrs*/)
211 {
212   if (wpt_tmp)  {
213     wpt_tmp->icon_descr = args;
214   }
215 }
216 
trk_coord(xg_string args,const QXmlStreamAttributes *)217 void KmlFormat::trk_coord(xg_string args, const QXmlStreamAttributes* /*attrs*/)
218 {
219   auto* trk_head = new route_head;
220   if (wpt_tmp && !wpt_tmp->shortname.isEmpty()) {
221     trk_head->rte_name  = wpt_tmp->shortname;
222   }
223   track_add_head(trk_head);
224 
225   const auto vecs = args.simplified().split(' ');
226   for (const auto& vec : vecs) {
227     const QStringList coords = vec.split(',');
228     auto csize = coords.size();
229     auto* trkpt = new Waypoint;
230 
231     if (csize == 3) {
232       trkpt->altitude = coords[2].toDouble();
233     }
234     if (csize == 2 || csize == 3) {
235       trkpt->latitude = coords[1].toDouble();
236       trkpt->longitude = coords[0].toDouble();
237     } else {
238       Warning() << MYNAME << ": malformed coordinates " << vec;
239     }
240     track_add_wpt(trk_head, trkpt);
241   }
242 
243   /* The track coordinates do not have a time associated with them. This is specified by using:
244    *
245    * <TimeSpan>
246    *   <begin>2017-08-21T17:00:05Z</begin>
247    *   <end>2017-08-21T17:22:32Z</end>
248    * </TimeSpan>
249    *
250    * If this is specified, then SetCreationDate
251    */
252   if (wpt_timespan_begin.isValid() && wpt_timespan_end.isValid()) {
253 
254     // If there are some Waypoints, then distribute the TimeSpan to all Waypoints
255     if (trk_head->rte_waypt_ct > 0) {
256       qint64 timespan_ms = wpt_timespan_begin.msecsTo(wpt_timespan_end);
257       if (trk_head->rte_waypt_ct < 2) {
258         fatal(MYNAME ": attempt to interpolate TimeSpan with too few points.");
259       }
260       qint64 ms_per_waypoint = timespan_ms / (trk_head->rte_waypt_ct - 1);
261       foreach (Waypoint* trackpoint, trk_head->waypoint_list) {
262         trackpoint->SetCreationTime(wpt_timespan_begin);
263         wpt_timespan_begin = wpt_timespan_begin.addMSecs(ms_per_waypoint);
264       }
265     }
266   }
267 }
268 
gx_trk_s(xg_string,const QXmlStreamAttributes *)269 void KmlFormat::gx_trk_s(xg_string /*args*/, const QXmlStreamAttributes* /*attrs*/)
270 {
271   gx_trk_head = new route_head;
272   if (wpt_tmp && !wpt_tmp->shortname.isEmpty()) {
273     gx_trk_head->rte_name  = wpt_tmp->shortname;
274   }
275   if (wpt_tmp && !wpt_tmp->description.isEmpty()) {
276     gx_trk_head->rte_desc  = wpt_tmp->description;
277   }
278   track_add_head(gx_trk_head);
279   delete gx_trk_times;
280   gx_trk_times = new QList<gpsbabel::DateTime>;
281   delete gx_trk_coords;
282   gx_trk_coords = new QList<std::tuple<int, double, double, double>>;
283 }
284 
gx_trk_e(xg_string,const QXmlStreamAttributes *)285 void KmlFormat::gx_trk_e(xg_string /*args*/, const QXmlStreamAttributes* /*attrs*/)
286 {
287   // Check that for every temporal value (kml:when) in a kml:Track there is a position (kml:coord) value.
288   // Check that for every temporal value (kml:when) in a gx:Track there is a position (gx:coord) value.
289   if (gx_trk_times->size() != gx_trk_coords->size()) {
290     fatal(MYNAME ": There were more coord elements than the number of when elements.\n");
291   }
292 
293   // In KML 2.3 kml:Track elements kml:coord and kml:when elements are not required to be in any order.
294   // In gx:Track elements all kml:when elements are required to precede all gx:coord elements.
295   // For both we allow any order.  Many writers using gx:Track elements don't adhere to the schema.
296   while (!gx_trk_times->isEmpty()) {
297     auto* trkpt = new Waypoint;
298     trkpt->SetCreationTime(gx_trk_times->takeFirst());
299     double lat, lon, alt;
300     int n;
301     std::tie(n, lat, lon, alt) = gx_trk_coords->takeFirst();
302     // An empty kml:coord element is permitted to indicate missing position data;
303     // the estimated position may be determined using some interpolation method.
304     // However if we get one we will throw away the time as we don't have a location.
305     // It is not clear that coord elements without altitude are allowed, but our
306     // writer produces them.
307     if (n >= 2) {
308       trkpt->latitude = lat;
309       trkpt->longitude = lon;
310       if (n >= 3) {
311         trkpt->altitude = alt;
312       }
313       track_add_wpt(gx_trk_head, trkpt);
314     } else {
315       delete trkpt;
316     }
317   }
318 
319   if (!gx_trk_head->rte_waypt_ct) {
320     track_del_head(gx_trk_head);
321   }
322   delete gx_trk_times;
323   gx_trk_times = nullptr;
324   delete gx_trk_coords;
325   gx_trk_coords = nullptr;
326 }
327 
gx_trk_when(xg_string args,const QXmlStreamAttributes *)328 void KmlFormat::gx_trk_when(xg_string args, const QXmlStreamAttributes* /*attrs*/)
329 {
330   if (! gx_trk_times) {
331     fatal(MYNAME ": gx_trk_when: invalid kml file\n");
332   }
333   gx_trk_times->append(xml_parse_time(args));
334 }
335 
gx_trk_coord(xg_string args,const QXmlStreamAttributes *)336 void KmlFormat::gx_trk_coord(xg_string args, const QXmlStreamAttributes* /*attrs*/)
337 {
338   if (! gx_trk_coords) {
339     fatal(MYNAME ": gx_trk_coord: invalid kml file\n");
340   }
341 
342   double lat, lon, alt;
343   int n = sscanf(CSTR(args), "%lf %lf %lf", &lon, &lat, &alt);
344   if (0 != n && 2 != n && 3 != n) {
345     fatal(MYNAME ": coord field decode failure on \"%s\".\n", qPrintable(args));
346   }
347   gx_trk_coords->append(std::make_tuple(n, lat, lon, alt));
348 }
349 
rd_init(const QString & fname)350 void KmlFormat::rd_init(const QString& fname)
351 {
352   xml_init(fname, build_xg_tag_map(this, kml_map), nullptr, kml_tags_to_ignore, kml_tags_to_skip, true);
353 }
354 
read()355 void KmlFormat::read()
356 {
357   xml_read();
358 }
359 
rd_deinit()360 void KmlFormat::rd_deinit()
361 {
362   xml_deinit();
363 }
364 
wr_init(const QString & fname)365 void KmlFormat::wr_init(const QString& fname)
366 {
367   char u = 's';
368   waypt_init_bounds(&kml_bounds);
369   kml_time_min = QDateTime();
370   kml_time_max = QDateTime();
371 
372   if (opt_units) {
373     u = tolower(opt_units[0]);
374   }
375 
376   switch (u) {
377   case 's':
378     fmt_setunits(units_statute);
379     break;
380   case 'm':
381     fmt_setunits(units_metric);
382     break;
383   case 'n':
384     fmt_setunits(units_nautical);
385     break;
386   case 'a':
387     fmt_setunits(units_aviation);
388     break;
389   default:
390     fatal("Units argument '%s' should be 's' for statute units, 'm' for metric, 'n' for nautical or 'a' for aviation.\n", opt_units);
391     break;
392   }
393   /*
394    * Reduce race conditions with network read link.
395    */
396   oqfile = new gpsbabel::File(fname);
397   oqfile->open(QIODevice::WriteOnly | QIODevice::Text);
398 
399   writer = new gpsbabel::XmlStreamWriter(oqfile);
400   writer->setAutoFormattingIndent(2);
401 }
402 
403 /*
404  * The magic here is to try to ensure that posnfilename is atomically
405  * updated.
406  */
wr_position_init(const QString & fname)407 void KmlFormat::wr_position_init(const QString& fname)
408 {
409   posnfilename = fname;
410   posnfilenametmp = QString("%1-").arg(fname);
411   realtime_positioning = 1;
412   max_position_points = atoi(opt_max_position_points);
413 }
414 
wr_deinit()415 void KmlFormat::wr_deinit()
416 {
417   writer->writeEndDocument();
418   delete writer;
419   writer = nullptr;
420   oqfile->close();
421   delete oqfile;
422   oqfile = nullptr;
423 
424   if (!posnfilenametmp.isEmpty()) {
425     // QFile::rename() can't replace an existing file, so do a QFile::remove()
426     // first (which can fail silently if posnfilename doesn't exist). A race
427     // condition can theoretically still cause rename to fail... oh well.
428     QFile::remove(posnfilename);
429     QFile::rename(posnfilenametmp, posnfilename);
430   }
431 }
432 
wr_position_deinit()433 void KmlFormat::wr_position_deinit()
434 {
435 //	kml_wr_deinit();
436   posnfilename.clear();
437   posnfilenametmp.clear();
438 }
439 
440 
kml_output_linestyle(char *,int width) const441 void KmlFormat::kml_output_linestyle(char* /*color*/, int width) const
442 {
443   // Style settings for line strings
444   writer->writeStartElement(QStringLiteral("LineStyle"));
445   writer->writeTextElement(QStringLiteral("color"), opt_line_color);
446   writer->writeTextElement(QStringLiteral("width"), QString::number(width));
447   writer->writeEndElement(); // Close LineStyle tag
448 }
449 
450 
kml_write_bitmap_style_(const QString & style,const QString & bitmap,int highlighted,int force_heading) const451 void KmlFormat::kml_write_bitmap_style_(const QString& style, const QString& bitmap,
452                                         int highlighted, int force_heading) const
453 {
454   int is_track = style.startsWith("track");
455   int is_multitrack = style.startsWith("multiTrack");
456 
457   writer->writeComment((highlighted ? QStringLiteral(" Highlighted ") : QStringLiteral(" Normal ")) + style + QStringLiteral(" style "));
458   writer->writeStartElement(QStringLiteral("Style"));
459   writer->writeAttribute(QStringLiteral("id"), style + (highlighted? QStringLiteral("_h") : QStringLiteral("_n")));
460 
461   writer->writeStartElement(QStringLiteral("IconStyle"));
462   if (highlighted) {
463     writer->writeTextElement(QStringLiteral("scale"), QStringLiteral("1.2"));
464   } else {
465     if (is_track) {
466       writer->writeTextElement(QStringLiteral("scale"), QStringLiteral(".5"));
467     }
468   }
469   /* Our icons are pre-rotated, so nail them to the maps. */
470   if (force_heading) {
471     writer->writeTextElement(QStringLiteral("heading"), QStringLiteral("0"));
472   }
473   writer->writeStartElement(QStringLiteral("Icon"));
474   writer->writeTextElement(QStringLiteral("href"), bitmap);
475   writer->writeEndElement(); // Close Icon tag
476   writer->writeEndElement(); // Close IconStyle tag
477 
478   if (is_track && !highlighted) {
479     writer->writeStartElement(QStringLiteral("LabelStyle"));
480     writer->writeTextElement(QStringLiteral("scale"), QStringLiteral("0"));
481     writer->writeEndElement(); //Close LabelStyle tag
482   }
483 
484   if (is_multitrack) {
485     kml_output_linestyle(opt_line_color,
486                          highlighted ? line_width + 2 :
487                          line_width);
488   }
489 
490   writer->writeEndElement(); // Close Style tag
491 }
492 
493 /* A wrapper for the above function to emit both a highlighted
494  * and non-highlighted version of the style to allow the icons
495  * to magnify slightly on a rollover.
496  */
kml_write_bitmap_style(kml_point_type pt_type,const QString & bitmap,const QString & customstyle) const497 void KmlFormat::kml_write_bitmap_style(kml_point_type pt_type, const QString& bitmap,
498                                        const QString& customstyle) const
499 {
500   int force_heading = 0;
501   QString style;
502   switch (pt_type) {
503   case kmlpt_track:
504     style = "track";
505     break;
506   case kmlpt_route:
507     style = "route";
508     break;
509   case kmlpt_waypoint:
510     style = "waypoint";
511     break;
512   case kmlpt_multitrack:
513     style = "multiTrack";
514     break;
515   case kmlpt_other:
516     style = customstyle;
517     force_heading = 1;
518     break;
519   default:
520     fatal("kml_output_point: unknown point type");
521     break;
522   }
523 
524   kml_write_bitmap_style_(style, bitmap, 0, force_heading);
525   kml_write_bitmap_style_(style, bitmap, 1, force_heading);
526 
527   writer->writeStartElement(QStringLiteral("StyleMap"));
528   writer->writeAttribute(QStringLiteral("id"), style);
529   writer->writeStartElement(QStringLiteral("Pair"));
530   writer->writeTextElement(QStringLiteral("key"), QStringLiteral("normal"));
531   writer->writeTextElement(QStringLiteral("styleUrl"), QStringLiteral("#") + style + QStringLiteral("_n"));
532   writer->writeEndElement(); // Close Pair tag
533   writer->writeStartElement(QStringLiteral("Pair"));
534   writer->writeTextElement(QStringLiteral("key"), QStringLiteral("highlight"));
535   writer->writeTextElement(QStringLiteral("styleUrl"), QStringLiteral("#") + style + QStringLiteral("_h"));
536   writer->writeEndElement(); // Close Pair tag
537   writer->writeEndElement(); // Close StyleMap tag
538 }
539 
kml_output_timestamp(const Waypoint * waypointp) const540 void KmlFormat::kml_output_timestamp(const Waypoint* waypointp) const
541 {
542   QString time_string = waypointp->CreationTimeXML();
543   if (!time_string.isEmpty()) {
544     writer->writeStartElement(QStringLiteral("TimeStamp"));
545     writer->writeTextElement(QStringLiteral("when"), time_string);
546     writer->writeEndElement(); // Close TimeStamp tag
547   }
548 }
549 
kml_td(gpsbabel::XmlStreamWriter & hwriter,const QString & boldData,const QString & data)550 void KmlFormat::kml_td(gpsbabel::XmlStreamWriter& hwriter, const QString& boldData, const QString& data)
551 {
552   hwriter.writeCharacters(QStringLiteral("\n"));
553   hwriter.writeStartElement(QStringLiteral("tr"));
554   hwriter.writeStartElement(QStringLiteral("td"));
555   hwriter.writeTextElement(QStringLiteral("b"), boldData);
556   hwriter.writeCharacters(data);
557   hwriter.writeEndElement(); // Close td tag
558   hwriter.writeEndElement(); // Close tr tag
559 }
560 
kml_td(gpsbabel::XmlStreamWriter & hwriter,const QString & data)561 void KmlFormat::kml_td(gpsbabel::XmlStreamWriter& hwriter, const QString& data)
562 {
563   hwriter.writeCharacters(QStringLiteral("\n"));
564   hwriter.writeStartElement(QStringLiteral("tr"));
565   hwriter.writeStartElement(QStringLiteral("td"));
566   hwriter.writeCharacters(data);
567   hwriter.writeEndElement(); // Close td tag
568   hwriter.writeEndElement(); // Close tr tag
569 }
570 
571 /*
572  * Output the track summary.
573  */
kml_output_trkdescription(const route_head * header,const computed_trkdata * td) const574 void KmlFormat::kml_output_trkdescription(const route_head* header, const computed_trkdata* td) const
575 {
576   if (!td || !trackdata) {
577     return;
578   }
579 
580   QString hstring;
581   gpsbabel::XmlStreamWriter hwriter(&hstring);
582 
583   writer->writeEmptyElement(QStringLiteral("snippet"));
584 
585   writer->writeStartElement(QStringLiteral("description"));
586 
587   hwriter.writeStartElement(QStringLiteral("table"));
588   if (!header->rte_desc.isEmpty()) {
589     kml_td(hwriter, QStringLiteral("Description"), QStringLiteral(" %1 ").arg(header->rte_desc));
590   }
591   const char* distance_units;
592   double distance = fmt_distance(td->distance_meters, &distance_units);
593   kml_td(hwriter, QStringLiteral("Distance"), QStringLiteral(" %1 %2 ").arg(QString::number(distance, 'f', 1), distance_units));
594   if (td->min_alt) {
595     const char* min_alt_units;
596     double min_alt = fmt_altitude(*td->min_alt, &min_alt_units);
597     kml_td(hwriter, QStringLiteral("Min Alt"), QStringLiteral(" %1 %2 ").arg(QString::number(min_alt, 'f', 3), min_alt_units));
598   }
599   if (td->max_alt) {
600     const char* max_alt_units;
601     double max_alt = fmt_altitude(*td->max_alt, &max_alt_units);
602     kml_td(hwriter, QStringLiteral("Max Alt"), QStringLiteral(" %1 %2 ").arg(QString::number(max_alt, 'f', 3), max_alt_units));
603   }
604   if (td->min_spd) {
605     const char* spd_units;
606     double spd = fmt_speed(*td->min_spd, &spd_units);
607     kml_td(hwriter, QStringLiteral("Min Speed"), QStringLiteral(" %1 %2 ").arg(QString::number(spd, 'f', 1), spd_units));
608   }
609   if (td->max_spd) {
610     const char* spd_units;
611     double spd = fmt_speed(*td->max_spd, &spd_units);
612     kml_td(hwriter, QStringLiteral("Max Speed"), QStringLiteral(" %1 %2 ").arg(QString::number(spd, 'f', 1), spd_units));
613   }
614   if (td->max_spd && td->start.isValid() && td->end.isValid()) {
615     const char* spd_units;
616     double elapsed = td->start.msecsTo(td->end)/1000.0;
617     double spd = fmt_speed(td->distance_meters / elapsed, &spd_units);
618     if (spd > 1.0)  {
619       kml_td(hwriter, QStringLiteral("Avg Speed"), QStringLiteral(" %1 %2 ").arg(QString::number(spd, 'f', 1), spd_units));
620     }
621   }
622   if (td->avg_hrt) {
623     kml_td(hwriter, QStringLiteral("Avg Heart Rate"), QStringLiteral(" %1 bpm ").arg(QString::number(*td->avg_hrt, 'f', 1)));
624   }
625   if (td->min_hrt) {
626     kml_td(hwriter, QStringLiteral("Min Heart Rate"), QStringLiteral(" %1 bpm ").arg(QString::number(*td->min_hrt)));
627   }
628   if (td->max_hrt) {
629     kml_td(hwriter, QStringLiteral("Max Heart Rate"), QStringLiteral(" %1 bpm ").arg(QString::number(*td->max_hrt)));
630   }
631   if (td->avg_cad) {
632     kml_td(hwriter, QStringLiteral("Avg Cadence"), QStringLiteral(" %1 rpm ").arg(QString::number(*td->avg_cad, 'f', 1)));
633   }
634   if (td->max_cad) {
635     kml_td(hwriter, QStringLiteral("Max Cadence"), QStringLiteral(" %1 rpm ").arg(QString::number(*td->max_cad)));
636   }
637   if (td->start.isValid() && td->end.isValid()) {
638     kml_td(hwriter, QStringLiteral("Start Time"), td->start.toPrettyString());
639     kml_td(hwriter, QStringLiteral("End Time"), td->end.toPrettyString());
640   }
641 
642   hwriter.writeCharacters(QStringLiteral("\n"));
643   hwriter.writeEndElement(); // Close table tag
644   //hwriter.writeEndDocument(); // FIXME: it seems like we should end the doc but it causes a reference mismatch by adding a final \n
645   writer->writeCharacters(QStringLiteral("\n"));
646   writer->writeCDATA(hstring);
647   writer->writeCharacters(QStringLiteral("\n"));
648   writer->writeEndElement(); // Close description tag
649 
650   /* We won't always have times. Garmin saved tracks, for example... */
651   if (td->start.isValid() && td->end.isValid()) {
652     writer->writeStartElement(QStringLiteral("TimeSpan"));
653     writer->writeTextElement(QStringLiteral("begin"), td->start.toPrettyString());
654     writer->writeTextElement(QStringLiteral("end"), td->end.toPrettyString());
655     writer->writeEndElement(); // Close TimeSpan tag
656   }
657 }
658 
659 
kml_output_header(const route_head * header,const computed_trkdata * td) const660 void KmlFormat::kml_output_header(const route_head* header, const computed_trkdata* td) const
661 {
662   writer->writeStartElement(QStringLiteral("Folder"));
663   writer->writeOptionalTextElement(QStringLiteral("name"), header->rte_name);
664   kml_output_trkdescription(header, td);
665 
666   if (export_points && header->rte_waypt_ct > 0) {
667     // Put the points in a subfolder
668     writer->writeStartElement(QStringLiteral("Folder"));
669     writer->writeTextElement(QStringLiteral("name"), QStringLiteral("Points"));
670   }
671 }
672 
kml_altitude_known(const Waypoint * waypoint) const673 int KmlFormat::kml_altitude_known(const Waypoint* waypoint) const
674 {
675   if (waypoint->altitude == unknown_alt) {
676     return 0;
677   }
678   // We see way more data that's sourced at 'zero' than is actually
679   // precisely at 0 MSL.
680   if (fabs(waypoint->altitude) < 0.01) {
681     return 0;
682   }
683   return 1;
684 }
685 
kml_write_coordinates(const Waypoint * waypointp) const686 void KmlFormat::kml_write_coordinates(const Waypoint* waypointp) const
687 {
688   if (kml_altitude_known(waypointp)) {
689     writer->writeTextElement(QStringLiteral("coordinates"),
690                              QString::number(waypointp->longitude, 'f', precision) + QString(",") +
691                              QString::number(waypointp->latitude, 'f', precision) + QString(",") +
692                              QString::number(waypointp->altitude, 'f', 2)
693                             );
694   } else {
695     writer->writeTextElement(QStringLiteral("coordinates"),
696                              QString::number(waypointp->longitude, 'f', precision) + QString(",") +
697                              QString::number(waypointp->latitude, 'f', precision)
698                             );
699   }
700 }
701 
702 /* Rather than a default "top down" view, view from the side to highlight
703  * topo features.
704  */
kml_output_lookat(const Waypoint * waypointp) const705 void KmlFormat::kml_output_lookat(const Waypoint* waypointp) const
706 {
707   writer->writeStartElement(QStringLiteral("LookAt"));
708   writer->writeTextElement(QStringLiteral("longitude"), QString::number(waypointp->longitude, 'f', precision));
709   writer->writeTextElement(QStringLiteral("latitude"), QString::number(waypointp->latitude, 'f', precision));
710   writer->writeTextElement(QStringLiteral("tilt"), QStringLiteral("66"));
711   writer->writeEndElement(); // Close LookAt tag
712 }
713 
kml_output_positioning(bool tessellate) const714 void KmlFormat::kml_output_positioning(bool tessellate) const
715 {
716   // These elements must be output as a sequence, i.e. in order.
717   if (extrude) {
718     writer->writeTextElement(QStringLiteral("extrude"), QStringLiteral("1"));
719   }
720 
721   if (tessellate) {
722     writer->writeTextElement(QStringLiteral("tessellate"), QStringLiteral("1"));
723   }
724 
725   if (floating) {
726     writer->writeTextElement(QStringLiteral("altitudeMode"), QStringLiteral("absolute"));
727   }
728 
729 }
730 
731 /* Output something interesting when we can for route and trackpoints */
kml_output_description(const Waypoint * pt) const732 void KmlFormat::kml_output_description(const Waypoint* pt) const
733 {
734   const char* alt_units;
735 
736   if (!trackdata) {
737     return;
738   }
739 
740   QString hstring;
741   gpsbabel::XmlStreamWriter hwriter(&hstring);
742 
743   double alt = fmt_altitude(pt->altitude, &alt_units);
744 
745   writer->writeStartElement(QStringLiteral("description"));
746   hwriter.writeCharacters(QStringLiteral("\n"));
747   hwriter.writeStartElement(QStringLiteral("table"));
748 
749   kml_td(hwriter, QStringLiteral("Longitude: %1 ").arg(QString::number(pt->longitude, 'f', precision)));
750   kml_td(hwriter, QStringLiteral("Latitude: %1 ").arg(QString::number(pt->latitude, 'f', precision)));
751 
752   if (kml_altitude_known(pt)) {
753     kml_td(hwriter, QStringLiteral("Altitude: %1 %2 ").arg(QString::number(alt, 'f', 3), alt_units));
754   }
755 
756   if (pt->heartrate) {
757     kml_td(hwriter, QStringLiteral("Heart rate: %1 ").arg(QString::number(pt->heartrate)));
758   }
759 
760   if (pt->cadence) {
761     kml_td(hwriter, QStringLiteral("Cadence: %1 ").arg(QString::number(pt->cadence)));
762   }
763 
764   /* Which unit is this temp in? C? F? K? */
765   if WAYPT_HAS(pt, temperature) {
766     kml_td(hwriter, QStringLiteral("Temperature: %1 ").arg(QString::number(pt->temperature, 'f', 1)));
767   }
768 
769   if WAYPT_HAS(pt, depth) {
770     const char* depth_units;
771     double depth = fmt_distance(pt->depth, &depth_units);
772     kml_td(hwriter, QStringLiteral("Depth: %1 %2 ").arg(QString::number(depth, 'f', 1), depth_units));
773   }
774 
775   if WAYPT_HAS(pt, speed) {
776     const char* spd_units;
777     double spd = fmt_speed(pt->speed, &spd_units);
778     kml_td(hwriter, QStringLiteral("Speed: %1 %2 ").arg(QString::number(spd, 'f', 1), spd_units));
779   }
780 
781   if WAYPT_HAS(pt, course) {
782     kml_td(hwriter, QStringLiteral("Heading: %1 ").arg(QString::number(pt->course, 'f', 1)));
783   }
784 
785   /* This really shouldn't be here, but as of this writing,
786    * Earth can't edit/display the TimeStamp.
787    */
788   if (pt->GetCreationTime().isValid()) {
789     QString time_string = pt->CreationTimeXML();
790     if (!time_string.isEmpty()) {
791       kml_td(hwriter, QStringLiteral("Time: %1 ").arg(time_string));
792     }
793   }
794 
795   hwriter.writeCharacters(QStringLiteral("\n"));
796   hwriter.writeEndElement(); // Close table tag
797   hwriter.writeEndDocument();
798   writer->writeCDATA(hstring);
799   writer->writeEndElement(); // Close description tag
800 }
801 
kml_recompute_time_bounds(const Waypoint * waypointp)802 void KmlFormat::kml_recompute_time_bounds(const Waypoint* waypointp)
803 {
804   if (waypointp->GetCreationTime().isValid()) {
805     if (!(kml_time_min.isValid()) ||
806         (waypointp->GetCreationTime() < kml_time_min)) {
807       kml_time_min = waypointp->GetCreationTime();
808     }
809     if (!(kml_time_max.isValid()) ||
810         (waypointp->GetCreationTime() > kml_time_max)) {
811       kml_time_max = waypointp->GetCreationTime();
812     }
813   }
814 }
815 
kml_add_to_bounds(const Waypoint * waypointp)816 void KmlFormat::kml_add_to_bounds(const Waypoint* waypointp)
817 {
818   waypt_add_to_bounds(&kml_bounds, waypointp);
819   kml_recompute_time_bounds(waypointp);
820 }
821 
kml_output_point(const Waypoint * waypointp,kml_point_type pt_type) const822 void KmlFormat::kml_output_point(const Waypoint* waypointp, kml_point_type pt_type) const
823 {
824   QString style;
825 
826   switch (pt_type) {
827   case kmlpt_track:
828     style = "#track";
829     break;
830   case kmlpt_route:
831     style = "#route";
832     break;
833   default:
834     fatal("kml_output_point: unknown point type");
835     break;
836   }
837 
838   if (export_points) {
839     writer->writeStartElement(QStringLiteral("Placemark"));
840     if (atoi(opt_labels)) {
841       writer->writeOptionalTextElement(QStringLiteral("name"), waypointp->shortname);
842     }
843     writer->writeEmptyElement(QStringLiteral("snippet"));
844     kml_output_description(waypointp);
845     kml_output_lookat(waypointp);
846     kml_output_timestamp(waypointp);
847 
848 
849     if (opt_deficon) {
850       writer->writeStartElement(QStringLiteral("Style"));
851       writer->writeStartElement(QStringLiteral("IconStyle"));
852       writer->writeStartElement(QStringLiteral("Icon"));
853       writer->writeTextElement(QStringLiteral("href"), opt_deficon);
854       writer->writeEndElement(); // Close Icon tag
855       writer->writeEndElement(); // Close IconStyle tag
856       writer->writeEndElement(); // Close Style tag
857     } else {
858       if (trackdirection && (pt_type == kmlpt_track)) {
859         QString value;
860         if (waypointp->speed < 1) {
861           value = QString("%1-none").arg(style);
862         } else {
863           value = QString("%1-%2").arg(style)
864                   .arg((int)(waypointp->course / 22.5 + .5) % 16);
865         }
866         writer->writeTextElement(QStringLiteral("styleUrl"), value);
867       } else {
868         writer->writeTextElement(QStringLiteral("styleUrl"), style);
869       }
870     }
871 
872     writer->writeStartElement(QStringLiteral("Point"));
873     kml_output_positioning(false);
874     kml_write_coordinates(waypointp);
875     writer->writeEndElement(); // Close Point tag
876 
877     writer->writeEndElement(); // Close Placemark tag
878   }
879 }
880 
kml_output_tailer(const route_head * header)881 void KmlFormat::kml_output_tailer(const route_head* header)
882 {
883 
884   if (export_points && header->rte_waypt_ct > 0) {
885     writer->writeEndElement(); // Close Folder tag
886   }
887 
888   // Add a linestring for this track?
889   if (export_lines && header->rte_waypt_ct > 0) {
890     int needs_multigeometry = 0;
891 
892     foreach (const Waypoint* tpt, header->waypoint_list) {
893       int first_in_trk = tpt == header->waypoint_list.front();
894       if (!first_in_trk && tpt->wpt_flags.new_trkseg) {
895         needs_multigeometry = 1;
896         break;
897       }
898     }
899     writer->writeStartElement(QStringLiteral("Placemark"));
900     writer->writeTextElement(QStringLiteral("name"), QStringLiteral("Path"));
901     if (!rotate_colors) {
902       writer->writeTextElement(QStringLiteral("styleUrl"), QStringLiteral("#lineStyle"));
903     }
904     if (header->line_color.bbggrr >= 0 || header->line_width >= 0 || rotate_colors) {
905       writer->writeStartElement(QStringLiteral("Style"));
906       writer->writeStartElement(QStringLiteral("LineStyle"));
907       if (rotate_colors) {
908         kml_step_color();
909         writer->writeTextElement(QStringLiteral("color"), QStringLiteral("%1%2")
910                                  .arg(kml_color_sequencer.color.opacity, 2, 16, QChar('0')).arg(kml_color_sequencer.color.bbggrr, 6, 16, QChar('0')));
911         writer->writeTextElement(QStringLiteral("width"), opt_line_width);
912       } else {
913         if (header->line_color.bbggrr >= 0) {
914           writer->writeTextElement(QStringLiteral("color"), QStringLiteral("%1%2")
915                                    .arg(header->line_color.opacity, 2, 16, QChar('0')).arg(header->line_color.bbggrr, 6, 16, QChar('0')));
916         }
917         if (header->line_width >= 0) {
918           writer->writeTextElement(QStringLiteral("width"), QString::number(header->line_width));
919         }
920       }
921       writer->writeEndElement(); // Close LineStyle tag
922       writer->writeEndElement(); // Close Style tag
923     }
924     if (needs_multigeometry) {
925       writer->writeStartElement(QStringLiteral("MultiGeometry"));
926     }
927 
928     foreach (const Waypoint* tpt, header->waypoint_list) {
929       int first_in_trk = tpt == header->waypoint_list.front();
930       if (tpt->wpt_flags.new_trkseg) {
931         if (!first_in_trk) {
932           writer->writeEndElement(); // Close coordinates tag
933           writer->writeEndElement(); // Close LineString tag
934         }
935         writer->writeStartElement(QStringLiteral("LineString"));
936         kml_output_positioning(true);
937         writer->writeStartElement(QStringLiteral("coordinates"));
938         writer->writeCharacters(QStringLiteral("\n"));
939       }
940       if (kml_altitude_known(tpt)) {
941         writer->writeCharacters(QString::number(tpt->longitude, 'f', precision) + QStringLiteral(",") +
942                                 QString::number(tpt->latitude, 'f', precision) + QStringLiteral(",") +
943                                 QString::number(tpt->altitude, 'f', 2) + QStringLiteral("\n")
944                                );
945       } else {
946         writer->writeCharacters(QString::number(tpt->longitude, 'f', precision) + QStringLiteral(",") +
947                                 QString::number(tpt->latitude, 'f', precision) + QStringLiteral("\n")
948                                );
949       }
950     }
951     writer->writeEndElement(); // Close coordinates tag
952     writer->writeEndElement(); // Close LineString tag
953     if (needs_multigeometry) {
954       writer->writeEndElement(); // Close MultiGeometry tag
955     }
956     writer->writeEndElement(); // Close Placemark tag
957   }
958 
959   writer->writeEndElement();  // Close folder tag
960 }
961 
962 /*
963  * Completely different writer for geocaches.
964  */
965 
966 // Text that's common to all tabs.
kml_gc_all_tabs_text(QString & cdataStr)967 void KmlFormat::kml_gc_all_tabs_text(QString& cdataStr)
968 {
969   // cdataStr.append("<a href=\"http://www.geocaching.com\"><img style=\"float: left; padding: 10px\" src=\"http://www.geocaching.com/images/nav/logo_sub.gif\" /> </a>\n");
970   cdataStr.append("<img align=\"right\" src=\"$[gc_icon]\" />\n");
971   cdataStr.append("<a href=\"https://www.geocaching.com/seek/cache_details.aspx?wp=$[gc_num]\"><b>$[gc_num]</b></a> <b>$[gc_name]</b> \n");
972   cdataStr.append("a $[gc_type],<br />on $[gc_placed] by <a href=\"https://www.geocaching.com/profile?id=$[gc_placer_id\">$[gc_placer]</a><br/>\n");
973   cdataStr.append("Difficulty: <img src=\"https://www.geocaching.com/images/stars/$[gc_diff_stars].gif\" alt=\"$[gc_diff]\" width=\"61\" height=\"13\" />\n");
974   cdataStr.append("&nbsp;Terrain: <img src=\"https://www.geocaching.com/images/stars/$[gc_terr_stars].gif\" alt=\"$[gc_terr]\" width=\"61\" height=\"13\" /><br />\n");
975   cdataStr.append("Size: <img src=\"https://www.geocaching.com/images/icons/container/$[gc_cont_icon].gif\" width=\"45\" height=\"12\" alt=\"$[gc_cont_icon]\"/>&nbsp;($[gc_cont_icon])<br />\n");
976 
977 }
978 
979 const QString KmlFormat::map_templates[] = {
980   R"(<a href="https://www.google.com/maps?q=$[gc_lat],$[gc_lon]" target="_blank">Google Maps</a>)",
981   R"(<a href="http://www.geocaching.com/map/default.aspx?lat=$[gc_lat]&lng=$[gc_lon]" target="_blank">Geocaching.com Google Map</a>)",
982   R"(<a href="http://www.mytopo.com/maps.cfm?lat=$[gc_lat]&lon=$[gc_lon]&pid=groundspeak" target="_blank">MyTopo Maps</a>)",
983   R"(<a href="http://www.mapquest.com/maps/map.adp?searchtype=address&formtype=latlong&latlongtype=decimal&latitude=$[gc_lat]&longitude=$[gc_lon]&zoom=10" target="_blank">MapQuest</a>)",
984   R"(<a href="http://www.bing.com/maps/default.aspx?v=2&sp=point.$[gc_lat]$[gc_lon]" target="_blank">Bing Maps</a>)",
985   R"(<a href="http://maps.randmcnally.com/#s=screen&lat=$[gc_lat]&lon=$[gc_lon]&zoom=13&loc1=$[gc_lat],$[gc_lon]" target="_blank">Rand McNally</a>)",
986   R"(<a href="http://www.opencyclemap.org/?zoom=12&lat=$[gc_lat]&lon=$[gc_lon]" target="_blank">Open Cycle Maps</a>)",
987   R"(<a href="http://www.openstreetmap.org/?mlat=$[gc_lat]&mlon=$[gc_lon]&zoom=12" target="_blank">Open Street Maps</a>)",
988   nullptr
989 };
990 
kml_gc_make_balloonstyletext() const991 void KmlFormat::kml_gc_make_balloonstyletext() const
992 {
993   QString cdataStr;
994 
995   writer->writeStartElement(QStringLiteral("BalloonStyle"));
996   writer->writeStartElement(QStringLiteral("text"));
997   cdataStr.append("\n");
998 
999   cdataStr.append("<!DOCTYPE html>\n");
1000   cdataStr.append("<html>\n");
1001   cdataStr.append("<head>\n");
1002   cdataStr.append("<link href=\"https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.css\" rel=\"stylesheet\" type=\"text/css\"/>\n");
1003   cdataStr.append("<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js\"></script>\n");
1004   cdataStr.append("<script src=\"https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js\"></script>\n");
1005   cdataStr.append("<script>\n");
1006   cdataStr.append("$(document).ready(function() {\n");
1007   cdataStr.append("  $(\"#tabs\").tabs();\n");
1008   cdataStr.append("});\n");
1009   cdataStr.append("</script>\n");
1010   cdataStr.append("</head>\n");
1011 
1012   cdataStr.append("<body>\n");
1013   cdataStr.append("<div id=\"tabs\">\n");
1014 
1015   // The tabbed menu bar.  Oddly, it has to be on top.
1016   cdataStr.append("<ul>\n");
1017   cdataStr.append("  <li><a href=\"#fragment-1\"><span>Description</span></a></li>\n");
1018   cdataStr.append("  <li><a href=\"#fragment-2\"><span>Logs</span></a></li>\n");
1019   cdataStr.append("  <li><a href=\"#fragment-3\"><span>Extras</span></a></li>\n");
1020   cdataStr.append("</ul>\n");
1021   cdataStr.append("\n");
1022 
1023   cdataStr.append("<div id=\"fragment-1\">\n");
1024   kml_gc_all_tabs_text(cdataStr);
1025   cdataStr.append("  <p />$[gc_issues]\n");
1026   cdataStr.append("  $[gc_short_desc]\n");
1027   cdataStr.append("  $[gc_long_desc]\n");
1028   cdataStr.append("</div>\n");
1029 
1030   cdataStr.append("<div id=\"fragment-2\">\n");
1031   kml_gc_all_tabs_text(cdataStr);
1032   cdataStr.append("  $[gc_logs]\n");
1033   cdataStr.append("</div>\n");
1034 
1035   // "Extra" stuff tab.
1036   cdataStr.append("<div id=\"fragment-3\">\n");
1037   kml_gc_all_tabs_text(cdataStr);
1038   cdataStr.append("  <h1>Extra Maps</h1>\n");
1039 
1040   cdataStr.append("  <ul>\n");
1041   // Fortunately, all the mappy map URLs take lat/longs in the URLs, so
1042   // the substitution is easy.
1043   for (int tp = 0; !map_templates[tp].isEmpty(); ++tp) {
1044     cdataStr.append("    <li>\n");
1045     cdataStr.append("    ");
1046     cdataStr.append(map_templates[tp]);
1047     cdataStr.append("</li>\n");
1048   }
1049   cdataStr.append("  <ul>\n");
1050 
1051   cdataStr.append("</div>\n"); //  fragment-3.
1052 
1053   cdataStr.append("</div>\n"); // tabs.
1054   cdataStr.append("</body>\n");
1055   cdataStr.append("</html>\n");
1056 
1057   writer->writeCDATA(cdataStr);
1058   writer->writeEndElement(); // Close text tag
1059   writer->writeEndElement(); // Close BalloonStyle tag
1060 }
1061 
kml_gc_make_balloonstyle() const1062 void KmlFormat::kml_gc_make_balloonstyle() const
1063 {
1064   // For Normal style of gecoaches, scale of label is set to zero
1065   // to make the label invisible.  On hover (highlight?) enlarge
1066   // the icon sightly and set the scale of the label to 1 so the
1067   // label pops.
1068   // It's unfortunate that we have to repeat so much of the template
1069   // but KML doesn't have a cascading style-like substance.
1070   //
1071   writer->writeStartElement(QStringLiteral("Style"));
1072   writer->writeAttribute(QStringLiteral("id"), QStringLiteral("geocache_n"));
1073   writer->writeStartElement(QStringLiteral("IconStyle"));
1074   writer->writeTextElement(QStringLiteral("scale"), QStringLiteral(".6"));
1075   writer->writeEndElement(); // Close IconStyle tag
1076   writer->writeStartElement(QStringLiteral("LabelStyle"));
1077   writer->writeTextElement(QStringLiteral("scale"), QStringLiteral("0"));
1078   writer->writeEndElement(); // Close LabelStyle tag
1079   kml_gc_make_balloonstyletext();
1080   writer->writeEndElement(); // Close Style tag
1081 
1082   writer->writeStartElement(QStringLiteral("Style"));
1083   writer->writeAttribute(QStringLiteral("id"), QStringLiteral("geocache_h"));
1084   writer->writeStartElement(QStringLiteral("IconStyle"));
1085   writer->writeTextElement(QStringLiteral("scale"), QStringLiteral(".8"));
1086   writer->writeEndElement(); // Close IconStyle tag
1087   writer->writeStartElement(QStringLiteral("LabelStyle"));
1088   writer->writeTextElement(QStringLiteral("scale"), QStringLiteral("1"));
1089   writer->writeEndElement(); // Close LabelStyle tag
1090   kml_gc_make_balloonstyletext();
1091   writer->writeEndElement(); // Close Style tag
1092 
1093   writer->writeStartElement(QStringLiteral("StyleMap"));
1094   writer->writeAttribute(QStringLiteral("id"), QStringLiteral("geocache"));
1095 
1096   writer->writeStartElement(QStringLiteral("Pair"));
1097   writer->writeTextElement(QStringLiteral("key"), QStringLiteral("normal"));
1098   writer->writeTextElement(QStringLiteral("styleUrl"), QStringLiteral("#geocache_n"));
1099   writer->writeEndElement(); // Close Pair tag
1100 
1101   writer->writeStartElement(QStringLiteral("Pair"));
1102   writer->writeTextElement(QStringLiteral("key"), QStringLiteral("highlight"));
1103   writer->writeTextElement(QStringLiteral("styleUrl"), QStringLiteral("#geocache_h"));
1104   writer->writeEndElement(); // Close Pair tag
1105 
1106   writer->writeEndElement(); // Close StyleMap tag
1107 }
1108 
kml_lookup_gc_icon(const Waypoint * waypointp)1109 QString KmlFormat::kml_lookup_gc_icon(const Waypoint* waypointp)
1110 {
1111   const char* icon;
1112   /* This could be done so much better in C99 with designated
1113    * initializers...
1114    */
1115   switch (waypointp->gc_data->type) {
1116   case gt_traditional:
1117     icon = "2.png";
1118     break;
1119   case gt_multi:
1120     icon = "3.png";
1121     break;
1122   case gt_virtual:
1123     icon = "4.png";
1124     break;
1125   case gt_letterbox:
1126     icon = "5.png";
1127     break;
1128   case gt_event:
1129     icon = "6.png";
1130     break;
1131   case gt_ape:
1132     icon = "7.png";
1133     break;
1134   case gt_locationless:
1135     icon = "8.png";
1136     break; // No unique icon.
1137   case gt_surprise:
1138     icon = "8.png";
1139     break;
1140   case gt_webcam:
1141     icon = "11.png";
1142     break;
1143   case gt_cito:
1144     icon = "13.png";
1145     break;
1146   case gt_earth:
1147     icon = "earthcache.png";
1148     break;
1149   case gt_mega:
1150     icon = "453.png";
1151     break;
1152   case gt_wherigo:
1153     icon = "1858.png";
1154     break;
1155   default:
1156     icon = "8.png";
1157     break;
1158   }
1159 
1160   return QString("https://www.geocaching.com/images/kml/%1").arg(icon);
1161 }
1162 
kml_lookup_gc_container(const Waypoint * waypointp)1163 const char* KmlFormat::kml_lookup_gc_container(const Waypoint* waypointp)
1164 {
1165   const char* cont;
1166 
1167   switch (waypointp->gc_data->container) {
1168   case gc_micro:
1169     cont="micro";
1170     break;
1171   case gc_regular:
1172     cont="regular";
1173     break;
1174   case gc_large:
1175     cont="large";
1176     break;
1177   case gc_small:
1178     cont="small";
1179     break;
1180   case gc_virtual:
1181     cont="virtual";
1182     break;
1183   case gc_other:
1184     cont="other";
1185     break;
1186   default:
1187     cont="not_chosen";
1188     break;
1189   }
1190 
1191   return cont;
1192 }
1193 
kml_gc_mkstar(int rating)1194 QString KmlFormat::kml_gc_mkstar(int rating)
1195 {
1196   QString star_content;
1197 
1198   if (rating < 0 || rating > 50 || rating % 5 != 0) {
1199     fatal("Bogus difficulty or terrain rating.");
1200   }
1201 
1202   if (0 == rating % 10) {
1203     star_content = QString("stars%1").arg(rating / 10);
1204   } else {
1205     star_content = QString("stars%1_%2").arg(rating / 10).arg(rating % 10);
1206   }
1207 
1208   return star_content;
1209 
1210 }
1211 
kml_geocache_get_logs(const Waypoint * wpt) const1212 QString KmlFormat::kml_geocache_get_logs(const Waypoint* wpt) const
1213 {
1214   QString r;
1215 
1216   const auto* fs_gpx = reinterpret_cast<fs_xml*>(wpt->fs.FsChainFind(kFsGpx));
1217 
1218   if (!fs_gpx) {
1219     return r;
1220   }
1221 
1222   xml_tag* root = fs_gpx->tag;
1223   xml_tag* curlog = xml_findfirst(root, "groundspeak:log");
1224   while (curlog) {
1225     // Unless we have a broken GPX input, these logparts
1226     // branches will always be taken.
1227     xml_tag* logpart = xml_findfirst(curlog, "groundspeak:type");
1228     if (logpart) {
1229       r = r + "<p><b>" + logpart->cdata + "</b>";
1230     }
1231 
1232     logpart = xml_findfirst(curlog, "groundspeak:finder");
1233     if (logpart) {
1234       r = r + " by " + logpart->cdata;
1235     }
1236 
1237     logpart = xml_findfirst(curlog, "groundspeak:date");
1238     if (logpart) {
1239       gpsbabel::DateTime t = xml_parse_time(logpart->cdata);
1240       if (t.isValid()) {
1241         r += t.date().toString(Qt::ISODate);
1242       }
1243     }
1244 
1245     logpart = xml_findfirst(curlog, "groundspeak:text");
1246     if (logpart) {
1247       QString encstr = xml_attribute(logpart->attributes, "encoded");
1248       bool encoded = !encstr.startsWith('F', Qt::CaseInsensitive);
1249 
1250       QString s;
1251       if (html_encrypt && encoded) {
1252         s = rot13(logpart->cdata);
1253       } else {
1254         s = logpart->cdata;
1255       }
1256 
1257       r = r + "<br />";
1258       char* t = html_entitize(s);
1259       r = r + t;
1260       xfree(t);
1261     }
1262 
1263     r += "</p>";
1264     curlog = xml_findnext(root, curlog, "groundspeak:log");
1265   }
1266   return r;
1267 }
1268 
kml_write_data_element(const QString & name,const QString & value) const1269 void KmlFormat::kml_write_data_element(const QString& name, const QString& value) const
1270 {
1271   writer->writeStartElement(QStringLiteral("Data"));
1272   writer->writeAttribute(QStringLiteral("name"), name);
1273   writer->writeTextElement(QStringLiteral("value"), value);
1274   writer->writeEndElement(); // Close Data tag
1275 }
1276 
kml_write_data_element(const QString & name,const int value) const1277 void KmlFormat::kml_write_data_element(const QString& name, const int value) const
1278 {
1279   writer->writeStartElement(QStringLiteral("Data"));
1280   writer->writeAttribute(QStringLiteral("name"), name);
1281   writer->writeTextElement(QStringLiteral("value"), QString::number(value));
1282   writer->writeEndElement(); // Close Data tag
1283 }
1284 
kml_write_data_element(const QString & name,const double value) const1285 void KmlFormat::kml_write_data_element(const QString& name, const double value) const
1286 {
1287   writer->writeStartElement(QStringLiteral("Data"));
1288   writer->writeAttribute(QStringLiteral("name"), name);
1289   writer->writeTextElement(QStringLiteral("value"), QString::number(value, 'f', 6));
1290   writer->writeEndElement(); // Close Data tag
1291 }
1292 
kml_write_cdata_element(const QString & name,const QString & value) const1293 void KmlFormat::kml_write_cdata_element(const QString& name, const QString& value) const
1294 {
1295   writer->writeStartElement(QStringLiteral("Data"));
1296   writer->writeAttribute(QStringLiteral("name"), name);
1297   writer->writeStartElement(QStringLiteral("value"));
1298   writer->writeCDATA(value);
1299   writer->writeEndElement(); // Close value tag
1300   writer->writeEndElement(); // Close Data tag
1301 }
1302 
kml_geocache_pr(const Waypoint * waypointp) const1303 void KmlFormat::kml_geocache_pr(const Waypoint* waypointp) const
1304 {
1305   const char* issues = "";
1306 
1307   writer->writeStartElement(QStringLiteral("Placemark"));
1308 
1309   writer->writeStartElement(QStringLiteral("name"));
1310   if (waypointp->HasUrlLink()) {
1311     UrlLink link = waypointp->GetUrlLink();
1312     writer->writeCDATA(link.url_link_text_);
1313   }
1314   writer->writeEndElement(); // Close name tag
1315 
1316   // Timestamp
1317   kml_output_timestamp(waypointp);
1318   QString date_placed;
1319   if (waypointp->GetCreationTime().isValid()) {
1320     date_placed = waypointp->GetCreationTime().toString("dd-MMM-yyyy");
1321   }
1322 
1323   writer->writeTextElement(QStringLiteral("styleUrl"), QStringLiteral("#geocache"));
1324   writer->writeStartElement(QStringLiteral("Style"));
1325   writer->writeStartElement(QStringLiteral("IconStyle"));
1326   writer->writeStartElement(QStringLiteral("Icon"));
1327   QString is = kml_lookup_gc_icon(waypointp);
1328   writer->writeTextElement(QStringLiteral("href"), is);
1329   writer->writeEndElement(); // Close Icon tag
1330   writer->writeEndElement(); // Close IconStyle tag
1331   writer->writeEndElement(); // Close Style tag
1332 
1333   writer->writeStartElement(QStringLiteral("ExtendedData"));
1334   if (!waypointp->shortname.isEmpty()) {
1335     kml_write_data_element("gc_num", waypointp->shortname);
1336   }
1337 
1338   if (waypointp->HasUrlLink()) {
1339     UrlLink link = waypointp->GetUrlLink();
1340     kml_write_data_element("gc_name", link.url_link_text_);
1341   }
1342 
1343   if (!waypointp->gc_data->placer.isEmpty()) {
1344     kml_write_data_element("gc_placer", waypointp->gc_data->placer);
1345   }
1346 
1347   kml_write_data_element("gc_placer_id", waypointp->gc_data->placer_id);
1348   kml_write_data_element("gc_placed", date_placed);
1349 
1350   kml_write_data_element("gc_diff_stars", kml_gc_mkstar(waypointp->gc_data->diff));
1351   kml_write_data_element("gc_terr_stars", kml_gc_mkstar(waypointp->gc_data->terr));
1352 
1353   kml_write_data_element("gc_cont_icon", kml_lookup_gc_container(waypointp));
1354 
1355   // Highlight any issues with the cache, such as temp unavail
1356   // or archived.
1357   if (waypointp->gc_data->is_archived == status_true) {
1358     issues = "&lt;font color=\"red\"&gt;This cache has been archived.&lt;/font&gt;&lt;br/&gt;\n";
1359   } else if (waypointp->gc_data->is_available == status_false) {
1360     issues = "&lt;font color=\"red\"&gt;This cache is temporarily unavailable.&lt;/font&gt;&lt;br/&gt;\n";
1361   }
1362   kml_write_data_element("gc_issues", issues);
1363 
1364   kml_write_data_element("gc_lat", waypointp->latitude);
1365   kml_write_data_element("gc_lon", waypointp->longitude);
1366 
1367   kml_write_data_element("gc_type", gs_get_cachetype(waypointp->gc_data->type));
1368   kml_write_data_element("gc_icon", is);
1369   kml_write_cdata_element("gc_short_desc", waypointp->gc_data->desc_short.utfstring);
1370   kml_write_cdata_element("gc_long_desc", waypointp->gc_data->desc_long.utfstring);
1371   QString logs = kml_geocache_get_logs(waypointp);
1372   kml_write_cdata_element("gc_logs", logs);
1373 
1374   writer->writeEndElement(); // Close ExtendedData tag
1375 
1376   // Location
1377   writer->writeStartElement(QStringLiteral("Point"));
1378   kml_write_coordinates(waypointp);
1379 
1380   writer->writeEndElement(); // Close Point tag
1381   writer->writeEndElement(); // Close Placemark tag
1382 }
1383 
1384 /*
1385  * WAYPOINTS
1386  */
1387 
kml_waypt_pr(const Waypoint * waypointp) const1388 void KmlFormat::kml_waypt_pr(const Waypoint* waypointp) const
1389 {
1390   QString icon;
1391 
1392 #if 0 // Experimental
1393   if (realtime_positioning) {
1394     writer->wrteStartTag("LookAt");
1395     writer->writeTextElement(QStringLiteral("longitude"), QString::number(waypointp->longitude, 'f', precision);
1396                              writer->writeTextElement(QStringLiteral("latitude"), QString::number(waypointp->latitude, 'f', precision);
1397                                  writer->writeTextElement(QStringLiteral("altitude"), QStringLiteral("1000"));
1398                                  writer->writeEndElement(); // Close LookAt tag
1399   }
1400 #endif
1401 
1402   if (waypointp->gc_data->diff && waypointp->gc_data->terr) {
1403     kml_geocache_pr(waypointp);
1404     return;
1405   }
1406 
1407   writer->writeStartElement(QStringLiteral("Placemark"));
1408 
1409   writer->writeOptionalTextElement(QStringLiteral("name"), waypointp->shortname);
1410 
1411   // Description
1412   if (waypointp->HasUrlLink()) {
1413     writer->writeEmptyElement(QStringLiteral("snippet"));
1414     UrlLink link = waypointp->GetUrlLink();
1415     if (!link.url_link_text_.isEmpty()) {
1416       QString odesc = link.url_;
1417       QString olink = link.url_link_text_;
1418       writer->writeStartElement(QStringLiteral("description"));
1419       writer->writeCDATA(QStringLiteral("<a href=\"%1\">%2</a>").arg(odesc, olink));
1420       writer->writeEndElement(); // Close description tag
1421     } else {
1422       writer->writeTextElement(QStringLiteral("description"), link.url_);
1423     }
1424   } else {
1425     if (waypointp->shortname != waypointp->description) {
1426       writer->writeOptionalTextElement(QStringLiteral("description"), waypointp->description);
1427     }
1428   }
1429 
1430   // Timestamp
1431   kml_output_timestamp(waypointp);
1432 
1433   // Icon - but only if it looks like a URL.
1434   icon = opt_deficon ? opt_deficon : waypointp->icon_descr;
1435   if (icon.contains("://")) {
1436     writer->writeStartElement(QStringLiteral("Style"));
1437     writer->writeStartElement(QStringLiteral("IconStyle"));
1438     writer->writeStartElement(QStringLiteral("Icon"));
1439     writer->writeTextElement(QStringLiteral("href"), icon);
1440     writer->writeEndElement(); // Close Icon tag
1441     writer->writeEndElement(); // Close IconStyle tag
1442     writer->writeEndElement(); // Close Style tag
1443   } else {
1444     writer->writeTextElement(QStringLiteral("styleUrl"), QStringLiteral("#waypoint"));
1445   }
1446 
1447   // Location
1448   writer->writeStartElement(QStringLiteral("Point"));
1449   kml_output_positioning(false);
1450   kml_write_coordinates(waypointp);
1451   writer->writeEndElement(); // Close Point tag
1452 
1453   writer->writeEndElement(); // Close Placemark tag
1454 }
1455 
1456 /*
1457  * TRACKPOINTS
1458  */
1459 
kml_track_hdr(const route_head * header) const1460 void KmlFormat::kml_track_hdr(const route_head* header) const
1461 {
1462   computed_trkdata td = track_recompute(header);
1463   if (header->rte_waypt_ct > 0 && (export_lines || export_points)) {
1464     kml_output_header(header, &td);
1465   }
1466 }
1467 
kml_track_disp(const Waypoint * waypointp) const1468 void KmlFormat::kml_track_disp(const Waypoint* waypointp) const
1469 {
1470   kml_output_point(waypointp, kmlpt_track);
1471 }
1472 
kml_track_tlr(const route_head * header)1473 void KmlFormat::kml_track_tlr(const route_head* header)
1474 {
1475   if (header->rte_waypt_ct > 0 && (export_lines || export_points)) {
1476     kml_output_tailer(header);
1477   }
1478 }
1479 
1480 /*
1481  * New for 2010, Earth adds "MultiTrack" as an extension.
1482  * Unlike every other format, we do the bulk of the work in the header
1483  * callback as we have to make multiple passes over the track queues.
1484  */
1485 
kml_mt_simple_array(const route_head * header,const char * name,wp_field member) const1486 void KmlFormat::kml_mt_simple_array(const route_head* header,
1487                                     const char* name,
1488                                     wp_field member) const
1489 {
1490   writer->writeStartElement(QStringLiteral("gx:SimpleArrayData"));
1491   writer->writeAttribute(QStringLiteral("name"), name);
1492 
1493   foreach (const Waypoint* wpt, header->waypoint_list) {
1494 
1495     switch (member) {
1496     case fld_power:
1497       writer->writeTextElement(QStringLiteral("gx:value"), QString::number(wpt->power, 'f', 1));
1498       break;
1499     case fld_cadence:
1500       writer->writeTextElement(QStringLiteral("gx:value"), QString::number(wpt->cadence));
1501       break;
1502     case fld_depth:
1503       writer->writeTextElement(QStringLiteral("gx:value"), QString::number(wpt->depth, 'f', 1));
1504       break;
1505     case fld_heartrate:
1506       writer->writeTextElement(QStringLiteral("gx:value"), QString::number(wpt->heartrate));
1507       break;
1508     case fld_temperature:
1509       writer->writeTextElement(QStringLiteral("gx:value"), QString::number(wpt->temperature, 'f', 1));
1510       break;
1511     default:
1512       fatal("Bad member type");
1513     }
1514   }
1515   writer->writeEndElement(); // Close SimpleArrayData tag
1516 }
1517 
1518 // True if at least two points in the track have timestamps.
track_has_time(const route_head * header)1519 int KmlFormat::track_has_time(const route_head* header)
1520 {
1521   int points_with_time = 0;
1522   foreach (const Waypoint* tpt, header->waypoint_list) {
1523     if (tpt->GetCreationTime().isValid()) {
1524       points_with_time++;
1525       if (points_with_time >= 2) {
1526         return 1;
1527       }
1528     }
1529   }
1530   return 0;
1531 }
1532 
1533 // Simulate a track_disp_all callback sequence for a single track.
write_as_linestring(const route_head * header)1534 void KmlFormat::write_as_linestring(const route_head* header)
1535 {
1536   kml_track_hdr(header);
1537   foreach (const Waypoint* tpt, header->waypoint_list) {
1538     kml_track_disp(tpt);
1539   }
1540   kml_track_tlr(header);
1541 
1542 }
1543 
kml_mt_hdr(const route_head * header)1544 void KmlFormat::kml_mt_hdr(const route_head* header)
1545 {
1546   int has_cadence = 0;
1547   int has_depth = 0;
1548   int has_heartrate = 0;
1549   int has_temperature = 0;
1550   int has_power = 0;
1551 
1552   // This logic is kind of inside-out for GPSBabel.  If a track doesn't
1553   // have enough interesting timestamps, just write it as a LineString.
1554   if (!track_has_time(header)) {
1555     write_as_linestring(header);
1556     return;
1557   }
1558 
1559   writer->writeStartElement(QStringLiteral("Placemark"));
1560   writer->writeOptionalTextElement(QStringLiteral("name"), header->rte_name);
1561   writer->writeTextElement(QStringLiteral("styleUrl"), QStringLiteral("#multiTrack"));
1562   writer->writeStartElement(QStringLiteral("gx:Track"));
1563   kml_output_positioning(false);
1564 
1565   foreach (const Waypoint* tpt, header->waypoint_list) {
1566     if (tpt->GetCreationTime().isValid()) {
1567       QString time_string = tpt->CreationTimeXML();
1568       writer->writeOptionalTextElement(QStringLiteral("when"), time_string);
1569     } else {
1570       writer->writeStartElement(QStringLiteral("when"));
1571       writer->writeEndElement(); // Close when tag
1572     }
1573   }
1574 
1575   // TODO: How to handle clamped, floating, extruded, etc.?
1576   foreach (const Waypoint* tpt, header->waypoint_list) {
1577     if (kml_altitude_known(tpt)) {
1578       writer->writeTextElement(QStringLiteral("gx:coord"),
1579                                QString::number(tpt->longitude, 'f', precision) + QString(" ") +
1580                                QString::number(tpt->latitude, 'f', precision) + QString(" ") +
1581                                QString::number(tpt->altitude, 'f', 2)
1582                               );
1583     } else {
1584       writer->writeTextElement(QStringLiteral("gx:coord"),
1585                                QString::number(tpt->longitude, 'f', precision) + QString(" ") +
1586                                QString::number(tpt->latitude, 'f', precision)
1587                               );
1588     }
1589 
1590     // Capture interesting traits to see if we need to do an ExtendedData
1591     // section later.
1592     if (tpt->cadence) {
1593       has_cadence = 1;
1594     }
1595     if (WAYPT_HAS(tpt, depth)) {
1596       has_depth = 1;
1597     }
1598     if (tpt->heartrate) {
1599       has_heartrate = 1;
1600     }
1601     if (WAYPT_HAS(tpt, temperature)) {
1602       has_temperature = 1;
1603     }
1604     if (tpt->power) {
1605       has_power = 1;
1606     }
1607   }
1608 
1609   if (has_cadence || has_depth || has_heartrate || has_temperature ||
1610       has_power) {
1611     writer->writeStartElement(QStringLiteral("ExtendedData"));
1612     writer->writeStartElement(QStringLiteral("SchemaData"));
1613     writer->writeAttribute(QStringLiteral("schemaUrl"), QStringLiteral("#schema"));
1614 
1615     if (has_cadence) {
1616       kml_mt_simple_array(header, kmt_cadence, fld_cadence);
1617     }
1618 
1619     if (has_depth) {
1620       kml_mt_simple_array(header, kmt_depth, fld_depth);
1621     }
1622 
1623     if (has_heartrate) {
1624       kml_mt_simple_array(header, kmt_heartrate, fld_heartrate);
1625     }
1626 
1627     if (has_temperature) {
1628       kml_mt_simple_array(header, kmt_temperature, fld_temperature);
1629     }
1630 
1631     if (has_power) {
1632       kml_mt_simple_array(header, kmt_power, fld_power);
1633     }
1634 
1635     writer->writeEndElement(); // Close SchemaData tag
1636     writer->writeEndElement(); // Close ExtendedData tag
1637   }
1638 }
1639 
kml_mt_tlr(const route_head * header) const1640 void KmlFormat::kml_mt_tlr(const route_head* header) const
1641 {
1642   if (track_has_time(header)) {
1643     writer->writeEndElement(); // Close gx:Track tag
1644     writer->writeEndElement(); // Close Placemark tag
1645   }
1646 }
1647 
1648 /*
1649  * ROUTES
1650  */
1651 
kml_route_hdr(const route_head * header) const1652 void KmlFormat::kml_route_hdr(const route_head* header) const
1653 {
1654   kml_output_header(header, nullptr);
1655 }
1656 
kml_route_disp(const Waypoint * waypointp) const1657 void KmlFormat::kml_route_disp(const Waypoint* waypointp) const
1658 {
1659   kml_output_point(waypointp, kmlpt_route);
1660 }
1661 
kml_route_tlr(const route_head * header)1662 void KmlFormat::kml_route_tlr(const route_head* header)
1663 {
1664   kml_output_tailer(header);
1665 }
1666 
1667 // For Earth 5.0 and later, we write a LookAt that encompasses
1668 // the bounding box of our entire data set and set the event times
1669 // to include all our data.
kml_write_AbstractView()1670 void KmlFormat::kml_write_AbstractView()
1671 {
1672   // Make a pass through all the points to find the bounds.
1673   auto kml_add_to_bounds_lambda = [this](const Waypoint* waypointp)->void {
1674     kml_add_to_bounds(waypointp);
1675   };
1676   if (waypt_count()) {
1677     waypt_disp_all(kml_add_to_bounds_lambda);
1678   }
1679   if (track_waypt_count())  {
1680     track_disp_all(nullptr, nullptr, kml_add_to_bounds_lambda);
1681   }
1682   if (route_waypt_count()) {
1683     route_disp_all(nullptr, nullptr, kml_add_to_bounds_lambda);
1684   }
1685 
1686   writer->writeStartElement(QStringLiteral("LookAt"));
1687 
1688   if (kml_time_min.isValid() || kml_time_max.isValid()) {
1689     writer->writeStartElement(QStringLiteral("gx:TimeSpan"));
1690     if (kml_time_min.isValid()) {
1691       writer->writeTextElement(QStringLiteral("begin"), kml_time_min.toPrettyString());
1692     }
1693     if (kml_time_max.isValid()) {
1694       // In realtime tracking mode, we fudge the end time by a few minutes
1695       // to ensure that the freshest data (our current location) is contained
1696       // within the timespan.   Earth's time may not match the GPS because
1697       // we may not be running NTP, plus it's polling a file (sigh) to read
1698       // the network position.  So we shove the end of the timespan out to
1699       // ensure the right edge of that time slider includes us.
1700       //
1701       gpsbabel::DateTime time_max = realtime_positioning ? kml_time_max.addSecs(600)
1702                                     : kml_time_max;
1703       writer->writeTextElement(QStringLiteral("end"), time_max.toPrettyString());
1704     }
1705     writer->writeEndElement(); // Close gx:TimeSpan tag
1706   }
1707 
1708 // If our BB spans the antemeridian, flip sign on one.
1709 // This doesn't make our BB optimal, but it at least prevents us from
1710 // zooming to the wrong hemisphere.
1711   if (kml_bounds.min_lon * kml_bounds.max_lon < 0) {
1712     kml_bounds.min_lon = -kml_bounds.max_lon;
1713   }
1714 
1715   writer->writeTextElement(QStringLiteral("longitude"), QString::number((kml_bounds.min_lon + kml_bounds.max_lon) / 2, 'f', precision));
1716   writer->writeTextElement(QStringLiteral("latitude"), QString::number((kml_bounds.min_lat + kml_bounds.max_lat) / 2, 'f', precision));
1717 
1718   // It turns out the length of the diagonal of the bounding box gives us a
1719   // reasonable guess for setting the camera altitude.
1720   double bb_size = gcgeodist(kml_bounds.min_lat, kml_bounds.min_lon,
1721                              kml_bounds.max_lat, kml_bounds.max_lon);
1722   // Clamp bottom zoom level.  Otherwise, a single point zooms to grass.
1723   if (bb_size < 1000) {
1724     bb_size = 1000;
1725   }
1726   writer->writeTextElement(QStringLiteral("range"), QString::number(bb_size * 1.3, 'f', 6));
1727 
1728   writer->writeEndElement(); // Close LookAt tag
1729 }
1730 
kml_mt_array_schema(const char * field_name,const char * display_name,const char * type) const1731 void KmlFormat::kml_mt_array_schema(const char* field_name, const char* display_name,
1732                                     const char* type) const
1733 {
1734   writer->writeStartElement(QStringLiteral("gx:SimpleArrayField"));
1735   writer->writeAttribute(QStringLiteral("name"), field_name);
1736   writer->writeAttribute(QStringLiteral("type"), type);
1737   writer->writeTextElement(QStringLiteral("displayName"), display_name);
1738   writer->writeEndElement(); // Close gx:SimpleArrayField tag
1739 }
1740 
write()1741 void KmlFormat::write()
1742 {
1743   const global_trait* traits = get_traits();
1744 
1745   // Parse options
1746   export_lines = (0 == strcmp("1", opt_export_lines));
1747   export_points = (0 == strcmp("1", opt_export_points));
1748   export_track = (0 ==  strcmp("1", opt_export_track));
1749   floating = (!! strcmp("0", opt_floating));
1750   extrude = (!! strcmp("0", opt_extrude));
1751   rotate_colors = (!! opt_rotate_colors);
1752   trackdata = (!! strcmp("0", opt_trackdata));
1753   trackdirection = (!! strcmp("0", opt_trackdirection));
1754   line_width = atol(opt_line_width);
1755   precision = atol(opt_precision);
1756 
1757   writer->writeStartDocument();
1758   // FIXME: This write of a blank line is needed for Qt 4.6 (as on Centos 6.3)
1759   // to include just enough whitespace between <xml/> and <gpx...> to pass
1760   // diff -w.  It's here for now to shim compatibility with our zillion
1761   // reference files, but this blank link can go away some day.
1762   writer->writeCharacters(QStringLiteral("\n"));
1763 
1764   writer->setAutoFormatting(true);
1765 
1766   writer->writeStartElement(QStringLiteral("kml"));
1767   writer->writeAttribute(QStringLiteral("xmlns"), QStringLiteral("http://www.opengis.net/kml/2.2"));
1768   writer->writeAttribute(QStringLiteral("xmlns:gx"), QStringLiteral("http://www.google.com/kml/ext/2.2"));
1769 
1770   writer->writeStartElement(QStringLiteral("Document"));
1771 
1772   if (realtime_positioning) {
1773     writer->writeTextElement(QStringLiteral("name"), QStringLiteral("GPS position"));
1774   } else {
1775     writer->writeTextElement(QStringLiteral("name"), QStringLiteral("GPS device"));
1776   }
1777 
1778   if (!gpsbabel_testmode()) {
1779     writer->writeTextElement(QStringLiteral("snippet"), QStringLiteral("Created ") +
1780                              current_time().toString());
1781   }
1782 
1783   kml_write_AbstractView();
1784 
1785   // Style settings for bitmaps
1786   if (route_waypt_count()) {
1787     kml_write_bitmap_style(kmlpt_route, ICON_RTE, nullptr);
1788   }
1789 
1790   if (track_waypt_count()) {
1791     if (trackdirection) {
1792       kml_write_bitmap_style(kmlpt_other, ICON_TRK, "track-none");
1793       for (int i = 0; i < 16; ++i) {
1794         kml_write_bitmap_style(kmlpt_other, QString(ICON_DIR).arg(i), QString("track-%1").arg(i));
1795       }
1796     } else {
1797       kml_write_bitmap_style(kmlpt_track, ICON_TRK, nullptr);
1798     }
1799     if (export_track)
1800       kml_write_bitmap_style(kmlpt_multitrack, ICON_MULTI_TRK,
1801                              "track-none");
1802   }
1803 
1804   kml_write_bitmap_style(kmlpt_waypoint, ICON_WPT, nullptr);
1805 
1806   if (track_waypt_count() || route_waypt_count()) {
1807     writer->writeStartElement(QStringLiteral("Style"));
1808     writer->writeAttribute(QStringLiteral("id"), QStringLiteral("lineStyle"));
1809     kml_output_linestyle(opt_line_color, line_width);
1810     writer->writeEndElement(); // Close Style tag
1811   }
1812 
1813   if (traits->trait_geocaches) {
1814     kml_gc_make_balloonstyle();
1815   }
1816 
1817   if (traits->trait_heartrate ||
1818       traits->trait_cadence ||
1819       traits->trait_power ||
1820       traits->trait_temperature ||
1821       traits->trait_depth) {
1822     writer->writeStartElement(QStringLiteral("Schema"));
1823     writer->writeAttribute(QStringLiteral("id"), QStringLiteral("schema"));
1824 
1825     if (traits->trait_heartrate) {
1826       kml_mt_array_schema(kmt_heartrate, "Heart Rate", "int");
1827     }
1828     if (traits->trait_cadence) {
1829       kml_mt_array_schema(kmt_cadence, "Cadence", "int");
1830     }
1831     if (traits->trait_power) {
1832       kml_mt_array_schema(kmt_power, "Power", "float");
1833     }
1834     if (traits->trait_temperature) {
1835       kml_mt_array_schema(kmt_temperature, "Temperature", "float");
1836     }
1837     if (traits->trait_depth) {
1838       kml_mt_array_schema(kmt_depth, "Depth", "float");
1839     }
1840     writer->writeEndElement(); // Close Schema tag
1841   }
1842 
1843   if (waypt_count()) {
1844     if (!realtime_positioning) {
1845       writer->writeStartElement(QStringLiteral("Folder"));
1846       writer->writeTextElement(QStringLiteral("name"), QStringLiteral("Waypoints"));
1847     }
1848 
1849     auto kml_waypt_pr_lambda = [this](const Waypoint* waypointp)->void {
1850       kml_waypt_pr(waypointp);
1851     };
1852     waypt_disp_all(kml_waypt_pr_lambda);
1853 
1854     if (!realtime_positioning) {
1855       writer->writeEndElement(); // Close Folder tag
1856     }
1857   }
1858 
1859   // Output trackpoints
1860   if (track_waypt_count()) {
1861     if (!realtime_positioning) {
1862       writer->writeStartElement(QStringLiteral("Folder"));
1863       writer->writeTextElement(QStringLiteral("name"), QStringLiteral("Tracks"));
1864     }
1865 
1866     kml_init_color_sequencer(track_count());
1867     if (export_track) {
1868       auto kml_mt_hdr_lambda = [this](const route_head* rte)->void {
1869         kml_mt_hdr(rte);
1870       };
1871       auto kml_mt_tlr_lambda = [this](const route_head* rte)->void {
1872         kml_mt_tlr(rte);
1873       };
1874       track_disp_all(kml_mt_hdr_lambda, kml_mt_tlr_lambda, nullptr);
1875     }
1876 
1877     auto kml_track_hdr_lambda = [this](const route_head* rte)->void {
1878       kml_track_hdr(rte);
1879     };
1880     auto kml_track_tlr_lambda = [this](const route_head* rte)->void {
1881       kml_track_tlr(rte);
1882     };
1883     auto kml_track_disp_lambda = [this](const Waypoint* waypointp)->void {
1884       kml_track_disp(waypointp);
1885     };
1886     track_disp_all(kml_track_hdr_lambda, kml_track_tlr_lambda,
1887                    kml_track_disp_lambda);
1888 
1889     if (!realtime_positioning) {
1890       writer->writeEndElement(); // Close Folder tag
1891     }
1892   }
1893 
1894   // Output routes
1895   if (route_waypt_count()) {
1896     if (!realtime_positioning) {
1897       writer->writeStartElement(QStringLiteral("Folder"));
1898       writer->writeTextElement(QStringLiteral("name"), QStringLiteral("Routes"));
1899 
1900       kml_init_color_sequencer(route_count());
1901       auto kml_route_hdr_lambda = [this](const route_head* rte)->void {
1902         kml_route_hdr(rte);
1903       };
1904       auto kml_route_tlr_lambda = [this](const route_head* rte)->void {
1905         kml_route_tlr(rte);
1906       };
1907       auto kml_route_disp_lambda = [this](const Waypoint* waypointp)->void {
1908         kml_route_disp(waypointp);
1909       };
1910       route_disp_all(kml_route_hdr_lambda,
1911                      kml_route_tlr_lambda, kml_route_disp_lambda);
1912       writer->writeEndElement(); // Close Folder tag
1913     }
1914   }
1915 
1916   writer->writeEndElement(); // Close Document tag.
1917   writer->writeEndElement(); // Close kml tag.
1918 }
1919 
1920 /*
1921  * This depends on the table being sorted correctly.
1922  */
kml_get_posn_icon(int freshness)1923 QString KmlFormat::kml_get_posn_icon(int freshness)
1924 {
1925   struct kml_tracking_icon {
1926     int freshness;
1927     QString icon;
1928   };
1929   static const QVector<kml_tracking_icon> kml_tracking_icons = {
1930     { 60, ICON_BASE "youarehere-60.png" }, // Red
1931     { 30, ICON_BASE "youarehere-30.png" }, // Yellow
1932     { 0,  ICON_BASE "youarehere-0.png" }, // Green
1933   };
1934 
1935   for (const auto& entry : kml_tracking_icons) {
1936     if (freshness >= entry.freshness) {
1937       return entry.icon;
1938     }
1939   }
1940   return ICON_NOSAT;
1941 }
1942 
wr_position(Waypoint * wpt)1943 void KmlFormat::wr_position(Waypoint* wpt)
1944 {
1945   static gpsbabel::DateTime last_valid_fix;
1946 
1947   wr_init(posnfilenametmp);
1948 
1949   if (!posn_trk_head) {
1950     posn_trk_head = new route_head;
1951     posn_trk_head->rte_name = "Track";
1952     track_add_head(posn_trk_head);
1953   }
1954 
1955   if (!last_valid_fix.isValid()) {
1956     last_valid_fix = current_time();
1957   }
1958 
1959   /* We want our waypoint to have a name, but not our trackpoint */
1960   if (wpt->shortname.isEmpty()) {
1961     if (wpt->fix == fix_none) {
1962       wpt->shortname = "ESTIMATED Position";
1963     } else {
1964       wpt->shortname = "Position";
1965     }
1966   }
1967 
1968   switch (wpt->fix) {
1969   case fix_none:
1970     wpt->shortname = "ESTIMATED Position";
1971     break;
1972   case fix_unknown:
1973     break;
1974   default:
1975     last_valid_fix = wpt->GetCreationTime();
1976   }
1977 
1978   wpt->icon_descr = kml_get_posn_icon(wpt->GetCreationTime().toTime_t() - last_valid_fix.toTime_t());
1979 
1980 
1981   /* In order to avoid clutter while we're sitting still, don't add
1982      track points if we've not moved a minimum distance from the
1983      beginning of our accumulated track. */
1984   if (posn_trk_head->waypoint_list.empty()) {
1985     track_add_wpt(posn_trk_head, new Waypoint(*wpt));
1986   } else {
1987     Waypoint* newest_posn= posn_trk_head->waypoint_list.back();
1988 
1989     if (radtometers(gcdist(RAD(wpt->latitude), RAD(wpt->longitude),
1990                            RAD(newest_posn->latitude), RAD(newest_posn->longitude))) > 50) {
1991       track_add_wpt(posn_trk_head, new Waypoint(*wpt));
1992     } else {
1993       /* If we haven't move more than our threshold, pretend
1994        * we didn't move at  all to prevent Earth from jittering
1995        * the zoom levels on us.
1996        */
1997       wpt->latitude = newest_posn->latitude;
1998       wpt->longitude = newest_posn->longitude;
1999     }
2000   }
2001 
2002   waypt_add(wpt);
2003   write();
2004   waypt_del(wpt);
2005 
2006   /*
2007    * If we are keeping only a recent subset of the trail, trim the
2008    * head here.
2009    */
2010   while (max_position_points &&
2011          (posn_trk_head->rte_waypt_ct >= max_position_points)) {
2012     Waypoint* tonuke = posn_trk_head->waypoint_list.front();
2013     track_del_wpt(posn_trk_head, tonuke);
2014   }
2015 
2016   wr_deinit();
2017 }
2018