gpxgeotag.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. #include <iostream>
  2. #include <cstring>
  3. #include <fstream>
  4. #include <vector>
  5. #include <cmath>
  6. #include <ctime>
  7. #include <limits>
  8. #include <iomanip>
  9. #include <stdexcept>
  10. #include <algorithm>
  11. #include "XMLParser.h"
  12. #include "Arguments.h"
  13. // ----------------------------------------------------------------------------
  14. namespace gpxtools
  15. {
  16. class GPXGeoTag : public XMLParserHandler
  17. {
  18. public:
  19. // -- Constructor -----------------------------------------------------------
  20. GPXGeoTag() :
  21. _arguments("gpxgeotag [OPTION].. [GPX-FILE] [JPG-FILE]\nGeo tag the photos with the GPX-files.\n", "gpxgeotag v0.1",
  22. "Geo tag the photos with the GPX-files, based on the time of the photos and the geopositions in the GPX-files."),
  23. _seconds (_arguments, true, 's', "seconds", "SEC", "the seconds offset for the time of the photos", ""),
  24. _minutes (_arguments, true, 'm', "minutes", "MIN", "the minutes offset for the time of the photos", ""),
  25. _hours (_arguments, true, 'h', "hours", "HOURS", "the hours offset for the time of the photos", ""),
  26. _xmlParser(this),
  27. _offset(0),
  28. _jpegnames(),
  29. _gpxnames(),
  30. _segments(),
  31. _timeStr()
  32. {
  33. }
  34. // -- Deconstructor ---------------------------------------------------------
  35. virtual ~GPXGeoTag()
  36. {
  37. }
  38. // -- Parse arguments -------------------------------------------------------
  39. bool processArguments(int argc, char *argv[])
  40. {
  41. std::vector<std::string> filenames;
  42. if (!_arguments.parse(argc,argv, filenames))
  43. {
  44. return false;
  45. }
  46. else if (!checkArguments(filenames))
  47. {
  48. return false;
  49. }
  50. else if (_gpxnames.empty())
  51. {
  52. return _xmlParser.parse(std::cin);
  53. }
  54. else
  55. {
  56. for (auto filename = _gpxnames.begin(); filename != _gpxnames.end(); ++filename)
  57. {
  58. if (!parseFile(*filename)) return false;
  59. }
  60. }
  61. return true;
  62. }
  63. // -- Check arguments ---------------------------------------------------------
  64. bool checkArguments(const std::vector<std::string> &filenames)
  65. {
  66. _offset = 0;
  67. if (!_seconds.value().empty())
  68. {
  69. try
  70. {
  71. _offset += std::stoi(_seconds.value());
  72. }
  73. catch (...)
  74. {
  75. std::cerr << "Invalid value for --" << _seconds.longOption() << " : " << _seconds.value() << std::endl;
  76. return false;
  77. }
  78. }
  79. if (!_minutes.value().empty())
  80. {
  81. try
  82. {
  83. _offset += (std::stoi(_minutes.value()) * 60);
  84. }
  85. catch (...)
  86. {
  87. std::cerr << "Invalid value for --" << _minutes.longOption() << " : " << _minutes.value() << std::endl;
  88. return false;
  89. }
  90. }
  91. if (!_hours.value().empty())
  92. {
  93. try
  94. {
  95. _offset += (std::stoi(_hours.value()) * 3600);
  96. }
  97. catch (...)
  98. {
  99. std::cerr << "Invalid value for --" << _hours.longOption() << " : " << _hours.value() << std::endl;
  100. return false;
  101. }
  102. }
  103. for (auto filename = filenames.begin(); filename != filenames.end(); ++filename)
  104. {
  105. std::size_t pos = filename->find_last_of('.');
  106. if (pos == std::string::npos)
  107. {
  108. std::cerr << "File " << *filename << " ignored" << std::endl;
  109. continue;
  110. }
  111. std::string extension = filename->substr(pos+1);
  112. std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
  113. if ((extension == "jpg") || (extension == "jpeg"))
  114. {
  115. _jpegnames.push_back(*filename);
  116. }
  117. else if (extension == "gpx")
  118. {
  119. _gpxnames.push_back(*filename);
  120. }
  121. else
  122. {
  123. std::cerr << "Unsupported file " << *filename << " ignored" << std::endl;
  124. }
  125. }
  126. if (_jpegnames.empty())
  127. {
  128. std::cerr << "No photo files present" << std::endl;
  129. return false;
  130. }
  131. return true;
  132. }
  133. // -- Perform the geo-tagging -----------------------------------------------
  134. void perform()
  135. {
  136. for (auto photo = _jpegnames.begin(); photo != _jpegnames.end(); ++photo)
  137. {
  138. time_t time = readExiv2Time(*photo) + _offset;
  139. if (time == time_t(-1))
  140. {
  141. std::cerr << "No Exif.Image.DateTime present in " << *photo << std::endl;
  142. continue;
  143. }
  144. double lat;
  145. double lon;
  146. if (!findGeoPosition(time, lat,lon))
  147. {
  148. std::cerr << "No GeoPosition found for " << *photo << std::endl;
  149. continue;
  150. }
  151. if (!updateExiv2GeoPosition(*photo, lat, lon))
  152. {
  153. std::cerr << "Unable to update the GeoPosition for " << *photo << std::endl;
  154. }
  155. std::cout << std::fixed << std::setprecision(5) << "GeoPosition (" << lat << "," << lon << ") set for " << *photo << std::endl;
  156. }
  157. }
  158. // -- Parse a file ----------------------------------------------------------
  159. bool parseFile(const std::string &filename)
  160. {
  161. bool ok =false;
  162. std::ifstream file(filename);
  163. if (file.is_open())
  164. {
  165. ok = _xmlParser.parse(file);
  166. file.close();
  167. }
  168. else
  169. {
  170. std::cerr << "Unable to open: " << filename << std::endl;
  171. }
  172. return ok;
  173. }
  174. private:
  175. // -- Types -----------------------------------------------------------------
  176. struct Point
  177. {
  178. Point() : _lat(0.0), _lon(0.0), _time(-1) {}
  179. double _lat;
  180. double _lon;
  181. time_t _time;
  182. };
  183. typedef std::vector<Point> Segment;
  184. typedef std::vector<Segment> Segments;
  185. // -- Methods ---------------------------------------------------------------
  186. static bool getDoubleAttribute(const Attributes &atts, const std::string &key, double &value)
  187. {
  188. auto iter = atts.find(key);
  189. if (iter == atts.end()) return false;
  190. if (iter->second.empty()) return false;
  191. try
  192. {
  193. value = std::stod(iter->second);
  194. return true;
  195. }
  196. catch(...)
  197. {
  198. return false;
  199. }
  200. }
  201. void processTimeStr(std::string &timeStr, time_t &time)
  202. {
  203. time = time_t(-1);
  204. XMLParser::trim(timeStr);
  205. if (timeStr.size() == 0) return;
  206. std::size_t index = timeStr.find(".");
  207. if (index != std::string::npos)
  208. {
  209. timeStr.erase(index, 4); // remove .000 millis
  210. }
  211. struct tm fields;
  212. memset(&fields, 0, sizeof(fields));
  213. #if __GLIBC__
  214. if (strptime(timeStr.c_str(), "%FT%T%z", &fields) == nullptr)
  215. #else
  216. if (strptime(timeStr.c_str(), "%Y-%m-%dT%TZ", &fields) == nullptr)
  217. #endif
  218. {
  219. std::cout << "Failed to convert: " << timeStr << std::endl;
  220. return;
  221. }
  222. time = mktime(&fields) - ::timezone;
  223. }
  224. time_t readExiv2Time(const std::string &filename)
  225. {
  226. const std::string Exiv2key = "Exif.Image.DateTime";
  227. std::string command = "exiv2 -pt -g " + Exiv2key + " print " + filename;
  228. //std::cout << "Fetch: " << command << std::endl;
  229. FILE *file = nullptr;
  230. if ((file = popen(command.c_str(), "r")) == nullptr)
  231. {
  232. std::cerr << "Unable to start " << command << ", error: " << errno << std::endl;
  233. return time_t(-1);
  234. }
  235. char buffer[512];
  236. while (fgets(buffer, sizeof(buffer), file) != nullptr)
  237. {
  238. std::string line(buffer);
  239. std::size_t index = 0;
  240. std::string key = readWord(line, index, ' ');
  241. readWord(line, index, ' '); // Type
  242. readWord(line, index, ' '); // Length
  243. std::string value = readWord(line, index, '\n');
  244. if ((key == Exiv2key) && (!value.empty()))
  245. {
  246. struct tm fields;
  247. memset(&fields, 0, sizeof(fields));
  248. if (strptime(value.c_str(), "%Y:%m:%d %T", &fields) != nullptr)
  249. {
  250. return mktime(&fields);
  251. }
  252. }
  253. }
  254. return time_t(-1);
  255. }
  256. std::string readWord(const std::string &line, std::size_t &index, char ch)
  257. {
  258. while ((index < line.size()) && (::isspace(line[index])))
  259. {
  260. index++;
  261. }
  262. std::string word;
  263. while ((index < line.size()) && (line[index] != ch))
  264. {
  265. word += line[index++];
  266. }
  267. return word;
  268. }
  269. bool findGeoPosition(time_t time, double &lat, double &lon)
  270. {
  271. for (auto segment = _segments.begin(); segment != _segments.end(); ++segment)
  272. {
  273. if (segment->empty()) continue;
  274. if ((time < segment->front()._time) || (time > segment->back()._time)) continue;
  275. auto last = segment->begin();
  276. for (auto point = segment->begin(); point != segment->end(); ++point)
  277. {
  278. if (time == point->_time)
  279. {
  280. lat = point->_lat;
  281. lon = point->_lon;
  282. return true;
  283. }
  284. if (time < point->_time)
  285. {
  286. lat = last->_lat + (point->_lat - last->_lat) * (double(time - last->_time) / double(point->_time - last->_time));
  287. lon = last->_lon + (point->_lon - last->_lon) * (double(time - last->_time) / double(point->_time - last->_time));
  288. return true;
  289. }
  290. last = point;
  291. }
  292. }
  293. return false;
  294. }
  295. bool updateExiv2GeoPosition(const std::string &photo, double lat, double lon)
  296. {
  297. std::string command = "exiv2 ";
  298. const std::string NS = (lat < 0 ? "S" : "N");
  299. command += "-M \"set Exif.GPSInfo.GPSLatitude " + deg2string(std::abs(lat)) + "\" ";
  300. command += "-M \"set Exif.GPSInfo.GPSLatitudeRef " + NS + "\" ";
  301. const std::string WE = (lon < 0 ? "W" : "E");
  302. command += "-M \"set Exif.GPSInfo.GPSLongitude " + deg2string(std::abs(lon)) + "\" ";
  303. command += "-M \"set Exif.GPSInfo.GPSLongitudeRef " + WE + "\" ";
  304. command += photo;
  305. // std::cout << "Update: " << command << std::endl;
  306. return (system(command.c_str()) == 0);
  307. }
  308. std::string deg2string(double position)
  309. {
  310. double degrees = 0.0;
  311. double minutes = 0.0;
  312. double seconds = 0.0;
  313. position = modf(position, &degrees);
  314. position *= 60.0;
  315. position = modf(position, &minutes);
  316. position *= (60.0 * 100.0);
  317. modf(position, &seconds);
  318. return std::to_string(int(degrees)) + "/1 " + std::to_string(int(minutes)) + "/1 " + std::to_string(int(seconds)) + "/100";
  319. }
  320. public:
  321. // -- Callbacks -------------------------------------------------------------
  322. virtual void startElement(const std::string &path, const std::string &name, const Attributes &attributes)
  323. {
  324. if (path == "/gpx/trk/trkseg")
  325. {
  326. _segments.push_back(Segment());
  327. }
  328. else if (path == "/gpx/trk/trkseg/trkpt")
  329. {
  330. Point point;
  331. if (getDoubleAttribute(attributes, "lat", point._lat) &&
  332. getDoubleAttribute(attributes, "lon", point._lon))
  333. {
  334. _segments.back().push_back(point);
  335. }
  336. }
  337. else if (path == "/gpx/trk/trkseg/trkpt/time")
  338. {
  339. // <time>2012-12-03T13:13:38Z</time>
  340. _timeStr.clear();
  341. }
  342. }
  343. virtual void text(const std::string &path, const std::string &text)
  344. {
  345. if (path == "/gpx/trk/trkseg/trkpt/time")
  346. {
  347. _timeStr.append(text);
  348. }
  349. }
  350. virtual void endElement(const std::string &path, const std::string &)
  351. {
  352. if (path == "/gpx/trk/trkseg/trkpt/time")
  353. {
  354. processTimeStr(_timeStr, _segments.back().back()._time);
  355. }
  356. }
  357. private:
  358. // -- Members ---------------------------------------------------------------
  359. arg::Arguments _arguments;
  360. arg::Argument _seconds;
  361. arg::Argument _minutes;
  362. arg::Argument _hours;
  363. XMLParser _xmlParser;
  364. int _offset;
  365. std::vector<std::string> _jpegnames;
  366. std::vector<std::string> _gpxnames;
  367. Segments _segments;
  368. std::string _timeStr;
  369. };
  370. }
  371. // -- Main program ------------------------------------------------------------
  372. int main(int argc, char *argv[])
  373. {
  374. gpxtools::GPXGeoTag gpxGeoTag;
  375. if (gpxGeoTag.processArguments(argc, argv))
  376. {
  377. gpxGeoTag.perform();
  378. return 0;
  379. }
  380. return 1;
  381. }