SimBrief integration #405 (#635)

* SimBrief integration #405

* Add briefing as API response; add acars_xml field #405
This commit is contained in:
Nabeel S
2020-03-23 09:31:35 -04:00
committed by GitHub
parent 04b9e37e1d
commit 9e5386264f
70 changed files with 6816 additions and 192 deletions

View File

@@ -80,7 +80,7 @@ test: phpcs
.PHONY: phpcs
phpcs:
@vendor/bin/php-cs-fixer fix --config=.php_cs -v --diff --dry-run
@vendor/bin/php-cs-fixer fix --config=.php_cs -v --diff --diff-format=udiff --dry-run
#.PHONY: phpstan
#phpstan:

View File

@@ -1,14 +1,11 @@
<?php
namespace App\Http\Resources;
use App\Contracts\Unit;
use Illuminate\Http\Resources\Json\Resource;
namespace App\Contracts;
/**
* Class Response
* Base class for a resource/response
*/
class Response extends Resource
class Resource extends \Illuminate\Http\Resources\Json\Resource
{
/**
* Iterate through the list of $fields and check if they're a "Unit"

View File

@@ -59,7 +59,7 @@ class Unit implements ArrayAccess
{
$response = [];
foreach ($this->responseUnits as $unit) {
$response[$unit] = $this[$unit];
$response[$unit] = $this[$unit] ?? 0;
}
return $response;

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Cron\Nightly;
use App\Contracts\Listener;
use App\Events\CronNightly;
use App\Services\SimBriefService;
/**
* Clear any expired SimBrief flight briefs that aren't attached to a PIREP
*/
class ClearExpiredSimbrief extends Listener
{
private $simbriefSvc;
public function __construct(SimBriefService $simbriefSvc)
{
$this->simbriefSvc = $simbriefSvc;
}
/**
* @param \App\Events\CronNightly $event
*/
public function handle(CronNightly $event): void
{
$this->simbriefSvc->removeExpiredEntries();
}
}

View File

@@ -0,0 +1,21 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use Carbon\Carbon;
use Faker\Generator as Faker;
$factory->define(App\Models\SimBrief::class, function (Faker $faker) {
return [
'id' => $faker->unique()->numberBetween(10, 10000000),
'user_id' => null,
'flight_id' => null,
'pirep_id' => null,
'acars_xml' => '',
'ofp_xml' => '',
'created_at' => Carbon::now('UTC')->toDateTimeString(),
'updated_at' => function (array $sb) {
return $sb['created_at'];
},
];
});

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Add a table to store the Simbrief data
*/
class AddSimbriefTable extends Migration
{
public function up()
{
Schema::create('simbrief', function (Blueprint $table) {
$table->string('id', 36); // The OFP ID
$table->unsignedInteger('user_id');
$table->string('flight_id', 36)->nullable();
$table->string('pirep_id', 36)->nullable();
$table->mediumText('acars_xml');
$table->mediumText('ofp_xml');
$table->timestamps();
$table->primary('id');
$table->index(['user_id', 'flight_id']);
$table->index('pirep_id');
$table->unique('pirep_id'); // Can only belong to a single PIREP
});
}
public function down()
{
Schema::drop('simbrief');
}
}

1
app/Database/seeds/dev/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
local.yml

View File

@@ -152,6 +152,20 @@
options: ''
type: number
description: 'How much the load factor can vary per-flight'
- key: simbrief.api_key
name: 'SimBrief API Key'
group: simbrief
value: ''
options: ''
type: string
description: 'Your SimBrief API key'
- key: simbrief.expire_days
name: 'SimBrief Expire Time'
group: simbrief
value: 5
options: ''
type: number
description: 'Days after how long to remove unused briefs'
- key: pireps.duplicate_check_time
name: 'PIREP duplicate time check'
group: pireps

View File

@@ -3,11 +3,14 @@
namespace App\Http\Controllers\Api;
use App\Contracts\Controller;
use App\Exceptions\AssetNotFound;
use App\Http\Resources\Flight as FlightResource;
use App\Http\Resources\Navdata as NavdataResource;
use App\Models\SimBrief;
use App\Repositories\Criteria\WhereCriteria;
use App\Repositories\FlightRepository;
use App\Services\FlightService;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Prettus\Repository\Criteria\RequestCriteria;
@@ -51,7 +54,18 @@ class FlightController extends Controller
*/
public function get($id)
{
$flight = $this->flightRepo->find($id);
$user = Auth::user();
$flight = $this->flightRepo->with([
'airline',
'subfleets',
'subfleets.aircraft',
'subfleets.fares',
'field_values',
'simbrief' => function ($query) use ($user) {
return $query->where('user_id', $user->id);
},
])->find($id);
$this->flightSvc->filterSubfleets(Auth::user(), $flight);
return new FlightResource($flight);
@@ -64,6 +78,7 @@ class FlightController extends Controller
*/
public function search(Request $request)
{
$user = Auth::user();
$where = [
'active' => true,
'visible' => true,
@@ -95,6 +110,9 @@ class FlightController extends Controller
'subfleets.aircraft',
'subfleets.fares',
'field_values',
'simbrief' => function ($query) use ($user) {
return $query->where('user_id', $user->id);
},
])
->paginate();
} catch (RepositoryException $e) {
@@ -109,6 +127,32 @@ class FlightController extends Controller
return FlightResource::collection($flights);
}
/**
* Output the flight briefing from simbrief or whatever other format
*
* @param string $id The flight ID
*
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
*/
public function briefing($id)
{
$user = Auth::user();
$w = [
'user_id' => $user->id,
'flight_id' => $id,
];
$simbrief = SimBrief::where($w)->first();
if ($simbrief === null) {
throw new AssetNotFound(new Exception('Flight briefing not found'));
}
return response($simbrief->acars_xml, 200, [
'Content-Type' => 'application/xml',
]);
}
/**
* Get a flight's route
*

View File

@@ -5,10 +5,8 @@ namespace App\Http\Controllers\Api;
use App\Contracts\Controller;
use App\Events\PirepPrefiled;
use App\Events\PirepUpdated;
use App\Exceptions\AircraftNotAtAirport;
use App\Exceptions\AircraftPermissionDenied;
use App\Exceptions\PirepCancelled;
use App\Exceptions\UserNotAtAirport;
use App\Http\Requests\Acars\CommentRequest;
use App\Http\Requests\Acars\FieldsRequest;
use App\Http\Requests\Acars\FileRequest;
@@ -22,7 +20,6 @@ use App\Http\Resources\PirepComment as PirepCommentResource;
use App\Http\Resources\PirepFieldCollection;
use App\Models\Acars;
use App\Models\Enums\AcarsType;
use App\Models\Enums\FlightType;
use App\Models\Enums\PirepFieldSource;
use App\Models\Enums\PirepSource;
use App\Models\Enums\PirepState;
@@ -113,7 +110,7 @@ class PirepController extends Controller
protected function checkCancelled(Pirep $pirep)
{
if ($pirep->cancelled) {
throw new PirepCancelled();
throw new PirepCancelled($pirep);
}
}
@@ -199,49 +196,9 @@ class PirepController extends Controller
$user = Auth::user();
$attrs = $this->parsePirep($request);
$attrs['user_id'] = $user->id;
$attrs['source'] = PirepSource::ACARS;
$attrs['state'] = PirepState::IN_PROGRESS;
if (!array_key_exists('status', $attrs)) {
$attrs['status'] = PirepStatus::INITIATED;
}
$pirep = new Pirep($attrs);
// See if this user is at the current airport
/* @noinspection NotOptimalIfConditionsInspection */
if (setting('pilots.only_flights_from_current')
&& $user->curr_airport_id !== $pirep->dpt_airport_id) {
throw new UserNotAtAirport($user, $pirep->dpt_airport);
}
// See if this user is allowed to fly this aircraft
if (setting('pireps.restrict_aircraft_to_rank', false)
&& !$this->userSvc->aircraftAllowed($user, $pirep->aircraft_id)) {
throw new AircraftPermissionDenied($user, $pirep->aircraft);
}
// See if this aircraft is at the departure airport
/* @noinspection NotOptimalIfConditionsInspection */
if (setting('pireps.only_aircraft_at_dpt_airport')
&& $pirep->aircraft_id !== $pirep->dpt_airport_id) {
throw new AircraftNotAtAirport($pirep->aircraft);
}
// Find if there's a duplicate, if so, let's work on that
$dupe_pirep = $this->pirepSvc->findDuplicate($pirep);
if ($dupe_pirep !== false) {
$pirep = $dupe_pirep;
$this->checkCancelled($pirep);
}
// Default to a scheduled passenger flight
if (!array_key_exists('flight_type', $attrs)) {
$attrs['flight_type'] = FlightType::SCHED_PAX;
}
$pirep->save();
$pirep = $this->pirepSvc->prefile($user, $attrs);
Log::info('PIREP PREFILED');
Log::info($pirep->id);

View File

@@ -26,8 +26,6 @@ class FlightController extends Controller
private $geoSvc;
/**
* FlightController constructor.
*
* @param AirlineRepository $airlineRepo
* @param AirportRepository $airportRepo
* @param FlightRepository $flightRepo
@@ -114,6 +112,7 @@ class FlightController extends Controller
'arr_icao' => $request->input('arr_icao'),
'dep_icao' => $request->input('dep_icao'),
'subfleet_id' => $request->input('subfleet_id'),
'simbrief' => !empty(setting('simbrief.api_key')),
]);
}
@@ -138,6 +137,7 @@ class FlightController extends Controller
'flights' => $flights,
'saved' => $saved_flights,
'subfleets' => $this->subfleetRepo->selectBoxList(true),
'simbrief' => !empty(setting('simbrief.api_key')),
]);
}

View File

@@ -9,6 +9,7 @@ use App\Models\Enums\PirepSource;
use App\Models\Enums\PirepState;
use App\Models\Enums\PirepStatus;
use App\Models\Pirep;
use App\Models\SimBrief;
use App\Repositories\AircraftRepository;
use App\Repositories\AirlineRepository;
use App\Repositories\AirportRepository;
@@ -19,6 +20,7 @@ use App\Repositories\PirepRepository;
use App\Services\FareService;
use App\Services\GeoService;
use App\Services\PirepService;
use App\Services\SimBriefService;
use App\Services\UserService;
use App\Support\Units\Fuel;
use App\Support\Units\Time;
@@ -28,9 +30,6 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Laracasts\Flash\Flash;
/**
* Class PirepController
*/
class PirepController extends Controller
{
private $aircraftRepo;
@@ -199,7 +198,7 @@ class PirepController extends Controller
*/
public function show($id)
{
$pirep = $this->pirepRepo->find($id);
$pirep = $this->pirepRepo->with(['simbrief'])->find($id);
if (empty($pirep)) {
Flash::error('Pirep not found');
return redirect(route('frontend.pirep.index'));
@@ -245,10 +244,20 @@ class PirepController extends Controller
// See if request has a ?flight_id, so we can pre-populate the fields from the flight
// Makes filing easier, but we can also more easily find a bid and close it
if ($request->has('flight_id')) {
$flight = $this->flightRepo->find($request->get('flight_id'));
$flight = $this->flightRepo->find($request->input('flight_id'));
$pirep = Pirep::fromFlight($flight);
}
/**
* They have a SimBrief ID, load that up and figure out the flight that it's from
*/
$simbrief_id = null;
if ($request->has('sb_id')) {
$simbrief_id = $request->input('sb_id');
$brief = SimBrief::find($simbrief_id);
$pirep = Pirep::fromSimBrief($brief);
}
return view('pireps.create', [
'aircraft' => null,
'pirep' => $pirep,
@@ -258,6 +267,7 @@ class PirepController extends Controller
'airport_list' => $this->airportRepo->selectBoxList(true),
'pirep_fields' => $this->pirepFieldRepo->all(),
'field_values' => [],
'simbrief_id' => $simbrief_id,
]);
}
@@ -338,6 +348,14 @@ class PirepController extends Controller
$this->saveFares($pirep, $request);
$this->pirepSvc->saveRoute($pirep);
if ($request->has('sb_id')) {
$brief = SimBrief::find($request->input('sb_id'));
/** @var SimBriefService $sbSvc */
$sbSvc = app(SimBriefService::class);
$sbSvc->attachSimbriefToPirep($pirep, $brief);
}
// Depending on the button they selected, set an initial state
// Can be saved as a draft or just submitted
if ($attrs['submit'] === 'save') {
@@ -375,6 +393,11 @@ class PirepController extends Controller
$pirep->aircraft->load('subfleet.fares');
}
$simbrief_id = null;
if ($pirep->simbrief) {
$simbrief_id = $pirep->simbrief->id;
}
$time = new Time($pirep->flight_time);
$pirep->hours = $time->hours;
$pirep->minutes = $time->minutes;
@@ -402,6 +425,7 @@ class PirepController extends Controller
'airline_list' => $this->airlineRepo->selectBoxList(),
'airport_list' => $this->airportRepo->selectBoxList(),
'pirep_fields' => $this->pirepFieldRepo->all(),
'simbrief_id' => $simbrief_id,
]);
}

