├── LICENSE
├── index.html
├── README.md
├── config.json
├── styles.css
└── script.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 nikoko107
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | OSINTMap - Outil d'Investigation Géospatiale
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
103 |
104 |
105 |
Requête Overpass générée :
106 |
107 |
108 | Copier la requête
109 |
110 |
111 |
112 |
113 |
114 |
115 |
Carte des résultats
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
Clic droit sur un marqueur pour ouvrir OSM ou Street View
126 |
127 |
128 |
129 |
130 |
131 |
132 | Recherche en cours...
133 |
134 |
135 |
136 |
137 |
138 |
139 |
🎯 Résultats de la recherche
140 |
141 |
142 |
143 |
144 | Nom
145 | Type
146 | Catégorie OSM
147 | Coordonnées
148 | Actions
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
📍 Éléments de référence (compléments)
160 |
161 |
162 |
163 |
164 | Rôle
165 | Nom
166 | Type
167 | Catégorie OSM
168 | Coordonnées
169 | Actions
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
Export des résultats
181 |
182 | Télécharger le rapport JSON
183 |
184 |
185 | Télécharger CSV
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OSINTMap - Outil d'Investigation Géospatiale
2 |
3 | Un outil web professionnel pour l'investigation géospatiale utilisant l'API Overpass d'OpenStreetMap. Conçu pour les analystes OSINT, enquêteurs et chercheurs nécessitant des recherches géographiques précises avec contraintes de proximité.
4 |
5 | 
6 |
7 |
8 | ## 🎯 Fonctionnalités Principales
9 |
10 | ### 🗺️ **Recherche Géospatiale Avancée**
11 | - **Zone de recherche personnalisée** : Dessinez votre zone d'investigation directement sur la carte
12 | - **Recherche principale** : Définissez les éléments que vous cherchez (écoles, hôpitaux, commerces, etc.)
13 | - **Compléments de proximité** : Ajoutez jusqu'à 5 critères de proximité avec distances personnalisées
14 | - **Filtres par nom** : Recherche exacte, contient, ou commence par
15 |
16 | ### 🎨 **Visualisation Interactive**
17 | - **Marqueurs colorés** : Différenciation visuelle automatique des types d'éléments
18 | - 🔵 **Bleu** : Résultats principaux de votre recherche
19 | - 🟢 **Vert** : Premier complément (élément de référence)
20 | - 🟠 **Orange** : Deuxième complément
21 | - 🔴 **Rouge** : Troisième complément
22 | - 🟣 **Violet** : Quatrième complément
23 | - 🟡 **Jaune** : Cinquième complément
24 |
25 | ### 📍 **Recherche d'Adresse Intégrée**
26 | - **Géocodage en temps réel** : Recherchez n'importe quelle adresse avec l'API Nominatim
27 | - **Suggestions automatiques** : Propositions d'adresses pendant la saisie
28 | - **Zoom automatique** : Navigation directe vers l'adresse sélectionnée
29 | - **Marqueur temporaire** : Visualisation de l'adresse avec possibilité de suppression
30 |
31 | ### 📊 **Interface de Résultats Séparée**
32 | - **Table des résultats principaux** : Liste des éléments trouvés correspondant à votre recherche
33 | - **Table des compléments** : Liste séparée des éléments de référence utilisés pour les contraintes de proximité
34 | - **Zoom interactif** : Clic sur une ligne pour zoomer sur la carte
35 | - **Liens externes** : Accès direct à OpenStreetMap et Google Street View
36 |
37 | ### 📤 **Export et Partage**
38 | - **Export JSON** : Rapport complet avec métadonnées et requête Overpass
39 | - **Export CSV** : Données tabulaires pour analyse dans Excel/LibreOffice
40 | - **Copie de requête** : Requête Overpass générée copiable pour utilisation externe
41 |
42 | ## 🚀 Guide d'Utilisation
43 |
44 | ### 1. **Définir la Zone de Recherche**
45 |
46 | 1. Cliquez sur **"Dessiner sur la carte"**
47 | 2. Cliquez et glissez sur la carte pour créer un rectangle de recherche
48 | 3. La zone apparaît en orange avec les coordonnées affichées
49 | 4. Utilisez **"Effacer la zone"** pour recommencer si nécessaire
50 |
51 | ### 2. **Configurer la Recherche Principale**
52 |
53 | 1. **Sélectionnez une catégorie** : Services, Transport, Bâtiments, etc.
54 | 2. **Choisissez les types** : Cochez les types spécifiques (ex: restaurant, école)
55 | 3. **Filtrage par nom** (optionnel) :
56 | - **Nom exact** : Recherche précise
57 | - **Contient** : Le nom contient le texte
58 | - **Commence par** : Le nom commence par le texte
59 |
60 | ### 3. **Ajouter des Compléments de Proximité**
61 |
62 | 1. Cliquez sur **"Ajouter un complément"**
63 | 2. Configurez chaque complément :
64 | - **Catégorie et types** : Comme pour la recherche principale
65 | - **Distance** : Rayon de proximité en mètres
66 | - **Nom** : Filtrage optionnel par nom
67 | 3. Répétez pour jusqu'à 5 compléments
68 |
69 | ### 4. **Lancer la Recherche**
70 |
71 | 1. Cliquez sur **"Rechercher"**
72 | 2. Attendez le chargement (indicateur de progression)
73 | 3. Les résultats apparaissent sur la carte avec des couleurs distinctes
74 |
75 | ### 5. **Analyser les Résultats**
76 |
77 | #### **Sur la Carte :**
78 | - **Marqueurs colorés** : Chaque type d'élément a sa couleur
79 | - **Popups informatifs** : Clic sur un marqueur pour voir les détails
80 | - **Clic droit** : Accès direct à OpenStreetMap
81 |
82 | #### **Dans les Listes :**
83 | - **🎯 Résultats de la recherche** : Vos éléments cibles
84 | - **📍 Éléments de référence** : Les compléments utilisés pour les contraintes
85 | - **Zoom interactif** : Clic sur une ligne pour zoomer sur la carte
86 | - **Sélection visuelle** : Ligne sélectionnée mise en évidence
87 |
88 | ### 6. **Navigation et Outils**
89 |
90 | #### **Recherche d'Adresse :**
91 | 1. Tapez une adresse dans le champ de recherche
92 | 2. Sélectionnez une suggestion
93 | 3. La carte zoome automatiquement sur l'adresse
94 | 4. Un marqueur rouge temporaire est ajouté
95 |
96 | #### **Export des Données :**
97 | - **JSON** : Rapport complet avec métadonnées
98 | - **CSV** : Données pour tableur
99 | - **Requête** : Code Overpass pour réutilisation
100 |
101 | ## 📋 Exemples d'Utilisation
102 |
103 | ### **Exemple 1 : Écoles près de transports**
104 | ```
105 | Zone : Dessiner autour d'une ville
106 | Recherche principale : Catégorie "Services" → Type "École"
107 | Complément 1 : Catégorie "Transport" → Type "Arrêt de bus" → Distance 300m
108 | Résultat : Toutes les écoles à moins de 300m d'un arrêt de bus
109 | ```
110 |
111 | ### **Exemple 2 : Restaurants avec parking et banque**
112 | ```
113 | Zone : Centre-ville
114 | Recherche principale : Catégorie "Services" → Type "Restaurant"
115 | Complément 1 : Catégorie "Services" → Type "Parking" → Distance 200m
116 | Complément 2 : Catégorie "Services" → Type "Banque" → Distance 500m
117 | Résultat : Restaurants avec parking à 200m ET banque à 500m
118 | ```
119 |
120 | ### **Exemple 3 : Recherche par nom spécifique**
121 | ```
122 | Zone : Région
123 | Recherche principale : Catégorie "Services" → Type "École" → Nom contient "Ferdinand"
124 | Complément 1 : Catégorie "Services" → Type "Bureau de poste" → Distance 1000m
125 | Résultat : Écoles contenant "Ferdinand" avec bureau de poste à 1km
126 | ```
127 |
128 | ## 🎨 Interface Utilisateur
129 |
130 | ### **Panneau de Recherche (Gauche)**
131 | - Configuration de la zone de recherche
132 | - Paramètres de recherche principale
133 | - Gestion des compléments de proximité
134 | - Affichage de la requête Overpass générée
135 |
136 | ### **Panneau de Résultats (Droite)**
137 | - Carte interactive avec marqueurs colorés
138 | - Recherche d'adresse intégrée
139 | - Compteur de résultats avec répartition par type
140 | - Listes séparées des résultats et compléments
141 | - Boutons d'export
142 |
143 | ## 🔧 Fonctionnalités Techniques
144 |
145 | ### **Requêtes Overpass Optimisées**
146 | - Génération automatique de requêtes complexes
147 | - Support des contraintes de proximité multiples
148 | - Gestion des bounding box personnalisées
149 | - Filtrage par nom avec expressions régulières
150 |
151 | ### **Visualisation Avancée**
152 | - Marqueurs Leaflet avec icônes colorées
153 | - Popups informatifs avec liens externes
154 | - Zoom automatique sur les résultats
155 | - Synchronisation carte-liste bidirectionnelle
156 |
157 | ### **Performance et Fiabilité**
158 | - Serveurs Overpass multiples avec basculement automatique
159 | - Gestion d'erreurs robuste
160 | - Interface responsive pour tous écrans
161 | - Timeout configurable pour les requêtes
162 |
163 | ## 📊 Types de Données Supportés
164 |
165 | ### **Catégories Principales :**
166 | - **Services et équipements** : Restaurants, banques, hôpitaux, écoles, etc.
167 | - **Transport** : Routes, arrêts, gares, ponts, etc.
168 | - **Bâtiments** : Résidentiel, commercial, industriel, public, etc.
169 | - **Éléments naturels** : Eau, forêts, parcs, plages, etc.
170 | - **Commerces** : Supermarchés, boutiques, pharmacies, etc.
171 | - **Tourisme** : Hôtels, attractions, musées, etc.
172 | - **Sites historiques** : Monuments, châteaux, ruines, etc.
173 | - **Militaire** : Bases, bunkers, zones d'entraînement, etc.
174 | - **Urgences** : Pompiers, ambulances, défibrillateurs, etc.
175 | - **Transport ferroviaire** : Gares, métro, tramway, etc.
176 |
177 | ### **Filtres Disponibles :**
178 | - **Par catégorie** : Recherche large par type d'élément
179 | - **Par type spécifique** : Sélection multiple de sous-types
180 | - **Par nom** : Filtrage textuel avec modes exact/contient/commence
181 | - **Par proximité** : Contraintes de distance avec éléments de référence
182 |
183 | ## 🛠️ Installation et Configuration
184 |
185 | ### **Prérequis**
186 | - Navigateur web moderne (Chrome, Firefox, Safari, Edge)
187 | - Connexion Internet pour les API externes
188 |
189 | ### **Utilisation**
190 | 1. Ouvrez `index.html` dans votre navigateur
191 | 2. L'application se charge automatiquement
192 | 3. Aucune installation ou configuration supplémentaire requise
193 |
194 | ### **APIs Utilisées**
195 | - **Overpass API** : Données OpenStreetMap
196 | - **Nominatim** : Géocodage d'adresses
197 | - **Leaflet** : Cartographie interactive
198 |
199 | ## 🔍 Cas d'Usage OSINT
200 |
201 | ### **Investigation Urbaine**
202 | - Localiser des établissements avec contraintes spécifiques
203 | - Analyser la densité de services dans une zone
204 | - Identifier des patterns géographiques suspects
205 |
206 | ### **Recherche de Personnes**
207 | - Trouver des lieux fréquentés avec critères multiples
208 | - Analyser l'environnement autour d'adresses connues
209 | - Identifier des points d'intérêt dans un périmètre
210 |
211 | ### **Analyse de Sécurité**
212 | - Évaluer l'accessibilité aux services d'urgence
213 | - Identifier les infrastructures critiques
214 | - Analyser les voies d'accès et de fuite
215 |
216 | ### **Recherche Académique**
217 | - Études de géographie urbaine
218 | - Analyse de l'accessibilité aux services
219 | - Recherche en aménagement du territoire
220 |
221 | ## 📝 Notes Techniques
222 |
223 | ### **Limitations**
224 | - Dépendant de la qualité des données OpenStreetMap
225 | - Timeout de 25 secondes pour les requêtes complexes
226 | - Limitation à 5 compléments de proximité simultanés
227 |
228 | ### **Optimisations**
229 | - Requêtes optimisées pour réduire la charge serveur
230 | - Cache des résultats pour éviter les requêtes répétées
231 | - Interface responsive pour tous types d'écrans
232 |
233 | ### **Sécurité**
234 | - Aucune donnée personnelle stockée
235 | - Requêtes anonymes vers les APIs publiques
236 | - Code source ouvert et auditable
237 |
238 | ## 🆘 Support et Dépannage
239 |
240 | ### **Problèmes Courants**
241 | - **Pas de résultats** : Vérifiez la zone de recherche et les critères
242 | - **Erreur de serveur** : L'outil bascule automatiquement vers un autre serveur
243 | - **Carte ne se charge pas** : Vérifiez votre connexion Internet
244 |
245 | ### **Conseils d'Utilisation**
246 | - Commencez par des zones de recherche petites
247 | - Utilisez des distances de proximité raisonnables (< 5km)
248 | - Testez d'abord sans filtres par nom pour valider la zone
249 |
250 | ---
251 |
252 | **Développé pour la communauté OSINT** - Outil libre et open source pour l'investigation géospatiale professionnelle.
253 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": {
3 | "amenity": {
4 | "label": "Services et équipements",
5 | "types": {
6 | "restaurant": "Restaurant",
7 | "cafe": "Café",
8 | "bar": "Bar",
9 | "fast_food": "Restauration rapide",
10 | "bank": "Banque",
11 | "atm": "Distributeur automatique",
12 | "hospital": "Hôpital",
13 | "clinic": "Clinique",
14 | "pharmacy": "Pharmacie",
15 | "school": "École",
16 | "university": "Université",
17 | "library": "Bibliothèque",
18 | "police": "Police",
19 | "fire_station": "Caserne de pompiers",
20 | "post_office": "Bureau de poste",
21 | "townhall": "Mairie",
22 | "fuel": "Station-service",
23 | "parking": "Parking",
24 | "church": "Église",
25 | "mosque": "Mosquée",
26 | "cinema": "Cinéma",
27 | "theatre": "Théâtre"
28 | }
29 | },
30 | "highway": {
31 | "label": "Routes et transport",
32 | "types": {
33 | "motorway": "Autoroute",
34 | "trunk": "Route nationale",
35 | "primary": "Route principale",
36 | "secondary": "Route secondaire",
37 | "tertiary": "Route tertiaire",
38 | "residential": "Route résidentielle",
39 | "service": "Route de service",
40 | "pedestrian": "Zone piétonne",
41 | "footway": "Chemin piéton",
42 | "cycleway": "Piste cyclable",
43 | "path": "Sentier",
44 | "track": "Piste",
45 | "bus_stop": "Arrêt de bus",
46 | "traffic_signals": "Feux de circulation",
47 | "stop": "Stop",
48 | "give_way": "Cédez le passage",
49 | "speed_camera": "Radar",
50 | "toll_booth": "Péage",
51 | "motorway_junction": "Échangeur autoroutier",
52 | "turning_circle": "Rond-point",
53 | "mini_roundabout": "Mini rond-point",
54 | "crossing": "Passage piéton",
55 | "ford": "Gué",
56 | "bridge": "Pont",
57 | "tunnel": "Tunnel"
58 | }
59 | },
60 | "building": {
61 | "label": "Bâtiments",
62 | "types": {
63 | "house": "Maison",
64 | "apartment": "Appartement",
65 | "residential": "Résidentiel",
66 | "commercial": "Commercial",
67 | "industrial": "Industriel",
68 | "office": "Bureau",
69 | "retail": "Commerce",
70 | "warehouse": "Entrepôt",
71 | "hospital": "Hôpital",
72 | "school": "École",
73 | "university": "Université",
74 | "church": "Église",
75 | "mosque": "Mosquée",
76 | "synagogue": "Synagogue",
77 | "temple": "Temple",
78 | "hotel": "Hôtel",
79 | "dormitory": "Dortoir",
80 | "train_station": "Gare",
81 | "transportation": "Transport",
82 | "public": "Public",
83 | "government": "Gouvernement",
84 | "civic": "Civique",
85 | "military": "Militaire",
86 | "police": "Police",
87 | "fire_station": "Caserne de pompiers",
88 | "prison": "Prison",
89 | "stadium": "Stade",
90 | "sports_hall": "Salle de sport",
91 | "gymnasium": "Gymnase",
92 | "theatre": "Théâtre",
93 | "cinema": "Cinéma",
94 | "museum": "Musée",
95 | "library": "Bibliothèque",
96 | "garage": "Garage",
97 | "parking": "Parking",
98 | "hangar": "Hangar",
99 | "shed": "Hangar",
100 | "greenhouse": "Serre",
101 | "barn": "Grange",
102 | "stable": "Écurie",
103 | "farm": "Ferme",
104 | "farm_auxiliary": "Bâtiment agricole",
105 | "ruins": "Ruines",
106 | "construction": "En construction"
107 | }
108 | },
109 | "natural": {
110 | "label": "Éléments naturels",
111 | "types": {
112 | "water": "Eau",
113 | "coastline": "Côte",
114 | "beach": "Plage",
115 | "cliff": "Falaise",
116 | "peak": "Sommet",
117 | "volcano": "Volcan",
118 | "glacier": "Glacier",
119 | "cave_entrance": "Entrée de grotte",
120 | "spring": "Source",
121 | "hot_spring": "Source chaude",
122 | "geyser": "Geyser",
123 | "tree": "Arbre",
124 | "tree_row": "Rangée d'arbres",
125 | "wood": "Bois",
126 | "forest": "Forêt",
127 | "grassland": "Prairie",
128 | "heath": "Lande",
129 | "moor": "Lande",
130 | "scrub": "Broussailles",
131 | "wetland": "Zone humide",
132 | "marsh": "Marais",
133 | "swamp": "Marécage",
134 | "bog": "Tourbière",
135 | "fen": "Marais alcalin",
136 | "salt_pond": "Marais salant",
137 | "mud": "Boue",
138 | "sand": "Sable",
139 | "rock": "Rocher",
140 | "stone": "Pierre",
141 | "scree": "Éboulis",
142 | "shingle": "Galets",
143 | "bare_rock": "Roche nue",
144 | "ridge": "Crête",
145 | "arete": "Arête",
146 | "saddle": "Col",
147 | "valley": "Vallée",
148 | "peninsula": "Péninsule",
149 | "cape": "Cap",
150 | "bay": "Baie",
151 | "strait": "Détroit"
152 | }
153 | },
154 | "landuse": {
155 | "label": "Utilisation du sol",
156 | "types": {
157 | "residential": "Résidentiel",
158 | "commercial": "Commercial",
159 | "industrial": "Industriel",
160 | "retail": "Commerce de détail",
161 | "institutional": "Institutionnel",
162 | "education": "Éducation",
163 | "recreation_ground": "Terrain de loisirs",
164 | "village_green": "Place du village",
165 | "religious": "Religieux",
166 | "cemetery": "Cimetière",
167 | "military": "Militaire",
168 | "prison": "Prison",
169 | "quarry": "Carrière",
170 | "landfill": "Décharge",
171 | "brownfield": "Friche industrielle",
172 | "greenfield": "Terrain vierge",
173 | "construction": "Construction",
174 | "railway": "Chemin de fer",
175 | "highway": "Route",
176 | "port": "Port",
177 | "airport": "Aéroport",
178 | "forest": "Forêt",
179 | "farmland": "Terre agricole",
180 | "farmyard": "Cour de ferme",
181 | "orchard": "Verger",
182 | "vineyard": "Vignoble",
183 | "plant_nursery": "Pépinière",
184 | "allotments": "Jardins familiaux",
185 | "meadow": "Prairie",
186 | "grass": "Herbe",
187 | "basin": "Bassin",
188 | "reservoir": "Réservoir",
189 | "salt_pond": "Marais salant"
190 | }
191 | },
192 | "leisure": {
193 | "label": "Loisirs et sport",
194 | "types": {
195 | "park": "Parc",
196 | "garden": "Jardin",
197 | "playground": "Aire de jeux",
198 | "sports_centre": "Centre sportif",
199 | "stadium": "Stade",
200 | "track": "Piste",
201 | "pitch": "Terrain de sport",
202 | "swimming_pool": "Piscine",
203 | "golf_course": "Terrain de golf",
204 | "miniature_golf": "Mini-golf",
205 | "tennis": "Tennis",
206 | "basketball": "Basketball",
207 | "football": "Football",
208 | "soccer": "Football",
209 | "volleyball": "Volleyball",
210 | "baseball": "Baseball",
211 | "athletics": "Athlétisme",
212 | "running": "Course à pied",
213 | "cycling": "Cyclisme",
214 | "horse_riding": "Équitation",
215 | "climbing": "Escalade",
216 | "fitness_centre": "Centre de fitness",
217 | "fitness_station": "Station de fitness",
218 | "marina": "Marina",
219 | "slipway": "Cale de mise à l'eau",
220 | "beach_resort": "Station balnéaire",
221 | "water_park": "Parc aquatique",
222 | "theme_park": "Parc d'attractions",
223 | "amusement_arcade": "Salle d'arcade",
224 | "bowling_alley": "Bowling",
225 | "escape_game": "Escape game",
226 | "hackerspace": "Hackerspace",
227 | "adult_gaming_centre": "Centre de jeux pour adultes",
228 | "dance": "Danse",
229 | "fishing": "Pêche",
230 | "hunting": "Chasse",
231 | "nature_reserve": "Réserve naturelle",
232 | "common": "Terrain communal",
233 | "dog_park": "Parc à chiens",
234 | "firepit": "Foyer",
235 | "picnic_table": "Table de pique-nique",
236 | "sauna": "Sauna",
237 | "outdoor_seating": "Terrasse"
238 | }
239 | },
240 | "shop": {
241 | "label": "Commerces",
242 | "types": {
243 | "supermarket": "Supermarché",
244 | "convenience": "Épicerie",
245 | "mall": "Centre commercial",
246 | "department_store": "Grand magasin",
247 | "general": "Magasin général",
248 | "kiosk": "Kiosque",
249 | "bakery": "Boulangerie",
250 | "butcher": "Boucherie",
251 | "cheese": "Fromagerie",
252 | "deli": "Charcuterie",
253 | "dairy": "Laiterie",
254 | "fishmonger": "Poissonnerie",
255 | "greengrocer": "Primeur",
256 | "health_food": "Magasin bio",
257 | "ice_cream": "Glacier",
258 | "pasta": "Pâtes",
259 | "pastry": "Pâtisserie",
260 | "seafood": "Fruits de mer",
261 | "spices": "Épices",
262 | "tea": "Thé",
263 | "wine": "Caviste",
264 | "alcohol": "Magasin d'alcool",
265 | "beverages": "Boissons",
266 | "brewing_supplies": "Fournitures de brassage",
267 | "water": "Eau",
268 | "clothes": "Vêtements",
269 | "fashion": "Mode",
270 | "boutique": "Boutique",
271 | "fabric": "Tissu",
272 | "leather": "Cuir",
273 | "tailor": "Tailleur",
274 | "watches": "Montres",
275 | "jewelry": "Bijouterie",
276 | "shoes": "Chaussures",
277 | "bag": "Maroquinerie",
278 | "beauty": "Beauté",
279 | "chemist": "Droguerie",
280 | "cosmetics": "Cosmétiques",
281 | "erotic": "Érotique",
282 | "hairdresser": "Coiffeur",
283 | "hearing_aids": "Appareils auditifs",
284 | "herbalist": "Herboriste",
285 | "massage": "Massage",
286 | "medical_supply": "Matériel médical",
287 | "nutrition_supplements": "Compléments alimentaires",
288 | "optician": "Opticien",
289 | "perfumery": "Parfumerie",
290 | "tattoo": "Tatouage",
291 | "bathroom_furnishing": "Salle de bain",
292 | "doityourself": "Bricolage",
293 | "electrical": "Électricité",
294 | "energy": "Énergie",
295 | "fireplace": "Cheminée",
296 | "florist": "Fleuriste",
297 | "garden_centre": "Jardinerie",
298 | "gas": "Gaz",
299 | "glaziery": "Vitrerie",
300 | "hardware": "Quincaillerie",
301 | "houseware": "Articles ménagers",
302 | "locksmith": "Serrurier",
303 | "paint": "Peinture",
304 | "trade": "Commerce",
305 | "antiques": "Antiquités",
306 | "bed": "Literie",
307 | "candles": "Bougies",
308 | "carpet": "Tapis",
309 | "curtain": "Rideaux",
310 | "furniture": "Meubles",
311 | "interior_decoration": "Décoration intérieure",
312 | "kitchen": "Cuisine",
313 | "lamps": "Lampes",
314 | "tiles": "Carrelage",
315 | "window_blind": "Stores",
316 | "computer": "Informatique",
317 | "electronics": "Électronique",
318 | "hifi": "Hi-fi",
319 | "mobile_phone": "Téléphone portable",
320 | "radiotechnics": "Radiotechnique",
321 | "vacuum_cleaner": "Aspirateur",
322 | "bicycle": "Vélo",
323 | "car": "Automobile",
324 | "car_parts": "Pièces auto",
325 | "car_repair": "Réparation auto",
326 | "fuel": "Carburant",
327 | "motorcycle": "Moto",
328 | "tyres": "Pneus",
329 | "books": "Librairie",
330 | "gift": "Cadeaux",
331 | "lottery": "Loterie",
332 | "newsagent": "Marchand de journaux",
333 | "stationery": "Papeterie",
334 | "ticket": "Billetterie",
335 | "travel_agency": "Agence de voyage",
336 | "laundry": "Laverie",
337 | "dry_cleaning": "Pressing",
338 | "e-cigarette": "Cigarette électronique",
339 | "funeral_directors": "Pompes funèbres",
340 | "money_lender": "Prêteur sur gages",
341 | "pawnbroker": "Mont-de-piété",
342 | "pet": "Animalerie",
343 | "pet_grooming": "Toilettage",
344 | "pyrotechnics": "Pyrotechnie",
345 | "religion": "Articles religieux",
346 | "second_hand": "Occasion",
347 | "tobacco": "Tabac",
348 | "toys": "Jouets",
349 | "vacant": "Vacant",
350 | "variety_store": "Bazar",
351 | "video": "Vidéo",
352 | "video_games": "Jeux vidéo",
353 | "weapons": "Armes"
354 | }
355 | },
356 | "tourism": {
357 | "label": "Tourisme",
358 | "types": {
359 | "hotel": "Hôtel",
360 | "motel": "Motel",
361 | "guest_house": "Maison d'hôtes",
362 | "hostel": "Auberge de jeunesse",
363 | "chalet": "Chalet",
364 | "apartment": "Appartement",
365 | "camp_site": "Camping",
366 | "caravan_site": "Camping-car",
367 | "alpine_hut": "Refuge alpin",
368 | "wilderness_hut": "Refuge",
369 | "attraction": "Attraction",
370 | "artwork": "Œuvre d'art",
371 | "gallery": "Galerie",
372 | "museum": "Musée",
373 | "theme_park": "Parc d'attractions",
374 | "zoo": "Zoo",
375 | "aquarium": "Aquarium",
376 | "viewpoint": "Point de vue",
377 | "picnic_site": "Aire de pique-nique",
378 | "information": "Information touristique",
379 | "map": "Carte",
380 | "board": "Panneau d'information",
381 | "guidepost": "Poteau indicateur",
382 | "office": "Office de tourisme"
383 | }
384 | },
385 | "historic": {
386 | "label": "Sites historiques",
387 | "types": {
388 | "monument": "Monument",
389 | "memorial": "Mémorial",
390 | "archaeological_site": "Site archéologique",
391 | "battlefield": "Champ de bataille",
392 | "boundary_stone": "Borne frontière",
393 | "building": "Bâtiment historique",
394 | "castle": "Château",
395 | "city_gate": "Porte de ville",
396 | "citywalls": "Remparts",
397 | "fort": "Fort",
398 | "manor": "Manoir",
399 | "palace": "Palais",
400 | "ruins": "Ruines",
401 | "tower": "Tour",
402 | "wayside_cross": "Croix de chemin",
403 | "wayside_shrine": "Oratoire",
404 | "wreck": "Épave",
405 | "yes": "Site historique"
406 | }
407 | },
408 | "military": {
409 | "label": "Sites militaires",
410 | "types": {
411 | "airfield": "Terrain d'aviation militaire",
412 | "bunker": "Bunker",
413 | "barracks": "Caserne",
414 | "checkpoint": "Poste de contrôle",
415 | "danger_area": "Zone de danger",
416 | "naval_base": "Base navale",
417 | "nuclear_explosion_site": "Site d'explosion nucléaire",
418 | "obstacle_course": "Parcours d'obstacles",
419 | "office": "Bureau militaire",
420 | "range": "Champ de tir",
421 | "training_area": "Zone d'entraînement",
422 | "trench": "Tranchée"
423 | }
424 | },
425 | "emergency": {
426 | "label": "Services d'urgence",
427 | "types": {
428 | "ambulance_station": "Station d'ambulance",
429 | "defibrillator": "Défibrillateur",
430 | "emergency_ward_entrance": "Entrée des urgences",
431 | "fire_alarm_box": "Avertisseur d'incendie",
432 | "fire_extinguisher": "Extincteur",
433 | "fire_flapper": "Battoir à feu",
434 | "fire_hose": "Lance à incendie",
435 | "fire_hydrant": "Bouche d'incendie",
436 | "landing_site": "Site d'atterrissage",
437 | "life_ring": "Bouée de sauvetage",
438 | "lifeguard": "Maître-nageur",
439 | "phone": "Téléphone d'urgence",
440 | "ses_station": "Station SES",
441 | "siren": "Sirène",
442 | "suction_point": "Point d'aspiration",
443 | "water_tank": "Réservoir d'eau"
444 | }
445 | },
446 | "railway": {
447 | "label": "Transport ferroviaire",
448 | "types": {
449 | "station": "Gare",
450 | "halt": "Halte",
451 | "tram_stop": "Arrêt de tram",
452 | "subway_entrance": "Entrée de métro",
453 | "rail": "Rail",
454 | "subway": "Métro",
455 | "tram": "Tramway",
456 | "light_rail": "Train léger",
457 | "monorail": "Monorail",
458 | "narrow_gauge": "Voie étroite",
459 | "preserved": "Ligne préservée",
460 | "funicular": "Funiculaire",
461 | "miniature": "Train miniature",
462 | "turntable": "Plaque tournante",
463 | "roundhouse": "Rotonde",
464 | "crossing": "Passage à niveau",
465 | "level_crossing": "Passage à niveau",
466 | "signal": "Signal",
467 | "switch": "Aiguillage",
468 | "railway_crossing": "Croisement ferroviaire",
469 | "buffer_stop": "Butoir"
470 | }
471 | },
472 | "public_transport": {
473 | "label": "Transport public",
474 | "types": {
475 | "stop_position": "Position d'arrêt",
476 | "platform": "Quai",
477 | "station": "Station",
478 | "stop_area": "Zone d'arrêt"
479 | }
480 | }
481 | },
482 | "defaultLocation": {
483 | "lat": 48.8566,
484 | "lon": 2.3522,
485 | "name": "Paris, France"
486 | },
487 | "overpassServers": [
488 | "https://overpass-api.de/api/interpreter",
489 | "https://overpass.kumi.systems/api/interpreter",
490 | "https://overpass.openstreetmap.ru/api/interpreter"
491 | ]
492 | }
493 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | /* Reset et base */
2 | * {
3 | margin: 0;
4 | padding: 0;
5 | box-sizing: border-box;
6 | }
7 |
8 | body {
9 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
11 | min-height: 100vh;
12 | color: #333;
13 | line-height: 1.6;
14 | }
15 |
16 | .container {
17 | max-width: 1400px;
18 | margin: 0 auto;
19 | padding: 20px;
20 | }
21 |
22 | /* Header */
23 | header {
24 | text-align: center;
25 | margin-bottom: 30px;
26 | color: white;
27 | }
28 |
29 | header h1 {
30 | font-size: 2.5rem;
31 | margin-bottom: 10px;
32 | text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
33 | }
34 |
35 | header p {
36 | font-size: 1.1rem;
37 | opacity: 0.9;
38 | }
39 |
40 | /* Layout principal */
41 | .main-content {
42 | display: grid;
43 | grid-template-columns: 1fr 1fr;
44 | gap: 30px;
45 | margin-bottom: 30px;
46 | }
47 |
48 | /* Panneaux */
49 | .search-panel, .results-panel {
50 | background: white;
51 | border-radius: 15px;
52 | padding: 25px;
53 | box-shadow: 0 10px 30px rgba(0,0,0,0.1);
54 | backdrop-filter: blur(10px);
55 | }
56 |
57 | .search-panel {
58 | background: rgba(255,255,255,0.95);
59 | }
60 |
61 | .results-panel {
62 | background: rgba(255,255,255,0.95);
63 | }
64 |
65 | /* Formulaire */
66 | #searchForm {
67 | margin-bottom: 25px;
68 | }
69 |
70 | /* Sélecteur de mode de recherche */
71 | .search-mode-selector {
72 | margin-bottom: 25px;
73 | padding: 20px;
74 | background: #f8f9fa;
75 | border-radius: 10px;
76 | border-left: 4px solid #667eea;
77 | }
78 |
79 | .search-mode-selector h3 {
80 | margin-bottom: 15px;
81 | color: #495057;
82 | font-size: 1.1rem;
83 | }
84 |
85 | .mode-options {
86 | display: flex;
87 | flex-direction: column;
88 | gap: 15px;
89 | }
90 |
91 | .mode-option {
92 | display: flex;
93 | align-items: flex-start;
94 | gap: 12px;
95 | padding: 15px;
96 | background: white;
97 | border: 2px solid #e9ecef;
98 | border-radius: 8px;
99 | cursor: pointer;
100 | transition: all 0.3s ease;
101 | }
102 |
103 | .mode-option:hover {
104 | border-color: #667eea;
105 | background: #f8f9ff;
106 | }
107 |
108 | .mode-option input[type="radio"] {
109 | margin: 0;
110 | width: 18px;
111 | height: 18px;
112 | accent-color: #667eea;
113 | }
114 |
115 | .mode-option input[type="radio"]:checked + span {
116 | color: #667eea;
117 | font-weight: 600;
118 | }
119 |
120 | .mode-option span {
121 | font-weight: 500;
122 | color: #495057;
123 | margin-bottom: 5px;
124 | display: block;
125 | }
126 |
127 | .mode-option small {
128 | color: #6c757d;
129 | font-size: 13px;
130 | line-height: 1.4;
131 | display: block;
132 | }
133 |
134 | .mode-option input[type="radio"]:checked ~ small {
135 | color: #495057;
136 | }
137 |
138 | /* Champs spécifiques au mode proximité */
139 | .proximity-fields {
140 | display: flex;
141 | flex-direction: column;
142 | gap: 15px;
143 | padding: 15px;
144 | background: #e8f4fd;
145 | border-radius: 8px;
146 | border-left: 3px solid #007bff;
147 | margin-bottom: 15px;
148 | }
149 |
150 | .proximity-fields .field-group label {
151 | color: #0056b3;
152 | font-weight: 600;
153 | }
154 |
155 | .proximity-fields .distance-input input {
156 | border-color: #007bff;
157 | }
158 |
159 | .proximity-fields .distance-input input:focus {
160 | border-color: #0056b3;
161 | box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
162 | }
163 |
164 | .conditions-container {
165 | margin-bottom: 20px;
166 | }
167 |
168 | .condition-group {
169 | margin-bottom: 15px;
170 | padding: 20px;
171 | background: #f8f9fa;
172 | border-radius: 10px;
173 | border-left: 4px solid #667eea;
174 | }
175 |
176 | .condition-header {
177 | display: flex;
178 | justify-content: space-between;
179 | align-items: center;
180 | margin-bottom: 15px;
181 | }
182 |
183 | .condition-number {
184 | font-weight: bold;
185 | color: #667eea;
186 | font-size: 1.1rem;
187 | }
188 |
189 | .remove-condition {
190 | background: #dc3545;
191 | color: white;
192 | border: none;
193 | border-radius: 50%;
194 | width: 30px;
195 | height: 30px;
196 | cursor: pointer;
197 | display: flex;
198 | align-items: center;
199 | justify-content: center;
200 | transition: all 0.3s ease;
201 | }
202 |
203 | .remove-condition:hover {
204 | background: #c82333;
205 | transform: scale(1.1);
206 | }
207 |
208 | .condition-fields {
209 | display: flex;
210 | flex-direction: column;
211 | gap: 20px;
212 | }
213 |
214 | .field-group {
215 | display: flex;
216 | flex-direction: column;
217 | gap: 8px;
218 | }
219 |
220 | .field-group label {
221 | font-weight: 600;
222 | color: #495057;
223 | font-size: 14px;
224 | }
225 |
226 | .field-group select,
227 | .field-group input {
228 | padding: 12px;
229 | border: 2px solid #e9ecef;
230 | border-radius: 8px;
231 | font-size: 14px;
232 | transition: all 0.3s ease;
233 | background: white;
234 | }
235 |
236 | .field-group select:focus,
237 | .field-group input:focus {
238 | outline: none;
239 | border-color: #667eea;
240 | box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
241 | }
242 |
243 | .field-group input:disabled {
244 | background: #f8f9fa;
245 | color: #6c757d;
246 | cursor: not-allowed;
247 | }
248 |
249 | /* Types container avec cases à cocher */
250 | .types-container {
251 | max-height: 120px;
252 | overflow-y: auto;
253 | border: 2px solid #e9ecef;
254 | border-radius: 8px;
255 | padding: 15px;
256 | background: white;
257 | }
258 |
259 | .types-container.empty {
260 | display: flex;
261 | align-items: center;
262 | justify-content: center;
263 | min-height: 60px;
264 | }
265 |
266 | .no-types {
267 | color: #6c757d;
268 | font-style: italic;
269 | margin: 0;
270 | text-align: center;
271 | }
272 |
273 | .type-checkbox {
274 | display: flex;
275 | align-items: center;
276 | gap: 8px;
277 | margin-bottom: 8px;
278 | padding: 5px;
279 | border-radius: 4px;
280 | transition: background-color 0.2s ease;
281 | }
282 |
283 | .type-checkbox:hover {
284 | background-color: #f8f9fa;
285 | }
286 |
287 | .type-checkbox input[type="checkbox"] {
288 | margin: 0;
289 | width: 16px;
290 | height: 16px;
291 | }
292 |
293 | .type-checkbox label {
294 | margin: 0;
295 | cursor: pointer;
296 | font-weight: normal;
297 | font-size: 13px;
298 | flex: 1;
299 | }
300 |
301 | /* Recherche par nom */
302 | .name-search {
303 | display: flex;
304 | gap: 10px;
305 | align-items: center;
306 | }
307 |
308 | .name-search select {
309 | min-width: 140px;
310 | }
311 |
312 | .name-search input {
313 | flex: 1;
314 | }
315 |
316 | /* Distance avec unité */
317 | .distance-input {
318 | display: flex;
319 | align-items: center;
320 | gap: 10px;
321 | }
322 |
323 | .distance-input input {
324 | flex: 1;
325 | max-width: 150px;
326 | }
327 |
328 | .distance-input .unit {
329 | color: #6c757d;
330 | font-size: 14px;
331 | font-weight: 500;
332 | }
333 |
334 | /* Contrôles */
335 | .controls {
336 | display: flex;
337 | flex-wrap: wrap;
338 | gap: 15px;
339 | align-items: center;
340 | justify-content: center;
341 | padding: 20px;
342 | background: #f8f9fa;
343 | border-radius: 10px;
344 | }
345 |
346 | .operators {
347 | display: flex;
348 | gap: 10px;
349 | }
350 |
351 | /* Boutons */
352 | .btn {
353 | padding: 12px 20px;
354 | border: none;
355 | border-radius: 8px;
356 | font-size: 14px;
357 | font-weight: 600;
358 | cursor: pointer;
359 | transition: all 0.3s ease;
360 | display: inline-flex;
361 | align-items: center;
362 | gap: 8px;
363 | text-decoration: none;
364 | }
365 |
366 | .btn:hover {
367 | transform: translateY(-2px);
368 | box-shadow: 0 5px 15px rgba(0,0,0,0.2);
369 | }
370 |
371 | .btn-primary {
372 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
373 | color: white;
374 | }
375 |
376 | .btn-secondary {
377 | background: #6c757d;
378 | color: white;
379 | }
380 |
381 | .btn-success {
382 | background: #28a745;
383 | color: white;
384 | }
385 |
386 | .btn-danger {
387 | background: #dc3545;
388 | color: white;
389 | }
390 |
391 | .btn-operator {
392 | background: #17a2b8;
393 | color: white;
394 | min-width: 50px;
395 | justify-content: center;
396 | }
397 |
398 | /* Section requête */
399 | .query-section {
400 | margin-top: 25px;
401 | padding: 20px;
402 | background: #f8f9fa;
403 | border-radius: 10px;
404 | }
405 |
406 | .query-section h3 {
407 | margin-bottom: 15px;
408 | color: #495057;
409 | }
410 |
411 | .query-display {
412 | background: #2d3748;
413 | color: #e2e8f0;
414 | padding: 15px;
415 | border-radius: 8px;
416 | font-family: 'Courier New', monospace;
417 | font-size: 13px;
418 | white-space: pre-wrap;
419 | word-break: break-all;
420 | min-height: 100px;
421 | margin-bottom: 15px;
422 | border: 1px solid #4a5568;
423 | }
424 |
425 | /* Carte */
426 | .map-container {
427 | margin-bottom: 25px;
428 | }
429 |
430 | .map-container h3 {
431 | margin-bottom: 15px;
432 | color: #495057;
433 | }
434 |
435 | /* Recherche d'adresse */
436 | .address-search {
437 | position: relative;
438 | display: flex;
439 | gap: 10px;
440 | margin-bottom: 15px;
441 | align-items: center;
442 | }
443 |
444 | .address-search input {
445 | flex: 1;
446 | padding: 10px 12px;
447 | border: 2px solid #e9ecef;
448 | border-radius: 8px;
449 | font-size: 14px;
450 | transition: all 0.3s ease;
451 | background: white;
452 | }
453 |
454 | .address-search input:focus {
455 | outline: none;
456 | border-color: #667eea;
457 | box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
458 | }
459 |
460 | .address-search button {
461 | padding: 10px 15px;
462 | min-width: auto;
463 | }
464 |
465 | .address-suggestions {
466 | position: absolute;
467 | top: 100%;
468 | left: 0;
469 | right: 50px;
470 | background: white;
471 | border: 2px solid #e9ecef;
472 | border-top: none;
473 | border-radius: 0 0 8px 8px;
474 | max-height: 200px;
475 | overflow-y: auto;
476 | z-index: 1000;
477 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
478 | }
479 |
480 | .address-suggestion {
481 | padding: 12px;
482 | cursor: pointer;
483 | border-bottom: 1px solid #f8f9fa;
484 | transition: background-color 0.2s ease;
485 | }
486 |
487 | .address-suggestion:hover {
488 | background-color: #f8f9fa;
489 | }
490 |
491 | .address-suggestion:last-child {
492 | border-bottom: none;
493 | }
494 |
495 | .address-suggestion .suggestion-name {
496 | font-weight: 600;
497 | color: #495057;
498 | margin-bottom: 4px;
499 | }
500 |
501 | .address-suggestion .suggestion-details {
502 | font-size: 12px;
503 | color: #6c757d;
504 | }
505 |
506 | #map {
507 | height: 400px;
508 | border-radius: 10px;
509 | border: 2px solid #e9ecef;
510 | }
511 |
512 | .map-info {
513 | margin-top: 10px;
514 | padding: 10px;
515 | background: #e3f2fd;
516 | border-radius: 8px;
517 | font-size: 14px;
518 | color: #1565c0;
519 | }
520 |
521 | /* Informations résultats */
522 | .results-info {
523 | margin-bottom: 25px;
524 | }
525 |
526 | .results-count {
527 | padding: 15px;
528 | background: #d4edda;
529 | border: 1px solid #c3e6cb;
530 | border-radius: 8px;
531 | color: #155724;
532 | font-weight: bold;
533 | text-align: center;
534 | }
535 |
536 | .loading {
537 | padding: 15px;
538 | background: #fff3cd;
539 | border: 1px solid #ffeaa7;
540 | border-radius: 8px;
541 | color: #856404;
542 | text-align: center;
543 | font-weight: bold;
544 | }
545 |
546 | .loading i {
547 | margin-right: 10px;
548 | }
549 |
550 | /* Liste des résultats */
551 | .results-list {
552 | margin-bottom: 25px;
553 | }
554 |
555 | .results-list h4 {
556 | margin-bottom: 15px;
557 | color: #495057;
558 | font-size: 1.1rem;
559 | }
560 |
561 | /* Sections séparées des résultats */
562 | .main-results-section {
563 | margin-bottom: 30px;
564 | }
565 |
566 | .complement-results-section {
567 | margin-bottom: 25px;
568 | }
569 |
570 | .main-results-section h4 {
571 | color: #2196f3;
572 | font-weight: 600;
573 | border-left: 4px solid #2196f3;
574 | padding-left: 12px;
575 | margin-bottom: 15px;
576 | }
577 |
578 | .complement-results-section h4 {
579 | color: #ff9800;
580 | font-weight: 600;
581 | border-left: 4px solid #ff9800;
582 | padding-left: 12px;
583 | margin-bottom: 15px;
584 | }
585 |
586 | .results-table-container {
587 | max-height: 400px;
588 | overflow-y: auto;
589 | border: 2px solid #e9ecef;
590 | border-radius: 8px;
591 | background: white;
592 | }
593 |
594 | #resultsTable {
595 | width: 100%;
596 | border-collapse: collapse;
597 | font-size: 14px;
598 | }
599 |
600 | #resultsTable thead {
601 | background: #f8f9fa;
602 | position: sticky;
603 | top: 0;
604 | z-index: 10;
605 | }
606 |
607 | #resultsTable th {
608 | padding: 12px 8px;
609 | text-align: left;
610 | font-weight: 600;
611 | color: #495057;
612 | border-bottom: 2px solid #dee2e6;
613 | white-space: nowrap;
614 | }
615 |
616 | #resultsTable td {
617 | padding: 10px 8px;
618 | border-bottom: 1px solid #dee2e6;
619 | vertical-align: middle;
620 | }
621 |
622 | #resultsTable tbody tr {
623 | cursor: pointer;
624 | transition: background-color 0.2s ease;
625 | }
626 |
627 | #resultsTable tbody tr:hover {
628 | background-color: #f8f9fa;
629 | }
630 |
631 | #resultsTable tbody tr.selected {
632 | background-color: #e3f2fd;
633 | border-left: 4px solid #2196f3;
634 | }
635 |
636 | .result-name {
637 | font-weight: 600;
638 | color: #495057;
639 | max-width: 150px;
640 | overflow: hidden;
641 | text-overflow: ellipsis;
642 | white-space: nowrap;
643 | }
644 |
645 | .result-type {
646 | color: #6c757d;
647 | font-size: 13px;
648 | }
649 |
650 | .result-category {
651 | font-family: 'Courier New', monospace;
652 | font-size: 12px;
653 | color: #007bff;
654 | background: #f8f9ff;
655 | padding: 2px 6px;
656 | border-radius: 4px;
657 | white-space: nowrap;
658 | }
659 |
660 | .result-coords {
661 | font-family: 'Courier New', monospace;
662 | font-size: 12px;
663 | color: #6c757d;
664 | white-space: nowrap;
665 | }
666 |
667 | .result-role {
668 | font-size: 12px;
669 | font-weight: 600;
670 | padding: 4px 8px;
671 | border-radius: 4px;
672 | white-space: nowrap;
673 | text-align: center;
674 | }
675 |
676 | .result-actions {
677 | display: flex;
678 | gap: 5px;
679 | align-items: center;
680 | }
681 |
682 | .result-actions .btn {
683 | padding: 4px 8px;
684 | font-size: 12px;
685 | min-width: auto;
686 | }
687 |
688 | .zoom-btn {
689 | background: #17a2b8;
690 | color: white;
691 | border: none;
692 | border-radius: 4px;
693 | cursor: pointer;
694 | transition: all 0.2s ease;
695 | }
696 |
697 | .zoom-btn:hover {
698 | background: #138496;
699 | transform: scale(1.05);
700 | }
701 |
702 | .external-link {
703 | color: #6c757d;
704 | text-decoration: none;
705 | font-size: 12px;
706 | transition: color 0.2s ease;
707 | }
708 |
709 | .external-link:hover {
710 | color: #495057;
711 | }
712 |
713 | /* Section export */
714 | .export-section h3 {
715 | margin-bottom: 15px;
716 | color: #495057;
717 | }
718 |
719 | .export-section {
720 | display: flex;
721 | flex-direction: column;
722 | gap: 10px;
723 | }
724 |
725 | /* Opérateurs logiques */
726 | .logic-operator {
727 | display: inline-block;
728 | padding: 5px 10px;
729 | background: #17a2b8;
730 | color: white;
731 | border-radius: 15px;
732 | font-size: 12px;
733 | font-weight: bold;
734 | margin: 5px;
735 | }
736 |
737 | .logic-operator.and {
738 | background: #28a745;
739 | }
740 |
741 | .logic-operator.or {
742 | background: #ffc107;
743 | color: #212529;
744 | }
745 |
746 | .logic-operator.paren {
747 | background: #6f42c1;
748 | }
749 |
750 | /* Responsive */
751 | @media (max-width: 1200px) {
752 | .main-content {
753 | grid-template-columns: 1fr;
754 | }
755 | }
756 |
757 | @media (max-width: 768px) {
758 | .container {
759 | padding: 15px;
760 | }
761 |
762 | header h1 {
763 | font-size: 2rem;
764 | }
765 |
766 | .condition-fields {
767 | grid-template-columns: 1fr;
768 | }
769 |
770 | .controls {
771 | flex-direction: column;
772 | }
773 |
774 | .operators {
775 | flex-wrap: wrap;
776 | justify-content: center;
777 | }
778 |
779 | #map {
780 | height: 300px;
781 | }
782 | }
783 |
784 | @media (max-width: 480px) {
785 | header h1 {
786 | font-size: 1.5rem;
787 | }
788 |
789 | .search-panel, .results-panel {
790 | padding: 15px;
791 | }
792 |
793 | .btn {
794 | padding: 10px 15px;
795 | font-size: 13px;
796 | }
797 | }
798 |
799 | /* Animations */
800 | @keyframes fadeIn {
801 | from {
802 | opacity: 0;
803 | transform: translateY(20px);
804 | }
805 | to {
806 | opacity: 1;
807 | transform: translateY(0);
808 | }
809 | }
810 |
811 | .condition-group {
812 | animation: fadeIn 0.3s ease;
813 | }
814 |
815 | /* Scrollbar personnalisée */
816 | .query-display::-webkit-scrollbar {
817 | width: 8px;
818 | }
819 |
820 | .query-display::-webkit-scrollbar-track {
821 | background: #4a5568;
822 | border-radius: 4px;
823 | }
824 |
825 | .query-display::-webkit-scrollbar-thumb {
826 | background: #718096;
827 | border-radius: 4px;
828 | }
829 |
830 | .query-display::-webkit-scrollbar-thumb:hover {
831 | background: #a0aec0;
832 | }
833 |
834 | /* Tooltips */
835 | [title] {
836 | position: relative;
837 | }
838 |
839 | /* États de validation */
840 | .condition-fields select:invalid,
841 | .condition-fields input:invalid {
842 | border-color: #dc3545;
843 | }
844 |
845 | .condition-fields select:valid,
846 | .condition-fields input:valid {
847 | border-color: #28a745;
848 | }
849 |
850 | /* Leaflet popup customization */
851 | .leaflet-popup-content-wrapper {
852 | border-radius: 8px;
853 | box-shadow: 0 5px 15px rgba(0,0,0,0.2);
854 | }
855 |
856 | .leaflet-popup-content {
857 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
858 | line-height: 1.4;
859 | }
860 |
861 | /* Indicateur de chargement pour la carte */
862 | .map-loading {
863 | position: absolute;
864 | top: 50%;
865 | left: 50%;
866 | transform: translate(-50%, -50%);
867 | background: rgba(255,255,255,0.9);
868 | padding: 20px;
869 | border-radius: 10px;
870 | text-align: center;
871 | z-index: 1000;
872 | }
873 |
874 | /* Messages d'erreur */
875 | .error-message {
876 | padding: 15px;
877 | background: #f8d7da;
878 | border: 1px solid #f5c6cb;
879 | border-radius: 8px;
880 | color: #721c24;
881 | margin: 10px 0;
882 | }
883 |
884 | .success-message {
885 | padding: 15px;
886 | background: #d4edda;
887 | border: 1px solid #c3e6cb;
888 | border-radius: 8px;
889 | color: #155724;
890 | margin: 10px 0;
891 | }
892 |
893 | /* Amélioration de l'accessibilité */
894 | .btn:focus,
895 | select:focus,
896 | input:focus {
897 | outline: 2px solid #667eea;
898 | outline-offset: 2px;
899 | }
900 |
901 | /* Print styles */
902 | @media print {
903 | body {
904 | background: white;
905 | }
906 |
907 | .search-panel, .results-panel {
908 | box-shadow: none;
909 | border: 1px solid #ddd;
910 | }
911 |
912 | .btn {
913 | display: none;
914 | }
915 |
916 | #map {
917 | height: 300px;
918 | border: 1px solid #ddd;
919 | }
920 | }
921 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | // Configuration globale
2 | let config = {};
3 | let map = null;
4 | let complementCount = 0;
5 | let currentResults = [];
6 | let markersLayer = null;
7 | let drawingMode = false;
8 | let boundingBox = null;
9 | let bboxRectangle = null;
10 |
11 | // Initialisation de l'application
12 | document.addEventListener('DOMContentLoaded', async () => {
13 | try {
14 | // Charger la configuration
15 | await loadConfig();
16 |
17 | // Initialiser la carte
18 | initializeMap();
19 |
20 | // Initialiser les événements
21 | initializeEventListeners();
22 |
23 | // Peupler les catégories initiales
24 | populateCategories();
25 |
26 | console.log('Application OSINT initialisée avec succès');
27 | } catch (error) {
28 | console.error('Erreur lors de l\'initialisation:', error);
29 | showError('Erreur lors de l\'initialisation de l\'application');
30 | }
31 | });
32 |
33 | // Configuration intégrée pour éviter les problèmes CORS
34 | async function loadConfig() {
35 | config = {
36 | "categories": {
37 | "amenity": {
38 | "label": "Services et équipements",
39 | "types": {
40 | "restaurant": "Restaurant",
41 | "cafe": "Café",
42 | "bar": "Bar",
43 | "fast_food": "Restauration rapide",
44 | "bank": "Banque",
45 | "atm": "Distributeur automatique",
46 | "hospital": "Hôpital",
47 | "clinic": "Clinique",
48 | "pharmacy": "Pharmacie",
49 | "school": "École",
50 | "university": "Université",
51 | "library": "Bibliothèque",
52 | "police": "Police",
53 | "fire_station": "Caserne de pompiers",
54 | "post_office": "Bureau de poste",
55 | "townhall": "Mairie",
56 | "fuel": "Station-service",
57 | "parking": "Parking",
58 | "church": "Église",
59 | "mosque": "Mosquée",
60 | "cinema": "Cinéma",
61 | "theatre": "Théâtre"
62 | }
63 | },
64 | "highway": {
65 | "label": "Routes et transport",
66 | "types": {
67 | "motorway": "Autoroute",
68 | "trunk": "Route nationale",
69 | "primary": "Route principale",
70 | "secondary": "Route secondaire",
71 | "tertiary": "Route tertiaire",
72 | "residential": "Route résidentielle",
73 | "service": "Route de service",
74 | "pedestrian": "Zone piétonne",
75 | "footway": "Chemin piéton",
76 | "cycleway": "Piste cyclable",
77 | "path": "Sentier",
78 | "track": "Piste",
79 | "bus_stop": "Arrêt de bus",
80 | "traffic_signals": "Feux de circulation",
81 | "stop": "Stop",
82 | "give_way": "Cédez le passage",
83 | "speed_camera": "Radar",
84 | "toll_booth": "Péage",
85 | "bridge": "Pont",
86 | "tunnel": "Tunnel"
87 | }
88 | },
89 | "building": {
90 | "label": "Bâtiments",
91 | "types": {
92 | "house": "Maison",
93 | "apartment": "Appartement",
94 | "residential": "Résidentiel",
95 | "commercial": "Commercial",
96 | "industrial": "Industriel",
97 | "office": "Bureau",
98 | "retail": "Commerce",
99 | "warehouse": "Entrepôt",
100 | "hospital": "Hôpital",
101 | "school": "École",
102 | "university": "Université",
103 | "church": "Église",
104 | "mosque": "Mosquée",
105 | "hotel": "Hôtel",
106 | "train_station": "Gare",
107 | "public": "Public",
108 | "government": "Gouvernement",
109 | "military": "Militaire",
110 | "police": "Police",
111 | "fire_station": "Caserne de pompiers",
112 | "stadium": "Stade",
113 | "theatre": "Théâtre",
114 | "cinema": "Cinéma",
115 | "museum": "Musée",
116 | "library": "Bibliothèque"
117 | }
118 | },
119 | "natural": {
120 | "label": "Éléments naturels",
121 | "types": {
122 | "water": "Eau",
123 | "coastline": "Côte",
124 | "beach": "Plage",
125 | "cliff": "Falaise",
126 | "peak": "Sommet",
127 | "volcano": "Volcan",
128 | "cave_entrance": "Entrée de grotte",
129 | "spring": "Source",
130 | "tree": "Arbre",
131 | "wood": "Bois",
132 | "forest": "Forêt",
133 | "grassland": "Prairie",
134 | "wetland": "Zone humide",
135 | "marsh": "Marais",
136 | "rock": "Rocher",
137 | "valley": "Vallée",
138 | "bay": "Baie"
139 | }
140 | },
141 | "landuse": {
142 | "label": "Utilisation du sol",
143 | "types": {
144 | "residential": "Résidentiel",
145 | "commercial": "Commercial",
146 | "industrial": "Industriel",
147 | "retail": "Commerce de détail",
148 | "education": "Éducation",
149 | "military": "Militaire",
150 | "quarry": "Carrière",
151 | "railway": "Chemin de fer",
152 | "port": "Port",
153 | "airport": "Aéroport",
154 | "forest": "Forêt",
155 | "farmland": "Terre agricole",
156 | "cemetery": "Cimetière"
157 | }
158 | },
159 | "leisure": {
160 | "label": "Loisirs et sport",
161 | "types": {
162 | "park": "Parc",
163 | "garden": "Jardin",
164 | "playground": "Aire de jeux",
165 | "sports_centre": "Centre sportif",
166 | "stadium": "Stade",
167 | "swimming_pool": "Piscine",
168 | "swimming_pool_private": "Piscine privée",
169 | "golf_course": "Terrain de golf",
170 | "tennis": "Tennis",
171 | "basketball": "Basketball",
172 | "football": "Football",
173 | "marina": "Marina",
174 | "beach_resort": "Station balnéaire",
175 | "theme_park": "Parc d'attractions",
176 | "nature_reserve": "Réserve naturelle"
177 | }
178 | },
179 | "shop": {
180 | "label": "Commerces",
181 | "types": {
182 | "supermarket": "Supermarché",
183 | "convenience": "Épicerie",
184 | "mall": "Centre commercial",
185 | "bakery": "Boulangerie",
186 | "butcher": "Boucherie",
187 | "pharmacy": "Pharmacie",
188 | "clothes": "Vêtements",
189 | "shoes": "Chaussures",
190 | "books": "Librairie",
191 | "electronics": "Électronique",
192 | "bicycle": "Vélo",
193 | "car": "Automobile",
194 | "fuel": "Carburant",
195 | "hairdresser": "Coiffeur",
196 | "florist": "Fleuriste"
197 | }
198 | },
199 | "tourism": {
200 | "label": "Tourisme",
201 | "types": {
202 | "hotel": "Hôtel",
203 | "motel": "Motel",
204 | "guest_house": "Maison d'hôtes",
205 | "hostel": "Auberge de jeunesse",
206 | "camp_site": "Camping",
207 | "attraction": "Attraction",
208 | "museum": "Musée",
209 | "gallery": "Galerie",
210 | "zoo": "Zoo",
211 | "viewpoint": "Point de vue",
212 | "information": "Information touristique"
213 | }
214 | },
215 | "historic": {
216 | "label": "Sites historiques",
217 | "types": {
218 | "monument": "Monument",
219 | "memorial": "Mémorial",
220 | "archaeological_site": "Site archéologique",
221 | "castle": "Château",
222 | "fort": "Fort",
223 | "ruins": "Ruines",
224 | "tower": "Tour",
225 | "palace": "Palais"
226 | }
227 | },
228 | "military": {
229 | "label": "Sites militaires",
230 | "types": {
231 | "airfield": "Terrain d'aviation militaire",
232 | "bunker": "Bunker",
233 | "barracks": "Caserne",
234 | "naval_base": "Base navale",
235 | "training_area": "Zone d'entraînement",
236 | "checkpoint": "Poste de contrôle"
237 | }
238 | },
239 | "emergency": {
240 | "label": "Services d'urgence",
241 | "types": {
242 | "ambulance_station": "Station d'ambulance",
243 | "fire_hydrant": "Bouche d'incendie",
244 | "defibrillator": "Défibrillateur",
245 | "phone": "Téléphone d'urgence",
246 | "siren": "Sirène"
247 | }
248 | },
249 | "railway": {
250 | "label": "Transport ferroviaire",
251 | "types": {
252 | "station": "Gare",
253 | "halt": "Halte",
254 | "tram_stop": "Arrêt de tram",
255 | "subway_entrance": "Entrée de métro",
256 | "rail": "Rail",
257 | "subway": "Métro",
258 | "tram": "Tramway",
259 | "light_rail": "Train léger",
260 | "monorail": "Monorail",
261 | "narrow_gauge": "Voie étroite",
262 | "preserved": "Ligne préservée",
263 | "funicular": "Funiculaire",
264 | "miniature": "Train miniature",
265 | "turntable": "Plaque tournante",
266 | "roundhouse": "Rotonde",
267 | "crossing": "Passage à niveau",
268 | "level_crossing": "Passage à niveau",
269 | "signal": "Signal",
270 | "switch": "Aiguillage",
271 | "railway_crossing": "Croisement ferroviaire",
272 | "buffer_stop": "Butoir"
273 | }
274 | },
275 | "public_transport": {
276 | "label": "Transport public",
277 | "types": {
278 | "stop_position": "Position d'arrêt",
279 | "platform": "Quai",
280 | "station": "Station",
281 | "stop_area": "Zone d'arrêt"
282 | }
283 | }
284 | },
285 | "geographicZones": {
286 | "continent": {
287 | "Europe": { "query": "area[\"name:en\"=\"Europe\"];", "lat": 54.5260, "lon": 15.2551 },
288 | "Amérique du Nord": { "query": "area[\"name:en\"=\"North America\"];", "lat": 45.0, "lon": -100.0 },
289 | "Asie": { "query": "area[\"name:en\"=\"Asia\"];", "lat": 34.0479, "lon": 100.6197 },
290 | "Afrique": { "query": "area[\"name:en\"=\"Africa\"];", "lat": -8.7832, "lon": 34.5085 },
291 | "Amérique du Sud": { "query": "area[\"name:en\"=\"South America\"];", "lat": -8.7832, "lon": -55.4915 },
292 | "Océanie": { "query": "area[\"name:en\"=\"Oceania\"];", "lat": -25.2744, "lon": 133.7751 }
293 | },
294 | "country": {
295 | "France": { "query": "area[\"ISO3166-1\"=\"FR\"][admin_level=2];", "lat": 46.2276, "lon": 2.2137 },
296 | "Allemagne": { "query": "area[\"ISO3166-1\"=\"DE\"][admin_level=2];", "lat": 51.1657, "lon": 10.4515 },
297 | "Espagne": { "query": "area[\"ISO3166-1\"=\"ES\"][admin_level=2];", "lat": 40.4637, "lon": -3.7492 },
298 | "Italie": { "query": "area[\"ISO3166-1\"=\"IT\"][admin_level=2];", "lat": 41.8719, "lon": 12.5674 },
299 | "Royaume-Uni": { "query": "area[\"ISO3166-1\"=\"GB\"][admin_level=2];", "lat": 55.3781, "lon": -3.4360 },
300 | "Belgique": { "query": "area[\"ISO3166-1\"=\"BE\"][admin_level=2];", "lat": 50.5039, "lon": 4.4699 },
301 | "Suisse": { "query": "area[\"ISO3166-1\"=\"CH\"][admin_level=2];", "lat": 46.8182, "lon": 8.2275 },
302 | "États-Unis": { "query": "area[\"ISO3166-1\"=\"US\"][admin_level=2];", "lat": 39.8283, "lon": -98.5795 },
303 | "Canada": { "query": "area[\"ISO3166-1\"=\"CA\"][admin_level=2];", "lat": 56.1304, "lon": -106.3468 }
304 | },
305 | "region": {
306 | "Île-de-France": { "query": "area[\"name\"=\"Île-de-France\"][admin_level=4];", "lat": 48.8499, "lon": 2.6370 },
307 | "Provence-Alpes-Côte d'Azur": { "query": "area[\"name\"=\"Provence-Alpes-Côte d'Azur\"][admin_level=4];", "lat": 43.9352, "lon": 6.0679 },
308 | "Auvergne-Rhône-Alpes": { "query": "area[\"name\"=\"Auvergne-Rhône-Alpes\"][admin_level=4];", "lat": 45.7640, "lon": 4.8357 },
309 | "Nouvelle-Aquitaine": { "query": "area[\"name\"=\"Nouvelle-Aquitaine\"][admin_level=4];", "lat": 45.7640, "lon": 0.8357 },
310 | "Occitanie": { "query": "area[\"name\"=\"Occitanie\"][admin_level=4];", "lat": 43.6047, "lon": 1.4442 },
311 | "Hauts-de-France": { "query": "area[\"name\"=\"Hauts-de-France\"][admin_level=4];", "lat": 50.4801, "lon": 2.7931 },
312 | "Grand Est": { "query": "area[\"name\"=\"Grand Est\"][admin_level=4];", "lat": 48.7000, "lon": 6.2000 },
313 | "Pays de la Loire": { "query": "area[\"name\"=\"Pays de la Loire\"][admin_level=4];", "lat": 47.4630, "lon": -0.7516 },
314 | "Bretagne": { "query": "area[\"name\"=\"Bretagne\"][admin_level=4];", "lat": 48.2020, "lon": -2.9326 },
315 | "Normandie": { "query": "area[\"name\"=\"Normandie\"][admin_level=4];", "lat": 49.1829, "lon": 0.3707 }
316 | },
317 | "city": {
318 | "Paris": { "query": "area[\"name\"=\"Paris\"][admin_level=8];", "lat": 48.8566, "lon": 2.3522 },
319 | "Lyon": { "query": "area[\"name\"=\"Lyon\"][admin_level=8];", "lat": 45.7640, "lon": 4.8357 },
320 | "Marseille": { "query": "area[\"name\"=\"Marseille\"][admin_level=8];", "lat": 43.2965, "lon": 5.3698 },
321 | "Toulouse": { "query": "area[\"name\"=\"Toulouse\"][admin_level=8];", "lat": 43.6047, "lon": 1.4442 },
322 | "Nice": { "query": "area[\"name\"=\"Nice\"][admin_level=8];", "lat": 43.7102, "lon": 7.2620 },
323 | "Nantes": { "query": "area[\"name\"=\"Nantes\"][admin_level=8];", "lat": 47.2184, "lon": -1.5536 },
324 | "Strasbourg": { "query": "area[\"name\"=\"Strasbourg\"][admin_level=8];", "lat": 48.5734, "lon": 7.7521 },
325 | "Montpellier": { "query": "area[\"name\"=\"Montpellier\"][admin_level=8];", "lat": 43.6110, "lon": 3.8767 },
326 | "Bordeaux": { "query": "area[\"name\"=\"Bordeaux\"][admin_level=8];", "lat": 44.8378, "lon": -0.5792 },
327 | "Lille": { "query": "area[\"name\"=\"Lille\"][admin_level=8];", "lat": 50.6292, "lon": 3.0573 }
328 | }
329 | },
330 | "defaultLocation": {
331 | "lat": 48.8566,
332 | "lon": 2.3522,
333 | "name": "Paris, France"
334 | },
335 | "overpassServers": [
336 | "https://overpass-api.de/api/interpreter",
337 | "https://overpass.kumi.systems/api/interpreter",
338 | "https://overpass.openstreetmap.ru/api/interpreter"
339 | ]
340 | };
341 | }
342 |
343 | // Initialiser la carte Leaflet
344 | function initializeMap() {
345 | try {
346 | const defaultLoc = config.defaultLocation;
347 | map = L.map('map').setView([defaultLoc.lat, defaultLoc.lon], 13);
348 |
349 | // Ajouter les tuiles OpenStreetMap
350 | L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
351 | maxZoom: 19,
352 | attribution: '© OpenStreetMap contributors'
353 | }).addTo(map);
354 |
355 | // Créer un groupe de marqueurs
356 | markersLayer = L.layerGroup().addTo(map);
357 |
358 | console.log('Carte initialisée avec succès');
359 | } catch (error) {
360 | console.error('Erreur lors de l\'initialisation de la carte:', error);
361 | showError('Erreur lors de l\'initialisation de la carte');
362 | }
363 | }
364 |
365 | // Initialiser tous les événements
366 | function initializeEventListeners() {
367 | const form = document.getElementById('searchForm');
368 | const addComplementBtn = document.getElementById('addComplement');
369 | const clearAllBtn = document.getElementById('clearAll');
370 | const copyQueryBtn = document.getElementById('copyQuery');
371 |
372 | // Événements du formulaire
373 | form.addEventListener('submit', handleFormSubmit);
374 | form.addEventListener('change', handleFormChange);
375 | form.addEventListener('input', handleFormChange);
376 |
377 | // Boutons de contrôle
378 | addComplementBtn.addEventListener('click', addComplement);
379 | clearAllBtn.addEventListener('click', clearAllConditions);
380 | copyQueryBtn.addEventListener('click', copyQueryToClipboard);
381 |
382 | // Initialiser l'interface après un délai
383 | setTimeout(() => {
384 | initializeBboxControls();
385 | populateMainCategories();
386 | initializeAddressSearch();
387 | }, 100);
388 | }
389 |
390 | // Peupler les catégories dans le premier dropdown
391 | function populateCategories() {
392 | const categorySelect = document.querySelector('.category');
393 | if (!categorySelect) return;
394 |
395 | // Vider les options existantes sauf la première
396 | categorySelect.innerHTML = 'Sélectionner une catégorie ';
397 |
398 | // Ajouter les catégories depuis la configuration
399 | Object.keys(config.categories).forEach(categoryKey => {
400 | const category = config.categories[categoryKey];
401 | const option = document.createElement('option');
402 | option.value = categoryKey;
403 | option.textContent = category.label;
404 | categorySelect.appendChild(option);
405 | });
406 | }
407 |
408 | // Gérer les changements dans le formulaire
409 | function handleFormChange(event) {
410 | if (event.target.classList.contains('category')) {
411 | updateTypesContainer(event.target);
412 | } else if (event.target.classList.contains('name-mode')) {
413 | updateNameInput(event.target);
414 | }
415 | }
416 |
417 | // Mettre à jour le container des types avec cases à cocher
418 | function updateTypesContainer(categorySelect) {
419 | const typesContainer = categorySelect.closest('.condition-fields').querySelector('.types-container');
420 | if (!typesContainer) return;
421 |
422 | const selectedCategory = categorySelect.value;
423 |
424 | // Vider le container
425 | typesContainer.innerHTML = '';
426 |
427 | if (!selectedCategory || !config.categories[selectedCategory]) {
428 | typesContainer.innerHTML = 'Sélectionnez d\'abord une catégorie
';
429 | typesContainer.classList.add('empty');
430 | return;
431 | }
432 |
433 | typesContainer.classList.remove('empty');
434 | const types = config.categories[selectedCategory].types;
435 |
436 | // Générer un ID unique basé sur le contexte
437 | let uniqueId;
438 | if (categorySelect.closest('.main-search')) {
439 | uniqueId = 'main';
440 | } else if (categorySelect.closest('.complement-item')) {
441 | const complementItem = categorySelect.closest('.complement-item');
442 | uniqueId = complementItem.id.replace('complement', 'comp');
443 | } else {
444 | uniqueId = 'default';
445 | }
446 |
447 | Object.keys(types).forEach(typeKey => {
448 | const checkboxDiv = document.createElement('div');
449 | checkboxDiv.className = 'type-checkbox';
450 |
451 | const checkbox = document.createElement('input');
452 | checkbox.type = 'checkbox';
453 | checkbox.id = `type_${uniqueId}_${typeKey}`;
454 | checkbox.value = typeKey;
455 | checkbox.name = `types_${uniqueId}`;
456 |
457 | const label = document.createElement('label');
458 | label.htmlFor = checkbox.id;
459 | label.textContent = `${types[typeKey]} (${selectedCategory}=${typeKey})`;
460 |
461 | checkboxDiv.appendChild(checkbox);
462 | checkboxDiv.appendChild(label);
463 | typesContainer.appendChild(checkboxDiv);
464 | });
465 | }
466 |
467 | // Mettre à jour l'input de nom selon le mode sélectionné
468 | function updateNameInput(nameModeSelect) {
469 | const nameInput = nameModeSelect.parentElement.querySelector('.name');
470 | if (!nameInput) return;
471 |
472 | const mode = nameModeSelect.value;
473 |
474 | if (mode === '') {
475 | nameInput.disabled = true;
476 | nameInput.value = '';
477 | nameInput.placeholder = 'Texte à rechercher';
478 | } else {
479 | nameInput.disabled = false;
480 | switch (mode) {
481 | case 'exact':
482 | nameInput.placeholder = 'Nom exact à rechercher';
483 | break;
484 | case 'contains':
485 | nameInput.placeholder = 'Texte contenu dans le nom';
486 | break;
487 | case 'starts':
488 | nameInput.placeholder = 'Début du nom';
489 | break;
490 | }
491 | }
492 | }
493 |
494 | // Ajouter une nouvelle condition
495 | function addCondition() {
496 | conditionCount++;
497 |
498 | const conditionsContainer = document.getElementById('conditionsContainer');
499 | const newConditionGroup = document.createElement('div');
500 | newConditionGroup.className = 'condition-group';
501 | newConditionGroup.id = `conditionGroup${conditionCount}`;
502 |
503 | newConditionGroup.innerHTML = `
504 |
505 |
511 |
512 |
513 |
514 | Catégorie :
515 |
516 | Sélectionner une catégorie
517 |
518 |
519 |
520 |
521 |
Types (sélection multiple) :
522 |
523 |
Sélectionnez d'abord une catégorie
524 |
525 |
526 |
527 |
528 |
Recherche par nom (optionnel) :
529 |
530 |
531 | Ignorer le nom
532 | Nom exact
533 | Contient
534 | Commence par
535 |
536 |
537 |
538 |
539 |
540 |
541 |
Distance de recherche :
542 |
543 |
544 | mètres
545 |
546 |
547 |
548 |
549 | `;
550 |
551 | conditionsContainer.appendChild(newConditionGroup);
552 |
553 | // Peupler les catégories pour la nouvelle condition
554 | const newCategorySelect = newConditionGroup.querySelector('.category');
555 | populateCategorySelect(newCategorySelect);
556 |
557 | // Afficher le bouton de suppression pour toutes les conditions sauf la première
558 | updateRemoveButtons();
559 | }
560 |
561 | // Peupler un select de catégorie spécifique
562 | function populateCategorySelect(categorySelect) {
563 | categorySelect.innerHTML = 'Sélectionner une catégorie ';
564 |
565 | Object.keys(config.categories).forEach(categoryKey => {
566 | const category = config.categories[categoryKey];
567 | const option = document.createElement('option');
568 | option.value = categoryKey;
569 | option.textContent = category.label;
570 | categorySelect.appendChild(option);
571 | });
572 | }
573 |
574 | // Supprimer une condition
575 | function removeCondition(conditionNumber) {
576 | const conditionGroup = document.getElementById(`conditionGroup${conditionNumber}`);
577 | if (conditionGroup) {
578 | conditionGroup.remove();
579 | updateRemoveButtons();
580 | }
581 | }
582 |
583 | // Mettre à jour la visibilité des boutons de suppression
584 | function updateRemoveButtons() {
585 | const conditionGroups = document.querySelectorAll('.condition-group');
586 | conditionGroups.forEach((group, index) => {
587 | const removeBtn = group.querySelector('.remove-condition');
588 | if (removeBtn) {
589 | removeBtn.style.display = conditionGroups.length > 1 ? 'flex' : 'none';
590 | }
591 | });
592 | }
593 |
594 | // Effacer toutes les conditions
595 | function clearAllConditions() {
596 | const conditionsContainer = document.getElementById('conditionsContainer');
597 | conditionsContainer.innerHTML = `
598 |
599 |
600 |
606 |
607 |
608 |
609 | Catégorie :
610 |
611 | Sélectionner une catégorie
612 |
613 |
614 |
615 |
616 |
Types (sélection multiple) :
617 |
618 |
Sélectionnez d'abord une catégorie
619 |
620 |
621 |
622 |
623 |
Recherche par nom (optionnel) :
624 |
625 |
626 | Ignorer le nom
627 | Nom exact
628 | Contient
629 | Commence par
630 |
631 |
632 |
633 |
634 |
635 |
636 |
Distance de recherche :
637 |
638 |
639 | mètres
640 |
641 |
642 |
643 |
644 |
645 | `;
646 |
647 | conditionCount = 1;
648 | populateCategories();
649 | clearResults();
650 | document.getElementById('queryDisplay').textContent = '';
651 | }
652 |
653 | // Ajouter un opérateur logique
654 | function addOperator(operator) {
655 | const queryDisplay = document.getElementById('queryDisplay');
656 | const currentQuery = queryDisplay.textContent;
657 |
658 | if (currentQuery.trim()) {
659 | queryDisplay.textContent = currentQuery + ' ' + operator + ' ';
660 | } else {
661 | queryDisplay.textContent = operator + ' ';
662 | }
663 | }
664 |
665 | // Gérer la soumission du formulaire
666 | async function handleFormSubmit(event) {
667 | event.preventDefault();
668 |
669 | try {
670 | showLoading(true);
671 |
672 | // Construire la requête selon le nouveau style
673 | const query = buildNewStyleQuery();
674 |
675 | // Afficher la requête
676 | document.getElementById('queryDisplay').textContent = query;
677 |
678 | // Exécuter la recherche
679 | const results = await executeOverpassQuery(query);
680 |
681 | // Afficher les résultats
682 | displayResults(results);
683 |
684 | // Préparer les exports
685 | prepareExports(results);
686 |
687 | } catch (error) {
688 | console.error('Erreur lors de la recherche:', error);
689 | showError('Erreur lors de la recherche: ' + error.message);
690 | } finally {
691 | showLoading(false);
692 | }
693 | }
694 |
695 | // Collecter toutes les conditions du formulaire (mode standard)
696 | function collectConditions() {
697 | const conditions = [];
698 | const conditionGroups = document.querySelectorAll('.condition-group');
699 |
700 | conditionGroups.forEach((group, index) => {
701 | const condition = group.querySelector('.condition');
702 | const conditionId = condition.id.replace('condition', '');
703 |
704 | const category = condition.querySelector('.category').value;
705 | const distance = condition.querySelector('.distance').value;
706 |
707 | // Collecter les types sélectionnés (cases à cocher)
708 | const selectedTypes = [];
709 | const typeCheckboxes = condition.querySelectorAll('.types-container input[type="checkbox"]:checked');
710 | typeCheckboxes.forEach(checkbox => {
711 | selectedTypes.push(checkbox.value);
712 | });
713 |
714 | // Collecter les informations de recherche par nom
715 | const nameMode = condition.querySelector('.name-mode').value;
716 | const nameValue = condition.querySelector('.name').value;
717 |
718 | if (category) {
719 | conditions.push({
720 | id: conditionId,
721 | category,
722 | types: selectedTypes,
723 | nameMode,
724 | nameValue,
725 | distance: distance || 1000
726 | });
727 | }
728 | });
729 |
730 | return conditions;
731 | }
732 |
733 | // Construire la requête Overpass (mode standard)
734 | function buildOverpassQuery(conditions) {
735 | let query = '[out:json][timeout:25];\n(\n';
736 |
737 | conditions.forEach((condition, index) => {
738 | const { category, types, nameMode, nameValue, distance } = condition;
739 |
740 | // Déterminer la zone de recherche
741 | const defaultLoc = config.defaultLocation;
742 | const area = `(around:${distance},${defaultLoc.lat},${defaultLoc.lon})`;
743 |
744 | // Construire la requête pour cette condition
745 | let conditionQuery = '';
746 |
747 | if (types && types.length > 0) {
748 | // Recherche avec types spécifiques sélectionnés (OU entre les types)
749 | types.forEach(type => {
750 | conditionQuery += ` node["${category}"="${type}"]${area};\n`;
751 | conditionQuery += ` way["${category}"="${type}"]${area};\n`;
752 | conditionQuery += ` relation["${category}"="${type}"]${area};\n`;
753 | });
754 | } else {
755 | // Recherche par catégorie seulement (tous les types)
756 | conditionQuery = ` node["${category}"]${area};\n`;
757 | conditionQuery += ` way["${category}"]${area};\n`;
758 | conditionQuery += ` relation["${category}"]${area};\n`;
759 | }
760 |
761 | // Ajouter le filtre par nom si spécifié
762 | if (nameMode && nameValue && nameValue.trim()) {
763 | let nameFilter = '';
764 | switch (nameMode) {
765 | case 'exact':
766 | nameFilter = `["name"="${nameValue}"]`;
767 | break;
768 | case 'contains':
769 | nameFilter = `["name"~"${nameValue}",i]`;
770 | break;
771 | case 'starts':
772 | nameFilter = `["name"~"^${nameValue}",i]`;
773 | break;
774 | }
775 |
776 | if (nameFilter) {
777 | conditionQuery = conditionQuery.replace(/\](\(around:[^)]+\));/g, `]${nameFilter}$1;`);
778 | }
779 | }
780 |
781 | query += conditionQuery;
782 | });
783 |
784 | query += ');\nout geom;';
785 |
786 | return query;
787 | }
788 |
789 | // Exécuter la requête Overpass
790 | async function executeOverpassQuery(query) {
791 | const servers = config.overpassServers || ['https://overpass-api.de/api/interpreter'];
792 |
793 | for (let serverUrl of servers) {
794 | try {
795 | console.log(`Tentative de requête sur ${serverUrl}`);
796 |
797 | const response = await fetch(serverUrl, {
798 | method: 'POST',
799 | headers: {
800 | 'Content-Type': 'application/x-www-form-urlencoded',
801 | },
802 | body: `data=${encodeURIComponent(query)}`
803 | });
804 |
805 | if (!response.ok) {
806 | throw new Error(`Erreur HTTP: ${response.status}`);
807 | }
808 |
809 | const data = await response.json();
810 |
811 | if (data.elements) {
812 | console.log(`Requête réussie sur ${serverUrl}, ${data.elements.length} éléments trouvés`);
813 | return data;
814 | } else {
815 | throw new Error('Réponse invalide du serveur');
816 | }
817 |
818 | } catch (error) {
819 | console.warn(`Échec sur ${serverUrl}:`, error);
820 | if (serverUrl === servers[servers.length - 1]) {
821 | throw new Error('Tous les serveurs Overpass sont indisponibles');
822 | }
823 | }
824 | }
825 | }
826 |
827 | // Afficher les résultats sur la carte
828 | function displayResults(data) {
829 | // Effacer les marqueurs existants
830 | markersLayer.clearLayers();
831 |
832 | const elements = data.elements || [];
833 | currentResults = elements;
834 |
835 | // Obtenir les données de recherche pour identifier les types d'éléments
836 | const searchData = collectMainSearchAndComplements();
837 |
838 | // Créer les marqueurs et compter les éléments valides
839 | const bounds = [];
840 | let validElementsCount = 0;
841 | const validElements = [];
842 |
843 | // Couleurs pour différencier les types d'éléments
844 | const markerColors = {
845 | main: 'blue', // Éléments principaux en bleu
846 | complement1: 'green', // Premier complément en vert
847 | complement2: 'orange', // Deuxième complément en orange
848 | complement3: 'red', // Troisième complément en rouge
849 | complement4: 'violet', // Quatrième complément en violet
850 | complement5: 'yellow' // Cinquième complément en jaune
851 | };
852 |
853 | elements.forEach((element, index) => {
854 | let lat, lon;
855 |
856 | // Déterminer les coordonnées selon le type d'élément
857 | if (element.type === 'node') {
858 | lat = element.lat;
859 | lon = element.lon;
860 | } else if (element.type === 'way' && element.geometry) {
861 | // Prendre le centre du way
862 | const coords = element.geometry;
863 | lat = coords.reduce((sum, coord) => sum + coord.lat, 0) / coords.length;
864 | lon = coords.reduce((sum, coord) => sum + coord.lon, 0) / coords.length;
865 | } else if (element.type === 'relation' && element.members) {
866 | // Ignorer les relations pour l'instant
867 | return;
868 | } else {
869 | return;
870 | }
871 |
872 | if (lat && lon) {
873 | validElementsCount++;
874 | bounds.push([lat, lon]);
875 |
876 | // Ajouter les coordonnées à l'élément pour la liste
877 | element.calculatedLat = lat;
878 | element.calculatedLon = lon;
879 | element.resultIndex = validElementsCount - 1;
880 |
881 | // Déterminer le type d'élément (principal ou complément)
882 | const elementType = determineElementType(element, searchData);
883 | element.elementType = elementType;
884 |
885 | validElements.push(element);
886 |
887 | // Choisir la couleur du marqueur selon le type
888 | const markerColor = markerColors[elementType] || 'blue';
889 |
890 | // Créer le marqueur avec la couleur appropriée
891 | const marker = L.marker([lat, lon], {
892 | icon: L.icon({
893 | iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-${markerColor}.png`,
894 | shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
895 | iconSize: [25, 41],
896 | iconAnchor: [12, 41],
897 | popupAnchor: [1, -34],
898 | shadowSize: [41, 41]
899 | })
900 | });
901 |
902 | // Créer le popup
903 | const tags = element.tags || {};
904 | const name = tags.name || 'Sans nom';
905 | const category = Object.keys(tags).find(key => config.categories[key]) || 'Inconnu';
906 | const type = tags[category] || 'Non spécifié';
907 |
908 | // Ajouter l'information du type d'élément dans le popup
909 | const elementTypeLabel = getElementTypeLabel(elementType);
910 |
911 | const popupContent = `
912 |
913 |
${name}
914 |
Type: ${type}
915 |
Catégorie: ${category}
916 |
Rôle: ${elementTypeLabel}
917 |
Coordonnées: ${lat.toFixed(6)}, ${lon.toFixed(6)}
918 |
926 |
927 | `;
928 |
929 | marker.bindPopup(popupContent);
930 |
931 | // Stocker la référence de l'élément dans le marqueur
932 | marker.elementData = element;
933 |
934 | // Ajouter le clic droit pour les liens
935 | marker.on('contextmenu', () => {
936 | window.open(`https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&zoom=18`, '_blank');
937 | });
938 |
939 | markersLayer.addLayer(marker);
940 | }
941 | });
942 |
943 | // Afficher le nombre de résultats valides avec répartition par type
944 | const resultsCount = document.getElementById('resultsCount');
945 | const typeBreakdown = getResultsBreakdown(validElements);
946 | resultsCount.innerHTML = `${validElementsCount} résultat(s) trouvé(s)${typeBreakdown} `;
947 | resultsCount.style.display = 'block';
948 |
949 | if (validElementsCount === 0) {
950 | showError('Aucun résultat trouvé pour cette recherche');
951 | hideResultsList();
952 | return;
953 | }
954 |
955 | // Ajuster la vue de la carte pour inclure tous les marqueurs
956 | if (bounds.length > 0) {
957 | map.fitBounds(bounds, { padding: [20, 20] });
958 | }
959 |
960 | // Afficher la liste des résultats
961 | populateResultsList(validElements);
962 | }
963 |
964 | // Déterminer le type d'élément (principal ou complément)
965 | function determineElementType(element, searchData) {
966 | const tags = element.tags || {};
967 |
968 | // Vérifier si c'est un élément principal
969 | if (searchData.mainSearch) {
970 | const mainCategory = searchData.mainSearch.category;
971 | const mainTypes = searchData.mainSearch.types || [];
972 |
973 | if (tags[mainCategory]) {
974 | if (mainTypes.length === 0 || mainTypes.includes(tags[mainCategory])) {
975 | // Vérifier aussi le nom si spécifié
976 | if (searchData.mainSearch.nameMode && searchData.mainSearch.nameValue) {
977 | const nameValue = searchData.mainSearch.nameValue.toLowerCase();
978 | const elementName = (tags.name || '').toLowerCase();
979 |
980 | switch (searchData.mainSearch.nameMode) {
981 | case 'exact':
982 | if (elementName === nameValue) return 'main';
983 | break;
984 | case 'contains':
985 | if (elementName.includes(nameValue)) return 'main';
986 | break;
987 | case 'starts':
988 | if (elementName.startsWith(nameValue)) return 'main';
989 | break;
990 | }
991 | } else {
992 | return 'main';
993 | }
994 | }
995 | }
996 | }
997 |
998 | // Vérifier si c'est un élément de complément
999 | for (let i = 0; i < searchData.complements.length; i++) {
1000 | const complement = searchData.complements[i];
1001 | const complementCategory = complement.category;
1002 | const complementTypes = complement.types || [];
1003 |
1004 | if (tags[complementCategory]) {
1005 | if (complementTypes.length === 0 || complementTypes.includes(tags[complementCategory])) {
1006 | // Vérifier aussi le nom si spécifié
1007 | if (complement.nameMode && complement.nameValue) {
1008 | const nameValue = complement.nameValue.toLowerCase();
1009 | const elementName = (tags.name || '').toLowerCase();
1010 |
1011 | switch (complement.nameMode) {
1012 | case 'exact':
1013 | if (elementName === nameValue) return `complement${i + 1}`;
1014 | break;
1015 | case 'contains':
1016 | if (elementName.includes(nameValue)) return `complement${i + 1}`;
1017 | break;
1018 | case 'starts':
1019 | if (elementName.startsWith(nameValue)) return `complement${i + 1}`;
1020 | break;
1021 | }
1022 | } else {
1023 | return `complement${i + 1}`;
1024 | }
1025 | }
1026 | }
1027 | }
1028 |
1029 | // Par défaut, considérer comme élément principal
1030 | return 'main';
1031 | }
1032 |
1033 | // Obtenir le libellé du type d'élément
1034 | function getElementTypeLabel(elementType) {
1035 | switch (elementType) {
1036 | case 'main':
1037 | return '🎯 Résultat principal';
1038 | case 'complement1':
1039 | return '🟢 Complément 1';
1040 | case 'complement2':
1041 | return '🟠 Complément 2';
1042 | case 'complement3':
1043 | return '🔴 Complément 3';
1044 | case 'complement4':
1045 | return '🟣 Complément 4';
1046 | case 'complement5':
1047 | return '🟡 Complément 5';
1048 | default:
1049 | return '📍 Élément';
1050 | }
1051 | }
1052 |
1053 | // Obtenir la répartition des résultats par type
1054 | function getResultsBreakdown(elements) {
1055 | const breakdown = {};
1056 |
1057 | elements.forEach(element => {
1058 | const type = element.elementType || 'main';
1059 | breakdown[type] = (breakdown[type] || 0) + 1;
1060 | });
1061 |
1062 | const parts = [];
1063 | if (breakdown.main) {
1064 | parts.push(`🎯 ${breakdown.main} principal(aux)`);
1065 | }
1066 |
1067 | for (let i = 1; i <= 5; i++) {
1068 | const key = `complement${i}`;
1069 | if (breakdown[key]) {
1070 | const colors = ['🟢', '🟠', '🔴', '🟣', '🟡'];
1071 | parts.push(`${colors[i-1]} ${breakdown[key]} complément ${i}`);
1072 | }
1073 | }
1074 |
1075 | return parts.join(' • ');
1076 | }
1077 |
1078 | // Peupler la liste des résultats avec séparation
1079 | function populateResultsList(elements) {
1080 | const resultsListContainer = document.getElementById('resultsListContainer');
1081 | const mainResultsTableBody = document.getElementById('mainResultsTableBody');
1082 | const complementResultsTableBody = document.getElementById('complementResultsTableBody');
1083 | const complementResultsSection = document.getElementById('complementResultsSection');
1084 |
1085 | if (!resultsListContainer || !mainResultsTableBody || !complementResultsTableBody) return;
1086 |
1087 | // Vider les tables existantes
1088 | mainResultsTableBody.innerHTML = '';
1089 | complementResultsTableBody.innerHTML = '';
1090 |
1091 | // Séparer les éléments principaux des compléments
1092 | const mainElements = elements.filter(element => element.elementType === 'main');
1093 | const complementElements = elements.filter(element => element.elementType !== 'main');
1094 |
1095 | // Peupler la table des résultats principaux
1096 | mainElements.forEach((element, index) => {
1097 | const row = createResultRow(element, index, false);
1098 | mainResultsTableBody.appendChild(row);
1099 | });
1100 |
1101 | // Peupler la table des compléments si il y en a
1102 | if (complementElements.length > 0) {
1103 | complementElements.forEach((element, index) => {
1104 | const globalIndex = mainElements.length + index; // Index global pour le zoom
1105 | const row = createResultRow(element, globalIndex, true);
1106 | complementResultsTableBody.appendChild(row);
1107 | });
1108 |
1109 | // Afficher la section des compléments
1110 | complementResultsSection.style.display = 'block';
1111 | } else {
1112 | // Masquer la section des compléments
1113 | complementResultsSection.style.display = 'none';
1114 | }
1115 |
1116 | // Afficher la liste
1117 | resultsListContainer.style.display = 'block';
1118 |
1119 | // Stocker les éléments pour les fonctions de zoom
1120 | window.currentValidElements = elements;
1121 | }
1122 |
1123 | // Créer une ligne de résultat
1124 | function createResultRow(element, index, isComplement) {
1125 | const tags = element.tags || {};
1126 | const name = tags.name || 'Sans nom';
1127 | const category = Object.keys(tags).find(key => config.categories[key]) || 'Inconnu';
1128 | const type = tags[category] || 'Non spécifié';
1129 | const categoryLabel = config.categories[category]?.label || category;
1130 | const typeLabel = config.categories[category]?.types[type] || type;
1131 |
1132 | const lat = element.calculatedLat;
1133 | const lon = element.calculatedLon;
1134 |
1135 | const row = document.createElement('tr');
1136 | row.dataset.elementId = element.id;
1137 | row.dataset.resultIndex = index;
1138 |
1139 | // Contenu de base pour toutes les lignes
1140 | let rowContent = '';
1141 |
1142 | // Ajouter la colonne "Rôle" pour les compléments
1143 | if (isComplement) {
1144 | const elementTypeLabel = getElementTypeLabel(element.elementType);
1145 | rowContent += `
1146 |
1147 | ${elementTypeLabel}
1148 |
1149 | `;
1150 | }
1151 |
1152 | // Colonnes communes
1153 | rowContent += `
1154 |
1155 | ${name}
1156 |
1157 |
1158 | ${typeLabel}
1159 |
1160 |
1161 | ${category}=${type}
1162 |
1163 |
1164 | ${lat.toFixed(6)}, ${lon.toFixed(6)}
1165 |
1166 |
1167 |
1175 |
1176 | `;
1177 |
1178 | row.innerHTML = rowContent;
1179 |
1180 | // Ajouter l'événement de clic sur la ligne
1181 | row.addEventListener('click', function() {
1182 | selectResultRow(this);
1183 | zoomToResult(index);
1184 | });
1185 |
1186 | return row;
1187 | }
1188 |
1189 | // Zoomer sur un résultat spécifique
1190 | function zoomToResult(resultIndex) {
1191 | if (!window.currentValidElements || !window.currentValidElements[resultIndex]) return;
1192 |
1193 | const element = window.currentValidElements[resultIndex];
1194 | const lat = element.calculatedLat;
1195 | const lon = element.calculatedLon;
1196 |
1197 | if (!lat || !lon) return;
1198 |
1199 | // Zoomer sur l'élément
1200 | map.setView([lat, lon], 18);
1201 |
1202 | // Trouver et ouvrir le popup du marqueur correspondant
1203 | markersLayer.eachLayer(marker => {
1204 | if (marker.elementData && marker.elementData.id === element.id) {
1205 | marker.openPopup();
1206 |
1207 | // Effet visuel temporaire
1208 | const originalIcon = marker.getIcon();
1209 | const highlightIcon = L.icon({
1210 | iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
1211 | shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
1212 | iconSize: [25, 41],
1213 | iconAnchor: [12, 41],
1214 | popupAnchor: [1, -34],
1215 | shadowSize: [41, 41]
1216 | });
1217 |
1218 | marker.setIcon(highlightIcon);
1219 |
1220 | // Restaurer l'icône originale après 2 secondes
1221 | setTimeout(() => {
1222 | marker.setIcon(originalIcon);
1223 | }, 2000);
1224 | }
1225 | });
1226 |
1227 | // Sélectionner la ligne correspondante dans la table
1228 | const rows = document.querySelectorAll('#resultsTable tbody tr');
1229 | rows.forEach(row => {
1230 | if (parseInt(row.dataset.resultIndex) === resultIndex) {
1231 | selectResultRow(row);
1232 | }
1233 | });
1234 |
1235 | showSuccess(`Zoom sur : ${element.tags?.name || 'Élément sélectionné'}`);
1236 | }
1237 |
1238 | // Sélectionner une ligne de résultat
1239 | function selectResultRow(row) {
1240 | // Désélectionner toutes les autres lignes dans toutes les tables
1241 | const allRows = document.querySelectorAll('#mainResultsTable tbody tr, #complementResultsTable tbody tr');
1242 | allRows.forEach(r => r.classList.remove('selected'));
1243 |
1244 | // Sélectionner la ligne cliquée
1245 | row.classList.add('selected');
1246 |
1247 | // Faire défiler pour s'assurer que la ligne est visible
1248 | row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1249 | }
1250 |
1251 | // Masquer la liste des résultats
1252 | function hideResultsList() {
1253 | const resultsListContainer = document.getElementById('resultsListContainer');
1254 | if (resultsListContainer) {
1255 | resultsListContainer.style.display = 'none';
1256 | }
1257 |
1258 | // Nettoyer les données
1259 | window.currentValidElements = null;
1260 | }
1261 |
1262 | // Préparer les exports
1263 | function prepareExports(data) {
1264 | const downloadReportBtn = document.getElementById('downloadReport');
1265 | const downloadCSVBtn = document.getElementById('downloadCSV');
1266 |
1267 | // Supprimer les anciens événements
1268 | downloadReportBtn.replaceWith(downloadReportBtn.cloneNode(true));
1269 | downloadCSVBtn.replaceWith(downloadCSVBtn.cloneNode(true));
1270 |
1271 | // Récupérer les nouveaux éléments
1272 | const newDownloadReportBtn = document.getElementById('downloadReport');
1273 | const newDownloadCSVBtn = document.getElementById('downloadCSV');
1274 |
1275 | // Ajouter les événements de clic
1276 | newDownloadReportBtn.addEventListener('click', () => exportJSON(data));
1277 | newDownloadCSVBtn.addEventListener('click', () => exportCSV(data));
1278 |
1279 | // Afficher les boutons
1280 | newDownloadReportBtn.style.display = 'inline-flex';
1281 | newDownloadCSVBtn.style.display = 'inline-flex';
1282 | }
1283 |
1284 | // Exporter en JSON
1285 | function exportJSON(data) {
1286 | const jsonReport = {
1287 | timestamp: new Date().toISOString(),
1288 | query: document.getElementById('queryDisplay').textContent,
1289 | resultsCount: data.elements.length,
1290 | boundingBox: boundingBox,
1291 | elements: data.elements
1292 | };
1293 |
1294 | const jsonContent = JSON.stringify(jsonReport, null, 2);
1295 | const blob = new Blob([jsonContent], { type: 'application/json' });
1296 | const url = URL.createObjectURL(blob);
1297 |
1298 | const link = document.createElement('a');
1299 | link.href = url;
1300 | link.download = `osint-report-${new Date().toISOString().split('T')[0]}.json`;
1301 | document.body.appendChild(link);
1302 | link.click();
1303 | document.body.removeChild(link);
1304 | URL.revokeObjectURL(url);
1305 |
1306 | showSuccess('Rapport JSON téléchargé avec succès');
1307 | }
1308 |
1309 | // Exporter en CSV
1310 | function exportCSV(data) {
1311 | const csvContent = generateCSV(data.elements);
1312 | const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
1313 | const url = URL.createObjectURL(blob);
1314 |
1315 | const link = document.createElement('a');
1316 | link.href = url;
1317 | link.download = `osint-results-${new Date().toISOString().split('T')[0]}.csv`;
1318 | document.body.appendChild(link);
1319 | link.click();
1320 | document.body.removeChild(link);
1321 | URL.revokeObjectURL(url);
1322 |
1323 | showSuccess('Fichier CSV téléchargé avec succès');
1324 | }
1325 |
1326 | // Générer le contenu CSV
1327 | function generateCSV(elements) {
1328 | const headers = ['ID', 'Type', 'Nom', 'Catégorie', 'Sous-type', 'Latitude', 'Longitude', 'Tags'];
1329 | let csv = headers.join(',') + '\n';
1330 |
1331 | elements.forEach(element => {
1332 | const tags = element.tags || {};
1333 | const name = (tags.name || '').replace(/"/g, '""');
1334 | const category = Object.keys(tags).find(key => config.categories[key]) || '';
1335 | const subtype = tags[category] || '';
1336 |
1337 | let lat = '', lon = '';
1338 | if (element.type === 'node') {
1339 | lat = element.lat;
1340 | lon = element.lon;
1341 | } else if (element.type === 'way' && element.geometry) {
1342 | const coords = element.geometry;
1343 | lat = coords.reduce((sum, coord) => sum + coord.lat, 0) / coords.length;
1344 | lon = coords.reduce((sum, coord) => sum + coord.lon, 0) / coords.length;
1345 | }
1346 |
1347 | const tagsStr = JSON.stringify(tags).replace(/"/g, '""');
1348 |
1349 | const row = [
1350 | element.id,
1351 | element.type,
1352 | `"${name}"`,
1353 | category,
1354 | subtype,
1355 | lat,
1356 | lon,
1357 | `"${tagsStr}"`
1358 | ];
1359 |
1360 | csv += row.join(',') + '\n';
1361 | });
1362 |
1363 | return csv;
1364 | }
1365 |
1366 | // Copier la requête dans le presse-papiers
1367 | async function copyQueryToClipboard() {
1368 | const query = document.getElementById('queryDisplay').textContent;
1369 |
1370 | if (!query.trim()) {
1371 | showError('Aucune requête à copier');
1372 | return;
1373 | }
1374 |
1375 | try {
1376 | await navigator.clipboard.writeText(query);
1377 | showSuccess('Requête copiée dans le presse-papiers');
1378 | } catch (error) {
1379 | console.error('Erreur lors de la copie:', error);
1380 | showError('Impossible de copier la requête');
1381 | }
1382 | }
1383 |
1384 | // Effacer les résultats
1385 | function clearResults() {
1386 | if (markersLayer) {
1387 | markersLayer.clearLayers();
1388 | }
1389 |
1390 | document.getElementById('resultsCount').style.display = 'none';
1391 | document.getElementById('downloadReport').style.display = 'none';
1392 | document.getElementById('downloadCSV').style.display = 'none';
1393 |
1394 | currentResults = [];
1395 | }
1396 |
1397 | // Afficher/masquer l'indicateur de chargement
1398 | function showLoading(show) {
1399 | const loadingIndicator = document.getElementById('loadingIndicator');
1400 | loadingIndicator.style.display = show ? 'block' : 'none';
1401 | }
1402 |
1403 | // Afficher un message d'erreur
1404 | function showError(message) {
1405 | const existingError = document.querySelector('.error-message');
1406 | if (existingError) {
1407 | existingError.remove();
1408 | }
1409 |
1410 | const errorDiv = document.createElement('div');
1411 | errorDiv.className = 'error-message';
1412 | errorDiv.innerHTML = ` ${message}`;
1413 |
1414 | const container = document.querySelector('.search-panel');
1415 | container.insertBefore(errorDiv, container.firstChild);
1416 |
1417 | setTimeout(() => {
1418 | errorDiv.remove();
1419 | }, 5000);
1420 | }
1421 |
1422 | // Afficher un message de succès
1423 | function showSuccess(message) {
1424 | const existingSuccess = document.querySelector('.success-message');
1425 | if (existingSuccess) {
1426 | existingSuccess.remove();
1427 | }
1428 |
1429 | const successDiv = document.createElement('div');
1430 | successDiv.className = 'success-message';
1431 | successDiv.innerHTML = ` ${message}`;
1432 |
1433 | const container = document.querySelector('.search-panel');
1434 | container.insertBefore(successDiv, container.firstChild);
1435 |
1436 | setTimeout(() => {
1437 | successDiv.remove();
1438 | }, 3000);
1439 | }
1440 |
1441 | // Gérer le changement de mode de recherche
1442 | function handleSearchModeChange(event) {
1443 | const searchMode = event.target.value;
1444 | const conditionsContainer = document.getElementById('conditionsContainer');
1445 | const operatorsDiv = document.querySelector('.operators');
1446 |
1447 | if (searchMode === 'proximity') {
1448 | // Mode proximité croisée : adapter l'interface
1449 | updateInterfaceForProximityMode();
1450 | operatorsDiv.style.display = 'none'; // Masquer les opérateurs logiques
1451 | } else {
1452 | // Mode standard : interface normale
1453 | updateInterfaceForStandardMode();
1454 | operatorsDiv.style.display = 'flex'; // Afficher les opérateurs logiques
1455 | }
1456 |
1457 | // Effacer les conditions existantes et recommencer
1458 | clearAllConditions();
1459 | }
1460 |
1461 | // Adapter l'interface pour le mode proximité croisée
1462 | function updateInterfaceForProximityMode() {
1463 | const conditionGroups = document.querySelectorAll('.condition-group');
1464 | conditionGroups.forEach((group, index) => {
1465 | const conditionHeader = group.querySelector('.condition-number');
1466 | if (index === 0) {
1467 | conditionHeader.textContent = 'Éléments à rechercher';
1468 | } else {
1469 | conditionHeader.textContent = `Critère de proximité ${index}`;
1470 | }
1471 |
1472 | // Ajouter des champs spécifiques au mode proximité
1473 | addProximityFields(group, index);
1474 | });
1475 |
1476 | // Mettre à jour le texte du bouton d'ajout
1477 | const addConditionBtn = document.getElementById('addCondition');
1478 | if (addConditionBtn) {
1479 | addConditionBtn.innerHTML = ' Ajouter un critère de proximité';
1480 | }
1481 | }
1482 |
1483 | // Adapter l'interface pour le mode standard
1484 | function updateInterfaceForStandardMode() {
1485 | const conditionGroups = document.querySelectorAll('.condition-group');
1486 | conditionGroups.forEach((group, index) => {
1487 | const conditionHeader = group.querySelector('.condition-number');
1488 | conditionHeader.textContent = `Condition ${index + 1}`;
1489 |
1490 | // Supprimer les champs spécifiques au mode proximité
1491 | removeProximityFields(group);
1492 | });
1493 | }
1494 |
1495 | // Ajouter des champs spécifiques au mode proximité
1496 | function addProximityFields(conditionGroup, index) {
1497 | const conditionFields = conditionGroup.querySelector('.condition-fields');
1498 | const distanceGroup = conditionFields.querySelector('.field-group:last-child');
1499 |
1500 | if (index > 0) {
1501 | // Pour les critères de proximité, ajouter distance min/max
1502 | const existingProximityFields = conditionFields.querySelector('.proximity-fields');
1503 | if (!existingProximityFields) {
1504 | const proximityFieldsDiv = document.createElement('div');
1505 | proximityFieldsDiv.className = 'proximity-fields';
1506 | proximityFieldsDiv.innerHTML = `
1507 |
1508 |
Distance minimale :
1509 |
1510 |
1511 | mètres
1512 |
1513 |
1514 |
1515 |
Distance maximale :
1516 |
1517 |
1518 | mètres
1519 |
1520 |
1521 | `;
1522 |
1523 | // Insérer avant le champ distance existant
1524 | conditionFields.insertBefore(proximityFieldsDiv, distanceGroup);
1525 |
1526 | // Masquer le champ distance standard
1527 | distanceGroup.style.display = 'none';
1528 | }
1529 | }
1530 | }
1531 |
1532 | // Supprimer les champs spécifiques au mode proximité
1533 | function removeProximityFields(conditionGroup) {
1534 | const proximityFields = conditionGroup.querySelector('.proximity-fields');
1535 | if (proximityFields) {
1536 | proximityFields.remove();
1537 | }
1538 |
1539 | // Réafficher le champ distance standard
1540 | const distanceGroup = conditionGroup.querySelector('.field-group:last-child');
1541 | if (distanceGroup) {
1542 | distanceGroup.style.display = 'block';
1543 | }
1544 | }
1545 |
1546 | // Construire la requête Overpass pour le mode proximité croisée
1547 | function buildProximityQuery(conditions) {
1548 | if (conditions.length === 0) return '';
1549 |
1550 | // Obtenir la zone géographique sélectionnée
1551 | const geoArea = getSelectedGeographicArea();
1552 |
1553 | // Définir la zone de recherche
1554 | let query = '[out:json][timeout:25];\n';
1555 | query += `// Définir la zone de recherche : ${geoArea.name}\n`;
1556 |
1557 | // Si c'est une bounding box, pas besoin de définir une zone nommée
1558 | if (boundingBox) {
1559 | query += '\n';
1560 | } else {
1561 | query += '(\n';
1562 | query += ` ${geoArea.query}\n`;
1563 | query += ')->.searchArea;\n\n';
1564 | }
1565 |
1566 | // Première condition = éléments à rechercher
1567 | const targetCondition = conditions[0];
1568 | const proximityConditions = conditions.slice(1);
1569 |
1570 | if (proximityConditions.length === 0) {
1571 | // Si pas de critères de proximité, recherche standard dans la zone
1572 | return buildStandardQueryInArea(targetCondition);
1573 | }
1574 |
1575 | // Construire les ensembles de référence pour chaque critère de proximité
1576 | proximityConditions.forEach((condition, index) => {
1577 | query += `// Critère de proximité ${index + 1}: ${getConditionDescription(condition)}\n`;
1578 | query += '(\n';
1579 | query += buildConditionQuery(condition, 'searchArea');
1580 | query += `)->.proximity${index + 1};\n\n`;
1581 | });
1582 |
1583 | // Construire la requête principale avec intersections de proximité
1584 | query += '// Recherche des éléments cibles avec critères de proximité\n';
1585 | query += '(\n';
1586 |
1587 | // Construire la requête pour les éléments cibles
1588 | const targetQuery = buildConditionQuery(targetCondition, 'searchArea');
1589 |
1590 | // Appliquer les filtres de proximité
1591 | let proximityFilters = '';
1592 | proximityConditions.forEach((condition, index) => {
1593 | const minDist = condition.minDistance || 0;
1594 | const maxDist = condition.maxDistance || condition.distance || 1000;
1595 |
1596 | if (minDist > 0) {
1597 | // Approche avec distance min/max (plus complexe)
1598 | proximityFilters += `(around.proximity${index + 1}:${maxDist})`;
1599 | } else {
1600 | // Distance maximale seulement
1601 | proximityFilters += `(around.proximity${index + 1}:${maxDist})`;
1602 | }
1603 | });
1604 |
1605 | // Appliquer les filtres à chaque type d'élément
1606 | const lines = targetQuery.split('\n').filter(line => line.trim());
1607 | lines.forEach(line => {
1608 | if (line.includes('node[') || line.includes('way[') || line.includes('relation[')) {
1609 | const modifiedLine = line.replace('(area.searchArea);', `${proximityFilters}(area.searchArea);`);
1610 | query += ` ${modifiedLine}\n`;
1611 | }
1612 | });
1613 |
1614 | // Gestion des distances minimales si spécifiées
1615 | const hasMinDistances = proximityConditions.some(c => c.minDistance > 0);
1616 | if (hasMinDistances) {
1617 | query += ');\n\n';
1618 | query += '// Exclure les éléments trop proches (distances minimales)\n';
1619 | query += '(\n';
1620 | query += ' ._;\n';
1621 |
1622 | proximityConditions.forEach((condition, index) => {
1623 | const minDist = condition.minDistance;
1624 | if (minDist > 0) {
1625 | query += ` - node(around.proximity${index + 1}:${minDist});\n`;
1626 | query += ` - way(around.proximity${index + 1}:${minDist});\n`;
1627 | }
1628 | });
1629 |
1630 | query += ');\n';
1631 | } else {
1632 | query += ');\n';
1633 | }
1634 |
1635 | query += '\n// Sortir les résultats\nout geom;';
1636 |
1637 | return query;
1638 | }
1639 |
1640 | // Construire une requête standard dans une zone
1641 | function buildStandardQueryInArea(condition) {
1642 | let query = '[out:json][timeout:25];\n';
1643 | query += '// Définir la zone de recherche : France\n';
1644 | query += '(\n';
1645 | query += ' area["ISO3166-1"="FR"][admin_level=2];\n';
1646 | query += ')->.searchArea;\n\n';
1647 | query += '(\n';
1648 | query += buildConditionQuery(condition, 'searchArea');
1649 | query += ');\nout geom;';
1650 |
1651 | return query;
1652 | }
1653 |
1654 | // Construire la requête pour une condition spécifique
1655 | function buildConditionQuery(condition, areaVar) {
1656 | const { category, types, nameMode, nameValue } = condition;
1657 | let conditionQuery = '';
1658 |
1659 | // Construire le filtre par nom si spécifié
1660 | let nameFilter = '';
1661 | if (nameMode && nameValue && nameValue.trim()) {
1662 | switch (nameMode) {
1663 | case 'exact':
1664 | nameFilter = `["name"="${nameValue}"]`;
1665 | break;
1666 | case 'contains':
1667 | nameFilter = `["name"~".*${nameValue}.*",i]`;
1668 | break;
1669 | case 'starts':
1670 | nameFilter = `["name"~"^${nameValue}",i]`;
1671 | break;
1672 | }
1673 | }
1674 |
1675 | // Déterminer le filtre de zone
1676 | let areaFilter = '';
1677 | if (boundingBox) {
1678 | const { south, west, north, east } = boundingBox;
1679 | areaFilter = `(bbox:${south},${west},${north},${east})`;
1680 | } else {
1681 | areaFilter = `(area.${areaVar})`;
1682 | }
1683 |
1684 | if (types && types.length > 0) {
1685 | // Recherche avec types spécifiques sélectionnés
1686 | types.forEach(type => {
1687 | conditionQuery += ` node["${category}"="${type}"]${nameFilter}${areaFilter};\n`;
1688 | conditionQuery += ` way["${category}"="${type}"]${nameFilter}${areaFilter};\n`;
1689 | conditionQuery += ` relation["${category}"="${type}"]${nameFilter}${areaFilter};\n`;
1690 | });
1691 | } else {
1692 | // Recherche par catégorie seulement
1693 | conditionQuery += ` node["${category}"]${nameFilter}${areaFilter};\n`;
1694 | conditionQuery += ` way["${category}"]${nameFilter}${areaFilter};\n`;
1695 | conditionQuery += ` relation["${category}"]${nameFilter}${areaFilter};\n`;
1696 | }
1697 |
1698 | return conditionQuery;
1699 | }
1700 |
1701 | // Obtenir une description lisible d'une condition
1702 | function getConditionDescription(condition) {
1703 | const { category, types, nameMode, nameValue } = condition;
1704 | let description = config.categories[category]?.label || category;
1705 |
1706 | if (types && types.length > 0) {
1707 | const typeLabels = types.map(type => config.categories[category]?.types[type] || type);
1708 | description += ` (${typeLabels.join(', ')})`;
1709 | }
1710 |
1711 | if (nameMode && nameValue) {
1712 | switch (nameMode) {
1713 | case 'exact':
1714 | description += ` avec nom exact "${nameValue}"`;
1715 | break;
1716 | case 'contains':
1717 | description += ` contenant "${nameValue}"`;
1718 | break;
1719 | case 'starts':
1720 | description += ` commençant par "${nameValue}"`;
1721 | break;
1722 | }
1723 | }
1724 |
1725 | return description;
1726 | }
1727 |
1728 | // Collecter les conditions avec support du mode proximité
1729 | function collectConditionsWithProximity() {
1730 | const conditions = [];
1731 | const conditionGroups = document.querySelectorAll('.condition-group');
1732 | const searchMode = 'proximity'; // Mode fixé à proximité
1733 |
1734 | conditionGroups.forEach((group, index) => {
1735 | const condition = group.querySelector('.condition');
1736 | const conditionId = condition.id.replace('condition', '');
1737 |
1738 | const category = condition.querySelector('.category').value;
1739 |
1740 | // Collecter les types sélectionnés
1741 | const selectedTypes = [];
1742 | const typeCheckboxes = condition.querySelectorAll('.types-container input[type="checkbox"]:checked');
1743 | typeCheckboxes.forEach(checkbox => {
1744 | selectedTypes.push(checkbox.value);
1745 | });
1746 |
1747 | // Collecter les informations de recherche par nom
1748 | const nameMode = condition.querySelector('.name-mode').value;
1749 | const nameValue = condition.querySelector('.name').value;
1750 |
1751 | // Collecter les distances selon le mode
1752 | let distance, minDistance, maxDistance;
1753 |
1754 | if (searchMode === 'proximity' && index > 0) {
1755 | // Mode proximité : utiliser min/max
1756 | const minDistInput = condition.querySelector('.min-distance');
1757 | const maxDistInput = condition.querySelector('.max-distance');
1758 | minDistance = minDistInput ? parseInt(minDistInput.value) || 0 : 0;
1759 | maxDistance = maxDistInput ? parseInt(maxDistInput.value) || 1000 : 1000;
1760 | } else {
1761 | // Mode standard : utiliser distance simple
1762 | const distanceInput = condition.querySelector('.distance');
1763 | distance = distanceInput ? parseInt(distanceInput.value) || 1000 : 1000;
1764 | }
1765 |
1766 | if (category) {
1767 | conditions.push({
1768 | id: conditionId,
1769 | category,
1770 | types: selectedTypes,
1771 | nameMode,
1772 | nameValue,
1773 | distance,
1774 | minDistance,
1775 | maxDistance
1776 | });
1777 | }
1778 | });
1779 |
1780 | return conditions;
1781 | }
1782 |
1783 | // Peupler les zones géographiques
1784 | function populateGeographicZones() {
1785 | const geoLevelSelect = document.getElementById('geoLevel');
1786 | const geoZoneSelect = document.getElementById('geoZone');
1787 |
1788 | if (!geoLevelSelect || !geoZoneSelect) return;
1789 |
1790 | // Événement pour le changement de niveau géographique
1791 | geoLevelSelect.addEventListener('change', function() {
1792 | updateGeographicZones(this.value);
1793 | });
1794 |
1795 | // Initialiser avec le pays (France) par défaut
1796 | updateGeographicZones('country');
1797 | }
1798 |
1799 | // Mettre à jour les zones géographiques selon le niveau sélectionné
1800 | function updateGeographicZones(level) {
1801 | const geoZoneSelect = document.getElementById('geoZone');
1802 | if (!geoZoneSelect) return;
1803 |
1804 | // Vider les options existantes
1805 | geoZoneSelect.innerHTML = 'Sélectionner une zone ';
1806 |
1807 | if (!level || !config.geographicZones[level]) return;
1808 |
1809 | // Ajouter les zones du niveau sélectionné
1810 | Object.keys(config.geographicZones[level]).forEach(zoneName => {
1811 | const option = document.createElement('option');
1812 | option.value = zoneName;
1813 | option.textContent = zoneName;
1814 | if (zoneName === 'France' && level === 'country') {
1815 | option.selected = true; // Sélectionner France par défaut
1816 | }
1817 | geoZoneSelect.appendChild(option);
1818 | });
1819 |
1820 | // Mettre à jour la carte si une zone est sélectionnée
1821 | if (level === 'country' && geoZoneSelect.value === 'France') {
1822 | updateMapView('France', level);
1823 | }
1824 | }
1825 |
1826 | // Mettre à jour la vue de la carte selon la zone sélectionnée
1827 | function updateMapView(zoneName, level) {
1828 | if (!map || !config.geographicZones[level] || !config.geographicZones[level][zoneName]) return;
1829 |
1830 | const zoneData = config.geographicZones[level][zoneName];
1831 | const lat = zoneData.lat;
1832 | const lon = zoneData.lon;
1833 |
1834 | // Déterminer le niveau de zoom selon le type de zone
1835 | let zoom;
1836 | switch (level) {
1837 | case 'continent':
1838 | zoom = 3;
1839 | break;
1840 | case 'country':
1841 | zoom = 6;
1842 | break;
1843 | case 'region':
1844 | zoom = 8;
1845 | break;
1846 | case 'city':
1847 | zoom = 12;
1848 | break;
1849 | default:
1850 | zoom = 6;
1851 | }
1852 |
1853 | map.setView([lat, lon], zoom);
1854 | }
1855 |
1856 | // Obtenir la requête de zone géographique sélectionnée
1857 | function getSelectedGeographicArea() {
1858 | const geoLevelSelect = document.getElementById('geoLevel');
1859 | const geoZoneSelect = document.getElementById('geoZone');
1860 |
1861 | if (!geoLevelSelect || !geoZoneSelect) {
1862 | // Valeur par défaut si les éléments n'existent pas
1863 | return {
1864 | query: 'area["ISO3166-1"="FR"][admin_level=2];',
1865 | name: 'France'
1866 | };
1867 | }
1868 |
1869 | const level = geoLevelSelect.value;
1870 | const zoneName = geoZoneSelect.value;
1871 |
1872 | if (!level || !zoneName || !config.geographicZones[level] || !config.geographicZones[level][zoneName]) {
1873 | // Valeur par défaut
1874 | return {
1875 | query: 'area["ISO3166-1"="FR"][admin_level=2];',
1876 | name: 'France'
1877 | };
1878 | }
1879 |
1880 | return {
1881 | query: config.geographicZones[level][zoneName].query,
1882 | name: zoneName
1883 | };
1884 | }
1885 |
1886 | // Initialiser les contrôles de bounding box
1887 | function initializeBboxControls() {
1888 | const drawBboxBtn = document.getElementById('drawBbox');
1889 | const clearBboxBtn = document.getElementById('clearBbox');
1890 |
1891 | if (!drawBboxBtn || !clearBboxBtn) return;
1892 |
1893 | // Événements pour les boutons
1894 | drawBboxBtn.addEventListener('click', startBboxDrawing);
1895 | clearBboxBtn.addEventListener('click', clearBoundingBox);
1896 |
1897 | // Événements de la carte pour le dessin
1898 | map.on('mousedown', onMapMouseDown);
1899 | map.on('mousemove', onMapMouseMove);
1900 | map.on('mouseup', onMapMouseUp);
1901 | }
1902 |
1903 | // Commencer le dessin de bounding box
1904 | function startBboxDrawing() {
1905 | drawingMode = true;
1906 | const drawBboxBtn = document.getElementById('drawBbox');
1907 | const clearBboxBtn = document.getElementById('clearBbox');
1908 |
1909 | drawBboxBtn.textContent = 'Cliquez et glissez sur la carte';
1910 | drawBboxBtn.disabled = true;
1911 |
1912 | // Changer le curseur de la carte
1913 | map.getContainer().style.cursor = 'crosshair';
1914 |
1915 | // Effacer la bounding box existante si elle existe
1916 | if (bboxRectangle) {
1917 | map.removeLayer(bboxRectangle);
1918 | bboxRectangle = null;
1919 | }
1920 | }
1921 |
1922 | // Variables pour le dessin
1923 | let isDrawing = false;
1924 | let startLatLng = null;
1925 |
1926 | // Gérer le début du dessin (mousedown)
1927 | function onMapMouseDown(e) {
1928 | if (!drawingMode) return;
1929 |
1930 | isDrawing = true;
1931 | startLatLng = e.latlng;
1932 |
1933 | // Empêcher le déplacement de la carte pendant le dessin
1934 | map.dragging.disable();
1935 | map.touchZoom.disable();
1936 | map.doubleClickZoom.disable();
1937 | map.scrollWheelZoom.disable();
1938 | map.boxZoom.disable();
1939 | map.keyboard.disable();
1940 | }
1941 |
1942 | // Gérer le mouvement de la souris (mousemove)
1943 | function onMapMouseMove(e) {
1944 | if (!drawingMode || !isDrawing || !startLatLng) return;
1945 |
1946 | // Supprimer le rectangle temporaire s'il existe
1947 | if (bboxRectangle) {
1948 | map.removeLayer(bboxRectangle);
1949 | }
1950 |
1951 | // Créer un nouveau rectangle temporaire
1952 | const bounds = L.latLngBounds(startLatLng, e.latlng);
1953 | bboxRectangle = L.rectangle(bounds, {
1954 | color: '#ff7800',
1955 | weight: 2,
1956 | fillOpacity: 0.1
1957 | }).addTo(map);
1958 | }
1959 |
1960 | // Gérer la fin du dessin (mouseup)
1961 | function onMapMouseUp(e) {
1962 | if (!drawingMode || !isDrawing || !startLatLng) return;
1963 |
1964 | isDrawing = false;
1965 | drawingMode = false;
1966 |
1967 | // Réactiver les contrôles de la carte
1968 | map.dragging.enable();
1969 | map.touchZoom.enable();
1970 | map.doubleClickZoom.enable();
1971 | map.scrollWheelZoom.enable();
1972 | map.boxZoom.enable();
1973 | map.keyboard.enable();
1974 |
1975 | // Restaurer le curseur
1976 | map.getContainer().style.cursor = '';
1977 |
1978 | // Finaliser la bounding box
1979 | const endLatLng = e.latlng;
1980 | finalizeBoundingBox(startLatLng, endLatLng);
1981 |
1982 | startLatLng = null;
1983 | }
1984 |
1985 | // Finaliser la bounding box
1986 | function finalizeBoundingBox(start, end) {
1987 | // Calculer les coordonnées de la bounding box
1988 | const south = Math.min(start.lat, end.lat);
1989 | const north = Math.max(start.lat, end.lat);
1990 | const west = Math.min(start.lng, end.lng);
1991 | const east = Math.max(start.lng, end.lng);
1992 |
1993 | // Stocker la bounding box
1994 | boundingBox = { south, west, north, east };
1995 |
1996 | // Mettre à jour l'interface
1997 | updateBboxDisplay();
1998 |
1999 | // Réactiver le bouton de dessin
2000 | const drawBboxBtn = document.getElementById('drawBbox');
2001 | const clearBboxBtn = document.getElementById('clearBbox');
2002 |
2003 | drawBboxBtn.innerHTML = ' Redessiner la zone';
2004 | drawBboxBtn.disabled = false;
2005 | clearBboxBtn.style.display = 'inline-flex';
2006 | }
2007 |
2008 | // Mettre à jour l'affichage de la bounding box
2009 | function updateBboxDisplay() {
2010 | if (!boundingBox) return;
2011 |
2012 | const bboxDisplay = document.getElementById('bboxDisplay');
2013 | const bboxCoords = document.getElementById('bboxCoords');
2014 |
2015 | if (bboxDisplay && bboxCoords) {
2016 | const coordsText = `${boundingBox.south.toFixed(6)}, ${boundingBox.west.toFixed(6)}, ${boundingBox.north.toFixed(6)}, ${boundingBox.east.toFixed(6)}`;
2017 | bboxCoords.value = coordsText;
2018 | bboxDisplay.style.display = 'block';
2019 | }
2020 | }
2021 |
2022 | // Effacer la bounding box
2023 | function clearBoundingBox() {
2024 | // Supprimer le rectangle de la carte
2025 | if (bboxRectangle) {
2026 | map.removeLayer(bboxRectangle);
2027 | bboxRectangle = null;
2028 | }
2029 |
2030 | // Réinitialiser les variables
2031 | boundingBox = null;
2032 | drawingMode = false;
2033 | isDrawing = false;
2034 | startLatLng = null;
2035 |
2036 | // Mettre à jour l'interface
2037 | const drawBboxBtn = document.getElementById('drawBbox');
2038 | const clearBboxBtn = document.getElementById('clearBbox');
2039 | const bboxDisplay = document.getElementById('bboxDisplay');
2040 | const bboxCoords = document.getElementById('bboxCoords');
2041 |
2042 | drawBboxBtn.innerHTML = ' Dessiner sur la carte';
2043 | drawBboxBtn.disabled = false;
2044 | clearBboxBtn.style.display = 'none';
2045 |
2046 | if (bboxDisplay) {
2047 | bboxDisplay.style.display = 'none';
2048 | }
2049 | if (bboxCoords) {
2050 | bboxCoords.value = '';
2051 | }
2052 |
2053 | // Restaurer le curseur
2054 | map.getContainer().style.cursor = '';
2055 |
2056 | // Réactiver les contrôles de la carte
2057 | map.dragging.enable();
2058 | map.touchZoom.enable();
2059 | map.doubleClickZoom.enable();
2060 | map.scrollWheelZoom.enable();
2061 | map.boxZoom.enable();
2062 | map.keyboard.enable();
2063 | }
2064 |
2065 | // Obtenir la requête de zone géographique sélectionnée (modifiée pour bounding box)
2066 | function getSelectedGeographicArea() {
2067 | // Si une bounding box est définie, l'utiliser
2068 | if (boundingBox) {
2069 | const { south, west, north, east } = boundingBox;
2070 | return {
2071 | query: `(bbox:${south},${west},${north},${east});`,
2072 | name: `Zone personnalisée (${south.toFixed(3)}, ${west.toFixed(3)}, ${north.toFixed(3)}, ${east.toFixed(3)})`
2073 | };
2074 | }
2075 |
2076 | // Sinon, utiliser la France par défaut
2077 | return {
2078 | query: 'area["ISO3166-1"="FR"][admin_level=2];',
2079 | name: 'France'
2080 | };
2081 | }
2082 |
2083 | // Peupler les catégories principales
2084 | function populateMainCategories() {
2085 | const categorySelect = document.querySelector('.main-search .category');
2086 | if (!categorySelect) return;
2087 |
2088 | // Vider les options existantes sauf la première
2089 | categorySelect.innerHTML = 'Sélectionner une catégorie ';
2090 |
2091 | // Ajouter les catégories depuis la configuration
2092 | Object.keys(config.categories).forEach(categoryKey => {
2093 | const category = config.categories[categoryKey];
2094 | const option = document.createElement('option');
2095 | option.value = categoryKey;
2096 | option.textContent = category.label;
2097 | categorySelect.appendChild(option);
2098 | });
2099 | }
2100 |
2101 | // Ajouter un complément de recherche
2102 | function addComplement() {
2103 | complementCount++;
2104 |
2105 | const complementsContainer = document.getElementById('complementsContainer');
2106 | const newComplement = document.createElement('div');
2107 | newComplement.className = 'complement-item';
2108 | newComplement.id = `complement${complementCount}`;
2109 |
2110 | newComplement.innerHTML = `
2111 |
2123 |
2124 |
2125 |
2126 | Catégorie :
2127 |
2128 | Sélectionner une catégorie
2129 |
2130 |
2131 |
2132 |
2133 |
Types (sélection multiple) :
2134 |
2135 |
Sélectionnez d'abord une catégorie
2136 |
2137 |
2138 |
2139 |
2140 |
Recherche par nom (optionnel) :
2141 |
2142 |
2143 | Ignorer le nom
2144 | Nom exact
2145 | Contient
2146 | Commence par
2147 |
2148 |
2149 |
2150 |
2151 |
2152 |
2153 |
Distance :
2154 |
2155 |
2156 | mètres
2157 |
2158 |
2159 |
2160 | `;
2161 |
2162 | complementsContainer.appendChild(newComplement);
2163 |
2164 | // Peupler les catégories pour le nouveau complément
2165 | const newCategorySelect = newComplement.querySelector('.category');
2166 | populateCategorySelect(newCategorySelect);
2167 |
2168 | // Mettre à jour la visibilité des boutons de suppression
2169 | updateComplementRemoveButtons();
2170 | }
2171 |
2172 | // Supprimer un complément
2173 | function removeComplement(complementNumber) {
2174 | const complement = document.getElementById(`complement${complementNumber}`);
2175 | if (complement) {
2176 | complement.remove();
2177 | updateComplementRemoveButtons();
2178 | }
2179 | }
2180 |
2181 | // Mettre à jour la visibilité des boutons de suppression des compléments
2182 | function updateComplementRemoveButtons() {
2183 | const complements = document.querySelectorAll('.complement-item');
2184 | complements.forEach((complement, index) => {
2185 | const removeBtn = complement.querySelector('.remove-complement');
2186 | if (removeBtn) {
2187 | removeBtn.style.display = complements.length > 1 ? 'flex' : 'none';
2188 | }
2189 | });
2190 | }
2191 |
2192 | // Collecter la recherche principale et les compléments
2193 | function collectMainSearchAndComplements() {
2194 | const result = {
2195 | mainSearch: null,
2196 | complements: []
2197 | };
2198 |
2199 | // Collecter la recherche principale
2200 | const mainSearch = document.querySelector('.main-search');
2201 | if (mainSearch) {
2202 | const category = mainSearch.querySelector('.category').value;
2203 |
2204 | if (category) {
2205 | // Collecter les types sélectionnés
2206 | const selectedTypes = [];
2207 | const typeCheckboxes = mainSearch.querySelectorAll('.types-container input[type="checkbox"]:checked');
2208 | typeCheckboxes.forEach(checkbox => {
2209 | selectedTypes.push(checkbox.value);
2210 | });
2211 |
2212 | // Collecter les informations de recherche par nom
2213 | const nameMode = mainSearch.querySelector('.name-mode').value;
2214 | const nameValue = mainSearch.querySelector('.name').value;
2215 |
2216 | result.mainSearch = {
2217 | category,
2218 | types: selectedTypes,
2219 | nameMode,
2220 | nameValue
2221 | };
2222 | }
2223 | }
2224 |
2225 | // Collecter les compléments
2226 | const complements = document.querySelectorAll('.complement-item');
2227 | complements.forEach((complement, index) => {
2228 | const category = complement.querySelector('.category').value;
2229 | const operator = complement.querySelector('.operator').value;
2230 | const distance = complement.querySelector('.distance').value;
2231 |
2232 | if (category) {
2233 | // Collecter les types sélectionnés
2234 | const selectedTypes = [];
2235 | const typeCheckboxes = complement.querySelectorAll('.types-container input[type="checkbox"]:checked');
2236 | typeCheckboxes.forEach(checkbox => {
2237 | selectedTypes.push(checkbox.value);
2238 | });
2239 |
2240 | // Collecter les informations de recherche par nom
2241 | const nameMode = complement.querySelector('.name-mode').value;
2242 | const nameValue = complement.querySelector('.name').value;
2243 |
2244 | result.complements.push({
2245 | operator,
2246 | category,
2247 | types: selectedTypes,
2248 | nameMode,
2249 | nameValue,
2250 | distance: parseInt(distance) || 100
2251 | });
2252 | }
2253 | });
2254 |
2255 | return result;
2256 | }
2257 |
2258 | // Construire la requête selon votre exemple
2259 | function buildNewStyleQuery() {
2260 | const searchData = collectMainSearchAndComplements();
2261 |
2262 | if (!searchData.mainSearch) {
2263 | throw new Error('Veuillez définir la recherche principale');
2264 | }
2265 |
2266 | // Obtenir la bounding box
2267 | if (!boundingBox) {
2268 | throw new Error('Veuillez dessiner une zone de recherche sur la carte');
2269 | }
2270 |
2271 | const { south, west, north, east } = boundingBox;
2272 |
2273 | // Construire la requête selon votre exemple
2274 | let query = `[out:json][timeout:25][bbox:${south},${west},${north},${east}];\n`;
2275 |
2276 | if (searchData.complements.length > 0) {
2277 | // 1. D'abord trouver les compléments dans la zone (éléments de référence)
2278 | searchData.complements.forEach((complement, index) => {
2279 | const complementName = getComplementVariableName(complement);
2280 | query += `// ${index + 1}. Trouver les ${getConditionDescription(complement)} dans la zone\n`;
2281 | query += '(\n';
2282 | query += buildMainSearchQuery(complement);
2283 | query += `)->.${complementName};\n\n`;
2284 | });
2285 |
2286 | // 2. Chercher les éléments principaux dans la zone avec contraintes de proximité
2287 | query += `// ${searchData.complements.length + 1}. Trouver les ${getConditionDescription(searchData.mainSearch)} dans la zone`;
2288 |
2289 | // Ajouter les contraintes de proximité dans le titre
2290 | const proximityDescriptions = searchData.complements.map((comp, index) =>
2291 | `à ${comp.distance}m des ${getConditionDescription(comp)}`
2292 | );
2293 | query += ` ${proximityDescriptions.join(' et ')}\n`;
2294 |
2295 | query += '(\n';
2296 |
2297 | // Construire la requête pour les éléments principaux avec toutes les contraintes de proximité
2298 | const { category, types, nameMode, nameValue } = searchData.mainSearch;
2299 |
2300 | // Construire le filtre par nom si spécifié
2301 | let nameFilter = '';
2302 | if (nameMode && nameValue && nameValue.trim()) {
2303 | switch (nameMode) {
2304 | case 'exact':
2305 | nameFilter = `[name="${nameValue}"]`;
2306 | break;
2307 | case 'contains':
2308 | nameFilter = `[name~"${nameValue}",i]`;
2309 | break;
2310 | case 'starts':
2311 | nameFilter = `[name~"^${nameValue}",i]`;
2312 | break;
2313 | }
2314 | }
2315 |
2316 | // Construire les filtres de proximité pour tous les compléments
2317 | let proximityFilters = '';
2318 | searchData.complements.forEach((complement, index) => {
2319 | const complementName = getComplementVariableName(complement);
2320 | proximityFilters += `(around.${complementName}:${complement.distance})`;
2321 | });
2322 |
2323 | // Appliquer à tous les types d'éléments
2324 | if (types && types.length > 0) {
2325 | // Recherche avec types spécifiques sélectionnés
2326 | types.forEach(type => {
2327 | query += ` nwr[${category}=${type}]${nameFilter}${proximityFilters};\n`;
2328 | });
2329 | } else {
2330 | // Recherche par catégorie seulement
2331 | query += ` nwr[${category}]${nameFilter}${proximityFilters};\n`;
2332 | }
2333 |
2334 | query += `)->.main_results;\n\n`;
2335 |
2336 | // 3. Sortir tous les résultats avec des tags pour les identifier
2337 | query += '// Sortir les résultats principaux\n';
2338 | query += '.main_results out center;\n\n';
2339 |
2340 | // 4. Sortir les compléments avec des tags pour les identifier
2341 | searchData.complements.forEach((complement, index) => {
2342 | const complementName = getComplementVariableName(complement);
2343 | query += `// Sortir les ${getConditionDescription(complement)}\n`;
2344 | query += `.${complementName} out center;\n`;
2345 | if (index < searchData.complements.length - 1) query += '\n';
2346 | });
2347 | } else {
2348 | // Si pas de compléments, recherche simple des éléments principaux
2349 | query += '// 1. Trouver les éléments principaux dans la zone\n';
2350 | query += '(\n';
2351 | query += buildMainSearchQuery(searchData.mainSearch);
2352 | query += ');\nout center;';
2353 | }
2354 |
2355 | return query;
2356 | }
2357 |
2358 | // Construire la requête pour la recherche principale
2359 | function buildMainSearchQuery(mainSearch) {
2360 | const { category, types, nameMode, nameValue } = mainSearch;
2361 | let query = '';
2362 |
2363 | // Construire le filtre par nom si spécifié
2364 | let nameFilter = '';
2365 | if (nameMode && nameValue && nameValue.trim()) {
2366 | switch (nameMode) {
2367 | case 'exact':
2368 | nameFilter = `[name="${nameValue}"]`;
2369 | break;
2370 | case 'contains':
2371 | nameFilter = `[name~"${nameValue}",i]`;
2372 | break;
2373 | case 'starts':
2374 | nameFilter = `[name~"^${nameValue}",i]`;
2375 | break;
2376 | }
2377 | }
2378 |
2379 | if (types && types.length > 0) {
2380 | // Recherche avec types spécifiques sélectionnés
2381 | types.forEach(type => {
2382 | query += ` nwr[${category}=${type}]${nameFilter};\n`;
2383 | });
2384 | } else {
2385 | // Recherche par catégorie seulement
2386 | query += ` nwr[${category}]${nameFilter};\n`;
2387 | }
2388 |
2389 | return query;
2390 | }
2391 |
2392 | // Construire la requête pour un complément
2393 | function buildComplementQuery(complement) {
2394 | const { category, types, nameMode, nameValue, distance } = complement;
2395 | let query = '';
2396 |
2397 | // Construire le filtre par nom si spécifié
2398 | let nameFilter = '';
2399 | if (nameMode && nameValue && nameValue.trim()) {
2400 | switch (nameMode) {
2401 | case 'exact':
2402 | nameFilter = `[name="${nameValue}"]`;
2403 | break;
2404 | case 'contains':
2405 | nameFilter = `[name~"${nameValue}",i]`;
2406 | break;
2407 | case 'starts':
2408 | nameFilter = `[name~"^${nameValue}",i]`;
2409 | break;
2410 | }
2411 | }
2412 |
2413 | if (types && types.length > 0) {
2414 | // Recherche avec types spécifiques sélectionnés
2415 | types.forEach(type => {
2416 | query += ` nwr[${category}=${type}]${nameFilter}(around.main_elements:${distance});\n`;
2417 | });
2418 | } else {
2419 | // Recherche par catégorie seulement
2420 | query += ` nwr[${category}]${nameFilter}(around.main_elements:${distance});\n`;
2421 | }
2422 |
2423 | return query;
2424 | }
2425 |
2426 | // Obtenir le nom de variable pour un complément
2427 | function getComplementVariableName(complement) {
2428 | const { category, types } = complement;
2429 |
2430 | // Créer un nom de variable basé sur la catégorie et le type
2431 | if (types && types.length > 0) {
2432 | const firstType = types[0];
2433 | return `${firstType.replace(/[^a-zA-Z0-9]/g, '_')}s`;
2434 | } else {
2435 | return `${category.replace(/[^a-zA-Z0-9]/g, '_')}s`;
2436 | }
2437 | }
2438 |
2439 | // Construire la requête pour la recherche principale avec référence
2440 | function buildMainSearchQueryWithReference(mainSearch, referenceName, distance) {
2441 | const { category, types, nameMode, nameValue } = mainSearch;
2442 | let query = '';
2443 |
2444 | // Construire le filtre par nom si spécifié
2445 | let nameFilter = '';
2446 | if (nameMode && nameValue && nameValue.trim()) {
2447 | switch (nameMode) {
2448 | case 'exact':
2449 | nameFilter = `[name="${nameValue}"]`;
2450 | break;
2451 | case 'contains':
2452 | nameFilter = `[name~"${nameValue}",i]`;
2453 | break;
2454 | case 'starts':
2455 | nameFilter = `[name~"^${nameValue}",i]`;
2456 | break;
2457 | }
2458 | }
2459 |
2460 | if (types && types.length > 0) {
2461 | // Recherche avec types spécifiques sélectionnés
2462 | types.forEach(type => {
2463 | query += ` nwr[${category}=${type}]${nameFilter}(around.${referenceName}:${distance});\n`;
2464 | });
2465 | } else {
2466 | // Recherche par catégorie seulement
2467 | query += ` nwr[${category}]${nameFilter}(around.${referenceName}:${distance});\n`;
2468 | }
2469 |
2470 | return query;
2471 | }
2472 |
2473 | // Construire la requête pour un complément avec référence
2474 | function buildComplementQueryWithReference(mainSearch, referenceName, distance) {
2475 | const { category, types, nameMode, nameValue } = mainSearch;
2476 | let query = '';
2477 |
2478 | // Construire le filtre par nom si spécifié
2479 | let nameFilter = '';
2480 | if (nameMode && nameValue && nameValue.trim()) {
2481 | switch (nameMode) {
2482 | case 'exact':
2483 | nameFilter = `[name="${nameValue}"]`;
2484 | break;
2485 | case 'contains':
2486 | nameFilter = `[name~"${nameValue}",i]`;
2487 | break;
2488 | case 'starts':
2489 | nameFilter = `[name~"^${nameValue}",i]`;
2490 | break;
2491 | }
2492 | }
2493 |
2494 | if (types && types.length > 0) {
2495 | // Recherche avec types spécifiques sélectionnés
2496 | types.forEach(type => {
2497 | query += ` nwr[${category}=${type}]${nameFilter}(around.${referenceName}:${distance});\n`;
2498 | });
2499 | } else {
2500 | // Recherche par catégorie seulement
2501 | query += ` nwr[${category}]${nameFilter}(around.${referenceName}:${distance});\n`;
2502 | }
2503 |
2504 | return query;
2505 | }
2506 |
2507 | // Initialiser la recherche d'adresse
2508 | function initializeAddressSearch() {
2509 | const addressInput = document.getElementById('addressInput');
2510 | const searchAddressBtn = document.getElementById('searchAddressBtn');
2511 | const addressSuggestions = document.getElementById('addressSuggestions');
2512 |
2513 | if (!addressInput || !searchAddressBtn || !addressSuggestions) return;
2514 |
2515 | let searchTimeout;
2516 |
2517 | // Recherche en temps réel pendant la saisie
2518 | addressInput.addEventListener('input', function() {
2519 | const query = this.value.trim();
2520 |
2521 | clearTimeout(searchTimeout);
2522 |
2523 | if (query.length < 3) {
2524 | addressSuggestions.style.display = 'none';
2525 | return;
2526 | }
2527 |
2528 | searchTimeout = setTimeout(() => {
2529 | searchAddresses(query);
2530 | }, 300);
2531 | });
2532 |
2533 | // Recherche au clic sur le bouton
2534 | searchAddressBtn.addEventListener('click', function() {
2535 | const query = addressInput.value.trim();
2536 | if (query.length >= 3) {
2537 | searchAddresses(query);
2538 | }
2539 | });
2540 |
2541 | // Recherche à l'appui sur Entrée
2542 | addressInput.addEventListener('keypress', function(e) {
2543 | if (e.key === 'Enter') {
2544 | const query = this.value.trim();
2545 | if (query.length >= 3) {
2546 | searchAddresses(query);
2547 | }
2548 | }
2549 | });
2550 |
2551 | // Masquer les suggestions quand on clique ailleurs
2552 | document.addEventListener('click', function(e) {
2553 | if (!addressInput.contains(e.target) && !addressSuggestions.contains(e.target)) {
2554 | addressSuggestions.style.display = 'none';
2555 | }
2556 | });
2557 | }
2558 |
2559 | // Rechercher des adresses avec l'API Nominatim
2560 | async function searchAddresses(query) {
2561 | const addressSuggestions = document.getElementById('addressSuggestions');
2562 |
2563 | try {
2564 | // Afficher un indicateur de chargement
2565 | addressSuggestions.innerHTML = '🔍 Recherche en cours...
';
2566 | addressSuggestions.style.display = 'block';
2567 |
2568 | // Construire l'URL de l'API Nominatim
2569 | const nominatimUrl = `https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&limit=5&q=${encodeURIComponent(query)}`;
2570 |
2571 | const response = await fetch(nominatimUrl, {
2572 | headers: {
2573 | 'User-Agent': 'OSINT-Recherche-Overpass/1.0'
2574 | }
2575 | });
2576 |
2577 | if (!response.ok) {
2578 | throw new Error(`Erreur HTTP: ${response.status}`);
2579 | }
2580 |
2581 | const results = await response.json();
2582 |
2583 | // Afficher les résultats
2584 | displayAddressSuggestions(results);
2585 |
2586 | } catch (error) {
2587 | console.error('Erreur lors de la recherche d\'adresse:', error);
2588 | addressSuggestions.innerHTML = '❌ Erreur lors de la recherche
';
2589 | addressSuggestions.style.display = 'block';
2590 |
2591 | setTimeout(() => {
2592 | addressSuggestions.style.display = 'none';
2593 | }, 3000);
2594 | }
2595 | }
2596 |
2597 | // Afficher les suggestions d'adresses
2598 | function displayAddressSuggestions(results) {
2599 | const addressSuggestions = document.getElementById('addressSuggestions');
2600 |
2601 | if (!results || results.length === 0) {
2602 | addressSuggestions.innerHTML = '🚫 Aucune adresse trouvée
';
2603 | addressSuggestions.style.display = 'block';
2604 |
2605 | setTimeout(() => {
2606 | addressSuggestions.style.display = 'none';
2607 | }, 3000);
2608 | return;
2609 | }
2610 |
2611 | // Construire la liste des suggestions
2612 | let suggestionsHTML = '';
2613 |
2614 | results.forEach((result, index) => {
2615 | const lat = parseFloat(result.lat);
2616 | const lon = parseFloat(result.lon);
2617 | const displayName = result.display_name;
2618 | const type = result.type || 'Lieu';
2619 |
2620 | // Extraire les informations principales
2621 | const address = result.address || {};
2622 | const name = address.house_number && address.road
2623 | ? `${address.house_number} ${address.road}`
2624 | : address.road || address.village || address.town || address.city || result.name || 'Sans nom';
2625 |
2626 | const location = [address.city, address.town, address.village, address.county]
2627 | .filter(Boolean)
2628 | .join(', ') || address.country || '';
2629 |
2630 | suggestionsHTML += `
2631 |
2632 |
${name}
2633 |
${location} • ${type}
2634 |
2635 | `;
2636 | });
2637 |
2638 | addressSuggestions.innerHTML = suggestionsHTML;
2639 | addressSuggestions.style.display = 'block';
2640 |
2641 | // Ajouter les événements de clic sur les suggestions
2642 | const suggestionElements = addressSuggestions.querySelectorAll('.address-suggestion');
2643 | suggestionElements.forEach(suggestion => {
2644 | suggestion.addEventListener('click', function() {
2645 | const lat = parseFloat(this.dataset.lat);
2646 | const lon = parseFloat(this.dataset.lon);
2647 | const name = this.dataset.name;
2648 |
2649 | // Zoomer sur l'adresse sélectionnée
2650 | zoomToAddress(lat, lon, name);
2651 |
2652 | // Masquer les suggestions
2653 | addressSuggestions.style.display = 'none';
2654 |
2655 | // Mettre à jour le champ de recherche
2656 | document.getElementById('addressInput').value = this.querySelector('.suggestion-name').textContent;
2657 | });
2658 | });
2659 | }
2660 |
2661 | // Zoomer sur une adresse
2662 | function zoomToAddress(lat, lon, name) {
2663 | if (!map) return;
2664 |
2665 | // Zoomer sur l'adresse
2666 | map.setView([lat, lon], 16);
2667 |
2668 | // Ajouter un marqueur temporaire
2669 | const addressMarker = L.marker([lat, lon], {
2670 | icon: L.icon({
2671 | iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
2672 | shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
2673 | iconSize: [25, 41],
2674 | iconAnchor: [12, 41],
2675 | popupAnchor: [1, -34],
2676 | shadowSize: [41, 41]
2677 | })
2678 | }).addTo(map);
2679 |
2680 | // Popup avec les informations de l'adresse
2681 | const popupContent = `
2682 |
2683 |
📍 Adresse recherchée
2684 |
${name}
2685 |
Coordonnées: ${lat.toFixed(6)}, ${lon.toFixed(6)}
2686 |
2687 |
2688 | Supprimer ce marqueur
2689 |
2690 |
2691 |
2692 | `;
2693 |
2694 | addressMarker.bindPopup(popupContent).openPopup();
2695 |
2696 | // Stocker la référence du marqueur pour pouvoir le supprimer
2697 | window.currentAddressMarker = addressMarker;
2698 |
2699 | showSuccess(`Adresse trouvée : ${name}`);
2700 | }
2701 |
2702 | // Supprimer le marqueur d'adresse
2703 | function removeAddressMarker() {
2704 | if (window.currentAddressMarker) {
2705 | map.removeLayer(window.currentAddressMarker);
2706 | window.currentAddressMarker = null;
2707 | showSuccess('Marqueur d\'adresse supprimé');
2708 | }
2709 | }
2710 |
2711 | // Fonctions utilitaires globales
2712 | window.removeCondition = removeCondition;
2713 | window.removeComplement = removeComplement;
2714 | window.removeAddressMarker = removeAddressMarker;
2715 | window.zoomToResult = zoomToResult;
2716 |
--------------------------------------------------------------------------------