PageSourceSearch

https://visapourlimage.com/wp-content/plugins/visa-plan-expos/assets/vpi-map.js?ver=1.2.6

js visapourlimage.com collected 2026-10-02 06:18:01 UTC 11,153 bytes, 241 lines download raw bytes

1(function(){
2  'use strict';
3
4  const places = Array.isArray(window.VPI_MAP_PLACES) ? window.VPI_MAP_PLACES : [];
5  const apps = document.querySelectorAll('[data-vpi-map]');
6  if (!apps.length) return;
7
8  apps.forEach(initApp);
9
10  function initApp(app){
11    const pins = app.querySelector('[data-vpi-pins]');
12    const list = app.querySelector('[data-vpi-list]');
13    const count = app.querySelector('[data-vpi-count]');
14    const sheet = app.querySelector('[data-vpi-sheet]');
15    const sheetContent = app.querySelector('[data-vpi-sheet-content]');
16    const status = app.querySelector('[data-vpi-status]');
17    const locateBtn = app.querySelector('[data-vpi-locate]');
18    const listBtn = app.querySelector('[data-vpi-list-toggle]');
19    const openNowBtn = app.querySelector('[data-vpi-open-now]');
20    const listWrap = app.querySelector('[data-vpi-list-wrap]');
21    const closeBtn = app.querySelector('[data-vpi-close]');
22    const searchInput = app.querySelector('[data-vpi-search]');
23
24    let userLocation = null;
25    let openFilter = false;
26    let query = '';
27
28    function esc(s){
29      return String(s == null ? '' : s).replace(/[&<>'"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#039;','"':'&quot;'}[c]));
30    }
31
32    function normalize(s){
33      return String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().trim();
34    }
35
36    function isOpenNow(place){
37      if (!place.open_from || !place.open_to || !place.exhibitions || !place.exhibitions.length) return null;
38      const now = new Date();
39      const start = new Date(2026, 7, 29, 0, 0, 0); // 29 Aug 2026
40      const end = new Date(2026, 8, 13, 23, 59, 59); // 13 Sep 2026
41      if (now < start || now > end) return false;
42      const mins = now.getHours()*60 + now.getMinutes();
43      const a = place.open_from.split(':').map(Number);
44      const b = place.open_to.split(':').map(Number);
45      const from = a[0]*60 + a[1];
46      const to = b[0]*60 + b[1];
47      return mins >= from && mins <= to;
48    }
49
50    function hoursLabel(place){
51      if (place.hours_note) return place.hours_note;
52      if (place.open_from && place.open_to) return 'Ouvert ' + place.open_from.replace(':','h') + ' – ' + place.open_to.replace(':','h');
53      return '';
54    }
55
56    function distanceMeters(aLat,aLng,bLat,bLng){
57      const R=6371000, rad=Math.PI/180;
58      const p1=aLat*rad,p2=bLat*rad,dp=(bLat-aLat)*rad,dl=(bLng-aLng)*rad;
59      const x=Math.sin(dp/2)**2 + Math.cos(p1)*Math.cos(p2)*Math.sin(dl/2)**2;
60      return 2*R*Math.atan2(Math.sqrt(x),Math.sqrt(1-x));
61    }
62
63    function distanceLabel(place){
64      if (!userLocation || place.lat == null || place.lng == null) return '';
65      const m = distanceMeters(userLocation.lat,userLocation.lng,place.lat,place.lng);
66      place._distance = m;
67      if (m < 1000) return Math.max(10,Math.round(m/10)*10) + ' m';
68      return (m/1000).toFixed(1).replace('.',',') + ' km';
69    }
70
71    function directionsUrl(place){
72      const destination = place.address || ((place.lat != null && place.lng != null) ? (place.lat + ',' + place.lng) : '');
73      if (!destination) return '';
74      return 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(destination) + '&travelmode=walking';
75    }
76
77    function placeMatches(place){
78      if (!query) return true;
79      const q = normalize(query);
80      const placeText = normalize([place.name, place.address, place.special_note].join(' '));
81      if (placeText.includes(q)) return true;
82      return (place.exhibitions || []).some(ex => normalize([ex.title, ex.photographer].join(' ')).includes(q));
83    }
84
85    function matchingExhibitions(place){
86      const expos = Array.isArray(place.exhibitions) ? place.exhibitions : [];
87      if (!query) return expos;
88      const q = normalize(query);
89      if (normalize([place.name, place.address].join(' ')).includes(q)) return expos;
90      return expos.filter(ex => normalize([ex.title, ex.photographer].join(' ')).includes(q));
91    }
92
93    function filteredPlaces(){
94      let arr = places.filter(placeMatches);
95      if (openFilter) arr = arr.filter(p => isOpenNow(p) === true);
96      if (userLocation) {
97        arr.forEach(p => distanceLabel(p));
98        arr.sort((a,b) => (a._distance ?? Infinity) - (b._distance ?? Infinity));
99      } else {
100        arr.sort((a,b) => (a.number||999) - (b.number||999));
101      }
102      return arr;
103    }
104
105    function expoSummary(place){
106      const expos = matchingExhibitions(place);
107      if (!expos.length) return place.special_note || '';
108      const first = expos[0];
109      let html = '<div class="vpi-card-exhibition">'+esc(first.title)+'</div>';
110      if (first.photographer) html += '<div class="vpi-card-photographer">
110'+esc(first.photographer)+'</div>';
111      if (expos.length > 1) html += '<div class="vpi-card-more">+'+(expos.length-1)+' autre'+(expos.length > 2 ? 's' : '')+' exposition'+(expos.length > 2 ? 's' : '')+'</div>';
112      return html;
113    }
114
115    function render(){
116      const current = filteredPlaces();
117      const expoTotal = current.reduce((sum,p) => sum + matchingExhibitions(p).length, 0);
118      count.textContent = current.length + (current.length > 1 ? ' lieux' : ' lieu') + (query ? ' · ' + expoTotal + ' expo' + (expoTotal > 1 ? 's' : '') : '');
119
120      pins.innerHTML = '';
121      current.forEach(place => {
122        if (place.x == null || place.y == null) return;
123        const open = isOpenNow(place);
124        const b = document.createElement('button');
125        b.type = 'button';
126        b.className = 'vpi-pin' + (open === false ? ' is-closed' : '');
127        b.style.left = place.x + '%';
128        b.style.top = place.y + '%';
129        b.textContent = place.number || '•';
130        b.dataset.placeNumber = place.number || '';
131        b.setAttribute('aria-label', (place.number ? place.number + '. ' : '') + place.name + ((place.exhibitions || []).length ? ', ' + place.exhibitions.length + ' exposition(s)' : ''));
132        b.addEventListener('click', () => showPlace(place));
133        pins.appendChild(b);
134      });
135
136      list.innerHTML = '';
137      if (!current.length) {
138        list.innerHTML = '<div class="vpi-map-empty">Aucun résultat. Essayez le nom d’un photographe, d’une exposition ou d’un lieu.</div>';
139        return;
140      }
141
142      current.forEach(place => {
143        const card = document.createElement('button');
144        card.type = 'button';
145        const open = isOpenNow(place);
146        card.className = 'vpi-place-card' + (open === false ? ' is-closed' : '') + (place.image ? ' has-image' : '');
147        const img = place.image ? '<img src="'+esc(place.image)+'" alt="">' : '';
148        const distance = distanceLabel(place);
149        const expoCount = (place.exhibitions || []).length;
150        card.innerHTML = img + '<div class="vpi-card-body">' +
151          '<div class="vpi-card-title"><span class="vpi-card-number">'+esc(place.number||'•')+'</span><span>'+esc(place.name)+'</span>'+(expoCount ? '<span class="vpi-card-count">'+expoCount+' expo'+(expoCount>1?'s':'')+'</span>' : '')+'</div>' +
152          '<div class="vpi-card-address">'+esc(place.address || '')+'</div>' +
153          expoSummary(place) +
154          '<div class="vpi-card-meta">'+esc([hoursLabel(place), distance].filter(Boolean).join(' · '))+'</div>' +
155          '</div>';
156        card.addEventListener('click', () => showPlace(place));
157        list.appendChild(card);
158      });
159    }
160
161    function exhibitionRows(place){
162      const expos = matchingExhibitions(place);
163      if (!expos.length) return '';
164      return '<div class="vpi-sheet-expos"><div class="vpi-sheet-section-title">'+expos.length+' exposition'+(expos.length > 1 ? 's' : '')+'</div>' +
165        expos.map(ex => {
166          const inner = (ex.photographer ? '<div class="vpi-sheet-photographer">'+esc(ex.photographer)+'</div>' : '') +
167                        '<div class="vpi-sheet-exhibition">'+esc(ex.title)+'</div>' +
168                        (ex.url ? '<span class="vpi-sheet-expo-arrow">→</span>' : '');
169          return ex.url ? '<a class="vpi-sheet-expo-row" href="'+esc(ex.url)+'">'+inner+'</a>' : '<div class="vpi-sheet-expo-row">'+inner+'</div>';
170        }).join('') + '</div>';
171    }
172
173    function showPlace(place){
174      const d = distanceLabel(place);
175      const dir = directionsUrl(place);
176      sheetContent.innerHTML =
177        '<h3 class="vpi-sheet-title"><span class="vpi-sheet-number">'+esc(place.number||'•')+'</span><span>'+esc(place.name)+'</span></h3>'+
178        (place.address ? '<p class="vpi-sheet-address">'+esc(place.address)+'</p>' : '')+
179        '<div class="vpi-sheet-meta">'+
180          (hoursLabel(place) ? '<div><span aria-hidden="true">◷</span> '+esc(hoursLabel(place))+'</div>' : '')+
181          (d ? '<div><span aria-hidden="true">↟</span> À '+esc(d)+'</div>' : '')+
182          (place.special_note ? '<div>'+esc(place.special_note)+'</div>' : '')+
183        '</div>'+
184        exhibitionRows(place)+
185        '<div class="vpi-sheet-actions">'+
186          (dir ? '<a class="vpi-sheet-link vpi-sheet-link-primary" href="'+esc(dir)+'" target="_blank" rel="noopener">Itinéraire à pied <span>↗</span></a>' : '')+
187        '</div>';
188
189      sheet.classList.add('is-open');
190      sheet.setAttribute('aria-hidden','false');
191
192      app.querySelectorAll('.vpi-pin').forEach(p => p.classList.remove('is-active'));
193      const target = app.querySelector('.vpi-pin[data-place-number="'+String(place.number)+'"]');
194      if (target) target.classList.add('is-active');
195    }
196
197    function closeSheet(){
198      sheet.classList.remove('is-open');
199      sheet.setAttribute('aria-hidden','true');
200      app.querySelectorAll('.vpi-pin').forEach(p => p.classList.remove('is-active'));
201    }
202
203    locateBtn.addEventListener('click', function(){
204      if (!navigator.geolocation) {
205        status.textContent = 'La géolocalisation n’est pas disponible sur cet appareil.';
206        return;
207      }
208      status.textContent = 'Localisation en cours…';
209      navigator.geolocation.getCurrentPosition(function(pos){
210        userLocation = {lat:pos.coords.latitude,lng:pos.coords.longitude};
211        status.textContent = 'Les lieux sont classés du plus proche au plus éloigné.';
212        locateBtn.classList.add('is-active');
213        render();
214        if (window.innerWidth < 768) listWrap.scrollIntoView({behavior:'smooth',block:'start'});
215      }, function(){
216        status.textContent = 'Impossible d’obtenir votre position. Vérifiez l’autorisation de localisation du navigateur.';
217      }, {enableHighAccuracy:true,timeout:8000,maximumAge:60000});
218    });
219
220    listBtn.addEventListener('click', function(){
221      listWrap.scrollIntoView({behavior:'smooth',block:'start'});
222    });
223
224    openNowBtn.addEventListener('click', function(){
225      openFilter = !openFilter;
226      openNowBtn.classList.toggle('is-active',openFilter);
227      render();
228    });
229
230    searchInput.addEventListener('input', function(){
231      query = this.value || '';
232      closeSheet();
233      render();
234    });
235
236    closeBtn.addEventListener('click', closeSheet);
237    document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSheet(); });
238
239    render();
240  }
241})();

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.