- Timestamp:
- 2021-02-26T19:39:51+13:00 (3 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
main/trunk/greenstone2/perllib/cpan/Image/ExifTool/Geotag.pm
r24107 r34921 6 6 # Revisions: 2009/04/01 - P. Harvey Created 7 7 # 2009/09/27 - PH Added Geosync feature 8 # 2009/06/25 - PH Read Garmin TCX track logs 9 # 2009/09/11 - PH Read ITC GPS track logs 10 # 2012/01/08 - PH Extract orientation information from PTNTHPR 11 # 2012/05/08 - PH Read Winplus Beacon .TXT files 12 # 2015/05/30 - PH Read Bramor gEO log files 13 # 2016/07/13 - PH Added ability to geotag date/time only 14 # 2019/07/02 - PH Added ability to read IMU CSV files 15 # 2019/11/10 - PH Also write pitch to CameraElevationAngle 16 # 2020/12/01 - PH Added ability to read DJI CSV log files 8 17 # 9 18 # References: 1) http://www.topografix.com/GPX/1/1/ … … 18 27 use vars qw($VERSION); 19 28 use Image::ExifTool qw(:Public); 20 21 $VERSION = '1.24'; 22 29 use Image::ExifTool::GPS; 30 31 $VERSION = '1.64'; 32 33 sub JITTER() { return 2 } # maximum time jitter 34 35 sub GetTime($); 23 36 sub SetGeoValues($$;$); 37 sub PrintFixTime($); 38 sub PrintFix($@); 24 39 25 40 # XML tags that we recognize (keys are forced to lower case) … … 42 57 pdop => 'pdop', # GPX 43 58 sat => 'nsats', # GPX 59 atemp => 'atemp', # GPX (Garmin 550t) 44 60 when => 'time', # KML 45 61 coordinates => 'coords', # KML 62 coord => 'coords', # KML, as written by Google Location History 63 begin => 'begin', # KML TimeSpan 64 end => 'time', # KML TimeSpan 65 course => 'dir', # (written by Arduino) 66 pitch => 'pitch', # (written by Arduino) 67 roll => 'roll', # (written by Arduino) 46 68 # XML containers (fix is reset at the opening tag of these properties) 47 69 wpt => '', # GPX … … 50 72 trackpoint => '', # Garmin 51 73 placemark => '', # KML 74 ); 75 76 # fix information keys which must be interpolated around a circle 77 my %cyclical = (lon => 1, track => 1, dir => 1, roll => 1); 78 79 # fix information keys for each of our general categories 80 my %fixInfoKeys = ( 81 'pos' => [ 'lat', 'lon' ], 82 track => [ 'track', 'speed' ], 83 alt => [ 'alt' ], 84 orient => [ 'dir', 'pitch', 'roll' ], 85 atemp => [ 'atemp' ], 86 ); 87 88 my %isOrient = ( dir => 1, pitch => 1, roll => 1 ); # test for orientation key 89 90 # conversion factors for GPSSpeed 91 my %speedConv = ( 92 'K' => 1.852, # km/h per knot 93 'M' => 1.150779448, # mph per knot 94 'k' => 'K', # (allow lower case) 95 'm' => 'M', 96 'km/h' => 'K', # (allow other formats) 97 'mph' => 'M', 52 98 ); 53 99 … … 63 109 # NoDate - flag if some points have no date (ie. referenced to 1970:01:01) 64 110 # IsDate - flag if some points have date 111 # Has - hash of flags for available information (track, orient, alt) 65 112 # - the fix information hash may contain: 66 113 # lat - signed latitude (required) … … 74 121 # sats - comma-separated list of active satellites 75 122 # nsats - number of active satellites 123 # track - track heading (deg true) 124 # dir - image direction (deg true) 125 # pitch - pitch angle (deg) 126 # roll - roll angle (deg) 127 # speed - speed (knots) 76 128 # first - flag set for first fix of track 77 129 # - concatenates new data with existing track data stored in ExifTool NEW_VALUE … … 80 132 { 81 133 local ($_, $/, *EXIFTOOL_TRKFILE); 82 my ($e xifTool, $val) = @_;134 my ($et, $val) = @_; 83 135 my ($raf, $from, $time, $isDate, $noDate, $noDateChanged, $lastDate, $dateFlarm); 84 my ($nmeaStart, $fixSecs, @fixTimes, $canCut, $cutPDOP, $cutHDOP, $cutSats, $lastFix); 85 86 unless (eval 'require Time::Local') { 136 my ($nmeaStart, $fixSecs, @fixTimes, $lastFix, %nmea, @csvHeadings); 137 my ($canCut, $cutPDOP, $cutHDOP, $cutSats, $e0, $e1, @tmp, $trackFile, $trackTime); 138 139 unless (eval { require Time::Local }) { 87 140 return 'Geotag feature requires Time::Local installed'; 88 141 } 89 142 # add data to existing track 90 my $geotag = $exifTool->GetNewValues('Geotag') || { }; 143 my $geotag = $et->GetNewValue('Geotag') || { }; 144 145 # initialize track points lookup 146 my $points = $$geotag{Points}; 147 $points or $points = $$geotag{Points} = { }; 148 149 # get lookup for available information types 150 my $has = $$geotag{Has}; 151 $has or $has = $$geotag{Has} = { 'pos' => 1 }; 152 91 153 my $format = ''; 92 154 # is $val track log data? 93 if ($val =~ /^(\xef\xbb\xbf)?<(\?xml|gpx) \s/) {155 if ($val =~ /^(\xef\xbb\xbf)?<(\?xml|gpx)[\s>]/) { 94 156 $format = 'XML'; 95 157 $/ = '>'; # set input record separator to '>' for XML/GPX data … … 98 160 } else { 99 161 # $val is track file name 100 open EXIFTOOL_TRKFILE, $val or return "Error opening GPS file '$val'"; 101 $raf = new File::RandomAccess(\*EXIFTOOL_TRKFILE); 102 unless ($raf->Read($_, 256)) { 103 close EXIFTOOL_TRKFILE; 104 return "Empty track file '$val'"; 105 } 106 # look for XML or GPX header (might as well allow UTF-8 BOM) 107 if (/^(\xef\xbb\xbf)?<(\?xml|gpx)\s/) { 108 $format = 'XML'; 109 $/ = '>'; # set input record separator to '>' for XML/GPX data 110 } elsif (/(\x0d\x0a|\x0d|\x0a)/) { 111 $/ = $1; 162 if ($et->Open(\*EXIFTOOL_TRKFILE, $val)) { 163 $trackFile = $val; 164 $raf = new File::RandomAccess(\*EXIFTOOL_TRKFILE); 165 unless ($raf->Read($_, 256)) { 166 close EXIFTOOL_TRKFILE; 167 return "Empty track file '${val}'"; 168 } 169 # look for XML or GPX header (might as well allow UTF-8 BOM) 170 if (/^(\xef\xbb\xbf)?<(\?xml|gpx)[\s>]/) { 171 $format = 'XML'; 172 $/ = '>'; # set input record separator to '>' for XML/GPX data 173 } elsif (/(\x0d\x0a|\x0d|\x0a)/) { 174 $/ = $1; 175 } else { 176 close EXIFTOOL_TRKFILE; 177 return "Invalid track file '${val}'"; 178 } 179 $raf->Seek(0,0); 180 $from = "file '${val}'"; 181 } elsif ($val eq 'DATETIMEONLY') { 182 $$geotag{DateTimeOnly} = 1; 183 $$geotag{IsDate} = 1; 184 $et->VPrint(0, 'Geotagging date/time only'); 185 return $geotag; 112 186 } else { 113 close EXIFTOOL_TRKFILE; 114 return "Invalid track file '$val'"; 115 } 116 $raf->Seek(0,0); 117 $from = "file '$val'"; 187 return "Error opening GPS file '${val}'"; 188 } 118 189 } 119 190 unless ($from) { … … 122 193 $from = 'data'; 123 194 } 124 # initialize track points lookup125 my $points = $$geotag{Points};126 $points or $points = $$geotag{Points} = { };127 195 128 196 # initialize cuts 129 my $maxHDOP = $e xifTool->Options('GeoMaxHDOP');130 my $maxPDOP = $e xifTool->Options('GeoMaxPDOP');131 my $minSats = $e xifTool->Options('GeoMinSats');197 my $maxHDOP = $et->Options('GeoMaxHDOP'); 198 my $maxPDOP = $et->Options('GeoMaxPDOP'); 199 my $minSats = $et->Options('GeoMinSats'); 132 200 my $isCut = $maxHDOP || $maxPDOP || $minSats; 133 201 … … 136 204 my $lastSecs = 0; 137 205 my $fix = { }; 206 my $csvDelim = $et->Options('CSVDelim'); 207 $csvDelim = ',' unless defined $csvDelim; 208 my (@saveFix, $timeSpan); 138 209 for (;;) { 139 210 $raf->ReadLine($_) or last; 140 211 # determine file format 141 212 if (not $format) { 142 if (/^<(\?xml|gpx) \s/) { # look for XML or GPX header213 if (/^<(\?xml|gpx)[\s>]/) { # look for XML or GPX header 143 214 $format = 'XML'; 144 } elsif (/^\$(PMGNTRK|GP(RMC|GGA|GLL|GSA)),/) { 215 # check for NMEA sentence 216 # (must ONLY start with ones that have timestamps! eg. not GSA or PTNTHPR!) 217 } elsif (/^.*\$([A-Z]{2}(RMC|GGA|GLL|ZDA)|PMGNTRK),/) { 145 218 $format = 'NMEA'; 146 219 $nmeaStart = $2 || $1; # save type of first sentence … … 149 222 $nmeaStart = 'B' ; 150 223 next; 151 } elsif (/^HFDTE( \d{2})(\d{2})(\d{2})/) {224 } elsif (/^HFDTE(?:DATE:)?(\d{2})(\d{2})(\d{2})/) { 152 225 my $year = $3 + ($3 >= 70 ? 1900 : 2000); 153 $dateFlarm = Time::Local::timegm(0,0,0,$1,$2-1,$year -1900);226 $dateFlarm = Time::Local::timegm(0,0,0,$1,$2-1,$year); 154 227 $nmeaStart = 'B' ; 155 228 $format = 'IGC'; … … 157 230 } elsif ($nmeaStart and /^B/) { 158 231 # parse IGC fixes without a date 159 $format = 'IGC'; 232 $format = 'IGC'; 233 } elsif (/^TP,D,/) { 234 $format = 'Winplus'; 235 } elsif (/^\s*\d+\s+.*\sypr\s*$/ and (@tmp=split) == 12) { 236 $format = 'Bramor'; 237 } elsif (((/\b(GPS)?Date/i and /\b(GPS)?(Date)?Time/i) or /\bTime\(seconds\)/i) and /\Q$csvDelim/) { 238 chomp; 239 @csvHeadings = split /\Q$csvDelim/; 240 $format = 'CSV'; 241 # convert recognized headings to our parameter names 242 foreach (@csvHeadings) { 243 my $param; 244 s/^GPS ?//; # remove leading "GPS" to simplify regex patterns 245 if (/^Time ?\(seconds\)$/i) { # DJI 246 # DJI CSV log files have a column "Time(seconds)" which is seconds since 247 # the start of the flight. The date/time is obtained from the file name. 248 $param = 'runtime'; 249 if ($trackFile and $trackFile =~ /(\d{4})-(\d{2})-(\d{2})[^\/]+(\d{2})-(\d{2})-(\d{2})[^\/]*$/) { 250 $trackTime = Image::ExifTool::TimeLocal($6,$5,$4,$3,$2-1,$1); 251 my $utc = PrintFixTime($trackTime); 252 my $tzs = Image::ExifTool::TimeZoneString([$6,$5,$4,$3,$2-1,$1-1900],$trackTime); 253 $et->VPrint(2, " DJI start time: $utc (local timezone is $tzs)\n"); 254 } else { 255 return 'Error getting start time from file name for DJI CSV track file'; 256 } 257 } elsif (/^Date ?Time/i) { # ExifTool addition 258 $param = 'datetime'; 259 } elsif (/^Date/i) { 260 $param = 'date'; 261 } elsif (/^Time(?! ?\(text\))/i) { # (ignore DJI "Time(text)" column) 262 $param = 'time'; 263 } elsif (/^(Pos)?Lat/i) { 264 $param = 'lat'; 265 } elsif (/^(Pos)?Lon/i) { 266 $param = 'lon'; 267 } elsif (/^(Pos)?Alt/i) { 268 $param = 'alt'; 269 } elsif (/^(Angle)?(Heading|Track)/i) { 270 $param = 'track'; 271 } elsif (/^(Angle)?Pitch/i or /^Camera ?Elevation ?Angle/i) { 272 $param = 'pitch'; 273 } elsif (/^(Angle)?Roll/i) { 274 $param = 'roll'; 275 } 276 if ($param) { 277 $et->VPrint(2, "CSV column '${_}' is $param\n"); 278 $_ = $param; 279 } else { 280 $et->VPrint(2, "CSV column '${_}' ignored\n"); 281 $_ = ''; # ignore this column 282 } 283 } 284 next; 160 285 } else { 161 286 # search only first 50 lines of file for a valid fix … … 170 295 my ($arg, $tok, $td); 171 296 s/\s*=\s*(['"])\s*/=$1/g; # remove unnecessary white space in attributes 297 # Workaround for KML generated by Google Location History: 298 # lat/lon/alt are space-separated; we want commas. 299 s{(\S+)\s+(\S+)\s+(\S+)(</gx:coord>)}{$1,$2,$3$4}; 172 300 foreach $arg (split) { 173 # parse attributes ( ie. GPX 'lat' and 'lon')301 # parse attributes (eg. GPX 'lat' and 'lon') 174 302 # (note: ignore namespace prefixes if they exist) 175 303 if ($arg =~ /^(\w+:)?(\w+)=(['"])(.*?)\3/g) { 176 304 my $tag = $xmlTag{lc $2}; 177 $$fix{$tag} = $4 if $tag; 305 if ($tag) { 306 $$fix{$tag} = $4; 307 if ($isOrient{$tag}) { 308 $$has{orient} = 1; 309 } elsif ($tag eq 'alt') { 310 # validate altitude 311 undef $$fix{alt} if defined $$fix{alt} and $$fix{alt} !~ /^[+-]?\d+\.?\d*/; 312 $$has{alt} = 1 if $$fix{alt}; # set "has altitude" flag if appropriate 313 } elsif ($tag eq 'atemp') { 314 $$has{atemp} = 1; 315 } 316 } 178 317 } 179 318 # loop through XML elements … … 186 325 # opened: start a new fix 187 326 $lastFix = $fix = { }; 327 undef @saveFix; 188 328 next; 189 329 } elsif ($fix and $lastFix and %$fix) { … … 198 338 if ($tag) { 199 339 if ($tag eq 'coords') { 340 # save other fixes if there are more than one 341 if (defined $$fix{lon} and defined $$fix{lat} and defined $$fix{alt}) { 342 push @saveFix, [ @$fix{'lon','lat','alt'} ]; 343 } 200 344 # read KML "Point" coordinates 201 345 @$fix{'lon','lat','alt'} = split ',', $1; 202 346 } else { 203 347 $$fix{$tag} = $1; 348 if ($isOrient{$tag}) { 349 $$has{orient} = 1; 350 } elsif ($tag eq 'alt') { 351 # validate altitude 352 undef $$fix{alt} if defined $$fix{alt} and $$fix{alt} !~ /^[+-]?\d+\.?\d*/; 353 $$has{alt} = 1 if $$fix{alt}; # set "has altitude" flag if appropriate 354 } elsif ($tag eq 'atemp') { 355 $$has{atemp} = 1; 356 } 204 357 } 205 358 } … … 209 362 } 210 363 # validate and store GPS fix 211 if (defined $$fix{lat} and defined $$fix{lon} and $$fix{'time'} and 212 $$fix{lat} =~ /^[+-]?\d+\.?\d*/ and 213 $$fix{lon} =~ /^[+-]?\d+\.?\d*/ and 214 $$fix{'time'} =~ /^(\d{4})-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(.*)/) 215 { 216 $time = Time::Local::timegm($6,$5,$4,$3,$2-1,$1-1900); 217 $time += $7 if $7; # add fractional seconds 218 my $tz = $8; 219 # adjust for time zone (otherwise assume UTC) 220 # - allow timezone of +-HH:MM, +-H:MM, +-HHMM or +-HH since 221 # the spec is unclear about timezone format 222 if ($tz =~ /^([-+])(\d+):(\d{2})\b/ or $tz =~ /^([-+])(\d{2})(\d{2})?\b/) { 223 $tz = ($2 * 60 + ($3 || 0)) * 60; 224 $tz *= -1 if $1 eq '+'; # opposite sign to change back to UTC 225 $time += $tz; 364 next unless defined $$fix{lat} and defined $$fix{lon} and $$fix{'time'}; 365 unless ($$fix{lat} =~ /^[+-]?\d+\.?\d*/ and $$fix{lon} =~ /^[+-]?\d+\.?\d*/) { 366 $e0 or $et->VPrint(0, "Coordinate format error in $from\n"), $e0 = 1; 367 next; 368 } 369 unless (defined($time = GetTime($$fix{'time'}))) { 370 $e1 or $et->VPrint(0, "Timestamp format error in $from\n"), $e1 = 1; 371 next; 372 } 373 $isDate = 1; 374 $canCut= 1 if defined $$fix{pdop} or defined $$fix{hdop} or defined $$fix{nsats}; 375 # generate extra fixes assuming an equally spaced track 376 if ($$fix{begin}) { 377 my $begin = GetTime($$fix{begin}); 378 undef $$fix{begin}; 379 if (defined $begin and $begin < $time) { 380 $$fix{span} = $timeSpan = ($timeSpan || 0) + 1; 381 my $i; 382 # duplicate the fix if there is only one so we will have 383 # a fix and the start and end of the TimeSpan 384 @saveFix or push @saveFix, [ @$fix{'lon','lat','alt'} ]; 385 for ($i=0; $i<@saveFix; ++$i) { 386 my $t = $begin + ($time - $begin) * ($i / scalar(@saveFix)); 387 my %f; 388 @f{'lon','lat','alt'} = @{$saveFix[$i]}; 389 $t += 0.001 if not $i and $$points{$t}; # (avoid dupicates) 390 $f{span} = $timeSpan; 391 $$points{$t} = \%f; 392 push @fixTimes, $t; 393 } 226 394 } 227 # validate altitude228 undef $$fix{alt} if defined $$fix{alt} and $$fix{alt} !~ /^[+-]?\d+\.?\d*/;229 $isDate = 1;230 $canCut= 1 if defined $$fix{pdop} or defined $$fix{hdop} or defined $$fix{nsats};231 $$points{$time} = $fix;232 push @fixTimes, $time; # save times of all fixes in order233 $fix = { };234 ++$numPoints;235 395 } 396 $$points{$time} = $fix; 397 push @fixTimes, $time; # save times of all fixes in order 398 $fix = { }; 399 undef @saveFix; 400 ++$numPoints; 236 401 } 237 402 } … … 240 405 /[\s>](\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2}(\.\d+)?)/; 241 406 next; 407 # 408 # Winplus Beacon text file 409 # 410 } elsif ($format eq 'Winplus') { 411 # TP,D, 44.933666667, -93.186555556, 10/26/2011, 19:07:28, 0 412 # latitude longitude date time 413 /^TP,D,\s*([-+]?\d+\.\d*),\s*([-+]?\d+\.\d*),\s*(\d+)\/(\d+)\/(\d{4}),\s*(\d+):(\d+):(\d+)/ or next; 414 $$fix{lat} = $1; 415 $$fix{lon} = $2; 416 $time = Time::Local::timegm($8,$7,$6,$4,$3-1,$5); 417 DoneFix: $isDate = 1; 418 $$points{$time} = $fix; 419 push @fixTimes, $time; 420 $fix = { }; 421 ++$numPoints; 422 next; 423 # 424 # Bramor gEO log file 425 # 426 } elsif ($format eq 'Bramor') { 427 # 1 0015 18.723675 50.672752 149 169.31 22/04/2015 07:06:55 169.31 8.88 28.07 ypr 428 # ? index latitude longitude alt track date time dir pitch roll 429 my @parts = split ' ', $_; 430 next unless @parts == 12 and $parts[11] eq 'ypr'; 431 my @d = split m{/}, $parts[6]; # date (dd/mm/YYYY) 432 my @t = split m{:}, $parts[7]; # time (HH:MM:SS) 433 next unless @d == 3 and @t == 3; 434 @$fix{qw(lat lon alt track dir pitch roll)} = @parts[2,3,4,5,8,9,10]; 435 # (add the seconds afterwards in case some models have decimal seconds) 436 $time = Time::Local::timegm(0,$t[1],$t[0],$d[0],$d[1]-1,$d[2]) + $t[2]; 437 # set necessary flags for extra available information 438 @$has{qw(alt track orient)} = (1,1,1); 439 goto DoneFix; # save this fix 440 } elsif ($format eq 'CSV') { 441 chomp; 442 my @vals = split /\Q$csvDelim/; 443 # 444 # CSV format output of GPS/IMU POS system 445 # Date* - date in DD/MM/YYYY format 446 # Time* - time in HH:MM:SS.SSS format 447 # [Pos]Lat* - latitude in decimal degrees 448 # [Pos]Lon* - longitude in decimal degrees 449 # [Pos]Alt* - altitude in m relative to sea level 450 # [Angle]Heading* - GPSTrack in degrees true 451 # [Angle]Pitch* - pitch angle in degrees 452 # [Angle]Roll* - roll angle in degrees 453 # (ExifTool enhancements allow for standard tag names or descriptions as the column headings, 454 # add support for time zones and flexible coordinates, and allow new DateTime and Shift columns) 455 # 456 my ($param, $date, $secs); 457 foreach $param (@csvHeadings) { 458 my $val = shift @vals; 459 last unless defined $val; 460 next unless $param; 461 if ($param eq 'datetime') { 462 local $SIG{'__WARN__'} = sub { }; 463 my $dateTime = $et->InverseDateTime($val); 464 if ($dateTime) { 465 $date = Image::ExifTool::GetUnixTime($val, 2); 466 $secs = 0; 467 } 468 } elsif ($param eq 'date') { 469 if ($val =~ m{^(\d{2})/(\d{2})/(\d{4})$}) { 470 $date = Time::Local::timegm(0,0,0,$1,$2-1,$3); 471 } elsif ($val =~ /(\d{4}).*?(\d{2}).*?(\d{2})/) { 472 $date = Time::Local::timegm(0,0,0,$3,$2-1,$1); 473 } 474 } elsif ($param eq 'time') { 475 if ($val =~ /^(\d{1,2}):(\d{2}):(\d{2}(\.\d+)?).*?(([-+])(\d{1,2}):?(\d{2}))?/) { 476 $secs = (($1 * 60) + $2) * 60 + $3; 477 # adjust for time zone if specified 478 $secs += ($7 * 60 + $8) * ($6 eq '-' ? 60 : -60) if $5; 479 } 480 } elsif ($param eq 'lat' or $param eq 'lon') { 481 $$fix{$param} = Image::ExifTool::GPS::ToDegrees($val, 1); 482 } elsif ($param eq 'runtime') { 483 $date = $trackTime; 484 $secs = $val; 485 } else { 486 $$fix{$param} = $val; 487 } 488 } 489 if ($date and defined $secs and defined $$fix{lat} and defined $$fix{lon}) { 490 $time = $date + $secs; 491 $$has{alt} = 1 if defined $$fix{alt}; 492 $$has{track} = 1 if defined $$fix{track}; 493 $$has{orient} = 1 if defined $$fix{pitch}; 494 goto DoneFix; 495 } 496 next; 242 497 } 243 498 my (%fix, $secs, $date, $nmea); 244 499 if ($format eq 'NMEA') { 245 500 # ignore unrecognized NMEA sentences 246 next unless /^\$(PMGNTRK|GP(RMC|GGA|GLL|GSA)),/; 247 $nmea = $2 || $1; 501 # (first 2 characters: GP=GPS, GL=GLONASS, GA=Gallileo, GN=combined, BD=Beidou) 502 next unless /^(.*)\$([A-Z]{2}(RMC|GGA|GLL|GSA|ZDA)|PMGNTRK|PTNTHPR),/; 503 $nmea = $3 || $2; 504 $_ = substr($_, length($1)) if length($1); 248 505 } 249 506 # 250 507 # IGC (flarm) (ref 4) 251 508 # 252 if ( $format eq 'IGC') {509 if ($format eq 'IGC') { 253 510 # B0939564531208N00557021EA007670089100207 254 511 # BHHMMSSDDMMmmmNDDDMMmmmEAaaaaaAAAAAxxyy … … 262 519 # wrap to next day if necessary 263 520 if ($dateFlarm) { 264 $dateFlarm += $secPerDay if $secs < $lastSecs ;521 $dateFlarm += $secPerDay if $secs < $lastSecs - JITTER(); 265 522 $date = $dateFlarm; 266 523 } 267 524 $nmea = 'B'; 268 #269 # Magellan eXplorist NMEA-like PMGNTRK sentence (optionally contains date)270 #271 } elsif ($nmea eq 'PMGNTRK') {272 # $PMGNTRK,4415.026,N,07631.091,W,00092,M,185031.06,A,,020409*65273 # $PMGNTRK,ddmm.mmm,N/S,dddmm.mmm,E/W,alt,F/M,hhmmss.ss,A/V,trkname,DDMMYY*cs274 /^\$PMGNTRK,(\d+)(\d{2}\.\d+),([NS]),(\d+)(\d{2}\.\d+),([EW]),(-?\d+\.?\d*),([MF]),(\d{2})(\d{2})(\d+)(\.\d+)?,A,(?:[^,]*,(\d{2})(\d{2})(\d+))?/ or next;275 $fix{lat} = ($1 + $2/60) * ($3 eq 'N' ? 1 : -1);276 $fix{lon} = ($4 + $5/60) * ($6 eq 'E' ? 1 : -1);277 $fix{alt} = $8 eq 'M' ? $7 : $7 * 12 * 0.0254;278 $secs = (($9 * 60) + $10) * 60 + $11;279 $secs += $12 if $12; # add fractional seconds280 if (defined $15) {281 # optional date is available in PMGNTRK sentence282 my $year = $15 + ($15 >= 70 ? 1900 : 2000);283 $date = Time::Local::timegm(0,0,0,$13,$14-1,$year-1900);284 }285 525 # 286 526 # NMEA RMC sentence (contains date) … … 288 528 } elsif ($nmea eq 'RMC') { 289 529 # $GPRMC,092204.999,A,4250.5589,S,14718.5084,E,0.00,89.68,211200,,*25 530 # $GPRMC,093657.007,,3652.835020,N,01053.104094,E,1.642,,290913,,,A*0F 290 531 # $GPRMC,hhmmss.sss,A/V,ddmm.mmmm,N/S,ddmmm.mmmm,E/W,spd(knots),dir(deg),DDMMYY,,*cs 291 /^\$GPRMC,(\d{2})(\d{2})(\d+)(\.\d+)?,A,(\d+)(\d{2}\.\d+),([NS]),(\d+)(\d{2}\.\d+),([EW]),[^,]*,[^,]*,(\d{2})(\d{2})(\d+)/ or next; 292 $fix{lat} = ($5 + $6/60) * ($7 eq 'N' ? 1 : -1); 293 $fix{lon} = ($8 + $9/60) * ($10 eq 'E' ? 1 : -1); 294 my $year = $13 + ($13 >= 70 ? 1900 : 2000); 532 /^\$[A-Z]{2}RMC,(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d*?)(\d{1,2}\.\d+),([NS]),(\d*?)(\d{1,2}\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/ or next; 533 next if $13 > 31 or $14 > 12 or $15 > 99; # validate day/month/year 534 $fix{lat} = (($5 || 0) + $6/60) * ($7 eq 'N' ? 1 : -1); 535 $fix{lon} = (($8 || 0) + $9/60) * ($10 eq 'E' ? 1 : -1); 536 $fix{speed} = $11 if length $11; 537 $fix{track} = $12 if length $12; 538 my $year = $15 + ($15 >= 70 ? 1900 : 2000); 295 539 $secs = (($1 * 60) + $2) * 60 + $3; 296 $secs += $4 if $4; # add fractional seconds 297 $date = Time::Local::timegm(0,0,0,$11,$12-1,$year-1900); 540 $date = Time::Local::timegm(0,0,0,$13,$14-1,$year); 298 541 # 299 542 # NMEA GGA sentence (no date) … … 301 544 } elsif ($nmea eq 'GGA') { 302 545 # $GPGGA,092204.999,4250.5589,S,14718.5084,E,1,04,24.4,19.7,M,,,,0000*1F 546 # $GPGGA,093657.000,3652.835020,N,01053.104094,E,,8,,166.924,M,40.9,M,,*77 303 547 # $GPGGA,hhmmss.sss,ddmm.mmmm,N/S,dddmm.mmmm,E/W,0=invalid,sats,hdop,alt,M,... 304 /^\$GPGGA,(\d{2})(\d{2})(\d+)(\.\d+)?,(\d+)(\d{2}\.\d+),([NS]),(\d+)(\d{2}\.\d+),([EW]),[1-6],(\d+)?,(\.\d+|\d+\.?\d*)?,(-?\d+\.?\d*)?,M?,/ or next; 305 $fix{lat} = ($5 + $6/60) * ($7 eq 'N' ? 1 : -1); 306 $fix{lon} = ($8 + $9/60) * ($10 eq 'E' ? 1 : -1); 307 $fix{nsats} = $11; 308 $fix{hdop} = $12; 309 $fix{alt} = $13; 548 /^\$[A-Z]{2}GGA,(\d{2})(\d{2})(\d+(\.\d*)?),(\d*?)(\d{1,2}\.\d+),([NS]),(\d*?)(\d{1,2}\.\d+),([EW]),[1-6]?,(\d+)?,(\.\d+|\d+\.?\d*)?,(-?\d+\.?\d*)?,M?/ or next; 549 $fix{lat} = (($5 || 0) + $6/60) * ($7 eq 'N' ? 1 : -1); 550 $fix{lon} = (($8 || 0) + $9/60) * ($10 eq 'E' ? 1 : -1); 551 @fix{qw(nsats hdop alt)} = ($11,$12,$13); 310 552 $secs = (($1 * 60) + $2) * 60 + $3; 311 $secs += $4 if $4; # add fractional seconds312 553 $canCut = 1; 313 554 # … … 317 558 # $GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D 318 559 # $GPGLL,ddmm.mmmm,N/S,dddmm.mmmm,E/W,hhmmss.sss,A/V*cs 319 /^\$ GPGLL,(\d+)(\d{2}\.\d+),([NS]),(\d+)(\d{2}\.\d+),([EW]),(\d{2})(\d{2})(\d+)(\.\d+),A/ or next;320 $fix{lat} = ( $1+ $2/60) * ($3 eq 'N' ? 1 : -1);321 $fix{lon} = ( $4+ $5/60) * ($6 eq 'E' ? 1 : -1);560 /^\$[A-Z]{2}GLL,(\d*?)(\d{1,2}\.\d+),([NS]),(\d*?)(\d{1,2}\.\d+),([EW]),(\d{2})(\d{2})(\d+(\.\d*)?),A/ or next; 561 $fix{lat} = (($1 || 0) + $2/60) * ($3 eq 'N' ? 1 : -1); 562 $fix{lon} = (($4 || 0) + $5/60) * ($6 eq 'E' ? 1 : -1); 322 563 $secs = (($7 * 60) + $8) * 60 + $9; 323 $secs += $10 if $10; # add fractional seconds324 564 # 325 565 # NMEA GSA sentence (satellite status, no date) … … 327 567 } elsif ($nmea eq 'GSA') { 328 568 # $GPGSA,A,3,04,05,,,,,,,,,,,pdop,hdop,vdop*HH 329 /^\$ GPGSA,[AM],([23]),((?:\d*,){11}(?:\d*)),(\d+\.?\d*|\.\d+)?,(\d+\.?\d*|\.\d+)?,(\d+\.?\d*|\.\d+)?\*/ or next;569 /^\$[A-Z]{2}GSA,[AM],([23]),((?:\d*,){11}(?:\d*)),(\d+\.?\d*|\.\d+)?,(\d+\.?\d*|\.\d+)?,(\d+\.?\d*|\.\d+)?\*/ or next; 330 570 @fix{qw(fixtype sats pdop hdop vdop)} = ($1.'d',$2,$3,$4,$5); 331 571 # count the number of acquired satellites … … 333 573 $fix{nsats} = scalar @a; 334 574 $canCut = 1; 575 # 576 # NMEA ZDA sentence (date/time, contains date) 577 # 578 } elsif ($nmea eq 'ZDA') { 579 # $GPZDA,093655.000,29,09,2013,,*58 580 # $GPZDA,hhmmss.ss,DD,MM,YYYY,tzh,tzm (hhmmss in UTC) 581 /^\$[A-Z]{2}ZDA,(\d{2})(\d{2})(\d{2}(\.\d*)?),(\d+),(\d+),(\d+)/ or next; 582 $secs = (($1 * 60) + $2) * 60 + $3; 583 $date = Time::Local::timegm(0,0,0,$5,$6-1,$7); 584 # 585 # Magellan eXplorist PMGNTRK (Proprietary MaGellaN TRacK) sentence (optional date) 586 # 587 } elsif ($nmea eq 'PMGNTRK') { 588 # $PMGNTRK,4415.026,N,07631.091,W,00092,M,185031.06,A,,020409*65 589 # $PMGNTRK,ddmm.mmm,N/S,dddmm.mmm,E/W,alt,F/M,hhmmss.ss,A/V,trkname,DDMMYY*cs 590 /^\$PMGNTRK,(\d+)(\d{2}\.\d+),([NS]),(\d+)(\d{2}\.\d+),([EW]),(-?\d+\.?\d*),([MF]),(\d{2})(\d{2})(\d+(\.\d*)?),A,(?:[^,]*,(\d{2})(\d{2})(\d+))?/ or next; 591 $fix{lat} = ($1 + $2/60) * ($3 eq 'N' ? 1 : -1); 592 $fix{lon} = ($4 + $5/60) * ($6 eq 'E' ? 1 : -1); 593 $fix{alt} = $8 eq 'M' ? $7 : $7 * 12 * 0.0254; 594 $secs = (($9 * 60) + $10) * 60 + $11; 595 if (defined $15) { 596 next if $13 > 31 or $14 > 12 or $15 > 99; # validate day/month/year 597 # optional date is available in PMGNTRK sentence 598 my $year = $15 + ($15 >= 70 ? 1900 : 2000); 599 $date = Time::Local::timegm(0,0,0,$13,$14-1,$year); 600 } 601 # 602 # Honeywell HMR3000 PTNTHPR (Heading Pitch Roll) sentence (no date) 603 # (ref http://www.gpsarea.com/uploadfile/download/introduce/hmr3000_manual.pdf) 604 # 605 } elsif ($nmea eq 'PTNTHPR') { 606 # $PTNTHPR,85.9,N,-0.9,N,0.8,N*HH 607 # $PTNTHPR,heading,heading status,pitch,pitch status,roll,roll status,*cs 608 # status: L=low alarm, M=low warning, N=normal, O=high warning 609 # P=high alarm, C=tuning analog circuit 610 # (ignore this information on any alarm status) 611 /^\$PTNTHPR,(-?[\d.]+),[MNO],(-?[\d.]+),[MNO],(-?[\d.]+),[MNO]/ or next; 612 @fix{qw(dir pitch roll)} = ($1,$2,$3); 335 613 336 614 } else { 337 615 next; # this shouldn't happen 338 616 } 617 # remember the NMEA formats we successfully read 618 $nmea{$nmea} = 1; 339 619 # use last date if necessary (and appropriate) 340 620 if (defined $secs and not defined $date and defined $lastDate) { 341 621 # wrap to next day if necessary 342 if ($secs < $lastSecs ) {622 if ($secs < $lastSecs - JITTER()) { 343 623 $lastSecs -= $secPerDay; 344 624 $lastDate += $secPerDay; … … 360 640 } 361 641 # 362 # Add NMEA fix to our lookup642 # Add NMEA/IGC fix to our lookup 363 643 # (this is much more complicated than it needs to be because 364 644 # the stupid NMEA format provides no end-of-fix indication) … … 426 706 if ($noDate and not $$geotag{NoDate}) { 427 707 if ($isDate) { 428 $e xifTool->Warn('Fixes are date-less -- will use time-only interpolation');708 $et->Warn('Fixes are date-less -- will use time-only interpolation'); 429 709 } else { 430 $e xifTool->Warn('Some fixes are date-less -- may use time-only interpolation');710 $et->Warn('Some fixes are date-less -- may use time-only interpolation'); 431 711 } 432 712 $$geotag{NoDate} = 1; … … 466 746 last; 467 747 } 468 my $verbose = $e xifTool->Options('Verbose');748 my $verbose = $et->Options('Verbose'); 469 749 if ($verbose) { 470 my $out = $exifTool->Options('TextOut'); 471 print $out "Loaded $numPoints points from GPS track log $from\n"; 750 my $out = $et->Options('TextOut'); 751 $format or $format = 'unknown'; 752 print $out "Loaded $numPoints points from $format-format GPS track log $from\n"; 472 753 print $out "Ignored $cutPDOP points due to GeoMaxPDOP cut\n" if $cutPDOP; 473 754 print $out "Ignored $cutHDOP points due to GeoMaxHDOP cut\n" if $cutHDOP; 474 755 print $out "Ignored $cutSats points due to GeoMinSats cut\n" if $cutSats; 475 756 if ($numPoints and $verbose > 1) { 476 print $out ' GPS track start: ' . Image::ExifTool::ConvertUnixTime($fixTimes[0]) . " UTC\n"; 757 my @lbl = ('start:', 'end: '); 758 # (fixes may be in reverse order in GPX files) 759 @lbl = reverse @lbl if $fixTimes[0] > $fixTimes[-1]; 760 print $out " GPS track $lbl[0] " . PrintFixTime($fixTimes[0]) . "\n"; 477 761 if ($verbose > 3) { 478 foreach $time (@fixTimes) { 479 $fix = $$points{$time} or next; 480 print $out ' ',Image::ExifTool::ConvertUnixTime($time),' UTC -'; 481 foreach (sort keys %$fix) { 482 print $out " $_=$$fix{$_}" unless $_ eq 'time'; 483 } 484 print $out "\n"; 485 } 486 } 487 print $out ' GPS track end: ' . Image::ExifTool::ConvertUnixTime($fixTimes[-1]) . " UTC\n"; 762 print $out PrintFix($points, $_) foreach @fixTimes; 763 } 764 print $out " GPS track $lbl[1] " . PrintFixTime($fixTimes[-1]) . "\n"; 488 765 } 489 766 } … … 491 768 # reset timestamp list to force it to be regenerated 492 769 delete $$geotag{Times}; 770 # set flags for available information 771 $$has{alt} = 1 if $nmea{GGA} or $nmea{PMGNTRK} or $nmea{B}; # alt 772 $$has{track} = 1 if $nmea{RMC}; # track, speed 773 $$has{orient} = 1 if $nmea{PTNTHPR}; # dir, pitch, roll 493 774 return $geotag; # success! 494 775 } 495 776 return "No track points found in GPS $from"; 777 } 778 779 780 #------------------------------------------------------------------------------ 781 # Get floating point UTC time 782 # Inputs: 0) XML time string 783 # Returns: floating point time or undef on error 784 sub GetTime($) 785 { 786 my $timeStr = shift; 787 $timeStr =~ /^(\d{4})-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(.*)/ or return undef; 788 my $time = Time::Local::timegm($6,$5,$4,$3,$2-1,$1); 789 $time += $7 if $7; # add fractional seconds 790 my $tz = $8; 791 # adjust for time zone (otherwise assume UTC) 792 # - allow timezone of +-HH:MM, +-H:MM, +-HHMM or +-HH since 793 # the spec is unclear about timezone format 794 if ($tz =~ /^([-+])(\d+):(\d{2})\b/ or $tz =~ /^([-+])(\d{2})(\d{2})?\b/) { 795 $tz = ($2 * 60 + ($3 || 0)) * 60; 796 $tz *= -1 if $1 eq '+'; # opposite sign to change back to UTC 797 $time += $tz; 798 } 799 return $time; 496 800 } 497 801 … … 502 806 sub ApplySyncCorr($$) 503 807 { 504 my ($e xifTool, $time) = @_;505 my $sync = $e xifTool->GetNewValues('Geosync');808 my ($et, $time) = @_; 809 my $sync = $et->GetNewValue('Geosync'); 506 810 if (ref $sync eq 'HASH') { 507 811 my $syncTimes = $$sync{Times}; … … 511 815 while ($i1 > $i0 + 1) { 512 816 my $pt = int(($i0 + $i1) / 2); 513 if ($time < $$syncTimes[$pt]) { 514 $i1 = $pt; 515 } else { 516 $i0 = $pt; 517 } 817 ($time < $$syncTimes[$pt] ? $i1 : $i0) = $pt; 518 818 } 519 819 my ($t0, $t1) = ($$syncTimes[$i0], $$syncTimes[$i1]); … … 530 830 } 531 831 return $sync; 832 } 833 834 #------------------------------------------------------------------------------ 835 # Scan outwards for a fix containing the requested parameter 836 # Inputs: 0) name of fix parameter, 1) reference to list of fix times, 837 # 2) reference to fix points hash, 3) index of starting time, 838 # 4) direction to scan (-1 or +1), 5) maximum time difference 839 # Returns: 0) time for fix containing requested information (or undef) 840 # 1) the corresponding fix, 2) the value of the requested fix parameter 841 sub ScanOutwards($$$$$$) 842 { 843 my ($key, $times, $points, $i, $dir, $maxSecs) = @_; 844 my $t0 = $$times[$i]; 845 for (;;) { 846 $i += $dir; 847 last if $i < 0 or $i >= scalar @$times; 848 my $t = $$times[$i]; 849 last if abs($t - $t0) > $maxSecs; # don't look too far 850 my $p = $$points{$t}; 851 my $v = $$p{$key}; 852 return($t,$p,$v) if defined $v; 853 } 854 return(); 855 } 856 857 #------------------------------------------------------------------------------ 858 # Find nearest fix containing the specified parameter 859 # Inputs: 0) ExifTool ref, 1) name of fix parameter, 2) reference to list of fix times, 860 # 3) reference to fix points hash, 4) index of starting time, 861 # 5) direction to scan (-1, +1 or undef), 6) maximum time difference 862 # Returns: reference to fix hash or undef 863 sub FindFix($$$$$$$) 864 { 865 my ($et, $key, $times, $points, $i, $dir, $maxSecs) = @_; 866 my ($t,$p); 867 if ($dir) { 868 ($t,$p) = ScanOutwards($key, $times, $points, $i, $dir, $maxSecs); 869 } else { 870 my ($t1, $p1) = ScanOutwards($key, $times, $points, $i, -1, $maxSecs); 871 my ($t2, $p2) = ScanOutwards($key, $times, $points, $i, 1, $maxSecs); 872 if (defined $t1) { 873 if (defined $t2) { 874 # both surrounding points are valid, so take the closest one 875 ($t, $p) = ($t - $t1 < $t2 - $t) ? ($t1, $p1) : ($t2, $p2); 876 } else { 877 ($t, $p) = ($t1, $p1); 878 } 879 } elsif (defined $t2) { 880 ($t, $p) = ($t2, $p2); 881 } 882 } 883 if (defined $p and $$et{OPTIONS}{Verbose} > 2) { 884 $et->VPrint(2, " Taking $key from fix:\n", PrintFix($points, $t)) 885 } 886 return $p; 532 887 } 533 888 … … 541 896 { 542 897 local $_; 543 my ($exifTool, $val, $writeGroup) = @_; 544 my $geotag = $exifTool->GetNewValues('Geotag'); 545 my ($fix, $time, $fsec, $noDate, $secondTry); 898 my ($et, $val, $writeGroup) = @_; 899 my $geotag = $et->GetNewValue('Geotag'); 900 my $verbose = $et->Options('Verbose'); 901 my ($fix, $time, $fsec, $noDate, $secondTry, $iExt, $iDir); 546 902 547 903 # remove date if none of our fixes had date information … … 549 905 550 906 # maximum time (sec) from nearest GPS fix when position is still considered valid 551 my $geoMaxIntSecs = $exifTool->Options('GeoMaxIntSecs'); 552 my $geoMaxExtSecs = $exifTool->Options('GeoMaxExtSecs'); 907 my $geoMaxIntSecs = $et->Options('GeoMaxIntSecs'); 908 my $geoMaxExtSecs = $et->Options('GeoMaxExtSecs'); 909 553 910 # use 30 minutes for a default 554 911 defined $geoMaxIntSecs or $geoMaxIntSecs = 1800; 555 912 defined $geoMaxExtSecs or $geoMaxExtSecs = 1800; 556 913 914 my $times = $$geotag{Times}; 557 915 my $points = $$geotag{Points}; 916 my $has = $$geotag{Has}; 558 917 my $err = ''; 559 918 # loop to try date/time value first, then time-only value … … 563 922 last; 564 923 } 565 my $times = $$geotag{Times};566 924 unless ($times) { 567 925 # generate sorted timestamp list for binary search … … 569 927 $times = $$geotag{Times} = \@times; 570 928 } 571 unless ($times and @$times ) {929 unless ($times and @$times or $$geotag{DateTimeOnly}) { 572 930 $err = 'GPS track is empty'; 573 931 last; 574 932 } 575 unless (eval 'require Time::Local') {933 unless (eval { require Time::Local }) { 576 934 $err = 'Geotag feature requires Time::Local installed'; 577 935 last; … … 593 951 } 594 952 if ($tz) { 595 $time = Time::Local::timegm($sec,$min,$hr,$day,$mon-1,$year -1900);953 $time = Time::Local::timegm($sec,$min,$hr,$day,$mon-1,$year); 596 954 # use timezone from date/time value 597 955 if ($tz ne 'Z') { … … 601 959 } else { 602 960 # assume local timezone 603 $time = Image::ExifTool::TimeLocal($sec,$min,$hr,$day,$mon-1,$year -1900);961 $time = Image::ExifTool::TimeLocal($sec,$min,$hr,$day,$mon-1,$year); 604 962 } 605 963 # add fractional seconds … … 607 965 608 966 # bring UTC time back to Jan. 1 if no date is given 609 $time %= $secPerDay if $noDate; 967 # (don't use '%' operator here because it drops fractional seconds) 968 $time -= int($time / $secPerDay) * $secPerDay if $noDate; 610 969 611 970 # apply time synchronization if available 612 my $sync = ApplySyncCorr($e xifTool, $time);971 my $sync = ApplySyncCorr($et, $time); 613 972 614 973 # save fractional seconds string 615 974 $fsec = ($time =~ /(\.\d+)$/) ? $1 : ''; 616 975 617 if ($e xifTool->Options('Verbose') > 1 and not $secondTry) {618 my $out = $e xifTool->Options('TextOut');619 my $str = "$fsec UTC";976 if ($et->Options('Verbose') > 1 and not $secondTry) { 977 my $out = $et->Options('TextOut'); 978 my $str = ''; 620 979 $str .= sprintf(" (incl. Geosync offset of %+.3f sec)", $sync) if defined $sync; 621 print $out ' Geotime value: ' . Image::ExifTool::ConvertUnixTime(int $time) . "$str\n"; 622 } 980 unless ($tz) { 981 my $tzs = Image::ExifTool::TimeZoneString([$sec,$min,$hr,$day,$mon-1,$year-1900],$time); 982 $str .= " (local timezone is $tzs)"; 983 } 984 print $out ' Geotime value: ' . PrintFixTime($time) . "$str\n"; 985 } 986 if (not $times or not @$times) { 987 $fix = { }; # dummy fix to geotag date/time only 623 988 # interpolate GPS track at $time 624 if ($time < $$times[0]) {989 } elsif ($time < $$times[0]) { 625 990 if ($time < $$times[0] - $geoMaxExtSecs) { 626 991 $err or $err = 'Time is too far before track'; 992 $et->VPrint(2, ' Track start: ', PrintFixTime($$times[0]), "\n") if $verbose > 2; 993 $fix = { } if $$geotag{DateTimeOnly}; 627 994 } else { 628 995 $fix = $$points{$$times[0]}; 996 $iExt = 0; $iDir = 1; 997 $et->VPrint(2, " Taking pos from fix:\n", 998 PrintFix($points, $$times[0])) if $verbose > 2; 629 999 } 630 1000 } elsif ($time > $$times[-1]) { 631 1001 if ($time > $$times[-1] + $geoMaxExtSecs) { 632 1002 $err or $err = 'Time is too far beyond track'; 1003 $et->VPrint(2, ' Track end: ', PrintFixTime($$times[-1]), "\n") if $verbose > 2; 1004 $fix = { } if $$geotag{DateTimeOnly}; 633 1005 } else { 634 1006 $fix = $$points{$$times[-1]}; 1007 $iExt = $#$times; $iDir = -1; 1008 $et->VPrint(2, " Taking pos from fix:\n", 1009 PrintFix($points, $$times[-1])) if $verbose > 2; 635 1010 } 636 1011 } else { … … 639 1014 while ($i1 > $i0 + 1) { 640 1015 my $pt = int(($i0 + $i1) / 2); 641 if ($time < $$times[$pt]) { 642 $i1 = $pt; 643 } else { 644 $i0 = $pt; 645 } 1016 ($time < $$times[$pt] ? $i1 : $i0) = $pt; 646 1017 } 647 1018 # do linear interpolation for position … … 650 1021 my $p1 = $$points{$t1}; 651 1022 # check to see if we are extrapolating before the first entry in a track 652 my $maxSecs = $$p1{first}? $geoMaxExtSecs : $geoMaxIntSecs;1023 my $maxSecs = ($$p1{first} and $geoMaxIntSecs) ? $geoMaxExtSecs : $geoMaxIntSecs; 653 1024 # don't interpolate if fixes are too far apart 654 if ($t1 - $t0 > $maxSecs) { 1025 # (but always interpolate fixes inside the same TimeSpan) 1026 if ($t1 - $t0 > $maxSecs and (not $$p1{span} or not $$points{$t0}{span} or 1027 $$p1{span} != $$points{$t0}{span})) 1028 { 655 1029 # treat as an extrapolation -- use nearest fix if close enough 656 my $tn = ($time - $t0 < $t1 - $time) ? $t0 : $t1; 1030 my $tn; 1031 if ($time - $t0 < $t1 - $time) { 1032 $tn = $t0; 1033 $iExt = $i0; 1034 } else { 1035 $tn = $t1; 1036 $iExt = $i1; 1037 } 657 1038 if (abs($time - $tn) > $geoMaxExtSecs) { 658 1039 $err or $err = 'Time is too far from nearest GPS fix'; 1040 $et->VPrint(2, ' Nearest fix: ', PrintFixTime($tn), "\n") if $verbose > 2; 1041 $fix = { } if $$geotag{DateTimeOnly}; 659 1042 } else { 660 1043 $fix = $$points{$tn}; 1044 $et->VPrint(2, " Taking pos from fix:\n", 1045 PrintFix($points, $tn)) if $verbose > 2; 661 1046 } 662 1047 } else { 663 my $f = $t1 == $t0 ? 0 : ($time - $t0) / ($t1 - $t0);1048 my $f0 = $t1 == $t0 ? 0 : ($time - $t0) / ($t1 - $t0); 664 1049 my $p0 = $$points{$t0}; 1050 $et->VPrint(2, " Interpolating between fixes (f=$f0):\n", 1051 PrintFix($points, $t0, $t1)) if $verbose > 2; 665 1052 $fix = { }; 666 # loop through latitude, longitude, and altitude if available 667 foreach (qw(lat lon alt)) { 668 next unless defined $$p0{$_} and defined $$p1{$_}; 669 $$fix{$_} = $$p1{$_} * $f + $$p0{$_} * (1 - $f); 1053 # loop through available fix information categories 1054 # (pos, track, alt, orient) 1055 my ($category, $key); 1056 Category: foreach $category (qw{pos track alt orient atemp}) { 1057 next unless $$has{$category}; 1058 my ($f, $p0b, $p1b, $f0b); 1059 # loop through specific fix information keys 1060 # (lat, lon, alt, track, speed, dir, pitch, roll) 1061 foreach $key (@{$fixInfoKeys{$category}}) { 1062 my $v0 = $$p0{$key}; 1063 my $v1 = $$p1{$key}; 1064 if (defined $v0 and defined $v1) { 1065 $f = $f0; 1066 } elsif (defined $f0b) { 1067 $v0 = $$p0b{$key}; 1068 $v1 = $$p1b{$key}; 1069 next unless defined $v0 and defined $v1; 1070 $f = $f0b; 1071 } else { 1072 # scan outwards looking for fixes with the required information 1073 # (NOTE: SHOULD EVENTUALLY DO THIS FOR EXTRAPOLATION TOO!) 1074 my ($t0b, $t1b); 1075 if (defined $v0) { 1076 $t0b = $t0; $p0b = $p0; 1077 } else { 1078 ($t0b,$p0b,$v0) = ScanOutwards($key,$times,$points,$i0,-1,$maxSecs); 1079 next Category unless defined $t0b; 1080 } 1081 if (defined $v1) { 1082 $t1b = $t1; $p1b = $p1; 1083 } else { 1084 ($t1b,$p1b,$v1) = ScanOutwards($key,$times,$points,$i1,1,$maxSecs); 1085 next Category unless defined $t1b; 1086 } 1087 # re-calculate the interpolation factor 1088 $f = $f0b = $t1b == $t0b ? 0 : ($time - $t0b) / ($t1b - $t0b); 1089 $et->VPrint(2, " Interpolating $category between fixes (f=$f):\n", 1090 PrintFix($points, $t0b, $t1b)) if $verbose > 2; 1091 } 1092 # must interpolate cyclical values differently 1093 if ($cyclical{$key} and abs($v1 - $v0) > 180) { 1094 # the acute angle spans the discontinuity, so add 1095 # 360 degrees to the smaller angle before interpolating 1096 $v0 < $v1 ? $v0 += 360 : $v1 += 360; 1097 $$fix{$key} = $v1 * $f + $v0 * (1 - $f); 1098 # longitude and roll ranges are -180 to 180, others are 0 to 360 1099 my $max = ($key eq 'lon' or $key eq 'roll') ? 180 : 360; 1100 $$fix{$key} -= 360 if $$fix{$key} >= $max; 1101 } else { 1102 # simple linear interpolation 1103 $$fix{$key} = $v1 * $f + $v0 * (1 - $f); 1104 } 1105 } 670 1106 } 671 1107 } … … 690 1126 $gpsAlt = abs $$fix{alt}; 691 1127 $gpsAltRef = ($$fix{alt} < 0 ? 1 : 0); 1128 } elsif ($$has{alt} and defined $iExt) { 1129 my $tFix = FindFix($et,'alt',$times,$points,$iExt,$iDir,$geoMaxExtSecs); 1130 if ($tFix) { 1131 $gpsAlt = abs $$tFix{alt}; 1132 $gpsAltRef = ($$tFix{alt} < 0 ? 1 : 0); 1133 } 692 1134 } 693 1135 # set new GPS tag values (EXIF, or XMP if write group is 'xmp') … … 700 1142 } 701 1143 # (capture error messages by calling SetNewValue in list context) 702 @r = $exifTool->SetNewValue(GPSLatitude => $$fix{lat}, %opts); 703 @r = $exifTool->SetNewValue(GPSLongitude => $$fix{lon}, %opts); 704 @r = $exifTool->SetNewValue(GPSAltitude => $gpsAlt, %opts); 705 @r = $exifTool->SetNewValue(GPSAltitudeRef => $gpsAltRef, %opts); 1144 @r = $et->SetNewValue(GPSLatitude => $$fix{lat}, %opts); 1145 @r = $et->SetNewValue(GPSLongitude => $$fix{lon}, %opts); 1146 @r = $et->SetNewValue(GPSAltitude => $gpsAlt, %opts); 1147 @r = $et->SetNewValue(GPSAltitudeRef => $gpsAltRef, %opts); 1148 if ($$has{track}) { 1149 my $tFix = $fix; 1150 if (not defined $$fix{track} and defined $iExt) { 1151 my $p = FindFix($et,'track',$times,$points,$iExt,$iDir,$geoMaxExtSecs); 1152 $tFix = $p if $p; 1153 } 1154 @r = $et->SetNewValue(GPSTrack => $$tFix{track}, %opts); 1155 @r = $et->SetNewValue(GPSTrackRef => (defined $$tFix{track} ? 'T' : undef), %opts); 1156 my ($spd, $ref); 1157 if (defined($spd = $$tFix{speed})) { 1158 $ref = $$et{OPTIONS}{GeoSpeedRef}; 1159 if ($ref and defined $speedConv{$ref}) { 1160 $ref = $speedConv{$ref} if $speedConv{$speedConv{$ref}}; 1161 $spd *= $speedConv{$ref}; 1162 } else { 1163 $ref = 'N'; # knots by default 1164 } 1165 } 1166 @r = $et->SetNewValue(GPSSpeed => $spd, %opts); 1167 @r = $et->SetNewValue(GPSSpeedRef => $ref, %opts); 1168 } 1169 if ($$has{orient}) { 1170 my $tFix = $fix; 1171 if (not defined $$fix{dir} and defined $iExt) { 1172 my $p = FindFix($et,'dir',$times,$points,$iExt,$iDir,$geoMaxExtSecs); 1173 $tFix = $p if $p; 1174 } 1175 @r = $et->SetNewValue(GPSImgDirection => $$tFix{dir}, %opts); 1176 @r = $et->SetNewValue(GPSImgDirectionRef => (defined $$tFix{dir} ? 'T' : undef), %opts); 1177 @r = $et->SetNewValue(CameraElevationAngle => $$tFix{pitch}, %opts); 1178 # Note: GPSPitch and GPSRoll are non-standard, and must be user-defined 1179 @r = $et->SetNewValue(GPSPitch => $$tFix{pitch}, %opts); 1180 @r = $et->SetNewValue(GPSRoll => $$tFix{roll}, %opts); 1181 } 1182 if ($$has{atemp}) { 1183 my $tFix = $fix; 1184 if (not defined $$fix{atemp} and defined $iExt) { 1185 # (not all fixes have atemp, so try interpolating specifically for this) 1186 my $p = FindFix($et,'atemp',$times,$points,$iExt,$iDir,$geoMaxExtSecs); 1187 $tFix = $p if $p; 1188 } 1189 @r = $et->SetNewValue(AmbientTemperature => $$tFix{atemp}, %opts); 1190 } 706 1191 unless ($xmp) { 707 @r = $exifTool->SetNewValue(GPSLatitudeRef => ($$fix{lat} > 0 ? 'N' : 'S'), %opts); 708 @r = $exifTool->SetNewValue(GPSLongitudeRef => ($$fix{lon} > 0 ? 'E' : 'W'), %opts); 709 @r = $exifTool->SetNewValue(GPSDateStamp => $gpsDate, %opts); 710 @r = $exifTool->SetNewValue(GPSTimeStamp => $gpsTime, %opts); 1192 my ($latRef, $lonRef); 1193 $latRef = ($$fix{lat} > 0 ? 'N' : 'S') if defined $$fix{lat}; 1194 $lonRef = ($$fix{lon} > 0 ? 'E' : 'W') if defined $$fix{lon}; 1195 @r = $et->SetNewValue(GPSLatitudeRef => $latRef, %opts); 1196 @r = $et->SetNewValue(GPSLongitudeRef => $lonRef, %opts); 1197 @r = $et->SetNewValue(GPSDateStamp => $gpsDate, %opts); 1198 @r = $et->SetNewValue(GPSTimeStamp => $gpsTime, %opts); 711 1199 # set options to edit XMP:GPSDateTime only if it already exists 712 1200 $opts{EditOnly} = 1; … … 714 1202 } 715 1203 unless ($exif) { 716 @r = $e xifTool->SetNewValue(GPSDateTime => "$gpsDate $gpsTime", %opts);1204 @r = $et->SetNewValue(GPSDateTime => "$gpsDate $gpsTime", %opts); 717 1205 } 718 1206 } else { 719 my %opts ;1207 my %opts = ( IgnorePermanent => 1 ); 720 1208 $opts{Replace} = 2 if defined $val; # remove existing new values 721 1209 $opts{Group} = $writeGroup if $writeGroup; 1210 722 1211 # reset any GPS values we might have already set 723 1212 foreach (qw(GPSLatitude GPSLatitudeRef GPSLongitude GPSLongitudeRef 724 GPSAltitude GPSAltitudeRef GPSDateStamp GPSTimeStamp GPSDateTime)) 1213 GPSAltitude GPSAltitudeRef GPSDateStamp GPSTimeStamp GPSDateTime 1214 GPSTrack GPSTrackRef GPSSpeed GPSSpeedRef GPSImgDirection 1215 GPSImgDirectionRef GPSPitch GPSRoll CameraElevationAngle 1216 AmbientTemperature)) 725 1217 { 726 my @r = $e xifTool->SetNewValue($_, undef, %opts);1218 my @r = $et->SetNewValue($_, undef, %opts); 727 1219 } 728 1220 } … … 735 1227 # 1) time difference string ("[+-]DD MM:HH:SS.ss"), geosync'd file name, 736 1228 # "GPSTIME@IMAGETIME", or "GPSTIME@FILENAME" 737 # Returns: geosync hash 1229 # Returns: geosync hash: 1230 # Offset = Offset in seconds for latest synchronization (GPS - image time) 1231 # Points = hash of all sync offsets keyed by image times in seconds 1232 # Times = sorted list of image synchronization times (keys in Points hash) 738 1233 # Notes: calling this routine with more than one geosync'd file causes time drift 739 1234 # correction to be implemented 740 1235 sub ConvertGeosync($$) 741 1236 { 742 my ($e xifTool, $val) = @_;743 my $sync = $e xifTool->GetNewValues('Geosync') || { };1237 my ($et, $val) = @_; 1238 my $sync = $et->GetNewValue('Geosync') || { }; 744 1239 my ($syncFile, $gpsTime, $imgTime); 745 1240 746 1241 if ($val =~ /(.*?)\@(.*)/) { 747 1242 $gpsTime = $1; 748 if (-f $2) { 749 $syncFile = $2; 750 } else { 751 $imgTime = $2; 752 } 1243 (-f $2 ? $syncFile : $imgTime) = $2; 753 1244 # (take care because "-f '1:30'" crashes ActivePerl 5.10) 754 1245 } elsif ($val !~ /^\d/ or $val !~ /:/) { … … 764 1255 'GPSDateTime', 'GPSTimeStamp'); 765 1256 $$info{Error} and warn("$$info{Err}\n"), return undef; 766 $gpsTime or $gpsTime = $$info{GPSDateTime} || $$info{GPSTimeStamp}; 1257 unless ($gpsTime) { 1258 $gpsTime = $$info{GPSDateTime} || $$info{GPSTimeStamp}; 1259 $gpsTime .= 'Z' if $gpsTime and not $$info{GPSDateTime}; 1260 } 1261 $gpsTime or warn("No GPSTimeStamp in '$syncFile\n"), return undef; 767 1262 my $tag; 768 1263 foreach $tag (@timeTags) { 769 1264 if ($$info{$tag}) { 770 1265 $imgTime = $$info{$tag}; 771 $e xifTool->VPrint(2, "Geosyncing with $tag from '$syncFile'\n");1266 $et->VPrint(2, "Geosyncing with $tag from '${syncFile}'\n"); 772 1267 last; 773 1268 } 774 1269 } 775 $gpsTime or warn("No GPSTimeStamp in '$syncFile\n"), return undef; 776 $imgTime or warn("No image timestamp in '$syncFile'\n"), return undef; 1270 $imgTime or warn("No image timestamp in '${syncFile}'\n"), return undef; 777 1271 } 778 1272 # add date to date-less timestamps … … 799 1293 # calculate Unix seconds since the epoch 800 1294 my $imgSecs = Image::ExifTool::GetUnixTime($imgDateTime, 1); 801 defined $imgSecs or warn("Invalid image time '$ imgTime'\n"), return undef;1295 defined $imgSecs or warn("Invalid image time '${imgTime}'\n"), return undef; 802 1296 my $gpsSecs = Image::ExifTool::GetUnixTime($gpsDateTime, 1); 803 defined $gpsSecs or warn("Invalid GPS time '$ gpsTime'\n"), return undef;1297 defined $gpsSecs or warn("Invalid GPS time '${gpsTime}'\n"), return undef; 804 1298 # add fractional seconds 805 1299 $gpsSecs += $1 if $gpsTime =~ /(\.\d+)/; … … 823 1317 $$sync{Points}{$imgSecs} = $$sync{Offset}; 824 1318 # print verbose output 825 if ($e xifTool->Options('Verbose') > 1) {1319 if ($et->Options('Verbose') > 1) { 826 1320 # print GPS and image timestamps in UTC 827 my $gps = Image::ExifTool::ConvertUnixTime($gpsSecs); 828 my $img = Image::ExifTool::ConvertUnixTime($imgSecs); 829 $gps .= $1 if $gpsTime =~ /(\.\d+)/; 830 $img .= $1 if $imgTime =~ /(\.\d+)/; 831 $exifTool->VPrint(1, "Added Geosync point:\n", 832 " GPS time stamp: $gps UTC\n", 833 " Image date/time: $img UTC\n"); 1321 $et->VPrint(1, "Added Geosync point:\n", 1322 ' GPS time stamp: ', PrintFixTime($gpsSecs), "\n", 1323 ' Image date/time: ', PrintFixTime($imgSecs), "\n"); 834 1324 } 835 1325 # save sorted list of image sync times if we have more than one … … 857 1347 858 1348 #------------------------------------------------------------------------------ 1349 # Print fix time 1350 # Inputs: 0) time since the epoch 1351 # Returns: UTC time string with fractional seconds 1352 sub PrintFixTime($) 1353 { 1354 my $time = $_[0] + 0.0005; # round off to nearest ms 1355 my $fsec = int(($time - int($time)) * 1000); 1356 return sprintf('%s.%.3d UTC', Image::ExifTool::ConvertUnixTime($time), $fsec); 1357 } 1358 1359 #------------------------------------------------------------------------------ 1360 # Print fix information 1361 # Inputs: 0) lookup for all fix points, 1-n) list of fix times 1362 # Returns: fix string (including leading indent and trailing newline) 1363 sub PrintFix($@) 1364 { 1365 local $_; 1366 my $points = shift; 1367 my $str = ''; 1368 while (@_) { 1369 my $time = shift; 1370 $str .= ' ' . PrintFixTime($time) . ' -'; 1371 my $fix = $$points{$time}; 1372 if ($fix) { 1373 foreach (sort keys %$fix) { 1374 $str .= " $_=$$fix{$_}" unless $_ eq 'time' or not defined $$fix{$_}; 1375 } 1376 } 1377 $str .= "\n"; 1378 } 1379 return $str; 1380 } 1381 1382 #------------------------------------------------------------------------------ 859 1383 1; # end 860 1384 … … 873 1397 This module loads GPS track logs, interpolates to determine position based 874 1398 on time, and sets new GPS values for geotagging images. Currently supported 875 formats are GPX, NMEA RMC/GGA/GLL, KML, IGC, Garmin XML and TCX, and 876 Magellan PMGNTRK. 1399 formats are GPX, NMEA RMC/GGA/GLL, KML, IGC, Garmin XML and TCX, Magellan 1400 PMGNTRK, Honeywell PTNTHPR, Winplus Beacon text, IMU CSV, DJI CSV, and 1401 Bramor gEO log files. 877 1402 878 1403 Methods in this module should not be called directly. Instead, the Geotag … … 881 1406 in the tag name documentation). 882 1407 1408 =head1 NOTES 1409 1410 To take advantage of attitude information in the PTNTHPR NMEA sentence, the 1411 user-defined tag GPSRoll, must be active. 1412 883 1413 =head1 AUTHOR 884 1414 885 Copyright 2003-20 11, Phil Harvey (phil at owl.phy.queensu.ca)1415 Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com) 886 1416 887 1417 This library is free software; you can redistribute it and/or modify it … … 912 1442 913 1443 =cut 914
Note:
See TracChangeset
for help on using the changeset viewer.