From 487f30d320af6c57f2abd564c29454e6025c60e1 Mon Sep 17 00:00:00 2001
From: Daniel Naab <dnaab@usgs.gov>
Date: Wed, 14 Mar 2018 10:22:16 -0500
Subject: [PATCH] Play first current and compare series (like the tooltips)

---
 .../scripts/components/hydrograph/audible.js  | 115 ++++++++----------
 .../components/hydrograph/audible.spec.js     |  67 +++++++++-
 .../scripts/components/hydrograph/index.js    |   2 +-
 assets/src/styles/components/_hydrograph.scss |   4 +
 4 files changed, 119 insertions(+), 69 deletions(-)

diff --git a/assets/src/scripts/components/hydrograph/audible.js b/assets/src/scripts/components/hydrograph/audible.js
index 31a5521a1..309b66e8e 100644
--- a/assets/src/scripts/components/hydrograph/audible.js
+++ b/assets/src/scripts/components/hydrograph/audible.js
@@ -1,10 +1,9 @@
-const { max, min } = require('d3-array');
 const { scaleLinear } = require('d3-scale');
 const { select } = require('d3-selection');
 const memoize = require('fast-memoize');
 const { createSelector, createStructuredSelector } = require('reselect');
 
-const { flatPointsSelector } = require('./timeseries');
+const { yScaleSelector } = require('./scales');
 const { tsDatumSelector } = require('./tooltip');
 
 const { dispatch, link } = require('../../lib/redux');
@@ -14,49 +13,25 @@ const { Actions } = require('../../store');
 // Higher tones get lower volume
 const volumeScale = scaleLinear().range([2, .3]);
 
-
 const AudioContext = window.AudioContext || window.webkitAudioContext;
 const getAudioContext = memoize(function () {
-    return AudioContext ? new AudioContext() : null;
-});
-
-// Create a compressor node, to prevent clipping noises
-const getCompressor = memoize(audioCtx => {
-    const compressor = audioCtx.createDynamicsCompressor();
-    compressor.threshold.setValueAtTime(-50, getAudioContext().currentTime);
-    compressor.knee.setValueAtTime(40, getAudioContext().currentTime);
-    compressor.ratio.setValueAtTime(12, getAudioContext().currentTime);
-    compressor.attack.setValueAtTime(0, getAudioContext().currentTime);
-    compressor.release.setValueAtTime(0.25, getAudioContext().currentTime);
-    return compressor;
+    return new AudioContext();
 });
 
