/** JavaScript file of utility functions.
* At present contains functions for sanitising of URLs,
* since tomcat 8+, being more compliant with URL/URI standards, is more strict about URLs.
*/
/*
Given a string consisting of a single character, returns the %hex (%XX)
https://www.w3resource.com/javascript-exercises/javascript-string-exercise-27.php
https://stackoverflow.com/questions/40100096/what-is-equivalent-php-chr-and-ord-functions-in-javascript
https://www.w3resource.com/javascript-exercises/javascript-string-exercise-27.php
*/
function urlEncodeChar(single_char_string) {
/*let hex = Number(single_char_string.charCodeAt(0)).toString(16);
var str = "" + hex;
str = "%" + str.toUpperCase();
return str;
*/
var hex = "%" + Number(single_char_string.charCodeAt(0)).toString(16).toUpperCase();
return hex;
}
/*
Tomcat 8 appears to be stricter in requiring unsafe and reserved chars
in URLs to be escaped with URL encoding
See section "Character Encoding Chart of
https://perishablepress.com/stop-using-unsafe-characters-in-urls/
Reserved chars:
; / ? : @ = &
-----> %3B %2F %3F %3A %40 %3D %26
[Now also reserved, but no special meaning yet in URLs (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)
and not required to be enforced yet, so we're aren't at present dealing with these:
! ' ( ) *
]
Unsafe chars:
" < > # % { } | \ ^ ~ [ ] ` and SPACE/BLANK
----> %22 %3C %3E %23 %25 %7B %7D %7C %5C %5E ~ %5B %5D %60 and %20
But the above conflicts with the reserved vs unreserved listings at
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
Possibly more info: https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid
And the bottom of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
lists additional characters that have been reserved since and which need encoding when in a URL component.
Javascript already provides functions encodeURI() and encodeURIComponent(), see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
However, the set of chars they deal with only partially overlap with the set of chars that need encoding as per the RFC3986 for URIs and RFC1738 for URLs discussed at
https://perishablepress.com/stop-using-unsafe-characters-in-urls/
We want to handle all the characters listed as unsafe and reserved at https://perishablepress.com/stop-using-unsafe-characters-in-urls/
so we define and use our own conceptually equivalent methods for both existing JavaScript methods:
- makeSafeURL() for Javascript's encodeURI() to make sure all unsafe characters in URLs are escaped by being URL encoded
- and makeSafeURLComponent() for JavaScript's encodeURIComponent to additionally make sure all reserved characters in a URL portion are escaped by being URL encoded too
Function makeSafeURL() is passed a string that represents a URL and therefore only deals with characters that are unsafe in a URL and which therefore require escaping.
Function makeSafeURLComponent() deals with portions of a URL that when decoded need not represent a URL at all, for example data like inline templates passed in as a
URL query string's parameter values. As such makeSafeURLComponent() should escape both unsafe URL characters and characters that are reserved in URLs since reserved
characters in the query string part (as query param values representing data) may take on a different meaning from their reserved meaning in a URL context.
*/
/* URL encodes both
- UNSAFE characters to make URL safe, by calling makeSafeURL()
- and RESERVED characters (characters that have reserved meanings within a URL) to make URL valid, since the url component parameter could use reserved characters
in a non-URL sense. For example, the inline template (ilt) parameter value of a URL could use '=' and '&' signs where these would have XSLT rather than URL meanings.
See end of https://www.w3schools.com/jsref/jsref_replace.asp to use a callback passing each captured element of a regex in str.replace()
*/
function makeURLComponentSafe(url_part, encode_percentages) {
// https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript
encode_percentages = encode_percentages || 1; // this method forces the URL-encoding of any % in url_part, e.g. do this for inline-templates that haven't ever been encoded
var url_encoded = makeURLSafe(url_part, encode_percentages);
//return url_encoded.replace(/;/g, "%3B").replace(/\//g, "%2F").replace(/\?/g, "%3F").replace(/\:/g, "%3A").replace(/\@/g, "%40").replace(/=/g, "%3D").replace(/\&/g,"%26");
url_encoded = url_encoded.replace(/[\;\/\?\:\@\=\&]/g, function(s) {
return urlEncodeChar(s);
});
return url_encoded;
}
/*
URL encode UNSAFE characters to make URL passed in safe.
Set encode_percentages to 1 (true) if you don't want % signs encoded: you'd do so if the url is already partly URL encoded.
*/
function makeURLSafe(url, encode_percentages) {
encode_percentages = encode_percentages || 0; // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript
var url_encoded = url;
if(encode_percentages) { url_encoded = url_encoded.replace(/\%/g,"%25"); } // encode % first
//url_encoded = url_encoded.replace(/ /g, "%20").replace(/\"/g,"%22").replace(/\/g,"%3E").replace(/\#/g,"%23").replace(/\{/g,"%7B").replace(/\}/g,"%7D");
//url_encoded = url_encoded.replace(/\|/g,"%7C").replace(/\\/g,"%5C").replace(/\^/g,"%5E").replace(/\[/g,"%5B").replace(/\]/g,"%5D").replace(/\`/g,"%60");
// Should we handle ~, but then what is its URL encoded value? Because https://meyerweb.com/eric/tools/dencoder/ URLencodes ~ to ~.
//return url_encoded;
url_encoded = url_encoded.replace(/[\ \"\<\>\#\{\}\|\\^\~\[\]\`]/g, function(s) {
return urlEncodeChar(s);
});
return url_encoded;
}
/***************
* MENU SCRIPTS *
***************/
function moveScroller() {
var move = function() {
var editbar = $("#editBar");
var st = $(window).scrollTop();
var fa = $("#float-anchor").offset().top;
if(st > fa) {
editbar.css({
position: "fixed",
top: "0px",
width: editbar.data("width"),
//width: "30%"
});
} else {
editbar.data("width", editbar.css("width"));
editbar.css({
position: "relative",
top: "",
width: ""
});
}
};
$(window).scroll(move);
move();
}
function floatMenu(enabled)
{
var menu = $(".tableOfContentsContainer");
if(enabled)
{
menu.data("position", menu.css("position"));
menu.data("width", menu.css("width"));
menu.data("right", menu.css("right"));
menu.data("top", menu.css("top"));
menu.data("max-height", menu.css("max-height"));
menu.data("overflow", menu.css("overflow"));
menu.data("z-index", menu.css("z-index"));
menu.css("position", "fixed");
menu.css("width", "300px");
menu.css("right", "0px");
menu.css("top", "100px");
menu.css("max-height", "600px");
menu.css("overflow", "auto");
menu.css("z-index", "200");
$("#unfloatTOCButton").show();
}
else
{
menu.css("position", menu.data("position"));
menu.css("width", menu.data("width"));
menu.css("right", menu.data("right"));
menu.css("top", menu.data("top"));
menu.css("max-height", menu.data("max-height"));
menu.css("overflow", menu.data("overflow"));
menu.css("z-index", menu.data("z-index"));
$("#unfloatTOCButton").hide();
$("#floatTOCToggle").prop("checked", false);
}
var url = gs.xsltParams.library_name + "?a=d&ftoc=" + (enabled ? "1" : "0") + "&c=" + gs.cgiParams.c;
$.ajax(url);
}
// TK Label Scripts
var tkMetadataSetStatus = "needs-to-be-loaded";
var tkMetadataElements = null;
function addTKLabelToImage(labelName, definition, name, comment) {
// lists of tkLabels and their corresponding codes, in order
let tkLabels = ["Attribution","Clan","Family","MultipleCommunities","CommunityVoice","Creative","Verified","NonVerified","Seasonal","WomenGeneral","MenGeneral",
"MenRestricted","WomenRestricted","CulturallySensitive","SecretSacred","OpenToCommercialization","NonCommercial","CommunityUseOnly","Outreach","OpenToCollaboration"];
let tkCodes = ["tk_a","tk_cl","tk_f","tk_mc","tk_cv","tk_cr","tk_v","tk_nv","tk_s","tk_wg","tk_mg","tk_mr","tk_wr","tk_cs","tk_ss","tk_oc","tk_nc","tk_co","tk_o","tk_cb"];
for (let i = 0; i < tkLabels.length; i++) {
if (labelName == tkLabels[i]) {
let labeldiv = document.querySelectorAll(".tklabels img");
for (image of labeldiv) {
let labelCode = image.src.substr(image.src.lastIndexOf("/") + 1).replace(".png", ""); // get tk label code from image file name
if (labelCode == tkCodes[i]) {
image.title = "TK " + name + ": " + definition + " Click for more details."; // set tooltip
if (image.parentElement.parentElement.parentElement.classList[0] != "tocSectionTitle") { // disable onclick event in favourites section
image.addEventListener("click", function(e) {
let currPopup = document.getElementsByClassName("tkPopup")[0];
if (currPopup == undefined || (currPopup != undefined && currPopup.id != labelCode)) {
let popup = document.createElement("div");
popup.classList.add("tkPopup");
popup.id = labelCode;
let popupText = document.createElement("span");
let heading = "
Traditional Knowledge Label:
" + name + "
";
let moreInformation = "
For more information about TK Labels, ";
let link = document.createElement("a");
link.innerHTML = "click here.";
link.href = "https://localcontexts.org/labels/traditional-knowledge-labels/";
link.target = "_blank";
popupText.innerHTML = heading + comment + moreInformation;
popupText.appendChild(link);
let closeButton = document.createElement("span");
closeButton.innerHTML = "×";
closeButton.id = "tkCloseButton";
closeButton.title = "Click to close window."
closeButton.addEventListener("click", function(e) {
closeButton.parentElement.remove();
});
popup.appendChild(closeButton);
popup.appendChild(popupText);
e.target.parentElement.appendChild(popup);
}
if (currPopup) currPopup.remove(); // remove existing popup div
});
}
}
}
}
}
}
function addTKLabelsToImages(lang) {
if (tkMetadataElements == null) {
console.error("ajax call not yet loaded tk label metadata set");
} else {
for (label of tkMetadataElements) { // for each tklabel element in tk.mds
let tkLabelName = label.attributes.name.value; // Element name=""
let attributes = label.querySelectorAll("[code=" + lang + "] Attribute"); // gets attributes for selected language
let tkName = attributes[0].textContent; // name="label"
let tkDefinition = attributes[1].textContent; // name="definition"
let tkComment = attributes[2].textContent; // name="comment"
addTKLabelToImage(tkLabelName, tkDefinition, tkName, tkComment);
}
}
}
function loadTKMetadataSetOld(lang) {
tkMetadataSetStatus = "loading";
$.ajax({
url: gs.variables["tkMetadataURL"],
success: function(xml) {
tkMetadataSetStatus = "loaded";
let parser = new DOMParser();
let tkmds = parser.parseFromString(xml, "text/xml");
tkMetadataElements = tkmds.querySelectorAll("Element");
if (document.readyState === "complete") {
addTKLabelsToImages(lang);
} else {
window.onload = function() {
addTKLabelsToImages(lang);
}
}
},
error: function() {
tkMetadataSetStatus = "no-metadata-set-for-this-collection";
console.log("No TK Label Metadata-set found for this collection");
}
});
};
function loadTKMetadataSet(lang, type) {
if (gs.variables["tkMetadataURL_"+type] == undefined) {
console.error("tkMetadataURL_"+type+" variable is not defined, can't load TK Metadata Set");
tkMetadataSetStatus = "no-metadata-set-for-this-"+type;
return;
}
tkMetadataSetStatus = "loading";
$.ajax({
url: gs.variables["tkMetadataURL_"+type],
async: false,
success: function(xml) {
tkMetadataSetStatus = "loaded";
let parser = new DOMParser();
let tkmds = parser.parseFromString(xml, "text/xml");
tkMetadataElements = tkmds.querySelectorAll("Element");
if (document.readyState === "complete") {
addTKLabelsToImages(lang);
} else {
window.onload = function() {
addTKLabelsToImages(lang);
}
}
},
error: function() {
tkMetadataSetStatus = "no-metadata-set-for-this-"+type;
console.log("No TK Label Metadata-set found for this "+type);
}
});
};
// Audio Scripts for Enriched Playback
var wavesurfer;
/**
* @param audio input audio file
* @param sectionData diarization data (.csv)
*/
function loadAudio(audio, sectionData) {
var inputFile = sectionData;
var mod_meta_base_url = gs.xsltParams.library_name + "?a=g&rt=r&ro=0&s=ModifyMetadata&s1.collection=" + gs.cgiParams.c + "&s1.site=" + gs.xsltParams.site_name + "&s1.d=" + gs.cgiParams.d;
var interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/"; // path to toolbar images
var GSSTATUS_SUCCESS = 11; // more information on codes found in: GSStatus.java
let editMode = false;
let currentRegion = {speaker: '', start: '', end: ''};
let currentRegions = [];
let itemType;
let dualMode = false;
let secondaryLoaded = false;
let selectedVersions = ['current'];
let waveformCursorX = 0;
let snappedToX = 0;
let snappedTo = "none";
let cursorPos = 0;
let ctrlDown = false;
let mouseDown = false;
let newRegionOffset = 0;
let editsMade = false;
let undoLevel = 0;
let undoStates = [];
let prevUndoState = "";
let tempZoomSave = 0;
let isZooming;
let canvasImages = {}; // stores canvas images of each version for fast loading from cache
let accentColour = "#66d640";
// let accentColour = "#F8C537";
let regionTransparency = "50";
let colourbrewerSet = colorbrewer.Set2[8];
let regionColourSet = [];
let waveformContainer = document.getElementById("waveform");
let waveformSpinner = document.getElementById('waveform-blocker');
let loader = document.getElementById('waveform-loader');
let initialLoad = true;
wavesurfer = WaveSurfer.create({ // wavesurfer options
autoCenterImmediately: true,
container: waveformContainer,
backend: "WebAudio",
// backgroundColor: "rgb(40, 54, 58)",
backgroundColor: "rgb(29, 43, 47)",
waveColor: "white",
progressColor: accentColour,
// progressColor: "grey",
// barWidth: 1,
// barHeight: 1.2,
// barGap: 2,
// barRadius: 1,
height: 140,
cursorColor: 'black',
// maxCanvasWidth: 32000,
minPxPerSec: 15, // default 20
partialRender: true, // use the PeakCache to improve rendering speed of large waveforms
pixelRatio: 1, // 1 results in faster rendering
scrollParent: true,
plugins: [
WaveSurfer.regions.create({
// formatTimeCallback: function(a, b) {
// return "TEST";
// }
}),
WaveSurfer.timeline.create({
container: "#wave-timeline",
secondaryColor: "white",
secondaryFontColor: "white",
notchPercentHeight: "0",
fontSize: "12",
// zoomDebounce: 30,
fontFamily: "Courier New"
}),
WaveSurfer.cursor.create({
showTime: true,
opacity: 1,
customShowTimeStyle: {
'background-color': '#000',
color: '#fff',
padding: '0.25rem',
'font-size': '12px'
},
formatTimeCallback: (num) => { return formatCursor(num); }
}),
],
});
// toolbar elements & event handlers
var audioContainer = document.getElementById("audioContainer");
var dualModeCheckbox = document.getElementById("dual-mode-checkbox");
var wave = document.getElementsByTagName("wave")[0];
var caretContainer = document.getElementById("caret-container");
var primaryCaret = document.getElementById("primary-caret");
var secondaryCaret = document.getElementById("secondary-caret");
var chapters = document.getElementById("chapters");
var chaptersContainer = document.getElementById("chapters-container");
var editPanel = document.getElementById("edit-panel");
var chapterButton = document.getElementById("chapterButton");
var chapterSearchInput = document.getElementById("chapter-search-input");
var zoomOutButton = document.getElementById("zoomOutButton");
var zoomSlider = document.getElementById("zoom-slider");
var zoomInButton = document.getElementById("zoomInButton");
var backButton = document.getElementById("backButton");
var playPauseButton = document.getElementById("playPauseButton");
var forwardButton = document.getElementById("forwardButton");
var editButton = document.getElementById("editorModeButton");
var downloadButton = document.getElementById("downloadButton");
var muteButton = document.getElementById("muteButton");
var volumeSlider = document.getElementById("volume-slider");
var fullscreenButton = document.getElementById("fullscreenButton");
var changeAllCheckbox = document.getElementById("change-all-checkbox");
var changeAllLabel = document.getElementById("change-all-label");
var speakerInput = document.getElementById("speaker-input");
var startTimeInput = document.getElementById("start-time-input");
var endTimeInput = document.getElementById("end-time-input");
var removeButton = document.getElementById("remove-button");
var createButton = document.getElementById("create-button");
var discardButton = document.getElementById("discard-button");
var undoButton = document.getElementById("undo-button");
var redoButton = document.getElementById("redo-button");
var saveButton = document.getElementById("save-button");
var hoverSpeaker = document.getElementById("hover-speaker");
var contextMenu = document.getElementById("context-menu");
var contextReplace = document.getElementById("context-menu-replace");
var contextOverdub = document.getElementById("context-menu-overdub");
var contextLock = document.getElementById("context-menu-lock");
var contextDelete = document.getElementById("context-menu-delete");
var timelineMenu = document.getElementById("timeline-menu");
var timelineMenuButton = document.getElementById("timeline-menu-button");
var timelineMenuHide = document.getElementById("timeline-menu-hide");
var timelineMenuDualMode = document.getElementById("timeline-menu-dualmode");
var timelineMenuRegionConflict = document.getElementById("timeline-menu-region");
var timelineMenuSpeakerConflict = document.getElementById("timeline-menu-speaker");
var versionSelectMenu = document.getElementById('version-select-menu');
var versionSelectLabels = document.querySelectorAll(".track-arrow");
var savePopup = document.getElementById("save-popup");
var savePopupBG = document.getElementById("save-popup-bg");
var savePopupCancel = document.getElementById("save-popup-cancel");
var savePopupCommit = document.getElementById("save-popup-commit");
var savePopupCommitMsg = document.getElementById("commit-message");
audioContainer.addEventListener('fullscreenchange', (e) => { fullscreenChanged() });
audioContainer.addEventListener('contextmenu', onRightClick);
audioContainer.addEventListener("keyup", keyUp);
audioContainer.addEventListener("keydown", keyDown);
dualModeCheckbox.addEventListener("change", () => { dualModeChanged() });
wave.addEventListener('scroll', (e) => { waveformScrolled() })
wave.addEventListener('mousemove', (e) => waveformCursorX = e.x);
primaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
secondaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
chapters.style.height = "0px";
chaptersContainer.style.height = "0px";
editPanel.style.height = "0px";
chapterButton.addEventListener("click", () => { toggleChapters() });
chapterSearchInput.addEventListener("input", chapterSearchInputChange)
zoomOutButton.addEventListener("click", () => { zoomSlider.stepDown(); zoomSlider.dispatchEvent(new Event("input")) });
zoomInButton.addEventListener("click", () => { zoomSlider.stepUp(); zoomSlider.dispatchEvent(new Event("input")) });
backButton.addEventListener("click", () => { wavesurfer.skipBackward(); });
playPauseButton.addEventListener("click", () => { wavesurfer.playPause() });
forwardButton.addEventListener("click", () => { wavesurfer.skipForward(); });
editButton.addEventListener("click", toggleEditMode);
downloadButton.addEventListener("click", () => { downloadURI(audio, audio.split(".dir/")[1]) });
muteButton.addEventListener("click", () => {
if (volumeSlider.value == 0) wavesurfer.setMute(false)
else wavesurfer.setMute(true)
});
volumeSlider.style["accent-color"] = accentColour;
fullscreenButton.addEventListener("click", toggleFullscreen);
zoomSlider.style["accent-color"] = accentColour;
changeAllCheckbox.addEventListener("change", () => { selectAllCheckboxChanged() });
speakerInput.addEventListener("input", speakerChange);
speakerInput.addEventListener("blur", speakerInputUnfocused);
createButton.addEventListener("click", createNewRegion);
removeButton.addEventListener("click", removeRegion);
discardButton.addEventListener("click", () => discardRegionChanges(false));
undoButton.addEventListener("click", undo);
redoButton.addEventListener("click", redo);
saveButton.addEventListener("click", saveRegionChanges);
document.addEventListener('click', documentClicked);
document.addEventListener('mouseup', () => mouseDown = false);
document.addEventListener('mousedown', (e) => { if (e.target.id !== "create-button") newRegionOffset = 0 }); // resets new region offset on click
document.querySelectorAll('input[type=number]').forEach(e => {
e.onchange = (e) => { changeStartEndTime(e) }; // updates speaker objects when number input(s) are changed
e.onblur = () => { prevUndoState = "" };
});
contextReplace.addEventListener("click", replaceSelected);
contextOverdub.addEventListener("click", overdubSelected);
contextLock.addEventListener("click", toggleLockSelected);
contextDelete.addEventListener("click", removeRightClicked);
timelineMenu.addEventListener("click", e => e.stopPropagation());
timelineMenuButton.addEventListener("click", timelineMenuToggle);
timelineMenuHide.addEventListener("click", timelineMenuHideClicked);
timelineMenuDualMode.addEventListener("click", () => { dualModeChanged() });
timelineMenuRegionConflict.addEventListener("click", showStartStopConflicts);
timelineMenuSpeakerConflict.addEventListener("click", showSpeakerNameConflicts);
savePopupCancel.addEventListener("click", toggleSavePopup)
savePopupCommit.addEventListener("click", commitChanges);
savePopupBG.addEventListener("click", toggleSavePopup);
versionSelectLabels.forEach(arrow => arrow.addEventListener('click', toggleVersionDropdown));
volumeSlider.addEventListener("input", function() {
wavesurfer.setVolume(this.value);
if (this.value == 0) {
muteButton.src = interface_bootstrap_images + "mute.svg";
muteButton.style.opacity = 0.6;
} else {
muteButton.src = interface_bootstrap_images + "unmute.svg";
muteButton.style.opacity = 1;
}
});
zoomSlider.addEventListener('input', function() { // slider changes waveform zoom
wavesurfer.zoom(Number(this.value) / 4);
if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
drawCurrentRegionBounds();
}
let handles = document.getElementsByClassName("wavesurfer-handle");
if (this.value < 20) {
for (var handle of handles) handle.style.setProperty("width", "1px", "important");
} else {
for (var handle of handles) handle.style.setProperty("width", "3px", "important");
}
});
showAudioLoader();
if (gs.variables.allowEditing === '0') { editButton.style.display = "none" }
wavesurfer.load(audio);
// wavesurfer events
wavesurfer.on('region-click', handleRegionClick);
wavesurfer.on('region-mouseenter', function(region) { // region hover effects
if (!mouseDown) {
handleRegionColours(region, true);
setHoverSpeaker(region.element.style.left, region.attributes.label.innerText);
if (!isInCurrentRegions(region)) {
removeRegionBounds();
drawRegionBounds(region, wave.scrollLeft, "black");
}
if (isCurrentRegion(region) && editMode) drawRegionBounds(region, wave.scrollLeft, "FireBrick");
}
});
wavesurfer.on('region-mouseleave', function(region) {
hoverSpeaker.innerHTML = "";
if (!mouseDown) {
if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false);
if (!editMode) hoverSpeaker.innerHTML = "";
removeRegionBounds();
if (currentRegion && currentRegion.speaker && getCurrentRegionIndex() != -1) {
setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
drawCurrentRegionBounds();
}
}
});
wavesurfer.on('region-in', function(region) { // play caret enters region
if (!mouseDown) {
handleRegionColours(region, true);
if (itemType == "chapter" && Array.from(chapters.children)[getIndexOfRegion(region)]) {
Array.from(chapters.children)[getIndexOfRegion(region)].scrollIntoView({
behavior: "smooth",
block: "nearest"
});
}
}
});
wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) });
wavesurfer.on('region-update-end', handleRegionEdit); // end of click-drag event
wavesurfer.on('region-updated', handleRegionSnap);
wavesurfer.on('error', error => console.log(error));
wavesurfer.on("play", () => { playPauseButton.src = interface_bootstrap_images + "pause.svg"; });
wavesurfer.on("pause", () => { playPauseButton.src = interface_bootstrap_images + "play.svg"; });
wavesurfer.on("mute", function(mute) {
if (mute) {
muteButton.src = interface_bootstrap_images + "mute.svg";
muteButton.style.opacity = 0.6;
volumeSlider.value = 0;
}
else {
muteButton.src = interface_bootstrap_images + "unmute.svg";
muteButton.style.opacity = 1;
volumeSlider.value = 1;
}
});
wavesurfer.on('ready', function() { // retrieve regions once waveforms have loaded
window.onbeforeunload = (e) => {
if (undoStates.length > 1) {
console.log('undoStates.length: ' + undoStates.length);
e.returnValue = "Data will be lost if you leave the page, are you sure?";
return "Data will be lost if you leave the page, are you sure?";
}
};
if (document.getElementById('new-canvas')) document.getElementById('new-canvas').remove();
setTimeout(() => { // if not delayed exportImage does not retrieve waveform (despite being in waveform-ready?)
var currVersion = selectedVersions[(!dualMode || primaryCaret.src.includes("fill")) ? 0 : 1];
for (let key in canvasImages) {
if (currVersion == key && canvasImages[key] == undefined) { canvasImages[key] = wavesurfer.exportImage() } // add waveform image to cache if one isn't already assigned to the version
}
}, 1000);
if (initialLoad) {
if (inputFile.endsWith("csv")) { // diarization if csv
itemType = "chapter";
if (localStorage.getItem('undoStates') && localStorage.getItem('undoLevel')) {
console.log('-- Loading regions from localStorage --');
undoStates = JSON.parse(localStorage.getItem('undoStates'));
undoLevel = JSON.parse(localStorage.getItem('undoLevel'));
primarySet.tempSpeakerObjects = undoStates[undoLevel].state;
primarySet.speakerObjects = cloneSpeakerObjectArray(primarySet.tempSpeakerObjects);
primarySet.uniqueSpeakers = [];
for (var item of primarySet.tempSpeakerObjects) {
if (!primarySet.uniqueSpeakers.includes(item.speaker)) primarySet.uniqueSpeakers.push(item.speaker);
}
populateChaptersAndRegions(primarySet);
if (undoStates[undoLevel].secState && undoStates[undoLevel].secState.length > 0) {
secondarySet.tempSpeakerObjects = undoStates[undoLevel].secState;
secondarySet.speakerObjects = cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects);
secondarySet.uniqueSpeakers = [];
for (var item of secondarySet.tempSpeakerObjects) {
if (!secondarySet.uniqueSpeakers.includes(item.speaker)) secondarySet.uniqueSpeakers.push(item.speaker);
}
secondaryLoaded = true;
}
updateRegionEditPanel();
} else {
loadCSVFile(inputFile, primarySet);
dualModeChanged(true, "true");
setTimeout(()=>{
dualModeChanged(true, "false");
}, 150)
}
} else if (inputFile.endsWith("json")) { // transcription if json
itemType = "word";
loadJSONFile(inputFile);
} else {
console.log("Filetype of " + inputFile + " not supported.")
}
chapters.style.cursor = "default"; // remove load cursor
wave.className = "audio-scroll";
$.ajax({
type: "GET",
url: gs.variables.metadataServerURL,
data: { a: 'get-fldv-info', site: gs.xsltParams.site_name, c: gs.cgiParams.c, d: gs.cgiParams.d },
dataType: "json",
}).then(data => {
for (var version of ["current", ...data]) {
canvasImages[version] = undefined;
let menuItem = document.createElement("div");
menuItem.classList.add("version-select-menu-item");
menuItem.id = version;
let text = version.includes("nminus") ? version.replace("nminus-", "Previous(") + ")" : version;
menuItem.innerText = text.charAt(0).toUpperCase() + text.slice(1);
menuItem.addEventListener('click', versionClicked);
let dataObj = { a: 'get-archives-metadata', site: gs.xsltParams.site_name, c: gs.cgiParams.c, d: gs.cgiParams.d, metaname: "commitmessage" };
if (version != "current") Object.assign(dataObj, {dv: version});
$.ajax({ // get commitmessage metadata to show as hover tooltip
type: "GET",
url: gs.variables.metadataServerURL,
data: dataObj,
dataType: "text",
}).then(comment => {
menuItem.title = "Commit message: " + comment;
versionSelectMenu.append(menuItem);
[...versionSelectMenu.children].sort((a,b) => a.innerText>b.innerText?1:-1).forEach(n=>versionSelectMenu.appendChild(n)); // sort alphabetically
}, (error) => { console.log("get-archives-metadata error:"); console.log(error); });
}
}, (error) => { console.log("get-fldv-info error:"); console.log(error); });
initialLoad = false;
}
// fixes blank waveform/regions when loading Current -> Prev.1 -> Prev.2
zoomSlider.value = 25;
zoomSlider.dispatchEvent(new Event("input"));
wavesurfer.zoom(50 / 4);
hideAudioLoader();
});
/**
* Draws string above waveform at the provided offset
* @param {number} offset Offset (from left) to desired location
* @param {string} name String to be drawn
*/
function setHoverSpeaker(offset, name) {
hoverSpeaker.innerHTML = name;
let newOffset = parseInt(offset.slice(0, -2)) - wave.scrollLeft;
hoverSpeaker.style.marginLeft = newOffset + "px";
}
/** Click handler, manages selected region/s, set swapping, region playing */
function handleRegionClick(region, e) {
if (e.target.classList.contains("region-menu")) return;
e.stopPropagation();
contextMenu.classList.remove('visible');
if (!editMode) { // play region audio on click
wavesurfer.play(region.start); // plays from start of region
} else { // select or deselect current region
if (!region.element) return;
if (region.element.classList.contains("region-top")) {
currSpeakerSet = primarySet;
swapCarets(true);
} else if (region.element.classList.contains("region-bottom")) {
currSpeakerSet = secondarySet;
swapCarets(false);
}
prevUndoState = "";
if (!e.ctrlKey && !e.shiftKey) {
currentRegions = [];
currentRegion = region;
currentRegion.speaker = currentRegion.attributes.label.innerText;
wavesurfer.backend.seekTo(currentRegion.start);
} else if (e.ctrlKey) { // control was held during click
if (currentRegions.length == 0 && isCurrentRegion(region)) {
removeCurrentRegion();
} else if (getCurrentRegionIndex() != -1 && isInCurrentRegions(region)) {
var removeIndex = getIndexInCurrentRegions(region);
if (removeIndex != -1) currentRegions.splice(removeIndex, 1);
if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed
currentRegion = currentRegions[0];
}
} else {
if (currentRegions.length < 1) currentRegions.push(currentRegion);
if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist
currentRegion = region;
currentRegion.speaker = currentRegion.attributes.label.innerText;
wavesurfer.backend.seekTo(currentRegion.start);
}
if (currentRegions.length == 1) currentRegions = []; // clear selected regions if there is only one
} else if (e.shiftKey) { // shift was held during click
clearChapterSearch();
if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) {
if (currentRegions && currentRegions.length > 0) {
if (Math.max(...getCurrentRegionsIndexes()) < getIndexOfRegion(region)) { // shifting forwards / down
currentRegions = currSpeakerSet.tempSpeakerObjects.slice(Math.min(...getCurrentRegionsIndexes()), getIndexOfRegion(region)+1);
} else { // shifting backwards / up
currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), Math.max(...getCurrentRegionsIndexes())+1);
}
} else {
if (getCurrentRegionIndex() < getIndexOfRegion(region)) { // shifting forwards / down
currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getCurrentRegionIndex(), getIndexOfRegion(region)+1);
} else { // shifting backwards / up
currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), getCurrentRegionIndex()+1);
}
}
}
}
if (changeAllCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) }
reloadRegionsAndChapters();
}
}
/**
* Returns index of given region within the currently selected regions
* @param {object} region Region within currently selected regions to return index for
* @returns {int} Index position of region
*/
function getIndexInCurrentRegions(region) {
for (var reg of currentRegions) {
var regSpeaker = reg.attributes ? reg.attributes.label.innerText : reg.speaker;
if (reg.start == region.start && reg.end == region.end && regSpeaker == region.attributes.label.innerText) {
return currentRegions.indexOf(reg);
}
}
return -1;
}
/**
* Returns index of region within speakerObject array
* @param {object} region Region to return index for
* @returns {int} Index position of region
*/
function getIndexOfRegion(region) {
for (var reg of currSpeakerSet.tempSpeakerObjects) {
if (region.attributes && reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label.innerText) {
return currSpeakerSet.tempSpeakerObjects.indexOf(reg);
}
}
return -1;
}
/**
* Builds metadata-server.pl URL to retrieve audio at given version
* @param {string} version GS document version to retrieve from (nminus-X)
*/
function getAudioURLFromVersion(version) {
let base_url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d;
if (version !== "current") base_url += "&dv=" + version // get fldv if not current version
return base_url + "&assocname=" + gs.documentMetadata.Audio;
}
/**
* Builds metadata-server.pl URL to retrieve CSV at given version
* @param {string} version GS document version to retrieve from (nminus-X)
*/
function getCSVURLFromVersion(version) {
let base_url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d;
if (version !== "current") base_url += "&dv=" + version; // get fldv if not current version
return base_url + "&assocname=" + "structured-audio.csv";
}
/** Version click handler, first checks if changes have been made and shows popup if true */
function versionClicked(e) {
let unsavedChanges = false;
if (undoStates.length > 0) { // only if changes have been made in track being changed FROM
let clickedVersionPos = e.target.parentElement.classList.contains('versionTop') ? 0 : 1;
for (var state of undoStates) {
if (state.changedTrack == selectedVersions[clickedVersionPos]) {
unsavedChanges = true;
break;
}
}
}
if (unsavedChanges) {
var areYouSure = "There are unsaved changes.\nAre you sure you want to lose changes made in this version?";
if (window.confirm(areYouSure)) {
console.log('OK');
discardRegionChanges(true);
changeVersion(e);
} else {
console.log('CANCEL');
return;
}
} else changeVersion(e);
}
/** Changes current audio/csv set to clicked version's equivalent */
function changeVersion(e) {
removeCurrentRegion();
var audio_url = getAudioURLFromVersion(e.target.id);
var csv_url = getCSVURLFromVersion(e.target.id);
versionSelectMenu.classList.remove('visible');
var setToUpdate = e.target.parentElement.classList.contains('versionTop') ? primarySet : secondarySet;
if (e.target.parentElement.classList.contains('versionTop')) {
if (!currSpeakerSet.isSecondary) {
if (dualMode) $(".region-top").remove();
else $(".wavesurfer-region").remove();
showAudioLoader();
// if (canvasImages[e.target.id]) { // if waveform image exists in cache
// drawImageOnWaveform(canvasImages[e.target.id]);
// }
wavesurfer.load(audio_url); // load audio
} else {
$(".region-top").remove();
}
document.getElementById('track-set-label-top').children[0].innerText = e.target.id.includes("nminus") ? e.target.id.replace("nminus-", "Previous(") + ")" : "Current"; // update top label text
selectedVersions[0] = e.target.id; // update the selected versions
} else {
if (currSpeakerSet.isSecondary) {
if (dualMode) $(".region-bottom").remove();
else $(".wavesurfer-region").remove();
showAudioLoader();
// if (canvasImages[e.target.id]) { // if waveform image exists in cache
// drawImageOnWaveform(canvasImages[e.target.id]);
// }
wavesurfer.load(audio_url);
} else {
$(".region-bottom").remove();
}
document.getElementById('track-set-label-bottom').children[0].innerText = e.target.id.includes("nminus") ? e.target.id.replace("nminus-", "Previous(") + ")" : "Current"; // update bottom label text
selectedVersions[1] = e.target.id;
}
loadCSVFile(csv_url, setToUpdate, true);
}
/** Utility function to download audio */
function downloadURI(loc, name) {
let link = document.createElement("a");
link.download = name;
link.href = loc;
link.click();
}
/** Document click listener for context box closure and region deselection */
function documentClicked(e) { // document on click
if (e.target.classList.contains("region-menu")) return;
contextMenu.classList.remove('visible');
timelineMenu.classList.remove('visible');
versionSelectMenu.classList.remove('visible');
versionSelectLabels.forEach(arrow => {
// arrow.style.transform = 'rotate(90deg)';
// arrow.style.paddingTop = '0';
arrow.style.display = 'inline';
});
if (editMode && e.target.tagName !== "INPUT" && e.target.tagName !== "IMG" && !e.target.classList.contains("ui-button") && !$("#audio-dropdowns").has($(e.target)).length
&& !e.target.classList.contains("context-menu-item")) {
let currReg = getCurrentRegionIndex() != -1 ? currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region : false; // save for deselection
let currRegs = getCurrentRegionsIndexes().length > 1 ? currentRegions : false; // save for deselection
removeCurrentRegion();
reloadChapterList();
if (currReg != false) regionLeave(currReg); // deselect curr region
if (currRegs != false) {
for (var reg of currRegs) {
regionLeave(reg.region); // deselect curr regions
regionLeave(reg.region); // deselect curr regions
}
}
removeRegionBounds();
removeButton.innerHTML = "Remove Selected Region";
updateRegionEditPanel();
}
}
/** Draws and returns padlock image at given parent element */
function drawPadlock(parent) {
let lockedImg = document.createElement("img");
lockedImg.classList.add("region-padlock");
lockedImg.src = interface_bootstrap_images + "lock.svg";
lockedImg.title = "This region is locked. Click to unlock region.";
parent.prepend(lockedImg);
return lockedImg;
}
/**
* Draws triple dot menu button and attaches click listener
* @param {object} region Region to attach menu button to
*/
function drawMenuButton(region) {
let menuImg = document.createElement("img");
menuImg.src = interface_bootstrap_images + "menu.svg";
menuImg.classList.add("region-menu");
menuImg.title = "Show region options";
menuImg.addEventListener("click", e => {
audioContainer.dispatchEvent(new MouseEvent("contextmenu", { clientX: menuImg.x + 20, clientY: menuImg.y + 5 }));
});
region.element.append(menuImg);
}
/**
* Attaches a click listener to given padlock element
* @param padlock Element to attach listener to
* @param region Associated region
* @param isChapter Whether padlock exists in chapter (true) or wavesurfer region (false)
*/
function attachPadlockListener(padlock, region, isChapter) {
if (isChapter == true) {
padlock.addEventListener('click', () => { // attach to chapter padlock
let index = getIndexOfRegion(region);
currSpeakerSet.tempSpeakerObjects[index].locked = false;
padlock.classList.add('hide');
if (currSpeakerSet.tempSpeakerObjects[index].region.element.firstChild) currSpeakerSet.tempSpeakerObjects[index].region.element.firstChild.remove();
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", index);
});
} else {
padlock.addEventListener('click', () => { // attach to region padlock
let index = getIndexOfRegion(region);
currSpeakerSet.tempSpeakerObjects[index].locked = false;
padlock.remove();
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", index);
});
}
}
/** Locks or unlocks selected region based on its current state */
function toggleLockSelected(e) { // locks / unlocks selected region(s)
if (e) e.stopPropagation();
if (getCurrentRegionIndex() != -1 && currentRegions.length <= 1) { // single selected
let currIndex = getCurrentRegionIndex();
currSpeakerSet.tempSpeakerObjects[currIndex].locked = !e.target.innerText.includes("Unlock");
if (currSpeakerSet.tempSpeakerObjects[currIndex].locked) {
chapters.childNodes[currIndex].childNodes[1].classList.remove('hide');
let lock = drawPadlock(currSpeakerSet.tempSpeakerObjects[currIndex].region.element);
attachPadlockListener(lock, currSpeakerSet.tempSpeakerObjects[currIndex].region, false);
contextLock.innerText = "Unlock Selected";
} else {
chapters.childNodes[currIndex].childNodes[1].classList.add('hide');
if (currSpeakerSet.tempSpeakerObjects[currIndex].region.element.getElementsByClassName("region-padlock").length > 0) {
currSpeakerSet.tempSpeakerObjects[currIndex].region.element.getElementsByClassName("region-padlock")[0].remove();
}
contextLock.innerText = "Lock Selected";
}
} else if (currentRegions.length > 1) { // multiple selected
let toLock = !e.target.innerText.includes("Unlock");
for (var idx of getCurrentRegionsIndexes()) {
currSpeakerSet.tempSpeakerObjects[idx].locked = toLock;
if (currSpeakerSet.tempSpeakerObjects[idx].locked) {
chapters.childNodes[idx].childNodes[1].classList.remove('hide');
if (currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock").length == 0) {
let lock = drawPadlock(currSpeakerSet.tempSpeakerObjects[idx].region.element);
attachPadlockListener(lock, currSpeakerSet.tempSpeakerObjects[idx].region, false);
}
contextLock.innerText = "Unlock Selected";
} else {
chapters.childNodes[idx].childNodes[1].classList.add('hide');
if (currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock").length > 0) {
currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock")[0].remove();
}
contextLock.innerText = "Lock Selected";
}
}
if (document.getElementById("context-menu-lock-2")) document.getElementById("context-menu-lock-2").remove();
}
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", getCurrentRegionIndex());
}
/** TODO */
function timelineMenuHideClicked(e) { // hides all regions and chapter/edit divs
if (!e.target.children[0].checked) {
e.target.children[0].checked = true;
timelineMenuDualMode.classList.add('disabled');
timelineMenuRegionConflict.classList.add('disabled');
timelineMenuSpeakerConflict.classList.add('disabled');
if (editPanel.style.height != "0px") toggleEditMode();
if (chapters.style.height != "0px") toggleChapters();
$('.wavesurfer-region').fadeOut(100);
}
else {
e.target.children[0].checked = false;
timelineMenuDualMode.classList.remove('disabled');
timelineMenuRegionConflict.classList.remove('disabled');
timelineMenuSpeakerConflict.classList.remove('disabled');
let fadeIn = true;
if (timelineMenuRegionConflict.firstElementChild.checked) {
showStartStopConflicts(e, true);
fadeIn = false;
}
if (timelineMenuSpeakerConflict.firstElementChild.checked) {
showSpeakerNameConflicts(e, true);
fadeIn = false;
}
if (fadeIn) $('.wavesurfer-region').fadeIn(100);
}
}
function chapterSearchInputChange(e) { // filters chapters and regions by given speaker name
if (e.isTrusted) { // triggered from user action
if (document.getElementById("chapter-alert")) document.getElementById("chapter-alert").remove();
let matches = 0;
for (var idx in chapters.children) {
if (chapters.children[idx].firstChild && chapters.children[idx].classList.contains("chapter") && currSpeakerSet.tempSpeakerObjects[idx]
&& currSpeakerSet.tempSpeakerObjects[idx].region && currSpeakerSet.tempSpeakerObjects[idx].region.element) {
if (e.composed) removeCurrentRegion(); // composed true if called from input, false if manually triggered event
if (!chapters.children[idx].firstChild.innerText.toLowerCase().includes(e.target.value.toLowerCase())) {
chapters.children[idx].style.display = "none";
currSpeakerSet.tempSpeakerObjects[idx].region.element.style.display = "none";
} else {
chapters.children[idx].style.display = "flex";
currSpeakerSet.tempSpeakerObjects[idx].region.element.style.display = "";
matches++;
if (e.target.value.length > 0) {
var reg = new RegExp(e.target.value, 'gi'); // [g]lobal, [i]gnore case
chapters.children[idx].firstChild.innerHTML = chapters.children[idx].firstChild.innerText.replace(reg, '$&'); // highlights matching text
} else {
chapters.children[idx].firstChild.innerHTML = chapters.children[idx].firstChild.innerText; // highlights matching text
}
}
}
}
flashChapters();
if (matches == 0) {
var msg = document.createElement("span");
msg.innerHTML = "No Matches!";
msg.id = "chapter-alert";
chapters.prepend(msg);
}
}
}
function clearChapterSearch() { // clears search filter and updates results
chapterSearchInput.value = "";
chapterSearchInput.dispatchEvent(new Event("input"));
}
function showStartStopConflicts(e, forceRun) { // hides regions that have identical start/stop time
removeCurrentRegion();
if ((dualMode && !timelineMenuRegionConflict.children[0].checked) || forceRun) {
timelineMenuRegionConflict.children[0].checked = true;
let primHide = [];
let secHide = [];
if (!timelineMenuSpeakerConflict.children[0].checked) hideAll();
for (var primIdx in primarySet.tempSpeakerObjects) {
for (var secIdx in secondarySet.tempSpeakerObjects) {
if (regionsMatch(primarySet.tempSpeakerObjects[primIdx], secondarySet.tempSpeakerObjects[secIdx])) { // if regions have same start/end time, hide
primHide.push(primIdx);
secHide.push(secIdx);
}
}
}
for (var primIdx in primarySet.tempSpeakerObjects) {
if (!primHide.includes(primIdx)) {
primarySet.tempSpeakerObjects[primIdx].region.element.style.display = "";
if (primaryCaret.src.includes('fill')) chapters.children[primIdx].style.display = "flex";
}
}
for (var secIdx in secondarySet.tempSpeakerObjects) {
if (!secHide.includes(secIdx)) {
secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = "";
if (secondaryCaret.src.includes('fill')) chapters.children[secIdx].style.display = "flex";
}
}
} else {
timelineMenuRegionConflict.children[0].checked = false;
if (timelineMenuSpeakerConflict.children[0].checked) showSpeakerNameConflicts(e, true);
else clearConflicts();
}
}
function showSpeakerNameConflicts(e, forceRun) { // shows regions that have identical start/stop time but different names
removeCurrentRegion();
if ((dualMode && !timelineMenuSpeakerConflict.children[0].checked) || forceRun) {
timelineMenuSpeakerConflict.children[0].checked = true;
if (!timelineMenuRegionConflict.children[0].checked) hideAll();
for (var primIdx in primarySet.tempSpeakerObjects) {
for (var secIdx in secondarySet.tempSpeakerObjects) {
if (regionsMatch(primarySet.tempSpeakerObjects[primIdx], secondarySet.tempSpeakerObjects[secIdx]) &&
primarySet.tempSpeakerObjects[primIdx].speaker != secondarySet.tempSpeakerObjects[secIdx].speaker) { // hide if regions match but names don't
primarySet.tempSpeakerObjects[primIdx].region.element.style.display = "";
secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = "";
if (primaryCaret.src.includes('fill')) chapters.children[primIdx].style.display = "flex";
else chapters.children[secIdx].style.display = "flex";
}
}
}
} else {
timelineMenuSpeakerConflict.children[0].checked = false;
if (timelineMenuRegionConflict.children[0].checked) showStartStopConflicts(e, true);
else clearConflicts();
}
}
function clearConflicts() { // shows all regions and chapters
for (var primIdx in primarySet.tempSpeakerObjects) {
for (var secIdx in secondarySet.tempSpeakerObjects) {
primarySet.tempSpeakerObjects[primIdx].region.element.style.display = "";
secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = "";
chapters.children[primIdx].style.display = "flex";
}
}
}
function hideAll() { // hides all regions and chapters
for (var primIdx in primarySet.tempSpeakerObjects) {
for (var secIdx in secondarySet.tempSpeakerObjects) {
primarySet.tempSpeakerObjects[primIdx].region.element.style.display = "none";
secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = "none";
chapters.children[primIdx].style.display = "none";
}
}
}
function timelineMenuToggle(e) { // shows / hides timeline menu
e.stopPropagation();
if (timelineMenu.classList.contains('visible')) {
timelineMenu.classList.remove('visible');
e.target.style.transform = 'rotate(0deg)';
}
else {
timelineMenu.classList.add('visible');
e.target.style.transform = 'rotate(-90deg)';
}
}
function handleRegionSnap(region, e) { // clips region to opposite set region if nearby, called on region update (lots)
if (editMode && currentRegion && !wavesurfer.isPlaying()) {
removeRegionBounds();
setHoverSpeaker(region.element.style.left, currentRegion.speaker);
drawRegionBounds(region, wave.scrollLeft, "FireBrick"); // gets set to red if currRegion
if (e && e.action === "resize" && dualMode && editMode && !ctrlDown) { // won't actuate on drag
let oppositeSet = secondarySet; // look down
if (currSpeakerSet.isSecondary) oppositeSet = primarySet; // look up
if (e.direction === "left") {
region.update({ start: getSnapValue(region.start, oppositeSet.tempSpeakerObjects)});
} else if (e.direction === "right") {
region.update({ end: getSnapValue(region.end, oppositeSet.tempSpeakerObjects)});
}
}
if (e && (e.action === "resize" || e.action === "drag")) {
setInputInSeconds(startTimeInput, region.start);
setInputInSeconds(endTimeInput, region.end);
}
}
}
/**
* Returns snap value if near [snapRadius] adjacent region edge
* @param newDragPos Drag position in seconds to check for
* @param speakerSet Adjacent region set
* @returns {number} If found, returns snapped position, otherwise returns input position
*/
function getSnapValue(newDragPos, speakerSet) {
var snapRadius = 1;
for (var region of speakerSet) { // scan opposite region for potential snapping points
if (newDragPos > parseFloat(region.start) - snapRadius && newDragPos < parseFloat(region.start) + snapRadius) {
snappedTo = "start";
if (snappedToX == 0) snappedToX = waveformCursorX;
return region.start;
}
if (newDragPos > parseFloat(region.end) - snapRadius && newDragPos < parseFloat(region.end) + snapRadius) {
snappedTo = "end";
if (snappedToX == 0) snappedToX = waveformCursorX;
return region.end;
}
if (snappedTo !== "none" && (waveformCursorX - snappedToX > 10 || waveformCursorX - snappedToX < -10)) {
snappedTo = "none";
snappedToX = 0;
return cursorPos;
}
}
return newDragPos;
}
function mmssToSeconds(input) {
var arr = input.split(":");
if (arr.length == 2) {
return (parseInt(arr[0]) * 60) + parseInt(arr[1]);
} else if (arr.length == 3) {
return (parseInt(arr[0]) * 3600) + (parseInt(arr[1]) * 60) + parseInt(arr[2]);
} else {
console.error("unexpected input to mmssToSeconds: " + input);
}
}
function removeRightClicked(e) {
if (!e.target.classList.contains('disabled')) {
removeRegion();
}
}
function replaceSelected(e) { // moves selected region across, replaces and removes any overlapping regions in the opposite set
if (!e.target.classList.contains('disabled')) {
let destinationSet = secondarySet; // replace down
if (currSpeakerSet.isSecondary) destinationSet = primarySet; // replace up
let currItems = [currentRegion];
if (currentRegions && currentRegions.length > 0) currItems = currentRegions;
for (let idx = 0; idx < currItems.length; idx++) { // handles both currentRegion and currentRegions
for (let idy = 0; idy < destinationSet.tempSpeakerObjects.length; idy++) {
var reg = destinationSet.tempSpeakerObjects[idy];
if ((parseFloat(reg.start) >= parseFloat(currItems[idx].start) && parseFloat(reg.start) <= parseFloat(currItems[idx].end)) ||
(parseFloat(reg.start) <= parseFloat(currItems[idx].start) && parseFloat(reg.end) >= parseFloat(currItems[idx].start))) {
destinationSet.tempSpeakerObjects.splice(idy, 1); // remove subsequent region
idy--;
}
}
}
copySelected(e, true);
reloadRegionsAndChapters();
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "replace", getCurrentRegionIndex());
}
}
function containsRegion(set, region) { // true if given region exists in given set
for (var item of set) {
if (regionsMatch(region, item)) return true;
}
return false;
}
function overdubSelected(e) { // moves selected region across, merges any overlapping regions in the opposite set
if (!e.target.classList.contains('disabled')) {
let destinationSet = secondarySet; // replace down
if (currSpeakerSet.isSecondary) destinationSet = primarySet; // replace up
let backup;
if (destinationSet.isSecondary) backup = cloneSpeakerObjectArray(primarySet.tempSpeakerObjects); // saves selected set as this process changes values in selected set
else backup = cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects);
copySelected(e, true);
if (!currentRegions || currentRegions.length < 1) { // overdub single
handleSameSpeakerOverlap(getCurrentRegionIndex(), destinationSet, true);
} else { // overdub multiple
for (var item of getCurrentRegionsIndexes().reverse()) { // reverse indexes so index doesn't break when regions are removed
handleSameSpeakerOverlap(item, destinationSet, true);
}
}
if (destinationSet.isSecondary) primarySet.tempSpeakerObjects = backup;
else secondarySet.tempSpeakerObjects = backup;
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "overdub", getCurrentRegionIndex());
reloadRegionsAndChapters();
}
}
function copySelected(e, skipUndoState) { // copies region to opposite set [utility function for replace and overdub]
if (!e.target.classList.contains('disabled')) {
let destinationSet = secondarySet; // copy down
if (currSpeakerSet.isSecondary) destinationSet = primarySet // copy up
var selectedRegion = currentRegion;
if (currentRegions && currentRegions.length > 1) { // copy multiple
destinationSet.tempSpeakerObjects.push(...selectedRegions); // append current regions to dest. set
// currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions)
// for (var reg of destinationSet.tempSpeakerObjects) { // restore currentRegions in dest. set
// for (var selReg of selectedRegions) {
// if (regionsMatch(reg, selReg) && !containsRegion(currentRegions, reg)) {
// currentRegions.push(reg);
// }
// }
// if (regionsMatch(reg, selectedRegion)) { currentRegion = reg; }
// }
} else { // copy singular
destinationSet.tempSpeakerObjects.push(selectedRegion); // append current region to dest. set
// currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions)
// for (var reg of destinationSet.tempSpeakerObjects) { // restore currentRegion in dest. set
// if (regionsMatch(reg, selectedRegion)) {
// currentRegion = reg;
// break;
// }
// }
}
reloadRegionsAndChapters();
if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "copy", getCurrentRegionIndex());
}
}
/**
* Shows context menu with various region options
* @param {MouseEvent} e Either right click event or left click triple menu click event
*/
function onRightClick(e) {
if ((e.target.classList.contains("wavesurfer-region") || e.target.id === "audioContainer" || e.target.classList.contains("chapter")) && editMode) {
e.preventDefault();
e.stopPropagation();
// set current region to clicked region LLLLLLL
let clickedRegion; // could be used to select clicked region
for (var reg of currSpeakerSet.tempSpeakerObjects) {
if (reg.region.element.title == e.target.title) {
clickedRegion = reg;
break;
}
}
// console.log(clickedRegion)
contextMenu.classList.add("visible");
if (e.clientX + 200 > $(window).width()) contextMenu.style.left = ($(window).width() - 220) + "px"; // ensure menu doesn't clip on right
else contextMenu.style.left = e.clientX + "px";
contextMenu.style.top = e.clientY + "px";
let lockConflict = false;
if (currentRegions.length > 1) {
let firstIsLocked = 0;
for (var reg of currentRegions) {
if (firstIsLocked === 0) firstIsLocked = reg.locked;
else if (firstIsLocked != reg.locked) lockConflict = true;
}
}
if (lockConflict) {
contextLock.classList.remove('disabled');
if (!document.getElementById("context-menu-lock-2")) {
let contextLock2 = contextLock.cloneNode();
contextLock.innerText = "Lock Selected";
contextLock2.innerText = "Unlock Selected";
contextLock2.id = "context-menu-lock-2";
contextLock2.addEventListener('click', toggleLockSelected);
contextLock.parentNode.insertBefore(contextLock2, contextLock.nextSibling);
}
} else {
contextLock.classList.remove('disabled');
let currIndex = getCurrentRegionIndex();
if (currSpeakerSet.tempSpeakerObjects[currIndex] && currSpeakerSet.tempSpeakerObjects[currIndex].locked) {
contextLock.innerText = "Unlock Selected";
chapters.childNodes[currIndex].childNodes[1].classList.remove('hide');
} else if (currSpeakerSet.tempSpeakerObjects[currIndex]) {
contextLock.innerText = "Lock Selected";
chapters.childNodes[currIndex].childNodes[1].classList.add('hide');
}
}
if (dualMode && currentRegion && currentRegion.speaker !== "") {
contextReplace.classList.remove('disabled');
contextOverdub.classList.remove('disabled');
} else {
contextDelete.classList.add('disabled');
contextLock.classList.add('disabled');
contextReplace.classList.add('disabled');
contextOverdub.classList.add('disabled');
}
if (currentRegion && currentRegion.speaker !== "") {
contextDelete.classList.remove('disabled');
contextLock.classList.remove('disabled');
}
if (dualMode) { // manipulate context texts
var actionDirection = currSpeakerSet.isSecondary ? "Up" : "Down";
contextReplace.innerHTML = "Replace Selected " + actionDirection;
contextOverdub.innerHTML = "Overdub Selected " + actionDirection;
}
}
}
function saveSelected(e) {
let csvContent = "data:text/csv;charset=utf-8," + currSpeakerSet.speakerObjects.map(item => "\n" + [item.speaker, item.start, item.end].join());
console.log(csvContent);
var encodedUri = encodeURI(csvContent);
window.open(encodedUri);
}
function keyUp(e) { // key up listener
if (e.key == "Control") ctrlDown = false;
if (e.target.tagName !== "INPUT") {
if (e.code === "Backspace" || e.code === "Delete") removeRegion();
else if (e.code === "Space") { wavesurfer.playPause(); }
else if (e.code === "ArrowLeft") wavesurfer.skipBackward();
else if (e.code === "ArrowRight") wavesurfer.skipForward();
else if (e.code === "KeyL") toggleLockSelected(e);
}
if (e.code == "KeyZ" && e.ctrlKey) undo();
else if (e.code == "KeyY" && e.ctrlKey) redo();
}
function keyDown(e) { // keydown listener
if (e.key == "Control") ctrlDown = true;
if (e.code == "Space" && e.target.tagName.toLowerCase() != "input") e.preventDefault();
}
/**
* Shows / hides secondary speaker set
* @param skipUndoState Utility param - skips the addition of an undo state
* @param overrideValue Utility param - overrides the checkbox state
*/
function dualModeChanged(skipUndoState, overrideValue) {
if (overrideValue) dualModeCheckbox.checked = overrideValue == "true" ? true : false;
else dualModeCheckbox.checked = !dualModeCheckbox.checked; // toggle dual mode checkbox
dualMode = dualModeCheckbox.checked;
currSpeakerSet = primarySet;
if (!dualMode) removeCurrentRegion();
clearChapterSearch();
reloadRegionsAndChapters();
if (dualMode) {
if (!secondaryLoaded) {
// var secondaryCSVURL = "http://localhost:8383/greenstone3/cgi-bin/metadata-server.pl?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.collectionMetadata.indexStem +
// "&d=" + gs.documentMetadata.Identifier + "&assocname=structured-audio.csv&dv=nminus-1";
var secondaryCSVURL = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.collectionMetadata.indexStem +
"&d=" + gs.documentMetadata.Identifier + "&assocname=structured-audio.csv&dv=nminus-1";
loadCSVFile(secondaryCSVURL, secondarySet);
secondaryLoaded = true; // ensure secondarySet doesn't get re-read > once
}
document.getElementById("caret-container").style.display = "flex";
timelineMenuRegionConflict.classList.remove("disabled");
timelineMenuSpeakerConflict.classList.remove("disabled");
$('#track-set-label-bottom').fadeIn(100);
selectedVersions[1] = document.getElementById('track-set-label-bottom').children[0].innerText;
} else {
caretClicked('primary-caret');
document.getElementById("caret-container").style.display = "none";
selectedVersions.splice(1, 1); // trim to one version in array
timelineMenuRegionConflict.firstElementChild.checked = false;
timelineMenuSpeakerConflict.firstElementChild.checked = false;
timelineMenuRegionConflict.classList.add("disabled");
timelineMenuSpeakerConflict.classList.add("disabled");
$('#track-set-label-bottom').fadeOut(100);
}
currSpeakerSet = primarySet;
if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dualModeChange", getCurrentRegionIndex());
}
/**
* Changes selected speaker set
* @param {string} id ID of clicked caret image
*/
function caretClicked(id) {
clearChapterSearch();
if (id === "primary-caret") {
currSpeakerSet = primarySet;
swapCarets(true);
} else if (id === "secondary-caret") {
currSpeakerSet = secondarySet;
swapCarets(false);
}
}
/**
* Loads destination waveform and audio if required, updates caret images
* @param {boolean} toPrimary whether destination set is primary (true) or secondary (false)
*/
function swapCarets(toPrimary) {
var currCaretIsPrimary = primaryCaret.src.includes("fill") ? true : false; // initial value before swap
if ((toPrimary && !currCaretIsPrimary) || (!toPrimary && currCaretIsPrimary)) {
removeCurrentRegion(); // ensure currentRegion is only removed if changing speakerSet
flashChapters();
reloadChapterList();
}
if (toPrimary) {
if (!currCaretIsPrimary) {
showAudioLoader();
if (canvasImages[selectedVersions[0]]) { // if waveform image exists in cache
drawImageOnWaveform(canvasImages[selectedVersions[0]]);
// hideAudioLoader();
}
// else showAudioLoader();
let url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name +
"&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d + "&assocname=" + gs.documentMetadata.Audio;
if (selectedVersions[0] !== "current") {
if (selectedVersions[0].includes("Previous")) url += "&dv=" + selectedVersions[0].replace("Previous(", "nminus-").replace(")", "");
else url += "&dv=" + selectedVersions[0];
}
wavesurfer.load(url);
}
primaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
secondaryCaret.src = interface_bootstrap_images + "caret-right.svg";
} else {
if (currCaretIsPrimary) {
showAudioLoader();
if (canvasImages[selectedVersions[1]]) {
drawImageOnWaveform(canvasImages[selectedVersions[1]]);
// hideAudioLoader();
}
// else showAudioLoader();
let url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name +
"&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d + "&assocname=" + gs.documentMetadata.Audio;
if (selectedVersions[1] !== "current") {
if (selectedVersions[1].includes("Previous")) url += "&dv=" + selectedVersions[1].replace("Previous(", "nminus-").replace(")", "");
else url += "&dv=" + selectedVersions[1];
}
wavesurfer.load(url);
}
primaryCaret.src = interface_bootstrap_images + "caret-right.svg";
secondaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
}
}
/**
* Shows spinning loader over waveform, hides regions
*/
function showAudioLoader() {
$('.wavesurfer-region').fadeOut(100);
$(".chapter").fadeOut(100);
$(".track-set-label").fadeOut(100);
waveformSpinner.style.display = 'block';
loader.style.display = "inline";
for (var ele of editPanel.children) ele.classList.add("disabled");
playPauseButton.classList.add("disabled");
}
/**
* Hides spinning loader, brings back regions
*/
function hideAudioLoader() {
$('.wavesurfer-region').fadeIn(100);
$(".chapter").fadeIn(100);
$("#track-set-label-top").fadeIn(100);
if (dualMode) $('#track-set-label-bottom').fadeIn(100);
waveformSpinner.style.display = 'none';
loader.style.display = "none";
for (var ele of editPanel.children) ele.classList.remove("disabled");
updateRegionEditPanel();
playPauseButton.classList.remove("disabled");
}
/**
* Draws given image URL on waveform
* @param image URL of image to be drawn
*/
function drawImageOnWaveform(image) {
// console.log('draw waveform image from cache')
if (document.getElementById('new-canvas')) document.getElementById('new-canvas').remove();
var newCanvas = document.createElement("div");
newCanvas.id = "new-canvas";
newCanvas.style.width = wavesurfer.drawer.canvases[0].wave.width + 'px';
newCanvas.style.height = '140px';
newCanvas.style.backgroundImage = "url('" + image + "')";
waveformContainer.appendChild(newCanvas);
}
/**
* Regenerates chapter list to update any changes made in speakerSet
*/
function reloadChapterList() {
chapters.innerHTML = "";
for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
let chapter = document.createElement("div");
chapter.classList.add("chapter");
chapter.id = "chapter" + i;
let speakerName = document.createElement("span");
speakerName.classList.add("speakerName");
speakerName.innerText = currSpeakerSet.tempSpeakerObjects[i].speaker;
let regionLocked = document.createElement("img");
regionLocked.src = interface_bootstrap_images + "lock.svg";
regionLocked.classList.add("speakerLocked", "hide");
attachPadlockListener(regionLocked, currSpeakerSet.tempSpeakerObjects[i].region, true);
if (currSpeakerSet.tempSpeakerObjects[i].locked && editMode) regionLocked.classList.remove("hide");
let speakerTime = document.createElement("span");
speakerTime.classList.add("speakerTime");
speakerTime.innerHTML = minutize(currSpeakerSet.tempSpeakerObjects[i].start) + " - " + minutize(currSpeakerSet.tempSpeakerObjects[i].end) + "s";
chapter.appendChild(speakerName);
chapter.appendChild(regionLocked);
chapter.appendChild(speakerTime);
chapter.addEventListener("click", chapterClicked);
chapter.addEventListener("mouseenter", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
if (chapterSearchInput.value.length > 0 && !speakerName.innerText.toLowerCase().includes(chapterSearchInput.value.toLowerCase())) {
chapter.style.display = "none";
currSpeakerSet.tempSpeakerObjects[i].region.element.style.display = "none";
}
chapters.appendChild(chapter);
}
}
/**
* Shows / hides chapter section
*/
let toggleChapters = function() {
if (chapters.style.height == "0px") {
chapters.style.height = "90%";
chaptersContainer.style.height = "30vh";
chapterSearchInput.placeholder = "Filter by Name...";
} else {
chapters.style.height = "0px";
chaptersContainer.style.height = "0px";
chapterSearchInput.placeholder = "";
}
}
/**
* Object representing elements of a diarization output
* @param {boolean} isSecondary Whether or not the set is secondary/bottom (true) or primary/top (false)
* @param {Array} uniqueSpeakers Array of all unique speaker names within the diarization data, used for colouring regions
* @param {Array} speakerObjects Array of objects containing speaker start/stop times and names
* @param {Array} tempSpeakerObjects Temporary version of speakerObjects, which can be reverted back to if required
*/
function SpeakerSet(isSecondary, uniqueSpeakers, speakerObjects, tempSpeakerObjects) {
this.isSecondary = isSecondary;
this.uniqueSpeakers = uniqueSpeakers;
this.speakerObjects = speakerObjects;
this.tempSpeakerObjects = tempSpeakerObjects;
}
let primarySet = new SpeakerSet(false, [], [], [], []);
let secondarySet = new SpeakerSet(true, [], [], [], []);
let currSpeakerSet = primarySet;
/**
* Reads diarization CSV file and populates speakerSet
* @param {string} filename Source destination of input CSV file
* @param {object} speakerSet speaker set to be populated
* @param {boolean} forcePopulate Forces redraw of regions and chapters
*/
function loadCSVFile(filename, speakerSet, forcePopulate) { // based on: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript
$.ajax({
type: "GET",
url: filename,
dataType: "text",
}).then(function(data) {
let dataLines = data.split(/\r\n|\n/);
let headers;
let startIndex = 0;
speakerSet.uniqueSpeakers = []; // used for obtaining unique colours
speakerSet.speakerObjects = []; // list of speaker items
if (dataLines[0].split(',').length === 3) headers = ["speaker", "start", "end"]; // assume speaker, start, end
else if (dataLines[0].split(',').length === 4) headers = ["speaker", "start", "end", "locked"]; // assume speaker, start, end, locked
for (let i = startIndex; i < dataLines.length; i++) {
let data = dataLines[i].split(',');
if (data.length == headers.length) {
let item = {};
for (let j = 0; j < headers.length; j++) {
item[headers[j]] = data[j];
if (j == 0 && !speakerSet.uniqueSpeakers.includes(data[j])) {
speakerSet.uniqueSpeakers.push(data[j]);
}
}
if (headers.length === 3) item['locked'] = false;
speakerSet.speakerObjects.push(item);
}
}
speakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(speakerSet.speakerObjects);
if (!speakerSet.isSecondary || forcePopulate) populateChaptersAndRegions(speakerSet); // prevents secondary set being drawn on first load
resetUndoStates(); // undo stack init
}, (error) => { console.log("loadCSVFile error:"); console.log(error); });
}
/**
* Populates chapter list div and regions on waveform with given speaker set
* @param {object} data Speaker set object with diarization data
*/
function populateChaptersAndRegions(data) {
// colorbrewer is a web tool for guidance in choosing map colour schemes based on a letiety of settings.
// this colour scheme is designed for qualitative data
if (regionColourSet.length < 1) {
for (let i = 0; i < data.uniqueSpeakers.length; i++) { // not tested in cases where there are more than 8 speakers!!
var adjIdx = i%8;
regionColourSet[adjIdx] = { name: data.uniqueSpeakers[i], colour: colourbrewerSet[adjIdx] }
}
}
let isSelectedSet = false;
if ((!data.isSecondary && primaryCaret.src.includes("fill")) || (data.isSecondary && secondaryCaret.src.includes("fill"))) isSelectedSet = true;
data.tempSpeakerObjects = sortSpeakerObjectsByStart(data.tempSpeakerObjects); // sort speakerObjects by start time
if (isSelectedSet || !dualMode) chapters.innerHTML = ""; // clear chapter div for re-population
for (let i = 0; i < data.tempSpeakerObjects.length; i++) {
let chapter = document.createElement("div");
chapter.classList.add("chapter");
chapter.id = "chapter" + i;
let speakerName = document.createElement("span");
speakerName.classList.add("speakerName");
speakerName.innerText = data.tempSpeakerObjects[i].speaker;
let regionLocked = document.createElement("img");
regionLocked.src = interface_bootstrap_images + "lock.svg";
regionLocked.classList.add("speakerLocked", "hide");
attachPadlockListener(regionLocked, data.tempSpeakerObjects[i].region, true);
if (data.tempSpeakerObjects[i].locked && editMode) regionLocked.classList.remove("hide");
let speakerTime = document.createElement("span");
speakerTime.classList.add("speakerTime");
speakerTime.innerHTML = minutize(data.tempSpeakerObjects[i].start) + " - " + minutize(data.tempSpeakerObjects[i].end) + "s";
chapter.appendChild(speakerName);
chapter.appendChild(regionLocked);
chapter.appendChild(speakerTime);
chapter.addEventListener("click", chapterClicked);
chapter.addEventListener("mouseenter", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
let selected = false;
let dummyRegion = { start: data.tempSpeakerObjects[i].start, end: data.tempSpeakerObjects[i].end };
if ((isSelectedSet || !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) {
chapter.classList.add("selected-chapter");
selected = true;
}
if (isSelectedSet || !dualMode) chapters.appendChild(chapter);
let regColour;
if (regionColourSet.find(item => item.name === data.tempSpeakerObjects[i].speaker)) {
regColour = regionColourSet.find(item => item.name === data.tempSpeakerObjects[i].speaker).colour;
} else {
regionColourSet.push({ name: data.tempSpeakerObjects[i].speaker, colour: colourbrewerSet[i%8]});
regColour = regionColourSet.at(-1).colour;
}
let associatedReg = wavesurfer.addRegion({ // create associated wavesurfer region
id: "region" + i,
start: data.tempSpeakerObjects[i].start,
end: data.tempSpeakerObjects[i].end,
drag: editMode,
resize: editMode,
attributes: {
label: speakerName,
},
color: regColour + regionTransparency,
//...(selected) && {color: "rgba(255,50,50,0.5)"},
});
// I think I checked with Finn that this fix by Dr Bainbridge is fine:
if(selected) {
associatedReg.color="rgba(255,50,50,0.5)";
}
data.tempSpeakerObjects[i].region = associatedReg;
if (selected && data.tempSpeakerObjects[i].locked) { // add padlock to regions if they are selected and locked
let lock = drawPadlock(associatedReg.element);
attachPadlockListener(lock, associatedReg, false);
}
if (selected) drawMenuButton(associatedReg);
}
if (waveformSpinner.style.display == 'block') $(".wavesurfer-region").fadeOut(100); // keep regions hidden until wavesurfer.load() has finished
let handles = document.getElementsByTagName('handle');
for (var handle of handles) handle.addEventListener('mousedown', () => mouseDown = true);
let regions = document.getElementsByTagName("region");
if (dualMode) {
if (document.getElementsByClassName("region-top").length == 0) {
for (var reg of regions) {
if (reg.classList.length == 1) reg.classList.add("region-top");
}
} else {
for (var rego of regions) {
if (!rego.classList.contains("region-top") && rego.classList.length == 1) rego.classList.add("region-bottom");
}
}
}
if (editMode) for (var reg of regions) reg.style.setProperty("z-index", "3", "important");
else for (var reg of regions) reg.style.setProperty("z-index", "1", "important");
chapterSearchInput.dispatchEvent(new Event("input"));
}
function loadJSONFile(filename) {
$.ajax({
type: "GET",
url: filename,
dataType: "text",
}).then(function(data){ populateWords(JSON.parse(data)) }, (error) => { console.log("loadJSONFile error:"); console.log(error); });
}
function populateWords(data) { // populates word section and adds regions to waveform
let transcription = data.transcription;
let words = data.words;
let wordContainer = document.createElement("div");
wordContainer.id = "word-container";
for (let i = 0; i < words.length; i++) {
let word = document.createElement("span");
word.id = "word" + i;
word.classList.add("word");
word.innerHTML = transcription.split(" ")[i];
word.addEventListener("click", e => { wordClicked(data, e.target.id) });
word.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
word.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
wordContainer.appendChild(word);
wavesurfer.addRegion({
id: "region" + i,
start: words[i].startTime,
end: words[i].endTime,
drag: false,
resize: false,
color: "rgba(255, 255, 255, 0.1)",
});
}
chapters.appendChild(wordContainer);
}
let chapterClicked = function(e) { // plays audio from start of chapter
var index = Array.from(chapters.children).indexOf(e.target);
if (currSpeakerSet.tempSpeakerObjects[index]) {
let clickedRegion = currSpeakerSet.tempSpeakerObjects[index].region;
handleRegionClick(clickedRegion, e);
}
}
function wordClicked(data, id) { // plays audio from start of word
let index = id.replace("word", "");
let start = data.words[index].startTime;
wavesurfer.play(start);
}
function chapterEnter(idx) {
let reg = currSpeakerSet.tempSpeakerObjects[idx].region;
regionEnter(reg);
setHoverSpeaker(reg.element.style.left, reg.attributes.label.innerText);
if (!isInCurrentRegions(reg)) {
removeRegionBounds();
drawRegionBounds(reg, wave.scrollLeft, "black");
}
}
function chapterLeave(idx) {
regionLeave(currSpeakerSet.tempSpeakerObjects[idx].region);
removeRegionBounds();
hoverSpeaker.innerHTML = "";
if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
drawCurrentRegionBounds();
}
}
/**
* Handles region and chapter colours
* @param {object} region Region element to adjust
* @param {boolean} highlight Whether or not region should be white-highlighted
*/
function handleRegionColours(region, highlight) { // handles region, chapter & word colours
if (!dualMode || (region.element.classList.contains("region-top") && primaryCaret.src.includes("fill")) || region.element.classList.contains("region-bottom") && secondaryCaret.src.includes("fill")) {
let colour;
if (highlight) {
colour = "rgb(81, 90, 90)";
regionEnter(region);
} else {
colour = "";
regionLeave(region);
}
if (isCurrentRegion(region) || isInCurrentRegions(region)) {
colour = "rgba(255, 50, 50, 0.5)";
}
if (chapters.childNodes[getIndexOfRegion(region)]) chapters.childNodes[getIndexOfRegion(region)].style.backgroundColor = colour;
}
}
function regionEnter(region) {
if (isCurrentRegion(region) || isInCurrentRegions(region)) {
region.update({ color: "rgba(255, 50, 50, 0.5)" });
} else {
region.update({ color: "rgba(255, 255, 255, 0.3)" });
}
if (editMode && currSpeakerSet.tempSpeakerObjects[getIndexOfRegion(region)] && currSpeakerSet.tempSpeakerObjects[getIndexOfRegion(region)].locked
&& region.element.getElementsByClassName("region-padlock").length == 0) { // hovered region is locked
let lock = drawPadlock(region.element);
attachPadlockListener(lock, region, false);
}
if (editMode && region.element.getElementsByClassName("region-menu").length == 0) {
drawMenuButton(region);
}
}
function regionLeave(region) {
if (itemType == "chapter") {
if (isCurrentRegion(region) || isInCurrentRegions(region)) {
region.update({ color: "rgba(255, 50, 50, 0.5)" });
// } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) {
} else {
let index = region.id.replace("region", "");
region.update({ color: regionColourSet.find(item => item.name === currSpeakerSet.tempSpeakerObjects[index].speaker).colour + regionTransparency });
}
if (region.element.getElementsByTagName("img").length > 0 && !isCurrentRegion(region) && !isInCurrentRegions(region)) {
for (let child of Array.from(region.element.children)) {
if (child.tagName == "IMG") {
child.remove();
}
}
}
} else {
region.update({ color: "rgba(255, 255, 255, 0.1)" });
}
}
function minutize(num) { // converts seconds to m:ss for chapters & waveform hover
let date = new Date(null);
date.setSeconds(num);
return date.toTimeString().split(" ")[0].substring(3);
}
function formatCursor(num) {
cursorPos = num;
return minutize(num);
}
function getLetter(val) {
let speakerNum = parseInt(val.replace("SPEAKER_",""));
return String.fromCharCode(65 + speakerNum); // 'A' == UTF-16 65
}
function toggleEditMode(skipDualModeToggle) { // toggles edit panel and redraws regions with resize handles
if (gs.variables.allowEditing === '1') {
toggleEditPanel();
updateRegionEditPanel();
reloadChapterList();
}
}
function toggleVersionDropdown(e) {
e.stopPropagation();
if (versionSelectMenu.classList.contains("visible")) {
e.target.style.display = 'inline';
versionSelectMenu.classList.remove("visible");
}
else {
e.target.style.display = 'none';
versionSelectMenu.classList.add("visible");
versionSelectMenu.style.top = "2rem";
versionSelectMenu.style.height = wave.clientHeight + wavesurfer.timeline.container.clientHeight + document.getElementById("audio-toolbar").clientHeight - 6 + "px";
if (e.target.parentElement.id.includes("top")) versionSelectMenu.classList.add("versionTop");
else versionSelectMenu.classList.remove("versionTop");
for (version of versionSelectMenu.children) { // handle disabling of regions if being viewed
if (selectedVersions.includes(version.id) || selectedVersions.includes(version.innerText)) version.classList.add('disabled');
else version.classList.remove('disabled');
}
}
}
function toggleEditPanel() { // show & hide edit panel
removeCurrentRegion();
hoverSpeaker.innerHTML = "";
if (editPanel.style.height == "0px") {
if (chapters.style.height == "0px") toggleChapters(); // expands chapter panel
editPanel.style.height = "30vh";
editPanel.style.padding = "0.5rem";
setRegionEditMode(true);
} else {
editPanel.style.height = "0px";
editPanel.style.padding = "0px";
setRegionEditMode(false);
}
}
function setRegionEditMode(state) {
editMode = state;
chapters.innerHTML = '';
$('.wavesurfer-region').hide();
reloadRegionsAndChapters(); // editMode sets drag/resize property when regions are redrawn
}
/**
* Handles the edit of region start time, stop time, or speaker name, updating the speaker set
* @param {object} region Region that has been updated
*/
function handleRegionEdit(region, e) {
if (region.element.classList.contains("region-bottom")) { currSpeakerSet = secondarySet; swapCarets(false) }
else { currSpeakerSet = primarySet; swapCarets(true) }
editsMade = true;
currentRegion = region;
wavesurfer.backend.seekTo(region.start);
let regionIndex = getCurrentRegionIndex();
currentRegion.speaker = currSpeakerSet.tempSpeakerObjects[regionIndex].speaker;
currSpeakerSet.tempSpeakerObjects[regionIndex].region = region;
currSpeakerSet.tempSpeakerObjects[regionIndex].start = region.start;
currSpeakerSet.tempSpeakerObjects[regionIndex].end = region.end;
var chaps = chapters.childNodes; // chapter list
chaps[regionIndex].childNodes[1].textContent = minutize(region.start) + " - " + minutize(region.end) + "s"; // update chapter item time
currSpeakerSet.tempSpeakerObjects[regionIndex].region.update({start: region.start, end: region.end}); // update start/end
handleSameSpeakerOverlap(getCurrentRegionIndex(), currSpeakerSet); // recalculate index in case start pos has changed
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dragdrop", getCurrentRegionIndex());
editLockedRegion(currSpeakerSet.tempSpeakerObjects[regionIndex], chaps);
editPanel.click(); // fixes buttons needing to be clicked twice (unknown cause!)
}
/**
* Shows popup to ensure user is aware they are editing a locked region
* @param {object} region Region that is being edited
*/
function editLockedRegion(region) { // ensures user is aware region being edited is locked
if (region.locked) {
let confirm = false;
confirm = window.confirm("Editing a locked region will unlock it, are you sure you want to continue?");
if (!confirm) undo(); // undo change if no
else { // remove lock if yes
region.locked = false;
if (region.region && region.region.element.firstChild) region.region.element.firstChild.remove(); // remove region padlock
if (chapters.childNodes[getCurrentRegionIndex()] && chapters.childNodes[getCurrentRegionIndex()].childNodes[1].tagName === "IMG") {
chapters.childNodes[getCurrentRegionIndex()].childNodes[1].classList.add('hide'); // remove chapter padlock
}
}
}
}
/**
* Merges same-speaker regions with overlapping bounds
* @param {int} regionIdx Index of dragged/edited region
* @param {object} speakerSet Speaker set dragged region exists in
* @param {boolean} skipCurrentRegionUpdate Whether or not to skip the updating of current region
*/
function handleSameSpeakerOverlap(regionIdx, speakerSet, skipCurrentRegionUpdate) {
let draggedRegion = speakerSet.tempSpeakerObjects[regionIdx]; // regionIdx may point to a different region within the for-loop after adjustments, so defined here
let draggedRegionSpeaker = draggedRegion.speaker;
for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) {
if (speakerSet.tempSpeakerObjects[i].speaker === draggedRegionSpeaker && !regionsMatch(draggedRegion, speakerSet.tempSpeakerObjects[i])) { // ensure speaker name match
if (parseFloat(speakerSet.tempSpeakerObjects[i].start) <= parseFloat(draggedRegion.end) && parseFloat(draggedRegion.start) <= parseFloat(speakerSet.tempSpeakerObjects[i].end)) { // ensure overlap
draggedRegion.start = Math.min(speakerSet.tempSpeakerObjects[i].start, draggedRegion.start);
draggedRegion.end = Math.max(speakerSet.tempSpeakerObjects[i].end, draggedRegion.end);
draggedRegion.region.update({start: Math.min(speakerSet.tempSpeakerObjects[i].start, draggedRegion.start), end: Math.max(speakerSet.tempSpeakerObjects[i].end, draggedRegion.end)});
if (!skipCurrentRegionUpdate) currentRegion = draggedRegion;
speakerSet.tempSpeakerObjects[i].region.remove();
speakerSet.tempSpeakerObjects.splice(i, 1); // remove consumed region
setInputInSeconds(startTimeInput, draggedRegion.region.start); // update number inputs
setInputInSeconds(endTimeInput, draggedRegion.region.end);
i = -1; // reset for loop to support multiple consumptions
}
}
}
for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) { // remove duplicates
if (speakerSet.tempSpeakerObjects[i] && speakerSet.tempSpeakerObjects[i+1]) {
if (regionsMatch(speakerSet.tempSpeakerObjects[i], speakerSet.tempSpeakerObjects[i+1])) {
speakerSet.tempSpeakerObjects[i+1].region.remove();
speakerSet.tempSpeakerObjects.splice(i+1, 1); // remove consumed region
i--;
}
}
}
}
/**
* Updates the edit panel elements based on various editing states
*/
function updateRegionEditPanel() {
if (currentRegion && currentRegion.speaker == "") {
removeButton.classList.add("disabled");
speakerInput.classList.add("disabled");
changeAllCheckbox.classList.add("disabled");
changeAllCheckbox.disabled = true;
disableStartEndInputs();
speakerInput.readOnly = true;
speakerInput.value = "";
} else {
removeButton.classList.remove("disabled");
speakerInput.classList.remove("disabled");
changeAllCheckbox.classList.remove("disabled");
if (!isZooming) changeAllCheckbox.disabled = false;
enableStartEndInputs();
speakerInput.readOnly = false;
}
if (editsMade) {
discardButton.classList.remove("disabled");
saveButton.classList.remove("disabled");
} else {
discardButton.classList.add("disabled");
saveButton.classList.add("disabled");
}
if (changeAllCheckbox.checked) {
// changeAllLabel.innerHTML = "Change all (x" + currentRegions.length + ")";
disableStartEndInputs();
}
if (currentRegion && currentRegion.speaker != "") {
speakerInput.value = currentRegion.speaker;
setInputInSeconds(startTimeInput, currentRegion.start);
setInputInSeconds(endTimeInput, currentRegion.end);
}
if (undoLevel - 1 < 0) undoButton.classList.add("disabled");
else undoButton.classList.remove("disabled");
if (undoLevel + 1 >= undoStates.length) redoButton.classList.add("disabled");
else redoButton.classList.remove("disabled");
}
/**
* Adds a new region to the waveform at the current caret location with the speaker name "NEW_SPEAKER"
*/
function createNewRegion() { // adds a new region to the waveform
clearChapterSearch();
var speaker = "NEW_SPEAKER"; // default name
if (!currSpeakerSet.uniqueSpeakers.includes(speaker)) { currSpeakerSet.uniqueSpeakers.push(speaker) }
var start = newRegionOffset + wavesurfer.getCurrentTime();
var end = newRegionOffset + wavesurfer.getCurrentTime() + 15;
newRegionOffset += 5; // offset new region if multiple new regions are created.
currSpeakerSet.tempSpeakerObjects.push({speaker: speaker, start: start, end: end});
editsMade = true;
currentRegions = [];
currentRegion = getRegionFromProps({speaker: speaker, start: start, end: end});
reloadRegionsAndChapters();
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "create", getCurrentRegionIndex());
}
function getRegionFromProps(props, speakerSet) { // find region using speaker, start & end time
if (!speakerSet) speakerSet = currSpeakerSet;
for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) {
if (speakerSet.tempSpeakerObjects[i].speaker === props.speaker && speakerSet.tempSpeakerObjects[i].start === props.start && speakerSet.tempSpeakerObjects[i].end === props.end) {
return speakerSet.tempSpeakerObjects[i];
}
}
console.log("getRegionFromProps failed to find matching region");
}
/**
* Removes the currently selected region or regions
*/
function removeRegion() {
if (!removeButton.classList.contains("disabled")) {
if (getCurrentRegionIndex() != -1) { // if currentRegion has been set
let currentRegionIndex = getCurrentRegionIndex();
let currentRegionIndexes = getCurrentRegionsIndexes();
let lockTemplate = { locked: currSpeakerSet.tempSpeakerObjects[currentRegionIndex].locked };
for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) {
currSpeakerSet.tempSpeakerObjects[i].region.remove();
currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove from tempSpeakerObjects
editsMade = true;
if (i >= 0) i--; // decrement index for side-by-side regions
if (!changeAllCheckbox.checked && currentRegions.length < 1) {
removeCurrentRegion();
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex);
updateRegionEditPanel();
reloadChapterList();
editLockedRegion(lockTemplate);
return; // jump out of function
}
} else if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i])) {
currSpeakerSet.tempSpeakerObjects[i].region.remove();
currSpeakerSet.tempSpeakerObjects.splice(i, 1);
if (i >= 0) i--;
}
}
removeCurrentRegion();
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed
updateRegionEditPanel();
reloadChapterList();
editLockedRegion(lockTemplate);
} else { console.log("no region selected") }
}
}
function regionsMatch(reg1, reg2) {
if (reg1 && reg2 && reg1.start == reg2.start && reg1.end == reg2.end) return true;
return false;
}
function isCurrentRegion(region) {
if (regionsMatch(currentRegion, region)) return true;
return false;
}
function isInCurrentRegions(region) {
if (currentRegions != []) {
for (let i = 0; i < currentRegions.length; i++) {
if (currentRegions[i].start == region.start && currentRegions[i].end == region.end) {
return true;
}
}
}
return false;
}
function getCurrentRegionIndex() { // returns the index of currently selected region
for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { return i }
}
return -1;
}
function getCurrentRegionsIndexes() { // returns the indexes of currently selected regions
let indexes = [];
for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i].region)) { indexes.push(i) }
}
return indexes;
}
function removeCurrentRegion() { // removes current region, regions and bound markers
currentRegion = {speaker: '', start: '', end: ''};
currentRegions = [];
removeRegionBounds();
hoverSpeaker.innerHTML = "";
}
function getRegionsWithSpeaker(speaker) { // returns all regions with the given speaker name
let out = [];
for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
if (currSpeakerSet.tempSpeakerObjects[i].speaker === speaker) { out.push(currSpeakerSet.tempSpeakerObjects[i]) }
}
return out;
}
function sortSpeakerObjectsByStart(speakerOb) { // sorts the speaker object array by start time
return speakerOb.sort(function(a,b) {
return a.start - b.start;
});
}
/**
* Changes the associated speaker name of a region, updating the speaker set
*/
function speakerChange() {
var newSpeaker = speakerInput.value;
clearChapterSearch();
if (newSpeaker && newSpeaker.trim() != "") {
speakerInput.style.outline = "2px solid transparent";
if (getCurrentRegionIndex() != -1) { // if a region is selected
var chaps = chapters.childNodes;
if (!currSpeakerSet.uniqueSpeakers.includes(newSpeaker)) { currSpeakerSet.uniqueSpeakers.push(newSpeaker) }
if (currentRegions && currentRegions.length < 1) { // single change
currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].speaker = newSpeaker; // update corrosponding speakerObject speaker
currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.attributes.label.innerText = newSpeaker;
chaps[getCurrentRegionIndex()].firstChild.textContent = newSpeaker; // update chapter text
} else if (currentRegions && currentRegions.length > 1) { // multiple changes
for (idx of getCurrentRegionsIndexes()) {
currSpeakerSet.tempSpeakerObjects[idx].speaker = newSpeaker;
currSpeakerSet.tempSpeakerObjects[idx].region.attributes.label.innerText = newSpeaker;
chaps[idx].firstChild.textContent = newSpeaker;
}
}
currentRegion.speaker = newSpeaker;
chapterLeave(getCurrentRegionIndex()); // update region bound text
editsMade = true;
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "speaker-change", getCurrentRegionIndex(), getCurrentRegionsIndexes());
editLockedRegion(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()]);
} else { console.log("no region selected") }
} else { console.log("no text in speaker input"); speakerInput.style.outline = "2px solid firebrick"; }
}
function speakerInputUnfocused() {
prevUndoState = "";
if (speakerInput.value == "" && !speakerInput.classList.contains("disabled")) {
speakerInput.style.outline = "2px solid firebrick";
window.alert("Speaker input cannot be left empty. Please enter a speaker name.");
setTimeout(() => speakerInput.focus(), 10); // timeout needed otherwise input isn't selected
} else speakerInput.style.outline = "2px transparent";
}
/**
* Selects all (or reverts select-all) regions matching any of the currently selected speaker names
* @param {boolean} skipUndoState Whether or not to skip the addition of an undo state
*/
function selectAllCheckboxChanged(skipUndoState) { // "Change all" toggled
if (changeAllCheckbox.checked) {
if (!isZooming) {
tempZoomSave = zoomSlider.value;
zoomTo(0); // zoom out to encompass all selected regions
}
let uniqueSelectedSpeakers;
if (currentRegions && currentRegions.length > 0) { // if more than one region selected
uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
} else uniqueSelectedSpeakers = [currentRegion.speaker];
currentRegions = [];
for (var speaker of uniqueSelectedSpeakers) {
for (var region of getRegionsWithSpeaker(speaker)) {
currentRegions.push(region);
region.region.update({color: "rgba(255,50,50,0.5)"});
}
}
} else {
if (!isZooming) {
zoomTo(tempZoomSave / 4); // zoom back in to previous level
}
currentRegions = []; // this will lose track of previously selected region*s*
}
reloadRegionsAndChapters();
if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "selectAllChange", getCurrentRegionIndex(), getCurrentRegionsIndexes());
}
function enableStartEndInputs() { // removes the 'disabled' tag from all time inputs
for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = false }
for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = false }
}
function disableStartEndInputs() { // adds the 'disabled' tag to all time inputs
for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].value = 0; startTimeInput.childNodes[idx].disabled = true; }
for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].value = 0; endTimeInput.childNodes[idx].disabled = true; }
}
/**
* Zooms wavesurfer waveform to destination zoom level, used in select all function
* @param {number} dest Destination zoom level
*/
function zoomTo(dest) {
isZooming = true;
changeAllCheckbox.disabled = true;
let isOut = false;
if (dest == 0) isOut = true;
zoomInterval = setInterval(() => {
if (isOut) {
if (zoomSlider.value != 0) {
if (zoomSlider.value > 50) zoomSlider.value -= 30; // ramp up for finer adjustments
else zoomSlider.stepDown();
wavesurfer.zoom(zoomSlider.value / 4);
} else {
clearInterval(zoomInterval);
isZooming = false;
changeAllCheckbox.disabled = false;
zoomSlider.dispatchEvent(new Event("input"));
}
} else {
if (zoomSlider.value / 4 < dest) {
if (zoomSlider.value > 50) zoomSlider.value += 30; // ramp up for finer adjustments
else zoomSlider.stepUp();
wavesurfer.zoom(zoomSlider.value / 4);
} else {
clearInterval(zoomInterval);
isZooming = false;
changeAllCheckbox.disabled = false;
zoomSlider.dispatchEvent(new Event("input"));
}
}
}, 10); // 10ms interval
}
function toggleSavePopup() { // shows / hides commit popup div
savePopupCommitMsg.value = savePopupCommitMsg.value.trim(); // clears initial whitespace caused by
if (savePopup.classList.contains("visible")) {
savePopup.classList.remove("visible");
savePopupBG.classList.remove("visible");
} else {
savePopup.classList.add("visible");
savePopupBG.classList.add("visible");
savePopup.children[0].innerText = "Commit changes for: " + selectedVersions[(!dualMode || primaryCaret.src.includes("fill")) ? 0 : 1];
}
}
function saveRegionChanges() { // saves tempSpeakerObjects to speakerObjects
if (!saveButton.classList.contains("disabled")) {
toggleSavePopup();
// old save functionality
// currSpeakerSet.speakerObjects = cloneSpeakerObjectArray(currSpeakerSet.tempSpeakerObjects);
// editsMade = false;
// removeCurrentRegion();
// reloadRegionsAndChapters();
// console.log("saved changes.");
}
}
/**
* Commits changes made to the currently selected set to Greenstone's version history system.
* Firstly increments FLDV, then saves commit message to document's metadata, then sets document's
* associated file to tempSpeakerObjects CSV.
*/
function commitChanges() {
if (savePopupCommitMsg.value && savePopupCommitMsg.value.length > 0) {
console.log('committing with message: ' + savePopupCommitMsg.value);
// inc fldv_history
$.ajax({
type: "GET",
url: mod_meta_base_url,
data: { "o": "json", "s1.a": "inc-fldv-nminus1" }
}).then((out) => {
console.log('fldv inc success with status code: ' + out.page.pageResponse.status.code);
if (out.page.pageResponse.status.code == GSSTATUS_SUCCESS) {
ajaxSetCommitMeta();
}
}, (error) => { console.log("inc-fldv-nminus1 error:\n" + error) });
toggleSavePopup();
} else {
window.alert("Commit message cannot be left empty.");
}
}
function ajaxSetCommitMeta() { // saves commit message to current document's metadata
$.ajax({
type: "GET",
url: mod_meta_base_url,
data: { "o" : "json", "s1.a": "set-archives-metadata", "s1.metaname": "commitmessage", "s1.metavalue": savePopupCommitMsg.value.trim(), "s1.metamode": "override" },
}).then((out) => {
console.log('commit success with status code: ' + out.page.pageResponse.status.code);
if (out.page.pageResponse.status.code == GSSTATUS_SUCCESS) {
ajaxSetAssocFile();
}
}, (error) => { console.log("commit_msg_url error:"); console.log(error); });
}
function ajaxSetAssocFile() { // sets current document's associated file to tempSpeakerObjects
$.ajax({
type: "POST",
url: gs.xsltParams.library_name,
data: { "o" : "json", "a": "g", "rt": "r", "ro": "0", "s": "ModifyMetadata", "s1.collection": gs.cgiParams.c, "s1.site": gs.xsltParams.site_name, "s1.d": gs.cgiParams.d,
"s1.a": "set-archives-assocfile", "s1.assocname": "structured-audio.csv", "s1.filedata": speakerObjToCSVText() },
}).then((out) => {
console.log('set-archives-assocfile success with status code: ' + out.page.pageResponse.status.code);
resetUndoStates();
}, (error) => { console.log("set_assoc_url error:"); console.log(error); });
}
function speakerObjToCSVText() { // converts tempSpeakerObjects to csv-like string
console.log(currSpeakerSet.tempSpeakerObjects.map(item => [item.speaker, item.start, item.end, item.locked]).join('\n'));
return currSpeakerSet.tempSpeakerObjects.map(item => [item.speaker, item.start, item.end, item.locked]).join('\n');
}
function discardRegionChanges(forceDiscard) { // resets tempSpeakerObjects to speakerObjects
if (!discardButton.classList.contains("disabled") || forceDiscard) {
let confirm = false;
if (!forceDiscard) { confirm = window.confirm("Are you sure you want to discard changes?"); }
if (confirm || forceDiscard) {
currSpeakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(currSpeakerSet.speakerObjects);
editsMade = false;
removeCurrentRegion();
resetUndoStates();
reloadRegionsAndChapters();
console.log("discarded changes");
}
}
}
/**
* Redraws edit panel, chapter list and wavesurfer regions from speaker set
*/
function reloadRegionsAndChapters() { // redraws edit panel, chapter list, wavesurfer regions
updateRegionEditPanel();
$(".region-top").remove();
$(".region-bottom").remove();
$(".wavesurfer-region").remove();
populateChaptersAndRegions(primarySet);
if (dualMode) {
populateChaptersAndRegions(secondarySet);
currSpeakerSet = primarySet;
}
updateCurrSpeakerSet();
if (editMode && currentRegion && currentRegion.speaker && getCurrentRegionIndex() != -1 && currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element) {
setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
drawCurrentRegionBounds();
}
if (currentRegions.length < 1) {
removeButton.innerHTML = "Remove Selected Region";
// enableStartEndInputs();
} else {
removeButton.innerHTML = "Remove Selected Regions (x" + currentRegions.length + ")";
var uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
speakerInput.value = uniqueSelectedSpeakers.join(", ");
}
}
/**
* Handles the change of a region's start or end time, updating hte speaker set
*/
function changeStartEndTime(e) { // start/end time input handler
let newStart = getTimeInSecondsFromInput(startTimeInput);
let newEnd = getTimeInSecondsFromInput(endTimeInput);
let duration = Math.floor(wavesurfer.getDuration()); // total duration of current audio
if (getCurrentRegionIndex() != -1) { // if there is a selected region
if (newEnd <= newStart) newStart = newEnd - 1; // when start time > end time, push region forward
if (newEnd <= 0) newEnd = 1;
if (newStart < 0) newStart = 0; // ensures region start doesn't go < 0s
if (newEnd > duration) newEnd = duration; // ensures region start doesn't go > duration
setInputInSeconds(startTimeInput, newStart);
setInputInSeconds(endTimeInput, newEnd);
let currRegIdx = getCurrentRegionIndex();
currSpeakerSet.tempSpeakerObjects[currRegIdx].start = newStart;
currSpeakerSet.tempSpeakerObjects[currRegIdx].end = newEnd;
currSpeakerSet.tempSpeakerObjects[currRegIdx].region.update({start: newStart, end: newEnd});
currentRegion.start = newStart;
currentRegion.end = newEnd;
editsMade = true;
handleSameSpeakerOverlap(currRegIdx, currSpeakerSet);
addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "change-time", getCurrentRegionIndex());
editLockedRegion(currSpeakerSet.tempSpeakerObjects[currRegIdx]);
} else {
console.log("no region selected");
setInputInSeconds(startTimeInput, 0);
setInputInSeconds(endTimeInput, 0);
}
}
/**
* Calculates time in seconds of start or end time input group
* @param {element} input Element of time input groups: hh:mm:ss
* @returns {int} Time in seconds
*/
function getTimeInSecondsFromInput(input) {
let hours = input.children[0].valueAsNumber;
let mins = input.children[1].valueAsNumber;
let secs = input.children[2].valueAsNumber;
return (hours * 3600) + (mins * 60) + secs;
}
/**
* Sets the start or end time element group inputs
* @param {element} input Element of time input group to be updated
* @param {int} seconds Duration in seconds to be converted into hh:mm:ss
*/
function setInputInSeconds(input, seconds) { // sets start or end input time when given seconds
let date = new Date(null);
date.setMilliseconds(seconds * 1000);
input.children[0].value = date.getHours() % 12;
input.children[1].value = date.getMinutes();
input.children[2].value = date.getSeconds() + "." + (Math.ceil(date.getMilliseconds() / 100) * 100);
document.querySelectorAll('input[type=number]').forEach(e => {
e.value = Math.round(e.valueAsNumber * 10) / 10; // to 1dp
if (e.classList.contains("seconds") && !e.value.includes(".")) { e.value = e.value + ".0"; }
else if (e.value.length === 1){ e.value = '0' + e.value; }// 0 padded on left
});
}
/**
* Adds a new undo state to the global undo state list
* @param {object} state Primary set at current state
* @param {object} secState Secondary set at current state
* @param {boolean} isSec Whether or not current change was made to primary (false) or secondary (true) set
* @param {boolean} dualMode Whether or not audio editor was in dual mode when undo state was added
* @param {string} type Type of change e.g "remove", "speaker-change"
* @param {int} currRegIdx Index of currently selected region (for restoration)
* @param {Array} currRegIdxs Index of currently selected regions, if applicable (for restoration)
*/
function addUndoState(state, secState, isSec, dualMode, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack
let newState = cloneSpeakerObjectArray(state.tempSpeakerObjects); // clone method removes references
let newSecState = cloneSpeakerObjectArray(secState.tempSpeakerObjects); // clone method removes references
let changedTrack = (type == "dualModeChange" || type == "selectAllChange") ? "none" : selectedVersions[isSec ? 1 : 0] // sets changedTrack to version name of edited region set
undoButton.classList.remove("disabled");
undoStates = undoStates.slice(0, undoLevel + 1); // trim to current level if undos have already been made
undoStates.push({state: newState, secState: newSecState, isSec: isSec, changedTrack: changedTrack, dualMode: dualMode, currentRegionIndex: currRegIdx, currentRegionIndexes: currRegIdxs, type: type});
if ((type === "change-time" && prevUndoState === "change-time") || (type === "speaker-change" && prevUndoState === "speaker-change")) { // checks if similar change was made previously
undoStates.splice(-2, 1); // remove second-to-last item in undoStates stack (merge last two changes into one to avoid multiple small edits)
prevUndoState = type;
} else undoLevel++;
prevUndoState = type;
redoButton.classList.add("disabled");
for (var item of undoStates) { // remove cyclic object references
item.state = cloneSpeakerObjectArray(item.state);
item.secState = cloneSpeakerObjectArray(item.secState);
}
localStorage.setItem('undoStates', JSON.stringify(undoStates)); // update localStorage items
localStorage.setItem('undoLevel', undoLevel);
}
/**
* Returns to the previous state in the undo state list
*/
function undo() {
if (!undoButton.classList.contains("disabled") && editMode) { // ensure there exist states to undo to
clearChapterSearch();
if (undoLevel - 1 < 0) console.log("ran out of undos");
else {
removeCurrentRegion();
let adjustedUndoLevel = undoLevel-1;
if (undoStates[undoLevel].type == "dualModeChange") { // toggle dual mode
dualModeChanged(true);
} else if (undoStates[undoLevel].type == "selectAllChange") { // toggle select all
changeAllCheckbox.checked = !changeAllCheckbox.checked;
selectAllCheckboxChanged(true);
} else {
primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].state.slice(0)); // slice & clone removes potential references between arrays
if (dualMode && undoStates[adjustedUndoLevel].secState && undoStates[adjustedUndoLevel].secState.length > 0) { // if secondary undoState exists
secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].secState.slice(0)); // slice & clone removes potential references between arrays
}
let selectedSpeakerSet;
// handle currentRegion change
if (undoStates[undoLevel] && undoStates[undoLevel].type && undoStates[undoLevel].type == "remove") { // if destination state type is remove
selectedSpeakerSet = (undoStates[undoLevel].isSec) ? secondarySet : primarySet;
if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
else caretClicked("primary-caret");
currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex]; // restore previous current state
} else if (undoStates[undoLevel].currentRegionIndex) {
if (!dualMode) selectedSpeakerSet = primarySet;
else {
selectedSpeakerSet = (undoStates[undoLevel-1].isSec) ? secondarySet : primarySet;
if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
else caretClicked("primary-caret");
}
currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex];
}
// handle currentRegions restoration
if (undoStates[undoLevel].currentRegionIndexes && undoStates[undoLevel].currentRegionIndexes.length > 1) {
for (var idx of undoStates[undoLevel].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
}
}
editsMade = true;
undoLevel--; // decrement undoLevel
reloadRegionsAndChapters();
localStorage.setItem('undoLevel', undoLevel);
if (undoLevel - 1 < 0) undoButton.classList.add("disabled");
else undoButton.classList.remove("disabled");
}
if (undoLevel < undoStates.length) redoButton.classList.remove("disabled");
}
}
/**
* Moves forward one state in the undo state list
*/
function redo() {
if (!redoButton.classList.contains("disabled") && editMode) { // ensure there exist states to redo to
clearChapterSearch();
if (undoLevel + 1 >= undoStates.length) console.log("ran out of redos");
else {
if (undoStates[undoLevel+1].type == "dualModeChange") { // toggle dual mode
dualModeChanged(true);
} else if (undoStates[undoLevel+1].type == "selectAllChange") { // toggle select all
changeAllCheckbox.checked = !changeAllCheckbox.checked;
selectAllCheckboxChanged(true);
} else {
primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].state.slice(0)); // set primary to new state
secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].secState.slice(0)); // set secondary to new state
let selectedSpeakerSet;
// handle currentRegion change
removeCurrentRegion();
if (undoLevel+2 < undoStates.length) {
if (undoStates[undoLevel+2] && undoStates[undoLevel+2].type && undoStates[undoLevel+2].type == "remove") {
selectedSpeakerSet = (undoStates[undoLevel+2].isSec) ? secondarySet : primarySet;
if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
else caretClicked("primary-caret");
currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+2].currentRegionIndex];
} else {
selectedSpeakerSet = (undoStates[undoLevel+1].isSec) ? secondarySet : primarySet;
if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
else caretClicked("primary-caret");
currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+1].currentRegionIndex];
}
if (undoStates[undoLevel+1].currentRegionIndexes && undoStates[undoLevel+1].currentRegionIndexes.length > 1) {
for (var idx of undoStates[undoLevel+1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
}
}
}
editsMade = true;
reloadRegionsAndChapters();
undoLevel++; // increment undoLevel
localStorage.setItem('undoLevel', undoLevel);
if (undoLevel + 1 > undoStates.length - 1) redoButton.classList.add("disabled");
else redoButton.classList.remove("disabled");
}
if (undoLevel < undoStates.length) undoButton.classList.remove("disabled");
}
}
function resetUndoStates() { // clear undo history
undoStates = [{state: cloneSpeakerObjectArray(primarySet.tempSpeakerObjects), secState: cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects)}];
undoLevel = 0;
localStorage.removeItem('undoLevel');
localStorage.removeItem('undoStates');
undoButton.classList.add("disabled");
redoButton.classList.add("disabled");
}
function waveformScrolled() { // waveform scroll handler
if (currentRegion.speaker && getCurrentRegionIndex() != -1) { // updates region bound markers if selected region exists
setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
drawCurrentRegionBounds();
}
if (document.getElementById('new-canvas')) { document.getElementById('new-canvas').style.left = "-" + wave.scrollLeft + 'px' } // update placeholder waveform scroll position
}
function drawCurrentRegionBounds() { // draws bounds of current region
removeRegionBounds();
let currIndexes = getCurrentRegionsIndexes();
if (getCurrentRegionIndex() != -1) drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, wave.scrollLeft, "FireBrick");
for (let i = 0; i < currIndexes.length; i++) {
drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, wave.scrollLeft, "FireBrick");
}
}
/**
* Draws bounding 'n' above hovered or selected region
* @param {object} region Region to have bound drawn for
* @param {number} scrollPos Scroll position of div, used to offset draw position
* @param {string} colour Colour to draw bound (black and FireBrick are used)
*/
function drawRegionBounds(region, scrollPos, colour) { // draws on canvas to show bounds of hovered/selected region
var hoverSpeakerCanvas = document.createElement("canvas");
hoverSpeakerCanvas.id = "hover-speaker-canvas";
hoverSpeakerCanvas.classList.add("region-bounds");
hoverSpeakerCanvas.width = audioContainer.clientWidth; // max width of drawn bounds
var ctx = hoverSpeakerCanvas.getContext("2d");
// ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency
ctx.lineWidth = 1;
if (colour == "FireBrick") ctx.lineWidth = 3;
if (currentRegions && currentRegions.length < 1 && isCurrentRegion(region) && editMode) {
colour = "FireBrick";
ctx.lineWidth = 3;
}
ctx.strokeStyle = colour;
ctx.beginPath();
ctx.moveTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 28);
ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 20);
ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 20);
ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 28);
ctx.stroke();
audioContainer.prepend(hoverSpeakerCanvas);
}
function removeRegionBounds() { // remove all region bound markers
let canvases = document.getElementsByClassName('region-bounds');
while (canvases[0]) canvases[0].parentNode.removeChild(canvases[0]);
}
function updateCurrSpeakerSet() { // updates 'currSpeakerSet' var
if (primaryCaret.src.includes("fill")) currSpeakerSet = primarySet;
else if (secondaryCaret.src.includes("fill")) currSpeakerSet = secondarySet;
}
function cloneSpeakerObjectArray(inputArray) { // clones speakerObjectArray without references (wavesurfer regions)
let output = [];
for (let i = 0; i < inputArray.length; i++) {
output.push({ speaker: inputArray[i].speaker, start: inputArray[i].start, end: inputArray[i].end, locked: (inputArray[i].locked === "true" || inputArray[i].locked === true) });
}
return output;
}
function flashChapters() { // flashes chapters a lighter colour momentarily to indicate an update/change
chapters.style.backgroundColor = "rgb(66, 84, 88)";
setTimeout(() => { chapters.style.backgroundColor = "rgb(40, 54, 58)" }, 500);
}
/** Fullscreen onChange handler, increases waveform height & adjusts padding/margin */
function fullscreenChanged() {
if (!audioContainer.classList.contains("fullscreen")) {
audioContainer.classList.add("fullscreen");
wavesurfer.setHeight(175); // increase waveform height
caretContainer.style.paddingLeft = "2rem";
caretContainer.style.height = wavesurfer.getHeight() + "px"; // set height to waveform height
audioContainer.prepend(caretContainer); // attach to audioContainer (otherwise doesn't show due to AC being fullscreen)
} else {
audioContainer.classList.remove("fullscreen");
wavesurfer.setHeight(140);
caretContainer.style.paddingLeft = "0";
caretContainer.style.height = wavesurfer.getHeight() + "px";
audioContainer.parentElement.prepend(caretContainer); // move back up in DOM hierarchy
}
setTimeout(() => { // ensures waveform shows
zoomOutButton.click();
zoomInButton.click();
}, 250);
}
/** Enables / disables the fullscreen view of audio player / editor */
function toggleFullscreen() {
if ((document.fullscreenElement && document.fullscreenElement !== null) ||
(document.webkitFullscreenElement && document.webkitFullscreenElement !== null) ||
(document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
(document.msFullscreenElement && document.msFullscreenElement !== null)) {
document.exitFullscreen();
} else {
if (audioContainer.requestFullscreen) {
audioContainer.requestFullscreen();
} else if (audioContainer.webkitRequestFullscreen) { /* Safari */
audioContainer.webkitRequestFullscreen();
} else if (audioContainer.msRequestFullscreen) { /* IE11 */
audioContainer.msRequestFullscreen();
}
}
}
}
/**
* Formats seconds to hh:mm:ss
* @param {number} duration
* @returns {string} Time in hh:mm:ss format
*/
function formatAudioDuration(duration) {
// console.log('duration: ' + duration);
let [hrs, mins, secs, ms] = duration.replace(".", ":").split(":");
return hrs + ":" + mins + ":" + secs;
}