railpage/railpagecore

View on GitHub
lib/Locos/Locomotive.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php

/** 
 * Loco database
 * @since Version 3.2
 * @author Michael Greenhill
 * @package Railpage
 */

namespace Railpage\Locos;

use Railpage\Locos\Liveries\Livery;
use Railpage\Users\User;
use Railpage\Users\Factory as UserFactory;
use Railpage\Users\Utility\UrlUtility as UserUrlUtility;
use Railpage\Images\Images;
use Railpage\Images\Image;
use Railpage\Assets\Asset;
use Railpage\ContentUtility;
use Railpage\Url;
use Railpage\Debug;
use Railpage\AppCore;
use DateTime;
use Exception;
use stdClass;
use Railpage\Registry;
use Railpage\Users\Utility\AvatarUtility;
use Railpage\Sightings\Sightings;
use Railpage\Sightings\Sighting;
    
/**
 * Locomotive
 */

class Locomotive extends Locos {
    
    /**
     * Registry cache key
     * @since Version 3.9.1
     * @const string REGISTRY_KEY
     */
    
    const REGISTRY_KEY = "railpage.locos.loco=%d";
    
    /**
     * Memcached/Redis cache key
     * @since Version 3.9.1
     * @const string CACHE_KEY
     */
    
    const CACHE_KEY = "railpage:locos.loco_id=%d";
    
    /**
     * Memcached key for loco descriptive text
     * @since Version 3.10.0
     * @const string CACHE_KEY_DESC
     */
    
    const CACHE_KEY_DESC = "railpage:locos.loco_id=%d;desc";
    
    /**
     * Loco ID
     * @since Version 3.2
     * @var int $id
     */
    
    public $id;
    
    /**
     * Loco number
     * @since Version 3.2
     * @var string $number Locomotive fleet number, eg R761 or NR101
     */
    
    public $number;
    
    /**
     * Flickr tag
     * @since Version 3.2
     * @var string $flickr_tag
     */
    
    public $flickr_tag;
    
    /**
     * Gauge
     * @since Version 3.2
     * @var string $gauge
     */
    
    public $gauge;
    
    /**
     * Gauge ID
     * @since Version 3.4
     * @var int $gauge_id
     */
    
    public $gauge_id;
    
    /**
     * Gauge - formatted to look nicer
     * @since Version 3.2
     * @var string $gauge_formatted
     */
    
    public $gauge_formatted;
    
    /**
     * Status ID
     * @since Version 3.2
     * @var int $status_id
     */
    
    public $status_id;
    
    /**
     * Loco status
     * @since Version 3.2
     * @var string $status
     */
    
    public $status;
    
    /**
     * Class ID
     * @since Version 3.2
     * @var int $class_id
     */
    
    public $class_id;
    
    /**
     * Class object
     * @since Version 3.8.7
     * @var \Railpage\Locos\LocoClass $class Instance of \Railpage\Locos\LocoClass that this locomotive belongs to
     */
    
    public $Class;
    
    /**
     * Alias of $this->Class
     * @since Version 3.2
     * @var \Railpage\Locos\LocoClass $class Instance of \Railpage\Locos\LocoClass that this locomotive belongs to
     */
    
    public $class;
            
    /**
     * All owners
     * @since Version 3.4
     * @var array $owners An array of owners of this locomotive
     */
    
    public $owners; 
    
    /**
     * Owner ID
     * @since Version 3.2
     * @var int $owner_id The ID of the current/newest owner
     */
    
    public $owner_id;
    
    /**
     * Owner name
     * @since Version 3.2
     * @var string $owner The formatted name of the current/newest owner
     */
    
    public $owner;
    
    /**
     * All operators
     * @since Version 3.4
     * @var array $operators An array of operators of this locomotive
     */
    
    public $operators; 
    
    /**
     * Operator ID
     * @since Version 3.2
     * @var int $operator_id The ID of the current/newest operator
     */
    
    public $operator_id;
    
    /**
     * Operator name
     * @since Version 3.2
     * @var string $operator The formatted name of the current/newest operator
     */
    
    public $operator;
    
    /**
     * Entered service date
     * @deprecated Deprecated since Version 3.8.7 - replaced by \Railpage\LocoClass\Date objects
     * @since Version 3.2
     * @var int $entered_service
     */
    
    public $entered_service;
    
    /**
     * Withdrawal date
     * @deprecated Deprecated since Version 3.8.7 - replaced by \Railpage\LocoClass\Date objects
     * @since Version 3.2
     * @var int $withdrawal_date
     */
    
    public $withdrawal_date;
    
    /**
     * Builders number
     * @since Version 3.2
     * @var string $builders_num The builders number
     */
    
    public $builders_num;
    
    /**
     * Loco photo ID
     * @since Version 3.2
     * @var int $photo_id ID of a Flickr photo to show as the cover photo of this locomotive
     */
    
    public $photo_id;
    
