Skip to content

Commit

Permalink
Merge pull request #532 from tighten/ajm/display-location
Browse files Browse the repository at this point in the history
Display locations on conference list
  • Loading branch information
andrewmile authored Jan 21, 2025
2 parents 7d07f50 + b8b759a commit 284e974
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 30 deletions.
12 changes: 7 additions & 5 deletions app/CallingAllPapers/ConferenceImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use App\Exceptions\InvalidAddressGeocodingException;
use App\Models\Conference;
use App\Services\Geocoder;
use App\Services\Geocoder\Geocoder;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
Expand Down Expand Up @@ -74,7 +74,7 @@ public function import(Event $event)
$this->updateConferenceFromCallingAllPapersEvent($conference, $event);

if (! $conference->latitude && ! $conference->longitude && $conference->location) {
$this->geocodeLatLongFromLocation($conference);
$this->geocodeLocation($conference);
}

if ($validator->fails()) {
Expand Down Expand Up @@ -112,13 +112,15 @@ private function nullifyInvalidLatLong($primary, $secondary)
return (float) $primary && (float) $secondary ? $primary : null;
}

private function geocodeLatLongFromLocation(Conference $conference): Conference
private function geocodeLocation(Conference $conference): void
{
try {
$conference->coordinates = $this->geocoder->geocode($conference->location);
$result = $this->geocoder->geocode($conference->location);
} catch (InvalidAddressGeocodingException $e) {
return;
}

return $conference;
$conference->coordinates = $result->getCoordinates();
$conference->location_name = $result->getLocationName();
}
}
35 changes: 35 additions & 0 deletions app/Console/Commands/BackfillConferenceLocationNames.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Console\Commands;

use App\Exceptions\InvalidAddressGeocodingException;
use App\Models\Conference;
use App\Services\Geocoder\Geocoder;
use Illuminate\Console\Command;

class BackfillConferenceLocationNames extends Command
{
protected $signature = 'app:backfill-conference-location-names';

protected $description = 'Backfill location names for future conferences';

public function handle()
{
$conferences = Conference::query()
->whereAfter(now())
->whereNotNull(['latitude', 'longitude'])
->whereNull('location_name')
->get();

$conferences->each(function ($conference) {
try {
$result = app(Geocoder::class)->geocode($conference->location);
} catch (InvalidAddressGeocodingException $e) {
return;
}

$conference->location_name = $result->getLocationName();
$conference->save();
});
}
}
1 change: 1 addition & 0 deletions app/Http/Requests/SaveConferenceRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function rules(): array
'before:starts_at',
],
'location' => ['nullable'],
'location_name' => ['nullable'],
'latitude' => ['nullable'],
'longitude' => ['nullable'],
'speaker_package' => ['nullable'],
Expand Down
1 change: 1 addition & 0 deletions app/Models/Conference.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Conference extends UuidBase
'author_id',
'title',
'location',
'location_name',
'latitude',
'longitude',
'description',
Expand Down
29 changes: 10 additions & 19 deletions app/Services/Geocoder.php → app/Services/Geocoder/Geocoder.php
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
<?php

namespace App\Services;
namespace App\Services\Geocoder;

use App\Casts\Coordinates;
use App\Exceptions\InvalidAddressGeocodingException;
use Illuminate\Support\Facades\Http;

class Geocoder
{
public function geocode(string $address): Coordinates
public function geocode(string $address): GeocoderResponse
{
if ($this->isInvalidAddress($address)) {
throw new InvalidAddressGeocodingException;
}

$response = $this->requestGeocoding($address);

if (! count($response['results'])) {
cache()->set('invalid-address::' . md5($address), true);
throw new InvalidAddressGeocodingException;
}

return new Coordinates(
$this->getCoordinate('lat', $response),
$this->getCoordinate('lng', $response),
);
return $this->requestGeocoding($address);
}

private function requestGeocoding($address)
{
return Http::acceptJson()
$response = Http::acceptJson()
->withHeaders([
'User-Agent' => 'Symposium CLI',
])
Expand All @@ -38,11 +27,13 @@ private function requestGeocoding($address)
'key' => config('services.google.maps.key'),
])
->json();
}