-const getOscillator = function(audioCtx, value) {
+export const createSound = memoize(/* eslint-disable no-unused-vars */ tsKey => {
+    const audioCtx = getAudioContext();
     const oscillator = audioCtx.createOscillator();
-    //oscillator.type = 'triangle';
-    oscillator.type = 'sine';
-
-    // Set the frequency, in hertz
-    oscillator.frequency.value = value;
-
-    return oscillator;
-};
-
-const getGainNode = function(audioCtx, value) {
     const gainNode = audioCtx.createGain();
-    gainNode.gain.value = volumeScale(value);
-    gainNode.gain.value = 1;
-
-    return gainNode;
-};
+    const compressor = audioCtx.createDynamicsCompressor();
 
-export const createSound = function (value) {
-    const audioCtx = getAudioContext();
-    const oscillator = getOscillator(audioCtx, value);
-    const gainNode = getGainNode(audioCtx, value);
-    const compressor = getCompressor(audioCtx);
+    compressor.threshold.setValueAtTime(-50, audioCtx.currentTime);
+    compressor.knee.setValueAtTime(40, audioCtx.currentTime);
+    compressor.ratio.setValueAtTime(12, audioCtx.currentTime);
+    compressor.attack.setValueAtTime(0, audioCtx.currentTime);
+    compressor.release.setValueAtTime(0.25, audioCtx.currentTime);
 
     // Connect the oscillator to the gainNode to modulate volume
+    oscillator.type = 'sine';
     oscillator.connect(gainNode);
 
     // Connect the gainNode to the compressor to address clipping
@@ -68,23 +43,35 @@ export const createSound = function (value) {
     // Start the oscillator
     oscillator.start();
 
-    // This rapidly ramps sound down
-    gainNode.gain.setTargetAtTime(0, audioCtx.currentTime, .2);
-};
+    return {oscillator, gainNode, compressor};
+});
 
-const audibleScale = function (domain) {
-    return scaleLinear().domain(domain).range([80, 1500]);
+export const updateSound = function ({enabled, points}) {
+    const audioCtx = getAudioContext();
+    for (const tsKey of Object.keys(points)) {
+        const point = points[tsKey];
+        const {oscillator, gainNode} = createSound(tsKey);
+        oscillator.frequency.setTargetAtTime(
+            enabled && point ? point : null,
+            audioCtx.currentTime,
+            .2
+        );
+        gainNode.gain.setTargetAtTime(
+            enabled && point ? volumeScale(point) : null,
+            audioCtx.currentTime,
+            .2
+        );
+    }
 };
 
 export const audibleInterfaceOnSelector = state => state.audibleInterfaceOn;
 
 export const audibleScaleSelector = memoize(tsKey => createSelector(
-    flatPointsSelector(tsKey),
-    (points) => {
-        return audibleScale([
-            min(points.map((datum) => datum.value)),
-            max(points.map((datum) => datum.value))
-        ]);
+    yScaleSelector,
+    (scale) => {
+        const audibleScale = scale.copy();
+        audibleScale.range([80, 1500]);
+        return audibleScale;
     }
 ));
 
@@ -94,15 +81,6 @@ export const audibleUI = function (elem) {
         return;
     }
 
-    elem.append('audio')
-        .attr('id', 'audible-controls')
-        .attr('controls', true)
-        .attr('muted', true)
-        .style('width', '100%')
-        .on('change', function () {
-            console.log(arguments);
-        });
-
     elem.append('input')
         .attr('type', 'checkbox')
         .attr('id', 'audible-checkbox')
@@ -121,18 +99,21 @@ export const audibleUI = function (elem) {
 
     // Listen for focus changes, and play back the audio representation of
     // the selected points.
-    // FIXME: Handle more than just the first current time series.
-    elem.call(link(function (elem, {datum, enabled, scale}) {
-        if (!enabled) {
-            return;
-        }
-        if (!datum) {
-            return;
-        }
-        createSound(scale(datum.value));
+    // TODO: Handle more than just the first time series of each tsKey. This can
+    // piggyback on work to support multiple tooltip selections.
+    elem.call(link(function (elem, {enabled, datumCurrent, datumCompare, yScaleCurrent, yScaleCompare}) {
+        updateSound({
+            points: {
+                current: datumCurrent ? yScaleCurrent(datumCurrent.value) : null,
+                compare: datumCompare ? yScaleCompare(datumCompare.value) : null
+            },
+            enabled
+        });
     }, createStructuredSelector({
-        datum: tsDatumSelector('current'),
         enabled: audibleInterfaceOnSelector,
-        scale: audibleScaleSelector('current')
+        datumCurrent: tsDatumSelector('current'),
+        datumCompare: tsDatumSelector('compare'),
+        yScaleCurrent: audibleScaleSelector('current'),
+        yScaleCompare: audibleScaleSelector('compare')
     })));
 };
diff --git a/assets/src/scripts/components/hydrograph/audible.spec.js b/assets/src/scripts/components/hydrograph/audible.spec.js
index 8b053701e..0eb8f213d 100644
--- a/assets/src/scripts/components/hydrograph/audible.spec.js
+++ b/assets/src/scripts/components/hydrograph/audible.spec.js
@@ -2,14 +2,79 @@ const { select } = require('d3-selection');
 
 const { audibleUI } = require('./audible');
 
+const { provide } = require('../../lib/redux');
+const { Actions, configureStore } = require('../../store');
+
+
+const TEST_STATE = {
+    series: {
+        timeSeries: {
+            '00060:current': {
+                startTime: new Date('2018-01-02T15:00:00.000-06:00'),
+                endTime: new Date('2018-01-02T15:00:00.000-06:00'),
+                points: [{
+                    dateTime: new Date('2018-01-02T15:00:00.000-06:00'),
+                    value: 10,
+                    qualifiers: ['P']
+                }],
+                method: 'method1',
+                tsKey: 'current',
+                variable: 45807197
+            },
+            '00060:compare': {
+                startTime: new Date('2018-01-02T15:00:00.000-06:00'),
+                endTime: new Date('2018-01-02T15:00:00.000-06:00'),
+                points: [{
+                    dateTime: new Date('2018-01-02T15:00:00.000-06:00'),
+                    value: 10,
+                    qualifiers: ['P']
+                }],
+                method: 'method1',
+                tsKey: 'compare',
+                variable: 45807197
+            }
+        },
+        variables: {
+            '45807197': {
+                variableCode: '00060',
+                oid: 45807197,
+                unit: {
+                    unitCode: 'unitCode'
+                }
+            }
+        }
+    },
+    currentVariableID: '45807197',
+    showSeries: {
+        current: true,
+        compare: true
+    }
+};
+
 
 describe('Audible interface', () => {
+    let container;
     beforeEach(() => {
+        container = select('body').append('div');
+        container
+            .call(provide(configureStore(TEST_STATE)))
+            .call(audibleUI);
     });
 
     afterEach(() => {
+        container.remove();
+    });
+
+    it('renders', () => {
+        const checkbox = select('#audible-checkbox');
+        expect(checkbox).toBeTruthy();
     });
 
-    it('checkbox created successfully', () => {
+    it('does nothing unexpected when playing a sound', () => {
+        Actions.toggleAudibleInterface(true);
+        Actions.setTooltipTime(
+            TEST_STATE.series.timeSeries['00060:current'].points[0].dateTime,
+            TEST_STATE.series.timeSeries['00060:compare'].points[0].dateTime
+        );
     });
 });
diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js
index 1663ae492..727a5bc60 100644
--- a/assets/src/scripts/components/hydrograph/index.js
+++ b/assets/src/scripts/components/hydrograph/index.js
@@ -358,9 +358,9 @@ const attachToNode = function (store, node, {siteno} = {}) {
     store.dispatch(Actions.resizeUI(window.innerWidth, node.offsetWidth));
     select(node)
         .call(provide(store))
-        .call(audibleUI)
         .call(timeSeriesGraph)
         .call(timeSeriesLegend)
+        .call(audibleUI)
         .select('.hydrograph-last-year-input')
             .on('change', dispatch(function () {
                 return Actions.toggleTimeseries('compare', this.checked);
diff --git a/assets/src/styles/components/_hydrograph.scss b/assets/src/styles/components/_hydrograph.scss
index 3bd94d7ca..b5435447d 100644
--- a/assets/src/styles/components/_hydrograph.scss
+++ b/assets/src/styles/components/_hydrograph.scss
@@ -203,3 +203,7 @@ table#select-timeseries {
     }
 
 }
+
+#audible-label {
+    padding-left: 2em;
+}
-- 
GitLab