    /**
     * Loco builder ID
     * @since Version 3.2
     * @var int $manufacturer_id
     */
    
    public $manufacturer_id;
    
    /**
     * Loco builder name
     * @since Version 3.2
     * @var int $manufacturer
     */
    
    public $manufacturer;
    
    /**
     * Date added
     * @since Version 3.2
     * @var int $date_added When this locomotive was added to the database
     */
    
    public $date_added;
    
    /**
     * Date modified
     * @since Version 3.2
     * @var int $date_modified When this locomotive was last modified in the database
     */
    
    public $date_modified;
    
    /**
     * Locomotive name
     * @since Version 3.2
     * @var string $name
     */
    
    public $name;
    
    /**
     * Locomotive data rating
     * @since Version 3.2
     * @var float $rating
     */
    
    public $rating;
    
    /**
     * Memcache key
     * @since Version 3.7.5
     * @var string $mckey The unique Memcached identifier of this locomotive
     */
    
    public $mckey; 
    
    /**
     * Loco URL
     * @since Version 3.8
     * @var string $url The link to this locomotive's page, relative to the site root of Railpage
     */
    
    public $url;
    
    /**
     * Asset ID for non-Flickr cover photo
     * @since Version 3.8.7
     * @var \Railpage\Assets\Asset $Asset An instance of \Railpage\Assets\Asset identified as the "primary" asset for this locomotive. Could be featuring the cover photo.
     */
    
    public $Asset;
    
    /**
     * Array of liveries worn by this locomotive
     * @since Version 3.8.7
     * @var array $liveries
     */
    
    public $liveries;
    
    /**
     * Loco meta data
     * @since Version 3.8.7
     * @var array $meta
     */
    
    public $meta;
    
    /**
     * Constructor
     * @since Version 3.2
     * @param int $id
     * @param int|string $classIdOrSlug
     * @param string $number
     */
    
    public function __construct($id = NULL, $classIdOrSlug = NULL, $number = NULL) {
        parent::__construct(); 
        
        $timer = Debug::getTimer();
        
        /**
         * Record this in the debug log
         */
            
        Debug::RecordInstance(NULL, $id);
        
        $this->bootstrap(); 
        
        if (filter_var($id, FILTER_VALIDATE_INT)) {
            $this->id = filter_var($id, FILTER_VALIDATE_INT);
        } else {
            $this->id = Utility\LocomotiveUtility::getLocoId($classIdOrSlug, $number); 
        }
        
        // Load the loco object
        if (filter_var($this->id, FILTER_VALIDATE_INT)) {
            $this->fetch(); 
        }
        
        $this->id = intval($this->id);
        
        Debug::logEvent(sprintf("%s(%d)", __METHOD__, $this->id), $timer); 
    }
    
    /**
     * Bootstrap this class
     * @since Version 3.9.1
     * @return void
     */
    
    private function bootstrap() {
        $this->namespace = sprintf("%s.%s", $this->Module->namespace, "loco");
        
        /**
         * List of templates
         */
        
        $this->Templates = new stdClass;
        $this->Templates->view = "loco";
        $this->Templates->edit = "loco.edit";
        $this->Templates->sightings = "loco.sightings";
    }
    
    /**
     * Populate this object with data returned from Memcached/Redis/DB
     * @since Version 3.9.1
     * @return void
     */
    
    private function populate() {
        
        $timer = Debug::getTimer();
        
        $row = Utility\LocomotiveUtility::fetchLocomotive($this); 
        
        if (!is_array($row) || count($row) === 0) {
            throw new Exception("Data for this locomotive could not be retrieved") ;
        }
        
        $lookup = Utility\DataUtility::getLocoColumnMapping(); 
        
        foreach ($row as $key => $val) {
            if (isset($lookup[$key])) {
                $var = $lookup[$key];
                $this->$var = $val;
            }
        }
        
        $ints = [ "gauge_id", "status_id", "class_id", "owner_id", "operator_id", "photo_id", "manufacturer_id" ];
        
        foreach ($ints as $int) {
            $this->$int = filter_var($this->$int, FILTER_VALIDATE_INT); 
        }
        
        $this->Class = Factory::CreateLocoClass($this->class_id); 
        $this->class = &$this->Class;
        
        $this->flickr_tag = trim(str_replace(" ", "", $this->Class->flickr_tag . "-" . $this->number));
        $this->gauge_formatted = format_gauge($this->gauge);
        $this->makeLinks();
        
        Debug::logEvent(__METHOD__, $timer); 
        
        return $row; 
    }
    
    /**
     * Load the URL object
     * @since Version 3.9.1
     * @return void
     */
    
