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