diff --git a/js/ui.js b/js/ui.js index ab9d02b2..1f6996e0 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1041,6 +1041,8 @@ exploreLink.addEventListener('click', async () => { enableMenuItem(['saveCSV']); adjustSpecDims(true) worker.postMessage({ action: 'update-state', globalOffset: 0, filteredOffset: {}}); + // Analysis is done + STATE.analysisDone = true; filterResults({species: undefined, range: STATE.explore.range}); resetResults({clearSummary: true, clearPagination: true, clearResults: true}); }); @@ -1485,7 +1487,8 @@ window.onload = async () => { chirpityOnly.forEach(element => element.classList.remove('d-none')); // Remove GPU option on Mac isMac && noMac.forEach(element => element.classList.add('d-none')); - DOM.contextAware.checked = config.detect.contextAware + DOM.contextAware.checked = config.detect.contextAware; + DOM.localSwitchContainer.classList.remove('d-none'); SNRSlider.disabled = false; } contextAwareIconDisplay(); @@ -2792,12 +2795,13 @@ function onChartData(args) { /* onResultsComplete is called when the last result is sent */ - function onResultsComplete({active = undefined} = {}){ + function onResultsComplete({active = undefined, select = undefined} = {}){ let table = document.getElementById('resultTableBody'); table.replaceWith(resultsBuffer); table = document.getElementById('resultTableBody'); PREDICTING = false; // Set active Row + if (active) { // Refresh node and scroll to active row: activeRow = table.rows[active]; @@ -2809,6 +2813,9 @@ function onChartData(args) { } else { activeRow.classList.add('table-active'); } + } else if (select) { + const row = getRowFromStart(table, select) + activeRow = table.rows[row]; } else { // if (STATE.mode === 'analyse') { activeRow = table.querySelector('.table-active'); @@ -2828,6 +2835,22 @@ function onChartData(args) { DOM.progressDiv.classList.add('d-none'); } + + function getRowFromStart(table, start){ + for (var i = 0; i < table.rows.length; i++) { + const row = table.rows[i]; + + // Get the value of the name attribute and split it on '|' + const nameValue = row.getAttribute('name'); + // State time is the second value in the name string + const startTime = nameValue?.split('|')[1]; + + // Check if the second value matches the 'select' variable + if (parseFloat(startTime) === start) { + return i; + } + } + } function onAnalysisComplete(){ PREDICTING = false; @@ -2879,6 +2902,10 @@ function onChartData(args) { } const limit = config.limit; const offset = (clicked - 1) * limit; + // Tell the worker about the new offset + const species = isSpeciesViewFiltered(true); + species ? worker.postMessage({action:'update-state', filteredOffset: {[species]: offset} }) : + worker.postMessage({action:'update-state', globalOffset: offset }); filterResults({offset: offset, limit:limit}) resetResults({clearSummary: false, clearPagination: false, clearResults: false}); } @@ -2957,13 +2984,18 @@ function onChartData(args) { }) { let tr = ''; + if (typeof (result) === 'string') { + // const nocturnal = config.detect.nocmig ? 'during the night' : ''; + generateToast({domID:'toastContainer', message: result}); + return + } if (index <= 1) { if (selection) { const selectionTable = document.getElementById('selectionResultTableBody'); selectionTable.textContent = ''; } else { - if (fileLoaded) showElement(['resultTableContainer', 'resultsHead'], false); + showElement(['resultTableContainer', 'resultsHead'], false); const resultTable = document.getElementById('resultTableBody'); resultTable.textContent = '' } @@ -2972,11 +3004,6 @@ function onChartData(args) { } if (!isFromDB && index > config.limit) { return - } - if (typeof (result) === 'string') { - const nocturnal = config.detect.nocmig ? 'during the night' : ''; - generateToast({domID:'toastContainer', message: result}); - //tr += `${result} (${LIST_MAP[config.list]} detected ${nocturnal} with at least ${config.detect.confidence}% confidence in the prediction)`; } else { const { timestamp, @@ -3404,7 +3431,7 @@ function onChartData(args) { } } - function filterResults({species = isSpeciesViewFiltered(true), updateSummary = true, offset = 0, limit = 500, range = undefined} = {}){ + function filterResults({species = isSpeciesViewFiltered(true), updateSummary = true, offset = undefined, limit = 500, range = undefined} = {}){ STATE.analysisDone && worker.postMessage({ action: 'filter', species: species, @@ -4439,7 +4466,8 @@ function track(event, action, name, value){ DBaction: action, batch: batch, confidence: confidence, - active: activeRow?.rowIndex - 1 // have to account for the header row + active: activeRow?.rowIndex - 1, // have to account for the header row + speciesFiltered: isSpeciesViewFiltered(true) }) } diff --git a/js/worker.js b/js/worker.js index 6d0cbae5..ab86c8f4 100644 --- a/js/worker.js +++ b/js/worker.js @@ -619,9 +619,10 @@ const prepSummaryStatement = (included) => { //console.log('Summary SQL statement:\n' + summaryStatement) } - const getTotal = async ({species = undefined, offset = 0, included = []}) => { + const getTotal = async ({species = undefined, offset = undefined, included = []}) => { let params = []; const range = STATE.mode === 'explore' ? STATE.explore.range : undefined; + offset = offset ?? (species !== undefined ? STATE.filteredOffset[species] : STATE.globalOffset); const useRange = range?.start; let SQL = ` WITH MaxConfidencePerDateTime AS ( SELECT confidence, @@ -1846,123 +1847,88 @@ const prepSummaryStatement = (included) => { predictWorkers = []; } - // const insertRecord = async (timestamp, key, speciesID, confidence, file) => { - // const isDaylight = isDuringDaylight(timestamp, STATE.lat, STATE.lon); - // const offset = key * 1000; - // let changes, fileID; - // confidence = Math.round(confidence); - // const db = STATE.db; - // let res = await db.getAsync('SELECT id FROM files WHERE name = ?', file); - // if (!res) { - // res = await db.runAsync('INSERT OR IGNORE INTO files VALUES ( ?,?,?,?,? )', - // undefined, file, metadata[file].duration, metadata[file].fileStart, undefined); - // fileID = res.lastID; - // changes = 1; - // } else { - // fileID = res.id; - // } - // if (changes) { - // const durationSQL = Object.entries(metadata[file].dateDuration) - // .map(entry => `(${entry.toString()},${fileID})`).join(','); - // // No "OR IGNORE" in this statement because it should only run when the file is new - // await db.runAsync(`INSERT OR IGNORE INTO duration VALUES ${durationSQL}`); - // } - // await db.runAsync('INSERT OR REPLACE INTO records VALUES (?,?,?,?,?,?,?,?,?,?)', - // metadata[file].fileStart + offset, key, fileID, speciesID, confidence, - // undefined, undefined, key + 3, undefined, isDaylight); - // } - async function batchInsertRecords(cname, label, files, originalCname) { const db = STATE.db; let params = [originalCname, STATE.detect.confidence]; t0 = Date.now(); - let query = ` - SELECT * FROM records - WHERE speciesID = ( - SELECT id FROM species WHERE cname = ? - ) - AND confidence >= ? `; - if (STATE.mode !== 'explore') { - query += ` - AND fileID in ( - SELECT id FROM files WHERE name IN (${files.map(() => '?').join(', ')}) - )` - params.push(...files); - } else if (STATE.explore.range.start) { - query += ` AND dateTime BETWEEN ${STATE.explore.range.start} AND ${STATE.explore.range.end}`; - } - const records = await STATE.db.allAsync(query, ...params); - const updatedID = db.getAsync('SELECT id FROM species WHERE cname = ?', cname); - let count = 0; - await db.runAsync('BEGIN'); - for (const item of records) { - const { dateTime, speciesID, fileID, position, end, comment, callCount } = item; - const { name } = await STATE.db.getAsync('SELECT name FROM files WHERE id = ?', fileID) - // Delete existing record - const changes = await db.runAsync('DELETE FROM records WHERE datetime = ? AND speciesID = ? AND fileID = ?', dateTime, speciesID, fileID) - count += await onInsertManualRecord({ - cname: cname, - start: position, - end: end, - comment: comment, - count: callCount, - file: name, - label: label, - batch: false, - originalCname: undefined - }) - } - await db.runAsync('END'); - DEBUG && console.log(`Batch record update took ${(Date.now() - t0) / 1000} seconds`) - - } + let query = `SELECT * FROM records WHERE speciesID = (SELECT id FROM species WHERE cname = ?) AND confidence >= ? `; + if (STATE.mode !== 'explore') { + query += ` AND fileID in (SELECT id FROM files WHERE name IN (${files.map(() => '?').join(', ')}))` + params.push(...files); + } else if (STATE.explore.range.start) { + query += ` AND dateTime BETWEEN ${STATE.explore.range.start} AND ${STATE.explore.range.end}`; + } + const records = await STATE.db.allAsync(query, ...params); + const updatedID = db.getAsync('SELECT id FROM species WHERE cname = ?', cname); + let count = 0; + await db.runAsync('BEGIN'); + for (const item of records) { + const { dateTime, speciesID, fileID, position, end, comment, callCount } = item; + const { name } = await STATE.db.getAsync('SELECT name FROM files WHERE id = ?', fileID) + // Delete existing record + const changes = await db.runAsync('DELETE FROM records WHERE datetime = ? AND speciesID = ? AND fileID = ?', dateTime, speciesID, fileID) + count += await onInsertManualRecord({ + cname: cname, + start: position, + end: end, + comment: comment, + count: callCount, + file: name, + label: label, + batch: false, + originalCname: undefined + }) + } + await db.runAsync('END'); + DEBUG && console.log(`Batch record update took ${(Date.now() - t0) / 1000} seconds`) + } - const onInsertManualRecord = async ({ cname, start, end, comment, count, file, label, batch, originalCname, confidence, active }) => { - if (batch) return batchInsertRecords(cname, label, file, originalCname) - start = parseFloat(start), end = parseFloat(end); - const startMilliseconds = Math.round(start * 1000); - let changes, fileID, fileStart; - const db = STATE.db; - const { speciesID } = await db.getAsync(`SELECT id as speciesID FROM species - WHERE cname = ?`, cname); - let res = await db.getAsync(`SELECT id,filestart FROM files WHERE name = ?`, file); - - - if (!res) { - // Manual records can be added off the bat, so there may be no record of the file in either db - fileStart = metadata[file].fileStart; - res = await db.runAsync('INSERT OR IGNORE INTO files VALUES ( ?,?,?,?,? )', - fileID, file, metadata[file].duration, fileStart, undefined); - fileID = res.lastID; - changes = 1; - let durationSQL = Object.entries(metadata[file].dateDuration) - .map(entry => `(${entry.toString()},${fileID})`).join(','); - await db.runAsync(`INSERT OR IGNORE INTO duration VALUES ${durationSQL}`); - } else { - fileID = res.id; - fileStart = res.filestart; - } - - const dateTime = fileStart + startMilliseconds; - const isDaylight = isDuringDaylight(dateTime, STATE.lat, STATE.lon); - confidence = confidence || 2000; - // Delete an existing record if it exists - const result = await db.getAsync(`SELECT id as originalSpeciesID FROM species WHERE cname = ?`, originalCname); - result?.originalSpeciesID && await db.runAsync('DELETE FROM records WHERE datetime = ? AND speciesID = ? AND fileID = ?', dateTime, result.originalSpeciesID, fileID) - const response = await db.runAsync('INSERT OR REPLACE INTO records VALUES ( ?,?,?,?,?,?,?,?,?,?)', - dateTime, start, fileID, speciesID, confidence, label, comment, end, parseInt(count), isDaylight); - - if (response.changes){ - STATE.db === diskDB ? UI.postMessage({ event: 'diskDB-has-records' }) : UI.postMessage({event: 'unsaved-records'}); - } - // WHY NOT USE FILTER DIRECTLY? - UI.postMessage({ - event: 'generate-alert', - // message: `${count} ${args.cname} record has been saved to the archive.`, - filter: true, - active: active - }) - } + const onInsertManualRecord = async ({ cname, start, end, comment, count, file, label, batch, originalCname, confidence, speciesFiltered }) => { + if (batch) return batchInsertRecords(cname, label, file, originalCname) + start = parseFloat(start), end = parseFloat(end); + const startMilliseconds = Math.round(start * 1000); + let changes, fileID, fileStart; + const db = STATE.db; + const { speciesID } = await db.getAsync(`SELECT id as speciesID FROM species WHERE cname = ?`, cname); + let res = await db.getAsync(`SELECT id,filestart FROM files WHERE name = ?`, file); + + if (!res) { + // Manual records can be added off the bat, so there may be no record of the file in either db + fileStart = metadata[file].fileStart; + res = await db.runAsync('INSERT OR IGNORE INTO files VALUES ( ?,?,?,?,? )', + fileID, file, metadata[file].duration, fileStart, undefined); + fileID = res.lastID; + changes = 1; + let durationSQL = Object.entries(metadata[file].dateDuration) + .map(entry => `(${entry.toString()},${fileID})`).join(','); + await db.runAsync(`INSERT OR IGNORE INTO duration VALUES ${durationSQL}`); + } else { + fileID = res.id; + fileStart = res.filestart; + } + + const dateTime = fileStart + startMilliseconds; + const isDaylight = isDuringDaylight(dateTime, STATE.lat, STATE.lon); + confidence = confidence || 2000; + // Delete an existing record if it exists + const result = await db.getAsync(`SELECT id as originalSpeciesID FROM species WHERE cname = ?`, originalCname); + result?.originalSpeciesID && await db.runAsync('DELETE FROM records WHERE datetime = ? AND speciesID = ? AND fileID = ?', dateTime, result.originalSpeciesID, fileID) + const response = await db.runAsync('INSERT OR REPLACE INTO records VALUES ( ?,?,?,?,?,?,?,?,?,?)', + dateTime, start, fileID, speciesID, confidence, label, comment, end, parseInt(count), isDaylight); + + if (response.changes){ + STATE.db === diskDB ? UI.postMessage({ event: 'diskDB-has-records' }) : UI.postMessage({event: 'unsaved-records'}); + } + // WHY NOT USE FILTER DIRECTLY? It's to get species and offset + // UI.postMessage({ + // event: 'generate-alert', + // // message: `${count} ${args.cname} record has been saved to the archive.`, + // filter: true, + // active: active + // }) + await getResults({species:speciesFiltered, select: start}); + await getSummary({species: speciesFiltered}); + } const generateInsertQuery = async (latestResult, file) => { const db = STATE.db; @@ -2063,25 +2029,19 @@ const prepSummaryStatement = (included) => { const fileProgress = predictionsReceived[file] / batchChunksToSend[file]; UI.postMessage({ event: 'progress', progress: progress, file: file }); if (fileProgress === 1) { - if (!STATE.selection) { - db.getAsync('SELECT id FROM files WHERE name = ?', file) - .then(row => { - if (!row) { - const result = `No predictions found in ${file}`; - UI.postMessage({ - event: 'new-result', - file: file, - result: result, - index: index, - selection: STATE.selection - }); - } - }) - .catch(error => console.log('Error generating new result', error)) - } + if (index === 0 ) { + const result = `No detections found in ${file}. Searched for records using the ${STATE.list} list and having a minimum confidence of ${STATE.detect.confidence/10}%` + UI.postMessage({ + event: 'new-result', + file: file, + result: result, + index: index, + selection: STATE.selection + }); + } updateFilesBeingProcessed(response.file) DEBUG && console.log(`File ${file} processed after ${(new Date() - predictionStart) / 1000} seconds: ${filesBeingProcessed.length} files to go`); - } + } !STATE.selection && (!DATASET || STATE.increment() === 0) && getSummary({ interim: true }); return response.worker } @@ -2342,7 +2302,8 @@ const prepSummaryStatement = (included) => { topRankin = STATE.topRankin, directory = undefined, format = undefined, - active = undefined + active = undefined, + select = undefined } = {}) => { let confidence = STATE.detect.confidence; if (offset === undefined) { // Get offset state @@ -2407,11 +2368,11 @@ const prepSummaryStatement = (included) => { sendResult(++index, 'No detections found in the selection', true) } else { species = species || ''; - sendResult(++index, `No ${species} detections found.`, true) + sendResult(++index, `No ${species} detections found using the ${STATE.list} list.`, true) } } } - STATE.selection || UI.postMessage({event: 'results-complete', active: active}); + STATE.selection || UI.postMessage({event: 'results-complete', active: active, select: select}); }; // Function to format the CSV export