<?php

namespace Franzz\Spot;

class GeoJson extends Geo {

	const EXT = '.geojson';
	const MAX_FILESIZE = 2; //MB
	const MAX_DEVIATION_FLAT = 0.1; //10%
	const MAX_DEVIATION_ELEV = 0.1; //10%

	public function __construct($sCodeName) {
		parent::__construct($sCodeName);
	}

	public function saveFile() {
		$this->addNotice('Saving '.$this->sFilePath);
		file_put_contents($this->sFilePath, $this->buildGeoJson());
	}

	public function isSimplicationRequired() {
		//Size in bytes
		$iFileSize = strlen($this->buildGeoJson());

		//Convert to MB
		$iFileSize = round($iFileSize / pow(1024, 2), 2);

		//Compare with max allowed size
		$bFileTooLarge = ($iFileSize > self::MAX_FILESIZE);
		if($bFileTooLarge) $this->addNotice('Output file is too large ('.$iFileSize.'MB > '.self::MAX_FILESIZE.'MB)');

		return $bFileTooLarge;
	}

	public function buildTracks($asTracks, $bSimplify=false) {
		$this->addNotice('Creating '.($bSimplify?'Simplified ':'').'GeoJson Tracks');

		$iGlobalInvalidPointCount = 0;
		$iGlobalPointCount = 0;

		$this->asTracks = array();
		foreach($asTracks as $asTrackProps) {
			$asOptions = $this->parseOptions($asTrackProps['cmt']);

			//Color mapping
			switch($asTrackProps['color']) {
				case 'DarkBlue':
					$sType = 'main';
					break;
				case 'Magenta':
					if($bSimplify && $asOptions[self::OPT_SIMPLE]!='keep') {
						$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (off-track)');
						continue 2; //discard tracks
					}
					else {
						$sType = 'off-track';
						break;
					}
				case 'Red':
					$sType = 'hitchhiking';
					break;
				default:
					$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (unknown color "'.$asTrackProps['color'].'")');
					continue 2; //discard tracks
			}

			$asTrack = array(
				'type'	=> 'Feature',
				'properties' => array(
					'name'			=> $asTrackProps['name'],
					'type'			=> $sType,
					'description'	=> $asTrackProps['desc']
				),
				'geometry'		=> array(
					'type'			=> 'LineString',
					'coordinates'	=> array()
				)
			);

			if($sType != 'hitchhiking' && str_contains($asTrackProps['desc'], ' ➜ ')) {
				list($sFrom, $sTo) = explode(' ➜ ', $asTrackProps['desc']);
				$asTrack['properties']['leg'] = ['from'=> $sFrom, 'to'=> $sTo];
			}

			//Track points
			$asTrackPoints = $asTrackProps['points'];
			$iPointCount = count($asTrackPoints);
			$iInvalidPointCount = 0;
			$asPrevPoint = array();
			foreach($asTrackPoints as $iIndex=>$asPoint) {
				$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:array();
				if($bSimplify && !empty($asPrevPoint) && !empty($asNextPoint)) {
					if(!$this->isPointValid($asPrevPoint, $asPoint, $asNextPoint)) {
						$iInvalidPointCount++;
						continue;
					}
				}
				$asTrack['geometry']['coordinates'][] = array_values($asPoint);
				$asPrevPoint = $asPoint;
			}
			$this->asTracks[] = $asTrack;

			$iGlobalInvalidPointCount += $iInvalidPointCount;
			$iGlobalPointCount += $iPointCount;
			if($iInvalidPointCount > 0) $this->addNotice('Removing '.$iInvalidPointCount.'/'.$iPointCount.' points ('.round($iInvalidPointCount / $iPointCount * 100, 1).'%) from '.$asTrackProps['name']);
		}

		if($bSimplify) $this->addNotice('Total: '.$iGlobalInvalidPointCount.'/'.$iGlobalPointCount.' points removed ('.round($iGlobalInvalidPointCount / $iGlobalPointCount * 100, 1).'%)');
	}

