<?php

require_once __DIR__ . '/../time.php';
require_once __DIR__ . '/../model.php';
require_once __DIR__ . '/../../view/debug.php';
require_once __DIR__ . '/common.php';
require __DIR__ . '/../lib/HaconDemoHAFASAPI.php';
require_once __DIR__ . '/../../locations/locations.php';
require_once __DIR__ . '/helpers/gb.nr/location-estimate.php';

class GB_HAFAS extends ProviderCommon implements RealTimeProvider
{
    protected static $HOME_REALM = 'gb.nr';

    public static function SourceName()
    {
        return "HAFAS";
    }

    public static function HomeRealm()
    {
        return self::$HOME_REALM;
    }

    public static function SupportedRealms()
    {
        return [self::$HOME_REALM];
    }

    public static function Capabilities()
    {
        return new InformationProviderCapabilities(CAPABILITY_PROVIDES_TIMETABLED |
            CAPABILITY_MAY_PROVIDE_REAL_TIME |
            CAPABILITY_SUPPORTS_JOURNEY_PLANNING);
    }

    protected static $HAFAS_ACCESS_ID = 'd04bd793-93d9-23b8-2654-e8b9a75d89d6';

    protected function GetImplementation()
    {
        return new HaconDemoHAFASAPI(self::$HAFAS_ACCESS_ID);
    }

    public function ArrivalOrDepartureBoard(string $loc, string $filterLoc, bool $isArrivals, DateTime $at, array $searchOptions = null)
    {
        return $this->ArrivalOrDepartureBoardImpl(true, $loc, $filterLoc, $isArrivals, $at, $searchOptions);
    }

    public function ServiceDetails(string $serviceId, string $locOfInterest = null, DateTime $atLocOfInterest = null, array $searchOptions = null)
    {
        return $this->ServiceDetailsImpl(true, $serviceId, $locOfInterest, $atLocOfInterest, $searchOptions);
    }

    public function ConnectionsBoard(string $loc, string $serviceId = null, DateTime $scheduledArrival = null, int $arrivalBoardIndex = null, array $searchOptions = null)
    {
        global $GB_TIMEZONE;

        if (isset($serviceId)) {
            $arrivalResult = $this->ServiceDetailsImpl(false, $serviceId, $loc, $scheduledArrival);
            if ($arrivalResult === FALSE)
                return FALSE;

            if (isset($arrivalResult->stopIndexOfInterest, $arrivalResult->locations[$arrivalResult->stopIndexOfInterest]->arrival->scheduled)) {
                $arrival = $arrivalResult->locations[$arrivalResult->stopIndexOfInterest]->arrival;  // FIXME This nonsense is b/c ServiceDetails* returns a Service, not ServiceAtStop
                $earliestDeparture = clone $arrival->scheduled;
                $earliestDeparture->add(new DateInterval('PT' . Locations::get_min_change_time_for_loc($loc) . 'M'));
                if (in_num_minutes($earliestDeparture) >= -30) {
                    foreach (array_slice($arrivalResult->locations, $arrivalBoardIndex, $arrivalResult->stopIndexOfInterest - $arrivalBoardIndex) as $stopToAvoid) {
                        if ($stopToAvoid instanceof CommercialStopLocation && !$stopToAvoid->isCancelled) {
                            $alreadyVisitedLocs[] = $stopToAvoid->genericId;
                        }
                    }
                }
            }
        } else {
            $arrivalResult = null;
            if (!isset($scheduledArrival))
                $scheduledArrival = new DateTime("now", $GB_TIMEZONE);
            $earliestDeparture = $scheduledArrival;
        }

        $departures = $this->DepartureBoardForConnections($loc, $earliestDeparture, $searchOptions);
        if ($departures === FALSE)
            return FALSE;

        if (isset($alreadyVisitedLocs)) {
            $departures = self::remove_services_visiting_avoided_location($departures, $alreadyVisitedLocs, $arrivalResult->genericId);
        }

        $departures = self::remove_terminating_services($departures, $loc);

        $departures = self::add_associated_locations($departures, $loc, $scheduledArrival, $earliestDeparture, $searchOptions);

        $departures = self::trim_later_departures($departures, $earliestDeparture);

        if (isset($arrival->realTime)) {
            $departures = self::decorate_with_connection_confidence($departures, $arrival->realTime);
        }

        $result = new ConnectionsBoard(self::Capabilities(), Locations::GetLocationName($loc), $arrivalResult, $departures);

        debugIfTestpilot($result, 'Normalised response');

        return $result;
    }

    private function ArrivalOrDepartureBoardImpl(bool $trace, string $loc, string $filterLoc, bool $isArrivals, DateTime $at, array $searchOptions = null)
    {
        $OpenLDBSVWS = $this->GetImplementation($loc);

        $providerLoc = Director::provider_id_for($loc);
        $providerFilterLoc = isset($filterLoc) ? Director::provider_id_for($filterLoc) : '';
        $incNotInService = (isset($searchOptions) && array_search(SEARCH_OPTION_SHOW_ECS, $searchOptions) !== false) ? 'true' : '';

        if (!array_search($providerLoc, self::$ARRDEP_NOT_RECOMMENDED_LOCATIONS)) {
            $response = $OpenLDBSVWS->GetArrDepBoardWithDetails(30, $providerLoc, $at->format('Y-m-d\TH:i:s'), 120, $providerFilterLoc, $isArrivals ? 'from' : 'to', '', 'PBS', $incNotInService);
            if (!isset($response->GetBoardWithDetailsResult)) {
                return FALSE;
            }

            $result = self::normalise_board_response($response->GetBoardWithDetailsResult, $loc, $filterLoc, $searchOptions);
        } else {
            if ($isArrivals) {
                $response = $OpenLDBSVWS->GetArrBoardWithDetails(30, $providerLoc, $at->format('Y-m-d\TH:i:s'), 120, $providerFilterLoc, 'from', '', 'PBS', $incNotInService);
            } else {
                $response = $OpenLDBSVWS->GetDepBoardWithDetails(30, $providerLoc, $at->format('Y-m-d\TH:i:s'), 120, $providerFilterLoc, 'to', '', 'PBS', $incNotInService);
            }
            if (!isset($response->GetBoardWithDetailsResult)) {
                return FALSE;
            }
            $result = self::normalise_board_response($response->GetBoardWithDetailsResult, $loc, $filterLoc, $searchOptions);
        }

        if ($trace) {
            debugIfTestpilot($response, 'Response');
            debugIfTestpilot($result, 'Normalised response');
        }

        return $result;
    }

    private function DepartureBoardForConnections(string $loc, DateTime $at, array $searchOptions = null)
    {
        $OpenLDBSVWS = $this->GetImplementation($loc);

        $providerLoc = Director::provider_id_for($loc);

        $response = $OpenLDBSVWS->GetDepBoardWithDetails(30, $providerLoc, $at->format('Y-m-d\TH:i:s'), 120, '', 'to', '', 'PBS', false);

        $result = self::normalise_board_response($response->GetBoardWithDetailsResult, $loc, null, $searchOptions);

        debugIfTestpilot($response, 'Response');
        debugIfTestpilot($result, 'Normalised response');

        return $result;
    }

    private function ServiceDetailsImpl(bool $trace, string $serviceId, string $locOfInterest = null, DateTime $atLocOfInterest = null, array $searchOptions = null)
    {
        $OpenLDBSVWS = $this->GetImplementation($serviceId);

        $rid = Director::provider_id_for($serviceId);
        $response = $OpenLDBSVWS->GetServiceDetailsByRID($rid);
        if (!isset($response->GetServiceDetailsResult)) {
            return FALSE;
        }
        $result = self::normalise_service($response->GetServiceDetailsResult, $response->GetServiceDetailsResult, $locOfInterest, $atLocOfInterest);

        if ($trace) {
            debugIfTestpilot($response, 'Response');
            debugIfTestpilot($result, 'Normalised response');
        }

        return $result;
    }