    private function makeLinks() {
        
        if (!$this->Class instanceof LocoClass) {
            return;
        }
        
        $this->url = new Url(strtolower($this->makeLocoURL($this->Class->slug, $this->number)));
        $this->url->edit = sprintf("%s?mode=loco.edit&id=%d", $this->Module->url, $this->id);
        $this->url->sightings = sprintf("%s/sightings", $this->url->url);
        $this->url->photos = sprintf("%s/photos", $this->url->url);
        $this->fwlink = $this->url->short;
        
    }
    
    /**
     * Load the locomotive object
     * @since Version 3.2
     * @version 3.2
     * @return boolean
     */
    
    public function fetch() {
        if (!filter_var($this->id, FILTER_VALIDATE_INT)) {
            throw new Exception("Cannot load loco object - loco ID not provided");
            return false;
        }
        
        $timer = Debug::getTimer();
        
        $this->mckey = sprintf(self::CACHE_KEY, $this->id);
        
        $row = $this->populate(); 
            
        /**
         * Set the meta data
         */
        
        $this->meta = isset($row['meta']) ? json_decode($row['meta'], true) : array(); 
        
        /**
         * Fetch a nicely formatted gauge
         */
        
        #$this->setGauge(new Gauge($row['loco_gauge_id'])); 
        $this->setGauge(Factory::Create("Gauge", $row['loco_gauge_id'])); 
        
        /**
         * If an asset ID exists and is greater than 0, create the asset object
         */
        
        if (isset($row['asset_id']) && $row['asset_id'] > 0) {
            try {
                $this->Asset = new Asset($row['asset_id']);
            } catch (Exception $e) {
                // throw it away
            }
        }
        
        /**
         * Do we need to update the database and Memcached records?
         */
        
        $doUpdate = false;
        
        /**
         * Get all owners of this locomotive
         */
        
        $this->reloadOrganisations("owners"); 
        $this->reloadOrganisations("operators"); 
        
        /**
         * Get the manufacturer
         */
        
        $this->loadManufacturer(); 
        
        /**
         * Set the StatsD namespaces
         */
        
        $this->StatsD->target->view = sprintf("%s.%d.view", $this->namespace, $this->id);
        $this->StatsD->target->edit = sprintf("%s.%d.view", $this->namespace, $this->id);
        
        /**
         * Update the database and Memcached records if required
         */
        
        if ($doUpdate) {
            $this->commit(); 
        }
        
        Debug::logEvent(__METHOD__, $timer); 
    }
    
    /**
     * Load the manufacturer for this locomotive
     * @since Version 3.9.1
     * @return void
     */
    
    private function loadManufacturer() {
        
        if (empty($this->manufacturer_id)) {
            $this->manufacturer_id  = $this->Class->manufacturer_id;
            $this->manufacturer     = $this->Class->manufacturer;
            
            return;
        }
        
        try {
            $builders = $this->listManufacturers(); 
            
            if (count($builders['manufacturers'])) {
                $this->manufacturer = $builders['manufacturers'][$this->manufacturer_id]['manufacturer_name'];
            }
        } catch (Exception $e) {
            // throw it away
        }
        
        return;
        
    }
    
    /**
     * Update the owners/operators
     * @since Version 3.9.1
     * @param string $type
     * @return void
     */
    
    private function reloadOrganisations($type) {
        
        if (substr($type, -1) !== "s") {
            $type .= "s";
        }
        
        $allowed = [ "owners", "operators" ];
        
        if (!in_array($type, $allowed)) {
            throw new InvalidArgumentException("Cannot update owners/operators/organisations: " . $type . " is an invalid organisation type"); 
        }
        
        $lookup = [
            "owners" => 1,
            "operators" => 2
        ];
        
        $type_id = $lookup[$type];
        
        $var_name = substr($type, 0, -1);
        $var_name_id = substr($type, 0, -1) . "_id";
        
        $this->$type = $this->getOrganisations($type_id); 
            
        reset($this->$type);
        $array = $this->$type;
        
        if (isset($array[0]['organisation_id']) && isset($array[0]['organisation_name'])) {
            $this->$var_name_id = $array[0]['organisation_id']; 
            $this->$var_name    = $array[0]['organisation_name']; 
            Debug::LogEvent(__METHOD__ . "() : Latest " . $var_name . " ID requires updating");
            
            return;
        }
        
        $this->$var_name_id = 0;
        $this->$var_name    = "Unknown";
        
        return;

    }
    
    /**
     * Validate
     * @since Version 3.2
     * @version 3.2
     * @return boolean
     */
    