View File

@@ -0,0 +1,166 @@
<?php
namespace App\Http\Controllers\Frontend;
use App\Exceptions\AssetNotFound;
use App\Models\SimBrief;
use App\Repositories\FlightRepository;
use App\Services\SimBriefService;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class SimBriefController
{
private $flightRepo;
private $simBriefSvc;
public function __construct(FlightRepository $flightRepo, SimBriefService $simBriefSvc)
{
$this->flightRepo = $flightRepo;
$this->simBriefSvc = $simBriefSvc;
}
/**
* Show the main OFP form
*
* @param Request $request
*
* @throws \Exception
*
* @return mixed
*/
public function generate(Request $request)
{
$flight_id = $request->input('flight_id');
$flight = $this->flightRepo->find($flight_id);
if (!$flight) {
flash()->error('Unknown flight');
return redirect(route('frontend.flights.index'));
}
$apiKey = setting('simbrief.api_key');
if (empty($apiKey)) {
flash()->error('Invalid SimBrief API key!');
return redirect(route('frontend.flights.index'));
}
$user = Auth::user();
$simbrief = SimBrief::select('id')->where([
'flight_id' => $flight_id,
'user_id' => $user->id,
])->first();
if ($simbrief) {
return redirect(route('frontend.simbrief.briefing', [$simbrief->id]));
}
return view('flights.simbrief_form', [
'flight' => $flight,
]);
}
/**
* Show the briefing
*
* @param string $id The OFP ID
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
*/
public function briefing($id)
{
$simbrief = SimBrief::find($id);
if (!$simbrief) {
flash()->error('SimBrief briefing not found');
return redirect(route('frontend.flights.index'));
}
return view('flights.simbrief_briefing', [
'simbrief' => $simbrief,
]);
}
/**
* Create a prefile of this PIREP with a given OFP. Then redirect the
* user to the newly prefiled PIREP
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function prefile(Request $request)
{
$sb = SimBrief::find($request->id);
if (!$sb) {
return redirect(route('frontend.flights.index'));
}
// Redirect to the prefile page, with the flight_id and a simbrief_id
$rd = route('frontend.pireps.create').'?sb_id='.$sb->id;
return redirect($rd);
}
/**
* Cancel the SimBrief request
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function cancel(Request $request)
{
$sb = SimBrief::find($request->id);
if (!$sb) {
$sb->delete();
}
return redirect(route('frontend.simbrief.prefile', ['id' => $request->id]));
}
/**
* Check whether the OFP was generated. Pass in two items, the flight_id and ofp_id
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function check_ofp(Request $request)
{
$ofp_id = $request->input('ofp_id');
$flight_id = $request->input('flight_id');
$simbrief = $this->simBriefSvc->checkForOfp(Auth::user()->id, $ofp_id, $flight_id);
if ($simbrief === null) {
$error = new AssetNotFound(new Exception('Simbrief OFP not found'));
return $error->getResponse();
}
return response()->json([
'id' => $simbrief->id,
]);
}
/**
* Generate the API code
*
* @param \Illuminate\Http\Request $request
*
* @throws \Exception
*
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function api_code(Request $request)
{
$apiKey = setting('simbrief.api_key', null);
if (empty($apiKey)) {
flash()->error('Invalid SimBrief API key!');
return redirect(route('frontend.flights.index'));
}
$api_code = md5($apiKey.$request->input('api_req'));
return response()->json([
'api_code' => $api_code,
]);
}
}

View File

@@ -2,10 +2,11 @@
namespace App\Http\Resources;
use App\Contracts\Resource;
use App\Support\Units\Distance;
use App\Support\Units\Fuel;
class Acars extends Response
class Acars extends Resource
{
/**
* Transform the resource into an array.

View File

@@ -2,10 +2,12 @@
namespace App\Http\Resources;
use App\Contracts\Resource;
/**
* ACARS table but only include the fields for the routes
* Class AcarsRoute
*/
class AcarsLog extends Response
class AcarsLog extends Resource
{
}

View File

@@ -2,10 +2,12 @@
namespace App\Http\Resources;
use App\Contracts\Resource;
/**
* ACARS table but only include the fields for the routes
* Class AcarsRoute
*/
class AcarsRoute extends Response
class AcarsRoute extends Resource
{
}

View File

@@ -2,6 +2,8 @@
namespace App\Http\Resources;
class Aircraft extends Response
use App\Contracts\Resource;
class Aircraft extends Resource
{
}

View File

@@ -2,7 +2,9 @@
namespace App\Http\Resources;
class Airline extends Response
use App\Contracts\Resource;
class Airline extends Resource
{
public function toArray($request)
{

View File

@@ -2,6 +2,8 @@
namespace App\Http\Resources;
class Airport extends Response
use App\Contracts\Resource;
class Airport extends Resource
{
}

View File

@@ -2,7 +2,9 @@
namespace App\Http\Resources;
class AirportDistance extends Response
use App\Contracts\Resource;
class AirportDistance extends Resource
{
public function toArray($request)
{

View File

@@ -2,7 +2,9 @@
namespace App\Http\Resources;
class Award extends Response
use App\Contracts\Resource;
class Award extends Resource
{
public function toArray($request)
{

View File

@@ -2,7 +2,9 @@
namespace App\Http\Resources;
class Bid extends Response
use App\Contracts\Resource;
class Bid extends Resource
{
public function toArray($request)
{

View File

@@ -2,7 +2,7 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Contracts\Resource;
/**
* @mixin \App\Models\Fare

View File

@@ -2,13 +2,15 @@
namespace App\Http\Resources;
use App\Contracts\Resource;
use App\Http\Resources\SimBrief as SimbriefResource;
use App\Support\Units\Distance;
use stdClass;
/**
* @mixin \App\Models\Flight
*/
class Flight extends Response
class Flight extends Resource
{
/**
* Set the fields on the flight object
@@ -55,9 +57,12 @@ class Flight extends Response
$res['distance'] = $distance->getResponseUnits();
$res['airline'] = new Airline($this->airline);
$res['subfleets'] = Subfleet::collection($this->subfleets);
$res['subfleets'] = Subfleet::collection($this->whenLoaded('subfleets'));
$res['fields'] = $this->setFields();
// Simbrief info
$res['simbrief'] = new SimbriefResource($this->whenLoaded('simbrief'));
return $res;
}
}

View File

@@ -2,7 +2,9 @@
namespace App\Http\Resources;
class JournalTransaction extends Response
use App\Contracts\Resource;
class JournalTransaction extends Resource
{
public function toArray($request)
{

View File

@@ -2,8 +2,8 @@
namespace App\Http\Resources;
use App\Contracts\Resource;
use App\Models\Enums\NavaidType;
use Illuminate\Http\Resources\Json\Resource;
class Navdata extends Resource
{

View File

@@ -2,11 +2,8 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Contracts\Resource;
/**
* Class Response
*/
class News extends Resource
{
/**

View File

@@ -2,11 +2,16 @@
namespace App\Http\Resources;
use App\Contracts\Resource;
use App\Http\Resources\SimBrief as SimbriefResource;
use App\Models\Enums\PirepStatus;
use App\Support\Units\Distance;
use App\Support\Units\Fuel;
class Pirep extends Response
/**
* @mixin \App\Models\Pirep
*/
class Pirep extends Resource
{
/**
* Transform the resource into an array.
@@ -75,6 +80,9 @@ class Pirep extends Response
// format to kvp
$res['fields'] = new PirepFieldCollection($this->fields);
// Simbrief info
$res['simbrief'] = new SimbriefResource($this->whenLoaded('simbrief'));
return $res;
}
}

View File

@@ -2,11 +2,8 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Contracts\Resource;
/**
* Class Response
*/
class PirepComment extends Resource
{
/**

View File

@@ -2,7 +2,7 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Contracts\Resource;
class Rank extends Resource
{

View File

@@ -2,7 +2,7 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Contracts\Resource;
class Setting extends Resource
{

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
/**
* @mixin \App\Models\SimBrief
*/
class SimBrief extends Resource
{
public function toArray($request)
{
return [
'id' => $this->id,
'url' => url(route('api.flights.briefing', ['id' => $this->id])),
];
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Contracts\Resource;
class Subfleet extends Resource
{

View File

@@ -2,7 +2,7 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Contracts\Resource;
class User extends Resource
{

View File

@@ -2,7 +2,7 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Contracts\Resource;
class UserBid extends Resource
{

View File

@@ -239,6 +239,12 @@ class Flight extends Model
return $this->hasMany(FlightFieldValue::class, 'flight_id');
}
public function simbrief()
{
// id = key from table, flight_id = reference key
return $this->belongsTo(SimBrief::class, 'id', 'flight_id');
}
public function subfleets()
{
return $this->belongsToMany(Subfleet::class, 'flight_subfleet');

View File

@@ -160,7 +160,7 @@ class Pirep extends Model
*
* @return \App\Models\Pirep
*/
public static function fromFlight(Flight $flight)
public static function fromFlight(Flight $flight): self
{
return new self([
'flight_id' => $flight->id,
@@ -175,6 +175,28 @@ class Pirep extends Model
]);
}
/**
* Create a new PIREP from a SimBrief instance
*
* @param \App\Models\SimBrief $simBrief
*
* @return \App\Models\Pirep
*/
public static function fromSimBrief(SimBrief $simBrief): self
{
return new self([
'flight_id' => $simBrief->flight->id,
'airline_id' => $simBrief->flight->airline_id,
'flight_number' => $simBrief->flight->flight_number,
'route_code' => $simBrief->flight->route_code,
'route_leg' => $simBrief->flight->route_leg,
'dpt_airport_id' => $simBrief->flight->dpt_airport_id,
'arr_airport_id' => $simBrief->flight->arr_airport_id,
'route' => $simBrief->xml->getRouteString(),
'level' => $simBrief->xml->getFlightLevel(),
]);
}
/**
* Get the flight ident, e.,g JBU1900
*
@@ -484,6 +506,11 @@ class Pirep extends Model
->latest();
}
public function simbrief()
{
return $this->belongsTo(SimBrief::class, 'id', 'pirep_id');
}
public function transactions()
{
return $this->hasMany(JournalTransaction::class, 'ref_model_id')

98
app/Models/SimBrief.php Normal file
View File

@@ -0,0 +1,98 @@
<?php
namespace App\Models;
use App\Contracts\Model;
use Illuminate\Support\Collection;
/**
* @property string $id The Simbrief OFP ID
* @property int $user_id The user that generated this
* @property string $flight_id Optional, if attached to a flight, removed if attached to PIREP
* @property string $pirep_id Optional, if attached to a PIREP, removed if attached to flight
* @property string $acars_xml
* @property string $ofp_xml
* @property string $ofp_html
* @property Collection $images
* @property Collection $files
* @property Flight $flight
* @property User $user
* @property SimBriefXML $xml
* @property string $acars_flightplan_url
*/
class SimBrief extends Model
{
public $table = 'simbrief';
public $incrementing = false;
protected $fillable = [
'id',
'user_id',
'flight_id',
'pirep_id',
'acars_xml',
'ofp_xml',
'created_at',
'updated_at',
];
/** @var \App\Models\SimBriefXML Store a cached version of the XML object */
private $xml_instance;
/**
* Return a SimpleXML object of the $ofp_xml
*
* @return \App\Models\SimBriefXML|null
*/
public function getXmlAttribute(): SimBriefXML
{
if (empty($this->attributes['ofp_xml'])) {
return null;
}
if (!$this->xml_instance) {
$this->xml_instance = simplexml_load_string(
$this->attributes['ofp_xml'],
SimBriefXML::class
);
}
return $this->xml_instance;
}
/**
* Returns a list of images
*/
public function getImagesAttribute(): Collection
{
return $this->xml->getImages();
}
/**
* Return all of the flight plans
*/
public function getFilesAttribute(): Collection
{
return $this->xml->getFlightPlans();
}
/*
* Relationships
*/
public function flight()
{
if (!empty($this->attributes['flight_id'])) {
return $this->belongsTo(Flight::class, 'flight_id');
}
if (!empty($this->attributes['pirep_id'])) {
return $this->belongsTo(Pirep::class, 'pirep_id');
}
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}

121
app/Models/SimBriefXML.php Normal file
View File

@@ -0,0 +1,121 @@
<?php
namespace App\Models;
use Illuminate\Support\Collection;
use SimpleXMLElement;
/**
* Represents the SimBrief XML instance with some helper methods
*/
class SimBriefXML extends SimpleXMLElement
{
/**
* Return a padded flight level
*
* @return string
*/
public function getFlightLevel(): string
{
if (empty($this->alternate->cruise_altitude)) {
return '0'; // unknown?
}
$fl = (int) ($this->alternate->cruise_altitude) / 100;
return str_pad($fl, 3, '0', STR_PAD_LEFT);
}
/**
* Retrieve all of the flightplans
*
* @return Collection
*/
public function getFlightPlans(): Collection
{
$fps = [];
$base_url = $this->fms_downloads->directory;
// TODO: Put vmsACARS on top
if (!empty($this->fms_downloads->vma)) {
$fps[] = [
'name' => $this->fms_downloads->vma->name->__toString(),
'url' => $base_url.$this->fms_downloads->vma->link,
];
}
foreach ($this->fms_downloads->children() as $child) {
if ($child->getName() === 'directory') {
continue;
}
$fps[] = [
'name' => $child->name->__toString(),
'url' => $base_url.$child->link,
];
}
return collect($fps);
}
/**
* Return a generator which sends out the fix values. This can be a long list
*
* @return \Generator
*/
public function getRoute()
{
foreach ($this->navlog->children()->fix as $fix) {
$type = $fix->type->__toString();
if ($type === 'apt') {
continue;
}
$ident = $fix->ident->__toString();
if ($ident === 'TOC' || $ident === 'TOD') {
continue;
}
yield $fix;
}
}
/**
* Get the route as a string
*
* @return string
*/
public function getRouteString(): string
{
if (!empty($this->general->route)) {
return $this->general->route->__toString();
}
$route = [];
foreach ($this->getRoute() as $fix) {
$route[] = $fix->ident->__toString();
}
return implode(' ', $route);
}
/**
* Retrieve all of the image links
*
* @return Collection
*/
public function getImages(): Collection
{
$images = [];
$base_url = $this->images->directory;
foreach ($this->images->map as $image) {
$images[] = [
'name' => $image->name->__toString(),
'url' => $base_url.$image->link,
];
}
return collect($images);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Providers;
use App\Cron\Hourly\RemoveExpiredBids;
use App\Cron\Hourly\RemoveExpiredLiveFlights;
use App\Cron\Nightly\ApplyExpenses;
use App\Cron\Nightly\ClearExpiredSimbrief;
use App\Cron\Nightly\NewVersionCheck;
use App\Cron\Nightly\PilotLeave;
use App\Cron\Nightly\RecalculateBalances;
@@ -29,6 +30,7 @@ class CronServiceProvider extends ServiceProvider
SetActiveFlights::class,
RecalculateStats::class,
NewVersionCheck::class,
ClearExpiredSimbrief::class,
],
CronWeekly::class => [

View File

@@ -72,6 +72,14 @@ class RouteServiceProvider extends ServiceProvider
Route::get('profile/regen_apikey', 'ProfileController@regen_apikey')->name('profile.regen_apikey');
Route::resource('profile', 'ProfileController');
// SimBrief stuff
Route::get('simbrief/generate', 'SimBriefController@generate')->name('simbrief.generate');
Route::post('simbrief/apicode', 'SimBriefController@api_code')->name('simbrief.api_code');
Route::get('simbrief/check_ofp', 'SimBriefController@check_ofp')->name('simbrief.check_ofp');
Route::get('simbrief/{id}', 'SimBriefController@briefing')->name('simbrief.briefing');
Route::get('simbrief/{id}/prefile', 'SimBriefController@prefile')->name('simbrief.prefile');
Route::get('simbrief/{id}/cancel', 'SimBriefController@cancel')->name('simbrief.cancel');
});
Route::group([
@@ -412,6 +420,7 @@ class RouteServiceProvider extends ServiceProvider
Route::get('flights', 'FlightController@index');
Route::get('flights/search', 'FlightController@search');
Route::get('flights/{id}', 'FlightController@get');
Route::get('flights/{id}/briefing', 'FlightController@briefing')->name('flights.briefing');
Route::get('flights/{id}/route', 'FlightController@route');
Route::get('pireps', 'UserController@pireps');

View File

@@ -130,7 +130,11 @@ class FlightService extends Service
*/
public function filterSubfleets($user, $flight)
{
/** @var \Illuminate\Support\Collection $subfleets */
$subfleets = $flight->subfleets;
if ($subfleets === null || $subfleets->count() === 0) {
return $flight;
}
/*
* Only allow aircraft that the user has access to in their rank

View File

@@ -8,9 +8,13 @@ use App\Events\PirepCancelled;
use App\Events\PirepFiled;
use App\Events\PirepRejected;
use App\Events\UserStatsChanged;
use App\Exceptions\AircraftNotAtAirport;
use App\Exceptions\AircraftPermissionDenied;
use App\Exceptions\PirepCancelNotAllowed;
use App\Exceptions\UserNotAtAirport;
use App\Models\Acars;
use App\Models\Enums\AcarsType;
use App\Models\Enums\FlightType;
use App\Models\Enums\PirepSource;
use App\Models\Enums\PirepState;
use App\Models\Enums\PirepStatus;
@@ -27,24 +31,84 @@ use Illuminate\Support\Facades\Log;
class PirepService extends Service
{
private $geoSvc;
private $pilotSvc;
private $userSvc;
private $pirepRepo;
/**
* @param GeoService $geoSvc
* @param PirepRepository $pirepRepo
* @param UserService $pilotSvc
* @param UserService $userSvc
*/
public function __construct(
GeoService $geoSvc,
PirepRepository $pirepRepo,
UserService $pilotSvc
UserService $userSvc
) {
$this->geoSvc = $geoSvc;
$this->pilotSvc = $pilotSvc;
$this->userSvc = $userSvc;
$this->pirepRepo = $pirepRepo;
}
/**
* Create a prefiled PIREP
*
* @param \App\Models\User $user
* @param array $attrs
*
* @throws \Exception
*
* @return \App\Models\Pirep
*/
public function prefile(User $user, array $attrs): Pirep
{
$attrs['user_id'] = $user->id;
$attrs['state'] = PirepState::IN_PROGRESS;
if (!array_key_exists('status', $attrs)) {
$attrs['status'] = PirepStatus::INITIATED;
}
// Default to a scheduled passenger flight
if (!array_key_exists('flight_type', $attrs)) {
$attrs['flight_type'] = FlightType::SCHED_PAX;
}
$pirep = new Pirep($attrs);
// See if this user is at the current airport
/* @noinspection NotOptimalIfConditionsInspection */
if (setting('pilots.only_flights_from_current')
&& $user->curr_airport_id !== $pirep->dpt_airport_id) {
throw new UserNotAtAirport($user, $pirep->dpt_airport);
}
// See if this user is allowed to fly this aircraft
if (setting('pireps.restrict_aircraft_to_rank', false)
&& !$this->userSvc->aircraftAllowed($user, $pirep->aircraft_id)) {
throw new AircraftPermissionDenied($user, $pirep->aircraft);
}
// See if this aircraft is at the departure airport
/* @noinspection NotOptimalIfConditionsInspection */
if (setting('pireps.only_aircraft_at_dpt_airport')
&& $pirep->aircraft_id !== $pirep->dpt_airport_id) {
throw new AircraftNotAtAirport($pirep->aircraft);
}
// Find if there's a duplicate, if so, let's work on that
$dupe_pirep = $this->findDuplicate($pirep);
if ($dupe_pirep !== false) {
$pirep = $dupe_pirep;
if ($pirep->cancelled) {
throw new \App\Exceptions\PirepCancelled($pirep);
}
}
$pirep->save();
return $pirep;
}
/**
* Create a new PIREP with some given fields
*
@@ -348,9 +412,9 @@ class PirepService extends Service
$ft = $pirep->flight_time;
$pilot = $pirep->user;
$this->pilotSvc->adjustFlightTime($pilot, $ft);
$this->pilotSvc->adjustFlightCount($pilot, +1);
$this->pilotSvc->calculatePilotRank($pilot);
$this->userSvc->adjustFlightTime($pilot, $ft);
$this->userSvc->adjustFlightCount($pilot, +1);
$this->userSvc->calculatePilotRank($pilot);
$pirep->user->refresh();
// Change the status
@@ -387,9 +451,9 @@ class PirepService extends Service
$user = $pirep->user;
$ft = $pirep->flight_time * -1;
$this->pilotSvc->adjustFlightTime($user, $ft);
$this->pilotSvc->adjustFlightCount($user, -1);
$this->pilotSvc->calculatePilotRank($user);
$this->userSvc->adjustFlightTime($user, $ft);
$this->userSvc->adjustFlightCount($user, -1);
$this->userSvc->calculatePilotRank($user);
$pirep->user->refresh();
}

View File

@@ -0,0 +1,155 @@
<?php
namespace App\Services;
use App\Contracts\Service;
use App\Models\Acars;
use App\Models\Enums\AcarsType;
use App\Models\Pirep;
use App\Models\SimBrief;
use Carbon\Carbon;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
class SimBriefService extends Service
{
private $httpClient;
public function __construct(GuzzleClient $httpClient)
{
$this->httpClient = $httpClient;
}
/**
* Check to see if the OFP exists server-side. If it does, download it and
* cache it immediately
*
* @param string $user_id User who generated this
* @param string $ofp_id The SimBrief OFP ID
* @param string $flight_id The flight ID
*
* @return SimBrief|null
*/
public function checkForOfp(string $user_id, string $ofp_id, string $flight_id): SimBrief
{
$uri = str_replace('{id}', $ofp_id, config('phpvms.simbrief_url'));
$opts = [
'connect_timeout' => 2, // wait two seconds by default
'allow_redirects' => false,
];
try {
$response = $this->httpClient->request('GET', $uri, $opts);
if ($response->getStatusCode() !== 200) {
return null;
}
} catch (GuzzleException $e) {
Log::error('Simbrief HTTP Error: '.$e->getMessage());
return null;
}
$body = $response->getBody()->getContents();
$attrs = [
'user_id' => $user_id,
'flight_id' => $flight_id,
'ofp_xml' => $body,
];
// TODO: Retrieve the ACARS XML and store that. For now, replace the doctype
$new_doctype = '<VMSAcars Type="FlightPlan" version="1.0" generated="'.time().'">';
$acars_xml = str_replace('<OFP>', $new_doctype, $body);
$acars_xml = str_replace('</OFP>', '</VMSAcars>', $acars_xml);
$acars_xml = str_replace("\n", '', $acars_xml);
$attrs['acars_xml'] = simplexml_load_string($acars_xml)->asXML();
// Save this into the Simbrief table, if it doesn't already exist
return SimBrief::updateOrCreate(
['id' => $ofp_id],
$attrs
);
}
/**
* Create a prefiled PIREP from a given brief.
*
* 1. Read from the XML the basic PIREP info (dep, arr), and then associate the PIREP
* to the flight ID
* 2. Remove the flight ID from the SimBrief field and assign the pirep_id to the row
* 3. Update the planned flight route in the acars table
* 4. Add additional flight fields (ones which match ACARS)
*
* @param $pirep
* @param SimBrief $simBrief The briefing to create the PIREP from
*
* @return \App\Models\Pirep
*/
public function attachSimbriefToPirep($pirep, SimBrief $simBrief): Pirep
{
$this->addRouteToPirep($pirep, $simBrief);
$simBrief->pirep_id = $pirep->id;
$simBrief->flight_id = null;
$simBrief->save();
return $pirep;
}
/**
* Add the route from a SimBrief flight plan to a PIREP
*
* @param Pirep $pirep
* @param SimBrief $simBrief
*
* @return Pirep
*/
protected function addRouteToPirep($pirep, SimBrief $simBrief): Pirep
{
// Clear previous entries
Acars::where(['pirep_id' => $pirep->id, 'type' => AcarsType::ROUTE])->delete();
// Create the flight route
$order = 1;
foreach ($simBrief->xml->getRoute() as $fix) {
$position = [
'name' => $fix->ident,
'pirep_id' => $pirep->id,
'type' => AcarsType::ROUTE,
'order' => $order++,
'lat' => $fix->pos_lat,
'lon' => $fix->pos_long,
];
$acars = new Acars($position);
$acars->save();
}
return $pirep;
}
/**
* Remove any expired entries from the SimBrief table. Expired means there's
* a flight_id attached to it, but no pirep_id (meaning it was never used for
* an actual flight)
*/
public function removeExpiredEntries(): void
{
$expire_days = setting('simbrief.expire_days', 5);
$expire_time = Carbon::now('UTC')->subDays($expire_days)->toDateTimeString();
$briefs = SimBrief::where([
['pirep_id', '=', ''],
['created_at', '<', $expire_time],
])->get();
foreach ($briefs as $brief) {
$brief->delete();
// TODO: Delete any assets
}
}
}

View File

@@ -152,8 +152,6 @@ if (!function_exists('setting')) {
* @param $key
* @param mixed $default
*
* @throws \Exception
*
* @return mixed|null
*/
function setting($key, $default = null)
@@ -193,7 +191,6 @@ if (!function_exists('public_asset')) {
{
$publicBaseUrl = app()->publicUrlPath();
$path = $publicBaseUrl.$path;
$path = str_replace('//', '/', $path);
return url($path, $parameters);

View File

@@ -55,6 +55,11 @@ return [
*/
'airport_lookup' => App\Services\AirportLookup\VaCentralLookup::class,
/*
* URL for where to lookup the Simbrief flight plans
*/
'simbrief_url' => 'https://www.simbrief.com/ofp/flightplans/xml/{id}.xml',
/*
* Your vaCentral API key
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,622 @@
'use strict';
/*
* SimBrief APIv1 Javascript Functions
* For use with VA Dispatch systems
* By Derek Mayer - contact@simbrief.com
*
* Any individual wishing to make use of this class must first contact me
* to obtain a unique API key; without which it will be impossible to connect
* to the API.
*
* Any attempt to circumvent the API authorization, steal another
* developer's API key, hack, compromise, or gain unauthorized access to
* the SimBrief website or it's web systems, or bypass or allow others to bypass
* the SimBrief.com login screen will result in immediate revocation of the
* associated API key, and in serious situations, legal action at my discretion.
*/
/*
* Settings and initial variables
*/
let sbform = "sbapiform";
let sbworkerurl = "https://www.simbrief.com/ofp/ofp.loader.api.php";
let sbworkerid = 'SBworker';
let sbcallerid = 'SBcaller';
let sbworkerstyle = 'width=600,height=315';
let sbworker;
let SBloop;
let ofp_id;
let flight_id;
let outputpage_save;
let outputpage_calc;
let fe_result;
let timestamp;
let api_code;
function simbriefsubmit(_flight_id, outputpage) {
flight_id = _flight_id;
if (sbworker) {
sbworker.close();
}
if (SBloop) {
window.clearInterval(SBloop);
}
api_code = null;
ofp_id = null;
fe_result = null;
timestamp = null;
outputpage_save = null;
outputpage_calc = null;
do_simbriefsubmit(outputpage);
}
async function do_simbriefsubmit(outputpage) {
//CATCH UNDEFINED OUTPUT PAGE, SET IT TO THE CURRENT PAGE
if (!outputpage) {
outputpage = location.href;
}
if (!timestamp) {
timestamp = Math.round(+new Date() / 1000);
}
outputpage_save = outputpage;
outputpage_calc = outputpage.replace("http://", "");
if (!api_code) {
const api_req = document.getElementsByName('orig')[0].value + document.getElementsByName('dest')[0].value + document.getElementsByName('type')[0].value + timestamp + outputpage_calc;
let apiCodeResp;
try {
apiCodeResp = await phpvms.request({
method: 'POST',
url: '/simbrief/apicode',
data: {
api_req,
flight_id,
}
});
} catch (e) {
console.log('request error', e);
return;
}
api_code = apiCodeResp.data.api_code;
console.log('API code response: ', api_code);
}
//IF API_CODE IS SET, FINALIZE FORM
var apiform = document.getElementById(sbform);
apiform.setAttribute("method", "get");
apiform.setAttribute("action", sbworkerurl);
apiform.setAttribute("target", sbworkerid);
var input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "apicode");
input.setAttribute("value", api_code);
apiform.appendChild(input);
var input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "outputpage");
input.setAttribute("value", outputpage_calc);
apiform.appendChild(input);
var input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "timestamp");
input.setAttribute("value", timestamp);
apiform.appendChild(input);
//LAUNCH FORM
window.name = sbcallerid;
LaunchSBworker();
apiform.submit();
//DETERMINE OFP_ID
ofp_id = timestamp + '_' + md5(document.getElementsByName('orig')[0].value + document.getElementsByName('dest')[0].value + document.getElementsByName('type')[0].value);
//LOOP TO DETECT WHEN THE WORKER PROCESS IS CLOSED
SBloop = window.setInterval(checkSBworker, 500);
}
/*
* Other related functions
*/
function LaunchSBworker() {
sbworker = window.open('about:blank', sbworkerid, sbworkerstyle)
//TEST FOR POPUP BLOCKERS
if (sbworker == null || typeof (sbworker) == 'undefined') {
alert('Please disable your pop-up blocker to generate a flight plan!');
} else {
if (window.focus) {
sbworker.focus();
}
}
}
function checkSBworker() {
if (sbworker && sbworker.closed) {
window.clearInterval(SBloop);
Redirect_caller();
}
}
async function Redirect_caller() {
/*
* First check that the file actually exists.
* It might not if the window was closed before completion.
*
* An external PHP file is used so as to avoid any "Same
* Origin" errors.
*/
let apiCodeResp;
try {
apiCodeResp = await phpvms.request({
method: 'GET',
url: '/simbrief/check_ofp',
params: {
flight_id,
ofp_id,
}
});
} catch (e) {
console.log('request error', e);
setTimeout(function () {
Redirect_caller();
}, 500);
return;
}
api_code = apiCodeResp.data.id;
console.log('API code response: ', api_code);
/*
* If the file exists, redirect to the specified Output Page.
*/
outputpage_save += '/' + ofp_id;
let apiform = document.createElement("form");
apiform.setAttribute("method", "get");
apiform.setAttribute("action", outputpage_save);
/*
* Analyse link to see if there are any prior GET params.
* If so, append them to the form
*/
let urlinfo = urlObject({'url': outputpage_save});
for (let key in urlinfo['parameters']) {
let input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", key);
input.setAttribute("value", urlinfo['parameters'][key]);
apiform.appendChild(input);
}
let input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "ofp_id");
input.setAttribute("value", ofp_id);
apiform.appendChild(input);
document.body.appendChild(apiform);
apiform.submit();
}
function sb_res_load(url) {
var fileref = document.createElement('script');
fileref.type = "text/javascript";
fileref.src = url + "&p=" + Math.floor(Math.random() * 10000000);
document.getElementsByTagName("head")[0].appendChild(fileref);
}
/*
* URLOBJECT function
* Courtesy Ayman Farhat
*/
function urlObject(options) {
"use strict";
/*global window, document*/
var url_search_arr,
option_key,
i,
urlObj,
get_param,
key,
val,
url_query,
url_get_params = {},
a = document.createElement('a'),
default_options = {
'url': window.location.href,
'unescape': true,
'convert_num': true
};
if (typeof options !== "object") {
options = default_options;
} else {
for (option_key in default_options) {
if (default_options.hasOwnProperty(option_key)) {
if (options[option_key] === undefined) {
options[option_key] = default_options[option_key];
}
}
}
}
a.href = options.url;
url_query = a.search.substring(1);
url_search_arr = url_query.split('&');
if (url_search_arr[0].length > 1) {
for (i = 0; i < url_search_arr.length; i += 1) {
get_param = url_search_arr[i].split("=");
if (options.unescape) {
key = decodeURI(get_param[0]);
val = decodeURI(get_param[1]);
} else {
key = get_param[0];
val = get_param[1];
}
if (options.convert_num) {
if (val.match(/^\d+$/)) {
val = parseInt(val, 10);
} else if (val.match(/^\d+\.\d+$/)) {
val = parseFloat(val);
}
}
if (url_get_params[key] === undefined) {
url_get_params[key] = val;
} else if (typeof url_get_params[key] === "string") {
url_get_params[key] = [url_get_params[key], val];
} else {
url_get_params[key].push(val);
}
get_param = [];
}
}
urlObj = {
protocol: a.protocol,
hostname: a.hostname,
host: a.host,
port: a.port,
hash: a.hash.substr(1),
pathname: a.pathname,
search: a.search,
parameters: url_get_params
};
return urlObj;
}
/*
* MD5 and UTF8_ENCODE functions
* Courtesy of phpjs.org
*/
function md5(str) {
// discuss at: http://phpjs.org/functions/md5/
// original by: Webtoolkit.info (http://www.webtoolkit.info/)
// improved by: Michael White (http://getsprink.com)
// improved by: Jack
// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// input by: Brett Zamir (http://brett-zamir.me)
// bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// depends on: utf8_encode
// example 1: md5('Kevin van Zonneveld');
// returns 1: '6e658d4bfcb59cc13f96c14450ac40b9'
var xl;
var rotateLeft = function (lValue, iShiftBits) {
return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
};
var addUnsigned = function (lX, lY) {
var lX4, lY4, lX8, lY8, lResult;
lX8 = (lX & 0x80000000);
lY8 = (lY & 0x80000000);
lX4 = (lX & 0x40000000);
lY4 = (lY & 0x40000000);
lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
if (lX4 & lY4) {
return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
}
if (lX4 | lY4) {
if (lResult & 0x40000000) {
return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
} else {
return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
}
} else {
return (lResult ^ lX8 ^ lY8);
}
};
var _F = function (x, y, z) {
return (x & y) | ((~x) & z);
};
var _G = function (x, y, z) {
return (x & z) | (y & (~z));
};
var _H = function (x, y, z) {
return (x ^ y ^ z);
};
var _I = function (x, y, z) {
return (y ^ (x | (~z)));
};
var _FF = function (a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
};
var _GG = function (a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
};
var _HH = function (a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
};
var _II = function (a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
};
var convertToWordArray = function (str) {
var lWordCount;
var lMessageLength = str.length;
var lNumberOfWords_temp1 = lMessageLength + 8;
var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
var lWordArray = new Array(lNumberOfWords - 1);
var lBytePosition = 0;
var lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition));
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
};
var wordToHex = function (lValue) {
var wordToHexValue = '',
wordToHexValue_temp = '',
lByte, lCount;
for (lCount = 0; lCount <= 3; lCount++) {
lByte = (lValue >>> (lCount * 8)) & 255;
wordToHexValue_temp = '0' + lByte.toString(16);
wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
}
return wordToHexValue;
};
var x = [],
k, AA, BB, CC, DD, a, b, c, d, S11 = 7,
S12 = 12,
S13 = 17,
S14 = 22,
S21 = 5,
S22 = 9,
S23 = 14,
S24 = 20,
S31 = 4,
S32 = 11,
S33 = 16,
S34 = 23,
S41 = 6,
S42 = 10,
S43 = 15,
S44 = 21;
str = utf8_encode(str);
x = convertToWordArray(str);
a = 0x67452301;
b = 0xEFCDAB89;
c = 0x98BADCFE;
d = 0x10325476;
xl = x.length;
for (k = 0; k < xl; k += 16) {
AA = a;
BB = b;
CC = c;
DD = d;
a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244);
d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314);
b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
a = addUnsigned(a, AA);
b = addUnsigned(b, BB);
c = addUnsigned(c, CC);
d = addUnsigned(d, DD);
}
var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
return temp.toUpperCase().substr(0, 10);
}
function utf8_encode(argString) {
// discuss at: http://phpjs.org/functions/utf8_encode/
// original by: Webtoolkit.info (http://www.webtoolkit.info/)
// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// improved by: sowberry
// improved by: Jack
// improved by: Yves Sucaet
// improved by: kirilloid
// bugfixed by: Onno Marsman
// bugfixed by: Onno Marsman
// bugfixed by: Ulrich
// bugfixed by: Rafal Kukawski
// bugfixed by: kirilloid
// example 1: utf8_encode('Kevin van Zonneveld');
// returns 1: 'Kevin van Zonneveld'
if (argString === null || typeof argString === 'undefined') {
return '';
}
// .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
var string = (argString + '');
var utftext = '',
start, end, stringl = 0;
start = end = 0;
stringl = string.length;
for (var n = 0; n < stringl; n++) {
var c1 = string.charCodeAt(n);
var enc = null;
if (c1 < 128) {
end++;
} else if (c1 > 127 && c1 < 2048) {
enc = String.fromCharCode(
(c1 >> 6) | 192, (c1 & 63) | 128
);
} else if ((c1 & 0xF800) != 0xD800) {
enc = String.fromCharCode(
(c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
);
} else {
// surrogate pairs
if ((c1 & 0xFC00) != 0xD800) {
throw new RangeError('Unmatched trail surrogate at ' + n);
}
var c2 = string.charCodeAt(++n);
if ((c2 & 0xFC00) != 0xDC00) {
throw new RangeError('Unmatched lead surrogate at ' + (n - 1));
}
c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000;
enc = String.fromCharCode(
(c1 >> 18) | 240, ((c1 >> 12) & 63) | 128, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
);
}
if (enc !== null) {
if (end > start) {
utftext += string.slice(start, end);
}
utftext += enc;
start = end = n + 1;
}
}
if (end > start) {
utftext += string.slice(start, stringl);
}
return utftext;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +1,14 @@
{
"/assets/frontend/js/app.js": "/assets/frontend/js/app.js?id=ccb02e6ada8be5b91964",
"/assets/frontend/css/now-ui-kit.css": "/assets/frontend/css/now-ui-kit.css?id=20b82d8dbacf7e058df2",
"/assets/frontend/css/now-ui-kit.css": "/assets/frontend/css/now-ui-kit.css?id=0866bc6acff338c55ea8",
"/assets/admin/css/vendor.min.css": "/assets/admin/css/vendor.min.css?id=f061660f5c4070d3d0ff",
"/assets/frontend/js/app.js.map": "/assets/frontend/js/app.js.map?id=576b33c9aa533da9b6a3",
"/assets/frontend/css/now-ui-kit.css.map": "/assets/frontend/css/now-ui-kit.css.map?id=fdc4f42ad9047d073145",
"/assets/frontend/js/app.js.map": "/assets/frontend/js/app.js.map?id=dc4f3662754a4451d12e",
"/assets/frontend/css/now-ui-kit.css.map": "/assets/frontend/css/now-ui-kit.css.map?id=9ebac1b7b755278cfd36",
"/assets/admin/css/vendor.min.css.map": "/assets/admin/css/vendor.min.css.map?id=bbc90660cdfd2b9b9009",
"/assets/admin/js/app.js": "/assets/admin/js/app.js?id=53dbfe0b2d69322a270a",
"/assets/admin/js/app.js.map": "/assets/admin/js/app.js.map?id=c28b2b55cdafc2a5b414",
"/assets/admin/js/app.js.map": "/assets/admin/js/app.js.map?id=282a039a5f543d8c9323",
"/assets/installer/js/app.js": "/assets/installer/js/app.js?id=62e02c2328c69e0bb6fb",
"/assets/installer/js/app.js.map": "/assets/installer/js/app.js.map?id=7bda13b284c53f56ff4a",
"/assets/installer/js/app.js.map": "/assets/installer/js/app.js.map?id=c7065cf7c26eb0c9547f",
"/assets/fonts/glyphicons-halflings-regular.woff2": "/assets/fonts/glyphicons-halflings-regular.woff2?id=349344e92fb16221dd56",
"/assets/admin/fonts/glyphicons-halflings-regular.woff2": "/assets/admin/fonts/glyphicons-halflings-regular.woff2?id=349344e92fb16221dd56",
"/assets/admin/img/clear.png": "/assets/admin/img/clear.png?id=63b3af84650a0145d61a",
@@ -19,7 +19,7 @@
"/assets/frontend/js/vendor.js": "/assets/frontend/js/vendor.js?id=0f56f07ddfc52e0265ea",
"/assets/admin/css/vendor.css": "/assets/admin/css/vendor.css?id=da3d6e838ded26ce93ae",
"/assets/admin/js/vendor.js": "/assets/admin/js/vendor.js?id=63a4fc6da97448db5b67",
"/assets/installer/css/vendor.css": "/assets/installer/css/vendor.css?id=b484734ef5549fc2baae",
"/assets/installer/css/vendor.css": "/assets/installer/css/vendor.css?id=236d69cda748d2e7b97b",
"/assets/installer/js/vendor.js": "/assets/installer/js/vendor.js?id=e191958da1971cc63bd9",
"/assets/global/js/vendor.js": "/assets/global/js/vendor.js?id=cdc2a84d3de901c5b9c5",
"/assets/global/css/vendor.css": "/assets/global/css/vendor.css?id=89afa5863f07a984ffeb"

View File

@@ -94,6 +94,7 @@
.btn-outline-warning { @include outline-buttons($warning-color, $warning-states-color); }
.btn-outline-danger { @include outline-buttons($danger-color, $danger-states-color); }
.btn-outline-default { @include outline-buttons($default-color, $default-states-color); }
.btn-outline-dark { @include outline-buttons($brand-inverse, $default-states-color); }
.btn-round{
border-width: $border-thin;

View File

@@ -2,8 +2,6 @@
@mixin btn-styles($btn-color, $btn-states-color) {
background-color: $btn-color;
&:hover,
&:focus,
&:not(:disabled):not(.disabled):active,

View File

@@ -1,7 +1,7 @@
<div class="card">
<div class="card-block" style="min-height: 0px; display: flex; justify-content: center; align-items: center;">
<div class="form-group text-right btn-primary my-bids">
<a href="{{ route('frontend.flights.bids') }}">{{ trans_choice('flights.mybid', 2) }}</a>
<div class="form-group text-right">
<a href="{{ route('frontend.flights.bids') }}" class="btn btn-outline-primary">{{ trans_choice('flights.mybid', 2) }}</a>
</div>
</div>
</div>

View File

@@ -28,7 +28,7 @@
</div>
<div class="clear mt-1" style="margin-top: 10px;">
{{ Form::submit(__('common.find'), ['class' => 'btn btn-primary']) }}&nbsp;
{{ Form::submit(__('common.find'), ['class' => 'btn btn-outline-primary']) }}&nbsp;
<a href="{{ route('frontend.flights.index') }}">@lang('common.reset')</a>
</div>
{{ Form::close() }}

View File

@@ -0,0 +1,234 @@
@extends('app')
@section('title', 'Briefing')
@section('content')
<div class="row">
<div class="col-sm-9">
<h2>{{ $simbrief->xml->general->icao_airline }}{{ $simbrief->xml->general->flight_number }}
: {{ $simbrief->xml->origin->icao_code }} to {{ $simbrief->xml->destination->icao_code }}</h2>
</div>
<div class="col-sm-3">
@if (empty($simbrief->pirep_id))
<a class="btn btn-outline-info pull-right btn-lg"
style="margin-top: -10px;margin-bottom: 5px"
href="{{ url(route('frontend.simbrief.prefile', [$simbrief->id])) }}">Prefile PIREP</a>
@endif
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="row">
<div class="col-6">
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;Dispatch Information
</h6>
<div class="form-container-body">
<div class="row">
<div class="col-4 text-center">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Flight</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->general->icao_airline }}{{ $simbrief->xml->general->flight_number }}</p>
</div>
</div>
<div class="col-4 text-center">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Departure</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->origin->icao_code }}@if(!empty($simbrief->xml->origin->plan_rwy))
/{{ $simbrief->xml->origin->plan_rwy }}
@endif
</p>
</div>
</div>
<div class="col-4 text-center">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Arrival</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->destination->icao_code }}@if(!empty($simbrief->xml->destination->plan_rwy))
/{{ $simbrief->xml->destination->plan_rwy }}
@endif
</p>
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-4 text-center">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Aircraft</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->aircraft->name }}</p>
</div>
</div>
<div class="col-4 text-center">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Enroute Time</p>
<p class="border border-dark rounded p-1 small text-monospace">
@minutestotime($simbrief->xml->times->sched_time_enroute / 60)</p>
</div>
</div>
<div class="col-4 text-center">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Cruise Altitude</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->general->initial_altitude }}</p>
</div>
</div>
</div>
<hr/>
@if (!empty($simbrief->xml->general->dx_rmk))
<div class="row">
<div class="col-12">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Dispatcher Remarks</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->general->dx_rmk }}</p>
</div>
</div>
</div>
@endif
@if (!empty($simbrief->xml->general->sys_rmk))
<div class="row">
<div class="col-12">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">System Remarks</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->general->sys_rmk }}</p>
</div>
</div>
</div>
@endif
</div>
</div>
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;Flight Plan
</h6>
<div class="form-container-body">
<div class="row">
<div class="col-12">
<p class="border border-dark rounded p-1 small text-monospace">
{!! str_replace("\n", "<br>", $simbrief->xml->atc->flightplan_text) !!}
</p>
</div>
</div>
</div>
</div>
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;Weather
</h6>
<div class="form-container-body">
<div class="row">
<div class="col-12">
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Departure METAR</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->weather->orig_metar }}</p>
</div>
<hr/>
<div><p class="small text-uppercase pb-sm-0 mb-sm-1">Arrival METAR</p>
<p class="border border-dark rounded p-1 small text-monospace">
{{ $simbrief->xml->weather->dest_metar }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;Download Flight Plan
</h6>
<div class="form-container-body">
<div class="row">
<div class="col-12">
<select id="download_fms_select" class="select2 custom-select">
@foreach($simbrief->files as $fms)
<option value="{{ $fms['url'] }}">{{ $fms['name'] }}</option>
@endforeach
</select>
<br/>
<input id="download_fms"
type="submit"
class="btn btn-outline-primary pull-right"
value="Download"/>
</div>
</div>
</div>
</div>
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;OFP
</h6>
<div class="form-container-body border border-dark">
<div class="overflow-auto" style="height: 600px;">
{!! $simbrief->xml->text->plan_html !!}
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;Flight Maps
</h6>
<div class="form-container-body">
@foreach($simbrief->images->chunk(2) as $images)
<div class="row">
@foreach($images as $image)
<div class="col-6 text-center">
<p>
<img src="{{ $image['url'] }}" alt="{{ $image['name'] }}"/>
<small class="text-muted">{{ $image['name'] }}</small>
</p>
</div>
@endforeach
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
@if (empty($simbrief->pirep_id))
<a class="btn btn-outline-info pull-right"
style="margin-top: -10px;margin-bottom: 5px"
href="{{ url(route('frontend.simbrief.prefile', [$simbrief->id])) }}">Prefile PIREP</a>
@endif
</div>
</div>
@endsection
@section('scripts')
<script>
$(document).ready(function () {
$("#download_fms").click(e => {
e.preventDefault();
const select = document.getElementById("download_fms_select");
const link = select.options[select.selectedIndex].value;
console.log('Downloading FMS: ', link);
window.open(link, '_blank');
});
});
</script>
@endsection

View File

@@ -0,0 +1,162 @@
@extends('app')
@section('title', 'Generate OFP')
@section('content')
<form id="sbapiform">
<div class="row">
<div class="col-md-12">
<h2>Create Flight Briefing</h2>
<div class="row">
<div class="col-8">
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;@lang('pireps.flightinformations')
</h6>
<div class="form-container-body">
<div class="row">
<div class="col-sm-4">
<label for="orig">Departure Airport</label>
<input id="orig"
name="orig"
type="text"
class="form-control"
placeholder="ZZZZ"
maxlength="4"
value="{{ $flight->dpt_airport_id }}"/>
</div>
<div class="col-sm-4">
<label for="dest">Arrival Airport</label>
<input id="dest"
name="dest"
type="text"
class="form-control"
placeholder=""
maxlength="4"
value="{{ $flight->arr_airport_id }}"/>
</div>
<div class="col-sm-4">
<label for="type">Aircraft</label>
<select id="type" name="type" class="custom-select select2">
<option value="a320">A320</option>
<option value="b738">B738</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="col-4">
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;Briefing Options
</h6>
<table class="table table-hover table-striped">
<tr>
<td>Units:</td>
<td><select name="units">
<option value="KGS">KGS</option>
<option value="LBS" selected>LBS</option>
</select></td>
</tr>
<tr>
<td>Cont Fuel:</td>
<td><select name="contpct">
<option value="auto" selected>AUTO</option>
<option value="0">0 PCT</option>
<option value="0.02">2 PCT</option>
<option value="0.03">3 PCT</option>
<option value="0.05">5 PCT</option>
<option value="0.1">10 PCT</option>
<option value="0.15">15 PCT</option>
<option value="0.2">20 PCT</option>
</select></td>
</tr>
<tr>
<td>Reserve Fuel:</td>
<td><select name="resvrule">
<option value="auto">AUTO</option>
<option value="0">0 MIN</option>
<option value="15">15 MIN</option>
<option value="30">30 MIN</option>
<option value="45" selected>45 MIN</option>
<option value="60">60 MIN</option>
<option value="75">75 MIN</option>
<option value="90">90 MIN</option>
</select></td>
</tr>
<tr>
<td>Detailed Navlog:</td>
<td><input type="hidden" name="navlog" value="0"><input type="checkbox" name="navlog" value="1"
checked>
</td>
</tr>
<tr>
<td>ETOPS Planning:</td>
<td><input type="hidden" name="etops" value="0"><input type="checkbox" name="etops" value="1"></td>
</tr>
<tr>
<td>Plan Stepclimbs:</td>
<td><input type="hidden" name="stepclimbs" value="0"><input type="checkbox" name="stepclimbs"
value="1"
checked>
</td>
</tr>
<tr>
<td>Runway Analysis:</td>
<td><input type="hidden" name="tlr" value="0"><input type="checkbox" name="tlr" value="1" checked>
</td>
</tr>
<tr>
<td>Include NOTAMS:</td>
<td><input type="hidden" name="notams" value="0"><input type="checkbox" name="notams" value="1"
checked>
</td>
</tr>
<tr>
<td>FIR NOTAMS:</td>
<td><input type="hidden" name="firnot" value="0"><input type="checkbox" name="firnot" value="1"></td>
</tr>
<tr>
<td>Flight Maps:</td>
<td><select name="maps">
<option value="detail">Detailed</option>
<option value="simple">Simple</option>
<option value="none">None</option>
</select></td>
</tr>
</table>
</div>
</div>
</div>
<input type="hidden" name="airline" value="{{ $flight->airline->icao }}">
<input type="hidden" name="fltnum" value="{{ $flight->flight_number }}">
<input type="hidden" name="date" value="01JAN14">
<input type="hidden" name="deph" value="12">
<input type="hidden" name="depm" value="30">
<input type="hidden" name="steh" value="2">
<input type="hidden" name="stem" value="15">
{{--<input type="hidden" name="reg" value="N123SB">
<input type="hidden" name="selcal" value="GR-FS">--}}
<input type="hidden" name="planformat" value="lido">
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="float-right">
<div class="form-group">
<input type="button"
onclick="simbriefsubmit('{{ $flight->id }}', '{{ url(route('frontend.simbrief.briefing', [''])) }}');"
class="btn btn-outline-primary"
value="Generate">
</div>
</div>
</div>
</div>
</form>
@endsection
@section('scripts')
<script src="{{public_asset('/assets/global/js/simbrief.apiv1.js')}}"></script>
@endsection

View File

@@ -70,8 +70,15 @@
</div>
<div class="row">
<div class="col-sm-12 text-right">
@if ($simbrief !== false)
<a href="{{ route('frontend.simbrief.generate') }}?flight_id={{ $flight->id }}"
class="btn btn-sm btn-outline-primary">
Create SimBrief Flight Plan
</a>
@endif
<a href="{{ route('frontend.pireps.create') }}?flight_id={{ $flight->id }}"
class="btn btn-sm btn-info">
class="btn btn-sm btn-outline-info">
{{ __('pireps.newpirep') }}
</a>
</div>

View File

@@ -7,7 +7,6 @@
<h2>@lang('pireps.newflightreport')</h2>
@include('flash::message')
@if(!empty($pirep))
wee
{{ Form::model($pirep, ['route' => 'frontend.pireps.store']) }}
@else
{{ Form::open(['route' => 'frontend.pireps.store']) }}

View File

@@ -303,22 +303,24 @@ flight reports that have been filed. You've been warned!
&nbsp;{{ trans_choice('common.field', 2) }}
</h6>
<div class="form-container-body">
@if(isset($pirep) && $pirep->fields)
@each('pireps.custom_fields', $pirep->fields, 'field')
@else
@each('pireps.custom_fields', $pirep_fields, 'field')
@endif
<table class="table table-striped">
@if(isset($pirep) && $pirep->fields)
@each('pireps.custom_fields', $pirep->fields, 'field')
@else
@each('pireps.custom_fields', $pirep_fields, 'field')
@endif
</table>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="float-right">
<div class="form-group">
{{ Form::hidden('flight_id') }}
{{ Form::hidden('sb_id', $simbrief_id) }}
@if(isset($pirep) && !$pirep->read_only)
{{ Form::button(__('pireps.deletepirep'), [

View File

@@ -5,7 +5,7 @@
<div class="row">
<div class="col-md-12">
<div style="float:right;">
<a class="btn btn-info pull-right btn-lg"
<a class="btn btn-outline-info pull-right btn-lg"
style="margin-top: -10px;margin-bottom: 5px"
href="{{ route('frontend.pireps.create') }}">@lang('pireps.filenewpirep')</a>
</div>

View File

@@ -3,12 +3,40 @@
@section('content')
<div class="row">
<div class="col-8">
<div class="row">
<div class="col-12">
<h2 style="margin-bottom: 5px;">{{$pirep->airline->code}}{{ $pirep->ident }}</h2>
<div class="col-sm-8">
<h2>{{ $pirep->airline->icao }}{{ $pirep->ident }}
: {{ $pirep->dpt_airport_id }} to {{ $pirep->arr_airport_id }}</h2>
</div>
<div class="col-sm-4">
{{-- Show the link to edit if it can be edited --}}
@if (!empty($pirep->simbrief))
<a href="{{ url(route('frontend.simbrief.briefing', [$pirep->simbrief->id])) }}"
class="btn btn-outline-info">View SimBrief</a>
@endif
@if(!$pirep->read_only)
<div class="float-right" style="margin-bottom: 10px;">
<form method="get"
action="{{ route('frontend.pireps.edit', $pirep->id) }}"
style="display: inline">
@csrf
<button class="btn btn-outline-info">@lang('common.edit')</button>
</form>
&nbsp;
<form method="post"
action="{{ route('frontend.pireps.submit', $pirep->id) }}"
style="display: inline">
@csrf
<button class="btn btn-outline-success">@lang('common.submit')</button>
</form>
</div>
</div>
@endif
</div>
</div>
<div class="row">
<div class="col-8">
<div class="row">
{{--
DEPARTURE INFO
@@ -70,29 +98,7 @@
--}}
<div class="col-4">
<h2>&nbsp;</h2>
{{-- Show the link to edit if it can be edited --}}
@if(!$pirep->read_only)
<div class="float-right" style="margin-bottom: 10px;">
<form method="get"
action="{{ route('frontend.pireps.edit', $pirep->id) }}"
style="display: inline">
@csrf
<button class="btn btn-info">@lang('common.edit')</button>
</form>
&nbsp;
<form method="post"
action="{{ route('frontend.pireps.submit', $pirep->id) }}"
style="display: inline">
@csrf
<button class="btn btn-success">@lang('common.submit')</button>
</form>
</div>
@endif
<table class="table table-striped">
<tr>
<td width="30%">@lang('common.state')</td>
<td>
@@ -102,6 +108,7 @@
</td>
</tr>
@if ($pirep->state !== PirepState::DRAFT)
<tr>
<td width="30%">@lang('common.status')</td>
<td>
@@ -110,6 +117,7 @@
</div>
</td>
</tr>
@endif
<tr>
<td>@lang('pireps.source')</td>
@@ -217,5 +225,22 @@
</div>
</div>
@endif
@endsection
@if(!empty($pirep->simbrief))
<div class="separator"></div>
<div class="row mt-5">
<div class="col-12">
<div class="form-container">
<h6><i class="fas fa-info-circle"></i>
&nbsp;OFP
</h6>
<div class="form-container-body border border-dark">
<div class="overflow-auto" style="height: 600px;">
{!! $pirep->simbrief->xml->text->plan_html !!}
</div>
</div>
</div>
</div>
</div>
@endif
@endsection

View File

@@ -39,16 +39,17 @@
@minutestotime($pirep->flight_time)
</td>
<td class="text-center">
@if($pirep->state === PirepState::PENDING)
<div class="badge badge-warning">
@elseif($pirep->state === PirepState::ACCEPTED)
<div class="badge badge-success">
@elseif($pirep->state === PirepState::REJECTED)
<div class="badge badge-danger">
@else
<div class="badge badge-info">
@endif
{{ PirepState::label($pirep->state) }}</div>
@php
$color = 'badge-info';
if($pirep->state === PirepState::PENDING) {
$color = 'badge-warning';
} elseif ($pirep->state === PirepState::ACCEPTED) {
$color = 'badge-success';
} elseif ($pirep->state === PirepState::REJECTED) {
$color = 'badge-danger';
}
@endphp
<div class="badge {{ $color }}">{{ PirepState::label($pirep->state) }}</div>
</td>
<td>
@if(filled($pirep->submitted_at))
@@ -58,7 +59,7 @@
<td>
@if(!$pirep->read_only)
<a href="{{ route('frontend.pireps.edit', [$pirep->id]) }}"
class="btn btn-info btn-sm"
class="btn btn-outline-info btn-sm"
style="z-index: 9999"
title="@lang('common.edit')">
@lang('common.edit')

View File

@@ -3,10 +3,6 @@
use App\Repositories\SettingRepository;
use App\Services\AirportService;
use App\Support\Metar;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
/**
* Test the parsing/support class of the metar
@@ -21,26 +17,6 @@ class MetarTest extends TestCase
$this->settingsRepo = app(SettingRepository::class);
}
/**
* @param string $filename
*/
private function mockXmlResponse($filename)
{
$mock = new MockHandler([
new Response(
200,
[
'Content-Type' => 'text/xml',
],
$this->readDataFile($filename)
),
]);
$handler = HandlerStack::create($mock);
$guzzleClient = new Client(['handler' => $handler]);
app()->instance(Client::class, $guzzleClient);
}
/**
* Make sure a blank metar doesn't give problems
*/

166
tests/SimBriefTest.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
use App\Models\Acars;
use App\Models\Enums\AcarsType;
use App\Models\SimBrief;
use App\Services\SimBriefService;
use App\Support\Utils;
use Carbon\Carbon;
class SimBriefTest extends TestCase
{
/**
* Load SimBrief
*
* @param \App\Models\User $user
*
* @return \App\Models\SimBrief
*/
protected function loadSimBrief($user): SimBrief
{
$flight = factory(App\Models\Flight::class)->create([
'dpt_airport_id' => 'OMAA',
'arr_airport_id' => 'OMDB',
]);
$this->mockXmlResponse('briefing.xml');
/** @var SimBriefService $sb */
$sb = app(SimBriefService::class);
return $sb->checkForOfp($user->id, Utils::generateNewId(), $flight->id);
}
/**
* Read from the SimBrief URL
*/
public function testReadSimbrief()
{
$this->user = factory(App\Models\User::class)->create();
$briefing = $this->loadSimBrief($this->user);
$this->assertNotEmpty($briefing->ofp_xml);
$this->assertNotNull($briefing->xml);
// Spot check reading of the files
$files = $briefing->files;
$this->assertEquals(45, $files->count());
$this->assertEquals(
'http://www.simbrief.com/ofp/flightplans/OMAAOMDB_PDF_1584226092.pdf',
$files->firstWhere('name', 'PDF Document')['url']
);
// Spot check reading of images
$images = $briefing->images;
$this->assertEquals(5, $images->count());
$this->assertEquals(
'http://www.simbrief.com/ofp/uads/OMAAOMDB_1584226092_ROUTE.gif',
$images->firstWhere('name', 'Route')['url']
);
$level = $briefing->xml->getFlightLevel();
$this->assertEquals('380', $level);
// Read the flight route
$routeStr = $briefing->xml->getRouteString();
$this->assertEquals(
'DCT BOMUP DCT LOVIM DCT RESIG DCT NODVI DCT OBMUK DCT LORID DCT '.
'ORGUR DCT PEBUS DCT EMOPO DCT LOTUK DCT LAGTA DCT LOVOL DCT',
$routeStr
);
}
/**
* Check that the API calls are working (simbrief in the response, can retrieve the briefing)
*/
public function testApiCalls()
{
$this->user = factory(App\Models\User::class)->create();
$briefing = $this->loadSimBrief($this->user);
// Check the flight API response
$response = $this->get('/api/flights/'.$briefing->flight_id);
$response->assertOk();
$flight = $response->json('data');
$this->assertNotNull($flight['simbrief']);
$this->assertEquals($briefing->id, $flight['simbrief']['id']);
$this->assertEquals(
'http://localhost/api/flights/'.$briefing->id.'/briefing',
$flight['simbrief']['url']
);
// Retrieve the briefing via API, and then check the doctype
$response = $this->get('/api/flights/'.$briefing->flight_id.'/briefing');
$response->assertOk();
// $response->assertHeader('Content-Type', 'application/xml');
$xml = simplexml_load_string($response->content());
$this->assertNotNull($xml);
$this->assertEquals('VMSAcars', $xml->getName());
$this->assertEquals('FlightPlan', $xml->attributes()->Type);
}
public function testAttachToPirep()
{
$user = factory(App\Models\User::class)->create();
$pirep = factory(App\Models\Pirep::class)->create([
'user_id' => $user->id,
'dpt_airport_id' => 'OMAA',
'arr_airport_id' => 'OMDB',
]);
$briefing = $this->loadSimBrief($user);
/** @var SimBriefService $sb */
$sb = app(SimBriefService::class);
$sb->attachSimbriefToPirep($pirep, $briefing);
/*
* Checks - ACARS entries for the route are loaded
*/
$acars = Acars::where(['pirep_id' => $pirep->id, 'type' => AcarsType::ROUTE])->get();
$this->assertEquals(12, $acars->count());
$fix = $acars->firstWhere('name', 'BOMUP');
$this->assertEquals($fix['name'], 'BOMUP');
$this->assertEquals($fix['lat'], 24.484639);
$this->assertEquals($fix['lon'], 54.578444);
$this->assertEquals($fix['order'], 1);
$briefing->refresh();
$this->assertEmpty($briefing->flight_id);
$this->assertEquals($pirep->id, $briefing->pirep_id);
}
/**
* Test clearing expired briefs
*/
public function testClearExpiredBriefs()
{
$user = factory(App\Models\User::class)->create();
$sb_ignored = factory(SimBrief::class)->create([
'user_id' => $user->id,
'flight_id' => 'a_flight_id',
'pirep_id' => 'a_pirep_id',
'created_at' => Carbon::now('UTC')->subDays(6)->toDateTimeString(),
]);
factory(SimBrief::class)->create([
'user_id' => $user->id,
'flight_id' => 'a_flight_Id',
'pirep_id' => '',
'created_at' => Carbon::now('UTC')->subDays(6)->toDateTimeString(),
]);
/** @var SimBriefService $sb */
$sb = app(SimBriefService::class);
$sb->removeExpiredEntries();
$all_briefs = SimBrief::all();
$this->assertEquals(1, $all_briefs->count());
$this->assertEquals($sb_ignored->id, $all_briefs[0]->id);
}
}

View File

@@ -125,6 +125,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return file_get_contents($p);
}
}
return false;
}
/**
@@ -149,6 +151,26 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
app()->instance(Client::class, $guzzleClient);
}
/**
* @param string $filename
*/
public function mockXmlResponse($filename)
{
$mock = new MockHandler([
new Response(
200,
[
'Content-Type' => 'text/xml',
],
$this->readDataFile($filename)
),
]);
$handler = HandlerStack::create($mock);
$guzzleClient = new Client(['handler' => $handler]);
app()->instance(Client::class, $guzzleClient);
}
/**
* Update a setting
*

4590
tests/data/briefing.xml Normal file

File diff suppressed because it is too large Load Diff