From e3481a4a35091b32b6fbee80c1c9ba2b6d7b50d6 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sun, 22 Dec 2024 21:53:57 +0100 Subject: Rework of halfnarp and fullnarp into a self contained repository. Still WIP --- static/fullnarp.js | 641 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 static/fullnarp.js (limited to 'static/fullnarp.js') diff --git a/static/fullnarp.js b/static/fullnarp.js new file mode 100644 index 0000000..8b7d36d --- /dev/null +++ b/static/fullnarp.js @@ -0,0 +1,641 @@ +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; + } + }; +} -- cgit v1.2.3