    private function validate() {
        
        if ($this->class instanceof LocoClass && !$this->Class instanceof LocoClass) {
            $this->Class = &$this->class;
        }
        
        if (empty($this->number)) {
            throw new Exception("No locomotive number specified");
        }
        
        if (!filter_var($this->class_id, FILTER_VALIDATE_INT) || $this->class_id === 0) {
            if ($this->Class instanceof LocoClass) {
                $this->class_id = $this->Class->id;
            } else {
                throw new Exception("Cannot add locomotive because we don't know which class to add it into");
            }
        }
        
        if (!filter_var($this->gauge_id, FILTER_VALIDATE_INT)) {
            throw new Exception("No gauge has been set");
        }
        
        if (!filter_var($this->status_id, FILTER_VALIDATE_INT)) {
            throw new Exception("No status has been set");
        }
        
        /**
         * Validate integers and set to zero if neccessary
         */
        
        $ints = [ "owner_id", "operator_id", "photo_id", "manufacturer_id" ];
        
        foreach ($ints as $int) {
            if (!filter_var($this->$int, FILTER_VALIDATE_INT)) {
                $this->$int = 0;
            }
        }
        
        /**
         * The database doesn't like NULLs so set them to an empty character
         */
        
        $texts = [ "entered_service", "withdrawal_date", "builders_num", "name" ];
        
        foreach ($texts as $text) {
            if (is_null($this->$text)) {
                $this->$text = "";
            }
        }
        
        return true;
    }
    
    /**
     * Commit changes to database
     * @since Version 3.2
     * @version 3.8.7
     * @return boolean
     */
    
    public function commit() {
        
        $timer = Debug::getTimer();
        
        $this->validate();
        
        $data = Utility\LocomotiveUtility::getSubmitData($this);
        
        if (!filter_var($this->id, FILTER_VALIDATE_INT)) {
            $rs = $this->db->insert("loco_unit", $data); 
            $this->id = $this->db->lastInsertId(); 
            
            $verb = "Insert";
        } else {
            $this->Memcached->delete($this->mckey);
            $this->Redis->delete($this->mckey); 
            
            $where = array(
                "loco_id = ?" => $this->id
            );
            
            $verb = "Update";
            
            $rs = $this->db->update("loco_unit", $data, $where); 
        }
        
        // Update the registry
        $Registry = Registry::getInstance(); 
        $regkey = sprintf(self::REGISTRY_KEY, $this->id); 
        $Registry->remove($regkey)->set($regkey, $this); 
        $this->Memcached->delete(sprintf(self::CACHE_KEY_DESC, $this->id));
        
        Debug::logEvent("Zend_DB: commit loco ID " . $this->id, $timer); 
        
        $this->makeLinks(); 
        
        return true;
    }
    
    /**
     * Add note to this loco or edit an existing one
     * @since Version 3.2
     * @param string $noteText
     * @param int $userId
     * @param int $noteId
     */
    
    public function addNote($noteText = false, $userId = false, $noteId = false) {
        if (!$noteText || empty($noteText)) {
            throw new Exception("No note text given"); 
        } 
        
        if (!$userId instanceof User && !filter_var($userId, FILTER_VALIDATE_INT)) {
            throw new Exception("No user provided"); 
        }
        
        if ($userId instanceof User) {
            $userId = $userId->id;
        }
        
        $data = array(
            "loco_id" => $this->id,
            "note_date" => time(),
            "note_text" => $noteText
        );
        
        if (!empty($userId)) {
            $data['user_id'] = $userId;
        }
        
        if ($noteId) {
            $where = array(
                "note_id = ?" => $noteId
            );
            
            $this->db->update("loco_notes", $data, $where);
            return true;
        } else {
            $this->db->insert("loco_notes", $data);
            return $this->db->lastInsertId(); 
        }
    }
    
    /**
     * Load notes
     * @since Version 3.2
     * @version 3.2
     * @return array
     */
    
    public function loadNotes() {
        $query = "SELECT n.*, u.username, user_avatar FROM loco_notes AS n LEFT JOIN nuke_users AS u ON n.user_id = u.user_id WHERE n.loco_id = ?";
        
        $notes = array(); 
        
        foreach ($this->db->fetchAll($query, $this->id) as $row) {
            if (!empty($row['user_avatar'])) {
                try {
                    #$User = UserFactory::CreateUser($row['user_id']);
                    
                    $row['user_avatar'] = AvatarUtility::Format($row['user_avatar'], 50, 50);
                    #$row['user_url'] = $User->url;
                    
                    $row['user_url'] = UserUrlUtility::MakeURLs($row);
                } catch (Exception $e) {
                    global $Error; 
                    $Error->save($e); 
                }
            }
            
            $notes[$row['note_id']] = $row; 
        }
        
        return $notes;
    }
    
    /**
     * Load dates
     * @since Version 3.2
     * @version 3.2
     * @return array
     */
    
    public function loadDates() {
        $query = "SELECT d.date_id, d.date, d.text, dt.loco_date_text AS title, dt.loco_date_id AS date_type_id
                    FROM loco_unit_date AS d
                    LEFT JOIN loco_date_type AS dt ON d.loco_date_id = dt.loco_date_id
                    WHERE d.loco_unit_id = ?
                    ORDER BY d.date DESC";
        
        return $this->db->fetchAll($query, $this->id);
    }
    
    /**
     * Add a date to this loco
     * @since Version 3.2
     * @param int $dateId
     * @param string $dateDate
     * @param string $dateText
     * @return boolean
     */
    
