function ImageRotator(delay_ms, idImg, idDiv, aImageUrls) {

	// period, in ms, at which the images are cycled
	//
	this.delay_ms = delay_ms;

	// this is the image which will be cycled
	//
	this.oImg = $(idImg);

	// this is a dev used for fading between the images.  it must
	// be the parent of oImg, be the same size as oImg, and
	// contain nothing but the oImg <img> element.
	//
	this.oDiv = $(idDiv);

	// this is the handle to our timer cookie, which can be used
	// to stop the animation
	//
	this.timer_id = null;

	// the image which will be cycled to next.  -1 means that
	// no replacement image has yet been loaded.
	//
	// this is an index into the this.aImages array.
	//
	this.next_image_idx = -1;

	// delay, in ms, between updates to the fade effect
	//
	// decreasing this value will make the fade occur faster
	// and take more CPU
	//
	this.fade_step_ms = 50;

	// the % opacity to reveal at each step in the fade.
	//
	// decreasing this will make the fade occur slower,
	// but it will appear more smoothly
	//
	this.fade_step_pct = 5;

	// number of times to change the image before stopping
	// set to 0 for unlimited cycles.
	//
	this.max_cycles = 50;

	// count of images that we've cycled through so far
	//
	this.num_cycles = 0;

	// preload the images
	//
	if (this.oImg && this.oDiv) {

		this.aImages = new Array(aImageUrls ? aImageUrls.length : 0);

		for (var i = 0; i < aImageUrls.length; ++i) {
			this.aImages[i] = new Image();
			this.aImages[i].src = aImageUrls[i];
		}
	}
}

ImageRotator.prototype.start = function() {

	if ( !this.isRunning() ) {

		// only start if all our variables are kosher
		//
		if (this.oImg && this.oDiv && this.aImages && this.aImages.length) {

			this.scheduleNextImage();
		}
	}
}

ImageRotator.prototype.stop = function() {

	if ( this.isRunning() ) {

		clearTimeout(this.timer_id);
		this.timer_id = null;
	}
}

ImageRotator.prototype.isRunning = function() {

	return (this.timer_id != null);
}


/*** protected ***/

ImageRotator.prototype.onTimerNextImage = function() {

//	alert(this.isRunning());
	this.next_image_idx = this.findNextImgIndex();

	if (-1 == this.next_image_idx) {

		// none of the images we could cycle to are loaded
		// yet.  wait for them to show up.
		//
		this.scheduleNextImage();

 	} else {

		// an image has been loaded for us to switch to
		//
		// change the background image of oDiv to this image.
		//
		// we will then increase the transparency of oImg, displaying the image in oDiv.
		//
		// when we've made oImg fully transparent, we will replace it with the new
		// image, restore its opacity to 100%, and wait to cycle to the next image.
		//
		this.oDiv.style.backgroundImage = 'url(' + this.aImages[this.next_image_idx].src + ')';
		this.scheduleFadeOut(100);
	}
}

// returns the index of the next image that is actually loaded,
// or -1 for none.
//
ImageRotator.prototype.findNextImgIndex = function() {

	var currentImage = this.next_image_idx;

	for (var i = 0; i < this.aImages.length; ++i) {
		++currentImage;
		if (currentImage >= this.aImages.length) {
			currentImage = 0;
		}

		var img = this.aImages[currentImage];

		// is it loaded?
		//
		if (img.complete && (img.width > 0)) {

			// we found a next image to show
			//
			return currentImage;
		}
	}

	// we didn't find any loaded images
	//
	return -1;
}
	
ImageRotator.prototype.scheduleNextImage = function() {

	this.timer_id = setTimeout(this.onTimerNextImage.bind(this), this.delay_ms);
}

ImageRotator.prototype.scheduleFadeOut = function(opacity) {

	this.timer_id = setTimeout(this.onFadeOut.bind(this, opacity), this.fade_step_ms);
}

ImageRotator.prototype.onFadeOut = function (opacity) {

//	alert('fading out with opacity=' + opacity);
	
	if (opacity < 0) {

		// we've fully displayed the image in the div.  Take the image we just revealed,
		// put it in oImg, and turn the display of oImg back on.
		//
		// Then, schedule our next image rotation.
		//

		// replace the <img> with the new one
		//
		this.oImg.src = this.aImages[this.next_image_idx].src;

		// when the <img> is replaced, restore its opacity to 100%
		// delay this slightly so that the browser has a chance to
		// replace the image first.
		//
		// without this delay, the old image has a small chance to
		// "flash" when the opacity is restored before the image
		// is replaced.
		//
		this.delayedRestoreOpacity();

		// we've displayed a new image, so add it to our count
		//
		++this.num_cycles;

		// have we maxed out on the number of images to show?
		//
		if (!this.max_cycles || (this.num_cycles < this.max_cycles)) {

			// schedule the next image replacement
			//
			this.scheduleNextImage();
		}

	} else {
		// change the opacity of the image, and schedule the next
		// step in the fade
		//
		this.setOpacity(this.oImg, opacity);

		var nextOpacity = opacity - this.fade_step_pct;
		this.scheduleFadeOut(nextOpacity);
	}
}

ImageRotator.prototype.delayedRestoreOpacity = function () {
	setTimeout(this.setOpacity.bind(this, this.oImg, 100), this.fade_step_ms);
}

// change the opacity for different browsers
// stolen from http://www.brainerror.net/scripts_js_blendtrans.php
//
ImageRotator.prototype.setOpacity = function(object, opacity) {

	if (opacity < 0) {
		opacity = 0;

	} else if (opacity > 100) {
		opacity = 100;
	}

    var oStyle = object.style;
    oStyle.opacity = (opacity / 100);
    oStyle.MozOpacity = (opacity / 100);
    oStyle.KhtmlOpacity = (opacity / 100);
    oStyle.filter = "alpha(opacity=" + opacity + ")";
}