private function getCoordinate($type, $response)
{
return data_get($response, "results.0.geometry.location.{$type}");
if (! count($response['results'])) {
cache()->set('invalid-address::' . md5($address), true);
throw new InvalidAddressGeocodingException;
}

return new GeocoderResponse($response);
}

private function isInvalidAddress($address)
Expand Down
56 changes: 56 additions & 0 deletions app/Services/Geocoder/GeocoderResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace App\Services\Geocoder;

use App\Casts\Coordinates;

class GeocoderResponse
{
public function __construct(protected $response) {}

public function getCoordinates(): Coordinates
{
return new Coordinates(
$this->getCoordinate('lat'),
$this->getCoordinate('lng'),
);
}

public function getLocationName(): string
{
$country = $this->getCountry();
$city = $this->getCity();

$values = $country === 'United States'
? [$city, $this->getState(), $country]
: [$city, $country];

return collect($values)->filter()->implode(', ');
}

private function getCoordinate($type)
{
return data_get($this->response, "results.0.geometry.location.{$type}");
}

private function getCity()
{
return $this->getAddressComponent(['locality', 'postal_town'])['long_name'] ?? null;
}

private function getState()
{
return $this->getAddressComponent(['administrative_area_level_1'])['short_name'] ?? null;
}

private function getCountry()
{
return $this->getAddressComponent(['country'])['long_name'] ?? null;
}

private function getAddressComponent(array $types)
{
return collect(data_get($this->response, 'results.0.address_components', []))
->firstWhere(fn ($component) => collect($component['types'])->intersect($types)->isNotEmpty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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('conferences', function (Blueprint $table) {
$table->string('location_name')->nullable()->after('location');
});
}

public function down(): void
{
Schema::table('conferences', function (Blueprint $table) {
$table->dropColumn('location_name');
});
}
};
24 changes: 24 additions & 0 deletions resources/js/components/LocationLookup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<slot :lookup="lookup" @keydown.enter.prevent></slot>
<input type="hidden" name="latitude" v-model="latitude">
<input type="hidden" name="longitude" v-model="longitude">
<input type="hidden" name="location_name" v-model="locationName">
</div>
</template>

Expand All @@ -13,6 +14,7 @@ export default {
return {
latitude: '',
longitude: '',
locationName: '',
};
},
methods: {
Expand All @@ -25,10 +27,32 @@ export default {
dropdown.addListener('place_changed', () => {
const place = dropdown.getPlace();
this.latitude = place.geometry.location.lat();
this.longitude = place.geometry.location.lng();
this.locationName = this.getLocationName(place.address_components);
});
},
getLocationName(components) {
const country = components.find(component => {
return component.types.includes('country');
})?.long_name;
const city = components.find(component => {
return component.types.includes('locality') ||
component.types.includes('postal_town');
})?.long_name;
const values = country === 'United States'
? [city, this.getState(components), country]
: [city, country];
return values.filter(v => v).join(', ');
},
getState(components) {
return components.find(component => {
return component.types.includes('administrative_area_level_1');
})?.short_name;
},
},
};
</script>
5 changes: 5 additions & 0 deletions resources/views/conferences/listing.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ class="text-danger"
@endif
</div>
</div>
@if ($conference->location_name || $conference->location)
<div class="pl-8 text-gray-500">
{{ $conference->location_name ?? $conference->location }}
</div>
@endif
<div class="mt-4 pl-8 space-y-3">
<x-info icon="calendar" icon-color="text-gray-400">
<span class="text-gray-400">Dates:</span>
Expand Down
59 changes: 56 additions & 3 deletions tests/Feature/CallingAllPapersConferenceImporterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
use App\Casts\Coordinates;
use App\Exceptions\InvalidAddressGeocodingException;
use App\Models\Conference;
use App\Services\Geocoder;
use App\Services\Geocoder\Geocoder;
use App\Services\Geocoder\GeocoderResponse;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\Test;
use Tests\MocksCallingAllPapers;
Expand Down Expand Up @@ -234,9 +235,14 @@ public function it_fills_latitude_and_longitude_from_location_if_lat_long_are_nu
$event->location = '10th St. & Constitution Ave. NW, Washington, DC';