    public function addDate($dateId = false, $dateDate = false, $dateText = false) {
        
        $Date = new Date;
        $Date->action = $dateText;
        $Date->action_id = $dateId;
        $Date->Date = new DateTime($dateDate); 
        
        $Date->commit(); 
        
        return true;
        
    }
    
    /**
     * Get link(s) of this loco
     * @since Version 3.2
     * @return array
     */
    
    public function links() {
        $query = "SELECT * FROM loco_link WHERE loco_id_a = ? OR loco_id_b = ?";
        $return = array();
        
        foreach ($this->db->fetchAll($query, array($this->id, $this->id)) as $row) {
            $article = $row['loco_id_a'] === $this->id ? "to" : "from";
            $key = $row['loco_id_a'] === $this->id ? "loco_id_b" : "loco_id_a";
            
            if ($row['link_type_id'] === RP_LOCO_RENUMBERED) {
                $return[$row['link_id']][$row[$key]] = "Renumbered " . $article;
            } elseif ($row['link_type_id'] === RP_LOCO_REBUILT) {
                $return[$row['link_id']][$row[$key]] = "Rebuilt " . $article;
            }
        }
        
        return $return;
    }
    
    /**
     * Save a correction for this loco
     * @since Version 3.2
     * @param string $text
     * @param int $userId
     */
    
    public function newCorrection($text = false, $userId = false) {
        
        $Correction = new Correction;
        $Correction->text = $text;
        $Correction->setUser(UserFactory::CreateUser($userId)); 
        $Correction->setObject($this);
        $Correction->commit(); 
        
        return true;
    }
    
    /**
     * Get corrections for this loco
     * @since Version 3.2
     * @param boolean $active
     * @return array
     */
    
    public function corrections($active = true) {
        if ($active) {
            $active_sql = " AND c.status = 0 ";
        } else {
            $active_sql = "";
        }
        
        $query = "SELECT c.correction_id, c.user_id, UNIX_TIMESTAMP(c.date) as date, c.status, c.text , u.username
            FROM loco_unit_corrections AS c
            LEFT JOIN nuke_users AS u ON c.user_id = u.user_id
            WHERE c.loco_id = ? " . $active_sql;
        
        $return = array(); 
        
        foreach ($this->db->fetchAll($query, $this->id) as $row) {
            $return[$row['correction_id']] = $row; 
        }
        
        return $return;
    }
    
    /**
     * Get ratings for this loco
     * @since Version 3.2
     * @param boolean $detailed
     * @return float
     */
    
    public function getRating($detailed = false) {
        if (!filter_var($this->id, FILTER_VALIDATE_INT)) {
            throw new Exception("Cannot fetch rating - no loco ID given"); 
        }
        
        if ($detailed) {
            $query = "SELECT 
                        COALESCE(AVG(rating), 0) as dec_avg, 
                        COALESCE(ROUND(AVG(rating)), 0) AS whole_avg,
                        COALESCE(COUNT(rating), 0) AS number_votes, 
                        COALESCE(SUM(rating), 0) AS total_points 
                        FROM rating_loco 
                        WHERE loco_id = ?"; 
            
            return $this->db->fetchRow($query, $this->id); 
            
            /*
            $row = array(
                "dec_avg" => 0,
                "whole_avg" => 0,
                "total_points" => 0,
                "number_votes" => 0
            );
            
            $row = $this->db->fetchRow($query, $this->id); 
            
            $row['dec_avg'] = empty($row['dec_avg']) ? 0 : $row['dec_avg'];
            $row['total_points'] = empty($row['total_points']) ? 0 : $row['total_points'];
            $row['number_votes'] = empty($row['number_votes']) ? 0 : $row['number_votes'];
            $row['whole_avg'] = round($row['dec_avg']);
            
            return $row;
            */
        }
        
        $query = "SELECT COALESCE(AVG(rating), '2.5') as average_rating FROM rating_loco WHERE loco_id = ?"; 
        
        return $this->db->fetchOne($query, $this->id); 
        
        /*
        $row = $this->db->fetchRow($query, $this->id);
        
        return isset($row['average_rating']) ? $row['average_rating'] : floatval("2.5"); 
        */
    }
    
    /**
     * Get this user's rating for this loco
     * @since Version 3.2
     * @param int $userId
     * @return float|boolean
     */
    
    public function userRating($userId = false) {
        if (!$userId instanceof User && !filter_var($userId, FILTER_VALIDATE_INT)) {
            throw new Exception("Cannot fetch user rating for this loco - no user given"); 
        }
        
        if ($userId instanceof User) {
            $userId = $userId->id;
        }
        
        $query = "SELECT rating FROM rating_loco WHERE user_id = ? AND loco_id = ? LIMIT 1"; 
        
        $rating = $this->db->fetchOne($query, array($userId, $this->id)); 
        
        return $rating;
    }
    
