I got myself into a tangle which probably involves multiple asynchronous callback situations.
I have a javascript function called populatePageArea()
Inside populatePageArea it traverses through this array-like variable called pages amongst other code.
function populatePagesArea() {
// there was some code before the for loop
for (var i=0, l=pages.length; i<l; i++) {
addToPagesArea(pages[i], "");
}
// some code after...
}
Inside addToPagesArea function, I used the FileAPI from HTML 5 to preview the drag'n dropped files amongst other code.
function addToPages(file, front) {
// there was some code before..
reader = new FileReader();
reader.onload = (function (theDiv) {
return function (evt) {
var backgroundimage = "url(" + evt.target.result + ")";
theDiv.css("background-image", backgroundimage);
var sizeSettings = getSizeSettingsFromPage(file, calculateRatio);
};
}(imageDiv));
// step#3 execute file reader
reader.readAsDataURL(file);
// there was some code after..
}
So every time I previewed the file, I also attempted to do some calculation on the dimensions of the file.
function getSizeSettingsFromPage(file, whenReady) {
reader = new FileReader();
reader.onload = function(evt) {
var image = new Image();
image.onload = function(evt) {
var width = this.width;
var height = this.height;
var filename = file.name;
if (whenReady) {
whenReady(width, height, filename);
}
};
image.src = evt.target.result;
};
reader.readAsDataURL(file);
}
function calculateRatio(width, height, filename) {
var ratio = width/height;
var object = new Object();
object['height'] = width;
object['width'] = height;
object['ratio'] = ratio;
object['size'] = 'Original';
for (var size in SIZES) {
var min = SIZES[size].ratio - 0.01;
var max = SIZES[size].ratio + 0.01;
if (ratio <= max && ratio >= min) {
object['size'] = size;
}
}
pageSizes.add(filename, object);
}
pageSizes as seen in calculateRatio is a global variable which is an array-like type of variable.
It is definitely empty BEFORE populatePagesArea gets called.
Here is the situation:
my code is:
populatePagesArea();
getMajorityPageSize(); // this acts on the supposedly non-empty pageSizes global variable
But because I think the calculateRatio has not been called on ALL the previewed images, the pageSizes is always empty when getMajorityPageSize is called.
how can i make sure that after populatePagesArea is called, getMajorityPageSize gets triggered ONLY after all the pages have undergone calculateRatio function?
I believe this is asynchronous callback. But I am not sure how to do it for an array of objects that will need to undergo an async callback function like calculateRatio.
The simple solution (I have marked my changes with // ***:
// ***
var totalPages;
function populatePagesArea() {
// there was some code before the for loop
// ***
totalPages = pages.length;
for (var i=0, l=pages.length; i<l; i++) {
addToPagesArea(pages[i], "");
}
// some code after...
}
function addToPages(file, front) {
// there was some code before..
reader = new FileReader();
reader.onload = (function (theDiv) {
return function (evt) {
var backgroundimage = "url(" + evt.target.result + ")";
theDiv.css("background-image", backgroundimage);
var sizeSettings = getSizeSettingsFromPage(file, calculateRatio);
// *** Check to see if we're done after every load
checkPagesReady();
};
}(imageDiv));
// step#3 execute file reader
reader.readAsDataURL(file);
// there was some code after..
}
// *** Call getMajorityPageSize() here, only after all pages have loaded.
function checkPagesReady() {
if (pageSizes.length >= totalPages)
getMajorityPageSize();
}
The better solution if you're going to be dealing with more asynchronous things later on would be to refactor your code using promises. Promises is an API designed for dealing with asynchronous programming in a systematic and organized way. It'll make your life a lot easier if you're going to be doing more async work. There's a lot of free libraries that support promises, one of the major players is Q.js.
Related
I have such code:
function processFiles(e) {
var filesInput = $('#files').prop('files');
var i, f;
for (i = 0, f = filesInput[i]; i != filesInput.length; ++i) {
var name = f.name;
console.log(name); //why here is the same file, even if i select in file input 2 different?
var reader = new FileReader();
reader.onload = function(e) {
var myFile = e.target.result;
console.log(myFile); //why here is the same file, even if i select in file input 2 different?
};
reader.readAsBinaryString(f);
}
}
$('#sbmt').click(function(e) {
processFiles();
});
but when i try to parse multiple files, i got the same file in for loop & .onload callback
what i do wrong?
why here is the same file, even if i select in file input 2 different?
Because nothing updates f in the for loop. You've set f in the initialization expression, but you don't update it in the update expression.
If you want to use the for to control f, be sure you update f:
for (i = 0, f = filesInput[i]; i != filesInput.length; ++i, f = filesInput[i]) {
// -------------------------------------------------------^^^^^^^^^^^^^^^^^^^
...but at that point, you're duplicating code; instead, I'd just move the sssignment into the loop body:
for (i = 0; i != filesInput.length; ++i) {
f = filesInput[i]
...or more likely I'd probably use forEach:
function processFiles(e) {
Array.from($('#files').prop('files')).forEach(function(f) {
var name = f.name;
var reader = new FileReader();
reader.onload = function(e) {
var myFile = e.target.result;
console.log(myFile);
};
reader.readAsBinaryString(f);
});
}
(Note that that uses Array.from from ES2015; you'll need a polyfill to use that in the wild for now...)
My 2 cents:
Took a crack at using Response (part of the new fetch api) to get the buffer with help of es6 and Promises
function processFiles(e) {
let files = $('#files')[0].files
let buffers = [...files].map(file => new Response(file).arraybuffer())
Promise.all(buffers).then(buffers => {
// concatinate(buffers)
})
}
I am struggling trying to re-work the idea/code found here (basic premise being working out a file type by simply looking at the first few bytes of data).
Ive got the bare bones of doing what i want - see JSFIDDLE.
Heres my code:
function readTheFiles(file){
var fileReader = new FileReader();
fileReader.onloadend = function(e) {
var fileHeader = new Uint8Array(e.target.result).subarray(0, 4);
var header = "";
for (var q = 0; q < fileHeader.length; q++) {
header += fileHeader[q].toString(16);
}
alert(header);
return header;
};
fileReader.readAsArrayBuffer(file);
}
$("#input-id").on('change', function(event) {
var files = event.target.files;
var i = 0;
for (var i = 0, file; file = files[i]; i++) {
var headString = readTheFiles(file);
alert(headString);
}
});
From what ive read (example 1, example 2) i am sure that the issue lies with calling a callback in the readTheFiles function - presumably the code is calling the alert(headString) line before the files have been loaded (hence why the alert within the readTheFiles function gives the expected result).
Im keen to understand the principal, rather than just get a solution, so any pointers/advice/assistance would be gratefully received.
Many Thanks
I am replying to your question about why your alert(headstring) call tells you "undefined". There may be better ways of discovering what type of file you are dealing with.
You are using an asynchronous process. I've modified and commented your code so that you can see what order things are happening in. You'll see that I have created a treat function in the same scope as your on("change", ...) function. Then I've passed this function as an argument to the readTheFiles() function, so that it can be called back later when the file has been read in.
function readTheFiles(file, callback, index){
var fileReader = new FileReader();
// 3. This function will be called when readAsArrayBuffer() has
// finished reading the file
fileReader.onloadend = function(e) {
var fileHeader = new Uint8Array(e.target.result).subarray(0, 4);
var header = "";
for (var q = 0; q < fileHeader.length; q++) {
header += fileHeader[q].toString(16);
}
callback(header, index);
};
// 2. This gets called almost as soon as fileReader has been created
fileReader.readAsArrayBuffer(file);
}
$("#input-id").on('change', function(event) {
var files = event.target.files;
var i = 0;
for (var i = 0, file; file = files[i]; i++) {
// 1. This is executed for each file that you selected, immediately
// after selection. The treat() function is sent as a callback.
// It will be called later, when the file has been read in.
// Passing `i` as an argument allows you to see which order the
// files are treated in.
var headString = readTheFiles(file, treat, i);
}
// 4. This is called by the callback(header) command inside the
// readTheFiles() function. The `treat` function was sent to
// readTheFiles as the `callback` argument. Putting brackets()
// after the function name executes it. Any value put inside
// the brackets is sent as an argument to this function.
function treat(header, index) {
alert("Header for file " + index + ":" + header);
}
});
I'm trying to use deferred/promise in a loop, but I get strange behavior. My code is as follows:
var images = [];
var numImages = Blobs.length;
var image = {};
console.log("numImages: " + numImages);
function doAsyncOriginal(i) {
var defer = $.Deferred();
image.original = Blobs[i].key;
image.resized = '';
image.thumbnail = '';
images.push(image);
console.log("i: " + i + " image: " + image.original);
console.log("images[" + i + "]: " + images[i].original);
defer.resolve(i);
return defer.promise();
}
$(function(){
var currentImage = doAsyncOriginal(0);
for(var i = 1; i < numImages; i++){
currentImage = currentImage.pipe(function(j) {
return doAsyncOriginal(j+1);
});
}
$.when(currentImage).done(function() {
console.log(JSON.stringify(images));
});
});
The Blob used in the code is an array of objects that I get from remote webservice, which contains properties about the images (it comes from filepicker.io's pickandstore method to be precise).
When I run this, I get the following in console:
numImages: 2
i: 0 image: pictures_originals/3QnQVZd0RryCr8H2Q0Iq_picture1.jpg
images[0]: pictures_originals/3QnQVZd0RryCr8H2Q0Iq_picture1.jpg
i: 1 image: pictures_originals/MD3KO6GjT8SNFYoPcG8J_picture2.jpg
images[1]: pictures_originals/MD3KO6GjT8SNFYoPcG8J_picture2.jpg
[
{
"original":"pictures_originals/MD3KO6GjT8SNFYoPcG8J_picture2.jpg",
"resized":"",
"thumbnail":""
},
{
"original":"pictures_originals/MD3KO6GjT8SNFYoPcG8J_picture2.jpg",
"resized":"",
"thumbnail":""
}
]
Although it shows images[0] and images[1] correctly, when printing separately, the object array shows only twice images[1]!!!
Am I doing something wrong???
Thanks in advance for your time.
UPDATE: I corrected the code based on comment of #TrueBlueAussie
You are reusing the same image object in every call to doAsyncOriginal(), so every element of your images array is pointing to the same object.
You need to create the object inside your function:
var image = {}; // <-- delete this
function doAsyncOriginal(i) {
var image = {};
// ...
}
This problem is unrelated to promises/deferreds, and promises/deferreds really aren't serving any purpose in your code. You could just do this:
$(function(){
var images = Blobs.map(function (blob) {
return {
original: blob.key,
resized: '',
thumbnail: ''
};
});
console.log(JSON.stringify(images));
});
In doAsyncOriginal you resolve your deferred before returning it's promise or even before adding the done handler on it.
You should delay the defer.resolve(i) call, so the deferred will be resolved later and enter the done handler...
function doAsyncOriginal(i) {
var defer = $.Deferred();
// ...
// Function.bind equivalent to jQuery.proxy
window.setTimeOut(defer.resolve.bind(defer, i), 0);
return defer.promise();
}
I am working on a HTML5 game and I have a function that should load an image and append it to an array for later displaying to a canvas. The code for the loading function is here...
function loadImage(url) {
box.images.total++;
image = new Image();
image.src = url;
image.onload = function () {
box.images.loaded++;
};
box.images[box.images.length] = image;
}
The code for the holding array is here...
var box = new Object({});
box.images = new Array([]);
And the rendering code is here...
loadImage("images/background.png");
while (box.images.loaded<box.images.total) {console.log("lol");}
box.ctx.drawImage(box.images[0], 0, 0);
My hope was that this would attempt to load the set of images and would increase the counter each time. Then when an image loads it would increase the loaded counter and then once all of the code has run the rest of the code would run. But I get an error saying "function was found that matched the signature provided" and the array appears to contain an empty element.
Also i'm in Chrome on Xubuntu 12.04
Update It turns out that the image was being put in index 1 not 0 and that was why the image wasn't loading. But it still doesn't render the image please help.
Another Update So it turns out that both total and loaded were NaN and so the while loop wasn't able to load at all. I set them to zero and the wile loop didn't terminate and it crashed my browser tab.
You need some sort of Promise or a callback function. For simplicity, I would suggest the callback. Example:
var box = {};
box.images = [];
box.images.total = box.images.loaded = 0;
function loadImage(url, callback) {
box.images.total++;
image = new Image();
image.onload = function () {
box.images.loaded++;
// We call something when the image has loaded
callback();
};
image.src = url;
box.images[box.images.length] = image;
}
function loadAllImages(urls, callback) {
// Loop through each image and load url
for (var i = 0; i < urls.length; ++i) {
var url = urls[i];
loadImage(url, function() {
// When everything loads, call callback
console.log(box.images.total, box.images.loaded);
if (box.images.loaded === box.images.total) callback();
});
}
}
window.onload = function() {
box.ctx = document.querySelector("canvas").getContext("2d");
loadAllImages([
"https://lh4.googleusercontent.com/-rNTjTbBztCY/AAAAAAAAAAI/AAAAAAAAAFM/RNwjnkgQ9eo/photo.jpg?sz=128",
"https://www.gravatar.com/avatar/20e068dd2ad8db5e1403ce6d8ac2ef99?s=128&d=identicon&r=PG&f=1"
], function() {
// Do whatever once everything has loaded
box.ctx.drawImage(box.images[0], 0, 0);
});
};
<canvas>
</canvas>
I'm facing a problem with canvas. No errors is returned. I'm trying to make a loader, that load every ressources like pictures, sounds, videos, etc, before the start of application. The loader must draw the number of ressourses loaded dynamically.
But at this moment, my loader has the result to freeze the browser until it draws the total of ressources loaded.
Tell me if i'm not clear :)
This is the code :
function SimpleLoader(){
var ressources ;
var canvas;
var ctx;
this.getRessources = function(){
return ressources;
};
this.setAllRessources = function(newRessources){
ressources = newRessources;
};
this.getCanvas = function(){
return canvas;
};
this.setCanvas = function(newCanvas){
canvas = newCanvas;
};
this.getCtx = function(){
return ctx;
};
this.setCtx = function(newCtx){
ctx = newCtx;
};
};
SimpleLoader.prototype.init = function (ressources, canvas, ctx){
this.setAllRessources(ressources);
this.setCanvas(canvas);
this.setCtx(ctx);
};
SimpleLoader.prototype.draw = function (){
var that = this;
this.getCtx().clearRect(0, 0, this.getCanvas().width, this.getCanvas().height);
this.getCtx().fillStyle = "black";
this.getCtx().fillRect(0,0,this.getCanvas().width, this.getCanvas().height)
for(var i = 0; i < this.getRessources().length; i++){
var data = this.getRessources()[i];
if(data instanceof Picture){
var drawLoader = function(nbLoad){
that.getCtx().clearRect(0, 0, that.getCanvas().width, that.getCanvas().height);
that.getCtx().fillStyle = "black";
that.getCtx().fillRect(0,0, that.getCanvas().width, that.getCanvas().height);
that.getCtx().fillStyle = "white";
that.getCtx().fillText("Chargement en cours ... " + Number(nbLoad) +"/"+ Number(100), that.getCanvas().width/2, 100 );
}
data.img = new Image();
data.img.src = data.src;
data.img.onload = drawLoader(Number(i)+1); //Update loader to reflect picture loading progress
} else if(data instanceof Animation){
/* Load animation */
} else if(data instanceof Video){
/* Load video */
} else if(data instanceof Sound){
/* Load sound */
}else {
}
}
};
So with this code, all resources are loaded, but i want to display the progress of loading. Some idea of what i missed?
You are "busy-looping" in the loader so the browser doesn't get a chance to redraw/update canvas.
You can implement a setTimeout(getNext, 0) or put the draw function outside polling current status in a requestAnimationFrame loop instead. I would recommend the former in this case.
In pseudo code, this is one way to get it working:
//Global:
currentItem = 0
total = numberOfItems
//The loop:
function getNextItem() {
getItem(currentItem++);
drawProgressToCanvas();
if (currentItem < total)
setTimeout(getNextItem(), 0);
else
isReady();
}
getNextItem(); //start the loader
Adopt as needed.
The setTimeout with a value of 0 will cue up a call next time there is time available (ie. after a redraw, empty event stack etc.). isReady() here is just one way to get to the next step when everything is loaded. (If you notice any problems using 0, try to use for example 16 instead.)
Using requestAnimationFrame is a more low-level and efficient way of doing it. Not all browsers support it at the moment, but there are poly-fills that will get you around that - For this kind of usage it is not so important, but just so you are aware of this option as well (in case you didn't already).