RxJS: Progress bar with countdown - ionic-framework

I am trying to make a button with progress bar and text underneath the button that will tell how much time left. I am using ionic progress bar that take value between [0 ... 1].
public timeToClose = 5 * 1000;
...
const interval = this.timeToClose / 100;
timer(0, interval).pipe(
tap((v) => this.progress = v / 100),
map(i => this.timeToClose - i * interval),
take(100),
tap((timeLeft) => this.timeLeft = Math.floor(timeLeft / 1000) + 1),
finalize(() => this.closeModal()),
).subscribe();
But this looks awfull. How can i make it better code?

I would do it like that:
of(5).pipe( // <- time in seconds we want the delay.
map(v => v * 1000),
switchMap(delay => {
const start = new Date().getTime();
return timer(0, 250).pipe( // <- speed of updates.
map(() => (new Date().getTime() - start) / delay),
takeWhile(result => result < 1, true),
);
}),
tap(v => this.progress = v), // <- your stuff
finalize(() => this.closeModal()), // <- your stuff
).subscribe();

I would like to suggest a different approach:
// Total amount of steps
const steps = 9;
// Total "loading" time in milliseconds
const totalTimeMs = 3000;
interval(totalTimeMs / steps).pipe(
take(steps),
map(step => step + 1),
tap(step => {
const progressPercent = step / steps * 100;
// update your progress bar
})).subscribe();

Related

Getting the cell given the cell value in google sheets using app script

I'm trying to write a script that tracks payment dates in google sheets (shows a different colour (either FontColor or Background) three days before payment, another colour on the day of payment and a totally different colour after the payment date.I'd appreciate if there's anyone with know how on how to use those values to get the cell name and use it to change the FontColor or alternatively if there's a better solution
Here is my google sheet
[![enter image description here][1]][1]
This is the code I've written to get the dates into a list
function myFunction() {
let spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let lastRow = spreadsheet.getLastRow();
let lastCol = spreadsheet.getLastColumn();
var dataRange = spreadsheet.getActiveSheet().getRange(2, 11, lastRow, lastCol)
dataRange.setFontColor("green")
var data = dataRange.getDisplayValues();
let dates=[];
for (let i=0; i < dates.length; i++ ) {
// console.log(dates[i])
if (dates[i] === new Date().toLocaleDateString()) {
dataRange.setBackground('pink')
} else if (dates[i]) {
// do sth
} else {
// maintain the current state
}
}
}
Does it need to be with scripts?? With conditional formatting that would be MUCH faster, easier and uploads constantly.
You can apply it to the entire sheet or to a specific range. Use this custom formula (change A1 with the top left formula of your range)
=if(A1="",FALSE,(A1 - Today()) < 0)
Get sure to set these conditions in the correct order (in these case it would be preferrable to be the past dates, the actual date and the close future dates). Like this:
Here you have a link to play with:
https://docs.google.com/spreadsheets/d/1zhEFRQwOyAYQwXfv5lYTjI7B-6fIfz1rgdCt3MGvzmI/edit?usp=sharing
Payment Tracker
function paymentTracker() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getRange(2, 11, sh.getLastRow() - 1, sh.getLastColumn() - 10);
rg.setFontColor("black")
const vs = rg.getDisplayValues();
const d = new Date();
//Logger.log('y: %s,m: %s,d: %s', d.getFullYear(), d.getMonth(), d.getDate());
const dt = new Date(d.getFullYear(), d.getMonth(), d.getDate());
const dtv = dt.valueOf();
const dt3 = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 3);
const dt3v = dt3.valueOf();
vs.forEach((r, i) => {
let ds = r.map(ds => {
let t = ds.split('/');
//Logger.log(JSON.stringify(t))
let v = new Date(Number(t[2]), Number(t[1]) - 1, Number(t[0])).valueOf();
let diff3 = dt3v - v;
if (dt3v == v) {
return "purple";
} else if (dtv == v) {
return "green";
} else {
return "pink";
}
});
sh.getRange(i + 2, 11, 1, ds.length).setBackgrounds([ds]);
})
}

How to subclass Polygon in Leaflet?

