1<!DOCTYPE html> 2<html lang="fr" style="height: 100%"> 3 <head> 4 <meta http-equiv="content-language" content="fr" /> 5<meta charset="UTF-8"/> 6<meta name="viewport" content="width=device-width,initial-scale=1.0"> 7 <title>Logiciel de randonnée gratuit</title> 8 <meta name="description" content="Notre logiciel gratuit permet de créer un circuit de randonnée sur une carte au 1:25000 de l'IGN et de voir son diagramme de dénivellation."/> 9 <link rel="canonical" href="https://www.visorando.com/logiciel-randonnee.html"/> 10 <link rel="alternate" hreflang="fr" href="https://www.visorando.com/logiciel-randonnee.html"/> 11 <link rel="alternate" hreflang="en" href="https://www.visorando.com/en/walk-planner.html"/> 12 <link rel="alternate" hreflang="de" href="https://www.visorando.com/de/tourenplaner-wandern.html"/> 13 <link rel="alternate" hreflang="x-default" href="https://www.visorando.com/en/walk-planner.html"/> 14 <link href="https://www.visorando.com/css.css?v=1790147326" rel="stylesheet"/> 15 <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" /> 16 <link href="https://www.visorando.com/templatesR/visorando/css/visorandomap.css?v=1790147288" rel="stylesheet"/> 17 <link href="https://www.visorando.com/templatesR/visorando/css/libs/izitoast.css?v=1790147288" rel="stylesheet"/> 18 <link href="https://www.visorando.com/templatesR/visorando/css/modal.css?v=1790147288" rel="stylesheet"/> 19 <link href="https://www.visorando.com/templatesR/visorando/css/mapbox-gl-draw.css?v=1790147288" rel="stylesheet"/> 20 <link href="https://www.visorando.com/templatesR/visorando/css/swiper.min.css?v=1790147288" rel="stylesheet"/> 21 <link href="https://www.visorando.com/templatesR/visorando/css/pages/mapSoftware.css?v=1790147288" rel="stylesheet"/> 22 <link href="https://www.visorando.com/templatesR/visorando/css/pages/softwareToolbar.css?v=1790147288" rel="stylesheet"/> 23
23<script type="text/javascript"> 24 var _paq = window._paq = window._paq || []; 25 _paq.push(['setCustomDimension', 1, 'Free']); 26 _paq.push(['setCustomDimension', 2, 'Private_Individual']); 27 _paq.push(['setCustomDimension', 3, 'Visitor']); 28 _paq.push(['setCustomDimension', 4, 'No']); 29 _paq.push(['setCustomDimension', 5, 'rando/newCircuitV2']); 30 (function() { 31 var mtmKeys = ['mtm_campaign', 'mtm_source', 'mtm_medium', 'mtm_content']; 32 var params = new URLSearchParams(window.location.search); 33 var mtmParams = []; 34 for (var i = 0; i < mtmKeys.length; i++) { 35 var val = params.get(mtmKeys[i]); 36 if (val) { 37 mtmParams.push(mtmKeys[i] + '=' + encodeURIComponent(val)); 38 } 39 } 40 if (mtmParams.length > 0) { 41 for (var j = 0; j < _paq.length; j++) { 42 if (_paq[j][0] === 'setCustomUrl') { 43 var url = _paq[j][1]; 44 url += (url.indexOf('?') === -1 ? '?' : '&') + mtmParams.join('&'); 45 _paq[j][1] = url; 46 break; 47 } 48 } 49 } 50 })(); 51 _paq.push(['trackPageView']); 52 _paq.push(['enableLinkTracking']); 53 _paq.push(['enableHeartBeatTimer']); 54 (function () { 55 var u = "//analytics.visorando.com/"; 56 _paq.push(['setTrackerUrl', u + 'matomo.php']); 57 _paq.push(['setSiteId', '1']); 58 var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; 59 g.type = 'text/javascript'; 60 g.async = true; 61 g.src = u + 'matomo.js'; 62 s.parentNode.insertBefore(g, s); 63 })(); 64</script>
64 65 66 <meta name="apple-itunes-app" content="app-id=969793415"/> 67<link rel="shortcut icon" href="https://www.visorando.com/images/favicons/favicon.ico"/> 68<link rel="icon" href="https://www.visorando.com/images/favicons/favicon-16.png" sizes="16x16" type="image/png"> 69<link rel="icon" href="https://www.visorando.com/images/favicons/favicon-32.png" sizes="32x32" type="image/png"> 70<link rel="icon" type="image/svg+xml" href="https://www.visorando.com/images/favicons/favicon.svg"> 71<!--Android--> 72<meta name="theme-color" content="#986F57"> 73<meta name="application-name" content="Visorando"> 74<meta name="mobile-web-app-capable" content="yes"> 75<!--iOS--> 76<link rel="apple-touch-icon" href="https://www.visorando.com/images/favicons/icon-180.png" sizes="180x180" type="image/png"> 77<meta name="apple-mobile-web-app-capable" content="yes"> 78<meta name="apple-mobile-web-app-status-bar-style" content="default"> 79<meta name="apple-mobile-web-app-title" content="Visorando"> 80 81<meta name="google-signin-client_id" content="564074217767-ofteei0v4sv04vjev17eeipnqaf17tsv.apps.googleusercontent.com"> 82<meta name="verify-v1" content="SrkH0+upkPGLTLLX6rJcrWue1uHFh3XVtz1ye3wlosg="/> 83 84<!--[if lt IE 9]>
85<script src="https://www.visorando.com/templatesR/visorando/js/html5shiv.js"></script>
vendor: 1 bytes, line 85
85
86<script src="https://www.visorando.com/templatesR/visorando/js/respond.min.js"></script>
86 87<![endif]--> </head> 88 <body style="height: 100%"> 89  90 91 92 <div class='top-bar clearfix'> 93 <div class='group-actions' > 94 <div class='nav'> 95 <a href='https://www.visorando.com/' target='_blank' title="Retour sur l'accueil du site" class="tippy"> 96 <i style="margin-right:0px" class="vr-icon vr-icon-home"></i> 97 </a><a href='javascript:;' id="undo" title="Annuler la dernière action" disabled="disabled" class="tippy"> 98 <i class="vr-icon vr-icon-arrow-back"></i> 99 <span class="xs:hidden">Annuler</span> 100 </a><a href='javascript:;' id="redo" title="Restaurer la dernière action" disabled="disabled" class="tippy"> 101 <i class="vr-icon vr-icon-arrow-forward"></i> 102 <span class="xs:hidden">Restaurer</span> 103 </a><a href='javascript:;' id="draw" title="Commencer un nouveau tracé" class="tippy mode draw_line_string advanced hide"> 104 <i class="vr-icon vr-icon-trace-vector"></i> 105 <span>Tracer</span> 106 </a><a href='javascript:;' title="Continuer un tracé" class='tippy advanced hide' id="continue"> 107 <i class="vr-icon vr-icon-edit"></i> 108 <span>Continuer</span> 109 </a><!--<a href='javascript:;' class='tippy advanced' id="go-to" title=""> 110 <i class="vr-icon vr-icon-search"></i> 111 <span></span> 112 </a>--><a href='javascript:;' id='save' class='tippy' title="Sauvegarder le(s) tracé(s)"> 113 <i class="vr-icon vr-icon-save"></i> 114 <span>Enregistrer</span> 115 </a><a href='javascript:;' id='print' class='tippy' title="Imprimer au format PDF"> 116 <i class="vr-icon vr-icon-print"></i> 117 <span>Imprimer/PDF</span> 118 </a><a href='javascript:;' class='tippy' id='export-gpx' title="Obtenir un tracé au format GPX"> 119 <i class="vr-icon vr-icon-upload"></i> 120 <span>Export GPX</span> 121 </a><a href='javascript:;' class='tippy advanced hide' id='import-gpx' title="Importer un ou plusieurs tracé(s) au format GPX"> 122 <i class="vr-icon vr-icon-download"></i> 123 <span>Import GPX</span> 124 </a><a href='javascript:;' class='tippy advanced hide' title="Activez cette option pour automatiquement centrer la carte sur le dernier point tracé" data-toggle-text="Désactivez cette option pour ne plus automatiquement centrer la carte sur le dernier point tracé" id="center-last-point"> 125 <i class="vr-icon vr-icon-trace-center-point"></i> 126 <span>Centrage automatique</span> 127 </a><a href='javascript:;' title="Continuer un tracé" class='tippy not-advanced' id="continue"> 128 <i class="vr-icon vr-icon-edit"></i> 129 <span>Continuer</span> 130 </a><a href='javascript:;' class='tippy' title="Prendre le même itinéraire pour tracer le retour d'un ou plusieurs tracé(s) sélectionné(s)" id="draw-back-path"> 131 <i class="vr-icon vr-icon-trace-loop-end"></i> 132 <span>Rebrousser chemin</span> 133 </a><a href='javascript:;' class='tippy' id="reset" title="Supprimer tous les points et tracés"> 134 <i class="vr-icon vr-icon-trash-suppr"></i> 135 <span>Tout supprimer</span> 136 </a><a href='javascript:;' id="delete" class='tippy advanced hide' title="Supprimer la sélection"> 137 <i class="vr-icon vr-icon-close"></i> 138 <span>Supprimer la sélection</span> 139 </a><a href='javascript:;' class='tippy advanced hide' title="Supprimer le dernier point d'un ou plusieurs tracé(s) sélectionné(s)" id="delete-last-point"> 140 <i class="vr-icon vr-icon-trace-delete-point"></i> 141 <span>Supprimer le dernier point</span> 142 </a><a href='javascript:;' id="cut" class='tippy advanced hide' title="Couper un tracé en deux"> 143 <i class="vr-icon vr-icon-cut"></i>
144 <span>Couper</span> 145 </a><a href='javascript:;' class='tippy advanced hide' title="Fusionner plusieurs tracés" id="merge"> 146 <i class="vr-icon vr-icon-union"></i> 147 <span>Fusionner</span> 148 </a><a href='javascript:;' class='tippy advanced hide' title="Tracer le retour d'un ou plusieurs tracé(s) sélectionné(s)" id="draw-close-path"> 149 <i class="vr-icon vr-icon-trace-distance-line"></i> 150 <span>Tracer le retour</span> 151 </a><a href='javascript:;' class='tippy advanced hide' title="Inverser le sens d'un ou de plusieurs tracé(s) sélectionné(s)" id="reverse-direction"> 152 <i class="vr-icon vr-icon-arrow-switch-horizontal"></i> 153 <span>Inverser le sens</span> 154 </a><a href='javascript:;' class='tippy advanced hide' title="Mesurer une distance entre des points" id="measure-rule"> 155 <i class="vr-icon vr-icon-measure-distance"></i> 156 <span>Distance en ligne droite</span> 157 </a><a href='javascript:;' class='tippy advanced hide' title="Afficher les coordonnées du point sélectionné, dans le format choisi dans le menu "Réglages des cartes et du logiciel" de votre compte" id="point-coordinates"> 158 <i class="vr-icon vr-icon-geoloc-enabled"></i> 159 <span>Coordonnées du point</span> 160 </a><a href='javascript:;' class='tippy advanced hide' title="Royaume-Unis uniquement : obtenir les coordonnées OS grid du point sélectionné" id="os-grid-ref"> 161 <i class="vr-icon vr-icon-geoloc-enabled"></i> 162 <span>OS grid (UK)</span> 163 </a><a href='javascript:;' class='tippy advanced hide' title="Choisir un nouveau point de départ" id="move-start"> 164 <i class="vr-icon vr-icon-place"></i> 165 <span>Modifier départ</span> 166 </a><a href='javascript:;' class='tippy active advanced hide' data-toggle-text="Départs affichés" data-toggle-text-tooltip="Activez cette option pour afficher les repères de départ et arrivée" title="Désactivez cette option pour cacher les repères de départ et arrivée" id="display-start"> 167 <i class="vr-icon vr-icon-eye-closed"></i> 168 <span>Départs affichés</span> 169 </a><!--<a href='javascript:;' class='tippy advanced' data-toggle-text="" title="" id="tape-measure"> 170 <i class="vr-icon vr-icon-trace-distance"></i> 171 <span></span> 172 </a>--><a href='javascript:;' class='tippy' id='help' data-toggle-text="Désactivez cette option pour cacher l'aide contextuelle" title="Activez cette option pour afficher l'aide contextuelle"> 173 <i class="vr-icon vr-icon-help-center"></i> 174 <span>Aide contextuelle</span> 175 </a><a href='javascript:;' class='tippy advanced active hide' data-toggle-text="Textes affichés" data-toggle-text-tooltip="Activez cette option pour afficher le texte des boutons" title="Désactivez cette option pour cacher le texte des boutons" id="hide-txt-button"> 176 <i class="vr-icon vr-icon-text"></i> 177 <span>Textes affichés</span> 178 </a></a><a href='javascript:;' id='switch-version' data-toggle-text-tooltip="Désactivez cette option pour accéder au logiciel simple (non premium)" data-toggle-text="Logiciel avancé" class='tippy' title="Activez cette option pour accéder au logiciel avancé (premium)"> 179 <i class="vr-icon vr-icon-crown"></i> 180 <span>Logiciel avancé</span> 181 </a> 182 </div> 183 </div> 184 <div class="mouse-coordinates-indicator mod" > 185 n.c 186 </div> 187 </div> 188 189 <div class="responsive-toolbar auto-trace-toolbar-enabled" id="responsive-toolbar"> 190 191 <a href="javascript:;" class="btn btn--ghost btn--square drag-handle" title="Déplacer la barre d'outils"> 192 <i class="vr-icon vr-icon-grab-dots-vertical"></i>
193 <span class="sr-only">Déplacer la barre d'outils</span> 194 </a> 195 196 <div class="toolbar-separator"></div> 197 198 199<div class="form-switch"> 200 <div class="form-switch--input"> 201 <input type="hidden" name="auto-trace-toggle" value="0"/> 202 <input value="1" type="checkbox" autocomplete="off" id="auto-trace-toggle" name="auto-trace-toggle" checked > 203 </div> 204 <div class="form-switch--label"> 205 <label for="auto-trace-toggle"> 206 Tracé automatique </label> 207 </div> 208</div> 209 210 <div class="toolbar-separator"></div> 211 212 <a href="javascript:;" class="btn btn--ghost btn--square tippy" id="center" title="Centrer la carte sur le(s) tracé(s)"> 213 <i class="vr-icon vr-icon-trace-fit-screen"></i> 214 <span class="sr-only">Centrer la carte sur le(s) tracé(s)</span> 215 </a> 216 217 <div class="dropdown drop--right" id="color-picker-dropdown"> 218 <a class="dropdown-toggle btn btn--ghost btn--square tippy" title="Couleur du tracé" data-toggle="dropdown" href="javascript:;"> 219 <div class="color-circle" style="background-color:#FF0000"></div> 220 </a> 221 <ul class="dropdown-menu"> 222 <li class="active"> 223 <a href="javascript:;" class="dropdown-item color-item" data-color="#FF0000"> 224 <span class="color-name">Rouge</span> 225 <div class="color-circle" style="background-color:#FF0000"></div> 226 </a> 227 </li> 228 <li > 229 <a href="javascript:;" class="dropdown-item color-item" data-color="#FF9900"> 230 <span class="color-name">Orange</span> 231 <div class="color-circle" style="background-color:#FF9900"></div> 232 </a> 233 </li> 234 <li > 235 <a href="javascript:;" class="dropdown-item color-item" data-color="#FFFF00"> 236 <span class="color-name">Jaune</span> 237 <div class="color-circle" style="background-color:#FFFF00"></div> 238 </a> 239 </li> 240 <li > 241 <a href="javascript:;" class="dropdown-item color-item" data-color="#00FF00"> 242 <span class="color-name">Vert</span> 243 <div class="color-circle" style="background-color:#00FF00"></div> 244 </a> 245 </li> 246 <li > 247 <a href="javascript:;" class="dropdown-item color-item" data-color="#0000FF"> 248 <span class="color-name">Bleu</span> 249 <div class="color-circle" style="background-color:#0000FF"></div> 250 </a> 251 </li> 252 <li > 253 <a href="javascript:;" class="dropdown-item color-item" data-color="#9900FF"> 254 <span class="color-name">Violet</span> 255 <div class="color-circle" style="background-color:#9900FF"></div> 256 </a> 257 </li> 258 <li > 259 <a href="javascript:;" class="dropdown-item color-item" data-color="#FF00FF"> 260 <span class="color-name">Rose</span> 261 <div class="color-circle" style="background-color:#FF00FF"></div> 262 </a> 263 </li> 264 <li > 265 <a href="javascript:;" class="dropdown-item color-item" data-color="#646464"> 266 <span class="color-name">Gris</span> 267 <div class="color-circle" style="background-color:#646464"></div> 268 </a> 269 </li> 270 <li > 271 <a href="javascript:;" class="dropdown-item color-item" data-color="#000000">
272 <span class="color-name">Noir</span> 273 <div class="color-circle" style="background-color:#000000"></div> 274 </a> 275 </li> 276 </ul> 277 </div> 278 279 <div class="toolbar-separator"></div> 280 281 <a href="javascript:;" class="btn btn--ghost btn--square btn--negative tippy" id="reset" title="Tout supprimer"> 282 <i class="vr-icon vr-icon-trash-suppr"></i> 283 <span class="sr-only">Tout supprimer</span> 284 </a> 285 </div> 286 287<div class="cd-panel cd-panel--from-right js-cd-panel-main"> 288 <header class="cd-panel__header"> 289 <h1>Aide</h1> 290 <a href="#0" class="cd-panel__close js-cd-close" data-sw="help-panel-close"><span class="sr-only">Fermer</span></a> 291 </header> 292 <div class="cd-panel__container"> 293 <div class="cd-panel__content"> 294 <div class="xs:p-4"> 295 <h2>Guide des fonctionnalités</h2> 296 <div> 297 <a href="https://youtu.be/41xD8jgQPV8" target="_blank" rel="noopener noreferrer" class="btn btn-secondary w-full">Vidéo de présentation<i class="vr-icon vr-icon-media-video"></i></a> 298 </div> 299 <br/> 300 <div class="cd-panel__legend"> 301 <div class="flex align-center"> 302 <div class="icon"><i class="vr-icon vr-icon-trace-vector"></i></div> 303 <div class="explanations">Activer/désactiver le mode traçage pour créer ou terminer votre itinéraire. 304 </div> 305 </div> 306 <div class="flex align-center"> 307 <div class="icon"><i class="vr-icon vr-icon-edit"></i></div> 308 <div class="explanations">Continuer le tracé sélectionné.</div> 309 </div> 310 <div class="flex align-center"> 311 <div class="icon"><i class="vr-icon vr-icon-search"></i></div> 312 <div class="explanations">Centrer la carte sur le lieu de départ/arrivée souhaité de votre randonnée. 313 </div> 314 </div> 315 <div class="flex align-center"> 316 <div class="icon"><i class="vr-icon vr-icon-save"></i></div> 317 <div class="explanations">Enregistrer les(s) tracé(s) dans "Mon compte > Mes Randonnées".</div> 318 </div> 319 <div class="flex align-center"> 320 <div class="icon"><i class="vr-icon vr-icon-print"></i></div> 321 <div class="explanations">Sauvegarder et exporter votre tracé dans une fiche PDF.</div> 322 </div> 323 <div class="flex align-center"> 324 <div class="icon"><i class="vr-icon vr-icon-download"></i></div> 325 <div class="explanations">Importer des traces avec des fichiers GPX.</div> 326 </div> 327 <div class="flex align-center"> 328 <div class="icon"><i class="vr-icon vr-icon-upload"></i></div> 329 <div class="explanations">Exporter les tracés sélectionnées en fichier GPX. Si la sélection est vide, tous les tracés valides sont exportés. 330 </div> 331 </div> 332 <div class="flex align-center"> 333 <div class="icon"><i class="vr-icon vr-icon-trace-fit-screen"></i></div> 334 <div class="explanations">Recentrer la carte sur vos/votre tracé(s).</div> 335 </div> 336 <div class="flex align-center"> 337 <div class="icon"><i class="vr-icon vr-icon-trace-loop"></i></div> 338 <div class="explanations">Fermer votre tracé en repassant par tous les points existants dans le sens inverse. 339 </div> 340 </div> 341 <div class="flex align-center"> 342 <div class="icon"><i class="vr-icon vr-icon-trash-suppr"></i></div> 343 <div class="explanations">Supprimer tous les tracés existants sur la carte.</div> 344 </div> 345 <div class="flex align-center"> 346 <div class="icon"><i class="vr-icon vr-icon-close"></i></div> 347 <div class="explanations">Supprimer les points de votre sélection.</div> 348 </div> 349 <div class="flex align-center"> 350 <div class="icon"><i class="vr-icon vr-icon-trace-delete-point"></i></div> 351 <div class="explanations">Supprimer le dernier point de chacun des tracés sélectionnés (cette action est également valable en cours de traçage). 352 </div> 353 </div> 354 <div class="flex align-center"> 355 <div class="icon"><i class="vr-icon vr-icon-cut"></i></div> 356 <div class="explanations">Scinder le tracé sélectionné en deux tracés au niveau du point sélectionné. 357 </div> 358 </div> 359 <div class="flex align-center"> 360 <div class="icon"><i class="vr-icon vr-icon-union"></i></div> 361 <div class="explanations">Fusionner les tracés sélectionnés (l'ordre dans lequel vous sélectionnez vos tracés influe la fusion). 362 </div> 363 </div> 364 <div class="flex align-center"> 365 <div class="icon"><i class="vr-icon vr-icon-trace-distance-line"></i></div> 366 <div class="explanations">Fermer le tracé sélectionné en ajoutant un point aux mêmes coordonnées que celui du départ. 367 </div> 368 </div> 369 <div class="flex align-center"> 370 <div class="icon"><i class="vr-icon vr-icon-arrow-switch-horizontal"></i></div> 371 <div class="explanations">Inverser le sens du tracé.</div> 372 </div> 373 <div class="flex align-center"> 374 <div class="icon"><i class="vr-icon vr-icon-eye-closed"></i></div> 375 <div class="explanations">Afficher/cacher les points de départ de tous les tracés.</div> 376 </div> 377 <div class="flex align-center"> 378 <div class="icon"><i class="vr-icon vr-icon-measure-distance"></i></div> 379 <div class="explanations">Mesurer la distance à vol d'oiseau entre au moins deux points.</div> 380 </div> 381 <div class="flex align-center"> 382 <div class="icon"><i class="vr-icon vr-icon-geoloc-enabled"></i></div> 383 <div class="explanations">Obtenir les coordonnées du point sélectionné (selon le format sélectionné dans votre profil : Mon compte > Réglages du Logiciel > Format des coordonnées géographiques). 384 </div> 385 </div> 386 <div class="flex align-center"> 387 <div class="icon" style="font-size:23px"><i class="vr-icon vr-icon-text"></i></div> 388 <div class="explanations">Afficher/cacher les textes des icônes de la barre des fonctionnalités en haut de l'écran. 389 </div> 390 </div> 391 <div class="flex align-center"> 392 <div class="icon svg"><img src="/templatesR/visorando/images/software/click-move.svg"/></div> 393 <div class="explanations">Se déplacer sur la carte.</div> 394 </div> 395 <div class="flex align-center"> 396 <div class="icon svg"><img src="/templatesR/visorando/images/software/mouse-wheel.svg"/></div> 397 <div class="explanations">Zoomer/Dézoomer la carte.</div> 398 </div> 399 <div class="flex align-center"> 400 <div class="icon svg"><img src="/templatesR/visorando/images/software/click-twice.svg"/></div> 401 <div class="explanations">Zoomer sur la carte.</div> 402 </div> 403 <div class="flex align-center"> 404 <div class="icon svg"><img src="/templatesR/visorando/images/software/enter-key.svg"/></div> 405 <div class="explanations">Terminer votre tracé lorsque vous êtes en mode traçage.</div> 406 </div> 407 <div class="flex align-center"> 408 <div class="icon svg biggest"><img src="/templatesR/visorando/images/software/shift-click.svg"/></div> 409 <div class="explanations">Sélectionner plusieurs points ou tracés. 410 </div> 411 </div> 412 <div class="flex align-center"> 413 <div class="icon svg biggest"><img src="/templatesR/visorando/images/software/shift-click-move.svg"/> 414 </div> 415 <div class="explanations">Créér une zone de sélection de points ou tracés.</div> 416 </div> 417 <div class="flex align-center"> 418 <div class="icon svg biggest"><img src="/templatesR/visorando/images/software/ctrl-z.svg"/></div> 419 <div class="explanations">Annuler la dernière action.</div> 420 </div> 421 <div class="flex align-center"> 422 <div class="icon svg biggest"><img src="/templatesR/visorando/images/software/ctrl-shift-z.svg"/></div> 423 <div class="explanations">Restaurer la dernière action.</div> 424 </div> 425 </div> 426 </div> 427 </div> 428 </div> 429</div> 430 <div class="bottom-right-bar"> 431 432 433 <div class="mapboxgl-ctrl mapboxgl-ctrl-group btn--contribution"> 434 <a id="goToDonationIcon" target="_blank" data-tippy-content="Contribuer à Visorando" href="https://www.visorando.com/coup-de-pouce.html"><i class="vr-icon vr-icon-heart-like"></i><span class="xs:ml-4">Contribuer</span></a> 435 </div> 436 437 <a href="javascript:void(0);" class="switch-handler" ><i class="vr-icon vr-icon-chevron-s-down" ></i></a> 438 <a href="javascript:void(0);
438" title="Lien vers la fiche du tracé (dernière version enregistrée)" class="hike-link tippy" ><i class="vr-icon vr-icon-link-external"></i></a> 439 <div class='tabs'> 440 <a href='javascript:;' class='tab active'><i class='vr-icon vr-icon-information-sign'></i> <span>Informations</span></a> 441 <a href='javascript:;' class='tab'><i class='vr-icon vr-icon-graph-line'></i> <span>Diagramme</span></a> 442 </div> 443 <div class="swiper-container swiper-tabs"> 444 <div class='swiper-wrapper'> 445 <div class='swiper-slide infos'> 446 <div class="swiper-container swiper-infos"> 447 <div class='swiper-wrapper'> 448 <div class='swiper-slide'> 449 <div class='duration'> 450 <img src="https://www.visorando.com/img/fiches/datasheet/duration.svg" alt="â" title="Durée" height="25" width="25" role="presentation" /> Durée <span>0</span> 451 </div> 452 <div class='distance'> 453 <img src="https://www.visorando.com/img/fiches/datasheet/distance.svg" alt="â" title="Distance" height="25" width="25" role="presentation" /> Distance <span>0</span> km </div> 454 <div class='pos-elevation'> 455 <img src="https://www.visorando.com/img/fiches/datasheet/elevation-up.svg" alt="â" title="Dénivelé positif" height="25" width="25" role="presentation" /> Dénivelé positif <span>0</span> m </div> 456 <div class='neg-elevation'> 457 <img src="https://www.visorando.com/img/fiches/datasheet/elevation-down.svg" alt="â" title="Dénivelé négatif" height="25" width="25" role="presentation" /> Dénivelé négatif <span>0</span> m </div> 458 <div class='high-point'> 459 <img src="https://www.visorando.com/img/fiches/datasheet/point-high.svg" alt="â²" title="Point haut" height="25" width="25" role="presentation" /> Point haut <span>0</span> m </div> 460 <div class='low-point'> 461 <img src="https://www.visorando.com/img/fiches/datasheet/point-low.svg" alt="â¼" title="Point bas" height="25" width="25" role="presentation" /> Point bas <span>0</span> m </div> 462 <div class='count-point'> 463 <img src="https://www.visorando.com/img/fiches/datasheet/points.svg" alt="â¢â¢â¢" title="Nombre de points" height="25" width="25" role="presentation" /> Nombre de points <span>0</span> 464 </div> 465 </div> 466 </div> 467 <div class="swiper-pagination"></div> 468 </div> 469 </div> 470 <div class='swiper-slide diagrams'> 471 <div class="swiper-container swiper-diagrams"> 472 <div class='swiper-wrapper'> 473 <div class='swiper-slide'> 474 <div id='placeholder-diagram' >Le diagramme de dénivelé se mettra automatiquement à jour lorsque vous aurez créé votre itinéraire.</div> 475 </div> 476 </div> 477 <div class="swiper-pagination"></div> 478 </div> 479 </div> 480 </div> 481 </div> 482 </div> 483 484<div id='boot-el-software' class='map-container loading' > 485 <div class="go-to" > 486 <form method="GET" class="form-inline"> 487 <label for="loc" class="sr-only">Rechercher </label> 488 <div class="inputs-group"> 489 <input id="loc" type="search" class="ui-autocomplete-input input" aria-autocomplete="list" aria-expanded="false" spellcheck="false" autocorrect="off" name="loc" autocomplete="off" placeholder="Lieu, sommet, régionâ¦"> 490 <div class="btns-group"> 491 <button type="submit" class="btn btn--square sr-only"><i class="vr-icon vr-icon-search"></i> Rechercher</button> 492 <button title="Ma position" type="button" class="btn btn--square btn-secondary btn-geoloc tippy"><i class="vr-icon vr-icon-geoloc-disabled"></i><span class="sr-only">Utiliser ma localisation</span></button> 493 </div> 494 </div> 495 <input type="hidden" id='meta-data' name="metaData" value='' /> 496 </form> 497 </div> 498 499 <div class='loader viso-loader'> 500 <div class="vr-loader" role="presentation"></div>
501 <span class="text-white">Chargementâ¦</span> 502 </div> 503 504 505</div> 506 507<div id="not-logged-popup" style="display:none;width:925px;max-width:96%" class="radius--lg xs:p-0 bg-vr-brown-1"> 508 <div class='container container--fluid'> 509 <div class="row no-gutters"> 510 <div class='col xs:12 sm:4 xs:p-4 sm:p-8 bg-neutral-0'> 511 <div class="text-center"> 512 <a class='inbl' href="https://www.visorando.com/"><img src="https://www.visorando.com/templatesR/visorando/images/logo_visorando.png" width="150" class="xs:my-4 sm:my-8" alt="Visorando" title="Visorando"/></a> 513 </div> 514 <form action="" method="post" id="quick-entry" role="form" class="xs:spacey-4"> 515 <div id="div_email"> 516 <label for="email">Adresse email</label> 517 <input type="email" name="email" id="email" class="input"> 518 </div> 519 <div id="div_password"> 520 <label for="password">Mot de passe</label> 521 <div class="input-password"> 522 <input type="password" name="password" id="password" class="input"> 523 <button type="button" class="input-password--toggle"> 524 <i class="vr-icon vr-icon-eye-open"></i> 525 </button> 526 </div> 527 <div id="progress_password" style="display: none"></div> 528 </div> 529 <div id="div_type"> 530 <label for="type">Type de compte</label> 531 <select id="type" name="type" class="input"> 532 <option value="0">Visorandonneur</option> 533 <option value="2">Association / Club / AMM</option> 534 <option value="3">Office de tourisme</option> 535 <option value="7">Collectivité territoriale</option> 536 <option value="10">Parc Naturel Régional</option> 537 <option value="9">Parc National</option> 538 <option value="11">Professionnel du tourisme</option> 539 <option value="13">Structure d'enseignement</option> 540 </select> 541 </div> 542 <input type="hidden" name="regJS" id="regJS" value="vtreg02"> 543 <input type="hidden" name="from" id="from" value="null"> 544 <input type="hidden" name="idRandonnee" id="idRandonnee" value="0"> 545 <input type="hidden" name="idPDFRU" id="idPDFRU" value="0"> 546 <input type="hidden" name="source" id="source" value="Formulaire inscription_logiciel"> 547 <input type="hidden" name="urlRedirect" id="urlRedirect" value=""> 548 <input type="hidden" name="deviceLocale" class="js-device-locale" value=""/>
549<script> 550 (function () { 551 var inputs = document.getElementsByClassName('js-device-locale'); 552 for (var i = 0; i < inputs.length; i++) { 553 inputs[i].value = navigator.language || ''; 554 } 555 })(); 556</script>
556 557 <button id="btnSubmit" class="btn btn--success w-full" type="submit">Je m'inscris</button> 558 <p>Si vous avez déjà un compte, <a href="https://www.visorando.com/index.php?component=user&task=loginForm&urlRedirect=https%3A%2F%2Fwww.visorando.com%2Flogiciel-randonnee.html">cliquez ici pour vous connecter</a></p> 559 </form> 560 </div> 561 <div class='col xs:12 sm:8 disp-fl xs:flex-middle xs:p-6 xs:pb-12 sm:p-16'> 562 <div> 563 <h1 class="jumbo-4 xs:mb-4 sm:mb-8">Testez le logiciel de randonnée <strong class="text-vr-brown-5">GRATUIT</strong></h1> 564 <ul class="list-unstyled sm:text--lg xs:spacey-2 sm:spacey-4 md:spacey-6 lh--sm"> 565 <li style="display:flex;gap:10px;align-items:center;"><i class="vr-icon vr-icon-valid"></i> Simple, intuitif et convivial, tracer vos randonnées devient un jeu d'enfant</li> 566 <li style="display:flex;gap:10px;align-items:center;"><i class="vr-icon vr-icon-valid"></i> Logiciel disponible sur l'application mobile</li> 567 <li style="display:flex;gap:10px;align-items:center;"><i class="vr-icon vr-icon-valid"></i> Tracez toutes vos envies sur le fond de carte de votre choix (Topo & Satellite IGN, OSM, Swisstopo, IGN Espagne + Belgique)</li> 568 <li style="display:flex;gap:10px;align-items:center;"><i class="vr-icon vr-icon-valid"></i> Exportez, imprimez et partagez en un clic vos tracés avec vos amis !</li> 569 <li style="display:flex;gap:10px;align-items:center;"><i class="vr-icon vr-icon-valid"></i> Retrouvez tous vos tracés sur votre compte Visorando</li> 570 </ul> 571 </div> 572 </div> 573 </div> 574 </div> 575
575<script src="https://www.visorando.com/js.js?v=1790147326" type="application/javascript"></script>
575 576
576<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js" type="application/javascript"></script>
576 577
577<script type="application/javascript" src="https://www.visorando.com/libs/js/highcharts/highcharts.stock.min.js?v=1790147288"></script>
577 578
578<script type="application/javascript" src="https://www.visorando.com/libs/js/visorando-map.umd.js?v=1790147288"></script>
578 579
579<script type="application/javascript" src="https://www.visorando.com/software.js?v=1790147326"></script>
579 580
580<script type="application/javascript" src="https://www.visorando.com/libs/js/mapbox-gl-draw.js?v1.3"></script>
580 581
581<script type="application/javascript" src="https://www.visorando.com/libs/js/jquery/jquery.validate.fr.js"></script>
581 582
582<script type="application/javascript" src="https://www.visorando.com/libs/js/mapbox-polyline/1.2.1/polyline.min.js"></script>
582 583
583<script type="application/javascript" src="https://www.visorando.com/libs/js/sprintf/1.1.2/sprintf.min.js"></script>
583 584 <link rel="preload" href="https://www.visorando.com/libs/js/i18n.js?v=1790147288" as="script"> 585
585<script src="https://www.visorando.com/libs/js/i18n.js?v=1790147288"></script>
585 586
586<script type="application/javascript"> 587 var baseUrl = 'https://www.visorando.com/'; 588 589 $(document).ready(function () { 590 591(function () { 592 const i18nJsonFile = "https:\/\/www.visorando.com\/i18n\/fr\/web-software.json?v=1790241218"; 593 594 if (!i18nJsonFile) return; 595 596 $.ajax(i18nJsonFile) 597 .done(function (text) { 598 try { 599 const data = typeof text === "string" ? JSON.parse(text) : text; 600 i18n.translator.add({values: data}); 601 } catch (e) { 602 console.error("i18n parsing error:", e); 603 } 604 }) 605 .fail(function (jqXHR, textStatus, errorThrown) { 606 console.error("i18n loading error:", { 607 status: jqXHR.status, 608 textStatus: textStatus, 609 errorThrown: errorThrown 610 }); 611 }); 612})(); 613 614// UMD 615(function (root, factory) { 616if (typeof define === "function" && define.amd) { 617 define(["jquery"], factory); 618} else if (typeof exports === "object") { 619 module.exports = factory(require("jquery")); 620} else { 621 root.VISORANDO_SOFTWARE = factory(root.$); 622} 623}(window, function ($) { 624 625let _module = (function(){ 626 let settings = { 627 mapEl: $('#boot-el-software'), // Element DOM qui va accueilir la map 628 groupActions: $('.group-actions'), 629 topBar: $('.top-bar'), 630 bottomRightBar: $('.bottom-right-bar'), 631 searchLocForm: $('.go-to > form'), 632 hikeUrlEl : $('.hike-link'), 633 rasterOpacityEl : $('#raster-opacity-handler'), 634 isAdvanced: false, 635 baseUrl : 'https://www.visorando.com/', 636 departureThreshold : 100, 637 hashCode : '52b51633f87aeb835332e1e883849420', 638 clubUrl : 'https://www.visorando.com/premium.html', 639 isCompatible: true, 640 coordinateFormat : 0, 641 idRando : 0, 642 titreRando : null, 643 idRandoHash : '0', 644 carteType : 'IGNTOPO', 645 colorPalette: ["#FF9900","#00FF00","#0000FF","#9900FF","#FF00FF","#646464","#000000","#d49867","#513027","#850d24","#007e77","#03d9ff","#008efc"], 646 hexColor : '#FF0000', 647 softwareOpts: false, 648 deviceType: 'desktop', 649 maxDistanceForAutoPlot: 200000, 650 heightDifferenceLimit: 10, 651 bounds: [ 652 [-5.152361, 41.285270], 653 [9.736311, 51.158016] 654 ], 655 currentUnits: { 656 altitude: 'm', 657 distance: 'km', 658 }, 659 localStorageExpiration : 1 * 24 * 60 * 60 * 1000, // 1 jour 660 elevationLayer : {"source":{"type":"raster","tiles":["https:\/\/tiles.visomap.com\/wmts\/slopes\/mercator\/{z}\/{x}\/{y}.png"],"tileSize":256},"layer":{"id":"elevation-layer","type":"raster","source":"elevation-layer","minzoom":9,"maxzoom":16},"name":"ALTIMETRIEMOUNTAIN"}, 661 user: { 662 uGid: 0, 663 hasPremiumAccess : false, 664 hasIGNAccess : false, 665 showPub : true, 666 isLoggedIn: false }, 667 }, 668 booted = false, 669 tippySingleton = null; 670 671 let _this = { 672 mapGl: null, // Instance MapLibre GL brute (via mapController.getMapInstance()) 673 mapController: null, // Instance MapController de VisorandoMap 674 drawGl: null, // Drawer GL instance 675 cookieHandler: null, // Cookies instance 676 swiper: { // Swiper instances 677 tabs: null, 678 infos: null, 679 layers: null, 680 diagrams: null 681 }, 682 helperTootlip: null, // Tippy instance (infobulle d'aide) 683 asyncTasks: [], // Tableau pour gérer tout type de traitement asynchrone 684 store: {}, // Objet pour gérer les points de chaque tracés 685 userPoints: {}, // Les points d'ancrage (clics utilisateur) 686 autoTracingEnabled: false, // Indique si le tracé automatique est activé 687 autoTraceOnGoing: false, // Flag pour indiquer si un traçage automatique est en cours 688 descriptionWaypointOriginalCount: 0, 689 vertexDeleteOnGoing: false, // Prevents a second Delete key press from targeting the whole selected trace. 690 blockingOperationCount: 0, // Prevents user inputs from interleaving with async path mutations. 691 blockingCursorStyle: null, 692 693 commonConfirmOpts: { 694 zindex: 500, 695 titleSize:'18px', 696 color: 'white', 697 closeOnClick: false, 698 overlayClose: true, 699 progressBar: false, 700 drag: false, 701 timeout: false, 702 close: false, 703 overlay: true, 704 position: 'center' 705 }, 706 707 getMap: function() { 708 return _this.mapGl; 709 }, 710 setInitialDescriptionWaypointNumbers: function () { 711 if (!settings.idRando) return; 712 713 const pathId = _this.path.getAll({saved: true})[0]; 714 const points = pathId ? _this.store[pathId] : null; 715 if (!Array.isArray(points)) return; 716 717 let waypointNumber = 0; 718 let originalCount = 0; 719 720 // The planner does not load structured-description content. It only tags existing intermediate 721 // waypoints with their initial description block number so missing numbers can be sent on save.
722 points.forEach(function (point, index) { 723 if (index === 0 || index === points.length - 1 || +point.wpt !== 1) return; 724 725 waypointNumber++; 726 if (!point.descriptionWaypointInitialNumber) { 727 point.descriptionWaypointInitialNumber = waypointNumber; 728 } 729 originalCount = Math.max(originalCount, +point.descriptionWaypointInitialNumber || 0); 730 }); 731 732 _this.descriptionWaypointOriginalCount = Math.max(_this.descriptionWaypointOriginalCount, originalCount); 733 }, 734 getDeletedDescriptionWaypointIndexes: function () { 735 if (!settings.idRando || !_this.descriptionWaypointOriginalCount) return []; 736 737 const pathId = _this.path.getAll({saved: true})[0]; 738 const points = pathId ? _this.store[pathId] : null; 739 if (!Array.isArray(points)) return []; 740 741 const remaining = {}; 742 points.forEach(function (point, index) { 743 if (index !== 0 && index !== points.length - 1 && +point.wpt === 1 && point.descriptionWaypointInitialNumber) { 744 remaining[String(point.descriptionWaypointInitialNumber)] = true; 745 } 746 }); 747 748 const deletedIndexes = []; 749 // Undo is handled naturally: if a tagged waypoint exists again at save time, its block is not deleted. 750 for (let index = 1; index <= _this.descriptionWaypointOriginalCount; index++) { 751 if (!remaining[String(index)]) { 752 deletedIndexes.push(index); 753 } 754 } 755 756 return deletedIndexes; 757 }, 758 rebaseDescriptionWaypointNumbers: function () { 759 if (!settings.idRando) return; 760 761 const pathId = _this.path.getAll({saved: true})[0]; 762 const points = pathId ? _this.store[pathId] : null; 763 if (!Array.isArray(points)) return; 764 765 let waypointNumber = 0; 766 // After a successful save, backend blocks are renumbered. Rebase client tags to that new canonical order. 767 points.forEach(function (point, index) { 768 if (index === 0 || index === points.length - 1 || +point.wpt !== 1) { 769 delete point.descriptionWaypointInitialNumber; 770 return; 771 } 772 773 waypointNumber++; 774 point.descriptionWaypointInitialNumber = waypointNumber; 775 }); 776 777 _this.descriptionWaypointOriginalCount = waypointNumber; 778 }, 779 isBlockingOperationOnGoing: function() { 780 return this.blockingOperationCount > 0; 781 }, 782 beginBlockingOperation: function() { 783 this.blockingOperationCount++; 784 if (this.blockingOperationCount !== 1 || !this.mapGl) return; 785 786 const canvas = this.mapGl.getCanvas(); 787 const container = this.mapGl.getCanvasContainer(); 788 this.blockingCursorStyle = canvas.style.cursor; 789 canvas.style.cursor = 'wait'; 790 $(container).addClass('software-wait-cursor'); 791 }, 792 endBlockingOperation: function() { 793 if (this.blockingOperationCount === 0) return; 794 795 this.blockingOperationCount--; 796 if (this.blockingOperationCount !== 0 || !this.mapGl) return; 797 798 const canvas = this.mapGl.getCanvas(); 799 const container = this.mapGl.getCanvasContainer(); 800 canvas.style.cursor = this.blockingCursorStyle || ''; 801 $(container).removeClass('software-wait-cursor'); 802 this.blockingCursorStyle = null; 803 }, 804 runBlockingOperation: async function(callback) { 805 this.beginBlockingOperation(); 806 try { 807 return await callback(); 808 } finally { 809 this.endBlockingOperation(); 810 } 811 }, 812 setupBlockingInputGuard: function() { 813 const blockWhenBusy = function(e) { 814 if (!_this.isBlockingOperationOnGoing()) return; 815 816 e.preventDefault(); 817 e.stopImmediatePropagation(); 818 }; 819 820 const container = this.mapGl.getCanvasContainer(); 821 ['pointerdown', 'mousedown', 'dblclick', 'touchstart'].forEach(function(eventName) { 822 container.addEventListener(eventName, blockWhenBusy, true); 823 }); 824 825 document.addEventListener('keydown', blockWhenBusy, true); 826 }, 827 /** 828 * Auto-trace un segment de chemin entre deux points. 829 * @param {string} idPath - L'identifiant du tracé 830 * @param startIndex 831 * @param endIndex 832 * @param viaIndex 833 * @param preserveStart 834 * @param preserveEnd 835 * @param {Object|null} options.swiperInstance - Instance Swiper (optionnelle) 836 * @param {Object|null} options.swiperTabs - Instance Swiper Tabs (optionnelle) 837 */ 838 autoTraceSegment: async function (idPath, startIndex, endIndex, viaIndex = null, preserveStart = false, preserveEnd = false) { 839 const originalCursorStyle = _this.mapGl.getCanvas().style.cursor; 840 const routingMode = getSelectedMode(); 841 842 try { 843 const feature = _this.drawGl.get(idPath); 844 if (!feature) { 845 throw new Error(sprintf( 846 i18n("error_trace_not_found"), 847 idPath 848 )); 849 } 850 851 const points = _this.store[idPath]; 852 if (!points || points.length < 2) throw new Error(i18n("error_autotrace_too_few_points")); 853 854 const startPoint = [points[startIndex].lng, points[startIndex].lat]; 855 const endPoint = [points[endIndex].lng, points[endIndex].lat]; 856 857 858 let locations = [{ lat: startPoint[1], lon: startPoint[0] }]; 859 860 let fallbackLegs = []; 861 if (viaIndex != null) { 862 const viaPoint = [points[viaIndex].lng, points[viaIndex].lat]; 863 locations.push({ lat: viaPoint[1], lon: viaPoint[0] }); 864 fallbackLegs.push([startPoint, viaPoint], [viaPoint, endPoint]); 865 } else { 866 fallbackLegs.push([startPoint, endPoint]); 867 } 868 locations.push({ lat: endPoint[1], lon: endPoint[0]}); 869 870 let segmentResult; 871 let msg = ''; 872 let error = false;
873 try { 874 const distance = calculateDistanceApprox(startPoint, endPoint)*1000; 875 if (distance > settings.maxDistanceForAutoPlot) { 876 error = true; 877 msg += i18n("autotrace_error_generic"); 878 msg += "<br />" + i18n("autotrace_distance_too_long", Math.floor(settings.maxDistanceForAutoPlot / 1000)); 879 await setTimeout(() => {}, 50); //Hack pour éviter un comportement bizarre avec Mapbox de la fonction drawGl.set() qui ajoute un point de trop si pas d'attente 880 segmentResult = { 881 success: true, 882 legs: [fallbackLegs] 883 }; 884 } else { 885 const selectedLayer = 886 // settings.carteType est maintenant synchronisé depuis le panel VisorandoMap 887 settings.carteType || 'IGNTOPO'; 888 segmentResult = await this.callRoute(locations, routingMode, selectedLayer); 889 if (segmentResult.success && segmentResult.legs < 1) { 890 console.warn(`Valhalla a retourné un segment bizarre, fallback en ligne droite.`); 891 error = true; 892 msg += i18n("autotrace_error_fallback") + "<br />"; 893 segmentResult = { 894 success: true, 895 legs: [fallbackLegs] 896 } 897 } else if(!segmentResult.success) { 898 segmentResult = { 899 success: false, 900 legs: [fallbackLegs] 901 } 902 msg += i18n("autotrace_error_fallback") + "<br />"; 903 } else { 904 msg += i18n("autotrace_msg_success_done") + "<br />" 905 } 906 } 907 } catch (err) { 908 console.warn("Services de routage indisponibles, fallback en ligne droite", err); 909 await setTimeout(() => {}, 50); //Hack pour éviter un comportement bizarre avec Mapbox de la fonction drawGl.set() qui ajoute un point de trop si pas d'attente 910 segmentResult = { 911 success: true, 912 legs: [fallbackLegs] 913 }; 914 } 915 916 const legs = segmentResult.legs; 917 918 let basePointsBefore, basePointsAfter; 919 if (preserveStart) { 920 basePointsBefore = _this.store[idPath].slice(0, startIndex + 1); 921 } else { 922 basePointsBefore = _this.store[idPath].slice(0, startIndex); 923 } 924 if (preserveEnd) { 925 basePointsAfter = _this.store[idPath].slice(endIndex); 926 } else { 927 basePointsAfter = _this.store[idPath].slice(endIndex + 1); 928 } 929 930 let freshPoints = []; 931 legs.forEach((leg, key) => { 932 freshPoints = freshPoints.concat(leg.map(c => ({ 933 lng: +c[0].toFixed(6), 934 lat: +c[1].toFixed(6), 935 // Altitude éventuellement fournie par le serveur (3e composante du coord) 936 ...(typeof c[2] === 'number' ? { altitude: c[2] } : {}), 937 id: _this.path.generateUUIDv4(), 938 meta: 'auto_vertex' 939 }))); 940 if (key===0 && viaIndex!=null) { 941 freshPoints[freshPoints.length-1].__isVia = true; 942 freshPoints[freshPoints.length-1].wpt = points[viaIndex].wpt; 943 freshPoints[freshPoints.length-1].idEG = points[viaIndex].idEG; 944 freshPoints[freshPoints.length-1].textWP = points[viaIndex].textWP; 945 freshPoints[freshPoints.length-1].textIndication = points[viaIndex].textIndication; 946 freshPoints[freshPoints.length-1].type = points[viaIndex].type; 947 freshPoints[freshPoints.length-1].angle = points[viaIndex].angle; 948 freshPoints[freshPoints.length-1].descriptionWaypointInitialNumber = points[viaIndex].descriptionWaypointInitialNumber; 949 } 950 freshPoints[freshPoints.length - 1].anchor = 1; 951 }); 952 if (preserveStart) { 953 if (freshPoints[0].lat === basePointsBefore[basePointsBefore.length - 1].lat && 954 freshPoints[0].lng === basePointsBefore[basePointsBefore.length - 1].lng) { 955 freshPoints.shift(); 956 } 957 freshPoints[0].anchor = 0; 958 } else { 959 freshPoints[0].anchor = 1; 960 } 961 if (preserveEnd) { 962 if (freshPoints[freshPoints.length - 1].lat === basePointsAfter[0].lat && 963 freshPoints[freshPoints.length - 1].lng === basePointsAfter[0].lng) { 964 freshPoints.pop(); 965 } 966 freshPoints[freshPoints.length - 1].anchor = 0; 967 } else { 968 freshPoints[freshPoints.length - 1].anchor = 1; 969 } 970 971 _this.store[idPath] = basePointsBefore.concat(freshPoints).concat(basePointsAfter); 972 973 _this.path.updateDrawGlCoordsWithPath([idPath]) 974 975 const indexes = []; 976 for (let i = 0; i < freshPoints.length; i++) { 977 indexes.push(basePointsBefore.length + i); 978 } 979 // Si le serveur a déjà fourni toutes les altitudes (DP 3D), on évite l'appel MNT. 980 // Le fallback ligne droite n'a pas d'altitude â on interroge alors le MNT. 981 const needAltitude = freshPoints.some(p => typeof p.altitude !== 'number'); 982 if (needAltitude) { 983 try { 984 await _this.getAltitudeForPoints(idPath, indexes); 985 } catch (err) { 986 console.warn(`Ãchec enregistrement point :`, err); 987 } 988 } 989 _this.path.computeOrder(idPath); 990 _this.mapGl.triggerRepaint(); 991 992 _this.mapGl.fire('autotraceSegment', { idPath }); 993 994 if (error) { 995 iziToast.error({ 996 message: msg + i18n("autotrace_click_to_finish") 997 }); 998 } else { 999 iziToast.success({ 1000 message: msg + i18n("autotrace_click_to_finish") 1001 }); 1002 } 1003 return true; 1004 } catch (err) { 1005 console.error("Erreur critique autoTraceSegment :", err); 1006 return false;
1007 } finally { 1008 if (!_this.isBlockingOperationOnGoing()) { 1009 _this.mapGl.getCanvas().style.cursor = originalCursorStyle; 1010 } 1011 } 1012 }, 1013 1014 /** 1015 * Helper pour changer de mode sans risque 1016 */ 1017 safeChangeMode: function (mode, options = {}) { 1018 try { 1019 const draw = this.drawGl; 1020 1021 // 1. Protection contre LineString trop courts pour 'simple_select' 1022 if (mode === 'simple_select' && options.featureIds?.length === 1) { 1023 const id = options.featureIds[0]; 1024 const feature = draw?.get?.(id); 1025 1026 if (feature?.geometry?.type === 'LineString') { 1027 const coords = feature.geometry.coordinates; 1028 if (!Array.isArray(coords) || coords.length < 2) { 1029 console.warn("safeChangeMode: tentative de 'simple_select' sur un LineString trop jeune â annulé"); 1030 return; 1031 } 1032 } 1033 } 1034 1035 // 2. Vérifier l'existence du feature pour les modes draw_* 1036 if (mode.startsWith("draw") && options.featureId && !draw.get(options.featureId)) { 1037 console.warn("safeChangeMode bloqué : feature introuvable pour", options.featureId); 1038 return; 1039 } 1040 1041 draw.changeMode(mode, options); 1042 } catch (err) { 1043 console.error(`safeChangeMode: échec lors du changement de mode vers '${mode}'`, err); 1044 } 1045 }, 1046 /** 1047 * Méthode pour initialiser l'interface de traçage auto 1048 */ 1049 initAutoTraceControls: function() { 1050 // Initialiser le tooltip 1051 tippy('.auto-trace-container', { 1052 content: i18n("label_tooltip_autotrace"), 1053 placement: 'bottom' 1054 }); 1055 1056 // Attacher l'événement au changement de l'interrupteur 1057 $('#auto-trace-toggle').on('change', function(e) { 1058 _this.toggleAutoTracing(this); 1059 }); 1060 1061 // Activer par défaut 1062 _this.autoTracingEnabled = true; 1063 $('#auto-trace-toggle') 1064 .prop('checked', true) 1065 .closest('#responsive-toolbar') 1066 .removeClass('auto-trace-toolbar-disabled') 1067 .addClass('auto-trace-toolbar-enabled'); 1068 1069 // Vérifier ensuite si l'utilisateur a explicitement désactivé la fonctionnalité 1070 const userPrefs = Cookies.getJSON('software.opts') || {}; 1071 if (userPrefs.hasOwnProperty('auto-trace') && userPrefs['auto-trace'] === false) { 1072 _this.autoTracingEnabled = false; 1073 $('#auto-trace-toggle') 1074 .prop('checked', false) 1075 .closest('#responsive-toolbar') 1076 .removeClass('auto-trace-toolbar-enabled') 1077 .addClass('auto-trace-toolbar-disabled'); 1078 } 1079 }, 1080 /** 1081 * Gestionnaire pour l'événement draw.vertex.create - création d'un point 1082 * Cette version améliorée gère l'auto-traçage et met à jour les métadonnées 1083 */ 1084 setupVertexCreateHandler: function() { 1085 this.mapGl.on('draw.vertex.create', async function(e) { 1086 if (!_this.autoTraceOnGoing && !_this.isBlockingOperationOnGoing()) { 1087 _this.autoTraceOnGoing = true; 1088 try { 1089 await _this.runBlockingOperation(async function() { 1090 const p = e.point; 1091 const idPath = p.properties.parent; 1092 _this.changePathColor(idPath, $('#color-picker-dropdown .active .dropdown-item').data('color') || settings.hexColor); 1093 1094 if (!idPath) return; 1095 1096 const coord = [ 1097 parseFloat(p.geometry.coordinates[0].toFixed(6)), 1098 parseFloat(p.geometry.coordinates[1].toFixed(6)) 1099 ]; 1100 // Ajouter le point au store 1101 _this.path.insertPoint(idPath, +p.properties.coord_path, { 1102 lng: coord[0], 1103 lat: coord[1], 1104 order: +p.properties.coord_path, 1105 anchor: 1 1106 }); 1107 1108 // en version simple, on doit désactiver le bouton de création de tracé si tracé >= 1 par ex 1109 if ($('#center-last-point').is('.active')) { 1110 setTimeout(function () { 1111 _this.mapGl.flyTo({ 1112 center: p.geometry.coordinates 1113 }); 1114 }, 30) 1115 } 1116 1117 if (_this.autoTracingEnabled && _this.store[idPath]?.length >= 2) { 1118 // Si l'auto-traçage est activé et il y a au moins 2 points 1119 try { 1120 // Lancer l'auto-traçage avec l'option isAutomatic=true 1121 await _this.autoTraceSegment(idPath, _this.store[idPath].length-2, _this.store[idPath].length-1); 1122 } catch (err) { 1123 console.error("Erreur auto-traçage:", err); 1124 } 1125 } 1126 await _this.getAltitudeForPoints(idPath, [p.properties.coord_path]); 1127 1128 _this.path.handleMarkersPath(true); 1129 await _this.updateTracePostOperation(idPath); 1130 _this.updateHelper(); 1131 _this.updateGroupActions(); 1132 1133 // Créer une entrée dans l'historique 1134 _this.history.operation(() => { 1135 return { 1136 store: Immutable.fromJS(_this.path.getPoints()), 1137 action: 'vertex.create' 1138 }; 1139 }, i18n("history_vertex_create")); 1140 }); 1141 } finally { 1142 _this.autoTraceOnGoing = false;
1143 } 1144 } else { 1145 iziToast.warning({ 1146 message: i18n("toast_msg_wait_autotrace_end") 1147 }) 1148 } 1149 }) 1150 }, 1151 1152 /** 1153 * Toggle du traçage automatique 1154 */ 1155 toggleAutoTracing: function(el) { 1156 const isChecked = $(el).prop('checked'); 1157 _this.autoTracingEnabled = isChecked; 1158 1159 _paq.push(['trackEvent', 1160 'Route planner', 1161 'Toggle autoplot', 1162 (isChecked ? 'Enable' : 'Disable') + ' autoplot', 1163 ]); 1164 1165 const message = this.autoTracingEnabled 1166 ? i18n("toast_msg_autotrace_enabled") 1167 : i18n("toast_msg_autotrace_disabled"); 1168 1169 iziToast.info({ 1170 message, 1171 position: "bottomCenter", 1172 timeout: 3000 1173 }); 1174 1175 // Stocker la préférence utilisateur 1176 _this.setCookie({ 'auto-trace': this.autoTracingEnabled }); 1177 }, 1178 1179 // ...existing code... (toggleAutoTracing) 1180 1181 init: async function(){ 1182 1183 // maplibregl.supported() a été supprimée dans MapLibre GL JS v5 â vérification WebGL directe 1184 const _webglSupported = (function() { 1185 try { 1186 const c = document.createElement('canvas'); 1187 return !!(c.getContext('webgl2') || c.getContext('webgl')); 1188 } catch(e) { return false; } 1189 })(); 1190 if(booted || !_webglSupported) return 1191 1192 // Ajouter l'initialisation du traçage automatique 1193 this.initAutoTraceControls(); 1194 1195 // Restaurer les préférences utilisateur 1196 const userPrefs = Cookies.getJSON('software.opts') || {}; 1197 if (userPrefs['auto-trace']) { 1198 this.autoTracingEnabled = true; 1199 $('#auto-trace-toggle').addClass('active'); 1200 } 1201 1202 iziToast.settings({ 1203 closeOnClick: true, 1204 displayMode: 'once', 1205 maxWidth: '350px', 1206 transitionOut: 'flipOutX', 1207 layout: 2, 1208 timeout: 5000, 1209 position: 'bottomCenter' 1210 }) 1211 1212 settings.isSimpleVersion = !settings.isAdvanced 1213 1214 if(!settings.softwareOpts){ 1215 // Options par défaut 1216 Cookies.set('software.opts', { 1217 "hide-txt": false, // Option pr affichage du texte après les icones 1218 "advanced": settings.isAdvanced, // Option mode avancée ou non 1219 "hide-tabs": false // Option pr cacher les onglets en bas à droite 1220 }, { expires: 365 }) 1221 } 1222 1223 tippy.setDefaultProps({ 1224 delay: [0, 0], 1225 theme: 'visorando', 1226 hideOnClick: settings.mapEl.hasClass('mobile-user') ? 'toggle' : false, 1227 arrow: false 1228 }); 1229 1230 var tippyInstances = tippy('.tippy', { 1231 content: function(reference) { 1232 var title = reference.getAttribute('title') 1233 reference.removeAttribute('title') 1234 return title 1235 } 1236 }) 1237 1238 tippySingleton = tippy.createSingleton(tippyInstances, { 1239 ignoreAttributes: true, 1240 offset: [0, 7], 1241 moveTransition: 'transform .2s ease-out', 1242 animateFill: true, 1243 overrides: ['placement', 'content', 'popperOptions'] 1244 }) 1245 1246 priorityNav.init({ 1247 mainNavWrapper:'.group-actions', 1248 mainNav:'.nav', 1249 navDropdownLabel:'', 1250 moved: function () { 1251 $.each($('.'+this.navDropdownClassName).find('> *'), function(){ 1252 var newProps = { 1253 placement: 'left', 1254 popperOptions: { 1255 modifiers: [ 1256 { 1257 name: 'flip', 1258 options: { 1259 fallbackPlacements: ['right', 'bottom'], 1260 }, 1261 }, 1262 ] 1263 } 1264 } 1265 this._tippy.setProps(newProps) 1266 tippySingleton.setProps(newProps) 1267 }) 1268 }, 1269 movedBack: function () { 1270 $.each(settings.groupActions.find('.nav > *'), function(){ 1271 var newProps = { 1272 placement: 'bottom', 1273 popperOptions: {} 1274 } 1275 this._tippy.setProps(newProps) 1276 tippySingleton.setProps(newProps) 1277 }) 1278 } 1279 }) 1280 1281 $(window).on('resize orientationchange', throttle(function(){ 1282 1283 var $el = settings.groupActions.find('.priority-nav__dropdown') 1284 if($el.length && $el.hasScrollBar()){ 1285 if(!$('head').find('#fix-scrollbar').length){ 1286 var scrollBarWidth = $el.prop('offsetWidth') - $el.prop('clientWidth') 1287 $('head').append('<style type="text/css" id="fix-scrollbar" >.nav__dropdown >
1287 a{padding-right:calc(2rem + '+scrollBarWidth+'px)}</style>') 1288 } 1289 }else{ 1290 $('head').find('#fix-scrollbar').remove() 1291 } 1292 if($el.is(':parent') && !$el.is('.show') && $el.prev('.is-open').length){ 1293 $el.addClass('show') 1294 } 1295 1296 $el.css('max-height', (window.innerHeight - 50)+'px') 1297 1298 }, 1000)); 1299 1300 $(window).on('orientationchange.software', _this.updateTopBar); 1301 1302 // Recalcul du menu overflow (priority-nav) lors du passage en plein écran 1303 // Un délai est nécessaire pour attendre la fin de la transition de layout du navigateur 1304 $(document).on('fullscreenchange.software webkitfullscreenchange.software', function() { 1305 setTimeout(function() { 1306 _this.updateTopBar(); 1307 }, 150); 1308 }); 1309 1310 $(document).on('keydown', throttleWithoutTrailing(function(e){ 1311 if($(e.target).is('input')) return 1312 1313 var _run = function(){ 1314 // 90 = touche 'z' 1315 var isUndo = e.which === 90 && (e.ctrlKey || e.metaKey) 1316 // redo 1317 if( isUndo && e.shiftKey ){ 1318 _try(function(){ 1319 if(_this.history.hasRedo()) _this.history.redo(e) 1320 }) 1321 } 1322 // undo 1323 else if( isUndo ){ 1324 _try(function(){ 1325 if(_this.history.hasUndo()) _this.history.undo(e) 1326 }) 1327 } 1328 } 1329 1330 var _try = function(callback){ 1331 if($.ajaxq.isRunning('store')) { 1332 $.ajaxq.getActiveRequest('store').done(function(){ 1333 if ($.ajaxq.getCountPendingRequest('store') === 0) { 1334 $(document).one('history.operation', function(){ 1335 callback() 1336 }) 1337 return 1338 } 1339 _try(callback) 1340 }) 1341 }else{ 1342 callback() 1343 } 1344 } 1345 1346 _run() 1347 1348 }, 200)); 1349 1350 $('#loc').autocomplete({ 1351 source: settings.baseUrl+'index.php?'+$.param({ 1352 component: 'ajax', 1353 task: 'searchEntiteAutoComplete', 1354 field: 1, 1355 v: 2 1356 }), 1357 minLength: 3, 1358 select:function(e,ui){ 1359 $(this).val(ui.item.label) 1360 .closest('form') 1361 .find('#meta-data').val(ui.item.metaData) 1362 .trigger('submit'); 1363 }, 1364 delay: 200 1365 }); 1366 1367 settings.hikeUrlEl.on('click', function(){ 1368 if(settings.idRando && settings.idRando > 0){ 1369 Object.assign(document.createElement('a'), { 1370 target: '_blank', 1371 href: settings.baseUrl+'index.php?'+$.param({ 1372 component: 'rando', 1373 task: 'showRando', 1374 idRandonnee: settings.idRando 1375 }) 1376 }).click() 1377 }else{ 1378 iziToast.info({ 1379 message: i18n("toast_msg_must_save_trace") 1380 }) 1381 } 1382 }) 1383 1384 // Synchroniser settings.carteType depuis le panel VisorandoMap 1385 $(document).on('change', '.vrmap-map-layers-panel input[name="baselayer"]', function () { 1386 const newLayer = $(this).val(); 1387 settings.carteType = newLayer; 1388 _paq.push(['trackEvent', 'Layers panel', 'Change base layer', newLayer]); 1389 }); 1390 1391 settings.searchLocForm.on('submit', function (e) { 1392 e.preventDefault() 1393 $.getJSON(settings.baseUrl + 'index.php', [ 1394 $.param({task: 'getBounds', component: 'software', carteType: settings.carteType}), 1395 $(this).serialize(), 1396 $.param(computeMapDim()) 1397 ].join('&'), function (res) { 1398 if (res.bounds) { 1399 _this.mapGl.fitBounds(res.bounds, { 1400 padding: 12, 1401 duration: 500 1402 }) 1403 } else { 1404 iziToast.info({ 1405 message: i18n("toast_msg_no_result_found"), 1406 timeout: 2000 1407 }) 1408 } 1409 1410 if (res.marker) { 1411 _this.markers.createOnce(res.marker, 'search-result', 'search-location-marker') 1412 } else { 1413 _this.markers.deleteById(null, true, 'search-result') 1414 } 1415 } 1416 ) 1417 }) 1418 1419 // ===================================================================== 1420 // Initialisation via VisorandoMap (MapController) 1421 // ===================================================================== 1422 const { MapController } = VisorandoMap; 1423 const boundsCenter = [
1424 (settings.bounds[0][0] + settings.bounds[1][0]) / 2, 1425 (settings.bounds[0][1] + settings.bounds[1][1]) / 2 1426 ]; 1427 1428 try { 1429 _this.mapController = new MapController({ 1430 // Options communes du site (URLs backend, catalogue, i18n, unités, gating 1431 // premium/gid). 1432 ...{"apiBaseUrl":"https://www.visorando.com/","stylesBaseUrl":"https://www.visorando.com","catalogUrl":"https://www.visorando.com/index.php?component=mapStyle&task=mapsCatalog","translationsBaseUrl":"https://www.visorando.com/i18n/visorando-map","locale":"fr","coordinateFormat":"DD","distanceUnit":"km","elevationUnit":"m","isPremium":false,"userGid":0,"premiumUrl":"https://www.visorando.com/premium.html"}, 1433 container: settings.mapEl.get(0).id, 1434 fullscreenContainer: document.documentElement, 1435 apiBaseUrl: settings.baseUrl, 1436 maxZoom: 19, 1437 // Page de dessin : le fond vectoriel topo n'est proposé qu'aux admins (incompatible 1438 // MapboxDraw + perfs pour le grand public). Pour un non-admin, un carteType TOPO_VECTO 1439 // enregistré est remappé vers IGNTOPO. 1440 baseLayer: (function (ct) { 1441 var allowed = ['IGNTOPO','IGNSAT','HIKINGMAP','MAPNIK','CYCLINGMAP']; 1442 return allowed.indexOf(ct) !== -1 ? ct : 'IGNTOPO'; 1443 })(settings.carteType), 1444 enableLayerSelector: true, 1445 showOverlayLegends: true, 1446 enablePitch: true, 1447 enableRotation: true, 1448 userGid: settings.user.uGid, 1449 isPremium: settings.user.hasPremiumAccess, 1450 loginUrl: "https://www.visorando.com/index.php?component=user&task=loginForm&urlRedirect=https%253A%252F%252Fwww.visorando.com%252Flogiciel-randonnee.html", 1451 enableTracking: typeof _paq !== 'undefined', 1452 }); 1453 1454 await _this.mapController.centerOnUserLocation({ 1455 latitude: boundsCenter[1], 1456 longitude: boundsCenter[0], 1457 zoom: 6, 1458 animate: false, 1459 }); 1460 1461 // Récupérer l'instance MapLibre brute pour MapboxDraw et les opérations bas-niveau 1462 this.mapGl = _this.mapController.getMapInstance(); 1463 this.setupBlockingInputGuard(); 1464 1465 } catch(e) { 1466 throw e; 1467 } 1468 1469 // Ajuster la vue aux bounds initiaux 1470 _this.mapGl.fitBounds(settings.bounds, { padding: 20, duration: 0 }); 1471 1472 // =================================================================== 1473 // Gestion clavier (sélections vertex / traces) 1474 // - Shift enfoncé â active la sélection de points (vertex visibles) 1475 // - Shift relâché â désactive la sélection de points 1476 // - Click sur un point â sélectionne la trace si non sélectionnée 1477 // =================================================================== 1478 1479 const keysDown = {}; 1480 let isBoxSelecting = false; 1481 1482 const layersToManage = [ 1483 'gl-draw-polygon-midpoint', 1484 'gl-draw-polygon-and-line-vertex-inactive', 1485 'gl-draw-polygon-and-line-vertex-inactive-custom' 1486 ]; 1487 1488 let originalFilters = {}; 1489 1490 // ------------------------------------------------------------------- 1491 // 1. Sauvegarde des filtres initiaux au chargement 1492 // NB : La carte est déjà chargée (waitForLoad). Cette fonction est appelée 1493 // explicitement APRÃS addControl(drawGl) pour que les layers Draw soient présents. 1494 // ------------------------------------------------------------------- 1495 const _onMapLoaded = () => { 1496 1497 layersToManage.forEach(layerId => { 1498 const layer = _this.mapGl.getLayer(layerId); 1499 if (layer) { 1500 originalFilters[layerId] = layer.filter; 1501 } 1502 });
1503 // Rendez le canvas focusable + retirez l'outline par défaut 1504 const canvas = _this.mapGl.getCanvas(); 1505 canvas.setAttribute('tabindex', '0'); 1506 canvas.style.outline = 'none'; 1507 1508 // Donne le focus au canvas à chaque interaction sur la carte 1509 const container = _this.mapGl.getCanvasContainer(); 1510 ['pointerdown', 'mousedown', 'touchstart', 'click'].forEach(evt => { 1511 container.addEventListener(evt, () => { 1512 if (document.activeElement !== canvas) { 1513 canvas.focus(); 1514 } 1515 }, { passive: true }); 1516 }); 1517 }; 1518 1519 // ------------------------------------------------------------------- 1520 // 2. Keydown : Shift = active les points pour sélection multiple 1521 // ------------------------------------------------------------------- 1522 $(document).on('keydown.draw-filter', (e) => { 1523 keysDown[e.key] = true; 1524 1525 if (!e.shiftKey || isBoxSelecting) return; 1526 1527 const selectedIds = _this.drawGl.getSelectedIds(); 1528 if (selectedIds.length === 0) return; 1529 1530 // Force la sélection des traces actives 1531 _this.safeChangeMode('simple_select', { featureIds: selectedIds }); 1532 1533 // Restaure les vertex visibles 1534 layersToManage.forEach(layerId => { 1535 if (_this.mapGl.getLayer(layerId) && originalFilters[layerId]) { 1536 _this.mapGl.setFilter(layerId, originalFilters[layerId]); 1537 } 1538 }); 1539 }); 1540 1541 // -------------------------------------------------------------------
1542 // 3. Keyup : relâchement Shift = retour état normal 1543 // ------------------------------------------------------------------- 1544 $(document).on('keyup.draw-filter', (e) => { 1545 delete keysDown[e.key]; 1546 1547 if (!keysDown['Shift']) { 1548 1549 // Restaure filtres vertex à lâétat initial 1550 layersToManage.forEach(layerId => { 1551 if (_this.mapGl.getLayer(layerId) && originalFilters[layerId]) { 1552 _this.mapGl.setFilter(layerId, originalFilters[layerId]); 1553 } 1554 }); 1555 } 1556 }); 1557 1558 // ------------------------------------------------------------------- 1559 // 4. Click sur point : si trace non sélectionnée â sélectionne la trace 1560 // ------------------------------------------------------------------- 1561 this.mapGl.on('click', (e) => { 1562 if (!e.originalEvent.shiftKey) return; 1563 1564 const features = _this.mapGl.queryRenderedFeatures(e.point, { 1565 layers: layersToManage 1566 }); 1567 1568 if (features.length > 0) { 1569 const point = features[0]; 1570 const parentId = point.properties.parent; 1571 1572 const selectedIds = _this.drawGl.getSelectedIds(); 1573 if (!selectedIds.includes(parentId)) { 1574 // On sélectionne la trace parent du point cliqué 1575 _this.safeChangeMode('simple_select', { featureIds: [parentId] }); 1576 1577 // Affiche ses vertex (nécessaire si shift est maintenu) 1578 layersToManage.forEach(layerId => { 1579 if (_this.mapGl.getLayer(layerId) && originalFilters[layerId]) { 1580 _this.mapGl.setFilter(layerId, originalFilters[layerId]); 1581 } 1582 }); 1583 } 1584 } 1585 }); 1586 1587 // ------------------------------------------------------------------- 1588 1589 if(!settings.user.isLoggedIn){ 1590 1591 $.fancybox.open({ 1592 src : '#not-logged-popup', 1593 type : 'inline', 1594 opts : { 1595 beforeClose: function(){ 1596 Object.assign(document.createElement('a'), { 1597 href: settings.baseUrl 1598 }).click() 1599 return false 1600 }, 1601 trapFocus: 1, 1602 idleTime: 0, 1603 smallBtn: 0, 1604 toolbar: 1, 1605 animationEffect: 'material', 1606 autoFocus: 0, 1607 keyboard: 0, 1608 slideShow: 0, 1609 fullScreen: 0, 1610 thumbs: 0, 1611 touch: 0, 1612 clickContent: false, 1613 clickSlide: false, 1614 clickOutside: false, 1615 dblclickContent: false, 1616 dblclickSlide: false, 1617 dblclickOutside: false 1618 } 1619 }); 1620 1621 var validator = $('#quick-entry').validate({ 1622 submitHandler: function(form) { 1623 1624 $(form).find('#regJS').val(settings.hashCode) 1625 1626 var _commonOpts = { 1627 displayMode: 'replace', 1628 id:'inscription', 1629 timeout: false, 1630 position: 'bottomCenter' 1631 } 1632 1633 $.ajax({ 1634 url: settings.baseUrl+'index.php?'+$.param({ 1635 component: 'software', 1636 task: 'register' 1637 }), 1638 dataType: 'json', 1639 type: 'post', 1640 beforeSend: function(){ 1641 settings.mapEl.addClass('loading high-z-index') 1642 $(form).find(':submit').attr('disabled', 'disabled').addClass('disabled') 1643 }, 1644 success: function(res){ 1645 1646 if(res.reset){ 1647 form.reset() 1648 } 1649 1650 if(res.error){ 1651 iziToast.error(Object.assign(_commonOpts, { 1652 message: escapeHtml(res.error) 1653 })) 1654 } 1655 1656 if(res.hasOwnProperty('msg')){ 1657 iziToast.info(Object.assign(_commonOpts, { 1658 message: escapeHtml(res.msg) 1659 })) 1660 } 1661 }, 1662 error: function(){ 1663 iziToast.error(Object.assign(_commonOpts, { 1664 message: i18n("toast_msg_registration_error") 1665 })) 1666 }, 1667 complete: function(){ 1668 settings.mapEl.removeClass('loading high-z-index') 1669 $(form).find(':submit').removeAttr('disabled').removeClass('disabled') 1670 }, 1671 data: $(form).serialize() 1672 }) 1673 }, 1674 rules: { 1675 "email": { 1676 email: true, 1677 required: true 1678 }, 1679 "password": { 1680 notEmpty: true, 1681 required: true, 1682 nowhitespace: true, 1683 minlength: 6 1684 } 1685 } 1686 }) 1687 1688 var pwdInput = $('#password', '#quick-entry'), 1689 pwdProgressBar = $('#progress_password', '#quick-entry'); 1690 1691 var pwdIndicator = tippy(pwdInput.get(0), { 1692 content: '', 1693 appendTo: $('#not-logged-popup').get(0), 1694 allowHTML: true, 1695 trigger: 'manual', 1696 arrow: tippy.roundArrow, 1697 offset: [0, 10], 1698 placement: 'right' 1699 }) 1700 1701 var levelStrength = [ 1702 { label: i18n("password_strength_weak"), class: 'weak' }, 1703 { label: i18n("password_strength_good"), class: 'good' }, 1704 { label: i18n("password_strength_strong"), class: 'strong' } 1705 ] 1706 1707 var evaluateStrength = function(){
1708 var value = pwdInput.val(), 1709 1710 hasLower = /[a-z]+/.test(value), 1711 hasUpp = /[A-Z]+/.test(value), 1712 hasNumber = /[0-9]+/.test(value), 1713 hasSpeChar = /[-!$%^&*()_+|~=`{}\[\]:\/;<>?,.@#]+/.test(value), 1714 1715 strengthCriteria = [hasLower, hasUpp, hasNumber, hasSpeChar], 1716 strength = strengthCriteria.filter(function(v){ return v }).length - 1 1717 1718 var advices = []; 1719 1720 if (!hasLower) { 1721 advices.push(i18n("password_advice_lowercase")); 1722 } 1723 if (!hasUpp) { 1724 advices.push(i18n("password_advice_uppercase")); 1725 } 1726 if (!hasNumber) { 1727 advices.push(i18n("password_advice_number")); 1728 } 1729 if (!hasSpeChar) { 1730 advices.push(i18n("password_advice_special")); 1731 } 1732 1733 return { 1734 advices: advices, 1735 indication: levelStrength[Math.min(Math.max(0, strength), levelStrength.length - 1)] 1736 }; 1737 } 1738 1739 pwdInput.on('focus', function () { 1740 validator.element( $(this).get(0) ) 1741 if(validator.errorList.length) return 1742 1743 var display = evaluateStrength() 1744 if(display.advices.length){ 1745 var ul = $('<ul/>'); 1746 $.each(display.advices, function (i, value) { 1747 ul.append($('<li/>', { 1748 text: value 1749 })); 1750 }); 1751 pwdIndicator.setContent(display.indication.label + ul.prop('outerHTML')) 1752 }else{ 1753 pwdIndicator.setContent(display.indication.label) 1754 } 1755 1756 pwdIndicator.show() 1757 pwdProgressBar.removeClass().addClass(display.indication.class).show() 1758 }).on('focusout', function(){ 1759 pwdProgressBar.hide() 1760 pwdIndicator.hide() 1761 }).on('input', function(){ 1762 validator.element( $(this).get(0) ) 1763 if(validator.errorList.length){ 1764 pwdProgressBar.hide() 1765 pwdIndicator.hide() 1766 }else{ 1767 var display = evaluateStrength() 1768 if(display.advices.length){ 1769 var ul = $('<ul/>'); 1770 $.each(display.advices, function (i, value) { 1771 ul.append($('<li/>', { 1772 text: value 1773 })); 1774 }); 1775 pwdIndicator.setContent($('<strong/>', {text : display.indication.label}).prop('outerHTML') + ul.prop('outerHTML')) 1776 }else{ 1777 pwdIndicator.setContent(display.indication.label) 1778 } 1779 pwdIndicator.show() 1780 pwdProgressBar.removeClass().addClass(display.indication.class).show() 1781 } 1782 }) 1783 1784 this.mapGl.once('load', function(){ 1785 settings.mapEl.removeClass('loading').addClass('done') 1786 settings.topBar.prependTo(settings.mapEl) 1787 $('body').addClass(settings.isSimpleVersion ? 'simple-version' : 'advanced-version') 1788 _this.mapGl.resize() 1789 }) 1790 1791 return 1792 } 1793 1794 function nearestSegmentIndex(map, coords, lngLat) { 1795 const p = map.project(lngLat); 1796 let bestK = -1, bestD = Infinity; 1797 for (let k = 0; k < coords.length - 1; k++) { 1798 const a = map.project({lng: coords[k][0], lat: coords[k][1]}); 1799 const b = map.project({lng: coords[k+1][0], lat: coords[k+1][1]}); 1800 const abx = b.x - a.x, aby = b.y - a.y; 1801 const apx = p.x - a.x, apy = p.y - a.y; 1802 const ab2 = abx*abx + aby*aby || 1; 1803 let t = (apx*abx + apy*aby) / ab2; t = Math.max(0, Math.min(1, t)); 1804 const px = a.x + t*abx, py = a.y + t*aby; 1805 const dx = p.x - px, dy = p.y - py; 1806 const d2 = dx*dx + dy*dy; 1807 if (d2 < bestD) { bestD = d2; bestK = k; } 1808 } 1809 return bestK; 1810 } 1811 1812 //Ajout d'un mode pour gérer les ancres avec les styles 1813 const Base = MapboxDraw.modes.simple_select; 1814 const DrawLineLocked = { 1815 // On étend le mode, mais on ne modifie PAS ses handlers 1816 ...Base, 1817 1818 onSetup(opts) { 1819 const state = Base.onSetup.call(this, opts); 1820 // Récupération robuste de la feature et de son id 1821 state.feature = null; 1822 state.featureId = null; 1823 state.anchorIndices = []; 1824 state.interceptDrag = false;
1825 state.draggingAnchor = null; // index effectif une fois le prune fait 1826 state.pendingAnchorIdx = null; // index de lâancre cliquée (si ancre) 1827 state._wasDragPanEnabled = false; 1828 return state; 1829 }, 1830 onTouchStart(state, e) { 1831 return this.onMouseDown(state, e); 1832 }, 1833 onMouseDown(state, e) { 1834 const ft = e?.featureTarget?.properties; 1835 1836 // 1) Si clique sur un VERTEX ancre : (ton flux existant) on arme juste le drag dâancre 1837 if (ft?.meta === 'vertex' && ft.coord_path && ft.parent) { 1838 state._wasDragPanEnabled = this.map.dragPan.isEnabled && this.map.dragPan.isEnabled(); 1839 if (state._wasDragPanEnabled) this.map.dragPan.disable(); 1840 1841 const coord_path = parseInt(ft.coord_path); 1842 const pathId = ft.parent; 1843 1844 if (_this.path.isAnchor(pathId, coord_path)) { 1845 state.featureId = pathId; 1846 state.feature = this._ctx.store.get(pathId); 1847 state.anchorIndices = _this.path.computeAnchorIndicesFromStore(state.featureId); 1848 state.draggingAnchor = null; 1849 // On arme le drag dâancre, sans prune pour lâinstant 1850 state.interceptDrag = true; 1851 state.pendingAnchorIdx = coord_path; 1852 1853 e.originalEvent?.preventDefault?.(); 1854 e.originalEvent?.stopPropagation?.(); 1855 } 1856 } 1857 // 2) Si clique sur la LIGNE (entre deux ancres) : on arme une insertion et on place le point 1858 if (ft?.meta === 'feature' && ft.id && ft.active === 'true') { 1859 state._wasDragPanEnabled = this.map.dragPan.isEnabled && this.map.dragPan.isEnabled(); 1860 if (state._wasDragPanEnabled) this.map.dragPan.disable(); 1861 1862 state.featureId = ft.id; 1863 state.feature = this._ctx.store.get(ft.id); 1864 state.anchorIndices = _this.path.computeAnchorIndicesFromStore(state.featureId); 1865 state.interceptDrag = true; 1866 1867 const coords = state.feature.coordinates; 1868 const k = nearestSegmentIndex(this.map, coords, e.lngLat); 1869 const insertIdx = k + 1; 1870 1871 const segmentStart = coords[k]; 1872 const segmentEnd = coords[k + 1]; 1873 1874 const projectedPoint = _this.projectPointOnSegment(e.lngLat, segmentStart, segmentEnd); 1875 1876 // INSÃRER la nouvelle ancre à k+1 1877 state.feature.addCoordinate(insertIdx, projectedPoint.lng, projectedPoint.lat); 1878 const pt = state.feature.getCoordinate(insertIdx); 1879 // Marquer âancreâ dans ton store externe 1880 const parentId = state.feature.id || state.featureId; 1881 _this.path.insertPoint(parentId, insertIdx, { 1882 lng: projectedPoint.lng.toFixed(6), 1883 lat: projectedPoint.lat.toFixed(6), 1884 anchor: 1 1885 }) 1886 this.map.fire('draw.vertex.update', { 1887 index: [insertIdx], 1888 parent: state.feature.id, 1889 points: [pt], 1890 skipAutoTrace: true 1891 }); 1892 1893 // Reset des flags pour ne PAS réinsérer au mouseup 1894 state.pendingAnchorIdx = insertIdx; 1895 state.draggingAnchor = null; 1896 } 1897 return Base.onMouseDown.call(this, state, e); 1898 }, 1899 onDrag(state, e) { 1900 // Si on intercepte et que le prune nâa pas encore été fait, on le fait MAINTENANT 1901 if (state.interceptDrag && state.draggingAnchor == null && state.pendingAnchorIdx != null) { 1902 const idx = state.pendingAnchorIdx; 1903 const coords = state.feature.coordinates; 1904 1905 state.anchorIndices = _this.path.computeAnchorIndicesFromStore(state.feature.id); 1906 const anchorIndex = state.anchorIndices.indexOf(idx); 1907 1908 const prev = _this.path.prevAnchor(idx, state.anchorIndices); 1909 const next = _this.path.nextAnchor(idx, state.anchorIndices, coords.length); 1910
1911 // Supprimer tous les points non-ancres STRICTEMENT entre prev et next 1912 // Itération à rebours ! 1913 for (let i = next - 1; i > prev; i--) { 1914 if (!_this.path.isAnchor(state.feature.id, i)) { 1915 state.feature.removeCoordinate(i); 1916 // (optionnel) Si tu tiens ton _this.store en phase, nettoie ici la clé correspondante. 1917 _this.path.deletePoint(state.feature.id, i); 1918 } 1919 } 1920 state.anchorIndices = _this.path.computeAnchorIndicesFromStore(state.feature.id); 1921 state.draggingAnchor = state.anchorIndices[anchorIndex]; 1922 this.doRender(state.featureId || state.initiallySelectedFeatureIds?.[0]); 1923 } 1924 1925 // Si on est en drag dâancre : on déplace SEULEMENT lâancre 1926 if (state.interceptDrag && state.draggingAnchor != null) { 1927 state.feature.updateCoordinate(state.draggingAnchor, e.lngLat.lng, e.lngLat.lat); 1928 this.doRender(state.featureId || state.initiallySelectedFeatureIds?.[0]); 1929 return; 1930 } 1931 1932 // Sinon, comportement natif 1933 return Base.onDrag.call(this, state, e); 1934 }, 1935 onTouchEnd(state, e) { 1936 return this.onMouseUp(state, e); 1937 }, 1938 onMouseUp(state, e) { 1939 // reset flags 1940 if (state.interceptDrag && state.draggingAnchor != null) { 1941 const pt = state.feature.getCoordinate(state.draggingAnchor); // [lng, lat] 1942 this.map.fire('draw.vertex.update', { 1943 index: [state.draggingAnchor], 1944 parent: state.feature.id, 1945 points: [pt] 1946 }); 1947 } 1948 state.pendingAnchorIdx = null; 1949 state.draggingAnchor = null; 1950 state.interceptDrag = false; 1951 if (state._wasDragPanEnabled) this.map.dragPan.enable(); 1952 state._wasDragPanEnabled = false; 1953 return Base.onMouseUp?.call(this, state, e); 1954 }, 1955 toDisplayFeatures(state, geojson, display) { 1956 Base.toDisplayFeatures.call(this, state, geojson, (g) => { 1957 // 1) Cacher tous les midpoints 1958 if (g.properties?.meta === 'midpoint') return; 1959 1960 // 2) Cacher les vertex non-ancres (Variante A) 1961 if (g.properties?.meta === 'vertex' && g.properties.coord_path) { 1962 const isAnchor = _this.path.isAnchor(g.properties.parent, g.properties.coord_path); 1963 if (!isAnchor) return; 1964 } 1965 1966 display(g); 1967 }); 1968 } 1969 }; 1970 1971 // Override du mode draw_line_string pour ignorer les clics droits 1972 // (le clic droit ouvre le menu contextuel et ne doit pas poser de point) 1973 const DrawLineStringBase = MapboxDraw.modes.draw_line_string; 1974 const DrawLineStringNoRightClick = { 1975 ...DrawLineStringBase, 1976 onClick(state, e) { 1977 // Ignorer tous les boutons autres que le bouton gauche (button === 0) 1978 if (e.originalEvent && e.originalEvent.button !== 0) return; 1979 return DrawLineStringBase.onClick.call(this, state, e); 1980 } 1981 }; 1982 1983 var modes = { 1984 ...MapboxDraw.modes, 1985 simple_select: DrawLineLocked, 1986 draw_line_string: DrawLineStringNoRightClick 1987 } 1988 1989 delete modes.draw_point; 1990 delete modes.draw_polygon; 1991 1992 const HOVER_RING_SRC = 'hover-ring-src'; 1993 const HOVER_RING_LYR = 'hover-ring-layer'; 1994 this.drawGl = new MapboxDraw({ 1995 /* Permet de gérer la (dé)sélection quand un tracé perd son (faux) "focus" 1996 (Ne concerne pas les points) */ 1997 handleBlurSelection: !settings.isSimpleVersion, 1998 displayControlsDefault: false, 1999 userProperties: true, 2000 colorPalette: settings.colorPalette, 2001 controls: { line_string: false }, 2002 modes: modes, 2003 defaultMode: 'draw_line_string', 2004 styles: [ 2005 { 2006 'id': 'gl-draw-line-inactive-outline', 2007 'maxzoom': 17, 2008 'type': 'line', 2009 'filter': ['all', 2010 ['==', 'active', 'false'], 2011 ['==', '$type', 'LineString'], 2012 ['!=', 'mode', 'static'] 2013 ], 2014 'layout': { 2015 'line-cap': 'round', 2016 'line-join': 'round' 2017 }, 2018 'paint': { 2019 'line-color': '#ffffff', 2020 'line-width': [ 2021 'interpolate', ['linear'], ['zoom'], 2022 8, 5, 2023 16, 7 2024 ], 2025 'line-opacity': 0.9 2026 } 2027 }, 2028 { 2029 'id': 'gl-draw-line-inactive', 2030 'type': 'line', 2031 'filter': ['all', 2032 ['==', 'active', 'false'], 2033 ['==', '$type', 'LineString'], 2034 ['!=', 'mode', 'static'] 2035 ], 2036 'layout': { 2037 'line-cap': 'round', 2038 'line-join': 'round' 2039 }, 2040 'paint': { 2041 'line-color': [ 2042 'case', 2043 ['has','user_color'], ["get","user_color"], 2044 ['has','color'], ["get","color"], 2045 settings.hexColor 2046 ], 2047 'line-width': [ 2048 'interpolate', ['linear'], ['zoom'], 2049 8, 1.5, 2050 16, 3.5 2051 ] 2052 } 2053 }, 2054 { 2055 'id': 'gl-draw-line-active-outline', 2056 'maxzoom': 17, 2057 'type': 'line', 2058 'filter': ['all', 2059 ['==', '$type', 'LineString'], 2060 ['==', 'active', 'true'] 2061 ], 2062 'layout': { 2063 'line-cap': 'round', 2064 'line-join': 'round' 2065 }, 2066 'paint': { 2067 'line-color': '#ffffff', 2068 'line-width': [ 2069 'interpolate', ['linear'], ['zoom'], 2070 8, 3, 2071 16, 6 2072 ], 2073 'line-opacity': 0.9 2074 } 2075 }, 2076 { 2077 'id': 'gl-draw-line-active', 2078 'type': 'line', 2079 'filter': ['all', 2080 ['==', '$type', 'LineString'], 2081 ['==', 'active', 'true'] 2082 ], 2083 'layout': { 2084 'line-cap': 'round', 2085 'line-join': 'round' 2086 }, 2087 'paint': { 2088 'line-color': [ 2089 'case', 2090 ['has','user_color'], ["get","user_color"], 2091 ['has','color'], ["get","color"], 2092 settings.hexColor 2093 ], 2094 'line-dasharray': [0.2, 1.5], 2095 'line-width': [ 2096 'interpolate', ['linear'], ['zoom'], 2097 8, 1.5, 2098 16, 3.5 2099 ] 2100 } 2101 }, 2102 { 2103 'id': 'gl-draw-polygon-midpoint', 2104 'type': 'circle', 2105 'filter': ['all', 2106 ['==', '$type', 'Point'], 2107 ['==', 'meta', 'midpoint'], 2108 ['==', 'active', 'true']], 2109 'paint': { 2110 'circle-radius': 6, 2111 'circle-color': [ 2112 "let", "_rgba", ["to-rgba", ["to-color", 2113 [ 2114 'case', 2115 ['has','user_color'], ["get","user_color"], 2116 ['has','color'], ["get","color"], 2117 settings.hexColor 2118 ] 2119 ]], 2120 [ 2121 "rgba", 2122 ["number", ["at", 0, ["var", "_rgba"]]], 2123 ["number", ["at", 1, ["var", "_rgba"]]], 2124 ["number", ["at", 2, ["var", "_rgba"]]], 2125 .5 2126 ] 2127 ] 2128 } 2129 }, 2130 { 2131 'id': 'gl-draw-polygon-and-line-vertex-inactive', 2132 'type': 'circle', 2133 'filter': ['all', 2134 ['==', 'meta', 'vertex'], 2135 ['==', '$type', 'Point'], 2136 ['==', 'active', 'false'], 2137 ['==', 'parent_active', 'true'],
2138 ['!=', 'mode', 'static'] 2139 ], 2140 'paint': { 2141 'circle-radius': [ 2142 'interpolate', ['linear'], ['zoom'], 2143 8, 3, 2144 16, 9 2145 ], 2146 'circle-stroke-color': '#ffffff', 2147 'circle-stroke-width': [ 2148 'interpolate', ['linear'], ['zoom'], 2149 8, 1, 2150 16, 2 2151 ], 2152 'circle-color': [ 2153 'case', 2154 ['has','user_color'], ["get","user_color"], 2155 ['has','color'], ["get","color"], 2156 settings.hexColor 2157 ], 2158 } 2159 }, 2160 { 2161 'id': 'gl-draw-point-active', 2162 'type': 'circle', 2163 'filter': ['all', 2164 ['==', '$type', 'Point'], 2165 ['!=', 'meta', 'midpoint'], 2166 ['==', 'active', 'true'], 2167 ['==', 'parent_active', 'true'] 2168 ], 2169 'paint': { 2170 'circle-radius': [ 2171 'interpolate', ['linear'], ['zoom'], 2172 8, 5, 2173 16, 11 2174 ], 2175 'circle-stroke-color': '#ffffff', 2176 'circle-stroke-width': 4, 2177 'circle-color': [ 2178 'case', 2179 ['has','user_color'], ["get","user_color"], 2180 ['has','color'], ["get","color"], 2181 settings.hexColor 2182 ], 2183 } 2184 }, 2185 { 2186 'id': 'gl-draw-start-marker', 2187 'type': 'circle', 2188 'layout': { 2189 'visibility': 'visible' 2190 }, 2191 'filter': ['all', 2192 ['==', '$type', 'Point'], 2193 ['has', 'coord_path'], 2194 ['==', 'coord_path', '0'], 2195 ['==', 'parent_active', 'false'], 2196 ['==', 'meta', 'vertex'] 2197 ], 2198 'paint': { 2199 'circle-radius': 6, 2200 'circle-stroke-width': 2, 2201 'circle-stroke-color': [ 2202 'case', 2203 ['has','user_color'], ["get","user_color"], 2204 ['has','color'], ["get","color"], 2205 settings.hexColor 2206 ], 2207 'circle-color': [ 2208 "let", "_rgba", ["to-rgba", ["to-color", 2209 [ 2210 'case', 2211 ['has','user_color'], ["get","user_color"], 2212 ['has','color'], ["get","color"], 2213 settings.hexColor 2214 ] 2215 ]], 2216 [ 2217 "rgba", 2218 ["number", ["at", 0, ["var", "_rgba"]]], 2219 ["number", ["at", 1, ["var", "_rgba"]]], 2220 ["number", ["at", 2, ["var", "_rgba"]]], 2221 .3 2222 ] 2223 ] 2224 } 2225 }, 2226 { 2227 id: 'gl-draw-auto-vertex', 2228 type: 'circle', 2229 filter: ['all', 2230 ['==', '$type', 'Point'], 2231 ['==', 'meta', 'auto_vertex'] 2232 ], 2233 paint: { 2234 'circle-radius': 0.0001, // camouflage intégral 2235 'circle-opacity': 0, 2236 'circle-color': 'rgba(0,0,0,0)', 2237 'circle-stroke-width': 0, 2238 'circle-stroke-color': 'rgba(0,0,0,0)' 2239 } 2240 }, 2241 { 2242 id: 'gl-draw-polygon-and-line-vertex-inactive-custom', 2243 type: 'circle', 2244 filter: ['==', 'meta', 'Point'], 2245 paint: { 2246 'circle-radius': 0.0001, // camouflage intégral 2247 'circle-opacity': 0, 2248 'circle-color': 'rgba(0,0,0,0)', 2249 'circle-stroke-width': 0, 2250 'circle-stroke-color': 'rgba(0,0,0,0)' 2251 } 2252 } 2253 ] 2254 }); 2255 function addHoverRingSourceAndLayer() { 2256 if (_this.mapGl.getSource(HOVER_RING_SRC)) return; // déjà ajouté 2257 _this.mapGl.addSource(HOVER_RING_SRC, { 2258 type: 'geojson', 2259 data: { type: 'FeatureCollection', features: [] } 2260 }); 2261 2262 _this.mapGl.addLayer({ 2263 id: HOVER_RING_LYR, 2264 type: 'circle', 2265 source: HOVER_RING_SRC, 2266 paint: { 2267 'circle-radius': [ 2268 'interpolate', ['linear'], ['zoom'], 2269 8, 3, 2270 16, 9 2271 ], 2272 'circle-stroke-color': '#FFFFFF', 2273 'circle-stroke-width': 3, 2274 'circle-color': [ 2275 "let", "_rgba", ["to-rgba", ["to-color", 2276 [ 2277 'case', 2278 ['has','user_color'], ["get","user_color"], 2279 ['has','color'], ["get","color"], 2280 settings.hexColor 2281 ] 2282 ]], 2283 [ 2284 "rgba", 2285 ["number", ["at", 0, ["var", "_rgba"]]], 2286 ["number", ["at", 1, ["var", "_rgba"]]], 2287 ["number", ["at", 2, ["var", "_rgba"]]], 2288 .33 2289 ] 2290 ] 2291 } 2292 }); 2293 2294 _this.mapGl.moveLayer(HOVER_RING_LYR); // au-dessus des calques draw 2295 } 2296 2297 // La carte est peut-être déjà chargée (cas normal après waitForLoad de Visorandomap) 2298 if (_this.mapGl.isStyleLoaded()) { 2299 addHoverRingSourceAndLayer(); 2300 } else { 2301 _this.mapGl.once('load', addHoverRingSourceAndLayer); 2302 } 2303 2304 function setHoverRing(lng, lat, color) { 2305 const src = _this.mapGl.getSource(HOVER_RING_SRC); 2306 if (!src) return; 2307 src.setData({ 2308 type: 'FeatureCollection', 2309 features: [{ 2310 type: 'Feature', 2311 geometry: { type: 'Point', coordinates: [lng, lat] }, 2312 properties: { 2313 user_color: color || settings.hexColor 2314 } 2315 }] 2316 }); 2317 } 2318 function clearHoverRing() { 2319 if (_this.mapGl.getSource(HOVER_RING_SRC)) _this.mapGl.getSource(HOVER_RING_SRC).setData({ type: 'FeatureCollection', features: [] }); 2320 } 2321 2322 this.mapGl.on('mousemove', (e) => { 2323 if (_this.isBlockingOperationOnGoing()) { 2324 _this.mapGl.getCanvas().style.cursor = 'wait'; 2325 return; 2326 } 2327 2328 function nearestSegmentAndPoint(map, coords, lngLat) { 2329 const p = _this.mapGl.project(lngLat); 2330 let best = { k: -1, px: 0, py: 0, d2: Infinity }; 2331 2332 for (let k = 0; k < coords.length - 1; k++) { 2333 const a = _this.mapGl.project({lng: coords[k][0], lat: coords[k][1]}); 2334 const b = _this.mapGl.project({lng: coords[k + 1][0], lat: coords[k + 1][1]}); 2335 const abx = b.x - a.x, aby = b.y - a.y; 2336 const apx = p.x - a.x, apy = p.y - a.y; 2337 const ab2 = abx * abx + aby * aby || 1; 2338 let t = (apx * abx + apy * aby) / ab2; 2339 t = Math.max(0, Math.min(1, t)); 2340 const px = a.x + t * abx, py = a.y + t * aby; 2341 const dx = p.x - px, dy = p.y - py, d2 = dx * dx + dy * dy; 2342 if (d2 < best.d2) best = {k, px, py, d2}; 2343 } 2344 2345 if (best.k < 0) return null; 2346 const snap = _this.mapGl.unproject({ x: best.px, y: best.py }); 2347 return { lng: snap.lng, lat: snap.lat, d2px: best.d2 }; 2348 } 2349 const mode = _this.drawGl?.getMode?.(); 2350 2351 if (mode === 'draw_line_string') { 2352 // Pas de hover en cours de dessin 2353 _this.mapGl.getCanvas().style.cursor = 'crosshair'; 2354 _this.updateHelper(); 2355 return clearHoverRing(); 2356 } 2357 2358 let feats = _this.mapGl.queryRenderedFeatures(e.point); 2359 2360 if (feats.length === 0) { 2361 _this.mapGl.getCanvas().style.cursor = 'grab'; 2362 _this.updateHelper(); 2363 clearHoverRing(); 2364 return; 2365 } 2366 2367 const points = feats.filter(f => 2368 f?.properties?.meta === 'vertex' && 2369 (f.source && /mapbox-gl-draw/.test(f.source)) 2370 ); 2371 2372 _this.mapGl.getCanvas().style.cursor = 'pointer'; 2373 2374 if (points.length) { 2375 _this.helperTootlip.setContent(i18n("tooltip_msg_click_and_drag_to_move_or_del")) 2376 clearHoverRing(); 2377 return; 2378 } 2379 2380 const featUnactive = feats.filter(f => 2381 f?.properties?.meta === 'feature' && 2382 (f?.geometry?.type === 'MultiLineString' || f?.geometry?.type === 'LineString') && 2383 (f?.layer?.id === 'gl-draw-line-inactive' || f?.layer?.id === 'gl-draw-line-inactive-outline') && 2384 f?.properties?.active === 'false' && 2385 (f.source && /mapbox-gl-draw/.test(f.source)) 2386 ); 2387 if (featUnactive.length) { 2388 _this.helperTootlip.setContent(i18n("tooltip_msg_select_trace")) 2389 _this.mapGl.getCanvas().style.cursor = 'default'; 2390 } 2391 2392 feats = feats.filter(f => 2393 f?.properties?.meta === 'feature' && 2394 (f?.geometry?.type === 'MultiLineString' || f?.geometry?.type === 'LineString') && 2395 f?.layer?.id === 'gl-draw-line-active' && 2396 f?.properties?.active === 'true' && 2397 (f.source && /mapbox-gl-draw/.test(f.source)) 2398 ); 2399 2400 if (!feats.length) { 2401 clearHoverRing(); return; 2402 } 2403 2404 const line = feats[0]; 2405 2406 if (!line.geometry || (line.geometry.type !== 'MultiLineString' && line.geometry.type !== 'LineString')) { 2407 clearHoverRing(); 2408 return; 2409 } 2410 2411 let coordinates = []; 2412 if (line.geometry.type === 'MultiLineString') { 2413 coordinates = line.geometry.coordinates.reduce((acc, cur) => acc.concat(cur), []); 2414 } else { 2415 coordinates = line.geometry.coordinates; 2416 } 2417 2418 const snap = nearestSegmentAndPoint(_this.mapGl, coordinates, e.lngLat); 2419 if (!snap) return clearHoverRing(); 2420 2421 _this.helperTootlip.setContent(i18n("tooltip_msg_click_to_add_anchor_or_drag")) 2422 setHoverRing(snap.lng, snap.lat, line.properties.user_color); 2423 }); 2424 2425 this.mapGl.on('mouseleave', clearHoverRing); 2426 2427 this.mapGl.on('draw.modechange', function (e) { 2428 if(settings.isSimpleVersion) { 2429 if(e.mode == _this.drawGl.modes.DRAW_LINE_STRING 2430 && _this.path.getAll().length >= 1) { 2431 2432 iziToast.error({ 2433 message: i18n("toast_msg_simple_trace_limit"), 2434 displayMode: 'replace', 2435 }) 2436 2437 _this.drawGl.changeMode('simple_select', { 2438 featureIds: _this.path.getAll() 2439 }) 2440 2441 e.mode = _this.drawGl.modes.DRAW_LINE_STRING 2442 2443 }else if( _this.path.getAll().length === 0 2444 && e.mode == _this.drawGl.modes.SIMPLE_SELECT) { 2445 2446 _this.drawGl.changeMode('draw_line_string') 2447 e.mode = _this.drawGl.modes.DRAW_LINE_STRING 2448 2449 } 2450 } 2451 _this.activeMode(e.mode) 2452 }) 2453 2454 this.mapGl.on('draw.warning', function(e){ 2455 iziToast.warning({ 2456 displayMode: 'replace', 2457 message: e.msg 2458 }) 2459 _this.updateGroupActions() 2460 }) 2461 2462 var onConfirmDelete = function(msg, onConfirm, onReject){ 2463 iziToast.show({ 2464 titleSize:'18px', 2465 color: 'white', 2466 closeOnClick: false, 2467 progressBar: false, 2468 drag: false, 2469 timeout: false, 2470 close: false, 2471 overlay: true, 2472 title: i18n("toast_title_confirmation"), 2473 message: msg, 2474 position: 'center', 2475 buttons: [ 2476 [`<button class="btn s">${i18n("toast_button_yes")}</button>`, 2477 function(instance, toast) { 2478 if($.isFunction(onConfirm)){ 2479 onConfirm.call(this, instance, toast) 2480 } 2481 }, 2482 true 2483 ], 2484 [`<button class="btn e">${i18n("toast_button_no")}</button>`, 2485 function(instance, toast) { 2486 if($.isFunction(onReject)){ 2487 onReject.call(this, instance, toast) 2488 } 2489 }, 2490 false 2491 ] 2492 ] 2493 }) 2494 } 2495 2496 this.mapGl.on('draw.vertex.confirm.delete', function (e) { 2497 // Filtrage : évite les points invalides (points fantômes ou hors bornes) 2498 e.points = e.points.filter(p => 2499 Array.isArray(_this.store[p.parent]) && 2500 _this.store[p.parent][p.index] 2501 ); 2502 2503 if (e.points.length === 0) { 2504 console.warn('[draw.vertex.confirm.delete] Aucun point valide à supprimer, annulation.'); 2505 return; 2506 } 2507 2508 let nbWpts = 0; 2509 2510 // Comptage des waypoints sécurisés 2511
2511 e.points.forEach(p => { 2512 const pt = _this.store[p.parent][p.index]; 2513 if (pt && +pt.wpt === 1) nbWpts++; 2514 }); 2515 2516 const triggerDelete = function () { 2517 2518 // Regroupement des points par tracé pour suppression 2519 const formattedArr = {}; 2520 e.points.forEach(p => { 2521 if (!formattedArr[p.parent]) { 2522 formattedArr[p.parent] = []; 2523 } 2524 formattedArr[p.parent].push(p.index); 2525 }); 2526 2527 // Envoi des events DELETE_VERTEX pour chaque trace 2528 Object.entries(formattedArr).forEach(([parent, indexes]) => { 2529 _this.drawGl.fireEvent('DELETE_VERTEX', { 2530 points: indexes, 2531 parent: parent 2532 }); 2533 }); 2534 2535 _this.drawGl.renderCanvas(); 2536 }; 2537 2538 if (nbWpts > 0) { 2539 let _confirmMsg = nbWpts === 1 2540 ? i18n("toast_confirm_msg_one_waypoint") 2541 : i18n("toast_confirm_msg_multiple_waypoints"); 2542 2543 onConfirmDelete(_confirmMsg, 2544 function (inst, toast) { 2545 triggerDelete(); 2546 inst.hide({ transitionOut: 'fadeOut' }, toast); 2547 }, 2548 function (inst, toast) { 2549 inst.hide({ transitionOut: 'fadeOut' }, toast); 2550 _this.drawGl.renderCanvas(); 2551 }); 2552 2553 } else { 2554 triggerDelete(); 2555 } 2556 2557 }) 2558 2559 this.mapGl.on('draw.confirm.delete', function (e) { 2560 2561 onConfirmDelete( 2562 sprintf( 2563 i18n("toast_msg_confirm_delete_traces"), 2564 e.countPath, 2565 getS(e.countPath) 2566 ), 2567 function(inst, toast){ 2568 inst.hide({ transitionOut: 'fadeOut' }, toast); 2569 _this.drawGl[e.action]({ confirm: false }); 2570 }, 2571 function(inst, toast){ 2572 inst.hide({ transitionOut: 'fadeOut' }, toast); 2573 _this.drawGl.renderCanvas(); 2574 _this.updateGroupActions(); 2575 } 2576 ) 2577 2578 }) 2579 2580 this.mapGl.on('draw.delete', function (e) { 2581 2582 var ids=[]; 2583 2584 for(var i=0; i < e.features.length; i++){ 2585 var id = e.features[i].id 2586 // vérification si le tracé est dans le store 2587 if(!_this.store.hasOwnProperty(id)){ 2588 continue; 2589 } 2590 ids.push(id) 2591 } 2592 2593 if(!ids.length){ 2594 if(settings.isSimpleVersion && !_this.path.getAll().length){ 2595 setTimeout(function(){ 2596 _this.drawGl.changeMode('draw_line_string') 2597 _this.activeMode('draw_line_string') 2598 }, 0) 2599 } 2600 return 2601 } 2602 2603 _this.markers.delete(Array.prototype.concat.apply([], ids.map(function(key){ 2604 return _this.store[key].filter(function(p,i){ 2605 return p.wpt == 1 || i==0 || i == _this.store[key].length-1 2606 }).map(function(p){ 2607 return p.id 2608 }) 2609 }))) 2610 2611 for(var j=0; j < ids.length; j++){ 2612 $.each(['infos', 'diagrams'], function(i, tab){ 2613 _this.tab.deleteSlide(tab, ids[j], _this.swiper[tab]) 2614 }) 2615 } 2616 2617 // On attend pas la fin du traitement asynchrone à cause de la suppression des markers 2618 _this.path.deletePath(ids) 2619 // BUGFIX : Ne réinitialiser que si TOUS les tracés sont supprimés 2620 // Sinon, on conserve le contexte d'édition de la rando existante 2621 if (_this.path.getAll().length === 0) { 2622 settings.idRando = 0; 2623 settings.idRandoHash = ''; 2624 } 2625 _this.updateGroupActions() 2626 _this.updateHelper() 2627 _this.storePathsLocalStorage() 2628 2629 // WORKAROUND (pas le choix): Forcer la ré-application des couleurs après suppression 2630 setTimeout(() => { 2631 const savedColors = JSON.parse(localStorage.getItem('visorando_paths_colors') || '{}'); 2632 const remainingPathIds = _this.path.getAll({ saved: true }); 2633 remainingPathIds.forEach(pathId => { 2634 if (savedColors[pathId]) { 2635 _this.changePathColor(pathId, savedColors[pathId]); 2636 } 2637 }); 2638 if (_this.swiper.infos && _this.swiper.infos.pagination) { 2639 _this.swiper.infos.pagination.update(); 2640 } 2641 if (_this.swiper.diagrams && _this.swiper.diagrams.pagination) { 2642 _this.swiper.diagrams.pagination.update(); 2643 } 2644 _this.drawGl.renderCanvas(); 2645 }, 0); 2646 2647 iziToast.success({ 2648 displayMode: 'replace', 2649 message: sprintf( 2650 i18n("toast_msg_traces_deleted"), 2651 ids.length, 2652 getS(ids.length), 2653 getS(ids.length) 2654 ) 2655 }) 2656 2657 if(settings.isSimpleVersion && !_this.path.getAll().length){ 2658 _this.drawGl.changeMode('draw_line_string') 2659 _this.activeMode('draw_line_string') 2660 } 2661 2662 _this.history.operation(function () { 2663 return { 2664 store: Immutable.fromJS(_this.store), 2665 action: 'deletePath' 2666 }; 2667 }, sprintf( 2668 i18n("history_msg_delete_traces"), 2669 ids.length, 2670 getS(ids.length) 2671 )); 2672 }) 2673 this.setupVertexCreateHandler(); 2674 2675 this.mapGl.on('draw.vertex.delete', async function (e) { 2676 if (_this.vertexDeleteOnGoing) return; 2677 _this.vertexDeleteOnGoing = true; 2678 _this.beginBlockingOperation(); 2679 2680 var ids = []; 2681 2682 try { 2683 for (const [index, i] of e.points.entries()) { 2684 if(!_this.store[e.parent]){ 2685 return 2686 } 2687 2688 var point = _this.store[e.parent][i]; 2689 if (_get(point, 'id')) { 2690 const anchorIndices = _this.path.computeAnchorIndicesFromStore(e.parent); 2691 2692 const prevAnchor = _this.path.prevAnchor(i, anchorIndices); 2693 const nextAnchor = _this.path.nextAnchor(i, anchorIndices, _this.store[e.parent].length - 1); 2694 2695 if (prevAnchor === Number(i)) { 2696 // Si on supprime le premier point, on doit supprimer tous les points jusqu'à la prochaine ancre 2697 for (let j = nextAnchor - 1; j >= i; j--) { 2698 ids.push(_this.store[e.parent][j].id); 2699 _this.drawGl.deletePoint(e.parent, j); 2700 _this.path.deletePoint(e.parent, j); 2701 } 2702 } else if (nextAnchor === Number(i)) { 2703 // Si on supprime le dernier point, on doit supprimer tous les points jusqu'à l'avant-dernière ancre 2704 for (let j = i; j > prevAnchor; j--) { 2705 ids.push(_this.store[e.parent][j].id); 2706 _this.drawGl.deletePoint(e.parent, j); 2707 _this.path.deletePoint(e.parent, j); 2708 } 2709 } else if (_this.autoTracingEnabled) { 2710 // MODE AUTO : retracage via routing 2711 ids.push(_this.store[e.parent][i].id); 2712 _this.path.deletePoint(e.parent, i); 2713 // Avoid an intermediate straight-line render from MapboxDraw; autoTraceSegment rewrites Draw from _this.store. 2714 await _this.autoTraceSegment(e.parent, prevAnchor, nextAnchor - 1, null, true, true); 2715 } else { 2716 // MODE MANUEL : relier prevAnchor -> nextAnchor en ligne droite 2717 ids.push(_this.store[e.parent][i].id); 2718 _this.drawGl.deletePoint(e.parent, i); 2719 _this.path.deletePoint(e.parent, i); 2720 _this.manualReplotSegment(e.parent, prevAnchor, nextAnchor - 1, ids); 2721 } 2722 } 2723 } 2724
2725 var doneFunc = async function(){ 2726 if(ids.length){ 2727 _this.markers.delete(ids); 2728 _this.path.handleMarkersPath(true); 2729 2730 iziToast.success({ 2731 message: sprintf( 2732 i18n("toast_msg_points_deleted"), 2733 ids.length, 2734 getS(ids.length), 2735 getS(ids.length) 2736 ), 2737 displayMode: 'replace' 2738 }) 2739 2740 _this.path.computeOrder(e.parent) 2741 await _this.updateTracePostOperation(e.parent) 2742 2743 _this.history.operation(function () { 2744 return { 2745 store: Immutable.fromJS(_this.store), 2746 action: 'deletePoint' 2747 }; 2748 }, sprintf( 2749 i18n("history_msg_delete_points"), 2750 ids.length, 2751 getS(ids.length) 2752 )); 2753 2754 if(settings.isSimpleVersion && !_this.path.getAll().length){ 2755 _this.drawGl.changeMode('draw_line_string'); 2756 _this.activeMode('draw_line_string'); 2757 } 2758 }else{ 2759 if(settings.isSimpleVersion && !_this.path.getAll().length){ 2760 setTimeout(function(){ 2761 _this.drawGl.changeMode('draw_line_string') 2762 _this.activeMode('draw_line_string') 2763 }, 0) 2764 } 2765 } 2766 } 2767 2768 await doneFunc() 2769 } finally { 2770 _this.vertexDeleteOnGoing = false; 2771 _this.endBlockingOperation(); 2772 } 2773 }) 2774 2775 this.mapGl.on('draw.vertex.update', async function (e) { 2776 await _this.runBlockingOperation(async function() { 2777 const indexes = []; 2778 const skipAutoTrace = e.skipAutoTrace === true; 2779 for (const [index, i] of e.index.entries()) { 2780 const point = _this.store[e.parent][i]; 2781 indexes.push(i); 2782 point.lat = Number(e.points[index][1].toFixed(6)); 2783 point.lng = Number(e.points[index][0].toFixed(6)); 2784 if (_this.autoTracingEnabled && !skipAutoTrace) { 2785 const anchorIndices = _this.path.computeAnchorIndicesFromStore(e.parent); 2786 if (i == (_this.store[e.parent].length-1)) { 2787 await _this.autoTraceSegment(e.parent, _this.path.prevAnchor(i, anchorIndices), i, null, true, false); 2788 } else if (i == 0) { 2789 await _this.autoTraceSegment(e.parent, i, _this.path.nextAnchor(i, anchorIndices, _this.store[e.parent].length - 1), null, false, true); 2790 } else { 2791 await _this.autoTraceSegment(e.parent, _this.path.prevAnchor(i, anchorIndices), _this.path.nextAnchor(i, anchorIndices, _this.store[e.parent].length - 1), i, true, true); 2792 } 2793 } 2794 } 2795 2796 if (indexes.length) { 2797 _this.path.handleMarkersPath(true); 2798 if (!_this.autoTracingEnabled || skipAutoTrace) { 2799 await _this.getAltitudeForPoints(e.parent, indexes); 2800 } 2801 await _this.updateTracePostOperation(e.parent); 2802 2803 _this.history.operation(function () { 2804 return { 2805 store: Immutable.fromJS(_this.store), 2806 action: 'updatePoint' 2807 }; 2808 }, sprintf( 2809 i18n("history_msg_update_points"), 2810 indexes.length, 2811 getS(indexes.length) 2812 )); 2813 } 2814 }); 2815 }) 2816 2817 $.each($.merge(settings.groupActions.find('a'), $('#reset, #center')), function(){ 2818 2819 _this.markers.deleteById(null, true, 'here-indicator') 2820 var _that = this 2821 if($(this).is('[id]')){ 2822 $(this).on('click', async function (e, payload) { 2823 2824 e.preventDefault() 2825 e.stopPropagation() 2826 2827 if ($(this).is('[disabled]')) { 2828 return 2829 } 2830 2831 var isHistory = $(this).is('#undo, #redo'), 2832 showHistoryToast = function (t) { 2833 iziToast.info({ 2834 id: 'history', 2835 class: 'history', 2836 displayMode: 'once', 2837 timeout: t, 2838 message: i18n("toast_msg_sync_pending") 2839 }) 2840 } 2841 2842 // toutes les actions sauf undo & redo pendant une mutation en back de l'historique 2843 if (!isHistory && settings.mapEl.hasClass('loading')) { 2844 showHistoryToast(4000) 2845 return 2846 } 2847 2848 if (!isHistory && _this.isBlockingOperationOnGoing()) { 2849 iziToast.info({ 2850 message: i18n("toast_msg_action_pending"), 2851 displayMode: 'replace', 2852 timeout: 2000 2853 }) 2854 return 2855 } 2856 2857 if ($.ajaxq.isRunning('store')) { 2858 2859 if (isHistory) { 2860 2861 showHistoryToast(false) 2862 2863 $.ajaxq.getActiveRequest('store').done(function () { 2864 if ($.ajaxq.getCountPendingRequest('store') === 0) { 2865 iziToast.hide({transitionOut: 'fadeOut'}, document.querySelector('.iziToast.history')) 2866 } 2867 $(_that).trigger('click') 2868 }) 2869 return 2870 2871 } else { 2872 2873 iziToast.info({ 2874 message: i18n("toast_msg_action_pending"), 2875 displayMode: 'replace', 2876 timeout: 2000 2877 }) 2878 return 2879 2880 } 2881 } 2882 2883 if (_this.actions.hasOwnProperty(this.id)) { 2884 if (_this.actions[this.id].constructor.name === 'AsyncFunction') { 2885 await _this.runBlockingOperation(() => _this.actions[this.id].call(this, e, payload)); 2886 } else { 2887 _this.actions[this.id].call(this, e, payload) 2888 } 2889 _this.updateHelper() 2890 _this.updateGroupActions() 2891 } 2892 }) 2893 } 2894 }) 2895 2896 var mapReady = this.addAsyncTask(); // permet de savoir quand est-ce que la map est prête 2897 2898 // Note : highcharts.stock.min.js est chargé de manière synchrone dans le template HTML 2899 // (avant visorando-map.umd.js), comme dans showCarteDiagramme2.php. 2900 2901 // Migration MapController : la carte est déjà chargée après waitForLoad() 2902 // L'événement 'styledata' ne se déclenchera plus au démarrage â résoudre mapReady immédiatement 2903 if (_this.mapGl.isStyleLoaded()) { 2904 mapReady.resolve(); 2905 } else { 2906 // Fallback défensif si le style n'est pas encore prêt (ne devrait pas arriver) 2907 _this.mapGl.once('styledata', function () { 2908 var waiting = function () { 2909 if (!_this.mapGl.isStyleLoaded()) { 2910 setTimeout(waiting, 50); 2911 } else { 2912 mapReady.resolve(); 2913 } 2914 }; 2915 waiting(); 2916 }); 2917 } 2918 2919 2920 mapReady.done(function(){ 2921 // On met à jour l'url pour que si l'utilisateur fait un F5, il retrouve ses tracés 2922 if(History.enabled){ 2923 if(settings.idRando){ 2924 2925 iziToast.info({ 2926 message: i18n("toast_msg_loading_from_existing"), 2927 timeout: 2000 2928 }) 2929 2930 } 2931 }else{ 2932 iziToast.warning({ 2933 message: i18n("toast_msg_history_api_unsupported"), 2934 timeout: false 2935 }) 2936 } 2937 2938 var canvas = _this.mapGl.getCanvas() 2939 2940 _this.helperTootlip = tippy(settings.mapEl.get(0), { 2941 content: i18n("tooltip_msg_help_enabled"), 2942 allowHTML: true, 2943 offset: [0, 19], 2944 followCursor: true, 2945 triggerTarget: canvas, 2946 placement: 'right' 2947 }) 2948 }) 2949 2950 _this.mapGl.loadImage('/img/fiches/direction-symbol.png', function(error, image) { 2951 2952 if (error) return; 2953 2954 var layerDirectionOpts = { 2955 type: 'symbol', 2956 filter: ['all', 2957 ['==', 'active', 'false'], 2958 ['==', '$type', 'LineString'], 2959 ['!=', 'mode', 'static'] 2960 ], 2961 layout: { 2962 'symbol-placement': 'line', 2963 'symbol-spacing': [ 2964 'interpolate', 2965 ['exponential', 0.5], 2966 ['zoom'], 2967 10, 2968 140, 2969 18, 2970 200 2971 ], 2972 'icon-allow-overlap': true, 2973 'icon-image': 'direction', 2974 'icon-size': .27 2975 }, 2976 paint: { 2977 'icon-color': [ 2978 'case', 2979 ['has', 'user_color'], ['get', 'user_color'], 2980 ['has', 'color'], ['get', 'color'], 2981 settings.hexColor 2982 ], 2983 'icon-opacity': .7 2984 } 2985 } 2986 2987 _this.mapGl.addImage('direction', image, {sdf: true}); 2988 2989 var catchSourceExist = function(){ 2990 2991 if(typeof _this.mapGl.getSource('mapbox-gl-draw') === 'undefined'){ 2992 setTimeout(catchSourceExist, 200); 2993 }else{ 2994 _this.mapGl.addLayer(Object.assign({}, { 2995 id: 'direction', 2996 source: 'mapbox-gl-draw' 2997 }, layerDirectionOpts)); 2998 } 2999 3000 } 3001 3002 catchSourceExist() 3003 3004 }) 3005 3006 this.mapGl.once('mousemove', function() { 3007 $('.mouse-coordinates-indicator').removeClass('hidden') 3008 }); 3009 3010 // Affichage de la position et de l'altitude de la souris dans l'indicateur en haut à droite 3011 (function() { 3012 var getElevationAtPoint = (VisorandoMap && VisorandoMap.getElevationAtPoint) ? VisorandoMap.getElevationAtPoint : null; 3013 var lastElevTime = 0; 3014 var pendingElev = null; 3015 var elevAnimFrame = null; 3016 var currentLat = null; 3017 var currentLng = null; 3018 3019 var updateAlt = function(lat, lng) { 3020 if (!getElevationAtPoint) return; 3021 var zoom = Math.min(Math.floor(_this.mapGl.getZoom()), 17); 3022 getElevationAtPoint(lat, lng, zoom).then(function(elevation) { 3023 if (lat !== currentLat || lng !== currentLng) return; 3024 var $altEl = $('.mouse-coordinates-indicator .ci-alt'); 3025 if (elevation !== null && !isNaN(elevation)) { 3026 var alt = _this.convertAltitude(elevation); 3027 $altEl.text('Alt\u00a0: ' + Math.round(alt) + '\u00a0' + settings.currentUnits.altitude); 3028 } else { 3029 $altEl.text('Alt\u00a0: â'); 3030 } 3031 }).catch(function() { /* silent */ }); 3032 }; 3033 3034 _this.mapGl.on('mousemove', function(e) { 3035 currentLat = e.lngLat.lat; 3036 currentLng = e.lngLat.lng; 3037 3038 var $indicator = $('.mouse-coordinates-indicator'); 3039 if ($indicator.hasClass('hidden')) $indicator.removeClass('hidden'); 3040 3041 var coordText = _this.getFormattedCoordinates({ lat: currentLat, lng: currentLng }); 3042 var altInit = getElevationAtPoint ? 'Alt\u00a0: \u2026' : ''; 3043 $indicator.html( 3044 '<span class="ci-coords">' + coordText + '</span>' + 3045 (altInit ? '<span class="ci-alt">' + altInit + '</span>' : '') 3046 ); 3047 3048 if (!getElevationAtPoint) return; 3049 3050 // Throttle les requêtes altitude à 100 ms max 3051 var now = Date.now(); 3052 if (now - lastElevTime >= 100) {
3053 lastElevTime = now; 3054 updateAlt(currentLat, currentLng); 3055 } else { 3056 pendingElev = { lat: currentLat, lng: currentLng }; 3057 if (elevAnimFrame !== null) cancelAnimationFrame(elevAnimFrame); 3058 elevAnimFrame = requestAnimationFrame(function() { 3059 if (pendingElev) { 3060 var p = pendingElev; 3061 lastElevTime = Date.now(); 3062 updateAlt(p.lat, p.lng); 3063 pendingElev = null; 3064 } 3065 elevAnimFrame = null; 3066 }); 3067 } 3068 }); 3069 3070 _this.mapGl.on('mouseout', function() { 3071 if (elevAnimFrame !== null) { 3072 cancelAnimationFrame(elevAnimFrame); 3073 elevAnimFrame = null; 3074 } 3075 pendingElev = null; 3076 currentLat = null; 3077 currentLng = null; 3078 $('.mouse-coordinates-indicator').text('n.c'); 3079 }); 3080 })(); 3081 3082 var handleMarkersVisibility = function(){ 3083 if(_this.drawGl.getMode() == 'draw_line_string') return 3084 var state = _this.drawGl.getState() 3085 if(state.selectedCoordPaths.length){ 3086 setTimeout(function(){ 3087 if($.ajaxq.isRunning('store')){ 3088 $.ajaxq.getActiveRequest('store').always(function () { 3089 settings.mapEl.removeClass('hide-markers') 3090 }) 3091 }else{ 3092 settings.mapEl.removeClass('hide-markers') 3093 } 3094 }, 0) 3095 } 3096 } 3097 3098 // MapLibre GL v5 : map.on() retourne void â chaînage impossible, appels séparés 3099 this.mapGl.on('mousemove', function (){ 3100 if(_this.drawGl.getMode() == 'draw_line_string') return 3101 var state = _this.drawGl.getState() 3102 if(state.selectedCoordPaths.length && state.dragMoving && state.canDragMove){ 3103 settings.mapEl.addClass('hide-markers') 3104 } 3105 }) 3106 this.mapGl.on('touchmove', function (){ 3107 if(_this.drawGl.getMode() == 'draw_line_string') return 3108 var state = _this.drawGl.getState() 3109 if(state.selectedCoordPaths.length && state.dragMoving && state.canDragMove){ 3110 settings.mapEl.addClass('hide-markers') 3111 } 3112 }) 3113 this.mapGl.on('mouseup', handleMarkersVisibility) 3114 this.mapGl.on('touchend', handleMarkersVisibility) 3115 3116 this.mapGl.on('click', _this.updateHelper) 3117 this.mapGl.on('touchstart', _this.updateHelper) 3118 3119 this.mapGl.on('draw.selectionchange', function(features){ 3120 if (features.features !== undefined && features.features.length > 0) { 3121 let color = features.features[0].properties.color || features.features[0].properties.user_color; 3122 $('#color-picker-dropdown ul li').removeClass('active'); 3123 $('#color-picker-dropdown ul li a').each(function() { 3124 if ($(this).data('color') === color) { 3125 $(this).parent().addClass('active'); 3126 $('#color-picker-dropdown .dropdown-toggle .color-circle').
3126css('background-color', color); 3127 } 3128 }); 3129 } 3130 _this.updateHelper() 3131 $.each(['infos', 'diagrams'], function(i, tab){ 3132 _this.tab.handleSelection(tab, _this.swiper[tab]) 3133 }) 3134 }) 3135 3136 // Version la plus simple possible utilisant uniquement l'API publique 3137 this.drawGl.setFeatureCoordinates = function(id, coords) { 3138 try { 3139 // Récupérer le feature original 3140 const feature = this.get(id); 3141 if (!feature) return false; 3142 3143 // Sauvegarder le mode et les propriétés 3144 const wasInDirectSelect = this.getMode() === 'direct_select'; 3145 const properties = {...feature.properties}; 3146 3147 // Créer une copie du feature avec les nouvelles coordonnées 3148 const updatedFeature = { 3149 id: id, 3150 type: 'Feature', 3151 properties: properties, 3152 geometry: { 3153 type: 'LineString', 3154 coordinates: coords 3155 } 3156 }; 3157 3158 // Utiliser la méthode add qui remplacera automatiquement le feature existant 3159 this.add(updatedFeature); 3160 3161 // Restaurer le mode d'édition 3162 if (wasInDirectSelect) { 3163 setTimeout(() => { 3164 this.changeMode('direct_select', { featureId: id }); 3165 }, 20); 3166 } 3167 3168 return true; 3169 } catch (err) { 3170 console.error("Erreur dans setFeatureCoordinates:", err); 3171 return false; 3172 } 3173 }; 3174 3175 this.mapGl.addControl(this.drawGl); 3176 3177 // =================================================================== 3178 // COMPAT iOS: mapbox-gl-draw touch â mouse bridge 3179 // mapbox-gl-draw has two interaction paths: 3180 // - Mouse (mousedown/mouseup): ALWAYS works, even on empty space 3181 // (no feature required at the click point). 3182 // - Touch (touchstart/touchend): ONLY works if a draw feature 3183 // exists under the finger (queryRenderedFeatures in z.touch). 3184 // â The first point of a trace is NEVER placed through this path. 3185 // 3186 // On iOS, synthetic mouse events from touch may not be delivered 3187 // (touch-action:none, pointer events, etc.). 3188 // This bridge intercepts touch pointer events on the canvas and fires 3189 // mousedown/mouseup events on the map to use the mouse path of 3190 // mapbox-gl-draw, which always works. 3191 // =================================================================== 3192 (function(map, draw) { 3193 // Only needed on iOS where touch-action:none prevents synthetic mouse events 3194 var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); 3195 if (!isIOS) return; 3196 3197 var canvas = map.getCanvas(); 3198 var downEvent = null; // last touch pointerdown 3199 3200 function buildMapEvent(type, e) { 3201 var rect = canvas.getBoundingClientRect(); 3202 var x = e.clientX - rect.left; 3203 var y = e.clientY - rect.top; 3204 var point = new maplibregl.Point(x, y); 3205 var lngLat = map.unproject(point); 3206 return { 3207 type: type, 3208 point: point, 3209 lngLat: lngLat, 3210 originalEvent: e, 3211 target: map, 3212 preventDefault: function() { e.preventDefault(); }, 3213 defaultPrevented: false 3214 }; 3215 } 3216 3217 canvas.addEventListener('pointerdown', function(e) { 3218 if (e.pointerType !== 'touch') return; 3219 downEvent = { pointerId: e.pointerId, time: Date.now(), x: e.clientX, y: e.clientY }; 3220 // Fire mousedown on the map â draw records r = {time, point}
3221 map.fire('mousedown', buildMapEvent('mousedown', e)); 3222 }, { passive: true }); 3223 3224 canvas.addEventListener('pointermove', function(e) { 3225 if (e.pointerType !== 'touch' || !downEvent) return; 3226 if (e.pointerId !== downEvent.pointerId) return; 3227 // Fire mousemove so draw can detect a drag 3228 map.fire('mousemove', buildMapEvent('mousemove', e)); 3229 }, { passive: true }); 3230 3231 canvas.addEventListener('pointerup', function(e) { 3232 if (e.pointerType !== 'touch' || !downEvent) return; 3233 if (e.pointerId !== downEvent.pointerId) return; 3234 // Fire mouseup â draw detects a click (distance < 4px, duration < 500ms) 3235 // and calls a.click(e) â clickAnywhere â adds a point 3236 map.fire('mouseup', buildMapEvent('mouseup', e)); 3237 downEvent = null; 3238 }); 3239 3240 canvas.addEventListener('pointercancel', function(e) { 3241 if (e.pointerType !== 'touch') return; 3242 downEvent = null; 3243 }); 3244 })(this.mapGl, this.drawGl); 3245 3246 // COMPAT: mapbox-gl-draw vérifie classList[0]==="mapboxgl-canvas" dans son keydown handler, 3247 // mais MapLibre utilise "maplibregl-canvas" â la détection clavier native de la lib est cassée. 3248 // On reproduit ici le comportement exact du keydown de la lib (setState keyCode + trash confirm). 3249 $(document).on('keydown.draw-delete', function(e) { 3250 if ($(e.target).is(':input')) return; 3251 var kc = e.keyCode; 3252 if (kc === 46 || kc === 8) { // Delete ou Backspace 3253 e.preventDefault(); 3254 if (_this.vertexDeleteOnGoing || _this.isBlockingOperationOnGoing()) return; 3255 var state = _this.drawGl.getState(); 3256 if (state) state.keyCode = kc; 3257 _this.drawGl.trash({ confirm: true }); 3258 } 3259 }); 3260 3261 // Carte deja chargee apres waitForLoad() - executer le code d'init post-load 3262 _onMapLoaded(); 3263 3264 // ------------------------------------------------------------------- 3265 // Maintenir les couches MapboxDraw (gl-draw-*) toujours au-dessus 3266 // des overlays ajoutés dynamiquement par VisorandoMap. 3267 // styledata est émis après chaque modification de style (addLayer, moveLayerâ¦). 3268 // Le debounce via requestAnimationFrame évite les appels répétés sur un même frame. 3269 // ------------------------------------------------------------------- 3270 const _keepDrawLayersOnTop = (() => { 3271 let pending = false; 3272 return () => { 3273 if (pending) return; 3274 pending = true; 3275 requestAnimationFrame(() => { 3276 pending = false; 3277 const style = _this.mapGl.getStyle?.(); 3278 if (!style?.layers) return; 3279 style.layers 3280 .filter(l => l.id.startsWith('gl-draw-')) 3281 .forEach(l => { 3282 if (_this.mapGl.getLayer(l.id)) { 3283 _this.mapGl.moveLayer(l.id); 3284 } 3285 }); 3286 }); 3287 }; 3288 })(); 3289 _this.mapGl.on('styledata', _keepDrawLayersOnTop); 3290 3291 // NavigationControl et FullscreenControl geres par VisorandoMap 3292 // NavigationControl et FullscreenControl geres par VisorandoMap (showControls par defaut) 3293 3294 const $geoloc = $(settings.searchLocForm).find('.btn-geoloc'); 3295 const geolocate = new maplibregl.GeolocateControl({ 3296 positionOptions: { 3297 enableHighAccuracy: true 3298 }, 3299 trackUserLocation: false 3300 }); 3301 this.mapGl.addControl(geolocate); // requis pour brancher tout l'écosystème 3302 $geoloc.on('click', (e) => { 3303 e.preventDefault(); 3304 $geoloc.addClass('btn-loading'); 3305 geolocate.trigger(); 3306 }); 3307 geolocate.on('geolocate', e => { 3308 $geoloc.removeClass('btn-loading geoloc-error').addClass('geoloc-enabled'); 3309 }); 3310 geolocate.on('error', (e) => { 3311 $geoloc.removeClass('btn-loading geoloc-enabled').addClass('geoloc-error'); 3312 }); 3313 3314 this.mapGl.addControl({ 3315 onAdd: function() { 3316 var isOpen = false;
3317 var btn = document.createElement('button'); 3318 btn.className = 'maplibregl-ctrl-icon mapboxgl-ctrl-help'; 3319 btn.type = 'button'; 3320 btn.setAttribute('title', 'Aide contextuelle'); 3321 btn.setAttribute('aria-label', 'Aide contextuelle'); 3322 btn.onclick = function() { 3323 isOpen 3324 ? (document.body.classList.remove('panel-opened'), btn.classList.remove('active')) 3325 : (document.body.classList.add('panel-opened'), btn.classList.add('active')); 3326 isOpen = !isOpen; 3327 }; 3328 var container = document.createElement('div'); 3329 container.className = 'maplibregl-ctrl maplibregl-ctrl-group'; 3330 container.appendChild(btn); 3331 return container; 3332 }, 3333 onRemove: function() {} 3334 }); 3335 3336 // AttributionControl, ScaleControl et drawGl (doublon) supprimes - geres par VisorandoMap 3337 3338 settings.bottomRightBar.find('.switch-handler').on('click', function(){ 3339 var el = $(this).closest(settings.bottomRightBar.selector) 3340 el.toggleClass('pushed-down') 3341 _this.setCookie({ 'hide-tabs': el.is('.pushed-down') }) 3342 }) 3343 3344 _this.swiper.diagrams = new Swiper(settings.bottomRightBar.find('.swiper-diagrams'), { 3345 autoHeight: true, 3346 effect: 'fade', 3347 fadeEffect: { 3348 crossFade: true 3349 }, 3350 pagination: { 3351 el: '.swiper-pagination', 3352 clickable: true, 3353 renderBullet: function(i, className){ 3354 var $bullet = $('<span />', { class: className }) 3355 3356 if($(this.slides[i]).is('[id]')){ 3357 var pathId = $(this.slides[i]).attr('id').split('-').pop() 3358 var feature = _this.drawGl.get(pathId) 3359 3360 if (feature && feature.properties) { 3361 const bulletColor = feature.properties.user_color || feature.properties.color || settings.hexColor; 3362 $bullet.css('background-color', bulletColor) 3363 .attr('data-path-id', pathId); 3364 } 3365 } 3366 3367 return $bullet.prop('outerHTML'); 3368 } 3369 }, 3370 mousewheel: {
3371 forceToAxis: false 3372 }, 3373 on: { 3374 transitionEnd: function(){ 3375 this.updateAutoHeight(0) 3376 _this.swiper.tabs.updateAutoHeight(200) 3377 } 3378 } 3379 }) 3380 3381 _this.swiper.tabs = new Swiper(settings.bottomRightBar.find('.swiper-tabs'), { 3382 autoHeight: true, 3383 on: { 3384 init: function () { 3385 _this.swiper.infos = new Swiper(settings.bottomRightBar.find('.swiper-container.swiper-infos'), { 3386 autoHeight: true, 3387 effect: 'fade', 3388 nested: true, 3389 fadeEffect: { 3390 crossFade: true 3391 }, 3392 pagination: { 3393 el: '.swiper-pagination', 3394 clickable: true, 3395 renderBullet: function(i, className) { 3396 const $bullet = $('<span />', { class: className }); 3397 const slide = this.slides[i]; 3398 3399 if (!$(slide).is('[id]')) { 3400 return $bullet.prop('outerHTML'); 3401 } 3402 3403 const pathId = $(slide).attr('id').split('-').pop(); 3404 const feature = _this.drawGl.get(pathId); 3405 3406 if (!feature || !feature.properties) { 3407 return $bullet.prop('outerHTML'); 3408 } 3409 3410 // Détermine la couleur à appliquer. La priorité est donnée à `user_color` (choix manuel) 3411 // sur `color` (couleur automatique), avec une couleur par défaut en dernier recours. 3412 const bulletColor = feature.properties.user_color || feature.properties.color || settings.hexColor; 3413 3414 $bullet.css('background-color', bulletColor).attr('data-path-id', pathId); 3415 3416 return $bullet.prop('outerHTML'); 3417 } 3418 }, 3419 mousewheel: {
3420 forceToAxis: false 3421 } 3422 }) 3423 3424 // Synchro des deux instances 3425 _this.swiper.infos.controller.control = _this.swiper.diagrams 3426 _this.swiper.diagrams.controller.control = _this.swiper.infos 3427 3428 var selectPathOnPaginationClick = function(instance){ 3429 _this.drawGl.changeMode('simple_select', { 3430 featureIds: [$(instance.pagination.bullets).eq(instance.activeIndex).data('path-id')] 3431 }) 3432 _this.activeMode('simple_select') 3433 } 3434 3435 _this.swiper.diagrams.on('paginationClick', selectPathOnPaginationClick) 3436 _this.swiper.infos.on('paginationClick', selectPathOnPaginationClick) 3437 3438 } 3439 } 3440 }) 3441 3442 $('.js-cd-close', 'body').on('click', function(e){ 3443 e.preventDefault() 3444 $('.mapboxgl-ctrl-help').trigger('click') 3445 }) 3446 3447 $('.expand-handler', 'body').on('click', function(e){ 3448 3449 var el = $(this).next('.expand-content') 3450 el.toggleClass('s') 3451 $(this).toggleClass('active') 3452 3453 if (el.is('.s')) 3454 el.slideDownTransition(true); 3455 else 3456 el.slideUpTransition(true); 3457 }) 3458 3459 settings.bottomRightBar.find('.tab').on('click', function(){ 3460 _this.swiper.tabs.slideTo($(this).index()) 3461 _this.markers.deleteById(null, true, 'here-indicator') 3462 }) 3463 3464 _this.swiper.tabs.on('slideChangeTransitionEnd', function(){ 3465 settings.bottomRightBar.find('.tab').eq(_this.swiper.tabs.realIndex).siblings().removeClass('active') 3466 .end().addClass('active') 3467 }) 3468 3469 // quand toutes les tâches sont terminées 3470 $.when.apply($, this.asyncTasks).done(function() { 3471 if(settings.isCompatible){ 3472 settings.mapEl.removeClass('loading') // bye bye le loader 3473 }else{ 3474 settings.mapEl.addClass('freeze') 3475 iziToast.warning({ 3476 message: i18n("toast_msg_browser_incompatible"), 3477 timeout: false, 3478 overlay: true, 3479 close: false, 3480 closeOnClick: false, 3481 drag: false, 3482 position: 'center' 3483 }) 3484 } 3485 3486 settings.mapEl.addClass('done') 3487 3488 settings.topBar.prependTo(settings.mapEl) 3489 _this.asyncTasks = [] // clean tasks 3490 3491 _this.activeMode(_this.drawGl.getMode()) 3492 3493 $('body').addClass(settings.isSimpleVersion ? 'simple-version' : 'advanced-version') 3494 if(settings.user.showPub){ 3495 $('body').addClass('d-pub') 3496 } 3497 3498 // Si version simple, aide activée + affichage départs 3499 if(settings.isSimpleVersion){ 3500 // L'aide dégrade l'UX plutot que l'inverse sur mobile... 3501 if(settings.deviceType != 'mobile'){ 3502 _this.activeAction('help') 3503 } else { 3504 _this.helperTootlip.disable() 3505 tippySingleton.disable() 3506 } 3507 }else{ 3508 // Si version avancée, mesure en temps réel activée 3509 _this.activeAction('tape-measure'); 3510 _this.helperTootlip.disable() 3511 tippySingleton.disable() 3512 } 3513 3514 _this.swiper.diagrams.placeHolder = 3515 $(_this.swiper.diagrams.$wrapperEl).find('#placeholder-diagram').clone() 3516 3517 $(window).trigger('resize') 3518 3519 // Quand on n'a pas snowpack/webpack ... 3520 if(!$('head').find('#current-layer').length){ 3521 $('head').append('<style type="text/css" id="current-layer" >
3521.cd-panel .legend .icon{background-image: url('+settings.baseUrl+'templatesR/visorando/images/software/layers-legend/DEFAULT.jpg)}</style>') 3522 } 3523 3524 let fetchGeoJSON = new $.Deferred() 3525 // Si la rando a des points (de la bdd ou fichier de points .var sur le serveur) 3526 if(settings.idRando){ 3527 $.getJSON(settings.baseUrl+'index.php', 3528 {component: 'software', task: 'getAllPath', idRandonnee: settings.idRando}, 3529 function(res) { 3530 _this.processInitPaths(res.paths, [], mapReady, fetchGeoJSON, false); 3531 } 3532 ) 3533 } else { 3534 _this.getPathsLocalStorage(mapReady, fetchGeoJSON); 3535 } 3536 3537 _this.activeAction('go-to') 3538 _this.mapGl.resize() 3539 3540 3541 Offline.on('confirmed-up', function(){ 3542 settings.mapEl.removeClass('loading') 3543 }) 3544 3545 /* Offline relance les requêtes qui étaient en pending (et donc qui ont fail pendant la perte 3546 * de la connexion) lorsque la connexion est rétablie */ 3547 Offline.on('up', function(){ 3548 iziToast.success({ 3549 timeout: 5000, 3550 displayMode: 'replace', 3551 id: 'network', 3552 message: i18n("toast_msg_connection_restored") 3553 }) 3554 }) 3555 3556 Offline.on('confirmed-down', function(){ 3557 settings.mapEl.addClass('loading') 3558 iziToast.warning({ 3559 timeout: false, 3560 id: 'network', 3561 message: i18n("toast_msg_connection_lost") 3562 }) 3563 }) 3564 }) 3565 3566 booted = true 3567 3568 return this 3569 }, 3570 manualReplotSegment: function (idPath, startAnchor, endAnchor, idsOpt) { 3571 if (!Number.isFinite(startAnchor) || !Number.isFinite(endAnchor) || endAnchor - startAnchor <= 1) return; 3572 for (let j = endAnchor - 1; j > startAnchor; j--) { 3573 if (_this.store[idPath] && _this.store[idPath][j]) { 3574 if (idsOpt) idsOpt.push(_this.store[idPath][j].id); 3575 _this.drawGl.deletePoint(idPath, j); 3576 _this.path.deletePoint(idPath, j); 3577 } 3578 } 3579 _this.drawGl.renderCanvas(); 3580 }, 3581 actions: { 3582 redo: function(e){ 3583 if(!_this.history.hasRedo()) return 3584 _this.history.redo(e) 3585 }, 3586 undo: function(e){ 3587 if(!_this.history.hasUndo()) return 3588 _this.history.undo(e) 3589 }, 3590 draw: function(){ 3591 var newMode = _this.drawGl.getMode() == 'draw_line_string' ? 3592 'simple_select' : 3593 'draw_line_string' 3594 3595 _this.drawGl.changeMode(newMode) 3596 _this.activeMode(newMode) 3597 3598 if(settings.isSimpleVersion){ 3599 $(this).trigger('focusout') 3600 } 3601 }, 3602 delete: function(){ 3603 _this.drawGl.trash() 3604 }, 3605 continue: function(e){ 3606 if( $.inArray(_this.drawGl.getMode(), ['simple_select', 'direct_select']) != -1 ){ 3607 3608 _this.path.handleFallbackSelection(); 3609 3610 var lineString = _this.drawGl.getSelected(); 3611 3612 if(lineString.features.length > 1) { 3613 iziToast.info({ 3614 message: i18n("toast_msg_select_only_one_trace") 3615 }) 3616 return 3617 }else if(lineString.features.length == 1) { 3618 3619 $(this).trigger('focusout') 3620 3621 _this.drawGl.changeMode('draw_line_string', { 3622 featureId: lineString.features[0].id, 3623 from: 'end'
3624 }) 3625 _this.activeMode('draw_line_string') 3626 3627 return 3628 } 3629 } 3630 // On peut vouloir continuer un autre tracé alors qu'on est en train de faire un autre tracé ... 3631 if(_this.drawGl.getMode() == 'draw_line_string'){ 3632 return 3633 } 3634 3635 iziToast.info({ 3636 message: (_this.path.getAll().length === 0) 3637 ? i18n("toast_msg_no_trace_on_map") 3638 : i18n("toast_msg_select_trace_to_continue") 3639 }); 3640 3641 }, 3642 center: function(e, duration){ 3643 if(!_this.path.getAll().length){ 3644 iziToast.error({ 3645 message: i18n("toast_msg_centering_impossible") 3646 }) 3647 return 3648 } 3649 3650 var featureCollection = _this.drawGl.getAll() 3651 var filteredFeatures = featureCollection.features.filter(function(feature){ 3652 return $.inArray(feature.id, _this.path.getAll()) != -1 3653 }) 3654 featureCollection.features = filteredFeatures 3655 3656 _this.mapGl.fitBounds(turf.bbox(featureCollection), { 3657 padding: { 3658 top: 30, 3659 bottom:30, 3660 left: 30, 3661 right: 70 3662 }, 3663 duration: duration || 500 3664 }) 3665 }, 3666 'move-start': async function(e){ 3667 3668 var coordinates = turf.coordAll(_this.drawGl.getSelectedPoints()); 3669 3670 if (coordinates.length > 1) { 3671 iziToast.info({ 3672 message: i18n("toast_msg_select_only_one_point") 3673 }); 3674 return; 3675 } 3676 3677 if (!coordinates.length) { 3678 iziToast.info({ 3679 message: i18n("toast_msg_select_start_point") 3680 }); 3681 return; 3682 } 3683 3684 var idPath = _this.drawGl.getSelectedIds()[0], 3685 coordinates = _this.store[idPath] 3686 const feature = _this.drawGl.get(idPath); 3687 3688 const line = turf.lineString([coordinates[0], coordinates[coordinates.length - 1]].map(function(p){ 3689 return [p.lng, p.lat] 3690 })), 3691 length = turf.length(line), 3692 atIndex = _this.drawGl.getState().selectedCoordPaths[0]; 3693 3694 if (+atIndex === 0) { 3695 iziToast.info({ 3696 message: i18n("toast_msg_already_start_point") 3697 }); 3698 return; 3699 } 3700 3701 iziToast.success({ 3702 message: i18n("toast_msg_departure_moved") 3703 }); 3704 3705 if(!settings.groupActions.find('#display-start').is('.active')){ 3706 _this.actions['display-start'].call(settings.groupActions.find('#display-start').get(0), e) 3707 }else{ 3708 if(_this.drawGl.getSelectedIds().length > 0){ 3709 _this.drawGl.changeMode('simple_select') 3710 _this.activeMode('simple_select') 3711 } 3712 } 3713 3714 let coords = JSON.parse(JSON.stringify(coordinates)); 3715 let splice = coords.splice(0, +atIndex + 1); 3716 _this.store[idPath] = [].concat([splice[splice.length - 1]], coords, splice); 3717 3718 if (_this.autoTracingEnabled) { 3719 await _this.autoTraceSegment(idPath, coords.length, coords.length + 1, null, true, true); 3720 } else { 3721 _this.path.updateDrawGlCoordsWithPath([idPath]); 3722 } 3723 3724 _this.path.computeOrder(idPath) 3725 _this.path.handleMarkersPath(true) 3726 await _this.updateTracePostOperation(idPath); 3727 3728 _this.history.operation(() => { 3729 return { 3730 store: Immutable.fromJS(_this.store), 3731 action: 'moveDeparture' 3732 }; 3733 }, i18n("history_msg_move_departure")); 3734 }, 3735 'delete-last-point': function(){ 3736 3737 _this.path.handleFallbackSelection() 3738 3739 if (!_this.drawGl.deleteLastPoint()) { 3740 iziToast.error({ 3741 message: i18n("toast_msg_action_impossible_no_trace") 3742 }); 3743 } 3744 3745 }, 3746 'center-last-point': function(){ 3747 3748 $(this).toggleClass('active') 3749 3750 var switchText = $(this).data('toggle-text'); 3751 $(this).data('toggle-text', $(this._tippy.popper).text()) 3752 this._tippy.setContent(switchText) 3753 tippySingleton.setContent(switchText) 3754 3755 }, 3756 'point-coordinates': function () { 3757 var coordinates = turf.coordAll(_this.drawGl.getSelectedPoints()); 3758 3759 if (coordinates.length > 1) { 3760 iziToast.info({ 3761 message: i18n("toast_msg_select_only_one_point") 3762 }); 3763 return; 3764 } 3765 3766 if (!coordinates.length) { 3767 iziToast.info({ 3768 message: i18n("toast_msg_select_one_point") 3769 }); 3770 return; 3771 } 3772 3773 var point = coordinates.pop() 3774 3775 iziToast.info({ 3776 class: 'selectable-txt', 3777 timeout: false, 3778 closeOnClick: false, 3779 drag: false, 3780 message: _this.getFormattedCoordinates({lng: point[0], lat: point[1]}) 3781 }) 3782 }, 3783 'os-grid-ref': function () { 3784 var coordinates = turf.coordAll(_this.drawGl.getSelectedPoints()); 3785 3786 if (coordinates.length > 1) { 3787 iziToast.info({ 3788 message: i18n("toast_msg_select_only_one_point") 3789 }) 3790 return 3791 } 3792 3793 if (!coordinates.length) { 3794 iziToast.info({ 3795 message: i18n("toast_msg_select_one_point") 3796 }) 3797 return 3798 } 3799 3800 var point = coordinates.pop() 3801 3802 var showInfo = function (point) { 3803 var wgs84 = LatLon(point[1], point[0]); 3804 var gridref = OsGridRef.latLonToOsGrid(wgs84); 3805 3806 if (!gridref.toString().length) { 3807 iziToast.error({ 3808 message: i18n("toast_msg_os_grid_unavailable") 3809 }) 3810 return 3811 } 3812 3813 iziToast.info({ 3814 class: 'selectable-txt', 3815 timeout: false, 3816 closeOnClick: false, 3817 drag: false, 3818 message: gridref.toString() + ' / ' + gridref.toString(6) 3819 }) 3820 } 3821 3822 if (typeof OsGridRef != "undefined") { 3823 showInfo(point) 3824 } else { 3825 $.ajax({ 3826 url: '/libs/js/osgridref.min.js', 3827 cache: true,
3828 dataType: 'script' 3829 }).done(function () { 3830 showInfo(point) 3831 }) 3832 } 3833 }, 3834 'measure-rule': function(){ 3835 3836 var coordinates = turf.coordAll(_this.drawGl.getSelectedPoints()); 3837 3838 if (coordinates.length < 2) { 3839 iziToast.info({ 3840 message: i18n("toast_msg_select_at_least_two_points") 3841 }); 3842 return; 3843 } 3844 3845 var lineString = turf.lineString(coordinates), 3846 length = turf.length(lineString, { 3847 units: settings.currentUnits.distance == 'mi' ? 'miles' : 'kilometers' 3848 }); 3849 3850 iziToast.info({ 3851 message: sprintf( 3852 i18n("toast_msg_distance_computed"), 3853 length.toFixed(3), 3854 settings.currentUnits.distance 3855 ) 3856 }); 3857 }, 3858 cut: function(e){ 3859 3860 if(_this.drawGl.getMode() == 'draw_line_string'){ 3861 _this.drawGl.changeMode('simple_select') 3862 _this.activeMode('simple_select') 3863 } 3864 3865 var points = _this.drawGl.getSelectedPoints().features; 3866 3867 if (!points.length) { 3868 iziToast.info({ 3869 message: i18n("toast_msg_select_one_point") 3870 }); 3871 return; 3872 } else if (points.length > 1) { 3873 iziToast.info({ 3874 message: i18n("toast_msg_select_only_one_point_again") 3875 }); 3876 }else if(points.length === 1){ 3877 3878 var featuresCollection = _this.drawGl.getSelected(), 3879 coordinates = turf.coordAll(featuresCollection), 3880 featureId = featuresCollection.features.pop().id, 3881 point = turf.nearestPoint( 3882 points.pop(), 3883 turf.points(coordinates) 3884 ), 3885 cutAt = point.properties.featureIndex; 3886 3887 if ($.inArray(cutAt, [0, coordinates.length - 1]) !== -1) { 3888 iziToast.error({ 3889 message: i18n("toast_msg_cut_extremity_impossible") 3890 }); 3891 return; 3892 } 3893 3894 var newId = _this.drawGl.cutLineStringAt(cutAt, featureId) 3895 var id = _this.store[featureId][cutAt].id 3896 3897 var coord = _this.store[featureId].splice(cutAt + 1, _this.store[featureId].length - (cutAt + 1)); 3898 _this.store[newId] = coord 3899 3900 var cloneLastPoint = JSON.parse(JSON.stringify(_this.store[featureId][_this.store[featureId].length-1])); 3901 cloneLastPoint.order = 0 3902 3903 // BUGFIX : Conserver le contexte d'édition lors de la découpe 3904 // Ne pas réinitialiser idRando pour permettre la modification de la rando existante 3905 // settings.idRando = 0; 3906 // settings.idRandoHash = ''; 3907 3908 // inserer au début de coord, dernier point de l'ancien tracé 3909 _this.path.insertPoint(newId, 0, cloneLastPoint) 3910 _this.drawGl.changeMode('simple_select', { 3911 featureIds: [featureId] 3912 }) 3913 3914 iziToast.success({ 3915 message: i18n("toast_msg_cut_success") 3916 }) 3917 3918 _this.path.handleMarkersPath(true) 3919 _this.updateTracePostOperation(newId); 3920 _this.updateTracePostOperation(featureId); 3921 3922 _this.history.operation(() => { 3923 return { 3924 store: Immutable.fromJS(_this.store), 3925 action: 'cutPath' 3926 }; 3927 }, sprintf( 3928 i18n("history_msg_cut_trace"), 3929 1, 3930 getS(1) 3931 )) 3932 } 3933 }, 3934 merge: function(e){ 3935 var ids = _this.drawGl.merge(); 3936 3937 if (ids.oldIds.length < 2) { 3938 iziToast.info({ 3939 message: i18n("toast_msg_select_at_least_two_traces") 3940 }); 3941 return; 3942 } 3943 3944 // BUGFIX : Conserver le contexte d'édition lors de la fusion 3945 // Ne pas réinitialiser idRando pour permettre la modification de la rando existante 3946 // settings.idRando = 0; 3947 // settings.idRandoHash = ''; 3948 var coordinates = []; 3949 3950 for(var i=0; i < ids.oldIds.length; i++){ 3951 3952 $.each(['infos', 'diagrams'], function(j, tab){ 3953 _this.tab.deleteSlide(tab, ids.oldIds[i], _this.swiper[tab]) 3954 }) 3955
3956 coordinates = coordinates.concat(_this.store[ids.oldIds[i]]) 3957 delete _this.store[ids.oldIds[i]] 3958 3959 } 3960 3961 _this.store[ids.newId] = coordinates 3962 _this.path.computeOrder(ids.newId) 3963 _this.path.handleMarkersPath(true) 3964 3965 _this.updateTracePostOperation(ids.newId); 3966 3967 iziToast.success({ 3968 message: sprintf( 3969 i18n("toast_msg_merge_success"), 3970 ids.oldIds.length, 3971 getS(ids.oldIds.length) 3972 ) 3973 }); 3974 3975 _this.history.operation(function () { 3976 return { 3977 store: Immutable.fromJS(_this.store), 3978 action: 'mergePaths' 3979 }; 3980 }, sprintf( 3981 i18n("history_msg_merge_paths"), 3982 ids.oldIds.length, 3983 getS(ids.oldIds.length) 3984 )); 3985 }, 3986 'draw-close-path': function(){ 3987 3988 _this.path.handleFallbackSelection() 3989 3990 var selectedPath = _this.drawGl.getSelectedIds(), 3991 len = selectedPath.length, 3992 alreadyClosed = 0; 3993 3994 if(selectedPath.length === 0){ 3995 iziToast.info({ 3996 message: i18n("toast_msg_select_at_least_one_trace") 3997 }) 3998 return 3999 }else{ 4000 selectedPath.forEach(function(pathId){ 4001 var coordinates = _this.store[pathId], 4002 firstPoint = coordinates[0], 4003 lastPoint = coordinates[coordinates.length - 1]; 4004 4005 if(firstPoint.lat == lastPoint.lat && firstPoint.lng == lastPoint.lng){ 4006 alreadyClosed++ 4007 } 4008 }) 4009 } 4010 4011 if(alreadyClosed > 0){ 4012 var s = getS(alreadyClosed); 4013 iziToast.error({ 4014 message: sprintf( 4015 i18n("toast_msg_merge_already_closed_error"), 4016 alreadyClosed, getS(alreadyClosed), getS(alreadyClosed), getS(alreadyClosed), getS(alreadyClosed) 4017 ) 4018 }) 4019 return 4020 } 4021 4022 _this.drawGl.closePath() 4023 4024 if($.ajaxq.isRunning('store')){ 4025 $.ajaxq.getActiveRequest('store').done(function(){ 4026 iziToast.success({ 4027 message: sprintf( 4028 i18n("toast_msg_finish_success"), 4029 len, 4030 getS(len), // pour "tracé" â "tracés" 4031 getS(len), // pour "terminé" â "terminés" 4032 getS(len) // pour "fermé" â "fermés" 4033 ) 4034 }) 4035 }) 4036 } 4037 }, 4038 'draw-back-path': function(e){ 4039 4040 _this.path.handleFallbackSelection() 4041 4042 var selectedPath = _this.drawGl.getSelectedIds(), 4043 alreadyClosed = 0, 4044 lengths = {} 4045 4046 if(!selectedPath.length){ 4047 iziToast.info({ 4048 message: i18n("toast_msg_select_at_least_one_trace") 4049 }) 4050 return 4051 }else{ 4052 selectedPath.forEach(function(pathId){ 4053 var coordinates = _this.store[pathId], 4054 firstPoint = coordinates[0], 4055 lastPoint = coordinates[coordinates.length - 1]; 4056 4057 if(firstPoint.lat == lastPoint.lat && firstPoint.lng == lastPoint.lng){ 4058 alreadyClosed++ 4059 } 4060 }) 4061 } 4062 4063 // On empêche l'action si un des tracés est déjà fermé 4064 if(alreadyClosed > 0){ 4065 var s = getS(alreadyClosed); 4066 iziToast.error({ 4067 message: sprintf( 4068 i18n("toast_msg_already_closed_error"), 4069 alreadyClosed, 4070 getS(alreadyClosed), // â tracé[s] 4071 getS(alreadyClosed), // â terminé[s] 4072 getS(alreadyClosed), // â fermé[s] 4073 getS(alreadyClosed) // â trouvé[s] 4074 ) 4075 }) 4076 return 4077 } 4078 4079 var ids = _this.drawGl.addBackPath() 4080 4081 _this.path.getAll({saved: true}).map(function(id){ 4082 lengths[id] = _this.store[id].length 4083 }) 4084 4085 // Pour chaque tracé... 4086 for(var i=0; i < ids.length; i++){ 4087 var backPath = JSON.parse(JSON.stringify(_this.store[ids[i]])).reverse() 4088 backPath.shift() 4089 4090 var length = _this.store[ids[i]].length 4091 4092 backPath = backPath.map(function(p){ 4093 return { 4094 lng: p.lng, 4095 lat: p.lat, 4096 altitude: p.altitude 4097 } 4098 }) 4099 4100 _this.store[ids[i]] = _this.store[ids[i]].concat(backPath) 4101 _this.path.computeOrder(ids[i]) 4102 4103 // Pour chaque nouveau point constituant le chemin du retour... 4104 for (var j=length; j < _this.store[ids[i]].length; j++){ 4105 // J'avoue c'est pas simple à lire... mais ai-je le choix ? 4106 _this.path.updatePoint(ids[i], j, { 4107 id: _this.path.generateUUIDv4() 4108 }) 4109 } 4110 _this.path.handleMarkersPath(true) 4111 _this.updateTracePostOperation(ids[i]); 4112 } 4113 4114 iziToast.success({ 4115 message: sprintf( 4116 i18n("toast_msg_back_success"), 4117 getS(ids.length), // â pour "Retour" â "Retours" 4118 getS(ids.length) // â pour "tracé" â "tracés" 4119 ) 4120 }) 4121 4122 _this.history.operation(() => { 4123 return { 4124 store: Immutable.fromJS(_this.store), 4125 action: 'backPath' 4126 }; 4127 }, sprintf( 4128 i18n("history_msg_back_path"), 4129 ids.length, 4130 getS(ids.length) 4131 )) 4132 }, 4133 'reverse-direction': function(e){ 4134 4135 _this.path.handleFallbackSelection() 4136 4137 var ids = _this.drawGl.reversePath() 4138 _this.path.handleMarkersPath(true) 4139 4140 if(!ids.length){ 4141 iziToast.info({ 4142 message: i18n("toast_msg_select_at_least_one_trace") 4143 }) 4144 return 4145 } 4146 for(var i=0; i < ids.length; i++){ 4147 _this.store[ids[i]].reverse() 4148 _this.path.computeOrder(ids[i]) 4149 _this.updateTracePostOperation(ids[i]); 4150 } 4151 4152 _this.path.handleMarkersPath(true) 4153 4154 iziToast.success({ 4155 displayMode: 'replace', 4156 message: sprintf(i18n("toast_msg_reverse_success"), getS(ids.length), getS(ids.length)) 4157 }) 4158 _this.history.operation(() => { 4159 return { 4160 store: Immutable.fromJS(_this.store), 4161 action: 'reversePath' 4162 }; 4163 }, sprintf( 4164 i18n("history_msg_reverse_path"), 4165 ids.length, 4166 getS(ids.length) 4167 )); 4168 }, 4169 reset: function(){ 4170 var ids = _this.path.getAll(); 4171 4172 if(!ids.length){ 4173 iziToast.info({ 4174 message: i18n("toast_msg_no_trace_to_delete") 4175 }) 4176 return 4177 } 4178 4179 iziToast.show({ 4180 titleSize:'18px', 4181 color: 'white', 4182 closeOnClick: false, 4183 progressBar: false, 4184 drag: false, 4185 timeout: false, 4186 close: false, 4187 overlay: true, 4188 title: i18n("toast_title_confirmation"), 4189 message: i18n("toast_msg_confirm_delete_all"), 4190 position: 'center', 4191 buttons: [ 4192 [`<button class="btn s">${i18n("toast_button_yes")}</button>`, 4193 function(instance, toast) { 4194 instance.hide({transitionOut: 'fadeOut'}, toast); 4195 _this.drawGl.delete(ids, {silent: false}); 4196 }, 4197 true 4198 ], 4199 [`<button class="btn e">${i18n("toast_button_no")}</button>`, 4200 function(instance, toast) { 4201 instance.hide({transitionOut: 'fadeOut'}, toast) 4202 }, 4203 false 4204 ] 4205 ] 4206 }) 4207 }, 4208 'go-to': function(){ 4209 var el = settings.searchLocForm 4210 4211 el.toggleClass('s') 4212 $(this).toggleClass('active') 4213 4214 if (el.is('.s')) 4215 el.slideDownTransition(true); 4216 else 4217 el.slideUpTransition(true); 4218 4219 /* bsTransitionEnd est géré par boostrap pour assurer un support minimal pour transitionend 4220 car chaque navigateur a fait sa soupe évidemment... */ 4221 el.one('bsTransitionEnd', function(){ 4222 if(el.hasClass('s')){ 4223 el.find('input[type=search]').get(0).focus() 4224 } 4225 }) 4226 }, 4227 'import-gpx': function(e){ 4228 4229 var _that = this 4230 4231 if(typeof Dropzone == 'function'){ 4232 4233 Dropzone.autoDiscover = false;
4234 4235 $.fancybox.open('<div><form class="drop-box dropzone flex">\n' + 4236 '<div class="dz-default dz-message">\n' + 4237 '<button class="dz-button" type="button">' + sprintf(i18n("dropzone_upload_msg"), "6MB") + '</button>' + 4238 '<div class="loader hide">0%</div>' + 4239 '</div>\n' + 4240 '<div class="fallback">\n' + 4241 '<input type="file" />\n' + 4242 '</div>\n' + 4243 '</form>' + 4244 '<div class="xs:mt-4 xs:spacey-2 md:mt-8">' + 4245 '<div class="form-switch">' + 4246 '<input type="checkbox" id="import-gpx-altitude" name="altitude" value="1" checked="checked" />' + 4247 '<label for="import-gpx-altitude">' + i18n("gpx_import_option_altitude") + '</label>' + 4248 '</div>' + 4249 '<div class="form-switch">' + 4250 '<input type="checkbox" id="import-gpx-waypoints" name="waypoints" value="1" checked="checked" />' + 4251 '<label for="import-gpx-waypoints">' + i18n("gpx_import_option_waypoints") + '</label>' + 4252 '</div>' + 4253 '</div>' + 4254 '</div>', 4255 { 4256 touch: false, 4257 baseClass: 'dropzone-layout', 4258 animationEffect: 'material' 4259 }); 4260 4261 var dropzone = $('.drop-box').dropzone({ 4262 url: settings.baseUrl+'index.php?'+$.param({ 4263 component:'software', 4264 task:'importGPX', 4265 idRandonnee: settings.idRando 4266 }), 4267 maxFilesize: 6, // MB 4268 paramName: 'gpx', 4269 previewsContainer: false, 4270 acceptedFiles: 'application/gpx+xml, .gpx', 4271 parallelUploads: 1, 4272 createImageThumbnails: false, // pas de gestion d'image 4273 timeout: 30000 // augmente le timeout si certains GPX sont lents 4274 }).get(0).dropzone; 4275 var importGpxBlockingStarted = false; 4276 4277 dropzone.on('sending', function(file, xhr, formData){ 4278 if (!importGpxBlockingStarted) { 4279 importGpxBlockingStarted = true; 4280 _this.beginBlockingOperation(); 4281 } 4282 formData.append('altitude', $('#import-gpx-altitude').is(':checked') ? 1 : 0); 4283 formData.append('waypoints', $('#import-gpx-waypoints').is(':checked') ? 1 : 0); 4284 $('#import-gpx-altitude, #import-gpx-waypoints').prop('disabled', true); 4285 }); 4286 4287 dropzone.on('queuecomplete', function(){ 4288 $('#import-gpx-altitude, #import-gpx-waypoints').prop('disabled', false); 4289 if (importGpxBlockingStarted) { 4290 importGpxBlockingStarted = false; 4291 _this.endBlockingOperation(); 4292 } 4293 }); 4294 4295 dropzone.on('totaluploadprogress', function(p){ 4296 $(this.element).find('.loader').text(Math.round(p)+'%') 4297 }) 4298 4299 dropzone.on('addedfile', function(){ 4300 $(this.element).find('.dz-message > *').addClass('hide') 4301 .not('.dz-button').removeClass('hide') 4302 }) 4303 4304 dropzone.on('queuecomplete', function(){ 4305 const element = this.element 4306 4307 const successfulFiles = dropzone.files.filter(file => file.status === 'success') 4308 4309 if (successfulFiles.length > 0) { 4310 iziToast.info({ 4311 message: i18n("toast_msg_upload_complete"), 4312 timeout: 3000, 4313 onOpened: function(){ 4314 if(dropzone.getQueuedFiles().length || dropzone.getUploadingFiles().length) return 4315 $(element).find('.dz-message > *').addClass('hide') 4316 .end().find('.dz-message > *').not('.loader').removeClass('hide') 4317 } 4318 }) 4319 } 4320 4321 // Nettoyage 4322 this.removeAllFiles() 4323 $.fancybox.close() 4324 4325 if (!successfulFiles.length) return 4326 4327 _this.actions.center(null, 1) 4328 _this.history.operation(function() { 4329 return { 4330 store: Immutable.fromJS(_this.store), 4331 action: 'importGPX' 4332 }; 4333 }, i18n("history_msg_import_gpx")) 4334 }) 4335 4336 dropzone.on('error', function(file, errorMessage){ 4337 let errorText = '' 4338 4339 if (typeof errorMessage === 'object' && errorMessage !== null && errorMessage.hasOwnProperty('error')) { 4340 // Réponse JSON structurée venant du serveur 4341 errorText = escapeHtml(errorMessage.error) 4342 } else if (typeof errorMessage === 'string') { 4343 // Texte brut d'erreur 4344 errorText = escapeHtml(errorMessage) 4345 } else { 4346 // Fallback générique 4347 errorText = i18n("toast_msg_upload_error", escapeHtml(file.name)) 4348 } 4349 4350 iziToast.error({ 4351 message: errorText, 4352 timeout: 8000 4353 }) 4354 }) 4355 4356 // Pour chaque fichier(s) 4357 dropzone.on('success', function(file, json){ 4358 4359 try { 4360 // On réinitialise 4361 sessionStorage.setItem("stoGpxTitle", ""); 4362 sessionStorage.setItem("stoGpxDescription", ""); 4363 4364 const paths = json.paths || {}; 4365 const pathIds = Object.keys(paths); 4366 let totalPoints = 0; 4367 4368 pathIds.forEach(id => { 4369 totalPoints += Array.isArray(paths[id]) ? paths[id].length : 0; 4370 }); 4371 4372 // Aucune trace importée ou aucun point dans les traces 4373 if (pathIds.length === 0 || totalPoints === 0) { 4374 iziToast.error({ 4375 message: i18n('gpx_import_error_no_points_found'), 4376 timeout: 6000 4377 }); 4378 return; 4379 } 4380 4381 // Avertissement si trace tronquée (le serveur fournit metaData.totalPoints et nbPointsMax) 4382 if (json.metaData && typeof json.metaData.totalPoints === 'number' && typeof json.metaData.nbPointsMax === 'number') { 4383 if (json.metaData.totalPoints > json.metaData.nbPointsMax) { 4384 iziToast.warning({ 4385 message: sprintf(i18n('gpx_import_warning_truncation'), json.metaData.nbPointsMax), 4386 timeout: 6000 4387 }); 4388 } 4389 } 4390 4391 // Récupération depuis le JSON (issu du GPX parser) 4392 if (json.name !== undefined) { 4393 sessionStorage.setItem("stoGpxTitle", Array.isArray(json.name) ? json.name.join(" - ") : json.name); 4394 } 4395 4396 if (json.desc !== undefined) { 4397 sessionStorage.setItem("stoGpxDescription", Array.isArray(json.desc) ? json.desc.join("\n") : json.desc); 4398 } 4399 } catch (error) { 4400 console.error(error); 4401 } 4402 4403 // BUGFIX : Ne pas réinitialiser le contexte d'édition lors d'un import GPX. 4404 // Si une rando est ouverte en édition (settings.idRando > 0), le serveur 4405 // assignera le 1er tracé à la rando existante et
4405créera de nouvelles randos 4406 // pour les tracés supplémentaires (même logique que pour le tracé manuel). 4407 // settings.idRando = 0; 4408 // settings.idRandoHash = ''; 4409 4410 if(_this.drawGl.getMode() != _this.drawGl.modes.SIMPLE_SELECT){ 4411 _this.drawGl.changeMode('simple_select') 4412 _this.activeMode('simple_select') 4413 } 4414 4415 $.each(json.paths, function(pathId, points){ 4416 4417 var path = turf.feature({ 4418 type: 'LineString', 4419 coordinates: [] 4420 }) 4421 4422 // skip iteration 4423 if(points.length < 2) { 4424 // On vérifie que metaData existe avant de l'utiliser 4425 if (json.metaData) { 4426 delete json.metaData[pathId] 4427 } 4428 return true 4429 } 4430 4431 // Par convention et pour maintenir une unicité dans drawGl 4432 path.id = pathId 4433 4434 // Pour chaque points du tracé courant... 4435 $.each(points, function(order, point){ 4436 point.order = +order 4437 _this.path.insertPoint(pathId, order, point) 4438 path.geometry.coordinates.push([point.lng, point.lat]) 4439 }) 4440 4441 _this.drawGl.add(path) 4442 _this.updateTracePostOperation(pathId) 4443 }) 4444 4445 _this.path.handleMarkersPath(true) 4446 }) 4447 4448 return 4449 } 4450 4451 settings.mapEl.addClass('loading high-z-index') 4452 4453 // Load async, optimisation à défaut de ne pas avoir webpack 4454 $.when.apply($, [ 4455 $.getCSS(['/templatesR/visorando/css/dropzone.min.css'], null, true), 4456 $.getScript('/libs/js/dropzone.min.js') 4457 ]).done(function(){ 4458 settings.mapEl.removeClass('loading high-z-index') 4459 _this.actions['import-gpx'].call(_that, e) 4460 }) 4461 }, 4462 'export-gpx': function(){ 4463 4464 if(_this.drawGl.getMode() === 'draw_line_string'){ 4465 iziToast.error({ 4466 message: i18n("toast_msg_export_unfinished_trace") 4467 }) 4468 return 4469 } 4470 4471 let nbFeatures = _this.path.getAll({saved: true}).length; 4472 if(nbFeatures > 1){ 4473 iziToast.error({ 4474 timeout: 10000, 4475 message: i18n("toast_msg_export_multiple_traces_error") 4476 }) 4477 return 4478 } 4479 4480 var idsPathSelected = _this.drawGl.getSelectedIds(), 4481 internalIdsPath = _this.path.getAll({saved: true}), 4482 idsPathForExport = null; 4483 4484 if(internalIdsPath.length > 0 && idsPathSelected.length === 0){ 4485 idsPathForExport = internalIdsPath 4486 }else if(idsPathSelected.length && internalIdsPath.length){ 4487 idsPathForExport = idsPathSelected 4488 }else if(!internalIdsPath.length){ 4489 iziToast.error({ 4490 message: i18n("toast_msg_no_trace_to_export"), 4491 timeout: 2000 4492 }) 4493 return 4494 } 4495 4496 // On est jamais trop prudent 4497 if(!idsPathForExport){ 4498 iziToast.error({ 4499 message: i18n("toast_msg_export_failed"), 4500 timeout: 2000 4501 }) 4502 return 4503 } 4504 4505 // Sauvegarde manuelle "programmatically" 4506 settings.groupActions.find('#save').trigger('click', [{preventConfirm: true}]) 4507 4508 $.ajaxq.getActiveRequest('store').done(function(res){ 4509 4510 if(res.error){ 4511 iziToast.error({ 4512 id: 'error', 4513 displayMode: 'replace', 4514 message: res.error 4515 }) 4516 return 4517 } 4518 4519 var a = document.createElement('a'); 4520 a.href = settings.baseUrl+'index.php?'+$.param({ 4521 component:'rando', 4522 task:'gpxRandoPre', 4523 idRandonnee: settings.idRandoHash 4524 }); 4525 document.body.append(a); 4526 a.click(); 4527 a.remove(); 4528 4529 }).fail(function(){ 4530 // Les causes peuvent être diverses...(ex: utilisateur déconnecté) 4531 iziToast.error({ 4532 message: i18n("toast_msg_gpx_generation_failed") 4533 }) 4534 }) 4535 }, 4536 'hide-txt-button': function(){ 4537 4538 var switchText = $(this).data('toggle-text'), 4539 switchTooltipText = $(this).data('toggle-text-tooltip'); 4540 4541 $(this).data('toggle-text', $(this).text()).data('toggle-text-tooltip', $(this._tippy.popper).text()) 4542 $(this).toggleClass('active').find('span').text(switchText) 4543 4544 this._tippy.setContent(switchTooltipText) 4545 tippySingleton.setContent(switchTooltipText) 4546 4547 settings.groupActions.addClass('mod').toggleClass('only-icon-mode') 4548 4549 if(settings.groupActions.is('.only-icon-mode')){ 4550 4551 $.each(settings.groupActions.find('.nav__dropdown > *'), function(){ 4552 priorityNav.toMenu(settings.groupActions.get(0), settings.groupActions.attr('instance')) 4553 }); 4554 4555 var toggleButton = settings.groupActions.find('.priority-nav__dropdown-toggle') 4556 if(toggleButton.is('.is-open')){ 4557 toggleButton.trigger('click') 4558 } 4559 } 4560 4561 tippy.hideAll({duration: 0}) 4562 4563 var catchEndFit = new $.Deferred() 4564 priorityNav.doesItFit(settings.groupActions.get(0), catchEndFit) 4565 4566 catchEndFit.then(function(){ 4567 settings.groupActions.removeClass('mod') 4568 $.each(settings.groupActions.find('.nav > *'), function(){ 4569 var newProps = { 4570 placement: 'bottom' 4571 } 4572 this._tippy.setProps(newProps) 4573 tippySingleton.setProps(newProps) 4574 }) 4575 }) 4576 4577 _this.setCookie({ 'hide-txt': settings.groupActions.is('.only-icon-mode') }) 4578 }, 4579 'switch-version': function(){ 4580 4581 if(!settings.user.hasPremiumAccess && !$(this).is('.active')){ 4582 4583 _paq.push(['trackEvent', 4584 'Logiciel simple', 4585 'Vers logiciel avancé', 4586 'Click on link that redirect to club page', 4587 ]); 4588 4589 Object.assign(document.createElement('a'), { 4590 target: '_blank', 4591 href: settings.clubUrl 4592 }).click() 4593 4594 return 4595 } 4596 4597 var elContext = this, 4598 run = function(reset) { 4599 var switchText = $(elContext).data('toggle-text'), 4600 switchTooltipText = $(elContext).data('toggle-text-tooltip'); 4601 4602 $(elContext).data('toggle-text', $(elContext).text()).data('toggle-text-tooltip', $(elContext._tippy.popper).text()) 4603 $(elContext).toggleClass('active').find('span').text(switchText) 4604 4605 elContext._tippy.setContent(switchTooltipText) 4606 tippySingleton.setContent(switchTooltipText) 4607 4608 settings.groupActions.addClass('mod').find('.advanced, .not-advanced').toggleClass('hide') 4609 settings.isSimpleVersion = !$(elContext).is('.active') 4610 4611 $('body').removeClass('simple-version advanced-version') 4612 .addClass(settings.isSimpleVersion ? 'simple-version' : 'advanced-version') 4613
4614 if(settings.isSimpleVersion && !settings.groupActions.find('#hide-txt-button').is('.active')){ 4615 settings.groupActions.find('#hide-txt-button').trigger('click') 4616 } 4617 4618 // if(settings.isSimpleVersion && !settings.groupActions.find('#go-to').is('.active')){ 4619 // _this.activeAction('go-to') 4620 // } 4621 4622 _this.drawGl.setBlurSelection(!settings.isSimpleVersion) 4623 4624 _this.setCookie({ 'advanced': !settings.isSimpleVersion }) 4625 4626 _this.updateTopBar() 4627 4628 if(reset){ 4629 // Nettoyage onglets 4630 var ids = _this.path.getAll() 4631 for (var i = 0; i < ids.length; i++) { 4632 $.each(['infos', 'diagrams'], function (p, tab) { 4633 _this.tab.deleteSlide(tab, ids[i], _this.swiper[tab]) 4634 }) 4635 } 4636 4637 // Nettoyage map 4638 _this.drawGl.delete(ids, {silent: false}); 4639 4640 var currentRequest = new $.Deferred() 4641 4642 setTimeout(function(){ currentRequest.resolve($.ajaxq.getActiveRequest('store')) }, 1) 4643 4644 settings.idRando = 0; 4645 settings.idRandoHash = ''; 4646 4647 currentRequest.done(function(xhr){ 4648 4649 if(!xhr) return 4650 xhr.done(function(){ 4651 // Reset le store 4652 _this.store = {} 4653 4654 iziToast.success({ 4655 displayMode: 0, 4656 message: i18n("toast_msg_instance_created") 4657 }) 4658 4659 }).always(function(){ 4660 settings.mapEl.removeClass('loading') 4661 // MAJ barre d'actions 4662 _this.updateGroupActions() 4663 // MAJ des helpers 4664 _this.updateHelper() 4665 }) 4666 4667 }) 4668 4669 return currentRequest 4670 4671 }else{ 4672 // MAJ barre d'actions 4673 _this.updateGroupActions() 4674 // MAJ des helpers 4675 _this.updateHelper() 4676 } 4677 } 4678 4679 // Si on veut aller vers la version simple & plus d'un tracé 4680 if(!settings.isSimpleVersion && _this.path.getAll().length > 1){ 4681 4682 iziToast.show({ 4683 titleSize:'18px', 4684 color: 'white', 4685 closeOnClick: false, 4686 progressBar: false, 4687 drag: false, 4688 timeout: false, 4689 close: false, 4690 overlay: true, 4691 title: i18n("toast_title_confirmation"), 4692 message: i18n("toast_msg_simple_mode_reset_warning"), 4693 position: 'center', 4694 buttons: [ 4695 [`<button class="btn s">${i18n("toast_button_yes")}</button>`, 4696 function(instance, toast) { 4697 instance.hide({transitionOut: 'fadeOut'}, toast) 4698 run(true).then(function(xhr){ 4699 4700 if(!xhr) return 4701 4702 xhr.done(function(){ 4703 _this.drawGl.changeMode('draw_line_string') 4704 _this.activeMode('draw_line_string') 4705 }) 4706 4707 }) 4708 }, 4709 true 4710 ], 4711 [`<button class="btn e">${i18n("toast_button_no")}</button>`, 4712 function(instance, toast) { 4713 instance.hide({transitionOut: 'fadeOut'}, toast) 4714 }, 4715 false 4716 ] 4717 ] 4718 }) 4719 4720 }else{ 4721 4722 run(false) 4723 4724 if(settings.isSimpleVersion){ 4725 if(_this.path.getAll().length >= 1){ 4726 _this.drawGl.changeMode('simple_select', { 4727 featureIds: _this.path.getAll() 4728 }) 4729 _this.activeMode('simple_select') 4730 }else { 4731 _this.drawGl.changeMode('draw_line_string') 4732 _this.activeMode('draw_line_string') 4733 } 4734 } 4735 } 4736 }, 4737 'display-start': function(){ 4738 4739 if(_this.path.getAll().length > 0){ 4740 _this.drawGl.changeMode('simple_select') 4741 _this.activeMode('simple_select') 4742 } 4743 4744 var switchText = $(this).data('toggle-text'), 4745 switchTooltipText = $(this).data('toggle-text-tooltip'); 4746 4747 $(this).data('toggle-text', $(this).text()).data('toggle-text-tooltip', $(this._tippy.popper).text()) 4748 $(this).toggleClass('active').find('span').text(switchText) 4749 4750 this._tippy.setContent(switchTooltipText) 4751 tippySingleton.setContent(switchTooltipText) 4752 4753 var none = $(this).is('.active') === false 4754 $('body').toggleClass('hide-start', none); 4755 4756 _this.mapGl.setLayoutProperty('gl-draw-start-marker', 'visibility', none ? 'none' : 'visible', 4757 {validate: false}) 4758 }, 4759 help: function(){ 4760 $(this).toggleClass('active') 4761 4762 var switchText = $(this).data('toggle-text'); 4763 $(this).data('toggle-text', $(this._tippy.popper).text()) 4764 4765 var helpActive = $(this).is('.active'); 4766 helpActive ? _this.helperTootlip.enable() : _this.helperTootlip.disable(); 4767 helpActive ? tippySingleton.enable() : tippySingleton.disable(); 4768 4769 var t = this 4770 // event loop 4771 setTimeout(function(){ 4772 t._tippy.setContent(switchText) 4773 tippySingleton.setContent(switchText) 4774 }, 0) 4775 }, 4776 save: function(e, payload){ 4777 4778 var _ids = _this.path.getAll({saved: true}), 4779 _allIds = _this.path.getAll(); 4780 4781 var gpxTitle = sessionStorage.getItem("stoGpxTitle") || ""; 4782 var gpxDescription = sessionStorage.getItem("stoGpxDescription") || ""; 4783 4784 // Un tracé non actif ne devrait pas avoir 1 seul point, mais ça arrive 4785 // Avant enregistrement, nettoyer automatiquement les tracés à 1 seul point pour empêcher toute erreur impossible à corriger. 4786 var singlePointPaths = []; 4787 _allIds.forEach(function (pathId) { 4788 if (_this.store[pathId] && _this.store[pathId].length === 1) { 4789 singlePointPaths.push(pathId); 4790 } 4791 }); 4792 4793 if (singlePointPaths.length > 0) { 4794 console.warn("Nettoyage automatique des tracés à 1 point:", singlePointPaths); 4795 4796 // Supprimer les features de la carte 4797 singlePointPaths.forEach(function (pathId) { 4798 try { 4799 _this.drawGl.delete(pathId); 4800 } catch (e) { 4801 console.warn("Impossible de supprimer le feature:", pathId, e); 4802 } 4803 }); 4804 4805 // Supprimer du store 4806 _this.path.deletePath(singlePointPaths); 4807 4808 // Mettre à jour l'interface 4809 _this.updateGroupActions(); 4810 _this.updateHelper(); 4811 _this.storePathsLocalStorage(); 4812 } 4813 4814 // Fin - Gestion cas points qui ont pu être sauvegardés sur le serveur 4815 var pathsToSave = _this.path.getPoints({saved: true}), 4816 nbFeatures = Object.keys(pathsToSave).length, 4817 payload = $.extend({}, payload) 4818 4819 if(nbFeatures < 1){ 4820 iziToast.error({ 4821 message: i18n("toast_msg_action_impossible_no_trace_on_map") 4822 }) 4823 return 4824 } 4825 4826 var confirmOpts = Object.assign({ 4827 title: i18n("toast_title_save_confirmation"), 4828 buttons: [ 4829 [`<button class="btn e">${i18n("toast_button_no")}</button>`, 4830 function(instance, toast) { 4831 instance.hide({transitionOut: 'fadeOut'}, toast) 4832 }, 4833 false 4834 ] 4835 ] 4836 }, _this.commonConfirmOpts) 4837 4838 var runSaveRequest = function(instance, toast){ 4839 var processResponse = function(res){ 4840 if(res.redirect){ 4841 // on vire la possibilité de faire un go(-1) dans l'historique 4842 History.replaceState( 4843 {idRandonnee: settings.idRando}, 4844 window.document.title, 4845 settings.baseUrl +'index.php?'+ $.param({ 4846 component: 'rando', 4847 task: 'newCircuitV2' 4848 }) 4849 ); 4850 window.location = res.redirect 4851 return 4852 } 4853 // Pas bien, à mettre ds le callback "error" de aja
4853xq 4854 // Pas de rework à ce jour car ce logiciel va droit à la poubelle 4855 if(res.error){ 4856 iziToast.error({ 4857 id: 'error', 4858 displayMode: 'replace', 4859 message: res.error 4860 }) 4861 return 4862 } 4863 _this.rebaseDescriptionWaypointNumbers(); 4864 if(res.confirm){ 4865 if(res.idRando){ 4866 settings.idRando = res.idRando; 4867 settings.idRandoHash = res.idRandoHash 4868 } 4869 if(payload.preventConfirm){ 4870 return 4871 } 4872 iziToast.show(Object.assign({ 4873 title: i18n("toast_title_action_choice"), 4874 message: res.confirm, 4875 buttons: [ 4876 [`<button class="btn s w-full"><i class="vr-icon vr-icon-file"></i> ${i18n("toast_button_view_sheet")}</button>`, 4877 function (instance, toast) { 4878 settings.hikeUrlEl.click() 4879 instance.hide({transitionOut: 'fadeOut'}, toast) 4880 }, 4881 true 4882 ], 4883 [`<button class="btn o w-full"><i class="vr-icon vr-icon-text"></i> ${i18n("toast_button_describe_route")}</button>`, 4884 function (_instance, _toast) { 4885 window.location = res.onConfirm 4886 }, 4887 true 4888 ], 4889 [`<button class="btn o w-full">${i18n("toast_button_close")}</button>`, 4890 function (_instance, _toast) { 4891 _instance.hide({transitionOut: 'fadeOut'}, _toast) 4892 }, 4893 false 4894 ] 4895 ] 4896 }, _this.commonConfirmOpts)) 4897 } 4898 } 4899 $.ajaxq('store', { 4900 url: settings.baseUrl+'index.php?component=software&task=save', 4901 dataType: 'json', 4902 type: 'post', 4903 beforeSend: function(){ 4904 _this.beginBlockingOperation(); 4905 settings.mapEl.addClass('loading') 4906 }, 4907 success: function(res){ 4908 if(payload.preventConfirm){ 4909 processResponse(res) 4910 }else{ 4911 if(instance){ 4912 instance.hide({transitionOut: 'fadeOut', 4913 onClosing: function(){ 4914 processResponse(res) 4915 } 4916 }, toast) 4917 }else{ 4918 processResponse(res) 4919 } 4920 } 4921 _this.cleanLocalStorage(); 4922 }, 4923 error: function(){ 4924 iziToast.error({ 4925 message: i18n("toast_msg_save_failed"), 4926 displayMode: 'replace' 4927 }) 4928 }, 4929 complete: function(){ 4930 settings.mapEl.removeClass('loading') 4931 _this.endBlockingOperation(); 4932 }, 4933 data: { 4934 idRandonnee: settings.idRando, 4935 carteType: settings.carteType, 4936 paths: JSON.stringify(pathsToSave), 4937 // Planner version, kept in the modification history (channel::softwareV2 / V3) 4938 editor: 'softwareV2', 4939 // Only waypoint numbers are sent; structured-description JSON remains server-side. 4940 deletedDescriptionWaypointIndexes: JSON.stringify(_this.getDeletedDescriptionWaypointIndexes()), 4941 gpxTitle: gpxTitle, 4942 gpxDescription: gpxDescription 4943 } 4944 }) 4945 } 4946 4947 if (payload.preventConfirm) { 4948 runSaveRequest() 4949 return 4950 } 4951 4952 if (nbFeatures === 1) { 4953 4954 if (settings.idRando && settings.idRando > 0) { 4955 4956 var randoLabel = settings.titreRando
4957 ? escapeHtml(settings.titreRando) 4958 : 'n°' + settings.idRando; 4959 confirmOpts.message = sprintf( 4960 i18n("toast_msg_save_origin_modification"), 4961 randoLabel 4962 ); 4963 4964 confirmOpts.buttons.unshift( 4965 [`<button class="btn s">${i18n("toast_button_yes")}</button>`, 4966 function (instance, toast) { 4967 runSaveRequest(instance, toast); 4968 }, 4969 true] 4970 ) 4971 4972 iziToast.show(confirmOpts) 4973 4974 } else { 4975 runSaveRequest(null, null) 4976 } 4977 4978 } else if (nbFeatures > 1) { 4979 4980 if (settings.idRando && settings.idRando > 0) { 4981 // Ãdition d'une rando existante + création de nouvelles : 4982 // 1er tracé du store â modifie la rando existante 4983 // Tracés suivants â nouvelles randonnées 4984 var randoLabel = settings.titreRando 4985 ? escapeHtml(settings.titreRando) 4986 : 'n°' + settings.idRando; 4987 confirmOpts.message = sprintf( 4988 i18n("toast_msg_save_multiple_traces_with_edit"), 4989 randoLabel, 4990 nbFeatures - 1 4991 ); 4992 } else { 4993 // Création de plusieurs nouvelles randonnées from scratch 4994 confirmOpts.message = sprintf( 4995 i18n("toast_msg_save_multiple_traces"), 4996 nbFeatures 4997 ); 4998 } 4999 5000 confirmOpts.buttons.unshift( 5001 [`<button class="btn s">${i18n("toast_button_yes")}</button>`, (instance, toast) => runSaveRequest(instance, toast), true] 5002 ); 5003 5004 iziToast.show(confirmOpts) 5005 } 5006 }, 5007 print: function(){ 5008 var nbFeatures = _this.path.getAll({saved: true}).length; 5009 5010 if (!nbFeatures) { 5011 iziToast.error({ message: i18n("toast_msg_no_valid_trace") }); 5012 return; 5013 } 5014 5015 if (nbFeatures > 1) { 5016 iziToast.error({ 5017 timeout: 10000, 5018 message: i18n("toast_msg_pdf_multiple_traces_error") 5019 }); 5020 return; 5021 } 5022 5023 // Sauvegarde manuelle "programmatically" 5024 settings.groupActions.find('#save').trigger('click', [{preventConfirm: true}]) 5025 5026 $.ajaxq.getActiveRequest('store').done(function(res){ 5027 5028 if(res.error){ 5029 iziToast.error({ 5030 id: 'error', 5031 displayMode: 'replace', 5032 message: res.error 5033 }) 5034 return 5035 } 5036 5037 var a = document.createElement('a'); 5038 a.href = settings.baseUrl+'index.php?'+$.param({ 5039 component:'rando', 5040 task:'pdfRandoPre', 5041 idRandonnee: settings.idRando 5042 }); 5043 document.body.append(a); 5044 a.click(); 5045 a.remove(); 5046 5047 }).fail(function(){ 5048 // Les causes peuvent être diverses...(ex: utilisateur déconnecté) 5049 iziToast.error({ 5050 message: i18n("toast_msg_pdf_generation_failed") 5051 }) 5052 }) 5053 } 5054 }, 5055 path: { 5056 handleFallbackSelection: function(opts){ 5057 5058 opts = $.extend({}, {showToast: true}, opts) 5059 5060 /* On simplifie la vie si un seul tracé */ 5061 if(_this.drawGl.getMode() === 'simple_select'){ 5062 var lineString = _this.drawGl.getSelected(), 5063 savedFeaturesId = this.getAll({saved: true}); 5064 savedFeaturesId 5065 5066 if(lineString.features.length === 0 && savedFeaturesId.length === 1){ 5067 // auto-selection 5068 _this.drawGl.changeMode('simple_select', { 5069 featureIds: [savedFeaturesId[0]] 5070 }) 5071 5072 if(opts.showToast){ 5073 iziToast.info({ 5074 displayMode: 'replace', 5075 message: i18n("toast_msg_auto_trace_selected") 5076 }) 5077 } 5078 } 5079 }
5080 _this.path.getAll().forEach((pathId) => { 5081 _this.updateTracePostOperation(pathId); 5082 }) 5083 }, 5084 handleMarkersPath(reset){ 5085 if(!!reset && !!_this.markers.list['wpts']){ 5086 for (var i = 0; i < _this.markers.list.wpts.length; i++) { 5087 _this.markers.list.wpts[i].remove() 5088 } 5089 } 5090 5091 _this.markers.list.wpts = [] 5092 5093 $.each(_this.store, function (pathId, pts) { 5094 5095 if(pts.length < 2) return 5096 5097 var lineString = turf.lineString([ 5098 [_this.store[pathId][0]['lng'], _this.store[pathId][0]['lat']], 5099 [_this.store[pathId][_this.store[pathId].length-1]['lng'], _this.store[pathId][_this.store[pathId].length-1]['lat']] 5100 ]), 5101 markerNum = 1, 5102 length = turf.length(lineString, { units: 'kilometers' }) * 1000; // km => m 5103 5104 var retourDepart = length <= settings.departureThreshold 5105 5106 for (var i = 0; i < pts.length; i++) { 5107 if(retourDepart) { 5108 if(i == pts.length - 1){ 5109 _this.markers.list.wpts.push(_this.markers.create({ 5110 className: 'marker start_end', 5111 coordinates: [pts[i]['lng'],pts[i]['lat']], 5112 attachToMap: true, 5113 id: pts[i].id 5114 })) 5115 } 5116 } else { 5117 if(i == 0) { 5118 _this.markers.list.wpts.push(_this.markers.create({ 5119 className: 'marker start', 5120 coordinates: [pts[i]['lng'],pts[i]['lat']], 5121 attachToMap: true, 5122 id: pts[i].id 5123 })) 5124 } else if(i == pts.length - 1) { 5125 _this.markers.list.wpts.push(_this.markers.create({ 5126 className: 'marker end', 5127 coordinates: [pts[i]['lng'],pts[i]['lat']], 5128 attachToMap: true, 5129 id: pts[i].id 5130 })) 5131 } 5132 } 5133 5134 // pas de wpt aux extrémités des tracés 5135 if( !(i == pts.length - 1 || i == 0) ){ 5136 if(pts[i].wpt == 1){ 5137 _this.markers.list.wpts.push(_this.markers.create({ 5138 className: 'marker', 5139 text: ''+markerNum, 5140 coordinates: [pts[i]['lng'],pts[i]['lat']], 5141 attachToMap: true, 5142 id: pts[i].id 5143 })) 5144 markerNum++ 5145 } 5146 } 5147 } 5148 }) 5149 5150 }, 5151 getPoints: function(opts){ 5152 5153 var opts = $.extend({}, {saved: true}, opts), 5154 allPath = Object.keys(_this.store), 5155 pts = {}; 5156 5157 for (var i = 0; i < allPath.length; i++) { 5158 pts[allPath[i]] = _this.store[allPath[i]] 5159 } 5160 5161 if (opts.saved) { 5162 $.each(pts, function (pathId) { 5163 if (pts[pathId].length <= 1) delete pts[pathId] 5164 }) 5165 } 5166 5167 return pts 5168 }, 5169 // Attention, comportement différent de getPoints() 5170 getAll: function(opts){ 5171 var opts = $.extend({}, {saved: false}, opts), 5172 allPath = Object.keys(_this.store); 5173 5174 if(opts.saved){ 5175 var filteredPath = []; 5176 5177 for(var i=0; i < allPath.length; i++){ 5178 // Vérifier d'abord que le store contient des données pour ce chemin 5179 if(!_this.store[allPath[i]] || !_this.store[allPath[i]].length) { 5180 continue; 5181 } 5182 filteredPath.push(allPath[i]); 5183 } 5184 5185 return filteredPath; 5186 }else{ 5187 // Filtrer les chemins sans données 5188 return allPath.filter(function(path) { 5189 return _this.store[path] && _this.store[path].length; 5190 }); 5191 } 5192 }, 5193 deletePath: function(idsPath){ 5194 if(!$.isArray(idsPath) || idsPath.length === 0) return 5195 for(var i=0; i < idsPath.length; i++){ 5196 delete _this.store[idsPath[i]] 5197 } 5198 }, 5199 deletePoint: function(idPath, index) { 5200 if(index !== -1){ 5201 _this.store[idPath].splice(index, 1) 5202 } 5203 }, 5204 computeOrder: function(idPath){ 5205 $.each(_this.store[idPath], function(i, p){ 5206 p.order = i 5207 }) 5208 }, 5209 // Insère un point dans le store du tracé 5210 insertPoint: function(idPath, index, data){ 5211 if (!_this.store[idPath]) { 5212 _this.store[idPath] = []; 5213 } 5214 if(typeof index != 'undefined'){ 5215 _this.store[idPath].splice(index, 0, data); 5216 if (typeof _this.store[idPath][index] != 'undefined') { 5217 _this.store[idPath][index].id = this.generateUUIDv4(); 5218 if(_this.store[idPath].length-1 != index){ 5219 _this.path.computeOrder(idPath) 5220 } 5221 } 5222 return 5223 } 5224 _this.store[idPath] = [data] 5225 }, 5226 //Create a UUIDv4 for points 5227 generateUUIDv4: function() { 5228 const bytes = crypto.getRandomValues(new Uint8Array(16)); 5229 5230 // Ajustements spécifiques UUIDv4 : 5231 bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4 5232 bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10xxxxxx 5233 5234 const toHex = (n) => n.toString(16).padStart(2, '0'); 5235 5236 return [ 5237 ...bytes.slice(0, 4),
5238 '-', 5239 ...bytes.slice(4, 6), 5240 '-', 5241 ...bytes.slice(6, 8), 5242 '-', 5243 ...bytes.slice(8, 10), 5244 '-', 5245 ...bytes.slice(10, 16), 5246 ].map(toHex).join(''); 5247 }, 5248 5249 // Permet de mettre à jour un point sur un tracé 5250 updatePoint: function(idPath, index, data){ 5251 if(_get(_this.store, idPath, false) === false) return 5252 var newData = $.extend({}, _this.store[idPath][index], data) 5253 5254 // Attention aux shallow copy ! 5255 delete newData.metaData 5256 _this.store[idPath][index] = newData 5257 }, 5258 // Renseigne le nombre de points dans un tracé 5259 countPoints: function(idPath){ 5260 return _this.store.hasOwnProperty(idPath) ? _this.store[idPath].length : 0 5261 }, 5262 //Pour savoir si un point est une ancre ou pas 5263 isAnchor: function(pathId, coordPath) { 5264 try { 5265 return _this.store[pathId][coordPath]?.anchor === 1; 5266 } catch (e) { 5267 return 0; 5268 } 5269 }, 5270 // Reconstruit la liste des indices d'ancres à partir du store 5271 computeAnchorIndicesFromStore: function(pathId) { 5272 const anchors = [] 5273 _this.store[pathId].forEach((point, index) => { 5274 if (point.anchor === 1) anchors.push(index); 5275 }); 5276 return anchors; 5277 }, 5278 nextAnchor: function(idx, anchorIndices, n) { 5279 idx = parseInt(idx); 5280 const set = new Set(anchorIndices); 5281 let next = idx; 5282 for (let i = idx + 1; i <= n; i++) {if (set.has(i)) { next = i; break; } } 5283 return next; 5284 }, 5285 prevAnchor: function(idx, anchorIndices) { 5286 idx = parseInt(idx); 5287 const set = new Set(anchorIndices); 5288 let prev = idx; 5289 for (let i = idx - 1; i >= 0; i--) { if (set.has(i)) { prev = i; break; } } 5290 return prev; 5291 }, 5292 /** 5293 * Obtient l'ID du tracé actif (en cours de dessin ou sélectionné) 5294 * @returns {string|null} ID du tracé ou null si aucun tracé actif 5295 */ 5296 getActivePathId: function() { 5297 // Vérification rapide de drawGl 5298 if (!_this.drawGl) { 5299 console.warn("getActivePathId: drawGl non disponible"); 5300 return null; 5301 } 5302 5303 try { 5304 // Cas 1: Mode dessin actif 5305 if (_this.drawGl.getMode() === 'draw_line_string') { 5306 const state = _this.drawGl.getState(); 5307 if (state?.line?.id) { 5308 return [state.line.id]; 5309 } 5310 } 5311 5312 // Cas 2: Mode sélection 5313 const selectedIds = _this.drawGl.getSelectedIds(); 5314 if (selectedIds.length > 0) { 5315 return selectedIds; 5316 } 5317 5318 // Aucun tracé actif 5319 return []; 5320 5321 } catch (err) { 5322 console.error("Erreur dans getActivePathId:", err); 5323 return null; 5324 } 5325 }, 5326 updateDrawGlCoordsWithPath: function (paths) { 5327 let newCoords = []; 5328 const state = _this.drawGl.getAll(); 5329 let redraw = true; 5330 if (paths.length === 0) { 5331 paths = Object.keys(_this.store); 5332 paths.push(...state.features.map(f => f.id)) 5333 } 5334 //Rendre uniques les valeurs de paths 5335 paths = [...new Set(paths)]; 5336 paths.forEach(idPath => { 5337 newCoords = []; 5338 if (_this.store[idPath]) { 5339 const index = state.features.findIndex(f => f.id === idPath);
5340 _this.store[idPath].forEach(point => { 5341 newCoords = newCoords.concat([[point.lng, point.lat]]); 5342 }) 5343 if (index === -1) { 5344 const originalFeature = _this.drawGl.get(idPath); 5345 const originalProperties = originalFeature ? originalFeature.properties : {}; 5346 state.features.push({ 5347 'type': 'Feature', 5348 'properties': originalProperties, 5349 'geometry': { 5350 'type': 'LineString', 5351 'coordinates': newCoords 5352 }, 5353 'id': idPath 5354 }); 5355 } else { 5356 state.features[index].geometry.coordinates = newCoords; 5357 } 5358 } else { 5359 state.features = state.features.filter(f => f.id !== idPath); 5360 } 5361 }); 5362 state.features.forEach(f => { 5363 if (!f.geometry.coordinates || f.geometry.coordinates.length < 2) { 5364 redraw = false; 5365 } 5366 }); 5367 if (redraw) { 5368 _this.drawGl.set(state); 5369 _this.drawGl.renderCanvas(); 5370 } 5371 } 5372 }, 5373 tab: { 5374 /** Stockage des instances ElevationChart (Visorandomap) par pathId */ 5375 elevationCharts: {}, 5376 5377 /** 5378 * Construit un objet Trail (domaine Visorandomap) depuis les points du store. 5379 * @param {string} idPath 5380 * @returns {Trail|null} 5381 */ 5382 buildTrail: function(idPath) { 5383 const rawPoints = _this.store[idPath] || []; 5384 const validPoints = rawPoints.filter(p => typeof p.altitude === 'number'); 5385 if (validPoints.length < 2) return null; 5386 5387 const { Trail, TrailPoint, Coordinates, Elevation } = VisorandoMap; 5388 const trailPoints = validPoints.map(p => 5389 new TrailPoint(new Coordinates(p.lat, p.lng), new Elevation(p.altitude)) 5390 ); 5391 return new Trail(idPath, trailPoints); 5392 }, 5393 5394 handleSelection: function(tabName, swiperInstance, idPath){ 5395 5396 var selectedIds = _this.drawGl.getSelectedIds(), 5397 slideIndex = -1; 5398 5399 if(selectedIds.length || idPath){ 5400 idPath = selectedIds && selectedIds[selectedIds.length - 1] || idPath 5401 // On affiche le dernier tracé sélectionné 5402 var slideIndex = $(swiperInstance.slides) 5403 .filter('#'+tabName+'-'+idPath).index() 5404 } 5405 5406 if(slideIndex != -1){ 5407 swiperInstance.slideTo(slideIndex) 5408 } 5409 }, 5410 render: async function(slide, tabName, data, swiperInstance, pathId){ 5411 switch (tabName) { 5412 case 'infos': 5413 Object.entries(data.metaData).forEach(([cls, val]) => { 5414 $(slide).find('.' + cls + ' > span').eq(0).text(val); 5415 }); 5416 5417 $(slide).data('metaData', data.metadata); 5418 break; 5419 5420 case 'diagrams': 5421 const feature = _this.drawGl.get(pathId); 5422 if (!feature) return; 5423 5424 const $infoSlide = $(_this.swiper.infos.slides).filter(function () { 5425 return this.id === 'infos-' + pathId; 5426 }).first(); 5427 5428 // Mise à jour des métadonnées à partir des points enrichis 5429 const params = data.metaData; 5430 5431 if (!params) { 5432 console.warn(`[render] Aucune métadonnée trouvée pour ${pathId}`); 5433 return; 5434 } 5435 5436 const deltaX = params['distance'] * 1000; 5437 const deltaY = +params['high-point'] - +params['low-point']; 5438 const ratio = (deltaY / deltaX * 3.7) * 100; 5439 const percentage = Math.max(35, Math.min(ratio, 60)); 5440 const padding = Math.max(0.02, Math.min(deltaX / deltaY * 0.00466, 6)); 5441 5442 const distanceMax = params['distance']; 5443 5444 // --- Visorandomap ElevationChart --- 5445 const { ElevationChart } = VisorandoMap; 5446 const existingChart = _this.tab.elevationCharts[pathId]; 5447 5448 // Si une instance existe déjà : mise à jour des données et de la couleur 5449 if (existingChart) { 5450 const updatedTrail = _this.tab.buildTrail(pathId); 5451 if (updatedTrail) { 5452 existingChart.update(updatedTrail); 5453 } 5454 const featureColor = feature.properties.user_color || feature.properties.color; 5455 if (featureColor) { 5456 existingChart.updateColor(featureColor); 5457 } 5458 swiperInstance.updateAutoHeight(0); 5459 _this.swiper.tabs.updateAutoHeight(200); 5460 return; 5461 } 5462 5463 // Création d'une nouvelle instance 5464 const trail = _this.tab.buildTrail(pathId); 5465 if (!trail) return; 5466 5467 5468 $(slide).empty(); 5469 const bootElem = $($('<div/>', {'class': 'boot-chart'})).appendTo(slide); 5470 5471 $(slide).off('mouseleave.indicator').on('mouseleave.indicator', function(){ 5472 _this.markers.deleteById(null, true, 'here-indicator'); 5473 }); 5474 5475 const seriesColor = feature.properties.user_color || feature.properties.color || undefined; 5476 const distanceUnit = settings.currentUnits.distance === 'mi' ? 'mi' : 'km'; 5477 const elevationUnit = settings.currentUnits.altitude === 'ft' ? 'ft' : 'm'; 5478 5479 // Marker local pour la synchronisation avec le diagramme d'élévation 5480 var hereMarker = null; 5481 5482 _this.tab.elevationCharts[pathId] = new ElevationChart({ 5483 container: bootElem.get(0), 5484 trail: trail, 5485 height: percentage + '%', 5486 distanceUnit: distanceUnit, 5487 elevationUnit: elevationUnit, 5488 seriesColor: seriesColor, 5489 slopeColoring: settings.slopeColoring, 5490 enableNavigator: false, 5491 enableScrollbar: false, 5492 comparativeScale: false, 5493 onHover: function(point) { 5494 if (!_this.mapGl) return; 5495 if (point) { 5496 if (!hereMarker) { 5497 var el = document.createElement('div'); 5498 el.className = 'elevation-chart-highlight';
5499 hereMarker = new maplibregl.Marker({ element: el, anchor: 'center' }) 5500 .setLngLat([point.position.lng, point.position.lat]) 5501 .addTo(_this.mapGl); 5502 } else { 5503 hereMarker.setLngLat([point.position.lng, point.position.lat]); 5504 } 5505 } else { 5506 if (hereMarker) { 5507 hereMarker.remove(); 5508 hereMarker = null; 5509 } 5510 } 5511 }, 5512 onClick: function(point) { 5513 if (point) { 5514 _this.markers.createOnce( 5515 [point.position.lng, point.position.lat], 5516 'here-indicator', 5517 'mapboxgl-user-location-dot' 5518 ); 5519 } 5520 } 5521 }); 5522 5523 const chartPathId = pathId; 5524 setTimeout(function(){ 5525 swiperInstance.updateAutoHeight(0); 5526 _this.swiper.tabs.updateAutoHeight(200); 5527 // Reflow du chart après redimensionnement du Swiper : 5528 // le container peut ne pas avoir eu ses dimensions finales 5529 // au moment de la création du chart (autoHeight Swiper). 5530 const elevChart = _this.tab.elevationCharts[chartPathId]; 5531 if (elevChart && typeof elevChart.reflow === 'function') { 5532 elevChart.reflow(); 5533 } 5534 }, 0); 5535 5536 break; 5537 default: 5538 } 5539 }, 5540 createSlide: function(tabName, swiperInstance){ 5541 if(!swiperInstance.slides.length){ 5542 tabName = 'default' 5543 } 5544 switch (tabName) { 5545 case 'infos': 5546 return $(swiperInstance.slides).eq(0).clone() 5547 .removeAttr('style id').attr('class', 'swiper-slide') 5548 break; 5549 default: 5550 return $('<div />', { class: 'swiper-slide' }).removeAttr('style') 5551 } 5552 }, 5553 deleteSlide: function(tabName, pathId, swiperInstance){ 5554 5555 var slide = $(swiperInstance.slides).filter('#'+tabName+'-'+pathId), 5556 slideIndex = slide.index(); 5557 5558 if(slideIndex == -1) return 5559 5560 switch (tabName) { 5561 case 'infos': 5562 5563 if(swiperInstance.slides.length == 1){ 5564 5565 slide.removeAttr('id').find('> div > span').text(0) 5566 5567 }else if(swiperInstance.slides.length){ 5568 5569 swiperInstance.removeSlide(slideIndex) 5570 swiperInstance.pagination.update() 5571 } 5572 5573 break; 5574 case 'diagrams': 5575 // Libération de la mémoire (évite les memory leaks) 5576 const chartToDestroy = _this.tab.elevationCharts[pathId]; 5577 if (chartToDestroy) { 5578 chartToDestroy.destroy(); 5579 delete _this.tab.elevationCharts[pathId]; 5580 5581 // Rafraîchir la couleur des instances restantes 5582 $.each(swiperInstance.slides, function(){ 5583 const slidePathId = ($(this).attr('id') || '').split('-').pop(); 5584 const currentChart = _this.tab.elevationCharts[slidePathId]; 5585 const currentFeature = slidePathId ? _this.drawGl.get(slidePathId) : null; 5586 if (currentChart && currentFeature && currentFeature.properties) { 5587 const featureColor = currentFeature.properties.user_color || currentFeature.properties.color; 5588 if (featureColor) { 5589 currentChart.updateColor(featureColor); 5590 } 5591 } 5592 }); 5593 } 5594 5595 if(swiperInstance.slides.length == 1){ 5596 slide.removeAttr('id').empty() 5597 $(swiperInstance.placeHolder.clone()).appendTo(slide) 5598 swiperInstance.updateAutoHeight(0) 5599 _this.swiper.tabs.updateAutoHeight(200) 5600 }
5600else if(swiperInstance.slides.length){ 5601 swiperInstance.removeSlide(slideIndex) 5602 swiperInstance.pagination.update() 5603 } 5604 5605 break; 5606 default: 5607 } 5608 }, 5609 update: function(tabName, pathId, data, swiperInstance){ 5610 5611 var shouldCreate = true; 5612 5613 $(swiperInstance.slides).each(function(){ 5614 if( $(this).is('[id]') ){ 5615 if(this.id == tabName+'-'+pathId){ 5616 _this.tab.render(this, tabName, data, swiperInstance, pathId) 5617 shouldCreate = false 5618 } 5619 }else{ 5620 // attach id + update existing orphan slide (ex: first page load) 5621 $(this).attr('id', tabName+'-'+pathId) 5622 _this.tab.render(this, tabName, data, swiperInstance, pathId) 5623 shouldCreate = false 5624 } 5625 }) 5626 5627 // Pas de slide dispo, pathId non encore affiché mais présent dans le store 5628 if(shouldCreate && $.inArray(pathId, _this.path.getAll()) != -1){ 5629 var slide = _this.tab.createSlide(tabName, swiperInstance) 5630 slide.attr('id', tabName+'-'+pathId) 5631 swiperInstance.appendSlide(slide) 5632 _this.tab.render(slide, tabName, data, swiperInstance, pathId) 5633 5634 if(_this.path.countPoints(pathId) > 1 && _this.drawGl.getMode() == _this.drawGl.modes.DRAW_LINE_STRING) { 5635 _this.tab.handleSelection(tabName, _this.swiper[tabName], pathId) 5636 } 5637 } 5638 5639 swiperInstance.pagination.update() 5640 } 5641 }, 5642 // Pas d'historique sur les états des tracés ou lorsqu'une action ne modifie pas la BDD 5643 history: { 5644 index: 0, 5645 historyStore: [{ 5646 store: Immutable.Map({}) 5647 }], 5648 annotations: Immutable.List([]), 5649 operation: function(fn, annotation){ 5650 this.annotations = this.annotations.slice(0, this.index + 1); 5651 this.historyStore = this.historyStore.slice(0, this.index + 1); 5652 var newVersion = fn(this.historyStore[this.index]); 5653 this.historyStore.push(newVersion); 5654 this.annotations = this.annotations.push(annotation); 5655 this.index++; 5656 _this.updateGroupActions(); 5657 $(document).trigger('history.operation') 5658 }, 5659 // Du fait des traitements asynchrones on a besoin de modifier l'historique 5660 commit: function(fn, index){ 5661 var _index = index || this.index 5662 if(!this.historyStore[_index]) return 5663 this.historyStore[_index] = fn(this.historyStore[_index], 5664 (this.historyStore.length) ? this.historyStore[_index-1]: null); 5665 }, 5666 get: function(){ 5667 return (!!this.historyStore[this.index]) ? this.historyStore[this.index] : null 5668 }, 5669 getAnnotation: function(atIndex){ 5670 var currentAnnotaion = this.annotations.get(atIndex) 5671 return (!!currentAnnotaion) ? currentAnnotaion : null 5672 }, 5673 getGeoJSON: function() { 5674 var storeData = this.get(); 5675 if(!storeData) return storeData; 5676 5677 var features = []; 5678 // Pour chaque tracé 5679 storeData.store.mapKeys(function(idPath) { 5680 // Récupérer le tracé actuel et ses propriétés 5681 var internalPath = _this.drawGl.get(idPath); 5682 5683 // Récupération complète des propriétés, pas seulement la couleur 5684 var internalProperties = internalPath ? 5685 Immutable.Map(internalPath.properties || {}) : 5686 Immutable.Map({}); 5687 5688 // Conserver à la fois color et user_color 5689 var internalColor = internalProperties.get('color'); 5690 var internalUserColor = internalProperties.get('user_color'); 5691 5692 var lineString = turf.feature({ 5693 type: 'LineString', 5694 coordinates: [] 5695 }); 5696 5697 // Chaque point de chaque tracé 5698 var points = storeData.store.get(idPath).toJS(); 5699 for(var j=0; j < points.length; j++) { 5700 var point = points[j]; 5701 lineString.geometry.coordinates.push([point.lng, point.lat]); 5702 } 5703 5704 lineString.id = idPath; 5705 5706 // Préserver toutes les propriétés importantes 5707 lineString.properties = { 5708 // Copier toutes les propriétés existantes si disponibles 5709 ...(internalPath ? internalPath.properties || {} : {}), 5710 // S'assurer que les propriétés de couleur sont définies 5711 color: internalColor || internalUserColor || settings.hexColor, 5712 user_color: internalUserColor || internalColor || settings.hexColor 5713 }; 5714 5715 features.push(lineString); 5716 }); 5717 5718 return turf.featureCollection(features); 5719 }, 5720 redo: function(e) { 5721 if (this.index < this.historyStore.length) { 5722 this.index++; 5723 // Empêcher de restaurer plus de 1 tracé en version simple en one shot 5724 if(settings.isSimpleVersion){ 5725 if(this.getGeoJSON().features.length > 1){ 5726 this.index--; 5727 iziToast.error({ 5728 displayMode: 'replace', 5729 message: i18n("toast_msg_simple_mode_limit") 5730 }) 5731 return 5732 } 5733 } 5734 } 5735 this.afterSwitchVersion(i18n("toast_msg_restore_done"), e) 5736 }, 5737 undo: function(e) { 5738 if (this.index > 0){ 5739 this.index--; 5740 // Empêcher de restaurer plus de 1 tracé en version simple en one shot 5741 if(settings.isSimpleVersion){ 5742 if(this.getGeoJSON().features.length > 1){ 5743 this.index++; 5744 iziToast.error({ 5745 displayMode: 'replace', 5746 message: i18n("toast_msg_simple_mode_limit") 5747 }) 5748 return 5749 } 5750 } 5751 } 5752 this.afterSwitchVersion(i18n("toast_msg_cancel_done"), e) 5753 }, 5754 afterSwitchVersion: function(msg, e){
5755 _this.path.getAll().forEach((pathId) => { 5756 $.each(['infos', 'diagrams'], function(i, tab){ 5757 _this.tab.deleteSlide(tab, pathId, _this.swiper[tab]) 5758 }) 5759 }); 5760 _this.activeMode(_this.drawGl.getMode()) 5761 _this.store = this.get().store.toJS() 5762 _this.path.updateDrawGlCoordsWithPath([]); 5763 _this.updateGroupActions() 5764 _this.path.handleMarkersPath(true) 5765 _this.path.getAll().forEach((pathId) => { 5766 _this.updateTracePostOperation(pathId) 5767 }) 5768 5769 if(settings.isSimpleVersion) _this.path.handleFallbackSelection({showToast: false}) 5770 }, 5771 hasUndo: function(){ 5772 return this.index !== 0 5773 }, 5774 hasRedo: function(){ 5775 return this.index !== this.historyStore.length - 1 5776 }, 5777 }, 5778 activeMode: function(mode){ 5779 $(_this.mapGl.getContainer()).alterClass('mode-*', 'mode-'+mode) 5780 5781 settings.groupActions.find('.mode').removeClass('active'). 5782 end().find('.'+mode).addClass('active'); 5783 5784 this.updateHelper(); 5785 }, 5786 activeAction: function(action, payload){ 5787 if(!settings.isCompatible) return 5788 var el = settings.groupActions.find('#'+action) 5789 5790 el.trigger('click', [payload]) 5791 if(!el.is('.active')){ 5792 el.addClass('active') 5793 } 5794 }, 5795 updateGroupActions: function(){ 5796 var countPath = _this.path.getAll().length 5797 5798 // Gestion bouton création de tracé 5799 // Un seul tracé possible pour la version simple (non premium) 5800 let newPathElHandler = settings.groupActions.find('#draw'), 5801 undoHandler = document.querySelectorAll('#undo'), 5802 redoHandler = document.querySelectorAll('#redo') 5803 if(settings.isSimpleVersion){ 5804 if(countPath >= 1){ 5805 newPathElHandler.attr('disabled', 'disabled') 5806 }else{ 5807 newPathElHandler.removeAttr('disabled') 5808 } 5809 }else{ 5810 newPathElHandler.removeAttr('disabled') 5811 } 5812 if (_this.history.hasRedo()) { 5813 redoHandler.forEach(e => { 5814 e.removeAttribute('disabled') 5815 }) 5816 } else { 5817 redoHandler.forEach(e => { 5818 e.setAttribute('disabled', 'disabled') 5819 }) 5820 } 5821 if (_this.history.hasUndo()) { 5822 undoHandler.forEach(e => { 5823 e.removeAttribute('disabled') 5824 }) 5825 } else { 5826 undoHandler.forEach(e => { 5827 e.setAttribute('disabled', 'disabled') 5828 }) 5829 } 5830 }, 5831 updateHelper: function(){ 5832 5833 _this.markers.deleteById(null, true, 'here-indicator') 5834 5835 if (!$('#help').is('.active')) return; 5836 5837 var currentMode = _this.drawGl.getMode(), 5838 currentState = _this.drawGl.getState() // metadonnées du mode 5839 5840 // Chaque mode a ses instructions 5841 switch (currentMode) { 5842 case 'draw_line_string': 5843 var vertexPos = currentState.currentVertexPosition, 5844 coordLength = currentState.line.coordinates.length; 5845 5846 if(vertexPos === 0 && coordLength <= 1 && !_this.store[currentState.line.id]){ 5847 _this.helperTootlip.setContent(i18n("tooltip_msg_place_first_point")) 5848 }else if(vertexPos === 1 && coordLength > 0){ 5849 _this.helperTootlip.setContent(i18n("tooltip_msg_place_second_point")) 5850 }else if(vertexPos > 0 && coordLength > 1){ 5851 _this.helperTootlip.setContent(i18n("tooltip_msg_finish_trace")) 5852 } 5853 break; 5854 case 'simple_select': 5855 if(_this.drawGl.getSelectedIds().length){ 5856 if(_this.drawGl.getSelectedPoints().features.length){ 5857 _this.helperTootlip.setContent(i18n("tooltip_msg_drag_points")) 5858 }else{ 5859 _this.helperTootlip.setContent(i18n("tooltip_msg_click_point_edit")) 5860 } 5861 } else { 5862 _this.helperTootlip.setContent(i18n("tooltip_msg_select_multiple_traces")) 5863 } 5864 break; 5865 default: 5866 _this.helperTootlip.disable() 5867 } 5868 }, 5869 markers: { 5870 list: {}, 5871 defaultOptions: { 5872 className: '', 5873 text: '', 5874 id: '', 5875 anchor: 'bottom', // ancre en bas : la pointe du SVG pin est exactement sur le point 5876 offset: [0, 0], 5877 attachToMap: false, 5878 coordinates: false 5879 }, 5880 delete: function(idPoints){ 5881 if(!!_this.markers.list['wpts']) { 5882 for(var j = 0; j < idPoints.length; j++){ 5883 for (var i = 0; i < _this.markers.list.wpts.length; i++) { 5884 5885 var el = _this.markers.list.wpts[i].getElement() 5886 5887 if(!!el.id && el.id == 'point-'+idPoints[j]) { 5888 _this.markers.list.wpts[i].remove() 5889 break; 5890 } 5891 5892 } 5893 } 5894 } 5895 }, 5896 create: function(markerOpts){ 5897 5898 markerOpts = $.extend({}, 5899 Object.assign(this.defaultOptions, { 5900 el : document.createElement('div') 5901 }), markerOpts) 5902 5903 markerOpts.el.className = markerOpts.className; 5904 markerOpts.el.style.cursor = 'default'; 5905
5906 if (isPromise(markerOpts.id)) { 5907 markerOpts.id 5908 .then(function(res) { 5909 const id = res?.id || res; 5910 markerOpts.el.id = id ? 'point-' + id : ''; 5911 }) 5912 .catch(function(err) { 5913 console.warn("Erreur lors de la résolution de markerOpts.id :", err); 5914 markerOpts.el.id = ''; // fallback raisonnable 5915 }); 5916 } else { 5917 markerOpts.el.id = ('' + markerOpts.id).length ? 'point-' + markerOpts.id : ''; 5918 } 5919 5920 var marker = new maplibregl.Marker({ element: markerOpts.el, anchor: markerOpts.anchor || 'bottom', offset: markerOpts.offset }) 5921 5922 if(markerOpts.text.length){ 5923 $(marker.getElement()).html($('<span/>', { 5924 'text': markerOpts.text 5925 }).prop('outerHTML')) 5926 } 5927 5928 if($.isArray(markerOpts.coordinates) && markerOpts.coordinates.length === 2){ 5929 marker.setLngLat(markerOpts.coordinates) 5930 } 5931 5932 if(markerOpts.attachToMap){ 5933 marker.addTo(_this.mapGl) 5934 } 5935 5936 return marker; 5937 }, 5938 createOnce: function(coordinates, id, className){ 5939 5940 var marker = null, 5941 alreadyExists = false; 5942 5943 if(!!this.list[id]){ 5944 alreadyExists = true 5945 marker = this.list[id] 5946 }else{ 5947 // Vive l'ES5...pourrave 5948 this.list[id] = marker = this.create({ 5949 className: className, 5950 id: id, 5951 anchor: 'center', // dot indicateur centré sur le point 5952 offset: [0, 0] // pas de compensation globe pour le dot 5953 }) 5954 } 5955 5956 marker.setLngLat(coordinates); 5957 5958 if(!alreadyExists) marker.addTo(_this.mapGl); 5959 5960 return marker; 5961 }, 5962 deleteById: function(marker, fromList, id){ 5963 if(!!fromList){ 5964 if(!this.list[id]) return 5965 this.list[id].remove() 5966 delete this.list[id] 5967 }else{ 5968 marker.remove() 5969 } 5970 } 5971 }, 5972 updateTopBar: function() { 5973 $.each(settings.groupActions.find('.nav__dropdown > *'), function() { 5974 priorityNav.toMenu(settings.groupActions.get(0), settings.groupActions.attr('instance')) 5975 }); 5976 5977 var toggleButton = settings.groupActions.find('.priority-nav__dropdown-toggle') 5978 if (toggleButton.is('.is-open')) { 5979 toggleButton.trigger('click') 5980 } 5981 5982 tippy.hideAll({duration: 0}) 5983 5984 var catchEndFit = new $.Deferred() 5985 priorityNav.doesItFit(settings.groupActions.get(0), catchEndFit) 5986 5987 catchEndFit.then(function () { 5988 settings.groupActions.removeClass('mod') 5989 _this.mapGl.resize() 5990 $.each(settings.groupActions.find('.nav > *'), function () { 5991 var newProps = { 5992 placement: 'bottom' 5993 } 5994 this._tippy.setProps(newProps) 5995 tippySingleton.setProps(newProps) 5996 }) 5997 }) 5998 }, 5999 6000 // Formate les coordonées selon les paramètres enregistrés ds le profil utilisateur 6001 getFormattedCoordinates: function(coordinates){ 6002 6003 switch (settings.coordinateFormat) { 6004 // DMS 6005 case 1: 6006 var toDMS = new dms(coordinates.lat, coordinates.lng), 6007 lng = toDMS.dmsArrays.longitude, 6008 lat = toDMS.dmsArrays.latitude; 6009 6010 return lng[0]+'° '+lng[1]+'\' '+Number.parseFloat(lng[2]).toFixed(4)+'" '+lng[3] 6011 +' - '+lat[0]+'° '+lat[1]+'\' '+Number.parseFloat(lat[2]).toFixed(4)+'" '+lat[3]; 6012 break; 6013 // UTM 220 6014 case 2: 6015 var _utm = UTM.fromLatLon(coordinates.lat, coordinates.lng) 6016 return 'X : '+_utm.easting.toFixed(2)+' - Y : '+_utm.northing.toFixed(2)+' - '+_utm.zoneNum+_utm.zoneLetter 6017 break; 6018 // DD 220 6019 default: 6020 var $lng = $('<span />', { 6021 class: 'lng', 6022 text : Number.parseFloat(coordinates.lng).toFixed(5) 6023 }) 6024 6025 var $lat= $('<span />', { 6026 class: 'lat', 6027 text : Number.parseFloat(coordinates.lat).toFixed(5) 6028 }) 6029 6030 return 'Lng : ' + $lng.html() + ' - '+'Lat : ' + $lat.html() 6031 } 6032 },
6033 setCookie: function(object){ 6034 Cookies.set('software.opts', Object.assign(Cookies.getJSON('software.opts'), object), { expires: 365 }) 6035 }, 6036 addAsyncTask: function(promise){ 6037 // Nous pouvons vouloir ajouter une promesse existante ou non 6038 var p = promise || new $.Deferred() 6039 this.asyncTasks.push(p) 6040 return p 6041 }, 6042 /** 6043 * Appelle Valhalla via votre proxy, décode la polyline 6044 * et renvoie un format unifié. 6045 * 6046 * @param {Array<{lat:number,lon:number}>} locations 6047 * @returns {Promise<{ success: boolean, points?: [number,number][], metaData?: object, error?: string }>} 6048 */ 6049 callRoute: async function (locations, costing, layer = null) { 6050 if (!Array.isArray(locations) || locations.length < 2) { 6051 return { success: false, error: i18n("api_error_need_two_points") }; 6052 } 6053 const payload = { 6054 locations, 6055 costing, 6056 ...(layer && { layer }) 6057 }; 6058 6059 const url = (settings.routingProxyUrl || '/index.php?component=software&task=proxyRouting'); 6060 6061 try { 6062 const response = await fetch(url, { 6063 method: 'POST', 6064 headers: { 'Content-Type': 'application/json' }, 6065 body: JSON.stringify(payload) 6066 }); 6067 6068 const text = await response.text(); 6069 const data = JSON.parse(text); 6070 6071 if (!response.ok || !data.success) { 6072 console.error(`Erreur API: `, data); 6073 return { 6074 success: false, 6075 error: data.error || `HTTP ${response.status}`, 6076 metaData: data.body || data.metaData 6077 }; 6078 } 6079
6080 if (typeof polyline === 'undefined' || typeof polyline.decode !== 'function') { 6081 return { success: false, error: i18n("api_error_polyline_module_missing") }; 6082 } 6083 6084 if (typeof data.legs === 'undefined') { 6085 return { success: false, error: i18n("api_error_polyline_data_missing") }; 6086 } 6087 let legs = []; 6088 data.legs.forEach(leg => { 6089 let shape = polyline.decode(leg.shape, 6); 6090 // Le serveur peut joindre les altitudes (DP 3D) alignées sur les points du shape : 6091 // on les porte comme 3e composante [lon, lat, alt] pour éviter un appel MNT côté client. 6092 const altitudes = Array.isArray(leg.altitudes) ? leg.altitudes : null; 6093 shape = shape.map(([lat, lon], i) => { 6094 const coord = [ 6095 parseFloat(lon.toFixed(6)), 6096 parseFloat(lat.toFixed(6)) 6097 ]; 6098 if (altitudes && typeof altitudes[i] === 'number') { 6099 coord[2] = altitudes[i]; 6100 } 6101 return coord; 6102 }); 6103 legs.push(shape); 6104 }); 6105 6106 6107 return { 6108 success: true, 6109 legs, 6110 metaData: data.metaData || {}, 6111 sourceService: data.sourceService 6112 }; 6113 } catch (err) { 6114 console.error("Erreur lors de l'appel au service :", err); 6115 return { success: false, error: err.message }; 6116 } 6117 }, 6118 6119 6120 /** 6121 * Enregistre ou met à jour un point en BDD 6122 * @param {string} idPath - ID du tracé 6123 * @param {Array} indexes - Indexes des points 6124 * @returns {Promise<Object>} - Données du point enregistré 6125 */ 6126 getAltitudeForPoints: async function(idPath, indexes) { 6127 const points = []; 6128 indexes.forEach(index => { 6129 if (!this.store[idPath] || !this.store[idPath][index]) { 6130 console.warn(`Point introuvable: chemin ${idPath}, index ${index}`); 6131 return; 6132 } 6133 6134 const point = this.store[idPath][index]; 6135 6136 const data = { 6137 lng: point.lng, 6138 lat: point.lat, 6139 id: point.id 6140 }; 6141 points.push(data) 6142 }); 6143 const datas = {}; 6144 datas.points = points; 6145 6146 async function getAltitudeFromMNT(datas) { 6147 let responseText; 6148 try { 6149 const fetchResponse = await fetch('/index.php?component=software&task=getAltitudeFromMNT', { 6150 method: 'POST', 6151 headers: { 6152 'Content-Type': 'application/json' 6153 }, 6154 body: JSON.stringify(datas), 6155 }); 6156 if (!fetchResponse.ok) { 6157 throw new Error(sprintf( 6158 i18n("api_error_http_status"), 6159 fetchResponse.status 6160 )); 6161 } 6162 responseText = await fetchResponse.text(); 6163 } catch(error) { 6164 console.error(`Erreur AJAX pour les points:`, error.message); 6165 throw new Error(i18n("api_error_invalid_server_response")); 6166 } 6167 let response; 6168 try { 6169 response = JSON.parse(responseText); 6170 } catch (e) { 6171 console.error("Erreur JSON.parse sur réponse serveur pour les points"); 6172 console.error("Réponse brute :", responseText); 6173 throw new Error(i18n("api_error_json_parse")); 6174 } 6175 6176 if (response.points && Array.isArray(response.points)) { 6177 return response.points; 6178 } else { 6179 throw new Error(i18n("api_error_invalid_points")); 6180 } 6181 } 6182 try { 6183 const responsePoints = await getAltitudeFromMNT(datas);
6184 responsePoints.forEach((pointResponse) => { 6185 if (pointResponse.id) { 6186 let path = _this.store[idPath]; 6187 let index = path.findIndex(function(p){ 6188 return p.id == pointResponse.id; 6189 }) 6190 if(index !== -1) { 6191 _this.path.updatePoint(idPath, index, { 6192 altitude: pointResponse.altitude, 6193 }) 6194 } else { 6195 throw new Error(sprintf( 6196 i18n("api_error_incompatible_point"), 6197 pointResponse 6198 )); 6199 } 6200 } 6201 }); 6202 console.info('Altitudes calculées avec succès'); 6203 } catch (error) { 6204 console.error(error); 6205 } 6206 }, 6207 6208 updateWaypoints: function (idPath) { 6209 const points = _this.store[idPath]; 6210 if (!points || !Array.isArray(points) || !points.length) return; 6211 const wpts = _this.markers?.list?.wpts || []; 6212 if (!Array.isArray(wpts) || !wpts.length) return; 6213 6214 points.forEach((point) => { 6215 for (let j = 0; j < wpts.length; j++) { 6216 const el = wpts[j].getElement?.(); 6217 if (!el) continue; 6218 6219 if (`point-${point.id}` !== el.id) continue; 6220 6221 const distance = Number(this.convertDistance(point?.distance ?? 0)).toFixed(2); 6222 const altitude = this.convertAltitude(point?.altitude ?? 0); 6223 const popupMessage = 6224 "<div><img height='28' width='28' src='/img/fiches/distance.svg' /> " + 6225 settings.currentUnits.distance + " " + distance + "</div>" + 6226 "<div><img height='28' width='28' src='/img/fiches/altitude.svg' /> alt " + 6227 altitude + settings.currentUnits.altitude + "</div>"; 6228 6229 const wptInfo = []; 6230 6231 if (typeof point.textWP === 'string' && point.textWP.trim() !== '') { 6232 wptInfo.push( 6233 "<div><img height='28' width='28' src='/img/fiches/indication.svg' /> " + 6234 point.textWP.trim() + 6235 "</div>" 6236 ); 6237 } 6238 6239 if (typeof point.htmlEG === 'string') { 6240 const eg = point.htmlEG.trim(); 6241 if (eg !== '') { 6242 wptInfo.push(eg); 6243 } 6244 } 6245 6246 const html = popupMessage + wptInfo.join(''); 6247 6248 wpts[j].setPopup( 6249 new maplibregl.Popup({ 6250 focusAfterOpen: false, 6251 offset: { bottom: [0, -50] } 6252 }).setHTML(html) 6253 ); 6254 6255 break; 6256 } 6257 }); 6258 }, 6259 /** 6260 * met à jour le diagramme et les infos du tracé 6261 * @param {string} idPath - ID du tracé 6262 */ 6263 updateTracePostOperation: async function(idPath, storeInLocalStorage = true) { 6264 if (!idPath || !_this.store[idPath] || _this.store[idPath].length < 2) { 6265 return; 6266 } 6267 // Accès au bon chemin 6268 const path = _this.store[idPath]; 6269 6270 // Génération des données pour le diagramme (distance cumulée + altitude) 6271 const altitudeChartData = []; 6272 let cumulativeDist = 0; 6273 let lastPointWithAltitude = null; // dernier point avec altitude traité 6274 6275 path.forEach((point, i) => { 6276 if (typeof point.altitude !== 'number') return; 6277 if (lastPointWithAltitude !== null) { 6278 // Calcul de la distance depuis le dernier point avec altitude connu 6279 // (et non plus depuis path[i-1] qui peut ne pas avoir d'altitude) 6280 const segmentDist = calculateDistanceApprox( 6281 [lastPointWithAltitude.lng, lastPointWithAltitude.lat], 6282 [point.lng, point.lat] 6283 ); 6284 cumulativeDist += segmentDist; 6285 } 6286 _this.store[idPath][i].distance = cumulativeDist; 6287 lastPointWithAltitude = point; 6288 6289 altitudeChartData.push([+_this.convertDistance(cumulativeDist.toFixed(3)), Math.round(_this.convertAltitude(point.altitude))]); 6290 6291 }) 6292 // Mise à jour des métadonnées à partir des points enrichis 6293 const data = {}; 6294 data.metaData = _this.computeMetaDataFromPoints(_this.store[idPath]); 6295 data.altitudeChartData = altitudeChartData; 6296 // Mise à jour du diagramme 6297 _this.tab.update('diagrams', idPath, data, _this.swiper.diagrams); 6298 _this.tab.update('infos', idPath, data, _this.swiper.infos); 6299 _this.updateWaypoints(idPath); 6300 if (storeInLocalStorage) { 6301 _this.storePathsLocalStorage(); 6302 } 6303 }, 6304 6305 /** 6306 * Calcule les métadonnées à partir d'une liste de points 6307 * @param {Array} points - Liste de points [lng, lat, altitude] 6308 * @returns {Object} - Métadonnées calculées 6309 */ 6310 computeMetaDataFromPoints: function (points) { 6311 let pos = 0, neg = 0, dist = 0, altitudeReference = 0; 6312 let high = -Infinity, low = Infinity; 6313 let prev = null; 6314 let countPts = 0; 6315 let deltaList = '';
6316 points.forEach((pt) => { 6317 if (typeof pt.altitude !== 'number') return; // ignore les points sans altitude 6318 6319 high = Math.max(high, pt.altitude); 6320 low = Math.min(low, pt.altitude); 6321 6322 if (prev) { 6323 const delta = pt.altitude - altitudeReference; 6324 if (Math.abs(delta) >= settings.heightDifferenceLimit) { 6325 altitudeReference = pt.altitude; 6326 if (delta > 0) pos += delta; 6327 else if (delta < 0) neg += Math.abs(delta); 6328 } 6329 dist += calculateDistanceApprox( 6330 [pt.lng, pt.lat], 6331 [prev.lng, prev.lat] 6332 ) * 1000; 6333 } else { 6334 altitudeReference = pt.altitude; 6335 } 6336 6337 prev = pt; 6338 countPts++; 6339 }); 6340 6341 const durationMinutes = Math.round((dist / 1000 + pos / 100) / 3.5 * 60); 6342 let h = Math.floor(durationMinutes / 60); 6343 let m = 5 * Math.round((durationMinutes % 60) / 5); 6344 if (m === 0) m = '00'; 6345 else if (m === 5) m = '05'; 6346 else if (m === 60) { 6347 m = '00'; 6348 h++; 6349 } 6350 6351 const duration = `${h > 0 ? h + 'h ' : ''}${m}${h === 0 && m > 0 ? 'min' : ''}`; 6352 6353 return { 6354 'duration': duration, 6355 'distance': this.convertDistance(dist / 1000).toFixed(2), 6356 'pos-elevation': this.convertAltitude(Math.round(pos)), 6357 'neg-elevation': this.convertAltitude(Math.round(neg)), 6358 'high-point': this.convertAltitude(Math.round(high)), 6359 'low-point': this.convertAltitude(Math.round(low)), 6360 'count-point': countPts 6361 }; 6362 }, 6363 storePathsLocalStorage: function () { 6364 try { 6365 const pathCopy = JSON.parse(JSON.stringify(this.store)); 6366 let colors = new Object(); 6367 Object.keys(this.store).forEach(function (idPath) { 6368 const feature = _this.drawGl.get(idPath); 6369 colors[idPath] = feature?.properties?.user_color || feature?.properties?.color || settings.hexColor; 6370 }); 6371 6372 // BUGFIX : Appel de cleanLocalStorage() au lieu de localStorage.clear() 6373 // qui effacerait tout le localStorage du domaine 6374 _this.cleanLocalStorage(); 6375 6376 // Puis réécriture des nouvelles valeurs 6377 localStorage.setItem('visorando_paths', JSON.stringify(pathCopy)); 6378 localStorage.setItem('visorando_paths_colors', JSON.stringify(colors)); 6379 localStorage.setItem('visorando_paths_idRando', settings.idRando); 6380 localStorage.setItem('visorando_paths_idRandoHash', settings.idRandoHash); 6381 localStorage.setItem('visorando_paths_time', Date.now()); 6382 } catch (e) { 6383 console.error("Erreur lors de la sauvegarde des tracés:", e); 6384 iziToast.error({ 6385 message: i18n("toast_msg_trace_too_long") 6386 }); 6387 } 6388 }, 6389 6390 cleanLocalStorage: function () { 6391 try { 6392 localStorage.removeItem('visorando_paths'); 6393 localStorage.removeItem('visorando_paths_colors'); 6394 localStorage.removeItem('visorando_paths_idRando'); 6395 localStorage.removeItem('visorando_paths_idRandoHash'); 6396 localStorage.removeItem('visorando_paths_time'); 6397 sessionStorage.removeItem("stoGpxTitle"); 6398 sessionStorage.removeItem("stoGpxDescription"); 6399 } catch (e) { 6400 console.error("Erreur lors du nettoyage du localStorage:", e); 6401 } 6402 }, 6403 6404 getPathsLocalStorage: async function (mapReady, fetchGeoJSON) { 6405 try { 6406 const localPaths = localStorage.getItem('visorando_paths'); 6407 if (localPaths === null || localPaths === 'null') {
6408 _this.emptyPathsFromLocalStorage(mapReady, fetchGeoJSON); 6409 return; 6410 } 6411 const paths = JSON.parse(localPaths); 6412 if (paths.length === 'undefined' || paths.length === 0) { 6413 _this.emptyPathsFromLocalStorage(mapReady, fetchGeoJSON); 6414 return; 6415 } 6416 6417 const colors = JSON.parse(localStorage.getItem('visorando_paths_colors')); 6418 6419 const id = localStorage.getItem('visorando_paths_idRando'); 6420 6421 settings.idRando = (!localPaths || JSON.parse(localPaths).length === 0 || ['null', null].includes(id) || isNaN(+id)) ? null : +id; 6422 6423 if (settings.idRando && settings.idRando > 0) { 6424 const response = await fetch('index.php?component=software&task=isAuthor&idRandonnee=' + settings.idRando, { 6425 method: 'GET' 6426 }); 6427 const json = await response.json(); 6428 if (!json || !json.isAuthor) { 6429 _this.emptyPathsFromLocalStorage(mapReady, fetchGeoJSON); 6430 return; 6431 } 6432 } 6433 settings.idRandoHash = localStorage.getItem('visorando_paths_idRandoHash'); 6434 if (settings.idRandoHash === null || settings.idRandoHash === 'null') { 6435 settings.idRandoHash = ''; 6436 } 6437 let dateLocalStorage = localStorage.getItem('visorando_paths_time'); 6438 if (dateLocalStorage === null || dateLocalStorage === 'null') { 6439 dateLocalStorage = 0; 6440 } 6441 if (dateLocalStorage && dateLocalStorage > 0) { 6442 const now = Date.now(); 6443 if ((now - dateLocalStorage) > settings.localStorageExpiration) { 6444 const confirmOpts = Object.assign({ 6445 title: i18n("toast_title_get_routes_from_local"), 6446 message: i18n("toast_msg_get_routes_from_local"), 6447 buttons: [ 6448 [`<button class="btn s">${i18n("toast_button_yes")}</button>`, 6449 function(instance, toast) { 6450 _this.processInitPaths(paths, [], mapReady, fetchGeoJSON); 6451 instance.hide({}, toast); 6452 }, 6453 true 6454 ], 6455 [`<button class="btn e">${i18n("toast_button_no")}</button>`, 6456 function(instance, toast) { 6457 _this.emptyPathsFromLocalStorage(mapReady, fetchGeoJSON); 6458 instance.hide({transitionOut: 'fadeOut'}, toast); 6459 }, 6460 false 6461 ] 6462 ] 6463 }, _this.commonConfirmOpts); 6464 6465 iziToast.show(confirmOpts) 6466 } else { 6467 _this.processInitPaths(paths, colors, mapReady, fetchGeoJSON); 6468 } 6469 } else { 6470 _this.processInitPaths([], [], mapReady, fetchGeoJSON); 6471 } 6472 6473 } catch (e) { 6474 console.error("Erreur lors de la récupération des tracés:", e); 6475 _this.processInitPaths([], [], mapReady, fetchGeoJSON); 6476 } 6477 }, 6478 emptyPathsFromLocalStorage: function (mapReady, fetchGeoJSON) { 6479 settings.idRando = 0; 6480 settings.idRandoHash = ''; 6481 _this.cleanLocalStorage(); 6482 _this.processInitPaths([], [], mapReady, fetchGeoJSON); 6483 }, 6484 processInitPaths: function(paths, colors, mapReady, fetchGeoJSON, storeInLocalStorage = true) { 6485 if (!paths || typeof paths !== 'object') { 6486 console.warn('processInitPaths: paths est null ou invalide', paths); 6487 return; 6488 } 6489 6490 var nbPath = Object.keys(paths).length, 6491 nbPoints = 0; 6492 6493 if(nbPath > 1 && settings.isSimpleVersion){ 6494 6495 iziToast.show({ 6496 message: settings.user.hasPremiumAccess ? 6497 i18n("toast_msg_advanced_mode_required") : 6498 i18n("toast_msg_premium_needed"), 6499 closeOnClick: false, 6500 color: 'white', 6501 progressBar: false, 6502 drag: false, 6503 timeout: false, 6504 close: false, 6505 overlay: true, 6506 position: 'center', 6507 buttons: settings.user.hasPremiumAccess ? 6508 [ 6509 [`<button class="btn s">${i18n("toast_button_activate_advanced")}</button>`, 6510 function(instance, toast) { 6511 instance.hide({transitionOut: 'fadeOut'}, toast) 6512 _this.activeAction('switch-version') 6513 }, 6514 true 6515 ] 6516 ] : [ 6517 [`<button class="btn s">${i18n("toast_button_subscribe_premium")}</button>`, 6518 function(instance, toast) { 6519 window.location = settings.clubUrl 6520 }, 6521 true 6522 ] 6523 ] 6524 }) 6525 6526 if(!settings.user.hasPremiumAccess){ 6527 return 6528 } 6529 } 6530 6531 mapReady.done(function(){ 6532 $.each(paths, function(pathId, points){ 6533 var path = turf.feature({ 6534 type: 'LineString', 6535 coordinates: [] 6536 }); 6537 6538 // skip iteration 6539 if(points.length < 2) { 6540 nbPath--; 6541 return true 6542 } 6543
6544 // Par convention et pour maintenir une unicité dans drawGl 6545 path.id = pathId 6546 nbPoints += points.length 6547 // Pour chaque points du tracé courant... 6548 $.each(points, function(order, point){ 6549 point.order = +order 6550 _this.path.insertPoint(pathId, order, point) 6551 6552 path.geometry.coordinates.push([point.lng, point.lat]) 6553 }) 6554 6555 _this.drawGl.add(path); 6556 }); 6557 6558 _this.path.handleMarkersPath(false) 6559 _this.setInitialDescriptionWaypointNumbers(); 6560 6561 if(_this.path.getAll().length){ 6562 _this.actions.center(null, 1) 6563 _this.history.historyStore[0] = { 6564 store: Immutable.fromJS(_this.store) 6565 } 6566 } 6567 6568 if (nbPoints) { 6569 const nbPathNum = Number(nbPath); 6570 const nbPointsNum = Number(nbPoints); 6571 6572 iziToast.success({ 6573 message: sprintf( 6574 i18n("toast_msg_trace_points_loaded_success"), 6575 nbPathNum 6576 ) 6577 }); 6578 } 6579 6580 if(nbPath) { 6581 _this.drawGl.changeMode('simple_select', { 6582 featureIds: _this.path.getAll() 6583 }) 6584 _this.activeMode('simple_select'); 6585 _this.updateGroupActions() 6586 _this.updateHelper() 6587 } else { 6588 _this.drawGl.changeMode('draw_line_string') 6589 _this.activeMode('draw_line_string') 6590 } 6591 6592 _this.path.getAll().forEach((pathId) => { 6593 let color = settings.hexColor; 6594 if (colors !== null && colors !== 'null' && colors[pathId] !== undefined && colors[pathId] !== null) { 6595 color = colors[pathId]; 6596 } 6597 _this.changePathColor(pathId, color); 6598 _this.drawGl.renderCanvas(); 6599 _this.updateTracePostOperation(pathId, storeInLocalStorage); 6600 }) 6601 6602 // Synchroniser le color picker avec la couleur du premier tracé chargé 6603 const firstPathId = _this.path.getAll()[0]; 6604 if (firstPathId) { 6605 const firstFeature = _this.drawGl.get(firstPathId); 6606 const firstColor = firstFeature?.properties?.user_color || firstFeature?.properties?.color; 6607 if (firstColor) { 6608 $('#color-picker-dropdown ul li').removeClass('active'); 6609 $('#color-picker-dropdown ul li a').each(function() { 6610 if ($(this).data('color') === firstColor) { 6611 $(this).parent().addClass('active'); 6612 $('#color-picker-dropdown .dropdown-toggle .color-circle').
6612css('background-color', firstColor); 6613 } 6614 }); 6615 } 6616 } 6617 6618 fetchGeoJSON.resolve() 6619 6620 }) 6621 }, 6622 changePathColor: function(pathId, color) { 6623 const feature = _this.drawGl.get(pathId); 6624 if (!feature) return; 6625 6626 feature.properties = feature.properties || {}; 6627 feature.properties.color = color; 6628 feature.properties.user_color = color; 6629 6630 if (typeof _this.drawGl.setFeatureProperty === 'function') { 6631 _this.drawGl.setFeatureProperty(pathId, 'color', color); 6632 _this.drawGl.setFeatureProperty(pathId, 'user_color', color); 6633 } 6634 }, 6635 convertDistance: function(km) { 6636 if (settings.currentUnits.distance=='mi') { 6637 const MILES_PER_KM = 0.621371; 6638 return km * MILES_PER_KM; 6639 } 6640 return km; 6641 }, 6642 6643 convertAltitude: function(meters) { 6644 if (settings.currentUnits.altitude=='ft') { 6645 const FEET_PER_METER = 3.28084; 6646 return Math.round(meters * FEET_PER_METER); 6647 } 6648 return Math.round(meters); 6649 }, 6650 projectPointOnSegment: function(clickPoint, segmentStart, segmentEnd) { 6651 const A = { x: segmentStart[0], y: segmentStart[1] }; 6652 const B = { x: segmentEnd[0], y: segmentEnd[1] }; 6653 const P = { x: clickPoint.lng, y: clickPoint.lat }; 6654 6655 const AB = { x: B.x - A.x, y: B.y - A.y }; 6656 const AP = { x: P.x - A.x, y: P.y - A.y }; 6657 6658 const AB_squared = AB.x * AB.x + AB.y * AB.y; 6659 const AP_AB = AP.x * AB.x + AP.y * AB.y; 6660 6661 let t = AB_squared !== 0 ? AP_AB / AB_squared : 0; 6662 t = Math.max(0, Math.min(1, t)); // Contraindre t entre 0 et 1 6663 6664 return { 6665 lng: A.x + t * AB.x, 6666 lat: A.y + t * AB.y 6667 }; 6668 } 6669 } 6670 // âââââââââââââââââââââââââââââââââââââââââââââââ 6671 // COLOR PICKER LOGIC 6672 // âââââââââââââââââââââââââââââââââââââââââââââââ 6673 $('#color-picker-dropdown .dropdown-item').on('click', function (e) { 6674 6675 var selectedColor = $(this).data('color'); 6676 $('#color-picker-dropdown li').removeClass('active'); 6677 $(this).closest('li').addClass('active'); 6678 $('#color-picker-dropdown .dropdown-toggle .color-circle').
6678css('background-color', selectedColor); 6679 6680 let activeId = _this.path.getActivePathId(); 6681 6682 // Si aucun tracé actif (ex : modification d'un circuit existant sans sélection explicite), 6683 // appliquer la couleur à tous les tracés disponibles 6684 if (!activeId || activeId.length === 0) { 6685 activeId = _this.path.getAll(); 6686 } 6687 6688 try { 6689 activeId.forEach((id) => { 6690 _this.changePathColor(id, selectedColor); 6691 $('.swiper-pagination-bullet[data-path-id=' + id + ']').css('background-color', selectedColor); 6692 }) 6693 _this.drawGl.renderCanvas(); 6694 6695 _this.path.getAll().forEach((pathId) => { 6696 _this.updateTracePostOperation(pathId); 6697 }) 6698 6699 if (typeof _this.path?.handleMarkersPath === 'function') { 6700 _this.path.handleMarkersPath(true); 6701 } 6702 6703 if (activeId.length > 0) { 6704 iziToast.success({ 6705 message: i18n("toast_msg_color_applied"), 6706 position: 'bottomCenter', 6707 timeout: 2000 6708 }); 6709 } 6710 6711 _paq.push(['trackEvent', 6712 'Route planner', 6713 'Change path color', 6714 'Set path color to ' + selectedColor, 6715 ]); 6716 6717 } catch (err) { 6718 console.error('Erreur couleur:', err); 6719 iziToast.error({ 6720 message: sprintf( 6721 i18n("toast_msg_color_change_failed"), 6722 err.message 6723 ), 6724 position: 'bottomCenter', 6725 timeout: 3000 6726 }); 6727 } 6728 }); 6729 6730 return _this 6731})() 6732 6733 6734 $.fn.hasScrollBar = function() { 6735 return this.prop('scrollHeight') > this.prop('clientHeight'); 6736 } 6737 6738 $.fn.alterClass = function ( removals, additions ) { 6739 6740 var self = this; 6741 if ( removals.indexOf( '*' ) === -1 ) { 6742 // Use native jQuery methods if there is no wildcard matching 6743 self.removeClass( removals ); 6744 return !additions ? self : self.addClass( additions ); 6745 } 6746 var patt = new RegExp( '\\s' + 6747 removals. 6748 replace( /\*/g, '[A-Za-z0-9-_]+' ). 6749 split( ' ' ). 6750 join( '\\s|\\s' ) + 6751 '\\s', 'g' ); 6752 self.each( function ( i, it ) { 6753 var cn = ' ' + it.className + ' '; 6754 while ( patt.test( cn ) ) { 6755 cn = cn.replace( patt, ' ' ); 6756 } 6757 it.className = $.trim( cn ); 6758 }); 6759 return !additions ? self : self.addClass( additions ); 6760 }; 6761 6762 /** 6763 * Calcule la distance approximative entre deux points en kilomètres 6764 * @param {Array} point1 - Coordonnées [lng, lat] 6765 * @param {Array} point2 - Coordonnées [lng, lat] 6766 * @returns {number} - Distance en kilomètres 6767 */ 6768 function calculateDistanceApprox(point1, point2) { 6769 const toRad = value => value * Math.PI / 180; 6770 const R = 6371; // Rayon de la Terre en km 6771 6772 const dLat = toRad(point2[1] - point1[1]); 6773 const dLon = toRad(point2[0] - point1[0]); 6774 6775 const a = Math.sin(dLat/2) * Math.sin(dLat/2) + 6776 Math.cos(toRad(point1[1])) * Math.cos(toRad(point2[1])) * 6777 Math.sin(dLon/2) * Math.sin(dLon/2); 6778 6779 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 6780 return R * c; 6781 } 6782 6783 function computeMapDim(){ 6784 var dim = _module.mapGl.getContainer().getBoundingClientRect() 6785 return { w: dim.width, h: dim.height }; 6786 } 6787 6788 function throttleWithoutTrailing(callback, delay) { 6789 var throttleTimeout = null; 6790 var storedEvent = null; 6791 6792 var throttledEventHandler = function(e) { 6793 storedEvent = e; 6794 6795 var shouldHandleEvent = !throttleTimeout; 6796 6797 if (shouldHandleEvent) { 6798 callback(storedEvent); 6799 6800 storedEvent = null; 6801 6802 throttleTimeout = setTimeout(function() { 6803 throttleTimeout = null; 6804 6805 if (storedEvent) { 6806 throttledEventHandler(storedEvent); 6807 } 6808 }, delay); 6809 } 6810 }; 6811 6812 return throttledEventHandler; 6813 }; 6814 6815 function throttle(func, limit) { 6816 var lastFunc; 6817 var lastRan; 6818 return function () { 6819 var context = this; 6820 var args = arguments; 6821 6822 if (!lastRan) {
6823 func.apply(context, args); 6824 lastRan = Date.now(); 6825 } else { 6826 clearTimeout(lastFunc); 6827 lastFunc = setTimeout(function () { 6828 if (Date.now() - lastRan >= limit) { 6829 func.apply(context, args); 6830 lastRan = Date.now(); 6831 } 6832 }, limit - (Date.now() - lastRan)); 6833 } 6834 }; 6835 }; 6836 6837 /** 6838 * Fonction pour récupérer le mode de transport sélectionné 6839 * @returns {string} - Le mode de transport sélectionné (pédestre ou cycliste) 6840 */ 6841 function getSelectedMode() { 6842 // Disable selector for now 6843 return 'pedestrian'; 6844 // const activeItem = document.querySelector('#auto-trace-modes .dropdown-item.active'); 6845 // return activeItem ? activeItem.dataset.mode : 'pedestrian'; // fallback safe 6846 } 6847 6848 function isPromise(promise){ 6849 return promise && typeof promise.then == 'function' 6850 } 6851 6852 function _get(obj, prop, defaultValue){ 6853 if(obj.hasOwnProperty(prop)){ 6854 return obj[prop] 6855 } 6856 return defaultValue || null 6857 } 6858 6859 $.extend({ 6860 getCSS: function(urls, callback, nocache){ 6861 $.when.apply($, 6862 $.map(urls, function(url){ 6863 if (!!nocache) url += '?_ts=' + new Date().getTime(); 6864 return $.get(url, function(){ 6865 $('<link>', {rel:'stylesheet', type:'text/css', 'href':url}).appendTo('head'); 6866 }); 6867 }) 6868 ).always(function(){ 6869 if (typeof callback=='function') callback(); 6870 }); 6871 }, 6872 }); 6873 6874 return _module.init() 6875})); 6876 6877 6878// iOS workaround: MapLibre v5 uses Pointer Events + touch-action:none on canvas, 6879// which prevents iOS from synthesizing click events on sibling elements. 6880// Fix: listen to pointerup globally and trigger click() on UI elements outside the map canvas. 6881(function() { 6882 if (!(/iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1))) return; 6883 6884 var UI_SELECTORS = 'a, button, label, input, [role="button"], .tab, .expand-handler'; 6885 var MAP_UI_SELECTORS = '.maplibregl-ctrl button, .maplibregl-ctrl a, .vrmap-close-panel, .vrmap-baselayer-thumbnail, .vrmap-map-layers-panel label, .vrmap-map-layers-panel input'; 6886 6887 document.addEventListener('pointerup', function(e) { 6888 if (e.button !== 0) return; 6889 var target = e.target.closest(UI_SELECTORS) || e.target.closest(MAP_UI_SELECTORS); 6890 if (target && !target.closest('.maplibregl-canvas-container')) { 6891 target.click(); 6892 } 6893 }); 6894})(); 6895 6896$(function () { 6897 const $toolbar = $('.responsive-toolbar'); 6898 let isDragging = false; 6899 let offsetX = 0; 6900 let offsetY = 0; 6901 let toolbarMoved = false; 6902 let autoTraceEnabled = $('#auto-trace-toggle').prop('checked'); 6903 let selectedColor = '#FF7F00'; 6904 let selectedMode = 'pédestre'; 6905 6906 // âââââââââââââââââââââââââââââââââââââââââââââââ 6907 // CENTER TOOLBAR INITIALLY (accurate centering) 6908 // âââââââââââââââââââââââââââââââââââââââââââââââ 6909 function centerToolbar() { 6910 if (toolbarMoved) return; 6911 6912 const winWidth = $(window).width(); 6913 const tbWidth = $toolbar.outerWidth(); 6914 const centeredLeft = (winWidth - tbWidth) / 2; 6915 6916 $toolbar.css({ 6917 left: `${centeredLeft}px`, 6918 top: '60px', 6919 transform: 'none', 6920 width: '' 6921 }); 6922 } 6923 6924 centerToolbar(); 6925 6926 // âââââââââââââââââââââââââââââââââââââââââââââââ 6927 // MODE BUTTON VISIBILITY CONTROL 6928 // âââââââââââââââââââââââââââââââââââââââââââââââ 6929 function refreshModeToggleVisibility() { 6930 if (autoTraceEnabled) { 6931 $('#auto-trace-mode-toggle').show(); 6932 } else { 6933 $('#auto-trace-mode-toggle').hide(); 6934 $('.modes-dropdown').hide(); 6935 } 6936 } 6937 6938 refreshModeToggleVisibility(); 6939 6940 // âââââââââââââââââââââââââââââââââââââââââââââââ 6941 // CLAMPING TOOLBAR INSIDE WINDOW 6942 // âââââââââââââââââââââââââââââââââââââââââââââââ 6943 function clampPosition(left, top) { 6944 const winWidth = $(window).width(); 6945 const winHeight = $(window).height(); 6946 const tbWidth = $toolbar.outerWidth(); 6947 const tbHeight = $toolbar.outerHeight(); 6948 return { 6949 left: Math.max(0, Math.min(left, winWidth - tbWidth)), 6950 top: Math.max(0, Math.min(top, winHeight - tbHeight)) 6951 }; 6952 } 6953 6954 // âââââââââââââââââââââââââââââââââââââââââââââââ 6955 // DRAG START (Mouse & Touch) 6956 // âââââââââââââââââââââââââââââââââââââââââââââââ 6957 function startDrag(pageX, pageY) { 6958 isDragging = true; 6959 toolbarMoved = true; 6960 6961 // Cacher les dropdowns pour éviter des sauts de layout 6962 $('.dropdown').removeClass('open'); 6963 6964 // Supprimer les transitions pendant le drag 6965 $toolbar.css({ 6966 transition: 'none', 6967 transform: 'none' 6968 }); 6969 6970 // Forcer le reflow 6971 void $toolbar[0].offsetWidth; 6972 6973 // Fixer la largeur actuelle pour éviter les sauts 6974 const currentWidth = $toolbar.outerWidth(); 6975 $toolbar.css('width', currentWidth + 'px'); 6976 6977 // Fixer position actuelle 6978 const offset = $toolbar.offset(); 6979 offsetX = pageX - offset.left; 6980 offsetY = pageY - offset.top; 6981 6982 $toolbar 6983 .addClass('dragging') 6984 .css({ 6985 position: 'fixed', 6986 left: offset.left + 'px', 6987 top: offset.top + 'px' 6988 }); 6989 } 6990
6991 function endDrag() { 6992 isDragging = false; 6993 6994 const left = parseInt($toolbar.css('left'), 10); 6995 const top = parseInt($toolbar.css('top'), 10); 6996 6997 $toolbar.removeClass('dragging'); 6998 6999 // Remet en place la transition douce 7000 $toolbar.css({ 7001 left: `${left}px`, 7002 top: `${top}px`, 7003 transform: 'none' 7004 }); 7005 7006 // Délaye la remise du width à auto (évite glitch visuel immédiat) 7007 setTimeout(() => { 7008 $toolbar.css({ 7009 width: '', 7010 transition: '' 7011 }); 7012 }, 50); 7013 } 7014 7015 // DRAG HANDLE ONLY â Mouse 7016 $toolbar.on('mousedown', '.drag-handle', function (e) { 7017 e.preventDefault(); 7018 e.stopPropagation(); 7019 startDrag(e.pageX, e.pageY); 7020 }); 7021 7022 // DRAG HANDLE ONLY â Touch 7023 $toolbar.on('touchstart', '.drag-handle', function (e) { 7024 const touch = e.originalEvent.touches[0]; 7025 e.preventDefault(); 7026 e.stopPropagation(); 7027 startDrag(touch.pageX, touch.pageY); 7028 }); 7029 7030 // âââââââââââââââââââââââââââââââââââââââââââââââ 7031 // ð± MOUSE DRAG EVENTS 7032 // âââââââââââââââââââââââââââââââââââââââââââââââ 7033 $toolbar.on('mousedown', '.drag-handle', function (e) { 7034 e.preventDefault(); 7035 e.stopPropagation(); 7036 startDrag(e.pageX, e.pageY); 7037 }); 7038 7039 $(document).on('mousemove', function (e) { 7040 if (isDragging) { 7041 e.preventDefault(); 7042 const { left, top } = clampPosition(e.pageX - offsetX, e.pageY - offsetY); 7043 $toolbar.css({ left: `${left}px`, top: `${top}px` }); 7044 } 7045 }); 7046 7047 $(document).on('mouseup', function () { 7048 if (isDragging) endDrag(); 7049 }); 7050 7051 // âââââââââââââââââââââââââââââââââââââââââââââââ 7052 // TOUCH DRAG EVENTS 7053 // âââââââââââââââââââââââââââââââââââââââââââââââ 7054 $toolbar.on('touchstart', '.drag-handle', function (e) { 7055 const touch = e.originalEvent.touches[0]; 7056 startDrag(touch.pageX, touch.pageY); 7057 }); 7058 7059 $(document).on('touchmove', function (e) { 7060 if (isDragging) { 7061 e.preventDefault(); 7062 const touch = e.originalEvent.touches[0]; 7063 const { left, top } = clampPosition(touch.pageX - offsetX, touch.pageY - offsetY); 7064 $toolbar.css({ left: `${left}px`, top: `${top}px` }); 7065 } 7066 }); 7067 7068 $(document).on('touchend', function () { 7069 if (isDragging) endDrag(); 7070 }); 7071 7072 // âââââââââââââââââââââââââââââââââââââââââââââââ 7073 // TOGGLES & DROPDOWNS 7074 // âââââââââââââââââââââââââââââââââââââââââââââââ 7075 // $('#auto-trace-mode-toggle').on('click', function (e) { 7076 // if (!autoTraceEnabled) return; 7077 // e.stopPropagation(); 7078 // $('.dropdown-menu').not('#auto-trace-modes').hide(); 7079 // $('#auto-trace-modes').toggle(); 7080 // }); 7081 7082 $('#auto-trace-toggle').on('click', function () { 7083 7084 autoTraceEnabled = !autoTraceEnabled; 7085 7086 $(this).closest('.responsive-toolbar').toggleClass('auto-trace-toolbar-enabled auto-trace-toolbar-disabled'); 7087 7088 if (typeof VISORANDO_SOFTWARE?.toggleAutoTracing === 'function') { 7089 VISORANDO_SOFTWARE.toggleAutoTracing(this); 7090 } 7091 refreshModeToggleVisibility(); 7092 }); 7093 7094 // $('#more-menu-toggle').on('click', function (e) { 7095 // e.stopPropagation(); 7096 // $('.dropdown-menu').not('#more-menu-dropdown').hide(); 7097 // $('#more-menu-dropdown').toggle(); 7098 // }); 7099 // 7100 // $('#color-picker-toggle').on('click', function (e) { 7101 // e.stopPropagation(); 7102 // $('.dropdown-menu').not('#color-picker-dropdown').hide(); 7103 // $('#color-picker-dropdown').toggle(); 7104 // }); 7105 7106 $(document).on('mousedown', function (e) { 7107 if (!$(e.target).closest('.responsive-toolbar').length) { 7108 $('.dropdown').removeClass('open'); 7109 } 7110 }); 7111 7112 // âââââââââââââââââââââââââââââââââââââââââââââââ 7113 // MODE SELECTOR 7114 // âââââââââââââââââââââââââââââââââââââââââââââââ 7115 $('#auto-trace-modes .dropdown-item').on('click', function () { 7116 7117 selectedMode = $(this).data('mode'); 7118 $('#auto-trace-modes li').removeClass('active'); 7119 $(this).closest('li').addClass('active'); 7120 $('#auto-trace-modes .dropdown-toggle .auto-trace-mode-text').text('Mode : ' + $(this).text()); 7121 7122 if (typeof VISORANDO_SOFTWARE !== 'undefined') { 7123 VISORANDO_SOFTWARE.selectedRoutingService = () => selectedRoutingService; 7124 VISORANDO_SOFTWARE.selectedRoutingMode = () => selectedMode; 7125 } 7126 7127 console.log('Mode de routage sélectionné :', selectedMode); 7128 }); 7129 7130 // âââââââââââââââââââââââââââââââââââââââââââââââ 7131 // AUTO-TRACE INIT PATCH 7132 // âââââââââââââââââââââââââââââââââââââââââââââââ 7133 if (typeof VISORANDO_SOFTWARE?.toggleAutoTracing === 'function') { 7134 if (!$('#auto-trace').hasClass('active')) { 7135 VISORANDO_SOFTWARE.toggleAutoTracing(document.getElementById('auto-trace-toggle')); 7136 } 7137 } 7138 7139 // âââââââââââââââââââââââââââââââââââââââââââââââ 7140 // OPTIONAL: RECENTER ON WINDOW RESIZE 7141 // âââââââââââââââââââââââââââââââââââââââââââââââ 7142 $(window).on('resize', function () { 7143 centerToolbar(); 7144 }); 7145}); 7146$("#goToDonationIcon").click(function () { 7147 7148 _paq.push(['trackEvent', 7149 'Redirect to donation', 7150 'Redirect to donation page', 7151 'Click on map button that redirect to donation page information', 7152 7153 7154 ]); return true; 7155}); 7156 }); 7157 7158 <!-- Icone de chargement de la page de recherche --> 7159 $('.searchDiv').find('form').submit(function (ev) { 7160 ev.preventDefault(); 7161 $(this).css("display", "none").parent().find('span').css('display', 'table'); 7162 var that = this; 7163 setTimeout(function () { 7164 that.submit(); 7165 }, 1); 7166 }); 7167 </script>
7167 7168 </body> 7169</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.