<?php
class CelStreets extends Cel {

	/**
	 * Méthode appelée avec une requête de type GET.
	 */
	public function getRessource() {
		$this->getStreetGeom();
	}
	
	private function getStreetGeom() {
		
		$erreurs = $this->verifierParametres();
		if(!empty($erreurs)) {
			$this->envoyerMessageErreur(400, implode("\n", $erreurs));
			exit;
		}
		
		$latitude_debut = $_GET['latitude_debut'];
		$longitude_debut = $_GET['longitude_debut'];
		$latitude_fin = $_GET['latitude_fin'];
		$longitude_fin = $_GET['longitude_fin'];
		
		$dist_max = round(self::haversineGreatCircleDistance($latitude_debut, $longitude_debut, $latitude_fin, $longitude_fin)/2);

		$url_tpl = $this->getUrlRequeteOverPass().urlencode($this->getRequetePasLoin($latitude_debut, $longitude_debut, $latitude_fin, $longitude_fin, $dist_max));
		$json = file_get_contents($url_tpl);
		
		$infos = json_decode($json, true);
		$this->filtrerNoeuds($infos, $latitude_debut, $longitude_debut, $latitude_fin, $longitude_fin);
		
		header('Content-type: application/json');
		echo json_encode($infos); exit;
	}
	
	private function verifierParametres() {
		$parametres_obligatoires = array('latitude_debut', 'longitude_debut', 'latitude_fin', 'longitude_fin');
		$erreurs = array();
		foreach($parametres_obligatoires as $param) {
			if(empty($_GET[$param]) || !is_numeric($_GET[$param])) {
				$erreurs[] = "Le paramètre $param est obligatoire et doit être un flottant";
			}
		}
		return $erreurs;
	}
	
	function getUrlRequeteOverPass() {
		return $this->config['cel']['url_service_rue'];
	}
	
	function getRequetePasLoin($latitude_debut, $longitude_debut, $latitude_fin, $longitude_fin, $dist = 50) {
		$req = 'way["highway"]
			(around:'.$dist.','.$latitude_debut.','.$longitude_debut.')
			(around:'.$dist.','.$latitude_fin.','.$longitude_fin.')
			;
			(
			._;
			>;
			);
		out geom;';
		
		return $req;
	}
	