$this->mockClient($event);
$this->mock(Geocoder::class, function ($mock) {
$response = $this->mock(GeocoderResponse::class, function ($mock) {
$mock->shouldReceive('getCoordinates')
->andReturn(new Coordinates('38.8921062', '-77.0259036'))
->shouldReceive('getLocationName');
});
$this->mock(Geocoder::class, function ($mock) use ($response) {
$mock->shouldReceive('geocode')
->andReturn(new Coordinates('38.8921062', '-77.0259036'));
->andReturn($response);
});

$importer = new ConferenceImporter(1);
Expand All @@ -248,6 +254,35 @@ public function it_fills_latitude_and_longitude_from_location_if_lat_long_are_nu
$this->assertEquals('-77.0259036', $conference->longitude);
}

#[Test]
public function it_fills_location_name(): void
{
$event = $this->eventStub;

$event->latitude = '0';
$event->longitude = '-82.682221';
$event->location = '10th St. & Constitution Ave. NW, Washington, DC';

$this->mockClient($event);
$response = $this->mock(GeocoderResponse::class, function ($mock) {
$mock->shouldReceive('getCoordinates')
->andReturn(new Coordinates('38.8921062', '-77.0259036'))
->shouldReceive('getLocationName')
->andReturn('Göteborg, Sweden');
});
$this->mock(Geocoder::class, function ($mock) use ($response) {
$mock->shouldReceive('geocode')
->andReturn($response);
});

$importer = new ConferenceImporter(1);
$importer->import($event);

$conference = Conference::first();

$this->assertEquals('Göteborg, Sweden', $conference->location_name);
}

#[Test]
public function it_keeps_lat_long_values_null_if_no_results(): void
{
Expand Down Expand Up @@ -438,4 +473,22 @@ public function conferences_with_null_start_are_valid_with_end_less_than_2_years

$this->assertEquals(1, Conference::count());
}

#[Test]
public function the_geocoder_is_not_called_for_conferences_already_having_coordinates(): void
{
$this->mockClient();
$spy = $this->spy(Geocoder::class);

$importer = new ConferenceImporter(1);
$event = $this->eventStub;
$event->location = 'Somewhere';
$event->latitude = 123;
$event->longitude = 321;

$importer->import($event);

$spy->shouldNotHaveReceived('geocode');
$this->assertEquals(1, Conference::count());
}
}
4 changes: 3 additions & 1 deletion tests/Feature/ConferenceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function user_can_create_conference(): void
}

#[Test]
public function a_conference_can_include_location_coordinates(): void
public function a_conference_can_include_location_coordinates_and_name(): void
{
$user = User::factory()->create();

Expand All @@ -43,6 +43,7 @@ public function a_conference_can_include_location_coordinates(): void
'url' => 'https://jedicon.com',
'latitude' => '37.7991531',
'longitude' => '-122.45050129999998',
'location_name' => 'San Francisco, CA, United States',
]);

$this->assertDatabaseHas(Conference::class, [
Expand All @@ -51,6 +52,7 @@ public function a_conference_can_include_location_coordinates(): void
'url' => 'https://jedicon.com',
'latitude' => '37.7991531',
'longitude' => '-122.45050129999998',
'location_name' => 'San Francisco, CA, United States',
]);
}

Expand Down
Loading

0 comments on commit 284e974

Please sign in to comment.