// Copyright Januar 2005, Walter Bislin, walter.bislins.ch // // 12.10.2014 // minor changes in code, no semantic changes // 29.04.2014: // upd: global constants moved to object -> IC.CState // err: MaxNLoading, new default = 2 (old = 1) for better performance on preload on fast connections // new: GetStatus(), ResetStatus() // upd: catch exceptions in CallLoadedFunc() // upd: callbacks are called even on error and abort events (check IC.Image(id).CacheState) // class CImgCache -------------------------------------------------------------------------------------- function CImgCache() { // public this.CheckLoadInterval = 100; // ms this.MaxNLoading = 2; this.LoadDelay = 0; // for debugging this.EnableStatusDisplay = true; // public read only this.NImages = 0; this.NLoading = 0; this.NUnloaded = 0; this.NError = 0; this.NAbort = 0; this.NLoaded = 0; this.Images = []; // constants this.CState = { LoadPending: 0, Loading: 1, Loaded: 2, Error: 3, Abort: 4 }; // private this.ErrorMsg = ''; this.OnAllLoaded = new xCallbackChain(); this.OnImgLoaded = new xCallbackChain(); this.OnLoadCalling = false; this.LoadNextCalling = false; this.PrioList = []; this.Timer = null; var me = this; this.OnCheckLoaded = function() { me.CheckLoaded(); }; } CImgCache.prototype.AddOnAllLoaded = function(aFunc) { this.OnAllLoaded.Add(aFunc); }; CImgCache.prototype.AddOnImgLoaded = function(aFunc) { this.OnImgLoaded.Add(aFunc); }; CImgCache.prototype.IsValid = function(aImageID) { return ((aImageID >= 0) && (aImageID < this.NImages)); }; CImgCache.prototype.PreloadImages = function( aUrls, aRoot ) // aUrls: array of string; aRoot: string { aRoot = xDefStr( aRoot, '' ); var len = aUrls.length; for (var i = 0; i < len; i++) { this.PreloadImage( aRoot+aUrls[i] ); } }; CImgCache.prototype.PreloadImage = function( aUrl, aOnLoadFunc, bPriority ) // load an image with higher priority if bPriority = true // returns an ImageID. { bPriority = xDefBool( bPriority, false ); var id = this.FindImage(aUrl); if (id >= 0) { var img = this.Images[id]; aOnLoadFunc = xDefFuncOrNull( aOnLoadFunc, img.OnLoadFunc ); if (img.CacheState == this.CState.Error || img.CacheState == this.CState.Abort) { this.ReloadImage( id, aOnLoadFunc ); } else { img.OnLoadFunc = aOnLoadFunc; } } else { aOnLoadFunc = xDefFuncOrNull( aOnLoadFunc, null ); id = this.AddImage( aUrl, aOnLoadFunc ); } if (bPriority) { if (this.Images[id].CacheState == this.CState.LoadPending && !this.InPrioList(id)) { this.PrioList[this.PrioList.length] = id; } } this.LoadNext(); return id; }; CImgCache.prototype.LoadImage = function( aUrl, aOnLoadFunc ) // load an image with higher priority // returns an ImageID. { return this.PreloadImage( aUrl, aOnLoadFunc, true ); }; CImgCache.prototype.ReloadImage = function( aImgID, aOnLoadFunc ) { var img = this.Images[aImgID]; img.OnLoadFunc = xDefFuncOrNull( aOnLoadFunc, null ); if (img.CacheState != this.CState.LoadPending) { img.CacheState = this.CState.LoadPending; this.NUnloaded++; this.DisplayStatus(); } }; CImgCache.prototype.FindImage = function( aUrl ) { var images = this.Images; var len = this.NImages; for (var i = 0; i < len; i++) { if (images[i].CacheUrl == aUrl) return i; } return -1; }; CImgCache.prototype.Image = function(aImageID) { // require IsValid(aImageID) return this.Images[aImageID]; }; CImgCache.prototype.ImageByUrl = function(aUrl) { var imgID = this.FindImage(aUrl); return (imgID >= 0) ? this.Image(imgID) : null; }; CImgCache.prototype.GetNUnloaded = function() { this.CheckLoaded(); return this.NUnloaded; }; CImgCache.prototype.IsLoaded = function(aImageID) { return (this.IsValid(aImageID) && (this.Images[aImageID].CacheState == this.CState.Loaded)); }; CImgCache.prototype.IsLoadedByUrl = function(aUrl) { // Sucht das Bild aUrl im Cache und testet, ob es ganz geladen ist. return this.IsLoaded(this.FindImage(aUrl)); }; CImgCache.prototype.ImageState = function(aImageID) { // require IsValid(aImageID) return (this.Images[aImageID].CacheState); }; CImgCache.prototype.ImageStateByUrl = function(aUrl) { var imgID = this.FindImage(aUrl); return (imgID >= 0) ? this.ImageState(imgID) : -1; }; CImgCache.prototype.ImageUrl = function(aImageID) { // require IsValid(aImageID) return this.Image(aImageID).CacheUrl; }; CImgCache.prototype.GetStatus = function() { var s = ''; if (this.NUnloaded > 0 || this.NError > 0 || this.NAbort > 0) { s += 'Bilder zu laden: noch ' + this.NUnloaded + ' von ' + this.NImages + '. '; if (this.NError > 0 || this.NAbort > 0) { s += '(Geladen: ' + this.NLoaded + '; '; s += 'Fehler: ' + this.NError + '; '; s += 'Abbruch: ' + this.NAbort + ')'; } // s += this.ErrorMsg; } return s; }; CImgCache.prototype.ResetStatus = function() { this.ErrorMsg = ''; this.NError = 0; this.NAbort = 0; }; // private: CImgCache.prototype.DisplayStatus = function() { if (this.EnableStatusDisplay) { window.status = this.GetStatus(); } }; CImgCache.prototype.AddImage = function( aUrl, aOnLoadFunc ) { var id = this.NImages; var img = new Image(); img.CacheUrl = aUrl; img.CacheState = this.CState.LoadPending; img.OnLoadFunc = aOnLoadFunc; img.WasLoaded = false; img.WasError = false; img.WasAbort = false; img.onload = function() { this.WasLoaded = true; }; img.onerror = function() { this.WasError = true; }; img.onabort = function() { this.WasAbrort = true; }; this.Images[id] = img; this.NUnloaded++; this.NImages++; this.DisplayStatus(); return id; }; CImgCache.prototype.InPrioList = function( aImageID ) { var pl = this.PrioList; var len = pl.length; for (var i = 0; i < len; i++) { if (pl[i] == aImageID) return true; } return false; }; CImgCache.prototype.LoadNext = function() { if (this.NUnloaded == 0 || this.LoadNextCalling) return; this.LoadNextCalling = true; while ((this.NUnloaded > 0) && (this.PrioList.length > 0) && (this.NLoading < this.MaxNLoading)) { var id = this.PrioList.shift(); this.StartLoading(id); } var found = true; while ((this.NUnloaded > 0) && found && (this.NLoading < this.MaxNLoading)) { var id = this.FindLoadPending(); if (id == -1) { found = false; } else { this.StartLoading(id); } } this.LoadNextCalling = false; }; CImgCache.prototype.FindLoadPending = function(){ var images = this.Images; var len = images.length; for (var id = 0; id < len; id++) { if (images[id].CacheState == this.CState.LoadPending) return id; } return -1; }; CImgCache.prototype.StartLoading = function( aImageID ) { // require IsValid(aImageID) if (this.Timer) { clearTimeout( this.Timer ); this.Timer = null; } var img = this.Images[aImageID]; if (img.CacheState == this.CState.LoadPending || img.CacheState == this.CState.Abort) { this.NLoading++; this.DisplayStatus(); img.CacheState = this.CState.Loading; if (this.LoadDelay > 0) { setTimeout( function(){ img.src = img.CacheUrl; }, this.LoadDelay ); } else { img.src = img.CacheUrl; } } // only start timer if not loading completed in statement obove!!! if (this.NLoading > 0 && this.Timer == null) { this.Timer = setTimeout( this.OnCheckLoaded, this.CheckLoadInterval ); } }; CImgCache.prototype.CheckLoaded = function() { if (this.Timer) { clearTimeout( this.Timer ); this.Timer = null; } for (var id = 0; id < this.NImages; id++) { var img = this.Images[id]; if (img.CacheState == this.CState.Loading) { if (img.complete || img.WasLoaded) { this.OnLoad( id ); // may call LoadNext and set the timer!!! } if (img.WasError) { this.OnError( id ); } if (img.WasAbort) { this.OnAbort( id ); } } } if (this.NLoading > 0 && this.Timer == null) { this.Timer = setTimeout( this.OnCheckLoaded, this.CheckLoadInterval ); } }; CImgCache.prototype.OnImage = function( aImageID ) { this.NLoading--; this.NUnloaded--; this.DisplayStatus(); this.CallLoadedFunc( aImageID ); this.OnImgLoaded.Call( aImageID ); if (this.NUnloaded == 0) { this.OnAllLoaded.Call(); } else { this.LoadNext(); } }; CImgCache.prototype.OnLoad = function( aImageID ) { if (this.Images[aImageID].CacheState != this.CState.Loading) return; this.NLoaded++; this.DisplayStatus(); this.Images[aImageID].CacheState = this.CState.Loaded; this.OnImage( aImageID ); }; CImgCache.prototype.OnError = function( aImageID ) { if (this.Images[aImageID].CacheState == this.CState.Loaded) { // some browsers (safari) are calling OnLoad and OnError! -> reset State to Loading this.Images[aImageID].CacheState = this.CState.Loading; this.NLoaded--; this.NLoading++; this.NUnloaded++; } if (this.Images[aImageID].CacheState != this.CState.Loading) return; this.NError++; this.ErrorMsg += ' Error loading ' + this.Images[aImageID].src; this.DisplayStatus(); this.Images[aImageID].CacheState = this.CState.Error; this.OnImage( aImageID ); }; CImgCache.prototype.OnAbort = function( aImageID ) { if (this.Images[aImageID].CacheState == this.CState.Loaded) { // some browsers (safari) are calling OnLoad and OnAbort! -> reset State to Loading this.Images[aImageID].CacheState = this.CState.Loading; this.NLoaded--; this.NLoading++; this.NUnloaded++; } if (this.Images[aImageID].CacheState != this.CState.Loading) return; this.NAbort++; this.DisplayStatus(); this.Images[aImageID].CacheState = this.CState.Abort; this.OnImage( aImageID ); }; CImgCache.prototype.CallLoadedFunc = function( aImageID ) { // prevents recursion! var img = this.Images[aImageID]; if (this.OnLoadCalling || !img.OnLoadFunc) return; this.OnLoadCalling = true; try { img.OnLoadFunc( aImageID ); } catch(e) { } this.OnLoadCalling = false; }; var IC = new CImgCache();