lib/Railcams/Camera.php
<?php
/**
* Railcam object
* @since Version 3.4
* @package Railpage
* @author Michael Greenhill
*/
namespace Railpage\Railcams;
use Exception;
use DateTime;
use DateTimeZone;
use Railpage\Url;
use Railpage\Debug;
use flickr_railpage;
use Rezzza\Flickr\Metadata;
use Rezzza\Flickr\ApiFactory;
use Rezzza\Flickr\Http\GuzzleAdapter;
use GuzzleHttp\Client;
use DOMDocument;
use DOMXpath;
/**
* Railcam class
*/
class Camera extends Railcams {
/**
* Camera ID
* @since Version 3.4
* @var int $id
*/
public $id;
/**
* Railcam type ID
* @since Version 3.8
* @var int $type_id
*/
public $type_id;
/**
* Railcam type object
* @since Version 3.8
* @var object $type;
*/
public $type;
/**
* Permalink
* @since Version 3.4
* @var string $permalink
*/
public $permalink;
/**
* Camera name
* @since Version 3.4
* @var string $name
*/
public $name;
/**
* Latitude
* @since Version 3.4
* @var string $lat
*/
public $lat;
/**
* Longitude
* @since Version 3.4
* @var string $lon
*/
public $lon;
/**
* Camera description
* @since Version 3.4
* @var string $desc
*/
public $desc;
/**
* Flickr NSID
* @since Version 3.4
* @var string $nsid
*/
public $nsid;
/**
* Camera timezone
* @since Version 3.4
* @var string $timezone
*/
public $timezone = "";
/**
* Route ID
* @since Version 3.4
* @var int $route_id
*/
public $route_id;
/**
* Flickr OAuth token
* @since Version 3.5
* @var string $flickr_oauth_token
*/
public $flickr_oauth_token = "";
/**
* Flickr OAuth secret
* @since Version 3.5
* @var string $flickr_oauth_secret
*/
public $flickr_oauth_secret = "";
/**
* Video store URL
* @since Version 3.5
* @var string $video_store_url
*/
public $video_store_url = "";
/**
* Live image URL
* @since Version 3.5
* @var string $live_image_url
*/
public $live_image_url = "";
/**
* Live video URL
* @since Version 3.7.5
* @var string $live_video_url
*/
public $live_video_url = "";
/**
* What is to the left of camera - eg Melbourne or Stawell
* @since Version 3.8
* @var string $left
*/
public $left;
/**
* What is to the right of camera - eg Melbourne or Stawell
* @since Version 3.8
* @var string $right
*/
public $right;
/**
* URL to this railcam
* @since Version 3.8.7
* @var string $url
*/
public $url;
/**
* Meta data for this camera
* @since Version 3.10.0
* @var array $meta
*/
public $meta;
/**
* Photo provider
* @since Version 3.9
* @var object $Provider
*/
public $Provider;
/**
* Constructor
* @since Version 3.4
* @param int $id
*/
public function __construct($id = null) {
parent::__construct();
$this->GuzzleClient = new Client;
if ($id != null) {
$this->id = $id;
// Fetch Railcam data
$this->load();
}
}
/**
* Load Railcam data from database
* @since Version 3.4
* @return boolean
*/
public function load() {
if (empty($this->id) || $this->id === false) {
throw new Exception("Cannot load Railcam - empty or invalid ID given");
}
if (!filter_var($this->id, FILTER_VALIDATE_INT)) {
$this->id = $this->db->fetchOne("SELECT id FROM railcams WHERE permalink = ?", $this->id);
}
$query = "SELECT * FROM railcams WHERE id = ?";
if (!$row = $this->db->fetchRow($query, $this->id)) {
throw new Exception("Could not find railcam ID " . $this->id . " in the database");
}
$this->name = $row['name'];
$this->type_id = $row['type_id'];
$this->lat = $row['lat'];
$this->lon = $row['lon'];
$this->nsid = $row['nsid'];
$this->desc = $row['desc'];
$this->timezone = $row['timezone'];
$this->permalink = $row['permalink'];
$this->route_id = $row['route_id'];
$this->video_store_url = $row['video_store_url'];
$this->live_image_url = $row['live_image_url'];
$this->live_video_url = $row['live_video_url'];
$this->flickr_oauth_token = $row['flickr_oauth_token'];
$this->flickr_oauth_secret = $row['flickr_oauth_secret'];
$this->right = $row['right'];
$this->left = $row['left'];
$this->url = new Url(sprintf("%s/%s", $this->Module->url, $row['permalink']));
$this->url->edit = sprintf("%s/edit", $this->url->url);
$this->url->archive = sprintf("%s/archive", $this->url->url);
$this->url->gallery = sprintf("%s/flickr/tag/railpage:railcam=%d", RP_WEB_ROOT, $this->id);
$this->url->live = sprintf("%s/live", $this->url->url);
$this->url->recent = sprintf("%s/recent", $this->url->url);
$this->url->photo = sprintf("%s/photo/", $this->url->url);
$this->url->flickr_auth = sprintf("%s/authenticate", $this->url->url);
$this->url->authenticate = $this->url->flickr_auth;
$this->url->edit_storage = sprintf("%s?mode=camera.storage.config", $this->url->url);
$this->meta = isset($row['meta']) ? json_decode($row['meta'], true) : array();
switch ($row['provider']) {
case "Flickr" :
$params = array(
"oauth_token" => $this->flickr_oauth_token,
"oauth_secret" => $this->flickr_oauth_secret,
"api_key" => RP_FLICKR_API_KEY
);
}
$provider = sprintf("\\Railpage\\Railcams\\Provider\\%s", $row['provider']);
$this->Provider = new $provider($params);
if (filter_var($this->type_id, FILTER_VALIDATE_INT) && $this->type_id > 0) {
$this->type = new Type($this->type_id);
}
}
/**
* Get photos from this camera
* @since Version 3.4
* @return array
* @param int $items_per_page
* @param int $page
* @param string $extras
* @param boolean $sort
*/
public function photos($items_per_page, $page, $extras, $sort = true) {
$url = "https://api.flickr.com/services/rest/?method=flickr.people.getPublicPhotos&api_key=" . RP_FLICKR_API_KEY . "&user_id=" . $this->nsid . "&extras=" . $extras . "&per_page=" . $items_per_page . "&page=" . $page . "&format=json&nojsoncallback=1";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$data = curl_exec($ch);
curl_close($ch);
$data = json_decode($data, true);
$photos = array();
// Localise the time to the camera's timezone
$now = new DateTime("now", new DateTimeZone($this->timezone));
$yesterday = new DateTime("yesterday", new DateTimeZone($this->timezone));
if ($sort) {
foreach ($data['photos']['photo'] as $photo) {
$timestamp = DateTime::createFromFormat("Y-m-d H:i:s", $photo['datetaken'], new DateTimeZone($this->timezone));
$date = $timestamp->format("Y-m-d");
$hour = $timestamp->format("H");
if ($date === $yesterday->format("Y-m-d")) {
if (!isset($photos[$date][$hour]['count'])) {
$photos[$date][$hour]['count'] = 1;
} else {
$photos[$date][$hour]['count']++;
}
$photos[$date][$hour]['photos'][] = $photo;
}
}
foreach ($photos as $day => $data) {
ksort($photos[$day]);
}
return $photos;
} else {
return $data['photos'];
}
}
/**
* Validate changes to this railcam
* @since Version 3.5
* @return boolean
*/
public function validate() {
if (empty($this->name)) {
throw new Exception("Could not validate railcam - name cannot be empty");
}
if (empty($this->nsid)) {
$this->nsid = "";
}
if (empty($this->permalink)) {
throw new Exception("Could not validate railcam - URL slug (permalink) cannot be empty");
}
if (!$this->Provider instanceof ProviderInterface) {
$this->Provider = new Provider\Flickr;
}
return true;
}
/**
* Commit changes to this railcam
* @since Version 3.5
* @return boolean
*/
public function commit() {
$this->validate();
$data = array(
"provider" => $this->Provider->getProviderName(),
"permalink" => $this->permalink,
"lat" => $this->lat,
"lon" => $this->lon,
"name" => $this->name,
"desc" => $this->desc,
"nsid" => $this->nsid,
"route_id" => $this->route_id,
"timezone" => $this->timezone,
"flickr_oauth_token" => $this->flickr_oauth_token,
"flickr_oauth_secret" => $this->flickr_oauth_secret,
"video_store_url" => $this->video_store_url,
"live_image_url" => $this->live_image_url,
"live_video_url" => $this->live_video_url,
"type_id" => $this->type_id,
"left" => $this->left,
"right" => $this->right,
"meta" => is_array($this->meta) ? json_encode($this->meta) : $this->meta
);
if (filter_var($this->id, FILTER_VALIDATE_INT)) {
// Update
$where = array(
"id = ?" => $this->id
);
$this->db->update("railcams", $data, $where);
return true;
}
// Insert
$this->db->insert("railcams", $data);
$this->id = $this->db->lastInsertId();
return true;
}
/**
* List videos in video store
* @since Version 3.5
* @return array
* @param int $num
*/
public function getVideos($num = 25) {
if (!$this->video_store_url) {
return false;
}
$videos = array();
$index = NULL;
if (substr($this->video_store_url, -1) != "/") {
$this->video_store_url .= "/";
}
$response = $this->GuzzleClient->get($this->video_store_url);
if ($response->getStatusCode() != 200) {
throw new Exception(sprintf("Failed to fetch videos from railcam: Error %s", $response->getStatusCode()));
}
$index = $response->getBody();
$doc = new DOMDocument();
$doc->loadHTML($index);
$xpath = new DOMXpath($doc);
$nodes = $xpath->query('//a');
$i = 0;
foreach($nodes as $node) {
$url = $node->getAttribute('href');
if ($i < $num && $url != "/" && substr($url, 0, 3) != "?C=") {
if (!strstr($url, $this->video_store_url)) {
$url = $this->video_store_url.$url;
}
$videos[] = $url;
$i++;
}
}
return $videos;
}
/**
* List private photos in Flickr using OAuth
* @since Version 3.5
* @param int $items_per_page
* @param int $page_num
* @param \DateTime|int $date_from
* @param \DateTime|int $date_to
* @return array
*/
public function getPhotos($items_per_page = 25, $page_num = 1, $date_from = false, $date_to = false) {
if (!empty($this->flickr_oauth_secret) && !empty($this->flickr_oauth_token)) {
// Fetch photos using OAuth
$f = new flickr_railpage(RP_FLICKR_API_KEY);
$f->oauth_token = $this->flickr_oauth_token;
$f->oauth_secret = $this->flickr_oauth_secret;
$f->cache = false;
$extras = "description,date_upload,date_taken,owner_name,original_format,last_update,geo,tags,machine_tags,o_dims,views,media,url_sq,url_t,url_s,url_q,url_m,url_n,url_z,url_c,url_l,url_o";
$args = array(
"extras" => $extras,
"per_page" => $items_per_page,
"page" => $page_num
);
if ($date_from != false) {
if (!$date_from instanceof DateTime) {
$date_from = new DateTime($date_from);
}
$args['min_taken_date'] = $date_from->format("Y-m-d 00:00:00");
}
if ($date_to != false) {
if (!$date_to instanceof DateTime) {
$date_to = new DateTime($date_to);
}
$args['max_taken_date'] = $date_to->format("Y-m-d 23:59:59");
}
$photos = $f->people_getPhotos(
$this->nsid,
$args
);
$return = $photos['photos'];
} else {
// Fetch photos using public Flickr APIs
$extras = "description,date_upload,date_taken,owner_name,icon_server,original_format,last_update,geo,tags,machine_tags,o_dims,views,media,path_alias,url_sq,url_t,url_s,url_q,url_m,url_n,url_z,url_c,url_l,url_o";
$photos = $this->photos($items_per_page, $page_num, $extras, false);
$return = $photos;
}
foreach ($return['photo'] as $id => $row) {
$return['photo'][$id]['time_relative'] = relative_date($row['dateupload']);
$return['photo'][$id]['description'] = isset($row['description']['_content']) ? $row['description']['_content'] : NULL;
$return['photo'][$id]['tags'] = explode(" ", $row['tags']);
// To-do: check for locos and liveries
}
return $return;
}
/**
* Get latest photo from this railcam
* @since Version 3.10.0
* @return \Railpage\Railcams\Photo
* @param boolean $update Update cached data if it's stale
*/
public function getLatest($update = true) {
if ($footage = $this->getLatestFootage("image")) {
return [
"id" => $footage['id'],
"title" => "",
"description" => "",
"dates" => [
"taken" => $footage['datestored']
],
"sizes" => [
"original" => [
"source" => $footage['url']['original']
]
]
];
}
$mckey = sprintf("railpage:railcam=%d;latest=1", $this->id);
/**
* Shitty, hacky way to handle Memcached expiry bug on Debian
*/
$mckey_age = $mckey . ";expiry";
$exp = $this->Memcached->fetch($mckey_age);
if ($update && (!$exp || $exp < time())) {
$this->Memcached->delete($mckey);
}
/**
* Fetch from Memcached, or load from API
*/
if (!$latest = $this->Memcached->fetch($mckey)) {
$latest = $this->getPhotos(1);
Debug::LogCLI("Fetched " . count($latest['photo']) . " photo(s)");
foreach ($latest['photo'] as $key => $photo) {
Debug::LogCLI("Processing photo...");
$photo['timezone'] = $this->timezone;
$Date = new DateTime($photo['datetaken']);
$Date->setTimezone(new DateTimeZone($this->timezone));
$photo['datetaken'] = $Date->format("c");
$latest['photo'][$key] = $photo;
}
Debug::LogCLI("Saving photo in Memcached");
$this->Memcached->save($mckey, $latest, 0);
Debug::LogCLI("Saving photo expiry in Memcached");
$this->Memcached->save($mckey_age, strtotime("+5 minutes"), 0);
}
return $this->getPhoto($latest['photo'][0]['id']);
}
/**
* Return a new Railpage\Railcams\Photo object
* @since Version 3.9
* @param int $photo_id
* @return \Railpage\Railcams\Photo
*/
public function getPhoto($photo_id = false) {
if (!filter_var($photo_id, FILTER_VALIDATE_INT)) {
throw new Exception(sprintf("Cannot fetch photo from railcam because \"%s\" is not a valid photo ID", $photo_id));
}
$Photo = (new Photo($photo_id))->setProvider($this->Provider)->setCamera($this)->load();
return $Photo;
}
/**
* Get photo info and sizes
* @since Version 3.5
* @param int $photo_id
* @return array
*/
public function getPhotoLegacy($photo_id = false) {
if (!$photo_id) {
throw new Exception("Cannot fetch photo info and sizes - no photo ID given");
return false;
}
$mckey = "railpage:railcam.photo.id=" . $photo_id;
#deleteMemcacheObject($mckey);
if ($return = getMemcacheObject($mckey)) {
$return['photo']['time_relative'] = relative_date($return['photo']['dateuploaded']);
return $return;
} else {
$use_rezzza = false;
if ($use_rezzza) {
$metadata = new Metadata(RP_FLICKR_API_KEY, RP_FLICKR_API_SECRET);
$metadata->setOauthAccess($this->flickr_oauth_token, $this->flickr_oauth_secret);
$factory = new ApiFactory($metadata, new GuzzleAdapter());
$photo_info = $factory->call('flickr.photos.getInfo', array(
'photo_id' => $photo_id,
));
if ($photo_info) {
$photo_sizes = $factory->call('flickr.photos.getSizes', array(
'photo_id' => $photo_id
));
}
/**
* do stuff!
*/
if ($photo_info && $photo_sizes) {
$return = array();
/**
* Photo info
*/
foreach ($photo_info->photo->attributes() as $a => $b) {
$return['photo'][$a] = $b->__toString();
}
foreach ($photo_info->photo->children() as $element) {
foreach ($element->attributes() as $a => $b) {
$return['photo'][$element->getName()][$a] = $b->__toString();
}
foreach ($element->children() as $child) {
foreach ($child->attributes() as $a => $b) {
$return['photo'][$element->getName()][$child->getName()][$a] = $b->__toString();
}
foreach ($child->children() as $blah) {
$return['photo'][$element->getName()][$child->getName()][$blah->getName()][$a] = $b->__toString();
foreach ($blah->attributes() as $a => $b) {
$return['photo'][$element->getName()][$child->getName()][$blah->getName()][$a] = $b->__toString();
}
}
}
}
/**
* Photo sizes
*/
$i = 0;
foreach ($photo_sizes->sizes->size as $key => $element) {
foreach ($element->attributes() as $a => $b) {
$return['photo']['sizes'][$i][$a] = $b->__toString();
}
$i++;
}
}
return $return;
}
$f = new flickr_railpage(RP_FLICKR_API_KEY);
$f->oauth_token = $this->flickr_oauth_token;
$f->oauth_secret = $this->flickr_oauth_secret;
$f->cache = false;
$return = array();
if ($return = $f->photos_getInfo($photo_id)) {
$return['photo']['sizes'] = $f->photos_getSizes(
$photo_id
);
$return['photo']['time_relative'] = relative_date($return['photo']['dateuploaded']);
setMemcacheObject($mckey, $return, strtotime("+2 hours"));
}
return $return;
}
}
/**
* Get photo context
* @since Version 3.7.2
* @param int $photo_id
* @return array
*/
public function getContext($photo_id = false) {
if (!$photo_id) {
throw new Exception("Cannot get photo context - no photo ID given");
return false;
}
$f = new flickr_railpage(RP_FLICKR_API_KEY);
$f->oauth_token = $this->flickr_oauth_token;
$f->oauth_secret = $this->flickr_oauth_secret;
$f->cache = false;
$return = array();
$return = $f->photos_getContext(
$photo_id
);
/*
$return['photo']['sizes'] = $f->photos_getSizes(
$photo_id
);
$return['photo']['time_relative'] = relative_date($return['photo']['dateuploaded']);
*/
return $return;
}
/**
* Get years within the camera archive
* @since Version 3.9
* @return array
*/
public function getArchiveYears() {
}
/**
* Get months within the camera archive for a given year
* @since Version 3.9
* @param int $year
* @return array
*/
public function getArchiveMonths($year) {
}
/**
* Get a associative array of this railcam data in a standardised format
* @since Version 3.9
* @return array
*/
public function getArray() {
return [
"id" => $this->id,
"name" => $this->name,
"description" => $this->desc,
//"provider" => $this->Provider->getProviderName(),
"url" => $this->url->getURLs(),
"lat" => $this->lat,
"lon" => $this->lon,
"timezone" => $this->timezone,
"auth" => [
"token" => Authorisation::getAuthToken($this)
]
];
}
/**
* Get storage destinations for this railcam
* @since Version 3.10.0
* @return array
*/
public function getStorageDestinations() {
$query = "SELECT * FROM railcam_storage WHERE camera_id = ?";
$result = $this->db->fetchAll($query, $this->id);
foreach ($result as $k => $v) {
$result[$k]['config'] = json_decode($v['config'], true);
}
return $result;
}
/**
* Get latest footage from the database
* @since Version 3.10.0
* @param string $type
* @return array
*/
public function getLatestFootage($type = "image") {
$type = strtolower(trim($type));
if (!in_array($type, [ "image", "video" ])) {
throw new InvalidArgumentException($type . " is not a valid footage type");
}
$query = "SELECT id FROM railcam_footage WHERE type = ? AND railcam_id = ? ORDER BY datestored DESC LIMIT 1";
$id = $this->db->fetchOne($query, [ $type, $this->id ]);
if (!filter_var($id, FILTER_VALIDATE_INT)) {
return false;
}
$footageObject = new Footage($this, $id);
return $footageObject->getArray();
}
}