    protected static function normalise_board_response($boardResponse, $genericLoc, $filterLoc, $searchOptions = null)
    {
        $genericResponse = new ArrDepBoardAtStop(self::Capabilities());

        $services = array();

        $genericResponse->genericId = $genericLoc;
        $genericResponse->messages = self::normalise_alert_messages($boardResponse);

        if (isset($boardResponse->trainServices)) {
            $boardResponse->trainServices->service = ensure_is_array($boardResponse->trainServices->service);
            $services = array_merge($services, $boardResponse->trainServices->service);
        }

        if (isset($boardResponse->busServices)) {
            $boardResponse->busServices->service = ensure_is_array($boardResponse->busServices->service);
            $services = array_merge($services, $boardResponse->busServices->service);
        }

        $services = self::normalise_service_list($services, $boardResponse, $genericLoc);
        $services = sort_board_entries_by_time($services);

        $genericResponse->services = $services;
        $genericResponse->locationName = Locations::GetLocationName($genericLoc);

        if (isset($filterLoc) && $filterLoc !== '') {
            $genericResponse->filterLoc = $filterLoc;
            $genericResponse->filterLocationName = Locations::GetLocationName($filterLoc);
        }

        return $genericResponse;
    }

    protected static function normalise_service_list(array $services, $response, $genericLoc)
    {
        $genericServiceList = [];
        foreach ($services as $service) {
            $genericServiceList[] = self::normalise_service($service, $response, $genericLoc);
        }
        return $genericServiceList;
    }

    protected static function normalise_service($service, $response, string $locOfInterest = null, DateTime $timeAtLocOfInterest = null)
    {
        global $GB_TIMEZONE;

        $genericService = isset($locOfInterest) ? new ServiceAtStop(self::Capabilities()) : new Service(self::Capabilities());

        // ids
        $genericService->providerId['uid'] = $service->uid;
        $genericService->providerId['rid'] = $service->rid;
        $genericService->providerId['trn'] = $service->trainid;
        if (isset($service->rsid)) {
            $genericService->providerId['rsid'] = $service->rsid;
        }
        $genericService->genericId = Director::generic_id_for(self::$HOME_REALM, 'rid', $service->rid);

        // starting om
        $genericService->scheduledDepartureDateFromOrigin = new DateTime($service->sdd, $GB_TIMEZONE);
        // what is it?
        switch ($service->category) {
            case 'BS':
                $genericService->serviceMode = SERVICE_MODE_BUS;
                break;
            case 'BR':
                $genericService->serviceMode = SERVICE_MODE_BUS_REPLACING_TRAIN;
                break;
            default:
                $genericService->serviceMode = SERVICE_MODE_TRAIN;
                break;
        }

        if (isset($service->length)) {
            $genericService->length = $service->length;
        }

        $genericService->operatorName = $service->operator;
        $genericService->operatorCode = $service->operatorCode;
        if ($genericService->operatorCode === 'ES') {
            $genericService->minCheckIn = 45;
        }

        // Is it an express?
        if ($service->category[0] === 'X') {
            $genericService->isExpress = true;
        }

        // Is it a sleeper?
        if ($service->category === 'XZ') {
            $genericService->isSleeper = true;
        }

        $genericService->isInService = !isset($service->isPassengerService);

        if (isset($service->origin)) {
            $service->origin->location = ensure_is_array($service->origin->location);
            foreach ($service->origin->location as $origin) {
                $genericService->origin[] = self::normalise_service_start_or_end_location($origin);
            }
        }

        if (isset($service->destination)) {
            $service->destination->location = ensure_is_array($service->destination->location);
            foreach ($service->destination->location as $destination) {
                $genericService->destination[] = self::normalise_service_start_or_end_location($destination);
            }
        }

        if (isset($service->currentOrigins)) {
            $service->currentOrigins->location = ensure_is_array($service->currentOrigins->location);
            foreach ($service->currentOrigins->location as $currentOrigin) {
                $genericService->modifiedOrigin[] = self::normalise_service_start_or_end_location($currentOrigin);
            }
        }

        if (isset($service->currentDestinations)) {
            $service->currentDestinations->location = ensure_is_array($service->currentDestinations->location);
            foreach ($service->currentDestinations->location as $currentDestination) {
                $genericService->modifiedDestination[] = self::normalise_service_start_or_end_location($currentDestination);
            }
        }

        $genericService->locations = [];
        if (isset($service->locations)) {
            $genericService->previousLocationsKnown = true;
            $genericService->subsequentLocationsKnown = true;
            foreach (self::normalise_service_locations($service->locations, $service) as $location) {
                $genericService->locations[] = $location;
                if ($locOfInterest !== null && $locOfInterest === $location->genericId) {
                    if (!isset($timeAtLocOfInterest) || (isset($location->arrival) && $timeAtLocOfInterest == $location->arrival->scheduled || isset($location->departure) && $timeAtLocOfInterest == $location->departure->scheduled)) {
                        $genericService->stopIndexOfInterest = count($genericService->locations) - 1;   // Mark this one
                    }
                }
            }
        } else {
            if (isset($service->previousLocations)) {
                $genericService->previousLocationsKnown = true;
                foreach (self::normalise_service_locations($service->previousLocations, $service) as $previousLocation) {
                    $genericService->locations[] = $previousLocation;
                }
            }

            $genericService->locations[] = self::normalise_service_location($service, $service, $response);
            $genericService->stopIndexOfInterest = count($genericService->locations) - 1;

            if (isset($service->subsequentLocations)) {
                $genericService->subsequentLocationsKnown = true;
                foreach (self::normalise_service_locations($service->subsequentLocations, $service) as $subsequentLocation) {
                    $genericService->locations[] = $subsequentLocation;
                }
            }
        }

        if ($genericService->locations[0]->genericId === $genericService->locations[count($genericService->locations) - 1]->genericId) {
            $genericService->isCircular = true;
        }

        // Estimate of where it is based upon this stop where anything is actual...
        $genericService = set_location_estimate($genericService);

        if (isset($service->cancelReason)) {
            $cancellation = new Problem;
            $cancellation->supplierSpecificCode = $service->cancelReason->_;
            if (!empty($service->cancelReason->near)) {
                $cancellation->problemLocation = $service->cancelReason->near;
            }
            $cancellation->reason = 'Cancelled due to ' . self::$NRE_DELAY_AND_CANCELLATION_REASONS[$service->cancelReason->_];
            if (isset($cancellation->problemLocation)) {
                $cancellation->reason = $cancellation->reason . ' near ' . $cancellation->problemLocation;
            }
            $genericService->cancellationReason = $cancellation;
        }

        if (isset($service->delayReason)) {
            $delay = new Problem;
            $delay->supplierSpecificCode = $service->delayReason->_;
            if (!empty($service->delayReason->near)) {
                $delay->problemLocation = $service->delayReason->near;
            }
            $delay->reason = 'Delayed due to ' . self::$NRE_DELAY_AND_CANCELLATION_REASONS[$service->delayReason->_];
            if (isset($delay->problemLocation)) {
                $delay->reason = $delay->reason . ' near ' . $delay->problemLocation;
            }
            $genericService->delayReason = $delay;
        }

        if (isset($service->formation->coaches)) {
            $genericService->formation = self::normalise_formation($service->formation);
        } else if (!isset($genericService->stopIndexOfInterest) && isset($service->formation->fmloc[0]->coaches)) {
            $genericService->formation = self::normalise_formation($service->formation->fmloc[0]);
        } else if ($genericService instanceof ServiceAtStop && isset($genericService->AtThisLocation()->formation)) {
            // Copy from stop to service
            $genericService->formation = $genericService->AtThisLocation()->formation;
        }

        // Dodgy formation? Sometimes train length is different from formation's vehicles count. Length is believable, so remove formation
        if (isset($genericService->formation) && isset($genericService->length)) {
            if (count($genericService->formation->vehicles) !== $genericService->length) {
                unset($genericService->formation);
            }
        }

        if (isset($genericService->formation)) {
            foreach ($genericService->formation->vehicles as $vehicle) {
                if (isset($vehicle->realTimeLoading)) {
                    $isFull = true;
                    if ($vehicle->realTimeLoading < 85) {   // Arbitrary, >= 85% seats means full
                        $isFull = false;                    // One veh not full -> train not full
                        break;
                    }
                }
            }
            if (isset($isFull)) {
                $genericService->reportedFull = $isFull;
            }
        }

        $genericService->source = self::SourceName();

        return $genericService;
    }

