├── icon-256.png ├── ubuntu.mono.ttf ├── map ├── images │ ├── layers.png │ ├── layers-2x.png │ ├── marker-icon.png │ ├── marker-icon-2x.png │ └── marker-shadow.png ├── uiMap.js └── leaflet.css ├── resources ├── icon-256.png ├── ubuntu.mono.ttf └── geo.svg ├── .gitignore ├── VerticalTabs ├── verticalTabs.scss ├── verticalTabs.css.map └── verticalTabs.css ├── ui.css ├── .eslintrc.js ├── uiNight.css ├── map.html ├── ariaSpeak.js ├── LICENSE ├── nightMode.js ├── settings ├── sliderControl.js ├── uiAdvanced.js ├── advanced.html ├── uiAddons.js ├── settings.html └── durationInput.js ├── about.js ├── index.css ├── README.md ├── processing ├── uiCommon.js ├── output.html ├── summarise.html ├── uiInput.js ├── uiOutput.js ├── uiSummarise.js ├── aligning.html └── uiSplit.js ├── about.html ├── schedule ├── schedule.js ├── dateInput.js ├── calculateSunriseSunset.js ├── schedule.html ├── scheduleEditor.js ├── uiScheduleEditor.js ├── roundingInput.js └── sunIntervalInput.js ├── versionChecker.js ├── timeZoneSelection.html ├── package.json ├── ui.js ├── timeHandler.js ├── scheduleBar.js └── constants.js /icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/icon-256.png -------------------------------------------------------------------------------- /ubuntu.mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/ubuntu.mono.ttf -------------------------------------------------------------------------------- /map/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/layers.png -------------------------------------------------------------------------------- /resources/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/resources/icon-256.png -------------------------------------------------------------------------------- /map/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/layers-2x.png -------------------------------------------------------------------------------- /resources/ubuntu.mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/resources/ubuntu.mono.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | dist/ 4 | package-lock.json 5 | .DS_Store 6 | .vscode/tasks.json 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /map/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/marker-icon.png -------------------------------------------------------------------------------- /map/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/marker-icon-2x.png -------------------------------------------------------------------------------- /map/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/marker-shadow.png -------------------------------------------------------------------------------- /VerticalTabs/verticalTabs.scss: -------------------------------------------------------------------------------- 1 | $horizontal-tabs-min: 350px; 2 | 3 | $vertical-tabs-min: 400px; 4 | 5 | $fixed-tab-size: 92px; 6 | 7 | $left-tabs-text-align: left; 8 | $right-tabs-text-align: right; 9 | 10 | @import "../node_modules/bootstrap-5-vertical-tabs/scss/responsive-vertical-tabs"; 11 | -------------------------------------------------------------------------------- /ui.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #FFFFFF; 3 | } 4 | 5 | .slider-selection { 6 | background: rgb(173, 189, 241); 7 | } 8 | 9 | input { 10 | border: thin solid #cfcfcf; 11 | } 12 | 13 | .tooltip .tooltip-inner { 14 | max-width: none; 15 | white-space: pre-wrap; 16 | text-align: left; 17 | } 18 | 19 | .greyStart::first-letter, .allGrey { 20 | color: #f0f0f0; 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'standard', 3 | rules: { 4 | semi: [2, 'always'], 5 | indent: ['error', 4], 6 | 'padded-blocks': ['error', 'always'], 7 | 'no-useless-escape': 0, 8 | 'object-curly-spacing': ['error', 'never'], 9 | 'no-extend-native': ['error', {exceptions: ['Array']}], 10 | 'standard/no-callback-literal': 0, 11 | 'no-throw-literal': 0 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /resources/geo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /uiNight.css: -------------------------------------------------------------------------------- 1 | .table>tbody>tr>td, 2 | .table>tbody>tr>th { 3 | background: #000000; 4 | color: #FFFFFF; 5 | } 6 | 7 | body { 8 | background: #000000; 9 | color: #FFFFFF; 10 | } 11 | 12 | input { 13 | background: #000000; 14 | color: #FFFFFF; 15 | border: thin solid #FFFFFF; 16 | } 17 | 18 | select { 19 | background: #000000; 20 | color: #FFFFFF; 21 | } 22 | 23 | .info-display-input { 24 | background-color: #000000; 25 | } 26 | 27 | #time-canvas { 28 | border: #FFFFFF; 29 | } 30 | 31 | .slider-horizontal .slider-selection { 32 | background: rgb(64, 100, 216); 33 | } 34 | 35 | .grey { 36 | color: #808080; 37 | } 38 | 39 | .greyStart::first-letter, .allGrey { 40 | color: #404040; 41 | } 42 | 43 | .tab-pane { 44 | background: #000000; 45 | color: #FFFFFF; 46 | } 47 | -------------------------------------------------------------------------------- /map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Select Location 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ariaSpeak.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * ariaSpeak.js 3 | * openacousticdevices.info 4 | * May 2023 5 | *****************************************************************************/ 6 | 7 | /** 8 | * Say something if user is using a screen reader 9 | * @param {string} text Text which should be said if viewed on a screen reader 10 | */ 11 | exports.speak = (text) => { 12 | 13 | const element = document.createElement('div'); 14 | const id = 'speak-' + Date.now(); 15 | element.setAttribute('id', id); 16 | element.setAttribute('aria-live', 'polite'); 17 | element.classList.add('visually-hidden'); 18 | document.body.appendChild(element); 19 | 20 | window.setTimeout(() => { 21 | 22 | document.getElementById(id).innerHTML = text; 23 | 24 | }, 100); 25 | 26 | window.setTimeout(() => { 27 | 28 | document.body.removeChild(document.getElementById(id)); 29 | 30 | }, 1000); 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 OpenAcousticDevices 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nightMode.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * nightMode.js 3 | * openacousticdevices.info 4 | * December 2019 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | const {app} = require('@electron/remote'); 10 | 11 | let nightMode = false; 12 | 13 | exports.isEnabled = () => { 14 | 15 | return nightMode; 16 | 17 | }; 18 | 19 | function setNightMode (nm) { 20 | 21 | nightMode = nm; 22 | 23 | const oldLink = document.getElementById('uiCSS'); 24 | const newLink = document.createElement('link'); 25 | 26 | newLink.setAttribute('id', 'uiCSS'); 27 | newLink.setAttribute('rel', 'stylesheet'); 28 | newLink.setAttribute('type', 'text/css'); 29 | 30 | if (nightMode) { 31 | 32 | newLink.setAttribute('href', app.getAppPath() + '/uiNight.css'); 33 | 34 | } else { 35 | 36 | newLink.setAttribute('href', app.getAppPath() + '/ui.css'); 37 | 38 | } 39 | 40 | document.getElementsByTagName('head').item(0).replaceChild(newLink, oldLink); 41 | 42 | } 43 | 44 | exports.setNightMode = setNightMode; 45 | 46 | exports.toggle = () => { 47 | 48 | setNightMode(!nightMode); 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /settings/sliderControl.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * sliderControl.js 3 | * openacousticdevices.info 4 | * April 2022 5 | *****************************************************************************/ 6 | 7 | /** 8 | * Disable playback slider and change CSS to display disabled cursor on hover 9 | */ 10 | function disableSlider (slider, div) { 11 | 12 | slider.disable(); 13 | 14 | const children = div.getElementsByTagName('*'); 15 | 16 | for (let i = 0; i < children.length; i++) { 17 | 18 | if (children[i].style) { 19 | 20 | children[i].style.cursor = 'not-allowed'; 21 | 22 | } 23 | 24 | } 25 | 26 | } 27 | 28 | /** 29 | * Enable playback slider and reset CSS cursor 30 | */ 31 | function enableSlider (slider, div) { 32 | 33 | slider.enable(); 34 | 35 | const children = div.getElementsByTagName('*'); 36 | 37 | for (let i = 0; i < children.length; i++) { 38 | 39 | if (children[i].style) { 40 | 41 | children[i].style.cursor = ''; 42 | 43 | } 44 | 45 | } 46 | 47 | } 48 | 49 | // Check if code is running on Config App or Filter Playground as importing/exporting works differently in Electron 50 | if (window && window.process && window.process.type) { 51 | 52 | exports.enableSlider = enableSlider; 53 | exports.disableSlider = disableSlider; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /about.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * about.js 3 | * openacousticdevices.info 4 | * June 2017 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | /* global document */ 10 | 11 | const electron = require('electron'); 12 | const {app, process} = require('@electron/remote'); 13 | const audiomoth = require('audiomoth-hid'); 14 | 15 | const nightMode = require('./nightMode.js'); 16 | 17 | const versionDisplay = document.getElementById('version-display'); 18 | const electronVersionDisplay = document.getElementById('electron-version-display'); 19 | const websiteLink = document.getElementById('website-link'); 20 | const audiomothHidVersionDisplay = document.getElementById('audiomoth-hid-version-display'); 21 | 22 | versionDisplay.textContent = 'Version ' + app.getVersion(); 23 | electronVersionDisplay.textContent = 'Running on Electron version ' + process.versions.electron; 24 | audiomothHidVersionDisplay.textContent = 'AudioMoth-HID module ' + audiomoth.version; 25 | 26 | electron.ipcRenderer.on('night-mode', (e, nm) => { 27 | 28 | if (nm !== undefined) { 29 | 30 | nightMode.setNightMode(nm); 31 | 32 | } else { 33 | 34 | nightMode.toggle(); 35 | 36 | } 37 | 38 | }); 39 | 40 | websiteLink.addEventListener('click', () => { 41 | 42 | electron.shell.openExternal('https://openacousticdevices.info'); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Ubuntu'; 3 | src: url('./resources/ubuntu.mono.ttf'); 4 | } 5 | 6 | input[type=number]::-webkit-inner-spin-button, 7 | input[type=number]::-webkit-outer-spin-button { 8 | -webkit-appearance: none; 9 | margin: 0; 10 | } 11 | 12 | *, *::after, *::before { 13 | -webkit-user-select: none; 14 | -webkit-user-drag: none; 15 | -webkit-app-region: no-drag; 16 | cursor: default; 17 | } 18 | 19 | .table>tbody>tr>td, 20 | .table>tbody>tr>th { 21 | border-color: transparent; 22 | border-top: none; 23 | } 24 | 25 | #disabled-filter-slider .slider-handle { 26 | display: none; 27 | } 28 | 29 | .grey { 30 | color: #D3D3D3; 31 | } 32 | 33 | .mono-text { 34 | font-family: 'Ubuntu'; 35 | font-size: 27px; 36 | } 37 | 38 | input { 39 | font-variant-numeric: tabular-nums; 40 | } 41 | 42 | .groupBorder { 43 | border: #cdcdcd thin solid; 44 | padding: 5px; 45 | } 46 | 47 | .leftElement { 48 | margin-left: 16px; 49 | } 50 | 51 | .rightElement { 52 | margin-right: 16px; 53 | text-align: right; 54 | } 55 | 56 | .greyStart { 57 | display: inline-block; 58 | } 59 | 60 | .form-select-sm { 61 | height: 25px; 62 | font-family: Helvetica, Arial, sans-serif; 63 | font-size: 10pt; 64 | } 65 | 66 | canvas { 67 | image-rendering: pixelated; 68 | image-rendering: crisp-edges; 69 | } 70 | 71 | .wider-col { 72 | height: 182px; 73 | width: 89.55%; 74 | } 75 | 76 | .button-icon { 77 | width: 13px; 78 | height: 13px; 79 | vertical-align: initial; 80 | } 81 | -------------------------------------------------------------------------------- /VerticalTabs/verticalTabs.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["../node_modules/bootstrap-5-vertical-tabs/scss/responsive-vertical-tabs.scss","../node_modules/bootstrap/scss/_variables.scss","verticalTabs.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBA;EACE,YALqB;EAMrB;EACA;EACA;;;AAIF;EACE;EACA;;;AAwBF;EApBE;EACA;EACA;EAoBA;;;AASF;EACE;;;AAIF;EAEE;EACA;;;AAGF;EACE,wBC+jCkC;ED9jClC,2BC8jCkC;ED7jClC;EACA;EACA;EACA,YEhFqB;;;AFmFvB;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EAEE;EACA;;;AAGF;EACE,yBCoiCkC;EDniClC,4BCmiCkC;EDliClC;EACA;EACA;EACA,YE1GsB;;;AF6GxB;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EAGE;EACA;;;AAGF;EACE;EACA,QApHqB;EAqHrB;;;AAIF;EAjGE,OEzCe;EF0Cf;EARA;EACA;EACA;EAwGA,yBCggCkC;ED//BlC;EACA;EACA,wBC6/BkC;ED5/BlC;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EAGE;EACA,OApJuB;;;AAuJzB;EACE;EACA,QAvJqB;EAwJrB;;;AAIF;EApIE,OEzCe;EF0Cf;EARA;EACA;EACA;EA2IA,yBC69BkC;ED59BlC;EACA;EACA,wBC09BkC;EDz9BlC;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;;;AAIF;EACE;IAxJA;IACA;IACA;IACA;IAuJE;IACA;IACA;;EAGF;IAtKA,OEzCe;IF0Cf;IARA;IACA;IACA;IA6KE,yBC27BgC;ID17BhC;IACA;IACA,wBCw7BgC;IDv7BhC;IACA;;EAGF;IACE,oBCg7BgC;ID/6BhC;;EAGF;IACE,kBC26BgC;ID16BhC,oBC06BgC;IDz6BhC;IACA,mBCw6BgC;;EDr6BlC;AAAA;IAEE;IACA;IACA;IACA;;EAIF;IA9LA;IACA;IACA;IACA;IA6LE;IACA;IACA;;EAGF;AAAA;IA5MA,OEzCe;IF0Cf;IARA;IACA;IACA;IAoNE;IACA,4BCm5BgC;IDl5BhC,2BCk5BgC;IDj5BhC;IACA;IACA;IACA;;EAGF;IACE;IACA,mBCu4BgC;IDt4BhC,qBC9PO;;EDiQT;AAAA;IAEE;IACA,oBCg4BgC;ID/3BhC,qBC+3BgC;ID93BhC,mBC83BgC;;;ADz3BpC;EACE;IACE;;EAGF;IACE;;EAIF;IACE;IACA;;EAGF;IACE;;EAGF;IACE;IACA;IACA;;;AAKJ;EACE;IACE;;EAGF;IACE","file":"verticalTabs.css"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AudioMoth Configuration App # 2 | An Electron-based application capable of configuring the functionality of the AudioMoth recording device and setting the onboard clock. 3 | 4 | For more details on the device itself, visit [www.openacousticdevices.info](http://www.openacousticdevices.info). 5 | 6 | ### Usage ### 7 | Once the repository has been cloned, you must either have electron-builder installed globally, or get it for the app specifically by running: 8 | ``` 9 | npm install 10 | ``` 11 | 12 | From then onwards, or if you already had electron-builder installed, start the application with: 13 | ``` 14 | npm run start 15 | ``` 16 | 17 | Package the application into an installer for your current platform with: 18 | ``` 19 | npm run dist [win64/win32/mac/linux] 20 | ``` 21 | 22 | This will place a packaged version of the app and an installer for the platform this command was run on into the `/dist` folder. Note that to sign the binary in macOS you will need to run the command above as 'sudo'. The codesign application will retreive the appropriate certificate from Keychain Access. 23 | 24 | For detailed usage instructions of the app itself and to download prebuilt installers of the latest stable version for all platforms, visit the app support site [here](http://www.openacousticdevices.info/config). 25 | 26 | ### Related Repositories ### 27 | * [AudioMoth-HID](https://github.com/OpenAcousticDevices/AudioMoth-HID) 28 | * [AudioMoth Time App](https://github.com/OpenAcousticDevices/AudioMoth-Time-App) 29 | 30 | ### License ### 31 | 32 | Copyright 2017 [Open Acoustic Devices](http://www.openacousticdevices.info/). 33 | 34 | [MIT license](http://www.openacousticdevices.info/license). 35 | -------------------------------------------------------------------------------- /processing/uiCommon.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiCommons.js 3 | * openacousticdevices.info 4 | * February 2021 5 | *****************************************************************************/ 6 | 7 | /* Functions which control elements common to the expansion and split windows */ 8 | 9 | const electron = require('electron'); 10 | 11 | const nightMode = require('../nightMode.js'); 12 | 13 | const fileButton = document.getElementById('file-button'); 14 | 15 | function getSelectedRadioValue (radioName) { 16 | 17 | return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value); 18 | 19 | } 20 | 21 | exports.getSelectedRadioValue = getSelectedRadioValue; 22 | 23 | /* Pause execution */ 24 | 25 | exports.sleep = (milliseconds) => { 26 | 27 | const date = Date.now(); 28 | let currentDate = null; 29 | 30 | do { 31 | 32 | currentDate = Date.now(); 33 | 34 | } while (currentDate - date < milliseconds); 35 | 36 | }; 37 | 38 | electron.ipcRenderer.on('night-mode', (e, nm) => { 39 | 40 | if (nm !== undefined) { 41 | 42 | nightMode.setNightMode(nm); 43 | 44 | } else { 45 | 46 | nightMode.toggle(); 47 | 48 | } 49 | 50 | }); 51 | 52 | /* Update text on selection button to reflect selection mode (file or folder selection) */ 53 | 54 | exports.updateButtonText = () => { 55 | 56 | const selectionType = getSelectedRadioValue('selection-radio'); 57 | 58 | if (selectionType === 0) { 59 | 60 | fileButton.innerText = 'Select Files'; 61 | 62 | } else { 63 | 64 | fileButton.innerText = 'Select Folders'; 65 | 66 | } 67 | 68 | }; 69 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | About AudioMoth Configuration App 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
AudioMoth Configuration App
20 |
21 |
22 |
Version 1.0
23 |
24 |
25 |
Running on Electron version 1.0
26 |
27 |
28 |
AudioMoth-HID module 1.0.0
29 |
30 |
31 | 34 |
35 |
36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /schedule/schedule.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * schedule.js 3 | * openacousticdevices.info 4 | * November 2019 5 | *****************************************************************************/ 6 | 7 | const constants = require('../constants.js'); 8 | 9 | let timePeriods = []; 10 | 11 | exports.getTimePeriodCount = () => { 12 | 13 | return timePeriods.length; 14 | 15 | }; 16 | 17 | exports.clear = () => { 18 | 19 | timePeriods = []; 20 | 21 | }; 22 | 23 | /* Recording period data structure getter and setter */ 24 | 25 | exports.getTimePeriods = () => { 26 | 27 | return timePeriods; 28 | 29 | }; 30 | 31 | /* Older firmware doesn't support time periods where startMins > endMins. Replace periods which do this with a period either side of midnight */ 32 | 33 | exports.getTimePeriodsNoWrap = () => { 34 | 35 | const noWrapTimePeriods = []; 36 | 37 | for (let i = 0; i < timePeriods.length; i++) { 38 | 39 | const startMins = timePeriods[i].startMins; 40 | const endMins = timePeriods[i].endMins; 41 | 42 | if (startMins >= endMins && endMins !== 0) { 43 | 44 | noWrapTimePeriods.push({ 45 | startMins, 46 | endMins: constants.MINUTES_IN_DAY 47 | }); 48 | 49 | noWrapTimePeriods.push({ 50 | startMins: 0, 51 | endMins 52 | }); 53 | 54 | } else { 55 | 56 | noWrapTimePeriods.push(timePeriods[i]); 57 | 58 | } 59 | 60 | } 61 | 62 | let sortedPeriods = noWrapTimePeriods; 63 | sortedPeriods = sortedPeriods.sort((a, b) => { 64 | 65 | return a.startMins - b.startMins; 66 | 67 | }); 68 | 69 | return sortedPeriods; 70 | 71 | }; 72 | 73 | exports.setTimePeriods = (tp) => { 74 | 75 | timePeriods = tp; 76 | 77 | }; 78 | -------------------------------------------------------------------------------- /schedule/dateInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global document */ 4 | 5 | const ui = require('./ui.js'); 6 | 7 | const dateInputs = document.getElementsByClassName('custom-date-input'); 8 | 9 | function isValidDate (input, d) { 10 | 11 | if (!(d instanceof Date)) { 12 | 13 | return false; 14 | 15 | } 16 | 17 | if (isNaN(d)) { 18 | 19 | return false; 20 | 21 | } 22 | 23 | const maxDate = new Date(input.getAttribute('max')); 24 | 25 | if (d > maxDate.getTime()) { 26 | 27 | return false; 28 | 29 | } 30 | 31 | const minDate = new Date(input.getAttribute('min')); 32 | 33 | if (d.getTime() < minDate.getTime()) { 34 | 35 | return false; 36 | 37 | } 38 | 39 | return true; 40 | 41 | } 42 | 43 | function setToLastValidDate (input, lastValidDateString) { 44 | 45 | input.value = lastValidDateString; 46 | 47 | } 48 | 49 | function handleFocusOut (e) { 50 | 51 | const input = e.srcElement; 52 | 53 | const inputDate = new Date(input.value); 54 | 55 | if (isValidDate(input, inputDate)) { 56 | 57 | input.setAttribute('lastValidDate', input.value); 58 | 59 | } else { 60 | 61 | setToLastValidDate(input, input.getAttribute('lastValidDate')); 62 | 63 | input.style.border = '1px solid #0000ff'; 64 | input.style.color = 'blue'; 65 | 66 | setTimeout(() => { 67 | 68 | if (!input.disabled) { 69 | 70 | input.classList.remove('grey'); 71 | 72 | } 73 | 74 | input.style.color = ''; 75 | input.style.border = ''; 76 | 77 | }, 1000); 78 | 79 | } 80 | 81 | } 82 | 83 | const today = new Date(); 84 | const todayString = ui.formatDateString(today); 85 | 86 | for (let i = 0; i < dateInputs.length; i++) { 87 | 88 | dateInputs[i].addEventListener('focusout', handleFocusOut); 89 | 90 | setToLastValidDate(dateInputs[i], todayString); 91 | 92 | dateInputs[i].setAttribute('lastValidDate', dateInputs[i].value); 93 | 94 | dateInputs[i].setAttribute('min', todayString); 95 | dateInputs[i].setAttribute('max', '2029-12-31'); 96 | 97 | } 98 | -------------------------------------------------------------------------------- /settings/uiAdvanced.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiAdvanced.js 3 | * openacousticdevices.info 4 | * January 2021 5 | *****************************************************************************/ 6 | 7 | const acousticConfigCheckBox = document.getElementById('acoustic-config-checkbox'); 8 | const dailyFolderCheckBox = document.getElementById('daily-folder-checkbox'); 9 | const energySaverModeCheckbox = document.getElementById('energy-saver-mode-checkbox'); 10 | const lowGainRangeCheckbox = document.getElementById('low-gain-range-checkbox'); 11 | const disable48DCFilterCheckbox = document.getElementById('disable-48-dc-filter-checkbox'); 12 | const filenameWithDeviceIDCheckbox = document.getElementById('filename-device-id-checkbox'); 13 | 14 | exports.isAcousticConfigRequired = () => { 15 | 16 | return acousticConfigCheckBox.checked; 17 | 18 | }; 19 | 20 | exports.isDailyFolderEnabled = () => { 21 | 22 | return dailyFolderCheckBox.checked; 23 | 24 | }; 25 | 26 | exports.isEnergySaverModeEnabled = () => { 27 | 28 | return energySaverModeCheckbox.checked; 29 | 30 | }; 31 | 32 | exports.isLowGainRangeEnabled = () => { 33 | 34 | return lowGainRangeCheckbox.checked; 35 | 36 | }; 37 | 38 | exports.is48DCFilterDisabled = () => { 39 | 40 | return disable48DCFilterCheckbox.checked; 41 | 42 | }; 43 | 44 | exports.isFilenameWithDeviceIDEnabled = () => { 45 | 46 | return filenameWithDeviceIDCheckbox.checked; 47 | 48 | }; 49 | 50 | exports.fillUI = (settings) => { 51 | 52 | acousticConfigCheckBox.checked = settings.requireAcousticConfig; 53 | energySaverModeCheckbox.checked = settings.energySaverModeEnabled; 54 | lowGainRangeCheckbox.checked = settings.lowGainRangeEnabled; 55 | disable48DCFilterCheckbox.checked = settings.disable48DCFilter; 56 | dailyFolderCheckBox.checked = settings.dailyFolders; 57 | filenameWithDeviceIDCheckbox.checked = settings.filenameWithDeviceIDEnabled; 58 | 59 | }; 60 | 61 | exports.prepareUI = (changeFunction) => { 62 | 63 | energySaverModeCheckbox.addEventListener('change', changeFunction); 64 | dailyFolderCheckBox.addEventListener('change', changeFunction); 65 | 66 | }; 67 | -------------------------------------------------------------------------------- /versionChecker.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * versionChecker.js 3 | * openacousticdevices.info 4 | * November 2020 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | /* global XMLHttpRequest */ 10 | 11 | const {app} = require('@electron/remote'); 12 | 13 | const semver = require('semver'); 14 | 15 | const pjson = require('./package.json'); 16 | 17 | /* Check current app version in package.json against latest version in repository's releases */ 18 | 19 | exports.checkLatestRelease = (callback) => { 20 | 21 | /* Check for internet connection */ 22 | 23 | if (!navigator.onLine) { 24 | 25 | callback({updateNeeded: false, error: 'No internet connection, failed to request app version information.'}); 26 | return; 27 | 28 | } 29 | 30 | const version = app.getVersion(); 31 | 32 | /* Transform repository URL into release API URL */ 33 | 34 | const repoGitURL = pjson.repository.url; 35 | let repoURL = repoGitURL.replace('.git', '/releases'); 36 | repoURL = repoURL.replace('github.com', 'api.github.com/repos'); 37 | 38 | const xmlHttp = new XMLHttpRequest(); 39 | xmlHttp.open('GET', repoURL, true); 40 | 41 | xmlHttp.onload = () => { 42 | 43 | if (xmlHttp.status === 200) { 44 | 45 | const responseJson = JSON.parse(xmlHttp.responseText); 46 | 47 | const latestVersion = responseJson[0].tag_name; 48 | 49 | console.log('Comparing latest release (' + latestVersion + ') with currently installed version (' + version + ')'); 50 | 51 | /* Compare current version in package.json to latest version pulled from Github */ 52 | 53 | const updateNeeded = semver.lt(version, latestVersion); 54 | 55 | callback({updateNeeded, latestVersion: updateNeeded ? latestVersion : version}); 56 | 57 | } 58 | 59 | }; 60 | 61 | xmlHttp.onerror = () => { 62 | 63 | console.error('Failed to pull release information.'); 64 | callback({updateNeeded: false, error: 'HTTP connection error, failed to request app version information.'}); 65 | 66 | }; 67 | 68 | /* Send request */ 69 | 70 | xmlHttp.send(null); 71 | 72 | }; 73 | -------------------------------------------------------------------------------- /timeZoneSelection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Select Custom Time Zone 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 | 29 |
30 | 31 |
32 | 33 |
UTC+00:00
34 |
35 |
36 | 37 |
38 | 39 | 40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /processing/output.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | Enable destination folder: 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | Generate subfolders in destination: 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 | 47 |
48 |
49 |
50 | Writing WAV files to source folder. 51 |
52 |
53 |
54 | 55 |
56 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AudioMoth-Config", 3 | "version": "1.12.1", 4 | "description": "The configuration app for the AudioMoth acoustic monitoring device.", 5 | "main": "main.js", 6 | "author": "openacousticdevices.info", 7 | "license": "ISC", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/OpenAcousticDevices/AudioMoth-Configuration-App.git" 11 | }, 12 | "scripts": { 13 | "postinstall": "install-app-deps", 14 | "start": "electron .", 15 | "dist": "node builder.js", 16 | "buildcss": "npx sass --load-path=node_modules VerticalTabs/verticalTabs.scss VerticalTabs/verticalTabs.css" 17 | }, 18 | "build": { 19 | "asar": { 20 | "smartUnpack": true 21 | }, 22 | "appId": "info.openacousticdevices.audiomothconfigurationapp", 23 | "mac": { 24 | "hardenedRuntime": true, 25 | "entitlements": "build/entitlements.mac.inherit.plist", 26 | "target": "dmg" 27 | }, 28 | "dmg": { 29 | "contents": [ 30 | { 31 | "x": 110, 32 | "y": 150 33 | }, 34 | { 35 | "x": 430, 36 | "y": 150, 37 | "type": "link", 38 | "path": "/Applications" 39 | } 40 | ], 41 | "artifactName": "AudioMothConfigurationAppSetup${version}.dmg" 42 | }, 43 | "win": { 44 | "target": "nsis", 45 | "icon": "build/icon.ico" 46 | }, 47 | "nsis": { 48 | "createDesktopShortcut": true, 49 | "oneClick": false, 50 | "allowToChangeInstallationDirectory": true, 51 | "artifactName": "AudioMothConfigurationAppSetup${version}.exe", 52 | "shortcutName": "AudioMoth Configuration App", 53 | "uninstallDisplayName": "AudioMoth Configuration App ${version}" 54 | }, 55 | "linux": { 56 | "icon": "build/", 57 | "category": "Utility" 58 | } 59 | }, 60 | "devDependencies": { 61 | "clean-css-cli": "^5.6.3", 62 | "electron": "^25.9.8", 63 | "electron-builder": "^24.6.3", 64 | "eslint": "^8.45.0", 65 | "eslint-config-standard": "^17.1.0", 66 | "eslint-plugin-import": "^2.27.5", 67 | "eslint-plugin-n": "^16.0.1", 68 | "eslint-plugin-node": "^11.1.0", 69 | "eslint-plugin-promise": "^6.1.1", 70 | "eslint-plugin-standard": "^4.1.0", 71 | "sass": "^1.69.5" 72 | }, 73 | "dependencies": { 74 | "@electron/remote": "^2.0.10", 75 | "@popperjs/core": "^2.11.8", 76 | "audiomoth-hid": "^2.3.0", 77 | "audiomoth-utils": "^1.7.0", 78 | "bootstrap": "5.3.1", 79 | "bootstrap-5-vertical-tabs": "^2.0.1", 80 | "bootstrap-slider": "^11.0.2", 81 | "clean": "^4.0.2", 82 | "electron-debug": "3.2.0", 83 | "electron-localshortcut": "^3.2.1", 84 | "electron-progressbar": "^2.1.0", 85 | "http-cache-semantics": "^4.1.1", 86 | "jquery": "^3.7.0", 87 | "jsonschema": "1.4.1", 88 | "minimatch": "^9.0.3", 89 | "semver": "^7.5.4", 90 | "showdown": "^2.1.0", 91 | "strftime": "0.10.2" 92 | }, 93 | "engines": { 94 | "node": ">=10.16.2" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /map/uiMap.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiMap.js 3 | * openacousticdevices.info 4 | * February 2024 5 | *****************************************************************************/ 6 | 7 | /* global L */ 8 | /* eslint-disable new-cap */ 9 | 10 | const electron = require('electron'); 11 | 12 | let map, marker; 13 | 14 | function convertToCoordinateArray (l) { 15 | 16 | while (l > 180) l -= 360; 17 | while (l < -180) l += 360; 18 | 19 | l = Math.round(100 * l); 20 | 21 | const positiveDirection = l >= 0; 22 | 23 | l = Math.abs(l); 24 | 25 | const degrees = Math.floor(l / 100); 26 | const hundredths = l % 100; 27 | 28 | return [degrees, hundredths, positiveDirection]; 29 | 30 | } 31 | 32 | electron.ipcRenderer.on('update-location-sub-window', (e, latArray, lonArray, changeZoom, newZoom) => { 33 | 34 | let lat = latArray[0] + latArray[1] / 100; 35 | 36 | if (latArray[2] === false) lat = -lat; 37 | 38 | let lon = lonArray[0] + lonArray[1] / 100; 39 | 40 | if (lonArray[2] === false) lon = -lon; 41 | 42 | marker.setLatLng([lat, lon]); 43 | 44 | if (changeZoom) { 45 | 46 | map.setView(L.latLng(lat, lon), newZoom); 47 | 48 | } else { 49 | 50 | map.setView(L.latLng(lat, lon)); 51 | 52 | } 53 | 54 | }); 55 | 56 | function updateMainWindow (latLng) { 57 | 58 | const latArray = convertToCoordinateArray(latLng.lat); 59 | const lonArray = convertToCoordinateArray(latLng.lng); 60 | 61 | electron.ipcRenderer.send('update-location-main-window', latArray, lonArray); 62 | 63 | } 64 | 65 | function setUpMap () { 66 | 67 | try { 68 | 69 | map = new L.Map('location-picker'); 70 | 71 | } catch (e) { 72 | 73 | console.log(e); 74 | 75 | } 76 | 77 | const attributionElement = document.getElementsByClassName('leaflet-control-attribution')[0]; 78 | attributionElement.innerHTML = 'Open Street Map'; 79 | 80 | map.doubleClickZoom.disable(); 81 | 82 | map.on('dblclick', (e) => { 83 | 84 | const latLng = e.latlng; 85 | const zoom = Math.min(map.getZoom() + 1, map.getMaxZoom()); 86 | 87 | console.log('Map marker moved to ' + latLng + ' zoom ' + zoom); 88 | 89 | marker.setLatLng(latLng); 90 | 91 | updateMainWindow(latLng); 92 | 93 | map.setView(latLng, zoom); 94 | 95 | }); 96 | 97 | const osm = new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {minZoom: 1, maxZoom: 17, attribution: ''}); 98 | 99 | const lat = 0; 100 | const lng = 0; 101 | 102 | map.addLayer(osm); 103 | 104 | if (!marker) { 105 | 106 | marker = new L.marker([lat, lng], {draggable: 'true'}); 107 | 108 | } else { 109 | 110 | marker.setLatLng([lat, lng]); 111 | 112 | } 113 | 114 | marker.on('dragend', (e) => { 115 | 116 | const latLng = e.target.getLatLng(); 117 | console.log('Map marker moved to ' + latLng); 118 | 119 | updateMainWindow(latLng); 120 | 121 | map.setView(latLng); 122 | 123 | }); 124 | 125 | map.addLayer(marker); 126 | 127 | } 128 | 129 | setUpMap(); 130 | -------------------------------------------------------------------------------- /schedule/calculateSunriseSunset.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * calculateSunriseSunset.js 3 | * openacousticdevices.info 4 | * December 2023 5 | *****************************************************************************/ 6 | 7 | const constants = require('../constants.js'); 8 | 9 | const ZENITH_ANGLES = [90.833 / 180 * Math.PI, 96 / 180 * Math.PI, 102 / 180 * Math.PI, 108 / 180 * Math.PI]; 10 | 11 | const DAYS_BY_MONTH = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; 12 | 13 | function isLeapYear (year) { 14 | 15 | return (year & 3) === 0 && ((year % 25) !== 0 || (year & 15) === 0); 16 | 17 | } 18 | 19 | function dayOfYear (year, month, day) { 20 | 21 | let days = DAYS_BY_MONTH[month] + day; 22 | 23 | if (isLeapYear(year) && month > 2) days++; 24 | 25 | return days; 26 | 27 | } 28 | 29 | function normalise (minutes) { 30 | 31 | if (minutes > constants.MINUTES_IN_DAY) minutes -= constants.MINUTES_IN_DAY; 32 | 33 | if (minutes < 0) minutes += constants.MINUTES_IN_DAY; 34 | 35 | return minutes; 36 | 37 | } 38 | 39 | function calculateSunsetAndSunrise (event, gamma, latitude, longitude) { 40 | 41 | /* Convert latitude to radians */ 42 | 43 | latitude = latitude / 180 * Math.PI; 44 | 45 | /* Calculate equation of time, declination and hour angle */ 46 | 47 | const zenith = ZENITH_ANGLES[event]; 48 | 49 | const equationOfTime = 229.18 * (0.000075 + 0.001868 * Math.cos(gamma) - 0.032077 * Math.sin(gamma) - 0.014615 * Math.cos(2 * gamma) - 0.040849 * Math.sin(2 * gamma)); 50 | 51 | const decl = 0.006918 - 0.399912 * Math.cos(gamma) + 0.070257 * Math.sin(gamma) - 0.006758 * Math.cos(2 * gamma) + 0.000907 * Math.sin(2 * gamma) - 0.002697 * Math.cos(3 * gamma) + 0.00148 * Math.sin(3 * gamma); 52 | 53 | let argument = Math.cos(zenith) / Math.cos(latitude) / Math.cos(decl) - Math.tan(latitude) * Math.tan(decl); 54 | 55 | const solution = argument > 1 ? constants.SUN_BELOW_HORIZON : argument < -1 ? constants.SUN_ABOVE_HORIZON : constants.NORMAL_SOLUTION; 56 | 57 | let trend = argument < 0 ? constants.DAY_LONGER_THAN_NIGHT : constants.DAY_SHORTER_THAN_NIGHT; 58 | 59 | if (argument < -1) argument = -1; 60 | 61 | if (argument > 1) argument = 1; 62 | 63 | const ha = Math.acos(argument); 64 | 65 | /* Calculate sunrise and sunset */ 66 | 67 | let sunrise = normalise(constants.MINUTES_IN_DAY / 2 - 4 * (longitude + ha / Math.PI * 180) - equationOfTime); 68 | 69 | let sunset = normalise(constants.MINUTES_IN_DAY / 2 - 4 * (longitude - ha / Math.PI * 180) - equationOfTime); 70 | 71 | sunrise = Math.round(sunrise) % constants.MINUTES_IN_DAY; 72 | 73 | sunset = Math.round(sunset) % constants.MINUTES_IN_DAY; 74 | 75 | let offsetSunrise = sunrise + constants.MINUTES_IN_DAY / 2; 76 | 77 | offsetSunrise = offsetSunrise % constants.MINUTES_IN_DAY; 78 | 79 | if (offsetSunrise === sunset) trend = constants.DAY_EQUAL_TO_NIGHT; 80 | 81 | return [solution, trend, sunrise, sunset]; 82 | 83 | } 84 | 85 | function degreesAndHundredthsToFloatingPoint (degrees, hundredths, direction) { 86 | 87 | let latLon = degrees + hundredths / 100; 88 | 89 | if (direction === false) latLon *= -1; 90 | 91 | return latLon; 92 | 93 | } 94 | 95 | exports.calculate = (event, year, month, day, degrees0, hundredths0, direction0, degrees1, hundredths1, direction1) => { 96 | 97 | const latitude = degreesAndHundredthsToFloatingPoint(degrees0, hundredths0, direction0); 98 | 99 | const longitude = degreesAndHundredthsToFloatingPoint(degrees1, hundredths1, direction1); 100 | 101 | /* Calculate fractional part of year */ 102 | 103 | const totalDaysInYear = isLeapYear(year) ? 366 : 365; 104 | 105 | const gamma = 2 * Math.PI * dayOfYear(year, month, day) / totalDaysInYear; 106 | 107 | /* Calculate sunrise and sunset */ 108 | 109 | return calculateSunsetAndSunrise(event, gamma, latitude, longitude); 110 | 111 | }; 112 | -------------------------------------------------------------------------------- /schedule/schedule.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 |
Start recording:
18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
End recording:
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 |
52 | First recording date (UTC): 53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 | Last recording date (UTC): 65 |
66 |
67 | 68 |
69 |
70 |
71 | -------------------------------------------------------------------------------- /settings/advanced.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Always require acoustic chime on switching to CUSTOM: 5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 | Use daily folder for generated WAV files: 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 | Use NiMH/LiPo voltage range for battery level indication: 27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | Disable 48Hz DC blocking filter: 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | Enable energy saver mode: 45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | Enable low gain range: 54 |
55 |
56 | 57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 | Enable magnetic switch for delayed start: 65 |
66 |
67 | 68 |
69 |
70 |
71 |
72 | Enable GPS for time setting: 73 |
74 |
75 | 76 |
77 |
78 |
79 | -------------------------------------------------------------------------------- /processing/summarise.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | Summarise AudioMoth Files 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | No folder selected. 33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 |
44 | 45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 | Writing summary file to source folder. 65 |
66 |
67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 | 81 |
82 |
83 |
84 | 85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /settings/uiAddons.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiAddons.js 3 | * openacousticdevices.info 4 | * October 2024 5 | *****************************************************************************/ 6 | 7 | const {dialog} = require('@electron/remote'); 8 | 9 | const magneticDelayCheckbox = document.getElementById('magnetic-delay-checkbox'); 10 | const gpsFixTimeCheckbox = document.getElementById('gps-time-checkbox'); 11 | const gpsBeforeAfterSelect = document.getElementById('gps-before-after-select'); 12 | const gpsBeforeAfterLabel = document.getElementById('gps-before-after-label'); 13 | const gpsFixTimeSelect = document.getElementById('gps-time-select'); 14 | const gpsFixTimeLabel = document.getElementById('gps-time-label'); 15 | 16 | let gpsHardwareWarningDisplayed = false; 17 | let magneticSwitchHardwareWarningDisplayed = false; 18 | 19 | exports.isMagneticSwitchEnabled = () => { 20 | 21 | return magneticDelayCheckbox.checked; 22 | 23 | }; 24 | 25 | exports.isTimeSettingFromGPSEnabled = () => { 26 | 27 | return gpsFixTimeCheckbox.checked; 28 | 29 | }; 30 | 31 | exports.gpsFixesBeforeAfterSetting = () => { 32 | 33 | return gpsBeforeAfterSelect.value; 34 | 35 | }; 36 | 37 | exports.getGpsFixTime = () => { 38 | 39 | return parseInt(gpsFixTimeSelect.value); 40 | 41 | }; 42 | 43 | function updateGpsUI () { 44 | 45 | gpsBeforeAfterSelect.disabled = !gpsFixTimeCheckbox.checked; 46 | gpsBeforeAfterSelect.style.color = gpsFixTimeCheckbox.checked ? '' : 'grey'; 47 | gpsBeforeAfterLabel.style.color = gpsFixTimeCheckbox.checked ? '' : 'grey'; 48 | 49 | gpsFixTimeSelect.disabled = !gpsFixTimeCheckbox.checked; 50 | gpsFixTimeSelect.style.color = gpsFixTimeCheckbox.checked ? '' : 'grey'; 51 | gpsFixTimeLabel.style.color = gpsFixTimeCheckbox.checked ? '' : 'grey'; 52 | 53 | } 54 | 55 | exports.fillUI = (settings) => { 56 | 57 | magneticDelayCheckbox.checked = settings.magneticSwitchEnabled; 58 | 59 | gpsFixTimeCheckbox.checked = settings.timeSettingFromGPSEnabled; 60 | gpsBeforeAfterSelect.value = settings.acquireGpsFixBeforeAfter; 61 | 62 | const gpsFixTimeSelectValues = [...gpsFixTimeSelect.options].map(o => o.value); 63 | 64 | const gpsFixTimeString = settings.gpsFixTime.toString(); 65 | 66 | /* Convert GPS fix time value to a string and check it against available values in dropdown */ 67 | 68 | gpsFixTimeSelect.value = gpsFixTimeSelectValues.includes(gpsFixTimeString) ? gpsFixTimeString : '2'; 69 | 70 | updateGpsUI(); 71 | 72 | }; 73 | 74 | exports.prepareUI = (changeFunction) => { 75 | 76 | gpsFixTimeCheckbox.addEventListener('change', changeFunction); 77 | gpsBeforeAfterSelect.addEventListener('change', changeFunction); 78 | gpsFixTimeSelect.addEventListener('change', changeFunction); 79 | 80 | }; 81 | 82 | function displayAdditionalHardwareWarning (featureString) { 83 | 84 | dialog.showMessageBox({ 85 | type: 'warning', 86 | buttons: ['OK'], 87 | title: 'Additional hardware required', 88 | message: 'Additional hardware is required to use the ' + featureString + ' feature. Do not use these settings if this hardware is not present.' 89 | }); 90 | 91 | } 92 | 93 | function displayGpsHardwareWarning () { 94 | 95 | if (gpsHardwareWarningDisplayed) { 96 | 97 | return; 98 | 99 | } 100 | 101 | displayAdditionalHardwareWarning('GPS time setting'); 102 | 103 | gpsHardwareWarningDisplayed = true; 104 | 105 | } 106 | 107 | exports.displayGpsHardwareWarning = displayGpsHardwareWarning; 108 | 109 | function displayMagneticSwitchHardwareWarning () { 110 | 111 | if (magneticSwitchHardwareWarningDisplayed) { 112 | 113 | return; 114 | 115 | } 116 | 117 | displayAdditionalHardwareWarning('magnetic switch'); 118 | 119 | magneticSwitchHardwareWarningDisplayed = true; 120 | 121 | } 122 | 123 | exports.displayMagneticSwitchHardwareWarning = displayMagneticSwitchHardwareWarning; 124 | 125 | gpsFixTimeCheckbox.addEventListener('change', () => { 126 | 127 | if (gpsFixTimeCheckbox.checked) { 128 | 129 | displayGpsHardwareWarning(); 130 | 131 | } 132 | 133 | updateGpsUI(); 134 | 135 | }); 136 | 137 | magneticDelayCheckbox.addEventListener('change', () => { 138 | 139 | if (magneticDelayCheckbox.checked) { 140 | 141 | displayMagneticSwitchHardwareWarning(); 142 | 143 | } 144 | 145 | }); 146 | -------------------------------------------------------------------------------- /ui.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * ui.js 3 | * openacousticdevices.info 4 | * July 2017 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | /* global document */ 10 | 11 | const electron = require('electron'); 12 | 13 | const strftime = require('strftime'); 14 | 15 | const timeHandler = require('./timeHandler.js'); 16 | const scheduleBar = require('./scheduleBar.js'); 17 | const nightMode = require('./nightMode.js'); 18 | const constants = require('./constants.js'); 19 | 20 | /* UI components */ 21 | 22 | const timeZoneLabel = document.getElementById('time-zone-label'); 23 | 24 | const timeDisplay = document.getElementById('time-display'); 25 | 26 | let timeZoneMode = constants.TIME_ZONE_MODE_UTC; 27 | 28 | let deviceDate; 29 | 30 | /* Date formatting functions */ 31 | 32 | function formatDateString (date) { 33 | 34 | const year = ('0000' + date.getUTCFullYear()).slice(-4); 35 | const month = ('00' + (date.getUTCMonth() + 1)).slice(-2); 36 | const day = ('00' + date.getUTCDate()).slice(-2); 37 | 38 | return year + '-' + month + '-' + day; 39 | 40 | } 41 | 42 | exports.formatDateString = formatDateString; 43 | 44 | function extractDateComponents (dateString) { 45 | 46 | const dateArray = dateString.split('-'); 47 | 48 | const year = parseInt(dateArray[0]); 49 | const month = parseInt(dateArray[1]); 50 | const day = parseInt(dateArray[2]); 51 | 52 | return { 53 | year, 54 | month, 55 | day 56 | }; 57 | 58 | } 59 | 60 | exports.extractDateComponents = extractDateComponents; 61 | 62 | /* Time zone mode function */ 63 | 64 | function getTimeZoneMode () { 65 | 66 | return timeZoneMode; 67 | 68 | } 69 | 70 | exports.getTimeZoneMode = getTimeZoneMode; 71 | 72 | /* Generate and display current time in either UTC or local time */ 73 | 74 | function showTime () { 75 | 76 | if (deviceDate) { 77 | 78 | const timeZoneOffset = timeHandler.getTimeZoneOffset(); 79 | 80 | const strftimeUTC = strftime.timezone(timeZoneOffset); 81 | 82 | if (timeDisplay) { 83 | 84 | timeDisplay.textContent = strftimeUTC('%H:%M:%S %d/%m/%Y', deviceDate); 85 | 86 | } 87 | 88 | } else { 89 | 90 | timeDisplay.textContent = '--:--:-- --/--/----'; 91 | 92 | } 93 | 94 | } 95 | 96 | exports.showTime = showTime; 97 | 98 | /* Run all UI update functions */ 99 | 100 | function update () { 101 | 102 | scheduleBar.updateCanvas(); 103 | 104 | if (timeDisplay) { 105 | 106 | showTime(); 107 | 108 | } 109 | 110 | } 111 | 112 | exports.update = update; 113 | 114 | exports.updateDate = (date) => { 115 | 116 | deviceDate = date; 117 | 118 | }; 119 | 120 | exports.disableTimeDisplay = () => { 121 | 122 | if (timeDisplay) { 123 | 124 | timeDisplay.classList.add('grey'); 125 | 126 | timeZoneLabel.classList.add('grey'); 127 | 128 | } 129 | 130 | }; 131 | 132 | exports.enableTimeDisplay = () => { 133 | 134 | if (timeDisplay) { 135 | 136 | timeDisplay.classList.remove('grey'); 137 | 138 | timeZoneLabel.classList.remove('grey'); 139 | 140 | } 141 | 142 | }; 143 | 144 | /* Switch between time zone modes (UTC, local, and custom) */ 145 | 146 | function setTimeZoneStatus (mode) { 147 | 148 | timeZoneMode = mode; 149 | 150 | } 151 | 152 | exports.setTimeZoneStatus = setTimeZoneStatus; 153 | 154 | function updateTimeZoneUI () { 155 | 156 | const timeZoneText = timeHandler.getTimeZoneText(); 157 | 158 | timeZoneLabel.innerHTML = timeZoneText; 159 | 160 | scheduleBar.clearSelectedPeriod(); 161 | 162 | update(); 163 | 164 | } 165 | 166 | exports.updateTimeZoneUI = updateTimeZoneUI; 167 | 168 | function setNightMode (nm) { 169 | 170 | nightMode.setNightMode(nm); 171 | 172 | scheduleBar.updateCanvas(); 173 | scheduleBar.drawTimeLabels(); 174 | 175 | } 176 | 177 | exports.setNightMode = setNightMode; 178 | 179 | function toggleNightMode () { 180 | 181 | nightMode.toggle(); 182 | 183 | scheduleBar.updateCanvas(); 184 | scheduleBar.drawTimeLabels(); 185 | 186 | } 187 | 188 | exports.toggleNightMode = toggleNightMode; 189 | 190 | exports.isNightMode = nightMode.isEnabled; 191 | 192 | electron.ipcRenderer.on('poll-night-mode', () => { 193 | 194 | electron.ipcRenderer.send('night-mode-poll-reply', nightMode.isEnabled()); 195 | 196 | }); 197 | 198 | exports.setSunriseSunsetEnabled = scheduleBar.setSunriseSunsetEnabled; 199 | exports.isSunriseSunsetEnabled = scheduleBar.isSunriseSunsetEnabled; 200 | exports.setSunriseSunset = scheduleBar.setSunriseSunset; 201 | exports.getSunrise = scheduleBar.getSunrise; 202 | exports.getSunset = scheduleBar.getSunset; 203 | -------------------------------------------------------------------------------- /processing/uiInput.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiInput.js 3 | * openacousticdevices.info 4 | * September 2023 5 | *****************************************************************************/ 6 | 7 | const {dialog, getCurrentWindow} = require('@electron/remote'); 8 | const currentWindow = getCurrentWindow(); 9 | 10 | const path = require('path'); 11 | const fs = require('fs'); 12 | 13 | const selectionRadios = document.getElementsByName('selection-radio'); 14 | 15 | let previousSelection = []; 16 | 17 | function getSelectedRadioValue (radioName) { 18 | 19 | return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value); 20 | 21 | } 22 | 23 | /* Open dialog and set files to be processed */ 24 | 25 | exports.selectRecordings = (fileRegex) => { 26 | 27 | let folderContents, filePath, fileName, recordings; 28 | 29 | const selectionTypes = ['openFile', 'openDirectory']; 30 | const selectionType = getSelectedRadioValue('selection-radio'); 31 | const properties = [selectionTypes[selectionType], 'multiSelections']; 32 | 33 | /* If files are being selected, limit selection to .wav files */ 34 | 35 | const filters = (selectionType === 0) ? [{name: 'wav', extensions: ['wav']}] : []; 36 | 37 | const openPath = previousSelection.length > 0 ? path.dirname(previousSelection[0]) : ''; 38 | 39 | const selection = dialog.showOpenDialogSync(currentWindow, { 40 | title: 'Select recording file or folder containing recordings', 41 | nameFieldLabel: 'Recordings', 42 | properties, 43 | filters, 44 | defaultPath: openPath 45 | }); 46 | 47 | if (selection) { 48 | 49 | previousSelection = selection; 50 | 51 | recordings = []; 52 | 53 | if (selectionType === 0) { 54 | 55 | for (let i = 0; i < selection.length; i++) { 56 | 57 | filePath = selection[i]; 58 | fileName = path.basename(filePath); 59 | 60 | /* Check if wav files match a given regex */ 61 | 62 | if (fileName.charAt(0) !== '.' && fileRegex.test(fileName.toUpperCase())) { 63 | 64 | recordings.push(filePath); 65 | 66 | } 67 | 68 | } 69 | 70 | } else { 71 | 72 | for (let i = 0; i < selection.length; i++) { 73 | 74 | folderContents = fs.readdirSync(selection[i]); 75 | 76 | for (let j = 0; j < folderContents.length; j++) { 77 | 78 | filePath = folderContents[j]; 79 | 80 | if (filePath.charAt(0) !== '.' && fileRegex.test(filePath.toUpperCase())) { 81 | 82 | recordings.push(path.join(selection[i], filePath)); 83 | 84 | } 85 | 86 | } 87 | 88 | } 89 | 90 | } 91 | 92 | return recordings; 93 | 94 | } 95 | 96 | }; 97 | 98 | function walk (dir) { 99 | 100 | let results = []; 101 | 102 | /* Check directory is not hidden */ 103 | 104 | const dirName = path.basename(dir); 105 | 106 | if (dirName.charAt(0) === '.') return results; 107 | 108 | /* Try to read the directory contents */ 109 | 110 | let list; 111 | 112 | try { 113 | 114 | list = fs.readdirSync(dir); 115 | 116 | } catch (e) { 117 | 118 | return results; 119 | 120 | } 121 | 122 | /* Process each file in the directory */ 123 | 124 | list.forEach((file) => { 125 | 126 | const filePath = path.join(dir, file); 127 | 128 | const stat = fs.statSync(filePath); 129 | 130 | if (stat && stat.isDirectory()) { 131 | 132 | results = results.concat(walk(filePath)); 133 | 134 | } else { 135 | 136 | if (file.charAt(0) !== '.') { 137 | 138 | results.push(filePath); 139 | 140 | } 141 | 142 | } 143 | 144 | }); 145 | 146 | return results; 147 | 148 | } 149 | 150 | exports.selectAllFilesInFolder = () => { 151 | 152 | const selection = dialog.showOpenDialogSync(currentWindow, { 153 | title: 'Select folder containing files', 154 | nameFieldLabel: 'Recordings', 155 | properties: ['openDirectory'], 156 | filters: [] 157 | }); 158 | 159 | return updateFilesInFolder(selection); 160 | 161 | }; 162 | 163 | function updateFilesInFolder (selection) { 164 | 165 | let files; 166 | 167 | if (selection && selection[0]) { 168 | 169 | if (!fs.existsSync(selection[0])) { 170 | 171 | return; 172 | 173 | } 174 | 175 | files = []; 176 | 177 | for (let i = 0; i < selection.length; i++) { 178 | 179 | const folderContents = walk(selection[i]); 180 | 181 | files = files.concat(folderContents); 182 | 183 | } 184 | 185 | return { 186 | selection, 187 | folder: selection[0], 188 | files 189 | }; 190 | 191 | } 192 | 193 | } 194 | 195 | exports.updateFilesInFolder = updateFilesInFolder; 196 | 197 | function resetPreviousSelection () { 198 | 199 | previousSelection = []; 200 | 201 | } 202 | 203 | if (selectionRadios.length > 0) { 204 | 205 | selectionRadios[0].addEventListener('change', resetPreviousSelection); 206 | selectionRadios[1].addEventListener('change', resetPreviousSelection); 207 | 208 | } 209 | -------------------------------------------------------------------------------- /processing/uiOutput.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiOutput.js 3 | * openacousticdevices.info 4 | * June 2022 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | /* global document */ 10 | 11 | const {dialog} = require('@electron/remote'); 12 | 13 | const outputCheckbox = document.getElementById('output-checkbox'); 14 | const outputButton = document.getElementById('output-button'); 15 | const outputLabel = document.getElementById('output-label'); 16 | 17 | const subdirectoriesLabel = document.getElementById('subdirectories-label'); 18 | const subdirectoriesCheckbox = document.getElementById('subdirectories-checkbox'); 19 | 20 | let outputDir = ''; 21 | 22 | const prefixInput = document.getElementById('prefix-input'); 23 | const prefixCheckbox = document.getElementById('prefix-checkbox'); 24 | const prefixLabel = document.getElementById('prefix-label'); 25 | 26 | const selectionRadios = document.getElementsByName('selection-radio'); 27 | 28 | function getSelectedRadioValue (radioName) { 29 | 30 | return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value); 31 | 32 | } 33 | 34 | /* Update label to notify user if a custom output directtory is being used */ 35 | 36 | function updateOutputLabel () { 37 | 38 | if (outputDir === '' || !outputCheckbox.checked) { 39 | 40 | outputLabel.textContent = 'Writing WAV files to source folder.'; 41 | 42 | } else { 43 | 44 | outputLabel.textContent = 'Writing WAV files to destination folder.'; 45 | 46 | } 47 | 48 | }; 49 | 50 | function updateSubdirectoriesCheckbox () { 51 | 52 | const selectionType = getSelectedRadioValue('selection-radio'); 53 | 54 | if (outputCheckbox.checked && outputDir !== '' && selectionType === 1) { 55 | 56 | subdirectoriesCheckbox.disabled = false; 57 | subdirectoriesLabel.classList.remove('grey'); 58 | 59 | } else { 60 | 61 | subdirectoriesCheckbox.disabled = true; 62 | subdirectoriesLabel.classList.add('grey'); 63 | 64 | } 65 | 66 | } 67 | 68 | /* Add listener which handles enabling/disabling custom output directory UI */ 69 | 70 | outputCheckbox.addEventListener('change', () => { 71 | 72 | updateOutputLabel(); 73 | 74 | updateSubdirectoriesCheckbox(); 75 | 76 | outputButton.disabled = !outputCheckbox.checked; 77 | 78 | }); 79 | 80 | /* Select a custom output directory. If Cancel is pressed, assume no custom direcotry is wantted */ 81 | 82 | outputButton.addEventListener('click', () => { 83 | 84 | const destinationName = dialog.showOpenDialogSync({ 85 | title: 'Select Destination', 86 | nameFieldLabel: 'Destination', 87 | multiSelections: false, 88 | properties: ['openDirectory'], 89 | defaultPath: outputDir 90 | }); 91 | 92 | if (destinationName !== undefined) { 93 | 94 | outputDir = destinationName[0]; 95 | 96 | } 97 | 98 | updateOutputLabel(); 99 | 100 | updateSubdirectoriesCheckbox(); 101 | 102 | }); 103 | 104 | /* Remove all characters which aren't A-Z, a-z, 0-9, and _ */ 105 | 106 | prefixInput.addEventListener('keydown', (e) => { 107 | 108 | if (prefixInput.disabled) { 109 | 110 | e.preventDefault(); 111 | return; 112 | 113 | } 114 | 115 | const reg = /[^A-Za-z-_0-9]{1}/g; 116 | 117 | if (reg.test(e.key)) { 118 | 119 | e.preventDefault(); 120 | 121 | } 122 | 123 | }); 124 | 125 | prefixInput.addEventListener('paste', (e) => { 126 | 127 | e.stopPropagation(); 128 | e.preventDefault(); 129 | 130 | if (prefixInput.disabled) { 131 | 132 | return; 133 | 134 | } 135 | 136 | /* Read text from clipboard */ 137 | 138 | const clipboardData = e.clipboardData || window.clipboardData; 139 | const pastedData = clipboardData.getData('Text'); 140 | 141 | /* Perform paste, but remove all unsupported characters */ 142 | 143 | prefixInput.value += pastedData.replace(/[^A-Za-z_0-9]{1}/g, ''); 144 | 145 | /* Limit max number of characters */ 146 | 147 | prefixInput.value = prefixInput.value.substring(0, prefixInput.maxLength); 148 | 149 | }); 150 | 151 | /* Add listener to handle enabling/disabling prefix UI */ 152 | 153 | prefixCheckbox.addEventListener('change', () => { 154 | 155 | if (prefixCheckbox.checked) { 156 | 157 | prefixLabel.classList.remove('grey'); 158 | prefixInput.classList.remove('grey'); 159 | prefixInput.disabled = false; 160 | 161 | } else { 162 | 163 | prefixLabel.classList.add('grey'); 164 | prefixInput.classList.add('grey'); 165 | prefixInput.disabled = true; 166 | 167 | } 168 | 169 | }); 170 | 171 | exports.disableOutputCheckbox = () => { 172 | 173 | outputCheckbox.disabled = true; 174 | 175 | }; 176 | 177 | exports.disableOutputButton = () => { 178 | 179 | outputButton.disabled = true; 180 | 181 | }; 182 | 183 | exports.enableOutputCheckbox = () => { 184 | 185 | outputCheckbox.disabled = false; 186 | 187 | }; 188 | 189 | exports.enableOutputButton = () => { 190 | 191 | if (outputCheckbox.checked) { 192 | 193 | outputButton.disabled = false; 194 | 195 | } 196 | 197 | }; 198 | 199 | exports.isCustomDestinationEnabled = () => { 200 | 201 | return outputCheckbox.checked; 202 | 203 | }; 204 | 205 | exports.getOutputDir = () => { 206 | 207 | return outputDir; 208 | 209 | }; 210 | 211 | exports.isCreateSubdirectoriesEnabled = () => { 212 | 213 | return subdirectoriesCheckbox.checked; 214 | 215 | }; 216 | 217 | selectionRadios[0].addEventListener('change', updateSubdirectoriesCheckbox); 218 | selectionRadios[1].addEventListener('change', updateSubdirectoriesCheckbox); 219 | -------------------------------------------------------------------------------- /timeHandler.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * timeHandler.js 3 | * openacousticdevices.info 4 | * November 2019 5 | *****************************************************************************/ 6 | 7 | const {ipcRenderer} = require('electron'); 8 | 9 | const constants = require('./constants.js'); 10 | const ui = require('./ui.js'); 11 | 12 | let storedTimeZoneOffset = 0; 13 | 14 | function sortPeriods (periods) { 15 | 16 | const sortedPeriods = periods.sort((a, b) => { 17 | 18 | return a.startMins - b.startMins; 19 | 20 | }); 21 | 22 | return sortedPeriods; 23 | 24 | } 25 | 26 | exports.sortPeriods = sortPeriods; 27 | 28 | function storeTimeZoneOffset () { 29 | 30 | storedTimeZoneOffset = 0; 31 | 32 | if (ui.getTimeZoneMode() === constants.TIME_ZONE_MODE_LOCAL) { 33 | 34 | const currentDate = new Date(); 35 | 36 | storedTimeZoneOffset = -currentDate.getTimezoneOffset(); 37 | 38 | } 39 | 40 | if (ui.getTimeZoneMode() === constants.TIME_ZONE_MODE_CUSTOM) { 41 | 42 | storedTimeZoneOffset = ipcRenderer.sendSync('request-custom-time-zone'); 43 | 44 | } 45 | 46 | } 47 | 48 | exports.storeTimeZoneOffset = storeTimeZoneOffset; 49 | 50 | function getTimeZoneOffset () { 51 | 52 | return storedTimeZoneOffset; 53 | 54 | } 55 | 56 | exports.getTimeZoneOffset = getTimeZoneOffset; 57 | 58 | /* ------------------------------------------------- Time zone conversion functions ------------------------------------------------- */ 59 | 60 | function shiftTime (time, toUTC) { 61 | 62 | /* Offset is given as UTC - local time in minutes */ 63 | 64 | let timeZoneOffset = storedTimeZoneOffset; 65 | 66 | timeZoneOffset = toUTC ? timeZoneOffset * -1 : timeZoneOffset; 67 | 68 | time = (time + timeZoneOffset) % constants.MINUTES_IN_DAY; 69 | 70 | /* If time zone offset move time over midnight */ 71 | 72 | if (time < 0) { 73 | 74 | time += constants.MINUTES_IN_DAY; 75 | 76 | } 77 | 78 | return time; 79 | 80 | } 81 | 82 | function shiftTimePeriod (timePeriod, toUTC) { 83 | 84 | const startMins = shiftTime(timePeriod.startMins, toUTC); 85 | const endMins = shiftTime(timePeriod.endMins, toUTC); 86 | 87 | return { 88 | startMins, 89 | endMins 90 | }; 91 | 92 | } 93 | 94 | exports.shiftTimePeriod = shiftTimePeriod; 95 | 96 | /* Convert a list of time periods from UTC to local */ 97 | 98 | function shiftTimePeriods (tps, toUTC) { 99 | 100 | let shiftedTimePeriods = []; 101 | 102 | for (let i = 0; i < tps.length; i++) { 103 | 104 | const timePeriod = tps[i]; 105 | const shiftedTimePeriod = shiftTimePeriod(timePeriod, toUTC); 106 | 107 | shiftedTimePeriods.push({ 108 | startMins: shiftedTimePeriod.startMins, 109 | endMins: shiftedTimePeriod.endMins 110 | }); 111 | 112 | } 113 | 114 | shiftedTimePeriods = sortPeriods(shiftedTimePeriods); 115 | 116 | shiftedTimePeriods = checkTimePeriodsForOverlaps(shiftedTimePeriods); 117 | 118 | return shiftedTimePeriods; 119 | 120 | } 121 | 122 | exports.shiftTimePeriods = shiftTimePeriods; 123 | 124 | /* ------------------------------------------------- Time period checks ------------------------------------------------- */ 125 | 126 | /* See if any newly created time periods overlap and can be merged */ 127 | 128 | function checkTimePeriodsForOverlaps (timePeriods) { 129 | 130 | for (let i = 0; i < timePeriods.length; i++) { 131 | 132 | for (let j = 0; j < timePeriods.length; j++) { 133 | 134 | if (timePeriods[i].startMins === timePeriods[j].startMins && timePeriods[i].endMins === timePeriods[j].endMins) { 135 | 136 | continue; 137 | 138 | } 139 | 140 | if (timePeriods[i].endMins === timePeriods[j].startMins) { 141 | 142 | timePeriods[i].endMins = timePeriods[j].endMins; 143 | timePeriods.splice(j, 1); 144 | 145 | return checkTimePeriodsForOverlaps(timePeriods); 146 | 147 | } 148 | 149 | } 150 | 151 | } 152 | 153 | return timePeriods; 154 | 155 | } 156 | 157 | exports.checkTimePeriodsForOverlaps = checkTimePeriodsForOverlaps; 158 | 159 | /* ------------------------------------------------- Other functions ------------------------------------------------- */ 160 | 161 | /* Get the text representation of the current timeZone */ 162 | 163 | function getTimeZoneText () { 164 | 165 | let timeZoneText = 'UTC'; 166 | 167 | if (storedTimeZoneOffset === 0) return timeZoneText; 168 | 169 | const timeZoneOffsetHours = storedTimeZoneOffset < 0 ? Math.ceil(storedTimeZoneOffset / constants.MINUTES_IN_HOUR) : Math.floor(storedTimeZoneOffset / constants.MINUTES_IN_HOUR); 170 | 171 | const timeZoneOffsetMins = Math.abs(storedTimeZoneOffset % constants.MINUTES_IN_HOUR); 172 | 173 | timeZoneText += storedTimeZoneOffset > 0 ? '+' : '-'; 174 | 175 | timeZoneText += Math.abs(timeZoneOffsetHours); 176 | 177 | if (timeZoneOffsetMins > 0) timeZoneText += ':' + ('00' + timeZoneOffsetMins).slice(-2); 178 | 179 | return timeZoneText; 180 | 181 | } 182 | 183 | exports.getTimeZoneText = getTimeZoneText; 184 | 185 | /* Pad the left of each time with zeroes */ 186 | 187 | function pad (n) { 188 | 189 | return (n < 10) ? ('0' + n) : n; 190 | 191 | } 192 | 193 | /* Convert the number of minutes through a day to a HH:MM formatted string */ 194 | 195 | function minsToTimeString (mins) { 196 | 197 | const timeHours = Math.floor(mins / constants.MINUTES_IN_HOUR); 198 | 199 | return pad(timeHours) + ':' + pad((mins - (timeHours * constants.MINUTES_IN_HOUR))); 200 | 201 | } 202 | 203 | exports.minsToTimeString = minsToTimeString; 204 | -------------------------------------------------------------------------------- /schedule/scheduleEditor.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * scheduleEditor.js 3 | * openacousticdevices.info 4 | * November 2019 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | const ui = require('../ui.js'); 10 | const timeHandler = require('../timeHandler.js'); 11 | const constants = require('../constants.js'); 12 | 13 | /* Remove a time from the recording period data structure and update UI to reflect change */ 14 | 15 | function removeTime (timePeriod, tps) { 16 | 17 | const startMins = timePeriod.startMins; 18 | 19 | for (let i = 0; i < tps.length; i++) { 20 | 21 | if (tps[i].startMins === startMins) { 22 | 23 | tps.splice(i, 1); 24 | 25 | } 26 | 27 | } 28 | 29 | return tps; 30 | 31 | } 32 | 33 | exports.removeTime = removeTime; 34 | 35 | /* Check to see if two periods of time overlap */ 36 | 37 | function isSubset (startTime1, endTime1, startTime2, endTime2) { 38 | 39 | const a = startTime1 < endTime1 && startTime2 < endTime2 && startTime2 >= startTime1 && endTime2 <= endTime1; 40 | const b = startTime1 > endTime1 && startTime2 < endTime2 && startTime2 >= startTime1 && (endTime2 >= startTime1 || endTime2 <= endTime1); 41 | const c = startTime1 > endTime1 && startTime2 < endTime2 && startTime2 <= endTime1 && endTime2 <= endTime1; 42 | const d = startTime1 > endTime1 && startTime2 > endTime2 && startTime2 > startTime1 && endTime2 < endTime1; 43 | const e = startTime1 === endTime1; 44 | 45 | return a || b || c || d || e; 46 | 47 | } 48 | 49 | function isSuperset (startTime1, endTime1, startTime2, endTime2) { 50 | 51 | return isSubset(startTime2, endTime2, startTime1, endTime1); 52 | 53 | } 54 | 55 | function startEndOverlaps (startTime1, endTime1, startTime2, endTime2) { 56 | 57 | const a = startTime1 < endTime1 && startTime2 > endTime2 && endTime2 >= startTime1 && startTime2 <= endTime1; 58 | const b = startTime1 > endTime1 && startTime2 < endTime2 && startTime2 <= endTime1 && endTime2 >= startTime1; 59 | const c = startTime1 > endTime1 && startTime2 > endTime2 && endTime2 >= startTime1 && startTime2 >= endTime1; 60 | const d = startTime1 > endTime1 && startTime2 > endTime2 && endTime2 <= startTime1 && startTime2 <= endTime1; 61 | 62 | return a || b || c || d; 63 | 64 | } 65 | 66 | function startOverlaps (startTime1, endTime1, startTime2, endTime2) { 67 | 68 | const a = startTime1 < endTime1 && startTime2 >= startTime1 && startTime2 <= endTime1; 69 | const b = startTime1 > endTime1 && startTime2 >= startTime1; 70 | const c = startTime1 > endTime1 && startTime2 <= endTime1; 71 | 72 | return a || b || c; 73 | 74 | } 75 | 76 | function endOverlaps (startTime1, endTime1, startTime2, endTime2) { 77 | 78 | const a = startTime1 < endTime1 && endTime2 >= startTime1 && endTime2 <= endTime1; 79 | const b = startTime1 > endTime1 && endTime2 >= startTime1; 80 | const c = startTime1 > endTime1 && endTime2 <= endTime1; 81 | 82 | return a || b || c; 83 | 84 | } 85 | 86 | /* Add a new recording period to the data structure and update UI */ 87 | 88 | function addTime (startMins, endMins, timePeriods) { 89 | 90 | endMins = endMins === constants.MINUTES_IN_DAY ? 0 : endMins; 91 | 92 | let newStart, newEnd; 93 | 94 | if (startMins === endMins) { 95 | 96 | timePeriods = []; 97 | 98 | timePeriods.push({ 99 | startMins, 100 | endMins 101 | }); 102 | 103 | return timePeriods; 104 | 105 | } 106 | 107 | for (let i = 0; i < timePeriods.length; i++) { 108 | 109 | const existingStartMins = timePeriods[i].startMins; 110 | let existingEndMins = timePeriods[i].endMins; 111 | existingEndMins = existingEndMins === constants.MINUTES_IN_DAY ? 0 : existingEndMins; 112 | 113 | /* Check if the new period is just a time inside an existing period */ 114 | 115 | if (isSubset(existingStartMins, existingEndMins, startMins, endMins)) { 116 | 117 | console.log('Subset'); 118 | 119 | return timePeriods; 120 | 121 | } 122 | 123 | if (isSuperset(existingStartMins, existingEndMins, startMins, endMins)) { 124 | 125 | console.log('Superset'); 126 | 127 | timePeriods = removeTime(timePeriods[i], timePeriods); 128 | 129 | timePeriods = addTime(startMins, endMins, timePeriods); 130 | 131 | return timePeriods; 132 | 133 | } 134 | 135 | if (startEndOverlaps(existingStartMins, existingEndMins, startMins, endMins)) { 136 | 137 | console.log('Start and end overlaps'); 138 | 139 | timePeriods = removeTime(timePeriods[i], timePeriods); 140 | 141 | timePeriods = addTime(startMins, startMins, timePeriods); 142 | 143 | return timePeriods; 144 | 145 | } 146 | 147 | if (startOverlaps(existingStartMins, existingEndMins, startMins, endMins)) { 148 | 149 | console.log('Start overlaps'); 150 | 151 | newStart = existingStartMins; 152 | newEnd = endMins; 153 | 154 | timePeriods = removeTime(timePeriods[i], timePeriods); 155 | 156 | timePeriods = addTime(newStart, newEnd, timePeriods); 157 | 158 | return timePeriods; 159 | 160 | } 161 | 162 | if (endOverlaps(existingStartMins, existingEndMins, startMins, endMins)) { 163 | 164 | console.log('End overlaps'); 165 | 166 | newStart = startMins; 167 | newEnd = existingEndMins; 168 | 169 | timePeriods = removeTime(timePeriods[i], timePeriods); 170 | 171 | timePeriods = addTime(newStart, newEnd, timePeriods); 172 | 173 | return timePeriods; 174 | 175 | } 176 | 177 | } 178 | 179 | timePeriods.push({ 180 | startMins, 181 | endMins 182 | }); 183 | 184 | return timePeriods; 185 | 186 | } 187 | 188 | function formatAndAddTime (startTimestamp, endTimestamp, timePeriods) { 189 | 190 | let utcPeriod; 191 | 192 | const timePeriod = { 193 | startMins: startTimestamp, 194 | endMins: endTimestamp 195 | }; 196 | 197 | const timeZoneMode = ui.getTimeZoneMode(); 198 | 199 | if (timeZoneMode === constants.TIME_ZONE_MODE_UTC) { 200 | 201 | utcPeriod = { 202 | startMins: (timePeriod.startMins % constants.MINUTES_IN_DAY), 203 | endMins: (timePeriod.endMins % constants.MINUTES_IN_DAY) 204 | }; 205 | 206 | } else { 207 | 208 | utcPeriod = timeHandler.shiftTimePeriod(timePeriod, true); 209 | 210 | } 211 | 212 | startTimestamp = utcPeriod.startMins; 213 | endTimestamp = utcPeriod.endMins; 214 | 215 | timePeriods = addTime(startTimestamp, endTimestamp, timePeriods); 216 | 217 | return timePeriods; 218 | 219 | } 220 | 221 | exports.addTime = addTime; 222 | exports.formatAndAddTime = formatAndAddTime; 223 | -------------------------------------------------------------------------------- /schedule/uiScheduleEditor.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiScheduleEditor.js 3 | * openacousticdevices.info 4 | * November 2019 5 | *****************************************************************************/ 6 | 7 | /* global Event */ 8 | 9 | const electron = require('electron'); 10 | 11 | const ariaSpeak = require('../ariaSpeak.js'); 12 | 13 | const schedule = require('../schedule/schedule.js'); 14 | const scheduleEditor = require('./scheduleEditor.js'); 15 | const scheduleBar = require('../scheduleBar.js'); 16 | const timeHandler = require('../timeHandler.js'); 17 | const ui = require('../ui.js'); 18 | const timeInput = require('./timeInput.js'); 19 | const constants = require('../constants.js'); 20 | 21 | /* UI components */ 22 | 23 | const timeList = document.getElementById('time-list'); 24 | const addTimeButton = document.getElementById('add-time-button'); 25 | const removeTimeButton = document.getElementById('remove-time-button'); 26 | const clearTimeButton = document.getElementById('clear-time-button'); 27 | 28 | const startTimeInput = timeInput.create('start-time-input', true); 29 | const endTimeInput = timeInput.create('end-time-input', true); 30 | 31 | const endTimeTextInput = timeInput.getTextInput(endTimeInput); 32 | timeInput.setNextElements(startTimeInput, [endTimeTextInput]); 33 | timeInput.setNextElements(endTimeInput, [addTimeButton, timeList]); 34 | 35 | /* Function which uses changed schedule to update life approximation */ 36 | 37 | let updateLifeDisplayOnChange; 38 | 39 | /* Pad the left of each time with zeroes */ 40 | 41 | function pad (n) { 42 | 43 | return (n < 10) ? ('0' + n) : n; 44 | 45 | } 46 | 47 | /* Obtain time periods from UI and add to data structure in response to button press */ 48 | 49 | function addTimeOnClick () { 50 | 51 | const startTimeSplit = timeInput.getValue(startTimeInput).split(':'); 52 | const endTimeSplit = timeInput.getValue(endTimeInput).split(':'); 53 | 54 | const startHours = parseInt(startTimeSplit[0], 10); 55 | const startMins = parseInt(startTimeSplit[1], 10); 56 | const endHours = parseInt(endTimeSplit[0], 10); 57 | const endMins = parseInt(endTimeSplit[1], 10); 58 | 59 | const startTimestamp = (startHours * constants.MINUTES_IN_HOUR) + startMins; 60 | let endTimestamp = (endHours * constants.MINUTES_IN_HOUR) + endMins; 61 | 62 | endTimestamp = (endTimestamp > 0) ? endTimestamp : 0; 63 | 64 | ariaSpeak.speak(pad(startHours) + ':' + pad(startMins) + ' to ' + pad(endHours) + ':' + pad(endMins)); 65 | 66 | let timePeriods = schedule.getTimePeriods(); 67 | timePeriods = scheduleEditor.formatAndAddTime(startTimestamp, endTimestamp, timePeriods); 68 | schedule.setTimePeriods(timePeriods); 69 | 70 | scheduleBar.updateCanvas(); 71 | updateTimeList(); 72 | scheduleBar.clearSelectedPeriod(); 73 | 74 | } 75 | 76 | function getTimePeriodFromList () { 77 | 78 | const values = timeList.value.split(','); 79 | 80 | const timePeriod = { 81 | startMins: parseInt(values[0], 10), 82 | endMins: parseInt(values[1], 10) 83 | }; 84 | 85 | /* Allows time list to display 24:00 as 0:00 */ 86 | timePeriod.endMins = (timePeriod.endMins === 0) ? constants.MINUTES_IN_DAY : 0; 87 | 88 | return timePeriod; 89 | 90 | } 91 | 92 | function removeTimeOnClick () { 93 | 94 | let timePeriods = schedule.getTimePeriods(); 95 | 96 | const timePeriod = getTimePeriodFromList(); 97 | 98 | const timeZoneMode = ui.getTimeZoneMode(); 99 | 100 | if (timeZoneMode === constants.TIME_ZONE_MODE_UTC) { 101 | 102 | timePeriods = scheduleEditor.removeTime(timePeriod, timePeriods); 103 | 104 | } else { 105 | 106 | let ltps = timeHandler.shiftTimePeriods(timePeriods, false); 107 | ltps = scheduleEditor.removeTime(timePeriod, ltps); 108 | 109 | timePeriods = timeHandler.shiftTimePeriods(ltps, true); 110 | 111 | } 112 | 113 | schedule.setTimePeriods(timePeriods); 114 | 115 | scheduleBar.updateCanvas(); 116 | updateTimeList(); 117 | removeTimeButton.disabled = true; 118 | scheduleBar.clearSelectedPeriod(); 119 | 120 | } 121 | 122 | /* Fill UI list with time periods from data structure */ 123 | 124 | function updateTimeList () { 125 | 126 | let tp; 127 | 128 | const timePeriods = schedule.getTimePeriods(); 129 | 130 | const timeZoneMode = ui.getTimeZoneMode(); 131 | 132 | if (timeZoneMode === constants.TIME_ZONE_MODE_UTC) { 133 | 134 | tp = timePeriods; 135 | 136 | } else { 137 | 138 | tp = timeHandler.shiftTimePeriods(timePeriods, false); 139 | 140 | } 141 | 142 | timeList.options.length = 0; 143 | 144 | /* Sort recording periods in order of occurrence */ 145 | 146 | tp = timeHandler.sortPeriods(tp); 147 | 148 | for (let i = 0; i < tp.length; i += 1) { 149 | 150 | const startMins = tp[i].startMins; 151 | let endMins = tp[i].endMins; 152 | endMins = endMins === 0 ? constants.MINUTES_IN_DAY : endMins; 153 | 154 | const timeZoneText = '(' + timeHandler.getTimeZoneText() + ')'; 155 | 156 | const option = document.createElement('option'); 157 | option.text = timeHandler.minsToTimeString(startMins) + ' - ' + timeHandler.minsToTimeString(endMins) + ' ' + timeZoneText; 158 | option.value = [startMins, endMins]; 159 | timeList.add(option); 160 | 161 | } 162 | 163 | /* Disable or enable action buttons in response to number time periods entered */ 164 | 165 | addTimeButton.disabled = (tp.length >= constants.MAX_PERIODS); 166 | clearTimeButton.disabled = (tp.length === 0); 167 | 168 | updateLifeDisplayOnChange(); 169 | 170 | } 171 | 172 | exports.updateTimeList = updateTimeList; 173 | 174 | electron.ipcRenderer.on('update-schedule', updateTimeList); 175 | 176 | function clearTimesOnClick () { 177 | 178 | schedule.setTimePeriods([]); 179 | scheduleBar.updateCanvas(); 180 | updateTimeList(); 181 | removeTimeButton.disabled = true; 182 | scheduleBar.clearSelectedPeriod(); 183 | 184 | } 185 | 186 | exports.disableRemoveTimeButton = () => { 187 | 188 | removeTimeButton.disabled = true; 189 | 190 | }; 191 | 192 | exports.prepareUI = (changeFunction) => { 193 | 194 | updateLifeDisplayOnChange = changeFunction; 195 | 196 | addTimeButton.addEventListener('click', addTimeOnClick); 197 | removeTimeButton.addEventListener('click', removeTimeOnClick); 198 | clearTimeButton.addEventListener('click', clearTimesOnClick); 199 | 200 | timeList.addEventListener('change', () => { 201 | 202 | removeTimeButton.disabled = (timeList.value === null || timeList.value === ''); 203 | 204 | if (timeList.value !== null && timeList.value !== '') { 205 | 206 | const valueSplit = timeList.value.split(','); 207 | const selectedTimePeriod = {startMins: parseInt(valueSplit[0]), endMins: parseInt(valueSplit[1])}; 208 | scheduleBar.setSelectedPeriod(selectedTimePeriod); 209 | 210 | } else { 211 | 212 | scheduleBar.clearSelectedPeriod(); 213 | 214 | } 215 | 216 | }); 217 | 218 | timeInput.setValue(endTimeInput, 24, 0); 219 | 220 | }; 221 | 222 | scheduleBar.prepareScheduleCanvas((selectedIndex) => { 223 | 224 | timeList.options.selectedIndex = selectedIndex; 225 | timeList.focus(); 226 | timeList.dispatchEvent(new Event('change')); 227 | 228 | }); 229 | -------------------------------------------------------------------------------- /processing/uiSummarise.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiSummarise.js 3 | * openacousticdevices.info 4 | * September 2023 5 | *****************************************************************************/ 6 | 7 | const electron = require('electron'); 8 | const {dialog, shell} = require('@electron/remote'); 9 | 10 | /* Get common functions to add night mode functionality */ 11 | require('./uiCommon.js'); 12 | 13 | const uiInput = require('./uiInput.js'); 14 | 15 | const audiomothUtils = require('audiomoth-utils'); 16 | 17 | const path = require('path'); 18 | 19 | const fileLabel = document.getElementById('file-label'); 20 | const fileButton = document.getElementById('file-button'); 21 | 22 | const outputCheckbox = document.getElementById('output-checkbox'); 23 | const outputButton = document.getElementById('output-button'); 24 | const outputLabel = document.getElementById('output-label'); 25 | 26 | const summariseButton = document.getElementById('summarise-button'); 27 | 28 | let files = []; 29 | let outputDir = ''; 30 | let sourceDir = ''; 31 | let selection = []; 32 | 33 | let summarising = false; 34 | 35 | /* Disable UI elements in main window while progress bar is open and downsample is in progress */ 36 | 37 | function disableUI () { 38 | 39 | fileButton.disabled = true; 40 | summariseButton.disabled = true; 41 | 42 | outputButton.disabled = true; 43 | 44 | } 45 | 46 | function enableUI () { 47 | 48 | fileButton.disabled = false; 49 | summariseButton.disabled = false; 50 | 51 | if (outputCheckbox.checked) { 52 | 53 | outputButton.disabled = false; 54 | 55 | } 56 | 57 | summarising = false; 58 | 59 | } 60 | 61 | /* Update label to reflect new file/folder selection */ 62 | 63 | function updateInputDirectoryDisplay (directoryArray) { 64 | 65 | if (!directoryArray) { 66 | 67 | fileLabel.innerHTML = 'No folder selected.'; 68 | summariseButton.disabled = true; 69 | 70 | } else if (directoryArray.length === 0) { 71 | 72 | fileLabel.innerHTML = 'No files found.'; 73 | summariseButton.disabled = true; 74 | 75 | } else { 76 | 77 | fileLabel.innerHTML = 'Found '; 78 | fileLabel.innerHTML += directoryArray.length + ' file'; 79 | fileLabel.innerHTML += (directoryArray.length === 1 ? '' : 's'); 80 | fileLabel.innerHTML += '.'; 81 | summariseButton.disabled = false; 82 | 83 | } 84 | 85 | } 86 | 87 | /* Summary file output */ 88 | 89 | /* Update label to notify user if a custom output directtory is being used */ 90 | 91 | function updateOutputLabel () { 92 | 93 | if (outputDir === '' || !outputCheckbox.checked) { 94 | 95 | outputLabel.textContent = 'Writing summary file to source folder.'; 96 | 97 | } else { 98 | 99 | outputLabel.textContent = 'Writing summary file to destination folder.'; 100 | 101 | } 102 | 103 | }; 104 | 105 | /* Add listener which handles enabling/disabling custom output directory UI */ 106 | 107 | outputCheckbox.addEventListener('change', () => { 108 | 109 | updateOutputLabel(); 110 | 111 | outputButton.disabled = !outputCheckbox.checked; 112 | 113 | }); 114 | 115 | outputButton.addEventListener('click', () => { 116 | 117 | const destinationName = dialog.showOpenDialogSync({ 118 | title: 'Select Destination', 119 | nameFieldLabel: 'Destination', 120 | multiSelections: false, 121 | properties: ['openDirectory'] 122 | }); 123 | 124 | if (destinationName !== undefined) { 125 | 126 | outputDir = destinationName[0]; 127 | 128 | } 129 | 130 | updateOutputLabel(); 131 | 132 | }); 133 | 134 | /* Select/process file(s) buttons */ 135 | 136 | fileButton.addEventListener('click', () => { 137 | 138 | const response = uiInput.selectAllFilesInFolder(); 139 | 140 | if (response) { 141 | 142 | selection = response.selection; 143 | 144 | files = response.files; 145 | 146 | sourceDir = response.folder; 147 | 148 | } 149 | 150 | updateInputDirectoryDisplay(files); 151 | 152 | }); 153 | 154 | /* Summarise functionality */ 155 | 156 | function summariseFiles () { 157 | 158 | if (!files) { 159 | 160 | return; 161 | 162 | } 163 | 164 | let successCount = 0; 165 | 166 | audiomothUtils.summariser.initialise(); 167 | 168 | for (let i = 0; i < files.length; i++) { 169 | 170 | /* If progress bar is closed, the summarise task is considered cancelled. This will contact the main thread and ask if that has happened */ 171 | 172 | const cancelled = electron.ipcRenderer.sendSync('poll-summarise-cancelled'); 173 | 174 | if (cancelled) { 175 | 176 | console.log('Summary cancelled.'); 177 | enableUI(); 178 | return; 179 | 180 | } 181 | 182 | /* Let the main thread know what value to set the progress bar to */ 183 | 184 | electron.ipcRenderer.send('set-summarise-bar-progress', i, 0); 185 | 186 | console.log('Summarising:', files[i]); 187 | console.log('-'); 188 | 189 | const summariseSuccess = audiomothUtils.summariser.summarise(sourceDir, files[i], (progress) => { 190 | 191 | electron.ipcRenderer.send('set-summarise-bar-progress', i, progress); 192 | electron.ipcRenderer.send('set-summarise-bar-file', i, path.basename(files[i])); 193 | 194 | }); 195 | 196 | if (summariseSuccess) { 197 | 198 | successCount++; 199 | 200 | } 201 | 202 | } 203 | 204 | /* Attempt to write summary file */ 205 | 206 | /* If no output path given, use the containing directory of the input files */ 207 | 208 | const outputPath = outputCheckbox.checked && outputDir !== '' ? outputDir : sourceDir; 209 | 210 | const finaliseResult = audiomothUtils.summariser.finalise(outputPath); 211 | 212 | /* Notify main thread that summarise is complete so progress bar is closed */ 213 | 214 | electron.ipcRenderer.send('set-summarise-bar-completed', successCount, finaliseResult); 215 | 216 | /* Show summarise file location */ 217 | 218 | try { 219 | 220 | shell.showItemInFolder(path.join(outputPath, 'SUMMARY.CSV')); 221 | 222 | } catch (error) { 223 | 224 | console.error('Failed to show summary file in file explorer'); 225 | 226 | } 227 | 228 | } 229 | 230 | summariseButton.addEventListener('click', () => { 231 | 232 | if (summarising) { 233 | 234 | return; 235 | 236 | } 237 | 238 | const response = uiInput.updateFilesInFolder(selection); 239 | 240 | if (response) { 241 | 242 | files = response.files; 243 | sourceDir = response.folder; 244 | 245 | updateInputDirectoryDisplay(files); 246 | 247 | summarising = true; 248 | disableUI(); 249 | 250 | electron.ipcRenderer.send('start-summarise-bar', files.length); 251 | setTimeout(summariseFiles, 2000); 252 | 253 | } else { 254 | 255 | dialog.showMessageBoxSync({ 256 | type: 'error', 257 | title: 'Folder Not Found', 258 | message: 'Selected folder no longer exists. Select a new location and try again.' 259 | }); 260 | 261 | files = []; 262 | sourceDir = ''; 263 | selection = []; 264 | 265 | updateInputDirectoryDisplay(files); 266 | 267 | } 268 | 269 | }); 270 | 271 | /* When the progress bar is complete and the summary window at the end has been displayed for a fixed amount of time, it will close and this re-enables the UI */ 272 | 273 | electron.ipcRenderer.on('summarise-summary-closed', enableUI); 274 | -------------------------------------------------------------------------------- /VerticalTabs/verticalTabs.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Responsive vertical tabs for Bootstrap 5 3 | * 4 | * Main stylesheet. @import "custom-variables.sccss" before 5 | * this file to compile into final CSS 6 | * 7 | * Copyright (c) 2020-2021 Tromgy (tromgy@yahoo.com) 8 | * 9 | * This software is supplied under the terms of the MIT License, a 10 | * copy of which should be located in the distribution where this 11 | * file was obtained (LICENSE.txt). A copy of the license may also be 12 | * found online at https://opensource.org/licenses/MIT. 13 | * 14 | */ 15 | .right-tabs.sideways-tabs, .left-tabs.sideways-tabs { 16 | margin-top: 5rem; 17 | border: none; 18 | position: relative; 19 | margin-bottom: 0; 20 | } 21 | 22 | .right-tabs.sideways-tabs, .left-tabs.sideways-tabs, .right-tabs.nav-tabs, .left-tabs.nav-tabs { 23 | height: 100%; 24 | flex-direction: column; 25 | } 26 | 27 | .ellipsis { 28 | white-space: nowrap; 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | padding-right: 0.5rem; 32 | } 33 | 34 | .vtabs .tab-clickable { 35 | cursor: pointer; 36 | } 37 | 38 | .left-tabs.nav-tabs { 39 | border-right: var(--bs-border-width) solid var(--bs-border-color); 40 | border-bottom: none; 41 | } 42 | 43 | .left-tabs .nav-link { 44 | border-top-left-radius: var(--bs-border-radius); 45 | border-bottom-left-radius: var(--bs-border-radius); 46 | border-bottom-right-radius: 0; 47 | border-top-right-radius: 0; 48 | margin-right: -var(--bs-border-width); 49 | text-align: left; 50 | } 51 | 52 | .left-tabs .nav-link:hover { 53 | border-right: var(--bs-border-width) solid transparent; 54 | } 55 | 56 | .left-tabs .nav-link.active { 57 | border-top: var(--bs-border-width) solid var(--bs-border-color); 58 | border-right: var(--bs-border-width) solid transparent; 59 | border-bottom: var(--bs-border-width) solid var(--bs-border-color); 60 | border-left: var(--bs-border-width) solid var(--bs-border-color); 61 | } 62 | 63 | .right-tabs.nav-tabs { 64 | border-left: var(--bs-border-width) solid var(--bs-border-color); 65 | border-bottom: none; 66 | } 67 | 68 | .right-tabs .nav-link { 69 | border-top-right-radius: var(--bs-border-radius); 70 | border-bottom-right-radius: var(--bs-border-radius); 71 | border-bottom-left-radius: 0; 72 | border-top-left-radius: 0; 73 | margin-left: -var(--bs-border-width); 74 | text-align: right; 75 | } 76 | 77 | .right-tabs .nav-link:hover { 78 | border-left: var(--bs-border-width) solid transparent; 79 | } 80 | 81 | .right-tabs .nav-link.active { 82 | border-top: var(--bs-border-width) solid var(--bs-border-color); 83 | border-right: var(--bs-border-width) solid var(--bs-border-color); 84 | border-bottom: var(--bs-border-width) solid var(--bs-border-color); 85 | border-left: var(--bs-border-width) solid transparent; 86 | } 87 | 88 | .left-tabs.sideways-tabs { 89 | border-right: none; 90 | left: -3.2rem; 91 | } 92 | 93 | .sideways-tabs.left-tabs .nav-item { 94 | transform: rotate(-90deg); 95 | height: 1rem; 96 | margin-bottom: calc(92px - 1rem); 97 | } 98 | 99 | .sideways-tabs.left-tabs .nav-link { 100 | width: 92px; 101 | text-align: center; 102 | white-space: nowrap; 103 | overflow: hidden; 104 | text-overflow: ellipsis; 105 | border-top-right-radius: var(--bs-border-radius); 106 | border-bottom-right-radius: 0; 107 | border-bottom-left-radius: 0; 108 | border-top-left-radius: var(--bs-border-radius); 109 | border-bottom: var(--bs-border-width) solid var(--bs-border-color); 110 | } 111 | 112 | .sideways-tabs.left-tabs .nav-link:hover { 113 | border-right: var(--bs-border-width) solid #e9ecef; 114 | } 115 | 116 | .sideways-tabs.left-tabs .nav-link.active { 117 | border-top: var(--bs-border-width) solid var(--bs-border-color); 118 | border-right: var(--bs-border-width) solid var(--bs-border-color); 119 | border-bottom: var(--bs-border-width) solid transparent; 120 | border-left: var(--bs-border-width) solid var(--bs-border-color); 121 | } 122 | 123 | .right-tabs.sideways-tabs { 124 | border-left: none; 125 | right: 3.2rem; 126 | } 127 | 128 | .sideways-tabs.right-tabs .nav-item { 129 | transform: rotate(90deg); 130 | height: 1rem; 131 | margin-bottom: calc(92px - 1rem); 132 | } 133 | 134 | .sideways-tabs.right-tabs .nav-link { 135 | width: 92px; 136 | text-align: center; 137 | white-space: nowrap; 138 | overflow: hidden; 139 | text-overflow: ellipsis; 140 | border-top-right-radius: var(--bs-border-radius); 141 | border-bottom-right-radius: 0; 142 | border-bottom-left-radius: 0; 143 | border-top-left-radius: var(--bs-border-radius); 144 | border-bottom: var(--bs-border-width) solid var(--bs-border-color); 145 | } 146 | 147 | .sideways-tabs.right-tabs .nav-link:hover { 148 | border-left: var(--bs-border-width) solid #e9ecef; 149 | } 150 | 151 | .sideways-tabs.right-tabs .nav-link.active { 152 | border-top: var(--bs-border-width) solid var(--bs-border-color); 153 | border-right: var(--bs-border-width) solid var(--bs-border-color); 154 | border-bottom: var(--bs-border-width) solid transparent; 155 | border-left: var(--bs-border-width) solid var(--bs-border-color); 156 | } 157 | 158 | .vtabs .accordion-header { 159 | display: none; 160 | } 161 | 162 | @media (max-width: 400px) { 163 | .left-tabs.nav-tabs { 164 | flex-direction: row; 165 | border-right: none; 166 | border-left: none; 167 | min-width: 100%; 168 | border-bottom: var(--bs-border-width) solid var(--bs-border-color); 169 | left: auto; 170 | margin-top: auto; 171 | } 172 | .left-tabs .nav-link { 173 | width: 92px; 174 | text-align: center; 175 | white-space: nowrap; 176 | overflow: hidden; 177 | text-overflow: ellipsis; 178 | border-top-right-radius: var(--bs-border-radius); 179 | border-bottom-right-radius: 0; 180 | border-bottom-left-radius: 0; 181 | border-top-left-radius: var(--bs-border-radius); 182 | margin-right: 0; 183 | margin-bottom: -var(--bs-border-width); 184 | } 185 | .left-tabs .nav-link.nav-link:hover { 186 | border-right-color: var(--bs-border-color); 187 | border-bottom-color: transparent; 188 | } 189 | .left-tabs .nav-link.active { 190 | border-top-color: var(--bs-border-color); 191 | border-right-color: var(--bs-border-color); 192 | border-bottom-color: transparent; 193 | border-left-color: var(--bs-border-color); 194 | } 195 | .sideways-tabs.left-tabs .nav-item, 196 | .sideways-tabs.right-tabs .nav-item { 197 | transform: none; 198 | height: auto; 199 | width: auto; 200 | margin-bottom: 0; 201 | } 202 | .right-tabs.nav-tabs { 203 | flex-direction: row; 204 | border-right: none; 205 | border-left: none; 206 | min-width: 100%; 207 | border-top: var(--bs-border-width) solid var(--bs-border-color); 208 | right: auto; 209 | margin-top: auto; 210 | } 211 | .sideways-tabs.right-tabs .nav-link, 212 | .right-tabs .nav-link { 213 | width: 92px; 214 | text-align: center; 215 | white-space: nowrap; 216 | overflow: hidden; 217 | text-overflow: ellipsis; 218 | border-top-right-radius: 0; 219 | border-bottom-right-radius: var(--bs-border-radius); 220 | border-bottom-left-radius: var(--bs-border-radius); 221 | border-top-left-radius: 0; 222 | margin-left: 0; 223 | margin-top: -var(--bs-border-width); 224 | border-bottom-color: transparent; 225 | } 226 | .right-tabs .nav-link:hover { 227 | border-top-color: transparent; 228 | border-left-color: var(--bs-border-color); 229 | border-bottom-color: #e9ecef; 230 | } 231 | .sideways-tabs.right-tabs .nav-link.active, 232 | .right-tabs .nav-link.active { 233 | border-top-color: transparent; 234 | border-right-color: var(--bs-border-color); 235 | border-bottom-color: var(--bs-border-color); 236 | border-left-color: var(--bs-border-color); 237 | } 238 | } 239 | @media (max-width: 350px) { 240 | .left-tabs.nav-tabs { 241 | display: none; 242 | } 243 | .right-tabs.nav-tabs { 244 | display: none; 245 | } 246 | .vtabs .tab-content > .tab-pane { 247 | display: block !important; 248 | opacity: 1; 249 | } 250 | .vtabs .accordion-header { 251 | display: block; 252 | } 253 | .vtabs button.accordion-button:focus { 254 | border: none; 255 | outline: none; 256 | box-shadow: none; 257 | } 258 | } 259 | @media (min-width: 351px) { 260 | .vtabs .accordion-item { 261 | border: none; 262 | } 263 | .vtabs .accordion-body.collapse { 264 | display: block; 265 | } 266 | } 267 | 268 | /*# sourceMappingURL=verticalTabs.css.map */ 269 | -------------------------------------------------------------------------------- /scheduleBar.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * scheduleBar.js 3 | * openacousticdevices.info 4 | * November 2019 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | /* global window, document */ 10 | 11 | const ui = require('./ui.js'); 12 | const timeHandler = require('./timeHandler.js'); 13 | const schedule = require('./schedule/schedule.js'); 14 | const constants = require('./constants.js'); 15 | const uiSun = require('./schedule/uiSun.js'); 16 | 17 | const timeCanvas = document.getElementById('time-canvas'); 18 | const timeContext = timeCanvas.getContext('2d'); 19 | const labelCanvas = document.getElementById('label-canvas'); 20 | const labelContext = labelCanvas.getContext('2d'); 21 | 22 | let scaleFactor; 23 | 24 | const sunCanvas = document.getElementById('sun-canvas'); 25 | 26 | let clickCallback; 27 | 28 | let selectedPeriod = null; 29 | 30 | /* Function to rescale */ 31 | 32 | function rescale (canvas) { 33 | 34 | scaleFactor = 1; 35 | 36 | if (Object.prototype.hasOwnProperty.call(window, 'devicePixelRatio')) { 37 | 38 | if (window.devicePixelRatio > 1) { 39 | 40 | scaleFactor = window.devicePixelRatio; 41 | 42 | } 43 | 44 | } 45 | 46 | if (scaleFactor > 1) { 47 | 48 | canvas.width = canvas.width * scaleFactor; 49 | canvas.height = canvas.height * scaleFactor; 50 | 51 | canvas.style.width = canvas.width / scaleFactor + 'px'; 52 | canvas.style.height = canvas.height / scaleFactor + 'px'; 53 | 54 | } 55 | 56 | } 57 | 58 | function drawPeriod (startMins, endMins, timeCanvas) { 59 | 60 | /* width / 1440 minutes */ 61 | const recX = startMins * timeCanvas.width / constants.MINUTES_IN_DAY; 62 | const recLen = (endMins - startMins) * timeCanvas.width / constants.MINUTES_IN_DAY; 63 | 64 | timeContext.fillRect(Math.round(recX), 0, Math.round(recLen), timeCanvas.height); 65 | 66 | } 67 | 68 | function updateCanvas () { 69 | 70 | let timePeriods = schedule.getTimePeriods(); 71 | 72 | const timeZoneMode = ui.getTimeZoneMode(); 73 | 74 | if (timeZoneMode !== constants.TIME_ZONE_MODE_UTC) { 75 | 76 | timePeriods = timeHandler.shiftTimePeriods(timePeriods, false); 77 | 78 | } 79 | 80 | const currentTimeDate = new Date(); 81 | 82 | timeContext.clearRect(0, 0, timeCanvas.width, timeCanvas.height); 83 | 84 | if (uiSun.usingSunSchedule()) { 85 | 86 | uiSun.updateSunUI(); 87 | 88 | } else { 89 | 90 | for (let i = 0; i < timePeriods.length; i++) { 91 | 92 | const startMins = timePeriods[i].startMins; 93 | let endMins = timePeriods[i].endMins; 94 | endMins = endMins === 0 ? constants.MINUTES_IN_DAY : endMins; 95 | 96 | if (selectedPeriod !== null && (selectedPeriod.startMins === startMins && selectedPeriod.endMins === endMins)) { 97 | 98 | timeContext.fillStyle = '#007BFF'; 99 | 100 | } else { 101 | 102 | timeContext.fillStyle = '#FF0000'; 103 | 104 | } 105 | 106 | if (startMins > endMins) { 107 | 108 | drawPeriod(startMins, constants.MINUTES_IN_DAY, timeCanvas); 109 | drawPeriod(0, endMins, timeCanvas); 110 | 111 | } else if (startMins === endMins) { 112 | 113 | drawPeriod(0, constants.MINUTES_IN_DAY, timeCanvas); 114 | 115 | } else { 116 | 117 | drawPeriod(startMins, endMins, timeCanvas); 118 | 119 | } 120 | 121 | } 122 | 123 | } 124 | 125 | /* 6am, midday and 6pm markers */ 126 | 127 | if (ui.isNightMode()) { 128 | 129 | timeContext.fillStyle = '#FFFFFF'; 130 | 131 | } else { 132 | 133 | timeContext.fillStyle = '#000000'; 134 | 135 | } 136 | 137 | timeContext.fillRect(Math.round(0.25 * timeCanvas.width), 0, 1 * scaleFactor, timeCanvas.height); 138 | timeContext.fillRect(Math.round(0.5 * timeCanvas.width), 0, 1 * scaleFactor, timeCanvas.height); 139 | timeContext.fillRect(Math.round(0.75 * timeCanvas.width), 0, 1 * scaleFactor, timeCanvas.height); 140 | 141 | let currentMins = (currentTimeDate.getUTCHours() * constants.MINUTES_IN_HOUR) + currentTimeDate.getUTCMinutes(); 142 | 143 | if (timeZoneMode !== constants.TIME_ZONE_MODE_UTC) { 144 | 145 | currentMins += timeHandler.getTimeZoneOffset(); 146 | 147 | } 148 | 149 | let currentX = currentMins * timeCanvas.width / constants.MINUTES_IN_DAY; 150 | 151 | /* Wrap time around midnight at both ends */ 152 | 153 | currentX = currentX % timeCanvas.width; 154 | currentX = currentX < 0 ? currentX + timeCanvas.width : currentX; 155 | 156 | timeContext.fillStyle = '#00AF00'; 157 | timeContext.fillRect(Math.floor(currentX), 0, 2 * scaleFactor, timeCanvas.height); 158 | 159 | } 160 | 161 | exports.updateCanvas = updateCanvas; 162 | 163 | /* Regularly update time period canvas so green line reflects current time */ 164 | 165 | function updateCanvasTimer () { 166 | 167 | updateCanvas(); 168 | setTimeout(updateCanvasTimer, 60000); 169 | 170 | } 171 | 172 | /* Update which period is selected when schedule bar is clicked */ 173 | 174 | function updateSelectedPeriod (event) { 175 | 176 | let timePeriods = schedule.getTimePeriods(); 177 | 178 | /* If there's only one possible time period and it covers the entire length of the schedule, don't bother with the full check */ 179 | 180 | if (timePeriods.length === 1 && timePeriods[0].startMins === timePeriods[0].endMins) { 181 | 182 | const selectedIndex = 0; 183 | 184 | if (clickCallback) { 185 | 186 | clickCallback(selectedIndex); 187 | 188 | } 189 | 190 | return; 191 | 192 | } 193 | 194 | const rect = timeCanvas.getBoundingClientRect(); 195 | const clickMins = (event.clientX - rect.left) / timeCanvas.width * constants.MINUTES_IN_DAY; 196 | 197 | if (ui.getTimeZoneMode() !== constants.TIME_ZONE_MODE_UTC) { 198 | 199 | timePeriods = timeHandler.shiftTimePeriods(timePeriods, false); 200 | 201 | } 202 | 203 | let selectedIndex = -1; 204 | 205 | for (let i = 0; i < timePeriods.length; i++) { 206 | 207 | const startMins = timePeriods[i].startMins; 208 | const endMins = timePeriods[i].endMins; 209 | 210 | if (startMins > endMins) { 211 | 212 | if ((clickMins >= startMins && clickMins < constants.MINUTES_IN_DAY) || (clickMins >= 0 && clickMins < endMins)) { 213 | 214 | selectedIndex = i; 215 | 216 | } 217 | 218 | } else { 219 | 220 | if (clickMins >= startMins && clickMins < endMins) { 221 | 222 | selectedIndex = i; 223 | 224 | } 225 | 226 | } 227 | 228 | } 229 | 230 | if (clickCallback) { 231 | 232 | clickCallback(selectedIndex); 233 | 234 | } 235 | 236 | } 237 | 238 | /* Set clicked period to specific index */ 239 | 240 | exports.setSelectedPeriod = (period) => { 241 | 242 | selectedPeriod = period; 243 | updateCanvas(); 244 | 245 | }; 246 | 247 | function clearSelectedPeriod () { 248 | 249 | selectedPeriod = null; 250 | updateCanvas(); 251 | 252 | }; 253 | 254 | exports.clearSelectedPeriod = clearSelectedPeriod; 255 | 256 | /* Draw labels below time period canvas */ 257 | 258 | function drawTimeLabels () { 259 | 260 | const fontSize = 0.32 * timeCanvas.height; 261 | 262 | labelContext.clearRect(0, 0, labelCanvas.width, labelCanvas.height); 263 | 264 | labelContext.font = fontSize + 'pt Helvetica'; 265 | 266 | if (ui.isNightMode()) { 267 | 268 | labelContext.fillStyle = '#FFFFFF'; 269 | 270 | } else { 271 | 272 | labelContext.fillStyle = '#000000'; 273 | 274 | } 275 | 276 | labelContext.fillText('00:00', 0, fontSize); 277 | labelContext.fillText('06:00', 0.25 * timeCanvas.width, fontSize); 278 | labelContext.fillText('12:00', 0.5 * timeCanvas.width, fontSize); 279 | labelContext.fillText('18:00', 0.751 * timeCanvas.width, fontSize); 280 | labelContext.fillText('24:00', timeCanvas.width, fontSize); 281 | 282 | } 283 | 284 | exports.drawTimeLabels = drawTimeLabels; 285 | 286 | exports.prepareScheduleCanvas = (callback) => { 287 | 288 | clickCallback = callback; 289 | timeCanvas.addEventListener('click', (event) => { 290 | 291 | if (!uiSun.usingSunSchedule()) { 292 | 293 | updateSelectedPeriod(event); 294 | 295 | } 296 | 297 | }); 298 | 299 | /* Rescale for resolution of screen */ 300 | rescale(timeCanvas); 301 | rescale(labelCanvas); 302 | rescale(sunCanvas); 303 | 304 | /* Draw labels below timeline */ 305 | drawTimeLabels(); 306 | 307 | /* Start recursive loop which keep canvas up to date */ 308 | updateCanvasTimer(); 309 | 310 | }; 311 | 312 | exports.setSchedule = (timePeriods) => { 313 | 314 | let tps = timePeriods; 315 | 316 | tps = timeHandler.sortPeriods(tps); 317 | 318 | schedule.setTimePeriods(tps); 319 | updateCanvas(); 320 | 321 | }; 322 | -------------------------------------------------------------------------------- /settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 26 | 29 | 33 | 36 | 39 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 69 | 72 | 75 | 78 | 79 |
816324896192250384
Sample rate (kHz): 21 | 22 | 24 | 25 | 27 | 28 | 30 | 32 | 34 | 35 | 37 | 38 | 40 | 41 | 43 | 44 |
LowMedHigh
Gain: 64 | 65 | 67 | 68 | 70 | 71 | 73 | 74 | 76 | 77 |
80 |
81 |
82 |
83 | 84 |
85 |
86 | Enable sleep/record cyclic recording: 87 |
88 |
89 | 90 |
91 |
92 | 93 |
94 |
95 | Sleep duration (s): 96 |
97 |
98 |
99 | 100 |
101 |
102 |
103 | 104 |
105 |
106 | Recording duration (s): 107 |
108 |
109 |
110 | 111 |
112 |
113 |
114 |
115 | 116 |
117 |
118 |
119 | Enable LED: 120 |
121 |
122 | 123 |
124 |
125 | 126 |
127 |
128 | Enable low-voltage cut-off: 129 |
130 |
131 | 132 |
133 |
134 | 135 |
136 |
137 | Enable battery level indication: 138 |
139 |
140 | 141 |
142 |
143 |
144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * recordingCOnfigurations.js 3 | * openacousticdevices.info 4 | * December 2019 5 | *****************************************************************************/ 6 | 7 | /* Setting parameters */ 8 | 9 | exports.CONFIGURATIONS = [{ 10 | trueSampleRate: 8, 11 | clockDivider: 4, 12 | acquisitionCycles: 16, 13 | oversampleRate: 1, 14 | sampleRate: 384000, 15 | sampleRateDivider: 48, 16 | recordCurrent: 9.22, 17 | energySaverRecordCurrent: 5.92, 18 | listenCurrent: 8.59, 19 | energySaverListenCurrent: 5.41 20 | }, { 21 | trueSampleRate: 16, 22 | clockDivider: 4, 23 | acquisitionCycles: 16, 24 | oversampleRate: 1, 25 | sampleRate: 384000, 26 | sampleRateDivider: 24, 27 | recordCurrent: 9.83, 28 | energySaverRecordCurrent: 6.63, 29 | listenCurrent: 8.72, 30 | energySaverListenCurrent: 5.54 31 | }, { 32 | trueSampleRate: 32, 33 | clockDivider: 4, 34 | acquisitionCycles: 16, 35 | oversampleRate: 1, 36 | sampleRate: 384000, 37 | sampleRateDivider: 12, 38 | recordCurrent: 11.3, 39 | energySaverRecordCurrent: 8.04, 40 | listenCurrent: 8.95, 41 | energySaverListenCurrent: 5.78 42 | }, { 43 | trueSampleRate: 48, 44 | clockDivider: 4, 45 | acquisitionCycles: 16, 46 | oversampleRate: 1, 47 | sampleRate: 384000, 48 | sampleRateDivider: 8, 49 | recordCurrent: 12.3, 50 | energySaverRecordCurrent: 8.93, 51 | listenCurrent: 9.14, 52 | energySaverListenCurrent: 5.98 53 | }, { 54 | trueSampleRate: 96, 55 | clockDivider: 4, 56 | acquisitionCycles: 16, 57 | oversampleRate: 1, 58 | sampleRate: 384000, 59 | sampleRateDivider: 4, 60 | recordCurrent: 15.8, 61 | energySaverRecordCurrent: 15.8, 62 | listenCurrent: 10.0, 63 | energySaverListenCurrent: 10.0 64 | }, { 65 | trueSampleRate: 192, 66 | clockDivider: 4, 67 | acquisitionCycles: 16, 68 | oversampleRate: 1, 69 | sampleRate: 384000, 70 | sampleRateDivider: 2, 71 | recordCurrent: 24.1, 72 | energySaverRecordCurrent: 24.1, 73 | listenCurrent: 11.5, 74 | energySaverListenCurrent: 11.5 75 | }, { 76 | trueSampleRate: 250, 77 | clockDivider: 4, 78 | acquisitionCycles: 16, 79 | oversampleRate: 1, 80 | sampleRate: 250000, 81 | sampleRateDivider: 1, 82 | recordCurrent: 26.4, 83 | energySaverRecordCurrent: 26.4, 84 | listenCurrent: 10.6, 85 | energySaverListenCurrent: 10.6 86 | }, { 87 | trueSampleRate: 384, 88 | clockDivider: 4, 89 | acquisitionCycles: 16, 90 | oversampleRate: 1, 91 | sampleRate: 384000, 92 | sampleRateDivider: 1, 93 | recordCurrent: 38.5, 94 | energySaverRecordCurrent: 38.5, 95 | listenCurrent: 12.7, 96 | energySaverListenCurrent: 12.7 97 | }]; 98 | 99 | /* Configuration settings to be used when a device is on firmware < 1.4.4 */ 100 | /* Only sent to devices. Not used in energy calculations */ 101 | 102 | exports.OLD_CONFIGURATIONS = [{ 103 | trueSampleRate: 8, 104 | clockDivider: 4, 105 | acquisitionCycles: 16, 106 | oversampleRate: 1, 107 | sampleRate: 128000, 108 | sampleRateDivider: 16 109 | }, { 110 | trueSampleRate: 16, 111 | clockDivider: 4, 112 | acquisitionCycles: 16, 113 | oversampleRate: 1, 114 | sampleRate: 128000, 115 | sampleRateDivider: 8 116 | }, { 117 | trueSampleRate: 32, 118 | clockDivider: 4, 119 | acquisitionCycles: 16, 120 | oversampleRate: 1, 121 | sampleRate: 128000, 122 | sampleRateDivider: 4 123 | }]; 124 | 125 | /* GPS energy consumption */ 126 | 127 | exports.GPS_FIX_TIME = 0.5 / 60.0; 128 | exports.GPS_FIX_CONSUMPTION = 30.0; 129 | 130 | exports.MINIMUM_GPS_FIX_TIME = 30; 131 | 132 | /* Packet lengths for each version */ 133 | 134 | exports.PACKET_LENGTH_VERSIONS = [{ 135 | firmwareVersion: [0, 0, 0], 136 | packetLength: 39 137 | }, { 138 | firmwareVersion: [1, 2, 0], 139 | packetLength: 40 140 | }, { 141 | firmwareVersion: [1, 2, 1], 142 | packetLength: 42 143 | }, { 144 | firmwareVersion: [1, 2, 2], 145 | packetLength: 43 146 | }, { 147 | firmwareVersion: [1, 4, 0], 148 | packetLength: 58 149 | }, { 150 | firmwareVersion: [1, 5, 0], 151 | packetLength: 59 152 | }, { 153 | firmwareVersion: [1, 6, 0], 154 | packetLength: 62 155 | }]; 156 | 157 | const FIRMWARE_OFFICIAL_RELEASE = 0; 158 | const FIRMWARE_OFFICIAL_RELEASE_CANDIDATE = 1; 159 | const FIRMWARE_CUSTOM_EQUIVALENT = 2; 160 | const FIRMWARE_UNSUPPORTED = 3; 161 | exports.FIRMWARE_OFFICIAL_RELEASE = FIRMWARE_OFFICIAL_RELEASE; 162 | exports.FIRMWARE_OFFICIAL_RELEASE_CANDIDATE = FIRMWARE_OFFICIAL_RELEASE_CANDIDATE; 163 | exports.FIRMWARE_CUSTOM_EQUIVALENT = FIRMWARE_CUSTOM_EQUIVALENT; 164 | exports.FIRMWARE_UNSUPPORTED = FIRMWARE_UNSUPPORTED; 165 | 166 | const EQUIVALENCE_REGEX = /E[0-9]+\.[0-9]+\.[0-9]+/g; 167 | exports.EQUIVALENCE_REGEX = EQUIVALENCE_REGEX; 168 | 169 | /* Remove trailing digit and check if description is in list of supported firmware descriptions */ 170 | 171 | exports.getFirmwareClassification = (desc) => { 172 | 173 | /* If official firmware or a release candidate of the official firmware */ 174 | 175 | if (desc === 'AudioMoth-Firmware-Basic') { 176 | 177 | return FIRMWARE_OFFICIAL_RELEASE; 178 | 179 | } 180 | 181 | if (desc.replace(/-RC\d+$/, '-RC') === 'AudioMoth-Firmware-Basic-RC') { 182 | 183 | return FIRMWARE_OFFICIAL_RELEASE_CANDIDATE; 184 | 185 | } 186 | 187 | const foundEquivalence = desc.match(EQUIVALENCE_REGEX); 188 | 189 | if (foundEquivalence) { 190 | 191 | return FIRMWARE_CUSTOM_EQUIVALENT; 192 | 193 | } 194 | 195 | return FIRMWARE_UNSUPPORTED; 196 | 197 | }; 198 | 199 | /** 200 | * @returns -1: A < B, 0: A === B, 1: A > B 201 | */ 202 | function compareSemanticVersion (version, major, minor, patch) { 203 | 204 | for (let i = 0; i < 3; i++) { 205 | 206 | const versionNumber = parseInt(version[i]); 207 | 208 | const comparator = i === 0 ? major : i === 1 ? minor : patch; 209 | 210 | if (versionNumber > comparator) { 211 | 212 | return 1; 213 | 214 | } else if (versionNumber < comparator) { 215 | 216 | return -1; 217 | 218 | } 219 | 220 | } 221 | 222 | return 0; 223 | 224 | } 225 | 226 | exports.compareSemanticVersion = compareSemanticVersion; 227 | 228 | exports.isOlderSemanticVersion = (version, major, minor, patch) => { 229 | 230 | return compareSemanticVersion(version, major, minor, patch) === -1; 231 | 232 | }; 233 | 234 | exports.isNewerSemanticVersion = (version, major, minor, patch) => { 235 | 236 | return compareSemanticVersion(version, major, minor, patch) === 1; 237 | 238 | }; 239 | 240 | exports.isSameSemanticVersion = (version, major, minor, patch) => { 241 | 242 | return compareSemanticVersion(version, major, minor, patch) === 0; 243 | 244 | }; 245 | 246 | exports.isOlderOrEqualSemanticVersion = (version, major, minor, patch) => { 247 | 248 | const comparisonResult = compareSemanticVersion(version, major, minor, patch); 249 | 250 | return comparisonResult === -1 || comparisonResult === 0; 251 | 252 | }; 253 | 254 | exports.isNewerOrEqualSemanticVersion = (version, major, minor, patch) => { 255 | 256 | const comparisonResult = compareSemanticVersion(version, major, minor, patch); 257 | 258 | return comparisonResult === 0 || comparisonResult === 1; 259 | 260 | }; 261 | 262 | /* Version number for the latest firmware */ 263 | 264 | const LATEST_FIRMWARE_VERSION_MAJOR = 1; 265 | const LATEST_FIRMWARE_VERSION_MINOR = 11; 266 | const LATEST_FIRMWARE_VERSION_PATCH = 0; 267 | 268 | exports.LATEST_FIRMWARE_VERSION_MAJOR = LATEST_FIRMWARE_VERSION_MAJOR; 269 | exports.LATEST_FIRMWARE_VERSION_MINOR = LATEST_FIRMWARE_VERSION_MINOR; 270 | exports.LATEST_FIRMWARE_VERSION_PATCH = LATEST_FIRMWARE_VERSION_PATCH; 271 | exports.LATEST_FIRMWARE_VERSION_ARRAY = [LATEST_FIRMWARE_VERSION_MAJOR, LATEST_FIRMWARE_VERSION_MINOR, LATEST_FIRMWARE_VERSION_PATCH]; 272 | exports.LATEST_FIRMWARE_VERSION_STRING = LATEST_FIRMWARE_VERSION_MAJOR.toString() + '.' + LATEST_FIRMWARE_VERSION_MINOR.toString() + '.' + LATEST_FIRMWARE_VERSION_PATCH.toString(); 273 | 274 | /* Time zone modes */ 275 | 276 | exports.TIME_ZONE_MODE_UTC = 0; 277 | exports.TIME_ZONE_MODE_LOCAL = 1; 278 | exports.TIME_ZONE_MODE_CUSTOM = 2; 279 | exports.TIME_ZONE_MODE_STRINGS = ['UTC', 'LOCAL', 'CUSTOM']; 280 | 281 | /* Calculation values */ 282 | 283 | exports.UINT32_MAX = 0xFFFFFFFF; 284 | exports.UINT16_MAX = 0xFFFF; 285 | 286 | exports.MILLISECONDS_IN_SECOND = 1000; 287 | 288 | exports.SECONDS_IN_MINUTE = 60; 289 | exports.SECONDS_IN_DAY = 86400; 290 | 291 | exports.MINUTES_IN_HOUR = 60; 292 | exports.MINUTES_IN_DAY = 1440; 293 | 294 | /* Schedule limitations */ 295 | 296 | exports.MAX_PERIODS = 4; 297 | 298 | /* Sunrise and sunset results */ 299 | 300 | exports.SUN_ABOVE_HORIZON = 0; 301 | exports.NORMAL_SOLUTION = 1; 302 | exports.SUN_BELOW_HORIZON = 2; 303 | 304 | exports.DAY_LONGER_THAN_NIGHT = 0; 305 | exports.DAY_EQUAL_TO_NIGHT = 1; 306 | exports.DAY_SHORTER_THAN_NIGHT = 2; 307 | 308 | exports.MINIMUM_SUN_RECORDING_GAP = 60; 309 | exports.SUN_RECORDING_GAP_MULTIPLIER = 4; 310 | 311 | /* Sun schedule modes */ 312 | 313 | exports.MODE_BEFORE_SUNRISE_AFTER_SUNRISE = 0; 314 | exports.MODE_BEFORE_SUNSET_AFTER_SUNSET = 1; 315 | exports.MODE_BEFORE_BOTH_AFTER_BOTH = 2; 316 | exports.MODE_BEFORE_SUNSET_AFTER_SUNRISE = 3; 317 | exports.MODE_BEFORE_SUNRISE_AFTER_SUNSET = 4; 318 | 319 | /* Sun events modes */ 320 | 321 | exports.SUNRISE_AND_SUNSET = 0; 322 | exports.CIVIL_DAWN_AND_DUSK = 1; 323 | exports.NAUTICAL_DAWN_AND_DUSK = 2; 324 | exports.ASTRONOMICAL_DAWN_AND_DUSK = 3; 325 | 326 | /* First/last recording date range */ 327 | 328 | exports.MIN_FIRST_LAST_DATE = '2020-01-01'; 329 | exports.MAX_FIRST_LAST_DATE = '2029-12-31'; 330 | -------------------------------------------------------------------------------- /schedule/roundingInput.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * roundingInput.js 3 | * openacousticdevices.info 4 | * January 2024 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | const DEFAULT_WIDTH = '60px'; 10 | const DEFAULT_HEIGHT = '25px'; 11 | 12 | const inputData = {}; 13 | 14 | exports.setEnabled = (div, setting) => { 15 | 16 | inputData[div.id].enabled = setting; 17 | 18 | const uiElements = getAllSpans(div); 19 | const textInput = getTextInput(div); 20 | 21 | if (setting) { 22 | 23 | textInput.style.backgroundColor = ''; 24 | textInput.style.border = ''; 25 | textInput.tabIndex = 0; 26 | 27 | for (let i = 0; i < uiElements.length; i++) { 28 | 29 | uiElements[i].classList.remove('grey'); 30 | uiElements[i].style.backgroundColor = ''; 31 | 32 | } 33 | 34 | updateGrey(div); 35 | 36 | } else { 37 | 38 | textInput.style.backgroundColor = 'EBEBE4'; 39 | textInput.style.border = '1px solid #cccccc'; 40 | 41 | for (let i = 0; i < uiElements.length; i++) { 42 | 43 | uiElements[i].classList.add('grey'); 44 | uiElements[i].style.backgroundColor = 'EBEBE4'; 45 | 46 | } 47 | 48 | textInput.tabIndex = -1; 49 | 50 | } 51 | 52 | }; 53 | 54 | /** 55 | * @param {Element} div Parent div element 56 | * @returns Time string 57 | */ 58 | function getValue (div) { 59 | 60 | return inputData[div.id].value; 61 | 62 | }; 63 | 64 | exports.getValue = getValue; 65 | 66 | // Data structure setters 67 | 68 | function setValue (div, value) { 69 | 70 | const maxValue = getMaxValue(); 71 | 72 | inputData[div.id].value = Math.min(value, maxValue); 73 | 74 | updateFieldSpan(div); 75 | 76 | updateGrey(div); 77 | 78 | } 79 | 80 | exports.setValue = setValue; 81 | 82 | function setSelectedIndex (div, index) { 83 | 84 | inputData[div.id].selectedIndex = index; 85 | 86 | } 87 | 88 | function setEntry (div, entry) { 89 | 90 | inputData[div.id].entry = entry; 91 | 92 | } 93 | 94 | function resetEntry (div) { 95 | 96 | inputData[div.id].entry = 0; 97 | 98 | } 99 | 100 | // Data structure getters 101 | 102 | function isEnabled (div) { 103 | 104 | return inputData[div.id].enabled; 105 | 106 | } 107 | 108 | function getSelectedIndex (div) { 109 | 110 | return inputData[div.id].selectedIndex; 111 | 112 | } 113 | 114 | function getEntry (div) { 115 | 116 | return inputData[div.id].entry; 117 | 118 | } 119 | 120 | // Get UI elements 121 | 122 | function getDurationSpan (div) { 123 | 124 | return div.getElementsByClassName('duration-span')[0]; 125 | 126 | } 127 | 128 | function getTextInput (div) { 129 | 130 | return div.getElementsByClassName('text-input')[0]; 131 | 132 | } 133 | 134 | function getAllSpans (div) { 135 | 136 | const holder = div.getElementsByClassName('holder')[0]; 137 | return holder.children; 138 | 139 | } 140 | 141 | // Update values 142 | 143 | function getMaxValue () { 144 | 145 | return 60; 146 | 147 | } 148 | 149 | function updateFieldSpan (div) { 150 | 151 | const value = getValue(div); 152 | 153 | const field = getDurationSpan(div); 154 | 155 | const stringValue = value.toString().padStart(2, '0'); 156 | 157 | field.innerText = stringValue; 158 | 159 | } 160 | 161 | // Keypress event handler 162 | 163 | function handleKeyDown (e) { 164 | 165 | const div = e.target.parentNode; 166 | 167 | if (e.key === 'Tab') { 168 | 169 | return; 170 | 171 | } 172 | 173 | e.preventDefault(); 174 | 175 | if (e.key === 'ArrowUp') { 176 | 177 | resetEntry(div); 178 | 179 | const currentValue = getValue(div); 180 | const maxValue = getMaxValue(); 181 | 182 | const newValue = currentValue + 1 > maxValue ? 0 : currentValue + 1; 183 | 184 | setValue(div, newValue); 185 | 186 | return; 187 | 188 | } 189 | 190 | if (e.key === 'ArrowDown') { 191 | 192 | resetEntry(div); 193 | 194 | const currentValue = getValue(div); 195 | const maxValue = getMaxValue(); 196 | 197 | const newValue = currentValue - 1 < 0 ? maxValue : currentValue - 1; 198 | 199 | setValue(div, newValue); 200 | 201 | updateGrey(div); 202 | 203 | return; 204 | 205 | } 206 | 207 | if (e.key === 'Backspace' || e.key === 'Delete') { 208 | 209 | resetEntry(div); 210 | setValue(div, 0); 211 | 212 | updateGrey(div); 213 | 214 | return; 215 | 216 | } 217 | 218 | const patt = /^[0-9]$/; 219 | 220 | if (patt.test(e.key)) { 221 | 222 | const digit = parseInt(e.key); 223 | 224 | let entry = getEntry(div); 225 | 226 | setEntry(div, 10 * entry + digit); 227 | entry = getEntry(div); 228 | 229 | const maxValue = getMaxValue(div); 230 | const value = Math.min(maxValue, entry); 231 | 232 | setValue(div, value); 233 | 234 | if (10 * value > maxValue || (maxValue === 0 && digit > 0)) { 235 | 236 | resetEntry(div); 237 | 238 | } 239 | 240 | } 241 | 242 | } 243 | 244 | /** 245 | * Highlight the span at index selectedIndex 246 | * @param {Element} div Time input div element 247 | */ 248 | function highlightInput (div) { 249 | 250 | const inputNode = getTextInput(div); 251 | 252 | const index = getSelectedIndex(div); 253 | 254 | const durationSpan = getDurationSpan(div); 255 | 256 | const deselectedColor = inputNode.style.background; 257 | 258 | if (index === 0) { 259 | 260 | durationSpan.style.color = ''; 261 | durationSpan.style.backgroundColor = 'Highlight'; 262 | durationSpan.style.color = 'HighlightText'; 263 | 264 | } else { 265 | 266 | durationSpan.style.color = ''; 267 | durationSpan.style.backgroundColor = deselectedColor; 268 | durationSpan.classList.remove('grey'); 269 | 270 | } 271 | 272 | updateGrey(div); 273 | 274 | } 275 | 276 | function updateGrey (div) { 277 | 278 | if (!isEnabled(div)) { 279 | 280 | return; 281 | 282 | } 283 | 284 | // Update grey 285 | 286 | const durationSpanNode = getDurationSpan(div); 287 | 288 | const inputValue = getValue(div); 289 | 290 | if (inputValue < 10) { 291 | 292 | durationSpanNode.classList.add('greyStart'); 293 | 294 | } else { 295 | 296 | durationSpanNode.classList.remove('greyStart'); 297 | 298 | } 299 | 300 | const selectedIndex = getSelectedIndex(div); 301 | 302 | if (selectedIndex === 0) { 303 | 304 | durationSpanNode.classList.remove('greyStart'); 305 | 306 | } 307 | 308 | } 309 | 310 | /** 311 | * Create all the UI elements needed and add a new data structure to the lookup table 312 | * @param {string} divName Name assigned to custom div 313 | * @param {function} focusOutFunction Function run when focus leaves input 314 | * @returns Parent div element 315 | */ 316 | exports.create = (divName, focusOutFunction) => { 317 | 318 | const customDiv = document.getElementById(divName); 319 | 320 | let width, height; 321 | 322 | const attributes = customDiv.attributes; 323 | 324 | width = customDiv.style.width; 325 | width = (width === '') ? DEFAULT_WIDTH : width; 326 | height = customDiv.style.height; 327 | height = (height === '') ? DEFAULT_HEIGHT : height; 328 | 329 | const parent = customDiv.parentNode; 330 | 331 | const id = customDiv.id; 332 | 333 | parent.removeChild(customDiv); 334 | 335 | const div = document.createElement('div'); 336 | div.style = 'position: relative;'; 337 | div.id = id; 338 | 339 | for (let i = 0; i < attributes.length; i++) { 340 | 341 | if (attributes[i].name !== 'style') { 342 | 343 | div.setAttribute(attributes[i].name, attributes[i].value); 344 | 345 | } 346 | 347 | } 348 | 349 | const numSpanStyle1 = 'display: inline-block; text-align: center; width: 15px;'; 350 | 351 | const inputNode = document.createElement('input'); 352 | inputNode.className = 'text-input'; 353 | inputNode.type = 'text'; 354 | inputNode.style = 'width: ' + width + '; height: ' + height + '; color: white; caret-color: transparent;'; 355 | 356 | div.appendChild(inputNode); 357 | 358 | const blockerNode = document.createElement('div'); 359 | blockerNode.style = 'position: absolute; top: 0px; margin-left: 0px; margin-top: 0px; width: 100%; height: 100%;'; 360 | 361 | const holderNode = document.createElement('div'); 362 | holderNode.className = 'holder'; 363 | holderNode.style = 'position: absolute; top: 0px; margin-left: 39px; margin-top: 3px;'; 364 | 365 | const durationSpanNode = document.createElement('span'); 366 | durationSpanNode.className = 'duration-span'; 367 | durationSpanNode.innerText = '00'; 368 | durationSpanNode.style = numSpanStyle1; 369 | 370 | holderNode.appendChild(durationSpanNode); 371 | 372 | blockerNode.appendChild(holderNode); 373 | div.appendChild(blockerNode); 374 | 375 | parent.appendChild(div); 376 | 377 | const data = {}; 378 | 379 | data.value = 0; 380 | data.width = 2; 381 | data.selectedIndex = -1; 382 | data.enabled = true; 383 | 384 | inputData[div.id] = data; 385 | 386 | updateGrey(div); 387 | 388 | inputNode.addEventListener('keydown', handleKeyDown); 389 | 390 | function handleSpanClick () { 391 | 392 | if (!isEnabled(div)) { 393 | 394 | return; 395 | 396 | } 397 | 398 | setSelectedIndex(div, 0); 399 | highlightInput(div); 400 | inputNode.focus(); 401 | 402 | updateGrey(div); 403 | 404 | } 405 | 406 | durationSpanNode.addEventListener('click', handleSpanClick); 407 | 408 | inputNode.addEventListener('focusin', () => { 409 | 410 | if (!isEnabled(div)) { 411 | 412 | inputNode.blur(); 413 | return; 414 | 415 | } 416 | 417 | resetEntry(div); 418 | 419 | if (getSelectedIndex(div) === -1) { 420 | 421 | setSelectedIndex(div, 0); 422 | highlightInput(div); 423 | 424 | } 425 | 426 | updateGrey(div); 427 | 428 | }); 429 | 430 | inputNode.addEventListener('focusout', () => { 431 | 432 | if (!isEnabled(div)) { 433 | 434 | return; 435 | 436 | } 437 | 438 | setSelectedIndex(div, -1); 439 | highlightInput(div); 440 | 441 | updateGrey(div); 442 | 443 | if (focusOutFunction) { 444 | 445 | focusOutFunction(); 446 | 447 | } 448 | 449 | }); 450 | 451 | inputNode.addEventListener('click', () => { 452 | 453 | if (!isEnabled(div)) { 454 | 455 | return; 456 | 457 | } 458 | 459 | setSelectedIndex(div, 0); 460 | highlightInput(div); 461 | inputNode.focus(); 462 | 463 | }); 464 | 465 | return div; 466 | 467 | }; 468 | -------------------------------------------------------------------------------- /processing/aligning.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | Synchronise AudioMoth Files 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 |
44 | 45 |
46 | 47 |
48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 76 | 77 |
FileFolder
78 |
79 | 80 |
81 | 82 |
83 |
84 | 85 |
86 |
87 |
88 | No AudioMoth WAV files selected. 89 |
90 |
91 |
92 | 93 |
94 | 95 |
96 | 97 |
98 | 99 |
100 | 101 |
102 | 103 |
104 | 105 |
106 |
107 | 108 | 109 |
110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 | 118 |
119 |
120 |
121 | 122 |
123 | 124 |
125 | 126 |
127 | 128 |
129 | 130 |
131 | 132 |
133 | 134 |
135 |
136 | 137 |
138 |
139 | 140 |
141 |
142 | 143 |
144 |
145 | 146 |
147 |
148 | 149 |
150 |
151 | 152 |
153 |
154 | 155 |
156 |
157 |
158 | Writing WAV files to source folder. 159 |
160 |
161 |
162 | 163 |
164 | 165 |
166 | 167 |
168 | 169 |
170 | 171 |
172 |
173 |
174 | 175 |
176 |
177 |
178 | 179 |
180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /map/leaflet.css: -------------------------------------------------------------------------------- 1 | /* required styles */ 2 | 3 | .leaflet-map-pane, 4 | .leaflet-tile, 5 | .leaflet-marker-icon, 6 | .leaflet-marker-shadow, 7 | .leaflet-tile-pane, 8 | .leaflet-tile-container, 9 | .leaflet-overlay-pane, 10 | .leaflet-shadow-pane, 11 | .leaflet-marker-pane, 12 | .leaflet-popup-pane, 13 | .leaflet-overlay-pane svg, 14 | .leaflet-zoom-box, 15 | .leaflet-image-layer, 16 | .leaflet-layer { 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | } 21 | .leaflet-container { 22 | overflow: hidden; 23 | -ms-touch-action: none; 24 | } 25 | .leaflet-tile, 26 | .leaflet-marker-icon, 27 | .leaflet-marker-shadow { 28 | -webkit-user-select: none; 29 | -moz-user-select: none; 30 | user-select: none; 31 | -webkit-user-drag: none; 32 | } 33 | .leaflet-marker-icon, 34 | .leaflet-marker-shadow { 35 | display: block; 36 | } 37 | /* map is broken in FF if you have max-width: 100% on tiles */ 38 | .leaflet-container img { 39 | max-width: none !important; 40 | } 41 | /* stupid Android 2 doesn't understand "max-width: none" properly */ 42 | .leaflet-container img.leaflet-image-layer { 43 | max-width: 15000px !important; 44 | } 45 | .leaflet-tile { 46 | filter: inherit; 47 | visibility: hidden; 48 | } 49 | .leaflet-tile-loaded { 50 | visibility: inherit; 51 | } 52 | .leaflet-zoom-box { 53 | width: 0; 54 | height: 0; 55 | } 56 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ 57 | .leaflet-overlay-pane svg { 58 | -moz-user-select: none; 59 | } 60 | 61 | .leaflet-tile-pane { z-index: 2; } 62 | .leaflet-objects-pane { z-index: 3; } 63 | .leaflet-overlay-pane { z-index: 4; } 64 | .leaflet-shadow-pane { z-index: 5; } 65 | .leaflet-marker-pane { z-index: 6; } 66 | .leaflet-popup-pane { z-index: 7; } 67 | 68 | .leaflet-vml-shape { 69 | width: 1px; 70 | height: 1px; 71 | } 72 | .lvml { 73 | behavior: url(#default#VML); 74 | display: inline-block; 75 | position: absolute; 76 | } 77 | 78 | 79 | /* control positioning */ 80 | 81 | .leaflet-control { 82 | position: relative; 83 | z-index: 7; 84 | pointer-events: auto; 85 | } 86 | .leaflet-top, 87 | .leaflet-bottom { 88 | position: absolute; 89 | z-index: 1000; 90 | pointer-events: none; 91 | } 92 | .leaflet-top { 93 | top: 0; 94 | } 95 | .leaflet-right { 96 | right: 0; 97 | } 98 | .leaflet-bottom { 99 | bottom: 0; 100 | } 101 | .leaflet-left { 102 | left: 0; 103 | } 104 | .leaflet-control { 105 | float: left; 106 | clear: both; 107 | } 108 | .leaflet-right .leaflet-control { 109 | float: right; 110 | } 111 | .leaflet-top .leaflet-control { 112 | margin-top: 10px; 113 | } 114 | .leaflet-bottom .leaflet-control { 115 | margin-bottom: 10px; 116 | } 117 | .leaflet-left .leaflet-control { 118 | margin-left: 10px; 119 | } 120 | .leaflet-right .leaflet-control { 121 | margin-right: 10px; 122 | } 123 | 124 | 125 | /* zoom and fade animations */ 126 | 127 | .leaflet-fade-anim .leaflet-tile, 128 | .leaflet-fade-anim .leaflet-popup { 129 | opacity: 0; 130 | -webkit-transition: opacity 0.2s linear; 131 | -moz-transition: opacity 0.2s linear; 132 | -o-transition: opacity 0.2s linear; 133 | transition: opacity 0.2s linear; 134 | } 135 | .leaflet-fade-anim .leaflet-tile-loaded, 136 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 137 | opacity: 1; 138 | } 139 | 140 | .leaflet-zoom-anim .leaflet-zoom-animated { 141 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); 142 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); 143 | -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); 144 | transition: transform 0.25s cubic-bezier(0,0,0.25,1); 145 | } 146 | .leaflet-zoom-anim .leaflet-tile, 147 | .leaflet-pan-anim .leaflet-tile, 148 | .leaflet-touching .leaflet-zoom-animated { 149 | -webkit-transition: none; 150 | -moz-transition: none; 151 | -o-transition: none; 152 | transition: none; 153 | } 154 | 155 | .leaflet-zoom-anim .leaflet-zoom-hide { 156 | visibility: hidden; 157 | } 158 | 159 | 160 | /* cursors */ 161 | 162 | .leaflet-clickable { 163 | cursor: pointer; 164 | } 165 | .leaflet-container { 166 | cursor: -webkit-grab; 167 | cursor: -moz-grab; 168 | } 169 | .leaflet-popup-pane, 170 | .leaflet-control { 171 | cursor: auto; 172 | } 173 | .leaflet-dragging .leaflet-container, 174 | .leaflet-dragging .leaflet-clickable { 175 | cursor: move; 176 | cursor: -webkit-grabbing; 177 | cursor: -moz-grabbing; 178 | } 179 | 180 | 181 | /* visual tweaks */ 182 | 183 | .leaflet-container { 184 | background: #ddd; 185 | outline: 0; 186 | } 187 | .leaflet-container a { 188 | color: #0078A8; 189 | } 190 | .leaflet-container a.leaflet-active { 191 | outline: 2px solid orange; 192 | } 193 | .leaflet-zoom-box { 194 | border: 2px dotted #38f; 195 | background: rgba(255,255,255,0.5); 196 | } 197 | 198 | 199 | /* general typography */ 200 | .leaflet-container { 201 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 202 | } 203 | 204 | 205 | /* general toolbar styles */ 206 | 207 | .leaflet-bar { 208 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 209 | border-radius: 4px; 210 | } 211 | .leaflet-bar a, 212 | .leaflet-bar a:hover { 213 | background-color: #fff; 214 | border-bottom: 1px solid #ccc; 215 | width: 26px; 216 | height: 26px; 217 | line-height: 26px; 218 | display: block; 219 | text-align: center; 220 | text-decoration: none; 221 | color: black; 222 | } 223 | .leaflet-bar a, 224 | .leaflet-control-layers-toggle { 225 | background-position: 50% 50%; 226 | background-repeat: no-repeat; 227 | display: block; 228 | } 229 | .leaflet-bar a:hover { 230 | background-color: #f4f4f4; 231 | } 232 | .leaflet-bar a:first-child { 233 | border-top-left-radius: 4px; 234 | border-top-right-radius: 4px; 235 | } 236 | .leaflet-bar a:last-child { 237 | border-bottom-left-radius: 4px; 238 | border-bottom-right-radius: 4px; 239 | border-bottom: none; 240 | } 241 | .leaflet-bar a.leaflet-disabled { 242 | cursor: default; 243 | background-color: #f4f4f4; 244 | color: #bbb; 245 | } 246 | 247 | .leaflet-touch .leaflet-bar a { 248 | width: 30px; 249 | height: 30px; 250 | line-height: 30px; 251 | } 252 | 253 | 254 | /* zoom control */ 255 | 256 | .leaflet-control-zoom-in, 257 | .leaflet-control-zoom-out { 258 | font: bold 18px 'Lucida Console', Monaco, monospace; 259 | text-indent: 1px; 260 | } 261 | .leaflet-control-zoom-out { 262 | font-size: 20px; 263 | } 264 | 265 | .leaflet-touch .leaflet-control-zoom-in { 266 | font-size: 22px; 267 | } 268 | .leaflet-touch .leaflet-control-zoom-out { 269 | font-size: 24px; 270 | } 271 | 272 | 273 | /* layers control */ 274 | 275 | .leaflet-control-layers { 276 | box-shadow: 0 1px 5px rgba(0,0,0,0.4); 277 | background: #fff; 278 | border-radius: 5px; 279 | } 280 | .leaflet-control-layers-toggle { 281 | background-image: url(images/layers.png); 282 | width: 36px; 283 | height: 36px; 284 | } 285 | .leaflet-retina .leaflet-control-layers-toggle { 286 | background-image: url(images/layers-2x.png); 287 | background-size: 26px 26px; 288 | } 289 | .leaflet-touch .leaflet-control-layers-toggle { 290 | width: 44px; 291 | height: 44px; 292 | } 293 | .leaflet-control-layers .leaflet-control-layers-list, 294 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 295 | display: none; 296 | } 297 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 298 | display: block; 299 | position: relative; 300 | } 301 | .leaflet-control-layers-expanded { 302 | padding: 6px 10px 6px 6px; 303 | color: #333; 304 | background: #fff; 305 | } 306 | .leaflet-control-layers-selector { 307 | margin-top: 2px; 308 | position: relative; 309 | top: 1px; 310 | } 311 | .leaflet-control-layers label { 312 | display: block; 313 | } 314 | .leaflet-control-layers-separator { 315 | height: 0; 316 | border-top: 1px solid #ddd; 317 | margin: 5px -10px 5px -6px; 318 | } 319 | 320 | 321 | /* attribution and scale controls */ 322 | 323 | .leaflet-container .leaflet-control-attribution { 324 | background: #fff; 325 | background: rgba(255, 255, 255, 0.7); 326 | margin: 0; 327 | } 328 | .leaflet-control-attribution, 329 | .leaflet-control-scale-line { 330 | padding: 0 5px; 331 | color: #333; 332 | } 333 | .leaflet-control-attribution a { 334 | text-decoration: none; 335 | } 336 | .leaflet-control-attribution a:hover { 337 | text-decoration: underline; 338 | } 339 | .leaflet-container .leaflet-control-attribution, 340 | .leaflet-container .leaflet-control-scale { 341 | font-size: 11px; 342 | } 343 | .leaflet-left .leaflet-control-scale { 344 | margin-left: 5px; 345 | } 346 | .leaflet-bottom .leaflet-control-scale { 347 | margin-bottom: 5px; 348 | } 349 | .leaflet-control-scale-line { 350 | border: 2px solid #777; 351 | border-top: none; 352 | line-height: 1.1; 353 | padding: 2px 5px 1px; 354 | font-size: 11px; 355 | white-space: nowrap; 356 | overflow: hidden; 357 | -moz-box-sizing: content-box; 358 | box-sizing: content-box; 359 | 360 | background: #fff; 361 | background: rgba(255, 255, 255, 0.5); 362 | } 363 | .leaflet-control-scale-line:not(:first-child) { 364 | border-top: 2px solid #777; 365 | border-bottom: none; 366 | margin-top: -2px; 367 | } 368 | .leaflet-control-scale-line:not(:first-child):not(:last-child) { 369 | border-bottom: 2px solid #777; 370 | } 371 | 372 | .leaflet-touch .leaflet-control-attribution, 373 | .leaflet-touch .leaflet-control-layers, 374 | .leaflet-touch .leaflet-bar { 375 | box-shadow: none; 376 | } 377 | .leaflet-touch .leaflet-control-layers, 378 | .leaflet-touch .leaflet-bar { 379 | border: 2px solid rgba(0,0,0,0.2); 380 | background-clip: padding-box; 381 | } 382 | 383 | 384 | /* popup */ 385 | 386 | .leaflet-popup { 387 | position: absolute; 388 | text-align: center; 389 | } 390 | .leaflet-popup-content-wrapper { 391 | padding: 1px; 392 | text-align: left; 393 | border-radius: 12px; 394 | } 395 | .leaflet-popup-content { 396 | margin: 13px 19px; 397 | line-height: 1.4; 398 | } 399 | .leaflet-popup-content p { 400 | margin: 18px 0; 401 | } 402 | .leaflet-popup-tip-container { 403 | margin: 0 auto; 404 | width: 40px; 405 | height: 20px; 406 | position: relative; 407 | overflow: hidden; 408 | } 409 | .leaflet-popup-tip { 410 | width: 17px; 411 | height: 17px; 412 | padding: 1px; 413 | 414 | margin: -10px auto 0; 415 | 416 | -webkit-transform: rotate(45deg); 417 | -moz-transform: rotate(45deg); 418 | -ms-transform: rotate(45deg); 419 | -o-transform: rotate(45deg); 420 | transform: rotate(45deg); 421 | } 422 | .leaflet-popup-content-wrapper, 423 | .leaflet-popup-tip { 424 | background: white; 425 | 426 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 427 | } 428 | .leaflet-container a.leaflet-popup-close-button { 429 | position: absolute; 430 | top: 0; 431 | right: 0; 432 | padding: 4px 4px 0 0; 433 | text-align: center; 434 | width: 18px; 435 | height: 14px; 436 | font: 16px/14px Tahoma, Verdana, sans-serif; 437 | color: #c3c3c3; 438 | text-decoration: none; 439 | font-weight: bold; 440 | background: transparent; 441 | } 442 | .leaflet-container a.leaflet-popup-close-button:hover { 443 | color: #999; 444 | } 445 | .leaflet-popup-scrolled { 446 | overflow: auto; 447 | border-bottom: 1px solid #ddd; 448 | border-top: 1px solid #ddd; 449 | } 450 | 451 | .leaflet-oldie .leaflet-popup-content-wrapper { 452 | zoom: 1; 453 | } 454 | .leaflet-oldie .leaflet-popup-tip { 455 | width: 24px; 456 | margin: 0 auto; 457 | 458 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 459 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 460 | } 461 | .leaflet-oldie .leaflet-popup-tip-container { 462 | margin-top: -1px; 463 | } 464 | 465 | .leaflet-oldie .leaflet-control-zoom, 466 | .leaflet-oldie .leaflet-control-layers, 467 | .leaflet-oldie .leaflet-popup-content-wrapper, 468 | .leaflet-oldie .leaflet-popup-tip { 469 | border: 1px solid #999; 470 | } 471 | 472 | 473 | /* div icon */ 474 | 475 | .leaflet-div-icon { 476 | background: #fff; 477 | border: 1px solid #666; 478 | } -------------------------------------------------------------------------------- /processing/uiSplit.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * uiSplit.js 3 | * openacousticdevices.info 4 | * February 2021 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | /* global document */ 10 | 11 | const electron = require('electron'); 12 | const {dialog, getCurrentWindow} = require('@electron/remote'); 13 | 14 | /* Get functions which control elements common to the expansion, split, and downsample windows */ 15 | const ui = require('./uiCommon.js'); 16 | const uiOutput = require('./uiOutput.js'); 17 | const uiInput = require('./uiInput.js'); 18 | 19 | const path = require('path'); 20 | const fs = require('fs'); 21 | 22 | const audiomothUtils = require('audiomoth-utils'); 23 | 24 | const currentWindow = getCurrentWindow(); 25 | 26 | const MAX_LENGTHS = [1, 5, 10, 15, 30, 60, 300, 600, 3600]; 27 | 28 | const maxLengthRadios = document.getElementsByName('max-length-radio'); 29 | 30 | const selectionRadios = document.getElementsByName('selection-radio'); 31 | 32 | const prefixCheckbox = document.getElementById('prefix-checkbox'); 33 | const prefixInput = document.getElementById('prefix-input'); 34 | 35 | const fileLabel = document.getElementById('file-label'); 36 | const fileButton = document.getElementById('file-button'); 37 | const splitButton = document.getElementById('split-button'); 38 | 39 | let files = []; 40 | let splitting = false; 41 | 42 | const DEFAULT_SLEEP_AMOUNT = 2000; 43 | 44 | /* Disable UI elements in main window while progress bar is open and split is in progress */ 45 | 46 | function disableUI () { 47 | 48 | fileButton.disabled = true; 49 | splitButton.disabled = true; 50 | selectionRadios[0].disabled = true; 51 | selectionRadios[1].disabled = true; 52 | 53 | for (let i = 0; i < maxLengthRadios.length; i++) { 54 | 55 | maxLengthRadios[i].disabled = true; 56 | 57 | } 58 | 59 | uiOutput.disableOutputCheckbox(); 60 | uiOutput.disableOutputButton(); 61 | 62 | prefixCheckbox.disabled = true; 63 | prefixInput.disabled = true; 64 | 65 | } 66 | 67 | function enableUI () { 68 | 69 | fileButton.disabled = false; 70 | splitButton.disabled = false; 71 | selectionRadios[0].disabled = false; 72 | selectionRadios[1].disabled = false; 73 | 74 | for (let i = 0; i < maxLengthRadios.length; i++) { 75 | 76 | maxLengthRadios[i].disabled = false; 77 | 78 | } 79 | 80 | uiOutput.enableOutputCheckbox(); 81 | uiOutput.enableOutputButton(); 82 | 83 | prefixCheckbox.disabled = false; 84 | 85 | if (prefixCheckbox.checked) { 86 | 87 | prefixInput.disabled = false; 88 | 89 | } 90 | 91 | splitting = false; 92 | 93 | } 94 | 95 | /* Split selected files */ 96 | 97 | function splitFiles () { 98 | 99 | if (!files) { 100 | 101 | return; 102 | 103 | } 104 | 105 | let successCount = 0; 106 | let errorCount = 0; 107 | const errors = []; 108 | const errorFiles = []; 109 | 110 | let errorFilePath; 111 | 112 | let sleepAmount = DEFAULT_SLEEP_AMOUNT; 113 | let successesWithoutError = 0; 114 | 115 | const unwrittenErrors = []; 116 | let lastErrorWrite = -1; 117 | 118 | let errorFileStream; 119 | 120 | for (let i = 0; i < files.length; i++) { 121 | 122 | /* If progress bar is closed, the split task is considered cancelled. This will contact the main thread and ask if that has happened */ 123 | 124 | const cancelled = electron.ipcRenderer.sendSync('poll-split-cancelled'); 125 | 126 | if (cancelled) { 127 | 128 | console.log('Split cancelled.'); 129 | enableUI(); 130 | return; 131 | 132 | } 133 | 134 | /* Let the main thread know what value to set the progress bar to */ 135 | 136 | electron.ipcRenderer.send('set-split-bar-progress', i, 0); 137 | 138 | const maxLength = MAX_LENGTHS[ui.getSelectedRadioValue('max-length-radio')]; 139 | 140 | console.log('Splitting:', files[i]); 141 | console.log('Maximum file length:', maxLength); 142 | 143 | console.log('-'); 144 | 145 | /* Check if the optional prefix/output directory setttings are being used. If left as null, splitter will put file(s) in the same directory as the input with no prefix */ 146 | 147 | let outputPath = null; 148 | 149 | if (uiOutput.isCustomDestinationEnabled()) { 150 | 151 | outputPath = uiOutput.getOutputDir(); 152 | 153 | if (uiOutput.isCreateSubdirectoriesEnabled() && selectionRadios[1].checked) { 154 | 155 | const dirnames = path.dirname(files[i]).replace(/\\/g, '/').split('/'); 156 | 157 | const folderName = dirnames[dirnames.length - 1]; 158 | 159 | outputPath = path.join(outputPath, folderName); 160 | 161 | if (!fs.existsSync(outputPath)) { 162 | 163 | fs.mkdirSync(outputPath); 164 | 165 | } 166 | 167 | } 168 | 169 | } 170 | 171 | const prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null; 172 | 173 | const response = audiomothUtils.split(files[i], outputPath, prefix, maxLength, (progress) => { 174 | 175 | electron.ipcRenderer.send('set-split-bar-progress', i, progress); 176 | electron.ipcRenderer.send('set-split-bar-file', i, path.basename(files[i])); 177 | 178 | }); 179 | 180 | if (response.success) { 181 | 182 | successCount++; 183 | successesWithoutError++; 184 | 185 | if (successesWithoutError >= 10) { 186 | 187 | sleepAmount = DEFAULT_SLEEP_AMOUNT; 188 | 189 | } 190 | 191 | } else { 192 | 193 | /* Add error to log file */ 194 | 195 | unwrittenErrors.push(errorCount); 196 | successesWithoutError = 0; 197 | errorCount++; 198 | errors.push(response.error); 199 | errorFiles.push(files[i]); 200 | 201 | electron.ipcRenderer.send('set-split-bar-error', path.basename(files[i])); 202 | 203 | if (errorCount === 1) { 204 | 205 | const errorFileLocation = uiOutput.isCustomDestinationEnabled() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); 206 | errorFilePath = path.join(errorFileLocation, 'ERRORS.TXT'); 207 | errorFileStream = fs.createWriteStream(errorFilePath, {flags: 'a'}); 208 | 209 | errorFileStream.write('-- Split --\r\n'); 210 | 211 | } 212 | 213 | const currentTime = new Date(); 214 | const timeSinceLastErrorWrite = currentTime - lastErrorWrite; 215 | 216 | if (timeSinceLastErrorWrite > 1000 || lastErrorWrite === -1) { 217 | 218 | lastErrorWrite = new Date(); 219 | 220 | const unwrittenErrorCount = unwrittenErrors.length; 221 | 222 | console.log('Writing', unwrittenErrorCount, 'errors'); 223 | 224 | let fileContent = ''; 225 | 226 | for (let e = 0; e < unwrittenErrorCount; e++) { 227 | 228 | const unwrittenErrorIndex = unwrittenErrors.pop(); 229 | fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\r\n'; 230 | 231 | } 232 | 233 | try { 234 | 235 | errorFileStream.write(fileContent); 236 | 237 | console.log('Error summary written to ' + errorFilePath); 238 | 239 | } catch (err) { 240 | 241 | console.error(err); 242 | electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, true); 243 | return; 244 | 245 | } 246 | 247 | } 248 | 249 | ui.sleep(sleepAmount); 250 | sleepAmount = sleepAmount / 2; 251 | 252 | } 253 | 254 | } 255 | 256 | /* If any errors occurred, do a final error write */ 257 | 258 | const unwrittenErrorCount = unwrittenErrors.length; 259 | 260 | if (unwrittenErrorCount > 0) { 261 | 262 | console.log('Writing remaining', unwrittenErrorCount, 'errors'); 263 | 264 | let fileContent = ''; 265 | 266 | for (let e = 0; e < unwrittenErrorCount; e++) { 267 | 268 | const unwrittenErrorIndex = unwrittenErrors.pop(); 269 | fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\r\n'; 270 | 271 | } 272 | 273 | try { 274 | 275 | errorFileStream.write(fileContent); 276 | 277 | console.log('Error summary written to ' + errorFilePath); 278 | 279 | errorFileStream.end(); 280 | 281 | } catch (err) { 282 | 283 | console.error(err); 284 | electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, true); 285 | return; 286 | 287 | } 288 | 289 | } 290 | 291 | /* Notify main thread that split is complete so progress bar is closed */ 292 | 293 | electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, false); 294 | 295 | } 296 | 297 | /* When the progress bar is complete and the summary window at the end has been displayed for a fixed amount of time, it will close and this re-enables the UI */ 298 | 299 | electron.ipcRenderer.on('split-summary-closed', enableUI); 300 | 301 | /* Update label to reflect new file/folder selection */ 302 | 303 | function updateInputDirectoryDisplay (directoryArray) { 304 | 305 | if (!directoryArray || directoryArray.length === 0) { 306 | 307 | fileLabel.innerHTML = 'No AudioMoth WAV files selected.'; 308 | splitButton.disabled = true; 309 | 310 | } else { 311 | 312 | fileLabel.innerHTML = 'Found '; 313 | fileLabel.innerHTML += directoryArray.length + ' AudioMoth WAV file'; 314 | fileLabel.innerHTML += (directoryArray.length === 1 ? '' : 's'); 315 | fileLabel.innerHTML += '.'; 316 | splitButton.disabled = false; 317 | 318 | } 319 | 320 | } 321 | 322 | /* Reset UI back to default state, clearing the selected files */ 323 | 324 | function resetUI () { 325 | 326 | files = []; 327 | 328 | fileLabel.innerHTML = 'No AudioMoth WAV files selected.'; 329 | 330 | splitButton.disabled = true; 331 | 332 | ui.updateButtonText(); 333 | 334 | } 335 | 336 | /* Whenever the file/folder radio button changes, reset the UI */ 337 | 338 | selectionRadios[0].addEventListener('change', resetUI); 339 | selectionRadios[1].addEventListener('change', resetUI); 340 | 341 | /* Select/process file(s) buttons */ 342 | 343 | fileButton.addEventListener('click', () => { 344 | 345 | const fileRegex = audiomothUtils.getFilenameRegex(audiomothUtils.SPLIT); 346 | 347 | files = uiInput.selectRecordings(fileRegex); 348 | 349 | updateInputDirectoryDisplay(files); 350 | 351 | ui.updateButtonText(); 352 | 353 | }); 354 | 355 | splitButton.addEventListener('click', () => { 356 | 357 | if (splitting) { 358 | 359 | return; 360 | 361 | } 362 | 363 | if ((!prefixCheckbox.checked || prefixInput.value === '') && (!uiOutput.isCustomDestinationEnabled() || uiOutput.getOutputDir() === '')) { 364 | 365 | dialog.showMessageBox(currentWindow, { 366 | type: 'error', 367 | title: 'Cannot split with current settings', 368 | message: 'Without a prefix or custom destination, splitting will overwrite the original file. Set one of these values to continue.' 369 | }); 370 | 371 | return; 372 | 373 | } 374 | 375 | /* Check if output location is the same as input */ 376 | 377 | for (let i = 0; i < files.length; i++) { 378 | 379 | if (!prefixCheckbox.checked || prefixInput.value === '') { 380 | 381 | /* If folder mode is enabled, the input folder is the same as the output and subdirectories are enabled, files will be overwritten as the paths will match */ 382 | 383 | if (selectionRadios[1].checked && uiOutput.isCustomDestinationEnabled() && uiOutput.isCreateSubdirectoriesEnabled()) { 384 | 385 | /* Get the parent folder of the selected file and compare that to the output directory */ 386 | 387 | const fileFolderPath = path.dirname(path.dirname(files[i])); 388 | 389 | if (uiOutput.getOutputDir() === fileFolderPath) { 390 | 391 | dialog.showMessageBox(currentWindow, { 392 | type: 'error', 393 | title: 'Cannot split with current settings', 394 | message: 'Output destination is the same as input destination and no prefix is selected.' 395 | }); 396 | 397 | return; 398 | 399 | } 400 | 401 | } 402 | 403 | if (uiOutput.getOutputDir() === path.dirname(files[i])) { 404 | 405 | dialog.showMessageBox(currentWindow, { 406 | type: 'error', 407 | title: 'Cannot split with current settings', 408 | message: 'Output destination is the same as input destination and no prefix is selected.' 409 | }); 410 | 411 | return; 412 | 413 | } 414 | 415 | } 416 | 417 | } 418 | 419 | splitting = true; 420 | disableUI(); 421 | 422 | electron.ipcRenderer.send('start-split-bar', files.length); 423 | setTimeout(splitFiles, 2000); 424 | 425 | }); 426 | -------------------------------------------------------------------------------- /schedule/sunIntervalInput.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * sunIntervalInput.js 3 | * openacousticdevices.info 4 | * November 2023 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | const DEFAULT_WIDTH = '60px'; 10 | const DEFAULT_HEIGHT = '25px'; 11 | 12 | const inputData = {}; 13 | 14 | function isAlternateImplementation (div) { 15 | 16 | return inputData[div.id].alternateImplementation; 17 | 18 | } 19 | 20 | exports.setEnabled = (div, setting) => { 21 | 22 | inputData[div.id].enabled = setting; 23 | 24 | const uiElements = getAllSpans(div); 25 | const textInput = getTextInput(div); 26 | 27 | if (setting) { 28 | 29 | textInput.style.backgroundColor = ''; 30 | textInput.style.border = ''; 31 | textInput.tabIndex = 0; 32 | 33 | for (let i = 0; i < uiElements.length; i++) { 34 | 35 | uiElements[i].classList.remove('grey'); 36 | uiElements[i].style.backgroundColor = ''; 37 | 38 | } 39 | 40 | updateGrey(div); 41 | 42 | } else { 43 | 44 | textInput.style.backgroundColor = 'EBEBE4'; 45 | textInput.style.border = '1px solid #cccccc'; 46 | 47 | for (let i = 0; i < uiElements.length; i++) { 48 | 49 | uiElements[i].classList.add('grey'); 50 | uiElements[i].style.backgroundColor = 'EBEBE4'; 51 | 52 | } 53 | 54 | textInput.tabIndex = -1; 55 | 56 | } 57 | 58 | }; 59 | 60 | /** 61 | * @param {Element} div Parent div element 62 | * @returns Time string 63 | */ 64 | function getValue (div) { 65 | 66 | return inputData[div.id].value; 67 | 68 | }; 69 | 70 | exports.getValue = getValue; 71 | 72 | // Data structure setters 73 | 74 | function setValue (div, value) { 75 | 76 | const maxValue = getMaxValue(); 77 | 78 | inputData[div.id].value = Math.min(value, maxValue); 79 | 80 | updateFieldSpan(div); 81 | 82 | updateGrey(div); 83 | 84 | } 85 | 86 | exports.setValue = setValue; 87 | 88 | function setSelectedIndex (div, index) { 89 | 90 | inputData[div.id].selectedIndex = index; 91 | 92 | } 93 | 94 | function setEntry (div, entry) { 95 | 96 | inputData[div.id].entry = entry; 97 | 98 | } 99 | 100 | function resetEntry (div) { 101 | 102 | inputData[div.id].entry = isAlternateImplementation(div) ? '' : 0; 103 | 104 | } 105 | 106 | // Data structure getters 107 | 108 | function isEnabled (div) { 109 | 110 | return inputData[div.id].enabled; 111 | 112 | } 113 | 114 | function getSelectedIndex (div) { 115 | 116 | return inputData[div.id].selectedIndex; 117 | 118 | } 119 | 120 | function getEntry (div) { 121 | 122 | return inputData[div.id].entry; 123 | 124 | } 125 | 126 | function getWidth (div) { 127 | 128 | return inputData[div.id].width; 129 | 130 | } 131 | 132 | // Get UI elements 133 | 134 | function getDurationSpan0 (div) { 135 | 136 | return div.getElementsByClassName('duration-span0')[0]; 137 | 138 | } 139 | 140 | function getDurationSpan1 (div) { 141 | 142 | return div.getElementsByClassName('duration-span1')[0]; 143 | 144 | } 145 | 146 | function getTextInput (div) { 147 | 148 | return div.getElementsByClassName('text-input')[0]; 149 | 150 | } 151 | 152 | function getAllSpans (div) { 153 | 154 | const holder = div.getElementsByClassName('holder')[0]; 155 | return holder.children; 156 | 157 | } 158 | 159 | // Update values 160 | 161 | function getMaxValue () { 162 | 163 | return 360; 164 | 165 | } 166 | 167 | function updateFieldSpan (div) { 168 | 169 | const value = getValue(div); 170 | 171 | const field0 = getDurationSpan0(div); 172 | const field1 = getDurationSpan1(div); 173 | 174 | const stringValue = value.toString().padStart(3, '0'); 175 | 176 | field0.innerText = stringValue.slice(0, 1); 177 | field1.innerText = stringValue.slice(1); 178 | 179 | } 180 | 181 | // Keypress event handler 182 | 183 | function handleKeyDown (e) { 184 | 185 | const div = e.target.parentNode; 186 | 187 | if (e.key === 'Tab') { 188 | 189 | return; 190 | 191 | } 192 | 193 | e.preventDefault(); 194 | 195 | if (e.key === 'ArrowUp') { 196 | 197 | resetEntry(div); 198 | 199 | const currentValue = getValue(div); 200 | const maxValue = getMaxValue(); 201 | 202 | const newValue = currentValue + 1 > maxValue ? 0 : currentValue + 1; 203 | 204 | setValue(div, newValue); 205 | 206 | return; 207 | 208 | } 209 | 210 | if (e.key === 'ArrowDown') { 211 | 212 | resetEntry(div); 213 | 214 | const currentValue = getValue(div); 215 | const maxValue = getMaxValue(); 216 | 217 | const newValue = currentValue - 1 < 0 ? maxValue : currentValue - 1; 218 | 219 | setValue(div, newValue); 220 | 221 | updateGrey(div); 222 | 223 | return; 224 | 225 | } 226 | 227 | if (e.key === 'Backspace' || e.key === 'Delete') { 228 | 229 | resetEntry(div); 230 | setValue(div, 0); 231 | 232 | updateGrey(div); 233 | 234 | return; 235 | 236 | } 237 | 238 | const patt = /^[0-9]$/; 239 | 240 | if (patt.test(e.key)) { 241 | 242 | const digit = parseInt(e.key); 243 | 244 | let entry = getEntry(div); 245 | 246 | if (isAlternateImplementation(div)) { 247 | 248 | setEntry(div, entry + digit); 249 | entry = getEntry(div); 250 | 251 | const maxValue = getMaxValue(div); 252 | const value = Math.min(maxValue, parseInt(entry)); 253 | 254 | setValue(div, value); 255 | 256 | const width = getWidth(div); 257 | 258 | if (10 * value > maxValue || (maxValue === 0 && digit > 0) || entry.length === width) { 259 | 260 | resetEntry(div); 261 | 262 | } 263 | 264 | } else { 265 | 266 | setEntry(div, 10 * entry + digit); 267 | entry = getEntry(div); 268 | 269 | const maxValue = getMaxValue(div); 270 | const value = Math.min(maxValue, entry); 271 | 272 | setValue(div, value); 273 | 274 | if (10 * value > maxValue || (maxValue === 0 && digit > 0)) { 275 | 276 | resetEntry(div); 277 | 278 | } 279 | 280 | } 281 | 282 | } 283 | 284 | } 285 | 286 | /** 287 | * Highlight the span at index selectedIndex 288 | * @param {Element} div Time input div element 289 | */ 290 | function highlightInput (div) { 291 | 292 | const inputNode = getTextInput(div); 293 | 294 | const index = getSelectedIndex(div); 295 | 296 | const durationSpan0 = getDurationSpan0(div); 297 | const durationSpan1 = getDurationSpan1(div); 298 | 299 | const deselectedColor = inputNode.style.background; 300 | 301 | if (index === 0) { 302 | 303 | durationSpan0.style.color = ''; 304 | durationSpan0.style.backgroundColor = 'Highlight'; 305 | durationSpan0.style.color = 'HighlightText'; 306 | durationSpan1.style.color = ''; 307 | durationSpan1.style.backgroundColor = 'Highlight'; 308 | durationSpan1.style.color = 'HighlightText'; 309 | 310 | } else { 311 | 312 | durationSpan0.style.color = ''; 313 | durationSpan0.style.backgroundColor = deselectedColor; 314 | durationSpan0.classList.remove('grey'); 315 | durationSpan1.style.color = ''; 316 | durationSpan1.style.backgroundColor = deselectedColor; 317 | durationSpan1.classList.remove('grey'); 318 | 319 | } 320 | 321 | updateGrey(div); 322 | 323 | } 324 | 325 | function updateGrey (div) { 326 | 327 | if (!isEnabled(div)) { 328 | 329 | return; 330 | 331 | } 332 | 333 | // Update grey 334 | 335 | const durationSpanNode0 = getDurationSpan0(div); 336 | const durationSpanNode1 = getDurationSpan1(div); 337 | 338 | const inputValue = getValue(div); 339 | 340 | if (inputValue < 10) { 341 | 342 | durationSpanNode0.classList.add('allGrey'); 343 | durationSpanNode1.classList.add('greyStart'); 344 | 345 | } else if (inputValue < 100) { 346 | 347 | durationSpanNode0.classList.add('allGrey'); 348 | durationSpanNode1.classList.remove('greyStart'); 349 | 350 | } else { 351 | 352 | durationSpanNode0.classList.remove('allGrey'); 353 | durationSpanNode1.classList.remove('greyStart'); 354 | 355 | } 356 | 357 | const selectedIndex = getSelectedIndex(div); 358 | 359 | if (selectedIndex === 0) { 360 | 361 | durationSpanNode0.classList.remove('allGrey'); 362 | durationSpanNode1.classList.remove('greyStart'); 363 | 364 | } 365 | 366 | } 367 | 368 | /** 369 | * Create all the UI elements needed and add a new data structure to the lookup table 370 | * @param {string} divName Name assigned to custom div 371 | * @param {boolean} alternateImplementation Whether or not to use alternate implementation which allows leading zeroes 372 | * @param {function} focusOutFunction Function run when focus leaves input 373 | * @returns Parent div element 374 | */ 375 | exports.create = (divName, alternateImplementation, focusOutFunction) => { 376 | 377 | const customDiv = document.getElementById(divName); 378 | 379 | let width, height; 380 | 381 | const attributes = customDiv.attributes; 382 | 383 | width = customDiv.style.width; 384 | width = (width === '') ? DEFAULT_WIDTH : width; 385 | height = customDiv.style.height; 386 | height = (height === '') ? DEFAULT_HEIGHT : height; 387 | 388 | const parent = customDiv.parentNode; 389 | 390 | const id = customDiv.id; 391 | 392 | parent.removeChild(customDiv); 393 | 394 | const div = document.createElement('div'); 395 | div.style = 'position: relative;'; 396 | div.id = id; 397 | 398 | for (let i = 0; i < attributes.length; i++) { 399 | 400 | if (attributes[i].name !== 'style') { 401 | 402 | div.setAttribute(attributes[i].name, attributes[i].value); 403 | 404 | } 405 | 406 | } 407 | 408 | const numSpanStyle1 = 'display: inline-block; text-align: center; width: 7px;'; 409 | const numSpanStyle2 = 'display: inline-block; text-align: center; width: 15px;'; 410 | 411 | const inputNode = document.createElement('input'); 412 | inputNode.className = 'text-input'; 413 | inputNode.type = 'text'; 414 | inputNode.style = 'width: ' + width + '; height: ' + height + '; color: white; caret-color: transparent;'; 415 | 416 | div.appendChild(inputNode); 417 | 418 | const blockerNode = document.createElement('div'); 419 | blockerNode.style = 'position: absolute; top: 0px; margin-left: 0px; margin-top: 0px; width: 100%; height: 100%;'; 420 | 421 | const holderNode = document.createElement('div'); 422 | holderNode.className = 'holder'; 423 | holderNode.style = 'position: absolute; top: 0px; margin-left: 32px; margin-top: 3px;'; 424 | 425 | const durationSpanNode0 = document.createElement('span'); 426 | durationSpanNode0.className = 'duration-span0'; 427 | durationSpanNode0.innerText = '0'; 428 | durationSpanNode0.style = numSpanStyle1; 429 | 430 | const durationSpanNode1 = document.createElement('span'); 431 | durationSpanNode1.className = 'duration-span1'; 432 | durationSpanNode1.innerText = '00'; 433 | durationSpanNode1.style = numSpanStyle2; 434 | 435 | holderNode.appendChild(durationSpanNode0); 436 | holderNode.appendChild(durationSpanNode1); 437 | 438 | blockerNode.appendChild(holderNode); 439 | div.appendChild(blockerNode); 440 | 441 | parent.appendChild(div); 442 | 443 | const data = {}; 444 | 445 | alternateImplementation = alternateImplementation === undefined ? false : alternateImplementation; 446 | 447 | data.alternateImplementation = alternateImplementation; 448 | data.value = 0; 449 | data.width = 3; 450 | data.selectedIndex = -1; 451 | data.entry = alternateImplementation ? '0' : 0; 452 | data.enabled = true; 453 | 454 | inputData[div.id] = data; 455 | 456 | updateGrey(div); 457 | 458 | inputNode.addEventListener('keydown', handleKeyDown); 459 | 460 | function handleSpanClick () { 461 | 462 | if (!isEnabled(div)) { 463 | 464 | return; 465 | 466 | } 467 | 468 | setSelectedIndex(div, 0); 469 | highlightInput(div); 470 | inputNode.focus(); 471 | 472 | updateGrey(div); 473 | 474 | } 475 | 476 | durationSpanNode0.addEventListener('click', handleSpanClick); 477 | durationSpanNode1.addEventListener('click', handleSpanClick); 478 | 479 | inputNode.addEventListener('focusin', () => { 480 | 481 | if (!isEnabled(div)) { 482 | 483 | inputNode.blur(); 484 | return; 485 | 486 | } 487 | 488 | resetEntry(div); 489 | 490 | if (getSelectedIndex(div) === -1) { 491 | 492 | setSelectedIndex(div, 0); 493 | highlightInput(div); 494 | 495 | } 496 | 497 | updateGrey(div); 498 | 499 | }); 500 | 501 | inputNode.addEventListener('focusout', () => { 502 | 503 | if (!isEnabled(div)) { 504 | 505 | return; 506 | 507 | } 508 | 509 | setSelectedIndex(div, -1); 510 | highlightInput(div); 511 | 512 | updateGrey(div); 513 | 514 | if (focusOutFunction) { 515 | 516 | focusOutFunction(); 517 | 518 | } 519 | 520 | }); 521 | 522 | inputNode.addEventListener('click', () => { 523 | 524 | if (!isEnabled(div)) { 525 | 526 | return; 527 | 528 | } 529 | 530 | setSelectedIndex(div, 0); 531 | highlightInput(div); 532 | inputNode.focus(); 533 | 534 | }); 535 | 536 | return div; 537 | 538 | }; 539 | -------------------------------------------------------------------------------- /settings/durationInput.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | * durationInput.js 3 | * openacousticdevices.info 4 | * November 2019 5 | *****************************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | /* global document, HTMLElement, customElements, Event */ 10 | 11 | const ariaSpeak = require('../ariaSpeak.js'); 12 | 13 | const DEFAULT_MAX_CHAR_LENGTH = 5; 14 | /* Currently, only single digit minimums are supported. Minimums of 10 or greater will require reimplementation */ 15 | const DEFAULT_MIN_VALUE = 0; 16 | const DEFAULT_MAX_VALUE = 43200; 17 | 18 | const DEFAULT_WIDTH = '100px'; 19 | const DEFAULT_HEIGHT = '25px'; 20 | 21 | function getSpan (node) { 22 | 23 | return node.getElementsByClassName('duration-span')[0]; 24 | 25 | } 26 | 27 | function getSpanFromChild (node) { 28 | 29 | return getSpan(node.parentNode); 30 | 31 | } 32 | 33 | function getTextInput (node) { 34 | 35 | return node.getElementsByClassName('duration-text-input')[0]; 36 | 37 | } 38 | 39 | function getAttributeValue (node, attributeName) { 40 | 41 | return node.parentNode.getAttribute(attributeName); 42 | 43 | } 44 | 45 | function setAttributeValue (node, attributeName, value) { 46 | 47 | node.parentNode.setAttribute(attributeName, value); 48 | 49 | } 50 | 51 | exports.getValue = (div) => { 52 | 53 | return parseInt(div.getAttribute('inputValue')); 54 | 55 | }; 56 | 57 | exports.setValue = (div, value) => { 58 | 59 | div.setAttribute('inputValue', value.toString()); 60 | div.setAttribute('numbersEntered', value.toString().length - 1); 61 | getSpan(div).innerText = value.toString(); 62 | 63 | }; 64 | 65 | exports.setEnabled = (div, setting) => { 66 | 67 | div.setAttribute('enabled', setting ? 'true' : 'false'); 68 | 69 | const span = getSpan(div); 70 | const textInput = getTextInput(div); 71 | 72 | if (setting) { 73 | 74 | textInput.style.backgroundColor = ''; 75 | textInput.style.border = ''; 76 | span.classList.remove('grey'); 77 | span.style.backgroundColor = ''; 78 | 79 | textInput.tabIndex = 0; 80 | 81 | } else { 82 | 83 | switch (process.platform) { 84 | 85 | case 'linux': 86 | case 'win32': 87 | textInput.style.backgroundColor = '#EBEBE4'; 88 | textInput.style.border = '1px solid #cccccc'; 89 | span.classList.add('grey'); 90 | span.style.backgroundColor = '#EBEBE4'; 91 | break; 92 | 93 | case 'darwin': 94 | span.classList.add('grey'); 95 | span.style.backgroundColor = 'white'; 96 | break; 97 | 98 | } 99 | 100 | textInput.tabIndex = -1; 101 | 102 | } 103 | 104 | }; 105 | 106 | function highlightInput (node) { 107 | 108 | const span = getSpanFromChild(node); 109 | 110 | if (getAttributeValue(node, 'selected') === 'true') { 111 | 112 | span.style.backgroundColor = 'Highlight'; 113 | span.style.color = 'HighlightText'; 114 | 115 | } else { 116 | 117 | span.style.color = ''; 118 | span.style.backgroundColor = ''; 119 | span.classList.remove('grey'); 120 | 121 | } 122 | 123 | } 124 | 125 | exports.updateHighlights = (div) => { 126 | 127 | highlightInput(div.getElementsByClassName('duration-holder')[0]); 128 | 129 | }; 130 | 131 | function updateValue (inputKey, node) { 132 | 133 | let newValue, maxValue; 134 | 135 | const currentNumbersEntered = parseInt(getAttributeValue(node, 'numbersEntered')); 136 | const currentInputValue = getAttributeValue(node, 'inputValue'); 137 | 138 | maxValue = getAttributeValue(node, 'maxValue'); 139 | maxValue = (maxValue === null) ? DEFAULT_MAX_VALUE : maxValue; 140 | 141 | if (currentNumbersEntered === 1) { 142 | 143 | newValue = inputKey; 144 | 145 | setAttributeValue(node, 'inputValue', parseInt(newValue)); 146 | getSpanFromChild(node).innerText = newValue; 147 | 148 | } else { 149 | 150 | if (currentInputValue === '0') { 151 | 152 | newValue = inputKey; 153 | 154 | setAttributeValue(node, 'inputValue', parseInt(newValue)); 155 | getSpanFromChild(node).innerText = newValue; 156 | 157 | setAttributeValue(node, 'numbersEntered', 1); 158 | 159 | } else { 160 | 161 | newValue = currentInputValue + inputKey; 162 | 163 | if (parseInt(newValue) > maxValue) { 164 | 165 | setAttributeValue(node, 'inputValue', maxValue.toString()); 166 | getSpanFromChild(node).innerText = maxValue.toString(); 167 | 168 | } else { 169 | 170 | setAttributeValue(node, 'inputValue', parseInt(newValue)); 171 | getSpanFromChild(node).innerText = newValue; 172 | 173 | } 174 | 175 | } 176 | 177 | } 178 | 179 | } 180 | 181 | function incrementValue (node) { 182 | 183 | let newValue, maxValue, minValue; 184 | 185 | maxValue = getAttributeValue(node, 'maxValue'); 186 | maxValue = (maxValue === null) ? DEFAULT_MAX_VALUE : parseInt(maxValue); 187 | 188 | newValue = parseInt(getAttributeValue(node, 'inputValue')) + 1; 189 | newValue = (newValue > maxValue) ? maxValue : newValue; 190 | 191 | setAttributeValue(node, 'inputValue', newValue); 192 | getSpanFromChild(node).innerText = newValue; 193 | 194 | minValue = getAttributeValue(node, 'minValue'); 195 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue); 196 | 197 | setAttributeValue(node, 'numbersEntered', minValue.toString().length - 1); 198 | 199 | } 200 | 201 | function decrementValue (node) { 202 | 203 | let newValue, minValue; 204 | 205 | minValue = getAttributeValue(node, 'minValue'); 206 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue); 207 | 208 | newValue = parseInt(getAttributeValue(node, 'inputValue')) - 1; 209 | newValue = (parseInt(newValue) < minValue) ? minValue : newValue; 210 | 211 | setAttributeValue(node, 'inputValue', newValue); 212 | getSpanFromChild(node).innerText = newValue; 213 | 214 | setAttributeValue(node, 'numbersEntered', minValue.toString().length - 1); 215 | 216 | } 217 | 218 | function handleKeyDown (e) { 219 | 220 | if (e.key === 'Tab') { 221 | 222 | return; 223 | 224 | } 225 | 226 | e.preventDefault(); 227 | 228 | if (e.key === 'ArrowUp') { 229 | 230 | incrementValue(e.target); 231 | e.target.parentNode.dispatchEvent(new Event('change')); 232 | return; 233 | 234 | } 235 | 236 | if (e.key === 'ArrowDown') { 237 | 238 | decrementValue(e.target); 239 | e.target.parentNode.dispatchEvent(new Event('change')); 240 | return; 241 | 242 | } 243 | 244 | if (e.key === 'Backspace' || e.key === 'Delete') { 245 | 246 | let minValue = getAttributeValue(e.target, 'minValue'); 247 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue); 248 | 249 | setAttributeValue(e.target, 'inputValue', minValue.toString()); 250 | getSpanFromChild(e.target).innerText = minValue.toString(); 251 | setAttributeValue(e.target, 'numbersEntered', '0'); 252 | 253 | e.target.parentNode.dispatchEvent(new Event('change')); 254 | 255 | return; 256 | 257 | } 258 | 259 | const patt = /^[0-9]$/; 260 | 261 | if (patt.test(e.key)) { 262 | 263 | const currentNumbersEntered = parseInt(getAttributeValue(e.target, 'numbersEntered')); 264 | 265 | let maxCharLength = getAttributeValue(e.target, 'maxcharlength'); 266 | maxCharLength = (maxCharLength === null) ? DEFAULT_MAX_CHAR_LENGTH : maxCharLength; 267 | 268 | if (currentNumbersEntered <= maxCharLength) { 269 | 270 | setAttributeValue(e.target, 'numbersEntered', (currentNumbersEntered + 1).toString()); 271 | updateValue(e.key, e.target); 272 | 273 | } 274 | 275 | e.target.parentNode.dispatchEvent(new Event('change')); 276 | 277 | } 278 | 279 | } 280 | 281 | function isEnabled (node) { 282 | 283 | return node.getAttribute('enabled') === 'true'; 284 | 285 | } 286 | 287 | class DurationInput extends HTMLElement { 288 | 289 | connectedCallback () { 290 | 291 | let width, height, minValue; 292 | 293 | const parent = this.parentNode; 294 | 295 | const attributes = this.attributes; 296 | 297 | width = this.style.width; 298 | width = (width === '') ? DEFAULT_WIDTH : width; 299 | height = this.style.height; 300 | height = (height === '') ? DEFAULT_HEIGHT : height; 301 | 302 | parent.removeChild(this); 303 | 304 | const divNode = document.createElement('div'); 305 | divNode.style = 'position: relative;'; 306 | 307 | for (let i = 0; i < attributes.length; i++) { 308 | 309 | if (attributes[i].name !== 'style') { 310 | 311 | divNode.setAttribute(attributes[i].name, attributes[i].value); 312 | 313 | } 314 | 315 | } 316 | 317 | minValue = divNode.getAttribute('minValue'); 318 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue); 319 | 320 | const inputNode = document.createElement('input'); 321 | inputNode.className = 'duration-text-input'; 322 | inputNode.type = 'text'; 323 | inputNode.style = 'width: ' + width + '; height: ' + height + '; color: white; caret-color: transparent;'; 324 | 325 | divNode.appendChild(inputNode); 326 | 327 | const blockerNode = document.createElement('div'); 328 | blockerNode.style = 'position: absolute; top: 0px; margin-left: 0px; margin-top: 0px; width: 100%; height: 100%;'; 329 | 330 | const holderNode = document.createElement('div'); 331 | holderNode.className = 'duration-holder'; 332 | holderNode.style = 'position: absolute; top: 0px; margin-left: 5%; margin-top: 3px; width: 90%;'; 333 | 334 | const spanNode = document.createElement('span'); 335 | spanNode.className = 'duration-span'; 336 | spanNode.innerText = minValue.toString(); 337 | spanNode.style = 'float: right;'; 338 | 339 | holderNode.appendChild(spanNode); 340 | 341 | blockerNode.appendChild(holderNode); 342 | 343 | divNode.appendChild(blockerNode); 344 | 345 | parent.appendChild(divNode); 346 | 347 | divNode.setAttribute('inputValue', minValue.toString()); 348 | divNode.setAttribute('numbersEntered', minValue.toString().length - 1); 349 | divNode.setAttribute('selected', 'false'); 350 | divNode.setAttribute('enabled', 'true'); 351 | 352 | inputNode.addEventListener('keydown', handleKeyDown); 353 | 354 | holderNode.addEventListener('click', () => { 355 | 356 | if (!isEnabled(divNode)) { 357 | 358 | return; 359 | 360 | } 361 | 362 | setAttributeValue(inputNode, 'selected', 'true'); 363 | highlightInput(inputNode); 364 | 365 | inputNode.focus(); 366 | 367 | const ariaLabel = divNode.getAttribute('aria-label'); 368 | let ariaDescription = ariaLabel || ''; 369 | ariaDescription += ' edit ' + spanNode.innerText; 370 | 371 | ariaSpeak.speak(ariaDescription); 372 | 373 | }); 374 | 375 | inputNode.addEventListener('focusin', () => { 376 | 377 | minValue = divNode.getAttribute('minValue'); 378 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue); 379 | 380 | if (!isEnabled(divNode)) { 381 | 382 | inputNode.blur(); 383 | return; 384 | 385 | } 386 | 387 | divNode.setAttribute('numbersEntered', minValue.toString().length - 1); 388 | setAttributeValue(inputNode, 'selected', 'true'); 389 | highlightInput(inputNode); 390 | 391 | const ariaLabel = divNode.getAttribute('aria-label'); 392 | let ariaDescription = ariaLabel || ''; 393 | ariaDescription += ' edit ' + spanNode.innerText; 394 | 395 | ariaSpeak.speak(ariaDescription); 396 | 397 | }); 398 | 399 | inputNode.addEventListener('focusout', () => { 400 | 401 | let maxValue; 402 | 403 | maxValue = divNode.getAttribute('maxValue'); 404 | maxValue = (maxValue === null) ? DEFAULT_MAX_VALUE : parseInt(maxValue); 405 | 406 | if (!isEnabled(divNode)) { 407 | 408 | return; 409 | 410 | } 411 | 412 | const currentInputValue = parseInt(getAttributeValue(inputNode, 'inputValue')); 413 | 414 | if (currentInputValue < minValue || currentInputValue > maxValue) { 415 | 416 | setAttributeValue(inputNode, 'inputValue', minValue.toString()); 417 | setAttributeValue(inputNode, 'numbersEntered', minValue.toString().length - 1); 418 | getSpanFromChild(inputNode).innerText = minValue.toString(); 419 | 420 | } 421 | 422 | setAttributeValue(inputNode, 'selected', 'false'); 423 | highlightInput(inputNode); 424 | 425 | divNode.dispatchEvent(new Event('change')); 426 | 427 | }); 428 | 429 | inputNode.addEventListener('click', () => { 430 | 431 | if (!isEnabled(divNode)) { 432 | 433 | return; 434 | 435 | } 436 | 437 | highlightInput(inputNode); 438 | inputNode.focus(); 439 | 440 | const ariaLabel = divNode.getAttribute('aria-label'); 441 | let ariaDescription = ariaLabel || ''; 442 | ariaDescription += ' edit ' + spanNode.innerText; 443 | 444 | ariaSpeak.speak(ariaDescription); 445 | 446 | }); 447 | 448 | } 449 | 450 | } 451 | 452 | customElements.define('duration-input', DurationInput); 453 | --------------------------------------------------------------------------------