	function getRequete($nord, $sud, $est, $ouest) {		
		return '(
			way["highway"]
			('.$sud.','.$ouest.','.$nord.','.$est.');
			>;
		);out geom;';
	}
	
	function filtrerNoeuds(&$infos, $latitude_debut, $longitude_debut, $latitude_fin, $longitude_fin) {	
		$distances_debut = array();
		$distances_fin = array();
		
		foreach($infos['elements'] as $index_e => &$element) {
			if(empty($element['geometry'])) {
				unset($infos['elements'][$index_e]);
			} else {
				$index_fin_tableau = count($element['geometry']) - 1;
				
				$distances_debut[$index_e] = -1;
				$distances_fin[$index_e] = -1;
				
				$index_point_plus_proche_debut = 0;
				$dist_point_plus_proche_debut = 999999999;
				$index_point_plus_proche_fin = $index_fin_tableau;
				$dist_point_plus_proche_fin = 9999999999;
				
				$index_debut = array(0, 1);
				$index_fin = array($index_fin_tableau -1, $index_fin_tableau);
				
				$element['geometryOrig'] = $element['geometry'];
				
				foreach($element['geometry'] as $index_g => &$geometry) {					
					// Calcul de la plus petite distance de la droite de rue au point
					// pour sélectionner la rue la plus proche des deux points
					if($index_g + 1 < count($element['geometry'])) {
						$geom_next = $element['geometry'][$index_g + 1];
						$this->stockerDistancePlusCourte($distances_debut, $index_debut, $index_e, $index_g, $geometry, $geom_next, $latitude_debut, $longitude_debut);
						$this->stockerDistancePlusCourte($distances_fin, $index_fin, $index_e, $index_g, $geometry, $geom_next, $latitude_fin, $longitude_fin);
					}
					
					$dist_debut= self::haversineGreatCircleDistance($latitude_debut, $longitude_debut, $geometry['lat'], $geometry['lon']);
					$dist_fin = self::haversineGreatCircleDistance($latitude_fin, $longitude_fin, $geometry['lat'], $geometry['lon']);
					if($dist_point_plus_proche_debut > $dist_debut) {
						$dist_point_plus_proche_debut = $dist_debut;
						$index_point_plus_proche_debut = $index_g;
					}
					
					if($dist_point_plus_proche_fin > $dist_fin) {
						$dist_point_plus_proche_fin = $dist_fin;
						$index_point_plus_proche_fin = $index_g;
					}
				}
				
				$index_min = min($index_point_plus_proche_debut, $index_point_plus_proche_fin);
				$index_max = max($index_point_plus_proche_debut, $index_point_plus_proche_fin);
								
				$infos_debut = array('lat' => (float)$latitude_debut, 'lon' => (float)$longitude_debut);
				$infos_fin = array('lat' => (float)$latitude_fin, 'lon' => (float)$longitude_fin);
				
				// Inversion des points de début et de fin si le début donné par l'utilisateur est plus 
				// proche de la fin
				if($index_min != $index_point_plus_proche_debut) {
					$tmp = $infos_debut;
					$infos_debut = $infos_fin;
					$infos_fin = $tmp;
				}
								
				// Projection des points de début et d'arrivée sur le segment de rue
				if($index_min < $index_fin_tableau) {
					$proj_debut = $this->getOrthoPoint($element['geometry'][$index_min], $element['geometry'][$index_min+1], $infos_debut);
					$index_insertion_point_debut = $this->getIndexInsertionDebut($element, $index_min, $infos_debut);
				} else {
					$proj_debut = $infos_debut;
					$index_insertion_point_debut = 0;
				}
				
				if($index_max > 0) {
					$proj_fin = $this->getOrthoPoint($element['geometry'][$index_max], $element['geometry'][$index_max-1], $infos_fin);
					$index_insertion_point_fin = $this->getIndexInsertionFin($element, $index_max, $infos_fin);
				} else {
					$proj_fin = $infos_fin;
					$index_insertion_point_fin = $index_fin_tableau;
				}

				$index_insertion_point_debut = $index_insertion_point_debut < 0 ? 0 : $index_insertion_point_debut;
				$index_insertion_point_fin = $index_insertion_point_fin > $index_fin_tableau ? $index_fin_tableau : $index_insertion_point_fin;
				
				$rd = array();
				for($ii = $index_insertion_point_debut+1; $ii <= $index_insertion_point_fin; $ii++) {
					$rd[] = $element['geometry'][$ii];
				}
				
				$proj_debut = $infos_debut;
				$proj_fin = $infos_fin;
				
				// Insertion des points utilisateurs en début et fin de tableau
				array_unshift($rd, $proj_debut);
				array_push($rd, $proj_fin);
				
				$element['geometry'] = $rd;
			}
		}
		
		if(!empty($distances_debut)) {
			asort($distances_debut);
			reset($distances_debut);
			$cle_plus_courte_debut = key($distances_debut);
		}
		
		if(!empty($distances_fin)) {
			asort($distances_fin);
			reset($distances_fin);
			$cle_plus_courte_fin = key($distances_fin);
		}
		
		if(!empty($distances_fin)) {
			$cle_choisie = $this->choisirIndexRuePlusProchePourDebutEtFin($cle_plus_courte_debut, $cle_plus_courte_fin, $distances_debut, $distances_fin);	
			$rue_plus_proche = $distances_fin[$cle_choisie];			
			unset($distances_fin[$cle_choisie]);
			$distances_fin = array($cle_choisie => $rue_plus_proche) + $distances_fin;
		}
		
		$elements_fmt = array();
		foreach($distances_fin as $index => $distance) {
			//echo '<pre>'.print_r($infos['elements'][$index], true).'</pre>';exit;
			$rue_proche = $infos['elements'][$index];
			$rue_proche['dist_min'] = $distance;
			$elements_fmt[] = $rue_proche;
		}
		$infos['elements'] = $elements_fmt;
	}
	
	function choisirIndexRuePlusProchePourDebutEtFin($cle_plus_courte_debut, $cle_plus_courte_fin, $distances_debut, $distances_fin) {
		
		$cle_choisie = $cle_plus_courte_fin;
		// Si la rue la plus proche n'est pas la même pour les deux points
		if($cle_plus_courte_debut != $cle_plus_courte_fin) {
			// on calcule celle qui est la mieux positionnée en moyenne
			$index_debut_tab_debut = array_search($cle_plus_courte_debut, array_keys($distances_debut));
			$index_debut_tab_fin = array_search($cle_plus_courte_debut, array_keys($distances_fin));
			
			$index_fin_tab_debut = array_search($cle_plus_courte_fin, array_keys($distances_debut));
			$index_fin_tab_fin = array_search($cle_plus_courte_fin, array_keys($distances_fin));
			
			$moy_index_debut = ($index_debut_tab_debut + $index_debut_tab_fin)/2;
			$moy_index_fin = ($index_fin_tab_debut + $index_fin_tab_fin)/2;
			
			$cle_choisie = ($moy_index_debut < $moy_index_fin) ? $cle_plus_courte_debut : $cle_plus_courte_fin;
			
			// Si ça ne suffit pas on prend celle qui a la distance moyenne la plus courte aux deux points
			if ($moy_index_debut ==  $moy_index_fin) {
				$moyenne_dist_cle_debut = ($distances_debut[$cle_plus_courte_debut] + $distances_fin[$cle_plus_courte_debut])/2;
				$moyenne_dist_cle_fin = ($distances_debut[$cle_plus_courte_fin] + $distances_fin[$cle_plus_courte_fin])/2;
					
				$cle_choisie =  ($moyenne_dist_cle_debut < $moyenne_dist_cle_fin) ? $cle_plus_courte_debut : $cle_plus_courte_fin;
			}
		}
		
		return $cle_choisie;
	}
	
	function getIndexInsertionDebut($element, $index_min, $debut) {
		// le point de début est situé entre $index_min et $index_min + 1
		$proj_debut = $this->getOrthoPoint($element['geometry'][$index_min], $element['geometry'][$index_min+1], $debut);		
		$point_deb = $element['geometry'][$index_min];
		$point_deb_plus_un = $element['geometry'][$index_min+1];
		$distance_point_deb_plus_un_proj = self::haversineGreatCircleDistance($point_deb_plus_un['lat'], $point_deb_plus_un['lon'], $proj_debut['lat'], $proj_debut['lon']);
		$distance_point_deb_deb_plus_un = self::haversineGreatCircleDistance($point_deb['lat'], $point_deb['lon'], $point_deb_plus_un['lat'], $point_deb_plus_un['lon']);
		
		if($distance_point_deb_plus_un_proj < $distance_point_deb_deb_plus_un) {
			// le point le plus proche doit être éliminé
			return $index_min + 1;
		} else {
			// il doit être gardé
			return $index_min;	
		}
	}
	
	function getIndexInsertionFin($element, $index_max, $fin) {
		$proj_fin = $this->getOrthoPoint($element['geometry'][$index_max], $element['geometry'][$index_max-1], $fin);
		$point_fin = $element['geometry'][$index_max];
		$point_fin_moins_un = $element['geometry'][$index_max-1];
		$distance_point_fin_moins_un_proj = self::haversineGreatCircleDistance($point_fin_moins_un['lat'], $point_fin_moins_un['lon'], $proj_fin['lat'], $proj_fin['lon']);
		$distance_point_fin_fin_moins_un = self::haversineGreatCircleDistance($point_fin['lat'], $point_fin['lon'], $point_fin_moins_un['lat'], $point_fin_moins_un['lon']);
	
		if($distance_point_fin_moins_un_proj < $distance_point_fin_fin_moins_un) {
			// le point le plus proche doit être éliminé
			return $index_max - 1;
		} else {
			// il doit être gardé
			return $index_max;
		}
	}
	
	function stockerDistancePlusCourte(&$distances, &$infos_index_position_point, $index_e, $index_g, $geometry, $geom_next, $latitude_point, $longitude_point) {
		$a = $geometry['lat'];
		$b = $geometry['lon'];
		$c = $geom_next['lat'];
		$d = $geom_next['lon'];
		$x = $latitude_point ;
		$y = $longitude_point ;
		$dist = $this->getShortestDistance($a, $b, $c, $d, $x, $y);
		if($distances[$index_e] == -1 || $dist < $distances[$index_e]) {
			$distances[$index_e] = $dist;
			$infos_index_position_point = array($index_g, $index_g+1);
		}
	}
	
	// projette le point C sur le segment AB
	function getOrthoPoint($a, $b, $c) {		
		$x1 = $a['lat']; 
		$y1 = $a['lon']; 
		$x2 = $b['lat']; 
		$y2 = $b['lon']; 
		$x3 = $c['lat']; 
		$y3 = $c['lon'];
		$px = $x2-$x1;
		$py = $y2-$y1;
		$dAB = $px*$px + $py*$py;
		$u = (($x3 - $x1) * $px + ($y3 - $y1) * $py) / $dAB;
		$x = $x1 + $u * $px;
		$y = $y1 + $u * $py;
		return array('lat' => $x, 'lon' => $y);  
	}
	
	function getShortestDistance($a, $b, $c, $d, $x, $y) {
		//Coordinates are (a,b) and (c,d)
		//the point (x,y) is the required point.
		//echo 'nagi ';
		//echo $c.' - '.$a." = ".($c-$a).'<br />';
		if(($c-$a) == 0) {
			return 56465465456485;
		}
		
		$m=($d-$b)/($c-$a);
		//echo $m."\n";
		
		//echo $y-($m*$x)-$b+($m*$a)."\n";
		$distance=abs($y-($m*$x)-$b+($m*$a))/sqrt(1+($m*$m));
		return  $distance;
	}
	
	function haversineGreatCircleDistance(
			$latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000)
	{
		// convert from degrees to radians
		$latFrom = deg2rad($latitudeFrom);
		$lonFrom = deg2rad($longitudeFrom);
		$latTo = deg2rad($latitudeTo);
		$lonTo = deg2rad($longitudeTo);
	
		$latDelta = $latTo - $latFrom;
		$lonDelta = $lonTo - $lonFrom;
	
		$angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) +
				cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2)));
		return $angle * $earthRadius;
	}
	
	public static function vincentyGreatCircleDistance($latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000)
	{
		// convert from degrees to radians
		$latFrom = deg2rad($latitudeFrom);
		$lonFrom = deg2rad($longitudeFrom);
		$latTo = deg2rad($latitudeTo);
		$lonTo = deg2rad($longitudeTo);
	
		$lonDelta = $lonTo - $lonFrom;
		$a = pow(cos($latTo) * sin($lonDelta), 2) +
		pow(cos($latFrom) * sin($latTo) - sin($latFrom) * cos($latTo) * cos($lonDelta), 2);
		$b = sin($latFrom) * sin($latTo) + cos($latFrom) * cos($latTo) * cos($lonDelta);
	
		$angle = atan2(sqrt($a), $b);
		return $angle * $earthRadius;
	}
}
?>