Refactoring EXIF GPS calculations

posted in: Tech Journal | 0

The Problem

I’ve been revising and refactoring the way I handle the EXIF/GPS calculations in hellobrucetrail.com so often that I’m starting to think I may be “overfactoring”.

Is “overfactoring” a word? A quick google search shows several hits that use that word so I might not have a valid basis for a claim that “you heard it here first”.

What’s the problem I’m solving? Well, in hellobrucetrail.com I have a collection of photos. Many of them have GPS coordinates embedded in their EXIF metadata. The problem I’m solving is to display in human-readable form the latitude and longitude coordinates where a particular photo was taken.

Why is this important? I want to display a google map for various places on or near the Bruce Trail, with markers showing the location where specific photos were taken. Seeing the markers on the google map gives a geographic context to where the photos were taken.

What have I accomplished so far? I have working logic that reads the EXIF metadata from an image file and processes the GPS data into a form that’s usable in the application. I’ve moved this logic around in the last few iterations in an effort to make it testable, and to come up with the most satisfactory design from an object-oriented perspective. I’ve settled on a design that answers the questions “What is the thing?” and “What may be done with the thing?”

  • Is “the thing” the Image? (An Image has EXIF data. GPS coordinates may be extracted from the EXIF).
  • Is “the thing” the GPS coordinates? (GPS coordinates are calculated from EXIF data which comes from an Image).
  • Is “the thing” the EXIF? (An EXIF object may return its GPS coordinates).

I’ve settled on “the thing” being an EXIF object. It has a method which returns its GPS coordinates in the desired format.


<?php

namespace App\Exif;

class Exif {

    public function getGPSFromFile($filename) {
        $exif = \Image::make($filename)->exif();
        return $this->getGPSFromEXIF($exif);
    }

    private function getGPSFromExif($exif) {

        $gps = ['latRef' => 'NA', 'lngRef' => 'NA', 'latDecimal' => 'NA', 'lngDecimal' => 'NA'];

        if(isset($exif['GPSLatitudeRef']) && isset($exif['GPSLongitudeRef'])) {

            $LatM = 1;
            $LngM = 1;
            if ($exif['GPSLatitudeRef'] == 'S') {
                $LatM = -1;
            }
            if ($exif['GPSLongitudeRef'] == 'W') {
                $LngM = -1;
            }

            $gps['latD'] = $exif["GPSLatitude"][0];
            $gps['latM'] = $exif["GPSLatitude"][1];
            $gps['latS'] = $exif["GPSLatitude"][2];
            $gps['lngD'] = $exif["GPSLongitude"][0];
            $gps['lngM'] = $exif["GPSLongitude"][1];
            $gps['lngS'] = $exif["GPSLongitude"][2];

            // Convert strings to numbers.
            foreach ($gps as $key => $value) {
                $pos = strpos($value, '/');
                if ($pos !== false) {
                    $temp = explode('/', $value);
                    $gps[$key] = $temp[0] / $temp[1];
                }
            }

            // Calculate the decimal degree.
            $gps['latDecimal'] = $LatM * ($gps['latD'] + ($gps['latM'] / 60) + ($gps['latS'] / 3600));
            $gps['lngDecimal'] = $LngM * ($gps['lngD'] + ($gps['lngM'] / 60) + ($gps['lngS'] / 3600));

            // Include directional references in return value.
            $gps['latRef'] = $exif['GPSLatitudeRef'];
            $gps['lngRef'] = $exif['GPSLongitudeRef'];

        }

        return $gps;
    }
}

The \Image object is an Intervention\Image\Facades\Image::class.

The body of getGPSFromExif($exif) is a function I adapted from something I found online, probably from this article, PHP extract GPS EXIF data.

I’m noticing that there is an existing PHP package called diversen / gps-from-exif on github. Perhaps in yet another refactoring I’ll try this package in lieu of my custom class.