1 /**********************************************************************************************
2 Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.de>
3 Copyright (C) 2019 Henri Hornburg <hrnbg@t-online.de>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 **********************************************************************************************/
19
20 #include "device/CDeviceGarmin.h"
21 #include "gis/ovl/CGisItemOvlArea.h"
22 #include "gis/prj/IGisProject.h"
23 #include "gis/proj_x.h"
24 #include "gis/rte/CGisItemRte.h"
25 #include "gis/trk/CGisItemTrk.h"
26 #include "gis/trk/CKnownExtension.h"
27 #include "gis/wpt/CGisItemWpt.h"
28 #include "helpers/CWptIconManager.h"
29 #include "version.h"
30
31 #include <QtXml>
32
33 const QString IGisProject::gpx_ns = "http://www.topografix.com/GPX/1/1";
34 const QString IGisProject::xsi_ns = "http://www.w3.org/2001/XMLSchema-instance";
35 const QString IGisProject::gpxx_ns = "http://www.garmin.com/xmlschemas/GpxExtensions/v3";
36 const QString IGisProject::gpxtpx_ns = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1";
37 const QString IGisProject::wptx1_ns = "http://www.garmin.com/xmlschemas/WaypointExtension/v1";
38 const QString IGisProject::rmc_ns = "urn:net:trekbuddy:1.0:nmea:rmc";
39 const QString IGisProject::ql_ns = "http://www.qlandkarte.org/xmlschemas/v1.1";
40 const QString IGisProject::gs_ns = "http://www.groundspeak.com/cache/1/0";
41 const QString IGisProject::tp1_ns = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1";
42 const QString IGisProject::gpxdata_ns = "http://www.cluetrust.com/XML/GPXDATA/1/0";
43
44
readXml(const QDomNode & xml,const QString & tag,qint32 & value)45 static void readXml(const QDomNode& xml, const QString& tag, qint32& value)
46 {
47 if(xml.namedItem(tag).isElement())
48 {
49 bool ok = false;
50 qint32 tmp = xml.namedItem(tag).toElement().text().toInt(&ok);
51 if(!ok)
52 {
53 tmp = qRound(xml.namedItem(tag).toElement().text().toDouble(&ok));
54 }
55 if(ok)
56 {
57 value = tmp;
58 }
59 }
60 }
61
readXml(const QDomNode & xml,const QString & tag,trkact_t & value)62 static void readXml(const QDomNode& xml, const QString& tag, trkact_t& value)
63 {
64 if(xml.namedItem(tag).isElement())
65 {
66 bool ok = false;
67 qint32 tmp = xml.namedItem(tag).toElement().text().toInt(&ok);
68 if(!ok)
69 {
70 value = CTrackData::trkpt_t::eAct20None;
71 }
72 else
73 {
74 value = trkact_t(tmp);
75 }
76 }
77 }
78
79
80 template<typename T>
readXml(const QDomNode & xml,const QString & tag,T & value)81 static void readXml(const QDomNode& xml, const QString& tag, T& value)
82 {
83 if(xml.namedItem(tag).isElement())
84 {
85 bool ok = false;
86 T tmp;
87
88 if(std::is_same<T, quint32>::value)
89 {
90 tmp = xml.namedItem(tag).toElement().text().toUInt(&ok);
91 }
92 else if(std::is_same<T, quint64>::value)
93 {
94 tmp = xml.namedItem(tag).toElement().text().toULongLong(&ok);
95 }
96 else if(std::is_same<T, qreal>::value)
97 {
98 tmp = xml.namedItem(tag).toElement().text().toDouble(&ok);
99 }
100 else if(std::is_same<T, bool>::value)
101 {
102 tmp = xml.namedItem(tag).toElement().text().toInt(&ok);
103 }
104
105 if(ok)
106 {
107 value = tmp;
108 }
109 }
110 }
111
readXml(const QDomNode & xml,const QString & tag,QString & value)112 static void readXml(const QDomNode& xml, const QString& tag, QString& value)
113 {
114 if(xml.namedItem(tag).isElement())
115 {
116 value = xml.namedItem(tag).toElement().text();
117 }
118 }
119
readXml(const QDomNode & xml,const QString & tag,QString & value,bool & isHtml)120 static void readXml(const QDomNode& xml, const QString& tag, QString& value, bool& isHtml)
121 {
122 if(xml.namedItem(tag).isElement())
123 {
124 const QDomNamedNodeMap& attr = xml.namedItem(tag).toElement().attributes();
125 isHtml = (attr.namedItem("html").nodeValue().toLocal8Bit().toLower() == "true");
126 value = xml.namedItem(tag).toElement().text();
127 }
128 }
129
readXml(const QDomNode & xml,const QString & tag,QDateTime & value)130 static void readXml(const QDomNode& xml, const QString& tag, QDateTime& value)
131 {
132 if(xml.namedItem(tag).isElement())
133 {
134 QString time = xml.namedItem(tag).toElement().text();
135 IUnit::parseTimestamp(time, value);
136 }
137 }
138
readXml(const QDomNode & xml,const QString & tag,QList<IGisItem::link_t> & l)139 static void readXml(const QDomNode& xml, const QString& tag, QList<IGisItem::link_t>& l)
140 {
141 if(xml.namedItem(tag).isElement())
142 {
143 const QDomNodeList& links = xml.toElement().elementsByTagName(tag);
144 int N = links.count();
145 for(int n = 0; n < N; ++n)
146 {
147 const QDomNode& link = links.item(n);
148
149 if(xml != link.parentNode())
150 {
151 continue;
152 }
153
154 IGisItem::link_t tmp;
155 tmp.uri.setUrl(link.attributes().namedItem("href").nodeValue());
156 readXml(link, "text", tmp.text);
157 readXml(link, "type", tmp.type);
158
159 l << tmp;
160 }
161 }
162 }
163
readXml(const QDomNode & xml,IGisItem::history_t & history)164 static void readXml(const QDomNode& xml, IGisItem::history_t& history)
165 {
166 if(xml.namedItem("ql:history").isElement())
167 {
168 const QDomElement& xmlHistory = xml.namedItem("ql:history").toElement();
169
170 const QDomNodeList& xmlEntries = xmlHistory.elementsByTagName("ql:event");
171 for(int n = 0; n < xmlEntries.count(); ++n)
172 {
173 const QDomNode& xmlEntry = xmlEntries.item(n);
174 IGisItem::history_event_t entry;
175 readXml(xmlEntry, "ql:icon", entry.icon);
176 readXml(xmlEntry, "ql:time", entry.time);
177 readXml(xmlEntry, "ql:comment", entry.comment);
178
179 history.events << entry;
180 }
181
182 history.histIdxInitial = history.events.size() - 1;
183 history.histIdxCurrent = history.histIdxInitial;
184 }
185 }
186
readXml(const QDomNode & xml,const QString & tag,QPoint & offsetBubble,quint32 & widthBubble)187 static void readXml(const QDomNode& xml, const QString& tag, QPoint& offsetBubble, quint32& widthBubble)
188 {
189 if(xml.namedItem(tag).isElement())
190 {
191 const QDomElement& xmlBubble = xml.namedItem(tag).toElement();
192 int x = xmlBubble.attributes().namedItem("xoff").nodeValue().toInt();
193 int y = xmlBubble.attributes().namedItem("yoff").nodeValue().toInt();
194 offsetBubble = QPoint(x, y);
195 widthBubble = xmlBubble.attributes().namedItem("width").nodeValue().toInt();
196 }
197 }
198
199
writeXml(QDomNode & xml,const QString & tag,qint32 val)200 static void writeXml(QDomNode& xml, const QString& tag, qint32 val)
201 {
202 if(val != NOINT)
203 {
204 QDomElement elem = xml.ownerDocument().createElement(tag);
205 xml.appendChild(elem);
206 QDomText text = xml.ownerDocument().createTextNode(QString::number(val));
207 elem.appendChild(text);
208 }
209 }
210
writeXml(QDomNode & xml,const QString & tag,quint32 val)211 static void writeXml(QDomNode& xml, const QString& tag, quint32 val)
212 {
213 if(val != NOINT)
214 {
215 QDomElement elem = xml.ownerDocument().createElement(tag);
216 xml.appendChild(elem);
217 QDomText text = xml.ownerDocument().createTextNode(QString::number(val));
218 elem.appendChild(text);
219 }
220 }
221
writeXml(QDomNode & xml,const QString & tag,quint64 val)222 static void writeXml(QDomNode& xml, const QString& tag, quint64 val)
223 {
224 if(val != 0)
225 {
226 QDomElement elem = xml.ownerDocument().createElement(tag);
227 xml.appendChild(elem);
228 QDomText text = xml.ownerDocument().createTextNode(QString::number(val));
229 elem.appendChild(text);
230 }
231 }
232
writeXml(QDomNode & xml,const QString & tag,const QString & val)233 static void writeXml(QDomNode& xml, const QString& tag, const QString& val)
234 {
235 if(!val.isEmpty())
236 {
237 QDomElement elem = xml.ownerDocument().createElement(tag);
238 xml.appendChild(elem);
239 QDomText text = xml.ownerDocument().createTextNode(val);
240 elem.appendChild(text);
241 }
242 }
243
writeXml(QDomNode & xml,const QString & tag,qreal val)244 static void writeXml(QDomNode& xml, const QString& tag, qreal val)
245 {
246 if(val != NOFLOAT)
247 {
248 QDomElement elem = xml.ownerDocument().createElement(tag);
249 xml.appendChild(elem);
250 QDomText text = xml.ownerDocument().createTextNode(QString("%1").arg(val, 0, 'f', 8));
251 elem.appendChild(text);
252 }
253 }
254
writeXml(QDomNode & xml,const QString & tag,const QString & val,bool isHtml)255 static void writeXml(QDomNode& xml, const QString& tag, const QString& val, bool isHtml)
256 {
257 if(!val.isEmpty())
258 {
259 QDomElement elem = xml.ownerDocument().createElement(tag);
260 xml.appendChild(elem);
261 QDomText text = xml.ownerDocument().createCDATASection(val);
262 elem.appendChild(text);
263 elem.setAttribute("html", isHtml ? "True" : "False");
264 }
265 }
266
writeXml(QDomNode & xml,const QString & tag,const QDateTime & time)267 static void writeXml(QDomNode& xml, const QString& tag, const QDateTime& time)
268 {
269 if(time.isValid())
270 {
271 QDomElement elem = xml.ownerDocument().createElement(tag);
272 xml.appendChild(elem);
273 QDomText text = xml.ownerDocument().createTextNode(time.toString("yyyy-MM-dd'T'hh:mm:ss.zzz'Z'"));
274 elem.appendChild(text);
275 }
276 }
277
278
writeXml(QDomNode & xml,const QString & tag,const QList<IGisItem::link_t> & links)279 static void writeXml(QDomNode& xml, const QString& tag, const QList<IGisItem::link_t>& links)
280 {
281 if(!links.isEmpty())
282 {
283 for(const IGisItem::link_t& link : links)
284 {
285 QDomElement elem = xml.ownerDocument().createElement(tag);
286 xml.appendChild(elem);
287
288 elem.setAttribute("href", link.uri.toString());
289 writeXml(elem, "text", link.text);
290 writeXml(elem, "type", link.type);
291 }
292 }
293 }
294
writeXml(QDomNode & xml,const IGisItem::history_t & history)295 static void writeXml(QDomNode& xml, const IGisItem::history_t& history)
296 {
297 if(history.events.size() > 1)
298 {
299 QDomElement xmlHistory = xml.ownerDocument().createElement("ql:history");
300 xml.appendChild(xmlHistory);
301 for(int i = 0; i <= history.histIdxCurrent; i++)
302 {
303 const IGisItem::history_event_t& event = history.events[i];
304 QDomElement xmlEvent = xml.ownerDocument().createElement("ql:event");
305 xmlHistory.appendChild(xmlEvent);
306 writeXml(xmlEvent, "ql:icon", event.icon);
307 writeXml(xmlEvent, "ql:time", event.time);
308 writeXml(xmlEvent, "ql:comment", event.comment);
309 }
310 }
311 }
312
writeXml(QDomNode & xml,const QString & tag,const QPoint & offsetBubble,quint32 widthBubble)313 static void writeXml(QDomNode& xml, const QString& tag, const QPoint& offsetBubble, quint32 widthBubble)
314 {
315 QDomElement elem = xml.ownerDocument().createElement(tag);
316 xml.appendChild(elem);
317
318 elem.setAttribute("xoff", offsetBubble.x());
319 elem.setAttribute("yoff", offsetBubble.y());
320 elem.setAttribute("width", widthBubble);
321 }
322
323
readXml(const QDomNode & node,const QString & parentTags,QHash<QString,QVariant> & extensions)324 static void readXml(const QDomNode& node, const QString& parentTags, QHash<QString, QVariant>& extensions)
325 {
326 QString tag = node.nodeName();
327 if((tag.left(8) == "ql:flags") || (tag.left(11) == "ql:activity"))
328 {
329 return;
330 }
331
332 QString tags = parentTags.isEmpty() ? tag : parentTags + "|" + tag;
333 const QDomNode& next = node.firstChild();
334 if(next.isText())
335 {
336 extensions[tags] = node.toElement().text();
337 }
338 else
339 {
340 const QDomNodeList& list = node.childNodes();
341 for(int i = 0; i < list.size(); i++)
342 {
343 readXml(list.at(i), tags, extensions);
344 }
345 }
346 }
347
readXml(const QDomNode & ext,QHash<QString,QVariant> & extensions)348 static void readXml(const QDomNode& ext, QHash<QString, QVariant>& extensions)
349 {
350 const QDomNodeList& list = ext.childNodes();
351 for(int i = 0; i < list.size(); i++)
352 {
353 readXml(list.at(i), "", extensions);
354 }
355
356 extensions.squeeze();
357 }
358
writeXml(QDomNode & ext,const QHash<QString,QVariant> & extensions)359 static void writeXml(QDomNode& ext, const QHash<QString, QVariant>& extensions)
360 {
361 if(extensions.isEmpty())
362 {
363 return;
364 }
365
366 QDomDocument doc = ext.ownerDocument();
367
368 QStringList keys = extensions.keys();
369 qSort(keys.begin(), keys.end(), [] (const QString& k1, const QString& k2) {
370 return CKnownExtension::get(k1).order < CKnownExtension::get(k2).order;
371 });
372
373 for(const QString& key : qAsConst(keys))
374 {
375 QStringList tags = key.split('|', QString::SkipEmptyParts);
376
377 if(tags.size() == 1)
378 {
379 QDomElement elem = doc.createElement(tags.first());
380 ext.appendChild(elem);
381 QDomText text = doc.createTextNode(extensions[key].toString());
382 elem.appendChild(text);
383 }
384 else
385 {
386 QDomNode node = ext;
387
388 QString lastTag = tags.last();
389 tags.pop_back();
390 for(const QString& tag : qAsConst(tags))
391 {
392 QDomNode child = node.firstChildElement(tag);
393 if(child.isNull())
394 {
395 QDomElement elem = doc.createElement(tags.first());
396 node.appendChild(elem);
397 node = elem;
398 }
399 else
400 {
401 node = child;
402 }
403 }
404 QDomElement elem = doc.createElement(lastTag);
405 node.appendChild(elem);
406
407 QDomText text = doc.createTextNode(extensions[key].toString());
408 elem.appendChild(text);
409 }
410 }
411 }
412
readMetadata(const QDomNode & xml,metadata_t & metadata)413 void IGisProject::readMetadata(const QDomNode& xml, metadata_t& metadata)
414 {
415 readXml(xml, "name", metadata.name);
416 readXml(xml, "desc", metadata.desc);
417
418 const QDomNode& xmlAuthor = xml.namedItem("author");
419 if(xmlAuthor.isElement())
420 {
421 readXml(xml, "name", metadata.author.name);
422
423 const QDomNode& xmlEmail = xmlAuthor.namedItem("email");
424 if(xmlEmail.isElement())
425 {
426 const QDomNamedNodeMap& attr = xmlEmail.attributes();
427 metadata.author.id = attr.namedItem("id").nodeValue();
428 metadata.author.domain = attr.namedItem("domain").nodeValue();
429 }
430
431 const QDomNode& xmlLink = xmlAuthor.namedItem("link");
432 if(xmlLink.isElement())
433 {
434 metadata.author.link.uri.setUrl(xmlLink.attributes().namedItem("href").nodeValue());
435 readXml(xmlLink, "text", metadata.author.link.text);
436 readXml(xmlLink, "type", metadata.author.link.type);
437 }
438 }
439
440 const QDomNode& xmlCopyright = xml.namedItem("copyright");
441 if(xmlCopyright.isElement())
442 {
443 metadata.copyright.author = xmlCopyright.attributes().namedItem("author").nodeValue();
444 readXml(xmlCopyright, "year", metadata.copyright.year);
445 readXml(xmlCopyright, "license", metadata.copyright.license);
446 }
447
448 readXml(xml, "link", metadata.links);
449 readXml(xml, "time", metadata.time);
450 readXml(xml, "keywords", metadata.keywords);
451
452 const QDomNode& xmlBounds = xml.namedItem("bounds");
453 if(xmlBounds.isElement())
454 {
455 const QDomNamedNodeMap& attr = xmlBounds.attributes();
456 metadata.bounds.setLeft( attr.namedItem("minlon").nodeValue().toDouble());
457 metadata.bounds.setTop( attr.namedItem("maxlat").nodeValue().toDouble());
458 metadata.bounds.setRight( attr.namedItem("maxlon").nodeValue().toDouble());
459 metadata.bounds.setBottom(attr.namedItem("minlat").nodeValue().toDouble());
460 }
461 }
462
writeMetadata(QDomDocument & doc,bool strictGpx11)463 QDomNode IGisProject::writeMetadata(QDomDocument& doc, bool strictGpx11)
464 {
465 QDomElement gpx = doc.createElement("gpx");
466 doc.appendChild(gpx);
467
468 gpx.setAttribute("version", "1.1");
469 gpx.setAttribute("creator", "QMapShack " VER_STR " http://www.qlandkarte.org/");
470 gpx.setAttribute("xmlns", gpx_ns);
471 gpx.setAttribute("xmlns:xsi", xsi_ns);
472
473 QString schemaLocation;
474 if(!strictGpx11)
475 {
476 gpx.setAttribute("xmlns:gpxx", gpxx_ns);
477 gpx.setAttribute("xmlns:gpxtpx", gpxtpx_ns);
478 gpx.setAttribute("xmlns:wptx1", wptx1_ns);
479 gpx.setAttribute("xmlns:rmc", rmc_ns);
480 gpx.setAttribute("xmlns:ql", ql_ns);
481 gpx.setAttribute("xmlns:tp1", tp1_ns);
482 gpx.setAttribute("xmlns:gpxdata", gpxdata_ns);
483
484
485
486 schemaLocation = QString()
487 + gpx_ns + " http://www.topografix.com/GPX/1/1/gpx.xsd "
488 + gpxx_ns + " http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd "
489 + gpxtpx_ns + " http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd "
490 + wptx1_ns + " http://www.garmin.com/xmlschemas/WaypointExtensionv1.xsd "
491 + ql_ns + " http://www.qlandkarte.org/xmlschemas/v1.1/ql-extensions.xsd "
492 + gpxdata_ns + " http://www.cluetrust.com/Schemas/gpxdata10.xsd";
493 }
494 else
495 {
496 schemaLocation = QString()
497 + gpx_ns + " http://www.topografix.com/GPX/1/1/gpx.xsd ";
498 }
499
500
501 gpx.setAttribute("xsi:schemaLocation", schemaLocation);
502
503 QDomElement xmlMetadata = doc.createElement("metadata");
504 gpx.appendChild(xmlMetadata);
505
506 writeXml(xmlMetadata, "name", metadata.name);
507 writeXml(xmlMetadata, "desc", html2Dev(metadata.desc));
508
509 if(!metadata.author.name.isEmpty())
510 {
511 QDomElement xmlAuthor = doc.createElement("author");
512 xmlMetadata.appendChild(xmlAuthor);
513
514 writeXml(xmlAuthor, "name", metadata.author.name);
515
516 if(!metadata.author.id.isEmpty() && !metadata.author.domain.isEmpty())
517 {
518 QDomElement xmlEmail = doc.createElement("email");
519 xmlAuthor.appendChild(xmlEmail);
520 xmlEmail.setAttribute("id", metadata.author.id);
521 xmlEmail.setAttribute("domain", metadata.author.domain);
522 }
523
524 if(metadata.author.link.uri.isValid())
525 {
526 QDomElement xmlLink = doc.createElement("link");
527 xmlAuthor.appendChild(xmlLink);
528
529 xmlLink.setAttribute("href", metadata.author.link.uri.toString());
530 writeXml(xmlLink, "text", metadata.author.link.text);
531 writeXml(xmlLink, "type", metadata.author.link.type);
532 }
533 }
534
535 if(!metadata.copyright.author.isEmpty())
536 {
537 QDomElement xmlCopyright = doc.createElement("copyright");
538 xmlMetadata.appendChild(xmlCopyright);
539
540 xmlCopyright.setAttribute("author", metadata.copyright.author);
541 writeXml(xmlCopyright, "year", metadata.copyright.year);
542 writeXml(xmlCopyright, "license", metadata.copyright.license);
543 }
544 writeXml(xmlMetadata, "link", metadata.links);
545 writeXml(xmlMetadata, "time", metadata.time);
546 writeXml(xmlMetadata, "keywords", metadata.keywords);
547
548 if(metadata.bounds.isValid())
549 {
550 QDomElement xmlBounds = doc.createElement("bounds");
551 xmlMetadata.appendChild(xmlBounds);
552
553 xmlBounds.setAttribute("minlat", metadata.bounds.bottom());
554 xmlBounds.setAttribute("minlon", metadata.bounds.left());
555 xmlBounds.setAttribute("maxlat", metadata.bounds.top());
556 xmlBounds.setAttribute("maxlon", metadata.bounds.right());
557 }
558
559 return gpx;
560 }
561
readGpx(const QDomNode & xml)562 void CGisItemWpt::readGpx(const QDomNode& xml)
563 {
564 readWpt(xml, wpt);
565 // decode some well known extensions
566 if(xml.namedItem("extensions").isElement())
567 {
568 const QDomNode& ext = xml.namedItem("extensions");
569 readXml(ext, "ql:key", key.item);
570 readXml(ext, "ql:flags", flags);
571 readXml(ext, "ql:bubble", offsetBubble, widthBubble);
572 readXml(ext, history);
573
574 const QDomNode& wptx1 = ext.namedItem("wptx1:WaypointExtension");
575 readXml(wptx1, "wptx1:Proximity", proximity);
576
577 const QDomNode& xmlCache = ext.namedItem("cache");
578 if(!xmlCache.isNull())
579 {
580 // read OC cache extensions
581 }
582 }
583
584 const QDomNode& xmlCache = xml.namedItem("groundspeak:cache");
585 if(!xmlCache.isNull() && !geocache.hasData)
586 {
587 readGcExt(xmlCache);
588 }
589 }
590
save(QDomNode & gpx,bool strictGpx11)591 void CGisItemWpt::save(QDomNode& gpx, bool strictGpx11)
592 {
593 QDomDocument doc = gpx.ownerDocument();
594
595 QDomElement xmlWpt = doc.createElement("wpt");
596 gpx.appendChild(xmlWpt);
597 writeWpt(xmlWpt, wpt, strictGpx11);
598
599 if(!strictGpx11)
600 {
601 // write the key as extension tag
602 QDomElement xmlExt = doc.createElement("extensions");
603 xmlWpt.appendChild(xmlExt);
604 writeXml(xmlExt, "ql:key", key.item);
605 writeXml(xmlExt, "ql:flags", flags);
606 writeXml(xmlExt, "ql:bubble", offsetBubble, widthBubble);
607 writeXml(xmlExt, history);
608
609 // write other well known extensions
610 QDomElement wptx1 = doc.createElement("wptx1:WaypointExtension");
611 xmlExt.appendChild(wptx1);
612 writeXml(wptx1, "wptx1:Proximity", proximity);
613
614 if(geocache.hasData /*&& geocache.service == eGC*/)
615 {
616 QDomElement xmlCache = doc.createElement("groundspeak:cache");
617 writeGcExt(xmlCache);
618 xmlWpt.appendChild(xmlCache);
619 }
620 }
621 }
622
readGcExt(const QDomNode & xmlCache)623 void CGisItemWpt::readGcExt(const QDomNode& xmlCache)
624 {
625 //Geocaches only have one link
626 if(wpt.links.isEmpty())
627 {
628 geocache.service = eUnknown;
629 }
630 else
631 {
632 if(wpt.links.first().uri.url(QUrl::RemovePath).contains("geocaching.com"))
633 {
634 geocache.service = eGcCom;
635 }
636 else if(wpt.links.first().uri.url(QUrl::RemovePath).contains("opencaching"))
637 {
638 geocache.service = eOc;
639 }
640 else if(wpt.links.first().uri.url(QUrl::RemovePath).contains("geocaching.su"))
641 {
642 geocache.service = eGcSu;
643 }
644 else
645 {
646 geocache.service = eUnknown;
647 }
648 }
649
650 const QDomNamedNodeMap& attr = xmlCache.attributes();
651 geocache.id = attr.namedItem("id").nodeValue().toInt();
652
653 QDomNode geocacheAttributes = xmlCache.namedItem("groundspeak:attributes");
654
655 geocache.archived = attr.namedItem("archived").nodeValue().toLocal8Bit().toLower() == "true";
656 geocache.available = attr.namedItem("available").nodeValue().toLocal8Bit().toLower() == "true";
657
658 for(QDomNode xmlAttribute = geocacheAttributes.firstChild(); !xmlAttribute.isNull(); xmlAttribute = xmlAttribute.nextSibling())
659 {
660 quint8 id = xmlAttribute.attributes().namedItem("id").nodeValue().toUInt();
661 if(id >= geocache.attributeMeanings.size())
662 {
663 qWarning() << "CGisItemWpt::readGcExt(): Ignore unknown attribute ID " << id;
664 continue;
665 }
666
667 qint8 intvalue = xmlAttribute.attributes().namedItem("inc").nodeValue().toUInt();
668 geocache.attributes[id] = (intvalue == 1);
669 if(id == 42) //42 is the code for 'Needs maintenance' and it only appears, when there attribute is set
670 {
671 geocache.needsMaintenance = true;
672 }
673 }
674
675 readXml(xmlCache, "groundspeak:name", geocache.name);
676 readXml(xmlCache, "groundspeak:placed_by", geocache.owner);
677 readXml(xmlCache, "groundspeak:type", geocache.type);
678 readXml(xmlCache, "groundspeak:container", geocache.container);
679 readXml(xmlCache, "groundspeak:difficulty", geocache.difficulty);
680 readXml(xmlCache, "groundspeak:terrain", geocache.terrain);
681 readXml(xmlCache, "groundspeak:short_description", geocache.shortDesc, geocache.shortDescIsHtml);
682 readXml(xmlCache, "groundspeak:long_description", geocache.longDesc, geocache.longDescIsHtml);
683 readXml(xmlCache, "groundspeak:encoded_hints", geocache.hint);
684 readXml(xmlCache, "groundspeak:country", geocache.country);
685 readXml(xmlCache, "groundspeak:state", geocache.state);
686
687 const QDomNodeList& logs = xmlCache.toElement().elementsByTagName("groundspeak:log");
688 uint N = logs.count();
689
690 for(uint n = 0; n < N; ++n)
691 {
692 const QDomNode& xmlLog = logs.item(n);
693 const QDomNamedNodeMap& attr = xmlLog.attributes();
694
695 geocachelog_t log;
696 log.id = attr.namedItem("id").nodeValue().toUInt();
697 readXml(xmlLog, "groundspeak:date", log.date);
698 readXml(xmlLog, "groundspeak:type", log.type);
699 if(xmlLog.namedItem("groundspeak:finder").isElement())
700 {
701 const QDomNamedNodeMap& attr = xmlLog.namedItem("groundspeak:finder").attributes();
702 log.finderId = attr.namedItem("id").nodeValue();
703 }
704
705 readXml(xmlLog, "groundspeak:finder", log.finder);
706 readXml(xmlLog, "groundspeak:text", log.text, log.textIsHtml);
707
708 geocache.logs << log;
709 }
710 geocache.hasData = true;
711 }
712
713
714
writeGcExt(QDomNode & xmlCache)715 void CGisItemWpt::writeGcExt(QDomNode& xmlCache)
716 {
717 QString str;
718 xmlCache.toElement().setAttribute("xmlns:groundspeak", "http://www.groundspeak.com/cache/1/0");
719 xmlCache.toElement().setAttribute("id", geocache.id);
720 xmlCache.toElement().setAttribute("archived", geocache.archived ? "True" : "False");
721 xmlCache.toElement().setAttribute("available", geocache.available ? "True" : "False");
722
723 writeXml(xmlCache, "groundspeak:name", geocache.name);
724 writeXml(xmlCache, "groundspeak:placed_by", geocache.owner);
725 writeXml(xmlCache, "groundspeak:type", geocache.type);
726 writeXml(xmlCache, "groundspeak:container", geocache.container);
727
728 QDomElement xmlAttributes = xmlCache.ownerDocument().createElement("groundspeak:attributes");
729 const QList<quint8>& attributes = geocache.attributes.keys();
730 for(quint8 attribute : attributes)
731 {
732 QDomElement xmlAttribute = xmlCache.ownerDocument().createElement("groundspeak:attribute");
733 xmlAttribute.setAttribute("id", attribute);
734 qint8 inc = geocache.attributes[attribute] ? 1 : 0;
735 xmlAttribute.setAttribute("inc", inc);
736 QDomText text;
737 if(attribute < geocache.attributeMeanings.size())
738 {
739 text = xmlCache.ownerDocument().createTextNode(geocache.attributeMeanings[attribute]);
740 }
741 xmlAttribute.appendChild(text);
742 xmlAttributes.appendChild(xmlAttribute);
743 xmlCache.appendChild(xmlAttributes);
744 }
745
746 if(geocache.difficulty == int(geocache.difficulty))
747 {
748 str.sprintf("%1.0f", geocache.difficulty);
749 }
750 else
751 {
752 str.sprintf("%1.1f", geocache.difficulty);
753 }
754 writeXml(xmlCache, "groundspeak:difficulty", str);
755
756 if(geocache.terrain == int(geocache.terrain))
757 {
758 str.sprintf("%1.0f", geocache.terrain);
759 }
760 else
761 {
762 str.sprintf("%1.1f", geocache.terrain);
763 }
764 writeXml(xmlCache, "groundspeak:terrain", str);
765 writeXml(xmlCache, "groundspeak:short_description", geocache.shortDesc, geocache.shortDescIsHtml);
766 writeXml(xmlCache, "groundspeak:long_description", geocache.longDesc, geocache.longDescIsHtml);
767 writeXml(xmlCache, "groundspeak:encoded_hints", geocache.hint);
768
769 if(!geocache.logs.isEmpty())
770 {
771 QDomElement xmlLogs = xmlCache.ownerDocument().createElement("groundspeak:logs");
772 xmlCache.appendChild(xmlLogs);
773
774 for(const geocachelog_t& log : qAsConst(geocache.logs))
775 {
776 QDomElement xmlLog = xmlCache.ownerDocument().createElement("groundspeak:log");
777 xmlLogs.appendChild(xmlLog);
778
779 xmlLog.setAttribute("id", log.id);
780 writeXml(xmlLog, "groundspeak:date", log.date);
781 writeXml(xmlLog, "groundspeak:type", log.type);
782
783 QDomElement xmlFinder = xmlCache.ownerDocument().createElement("groundspeak:finder");
784 xmlLog.appendChild(xmlFinder);
785
786 QDomText _finder_ = xmlCache.ownerDocument().createCDATASection(log.finder);
787 xmlFinder.appendChild(_finder_);
788 xmlFinder.setAttribute("id", log.finderId);
789
790 writeXml(xmlLog, "groundspeak:text", log.text, log.textIsHtml);
791 }
792 }
793 }
794
795
readTrk(const QDomNode & xml,CTrackData & trk)796 void CGisItemTrk::readTrk(const QDomNode& xml, CTrackData& trk)
797 {
798 readXml(xml, "name", trk.name);
799 readXml(xml, "cmt", trk.cmt);
800 readXml(xml, "desc", trk.desc);
801 readXml(xml, "src", trk.src);
802 readXml(xml, "link", trk.links);
803 readXml(xml, "number", trk.number);
804 readXml(xml, "type", trk.type);
805
806 const QDomNodeList& trksegs = xml.toElement().elementsByTagName("trkseg");
807 int N = trksegs.count();
808 trk.segs.resize(N);
809 for(int n = 0; n < N; ++n)
810 {
811 const QDomNode& trkseg = trksegs.item(n);
812 CTrackData::trkseg_t& seg = trk.segs[n];
813
814 const QDomNodeList& xmlTrkpts = trkseg.toElement().elementsByTagName("trkpt");
815 int M = xmlTrkpts.count();
816 seg.pts.resize(M);
817 for(int m = 0; m < M; ++m)
818 {
819 CTrackData::trkpt_t& trkpt = seg.pts[m];
820 const QDomNode& xmlTrkpt = xmlTrkpts.item(m);
821 readWpt(xmlTrkpt, trkpt);
822
823 const QDomNode& ext = xmlTrkpt.namedItem("extensions");
824 if(ext.isElement())
825 {
826 readXml(ext, "ql:flags", trkpt.flags);
827 readXml(ext, "ql:activity", trkpt.activity);
828 trkpt.sanitizeFlags();
829 readXml(ext, trkpt.extensions);
830 }
831 }
832 }
833
834 // decode some well known extensions
835 const QDomNode& ext = xml.namedItem("extensions");
836 if(ext.isElement())
837 {
838 readXml(ext, "ql:key", key.item);
839 readXml(ext, "ql:flags", flags);
840 readXml(ext, history);
841
842 const QDomNode& gpxx = ext.namedItem("gpxx:TrackExtension");
843 readXml(gpxx, "gpxx:DisplayColor", trk.color);
844 setColor(str2color(trk.color));
845 }
846
847 deriveSecondaryData();
848 }
849
850
851
save(QDomNode & gpx,bool strictGpx11)852 void CGisItemTrk::save(QDomNode& gpx, bool strictGpx11)
853 {
854 QDomDocument doc = gpx.ownerDocument();
855
856 QDomElement xmlTrk = doc.createElement("trk");
857 gpx.appendChild(xmlTrk);
858
859 writeXml(xmlTrk, "name", trk.name);
860 writeXml(xmlTrk, "cmt", html2Dev(trk.cmt, strictGpx11));
861 writeXml(xmlTrk, "desc", html2Dev(trk.desc, strictGpx11));
862 writeXml(xmlTrk, "src", trk.src);
863 writeXml(xmlTrk, "link", trk.links);
864 writeXml(xmlTrk, "number", trk.number);
865 writeXml(xmlTrk, "type", trk.type);
866
867 if(!strictGpx11)
868 {
869 // write the key as extension tag
870 QDomElement xmlExt = doc.createElement("extensions");
871 xmlTrk.appendChild(xmlExt);
872 writeXml(xmlExt, "ql:key", key.item);
873 writeXml(xmlExt, "ql:flags", flags);
874 writeXml(xmlExt, history);
875
876 // write other well known extensions
877 QDomElement gpxx = doc.createElement("gpxx:TrackExtension");
878 xmlExt.appendChild(gpxx);
879 writeXml(gpxx, "gpxx:DisplayColor", trk.color);
880 }
881
882 for(const CTrackData::trkseg_t& seg : qAsConst(trk.segs))
883 {
884 QDomElement xmlTrkseg = doc.createElement("trkseg");
885 xmlTrk.appendChild(xmlTrkseg);
886
887 for(const CTrackData::trkpt_t& pt : seg.pts)
888 {
889 QDomElement xmlTrkpt = doc.createElement("trkpt");
890 xmlTrkseg.appendChild(xmlTrkpt);
891 writeWpt(xmlTrkpt, pt, strictGpx11);
892
893 if(!strictGpx11)
894 {
895 QDomElement xmlExt = doc.createElement("extensions");
896 xmlTrkpt.appendChild(xmlExt);
897 writeXml(xmlExt, "ql:flags", pt.flags);
898 writeXml(xmlExt, "ql:activity", pt.activity);
899 writeXml(xmlExt, pt.extensions);
900 }
901 }
902 }
903 }
904
readRte(const QDomNode & xml,rte_t & rte)905 void CGisItemRte::readRte(const QDomNode& xml, rte_t& rte)
906 {
907 readXml(xml, "name", rte.name);
908 readXml(xml, "cmt", rte.cmt);
909 readXml(xml, "desc", rte.desc);
910 readXml(xml, "src", rte.src);
911 readXml(xml, "link", rte.links);
912 readXml(xml, "number", rte.number);
913 readXml(xml, "type", rte.type);
914
915 const QDomNodeList& xmlRtepts = xml.toElement().elementsByTagName("rtept");
916 int M = xmlRtepts.count();
917 rte.pts.resize(M);
918 for(int m = 0; m < M; ++m)
919 {
920 rtept_t& rtept = rte.pts[m];
921 const QDomNode& xmlRtept = xmlRtepts.item(m);
922 readWpt(xmlRtept, rtept);
923 rtept.icon = CWptIconManager::self().getWptIconByName(rtept.sym, rtept.focus);
924 }
925
926 // decode some well known extensions
927 if(xml.namedItem("extensions").isElement())
928 {
929 const QDomNode& ext = xml.namedItem("extensions");
930 readXml(ext, "ql:key", key.item);
931 }
932 }
933
934
save(QDomNode & gpx,bool strictGpx11)935 void CGisItemRte::save(QDomNode& gpx, bool strictGpx11)
936 {
937 QDomDocument doc = gpx.ownerDocument();
938
939 QDomElement xmlRte = doc.createElement("rte");
940 gpx.appendChild(xmlRte);
941
942 writeXml(xmlRte, "name", rte.name);
943 writeXml(xmlRte, "cmt", html2Dev(rte.cmt, strictGpx11));
944 writeXml(xmlRte, "desc", html2Dev(rte.desc, strictGpx11));
945 writeXml(xmlRte, "src", rte.src);
946 writeXml(xmlRte, "link", rte.links);
947 writeXml(xmlRte, "number", rte.number);
948 writeXml(xmlRte, "type", rte.type);
949
950 if(!strictGpx11)
951 {
952 // write the key as extension tag
953 QDomElement xmlExt = doc.createElement("extensions");
954 xmlRte.appendChild(xmlExt);
955 writeXml(xmlExt, "ql:key", key.item);
956 }
957
958 for(const rtept_t& pt : qAsConst(rte.pts))
959 {
960 QDomElement xmlRtept = doc.createElement("rtept");
961 xmlRte.appendChild(xmlRtept);
962 writeWpt(xmlRtept, pt, strictGpx11);
963 }
964 }
965
readArea(const QDomNode & xml,area_t & area)966 void CGisItemOvlArea::readArea(const QDomNode& xml, area_t& area)
967 {
968 readXml(xml, "ql:name", area.name);
969 readXml(xml, "ql:cmt", area.cmt);
970 readXml(xml, "ql:desc", area.desc);
971 readXml(xml, "ql:src", area.src);
972 readXml(xml, "ql:link", area.links);
973 readXml(xml, "ql:number", area.number);
974 readXml(xml, "ql:type", area.type);
975 readXml(xml, "ql:color", area.color);
976 readXml(xml, "ql:width", area.width);
977 readXml(xml, "ql:style", area.style);
978 readXml(xml, "ql:opacity", area.opacity);
979 readXml(xml, "ql:key", key.item);
980 readXml(xml, "ql:flags", flags);
981 readXml(xml, history);
982
983 const QDomNodeList& xmlPts = xml.toElement().elementsByTagName("ql:point");
984 int M = xmlPts.count();
985 area.pts.resize(M);
986 for(int m = 0; m < M; ++m)
987 {
988 pt_t& pt = area.pts[m];
989 const QDomNode& xmlPt = xmlPts.item(m);
990 readWpt(xmlPt, pt);
991 }
992
993 setColor(str2color(area.color));
994
995 deriveSecondaryData();
996 }
997
save(QDomNode & gpx,bool strictGpx11)998 void CGisItemOvlArea::save(QDomNode& gpx, bool strictGpx11)
999 {
1000 QDomDocument doc = gpx.ownerDocument();
1001
1002 QDomElement xmlArea = doc.createElement("ql:area");
1003 gpx.appendChild(xmlArea);
1004
1005 writeXml(xmlArea, "ql:name", area.name);
1006 writeXml(xmlArea, "ql:cmt", area.cmt);
1007 writeXml(xmlArea, "ql:desc", area.desc);
1008 writeXml(xmlArea, "ql:src", area.src);
1009 writeXml(xmlArea, "ql:link", area.links);
1010 writeXml(xmlArea, "ql:number", area.number);
1011 writeXml(xmlArea, "ql:type", area.type);
1012 writeXml(xmlArea, "ql:color", area.color);
1013 writeXml(xmlArea, "ql:width", area.width);
1014 writeXml(xmlArea, "ql:style", area.style);
1015 writeXml(xmlArea, "ql:opacity", area.opacity);
1016 writeXml(xmlArea, "ql:key", key.item);
1017 writeXml(xmlArea, "ql:flags", flags);
1018 writeXml(xmlArea, history);
1019
1020 for(const pt_t& pt : qAsConst(area.pts))
1021 {
1022 QDomElement xmlPt = doc.createElement("ql:point");
1023 xmlArea.appendChild(xmlPt);
1024 writeWpt(xmlPt, pt, strictGpx11);
1025 }
1026 }
1027
readWpt(const QDomNode & xml,wpt_t & wpt)1028 void IGisItem::readWpt(const QDomNode& xml, wpt_t& wpt)
1029 {
1030 const QDomNamedNodeMap& attr = xml.attributes();
1031 wpt.lat = attr.namedItem("lat").nodeValue().toDouble();
1032 wpt.lon = attr.namedItem("lon").nodeValue().toDouble();
1033
1034 readXml(xml, "ele", wpt.ele);
1035 readXml(xml, "time", wpt.time);
1036 readXml(xml, "magvar", wpt.magvar);
1037 readXml(xml, "geoidheight", wpt.geoidheight);
1038 readXml(xml, "name", wpt.name);
1039 readXml(xml, "cmt", wpt.cmt);
1040 readXml(xml, "desc", wpt.desc);
1041 readXml(xml, "src", wpt.src);
1042 readXml(xml, "link", wpt.links);
1043 readXml(xml, "sym", wpt.sym);
1044 readXml(xml, "type", wpt.type);
1045 readXml(xml, "fix", wpt.fix);
1046 readXml(xml, "sat", wpt.sat);
1047 readXml(xml, "hdop", wpt.hdop);
1048 readXml(xml, "vdop", wpt.vdop);
1049 readXml(xml, "pdop", wpt.pdop);
1050 readXml(xml, "ageofdgpsdata", wpt.ageofdgpsdata);
1051 readXml(xml, "dgpsid", wpt.dgpsid);
1052
1053 // some GPX 1.0 backward compatibility
1054 QString url;
1055 readXml(xml, "url", url);
1056 if(!url.isEmpty())
1057 {
1058 link_t link;
1059 link.uri.setUrl(url);
1060 readXml(xml, "urlname", link.text);
1061
1062 wpt.links << link;
1063 }
1064 }
1065
1066
writeWpt(QDomElement & xml,const wpt_t & wpt,bool strictGpx11)1067 void IGisItem::writeWpt(QDomElement& xml, const wpt_t& wpt, bool strictGpx11)
1068 {
1069 QString str;
1070
1071 str.sprintf("%1.8f", wpt.lat);
1072 xml.setAttribute("lat", str);
1073 str.sprintf("%1.8f", wpt.lon);
1074 xml.setAttribute("lon", str);
1075
1076 writeXml(xml, "ele", wpt.ele);
1077 writeXml(xml, "time", wpt.time);
1078 writeXml(xml, "magvar", wpt.magvar);
1079 writeXml(xml, "geoidheight", wpt.geoidheight);
1080 writeXml(xml, "name", wpt.name);
1081 writeXml(xml, "cmt", html2Dev(wpt.cmt, strictGpx11));
1082 writeXml(xml, "desc", html2Dev(wpt.desc, strictGpx11));
1083 if(isOnDevice() != IDevice::eTypeGarmin)
1084 {
1085 writeXml(xml, "src", wpt.src);
1086 }
1087 writeXml(xml, "link", wpt.links);
1088 writeXml(xml, "sym", wpt.sym);
1089 writeXml(xml, "type", wpt.type);
1090 writeXml(xml, "fix", wpt.fix);
1091 writeXml(xml, "sat", wpt.sat);
1092 writeXml(xml, "hdop", wpt.hdop);
1093 writeXml(xml, "vdop", wpt.vdop);
1094 writeXml(xml, "pdop", wpt.pdop);
1095 writeXml(xml, "ageofdgpsdata", wpt.ageofdgpsdata);
1096 writeXml(xml, "dgpsid", wpt.dgpsid);
1097 }
1098
1099
createAdventureFromProject(IGisProject * project,const QString & gpxFilename)1100 void CDeviceGarmin::createAdventureFromProject(IGisProject* project, const QString& gpxFilename)
1101 {
1102 if(pathAdventures.isEmpty())
1103 {
1104 return;
1105 }
1106
1107 QDomDocument doc;
1108
1109 QDomElement adventure = doc.createElement("Adventure");
1110 doc.appendChild(adventure);
1111 adventure.setAttribute("xmlns", "http://www.garmin.com/xmlschemas/GarminAdventure/v1");
1112
1113 writeXml(adventure, "GlobalId", project->getKey());
1114 writeXml(adventure, "Name", project->getName());
1115
1116 QDomElement item = doc.createElement("Item");
1117 adventure.appendChild(item);
1118 writeXml(item, "DataType", "GPSData");
1119 writeXml(item, "Location", gpxFilename);
1120
1121 writeXml(adventure, "Description", IGisItem::removeHtml(project->getDescription()));
1122
1123 const int N = project->childCount();
1124 for(int i = 0; i < N; i++)
1125 {
1126 CGisItemTrk* track = dynamic_cast<CGisItemTrk*>(project->child(i));
1127 if(track != nullptr)
1128 {
1129 const CTrackData& trk = track->getTrackData();
1130 if(trk.segs.isEmpty())
1131 {
1132 continue;
1133 }
1134
1135 if(trk.segs.first().pts.isEmpty())
1136 {
1137 continue;
1138 }
1139
1140 const CTrackData::trkpt_t& origin = trk.segs.first().pts.first();
1141
1142 QDomElement startPosition = doc.createElement("StartPosition");
1143 adventure.appendChild(startPosition);
1144 writeXml(startPosition, "Lat", origin.lat);
1145 writeXml(startPosition, "Lon", origin.lon);
1146
1147 writeXml(adventure, "Activity", tr("Unknown"));
1148 writeXml(adventure, "Distance", track->getTotalDistance());
1149 writeXml(adventure, "Duration", track->getTotalElapsedSecondsMoving());
1150 writeXml(adventure, "Ascent", track->getTotalAscent());
1151 writeXml(adventure, "Descent", track->getTotalDescent());
1152 writeXml(adventure, "Difficulty", 1);
1153 writeXml(adventure, "NumRatings", 0);
1154 writeXml(adventure, "MainTrackId", track->getName());
1155
1156 QDomElement waypointOrder = doc.createElement("WaypointOrder");
1157 adventure.appendChild(waypointOrder);
1158
1159 for(const CTrackData::trkpt_t& trkpt : trk)
1160 {
1161 if(trkpt.keyWpt.item.isEmpty())
1162 {
1163 continue;
1164 }
1165
1166 const CGisItemWpt* wpt = dynamic_cast<CGisItemWpt*>(project->getItemByKey(trkpt.keyWpt));
1167 if(wpt == nullptr)
1168 {
1169 continue;
1170 }
1171
1172 QDomElement waypoints = doc.createElement("Waypoints");
1173 waypointOrder.appendChild(waypoints);
1174
1175 writeXml(waypoints, "ID", wpt->getName());
1176 writeXml(waypoints, "DistanceFromOrigin", trkpt.distance);
1177 }
1178
1179 break;
1180 }
1181 }
1182
1183
1184 const QDir dirAdventures(dir.absoluteFilePath(pathAdventures));
1185 QString filename = dirAdventures.absoluteFilePath(project->getKey() + ".adv");
1186 QFile file(filename);
1187
1188 CDeviceMountLock mountLock(*this);
1189
1190 file.open(QIODevice::WriteOnly);
1191 QTextStream out(&file);
1192 out.setCodec("UTF-8");
1193 out << "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>" << endl;
1194 out << doc.toString();
1195 file.close();
1196 }
1197