/** * Phikipathia - Philosophy Wikipedia Path Visualization * Features: Clickable Wikipedia links, Collapsible Tree Nodes, Clean Branch Lines, Wikipedia Infobox, Tab Favicon Spinner */ (() => { 'use strict'; // 1. Cached DOM Element References const favicon = document.getElementById('favicon'); const taal = document.getElementById('taal'); const start = document.getElementById('start'); const einde = document.getElementById('einde'); const toevoegKnop = document.getElementById('toevoegKnop'); const wisKnop = document.getElementById('wisKnop'); const visualisatie = document.getElementById('visualisatie'); const logo = document.getElementById('logo'); const waarschuwing = document.getElementById('waarschuwing'); const errorBanner = document.getElementById('errorBanner'); const errorMessage = document.getElementById('errorMessage'); const errorDismiss = document.getElementById('errorDismiss'); // Wikipedia Infobox Stats Elements const statistieken = document.getElementById('statistieken'); const statHops = document.getElementById('statHops'); const statStatusText = document.getElementById('statStatusText'); const statReroutesRow = document.getElementById('statReroutesRow'); const statReroutes = document.getElementById('statReroutes'); const statTotalPaths = document.getElementById('statTotalPaths'); // 2. Favicon Animation Engine (Canvas Offscreen Rendering) const faviconSrc = './images/wikipedia_edit.png'; const faviconImg = new Image(); faviconImg.src = faviconSrc; const faviconCanvas = document.createElement('canvas'); faviconCanvas.width = 32; faviconCanvas.height = 32; const faviconCtx = faviconCanvas.getContext('2d'); let faviconAngle = 0; let faviconTimer = null; function renderFaviconFrame() { if (!favicon || !faviconImg.complete || !faviconCtx) return; faviconCtx.clearRect(0, 0, 32, 32); faviconCtx.save(); faviconCtx.translate(16, 16); faviconCtx.rotate((faviconAngle * Math.PI) / 180); faviconCtx.drawImage(faviconImg, -16, -16, 32, 32); faviconCtx.restore(); favicon.href = faviconCanvas.toDataURL('image/png'); faviconAngle = (faviconAngle + 12) % 360; } function startFaviconSpin() { if (faviconTimer) return; faviconAngle = 0; faviconTimer = setInterval(renderFaviconFrame, 40); } function stopFaviconSpin() { if (faviconTimer) { clearInterval(faviconTimer); faviconTimer = null; } if (favicon) { favicon.href = faviconSrc; } } // 3. Encapsulated Application State let huidigeTaal = null; let huidigeDoel = null; let isSearching = false; let totalPathsExplored = 0; // 4. Helper: Extract input value or placeholder function getOrDefault(inputElem) { if (!inputElem) return ''; const val = inputElem.value.trim(); return val === '' ? (inputElem.getAttribute('placeholder') || '').trim() : val; } // Helper: Construct Wikipedia Article URL function getWikiUrl(title, lang) { const langCode = lang || getOrDefault(taal) || 'en'; const slug = encodeURIComponent(title.replace(/ /g, '_')); return `https://${langCode}.wikipedia.org/wiki/${slug}`; } // 5. UI: Error & Stats Infobox Management function showError(message, prefix = '↯ ') { if (!errorBanner || !errorMessage) return; errorMessage.textContent = `${prefix}${message}`; errorBanner.hidden = false; } function hideError() { if (!errorBanner) return; errorBanner.hidden = true; if (errorMessage) { errorMessage.textContent = ''; } } function updateStats(data) { if (!statistieken) return; statistieken.hidden = false; const hops = data.stappen !== undefined ? data.stappen : (Array.isArray(data.pad) ? Math.max(0, data.pad.length - 1) : 0); if (statHops) statHops.textContent = hops; if (statStatusText) { if (data.status === 'success' || (!data.status && data.pad && !data.error)) { statStatusText.textContent = `${huidigeDoel} bereikt`; statStatusText.style.color = '#196f3d'; statStatusText.style.fontWeight = 'bold'; } else if (data.status === 'cyclus' || (data.error && data.error.includes('Cyclus'))) { statStatusText.textContent = 'Cyclus gedetecteerd'; statStatusText.style.color = '#7d6608'; statStatusText.style.fontWeight = 'bold'; } else { statStatusText.textContent = 'Fout / Doodlopend'; statStatusText.style.color = '#78281f'; statStatusText.style.fontWeight = 'bold'; } } if (statReroutesRow && statReroutes) { if (data.herleid && data.herleid > 0) { statReroutes.textContent = data.herleid; statReroutesRow.hidden = false; } else { statReroutesRow.hidden = true; } } if (statTotalPaths) { statTotalPaths.textContent = totalPathsExplored; } } // 6. UI: Dynamic Warning Visibility function inputWaarschuwing() { if (!waarschuwing) return; const currentLangVal = getOrDefault(taal); const currentTargetVal = getOrDefault(einde); waarschuwing.hidden = (huidigeTaal === currentLangVal && huidigeDoel === currentTargetVal); } // 7. UI: Loading State & Animation Toggles function setLoadingState(isLoading) { isSearching = isLoading; if (toevoegKnop) toevoegKnop.disabled = isLoading; if (logo) { if (isLoading) logo.classList.add('spinning'); else logo.classList.remove('spinning'); } if (isLoading) { startFaviconSpin(); hideError(); } else { stopFaviconSpin(); } } // 8. DOM: Tree Node Creation function insertTreeNode(title, parentElem, elementId, badgeText = null) { if (!parentElem) return null; const existingLists = parentElem.querySelectorAll(':scope > ul'); const isBranchSplit = existingLists.length >= 1; if (isBranchSplit && parentElem.tagName === 'LI') { parentElem.classList.add('has-split-branch'); // Mark primary branch (Branch 1) so a clean vertical connector line is drawn if (existingLists[0] && !existingLists[0].classList.contains('branch-primary')) { existingLists[0].classList.add('branch-primary'); } } const wrap = document.createElement('ul'); if (isBranchSplit) { wrap.classList.add('branch-split'); } const newItem = document.createElement('li'); newItem.setAttribute('item', title); // Clickable Wikipedia Article Link const wikiLink = document.createElement('a'); wikiLink.className = 'wiki-link'; wikiLink.href = getWikiUrl(title, huidigeTaal); wikiLink.target = '_blank'; wikiLink.rel = 'noopener noreferrer'; wikiLink.textContent = title; newItem.appendChild(wikiLink); if (badgeText) { const badge = document.createElement('span'); badge.className = 'loop-marker'; badge.textContent = badgeText; newItem.appendChild(badge); } wrap.appendChild(newItem); if (elementId !== undefined) { wrap.id = elementId; } parentElem.appendChild(wrap); // Add Collapsible toggle button to parent element if (parentElem.tagName === 'LI' && !parentElem.querySelector(':scope > .toggle-btn')) { const toggleBtn = document.createElement('span'); toggleBtn.className = 'toggle-btn'; toggleBtn.textContent = '˅'; toggleBtn.title = 'Klap in / uit'; toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); parentElem.classList.toggle('collapsed'); }); parentElem.insertBefore(toggleBtn, parentElem.firstChild); } return newItem; } // 9. Tree Reset & Clear function refreshTree() { const langVal = getOrDefault(taal); const targetVal = getOrDefault(einde); if (huidigeDoel !== targetVal || huidigeTaal !== langVal) { huidigeTaal = langVal; huidigeDoel = targetVal; totalPathsExplored = 0; if (visualisatie) { visualisatie.replaceChildren(); insertTreeNode(huidigeDoel, visualisatie, 'wortel'); } if (statistieken) statistieken.hidden = true; inputWaarschuwing(); } } function clearTree() { if (visualisatie) { visualisatie.replaceChildren(); insertTreeNode(getOrDefault(einde), visualisatie, 'wortel'); } totalPathsExplored = 0; if (statistieken) statistieken.hidden = true; hideError(); inputWaarschuwing(); } // 10. Path Rendering function renderPath(data) { if (!data) return; totalPathsExplored += 1; if (data.error && (!data.pad || data.pad.length === 0)) { showError(data.error, '↯ '); return; } const padList = Array.isArray(data.pad) ? [...data.pad] : []; if (padList.length === 0) { showError(data.error || 'Geen geldig pad ontvangen van de server.', '↯ '); return; } let parentNode = visualisatie; const existingNodes = visualisatie.getElementsByTagName('li'); const existingNames = Array.from(existingNodes).map(item => item.getAttribute('item')); let i = 0; while (parentNode === visualisatie && i < padList.length) { const foundIdx = existingNames.indexOf(padList[i]); if (foundIdx !== -1) { parentNode = existingNodes[foundIdx]; } i += 1; } if (parentNode === visualisatie) { const rootLi = visualisatie.querySelector('#wortel li'); if (rootLi) { parentNode = rootLi; } } const remainingItems = padList.slice(0, i - 1); const reversedItems = remainingItems.reverse(); for (let idx = 0; idx < reversedItems.length; idx++) { const item = reversedItems[idx]; let badge = null; if (idx === 0 && data.status === 'cyclus') { badge = 'cyclus'; } parentNode = insertTreeNode(item, parentNode, undefined, badge); } updateStats(data); if (data.error) { showError(data.error, 'ℹ '); } } // 11. API Search Execution function zoek() { if (isSearching) return; setLoadingState(true); refreshTree(); const verzoek = { taal: getOrDefault(taal), start: getOrDefault(start), einde: getOrDefault(einde) }; const encodedData = encodeURIComponent(JSON.stringify(verzoek)); const endpointUrl = `cgi-bin/init.py?data=${encodedData}`; fetch(endpointUrl) .then(response => { if (!response.ok) { return response.json() .then(errJson => { throw new Error(errJson.error || `Server HTTP ${response.status}`); }) .catch(parseErr => { if (parseErr.message && !parseErr.message.includes('JSON')) throw parseErr; throw new Error(`Server HTTP ${response.status}`); }); } return response.json(); }) .then(data => { renderPath(data); }) .catch(err => { showError(err.message || 'Er is een netwerkfout opgetreden.', '‼ '); }) .finally(() => { setLoadingState(false); }); } // 12. Event Binding & Initialization function init() { if (toevoegKnop) toevoegKnop.addEventListener('click', zoek); if (wisKnop) wisKnop.addEventListener('click', clearTree); if (taal) { taal.addEventListener('input', inputWaarschuwing); taal.addEventListener('change', inputWaarschuwing); } if (einde) einde.addEventListener('input', inputWaarschuwing); if (errorDismiss) errorDismiss.addEventListener('click', hideError); [taal, start, einde].forEach(input => { if (input) { input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); zoek(); } }); } }); refreshTree(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();