    protected static function normalise_formation($formation)
    {
        $genericFormation = new Formation();
        if (isset($formation->coaches->coach)) {
            foreach ($formation->coaches->coach as $coach) {
                $genericVehicle = new Vehicle();

                $genericVehicle->identity = $coach->number;
                $genericVehicle->comfortName = $coach->coachClass;
                if ($genericVehicle->comfortName === 'First') {
                    $genericVehicle->comfortShortName = '1st';
                } else if ($genericVehicle->comfortName === 'Mixed') {
                    $genericVehicle->comfortShortName = '1st|Std';
                }
                if (isset($coach->toilet) && $coach->toilet->_ !== 'None') {
                    $genericVehicle->hasToilet = true;
                    $genericVehicle->isUniversalToilet = $coach->toilet->_ === 'Accessible';
                    if ($coach->toilet->status !== 'Unknown') {
                        $genericVehicle->toiletKnownInService = $coach->toilet->status === 'InService';
                        $genericVehicle->toiletKnownOutOfService = $coach->toilet->status === 'NotInService';
                    }
                } else {
                    $genericVehicle->hasToilet = false;
                }

                if (isset($coach->loading)) {
                    $genericVehicle->realTimeLoading = $coach->loading->_;
                }

                $genericFormation->vehicles[] = $genericVehicle;
            }
        }

        if (isset($formation->avgLoading)) {
            $genericFormation->averageLoading = $formation->avgLoading;
        }

        return $genericFormation;
    }

    protected static function normalise_service_start_or_end_location($location)
    {
        $genericLocation = new ServiceStartOrEndLocation;

        if (isset($location->crs)) {
            $genericLocation->name = self::display_name($location->crs, $location->locationName);
            $genericLocation->providerId['crs'] = $location->crs;
            $genericLocation->genericId = Director::generic_id_for(self::$HOME_REALM, 'crs', $location->crs);
            if (isset($location->via)) {
                $genericLocation->via = $location->via;
            }
        } else {
            $genericLocation->name = $location->tiploc;
        }

        return $genericLocation;
    }

    protected static function normalise_service_locations($callingPoints, $service, $response = null)
    {
        $genericCallingPoints = array();

        $callingPoints->location = ensure_is_array($callingPoints->location);
        foreach ($callingPoints->location as $callingPoint) :
            if (isset($callingPoint->crs) || trim($callingPoint->tiploc) !== $callingPoint->locationName) {
                $genericCallingPoints[] = self::normalise_service_location($callingPoint, $service, $response ?? $callingPoint);
            }
        endforeach;

        return $genericCallingPoints;
    }

    protected static function normalise_service_location($objCarryingCallingPointInfo, $objCarryingService, $objCarryingLocation)
    {
        if (isset($objCarryingCallingPointInfo->isPass)) {
            $genericCallingPoint = new PassLocation;
        } else {
            if (isset($objCarryingCallingPointInfo->isOperational)) {
                $genericCallingPoint = new OperationalStopLocation;
            } else {
                $genericCallingPoint = new CommercialStopLocation;
            }
        }

        $genericCallingPoint->name = isset($objCarryingLocation->crs) ? self::display_name($objCarryingLocation->crs, $objCarryingLocation->locationName) : $objCarryingLocation->locationName;
        if (isset($objCarryingLocation->crs)) {
            $genericCallingPoint->providerId['crs'] = $objCarryingLocation->crs;
            $genericCallingPoint->genericId = Director::generic_id_for(self::$HOME_REALM, 'crs', $objCarryingLocation->crs);
        } else {
            $genericCallingPoint->genericId = Director::generic_id_for(self::$HOME_REALM, 'tiploc', $objCarryingLocation->tiploc);
        }

        $genericCallingPoint->isCancelled = isset($objCarryingLocation->isCancelled) || isset($objCarryingService->isCancelled);

        if (isset($objCarryingCallingPointInfo->platform)) {
            $genericCallingPoint->platformOrStop = $objCarryingCallingPointInfo->platform;
            $genericCallingPoint->subsequentDeparturesInfo = null;
        }

        if (isset($objCarryingCallingPointInfo->activities)) {
            switch (substr($objCarryingCallingPointInfo->activities, 0, 2)) {
                case 'R ':
                    $genericCallingPoint->platformOrStopSpecial = STOP_TYPE_RAILWAY_REQUEST;
                    break;
                case 'D ':
                    $genericCallingPoint->platformOrStopSpecial = STOP_TYPE_SET_DOWN_ONLY;
                    break;
                case 'N ':
                    $genericCallingPoint->platformOrStopSpecial = STOP_TYPE_UNADVERTISED;
                    break;
                case 'U ':
                    $genericCallingPoint->platformOrStopSpecial = STOP_TYPE_PICK_UP_ONLY;
                    break;
                case 'TB':
                    $genericCallingPoint->platformOrStopSpecial = STOP_TYPE_ORIGIN;
                    break;
                case 'TF':
                    $genericCallingPoint->platformOrStopSpecial = STOP_TYPE_TERMINUS;
                    break;
                default:
                    $genericCallingPoint->platformOrStopSpecial = STOP_TYPE_INTERMEDIATE;
            }
        } else {
            $genericCallingPoint->platformOrStopSpecial = STOP_TYPE_INTERMEDIATE;
        }

        if (isset($objCarryingCallingPointInfo->platformIsHidden)) {
            $genericCallingPoint->platformOrStopState = PLATFORM_OR_STOP_SECRET;
        } else {
            $genericCallingPoint->platformOrStopState = PLATFORM_OR_STOP_REAL_TIME;
        }


        if (isset($objCarryingCallingPointInfo->isPass)) {
            $genericCallingPoint->pass = self::normalise_arrival_departure_or_pass($objCarryingCallingPointInfo, $objCarryingService, $genericCallingPoint, false);
        } else {
            if (isset($objCarryingCallingPointInfo->sta) && (!isset($genericCallingPoint->platformOrStopSpecial) || $genericCallingPoint->platformOrStopSpecial !== STOP_TYPE_ORIGIN)) {
                $genericCallingPoint->arrival = self::normalise_arrival_departure_or_pass($objCarryingCallingPointInfo, $objCarryingService, $genericCallingPoint, true);
            }
            if (isset($objCarryingCallingPointInfo->std) && (!isset($genericCallingPoint->platformOrStopSpecial) || $genericCallingPoint->platformOrStopSpecial !== STOP_TYPE_TERMINUS)) {
                $genericCallingPoint->departure = self::normalise_arrival_departure_or_pass($objCarryingCallingPointInfo, $objCarryingService, $genericCallingPoint, false);
            }
        }

        if (isset($objCarryingCallingPointInfo->lateness, $genericCallingPoint->departure) && $genericCallingPoint->departure->IsConfirmed()) {
            $genericCallingPoint->lateness = $objCarryingCallingPointInfo->lateness;
        }

        if (isset($objCarryingCallingPointInfo->associations)) {
            foreach ($objCarryingCallingPointInfo->associations as $associationList) {
                $associationList = ensure_is_array($associationList);
                foreach ($associationList as $association) {
                    if ($association->category === 'divide' || $association->category === 'LinkTo') {
                        $associatedService = new AssociatedService;
                        $associatedService->associationType = $association->category === 'divide' ? ASSOCIATION_TYPE_SPLIT : ASSOCIATION_TYPE_CONNECTION;
                        $associatedService->otherServiceId['uid'] = $association->uid;
                        $associatedService->otherServiceId['trn'] = $association->trainid;
                        $associatedService->otherServiceId['rid'] = $association->rid;
                        if (isset($association->rsid)) $associatedService->otherServiceId['rsid'] = $association->rsid;
                        $associatedService->otherServiceGenericId = Director::generic_id_for(self::$HOME_REALM, '.rid', $association->rid);
                        $associatedService->otherServiceDistalLoc = new ServiceStartOrEndLocation;
                        $associatedService->otherServiceDistalLoc->name = self::display_name($association->destCRS, $association->destination);
                        $associatedService->otherServiceDistalLoc->providerId['crs'] = $association->destCRS;
                        $associatedService->otherServiceDistalLoc->genericId = Director::generic_id_for(self::$HOME_REALM, 'crs', $association->destCRS);
                        $genericCallingPoint->associations[] = $associatedService;
                    } else if ($association->category === 'join') {
                        $associatedService = new AssociatedService;
                        $associatedService->associationType = ASSOCIATION_TYPE_JOIN;
                        $associatedService->otherServiceId['uid'] = $association->uid;
                        $associatedService->otherServiceId['trn'] = $association->trainid;
                        $associatedService->otherServiceId['rid'] = $association->rid;
                        if (isset($association->rsid)) $associatedService->otherServiceId['rsid'] = $association->rsid;
                        $associatedService->otherServiceGenericId = Director::generic_id_for(self::$HOME_REALM, '.rid', $association->rid);
                        $associatedService->otherServiceDistalLoc = new ServiceStartOrEndLocation;
                        $associatedService->otherServiceDistalLoc->name = self::display_name($association->originCRS, $association->origin);
                        $associatedService->otherServiceDistalLoc->providerId['crs'] = $association->originCRS;
                        $associatedService->otherServiceDistalLoc->genericId = Director::generic_id_for(self::$HOME_REALM, 'crs', $association->originCRS);
                        $genericCallingPoint->associations[] = $associatedService;
                    }
                }
            }
        }

        if (!isset($objCarryingCallingPointInfo->isPass) && isset($objCarryingService->formation->fmloc[0]->coaches)) {
            $formationAtTiplocKey = array_search($objCarryingLocation->tiploc, array_column($objCarryingService->formation->fmloc, 'tiploc'));
            $genericCallingPoint->formation = self::normalise_formation($objCarryingService->formation->fmloc[$formationAtTiplocKey]);
        }

        return $genericCallingPoint;
    }

