let ws; // WebSocket instance function toggle_grid(whichDay) { var vclasses= [['in-list'], ['in-calendar', 'onlyday1'], ['in-calendar', 'onlyday2'], ['in-calendar', 'onlyday3'], ['in-calendar', 'onlyday4'], ['in-calendar', 'alldays']]; document.body.classList.remove( 'alldays', 'onlyday1', 'onlyday2', 'onlyday3', 'onlyday4', 'in-list', 'in-calendar'); if( whichDay < 0 || whichDay > 5 ) return; document.body.classList.add(...vclasses[whichDay]); } function distribute_votes() { document.querySelectorAll('.event').forEach( function(element) { var eid = element.getAttribute('event_id'); var abs = window.votes[eid]; var klasse = 5000; if (abs < 2000) { klasse = 2000; } if (abs < 1000) { klasse = 1000; } if (abs < 500) { klasse = 500; } if (abs < 200) { klasse = 200; } if (abs < 100) { klasse = 100; } if (abs < 50) { klasse = 50; } if (abs < 20) { klasse = 20; } if (abs < 10) { klasse = 10; } if (!abs) klasse = 10; var abselem = element.querySelector('.absval'); if (abselem) abselem.textContent = '' + abs; else { var abselem = document.createElement('div'); abselem.textContent = '' + abs; abselem.classList.add('absval'); element.insertBefore(abselem, element.firstChild); } element.classList.add('class_' + klasse); }); } function corr_for_eventids(id1, id2) { var d = 0, c = 0, cd = 0, l = window.raw_votes.length; for (item of window.raw_votes) { var x = 0; if( item.indexOf(id1) > -1 ) { ++d; x++;} if( item.indexOf(id2) > -1 ) { ++c; cd+=x; } } var mid = 0; // if ( d * c ) mid = Math.round( 4.0 * ( ( cd * l ) / ( c * d ) ) * ( cd / d + cd / c ) ); if ( d * c ) mid = Math.round( 4 * l * cd * cd * ( c + d ) / ( c * c * d * d ) ) if (mid>9) mid=9; return mid; } function show_all_correlates(el) { /* First identify the room to see what other rooms to consider correlates always grow from the top slot to the right, unless there's an overlapping event to the left that starts earlier */ var event_room = el.getAttribute('fullnarp-room'); var event_day = el.getAttribute('fullnarp-day'); var event_time = el.getAttribute('fullnarp-time'); if (!event_time) return; var event_start; try { event_start = time_to_mins(event_time); } catch(e) { return; } var event_duration = el.getAttribute('fullnarp-duration') / 60; /* Only test events to the right, if they start at the exact same time */ document.querySelectorAll('.event.day_'+event_day).forEach( function(check_el, index) { var check_room = check_el.getAttribute('fullnarp-room'); if (event_room == check_room) return; var check_time = check_el.getAttribute('fullnarp-time'); if (!check_time) return; var check_start = time_to_mins(check_time); var check_duration = check_el.getAttribute('fullnarp-duration') / 60; var dist = check_el.getAttribute('fullnarp-room') - event_room; var overlap = check_start < event_start + event_duration && event_start < check_start + check_duration; if (!overlap) return; if (event_start == check_start && dist <= 0) return; if (event_start < check_start) return; var corr = corr_for_eventids(el.getAttribute('event_id'), check_el.getAttribute('event_id')); var dir = dist > 0 ? 'r' : 'l'; var div = document.createElement('div'); div.classList.add('corrweb', dir.repeat(Math.abs(dist)), 'day_' + event_day, 'room' + event_room, 'time_' + event_time, 'corr_d_' + corr); document.body.appendChild(div); }) } function display_correlation() { var selected = document.querySelectorAll('.selected'); if( selected.length == 1 ) { selected = selected[0]; document.querySelectorAll('.event').forEach(elem => mark_correlation(elem, selected)); } if (document.body.classList.contains('correlate')) distribute_votes(); document.body.classList.toggle('correlate'); } function mark_correlation(dest, comp) { var id1 = dest.getAttribute('event_id'); var id2 = comp.getAttribute('event_id'); var d = 0, c = 0, cd = 0, l = window.raw_votes.length; for (vote of window.raw_votes) { var x = 0; if( vote.indexOf(id1) > -1 ) { ++d; x++;} if( vote.indexOf(id2) > -1 ) { ++c; cd+=x; } } var mid = 0; // if ( d * c ) mid = Math.round( 4.0 * ( ( cd * l ) / ( c * d ) ) * ( cd / d + cd / c ) ); if ( d * c ) mid = Math.round( 4 * l * cd * cd * ( c + d ) / ( c * c * d * d ) ) if (mid>9) mid=9; dest.className = dest.className.replace(/\bcorr_\S+/g, ''); dest.setAttribute('corr', mid); dest.querySelector('.absval').textContent = mid + ':' + Math.round( 100 * cd / d ) + '%:' + Math.round( 100 * cd / c ) + '%'; } function mark_avail(el) { el.classList.toggle('unavailable', !check_avail(el, el.getAttribute('fullnarp-day'), el.getAttribute('fullnarp-time'))); } function time_to_mins(time) { var hour_mins = /(\d\d)(\d\d)/.exec(time); if( hour_mins[1] < 9 ) { hour_mins[1] = 24 + hour_mins[1]; } return 60 * hour_mins[1] + 1 * hour_mins[2]; } function check_avail(el, day, time ) { var all_available = true; var speakers = window.event_speakers[el.getAttribute('event_id')]; if (!speakers) return false; try { var event_times = /(\d\d)(\d\d)/.exec(time); var event_duration = el.getAttribute('fullnarp-duration') / 60; var event_start = Number(event_times[1]); if (event_start < 9) { event_start = 24 + event_start; } var event_start_date = new Date("2024-12-27T00:00:00+01:00"); event_start_date.setTime(event_start_date.getTime() + 60000 * ( 24 * 60 * (day - 1) + event_start * 60 + 1 * Number(event_times[2])) ); var event_end_date = new Date(); event_end_date.setTime(event_start_date.getTime() + 60000 * event_duration); } catch (error) { return false; } /* Check availability of all speakers */ for (speaker of speakers) { /* Now if at least one day is set, each missing day means unavailable, */ var have_avails = false, unavail = true; for (avail of speaker.availabilities) { have_avails = true; var availtime_start = new Date(avail.start); var availtime_end = new Date(avail.end); if( event_start_date >= availtime_start && event_end_date <= availtime_end ) unavail = false; } /* If at least one speaker is unavail, check fails */ if( have_avails && unavail ) { all_available = false; return false; } } return all_available; } /* Needs to be done for each moved and all previously conflicting events */ function mark_conflict(el) { var event_start = time_to_mins(el.getAttribute('fullnarp-time')); var event_duration = el.getAttribute('fullnarp-duration') / 60; var conflict = false; /* We do only need to check events in the same room at the same day for conflicts */ document.querySelectorAll('.event.day_'+el.getAttribute('fullnarp-day')+'.room'+el.getAttribute('fullnarp-room')).forEach( function(check_el) { if( el.getAttribute('event_id') == check_el.getAttribute('event_id') ) { return true; } var check_start = time_to_mins(check_el.getAttribute('fullnarp-time')); var check_duration = check_el.getAttribute('fullnarp-duration') / 60; if( check_start < event_start + event_duration && event_start < check_start + check_duration ) { check_el.classList.add('conflict'); conflict = true; } }); el.classList.toggle('conflict', conflict); } /* remove day, room and time from an event */ function remove_event(event_id) { var el = document.getElementById(event_id); el.classList.add('pending'); if (ws && ws.readyState === WebSocket.OPEN) { var message = { lastupdate: window.lastupdate, removeevent: event_id } ws.send(JSON.stringify(message)); console.log('Sent:', message); } else { el.removeClass('pending'); el.addClass('failed'); } } /* provide time OR hour + minute, time overrides */ function set_all_attributes(event_id, day, room, time, from_server) { var el = document.getElementById(event_id); el.className = el.className.replace( /\btime_\S+ ?/g, '').replace( /\broom\S+ ?/g, '').replace( /\bday_\S+ ?/g, ''); el.classList.add( time, day, room ); el.setAttribute('fullnarp-day', day.replace('day_','')); el.setAttribute('fullnarp-time', time.replace('time_','')); el.setAttribute('fullnarp-room', room.replace('room','')); el.classList.remove('pending'); if (!from_server) { el.classList.add('pending'); if (ws && ws.readyState === WebSocket.OPEN) { var message = { lastupdate: window.lastupdate, setevent: event_id, day: el.getAttribute('fullnarp-day'), room: el.getAttribute('fullnarp-room'), time: el.getAttribute('fullnarp-time') } ws.send(JSON.stringify(message)); console.log('Sent:', message); } else { el.classList.remove('pending'); el.classList.add('failed'); } } /* When moving an element, conflict may have been resolved ... */ document.querySelectorAll('.conflict').forEach(elem => mark_conflict(elem)); /* ... or introduced */ mark_conflict(el); mark_avail(el); if (document.body.classList.contains('showcorrweb')) { document.querySelectorAll('.corrweb').forEach(elem => elem.remove()); document.querySelectorAll('.event').forEach(elem => show_all_correlates(elem)); } } function signalFullnarpConnect(state) { document.body.classList.remove('fullnarp-connected', 'fullnarp-connecting', 'fullnarp-disconnected'); document.body.classList.add(state); } function getFullnarpData(lastupdate) { signalFullnarpConnect('fullnarp-connecting'); ws = new WebSocket('wss://erdgeist.org/38C3/halfnarp/fullnarp-ws'); ws.onopen = () => { console.log('Connected to WebSocket server'); //stateElement.textContent = 'Connected'; }; ws.onmessage = (event) => { signalFullnarpConnect('fullnarp-connected'); const data = JSON.parse(event.data); console.log('Received:', data); for (const [eventid, event_new] of Object.entries(data.data)) { if (document.getElementById(eventid)) set_all_attributes(eventid, 'day_'+event_new['day'], 'room'+event_new['room'], 'time_'+event_new['time'], true ) } window.lastupdate = data.current_version; current_version_string = ('00000'+data.current_version).slice(-5); document.querySelector('.version').innerHTML = 'Version: '+data.current_version+''; }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; ws.onclose = () => { console.log('Disconnected from WebSocket server'); signalFullnarpConnect('fullnarp-disconnected'); // stateElement.textContent = 'Disconnected'; // Optionally attempt to reconnect after a delay setTimeout(getFullnarpData, 5000); }; }; function do_the_fullnarp() { var halfnarpAPI = 'talks_38C3.json'; var fullnarpAPI = 'votes_38c3.json'; var allrooms = ['1','2','3'] var allminutes = ['00','05','10','15','20','25','30','35','40','45','50','55'] var allhours = ['10','11','12','13','14','15','16','17','18','19','20','21','22','23','00','01','02']; var alldays = ['1','2','3','4']; var voted = 0; window.event_speakers = {}; window.votes = {}; /* Add handler for type ahead search input field */ var filter = document.getElementById('filter'); filter.onpaste = filter.oncut = filter.onkeypress = filter.onkeydown = filter.onkeyup = function() { var cnt = this.value.toLowerCase(); if( cnt.length ) document.querySelectorAll('.event').forEach(elem => elem.style.display = (elem.textContent || elem.innerText || '').toLowerCase().includes(cnt) ? "block" : "none" ); else document.querySelectorAll('.event').forEach(elem => elem.style.display = "block"); }; /* Add click handlers for event div sizers */ document.querySelector('.vsmallboxes').onclick = function() { document.body.classList.remove('size-medium', 'size-large'); document.body.classList.add('size-small'); }; document.querySelector('.vmediumboxes').onclick = function() { document.body.classList.remove('size-small', 'size-large'); document.body.classList.add('size-medium'); }; document.querySelector('.vlargeboxes').onclick = function() { document.body.classList.remove('size-small', 'size-medium'); document.body.classList.add('size-large'); }; /* Add callbacks for view selector */ document.querySelector('.vlist').onclick = function() { toggle_grid(0); }; document.querySelector('.vday1').onclick = function() { toggle_grid(1); }; document.querySelector('.vday2').onclick = function() { toggle_grid(2); }; document.querySelector('.vday3').onclick = function() { toggle_grid(3); }; document.querySelector('.vday4').onclick = function() { toggle_grid(4); }; document.querySelector('.vdays').onclick = function() { toggle_grid(5); }; document.querySelector('.vleft').onclick = function() { document.body.classList.toggle('still-left'); }; document.querySelector('.vhalf').onclick = function() { document.body.classList.toggle('absolute'); }; document.querySelector('.vcorr').onclick = display_correlation; document.querySelector('.vlang').onclick = function() { document.body.classList.toggle('languages'); }; document.querySelector('.vtrack').onclick = function() { document.body.classList.toggle('all-tracks'); }; document.querySelector('.vweb').onclick = function() { if (document.body.classList.contains('showcorrweb')) document.querySelectorAll('.corrweb').forEach(elem => elem.remove()); else document.querySelectorAll('.event').forEach(elem => show_all_correlates(elem)); document.body.classList.toggle('showcorrweb'); }; /* Make the trashbin a drop target */ var trash = document.querySelector('.trashbin'); trash.setAttribute('dropzone','move'); trash.ondragover = function (event) { event.preventDefault(); // allows us to drop this.classList.add('over'); return false; }; trash.ondragleave = function (event) { this.classList.remove('over'); }; trash.ondrop = function (event) { event.stopPropagation(); set_all_attributes(event.dataTransfer.getData('Text'), 'day_0', 'room_0', 'time_0000', false); return false; }; /* Create hour guides */ for (hour of allhours) { var elem = document.createElement('hr'); elem.classList.add('guide', 'time_' + hour + '00'); document.body.append(elem); elem = document.createElement('div'); elem.textContent = hour + '00'; elem.classList.add('guide', 'time_' + hour + '00'); document.body.append(elem); for (minute of allminutes) { for (room of allrooms) { for (day of alldays) { elem = document.createElement('div'); elem.classList.add('grid', 'time_' + hour + minute, 'day_' + day, 'room' + room ); elem.textContent = minute; elem.setAttribute('dropzone', 'move'); elem.setAttribute('hour', '' + hour + minute); elem.setAttribute('day', '' + day); elem.setAttribute('room', room); document.body.append(elem); elem.ondragover = function (event) { event.preventDefault(); // allows us to drop this.classList.add('over'); return false; } elem.ondragleave = function (event) { this.classList.remove('over'); } elem.ondrop = function (event) { event.stopPropagation(); set_all_attributes(event.dataTransfer.getData('Text'), 'day_' + this.getAttribute('day'), 'room' + this.getAttribute('room'), 'time_' + this.getAttribute('hour'), false ); /* Don't go back to list view on successful drop */ document.body.classList.remove('was-list'); return false; } } } } } /* Fetch list of votes to display */ fetch(`${fullnarpAPI}?format=json`) .then(response => { if (!response.ok) { throw new Error(`HTTP error when fetching fullnarp data! status: ${response.status}`); } return response.json(); }).then(data => { window.raw_votes = data; for (eventidlist of data) for (eventid of eventidlist) window.votes[eventid] = 1 + (window.votes[eventid] || 0 ); if( ++voted == 2 ) { window.lastupdate = 0; distribute_votes(); getFullnarpData(0); } }).catch(error => { console.error('Fetch error:', error); }); /* Fetch list of lectures to display */ fetch(`${halfnarpAPI}?format=json`) .then(response => { if (!response.ok) { throw new Error(`HTTP error when fetching halfnarp data! status: ${response.status}`); } return response.json(); }).then(data => { for (item of data) { /* Take copy of hidden event template div and select them, if they're in list of previous prereferences */ var t = document.getElementById('template').cloneNode(true); var event_id = item.event_id.toString(); t.classList.add('event', 'duration_' + item.duration, 'lang_' + (item.language || 'en')); t.setAttribute('event_id', event_id); t.setAttribute('id', 'event_' + event_id) t.setAttribute( 'fullnarp-duration', item.duration); /* Sort textual info into event div */ t.querySelector('.title').textContent = item.title; t.querySelector('.speakers').textContent = item.speaker_names; t.querySelector('.abstract').append(item.abstract); /* Store speakers and their availabilities */ window.event_speakers[event_id] = item.speakers; for (speaker of item.speakers) { var have_avails = false; if (!speaker.availabilities) console.log("Foo"); for (avail of speaker.availabilities) { if (avail.id ) { have_avails = true; break; } } if (!have_avails) t.classList.add('has_unavailable_speaker'); } t.setAttribute('draggable', 'true'); /* Make the event drag&droppable */ t.ondragstart = function( event, ui ) { event.stopPropagation(); event.dataTransfer.setData('text/plain', this.id ); event.dataTransfer.dropEffect = 'move'; event.dataTransfer.effectAllowed = 'move'; event.target.classList.add('is-dragged'); } /* While dragging make source element small enough to allow dropping below its original area */ t.ondrag = function( event, ui ) { event.stopPropagation(); event.target.classList.add('is-dragged'); /* When drag starts in list view, switch to calendar view */ if( document.body.classList.contains('in-list') ) { toggle_grid(5); document.body.classList.add('was-list'); } if( document.body.classList.contains('in-drag') ) return; document.body.classList.add('in-drag'); /* mark all possible drop points regarding to availability */ for (hour of allhours) for (minute of allminutes) for (day of alldays) document.querySelectorAll('.grid.day_'+day+'.time_'+hour+minute).forEach(elem => elem.classList.toggle('possible', check_avail(event.target, day, hour+minute))); } t.ondragend = function( event, ui ) { event.stopPropagation(); /* We removed in-list and the drop did not succeed. Go back to list view */ if (document.body.classList.contains('was-list')) toggle_grid(0); document.querySelectorAll('.over').forEach(elem => elem.classList.remove('over')); document.querySelectorAll('.is-dragged').forEach(elem => elem.classList.remove('id-dragged')); document.querySelectorAll('.possible').forEach(elem => elem.classList.remove('possible')); document.body.classList.remove('in-drag', 'was-list'); } /* start_time: 2014-12-29T21:15:00+01:00" */ var start_time = new Date(item.start_time); var day = start_time.getDate()-26; var hour = start_time.getHours(); var mins = start_time.getMinutes(); /* After midnight: sort into yesterday */ if( hour < 9 ) day--; /* Fix up room for 38c3 */ room = (item.room_id || 'room_unknown').toString().replace('471','room1').replace('472','room2').replace('473','room3'); /* Apply attributes to sort events into calendar */ t.classList.add(room, 'day_' + day, 'time_' + (hour<10?'0':'') + hour + (mins<10?'0':'') + mins); t.setAttribute('fullnarp-day', day); t.setAttribute('fullnarp-time', (hour<10?'0':'') + hour + (mins<10?'0':'') + mins ); t.setAttribute('fullnarp-room', room.replace('room','')); mark_avail(t); t.onclick = function(event) { _this = this; document.body.classList.remove('in-drag'); if (document.body.classList.contains('correlate')) { document.querySelectorAll('.selected').forEach(elem => elem.classList.remove('selected')); document.querySelectorAll('.event').forEach(elem => mark_correlation(elem, _this)); } _this.classList.toggle('selected'); document.querySelectorAll('.info').forEach(elem => elem.classList.add('hidden')); event.stopPropagation(); } /* Put new event into DOM tree. Track defaults to 'Other' */ var track = item.track_id.toString(); t.classList.add('track_' + track ); var d = document.getElementById(track); if (!d) d = document.querySelector('#Other'); d.append(t); }; if( ++voted == 2 ) { window.lastupdate = 0; distribute_votes(); getFullnarpData(0); } }).catch(error => { console.error('Fetch error:', error); }); document.onkeypress = function(e) { document.body.classList.remove('in-drag'); if( document.activeElement.tagName == 'INPUT' || document.activeElement.tagName == 'TEXTAREA' ) return; switch( e.charCode ) { case 115: case 83: /* s */ var selected = document.querySelectorAll('.selected'); if( selected.length != 2 ) return; var id0 = selected[0].getAttribute('id'); var day0 = selected[0].getAttribute('fullnarp-day'); var hour0 = selected[0].getAttribute('fullnarp-time'); var room0 = selected[0].getAttribute('fullnarp-room'); var id1 = selected[1].getAttribute('id'); var day1 = selected[1].getAttribute('fullnarp-day'); var hour1 = selected[1].getAttribute('fullnarp-time'); var room1 = selected[1].getAttribute('fullnarp-room'); set_all_attributes(id0, day1, room1, hour1, false); set_all_attributes(id1, day0, room0, hour0, false); break; case 48: case 94: /* 0 */ toggle_grid(5); break; case 49: case 50: case 51: case 52: /* 1-4 */ toggle_grid(e.charCode-48); break; case 76: case 108: /* l */ toggle_grid(0); break; case 68: case 100: /* d */ toggle_grid(5); break; case 73: case 105: /* i */ document.body.classList.remove('all-tracks'); document.body.classList.toggle('languages'); break; case 65: case 97: /* a */ case 72: case 104: /* h */ document.body.classList.toggle('absolute'); break; case 81: case 113: /* q */ document.querySelectorAll('.selected').forEach(elem => elem.classList.remove('selected')); break; case 84: case 116: /* t */ document.body.classList.remove('languages'); document.body.classList.toggle('all-tracks'); break; case 85: case 117: /* u */ document.body.classList.toggle('still-left'); break; case 67: case 99: /* c */ display_correlation(); break; case 87: case 119: /* w */ if (document.body.classList.contains('showcorrweb')) document.querySelectorAll('.corrweb').forEach(elem => elem.remove()); else document.querySelectorAll('.event').forEach(elem => show_all_correlates(elem)); document.body.classList.toggle('showcorrweb'); break; } }; }