This repository has been archived on 2026-08-18. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
2022ST-project-Phikipathia/index.js

386 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
}
})();