<?php

require_once('MDB2.php');

require_once('ReferenceData.interface.php');
class ReferenceData implements iReferenceData
{
	private $statistics;
	private $mdb2;
	private $range;
	private $mean;
	private $variance;

	public function __construct()
	{
		$this->statistics = array();
		$this->mdb2 =& MDB2::factory(DEFAULT_DSN);
		$this->range['Max_lat'] = (float)DEFAULT_LAT;
		$this->range['Min_lat'] = (float)DEFAULT_LAT;
		$this->range['Max_lon'] = (float)DEFAULT_LON;
		$this->range['Min_lon'] = (float)DEFAULT_LON;
		$this->mean = 0.0;
		$this->variance = 0.0;
	}

	public function generateStatistics($Coordinates, $Radius, $TimePeriod, $DayType, $WeatherType, $MinTrafficSeverity)
	{
		$this->generateRange($Coordinates, $Radius);
		$weather_coords = $this->queryDatabaseWeatherCoords($Coordinates);
		$this->queryDatabaseStats($TimePeriod, $DayType, $WeatherType, $MinTrafficSeverity, $weather_coords);
		if(isset($Coordinates[1]))
			$this->cropStatistics($Coordinates);
		$this->calcVariance();
	}

	private function generateRange($Coordinates, $Radius)
	{
		$this->range = $this->calcRange($Coordinates, $Radius);
	}

	private function queryDatabaseWeatherCoords($Coordinates)
	{
		$weatherQuery = '
				SELECT DISTINCT Latitude, Longitude
				FROM Wunderground_RSS_weather
				';

		$weatherResult = $this->mdb2->query($weatherQuery);
                $result = $weatherResult->fetchAll();

		foreach($result as &$value)
		{
			if(empty($min_dist))
			{
				$min_dist['dist'] = $this->calcDistance($Coordinates[0]['lat'], $Coordinates[0]['lon'], $value[0], $value[1]);
				$min_dist['lat'] = $value[0];
				$min_dist['lon'] = $value[1];
			}
			else
			{
				$candidate = $this->calcDistance($Coordinates[0]['lat'], $Coordinates[0]['lon'], $value[0], $value[1]);
				if (is_numeric($candidate) && $candidate < $min_dist['dist'])
				{
					$min_dist['dist'] = $candidate;
					$min_dist['lat'] = $value[0];
					$min_dist['lon'] = $value[1];
				}
			}
		}

		return $min_dist;
	}

	private function queryDatabaseStats($TimePeriod, $DayType, $WeatherType, $MinTrafficSeverity, $weather_coords)
	{
		$trafficQuery = '
				SELECT DISTINCT Yahoo_REST_traffic.Latitude, Yahoo_REST_traffic.Longitude,
					SUM(Yahoo_REST_traffic.Severity)
				FROM Yahoo_REST_traffic, Wunderground_RSS_weather
				WHERE Category = "incident"
				';
		$trafficQuery .= " AND ".$this->generateRangeSQL();
		$trafficQuery .= " AND ".$this->generateTimePeriodSQL($TimePeriod);
		$trafficQuery .= " AND ".$this->generateDayTypeSQL($DayType);
		$trafficQuery .= " AND ".$this->generateMinTrafficSeveritySQL($MinTrafficSeverity);
		$trafficQuery .= " AND ".$this->generateWeatherTypeSQL($WeatherType);
		$trafficQuery .= " AND ".$this->generateWeatherCoordsSQL($weather_coords);
		$trafficQuery .= ' AND (DATE_FORMAT(Yahoo_REST_traffic._Created, "%y:%m:%d:%k%i") =
			                DATE_FORMAT(Wunderground_RSS_weather._Created, "%y:%m:%d:%k%i")) ';
		$trafficQuery .= ' GROUP BY Yahoo_REST_traffic.Latitude, Yahoo_REST_traffic.Longitude ';
		$trafficQuery .= ' ORDER BY SUM(Yahoo_REST_traffic.Severity) DESC ';

		$trafficResult = $this->mdb2->query($trafficQuery);

