// ==================== SEARCH ==================== function initSearch() { const btn = document.getElementById('searchBtn'); const modal = document.getElementById('searchModal'); const submit = document.getElementById('searchSubmit'); const result = document.getElementById('searchResult'); // Toggle modal btn.addEventListener('click', (e) => { e.stopPropagation(); modal.style.display = modal.style.display === 'none' ? 'block' : 'none'; if (modal.style.display === 'block') result.innerHTML = ''; }); // Close when clicking outside document.addEventListener('click', (e) => { if (!modal.contains(e.target) && e.target !== btn) { modal.style.display = 'none'; } }); submit.addEventListener('click', async () => { const lat1 = parseFloat(document.getElementById('sLat1').value); const lon1 = parseFloat(document.getElementById('sLon1').value); const lat2 = parseFloat(document.getElementById('sLat2').value); const lon2 = parseFloat(document.getElementById('sLon2').value); if (isNaN(lat1) || isNaN(lon1)) { result.innerHTML = '
Vui lòng nhập ít nhất Lat 1 và Long 1.
'; return; } submit.disabled = true; submit.textContent = 'Searching...'; result.innerHTML = ''; try { const response = await searchPoints({ lat1, lon1, lat2: isNaN(lat2) ? null : lat2, lon2: isNaN(lon2) ? null : lon2 }); renderSearchResult(result, response); } catch (err) { result.innerHTML = `
Lỗi: ${err.message}
`; } finally { submit.disabled = false; submit.textContent = 'Search'; } }); } /** * Fake API call — replace with real fetch when backend is ready. * Expected response: { data: [...], label: "", warning: "", message: "" } */ async function searchPoints({ lat1, lon1, lat2, lon2 }) { const points = [{ lat: lat1, lon: lon1 }]; if (lat2 != null && lon2 != null) points.push({ lat: lat2, lon: lon2 }); const _apiBase = location.hostname === 'localhost' ? 'http://localhost:4000/api' : '/api'; const resp = await fetch(`${_apiBase}/search`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': sessionStorage.getItem('admin_api_key') || '' }, body: JSON.stringify({ points }) }); if (!resp.ok) { const txt = await resp.text(); throw new Error(txt || `HTTP ${resp.status}`); } return resp.json(); } function renderSearchResult(container, resp) { let html = ''; if (resp.label) html += `
${resp.label}
`; if (resp.tts) html += `
${resp.tts}
`; if (resp.warning) html += `
⚠ ${resp.warning}
`; if (resp.message) html += `
${resp.message}
`; // Auto-play TTS audio if tts text provided — call TTS service async, play when ready if (resp.tts) { const existing = document.getElementById('tts-audio-player'); if (existing) existing.remove(); fetch('http://localhost:4020/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: resp.tts }) }) .then(r => r.ok ? r.json() : null) .then(data => { if (!data || !data.url) return; const audio = document.createElement('audio'); audio.id = 'tts-audio-player'; audio.src = data.url; document.body.appendChild(audio); audio.play().catch(() => {}); }) .catch(() => {}); } if (Array.isArray(resp.data) && resp.data.length > 0) { resp.data.forEach((pt, i) => { const streets = Array.isArray(pt.street_names) && pt.street_names.length ? pt.street_names.join(' - ') : '-'; const isFirst = i === 0, isLast = i === resp.data.length - 1; const marker = isFirst ? '● Start' : isLast ? '● End' : `○ ${i + 1}`; html += `
${marker} ${streets} ${Number(pt.lat).toFixed(6)}, ${Number(pt.lon).toFixed(6)}
`; }); drawSearchPath(resp.data); } else { html += '
Không tìm thấy điểm nào.
'; clearSearchPath(); } container.innerHTML = html; } // ── Search path SVG overlay ─────────────────────────────────────────────────── function _getOrCreateSearchSvg() { let svg = document.getElementById('search-path-svg'); if (!svg) { svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.id = 'search-path-svg'; svg.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:50;overflow:visible;'; const coords = document.querySelector('.coordinates'); if (coords) coords.appendChild(svg); } return svg; } function clearSearchPath() { const svg = document.getElementById('search-path-svg'); if (svg) svg.innerHTML = ''; } function drawSearchPath(pathPts) { const svg = _getOrCreateSearchSvg(); svg.innerHTML = ''; if (!pathPts || pathPts.length < 2) return; const mapSize = parseInt(document.getElementById('sSize')?.value) || 400; const sizePer = mapSize / 2400; const mapContainer = document.querySelector('.container'); const sl = mapContainer ? mapContainer.scrollLeft : 0; const st = mapContainer ? mapContainer.scrollTop : 0; const pts = pathPts.map(p => { const px = latLonToPixel(parseFloat(p.lat), parseFloat(p.lon), sizePer); return `${px.left - sl},${px.top - st}`; }).join(' '); // Path line const line = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); line.setAttribute('points', pts); line.setAttribute('fill', 'none'); line.setAttribute('stroke', '#FF5722'); line.setAttribute('stroke-width', '3'); line.setAttribute('stroke-linecap', 'round'); line.setAttribute('stroke-linejoin', 'round'); line.setAttribute('opacity', '0.85'); svg.appendChild(line); // Start & end markers const first = pathPts[0], last = pathPts[pathPts.length - 1]; [[first, '#4CAF50'], [last, '#F44336']].forEach(([p, color]) => { const px = latLonToPixel(parseFloat(p.lat), parseFloat(p.lon), sizePer); const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); c.setAttribute('cx', px.left - sl); c.setAttribute('cy', px.top - st); c.setAttribute('r', '6'); c.setAttribute('fill', color); c.setAttribute('stroke', '#fff'); c.setAttribute('stroke-width', '2'); svg.appendChild(c); }); // Redraw on scroll so path stays aligned if (mapContainer && !mapContainer._searchScrollBound) { mapContainer._searchScrollBound = true; mapContainer.addEventListener('scroll', () => drawSearchPath(_lastSearchPath)); } window._lastSearchPath = pathPts; }