    protected static function normalise_arrival_departure_or_pass(stdClass $objCarryingCallingPointInfo, stdClass $objectCarryingService, StopLocation $loc, bool $isArrival)
    {
        $tz = Locations::get_timezone_for_loc($loc->genericId);

        if ($isArrival) {
            $st = $objCarryingCallingPointInfo->sta;
            $et = $objCarryingCallingPointInfo->eta ?? null;
            $at = $objCarryingCallingPointInfo->ata ?? null;
            $realTimeType = $objCarryingCallingPointInfo->arrivalType ?? null;
        } else {
            $st = $objCarryingCallingPointInfo->std;
            $et = $objCarryingCallingPointInfo->etd ?? null;
            $at = $objCarryingCallingPointInfo->atd ?? null;
            $realTimeType = $objCarryingCallingPointInfo->departureType ?? null;
        }

        if (isset($st)) {
            $event = new StopMovement;
            $event->scheduled = new DateTime($st, $tz);

            if ($realTimeType === 'Delayed') {
                $event->state = REAL_TIME_UNKNOWN_DELAY;
            } else if ($realTimeType === 'NoLog') {
                $event->state = REAL_TIME_SCHEDULED_ONLY;
            } else {
                if ($realTimeType === 'Actual') {
                    $event->realTime = new DateTime($at, $tz);
                    $event->state = REAL_TIME_EVENT_CONFIRMED;
                } else { // realTimeType is 'Forecast'
                    $event->realTime = new DateTime($et, $tz);
                    if ($event->realTime->getTimestamp() - $event->scheduled->getTimestamp() < DELAY_THRESHOLD_SECONDS) {
                        // On-time is only completely trustworthy for trains. Until RT is reliably present for buses and ships, only show it when a delay is known
                        if ($objectCarryingService->trainid !== "0B00" && $objectCarryingService->trainid !== "0S00") {
                            $event->state = REAL_TIME_EXPECTED_ON_TIME;
                        } else {
                            unset($event->realTime);
                            $event->state = REAL_TIME_SCHEDULED_ONLY;
                        }
                    } else {
                        $event->state = REAL_TIME_KNOWN_DELAY;
                    }
                }
            }

            return $event;
        }

        return false;
    }

    protected static function remove_services_visiting_avoided_location(ArrDepBoardAtStop $response, array $avoidLocs, string $avoidService)
    {
        $avoidLocs = ensure_is_array($avoidLocs);

        $services = $response->services;
        if (isset($services)) {
            foreach ($services as $formationAtTiplocKey => $service) {
                foreach ($avoidLocs as $avoidLoc) {
                    if ($service->VisitsLocationAfterIndex($avoidLoc, $service->stopIndexOfInterest ?? 0) || $service->genericId === $avoidService) {
                        unset($services[$formationAtTiplocKey]);
                        break;
                    }
                }
            }
        }
        $response->services = $services;

        return $response;
    }

    protected static function remove_terminating_services(ArrDepBoardAtStop $response, $loc)
    {
        $services = $response->services;
        if (isset($services)) {
            foreach ($services as $formationAtTiplocKey => $service) {
                foreach ($service->destination as $destination) {
                    if ($destination->genericId === $loc) {
                        unset($services[$formationAtTiplocKey]);
                        break;
                    }
                }
            }
        }
        $response->services = $services;

        return $response;
    }

    protected static function decorate_with_connection_confidence(ArrDepBoardAtStop $response, DateTime $expectedArrival)
    {
        $services = $response->services;

        $minChangeTime = Locations::get_min_change_time_for_loc($response->genericId);

        if (isset($services)) {
            foreach ($services as $service) {
                if ($service instanceof ServiceAtStop && isset($service->AtThisLocation()->departure)) {
                    if (!isset($service->AtThisLocation()->departure->realTime)) {
                        $service->connectionConfidence = CONNECTION_CONFIDENCE_UNKNOWN;
                    } else if ($service->AtThisLocation()->isCancelled) {
                        $service->connectionConfidence = CONNECTION_CONFIDENCE_CANCELLED;
                    } else if ($service->AtThisLocation()->departure->state == REAL_TIME_UNKNOWN_DELAY) {
                        $service->connectionConfidence = CONNECTION_CONFIDENCE_IN_DOUBT;
                    } else {
                        $changeTime = minutes_later($expectedArrival, isset($service->AtThisLocation()->departure->realTime) ? $service->AtThisLocation()->departure->realTime : $service->AtThisLocation()->departure->scheduled);
                        if (isset($service->minCheckIn)) {
                            $changeTime = $changeTime - $service->minCheckIn;
                        }
                        if ($changeTime < 0) {
                            $service->connectionConfidence = CONNECTION_CONFIDENCE_IMPOSSIBLE;
                        } else if ($changeTime < min($minChangeTime, 10)) { // Increase minimum connection to 10 minutes for remote bus stops
                            $service->connectionConfidence = CONNECTION_CONFIDENCE_IN_DOUBT;
                        } else {
                            $service->connectionConfidence = CONNECTION_CONFIDENCE_LIKELY;
                        }
                    }
                }
            }
        }

        $response->services = $services;

        return $response;
    }

