Question

Alright, I thought I had this whole setTimeout thing perfect but I seem to be horribly mistaken.

I'm using excanvas and javascript to draw a map of my home state, however the drawing procedure chokes the browser. Right now I'm forced to pander to IE6 because I'm in a big organisation, which is probably a large part of the slowness.

So what I thought I'd do is build a procedure called distributedDrawPolys (I'm probably using the wrong word there, so don't focus on the word distributed) which basically pops the polygons off of a global array in order to draw 50 of them at a time.

This is the method that pushes the polygons on to the global array and runs the setTimeout:

 for (var x = 0; x < polygon.length; x++) {
      coordsObject.push(polygon[x]);
      fifty++;
      if (fifty > 49) {
           timeOutID = setTimeout(distributedDrawPolys, 5000);
           fifty = 0;
      }
 }

I put an alert at the end of that method, it runs in practically a second.

The distributed method looks like:

 function distributedDrawPolys()
 {
      if (coordsObject.length > 0) {
           for (x = 0; x < 50; x++) { //Only do 50 polygons
                var polygon = coordsObject.pop();
                var coordinate = polygon.selectNodes("Coordinates/point");
                var zip = polygon.selectNodes("ZipCode");
                var rating = polygon.selectNodes("Score");
                if (zip[0].text.indexOf("HH") == -1) {
                     var lastOriginCoord = [];
                     for (var y = 0; y < coordinate.length; y++) {
                          var point = coordinate[y];
                          latitude = shiftLat(point.getAttribute("lat"));
                          longitude = shiftLong(point.getAttribute("long"));
                          if (y == 0) {
                               lastOriginCoord[0] = point.getAttribute("long");
                               lastOriginCoord[1] = point.getAttribute("lat");
                          }
                          if (y == 1) {
                               beginPoly(longitude, latitude);
                          }
                          if (y > 0) {
                               if (translateLongToX(longitude) > 0 && translateLongToX(longitude) < 800 && translateLatToY(latitude) > 0 && translateLatToY(latitude) < 600) {
                                    drawPolyPoint(longitude, latitude);
                               }
                          }
                     }
                     y = 0;
                     if (zip[0].text != targetZipCode) {
                          if (rating[0] != null) {
                               if (rating[0].text == "Excellent") {
                                    endPoly("rgb(0,153,0)");
                               }
                               else if (rating[0].text == "Good") {
                                    endPoly("rgb(153,204,102)");
                               }
                               else if (rating[0].text == "Average") {
                                    endPoly("rgb(255,255,153)");
                               }
                          }
                          else { endPoly("rgb(255,255,255)"); }
                     }
                     else {
                     endPoly("rgb(255,0,0)");
                     }
                }
           }
      }
 }

Edit: fixed the format

So I thought the setTimeout method would allow the site to draw the polygons in groups so the users would be able to interact with the page while it was still drawing. What am I doing wrong here?

Was it helpful?

Solution

Change the code to

for (var x = 0; x < polygon.length; x++) {
    coordsObject.push(polygon[x]);
}
distributedDrawPolys();

function distributedDrawPolys()
{
    if (coordsObject.length > 0) {
        for (x = 0; x < 50; x++) {
            ...
        }
        setTimeout("distributedDrawPolys()", 5000); //render next 50 polys in 5 sec
    }
}

OTHER TIPS

If your loop is running in under a second, all of your setTimeout calls will stack up trying to fire off about five seconds later.

If you want to give the browser breathing room for intermediate rendering, push all of your objects on the stack, then call the function with a limit, and have the function schedule itself when it's done that many objects. Semi-pseudocode:

var theArray = [];
var limit = 50;

function showStuff() {
    for (...) {
        // push objects on theArray
    }

    renderArrayInBatches();
}

function renderArrayInBatches() {
    var counter;

    for (counter = limit; counter > 0; --counter) {
        // pop an object and render it
    }
    if (theArray.length > 0) {
        setTimeout(renderArrayInBatches, 10);
    }
}

That builds the array all in one go, then triggers the first batch (up to limit) of rendering. At the end of the first batch, if there's more rendering to do, it schedules it to happen about 10ms later. In fact, it will happen no sooner than 10ms later and quite possibly later than that, if the browser is still busy with other things. (Re 10ms: most browsers won't schedule something for sooner than 10ms from now.) (Edit Andy E points out, quite correctly, that you may as well fold the logic related to what needs to be rendered into the rendering function directly rather than first building the array, then processing it. Doesn't change the above much except for the array part, how you do the chaining and the "breathing room" remains the same.)

Not knowing the excanvas stuff you're using, you may find you need to adjust the timeout time, but I tend to doubt it -- it's basically a "yield" operation which lets the browser do some things and come back to you.

Note that the psuedo-code sample above is using what appear to be globals. I wouldn't recommend actually using globals. You might even want to do this instead:

function showStuff() {
    var theArray = [];
    var limit = 50;

    for (...) {
        // push objects on theArray
    }

    renderArrayInBatches();

    function renderArrayInBatches() {
        var counter;

        for (counter = limit; counter > 0; --counter) {
            // pop an object and render it
        }
        if (theArray.length > 0) {
            setTimeout(renderArrayInBatches, 10);
        }
    }
}

...but I didn't like to complicate the main answer by introducing the closure (although technically both codeblocks involve closures).

No, you want something different.

var batchsize = 50; 
function drawPolys(start) {
    for (var x = start; x < polygon.length; x++) {
        drawOnePolygon(polygon[x]);
        if (start + batchsize <= x) {
            // quit this invocation, and schedule the next
            setTimeout(function(){drawPolys(x+1);}, 400);
            return;  // important
        }
    }
}

then drawOnePolygon must be something like this:

function drawOnePolygon(p) {
    var coordinate = polygon.selectNodes("Coordinates/point");
    //...and so on, continuing from your code above.
}

kick it off with:

drawPolys(0); 

If you call it once every five seconds, and it does 1 second of work each time, the browser will be choked for interaction 20% of the time.

You could try and chop up your big function and run it in chunks to make the experience smoother.

This doesn't work as expected. By the time your first function starts executing, your global array already contains 50 elements. You just end up operating on the same data 50 times.

What you can do is to chain your setTimeout so that the next function executes after the previous method.

Here's a simple implementation:

var coordObj = [...]; //50 or whatever elements
(function() {
    if (coordObj.length === 0) return; //Guardian
    var obj = coordObj.pop(); //or .shift(), based on the order desired.
    doStuffWithCoordObj(obj);
    setTimeout(arguments.callee,0); //call this anonymous function after a timeout
})();
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top