    /**
     * Set user rating for this loco
     * @since Version 3.2
     * @param int $userId
     * @param float $rating
     * @return boolean
     */
     
    public function setRating($userId = false, $rating = false) {
        if (!$userId instanceof User && !filter_var($userId, FILTER_VALIDATE_INT)) {
            throw new Exception("Cannot set user rating for this loco - no user given"); 
        }
        
        if ($userId instanceof User) {
            $userId = $userId->id;
        }
        
        if (!filter_var($rating, FILTER_VALIDATE_INT)) {
            throw new Exception("Cannot set user rating for this loco - no rating given"); 
        }
        
        $rating = floatval($rating); 
        
        $data = array(
            "loco_id" => $this->id,
            "user_id" => $userId,
            "rating" => $rating,
            "date" => new \Zend_Db_Expr('NOW()')
        );
        
        if ($this->userRating($userId)) {
            $where = array(
                "user_id = ?" => $userId,
                "loco_id = ?" => $this->id
            );
            
            $this->db->update("rating_loco", $data, $where);
        } else {
            $this->db->insert("rating_loco", $data);
        }
        
        return true;
    }
    
    /**
     * Get liveries carried by this loco
     * Based on tagged Flickr photos
     * @since Version 3.2
     * @param object $f
     * @return array|boolean
     */
    
    public function getLiveries($f = false) {
        
        return Utility\LocomotiveUtility::getLiveriesForLocomotive($this->id); 
        
    }
    
    /**
     * Get organisation links by the given type
     * @since Version 3.4
     * @param int $orgType
     * @param int $limit
     * @return array
     */
    
    public function getOrganisations($orgType = null, $limit = null) {
        
        $limit_sql = NULL;
        $org_sql = "";
        
        if (!is_null($limit)) {
            $limit_sql = "LIMIT 0, 1"; 
        }
        
        $params = array($this->id); 
        
        if (filter_var($orgType, FILTER_VALIDATE_INT)) {
            $org_sql = " AND ot.id = ?";
            $params[] = $orgType;
        }
        
        $query = "SELECT o.*, op.operator_id AS organisation_id, op.operator_name AS organisation_name FROM loco_org_link AS o LEFT JOIN loco_org_link_type AS ot ON ot.id = o.link_type LEFT JOIN operators AS op ON op.operator_id = o.operator_id WHERE o.loco_id = ? " . $org_sql . " ORDER BY ot.id, o.link_weight DESC ".$limit_sql.""; 
        
        $return = $this->db->fetchAll($query, $params);
        return $return;
        
    }
    
    /**
     * Add an organisation link
     * @since Version 3.4
     * @param int $orgId
     * @param int $orgType
     * @param int $date
     * @param int $weight
     */
    
    public function addOrganisation($orgId = null, $orgType = null, $date = null, $weight = 0) {
        if (!filter_var($orgId, FILTER_VALIDATE_INT)) {
            throw new Exception("Could not add new organisation link - no org_id given"); 
            return false;
        }
        
        if (!filter_var($orgType, FILTER_VALIDATE_INT)) {
            throw new Exception("Could not add new organisation link - no org_type_id given"); 
            return false;
        }
        
        $data = array(
            "loco_id" => $this->id,
            "operator_id" => $orgId,
            "link_type" => $orgType,
            "link_weight" => $weight
        );
        
        if (!is_null($date)) {
            $timestamp = strtotime($date); 
            
            $data['link_date'] = date("Y-m-d H:i:s", $timestamp);
        }
        
        return $this->db->insert("loco_org_link", $data);
    }
    
    /**
     * Delete an organisation link
     * @since Version 3.4
     * @param int $orgLinkId
     * @return boolean
     */
    
    public function deleteOrgLink($orgLinkId = null) {
        if (!filter_var($orgLinkId, FILTER_VALIDATE_INT)) {
            throw new Exception("Could not delete org link - no org_link_id specified"); 
            return false;
        }
        
        $where = array("id = ?" => $orgLinkId); 
        
        $this->db->delete("loco_org_link", $where);
        
        return true;
    }
    
    /** 
     * Log an event 
     * @since Version 3.5
     * @param int $userId
     * @param string $title
     * @param array $args
     */
    
    public function logEvent($userId = false, $title = false, $args = false) {
        if (!$user_id) {
            throw new Exception("Cannot log event, no User ID given"); 
            return false;
        }
        
        if (!$title) {
            throw new Exception("Cannot log event, no title given"); 
            return false;
        }
        
        $Event = new \Railpage\SiteEvent; 
        $Event->user_id = $userId; 
        $Event->title = $title;
        $Event->args = $args; 
        $Event->key = "loco_id";
        $Event->value = $this->id;
        $Event->module_name = "locos";
        
        if ($title === "Photo tagged") {
            $Event->module_name = "flickr";
        }
        
        $Event->commit();
        
        return true;
    }
    