    protected static function add_associated_locations(ArrDepBoardAtStop $departures, string $loc, DateTime $scheduledArrival, DateTime $at, $searchOptions)
    {
        $services = $departures->services;

        $otherLocs = Locations::get_peer_locations($loc);
        if ($otherLocs) {
            foreach ($otherLocs as $otherLoc) {
                // Time to platform at other station is half the sum of the interchange times, plus the walk
                $minChangeTime = (int) ((Locations::get_min_change_time_for_loc($loc) + Locations::get_min_change_time_for_loc($otherLoc)) / 2) + Locations::get_walk_duration_minutes($loc, $otherLoc);
                $otherLocDeparture = clone $at;
                $otherLocDeparture->add(new DateInterval('PT' . $minChangeTime . 'M'));

                $railDepartures = Director::DepArrBoard($otherLoc, '', false, $otherLocDeparture, $searchOptions);
                if ($railDepartures) {
                    $railDepartures = self::remove_terminating_services($railDepartures, $loc);
                    foreach ($railDepartures->services as $otherLocService) {
                        $otherLocService->AtThisLocation()->platformOrStop = $railDepartures->locationName . (isset($otherLocService->AtThisLocation()->platformOrStop) ? (', platform ' . $otherLocService->AtThisLocation()->platformOrStop) : '');
                        $otherLocService->AtThisLocation()->platformOrStopAtOtherLocId = $otherLoc;
                        $services[] = $otherLocService;
                    }

                    // Add alert messages from other location
                    // FIXME May have duplicates
                    if (isset($railDepartures->messages)) {
                        foreach ($railDepartures->messages as $message) {
                            $departures->messages[] = $message;
                        }
                    }
                }
            }
        }

        $otherLocs = Locations::get_associated_locations($loc);
        if (count($otherLocs) > 20) { // Don't go mad with my free account :-)
            $otherLocs = array_slice($otherLocs, 0, 20);
        }
        if (isset($otherLocs)) {
            $servicesSeenAlready = [];
            usort($otherLocs, function (array $loc1, array $loc2) { // Sort by distance
                return ($loc1[1] <=> $loc2[1]);
            });
            foreach ($otherLocs as $otherLoc) {
                // Time to bus/tram/etc. stop is half the interchange time, plus the walk
                $minChangeTime = (int) (Locations::get_min_change_time_for_loc($loc) / 2) + Locations::get_walk_duration_minutes_from_distance($otherLoc[1]);
                $otherLocDeparture = clone $at;
                $otherLocDeparture->add(new DateInterval('PT' . $minChangeTime . 'M'));

                $searchOptions[] = SEARCH_OPTION_AVOID_NEXT_BUSES_API;
                $otherDepartures = Director::DepArrBoard($otherLoc[0], '', false, $otherLocDeparture, $searchOptions);

                if (isset($otherDepartures->services)) {
                    foreach ($otherDepartures->services as $index => $otherLocService) {
                        if ((!isset($otherLocService->AtThisLocation()->departure->scheduled) || $otherLocService->AtThisLocation()->departure->scheduled < $scheduledArrival) &&
                            (!isset($otherLocService->AtThisLocation()->departure->realTime) || $otherLocService->AtThisLocation()->departure->realTime < $scheduledArrival)
                        ) {
                            continue;   // Expected to arrive before train, no point in showing it
                        }

                        if (array_search($otherLocService->destination->name, $servicesSeenAlready) !== false) {
                            continue;   // We have already handled departures with this route and destination
                        }

                        // We now have a departure worth showing...  Any more departures?
                        $servicesSeenAlready[] = $otherLocService->destination->name;

                        if ($index < count($otherDepartures->services) - 1) {
                            for ($laterIndex = $index + 1; $laterIndex < count($otherDepartures->services); $laterIndex++) {
                                if (isset($otherDepartures->services[$laterIndex]) && $otherDepartures->services[$laterIndex]->destination->name === $otherLocService->destination->name) {  // Some stops may serve different directions/destinations for one route, this one matches
                                    $otherLocService->AtThisLocation()->subsequentDepartures[] = $otherDepartures->services[$laterIndex]->AtThisLocation()->departure;
                                    unset($otherDepartures->services[$laterIndex]); // Remove from the list
                                }
                            }
                        }

                        $otherLocService->AtThisLocation()->platformOrStopAtOtherLocId = $otherLoc[0];
                        $services[] = $otherLocService;
                    }
                }
            }
        }

        $services = sort_board_entries_by_time($services);

        $departures->services = $services;

        return $departures;
    }

    protected static function trim_later_departures(ArrDepBoardAtStop $departures, DateTime $at)
    {
        $services = $departures->services;

        // Get one hour cut-off
        $cutoffTime = clone $at;
        $cutoffTime->add(new DateInterval(('PT1H')));

        foreach ($services as $index => $service) {
            $time = isset($service->AtThisLocation()->departure) ?
                $service->AtThisLocation()->departure->realTime ?? $service->AtThisLocation()->departure->scheduled :
                $service->AtThisLocation()->arrival->realTime ?? $service->AtThisLocation()->arrival->scheduled;
            if ($time > $cutoffTime) {
                unset($services[$index]);
            }
        }

        $departures->services = $services;

        return $departures;
    }

    protected static function normalise_alert_messages($response)
    {
        global $config;
        $genericMessages = [];

        if (isset($response->nrccMessages)) {
            $response->nrccMessages->message = ensure_is_array($response->nrccMessages->message);
            foreach ($response->nrccMessages->message as $message) {
                if (isset($config['IgnoreMsg'])) {
                    foreach ($config['IgnoreMsg'] as $ignoreText) {
                        if (strpos($message->xhtmlMessage, $ignoreText) !== false) {
                            continue 2; // match found, continue with next message if any
                        }
                    }
                }

                $cleanedMessage = strip_tags($message->xhtmlMessage, '<a>');
                // Is there a URL?
                $url = null;
                $hrefStart = strpos($cleanedMessage, 'href="');
                if ($hrefStart !== false) {
                    $hrefStart = $hrefStart + strlen('href="');
                    $hrefEnd = strpos($cleanedMessage, '"', $hrefStart);
                    if ($hrefEnd !== false) {
                        $url = substr($cleanedMessage, $hrefStart, $hrefEnd - $hrefStart);
                        $lastSentenceEnds = strrpos(substr($cleanedMessage, 0, $hrefStart), '.');
                        if ($lastSentenceEnds !== false) {
                            $cleanedMessage = substr($cleanedMessage, 0, $lastSentenceEnds + 1);
                        }
                    }
                }

                switch ($message->category) {
                    case 'Train service':
                        $category = ALERT_MESSAGE_CATEGORY_SERVICE;
                        break;
                    case 'Station':
                        $category = ALERT_MESSAGE_CATEGORY_STOP;
                        break;
                    default:
                        $category = ALERT_MESSAGE_CATEGORY_OTHER;
                }
                switch ($message->severity) {
                    case 'Major':
                        $severity = ALERT_MESSAGE_SEVERITY_HIGH;
                        break;
                    case 'Minor':
                        $severity = ALERT_MESSAGE_SEVERITY_LOW;
                        break;
                    default:
                        $severity = STREAM_NOTIFY_SEVERITY_INFO;
                }
                $genericMessages[] = new AlertMessage($cleanedMessage, $category, $severity, $url);
            }
        }

        return $genericMessages;
    }

    protected static function display_name(string $crs, string $defaultName)
    {
        return self::$LOC_PREFERRED_NAMES[$crs] ?? $defaultName;
    }

