Skip to content

Instantly share code, notes, and snippets.

@dedeibel
Last active September 25, 2021 15:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dedeibel/fc58a56935dccd6a7d8d585c769f72cf to your computer and use it in GitHub Desktop.
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
/**
* 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