<?php
declare(strict_types = 1);

/**
 * Gestion de la géolocalisation.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class Geolocation
{
	const MINUTE_SYMBOLS =
	[
		// Apostrophe.
		'\x{0027}',

		// Accents aigu et grave.
		'\x{0060}', '\x{00B4}',

		// Lettres modificatives.
		'\x{02B9}', '\x{02BB}-\x{02BF}', '\x{02C8}', '\x{02CA}-\x{02CC}',
		'\x{02CE}', '\x{02CF}', '\x{02D2}', '\x{02D3}', '\x{02DB}', '\x{02F4}',

		// Guillemets apostrophes et virgules.
		'\x{2018}-\x{201B}',

		// Prime.
		'\x{2032}', '\x{2035}'
	];

	const SECOND_SYMBOLS =
	[
		// Guillemet double.
		'\x{0022}',

		// Lettres modificatives.
		'\x{02BA}', '\x{02DD}', '\x{02EE}', '\x{02F5}', '\x{02F6}',

		// Guillemets double apostrophes et virgules.
		'\x{201C}-\x{201F}',

		// Double prime.
		'\x{2033}', '\x{2036}'
	];



	/**
	 * Vérifie et retourne des coordonnées au format décimal.
	 *
	 * @param mixed $latitude
	 * @param mixed $longitude
	 *
	 * @return mixed
	 */
	public static function checkCoordinates($latitude, $longitude)
	{
		if (($latitude = self::getDecimalCoordinate($latitude)) === FALSE
		|| $latitude > 90 || $latitude < -90)
		{
			return __('Format de la latitude incorrect.');
		}

		if (($longitude = self::getDecimalCoordinate($longitude)) === FALSE
		|| $longitude > 180 || $longitude < -180)
		{
			return __('Format de la longitude incorrect.');
		}

		return
		[
			'latitude' => substr((string) $latitude, 0, 17),
			'longitude' => substr((string) $longitude, 0, 18)
		];
	}

	/**
	 * Modifie les coordonnées de plusieurs catégories.
	 *
	 * @param array $cat_coords
	 *   Coordonnées associées à l'identifiant d'une catégorie.
	 *   Exemple :
	 *   $cat_coords = [7 => ['latitude' => 46.5, 'longitude' => 3]];
	 *
	 * @return int
	 *   Retourne le nombre de catégories affectées
	 *   ou -1 en cas d'erreur.
	 */
	public static function editCategories(array $cat_coords): int
	{
		return self::_edit($cat_coords, 'cat', 'categories');
	}

	/**
	 * Modifie les coordonnées de plusieurs fichiers.
	 *
	 * @param array $items_coords
	 *   Coordonnées associées à l'identifiant d'un fichier.
	 *
	 * @return int
	 *   Retourne le nombre de fichiers affectés
	 *   ou -1 en cas d'erreur.
	 */
	public static function editItems(array $items_coords): int
	{
		return self::_edit($items_coords, 'item', 'items');
	}

	/**
	 * Convertit une coordonnée (latitude ou longitude) depuis
	 * n'importe quel format (DD, DDM ou DMS) vers le format DD.
	 *
	 * Exemple : 45° 54' 36" N (DMS), 45° 54.6' N (DDM) ou 45,91° (DD)
	 * deviendra 45.91.
	 *
	 * DD = Decimal Degrees (Degrés Décimaux)
	 * DDM = Degrees Decimal Minutes (Degrés Minutes Décimales)
	 * DMS = Degrees Minutes Seconds (Degrés Minutes Secondes)
	 *
	 * @param mixed $coord
	 *
	 * @return mixed
	 *   Retourne une chaîne contenant la coordonnée au format décimal, ou
	 *   FALSE si la coordonnée n'est pas fournie dans un format valide.
	 */
	public static function getDecimalCoordinate($coord)
	{
		// (float) 1.0E-12 => (string) 0.000000000001
		// (float) 7.45101E-11 => (string) 0.0000000000745101
		$to_string = function($val): string
		{
			$val = strval($val);
			if (preg_match('`^(\d+(?:\.\d+))E\-(\d+)$`i', $val, $m))
			{
				$val = '0.' . str_repeat('0', $m[2] - 1) . str_replace('.', '', $m[1]);
			}
			return ($val[0] ?? '') == '+' ? substr($val, 1) : $val;
		};

		$coord = $to_string($coord);

		if (strlen($coord) > 99)
		{
			return FALSE;
		}

		$coord = Utility::trimAll(preg_replace('`\s+`', ' ', $coord));
		$minute_symbols = implode('', self::MINUTE_SYMBOLS);
		$second_symbols = implode('', self::SECOND_SYMBOLS);

		// Format DDM.
		$degree = '\s*(\d{1,3})[°,]';
		$minute = '\s*(\d{1,2}[.,]\d+)\s*[' . $minute_symbols . ']?';
		$regexp = '`^([-+])?' . $degree . $minute . '(?:\s*([ENOSW]))?$`ui';
		if (preg_match($regexp, $coord, $m))
		{
			$m[3] = str_replace(',', '.', $m[3]);
			$s = isset($m[4]) ? (in_array(strtoupper($m[4]), ['E','N']) ? '' : '-') : $m[1];
			return $to_string($s . ($m[2] + (($m[3] ?? 0) / 60)));
		}

		$coord = str_replace(',', '.', $coord);

		// Format DD.
		if (preg_match('`^[-+]?\d{1,3}(?:\.\d+)?\s*(?:°)?(?:\s*[ENOSW])?$`i', $coord))
		{
			return $to_string((float) $coord);
		}

		// Format DMS.
		$degree = '\s*(\d{1,3})\s*(?:°)?';
		$minute = '(?:\s*(\d{1,2})\s*[' . $minute_symbols . ']?)?';
		$second = '(?:\s*(?<!°\s)(?<![°\d])(\d+(?:\.\d+)?)\s*[' . $second_symbols . ']?)?';
		$regexp = '`^([-+])?' . $degree . $minute . $second . '\s*(?:°\s*)?([ENOSW])?$`ui';
		if (preg_match('`^([ENOSW])(\d.+)$`ui', $coord, $m))
		{
			$coord = $m[2] . $m[1];
		}
		if (preg_match($regexp, $coord, $m))
		{
			$s = isset($m[5]) ? (in_array(strtoupper($m[5]), ['E','N']) ? '' : '-') : $m[1];
			return $to_string($s . ($m[2] + (($m[3] ?? 0) / 60) + (($m[4] ?? 0) / 3600)));
		}

		return FALSE;
	}

	/**
	 * Convertit des coordonnées depuis le format décimal DD
	 * vers le format sexagésimal DMS ou DDM.
	 *
	 * Exemple de conversion avec une latitude de 45.91 :
	 *    45° 54' 36" N (format DMS par défaut)
	 *    45° 54.6' N (format DDM).
	 *
	 * @param mixed $latitude
	 * @param mixed $longitude
	 * @param string $format
	 *
	 * @return array
	 */
	public static function getSexagesimalCoordinates($latitude, $longitude,
	string $format = 'DMS'): array
	{
		$convert = function($coord, string $p, string $n) use ($format): string
		{
			$l = $p;
			$coord = (string) $coord;
			if (substr($coord, 0, 1) == '-')
			{
				$coord = substr($coord, 1);
				$l = $n;
			}

			$coord = (float) $coord;
			$degree = (int) $coord;
			$coord = ($coord - $degree) * 60;

			if ($format == 'DDM')
			{
				return sprintf('%s° %s\' %s', $degree, round($coord, 4), $l);
			}

			$minute = (int) $coord;
			$coord = ($coord - $minute) * 60;
			$second = round($coord, 2);

			return sprintf('%s° %s\' %s" %s', $degree, $minute, $second, $l);
		};

		return
		[
			'latitude' => $convert($latitude, 'N', 'S'),
			'longitude' => $convert($longitude, 'E', 'W')
		];
	}



	/**
	 * Modifie les coordonnées de plusieurs fichiers ou catégories.
	 *
	 * @param array $coords
	 * @param string $prefix
	 * @param string $table
	 *
	 * @return int
	 *   Retourne le nombre d'objets affectés
	 *   ou -1 en cas d'erreur.
	 */
	private static function _edit(array $coords, string $prefix, string $table): int
	{
		$params = [];

		// Vérification du format des coordonnées.
		foreach ($coords as $id => &$c)
		{
			if (!isset($c['latitude']) || !isset($c['longitude']))
			{
				continue;
			}

			if (Utility::trimAll($c['latitude']) === ''
			 && Utility::trimAll($c['longitude']) === '')
			{
				$params[$id] =
				[
					'lat' => NULL,
					'long' => NULL,
					'id' => (int) $id
				];
			}

			else if (is_array($latlng = self::checkCoordinates($c['latitude'], $c['longitude'])))
			{
				$params[$id] =
				[
					'lat' => $latlng['latitude'],
					'long' => $latlng['longitude'],
					'id' => (int) $id
				];
			}
		}
		if (!$params)
		{
			return 0;
		}

		// On ne conserve que les objets dont les coordonnées sont différentes.
		$sql = 'SELECT %1$s_id, %1$s_lat, %1$s_long FROM {%2$s} WHERE %1$s_id IN (%3$s)';
		if (!DB::execute(sprintf($sql, $prefix, $table, DB::inInt(array_column($params, 'id')))))
		{
			return -1;
		}
		foreach (DB::fetchAll($prefix . '_id') as $id => &$i)
		{
			if ($i[$prefix . '_lat'] == $params[$id]['lat']
			 && $i[$prefix . '_long'] == $params[$id]['long'])
			{
				unset($params[$id]);
			}
		}
		if (!$params)
		{
			return 0;
		}

		// Mise à jour de la base de données.
		$sql = 'UPDATE {%2$s} SET %1$s_lat = :lat, %1$s_long = :long WHERE %1$s_id = :id';
		if (!DB::execute(sprintf($sql, $prefix, $table), array_values($params)))
		{
			return -1;
		}

		return DB::rowCount();
	}
}
?>