Last active
September 25, 2021 15:50
-
-
Save dedeibel/fc58a56935dccd6a7d8d585c769f72cf to your computer and use it in GitHub Desktop.
Zooniverse Active Asteroids optimized key control - use a browser addon to automatically load js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2021 Benjamin Peter | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
let _dd_doneText = 'Done'; | |
let _dd_noText = 'No'; | |
let _dd_yesText = 'Yes'; | |
var _dd_keyEventListener; | |
var _dd_locationChangeListener; | |
var _dd_doneTimer; | |
var _dd_checkForNeedToReinitializeTimer; | |
var _dd_checkInitializedInterval; | |
let _dd_helpId = 'ddHelpContainer'; | |
function _dd_findAnswer(needleText) { | |
let answers = document.querySelectorAll('div.classifier label.answer'); | |
for (let answer of answers) { | |
let lp = answer.querySelector('p'); | |
if (!lp) { | |
continue; | |
} | |
let ltext = lp.textContent; | |
if (ltext === needleText) { | |
return answer; | |
} | |
} | |
return null; | |
} | |
function _dd_getDone() { | |
let buttons = document.querySelectorAll('div.classifier button'); | |
for (let lbutton of buttons) { | |
let lspan = lbutton.querySelector('span'); | |
if (!lspan) { | |
continue; | |
} | |
let stext = lspan.textContent; | |
if (stext === _dd_doneText) { | |
return lbutton; | |
} | |
} | |
return null; | |
} | |
function _dd_isAnswerActive(answerNode) { | |
return answerNode.classList.contains('active'); | |
} | |
// answer: boolean, yes / no | |
// returns true if answer was already active | |
function _dd_answer(answer) { | |
let answerNo = _dd_findAnswer(_dd_noText); | |
let answerYes = _dd_findAnswer(_dd_yesText); | |
console.log('dd: y/n is:', answerYes, answerNo); | |
if (answer) { | |
if (_dd_isAnswerActive(answerYes)) { | |
return true; | |
} | |
answerYes.click(); | |
return false; | |
} else { | |
if (_dd_isAnswerActive(answerNo)) { | |
return true; | |
} | |
answerNo.click(); | |
return false; | |
} | |
} | |
function _dd_clickDone() { | |
_dd_getDone().click(); | |
_dd_checkForNeedToReinitializeLater(); | |
} | |
/* Selects and answer of confirms it if already selected */ | |
function _dd_answerOrConfirm(answer) { | |
if (_dd_answer(answer)) { | |
// answer was already selected, confirm it | |
console.log('dd: clearing done timer:', _dd_doneTimer); | |
if (_dd_doneTimer) { | |
clearTimeout(_dd_doneTimer); | |
} | |
_dd_doneTimer = setTimeout(() => { | |
console.log('dd: checking y/no state'); | |
let answerNo = _dd_findAnswer(_dd_noText); | |
let answerYes = _dd_findAnswer(_dd_yesText); | |
console.log('dd: y/n is:', answerYes, answerNo); | |
if (_dd_isAnswerActive(answerYes) || _dd_isAnswerActive(answerNo)) { | |
console.log('dd: clicking done'); | |
_dd_clickDone(); | |
} | |
_dd_doneTimer = null; | |
}, 500); | |
} | |
} | |
/* Left side container for the subject image - holds the upstream key event | |
* handlers we want to trigger */ | |
function _dd_getSubjectContainer() { | |
return document.querySelector('.subject-container > div'); | |
} | |
function _dd_dispatch(eventName, key, which) { | |
// Works only for firefox | |
// var keyboardEvent = new KeyboardEvent(eventName, { | |
// "key": key, // ignored by zooniverse actually | |
// "which": which | |
// }); | |
// keyboardEvent.which = which; // for chrome | |
// Works for firefox and chrome | |
// seen: https://bugs.chromium.org/p/chromium/issues/detail?id=679439 | |
var keyboardEvent = document.createEvent("Events"); | |
keyboardEvent.initEvent("keydown", true, true); | |
keyboardEvent.key = key; | |
keyboardEvent.keyCode = which; | |
keyboardEvent.which = which; | |
_dd_getSubjectContainer().dispatchEvent(keyboardEvent); | |
} | |
function _dd_resetZoom() { | |
let resetButton = document.querySelector('button.reset'); | |
resetButton.click(); | |
_dd_setSuperZoomedMarker(false); | |
} | |
function _dd_zoomIn() { | |
_dd_dispatch("keydown", "Equals", 61); | |
} | |
function _dd_getSubjectUrl() { | |
return document.querySelector('.subject-container img.subject').src; | |
} | |
function _dd_isSuperZoomed() { | |
// value still set to the URL when the super zoom was activated | |
return _dd_getSubjectContainer().dataset.dd_super_zoomed === _dd_getSubjectUrl(); | |
} | |
/* marks super zoom active by setting a dom data attribute */ | |
function _dd_setSuperZoomedMarker(superZoomed) { | |
if (superZoomed) { | |
/* remember the current subject URL since this one changes on "done" and | |
* when the zoom is reset */ | |
_dd_getSubjectContainer().dataset.dd_super_zoomed = _dd_getSubjectUrl(); | |
} | |
else { | |
delete _dd_getSubjectContainer().dataset.dd_super_zoomed; | |
} | |
} | |
function _dd_superZoom() { | |
_dd_resetZoom(); // reset first to assure a defined level | |
for (let i = 0; i < 9; i++) { | |
_dd_zoomIn(); | |
} | |
_dd_setSuperZoomedMarker(true); | |
} | |
function _dd_toggleSuperZoom() { | |
if (_dd_isSuperZoomed()) { | |
_dd_resetZoom(); | |
} else { | |
_dd_superZoom(); | |
} | |
} | |
function _dd_installEventHandler() { | |
console.log('dd: installing key handler'); | |
if (_dd_keyEventListener) { // reinstall, usefull during development | |
console.log('dd: cleaning previous key handler'); | |
document.removeEventListener('keydown', _dd_keyEventListener, false); | |
} | |
_dd_keyEventListener = (event) => { | |
// console.log("dd: key:", event.key); | |
if (event.key === '4') { | |
_dd_dispatch("keydown", "ArrowLeft", 37); | |
} | |
else if (event.key === '8') { | |
_dd_dispatch("keydown", "ArrowUp", 38); | |
} | |
else if (event.key === '6') { | |
_dd_dispatch("keydown", "ArrowRight", 39); | |
} | |
else if (event.key === '2') { | |
_dd_dispatch("keydown", "ArrowDown", 40); | |
} | |
else if (event.key === '+') { | |
_dd_zoomIn(); | |
} | |
else if (event.key === '-') { | |
_dd_dispatch("keydown", "Minus", 173); | |
} | |
else if (event.key === '*') { | |
let resetButton = document.querySelector('button.reset'); | |
resetButton.click(); | |
} | |
else if (event.key === '5') { | |
let secretButtons = document.querySelectorAll('button.secret-button'); | |
for (let sButton of secretButtons) { | |
if (sButton.title === 'Invert image') { | |
sButton.click(); | |
break; | |
} | |
} | |
} | |
else if (event.key === '9') { | |
let rotateButton = document.querySelector('button.rotate'); | |
rotateButton.click(); | |
} | |
else if (event.key === '7') { | |
let rotateButton = document.querySelector('button.rotate'); | |
rotateButton.click(); | |
rotateButton.click(); | |
rotateButton.click(); | |
} | |
else if (event.key === '0') { | |
_dd_toggleSuperZoom(); | |
} | |
else if (event.key === '1') { | |
_dd_answerOrConfirm(false); | |
} | |
else if (event.key === '3') { | |
_dd_answerOrConfirm(true); | |
} | |
}; | |
document.addEventListener('keydown', _dd_keyEventListener, false); | |
} | |
/* box containing the yes no and done buttons, we add the help box to the end */ | |
function _dd_getRightBoxContainer() { | |
return document.querySelector('div.classifier > div:last-child'); | |
} | |
function _dd_installHelpBox() { | |
console.log("dd: installing help box"); | |
let rightBoxContainer = _dd_getRightBoxContainer(); | |
let helpContainer = document.createElement("div"); | |
helpContainer.id = _dd_helpId; | |
helpContainer.style = 'padding: 0 2em;'; | |
let help = document.createElement("p"); | |
help.append( | |
'7, 8 – Rotate ccw, cw', document.createElement("br"), | |
'4, 6, 2, 8 – Move left, right, down, up', document.createElement("br"), | |
'5 – Invert image', document.createElement("br"), | |
'0 – Toggle zoom to center', document.createElement("br"), | |
'1 – No (second time to confirm)', document.createElement("br"), | |
'3 – Yes (second time to confirm)', document.createElement("br"), | |
'+, -, * – Zoom in, out, reset', document.createElement("br") | |
); | |
helpContainer.append(help); | |
rightBoxContainer.append(helpContainer); | |
} | |
function _dd_installHelpBoxIfMissing() { | |
if (! document.getElementById(_dd_helpId)) { | |
_dd_installHelpBox(); | |
} | |
} | |
/* classify buttons and subject container is present */ | |
function _dd_isHtmlInitialized() { | |
return document.querySelector('div.classifier button') != null && _dd_getSubjectContainer() != null; | |
} | |
/* after a classification ("done") we have no event here so just wait | |
* some time to reinstall the help box, I noticed situations where the | |
* whole classification box was renewed even though the URL was not changed */ | |
function _dd_checkForNeedToReinitializeLater() { | |
if (_dd_checkForNeedToReinitializeTimer) { | |
clearTimeout(_dd_checkForNeedToReinitializeTimer); | |
} | |
_dd_checkForNeedToReinitializeTimer = setTimeout(() => { | |
console.log('dd: checking for reinitialization'); | |
_dd_installHelpBoxIfMissing(); | |
_dd_checkForNeedToReinitializeTimer = null; | |
}, 1000); | |
} | |
/* initial situation when entering the page initially or after URL change */ | |
function _dd_waitForPageInitialized(cb) { | |
console.log('dd: wait for page initialized'); | |
if (_dd_checkInitializedInterval) { | |
console.log('dd: clearing previous check interval'); | |
clearInterval(_dd_checkInitializedInterval); | |
} | |
_dd_checkInitializedInterval = setInterval(() => { | |
console.log('dd: checking if classifier initialized'); | |
if (_dd_isHtmlInitialized()) { | |
console.log('dd: initialized, clearing interval, calling cb'); | |
clearInterval(_dd_checkInitializedInterval); | |
_dd_checkInitializedInterval = 0; | |
cb(); | |
} | |
}, | |
150, 200); | |
} | |
function _dd_activatePlugin() { | |
console.log('dd: activating keyboard plugin'); | |
_dd_waitForPageInitialized(() => { | |
_dd_installEventHandler(); | |
_dd_installHelpBoxIfMissing(); | |
}); | |
} | |
/* zooniverse seems to be a single page app but changes the url. So listen for | |
* navigation events and reactive the plugin when the classify page is reached. | |
* Only needs to be installed once */ | |
function _dd_installLocationListener() { | |
console.log('dd: installing location listener'); | |
if (_dd_locationChangeListener) { // reinstall, usefull during development | |
console.log('dd: cleaning previous location listener'); | |
window.removeEventListener('locationchange', _dd_locationChangeListener); | |
_dd_locationChangeListener = null; | |
} | |
_dd_locationChangeListener = () => { | |
console.log('dd: location changed', document.location.href); | |
if (document.location.href.includes('/classify')) { | |
console.log('dd: classify URL, installing plugin'); | |
_dd_activatePlugin(); | |
} | |
}; | |
window.addEventListener('locationchange', _dd_locationChangeListener); | |
} | |
_dd_installLocationListener(); | |
_dd_activatePlugin(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment