1/** 2 * Module Montagut Stores JS - Version optimisée 3 * 4 * @author Evol.digital 5 * @copyright 2025 Montagut 6 * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) 7 */ 8 9// Variables globales pour la carte 10var montagutMap = null; 11var montagutMarkers = []; 12var nonOfficialMarkers = []; 13var officialMarkers = []; 14var montagutMarkerCluster = null; 15 16document.addEventListener('DOMContentLoaded', function() { 17 // Initialiser la carte si on est sur la page des magasins 18 if (document.getElementById('map')) { 19 initializeMap(); 20 } 21 22 // Code spécifique pour la page des magasins 23 if (document.querySelector('.montagut-stores')) { 24 // Gestion des erreurs de chargement des images 25 document.querySelectorAll('.store-image img').forEach(function(img) { 26 img.addEventListener('error', function() { 27 this.src = prestashop.urls.base_url + 'img/cms/stores/default.jpg'; 28 }); 29 }); 30 31 // Gestion du focus sur les champs de recherche 32 document.querySelectorAll('.search-form input, .search-form select').forEach(function(input) { 33 input.addEventListener('focus', function() { 34 this.parentNode.classList.add('focused'); 35 }); 36 37 input.addEventListener('blur', function() { 38 this.parentNode.classList.remove('focused'); 39 }); 40 }); 41 42 // Empêcher que le clic sur les liens dans les cartes de magasin ne déclenche le clic sur la carte 43 document.querySelectorAll('.store-card a').forEach(function(link) { 44 link.addEventListener('click', function(e) { 45 e.stopPropagation(); 46 }); 47 }); 48 49 // Gestion du bouton "Voir sur la carte" pour les magasins 50 document.querySelectorAll('.map-link-button').forEach(function(button) { 51 button.addEventListener('click', function(e) { 52 e.preventDefault(); 53 e.stopPropagation(); 54 55 // Récupérer les coordonnées du magasin 56 var storeLat = this.getAttribute('data-lat'); 57 var storeLng = this.getAttribute('data-lng'); 58 59 if (storeLat && storeLng && montagutMap) { 60 // Animation de scroll vers la carte 61 document.getElementById('map').scrollIntoView({ 62 behavior: 'smooth', 63 block: 'center' 64 }); 65 66 // Centrer la carte sur le magasin avec animation 67 montagutMap.flyTo([storeLat, storeLng], 17, { 68 animate: true, 69 duration: 1.5 70 }); 71 72 // Délai avant d'ouvrir le popup pour que l'animation soit fluide 73 setTimeout(function() { 74 // Chercher et ouvrir le popup correspondant 75 montagutMap.eachLayer(function(layer) { 76 if (layer instanceof L.Marker) { 77 var markerLat = layer.getLatLng().lat.toFixed(6); 78 var markerLng = layer.getLatLng().lng.toFixed(6); 79 80 if (markerLat === parseFloat(storeLat).toFixed(6) && 81 markerLng === parseFloat(storeLng).toFixed(6)) { 82 layer.openPopup(); 83 } 84 } 85 }); 86 }, 1000); 87 } 88 }); 89 }); 90 } 91 92 // Gestion du formulaire de recherche 93 var searchForm = document.getElementById('store-search-form'); 94 if (searchForm) { 95 searchForm.addEventListener('submit', handleStoreSearch); 96 } 97 98 // Redimensionnement de la fenêtre - pour que la carte s'adapte 99 window.addEventListener('resize', function() { 100 if (montagutMap) { 101 montagutMap.invalidateSize(); 102 } 103 }); 104}); 105 106/** 107 * Initialise la carte Leaflet avec animations et fonctionnalités améliorées 108 */ 109function initializeMap() { 110 // Création de la carte si l'élément existe 111 var mapElement = document.getElementById('map'); 112 if (!mapElement) return; 113 114 // Création de la carte centrée sur la France 115 montagutMap = L.map('map', { 116 zoomControl: false, // Nous ajouterons les contrôles de zoom manuellement 117 scrollWheelZoom: true, 118 dragging: true, 119 tap: true 120 }
120).setView([46.603354, 1.888334], 5); 121 122 // Ajouter les contrôles de zoom dans le coin en bas à droite 123 L.control.zoom({ 124 position: 'bottomright' 125 }).addTo(montagutMap); 126 127 // Ajout des tuiles avec style élégant monochrome 128 /* L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { 129 attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>', 130 subdomains: 'abcd', 131 maxZoom: 19 132 }).addTo(montagutMap);*/ 133 L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { 134 attribution: '© OpenStreetMap contributors', 135 maxZoom: 19 136 }).addTo(montagutMap); 137 138 139 // Récupérer les magasins à partir de l'élément data 140 var storesData = document.getElementById('stores-data'); 141 if (storesData) { 142 try { 143 var stores = JSON.parse(storesData.textContent); 144 addStoreMarkers(stores); 145 } catch (e) { 146 console.error("Erreur lors de la récupération des données des magasins:", e); 147 } 148 } 149 150 // Ajouter un bouton de géolocalisation élégant 151 L.control.locate({ 152 position: 'bottomright', 153 drawCircle: true, 154 followMarker: true, 155 setView: 'untilPan', 156 keepCurrentZoomLevel: false, 157 strings: { 158 title: "Me localiser" 159 }, 160 locateOptions: { 161 enableHighAccuracy: true, 162 maxZoom: 15 163 }, 164 icon: 'fas fa-location-arrow', 165 iconLoading: 'fas fa-spinner fa-spin', 166 markerStyle: { 167 weight: 2, 168 opacity: 0.9, 169 fillOpacity: 0.4 170 }, 171 compassStyle: { 172 weight: 1, 173 opacity: 0.9, 174 fillOpacity: 0.4, 175 radius: 5 176 } 177 }).addTo(montagutMap); 178 179 // Ajouter une animation au chargement de la carte 180 mapElement.style.opacity = '0'; 181 mapElement.style.transition = 'opacity 1s ease'; 182 183 setTimeout(function() { 184 mapElement.style.opacity = '1'; 185 }, 300); 186} 187/** 188 * Ajoute les marqueurs des magasins à la carte avec popups améliorés et positionnement optimisé 189 */ 190function addStoreMarkers(stores) { 191 if (!montagutMap || !stores || !stores.length) return; 192 193 // Vider les marqueurs existants 194 if (montagutMarkers.length > 0) { 195 montagutMarkers.forEach(function(marker) { 196 montagutMap.removeLayer(marker); 197 }); 198 montagutMarkers = []; 199 officialMarkers = []; 200 nonOfficialMarkers = []; 201 } 202 203 if (montagutMarkerCluster) { 204 montagutMap.removeLayer(montagutMarkerCluster); 205 } 206 207 // Créer un marqueur personnalisé pour les magasins non officiels 208 var storeIcon = L.icon({ 209 iconUrl: prestashop.urls.base_url + 'modules/montagutstores/views/img/marker.png', 210 iconSize: [40, 40], 211 iconAnchor: [20, 40], 212 popupAnchor: [0, -40], 213 shadowUrl: prestashop.urls.base_url + 'modules/montagutstores/views/img/marker-shadow.svg', 214 shadowSize: [50, 50], 215 shadowAnchor: [15, 50] 216 }); 217 218 // Marqueur pour les magasins officiels avec une taille légèrement plus grande 219 var officialIcon = L.icon({ 220 iconUrl: prestashop.urls.base_url + 'modules/montagutstores/views/img/marker-official.png', 221 iconSize: [48, 48], 222 iconAnchor: [24, 48], 223 popupAnchor: [0, -48], 224 shadowUrl: prestashop.urls.base_url + 'modules/montagutstores/views/img/marker-shadow.svg', 225 shadowSize: [50, 50], 226 shadowAnchor: [15, 50] 227 }); 228 229 // Ajouter un marqueur pour chaque magasin 230 stores.forEach(function(store) { 231 if (store.lat && store.lng) { 232 var marker = L.marker([store.lat, store.lng], { 233 icon: store.is_official ? officialIcon : storeIcon, 234 title: store.name, 235 alt: 'Montagut ' + store.name, 236 riseOnHover: true 237 }); 238 239 // Formater les heures d'ouverture pour le popup 240 var hoursHtml = ''; 241 if (store.hours && Object.keys(store.hours).length > 0) { 242 hoursHtml = '<div class="store-popup-hours"><h5><i class="fas fa-clock"></i> Horaires d\'ouverture</h5>'; 243 244 for (var day in store.hours) { 245 var hours = store.hours[day]; 246 var displayHours = ''; 247 248 // Gestion des heures au format tableau 249 if (Array.isArray(hours)) { 250 if (hours.length === 0 || (hours.length === 1 && !hours[0])) { 251 displayHours = 'Fermé'; 252 } else { 253 var timeSlots = []; 254 // Construire les créneaux horaires
255 hours.forEach(function(slot) { 256 if (slot && slot.trim() !== '') { 257 timeSlots.push(slot); 258 } 259 }); 260 261 if (timeSlots.length > 0) { 262 displayHours = timeSlots.join(' / '); 263 } else { 264 displayHours = 'Fermé'; 265 } 266 } 267 } else { 268 // Format chaîne classique 269 displayHours = hours || 'Fermé'; 270 } 271 272 hoursHtml += '<p><span>' + day + '</span><span>' + displayHours + '</span></p>'; 273 } 274 275 hoursHtml += '</div>'; 276 } 277 278 // Utiliser l'image téléchargée si disponible 279 var storeImageHtml = ''; 280 /* if (store.image_url) { 281 storeImageHtml = ` 282 <div class="store-popup-image"> 283 <img src="${store.image_url}" alt="${store.name}" onerror="this.src='${prestashop.urls.base_url}img/cms/stores/default.jpg'"> 284 </div> 285 `; 286 }*/ 287 288 // Contenu du popup amélioré au format paysage 289 var popupContent = ` 290 <div class="store-popup landscape"> 291 <div class="popup-row"> 292 ${storeImageHtml} 293 <div class="popup-content"> 294 <div class="store-popup-header"> 295 <h4>${store.name}</h4> 296 <p>${store.city}, ${store.country}</p> 297 ${store.is_official ? '<div class="store-popup-badge">Officiel</div>' : ''} 298 </div> 299 <div class="store-popup-info"> 300 <p><i class="fas fa-map-marker-alt"></i> ${store.address}<br/>${store.postcode} ${store.city}</p> 301 ${store.phone ? '<p><i class="fas fa-phone"></i> <a href="tel:' + store.phone.replace(/\s/g, '') + '">' + store.phone + '</a></p>' : ''} 302 ${store.email ? '<p><i class="fas fa-envelope"></i> <a href="mailto:' + store.email + '">' + store.email + '</a></p>' : ''} 303 </div> 304 ${hoursHtml} 305 <div class="store-popup-links"> 306 <a href="https://www.google.com/maps/dir/?api=1&destination=${store.lat},${store.lng}" target="_blank"> 307 <i class="fas fa-directions"></i> Itinéraire 308 </a> 309 </div> 310 </div> 311 </div> 312 </div> 313 `; 314 315 // Ajouter un lien vers la page CMS si elle existe 316 if (store.id_cms) { 317 // Note: Nécessiterait d'ajouter l'URL de la page CMS dans les données du store 318 var cmsUrl = prestashop.urls.base_url + 'content/category/' + store.id_cms + '-store'; 319 320 // Utilisez une opération DOM réelle pour manipuler le HTML ici 321 var tempDiv = document.createElement('div'); 322 tempDiv.innerHTML = popupContent; 323 324 var linksDiv = tempDiv.querySelector('.store-popup-links'); 325 if (linksDiv) { 326 var pageLink = document.createElement('a'); 327 pageLink.href = cmsUrl; 328 pageLink.innerHTML = '<i class="fas fa-info-circle"></i> Voir la page'; 329 pageLink.style.marginLeft = '10px'; 330 331 linksDiv.appendChild(pageLink); 332 333 popupContent = tempDiv.innerHTML; 334 } 335 } 336 337 // Créer le popup avec animation 338 var popup = L.popup({ 339 closeButton: true, 340 autoClose: true, 341 className: 'store-popup-container landscape-popup', 342 minWidth: 520, // Plus large pour le format paysage 343 maxHeight: 500 // Limiter la hauteur 344 }).setContent(popupContent); 345 346 marker.bindPopup(popup); 347 montagutMarkers.push(marker); 348 349 // Séparer les marqueurs officiels et non officiels 350 if (store.is_official) { 351 officialMarkers.push(marker); 352 } else { 353 nonOfficialMarkers.push(marker); 354 } 355 356 // Ajouter un événement au clic sur le marqueur pour repositionner la vue 357 marker.on('click', function() { 358 // Position du marqueur 359 var popupLocation = marker.getLatLng(); 360 361 // Décalage vertical (valeur positive pour déplacer le marqueur vers le bas de l'écran) 362 // Plus la valeur est grande, plus le marqueur sera bas dans la carte 363 var yOffset = -250; 364 365 // Calculer une nouvelle position pour que le popup soit bien visible 366 // Déplacer le centre de la carte vers le haut, ce qui place le marker plus bas 367 var offsetPoint = montagutMap.project(popupLocation).subtract([0, -yOffset]); 368 var newCenter = montagutMap.unproject(offsetPoint); 369 370 // Animer vers cette nouvelle position 371 setTimeout(function() {
372 montagutMap.setView(newCenter, montagutMap.getZoom(), { 373 animate: true, 374 duration: 0.5 375 }); 376 }, 50); 377 }); 378 379 // Animation du marqueur au survol 380 marker.on('mouseover', function() { 381 this.setZIndexOffset(1000); 382 }); 383 384 marker.on('mouseout', function() { 385 this.setZIndexOffset(0); 386 }); 387 } 388 }); 389 390 // Ajouter les marqueurs officiels directement à la carte sans clustering 391 officialMarkers.forEach(function(marker) { 392 marker.addTo(montagutMap); 393 }); 394 395 // Utiliser MarkerCluster uniquement pour les magasins non officiels 396 montagutMarkerCluster = L.markerClusterGroup({ 397 showCoverageOnHover: false, 398 zoomToBoundsOnClick: true, 399 spiderfyOnMaxZoom: true, 400 animateAddingMarkers: true, 401 disableClusteringAtZoom: 16, 402 maxClusterRadius: 60, 403 iconCreateFunction: function(cluster) { 404 var childCount = cluster.getChildCount(); 405 var size = 40; 406 407 if (childCount > 50) { 408 size = 60; 409 } else if (childCount > 20) { 410 size = 50; 411 } 412 413 return L.divIcon({ 414 html: '<div><span>' + childCount + '</span></div>', 415 className: 'marker-cluster' + (childCount < 10 ? ' marker-cluster-small' : childCount < 30 ? ' marker-cluster-medium' : ' marker-cluster-large'), 416 iconSize: new L.Point(size, size) 417 }); 418 } 419 }); 420 421 // Ajouter uniquement les marqueurs non officiels au cluster 422 nonOfficialMarkers.forEach(function(marker) { 423 montagutMarkerCluster.addLayer(marker); 424 }); 425 426 montagutMap.addLayer(montagutMarkerCluster); 427 428 // Ajuster la vue pour montrer tous les marqueurs si possible 429 if (montagutMarkers.length > 0) { 430 var bounds = L.featureGroup(montagutMarkers).getBounds(); 431 montagutMap.fitBounds(bounds, { 432 padding: [50, 50], 433 maxZoom: 15, 434 animate: true, 435 duration: 1 436 }); 437 } 438} 439 440/** 441 * Gestion du formulaire de recherche - Version améliorée 442 */ 443function handleStoreSearch(e) { 444 e.preventDefault(); 445 446 var form = e.target; 447 var country = document.getElementById('store-country').value; 448 var postcode = document.getElementById('store-postcode').value; 449 var address = document.getElementById('store-address').value; 450 451 // URL de l'action de recherche 452 var searchUrl = form.getAttribute('data-search-url'); 453 454 if (!searchUrl) { 455 console.error("URL de recherche non définie"); 456 return; 457 } 458 459 // Afficher un indicateur de chargement élégant 460 var mapElement = document.getElementById('map'); 461 var loaderOverlay = document.createElement('div'); 462 loaderOverlay.className = 'map-loader-overlay'; 463 loaderOverlay.innerHTML = '<div class="loader"></div><p>Recherche en cours...</p>'; 464 loaderOverlay.style.position = 'absolute'; 465 loaderOverlay.style.top = '0'; 466 loaderOverlay.style.left = '0'; 467 loaderOverlay.style.right = '0'; 468 loaderOverlay.style.bottom = '0'; 469 loaderOverlay.style.backgroundColor = 'rgba(255, 255, 255, 0.8)'; 470 loaderOverlay.style.display = 'flex'; 471 loaderOverlay.style.flexDirection = 'column'; 472 loaderOverlay.style.alignItems = 'center'; 473 loaderOverlay.style.justifyContent = 'center'; 474 loaderOverlay.style.zIndex = '1000'; 475 476 mapElement.style.position = 'relative'; 477 mapElement.appendChild(loaderOverlay); 478 479 // Animation de scroll vers la carte 480 mapElement.scrollIntoView({ 481 behavior: 'smooth', 482 block: 'center' 483 }); 484 485 // Effectuer la requête AJAX 486 var xhr = new XMLHttpRequest(); 487 xhr.open('POST', searchUrl, true); 488 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 489 490 xhr.onreadystatechange = function() { 491 if (xhr.readyState === 4) { 492 // Supprimer le loader 493 if (loaderOverlay.parentNode) { 494 loaderOverlay.parentNode.removeChild(loaderOverlay); 495 } 496 497 if (xhr.status === 200) { 498 try { 499 var response = JSON.parse(xhr.responseText); 500 501 if (response.success && response.stores.length > 0) { 502 // NE PAS mettre à jour les marqueurs - garder tous les magasins visibles 503 // Modification: au lieu de remplacer les marqueurs, nous zoomons simplement sur les résultats 504 505 // Créer un groupe de marqueurs pour les résultats de recherche uniquement 506 var searchBounds = L.latLngBounds(); 507 508 // Ajouter chaque magasin trouvé aux boun
508ds 509 response.stores.forEach(function(store) { 510 if (store.lat && store.lng) { 511 searchBounds.extend([store.lat, store.lng]); 512 } 513 }); 514 515 // Si nous avons des magasins valides, ajuster la vue 516 if (!searchBounds.isValid()) { 517 // Si aucun magasin n'a de coordonnées valides, utiliser le centre par défaut 518 if (response.center && response.zoom) { 519 montagutMap.flyTo([response.center.lat, response.center.lng], response.zoom, { 520 animate: true, 521 duration: 1.5 522 }); 523 } 524 } else { 525 // Zoomer sur les résultats avec une marge 526 montagutMap.flyToBounds(searchBounds, { 527 padding: [50, 50], 528 maxZoom: 15, 529 animate: true, 530 duration: 1.5 531 }); 532 } 533 534 // Afficher un message de succès 535 showMapMessage('success', 'Nous avons trouvé ' + response.count + ' magasin(s) correspondant à votre recherche.'); 536 } else { 537 // Aucun magasin trouvé 538 showMapMessage('warning', 'Aucun magasin trouvé pour votre recherche.'); 539 } 540 } catch (e) { 541 console.error("Erreur lors du traitement de la réponse:", e); 542 showMapMessage('error', 'Une erreur est survenue lors de la recherche.'); 543 } 544 } else { 545 // Erreur HTTP 546 console.error("Erreur HTTP:", xhr.status); 547 showMapMessage('error', 'Une erreur est survenue lors de la recherche.'); 548 } 549 } 550 }; 551 552 xhr.send('ajax=1&action=search&country=' + country + '&postcode=' + postcode + '&address=' + address); 553} 554 555/** 556 * Affiche un message sur la carte 557 */ 558function showMapMessage(type, message) { 559 var mapElement = document.getElementById('map'); 560 var messageOverlay = document.createElement('div'); 561 messageOverlay.className = 'map-message map-message-' + type; 562 messageOverlay.innerHTML = message; 563 messageOverlay.style.position = 'absolute'; 564 messageOverlay.style.top = '20px'; 565 messageOverlay.style.left = '50%'; 566 messageOverlay.style.transform = 'translateX(-50%)'; 567 messageOverlay.style.padding = '10px 20px'; 568 messageOverlay.style.borderRadius = '5px'; 569 messageOverlay.style.zIndex = '1000'; 570 messageOverlay.style.boxShadow = '0 5px 15px rgba(0, 0, 0, 0.1)'; 571 messageOverlay.style.fontWeight = '500'; 572 messageOverlay.style.opacity = '0'; 573 messageOverlay.style.transition = 'opacity 0.3s ease'; 574 575 // Style selon le type 576 if (type === 'success') { 577 messageOverlay.style.backgroundColor = '#d4edda'; 578 messageOverlay.style.color = '#155724'; 579 messageOverlay.style.border = '1px solid #c3e6cb'; 580 } else if (type === 'warning') { 581 messageOverlay.style.backgroundColor = '#fff3cd'; 582 messageOverlay.style.color = '#856404'; 583 messageOverlay.style.border = '1px solid #ffeeba'; 584 } else if (type === 'error') { 585 messageOverlay.style.backgroundColor = '#f8d7da'; 586 messageOverlay.style.color = '#721c24'; 587 messageOverlay.style.border = '1px solid #f5c6cb'; 588 } 589 590 mapElement.appendChild(messageOverlay); 591 592 // Animation d'apparition 593 setTimeout(function() { 594 messageOverlay.style.opacity = '1'; 595 }, 100); 596 597 // Disparition après 5 secondes 598 setTimeout(function() { 599 messageOverlay.style.opacity = '0'; 600 601 setTimeout(function() { 602 if (messageOverlay.parentNode) { 603 messageOverlay.parentNode.removeChild(messageOverlay); 604 } 605 }, 300); 606 }, 5000); 607} 608 609/** 610 * Ajouts spécifiques à intégrer dans le fichier front.js pour gérer le nouveau design 611 */ 612 613document.addEventListener('DOMContentLoaded', function() { 614 // Gestion des erreurs de chargement des images de fond 615 document.querySelectorAll('.store-image-bg').forEach(function(bg) { 616 // Créer une image temporaire pour vérifier si l'URL de fond fonctionne 617 var tempImg = new Image(); 618 619 tempImg.onerror = function() { 620 // Si l'image ne charge pas, utiliser l'image par défaut 621 var defaultImg = bg.getAttribute('data-default'); 622 if (defaultImg) { 623 bg.style.backgroundImage = 'url("' + defaultImg + '")'; 624 bg.classList.add('error'); 625 } 626 }; 627 628 // Extraire l'URL de l'image de fond du style inline 629 var bgUrl = window.getComputedStyle(bg).backgroundImage; 630 var urlMatch = bgUrl.match(/url\(['"]?([^'"]+)['"]?\)/); 631 632 if (urlMatch && urlMatch[1]) { 633 tempImg.src = urlMatch[1]; 634 } 635 }); 636 637 // Gestion du bouton "Voir sur la carte" pour les magasins (nouvelle version) 638 document.querySelectorAll('.map-link-button-modern').forEach(function(button) { 639 button.addEventListener('click', function(e) { 640 e.preventDefault(); 641 e.stopPropagation(); 642 643 // Récupérer les coordonnées du magasin 644 var storeLat = this.getAttribute('data-lat'); 645 var storeLng = this.getAttribute('data-lng'); 646 647 if (storeLat && storeLng && montagutMap) { 648 // Animation de scroll vers la carte 649 document.getElementById('map').scrollIntoView({ 650 behavior: 'smooth', 651 block: 'center' 652 }); 653 654 // Centrer la carte sur le magasin avec animation 655 montagutMap.flyTo([storeLat, storeLng], 17, { 656 animate: true, 657 duration: 1.5 658 }); 659 660 // Délai avant d'ouvrir le popup pour que l'animation soit fluide 661 setTimeout(function() {
662 // Chercher et ouvrir le popup correspondant 663 montagutMap.eachLayer(function(layer) { 664 if (layer instanceof L.Marker) { 665 var markerLat = layer.getLatLng().lat.toFixed(6); 666 var markerLng = layer.getLatLng().lng.toFixed(6); 667 668 if (markerLat === parseFloat(storeLat).toFixed(6) && 669 markerLng === parseFloat(storeLng).toFixed(6)) { 670 layer.openPopup(); 671 } 672 } 673 }); 674 }, 1000); 675 } 676 }); 677 }); 678});
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.