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(" 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]\"/> ($[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 = "<font color=\"red\">This cache has been archived.</font><br/>\n";
1359 } else if (waypointp->gc_data->is_available == status_false) {
1360 issues = "<font color=\"red\">This cache is temporarily unavailable.</font><br/>\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