		$this->statistics = $trafficResult->fetchAll();
	}

	private function generateRangeSQL()
	{
		return '
			(Yahoo_REST_traffic.Latitude <= '.$this->range['Max_lat'].'
			AND Yahoo_REST_traffic.Latitude >= '.$this->range['Min_lat'].'
			AND Yahoo_REST_traffic.Longitude <= '.$this->range['Max_lon'].'
			AND Yahoo_REST_traffic.Longitude >= '.$this->range['Min_lon'].')
			';
	}

	private function generateTimePeriodSQL($TimePeriod)
	{
		switch($TimePeriod)
		{
			case "1":	// All
				return ' 1 ';
			case "2":	// AM
				return '
					(DATE_FORMAT(Yahoo_REST_traffic._Created, "%p") = "AM")
					';
			case "3":	// PM
				return '
					(DATE_FORMAT(Yahoo_REST_traffic._Created, "%p") = "PM")
					';
		}
	}


	private function generateDayTypeSQL($DayType)
	{
		switch($DayType)
		{
			case "1":	// All
				return ' 1 ';
			case "2":	// Weekday
				return '
					((DAYOFWEEK(Yahoo_REST_traffic._Created) > 1)
					AND (DAYOFWEEK(Yahoo_REST_traffic._Created) < 7))
					'; 
			case "3":	// Weekend
				return '
					((DAYOFWEEK(Yahoo_REST_traffic._Created) = 1)
					OR (DAYOFWEEK(Yahoo_REST_traffic._Created) = 7))
					';
		}
	}

	private function generateMinTrafficSeveritySQL($MinTrafficSeverity)
	{
		switch($MinTrafficSeverity)
		{
			case "1":	// All
				return ' 1 ';
			case "2":
				return '
					Yahoo_REST_traffic.Severity >= 2
					';
			case "3":
				return '
					Yahoo_REST_traffic.Severity >= 3
					';
			case "4":
				return '
					Yahoo_REST_traffic.Severity >= 4
					';
			case "5":
				return '
					Yahoo_REST_traffic.Severity >= 5
					';
		}
	}

	private function generateWeatherTypeSQL($WeatherType)
	{
		switch($WeatherType)
		{
			case "1":	// All
				return ' 1 ';
			case "2":	// Clear
				return '
					(Wunderground_RSS_weather.Conditions LIKE "%clear%")
					';
			case "3":	// Cloudy
				return '
					(Wunderground_RSS_weather.Conditions LIKE "%cloud%"
					OR Wunderground_RSS_weather.Conditions LIKE "%overcast%")
					';
			case "4":	// Precip
				return '
					(Wunderground_RSS_weather.Conditions LIKE "%snow%"
					OR Wunderground_RSS_weather.Conditions LIKE "%rain%"
					OR Wunderground_RSS_weather.Conditions LIKE "%ice%"
					OR Wunderground_RSS_weather.Conditions LIKE "%fog%"
					OR Wunderground_RSS_weather.Conditions LIKE "%mist%"
					OR Wunderground_RSS_weather.Conditions LIKE "%drizzle%")
					';
		}
	}

	private function generateWeatherCoordsSQL($weather_coords)
	{
                return '
                        (Wunderground_RSS_weather.Latitude = '.$weather_coords['lat'].'
                        AND Wunderground_RSS_weather.Longitude = '.$weather_coords['lon'].')
                        ';
	}

	private function calcRange($Coordinates, $Radius)
	{
		if(isset($Coordinates[1])) {
			$Range['Max_lat'] = (float)$Coordinates[0]['lat'];
			$Range['Min_lat'] = (float)$Coordinates[0]['lat'];
			$Range['Max_lon'] = (float)$Coordinates[0]['lon'];
			$Range['Min_lon'] = (float)$Coordinates[0]['lon'];

			for($i = 1; $i < count($Coordinates); $i++)
			{
				if ((float)$Coordinates[$i]['lat'] > (float)$Range['Max_lat'])
					$Range['Max_lat'] = (float)$Coordinates[$i]['lat'];
				else if ((float)$Coordinates[$i]['lat'] < $Range['Min_lat'])
					$Range['Min_lat'] = (float)$Coordinates[$i]['lat'];
				if ((float)$Coordinates[$i]['lon'] > $Range['Max_lon'])
					$Range['Max_lon'] = (float)$Coordinates[$i]['lon'];
				else if ((float)$Coordinates[$i]['lon'] < $Range['Min_lon'])
					$Range['Min_lon'] = (float)$Coordinates[$i]['lon'];
			}
		}

		else
		{
			(float)$delta_lat = $Radius * 360.0 / 24902;
			(float)$delta_lon = $Radius * 360 / (24902 * cos(deg2rad((float)$Coordinates[0]['lat'])));
			$Range['Max_lat'] = $Coordinates[0]['lat'] + $delta_lat;
			$Range['Min_lat'] = $Coordinates[0]['lat'] - $delta_lat;
			$Range['Max_lon'] = $Coordinates[0]['lon'] + $delta_lon;
			$Range['Min_lon'] = $Coordinates[0]['lon'] - $delta_lon;
		}

		return $Range;
	}

	private function cropStatistics($Coordinates)
	{
		$temp_statistics = array();
		$count = 0;
		for($i = 0; $i < count($this->statistics); $i++)
		{
			for($j = 1; $j < (count($Coordinates) - 2); $j++)
			{
				$p1 = array($Coordinates[$j]['lat'], $Coordinates[$j]['lon']);
				$p2 = array($Coordinates[$j+1]['lat'], $Coordinates[$j+1]['lon']);
				$p3 = array($this->statistics[$i][0], $this->statistics[$i][1]);
				if ((float)$this->calcPerpDistance($p1, $p2, $p3) < 5.0) // 5 meters
				{
					$temp_statistics[$count] = $this->statistics[$i];
					$count++;
					break 1;
				}
			}
		}
		$this->statistics = array();
		$this->statistics = $temp_statistics;

	}

	// variance alg idea from http://www.ajdesigner.com/php_code_statistics/variance_sample.php
	private function calcVariance()
	{
		$temp_array = array();
		$variance = 0.0;
		$mean = 0.0;

		foreach ($this->statistics as &$row)
			array_push($temp_array, $row[2]);

		$array_sum = array_sum($temp_array);
		$number_elements = count($temp_array);

		$mean = $array_sum / $number_elements;

		for ($i = 0; $i < $number_elements; $i++)
			$variance = $variance + ($temp_array[$i] - $mean) * ($temp_array[$i] - $mean);

		$variance = $variance / ($number_elements - 1.0);

		$this->mean = $mean;
		$this->variance = $variance;
	}

	public function getStatistics()
	{
		return $this->statistics;
	}

	public function getRange()
	{
		return $this->range;
	}

	public function getMean()
	{
		return $this->mean;
	}

	public function getVariance()
	{
		return $this->variance;
	}

	// idea from other API
	private function calcDistance($lat1, $lon1, $lat2, $lon2)
	{
		return 69.09 * rad2deg(acos(sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * 
			cos(deg2rad($lat2)) * cos(deg2rad($lon1 - $lon2))));
	}

	// Peucker-Douglas distance algorithms from http://groups.google.com.tj/group/Google-Maps-API/browse_thread/thread/76e04535156814aa/d6824880303e0e9e#d6824880303e0e9e
	private function calcPerpDistance($point1, $point2, $point_off)
	{
        	$dist = $this->calcHaversineDistanceMeters($point1, $point_off);

		$angle  = $this->calcAngle3Points($point1, $point2, $point_off);
		return sprintf("%0.6f", abs($dist * sin($angle)) );
	}
	private function calcHaversineDistanceMeters($p1, $p2)
	{
	        (float)$a = (float)pi()/180.0;
		(float)$b = $p1[0] * $a ;
		(float)$c = $p2[0] * $a ;
		(float)$d = $b - $c ;
		(float)$e = ($p1[1] * $a) - ($p2[1] * $a) ;
		(float)$f = 2 * asin( sqrt( (pow(sin($d/2),2) ) + cos($b) * cos($c) * (pow(sin($e/2),2))));
	        return sprintf("%0.4f", $f * 6378137.0);
	}
	private function calcAngle3Points($p1, $p2, $p3){
		$m1 = $this->calcSlope($p2, $p1);
		$m2 = $this->calcSlope($p3, $p1);

		return ((float)$m2 - (float)$m1);
	}
	private function calcSlope($p1, $p2)
	{
        	return sprintf("%0.6f", atan2( ((float)$p2[1] - $p1[1]),( (float)$p2[0] -$p1[0] )) );
	}

}

?>