I am trying to create a Hex class for Leaflet where the center and side length should be in screen units (pixels) as opposed to lat/lng to obtain an effect like this over the map.
Code is here:
L.Hex = L.Polygon.extend({
initialize: function (args) {
console.log('arguments', JSON.stringify(arguments));
console.log('arguments[0]', JSON.stringify(arguments[0]));
// options = options || {};
this.center = args[0];
this.size = args[1];
this.options = args[2] || {};
L.Polygon.prototype.initialize.call(this, [], this.options);
},
points: function(center, size, map){
var latlngs = [0, 1, 2, 3, 4, 5, 6]
.map(p => this.hexCorner(center, size, p))
.map(p => map.layerPointToLatLng(p));
return latlngs;
},
hexCorner: function(center, size, i){
var angle_deg = 60 * i - 30;
var angle_rad = Math.PI / 180 * angle_deg;
return [center[0] + size * Math.cos(angle_rad),
center[1] + size * Math.sin(angle_rad)]
},
})
L.hex = function(){ return new L.Hex(arguments) }
Since the coordinates are in user space I think can only calculate them after adding to the map - but that's what fails. If add the points like this:
var h = L.hex([100, 100], 30);
var points = h.points([100, 100], 30, e.map);
h.setLatLngs(points);
h.addTo(map);
things work ok but my best attempt at onAdd which is this:
onAdd: function (map) {
var points = this.points(this.center, this.size, map);
this.setLatLngs(points);
map.addLayer(this);
},
with message TypeError: t._path is undefined
So the question is: where is the problem and how should I do it otherwise?
Change this.setLatLngs(points) to this._setLatLngs(points);.
With setLatLngs() the function this.redraw(); is called and this needs the map variable, which is applied with map.addLayer(this)

OfflineAudioContext Latency Shift

