using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using TMPro; using Unity.VisualScripting; using UnityEngine; public class StoreLocatorUI : MonoBehaviour { [Header("Références UI")] public TMP_InputField postalCodeField; // ton TMP PostalCodeField public TMP_Dropdown dropdown; // Dropdown des VILLES public TMP_Dropdown addressDropdown; // Dropdown des ADRESSES de la ville sélectionnée [Header("FSA Locator")] public FsaLocator fsaLocator; // réf. à ton composant FsaLocator dans la scène [Header("Config")] public int maxResults = 5; // nombre de magasins à afficher (5) [Serializable] private class StoreDistance { public string town; public string name; public string address; public string lineRaw; // la ligne brute de _G.UIT_LibraryStores si tu veux la réutiliser public double distanceKm; } // Tous les magasins trouvés, triés par distance (conservés pour filtrer par ville) private List _sorted; // Accès depuis d'autres scripts (ex. StoreSelection) sans câblage Inspector. public static StoreLocatorUI Instance { get; private set; } // Vrai quand une recherche par code postal a produit des distances (= mode code postal). public bool HasDistances => _sorted != null && _sorted.Count > 0; private void Awake() { Instance = this; } // À appeler quand on bascule sur une recherche par NOM de ville : on oublie // les distances du code postal pour que PopulateAddresses n'affiche plus d'anciennes valeurs. public void ClearDistances() { _sorted = null; } // Appelé par le bouton SearchPC (OnClick) public void OnSearchPostalCode() { string pc = postalCodeField != null ? postalCodeField.text : string.Empty; if (string.IsNullOrWhiteSpace(pc)) { Debug.LogWarning("StoreLocatorUI: code postal vide."); return; } // 1) Récupérer la position approximative du client via le FSA if (!fsaLocator.TryGetLatLonFromPostal(pc, out double userLat, out double userLon)) { Debug.LogWarning("StoreLocatorUI: impossible de trouver les coordonnées pour le code postal: " + pc); ClearDropdown("Aucun magasin trouvé (code postal invalide)"); return; } // 2) Construire une liste (distance, magasin) List distances = ComputeStoreDistances(userLat, userLon); //print() if (distances == null || distances.Count == 0) { ClearDropdown("Aucun magasin disponible " +distances.Count); return; } // 3) Trier par distance (liste complète conservée pour filtrer les adresses par ville) var ordered = distances.OrderBy(s => s.distanceKm).ToList(); // On ne garde que les magasins à moins de 50 km quand il y en a plusieurs. // S'il n'y en a qu'un (ou aucun sous 50 km), on garde au moins le plus proche. const double maxKm = 50.0; if (ordered.Count > 1) { var near = ordered.Where(s => s.distanceKm < maxKm).ToList(); _sorted = near.Count > 0 ? near : ordered.Take(1).ToList(); } else { _sorted = ordered; } // 4) Remplir le Dropdown des VILLES (les plus proches en premier) dropdown.ClearOptions(); List townOptions = new(); HashSet addedTowns = new(); // évite les doublons de ville foreach (var s in _sorted) { if (addedTowns.Contains(s.town)) continue; addedTowns.Add(s.town); townOptions.Add(new TMP_Dropdown.OptionData(s.town)); if (addedTowns.Count >= maxResults) // on garde les N villes les plus proches break; } dropdown.AddOptions(townOptions); // On remet sur la ville la plus proche SANS notifier : sinon, si l'index reste 0, // onValueChanged ne se déclencherait pas et les adresses garderaient l'ancienne distance. if (townOptions.Count > 0) dropdown.SetValueWithoutNotify(0); dropdown.RefreshShownValue(); // 5) Afficher les adresses de la ville sélectionnée. // On attend une frame pour passer APRÈS les autres actions du bouton // (ex. StoreSelection.TownSelect) qui, sinon, écraseraient nos adresses avec distance. StartCoroutine(PopulateAddressesNextFrame()); } private System.Collections.IEnumerator PopulateAddressesNextFrame() { yield return null; // laisse s'exécuter les autres OnClick du bouton (TownSelect, etc.) PopulateAddresses(); } // Remplit le dropdown des adresses pour la ville actuellement sélectionnée, // triées par distance, avec la distance à la fin de chaque adresse. // Public : peut être branché sur le OnValueChanged du TownSelector dans l'Inspector. public void PopulateAddresses() { if (addressDropdown == null || _sorted == null || dropdown == null || dropdown.options.Count == 0) return; string selectedTown = dropdown.options[dropdown.value].text; addressDropdown.ClearOptions(); List addrOptions = new(); HashSet addedAddr = new(); // évite les doublons d'adresse // _sorted est déjà trié par distance -> les adresses sortent dans l'ordre de distance foreach (var s in _sorted) { if (s.town != selectedTown) continue; if (addedAddr.Contains(s.address)) continue; addedAddr.Add(s.address); // distance à la fin de l'adresse : "123 rue X — 12.3 km" string label = $"{s.address} — {s.distanceKm.ToString("0.0", CultureInfo.InvariantCulture)} km"; addrOptions.Add(new TMP_Dropdown.OptionData(label)); } addressDropdown.AddOptions(addrOptions); if (addrOptions.Count > 0) addressDropdown.value = 0; addressDropdown.RefreshShownValue(); } private void ClearDropdown(string message) { if (dropdown == null) return; dropdown.ClearOptions(); dropdown.AddOptions(new List { new TMP_Dropdown.OptionData(message) }); dropdown.value = 0; dropdown.RefreshShownValue(); } private List ComputeStoreDistances(double userLat, double userLon) { List list = new(); // 🔹 Indices de colonnes (utilise ton système existant) int indLongitude = UIT.Ind(Library.Store, Header.Longitude); int indLatitude = UIT.Ind(Library.Store, Header.Latitude); int indName = UIT.Ind(Library.Store, Header.Store_Name); // adapte ce header si besoin int indTown = UIT.Ind(Library.Store, Header.Town); int indAddress = UIT.Ind(Library.Store, Header.Address); // 🔹 Chaque ligne de magasin dans ta librairie // Adapte le séparateur de lignes si nécessaire (\n ou autre) //string[] lines = _G.UIT_LibraryStores.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); foreach (string line in _G.UIT_LibraryStores) { if(string.IsNullOrEmpty(line) && line!=_G.UIT_LibraryStores[0]){continue;} // Adapte le séparateur de colonnes si ce n'est pas une virgule. string[] cols = line.Split(','); if (cols.Length <= Math.Max(indAddress, Math.Max(indLongitude, Math.Max(indLatitude, indName)))) continue; string sLat = cols[indLatitude].Trim(); string sLon = cols[indLongitude].Trim(); string sName = cols[indName].Trim(); string stown= cols[indTown].Trim(); string sAddress = cols[indAddress].Trim(); if (string.IsNullOrEmpty(sLat) || string.IsNullOrEmpty(sLon)) continue; if (!double.TryParse(sLat, NumberStyles.Float, CultureInfo.InvariantCulture, out double storeLat)) continue; if (!double.TryParse(sLon, NumberStyles.Float, CultureInfo.InvariantCulture, out double storeLon)) continue; double d = HaversineDistanceKm(userLat, userLon, storeLat, storeLon); list.Add(new StoreDistance { town = stown, name = sName, address = sAddress, lineRaw = line, distanceKm = d }); } return list; } private double HaversineDistanceKm(double lat1, double lon1, double lat2, double lon2) { const double R = 6371.0; // rayon de la Terre en km double dLat = Deg2Rad(lat2 - lat1); double dLon = Deg2Rad(lon2 - lon1); double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Cos(Deg2Rad(lat1)) * Math.Cos(Deg2Rad(lat2)) * Math.Sin(dLon / 2) * Math.Sin(dLon / 2); double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); return R * c; } private double Deg2Rad(double deg) => deg * Math.PI / 180.0; }