    /**
     * Get events recorded against this class
     * @since Version 3.5
     * @return array
     */
    
    public function getEvents() {
        $query = "SELECT ll.*, u.username FROM log_locos AS ll LEFT JOIN nuke_users AS u ON ll.user_id = u.user_id WHERE ll.loco_id = ? ORDER BY timestamp DESC"; 
        
        $return = array(); 
        
        foreach ($this->db->fetchAll($query, $this->id) as $row) {
            $row['timestamp'] = DateTime::createFromFormat("Y-m-d H:i:s", $row['timestamp']); 
            $row['args'] = json_decode($row['args'], true);
            $return[] = $row; 
        }
        
        return $return;
    }
    
    /**
     * Get a locomotive org link
     * @since Version 3.5
     * @param int $id
     * @return array
     */
    
    public function getOrgLink($id = false) {
        if (!$id) {
            throw new Exception("Cannot fetch organisation link - no org link ID given"); 
            return false;
        }
        
        $query = "SELECT o.*, ot.name AS link_type_name, op.operator_name FROM loco_org_link AS o LEFT JOIN loco_org_link_type AS ot ON o.link_type = ot.id LEFT JOIN operators AS op ON op.operator_id = o.operator_id WHERE o.id = ?"; 
        
        return $this->db->fetchRow($query, $id);
    }
    
    /**
     * Loco sightings
     * @since Version 3.5
     * @return array
     */
    
    public function sightings() {
        $Sightings = new Sightings;
        
        return $Sightings->findLoco($this->id); 
    }
    
    /**
     * Get contributors of this locomotive
     * @since Version 3.7.5
     * @return array
     */
    
    public function getContributors() {
        
        $key = sprintf(self::CACHE_KEY, $this->id) . ";contributors";
        
        if ($contributors = $this->Redis->fetch($key)) {
            return $contributors;
        }
        
        $return = array(); 
        
        $Sphinx = AppCore::getSphinx();
        
        $query = $Sphinx->select("user_id", "username")
                        ->from("idx_logs")
                        ->match("module", "locos")
                        ->where("key", "=", "loco_id")
                        ->where("value", "=", intval($this->id))
                        ->groupBy("user_id");
        
        $result = $query->execute();
        
        foreach ($result as $row) {
            $return[$row['user_id']] = $row['username'];
        }
        
        $this->Redis->save($key, $return, strtotime("+2 hours"));
        
        return $return;
        
    }
    
    /**
     * Return an array of tags appliccable to this loco
     * @since Version 3.7.5
     * @return array
     */
    
    public function getTags() {
        $tags = $this->Class->getTags(); 
        $tags[] = "railpage:loco=" . $this->number;
        $tags[] = $this->flickr_tag;
        $tags[] = $this->number;
        
        asort($tags);
        
        return $tags;
    }
    
    /**
     * Add an asset to this locomotive
     * @since Version 3.8
     * @param array $data
     * @return boolean
     */
    
    public function addAsset($data = false) {
        
        return Utility\LocosUtility::addAsset($this->namespace, $this->id, $data); 
        
    }
    
    /**
     * Get next locomotive
     * @since Version 3.8.7
     * @return \Railpage\Locos\Locomotive
     */
    
    public function next() {
        $members = $this->Class->members(); 
        
        if ($members['stat'] === "ok") {
            // Get the previous loco in this class
            
            $break = false;
            
            foreach ($members['locos'] as $row) {
                if ($break === true) {
                    return new Locomotive($row['loco_id']);
                }
                
                if ($row['loco_id'] === $this->id) {
                    $break = true;
                }
            }
        }
    }
    
    /**
     * Get previous locomotive
     * @since Version 3.8.7
     * @return \Railpage\Locos\Locomotive
     */
    
    public function previous() {
        $members = $this->Class->members(); 
        
        // Get the next loco in this class
        if ($members['stat'] === "ok") {
            
            $break = false;
            
            $members['locos'] = array_reverse($members['locos']);
            foreach ($members['locos'] as $row) {
                if ($break === true) {
                    return new Locomotive($row['loco_id']);
                }
                
                if ($row['loco_id'] === $this->id) {
                    $break = true;
                }
            }
        }
    }
    
    /**
     * Set the cover photo for this locomotive
     * @since Version 3.8.7
     * @param $Image Either an instance of \Railpage\Images\Image or \Railpage\Assets\Asset
     * @return $this
     */
    