    protected static $NRE_DELAY_AND_CANCELLATION_REASONS = array(    // TODO Get these from the webservice if possible or from Darwin download (via Beagle?)
        '100' => 'a broken down train',
        '101' => 'a delay on a previous journey',
        '102' => 'a derailed train',
        '104' => 'a fire at a station',
        '105' => 'a fire at a station earlier',
        '106' => 'a landslip',
        '107' => 'a line-side fire',
        '108' => 'a member of train crew being unavailable',
        '109' => 'a passenger being taken ill',
        '110' => 'a passenger having been taken ill earlier',
        '111' => 'a person hit by a train',
        '112' => 'a person hit by a train earlier',
        '113' => 'a problem at a level crossing',
        '114' => 'a problem currently under investigation',
        '115' => 'a problem near the railway',
        '116' => 'a problem with a river bridge',
        '117' => 'a problem with line side equipment',
        '118' => 'a security alert',
        '119' => 'a train derailed earlier',
        '120' => 'a train fault',
        '121' => 'a train late from the depot',
        '122' => 'a train late from the depot earlier',
        '123' => 'a trespass incident',
        '124' => 'a vehicle striking a bridge',
        '125' => 'a vehicle striking a bridge earlier',
        '126' => 'an earlier broken down train',
        '128' => 'an earlier landslip',
        '129' => 'an earlier line-side fire',
        '130' => 'an earlier operating incident',
        '131' => 'an earlier problem at a level crossing',
        '132' => 'an earlier problem near the railway',
        '133' => 'an earlier problem with a river bridge',
        '134' => 'an earlier problem with line side equipment',
        '135' => 'an earlier security alert',
        '136' => 'an earlier train fault',
        '137' => 'an earlier trespass incident',
        '138' => 'an obstruction on the line',
        '139' => 'an obstruction on the line earlier',
        '140' => 'an operating incident',
        '141' => 'an unusually large passenger flow',
        '142' => 'an unusually large passenger flow earlier',
        '143' => 'animals on the line',
        '144' => 'animals on the line earlier',
        '145' => 'congestion caused by earlier delays',
        '146' => 'disruptive passengers',
        '147' => 'disruptive passengers earlier',
        '148' => 'earlier electrical supply problems',
        '149' => 'earlier emergency engineering works',
        '150' => 'earlier industrial action',
        '151' => 'earlier overhead wire problems',
        '152' => 'earlier over-running engineering works',
        '153' => 'earlier signalling problems',
        '154' => 'earlier vandalism',
        '155' => 'electrical supply problems',
        '156' => 'emergency engineering works',
        '157' => 'emergency services dealing with a prior incident',
        '158' => 'emergency services dealing with an incident',
        '159' => 'fire alarms sounding at a station',
        '160' => 'fire alarms sounding earlier at a station',
        '161' => 'flooding',
        '162' => 'flooding earlier',
        '163' => 'fog',
        '164' => 'fog earlier',
        '165' => 'high winds',
        '166' => 'high winds earlier',
        '167' => 'industrial action',
        '168' => 'lightning having damaged equipment',
        '169' => 'overhead wire problems',
        '170' => 'over-running engineering works',
        '171' => 'passengers transferring between trains',
        '172' => 'passengers transferring between trains earlier',
        '173' => 'poor rail conditions',
        '174' => 'poor rail conditions earlier',
        '175' => 'poor weather conditions',
        '176' => 'poor weather conditions earlier',
        '177' => 'safety checks being made',
        '178' => 'safety checks having been made earlier',
        '179' => 'signalling problems',
        '180' => 'snow',
        '181' => 'snow earlier',
        '182' => 'speed restrictions having been imposed',
        '183' => 'train crew having been unavailable earlier',
        '184' => 'vandalism',
        '185' => 'waiting earlier for a train crew member',
        '186' => 'waiting for a train crew member',
        '187' => 'engineering works',
        '501' => 'a broken down train',
        '502' => 'a broken windscreen on the train',
        '503' => 'a shortage of trains because of accident damage',
        '504' => 'a shortage of trains because of extra safety inspections',
        '505' => 'a shortage of trains because of vandalism',
        '506' => 'a shortage of trains following damage by snow and ice',
        '507' => 'more trains than usual needing repairs at the same time',
        '508' => 'the train for this service having broken down',
        '509' => 'this train breaking down',
        '510' => 'a collision between trains',
        '511' => 'a collision with the buffers at a station',
        '512' => 'a derailed train',
        '513' => 'a derailment within the depot',
        '514' => 'a low speed derailment',
        '515' => 'a train being involved in an accident',
        '516' => 'trains being involved in an accident',
        '517' => 'a fire at a station',
        '518' => 'a fire at a station earlier today',
        '519' => 'a landslip',
        '520' => 'a fire next to the track',
        '521' => 'a fire on a train',
        '522' => 'a member of on train staff being taken ill',
        '523' => 'a shortage of on train staff',
        '524' => 'a shortage of train conductors',
        '525' => 'a shortage of train crew',
        '526' => 'a shortage of train drivers',
        '527' => 'a shortage of train guards',
        '528' => 'a shortage of train managers',
        '529' => 'severe weather preventing train crew getting to work',
        '530' => 'the train conductor being taken ill',
        '531' => 'the train driver being taken ill',
        '532' => 'the train guard being taken ill',
        '533' => 'the train manager being taken ill',
        '534' => 'a passenger being taken ill at a station',
        '535' => 'a passenger being taken ill on a train',
        '536' => 'a passenger being taken ill on this train',
        '537' => 'a passenger being taken ill at a station earlier today',
        '538' => 'a passenger being taken ill on a train earlier today',
        '539' => 'a passenger being taken ill on this train earlier in its journey',
        '540' => 'a person being hit by a train',
        '541' => 'a person being hit by a train earlier today',
        '542' => 'a collision at a level crossing',
        '543' => 'a fault with barriers at a level crossing',
        '544' => 'a road accident at a level crossing',
        '545' => 'a road vehicle colliding with level crossing barriers',
        '546' => 'a road vehicle damaging track at a level crossing',
        '547' => 'a problem currently under investigation',
        '548' => 'a burst water main near the railway',
        '549' => 'a chemical spillage near the railway',
        '550' => 'a fire near the railway involving gas cylinders',
        '551' => 'a fire near the railway suspected to involve gas cylinders',
        '552' => 'a fire on property near the railway',
        '553' => 'a gas leak near the railway',
        '554' => 'a road accident near the railway',
        '555' => 'a wartime bomb near the railway',
        '556' => 'ambulance service dealing with an incident near the railway',
        '557' => 'emergency services dealing with an incident near the railway',
        '558' => 'fire brigade dealing with an incident near the railway',
        '559' => 'police dealing with an incident near the railway',
        '560' => 'a boat colliding with a bridge',
        '561' => 'a fault with a swing bridge over a river',
        '562' => 'a problem with a river bridge',
        '563' => 'a problem with line-side equipment',
        '564' => 'a security alert at a station',
        '565' => 'a security alert on another train',
        '566' => 'a security alert on this train',
        '567' => 'a train derailment earlier today',
        '568' => 'a train derailment yesterday',
        '569' => 'a fault occurring when attaching a part of a train',
        '570' => 'a fault occurring when attaching a part of this train',
        '571' => 'a fault occurring when detaching a part of a train',
        '572' => 'a fault occurring when detaching a part of this train',
        '573' => 'a fault on a train in front of this one',
        '574' => 'a fault on this train',
        '575' => 'this train being late from the depot',
        '576' => 'trespassers on the railway',
        '577' => 'a bus colliding with a bridge',
        '578' => 'a lorry colliding with a bridge',
        '579' => 'a road vehicle colliding with a bridge',
        '580' => 'a bus colliding with a bridge earlier on this train\'s journey',
        '581' => 'a bus colliding with a bridge earlier today',
        '582' => 'a lorry colliding with a bridge earlier on this train\'s journey',
        '583' => 'a lorry colliding with a bridge earlier today',
        '584' => 'a road vehicle colliding with a bridge earlier on this train\'s journey',
        '585' => 'a road vehicle colliding with a bridge earlier today',
        '586' => 'a broken down train earlier today',
        '587' => 'an earlier landslip',
        '588' => 'a fire next to the track earlier today',
        '589' => 'a fire on a train earlier today',
        '590' => 'a coach becoming uncoupled on a train earlier in its journey',
        '591' => 'a coach becoming uncoupled on a train earlier today',
        '592' => 'a coach becoming uncoupled on this train earlier in its journey',
        '593' => 'a coach becoming uncoupled on this train earlier today',
        '594' => 'a train not stopping at a station it was supposed to earlier in its journey',
        '595' => 'a train not stopping at a station it was supposed to earlier today',
        '596' => 'a train not stopping in the correct position at a station earlier in its journey',
        '597' => 'a train not stopping in the correct position at a station earlier today',
        '598' => 'a train\'s automatic braking system being activated earlier in its journey',
        '599' => 'a train\'s automatic braking system being activated earlier today',
        '600' => 'an operational incident earlier in its journey',
        '601' => 'an operational incident earlier today',
        '602' => 'this train not stopping at a station it was supposed to earlier in its journey',
        '603' => 'this train not stopping at a station it was supposed to earlier today',
        '604' => 'this train not stopping in the correct position at a station earlier in its journey',
        '605' => 'this train not stopping in the correct position at a station earlier today',
        '606' => 'this train\'s automatic braking system being activated earlier in its journey',
        '607' => 'this train\'s automatic braking system being activated earlier today',
        '608' => 'a collision at a level crossing earlier today',
        '609' => 'a collision at a level crossing yesterday',
        '610' => 'a fault with barriers at a level crossing earlier today',
        '611' => 'a fault with barriers at a level crossing yesterday',
        '612' => 'a road accident at a level crossing earlier today',
        '613' => 'a road accident at a level crossing yesterday',
        '614' => 'a road vehicle colliding with level crossing barriers earlier today',
        '615' => 'a road vehicle colliding with level crossing barriers yesterday',
        '616' => 'a road vehicle damaging track at a level crossing earlier today',
        '617' => 'a road vehicle damaging track at a level crossing yesterday',
        '618' => 'a burst water main near the railway earlier today',
        '619' => 'a burst water main near the railway yesterday',
        '620' => 'a chemical spillage near the railway earlier today',
        '621' => 'a chemical spillage near the railway yesterday',
        '622' => 'a fire near the railway involving gas cylinders earlier today',
        '623' => 'a fire near the railway involving gas cylinders yesterday',
        '624' => 'a fire near the railway suspected to involve gas cylinders earlier today',
        '625' => 'a fire near the railway suspected to involve gas cylinders yesterday',
        '626' => 'a fire on property near the railway earlier today',
        '627' => 'a fire on property near the railway yesterday',
        '628' => 'a gas leak near the railway earlier today',
        '629' => 'a gas leak near the railway yesterday',
        '630' => 'a road accident near the railway earlier today',
        '631' => 'a road accident near the railway yesterday',
        '632' => 'a wartime bomb near the railway earlier today',
        '633' => 'a wartime bomb near the railway yesterday',
        '634' => 'a wartime bomb which has now been made safe',
        '635' => 'ambulance service dealing with an incident near the railway earlier today',
        '636' => 'ambulance service dealing with an incident near the railway yesterday',
        '637' => 'emergency services dealing with an incident near the railway earlier today',
        '638' => 'emergency services dealing with an incident near the railway yesterday',
        '639' => 'fire brigade dealing with an incident near the railway earlier today',
        '640' => 'fire brigade dealing with an incident near the railway yesterday',
        '641' => 'police dealing with an incident near the railway earlier today',
        '642' => 'police dealing with an incident near the railway yesterday',
        '643' => 'a boat colliding with a bridge earlier today',
        '644' => 'a fault with a swing bridge over a river earlier today',
        '645' => 'a problem with a river bridge earlier today',
        '646' => 'an earlier problem with line-side equipment',
        '647' => 'a security alert earlier today',
        '648' => 'a fault on this train which is now fixed',
        '649' => 'trespassers on the railway earlier in this train\'s journey',
        '650' => 'trespassers on the railway earlier today',
        '651' => 'a bicycle on the track',
        '652' => 'a road vehicle blocking the railway',
        '653' => 'a supermarket trolley on the track',
        '654' => 'a train hitting an obstruction on the line',
        '655' => 'a tree blocking the railway',
        '656' => 'an obstruction on the track',
        '657' => 'checking reports of an obstruction on the line',
        '658' => 'this train hitting an obstruction on the line',
        '659' => 'a bicycle on the track earlier on this train\'s journey',
        '660' => 'a bicycle on the track earlier today',
        '661' => 'a road vehicle blocking the railway earlier on this train\'s journey',
        '662' => 'a road vehicle blocking the railway earlier today',
        '663' => 'a supermarket trolley on the track earlier on this train\'s journey',
        '664' => 'a supermarket trolley on the track earlier today',
        '665' => 'a train hitting an obstruction on the line earlier on this train\'s journey',
        '666' => 'a train hitting an obstruction on the line earlier today',
        '667' => 'a tree blocking the railway earlier on this train\'s journey',
        '668' => 'a tree blocking the railway earlier today',
        '669' => 'an obstruction on the track earlier on this train\'s journey',
        '670' => 'an obstruction on the track earlier today',
        '671' => 'checking reports of an obstruction on the line earlier on this train\'s journey',
        '672' => 'checking reports of an obstruction on the line earlier today',
        '673' => 'this train hitting an obstruction on the line earlier in its journey',
        '674' => 'this train hitting an obstruction on the line earlier on this train\'s journey',
        '675' => 'this train hitting an obstruction on the line earlier today',
        '676' => 'a coach becoming uncoupled on a train',
        '677' => 'a coach becoming uncoupled on this train',
        '678' => 'a train not stopping at a station it was supposed to',
        '679' => 'a train not stopping in the correct position at a station',
        '680' => 'a train\'s automatic braking system being activated',
        '681' => 'an operational incident',
        '682' => 'this train not stopping at a station it was supposed to',
        '683' => 'this train not stopping in the correct position at a station',
        '684' => 'this train\'s automatic braking system being activated',
        '685' => 'overcrowding',
        '686' => 'overcrowding as this train has fewer coaches than normal',
        '687' => 'overcrowding because an earlier train had fewer coaches than normal',
        '688' => 'overcrowding because of a concert',
        '689' => 'overcrowding because of a football match',
        '690' => 'overcrowding because of a marathon',
        '691' => 'overcrowding because of a rugby match',
        '692' => 'overcrowding because of a sporting event',
        '693' => 'overcrowding because of an earlier cancellation',
        '694' => 'overcrowding because of an event',
        '695' => 'overcrowding earlier on this train\'s journey',
        '696' => 'animals on the railway',
        '697' => 'cattle on the railway',
        '698' => 'horses on the railway',
        '699' => 'sheep on the railway',
        '700' => 'animals on the railway earlier today',
        '701' => 'cattle on the railway earlier today',
        '702' => 'horses on the railway earlier today',
        '703' => 'sheep on the railway earlier today',
        '704' => 'passengers causing a disturbance on a train',
        '705' => 'passengers causing a disturbance on this train',
        '706' => 'passengers causing a disturbance earlier in this train\'s journey',
        '707' => 'passengers causing a disturbance on a train earlier today',
        '708' => 'a fault with the electric third rail earlier on this train\'s journey',
        '709' => 'a fault with the electric third rail earlier today',
        '710' => 'damage to the electric third rail earlier on this train\'s journey',
        '711' => 'damage to the electric third rail earlier today',
        '712' => 'failure of the electricity supply earlier on this train\'s journey',
        '713' => 'failure of the electricity supply earlier today',
        '714' => 'the electricity being switched off for safety reasons earlier on this train\'s journey',
        '715' => 'the electricity being switched off for safety reasons earlier today',
        '716' => 'urgent repairs to a bridge earlier today',
        '717' => 'urgent repairs to a tunnel earlier today',
        '718' => 'urgent repairs to the railway earlier today',
        '719' => 'urgent repairs to the track earlier today',
        '720' => 'expected industrial action earlier today',
        '721' => 'expected industrial action yesterday',
        '722' => 'industrial action earlier today',
        '723' => 'industrial action yesterday',
        '724' => 'an object being caught on the overhead electric wires earlier on this train\'s journey',
        '725' => 'an object being caught on the overhead electric wires earlier today',
        '726' => 'damage to the overhead electric wires earlier on this train\'s journey',
        '727' => 'damage to the overhead electric wires earlier today',
        '728' => 'earlier engineering works not being finished on time',
        '729' => 'a fault with the on train signalling system earlier on this train\'s journey',
        '730' => 'a fault with the on train signalling system earlier today',
        '733' => 'a fault with the signalling system earlier on this train\'s journey',
        '734' => 'a fault with the signalling system earlier today',
        '737' => 'the fire alarm sounding in a signalbox earlier on this train\'s journey',
        '738' => 'the fire alarm sounding in a signalbox earlier today',
        '739' => 'the fire alarm sounding in the signalling centre earlier on this train\'s journey',
        '740' => 'the fire alarm sounding in the signalling centre earlier today',
        '741' => 'attempted theft of overhead line electrification equipment earlier today',
        '742' => 'attempted theft of overhead line electrification equipment yesterday',
        '743' => 'attempted theft of railway equipment earlier today',
        '744' => 'attempted theft of railway equipment yesterday',
        '745' => 'attempted theft of signalling cables earlier today',
        '746' => 'attempted theft of signalling cables yesterday',
        '747' => 'attempted theft of third rail electrification equipment earlier today',
        '748' => 'attempted theft of third rail electrification equipment yesterday',
        '749' => 'theft of overhead line electrification equipment earlier today',
        '750' => 'theft of overhead line electrification equipment yesterday',
        '751' => 'theft of railway equipment earlier today',
        '752' => 'theft of railway equipment yesterday',
        '753' => 'theft of signalling cables earlier today',
        '754' => 'theft of signalling cables yesterday',
        '755' => 'theft of third rail electrification equipment earlier today',
        '756' => 'theft of third rail electrification equipment yesterday',
        '757' => 'vandalism at a station earlier today',
        '758' => 'vandalism at a station yesterday',
        '759' => 'vandalism of railway equipment earlier today',
        '760' => 'vandalism of railway equipment yesterday',
        '761' => 'vandalism on a train earlier today',
        '762' => 'vandalism on a train yesterday',
        '763' => 'vandalism on this train earlier today',
        '764' => 'vandalism on this train yesterday',
        '765' => 'a fault with the electric third rail',
        '766' => 'damage to the electric third rail',
        '767' => 'failure of the electricity supply',
        '768' => 'the electricity being switched off for safety reasons',
        '769' => 'urgent repairs to a bridge',
        '770' => 'urgent repairs to a tunnel',
        '771' => 'urgent repairs to the railway',
        '772' => 'urgent repairs to the track',
        '773' => 'the emergency services dealing with an incident earlier today',
        '774' => 'ambulance service dealing with an incident',
        '775' => 'fire brigade dealing with an incident',
        '776' => 'police dealing with an incident',
        '777' => 'the emergency services dealing with an incident',
        '778' => 'the fire alarm sounding at a station',
        '779' => 'the fire alarm sounding at a station earlier today',
        '780' => 'a burst water main flooding the railway',
        '781' => 'a river flooding the railway',
        '782' => 'flood water making the railway potentially unsafe',
        '783' => 'flooding',
        '784' => 'heavy rain flooding the railway',
        '785' => 'predicted flooding',
        '786' => 'the sea flooding the railway',
        '787' => 'a burst water main flooding the railway earlier today',
        '788' => 'a river flooding the railway earlier today',
        '789' => 'flood water making the railway potentially unsafe earlier today',
        '790' => 'flooding earlier in this train\'s journey',
        '791' => 'flooding earlier today',
        '792' => 'heavy rain flooding the railway earlier today',
        '793' => 'predicted flooding earlier today',
        '794' => 'the sea flooding the railway earlier today',
        '795' => 'thick fog',
        '796' => 'thick fog earlier in this train\'s journey',
        '797' => 'thick fog earlier today',
        '798' => 'forecasted high winds',
        '799' => 'high winds',
        '800' => 'high winds earlier in this train\'s journey',
        '801' => 'high winds earlier today',
        '802' => 'expected industrial action',
        '803' => 'industrial action',
        '804' => 'lightning damaging a station',
        '805' => 'lightning damaging a train',
        '806' => 'lightning damaging equipment',
        '807' => 'lightning damaging the electricity supply',
        '808' => 'lightning damaging the signalling system',
        '809' => 'lightning damaging this train',
        '810' => 'an object being caught on the overhead electric wires',
        '811' => 'damage to the overhead electric wires',
        '812' => 'engineering works not being finished on time',
        '813' => 'forecasted slippery rails',
        '814' => 'ice preventing this train getting electricity from the third rail',
        '815' => 'ice preventing trains getting electricity from the third rail',
        '816' => 'slippery rails',
        '817' => 'slippery rails earlier in this train\'s journey',
        '818' => 'slippery rails earlier today',
        '819' => 'forecasted severe weather',
        '820' => 'severe weather',
        '821' => 'severe weather earlier',
        '822' => 'severe weather earlier in this train\'s journey',
        '823' => 'severe weather earlier today',
        '824' => 'a safety inspection of the track',
        '825' => 'a safety inspection on a train',
        '826' => 'a safety inspection on this train',
        '827' => 'a safety inspection of the track earlier today',
        '828' => 'a safety inspection on a train earlier today',
        '829' => 'a safety inspection on this train earlier in its journey',
        '830' => 'a fault with the on train signalling system',
        '831' => 'a fault with the radio system between the driver and the signaller',
        '832' => 'a fault with the signalling system',
        '834' => 'the fire alarm sounding in a signalbox',
        '835' => 'the fire alarm sounding in the signalling centre',
        '836' => 'forecasted heavy snow',
        '837' => 'heavy snow',
        '838' => 'heavy snow earlier in this train\'s journey',
        '839' => 'heavy snow earlier today',
        '840' => 'heavy snow over recent days',
        '841' => 'a speed restriction',
        '842' => 'a speed restriction because of fog',
        '843' => 'a speed restriction because of fog earlier on this train\'s journey',
        '844' => 'a speed restriction because of fog earlier today',
        '845' => 'a speed restriction because of heavy rain',
        '846' => 'a speed restriction because of heavy rain earlier on this train\'s journey',
        '847' => 'a speed restriction because of heavy rain earlier today',
        '848' => 'a speed restriction because of high track temperatures',
        '849' => 'a speed restriction because of high track temperatures earlier on this train\'s journey',
        '850' => 'a speed restriction because of high track temperatures earlier today',
        '851' => 'a speed restriction because of high winds',
        '852' => 'a speed restriction because of high winds earlier on this train\'s journey',
        '853' => 'a speed restriction because of high winds earlier today',
        '854' => 'a speed restriction because of severe weather',
        '855' => 'a speed restriction because of severe weather earlier on this train\'s journey',
        '856' => 'a speed restriction because of severe weather earlier today',
        '857' => 'a speed restriction because of snow and ice',
        '858' => 'a speed restriction because of snow and ice earlier on this train\'s journey',
        '859' => 'a speed restriction because of snow and ice earlier today',
        '860' => 'a speed restriction earlier on this train\'s journey',
        '861' => 'a speed restriction earlier today',
        '862' => 'a speed restriction in a tunnel',
        '863' => 'a speed restriction in a tunnel earlier on this train\'s journey',
        '864' => 'a speed restriction in a tunnel earlier today',
        '865' => 'a speed restriction over a bridge',
        '866' => 'a speed restriction over a bridge earlier on this train\'s journey',
        '867' => 'a speed restriction over a bridge earlier today',
        '868' => 'a speed restriction over an embankment',
        '869' => 'a speed restriction over an embankment earlier on this train\'s journey',
        '870' => 'a speed restriction over an embankment earlier today',
        '871' => 'a speed restriction over defective track',
        '872' => 'a speed restriction over defective track earlier on this train\'s journey',
        '873' => 'a speed restriction over defective track earlier today',
        '874' => 'attempted theft of overhead line electrification equipment',
        '875' => 'attempted theft of railway equipment',
        '876' => 'attempted theft of signalling cables',
        '877' => 'attempted theft of third rail electrification equipment',
        '878' => 'theft of overhead line electrification equipment',
        '879' => 'theft of railway equipment',
        '880' => 'theft of signalling cables',
        '881' => 'theft of third rail electrification equipment',
        '882' => 'vandalism at a station',
        '883' => 'vandalism of railway equipment',
        '884' => 'vandalism on a train',
        '885' => 'vandalism on this train',
        '886' => 'train crew being delayed',
        '887' => 'train crew being delayed by service disruption',
        '888' => 'a bridge being damaged',
        '889' => 'a bridge being damaged by a boat',
        '890' => 'a bridge being damaged by a road vehicle',
        '891' => 'a bridge having collapsed',
        '892' => 'a broken rail',
        '893' => 'a late departure while the train was cleaned specially',
        '894' => 'a late running freight train',
        '895' => 'a late running train being in front of this one',
        '896' => 'a points failure',
        '897' => 'a power cut at the station',
        '898' => 'a problem with the station lighting',
        '899' => 'a rail buckling in the heat',
        '900' => 'a railway embankment being damaged',
        '901' => 'a shortage of station staff',
        '902' => 'a tunnel being closed for safety reasons',
        '903' => 'an incident at the airport',
        '904' => 'congestion',
        '905' => 'the communication alarm being activated on a train',
        '906' => 'the communication alarm being activated on this train',
        '907' => 'the train departing late to maintain customer connections',
        '908' => 'the train making extra stops because a train was cancelled',
        '909' => 'the train making extra stops because of service disruption',
        '910' => 'waiting for a part of the train to be attached',
        '911' => 'a fault on a train',
        '912' => 'a problem with platform equipment',
        '913' => 'a fault on a train earlier',
        '914' => 'issues with communication systems',
        '915' => 'a problem in the depot',
        '916' => 'signalling staff being unavailable',
        '917' => 'staff training',
        '918' => 'a short-notice change to the timetable',
        '919' => 'misuse of a level crossing'
    );
    protected static $ARRDEP_NOT_RECOMMENDED_LOCATIONS = ['CHX', 'WAT', 'VIC', 'PAD', 'MYB', 'EUS', 'STP', 'KGX', 'MOG', 'LST', 'FST', 'CST'];

    protected static $LOC_PREFERRED_NAMES = array(
        'AFK' => "Ashford (Kent)",
        'EBD' => 'Ebbsfleet',
        'SRA' => 'Stratford Regional',
        'SFA' => 'Stratford HS1',
        'STP' => 'London St Pancras',
        'ZFD' => 'Farringdon'
    );
}
