Skip to content

Commit

Permalink
GPS (#29)
Browse files Browse the repository at this point in the history
* wip

* Refactoring
  • Loading branch information
GeniJaho authored Jan 10, 2024
1 parent 841e02e commit 7afcbe0
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 4 deletions.
85 changes: 85 additions & 0 deletions app/Actions/Photos/ExtractLocationFromPhotoAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace App\Actions\Photos;

use Illuminate\Http\UploadedFile;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\ImageManager;

class ExtractLocationFromPhotoAction implements ExtractsLocationFromPhoto
{
public function run(UploadedFile $photo): array
{
$manager = new ImageManager(new Driver());
$image = $manager->read($photo);
$exif = $image->exif('GPS');

if ($this->isExifInvalid($exif)) {
return [];
}

$result = $this->convertArrayToLatLng((array) $exif);

if ($result['latitude'] === 0.0 && $result['longitude'] === 0.0) {
return [];
}

return $result;
}

private function convertArrayToLatLng(array $exif): array
{
$GPSLatitudeRef = $exif['GPSLatitudeRef'];
$GPSLatitude = $exif['GPSLatitude'];
$GPSLongitudeRef = $exif['GPSLongitudeRef'];
$GPSLongitude = $exif['GPSLongitude'];

$latDegrees = $this->gpsToNumeric($GPSLatitude[0] ?? null);
$latMinutes = $this->gpsToNumeric($GPSLatitude[1] ?? null);
$latSeconds = $this->gpsToNumeric($GPSLatitude[2] ?? null);

$lonDegrees = $this->gpsToNumeric($GPSLongitude[0] ?? null);
$lonMinutes = $this->gpsToNumeric($GPSLongitude[1] ?? null);
$lonSeconds = $this->gpsToNumeric($GPSLongitude[2] ?? null);

$latDirection = ($GPSLatitudeRef === 'W' || $GPSLatitudeRef === 'S') ? -1 : 1;
$lonDirection = ($GPSLongitudeRef === 'W' || $GPSLongitudeRef === 'S') ? -1 : 1;

$latitude = $latDirection * ($latDegrees + ($latMinutes / 60) + ($latSeconds / (60 * 60)));
$longitude = $lonDirection * ($lonDegrees + ($lonMinutes / 60) + ($lonSeconds / (60 * 60)));

return ['latitude' => $latitude, 'longitude' => $longitude];
}

private function gpsToNumeric(?string $coordinates): float
{
if (! $coordinates) {
return 0.0;
}

$parts = explode('/', $coordinates);

if ($parts === []) {
return 0.0;
}

if (count($parts) === 1) {
return (float) $parts[0];
}

if ((float) $parts[1] === 0.0) {
return 0.0;
}

return (float) $parts[0] / (float) $parts[1];
}

private function isExifInvalid(mixed $exif): bool
{
return ! $exif ||
! isset($exif['GPSLatitudeRef']) ||
! isset($exif['GPSLatitude']) ||
! isset($exif['GPSLongitudeRef']) ||
! isset($exif['GPSLongitude']);
}
}
10 changes: 10 additions & 0 deletions app/Actions/Photos/ExtractsLocationFromPhoto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Actions\Photos;

use Illuminate\Http\UploadedFile;

interface ExtractsLocationFromPhoto
{
public function run(UploadedFile $photo): array;
}
13 changes: 10 additions & 3 deletions app/Http/Controllers/UploadPhotosController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,29 @@

namespace App\Http\Controllers;

use App\Actions\Photos\ExtractsLocationFromPhoto;
use App\Http\Requests\StorePhotosRequest;
use App\Models\Photo;

class UploadPhotosController extends Controller
{
public function store(StorePhotosRequest $request)
{
public function store(
StorePhotosRequest $request,
ExtractsLocationFromPhoto $extractLocation,
): array {
$user = auth()->user();

$photo = $request->file('photo');

$location = $extractLocation->run($photo);

$originalFileName = $photo->getClientOriginalName();

Photo::create([
'path' => $photo->storeAs('photos', $originalFileName, 'public'),
'user_id' => $user->id,
'path' => $photo->storeAs('photos', $originalFileName, 'public'),
'latitude' => $location['latitude'] ?? null,
'longitude' => $location['longitude'] ?? null,
]);

return [];
Expand Down
6 changes: 6 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace App\Providers;

use App\Actions\Photos\ExtractLocationFromPhotoAction;
use App\Actions\Photos\ExtractsLocationFromPhoto;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
Expand All @@ -24,5 +26,9 @@ public function boot(): void
Schema::defaultStringLength(191);

Model::unguard();

Model::shouldBeStrict();

$this->app->bind(ExtractsLocationFromPhoto::class, ExtractLocationFromPhotoAction::class);
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"filament/filament": "^3.1",
"guzzlehttp/guzzle": "^7.2",
"inertiajs/inertia-laravel": "^0.6.8",
"intervention/image": "^3.2",
"laravel/framework": "^10.10",
"laravel/jetstream": "^3.2",
"laravel/sanctum": "^3.2",
Expand Down
121 changes: 120 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('photos', function (Blueprint $table) {
$table->decimal('latitude', 10, 8)->after('path')->nullable();
$table->decimal('longitude', 11, 8)->after('latitude')->nullable();
});
}
};
Binary file added storage/app/photo-with-gps.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions tests/Doubles/FakeExtractLocationFromPhotoAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Tests\Doubles;

use App\Actions\Photos\ExtractsLocationFromPhoto;
use Illuminate\Http\UploadedFile;

class FakeExtractLocationFromPhotoAction implements ExtractsLocationFromPhoto
{
public function run(UploadedFile $photo): array
{
return [];
}
}
23 changes: 23 additions & 0 deletions tests/Feature/Photos/UploadPhotosTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

use App\Actions\Photos\ExtractLocationFromPhotoAction;
use App\Actions\Photos\ExtractsLocationFromPhoto;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
Expand All @@ -22,6 +24,27 @@
Storage::disk('public')->assertExists('photos/photo.jpg');
});

test('a user can upload photos with location data', function () {
Storage::fake('public');
$this->swap(ExtractsLocationFromPhoto::class, new ExtractLocationFromPhotoAction());
$this->actingAs($user = User::factory()->create());

$file = UploadedFile::fake()->createWithContent(
'photo.jpg',
file_get_contents(storage_path('app/photo-with-gps.jpg')),
);

$response = $this->post('/upload', ['photo' => $file]);

$response->assertOk();

expect($user->photos()->count())->toBe(1);

$photo = $user->photos()->first();
expect($photo->latitude)->toBe(40.053030045789)
->and($photo->longitude)->toBe(-77.15449870066);
});

test('a photo can not be larger than 20MB', function () {
Storage::fake('public');
$this->actingAs($user = User::factory()->create());
Expand Down
4 changes: 4 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Tests;

use App\Actions\Photos\ExtractsLocationFromPhoto;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Doubles\FakeExtractLocationFromPhotoAction;

abstract class TestCase extends BaseTestCase
{
Expand All @@ -13,5 +15,7 @@ protected function setUp(): void
parent::setUp();

$this->withoutVite();

$this->swap(ExtractsLocationFromPhoto::class, new FakeExtractLocationFromPhotoAction());
}
}

0 comments on commit 7afcbe0

Please sign in to comment.