    public function setCoverImage($Image) {
        
        /**
         * Zero out any existing images
         */
        
        $this->photo_id = NULL;
        $this->Asset = NULL;
        
        if (isset($this->meta['coverimage'])) {
            unset($this->meta['coverimage']);
        }
        
        /**
         * $Image is a Flickr image
         */
        
        if ($Image instanceof Image && $Image->provider === "flickr") {
            $this->photo_id = $Image->photo_id;
            $this->commit(); 
            
            return $this;
        }
        
        /**
         * Image is a site asset
         */
        
        if ($Image instanceof Asset) {
            $this->Asset = clone $Image;
            $this->commit(); 
            
            return $this;
        }
        
        /**
         * Image is a generic image, so we'll just store the Image ID and fetch it later with $this->getCoverImage()
         */
        
        $this->meta['coverimage'] = array(
            "id" => $Image->id,
            "title" => $Image->title,
            "sizes" => $Image->sizes,
            "url" => $Image->url instanceof Url ? $Image->url->getURLs() : $Image->url
        );
        
        $this->commit(); 
        
        return $this;
    }
    
    /**
     * Get the cover photo for this locomotive
     * @since Version 3.8.7
     * @return array
     * @todo Set the AssetProvider (requires creating AssetProvider)
     */
    
    public function getCoverImage() {
        
        return Utility\CoverImageUtility::getCoverImageOfObject($this);
        
    }
    
    /**
     * Check if this loco class has a cover image
     * @since Version 3.9
     * @return boolean
     */
    
    public function hasCoverImage() {
        
        return Utility\CoverImageUtility::hasCoverImage($this); 
        
    }
    
    /**
     * Get locomotive data as an associative array
     * @since Version 3.9
     * @return array
     */
    
    public function getArray() {
        return array(
            "id" => $this->id,
            "number" => $this->number,
            "name" => $this->name,
            "gauge" => $this->gauge,
            "status" => array(
                "id" => $this->status_id,
                "text" => strval(new Status($this->status_id))
            ),
            "manufacturer" => array(
                "id" => $this->manufacturer_id,
                "text" => $this->manufacturer
            ),
            "class" => $this->Class->getArray(),
            "url" => $this->url->getURLs()
        );
    }
    
    /**
     * Set the locomotive class
     * @since Version 3.9.1
     * @param \Railpage\Locos\LocoClass $LocoClass
     * @return \Railpage\Locos\Locomotive
     */
    
    public function setLocoClass(LocoClass $LocoClass) {
        $this->Class = $LocoClass;
        $this->class = $LocoClass;
        $this->class_id = $LocoClass->id;
        
        return $this;
    }
    
    /**
     * Set the locomotive gauge
     * @since Version 3.9.1
     * @param \Railpage\Locos\Gauge $Gauge
     * @return \Railpage\Locos\Locomotive
     */
    
    public function setGauge(Gauge $Gauge) {
        $this->gauge_id = $Gauge->id;
        $this->gauge = $Gauge->getArray(); 
        $this->gauge_formatted = (string) $Gauge;
        
        return $this;
    }
    
    /**
     * Get the gauge
     * @since Version 3.9.1
     * @return \Railpage\Locos\Gauge
     */
    
    public function getGauge() {
        return new Gauge($this->gauge_id);
    }
    
    /**
     * Set the manufacturer
     * @since Version 3.9.1
     * @param \Railpage\Locos\Manufacturer $Manufacturer
     * @return \Railpage\Locos\LocoClass
     */
    
    public function setManufacturer(Manufacturer $Manufacturer) {
        $this->manufacturer_id = $Manufacturer->id;
        $this->manufacturer = $Manufacturer->name;
        
        return $this;
    }
    
    /**
     * Get the loco manufacturer
     * @since Version 3.9.1
     * @return \Railpage\Locos\Manufacturer
     */
    
    public function getManufacturer() {
        
        return Factory::Create("Manufacturer", $this->manufacturer_id);
        
    }
    
    /**
     * Generate descriptive text
     * @since Version 3.9.1
     * @return string
     */
    
    public function generateDescription() {
        
        $mckey = sprintf(self::CACHE_KEY_DESC, $this->id); 
        
        if ($str = $this->Memcached->fetch($mckey)) {
            return $str;
        }
        
        $bits = array(); 
        
        /**
         * Built as... by...
         */
        
        $bits = Utility\LocomotiveUtility::getDescriptionBits_Manufacturer($this, $bits); 
        
        /**
         * Process the dates
         */
        
        $bits = Utility\LocomotiveUtility::getDescriptionBits_Dates($this, $bits); 
                    
        /**
         * The loco is currently...
         */
        
        $bits = Utility\LocomotiveUtility::getDescriptionBits_Status($this, $bits); 
        
        /**
         * Join it all together
         */
                    
        $str = trim(implode("", $bits)); 
        
        if (preg_match("/([a-zA-Z0-9]+)/", substr($str, -1))) {
            $str .= ".";
        }
        
        if (substr($str, -1) === ",") {
            $str = substr($str, 0, -1) . ".";
        }
        
        $this->Memcached->save($mckey, $str, strtotime("+1 year"));
        
        return $str;
    }
    
    /**
     * Echo this locomotive as a string
     * @since Version 3.9.1
     * @return string
     */
    
    public function __toString() {
        return (string) $this->number;
    }
}