	public function sortOffTracks() {
		$this->addNotice('Sorting off-tracks');

		//Find first & last track points
		$asTracksEnds = array();
		$asTracks = array();
		foreach($this->asTracks as $iTrackId=>$asTrack) {
			$sTrackId = 't'.$iTrackId;
			$asTracksEnds[$sTrackId] = array('first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates']));
			$asTracks[$sTrackId] = $asTrack;
		}

		//Find variants close-by tracks
		$asClonedTracks = $asTracks;
		foreach($asClonedTracks as $sTrackId=>$asTrack) {
			if($asTrack['properties']['type'] != 'off-track') continue;

			$iMinDistance = INF;
			$sConnectedTrackId = 0;
			$iPosition = 0;

			//Test all track ending points to find the closest
			foreach($asTracksEnds as $sTrackEndId=>$asTrackEnds) {
				if($sTrackEndId != $sTrackId) {
					//Calculate distance between the last point of the track and every starting point of other tracks
					$iDistance = self::getDistance($asTracksEnds[$sTrackId]['last'], $asTrackEnds['first']);
					if($iDistance < $iMinDistance) {
						$sConnectedTrackId = $sTrackEndId;
						$iPosition = 0; //Track before the Connected Track
						$iMinDistance = $iDistance;
					}

					//Calculate distance between the first point of the track and every ending point of other tracks
					$iDistance = self::getDistance($asTracksEnds[$sTrackId]['first'], $asTrackEnds['last']);
					if($iDistance < $iMinDistance) {
						$sConnectedTrackId = $sTrackEndId;
						$iPosition = +1; //Track after the Connected Track
						$iMinDistance = $iDistance;
					}
				}
			}

			//Move track
			unset($asTracks[$sTrackId]);
			$iOffset = array_search($sConnectedTrackId, array_keys($asTracks)) + $iPosition;
			$asTracks = array_slice($asTracks, 0, $iOffset) + array($sTrackId => $asTrack) + array_slice($asTracks, $iOffset);
		}

		$this->asTracks = array_values($asTracks);
	}

	public function getCenter() {
		$asCoords = array();
		$asMainTracks = array_filter($this->asTracks, function ($astrack) {return $astrack['properties']['type'] == 'main';});
		foreach($asMainTracks as $asMainTrack) {
			foreach($asMainTrack['geometry']['coordinates'] as $aiCoords) {
				$asCoords[] = $aiCoords;
			}
		}

		return $asCoords[(int) floor(count($asCoords) / 2)];
	}

	private function parseOptions($sComment) {
		$sComment = strip_tags(html_entity_decode($sComment));
		$asOptions = array(self::OPT_SIMPLE=>'');
		foreach(explode("\n", $sComment) as $sLine) {
			$asOptions[mb_strtolower(trim(mb_strstr($sLine, ':', true)))] = mb_strtolower(trim(mb_substr(mb_strstr($sLine, ':'), 1)));
		}
		return $asOptions;
	}

	private function isPointValid($asPointA, $asPointO, $asPointB) {
		/*	A----O	   Calculate angle AO^OB
		*        \	   If angle is within [90% Pi ; 110% Pi], O can be discarded
		*         \   O is valid otherwise
		*          B
		*/

		//Path Turn Check                         -> ->     ->     ->
		//Law of Cosines (vector): angle = arccos(OA.OB / ||OA||.||OB||)
		$fVectorOA = array('lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat']));
		$fVectorOB = array('lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat']));

		$fLengthOA = sqrt(pow($asPointA['lon'] - $asPointO['lon'], 2) + pow($asPointA['lat'] - $asPointO['lat'], 2));
		$fLengthOB = sqrt(pow($asPointO['lon'] - $asPointB['lon'], 2) + pow($asPointO['lat'] - $asPointB['lat'], 2));

		$fVectorOAxOB = $fVectorOA['lon'] * $fVectorOB['lon'] + $fVectorOA['lat'] * $fVectorOB['lat'];
		$fAngleAOB = ($fLengthOA != 0 && $fLengthOB != 0) ? acos($fVectorOAxOB/($fLengthOA * $fLengthOB)) : 0;

		//Elevation Check
		//Law of Cosines: angle = arccos((OB² + AO² - AB²) / (2*OB*AO))
		$fLengthAB = sqrt(pow($asPointB['ele'] - $asPointA['ele'], 2) + pow($fLengthOA + $fLengthOB, 2));
		$fLengthAO = sqrt(pow($asPointO['ele'] - $asPointA['ele'], 2) + pow($fLengthOA, 2));
		$fLengthOB = sqrt(pow($asPointB['ele'] - $asPointO['ele'], 2) + pow($fLengthOB, 2));
		$fAngleAOBElev = ($fLengthOB != 0 && $fLengthAO != 0) ? (acos((pow($fLengthOB, 2) + pow($fLengthAO, 2) - pow($fLengthAB, 2)) / (2 * $fLengthOB * $fLengthAO))) : 0;

		return ($fAngleAOB <= (1 - self::MAX_DEVIATION_FLAT) * M_PI || $fAngleAOB >= (1 + self::MAX_DEVIATION_FLAT) * M_PI ||
				$fAngleAOBElev <= (1 - self::MAX_DEVIATION_ELEV) * M_PI || $fAngleAOBElev >= (1 + self::MAX_DEVIATION_ELEV) * M_PI);
	}

	private function buildGeoJson() {
		return json_encode(array('type'=>'FeatureCollection', 'features'=>$this->asTracks));
	}

	private static function getDistance($asPointA, $asPointB)	{
		$fLatFrom = $asPointA[1];
		$fLonFrom = $asPointA[0];
		$fLatTo = $asPointB[1];
		$fLonTo = $asPointB[0];

		$fRad = M_PI / 180;

		//Calculate distance from latitude and longitude
		$fTheta = $fLonFrom - $fLonTo;
		$fDistance = sin($fLatFrom * $fRad) * sin($fLatTo * $fRad) +  cos($fLatFrom * $fRad) * cos($fLatTo * $fRad) * cos($fTheta * $fRad);

		return acos($fDistance) / $fRad * 60 *  1.853;
	}
}
