Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95ed0e339f | ||
|
|
28fd782a40 | ||
|
|
3c66a3945c | ||
|
|
a68e31dad7 | ||
|
|
023ce6f964 |
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Achievement;
|
use App\Models\Achievement;
|
||||||
|
use App\Models\AchievementCategory;
|
||||||
use App\Models\Aircraft;
|
use App\Models\Aircraft;
|
||||||
use App\Models\Airport;
|
use App\Models\Airport;
|
||||||
use App\Models\Alliance;
|
use App\Models\Alliance;
|
||||||
@@ -105,8 +106,14 @@ class UserProfileController extends Controller
|
|||||||
$achievements = Achievement::with(['category', 'difficulty'])
|
$achievements = Achievement::with(['category', 'difficulty'])
|
||||||
->orderBy('id', 'asc')
|
->orderBy('id', 'asc')
|
||||||
->get()
|
->get()
|
||||||
->groupBy(fn(Achievement $a) => $a->category->name)
|
->groupBy(fn(Achievement $a) => $a->category->internal_name)
|
||||||
->map(fn($group) => $group->sortBy('sort_order')->values());
|
->sortBy(function ($group, $internalName) {
|
||||||
|
$index = array_search($internalName, AchievementCategory::ORDER);
|
||||||
|
return $index === false ? PHP_INT_MAX : $index;
|
||||||
|
})
|
||||||
|
->mapWithKeys(fn($group) => [
|
||||||
|
$group->first()->category->name => $group->sortBy('sort_order')->values(),
|
||||||
|
]);
|
||||||
|
|
||||||
$userAchievements = $user->achievements()
|
$userAchievements = $user->achievements()
|
||||||
->with('achievement')
|
->with('achievement')
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
*/
|
*/
|
||||||
class AchievementCategory extends Model
|
class AchievementCategory extends Model
|
||||||
{
|
{
|
||||||
|
const array ORDER = [
|
||||||
|
'number_of_flights',
|
||||||
|
'distance',
|
||||||
|
'general_flying',
|
||||||
|
'countries_and_continents',
|
||||||
|
'aircraft',
|
||||||
|
'airlines_and_alliances',
|
||||||
|
'fun_challenges',
|
||||||
|
];
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'internal_name',
|
'internal_name',
|
||||||
'name',
|
'name',
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Achievement;
|
||||||
|
use App\Models\AchievementCategory;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$newCategory = AchievementCategory::create([
|
||||||
|
'internal_name' => 'number_of_flights',
|
||||||
|
'name' => 'Number of Flights',
|
||||||
|
'description' => 'Achievements for reaching flight count milestones.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$achievementsToUpdate = [
|
||||||
|
'general_flying.first_flight',
|
||||||
|
'general_flying.10_flights',
|
||||||
|
'general_flying.500_flights',
|
||||||
|
'general_flying.50_flights',
|
||||||
|
'general_flying.100_flights',
|
||||||
|
'general_flying.1000_flights',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($achievementsToUpdate as $internalName) {
|
||||||
|
$achievement = Achievement::where('internal_name', $internalName)->first();
|
||||||
|
|
||||||
|
if (!$achievement) {
|
||||||
|
continue; // or throw, depending how strict you want this
|
||||||
|
}
|
||||||
|
|
||||||
|
$achievement->update([
|
||||||
|
'achievement_category_id' => $newCategory->id,
|
||||||
|
'internal_name' => str_replace('general_flying.', 'number_of_flights.', $internalName),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$oldCategory = AchievementCategory::where('internal_name', 'general_flying')->first();
|
||||||
|
|
||||||
|
if ($oldCategory) {
|
||||||
|
Achievement::where('internal_name', 'like', 'number_of_flights.%')
|
||||||
|
->get()
|
||||||
|
->each(function (Achievement $achievement) use ($oldCategory) {
|
||||||
|
$achievement->update([
|
||||||
|
'achievement_category_id' => $oldCategory->id,
|
||||||
|
'internal_name' => str_replace('number_of_flights.', 'general_flying.', $achievement->internal_name),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
AchievementCategory::where('internal_name', 'number_of_flights')->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -370,13 +370,37 @@ export default defineComponent({
|
|||||||
link.click()
|
link.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loopPoints(origin: LngLat, radiusDeg = 0.2, steps = 48): LngLat[] {
|
||||||
|
const [lng, lat] = origin
|
||||||
|
|
||||||
|
// Widen the longitude radius to compensate for mercator distortion at this latitude
|
||||||
|
const lngRadius = radiusDeg / Math.max(Math.cos(lat * Math.PI / 180), 0.15)
|
||||||
|
|
||||||
|
// Center the loop above the origin so the airport point sits at the bottom of the loop
|
||||||
|
const centerLat = lat + radiusDeg
|
||||||
|
|
||||||
|
const points: LngLat[] = []
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const theta = (i / steps) * Math.PI * 2
|
||||||
|
points.push([
|
||||||
|
lng + Math.sin(theta) * lngRadius,
|
||||||
|
centerLat - radiusDeg * Math.cos(theta),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return points // first and last point === origin, at the bottom of the loop
|
||||||
|
}
|
||||||
|
|
||||||
const getArc = (flight: Flight): LngLat[] => {
|
const getArc = (flight: Flight): LngLat[] => {
|
||||||
const key = routeKey(flight.departure_airport, flight.arrival_airport)
|
const key = routeKey(flight.departure_airport, flight.arrival_airport)
|
||||||
if (!arcCache.has(key)) {
|
if (!arcCache.has(key)) {
|
||||||
arcCache.set(key, greatCirclePoints(
|
const isSameAirport = flight.departure_airport.id === flight.arrival_airport.id
|
||||||
[flight.departure_airport.longitude_deg, flight.departure_airport.latitude_deg],
|
|
||||||
[flight.arrival_airport.longitude_deg, flight.arrival_airport.latitude_deg],
|
arcCache.set(key, isSameAirport
|
||||||
))
|
? loopPoints([flight.departure_airport.longitude_deg, flight.departure_airport.latitude_deg])
|
||||||
|
: greatCirclePoints(
|
||||||
|
[flight.departure_airport.longitude_deg, flight.departure_airport.latitude_deg],
|
||||||
|
[flight.arrival_airport.longitude_deg, flight.arrival_airport.latitude_deg],
|
||||||
|
))
|
||||||
}
|
}
|
||||||
return arcCache.get(key)!
|
return arcCache.get(key)!
|
||||||
}
|
}
|
||||||
@@ -666,8 +690,10 @@ export default defineComponent({
|
|||||||
? { top: 20, bottom: 20, left: 55, right: 55 }
|
? { top: 20, bottom: 20, left: 55, right: 55 }
|
||||||
: { top: 60, bottom: 60, left: 60, right: 60 }
|
: { top: 60, bottom: 60, left: 60, right: 60 }
|
||||||
|
|
||||||
const lngs = props.flights.flatMap(f => [f.departure_airport.longitude_deg, f.arrival_airport.longitude_deg])
|
// Use full arc/loop geometry, not just endpoints, so loops aren't clipped
|
||||||
const lats = props.flights.flatMap(f => [f.departure_airport.latitude_deg, f.arrival_airport.latitude_deg])
|
const allPoints = props.flights.flatMap(getArc)
|
||||||
|
const lngs = allPoints.map(p => p[0])
|
||||||
|
const lats = allPoints.map(p => p[1])
|
||||||
|
|
||||||
const minLat = Math.min(...lats)
|
const minLat = Math.min(...lats)
|
||||||
const maxLat = Math.max(...lats)
|
const maxLat = Math.max(...lats)
|
||||||
@@ -689,7 +715,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
map!.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding, duration: 0 })
|
map!.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding, duration: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Map init ──────────────────────────────────────────────────────────
|
// ── Map init ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const initMap = (): void => {
|
const initMap = (): void => {
|
||||||
|
|||||||
Reference in New Issue
Block a user