lib/Locations/Location.php
<?php
/**
* Locations module
* @since Version 3.0
* @package Railpage
* @author Michael Greenhill
*/
namespace Railpage\Locations;
use Railpage\Place;
use Railpage\PlaceUtility;
use Exception;
use InvalidArgumentException;
use DateTime;
use Railpage\Images\Images;
use Railpage\Images\Image;
use Railpage\Images\MapImage;
use Railpage\ContentUtility;
use Railpage\Users\User;
use Railpage\Users\Factory as UserFactory;
use Railpage\Url;
/**
* Location class
* @since Version 3.3
*/
class Location extends Locations {
/**
* Registry cache key
* @since Version 3.9.1
* @const string REGISTRY_KEY
*/
const REGISTRY_KEY = "railpage.location=%d";
/**
* Status: inactive
* @since Version 3.9.1
* @const int STATUS_INACTIVE
*/
const STATUS_INACTIVE = 0;
/**
* Status: active
* @since Version 3.9.1
* @const int STATUS_ACTIVE
*/
const STATUS_ACTIVE = 1;
/**
* Location ID
* @since Version 3.0.1
* @version 3.0.1
* @var int $id
*/
public $id;
/**
* Location latitude
* @since Version 3.0.1
* @version 3.0.1
* @var string $lat
*/
public $lat;
/**
* Location longitude
* @since Version 3.0.1
* @version 3.0.1
* @var string $lon
*/
public $lon;
/**
* Location country
* @since Version 3.0.1
* @version 3.0.1
* @var string $country
*/
public $country;
/**
* Location region
* @since Version 3.0.1
* @version 3.0.1
* @var string $region
*/
public $region;
/**
* Location locality
* @since Version 3.0.1
* @version 3.0.1
* @var string $locality
*/
public $locality;
/**
* Location name
* @since Version 3.0.1
* @version 3.0.1
* @var string $name
*/
public $name;
/**
* Location description
* @since Version 3.0.1
* @version 3.0.1
* @var string $desc
*/
public $desc;
/**
* Location forum topic ID
* @since Version 3.0.1
* @version 3.0.1
* @var int topicid
*/
public $topicid;
/**
* Location map zoom
* @since Version 3.0.1
* @version 3.0.1
* @var int $zoom
*/
public $zoom;
/**
* Location active
* @since Version 3.0.1
* @version 3.0.1
* @var int $active
*/
public $active;
/**
* Location date added
* @since Version 3.0.1
* @version 3.0.1
* @var int $date_added
*/
public $date_added;
/**
* Location date modified
* @since Version 3.0.1
* @version 3.0.1
* @var int $date_modified
*/
public $date_modified;
/**
* Location author user ID
* @since Version 3.0.1
* @version 3.0.1
* @var int $user_id
*/
public $user_id;
/**
* Location author username
* @since Version 3.6
* @version 3.6
* @var string $username
*/
public $username;
/**
* Location number of likes
* @since Version 3.5
* @version 3.5
* @var int $likes
*/
public $likes;
/**
* URL Slug
* @since Version 3.7.5
* @var string $slug
*/
public $slug;
/**
* Railway type and traffic
* @since Version 3.7.5
* @var string $traffic
*/
public $traffic;
/**
* Environment
* @since Version 3.7.5
* @var string $environment
*/
public $environment;
/**
* Directions - driving
* @since Version 3.7.5
* @var string $directions_driving
*/
public $directions_driving;
/**
* Directions - car parking
* @since Version 3.7.5
* @var string $directions_parking
*/
public $directions_parking;
/**
* Directions - public transport
* @since Version 3.7.5
* @var string $directions_pt
*/
public $directions_pt;
/**
* Amenities
* @since Version 3.7.5
* @var string $amenities
*/
public $amenities;
/**
* URL
* @since Version 3.8
* @var string $url
*/
public $url;
/**
* Geographic region containing this location
* @since Version 3.8.7
* @var \Railpage\Locations\Region $Region
*/
public $Region;
/**
* Geoplace ID
* @since Version 3.9.1
* @var int $geoplace_id
*/
private $geoplace_id;
/**
* Constructor
* @since Version 3.0.1
* @version 3.7.5
* @param int $location_id
*/
public function __construct($location_id = false) {
parent::__construct();
if ($id = filter_var($location_id, FILTER_VALIDATE_INT)) {
$this->id = $id;
$this->load();
} elseif (is_string($location_id) && $id = filter_var($location_id, FILTER_SANITIZE_STRING)) {
$this->id = $this->db->fetchOne("SELECT id FROM location WHERE slug = ?", $id);
if (filter_var($this->id, FILTER_VALIDATE_INT)) {
$this->load();
}
}
}
/**
* Load the location data
* @since Version 3.0.1
* @version 3.7.5
* @return boolean
*/
private function load() {
$this->mckey = sprintf("railpage:locations.location=%d", $this->id);
if (!$row = $this->Memcached->fetch($this->mckey)) {
$query = "SELECT location.*, u.username, count(locations_like.location_id) AS likes FROM location LEFT JOIN locations_like ON location.id = locations_like.location_id LEFT JOIN nuke_users AS u ON u.user_id = location.user_id WHERE location.id = ?";
$row = $this->db->fetchRow($query, $this->id);
}
if (!isset($row) || !is_array($row) || count($row) === 0 || !filter_var($row['id'], FILTER_VALIDATE_INT)) {
throw new Exception("Unable to fetch data for location ID " . $this->id);
}
$this->lat = $row['lat'];
$this->lon = $row['long'];
$this->country = $row['country'];
$this->region = $row['region'];
$this->locality = $row['locality'];
$this->name = $row['name'];
$this->desc = $row['desc'];
$this->topicid = $row['topicid'];
$this->zoom = $row['zoom'];
$this->active = $row['active'];
$this->date_added = $row['date_added'];
$this->date_modified= $row['date_modified'];
$this->user_id = $row['user_id'];
$this->slug = $row['slug'];
$this->traffic = $row['traffic'];
$this->environment = $row['environment'];
$this->amenities = $row['amenities'];
$this->directions_pt = $row['directions_pt'];
$this->directions_driving = $row['directions_driving'];
$this->directions_parking = $row['directions_parking'];
$this->geoplace_id = isset($row['geoplace_id']) ? $row['geoplace_id'] : 0;
/**
* If the URL slug is empty, let's create one now
*/
if (empty($this->slug) && filter_var($this->id, FILTER_VALIDATE_INT)) {
$this->createSlug();
$this->commit();
}
if (isset($row)) {
$this->Region = new Region($row['country'], $row['region_slug']);
}
if (!empty($this->lat) && !empty($this->lon)) {
$this->Image = new MapImage($this->lat, $this->lon);
$this->Image->title = $this->name . ", " . $this->Region->Country->name;
}
$this->url = new Url(sprintf("%s/%s", $this->makeRegionPermalink(), $this->slug));
$this->url->suncalc = sprintf("http://suncalc.net/#/%s,%s,%d/%s/%s/", $this->lat, $this->lon, $this->zoom, (new DateTime)->format("Y.m.d"), (new DateTime)->format("H:m"));
$this->url->googlemaps = sprintf("http://maps.google.com.au/maps?q=%s,%s", $this->lat, $this->lon);
#$this->url->staticmap = sprintf("https://api.mapbox.com/v4/mapbox.streets-basic/pin-l-camera+285A98(%s,%s)/%s,%s,%d/800x600.png?access_token=%s", $this->lon, $this->lat, $this->lon, $this->lat, 15, "pk.eyJ1IjoiZG9jdG9yamJlYW0iLCJhIjoiU1QtYnktZyJ9.R2caqw77mb_7VU5M9-gPug");
$this->url->staticmap = sprintf("https://maps.googleapis.com/maps/api/staticmap?center=%s,%s&key=%s&zoom=%d&size=%dx%d&sensor=false&markers=color:red|%s,%s", $this->lat, $this->lon, $this->Config->Google->API_Key, $this->zoom, $this->Config->OpenGraphMapWidth, $this->Config->OpenGraphMapHeight, $this->lat, $this->lon);
if ($this->geoplace_id == 0) {
$this->updateGeoplace();
}
}
/**
* Create a URL slug
* @since Version 3.7.5
* @return void
*/
private function createSlug() {
$proposal = ContentUtility::generateUrlSlug($this->name);
$result = $this->db->fetchAll("SELECT id FROM location WHERE slug = ?", $proposal);
if (count($result)) {
$proposal .= count($result);
}
$this->slug = $proposal;
}
/**
* Validate the location before committing changes
* @since Version 3.0.1
* @version 3.0.1
* @return boolean
* @todo var validation
*/
private function validate() {
if (!filter_var($this->lat, FILTER_VALIDATE_FLOAT)) {
throw new Exception("Cannot validate location - no latitude value");
}
if (!filter_var($this->lon, FILTER_VALIDATE_FLOAT)) {
throw new Exception("Cannot validate location - no longitude value");
}
/*
if (empty(filter_var($this->country, FILTER_SANITIZE_STRING))) {
throw new Exception("Cannot validate location - no country value");
}
if (empty(filter_var($this->region, FILTER_SANITIZE_STRING))) {
throw new Exception("Cannot validate location - no region value");
}
*/
if (empty(filter_var($this->name, FILTER_SANITIZE_STRING))) {
throw new Exception("Cannot validate location - no name specified");
}
if (empty(filter_var($this->desc, FILTER_SANITIZE_STRING))) {
throw new Exception("Cannot validate location - no description specified");
}
$nulls = [ "traffic", "environment", "amenities", "directions_pt", "directions_driving", "directions_parking" ];
foreach ($nulls as $var) {
if (is_null($this->$var)) {
$this->$var = "";
}
}
if (!filter_var($this->zoom, FILTER_VALIDATE_INT)) {
$this->zoom = 12;
}
if (!filter_var($this->active)) {
$this->active = self::STATUS_INACTIVE;
}
if (empty($this->slug)) {
$this->createSlug();
}
if (!filter_var($this->topicid, FILTER_VALIDATE_INT)) {
$this->topicid = 0;
}
$this->geocode();
return true;
}
/**
* Set the geocode data for this location
* @since Version 3.9.1
* @return void
*/
private function geocode() {
if (!empty($this->region) && !empty($this->country) && !empty($this->locality)) {
#return;
}
$woe = PlaceUtility::LatLonWoELookup($this->lat, $this->lon);
$woe = PlaceUtility::formatWoE($woe);
$this->country = $woe['country_code'];
$this->region = $woe['region_code'];
$this->locality = $woe['neighbourhood'];
$this->neighbourhood = null; // $this->neighbourhood is ignored
if (empty($this->region)) {
$this->region = $woe['region'];
}
/**
* Shit's a little bit broken, it's ok
*/
if (empty($this->region) || empty($this->country)) {
$woe = PlaceUtility::LatLonWoELookup($this->lat, $this->lon, true); // force lookup, ignore any cached data
$woe = PlaceUtility::formatWoE($woe);
$this->region = $woe['region_code'];
$this->country = $woe['country_code'];
}
if (empty($this->region)) {
$this->region = strtoupper($woe['region_name']);
}
#printArray($this->region);die;
return;
/*
// Fetch geodata and populate the vars
//$url = "http://maps.google.com/maps/geo?q=".$this->lat.",".$this->lon."&output=json&sensor=false";
$url = "http://maps.googleapis.com/maps/api/geocode/json?latlng=" . $this->lat . "," . $this->lon . "&sensor=false";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($ch);
$jsondata = json_decode($data, true);
curl_close($ch);
if (isset($jsondata['results'][0]['address_components'])) {
$row = $jsondata['results'][0]['address_components'];
}
if (isset($jsondata['Placemark']) && $area = $jsondata['Placemark'][0]['AddressDetails']['Country']) {
$this->country = $area['CountryNameCode'];
$this->region = isset($area['AdministrativeArea']['AdministrativeAreaName']) ? $area['AdministrativeArea']['AdministrativeAreaName'] : NULL;
$this->locality = isset($area['AdministrativeArea']['Locality']['LocalityName']) ? $area['AdministrativeArea']['Locality']['LocalityName'] : NULL;
$this->neighbourhood = isset($area['CountryNameCode']['someotherarea']) ? $area['CountryNameCode']['someotherarea'] : NULL;
} elseif (isset($row)) {
// Loop through the results and try to populate the object
foreach ($row as $area) {
if ($area['types'][0] == "country") {
$this->country = $area['short_name'];
}
if ($area['types'][0] == "administrative_area_level_2") {
$this->region = $area['long_name'];
}
if ($area['types'][0] == "administrative_area_level_3") {
$this->neighbourhood = $area['long_name'];
}
if ($area['types'][0] == "locality") {
$this->locality = $area['long_name'];
}
}
}
if (empty($this->country) || empty($this->region) || empty($this->locality)) {
// Google doesn't give data in a consistent bloody format - go here instead
$url = sprintf("http://www.geoplugin.net/extras/location.gp?lat=%s&long=%s&format=php", $this->lat, $this->lon);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$geodata = curl_exec($ch);
$geodata = unserialize($geodata);
curl_close($ch);
$this->country = $geodata['geoplugin_countryCode'];
$this->region = $geodata['geoplugin_region'];
$this->locality = $geodata['geoplugin_place'];
$this->neighbourhood = NULL;
}
return;
*/
}
/**
* Commit changes to the database
* @since Version 3.0.1
* @version 3.7.5
* @return boolean
*/
public function commit() {
/**
* Fill in the gaps with the new Place object
*/
if (empty($this->region) || empty($this->country)) {
$Place = Place::Factory($this->lat, $this->lon);
if (!empty($Place->Country->code)) {
$this->country = $Place->Country->code;
}
if (!empty($Place->Region->name)) {
$this->region = $Place->Region->name;
}
}
$find = array(
"@Victoria@i",
"@New South Wales@i",
"@Queensland@i",
"@South Australia@i",
"@Tasmania@i",
"@Northern Territory@i",
"@Western Australia@i",
"@Australian Capital Territory@i"
);
$replace = array(
"VIC",
"NSW",
"QLD",
"SA",
"TAS",
"NT",
"WA",
"ACT"
);
$this->region = preg_replace($find, $replace, $this->region);
// Defaults for new locations
if (empty($this->id)) {
$this->active = 0;
$this->date_added = time();
$this->date_modified = time();
}
/**
* Validate the data
*/
$this->validate();
$data = array(
"lat" => $this->lat,
"long" => $this->lon,
"country" => $this->country,
"region" => $this->region,
"region_slug" => $this->makeRegionSlug(),
"locality" => $this->locality,
"name" => trim($this->name),
"desc" => $this->desc,
"topicid" => $this->topicid,
"zoom" => $this->zoom,
"active" => $this->active,
"date_added" => $this->date_added,
"date_modified" => $this->date_modified,
"user_id" => $this->user_id,
"slug" => $this->slug,
"traffic" => $this->traffic,
"environment" => $this->environment,
"directions_driving" => $this->directions_driving,
"directions_parking" => $this->directions_parking,
"directions_pt" => $this->directions_pt,
"amenities" => $this->amenities
);
if (filter_var($this->id, FILTER_VALIDATE_INT) && $this->id > 0) {
$where = array(
"id = ?" => $this->id
);
$this->db->update("location", $data, $where);
$this->Memcached->delete($this->mckey);
} else {
$this->db->insert("location", $data);
$this->id = $this->db->lastInsertId();
$this->Memcached->delete("railpage:locations.newest");
$this->mckey = sprintf("railpage:locations.location=%d", $this->id);
}
$this->updateGeoplace();
}
/**
* Update the geoplace linked to this location
* @since Version 3.9.1
* @return void
*/
private function updateGeoplace() {
$id = PlaceUtility::findGeoPlaceID($this->lat, $this->lon);
if ($id != $this->geoplace_id) {
$data = [ "geoplace" => $id ];
$where = [ "id = ?" => $this->id ];
$this->db->update("location", $data, $where);
$this->Memcached->delete($this->mckey);
}
return;
}
/**
* Get photos for a given location
*
* This function uses mysqli::multi_query() as it was buggering up all subsequent SQL queries on the page
* @since Version 3.0
* @version 3.7.5
* @param int $num
* @param int $start
*/
public function getPhotosForSite($num = 10, $start = 0) {
$Place = new Place($this->lat, $this->lon);
return $Place->getPhotosFromSphinx($num);
if (!$this->id || !$this->db) {
return false;
}
$cachekey = sprintf("%s;photos;num=%s;start=%s", $this->mckey, intval($num), intval($start));
if ($result = $this->Memcached->fetch($cachekey)) {
return $result;
}
$return = array();
// Ditch the stored procedure. Just do it through the database connection
$lat = $this->lat;
$lon = $this->lon;
$query = "SELECT flickr_geodata.*, 3956 * 2 * ASIN(SQRT(POWER(SIN((" . $lat . " - flickr_geodata.lat) * pi() / 180 / 2), 2) + COS(" . $lat . " * pi() / 180) * COS(" . $lat . " * pi() / 180) * POWER(SIN((" . $lon . " - flickr_geodata.lon) * pi() / 180 / 2), 2))) AS distance
FROM flickr_geodata, location
WHERE location.id = " . $this->id . "
AND flickr_geodata.lon BETWEEN (
" . $lon . " - " . $this->photoRadius . " / abs(cos(radians(" . $lat . ")) * 69)
) AND (
" . $lon . " + " . $this->photoRadius . " / abs(cos(radians(" . $lat . ")) * 69)
)
AND flickr_geodata.lat BETWEEN (
" . $lat . " - (" . $this->photoRadius . " / 69)
) AND (
" . $lat . " + (" . $this->photoRadius . " / 69)
)
HAVING distance < " . $this->photoRadius . "
ORDER BY distance
LIMIT ?";
$params = array(
$num
);
$return = array();
$square_size = 180;
foreach ($this->db->fetchAll($query, $params) as $data) {
$key = $data['photo_id'];
$return[$key]['size_sq'] = RP_PROTOCOL . "://" . filter_input(INPUT_SERVER, "HTTP_HOST", FILTER_SANITIZE_STRING) . "/image_resize.php?q=90&w=" . $square_size . "&h=" . $square_size . "&square=true&image=" . str_replace("?zz=1", "", $data['size4']);
$return[$key]['size_sq_w'] = $square_size;
$return[$key]['size_sq_h'] = $square_size;
$data['id'] = $data['photo_id'];
$data['url_sq'] = $data['size0'];
$data['width_sq'] = $data['size0_w'];
$data['height_sq'] = $data['size0_h'];
$data['url_t'] = $data['size1'];
$data['width_t'] = $data['size1_w'];
$data['height_t'] = $data['size1_h'];
$data['url_s'] = $data['size2'];
$data['width_s'] = $data['size2_w'];
$data['height_s'] = $data['size2_h'];
$data['url_q'] = NULL;
$data['width_q'] = NULL;
$data['height_q'] = NULL;
$data['url_m'] = $data['size3'];
$data['width_m'] = $data['size3_w'];
$data['height_m'] = $data['size3_h'];
$data['url_n'] = $data['size6'];
$data['width_n'] = $data['size6_w'];
$data['height_n'] = $data['size6_h'];
$data['url_z'] = $data['size4'];
$data['width_z'] = $data['size4_w'];
$data['height_z'] = $data['size4_h'];
$data['url_l'] = $data['size5'];
$data['width_l'] = $data['size5_w'];
$data['height_l'] = $data['size5_h'];
$data['url_c'] = $data['size7'];
$data['width_c'] = $data['size7_w'];
$data['height_c'] = $data['size7_h'];
$data['url_o'] = $data['size8'];
$data['width_c'] = $data['size8_w'];
$data['height_c'] = $data['size8_h'];
$data['nicetags'] = explode(" ", $data['tags']);
$return[$key] = $data;
}
$this->Memcached->save($cachekey, $return, strtotime("+1 day"));
return $return;
}
/**
* Check if user has "liked" this location
* @since Version 3.0
* @param int|bool $user_id
* @return boolean
*/
public function doesUserLike($user_id = false) {
if ($user_id instanceof User) {
$user_id = $user_id->id;
}
if (!filter_var($user_id, FILTER_VALIDATE_INT)) {
return false;
}
$query = "SELECT * FROM locations_like WHERE location_id = ? AND user_id = ?";
if ($row = $this->db->fetchRow($query, array($this->id, $user_id))) {
return true;
}
return false;
}
/**
* Recommend a location
* @since Version 3.0
* @param int|bool $user_id
* @return boolean
*/
public function recommend($user_id = false) {
if ($user_id instanceof User) {
$user_id = $user_id->id;
}
if (!filter_var($user_id, FILTER_VALIDATE_INT)) {
throw new InvalidArgumentException("No user ID provided");
}
if ($this->doesUserLike($user_id)) {
return false;
}
$data = array(
"location_id" => $this->id,
"user_id" => $user_id
);
$this->db->insert("locations_like", $data);
return true;
}
/**
* Approve a location
* @since Version 3.6
* @return boolean
*/
public function approve() {
if (empty($this->id)) {
throw new Exception("Cannot approve location - no location created yet");
return false;
}
$this->active = 1;
$this->date_modified = time();
$this->commit();
}
/**
* Reject / delete a location
* @since Version 3.6
* @return boolean
*/
public function reject() {
if (empty($this->id)) {
throw new Exception("Cannot reject location - no location created yet");
return false;
}
$where = array(
"id = ?" => $this->id
);
$this->db->delete("location", $where);
return true;
}
/**
* Get contributors of this location
* @since Version 3.7.5
* @return array
*/
public function getContributors() {
$query = "SELECT username FROM nuke_users WHERE user_id = ?";
$return = array();
$return[$this->user_id] = $this->db->fetchOne($query, $this->user_id);
$query = "SELECT DISTINCT l.user_id, u.username FROM log_general AS l LEFT JOIN nuke_users AS u ON u.user_id = l.user_id WHERE l.module = ? AND l.key = ? AND l.value = ? AND l.user_id != ?";
foreach ($this->db->fetchAll($query, array("locations", "id", $this->id, $this->user_id)) as $row) {
$return[$row['user_id']] = $row['username'];
}
return $return;
}
/**
* Get dates for this location
* @since Version 3.8.7
* @return array
*/
public function getDates() {
return $this->db->fetchAll("SELECT * FROM location_date WHERE location_id = ?", $this->id);
}
/**
* Get an array of this data
* @since Version 3.9.1
* @return array
*/
public function getArray() {
return array(
"id" => $this->id,
"lat" => $this->lat,
"lon" => $this->lon,
"zoom" => $this->zoom,
"name" => $this->name,
"desc" => $this->desc,
"traffic" => is_object($this->traffic) ? $this->traffic->__toString() : $this->traffic,
"environment" => is_object($this->environment) ? $this->environment->__toString() : $this->environment,
"directions_driving" => is_object($this->directions_driving) ? $this->directions_driving->__toString() : $this->directions_driving,
"directions_parking" => is_object($this->directions_parking) ? $this->directions_parking->__toString() : $this->directions_parking,
"directions_pt" => is_object($this->directions_pt) ? $this->directions_pt->__toString() : $this->directions_pt,
"amenities" => is_object($this->amenities) ? $this->amenities->__toString() : $this->amenities,
"url" => $this->url instanceof Url ? $this->url->getURLs() : array("url" => $this->url)
);
}
/**
* Get this object as a string
* @since Version 3.9.1
* @return string
*/
public function __toString() {
return $this->name;
}
}