I'm using OfflineAudioContext to download an input file with effects applied. The download works great and it's really fast, but the problem I'm running into is that when I apply gain, I'm using an analyser to signal when the gain should increase or decrease.
This works great when playing the audio using an AudioContext, but the offline version causes the timing of the gain to shift very noticeably. The increase is starting late and the decrease is starting late. It's like there's a latency shift overall.
Is there a way to combat this shift? I'm fine with the rendering process taking longer.
var chunks = [];
var fileInput = document.getElementById("input");
var process = document.getElementById("process");
//Load audio file listener
process.addEventListener(
"click",
function () {
// Web Audio
var audioCtx2 = new (AudioContext || webkitAudioContext)();
// Reset buttons and log
$("#log").empty();
$("#download_link").addClass("d-none");
$("#repeat_link").addClass("d-none");
// Check for file
if (fileInput.files[0] == undefined) {
if ($("#upload_err").hasClass("d-none")) {
$("#upload_err").removeClass("d-none");
}
return false;
}
var reader1 = new FileReader();
reader1.onload = function (ev) {
// console.log("Reader loaded.");
var tempBuffer = audioCtx2.createBufferSource();
// Decode audio
audioCtx2.decodeAudioData(ev.target.result).then(function (buffer) {
// console.log("Duration1 = " + buffer.duration);
var offlineAudioCtx = new OfflineAudioContext({
numberOfChannels: 2,
length: 44100 * buffer.duration,
sampleRate: 44100,
});
// console.log("test 1");
// Audio Buffer Source
var soundSource = offlineAudioCtx.createBufferSource();
var analyser2d = offlineAudioCtx.createAnalyser();
var dgate1 = offlineAudioCtx.createGain();
var dhpf = offlineAudioCtx.createBiquadFilter();
var dhum60 = offlineAudioCtx.createBiquadFilter();
var dcompressor = offlineAudioCtx.createDynamicsCompressor();
dhpf.type = "highpass";
dhpf.Q.value = 0.5;
dhum60.type = "notch";
dhum60.Q.value = 130;
dcompressor.knee.setValueAtTime(40, offlineAudioCtx.currentTime);
dcompressor.attack.setValueAtTime(0.1, offlineAudioCtx.currentTime);
dcompressor.release.setValueAtTime(0.2, offlineAudioCtx.currentTime);
var reader2 = new FileReader();
// console.log("Created Reader");
reader2.onload = function (ev) {
// console.log("Reading audio data to buffer...");
$("#log").append("<p>Buffering...</p>");
soundSource.buffer = buffer;
let context = offlineAudioCtx;
//Before Effects
analyser2d = context.createAnalyser();
analyser2d.fftSize = 2048;
analyser2d.smoothingTimeConstant = 0.85;
const sampleBuffer = new Float32Array(analyser2d.fftSize);
function loop() {
analyser2d.getFloatTimeDomainData(sampleBuffer);
let sumOfSquares = 0;
for (let i = 0; i < sampleBuffer.length; i++) {
sumOfSquares += sampleBuffer[i] ** 2;
}
const avgPowerDecibels = Math.round(
10 * Math.log10(sumOfSquares / sampleBuffer.length)
);
const gainset = avgPowerDecibels > -50 ? 1 : 0;
//real-time effects choices start
if (
document.getElementById("gate").getAttribute("data-active") ===
"true"
) {
dgate1.gain.setTargetAtTime(
gainset,
offlineAudioCtx.currentTime,
0.05
);
} else if (
document.getElementById("gate").getAttribute("data-active") ===
"false"
) {
dgate1.gain.setTargetAtTime(
1,
offlineAudioCtx.currentTime,
0.05
);
}
if (
document.getElementById("hpf").getAttribute("data-active") ===
"true"
) {
dhpf.frequency.value = 90;
} else if (
document.getElementById("hpf").getAttribute("data-active") ===
"false"
) {
dhpf.frequency.value = 0;
}
if (
document.getElementById("hum").getAttribute("data-active") ===
"true"
) {
dhum60.frequency.value = 60;
} else if (
document.getElementById("hum").getAttribute("data-active") ===
"false"
) {
dhum60.frequency.value = 0;
}
if (
document.getElementById("comp").getAttribute("data-active") ===
"true"
) {
dcompressor.threshold.setValueAtTime(
-30,
offlineAudioCtx.currentTime
);
dcompressor.ratio.setValueAtTime(
3.5,
offlineAudioCtx.currentTime
);
} else if (
document.getElementById("comp").getAttribute("data-active") ===
"false"
) {
dcompressor.threshold.setValueAtTime(
0,
offlineAudioCtx.currentTime
);
dcompressor.ratio.setValueAtTime(1, offlineAudioCtx.currentTime);
}
// Display value.
requestAnimationFrame(loop);
}
loop();
soundSource
.connect(analyser2d)
.connect(dhpf)
.connect(dhum60)
.connect(dgate1)
.connect(dcompressor);
dcompressor.connect(offlineAudioCtx.destination);
offlineAudioCtx
.startRendering()
.then(function (renderedBuffer) {
// console.log('Rendering completed successfully.');
$("#log").append("<p>Rendering new file...</p>");
//var song = offlineAudioCtx.createBufferSource();
console.log(
"OfflineAudioContext.length = " + offlineAudioCtx.length
);
split(renderedBuffer, offlineAudioCtx.length);
$("#log").append("<p>Finished!</p>");
})
.catch(function (err) {
// console.log('Rendering failed: ' + err);
$("#log").append("<p>Rendering failed.</p>");
});
soundSource.loop = false;
};
reader2.readAsArrayBuffer(fileInput.files[0]);
soundSource.start(0);
});
};
reader1.readAsArrayBuffer(fileInput.files[0]);
},
false
);
I've included what I believe is the relevant portion of the code. Let me know if more is needed. Thanks!
This is doesn't work because the OfflineAudioContext runs faster (sometimes hundreds of times faster) than realtime. Your loop that gets the analyser data is done every 16 ms or so. In a realtime system, this might be accurate enough, but the offline context could be running much, much faster than realtime so by the time you've grabbed the analyser data, much more time has passed. (You can probably see this by printing out the context currentTime in the loop. It will probably increment by more (much more) than 16 ms.
The best way to do this is to use context.suspend(t) to suspend the context at known times so you can grab the analyser data synchronously. Note that the time is rounded so it might not be exactly the time you want but perhaps close enough. Note also that AFAIK, Firefox has not implemented this, and neither has Safari (but will soon).
Here's a short snippet with how do use suspend. There's more than one way though. Untested, but I think the general idea is correct:
// Grab the data every 16 ms, roughly. `suspend` rounds the time up to
// the nearest multiple of 128 frames (or 128/context.sampleRate time).
for (t = 0; t < <length of buffer>; t += 0.016) {
context.suspend(t)
.then(() => {
// Use analyser to grab the data and compute the values.
// Use setValueAtTime and friends to adjust the gain and compressor
// appropriately. Use context.currentTime to know at what time
// the context was suspended, and schedule the modifications to be
// at least 128 samples in the future. (I think).
})
.then(() => context.resume());
}
An alternative is to create a realtime context and process it as you do now, and when the buffer is finished playing, close the context. Of course, you'll have to add something (ScriptProcessorNode, AudioWorkletNode, or MediaRecorder) to capture the rendered data.
If none of these work for you, then I'm not sure what the alternatives would be.

Create Shape / Drag and Drop

I'm trying to work with a createjs canvas to add new states (circles) on empty spots of stage on click, but drag existing points on click. I'm assuming the problem is that when I try to remove the stagemousemove event in handleMove it doesn't properly remove since it doesn't have a dedicated static handler, but I'm not sure how I'd even create that handler.
// create new state (circle)
function createState(e) {
x = e.stageX
y = e.stageY
if (stage.children.every(el =>
!(el.hitTest(x, y)) &&
!(el.hitTest(x + 30, y + 30)) &&
!(el.hitTest(x + 30, y - 30)) &&
!(el.hitTest(x - 30, y + 30)) &&
!(el.hitTest(x - 30, y - 30))
)) {
state = new createjs.Shape()
state.graphics.ss(3)
.s('#777')
.f('#eaeaea')
.dc(x, y, 30)
stage.addChild(state)
state.on('mousedown', handleMove)
stage.update()
}
}
// drag and drop
function handleMove(e) {
el = stage.getObjectUnderPoint(stage.mouseX, stage.mouseY);
var offset = {
x: el.x - e.stageX,
y: el.y - e.stageY
}
stage.on('stagemousemove', (evt) => {
el.x = offset.x + evt.stageX
el.y = offset.y + evt.stageY
stage.update()
})
stage.on('stagemouseup', (e) => {
stage.off('stagemousemove', (evt) => {
el.x = offset.x + evt.stageX
el.y = offset.y + evt.stageY
stage.update()
})
})
}
// determines whether to create new point
function handler(e) {
if (stage.getObjectUnderPoint(stage.mouseX, stage.mouseY) == null)
createState(e)
}
stage.on('stagemousedown', handler)
When you use the off() method, you must pass a reference to the result of the on() call.
let listener = stage.on('stagemousemove', (evt) => {
el.x = offset.x + evt.stageX
el.y = offset.y + evt.stageY
stage.update()
});
stage.on('stagemouseup', (e) => {
stage.off('stagemousemove', listener);
});
The on method creates a function closure for you automatically to maintain scope, which ES5 JavaScript does not do. This lets you pass a scope parameter to on so this is the expected scope.
When using arrow functions, this closure happens during ES5 transpiling. You can not just pass a similar function to the off, since they are not equal.
Hope that helps!

What is wrong with my coffeescript translation of this JS?

Trying to improve my Coffeescript skills, so thought I'd covert this jcrop demo. However it's not working as expect. Specifically, the redraw function does not appear to be being called.
This works fine.
// Create a new Selection object extended from Selection
var CircleSel = function(){ };
// Set the custom selection's prototype object to be an instance
// of the built-in Selection object
CircleSel.prototype = new $.Jcrop.component.Selection();
// Then we can continue extending it
$.extend(CircleSel.prototype,{
zoomscale: 1,
attach: function(){
this.frame.css({
background: 'url(' + $('#target')[0].src.replace('750','750') + ')'
});
},
positionBg: function(b){
var midx = ( b.x + b.x2 ) / 2;
var midy = ( b.y + b.y2 ) / 2;
var ox = (-midx*this.zoomscale)+(b.w/2);
var oy = (-midy*this.zoomscale)+(b.h/2);
//this.frame.css({ backgroundPosition: ox+'px '+oy+'px' });
this.frame.css({ backgroundPosition: -(b.x+1)+'px '+(-b.y-1)+'px' });
},
redraw: function(b){
// Call original update() method first, with arguments
$.Jcrop.component.Selection.prototype.redraw.call(this,b);
this.positionBg(this.last);
return this;
},
prototype: $.Jcrop.component.Selection.prototype
});
But when I try to write this in Coffescript it fails
CircleSel.prototype = new ($.Jcrop.component.Selection)
$.extend CircleSel.prototype,
zoomscale: 1
attach: ->
#frame.css background: 'url(' + $('#target')[0].src.replace('750', '750') + ')'
return
positionBg: (b) ->
midx = (b.x + b.x2) / 2
midy = (b.y + b.y2) / 2
ox = -midx * #zoomscale + b.w / 2
oy = -midy * #zoomscale + b.h / 2
#frame.css backgroundPosition: -(b.x + 1) + 'px ' + -b.y - 1 + 'px'
return
# this redraw function is not being called, everything else appears to work fine
redraw: (b) ->
$.Jcrop.component.Selection::redraw.call this, b
#positionBg #last
this
prototype: $.Jcrop.component.Selection.prototype
What have I done wrong?