Index: includes/Exif.php =================================================================== --- includes/Exif.php (revision 61759) +++ includes/Exif.php (working copy) @@ -255,13 +255,13 @@ 'gps' => array( 'GPSVersionID' => Exif::BYTE, # GPS tag version 'GPSLatitudeRef' => Exif::ASCII, # North or South Latitude #p52-53 - 'GPSLatitude' => Exif::RATIONAL, # Latitude + 'GPSLatitude' => Exif::RATIONAL, # Latitude (special workaround handling) 'GPSLongitudeRef' => Exif::ASCII, # East or West Longitude #p53 - 'GPSLongitude' => Exif::RATIONAL, # Longitude - 'GPSAltitudeRef' => Exif::BYTE, # Altitude reference + 'GPSLongitude' => Exif::RATIONAL, # Longitude (special workaround handling) + 'GPSAltitudeRef' => Exif::BYTE, # Altitude reference 'GPSAltitude' => Exif::RATIONAL, # Altitude 'GPSTimeStamp' => Exif::RATIONAL, # GPS time (atomic clock) - 'GPSSatellites' => Exif::ASCII, # Satellites used for measurement + 'GPSSatellites' => Exif::ASCII, # Satellites used for measurement 'GPSStatus' => Exif::ASCII, # Receiver status #p54 'GPSMeasureMode' => Exif::ASCII, # Measurement mode #p54-55 'GPSDOP' => Exif::RATIONAL, # Measurement precision @@ -273,16 +273,16 @@ 'GPSImgDirection' => Exif::RATIONAL, # Direction of image 'GPSMapDatum' => Exif::ASCII, # Geodetic survey data used 'GPSDestLatitudeRef' => Exif::ASCII, # Reference for latitude of destination #p56 - 'GPSDestLatitude' => Exif::RATIONAL, # Latitude destination + 'GPSDestLatitude' => Exif::RATIONAL, # Latitude destination (special workaround handling) 'GPSDestLongitudeRef' => Exif::ASCII, # Reference for longitude of destination #p57 - 'GPSDestLongitude' => Exif::RATIONAL, # Longitude of destination + 'GPSDestLongitude' => Exif::RATIONAL, # Longitude of destination (special workaround handling) 'GPSDestBearingRef' => Exif::ASCII, # Reference for bearing of destination #p57 'GPSDestBearing' => Exif::RATIONAL, # Bearing of destination 'GPSDestDistanceRef' => Exif::ASCII, # Reference for distance to destination #p57-58 'GPSDestDistance' => Exif::RATIONAL, # Distance to destination 'GPSProcessingMethod' => Exif::UNDEFINED, # Name of GPS processing method 'GPSAreaInformation' => Exif::UNDEFINED, # Name of GPS area - 'GPSDateStamp' => Exif::ASCII, # GPS date + 'GPSDateStamp' => Exif::ASCII, # GPS date 'GPSDifferential' => Exif::SHORT, # GPS differential correction ), ); @@ -348,6 +348,39 @@ } } + // Work around a bug in the definitions above: GPS coordinates may be arrays of degree-minutes-seconds + // If we find such a coordinate, we convert it here to single a fractional degree. + // This work-around fixes bug 13172 without changing the metadata format at all. (But leaves the + // other fields in the metadata that also should be arrays untouched.) + foreach( array( 'GPSLatitude' => 90, 'GPSLongitude' => 180, 'GPSDestLatitude' => 90, 'GPSDestLongitude' => 180) as $gpsTag => $max) { + if( !array_key_exists( $gpsTag, $this->mFilteredExifData ) ) continue; + $data = $this->mFilteredExifData[$gpsTag]; + if( !is_array( $data ) ) continue; + $val = 0; + $divisor = 1; + foreach( $data as $v ) { + $m = array(); + if( !preg_match( '/^(\d+)\/(\d+)$/', $v, $m ) || $m[2] == 0 ) break; + $val += ( $m[1] / $m[2] ) / $divisor; + $divisor *= 60; + if( $divisor > 3600 ) break; + } + if( $val <= 0.0 ) { + $num = 0; $denom = 1; + } else { + // 7 digits is more than enough. It corresponds to an accuracy of about 1cm, + // which is more than the current GPS systems can deliver in terms of + // precision. Even if more precise locations should appear in the future + // in EXIF data, 1cm is surely precise enough to pinpoint a photographer's + // location. + $val = round( $val, 7 ); + if ( $val > $max ) $val = $max; + $denom = 10000000; // 10**7 + $num = intval( $val * $denom ); + } + $this->mFilteredExifData[$gpsTag] = $num . "/" . $denom; + } + foreach( $this->mFilteredExifData as $k => $v ) { if ( !$this->validate($k, $v) ) { $this->debug( $v, __FUNCTION__, "'$k' contained invalid data" ); @@ -613,6 +646,14 @@ var $mExif; /** + * Values for the geolocation coordinate references + * + * @var array + * @private + */ + var $mGeoReferences; + + /** * Constructor * * @param $exif Array: the Exif data to format ( as returned by @@ -620,6 +661,13 @@ */ function FormatExif( $exif ) { $this->mExif = $exif; + $this->mGeoReferences = array( + 'GPSLatitudeRef' => null + ,'GPSLongitudeRef' => null + ,'GPSDestLatitudeRef' => null + ,'GPSDestLongitudeRef' => null + ,'GPSAltitudeRef' => 0 // Default: above sea level + ); } /** @@ -638,6 +686,11 @@ $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3; unset( $tags['ResolutionUnit'] ); + foreach( $this->mGeoReferences as $gpsRefTag => $value) { + if( !array_key_exists( $gpsRefTag, $tags ) ) continue; + $this->mGeoReferences[$gpsRefTag] = $tags[$gpsRefTag]; + } + foreach( $tags as $tag => $val ) { switch( $tag ) { case 'Compression': @@ -967,6 +1020,28 @@ } break; + case 'GPSLatitude': + case 'GPSDestLatitude': + case 'GPSLongitude': + case 'GPSDestLongitude': + $tags[$tag] = $this->formatCoordinate( $val, $this->mGeoReferences[$tag . 'Ref'] ); + break; + + case 'GPSAltitudeRef': + switch( $val ) { + case 0: case 1: + $tags[$tag] = $this->msg( 'GPSAltitude', $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + + case 'GPSAltitude' : + $tags[$tag] = $this->msg( 'GPSAltitudeValue', $this->mGeoReferences[$tag . 'Ref'], $this->formatNum ( $val ) ); + break; + case 'GPSStatus': switch( $val ) { case 'A': case 'V': @@ -990,7 +1065,6 @@ break; case 'GPSSpeedRef': - case 'GPSDestDistanceRef': switch( $val ) { case 'K': case 'M': case 'N': $tags[$tag] = $this->msg( 'GPSSpeed', $val ); @@ -1001,6 +1075,17 @@ } break; + case 'GPSDestDistanceRef': + switch( $val ) { + case 'K': case 'M': case 'N': + $tags[$tag] = $this->msg( 'GPSDestDistance', $val ); + break; + default: + $tags[$tag] = $val; + break; + } + break; + case 'GPSTrackRef': case 'GPSImgDirectionRef': case 'GPSDestBearingRef': @@ -1120,6 +1205,39 @@ } /** + * Format a coordinate value. + * + * @param $num Mixed: coordinate value in degrees + * @param $direction String (optional): 'N', 'S', 'E', or 'W' + * @return String: coordinate value in degrees, minutes and seconds + * @private + */ + function formatCoordinate( $num, $direction=null ) { + global $wgContLang; + + $m = array(); + if ( is_numeric( $num ) ) { + $den = 1; // we use fmod() below so that $num can be a float too + $fnum = $this->formatNum( round ( $num, 7 ) ); + } else if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) && $m[2] != 0 ) { + $num = $m[1]; $den = $m[2]; + $fnum = $this->formatNum( round ( $num / $den , 7 ) ); + } else { + return $this->formatNum( $num ); + } + $deg = intval( $num / $den ); + $num = 60 * fmod( $num, $den ); + $min = intval( $num / $den ); + $num = 60 * fmod( $num, $den ); + // Limit seconds to at most 3 fractional digits: that's already in the + // millimeter range! + $sec = $this->formatNum( round ( $num / $den , 3 ) ); + $tag = 'exif-coordinate-format'; + if ( $direction != null ) $tag .= '-' . $wgContLang->lc( $direction ); + return wfMsg( $tag, $deg, $min, $sec, $fnum ); + } + + /** * Calculate the greatest common divisor of two integers. * * @param $a Integer: FIXME @@ -1145,6 +1263,7 @@ } return $a; } + } /** Index: languages/messages/MessagesEn.php =================================================================== --- languages/messages/MessagesEn.php (revision 61756) +++ languages/messages/MessagesEn.php (working copy) @@ -3674,6 +3674,13 @@ 'exif-gpsdatestamp' => 'GPS date', 'exif-gpsdifferential' => 'GPS differential correction', +# Coordinate value in deg/min/sec, used for GPS location data. $4 is same in decimal degrees. +'exif-coordinate-format' => '$1° $2\' $3"', +'exif-coordinate-format-n' => '$1° $2\' $3" N', +'exif-coordinate-format-s' => '$1° $2\' $3" S', +'exif-coordinate-format-e' => '$1° $2\' $3" E', +'exif-coordinate-format-w' => '$1° $2\' $3" W', + # Make & model, can be wikified in order to link to the camera and model name 'exif-make-value' => '$1', # do not translate or duplicate this message to other languages 'exif-model-value' => '$1', # do not translate or duplicate this message to other languages @@ -3840,6 +3847,19 @@ 'exif-gpsspeed-m' => 'Miles per hour', 'exif-gpsspeed-n' => 'Knots', +# Pseudotags used for GPSDestDistanceRef +'exif-gpsdestdistance-k' => 'Kilometers', +'exif-gpsdestdistance-m' => 'Miles', +'exif-gpsdestdistance-n' => 'Nautical miles', + +# Pseudotags used for GPSAltitudeRef +'exif-gpsaltitude-0' => 'Meters above sea level', +'exif-gpsaltitude-1' => 'Meters below sea level', + +# Pseudotags used for GPSAltitudeRef +'exif-gpsaltitudevalue-0' => '+ $1 m', +'exif-gpsaltitudevalue-1' => '- $1 m', + # Pseudotags used for GPSTrackRef, GPSImgDirectionRef and GPSDestBearingRef 'exif-gpsdirection-t' => 'True direction', 'exif-gpsdirection-m' => 'Magnetic direction',