
if(typeof strSorry == "undefined"){                                             //ER: make language-pack optional; was: if(!bLanguageDefined){
   strSorry = "I'm Sorry, your cart is full;  please proceed to checkout.";
   strAdded = "Added to your shopping cart:";
   strAddedQuantity = "Quantity: ";                                             //ER: new;  needed by, but not added by, Stefko mods
   strAddedProduct  = "Product:  ";                                             //ER: new;  needed by, but not added by, Stefko mods
   strRemove = "Click 'OK' to remove this product from your shopping cart.";
   strILabel = "Product ID &nbsp; &nbsp; ";
   strDLabel = "Product Name";
   strQLabel = "Qty";
   strPLabel = "Unit Price";
   strWLabel = "Weight";                                                        //ER: new - for displaying Product WEIGHT in ManageCart;  replaces strSLabel
   strZLabel = "Size";                                                          //ER: new - for displaying Product SIZE in ManageCart
   strALabel = "Amount";                                                        //ER: new - replaces Stefko's "Ext." for displaying Price*Qty in CheckoutCart
   strRLabel = "&nbsp;";                                                        //ER: was: "Remove from Cart"  (Netscape-4 needs NBSP rather than EMPTYSTRING)
   strRButton= "Remove";                                              //ER: was: "Remove"
   strMButton= "More Info";                                                     //ER: new;  for the DynamicWtSzColumns option
   strLButton= "Less Info";                                                     //ER: new;  for the DynamicWtSzColumns option
   strSUB = "SUBTOTAL";
   strWTSZTOT = "PACKAGE ATTRIBUTES";                                           //ER: new;  Stefko had strWTOT="TOTAL WEIGHT";  but now it's for Weight+Size
   strSHIP = "POSTAGE";
   strTAX = "TAX";
   strTOT = "TOTAL";
   strErrQty = "Invalid Quantity.";
   strNewQty = "Please enter new quantity:";
   strSHIPPINGZONE = "DELIVERY OPTIONS";                                        //ER: new;  needed by, but not added by, Stefko mods
   strTAXABLEREGION = "TAXABLE<BR>REGION";                                      //ER: new;  needed by my rewrite of the sales-tax-by-region code
   strEA = "";                                                               //ER: new;  needed by the original NopDesign version
   strCartEmpty = "Your cart is empty";                                         //ER: new;  needed by the original NopDesign version
   strAsMultiple = "as multiple packages:";                                     //ER: new;  for message from ComputeShipping
   strAsSingle = "as-one:";                                                     //ER: new;  for message from ComputeShipping
   strBroken="our shipping-calculator is broken; please inform our webmaster";  //ER: new;  message from ComputeShipping
   strTotalNaN="Your browser's javascript appears to be broken; another browser may help; a reboot may help; if problem persists, please inform our webmaster";  //ER: new;  message from ValidateCart, when total is not a number
   strINCLUDEDINTOTAL = "Included in Total";                                    //ER: new;  for the DisplayTaxIncluded option
   Language = "en";
}

//Options for Programmers:
OutputItemId         = "ID_";
OutputItemQuantity   = "QUANTITY_";
OutputItemPrice      = "PRICE_";
OutputItemName       = "NAME_";         //2008-02-07: now includes what was formerly in AddtlInfo
OutputItemWeight     = "WEIGHT_";       //Renamed by Stefko: Shipping-->Weight
OutputItemLength     = "LENGTH_";       //ER: new
OutputItemWidth      = "WIDTH_";        //ER: new
OutputItemHeight     = "HEIGHT_";       //ER: new
OutputItemThumb      = "THUMB_";    //2008-02-07: yanked, see OutputItemName
OutputOrderZone      = "SHIPZONE";      //Added by Stefko
OutputOrderRegion    = "TAXREGION";     //ER: new
OutputOrderSubtotal  = "SUBTOTAL";
OutputOrderShipping  = "SHIPPING";
OutputOrderTax       = "TAX";
OutputOrderTotal     = "TOTAL";
AppendItemNumToOutput= true;            //MUST be true for at least PayPal & Google-Checkout;  ER: suspect no-one ever uses false??
  CartID             = "C";             //empty-string is fine for the first cart on one website                                                COND-DEMO1-NOCI
Debug                = 0;               //suppress DEBUG alerts
function DEBUG(str)   {if(Debug)   alert(str);}         //any nonzero value produces the DEBUG (sanity-test) alerts
function DEBUG1(str)  {if(Debug&1) alert(str);}         //the  1-bit controls DEBUG1  alerts
function DEBUG2(str)  {if(Debug&2) alert(str);}         //the  2-bit controls DEBUG2  alerts
function DEBUG4(str)  {if(Debug&4) alert(str);}         //the  4-bit controls DEBUG4  alerts
function DEBUG8(str)  {if(Debug&8) alert(str);}         //the  8-bit controls DEBUG8  alerts
function DEBUG16(str) {if(Debug&16)alert(str);}         //the 16-bit controls DEBUG16 alerts
if(window.location.href.substring(0,5)=="file:") Debug|=256;  else Debug=0;     //2008-02-20: smart (Opera-like) behaviour wrt Debug-alerts...

//Options for Everyone:
MoneySymbol          = "&pound;";           //use dollar-symbol
DisplayPopupOnAdd    = false;         //suppress "add to cart" popups  (DEFAULT true in NopDesign version)
DisplayPopupOnRemove = false;         //suppress "remove from cart" popups  (not suppressable in NopDesign version)
DisplayChangeQty     = true;         //suppress changeable quantity-field  (added by Stefko)
DisplayWtColumn      = false;         //don't show a Shipping-WEIGHT column in ManageCart
DisplaySzColumn      = false;         //don't show a Shipping-SIZE column in ManageCart
DynamicWtSzColumns   = 3;             //a clickable "More Info" button, to have both Shipping-WEIGHT+SIZE columns shown
WTUNITS              = "g";           //grams  (needed by but not added by Stefko mods; he used "lbs")
SZUNITS              = "cm";          //units for Item & Package SIZEs
WTROUND              = 1;             //want Package-Weight rounded-up to integer
SZROUND              = 10;            //Package-Length,Width,Height rounded to multiple of 0.1
MoneyPLACES          = 2;             //currency needs two decimal places as in dollars and cents
DisplayPkgAttrRow    = false;          //show the total size & weight
DisplayShippingRow   = true;          //show the shipping-cost-line
DisplayTaxRow        = false;          //show the tax-line
DisplayTaxIncluded   = false;         //do not show tax-included prices
TaxNames             = ["PST","GST"]; //names for the two sales-taxes in Canada
TaxRates             = 0; //a 7% tax for Region#0, 5% for Regions #0 and #2, and 13% for #1 (Manitoba PST, Canadian GST, HST)  
TaxesByRegion        = 0; //Region#0 gets Tax#0+Tax#1; Region#1 gets Tax#2, Region#2 gets Tax#1                                 
RegionTable          = ["Manitoba", "Canadian HST Province", "Other Canadian Province", "Other Country"];//for 3 taxes in 4 Regions
RegionFromZone       = [[0,1,2],[3],[3]];             //customer in Zone#0 can be in Region#0 or 1 or 2;  Zone1 or 2 implies Region3
RegionDefault        = 0;                             //default to Region#0;
RegionPrompt         = "";    //prompt for MB
DefaultDonation      = 0.01;          //default Donation-Amount for donor who provides an invalid amount                                      COND-DEMO1-DEMO3-DEMO4
MinimumDonation      = 0.01;          //minimum Donation-Amount                                                                               COND-DEMO1-DEMO2-DEMO3-DEMO4
MinimumDonationPrompt= "We're sorry but we're unable to accept a donation of less than 0.01"; //                                              COND-DEMO1-DEMO2-DEMO3-DEMO4
MinimumOrder         = 0.01;                                                                  //prevent empty-cart to checkout
MinimumOrderPrompt   = "Your cart is empty; please order something before checking out.";     //minimum-order prompt                          COND-DEMO1-NOCI-DEMO2-DEMO4
PrefDonation         = "nDO";                         //donations have IDs beginning with nDO
TaxesByID            = {n:[], p:[0], g:[1,2]};        //product-IDs n* get no tax, p* get Tax#0 only, g* get Tax#1 and #2 only                COND-DEMO1
SameCountry          = 2;                             //highest region in same country, used only if ShipTaxName being used                   COND-DEMO1
gcCurrency           = "GBP";                         //currency-code for Google-Checkout                                                     COND-DEMO1-NOCI-DEMO2
NotesOnItem          = true;          //want shipping+region-notes onto last item                                                             COND-DEMO1-NOCI-DEMO3-DEMO4

//Payment Processor Options:
PaymentProcessor     = "pp";          //payment-processor for ManageCart;  can be overridden in shoppingcart.htm                              COND-DEMO1
PaymentProcessor2    = "cgi";         //payment-processor for CheckoutCart;  can be overridden in shoppingcheckout.htm
AllInOne             = false;          //ignore cart-support with PayPal                                                                       COND-DEMO1-NOCI-DEMO3-DEMO4

//========================================================================
// Shipping-Info Table
// -------------------
// Several examples are provided, to illustrate how you can go about 
// creating a table that describes shipping from your location, by the 
// package-deliverer you've chosen.  
//
// RESTRICTION: in the present version, the Length & Width must be the
// same in all pkginfo-entries for one zone.
//
// NOTE:  "*" in last column (of PkgClass) indicates multiple of these 
// (or smaller) may cost less than shipping as one package, and the 
// shipping-calculator is to try it both ways;  this feature works best 
// when size matters and Packing-Rules are provided.
// (!!future enhancement: program to derive the "*" info itself.)
//========================================================================
//
  //----------------------------------------------------------------------                                                                      COND-DEMO1-NOCI
  //---EXAMPLE-1: for Canadian-retailer who ships by Canada-Post---                                                                             COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI
  // This first example is for a Canadian retailer using Canada-Post,                                                                           COND-DEMO1-NOCI
  // Weight & Size based shipping, with Weight in grams, sizes in centimetres;                                                                  COND-DEMO1-NOCI
  // (you'll need minor changes even if you are a Canadian retailer, if your                                                                    COND-DEMO1-NOCI
  // location is other than Winnipeg).                                                                                                          COND-DEMO1-NOCI
  // (Separate examples illustrate shipping via UPS & USPS for a retailer                                                                       COND-DEMO1-NOCI
  // in the USA, via TNT-Post for a European retailer, and via Royal-Mail                                                                       COND-DEMO1-NOCI
  // for a British retailer.)                                                                                                                   COND-DEMO1-NOCI
  //----------------------------------------------------------------------                                                                      COND-DEMO1-NOCI
  ShipTable = [];                                                                               //init array                                    COND-DEMO1-NOCI
ShipTable[0]= new ShipEntry("UK 1st Class Recorded", []);
ShipTable[1]= new ShipEntry("Within the EU", []);
ShipTable[2]= new ShipEntry("Rest of World", []);
nullsize= new Size(0,0,0);


ShipTable[0].pkginfo[0]=new PkgClass( 999999, nullsize, 4.95, 0, 100,  "");
ShipTable[1].pkginfo[0]=new PkgClass( 999999, nullsize, 8.00, 0, 100,  "");
ShipTable[2].pkginfo[0]=new PkgClass( 999999, nullsize, 15.00, 0, 100,  "");

ZoneDefault = 0;                                                              //make Zone#2 the Default                                       COND-DEMO1-NOCI
ZonePrompt  = "Please select postage option before continuing";                       //do not prompt for no-zone-selected, but simply use ZoneDefault
ShipTaxRate = 0.0;                     //using 0.06 to indicate that Canadian GST must be added to shipping-prices quoted by CanadaPost
ShipTaxName = ""; //show the tax included in shipping                                                             COND-DEMO1
HandlingChargePerOrder =        0.00;         //HandlingCharge for the first package in an order
HandlingChargePerExtraPackage = 0.25;         //HandlingCharge per additional package
  //                                                                                                                                            COND-DEMO1-NOCI
  // NOTES:                                                                                                                                     COND-DEMO1-NOCI
  // ------                                                                                                                                     COND-DEMO1-NOCI
  // 1. Canada-Post's size-limit for Oversized-Letter (OSL) is 38x27x2cm, however the closest (readily-obtainable) padded envelope,             COND-DEMO1-NOCI
  // 14.5x9.5 inches, offers usable inside dimensions of 33.6x23.4x1.8 cm,  so that's how the limit is expressed above.                         COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI
  // 2. Canada-Post's size-limit for Small-Packet (SP) is Length<=60cm AND Length+Width+Height<=90cm;                                           COND-DEMO1-NOCI
  // that limit is expressed above as 33.6x23.4x33.0 (which sums to 90cm) in order to simplify the package-size calculations;                   COND-DEMO1-NOCI
  // (for the items in the PackingRule-example below, could do very slightly better by expressing it as 32x23x35, and making all pkgs 32x23).   COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI
  // 3. For its OSL, LSP, SP(*) categories, Canada-Post prices are exactly 3-way (within-Canada, to-USA, to-International) as shown above;      COND-DEMO1-NOCI
  // however, for PARCELS (P) their rate varies by City/Province within Canada, by State for to-USA, and by Country for International;          COND-DEMO1-NOCI
  // the Parcel-rates shown above are: Winnipeg-to-Halifax used for within-Canada, to-Florida for USA, and to-Australia for International.      COND-DEMO1-NOCI
  // Parcel prices consist of base-rate plus fuel-surcharge; base-rates are revised once per year at most; but fuel-surcharges vary slightly    COND-DEMO1-NOCI
  // from week to week; the prices shown were checked on 2007-07-15.                                                                            COND-DEMO1-NOCI
  // Parcels are priced in 0.5kg ranges; i.e. weight gets rounded-up to multiple of 500g.                                                       COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI
  // 3-b. Revised for rates that begin 2008-01-14;  the LSP and SP rates were estimated as those have yet to be published;                      COND-DEMO1-NOCI
  // note: SP to-International will now be broken into seven zones, but the details remain unpublished, as of 2008-01-13;                       COND-DEMO1-NOCI
  // the parcel rates were increased by 5.1% but not checked further.                                                                           COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI
  // 4. Some EXAMPLES where SPLITTING-UP is beneficial (with 2007 Canada-Post rates) are:                                                       COND-DEMO1-NOCI
  // within-Canada, sending FIVE 500g OSL's costs less than one 2.5kg parcel (FIVE 500g OSL's even wins over a 2.0kg parcel);                   COND-DEMO1-NOCI
  // to-International, sending FIVE 2kg SP's costs less than one 10kg parcel;                                                                   COND-DEMO1-NOCI
  // the to-USA pricing is much closer to being sensible,  however sending 1kg SP + 500g OSL costs less than one 1.5kg parcel.                  COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI


//========================================================================
// Packing-Rule Info (OPTIONAL)
// ----------------------------
// The program will work without this Packing-Rule info, but will then
// be more prone to overcharging the customer for shipping.
// You may want to pre-compute packing rules like these, which will be
// feasible provided your list of item-sizes is fairly short.
// (And if its really short, you probably won't need them:-)
//
//========================================================================
PackTable = [];                 //--LEAVE THIS LINE even when omitting Packing-Rule Info
  //--OPTIONAL INFO - the rest of this section may be omitted--                                                                                 COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI
  //--Item sizes and Package sizes -- for NativeOrchid.Org:                                                                                     COND-DEMO1-NOCI
  itmca= new Size(32.0, 23.00, 0.3);    //calendar  size:32x23x0.3cm             weight:101g  (07oct18:new, 07nov15:95g->101g)                  COND-DEMO1-NOCI
  itmbk= new Size(23.0, 15.00, 1.2);    //book 23x15x1.2cm (8.5x5.5x0.5 inches)  weight:333.333g                                                COND-DEMO1-NOCI
  itmdv= new Size(15.0, 13.00, 0.6);    //DVD in a jewel-case                    weight:50g                                                     COND-DEMO1-NOCI
  itmlg= new Size(11.5,  8.00, 1.8);    //large-pin                              weight:16g                                                     COND-DEMO1-NOCI
  itmsm= new Size( 8.0,  5.75, 1.8);    //small-pin;  N of these will be simplified to N/2 (rounded up) of the preceding                        COND-DEMO1-NOCI
  PKG1=  new Size(32.0, 23.00, 1.8);    //package-size that qualifies as a CanadaPost Oversized-Letter                                          COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI
  //--Packing-Rules to make PKG1's of at most 500g, that qualify for CanadaPost's Oversized-Letter (OSL) rate:                                  COND-DEMO1-NOCI
  packTo500g = [];                                                      //                                                                      COND-DEMO1-NOCI
  packTo500g[0]= new PackingRule([itmsm],  [2],                itmlg);  //2 itmsm equivalent to one itmlg                                       COND-DEMO1-NOCI
  packTo500g[1]= new PackingRule([itmbk,itmdv,itmlg], [1,3,1], PKG1);   //size permits [1,4,1] and [2,2,0] but weight prevents                  COND-DEMO1-NOCI
  packTo500g[2]= new PackingRule([itmbk,itmdv,itmlg], [1,1,4], PKG1);   //                                                                      COND-DEMO1-NOCI
  packTo500g[3]= new PackingRule([      itmdv,itmlg], [  6,2], PKG1);   //                                                                      COND-DEMO1-NOCI
  packTo500g[4]= new PackingRule([      itmdv,itmlg], [  3,5], PKG1);   //                                                                      COND-DEMO1-NOCI
  packTo500g[5]= new PackingRule([            itmlg], [    8], PKG1);   //                                                                      COND-DEMO1-NOCI
  packTo500g[6]= new PackingRule([itmbk,itmdv,itmca], [1,1,1], PKG1);   //weight prevents more...  (itmca rules added 07oct18)                  COND-DEMO1-NOCI
  packTo500g[7]= new PackingRule([      itmdv,itmca], [  4,2], PKG1);   //                                                                      COND-DEMO1-NOCI
  packTo500g[8]= new PackingRule([      itmdv,itmca], [  2,3], PKG1);   //07nov15: 2+4->2+3                                                     COND-DEMO1-NOCI
  packTo500g[8]= new PackingRule([      itmdv,itmca], [  1,4], PKG1);   //07nov15: 0+5->1+4                                                     COND-DEMO1-NOCI
  //                                                                                                                                            COND-DEMO1-NOCI
  PackTable[0] = packTo500g;    //Within-Canada                                                                                                 COND-DEMO1-NOCI
  PackTable[1] = packTo500g;    //To-USA                                                                                                        COND-DEMO1-NOCI
  PackTable[2] = packTo500g;    //To-International                                                                                              COND-DEMO1-NOCI
  //==!!Future Enhancement:  a script that reads a given set of HTML files, extracting items for sale, and computing the PackingRules.          COND-DEMO1-NOCI
  //NOTE: our calendars demonstrate a flaw in the "Shipping-Weight" approach:  one 95g calendar plus packaging weighs just over 100g;           COND-DEMO1-NOCI
  //  2 plus packaging just over 200g;  however 5 plus packaging is just UNDER 500g.  Without a modification for weight-of-packaging,           COND-DEMO1-NOCI
  //  the only answer is to use a shipping-weight of 101g and thus overcharge (on shipping) for an order of 5 calendars.                        COND-DEMO1-NOCI


//======================================================================||
//----------------------------------------------------------------------||
// YOU DO NOT NEED TO MAKE ANY MODIFICATIONS BELOW THIS LINE            ||
// If you wish to venture below this line and are new to javascript,    ||
// then I recommend:  http://www.crockford.com/javascript/survey.html;  ||
// (my code would be better had I read it first:-)                      ||
//----------------------------------------------------------------------||
//======================================================================||


//========================================================================
// Objects and Methods related to Package-Size;
// by Eugene Reimer 2007-07-01.
//
// Some notes to myself:
// -- don't really need constructors;  eg: ShipEntry(A,B) --> {zone:A, pkginfo:B}
// -- possibly avoid size-field & doubly-dotted-selectors??  the SizeXX functions can accept any object having L,W,H fields...
// -- could rewrite ShipTable & packBySz initializers using ARRAYOBJECT.push
//    also ARRAYOBJECT[ARRAYOBJECT.length]=xxx;  -->  ARRAYOBJECT.push(xxx);
// -- was tempted to use for(VAR in ARRAYOBJECT) iteration, but it's illegal/ill-advised for arrays (only for objects);  also doesn't do what's wanted...
// -- wondered whether it's possible to redefine/overload the == operator instead of my SizeEQ routine;  found some info in:
//    http://www.mozilla.org/js/language/js20-2000-07/libraries/operator-overloading.html  -- doesn't sound promising...
// -- oldest supported browsers:  Netscape-4.06 and IE4/5 conform to ECMA-262-Edition-1 (Javascript-1.3/JScript-3.0), which has push,unshift,split, but not regexprs!
//      note: slice, substr not in ECMA-262-Edition-1, yet both of those browsers had them??  (using only substring to be safe)
//      indexOf is nonstd, yet both browsers had it  --see http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:indexOf  if code needed
//      NOTE: getAttribute and/or the DOM being used (for new-style attributes) are not supported by Netscape-4.7--!!--
//
// Future enhancements:
// May use parts of the Bin-Packing algorithm by Martello, Pisinger, Vigo,
// as published in http://www.diku.dk/~pisinger/3dbpp.c
// (see also http://forums.devshed.com/software-design-43/3d-bin-packing-49217.html);
//========================================================================
var PkgQueue = null;                                                    //an array of Qszwt entries;  describes the items (later the packages) in the current order
var PkgAsOne = null;                                                    //of type Qszwt;  the resulting package size & weight
var sComputeShippingNote="";                                            //an additional note about shipping to be shown to the customer during View-Cart
var gVat=0;                                                             //amount of VAT-tax included in the shipping-cost
function PrefEQ(A,B) {return A.substring(0,B.length)==B;}               //compare string-prefix
function NumberZ(s) {var N=Number(s); if(isNaN(N))N=0; return(N);}      //like the javascript Number, except it returns zero instead of NaN
function Integer(s) {return Math.round(NumberZ(s));}
moneyEps= Math.pow(10, -MoneyPLACES);                                   //smallest nonzero monetary-amount
MoneyROUND_FRA= Math.pow(10, +MoneyPLACES);                             //replaces 100 in all money-rounding  when PLACES>0
MoneyROUND_NOF= Math.pow(10, -MoneyPLACES);                             //for reciprocal-based money-rounding when PLACES<=0
function CentsFRA(f) {return  Math.round(f*MoneyROUND_FRA) / MoneyROUND_FRA;}
function CentsNOF(f) {return  Math.round(f/MoneyROUND_NOF) * MoneyROUND_NOF;}
Cents = (MoneyPLACES>0 ?CentsFRA :CentsNOF);                            //money-rounding function, init according to which rounding-method is needed
function WtRndUP(x) {return Math.ceil( x*WTROUND)/WTROUND;}             //weight-rounding function that rounds-UP
function WtRnd(x)   {return Math.round(x*WTROUND)/WTROUND;}             //weight-rounding function
function SzRnd(x)   {return Math.round(x*SZROUND)/SZROUND;}             //dimension-rounding function
//function Element(E,S) {for(var e=S.length;e--;)if(E==S[e])return true; return false;}   //is-an-element-of for set as an array; may switch to bitstring...
//function Element(E,S) {return (S&(1<<E))!=0;}                                         //is-an-element-of for bitstring  (set with elements 0..31)
//
//--sanity-checking and initialization related to Options:
while(PackTable.length < ShipTable.length) PackTable.push([]);                                                                          //ensure an entry for each Zone
if(RegionFromZone.length && RegionFromZone.length < ShipTable.length) DEBUG("RegionFromZone must have as many entries as ShipTable");
X=[];for(Z=ShipTable.length;Z--;)X[Z]=false; for(Z=RegionFromZone.length;Z--;)X[Z]=(RegionFromZone[Z].length==1);  RegionFromZoneOvA=X; //RfromZ-Overrides boolean-array
X=0; Z=RegionFromZone.length; if(Z>0) {X=1; while(Z--)X&=RegionFromZoneOvA[Z];}  RegionFromZoneOverrides=X;                             //was an option, now derived
if(RegionFromZone.length) {X=[];for(R=0;R<RegionTable.length;++R)X.push(R); while(RegionFromZone.length < ShipTable.length)RegionFromZone.push(X);} //entry for each zone
if(TaxNames.length>=2) while(TaxNames.length < TaxRates.length) TaxNames.push("UnNamedTax");                                            //ensure an entry for each Tax
while(TaxesByRegion.length < RegionTable.length) TaxesByRegion.push([]);                                                                //ensure an entry for each Region
RegionsUsed=(RegionTable.length>=2);                                                                                                    //replaces TaxRateRegional!=0
X=[];for(R=RegionTable.length;R--;)X.push(0); for(RZ=RegionFromZone,Z=RZ.length;Z--;) {E=RZ[Z];for(K=E.length;K--;)++X[E[K]];}
Y=[];for(R=X.length;R--;)Y[R]=(X[R]==1); ZoneFromRegionOvA=Y;                                                                           //ZfromR-Overrides boolean-array
//------------------------------------------------------------------------
// CONSTRUCTOR:  ShipEntry (zone, pkginfo)
//------------------------------------------------------------------------
function ShipEntry (zone,pkginfo){
   this.zone=zone;
   this.pkginfo=pkginfo;
}
//------------------------------------------------------------------------
// CONSTRUCTOR:  PkgClass (weight, size, costfixed, costperwtunit, wtunit, flag)
//------------------------------------------------------------------------
function PkgClass (weight,size,costfixed,costperwtunit,wtunit,flag){
   this.weight=weight;
   this.size=size;
   this.costfixed=costfixed;
   this.costperwtunit=costperwtunit;
   this.wtunit=wtunit;
   this.flag=flag;
}
//------------------------------------------------------------------------
// CONSTRUCTOR:  PackingRule (itmsizeinfo, itmqtyinfo, pkgsize)
//------------------------------------------------------------------------
function PackingRule (itmsizeinfo,itmqtyinfo,pkgsize){
   this.itmsizeinfo=itmsizeinfo;
   this.itmqtyinfo=itmqtyinfo;
   this.pkgsize=pkgsize;
}
//------------------------------------------------------------------------
// CONSTRUCTOR:  Size (Length, width, height)
//------------------------------------------------------------------------
function Size (Length,width,height){
   this.Length=NumberZ(Length); //calling it this.length could be trouble since objects already have a length attribute?
   this.width =NumberZ(width );
   this.height=NumberZ(height);
}
//------------------------------------------------------------------------
// CONSTRUCTOR:  Qszwt (qty, size, weighteach)
//------------------------------------------------------------------------
function Qszwt (qty,size,weighteach){
   this.qty   =Integer(qty);
   this.size  =size;
   this.weight=NumberZ(weighteach) * qty;
   this.wt=[];  for(var w=0; w<qty; ++w) this.wt[w]= NumberZ(weighteach);
   //the wt array is needed to handle same-size items having different weights
}
//------------------------------------------------------------------------
// FUNCTION:  SizeStr (size) -- convert size to string
//------------------------------------------------------------------------
function SizeStr (size){
   return(size.Length + "x" + size.width + "x" + size.height);
}
//------------------------------------------------------------------------
// FUNCTION:  SizeVolume (size) -- returns the volume==Length*width*height;
//------------------------------------------------------------------------
function SizeVolume (size){
   return(size.Length * size.width * size.height);
}
//------------------------------------------------------------------------
// FUNCTION:  SizeEQ (size1, size2) -- compare two sizes for equality
//------------------------------------------------------------------------
function SizeEQ (size1,size2){
   return(size1.Length==size2.Length && size1.width==size2.width && size1.height==size2.height);
}
//------------------------------------------------------------------------
// FUNCTION:  InitPkgQueue() -- initialize the PkgQueue
//------------------------------------------------------------------------
function InitPkgQueue(){
   PkgQueue = [];       //init to an empty array
}
//------------------------------------------------------------------------
// FUNCTION:  AddPkgQueueEntry (qty, size, weighteach) -- revise the global PkgQueue, an array-of-Qszwt;
// if there is an entry for size in PkgQueue, then update its qty & weight;
// otherwise add an entry.
//------------------------------------------------------------------------
function AddPkgQueueEntry (qty,size,weighteach){
   for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PkgQueue[i].size, size)){
      PkgQueue[i].qty += Integer(qty);
      PkgQueue[i].weight += NumberZ(weighteach)*Integer(qty);  for(var w=0; w<qty; ++w) PkgQueue[i].wt.push(NumberZ(weighteach));
      return;
   }
   PkgQueue.push(new Qszwt(qty,size,weighteach));
}
//------------------------------------------------------------------------
// FUNCTION:  RemovePkgQueueEntry (i) -- remove the PkgQueue[i] entry
//------------------------------------------------------------------------
function RemovePkgQueueEntry (i){
   PkgQueue.splice(i, 1);
}
//------------------------------------------------------------------------
// FUNCTION:  ShowPkgQueue -- display the PkgQueue  (convert to string)
//------------------------------------------------------------------------
function ShowPkgQueue(){
   var str="";
   for(var i=0; i<PkgQueue.length; ++i){
      str+= "qty:"+PkgQueue[i].qty +"; sz:"+SizeStr(PkgQueue[i].size) +"; wt:"+Math.round(PkgQueue[i].weight) +" [";
      for(var w=0; w<PkgQueue[i].qty; ++w) str+= Math.round(PkgQueue[i].wt[w]) + " ";
      str+= "]\n";
   }
   return str;
}
//------------------------------------------------------------------------
// FUNCTION:  PickAndApplyPackingRule -- find & apply the best matching PackingRule,
// thereby simplifying the global PkgQueue;  returns false if no rule matches;
// Algorithm:
// if we have any size mentioned in only one PackingRule, then apply that rule;
// otherwise, pick the best matching rule (by volumetric efficiency**) and apply it
//------------------------------------------------------------------------
function PickAndApplyPackingRule(PackingRule){
   var SZ=null;  var P=null;  var bestGoodness=0;
   for(var i=0; i<PkgQueue.length; ++i){
      var sz=PkgQueue[i].size,  p=null,  ct=0;
      for(var r=0; r<PackingRule.length; ++r) for(var e=0; e<PackingRule[r].itmsizeinfo.length; ++e) if(SizeEQ(PackingRule[r].itmsizeinfo[e], sz)){
         p=r; ++ct; break;
      }
      if(ct==1) {SZ=sz; P=p; break;}                            //--have found P an ONLY rule for size SZ
   }
   if(SZ==null){
      var minRV=99999999; for(var r=0; r<PackingRule.length; ++r) {var RV= SizeVolume(PackingRule[r].pkgsize); if(RV<minRV) minRV=RV;}
      for(var r=0; r<PackingRule.length; ++r){
         var MV=0;
         for(var e=0; e<PackingRule[r].itmsizeinfo.length; ++e) for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PackingRule[r].itmsizeinfo[e], PkgQueue[i].size)){
            MV+= SizeVolume(PkgQueue[i].size) * Math.min(PkgQueue[i].qty, PackingRule[r].itmqtyinfo[e]);
         }
         var RV=SizeVolume(PackingRule[r].pkgsize),  relRV=RV/minRV;
         var VE=MV/RV;                                          //volumetric-efficiency (Matching-Volume over Result-Volume)
         var g=VE/relRV;                                        //(**)modified volumetric-efficiency, to favour rule with smaller result-size
         if(g>bestGoodness) {P=r; bestGoodness=g;}              //--have found a new BEST rule P
      }
   }
   if(P==null) return false;    //--indicate NO matching rule found
   if(SZ!=null)  sRule= "PackingRule[" + P + "] is ONLY rule for sz:" + SizeStr(SZ) + "\n";
   else          sRule= "PackingRule[" + P + "] is BEST g:" + Math.round(bestGoodness*1000)/1000 + "\n";
   sRules+=sRule;
   //now apply rule P; first reducing qty or removing matched PkgQueue-entries, then adding an entry for the resulting pkg-size;
   var wei=0;
   for(var e=0; e<PackingRule[P].itmsizeinfo.length; ++e) for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PackingRule[P].itmsizeinfo[e], PkgQueue[i].size)){
      var Q= Math.min(PkgQueue[i].qty, PackingRule[P].itmqtyinfo[e]);
      //wei+= PkgQueue[i].weight * Q;
      for(w=0; w<Q; ++w) {wei+= PkgQueue[i].wt[w]; PkgQueue[i].weight-= PkgQueue[i].wt[w];}
      PkgQueue[i].qty-= Q;
      if(PkgQueue[i].qty==0) RemovePkgQueueEntry(i);
      else                   PkgQueue[i].wt.splice(0, Q);
   }
   AddPkgQueueEntry(1, PackingRule[P].pkgsize, wei);
   return true;
}
sRule="";       //global var showing most recently applied PackingRule, for DEBUG purposes only
sRules="";      //global var showing which PackingRules were applied,   for DEBUG purposes only
//------------------------------------------------------------------------
// FUNCTION:  ComputePackageSize -- compute sum of item-sizes from global PkgQueue;
// such summing, not altogether straightforward, is known as the 3D Bin Packing Problem;
// RESULT:  global PkgQueue gets a simplified list of sub-packets;
//          global PkgAsOne describes the order as a single package;
// Algorithm:
// while any packingrule matches the item-sizes in PkgQueue do
//    pick and apply the best matching rule (see PickAndApplyPackingRule)
// done
//------------------------------------------------------------------------
function ComputePackageSize (ZoneParam){
   DEBUG2(ShowPkgQueue());
   var PR=PackTable[ZoneParam];
   if(PR.length>0){
      //---use Packing-Rules Method;
      // Condidered doing this twice with two sets of packing-rules:  BySz to get the smallest packages,  ByWt to get weight-limited packages;
      // (2nd would need to start with a copy of PkgQueue from before 1st; see System.arraycopy;  OR: better to construct new list in both steps)
      // but convinced myself that such complexity was not needed.  The ByWt rules will sometimes make the single-package bigger than optimal,
      // but only for an order of predominantly high-density items;  ergo, such a non-minimal package-size will never force a higher price;
      // and that likely holds for any user, any pkg-deliverer, or close enough...
      sRules="Zone:"+ShipTable[ZoneParam].zone+"\n";
      //==!!possibly add some sanity-checking on the PackingRules??
      while((PkgQueue.length>1 || (PkgQueue.length==1 && PkgQueue[0].qty>1)) && PickAndApplyPackingRule(PR)) DEBUG2(sRule+ShowPkgQueue());      //was: {}
      DEBUG1(sRules+"Packages:\n"+ShowPkgQueue());
   }
   //---use Crude Method, both in the absence of, and AFTER using PackingRules (to compute size as single package)
   var thk=0,  len=0,  wid=0,  wei=0;
   for(var i=0; i<PkgQueue.length; ++i){
      if(PkgQueue[i].size.Length > len) len = PkgQueue[i].size.Length;
      if(PkgQueue[i].size.width  > wid) wid = PkgQueue[i].size.width ;
      thk += PkgQueue[i].size.height * PkgQueue[i].qty;
      wei += PkgQueue[i].weight;
   }
   PkgAsOne = new Qszwt(1, new Size(len,wid,SzRnd(thk)), WtRndUP(wei));         //overall package size and weight (weight rounded up)
   //==!!NEEDED: Crude-Method, try placing several small items (LxW) into one layer, guided by package-size defns -- in case PackingRules omitted;
   //  OR: supply a script that reads a given set of HTML files, extracting items for sale, and then computes the PackingRules  (and make PackingRules required)
}
//------------------------------------------------------------------------
// FUNCTION: ComputeShipping -- compute the shipping cost for size and weight of package
//------------------------------------------------------------------------
function ComputeShipping (ZoneParam){
   sComputeShippingNote="";
   if(PkgAsOne.weight==0 && PkgAsOne.size.height==0) return 0.00;
   var Ship= ShipTable[ZoneParam].pkginfo;                      //Ship is array of PkgClass(weight,size,costfixed,costperwtunit,wtunit,flag)
   function PricePkg(Ship,weight,height){                       //function to lookup price for one pkg
      for(var c=0; c<Ship.length; ++c) if(weight<=Ship[c].weight && height<=Ship[c].size.height)
      {  return  Cents(Ship[c].costfixed + Ship[c].costperwtunit * Math.ceil(weight / Ship[c].wtunit)); }       //return shipping-price
      return 99999.99;                                                                                          //return illegal-weight-or-size indication
   }
   var asOne= PricePkg(Ship, PkgAsOne.weight, PkgAsOne.size.height);            //---compute price as a single package
   
   var asMult=99999.99,  FC=null,  iN=0;
   for(var c=0; c<Ship.length; ++c) if(Ship[c].flag=="*") FC=c;
   if(FC!=null){                                                                //---also price as multiple smaller packages (to handle pricing anomalies...)
      var maxHt=Ship[FC].size.height;
      var maxWt=Ship[FC].weight;
      var accHt=0,  accWt=0, sW="";  function R(f) {return " "+Math.ceil(f)+WTUNITS;}
      asMult=0;
      for(var i=PkgQueue.length; i--;) for(var j=PkgQueue[i].qty; j--;){        //do in reverse order (doing lightest ones first is best, at least for my examples:-)
         var Wt= PkgQueue[i].wt[j];
         var Ht= PkgQueue[i].size.height;
         if( Wt>maxWt || Ht>maxHt) {asMult=99999.99; break;}                    //having ByWt-packing, now treat this as "separate packages impossible"  !!double-break??
         if(accWt+Wt>maxWt || accHt+Ht>maxHt){                                  //handle previously accumulated little ones
            asMult+= PricePkg(Ship, accWt, accHt);  ++iN; sW+=R(accWt);
            accHt=0; accWt=0;
         }
         accWt+=Wt;  accHt+=Ht;                                                 //accumulate another little one
      }
      if(accWt+accHt){asMult+=PricePkg(Ship,accWt,accHt); ++iN; sW+=R(accWt);}  //finish off any non-handled accumulation
   }
   var asOneVat=  Cents(asOne *ShipTaxRate);
   var asMultVat= Cents(asMult*ShipTaxRate);
   asOne += asOneVat  + HandlingChargePerOrder;                                         //add HandlingCharges BEFORE comparing
   asMult+= asMultVat + HandlingChargePerOrder + (iN-1)*HandlingChargePerExtraPackage;  //add HandlingCharges BEFORE comparing
   var cost;
   if(asOne<=asMult){                                                                                                                   //as one package
      cost=asOne;  gVat=asOneVat;
   }else{                                                                                                                               //as multiple packages
      cost=asMult; gVat=asMultVat;
      if(strAsMultiple)sComputeShippingNote="("+strAsMultiple+sW+(strAsSingle?"; "+strAsSingle+MoneySymbol+moneyFormat(asOne):"")+")";  //sW+asOne can be suppressed...
   }
   if(cost>=99999) {sComputeShippingNote=strBroken;  return 99999.99;}
   return  cost;
   //
   //!!consider: keep info on the items within each packet, so can produce packing-instructions showing which items go into which size envelope (for the shipping dept)
}
//------------------------------------------------------------------------
// FUNCTION: NewZone -- update the ZoneSelected cookie
//------------------------------------------------------------------------
function NewZone (ZoneParam){
   SetCookie("ZoneSelected", ZoneParam, null, "/");
   var RegionCookie= iGetCookie("RegionSelected");
   if(RegionCookie!=null && RegionFromZone.length && !Element(RegionCookie, RegionFromZone[ZoneParam]))  DeleteCookie("RegionSelected","/");    //delete cookie if now illegal
   location.href=location.href;
}
//------------------------------------------------------------------------
// FUNCTION: NewRegion -- update the RegionSelected cookie
//------------------------------------------------------------------------
function NewRegion (RegionParam){
   SetCookie("RegionSelected", RegionParam, null, "/");
   var ZoneCookie= iGetCookie("ZoneSelected");
   if(ZoneCookie!=null && RegionFromZone.length && !Element(RegionParam, RegionFromZone[ZoneCookie]))  DeleteCookie("ZoneSelected","/");        //delete cookie if now illegal
   location.href=location.href;
}
//------------------------------------------------------------------------
// FUNCTION: MoreLessInfo -- do toggling-update to the MoreState cookie, for DynamicWtSzColumns option
//------------------------------------------------------------------------
function MoreLessInfo(){
   var MoreState=iGetCookie("MoreState");  if(MoreState==null) MoreState= (DisplayWtColumn?1:0)*2 + (DisplaySzColumn?1:0);
   MoreState= ((MoreState&DynamicWtSzColumns)==DynamicWtSzColumns ?0 :DynamicWtSzColumns);      //toggle Wt&Sz-state as per DynamicWtSzColumns option
   SetCookie("MoreState", MoreState, null, "/");                                                //update cookie
   location.href=location.href;                                                                 //redraw page
}


//========================================================================
// The rest still resembles the NopDesign version of nopcart.js (with some Stefko mods).
// ER: have revised ALL  parseFloat --> NumberZ (to permit whole or fractional number where fractional was needed)
// ER: have revised ALL  parseInt   --> Integer (new function that Rounds to whole-number, and avoids the leading-zero-means-octal gotcha)
// ER: introduced a few functions to reduce duplication, thus making future maintenance less tedious / error-prone
// ER: my other early revisions are flagged with a comment containing "ER:"  (later ones have a YYYY-MM-DD date instead)
// ER: after my Quantity-Discount Pricing mods (2007-08-07), any resemblance to the original is but faint
//========================================================================


//------------------------------------------------------------------------
// FUNCTION: NumberV
// PURPOSE: convert string to number, for CKquantity, CKprice routines
//------------------------------------------------------------------------
function NumberV(checkString) {
   var sNewString="", K=0;
   for(var i=0; i<checkString.length; ++i){
      ch = checkString.substring(i, i+1);
      if(ch>="0" && ch<="9")     sNewString += ch;      //keep all digits
      else if(ch=="." && ++K==1) sNewString += ch;      //ER: keep only the first dot
   }
   return(NumberZ(sNewString));
}
//------------------------------------------------------------------------
// FUNCTION: CKquantity
// PARAMETERS: Quantity to validate
// RETURNS: Quantity as a whole-number, but in a string
// PURPOSE: Make sure quantity is a whole-number
//------------------------------------------------------------------------
function CKquantity(checkString) {
   var N=Integer(NumberV(checkString));  if(N==0) N=1;  return(""+N);
}
//------------------------------------------------------------------------
// FUNCTION: CKprice
// PARAMETERS: Price to validate
// RETURNS: Price as a number, but in a string
// ER: introduced this routine to validate an Online-Donation amount;
//------------------------------------------------------------------------
function CKprice(checkString) {
   var N=Cents(NumberV(checkString));
   if(N==0) N=DefaultDonation;  else if(N<MinimumDonation) {N=MinimumDonation; alert(MinimumDonationPrompt);}
   return(moneyFormat(N));
}

//------------------------------------------------------------------------
// FUNCTION: AddToCart
// PARAMETERS: Form Object
// RETURNS: false if nothing added because of error-message
// PURPOSE: Add a product to the user's shopping cart, by updating Cookies, optionally with a popup prompt
//------------------------------------------------------------------------
function AddToCart(thisForm) {
   var iNumberOrdered = 0;
   var bAlreadyInCart = false;
   var notice = "";
   var ELE, ATR;
   ELE=thisForm;                                                                //Handle the old-style name=... value=... way of specifying attributes
   sID       = "";      if(ATR= ELE._ID || ELE.ID || ELE.ID_NUM ) sID      =ATR.value;          //2008-02-09 also support old name ID_NUM
   sQUANTITY = "1";     if(ATR= ELE._QUANTITY   || ELE.QUANTITY ) sQUANTITY=ATR.value;
   sPRICE    = "0.00";  if(ATR= ELE._PRICE      || ELE.PRICE    ) sPRICE   =ATR.value;
   sNAME     = "";      if(ATR= ELE._NAME       || ELE.NAME     ) sNAME    =ATR.value;
   sWEIGHT   = "0";     if(ATR= ELE._WEIGHT     || ELE.WEIGHT   ) sWEIGHT  =ATR.value;          //ER: was called sSHIPPING
   sLENGTH   = "0";     if(ATR= ELE._LENGTH     || ELE.LENGTH   ) sLENGTH  =ATR.value;          //ER: new
   sWIDTH    = "0";     if(ATR= ELE._WIDTH      || ELE.WIDTH    ) sWIDTH   =ATR.value;          //ER: new
   sHEIGHT   = "0";     if(ATR= ELE._HEIGHT     || ELE.HEIGHT   ) sHEIGHT  =ATR.value;          //ER: new
   sTHUMB   = "";       if(ATR= ELE._THUMB     || ELE.THUMB   ) sTHUMB  =ATR.value;          //ER: new
   sPROMPT   = "";                                                              //2008-01-21: a prompt-string from a selector
   for(var i=0;i<thisForm.elements.length;++i){                                 //2008-02-13: go thru hidden elements handling attrname=attrvalue
      ELE=thisForm.elements[i];
      if(ELE.type!="hidden") continue;
      if(!ELE.getAttribute)  continue;                                          //2008-03-10 skip if old browser that doesn't support getAttribute
      if(     ATR= ELE.getAttribute("_ID")      || ELE.getAttribute("ID_NUM")   ) sID      = ATR;       //avoid attribute named ID
      if(     ATR= ELE.getAttribute("_QUANTITY")|| ELE.getAttribute("QUANTITY") ) sQUANTITY= ATR;
      if(     ATR= ELE.getAttribute("_PRICE")   || ELE.getAttribute("PRICE")    ) sPRICE   = ATR;
      if(     ATR= ELE.getAttribute("_NAME")                                    ) sNAME    = ATR;       //avoid attribute named NAME
      if(     ATR= ELE.getAttribute("_WEIGHT")  || ELE.getAttribute("WEIGHT")   ) sWEIGHT  = ATR;
      if(     ATR= ELE.getAttribute("_LENGTH")  || ELE.getAttribute("LENGTH")   ) sLENGTH  = ATR;
      if(     ATR= ELE.getAttribute("_WIDTH")                                   ) sWIDTH   = ATR;       //avoid attribute named WIDTH
      if(     ATR= ELE.getAttribute("_HEIGHT")                                  ) sHEIGHT  = ATR;       //avoid attribute named HEIGHT
      if(     ATR= ELE.getAttribute("_THUMB")   || ELE.getAttribute("THUMB")   ) sTHUMB  = ATR;       //avoid attribute named HEIGHT
   }
   //sADDTLINFO= "";                                                            //2008-02-07: old ADDITIONALINFOn selectors yanked and replaced with new method below
   //if(thisForm.ADDITIONALINFO !=null) sADDTLINFO =         thisForm.ADDITIONALINFO [thisForm.ADDITIONALINFO.selectedIndex].value;
   //if(thisForm.ADDITIONALINFO2!=null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO2[thisForm.ADDITIONALINFO2.selectedIndex].value;
   //if(thisForm.ADDITIONALINFO3!=null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO3[thisForm.ADDITIONALINFO3.selectedIndex].value;
   //if(thisForm.ADDITIONALINFO4!=null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO4[thisForm.ADDITIONALINFO4.selectedIndex].value;
   //if(thisForm.USERENTRY      !=null) sADDTLINFO += (sADDTLINFO?"; ":"") + thisForm.USERENTRY.value;  //ER: avoid leading semicolon
   //if(sADDTLINFO) sNAME+="; "+sADDTLINFO;//==TEMP==
                                                                                //2008-02-07:  new selectors with AddOneOfMany-features;  plus they can be Radio-buttons
   for(var N=0;N<=2;++N) for(var n=0;n<=9;++n){                                 //2008-02-07:  and they can supply Replacement OR To-Be-Added values (leading plus-sign)
      var selname=["ADDITIONALINFO","USERCHOICE","_USERCHOICE"][N] + (n?n:"");  //support  ADDITIONALINFOn  USERCHOICEn  _USERCHOICEn  for n=emptystring,1..9
      var selector=thisForm[selname];
      if(selector==null) continue;
      if(typeof selector.selectedIndex == "undefined"){                         //for a RADIO-button-selector need a loop testing the CHECKED-attribute
         for(var i=0;i<selector.length;++i) if(selector[i].checked) ELE=selector[i];
      }else{                                                                    //for a SELECT-box;  could use a loop testing the SELECTED-attribute
         ELE=selector[selector.selectedIndex];
      }
      function NewStr(OLD,NEW) {return    (NEW.substring(0,1)=="+" ?        OLD +        NEW.substring(1,NEW.length)  :NEW);}   //NewStr: plus-sign indicates catenation
      function NewNum(OLD,NEW) {return ""+(NEW.substring(0,1)=="+" ?NumberZ(OLD)+NumberZ(NEW.substring(1,NEW.length)) :NEW);}   //NewNum: plus-sign indicates addition

      if(!ELE.getAttribute)  if(ATR= ELE.value                                  ) sNAME    +=" "+ATR;                   //old-style here; 2008-03-18 only for old browser
      if(!ELE.getAttribute)  continue;                                                                                  //2008-03-10 skip if old browser w/o getAttribute

      if(     ATR= ELE.getAttribute("_ID")      || ELE.getAttribute("ID")       ) sID      = NewStr(sID,ATR);           //beware of attribute named ID
      else if(ATR=                                 ELE.getAttribute("ID_NUM")   ) sID      = NewStr(sID,ATR);           //support old name
      if(     ATR= ELE.getAttribute("_QUANTITY")|| ELE.getAttribute("QUANTITY") ) sQUANTITY= NewNum(sQUANTITY,ATR);     //possibly useful here??
      if(     ATR= ELE.getAttribute("_PRICE")   || ELE.getAttribute("PRICE")    ) sPRICE   = NewNum(sPRICE,ATR);
      if(     ATR= ELE.getAttribute("_NAME")                                    ) sNAME    = NewStr(sNAME,ATR); 
      else if(ATR= ELE.value                                                    ) sNAME   +=" "+ATR;                    //2008-03-18 old-style still here for new browser
      else if(ELE.type!="radio"  &&  (ATR=         ELE.getAttribute("NAME"))    ) sNAME    = NewStr(sNAME,ATR);         //beware of NAME; 2008-03-18 avoid on radio
      if(     ATR= ELE.getAttribute("_WEIGHT")  || ELE.getAttribute("WEIGHT")   ) sWEIGHT  = NewNum(sWEIGHT,ATR);
      if(     ATR= ELE.getAttribute("_LENGTH")  || ELE.getAttribute("LENGTH")   ) sLENGTH  = NewNum(sLENGTH,ATR);
      if(     ATR= ELE.getAttribute("_WIDTH")   || ELE.getAttribute("WIDTH")    ) sWIDTH   = NewNum(sWIDTH,ATR);        //beware of attribute named WIDTH
      if(     ATR= ELE.getAttribute("_HEIGHT")  || ELE.getAttribute("HEIGHT")   ) sHEIGHT  = NewNum(sHEIGHT,ATR);       //beware of attribute named HEIGHT
      if(     ATR= ELE.getAttribute("_THUMB")  || ELE.getAttribute("THUMB")   ) sTHUMB  = NewNum(sTHUMB,ATR);
      if(     ATR= ELE.getAttribute("_PROMPT")  || ELE.getAttribute("PROMPT")   ) sPROMPT  += (sPROMPT?"; ":"")+ATR;
      //--note: for INPUT-TYPE=RADIO/HIDDEN the names NAME, ID, WIDTH, HEIGHT are not available, and that's why I went to _ID _NAME etc (briefly used PRODID PRODNAME)
      //but they work in SELECT-OPTION, so am leaving support for them;
      //2008-03-18: skip NAME on radio to avoid getting NAME=USERCHOICE;  also skip VALUE in new browser when _NAME present  (may need rethink??)
   }
   if(sID+sNAME=="" && sPROMPT=="") sPrompt="Please select an option";          //2008-01-21: (AddOneOfManyToCart uses sNAME=="selected")
   if(sPROMPT!="")  {alert(sPROMPT);  return false;}                            //2008-01-21: prompt, and avoid adding (null) product
   if(PrefEQ(sID,PrefDonation))  sPRICE=CKprice(sPRICE);                        //2008-02-04: validation for donation-amount (default & minimum)
   if(     ATR=thisForm._USERTEXT||thisForm.USERTEXT) {if(ATR.value)sNAME+= "; " + ATR.value;}          //2008-02-07: now directly onto sNAME  (was onto sADDTLINFO)
   else if(ATR=thisForm.USERENTRY)                    {if(ATR.value)sNAME+= "; " + ATR.value;}          //support old name

   //If this product already in the cart, then combine them instead of adding another.
   iNumberOrdered= iGetCookie("NumberOrdered",0);
   for(var i=1; i<=iNumberOrdered; ++i){
      GetRow(i);                                                //ER: get fields for row-i
      if(fields[0] == sID    &&
         fields[3] == sNAME  &&   
         fields[8] == sTHUMB   &&                                               //2008-02-07 removed:  fields[8] == sADDTLINFO &&
        (fields[2] == sPRICE || PrefEQ(sID,PrefDonation))                       //2008-02-04: donations can be combined even when amount different
      ){                                                        //---already in cart, so combine;  ER: a match on all but PRICE deserves a DEBUG-alert?
         bAlreadyInCart = true;
         if(PrefEQ(sID,PrefDonation)){                          //donations are combined by summing amounts (new 2008-02-04)
            dbUpdatedOrder = sID + "|" +
               sQUANTITY + "|" +
               (Number(sPRICE)+Number(fields[2])) + "|" +
               sNAME     + "|" +
               sWEIGHT   + "|" +
               sLENGTH   + "|" +
               sWIDTH    + "|" +
           	   sHEIGHT    + "|" +
               sTHUMB;                                         //2008-02-07 removed:   + "|" +  sADDTLINFO;
         }else{                                                 //non-donations are combined by summing quantities
            dbUpdatedOrder = sID + "|" +
               (Integer(sQUANTITY)+Integer(fields[1])) + "|" +
               sPRICE    + "|" +
               sNAME     + "|" +
               sWEIGHT   + "|" +
               sLENGTH   + "|" +
               sWIDTH    + "|" +
               sHEIGHT    + "|" +
               sTHUMB;                                            //2008-02-07 removed:   + "|" + sADDTLINFO;
         }
         sNewOrder = "Order." + i;
         DeleteCookie(sNewOrder, "/");
         SetCookie(sNewOrder, dbUpdatedOrder, null, "/");
         notice = strAdded + "\n-------------------------------------\n" + strAddedQuantity + sQUANTITY + "\n" + strAddedProduct + sNAME;
         break;
      }
   }
   if(!bAlreadyInCart){                                         //---not in cart, so add it
      iNumberOrdered++;
      if(iNumberOrdered > 15) alert(strSorry);                  //limit nbr-rows in cart to 15;  ER: was 12  (the limit is 20 cookies for one domain)
      else {
         dbUpdatedOrder = sID + "|" +
            sQUANTITY + "|" +
            sPRICE    + "|" +
            sNAME     + "|" +
            sWEIGHT   + "|" +
            sLENGTH   + "|" +
            sWIDTH    + "|" +
            sHEIGHT    + "|" +
            sTHUMB;                                                //2008-02-07 removed:   + "|" + sADDTLINFO;
         sNewOrder = "Order." + iNumberOrdered;
         SetCookie(sNewOrder,       dbUpdatedOrder, null, "/");
         SetCookie("NumberOrdered", iNumberOrdered, null, "/");
         notice = strAdded + "\n-------------------------------------\n" + strAddedQuantity + sQUANTITY + "\n" + strAddedProduct + sNAME;
      }
   }
   if(DisplayPopupOnAdd && notice!="")  alert(notice);
   return true;
}


//------------------------------------------------------------------------
// FUNCTION: moneyFormat
// PARAMETERS: Number to be formatted
// RETURNS: Formatted Number
// PURPOSE: convert float to #.## string
// ER: first I rewrote this to something I could understand, since supporting MoneyPLACES option was unthinkable otherwise;
//------------------------------------------------------------------------
function moneyFormatFRA(input) {        //for any currency that uses a fraction, example US-dollars
   var cents= "" + Math.round(input * MoneyROUND_FRA);
   while(cents.length < MoneyPLACES+1)  cents="0"+cents;
   return  cents.substring(0, cents.length-MoneyPLACES) + "." + cents.substring(cents.length-MoneyPLACES, cents.length);
   //
   ////--ER: was:
   //var dollars= Math.floor(input);
   //var tmp= new String(input);
   //for(var decimalAt=0; decimalAt < tmp.length; decimalAt++){
   //   if(tmp.charAt(decimalAt)==".") break;
   //}
   //var cents= "" + Math.round(input * 100);
   //cents= cents.substring(cents.length-2, cents.length)
   //dollars += ((tmp.charAt(decimalAt+2)=="9")&&(cents=="00"))? 1 : 0;
   //if(cents=="0") cents = "00";
   //return  dollars + "." + cents;
}
function moneyFormatNOF(input) {        //ER: for a currency that uses no fraction (and may need rounding to a multiple of 1000 etc)
   return ""+Cents(input);
}
moneyFormat = (MoneyPLACES>0 ?moneyFormatFRA :moneyFormatNOF);  //ER: init function according to whether a fraction is needed  (MoneyPLACES indicates that)

//------------------------------------------------------------------------
// FUNCTION: SetCookie
// PARAMETERS: name, value, expiration date, path, domain, security
// RETURNS: Null
// PURPOSE: Store a cookie in the users browser
//------------------------------------------------------------------------
function SetCookie (name,value,expires,path,domain,secure) {
   document.cookie = CartID + name + "=" + escape (value) +
   ((expires) ? "; expires=" + expires.toGMTString() : "") +
   ((path) ? "; path=" + path : "") +
   ((domain) ? "; domain=" + domain : "") +
   ((secure) ? "; secure" : "");
}
//------------------------------------------------------------------------
// FUNCTION: DeleteCookie
// PARAMETERS: Cookie name, path, domain
// RETURNS: null
// PURPOSE: Remove a cookie from users browser.
//------------------------------------------------------------------------
function DeleteCookie (name,path,domain) {
   if(GetCookie(name)){
      document.cookie = CartID + name + "=" +
      ((path) ? "; path=" + path : "") +
      ((domain) ? "; domain=" + domain : "") +
      "; expires=Thu, 01-Jan-70 00:00:01 GMT";
   }
}
//------------------------------------------------------------------------
// FUNCTION: getCookieVal
// PARAMETERS: offset
// RETURNS: URL unescaped Cookie Value
// PURPOSE: Get a specific value from a cookie
//------------------------------------------------------------------------
function getCookieVal (offset) {
   var endstr = document.cookie.indexOf (";", offset);
   if(endstr == -1)  endstr= document.cookie.length;
   return unescape(document.cookie.substring(offset, endstr));
}
//------------------------------------------------------------------------
// FUNCTION: GetCookie
// PARAMETERS: Name
// PURPOSE: Retrieve cookie from users browser
// RETURNS: Value in Cookie as a string,  or null if no such cookie exists
//------------------------------------------------------------------------
function GetCookie (name) {
   var arg = CartID + name + "=";
   var alen = arg.length;
   var clen = document.cookie.length;
   var i = 0;
   while(i < clen){
      var j = i + alen;
      if(document.cookie.substring(i, j)==arg)  return(getCookieVal(j));
      i = document.cookie.indexOf(" ", i) + 1;
      if(i == 0) break;
   }
   return(null);
}
//------------------------------------------------------------------------
// FUNCTION: iGetCookie
// PARAMETERS: Name, DEF
// PURPOSE: Retrieve an INTEGER cookie from users browser
// RETURNS: Value in Cookie as an Integer,  or DEF if no such cookie exists
//------------------------------------------------------------------------
function iGetCookie (name, DEF) {  if(DEF==null)DEF=null;  //if DEF is omitted, use null as DEF
   var r= GetCookie(name);
   return (r==null ?DEF :Integer(r));
}
//------------------------------------------------------------------------
// FUNCTION: FixCookieDate
// PARAMETERS: date
// RETURNS: date
// PURPOSE: Fix cookie date, store back in date
//------------------------------------------------------------------------
//function FixCookieDate (date) {
//   var base= new Date(0);
//   var skew= base.getTime();
//   date.setTime(date.getTime() - skew);
//}
//------------------------------------------------------------------------
// FUNCTION: GetRow
// PURPOSE: read one cart-row from the cookie-database
// RETURNS: global array fields, an array containing the fields
// NOTE: in the database-format, fields are separated by "|"
// ER: made this a function to improve maintainability.
//------------------------------------------------------------------------
function GetRow(i){
   RowKey = "Order." + i;
   dbrow = "";
   dbrow = GetCookie(RowKey);
   Token0 = dbrow.indexOf("|", 0);
   Token1 = dbrow.indexOf("|", Token0+1);
   Token2 = dbrow.indexOf("|", Token1+1);
   Token3 = dbrow.indexOf("|", Token2+1);
   Token4 = dbrow.indexOf("|", Token3+1);
   Token5 = dbrow.indexOf("|", Token4+1);
   Token6 = dbrow.indexOf("|", Token5+1);
   Token7 = dbrow.indexOf("|", Token6+1);               //scrapped 2008-02-07
   fields = [];
   fields[0] = dbrow.substring(0,        Token0);       //Product-ID
   fields[1] = dbrow.substring(Token0+1, Token1);       //Quantity
   fields[2] = dbrow.substring(Token1+1, Token2);       //Price
   fields[3] = dbrow.substring(Token2+1, Token3);       //Product-Name
   fields[4] = dbrow.substring(Token3+1, Token4);       //Weight
   fields[5] = dbrow.substring(Token4+1, Token5);       //Length;       ER: new
   fields[6] = dbrow.substring(Token5+1, Token6);       //Width;        ER: new
   fields[7] = dbrow.substring(Token6+1, dbrow.length); //Height;       ER: new;                                2008-02-07 revised Token7-->dbrow.length
   fields[8] = dbrow.substring(Token7+1, dbrow.length); //Addtl-Info;   ER: was fields[5] in NopDesign version  2008-02-07 yanked
}
//------------------------------------------------------------------------
// FUNCTION: RemoveFromCart
// PARAMETERS: Row Number to Remove
// RETURNS: Null
// PURPOSE: Remove an item from a users shopping cart
//------------------------------------------------------------------------
function RemoveFromCart(RemOrder) {
   if( (DisplayPopupOnRemove ? confirm(strRemove) : true) ){    //ER: suppress the confirm when DisplayPopupOnRemove==false
      NumberOrdered = iGetCookie("NumberOrdered",0);
      for(var i=RemOrder; i < NumberOrdered; ++i){
         NewOrder1 = "Order." + (i+1);
         NewOrder2 = "Order." + (i);
         database = GetCookie(NewOrder1);
         SetCookie (NewOrder2, database, null, "/");
      }
      NewOrder = "Order." + NumberOrdered;
      SetCookie ("NumberOrdered", (NumberOrdered>0?NumberOrdered-1:0), null, "/");
      DeleteCookie(NewOrder, "/");
      location.href=location.href;
   }
}
//------------------------------------------------------------------------
// FUNCTION: EmptyTheCart
// PURPOSE: Remove all items from a users shopping cart.
// Intended for a thanks-for-your-purchase page (that your payment-processor is instructed to return to),
// since after checkout one expects an empty cart...
//------------------------------------------------------------------------
function EmptyTheCart(){
   NumberOrdered = iGetCookie("NumberOrdered",0);
   for(var i=1; i <= NumberOrdered; ++i){
      NewOrder = "Order." + i;
      DeleteCookie(NewOrder, "/");
   }
   SetCookie("NumberOrdered", 0, null, "/");
}
//------------------------------------------------------------------------
// FUNCTION: ChangeQuantity
// PARAMETERS: Order Number to Change Quantity
// RETURNS: Null
// PURPOSE: Change quantity of an item in the shopping cart
//------------------------------------------------------------------------
function ChangeQuantity(OrderItem,NewQuantity) {
   if(isNaN(NewQuantity)){
      alert(strErrQty);
   }else{
      GetRow(OrderItem);        //ER: get fields for row-OrderItem
      dbUpdatedOrder = fields[0] + "|" +
         NewQuantity + "|" +
         fields[2]   + "|" +
         fields[3]   + "|" +
         fields[4]   + "|" +
         fields[5]   + "|" +
         fields[6]   + "|" +
         fields[7];             //2008-02-07 removed:  + "|" + fields[8];
      sNewOrder = "Order." + OrderItem;
      DeleteCookie(sNewOrder, "/");
      SetCookie(sNewOrder, dbUpdatedOrder, null, "/");
      location.href=location.href;
   }
}
////------------------------------------------------------------------------
//// FUNCTION:  RadioChecked
//// PARAMETERS:  Radio button to check
//// RETURNS:   True if a radio has been checked
//// PURPOSE:   Form validation
//// ER: NOT USED
////------------------------------------------------------------------------
//function RadioChecked( radiobutton ) {
//   var bChecked = false;
//   var rlen = radiobutton.length;
//   for(var i=0; i < rlen; ++i)  if(radiobutton[i].checked)  bChecked = true;
//   return bChecked;
//}
////------------------------------------------------------------------------
//// FUNCTION: QueryString
//// PARAMETERS: Key to read
//// RETURNS: value of key
//// PURPOSE: Read data passed in via GET mode
//// ER: NOT USED
////------------------------------------------------------------------------
//QueryString.keys = [];
//QueryString.values = [];
//function QueryString(key) {
//   var value = null;
//   for(var i=0;i<QueryString.keys.length;++i){
//      if (QueryString.keys[i]==key) {
//         value = QueryString.values[i];
//         break;
//      }
//   }
//   return value;
//}
////------------------------------------------------------------------------
//// FUNCTION: QueryString_Parse
//// PARAMETERS: (URL string)
//// PURPOSE: Parse query string data;  must be called before QueryString
//// ER: NOT USED
////------------------------------------------------------------------------
//function QueryString_Parse() {
//   var query= window.location.search.substring(1);
//   var pairs= query.split("&");  for(var i=0;i>pairs.length;++i){
//      var pos = pairs[i].indexOf("=");
//      if (pos >= 0) {
//         var argname = pairs[i].substring(0,pos);
//         var value = pairs[i].substring(pos+1);
//         QueryString.keys[QueryString.keys.length] = argname;
//         QueryString.values[QueryString.values.length] = value;
//      }
//   }
//}


//------------------------------------------------------------------------
// FUNCTION: ReadCartComputePrices
// PURPOSE:  Read all rows from the cookies
// RETURNS:  globals iNumberOrdered, and Cart an array with Cart[i] being row-i;
// NOTE THAT:
//      Cart[i].ID        replaces all subsequent uses of  fields[0];
//      Cart[i].QUANTITY  replaces all subsequent uses of  fields[1]  OR  Integer(fields[1]);
//      Cart[i].PRICEAVG  replaces all subsequent uses of  fields[2]  OR  NumberZ(fields[2]);
//      Cart[i].NAME      replaces all subsequent uses of  fields[3];
//      Cart[i].WEIGHT    replaces all subsequent uses of  fields[4]  OR  NumberZ(fields[4]);
//      Cart[i].LENGTH    replaces all subsequent uses of  fields[5]  OR  NumberZ(fields[5]);
//      Cart[i].WIDTH     replaces all subsequent uses of  fields[6]  OR  NumberZ(fields[6]);
//      Cart[i].HEIGHT    replaces all subsequent uses of  fields[7]  OR  NumberZ(fields[7]);
//      Cart[i].ADDTLINFO replaces all subsequent uses of  fields[8]  <-- scrapped 2008-02-07
//      Cart[i].PRICE     is the entire string from Form.PRICE; but should never be needed outside of this routine;
// Form.PRICE now consists of comma-separated terms such as:
//      3.95                      -- means 3.95 each
//      3.95,2:3.00               -- means 3.95 for the first, 2nd and subsequent are 3.00 each;
//      3.95,10=2.99              -- means 3.95 each, or exactly 10 for 29.90;
//      3.95,10=2.99,10:2.99      -- means 3.95 each, 10 or more are 2.99 each;
//      3.95,2:3.00,4:2.75,8:2.50 -- means 2nd...are 3.00, 4th...are 2.75, 8th and subsequent are 2.50;
//      3.95,2:-30,GRP01          -- means 2nd and subsequent are 30% off, and this applies across all products in the group GRP01;
//      can have any number of ":" or "=" terms, at most one starting with a letter to name a group;
//      ":" and "=" terms may be interspersed, or not, but the ":" terms must appear in increasing order by left-side, and "=" terms likewise;
//      RESTRICTION: exact-quantity ("=") pricing is only permitted within a group where all products have identical prices;
//      anything else would be so bloody hard to explain, it's hard to see anyone wanting it;
// ER: All-at-once reading is essential for a reasonably efficient implementation of Quantity-Discount Pricing,
//      since several passes are needed to compute prices, and those are needed before the pass that displays & does payment-processing;
//      (more than one preliminary pass is needed to support Product-Groups);
// ER: also moved the Shipping & Tax calculation code here, from ManageCart/CheckoutCart, setting globals: fTotal, fShipping, fTax, ZoneSelected, RegionSelected,
//      to further reduce duplication thus lessening the tedium & error-proneness of making modifications.
// Q: To get deterministic n-or-more pricing, independent of the order items added to cart, could sort by price within group?
// 2007-12-04: now applying Cents-rounding to PRICEAVG on the assumption that payment-processors can't handle more fractional digits than is the currency norm;
// 2008-02-03: now applying Cents-rounding to PRICEAVG earlier (here) so that amounts shown by ViewCart will agree with what PayPal etc will be displaying;
//------------------------------------------------------------------------
function ReadCartComputePrices(){
   var Dig="0123456789", Lwr="abcdefghijklmnopqrstuvwxyz", Upr="ABCDEFGHIJKLMNOPQRSTUVWXYZ", Let=Lwr+Upr;       //constants for Is-testing
   function Is(c,pat) {return pat.indexOf(c)!=-1;}
   var i, k;
   var KK=-1;                                   //row-nbr for Coupon-discount
   var C, G, D, X, K;                           //results from the Pparse routine
   function Pparse(priceparm){                  //the Pparse PRICE-parsing routine
      C=0; G=""; D=[]; X=[]; K=null;
      if(priceparm.substring(0,2)==">="){       //a coupon (2008-03-06)
         var x=priceparm.substring(2).split(":"), y=x[1].indexOf("%"); if(y==-1)y=x[1].length;
         K= {min:NumberZ(x[0]), amt:NumberZ(x[1].substring(0,y)), pct:x[1].substring(y)};
         //alert("min:"+K.min+"; amt:"+K.amt+"; pct:"+K.pct+";");
      }else for(var price=priceparm.split(","), J=0; J<price.length; ++J){
         var T=price[J];
         if(     T.indexOf("=")!=-1)            {var x=T.split("="); X.push( {q:Integer(x[0]), p:NumberZ(x[1])} );}
         else if(T.indexOf(":")!=-1)            {var x=T.split(":"); D.push( {q:Integer(x[0]), p:NumberZ(x[1])} );}
         else if(Is(T.substring(0,1),Let))      G=T;
         else                                   C=NumberZ(T);
      }
   }
   //====read all rows from the cookies====
   Cart = [];                                           //init global Cart array
   iNumberOrdered=iGetCookie("NumberOrdered",0);        //get the nbr-rows-in-cart cookie
   for(i=1; i<=iNumberOrdered; ++i){
      GetRow(i);                                        //get fields for row-i
      Pparse(fields[2]);                                //parse the PRICE string
      Cart[i]= {
         ID:               fields[0], 
         QUANTITY: Integer(fields[1]), 
         PRICE:            fields[2], 
         NAME:             fields[3], 
         WEIGHT:   NumberZ(fields[4]), 
         LENGTH:   NumberZ(fields[5]), 
         WIDTH:    NumberZ(fields[6]), 
         HEIGHT:   NumberZ(fields[7]), 
         THUMB:        fields[8],                   //2008-02-07 scrapped ADDTLINFO aka fields[8]
         C:C, G:G, D:D, X:X, K:K, PRICEAVG:null         //the parsed-price fields; consider using ID as G if no group specified(?)
      }
   }
   //====compute prices====
   for(i=1; i<=iNumberOrdered; ++i){
      if(Cart[i].PRICEAVG!=null) continue;                                      //skip if row already done (due to groups)
      C=Cart[i].C;  G=Cart[i].G;  D=Cart[i].D;  X=Cart[i].X;  K=Cart[i].K;      //info from Pparse
      function eEQ(A,B) {return A.q==B.q && A.p==B.p;}                                                                                  //compare q-p elements
      function aEQ(A,B) {if(A.length!=B.length)return false; for(var k=A.length;k--;)if(!eEQ(A[k],B[k]))return false; return true;}     //compare array of q-p elements
      function pEQ(A,B) {return A.C==B.C && aEQ(A.D,B.D) && aEQ(A.X,B.X);}                                                              //compare parsed prices
      function str(X) {var s="["; for(var i=0;i<X.length;++i)s+="{"+X[i].q+","+X[i].p+"},"; s+="]"; return s;}  //convert X or D to string
      function pp(P)  {return (P>=0 ?P :C*(100+P)/100);}                                                        //convert negative price to a percent-off-wrt-C price
      var q=Cart[i].QUANTITY;                                                                           //q is qty of this product
      var Q=q;   if(G!="")for(Q=0, k=1; k<=iNumberOrdered; ++k) if(Cart[k].G==G) Q+= Cart[k].QUANTITY;  //Q is qty across all products in this group
      var g=[i]; if(G!="")for(g=[],k=1; k<=iNumberOrdered; ++k) if(Cart[k].G==G) g.push(k);             //g is array of indices for rows in this group
      var ix=-1; for(k=X.length; k--;) if(X[k].q<=Q) {ix=k; break;}             //find the biggest applicable exact-qty ("=") discount;  requires ordered terms
      var id=-1; for(k=D.length; k--;) if(D[k].q<=Q) {id=k; break;}             //find the biggest applicable n-or-more (":") discount;  requires ordered terms
      DEBUG4("row:"+i+" itm:"+Cart[i].ID+" PRICE:"+Cart[i].PRICE+" C:"+C+" G:"+G+" X:"+str(X)+" D:"+str(D)+" g:"+g+" ix:"+ix+" id:"+id);
      if(X.length>0){
       //var m=[];for(k=g.length;k--;)if(Cart[g[k]].PRICE!=Cart[i].PRICE)m.push(g[k]);  //sanity-check prices across group, using simple string-comparisons
         var m=[];for(k=g.length;k--;)if(!pEQ(Cart[g[k]], Cart[i]))m.push(g[k]);        //sanity-check prices across group, comparing C,D,X to permit minor differences
         if(m.length>0) DEBUG("group:"+G+" has exact-qty discount but PRICE on row:"+i+" conflicts with rows:"+m);      //issue DEBUG alert
         if(m.length>0) {for(k=g.length; k--;) Cart[g[k]].PRICEAVG=C;  continue;}       //suppress further such DEBUG alerts for the other rows in group
      }
      var A, QQ, q2, I;
      if(K){                                                                    //---a Coupon-Discount, will be applied later (2008-03-06)---
         Cart[i].QUANTITY=1;                                                    //override quantity, forcing it to be one
         Cart[i].PRICEAVG=0;                                                    //set price-apiece to zero, until we find out if qualifications are met
         KK=i;
      }else if(ix!=-1){                                                         //---apply an exact-quantity discount, or both kinds, to all rows in group---
         A=0;  QQ=Q;
         while(Q!=0){                                                           //until all items in group are priced or no more applicable exact-discounts
            q2= Math.floor(Q / X[ix].q) * X[ix].q;                              //q2 is the largest multiple of X[ix].q thats less-than-or-equal-to Q
            A += q2*pp(X[ix].p);  Q-=q2;                                        //sell q2 at price X[ix].p, revising Q to reflect remaining qty
            DEBUG4("sell "+q2+" at:"+pp(X[ix].p)+" Q:"+Q);
            --ix; while(ix>=0 && X[ix].q>Q) --ix;                               //revise ix for the next applicable exact-discount, if any
            if(ix==-1)break                                                     //leave loop if none
            if(id!=-1 && pp(D[id].p)<pp(X[ix].p))break;                         //2008-03-01 leave if the n-or-more price beats next applicable exact-discount price
         }
         if(Q>0) A += Q * (id!=-1 ? pp(D[id].p) :C);                            //price the rest using the applicable n-or-more-price, or C if none
         var priceavg= Cents(A/QQ);                                             //2008-02-03: was Cents(A) / QQ
         for(k=g.length; k--;)  {I=g[k];  Cart[I].PRICEAVG = priceavg;}         //each row in group gets the same price apiece

      }else if(id!=-1){                                                         //---apply an n-or-more discount, to all rows in group---
         var ID, QD=0;                                                          //QD is the count of items already priced
         for(k=0; k<g.length; ++k){  I=g[k];                                    //for each row I in group do
            A=0; q=Cart[I].QUANTITY; C=Cart[I].C;  D=Cart[I].D;
            if(D.length==0 || D[0].q!=1)  D.unshift( {q:1, p:C} );              //augment D to include a price for the first one(s), to simplify search-loops, etc
            while(q>0){
               for(ID=0;;++ID) if(ID+1==D.length||QD+1<D[ID+1].q)break;         //now D[ID].p is the price for the QD+1'th (next) item
               q2=q;  if(ID+1<D.length) q2=Math.min(q, D[ID+1].q-1-QD);         //q2 is nbr of items to sell at price D[ID].p
               A+= q2*pp(D[ID].p);  QD+=q2;  q-=q2;                             //sell q2 items at D[ID].p, revising QD & q
               DEBUG4("sell "+q2+" at:"+pp(D[ID].p)+" ID:"+ID+" QD:"+QD);
               if(q2<=0) {DEBUG("ReadCartComputePrices is broken"); break;}
            }
            Cart[I].PRICEAVG = Cents(A / Cart[I].QUANTITY);                     //price apiece;  2008-02-03: was Cents(A) / Cart[I].QUANTITY
         }
      }else{                                                                    //---apply constant price to one row---
         Cart[i].PRICEAVG = Cents(C);                                           //price apiece;  2008-02-03: was C
      }
   }
   if(KK!=-1){                                                                  //---now apply a Coupon-Discount, if any (2008-03-06)---
      for(fTotal=0, i=1; i<=iNumberOrdered; ++i) fTotal+=Cart[i].QUANTITY*Cart[i].PRICEAVG;             //need the pre-tax pre-shipping subtotal, to see if applicable
      K=Cart[KK].K;
      if(fTotal>=K.min) Cart[KK].PRICEAVG= (K.pct ?fTotal*K.amt/100 : -Math.min(-K.amt,fTotal));        //if applicable, compute the (negative) amount of discount
      AllInOne=true;                                                                                    //2008-03-09 because Paypal cant handle negative price (Google?)
   }
   //====total, shipping, tax calculations====
   ZoneSelected=  iGetCookie("ZoneSelected");   ZoneChecked=ZoneSelected;                               //get zone cookie
   RegionSelected=iGetCookie("RegionSelected"); RegionChecked=RegionSelected;                           //ER: get region cookie
   if(ZoneSelected==null) ZoneSelected=ZoneDefault;                                                             //use zone-default if none selected;  ER: default was 8
   if(RegionFromZone.length && RegionSelected==null)       RegionSelected=RegionFromZone[ZoneSelected][0];      //ER: use RegionFromZone option to set RegionSelected
   if(RegionFromZoneOverrides)                             RegionSelected=RegionFromZone[ZoneSelected][0];
   if(ZoneChecked!=null && RegionFromZoneOvA[ZoneChecked]) RegionSelected=RegionFromZone[ZoneChecked] [0];
   if(RegionSelected==null)                                RegionSelected=RegionDefault;                //ER: use region-default, when RegionFromZone not being used
   if(RegionFromZone.length && !Element(RegionSelected,RegionFromZone[ZoneSelected])){                  //ER: validity-check, the Zone+Region combination is invalid:
      if(ZoneChecked!=null || RegionChecked==null){                                                     //ER: if user has picked Zone or neither, then revise Region
         RegionSelected=RegionFromZone[ZoneSelected][0];                                                //ER: revise RegionSelected to make it legal for Zone
      }else{                                                                                            //ER: otherwise, revise Zone to achieve consistency
         for(var Z=RegionFromZone.length;Z--;) if(Element(RegionSelected, RegionFromZone[Z])) break;    //ER: find a valid Zone Z
         if(Z>=0) ZoneSelected=Z;  else DEBUG("RegionFromZone option is invalid");                      //ER: revise ZoneSelected to make it legal for Region
      }
   }
   if(RegionChecked!=null) RegionChecked= RegionSelected;                                               //ER: validity-check RegionChecked
   if(ZoneChecked  !=null) ZoneChecked  = ZoneSelected;                                                 //ER: validity-check ZoneChecked
   if(RegionsUsed && RegionPrompt!="" && !RegionFromZoneOverrides && !(ZoneChecked!=null && RegionFromZoneOvA[ZoneChecked])
   ) {}                                                                                                 //ER: leave Region unchecked if user will have to pick
   else RegionChecked=RegionSelected;                                                                   //ER: show as checked a Region that suffices
   if(ShipTable.length>1 && ZonePrompt!="" && !(RegionChecked!=null && ZoneFromRegionOvA[RegionChecked])
   ) {}                                                                                                 //ER: leave Zone unchecked if user will have to pick
   else ZoneChecked=ZoneSelected;                                                                       //ER: show as checked a Zone that suffices
   //--ER: original version of above was ok for customer selecting Zone first, but would seriously harrass customer trying to select Region before Zone;
   // have fixed:  (2) revised validity-checking above;  and reordered, so setting Checked by PromptNotNeeded is done last, and for Zone after Region;
   // have fixed:  (3) NewZone routine now deletes Region-cookie if illegal;  NewRegion now deletes Zone-cookie if illegal;  (1) was to revise such cookie;
   //                  can continue to regard the presence of a Zone or Region cookie as proof the user has selected (and it hasn't been altered since);
   InitPkgQueue();                                                                      //ER: initialize the PkgQueue object (used to compute package-size)
   fTotal=0; fTaxA=[]; for(R=0;R<RegionTable.length;++R)fTaxA[R]=0; g_TotalQty=0;       //initialize subtotal and tax-subtotals
   var taxnbrs=TaxesByRegion[RegionSelected] || [];                                     //lookup which taxes apply based on RegionSelected
   var taxrate=[]; for(T=0;T<TaxRates.length;++T)taxrate[T]=0;
   for(N=0;N<taxnbrs.length;++N)taxrate[taxnbrs[N]]=TaxRates[taxnbrs[N]];               //ER: init taxrates based on RegionSelected
   for(i=1; i<=iNumberOrdered; ++i){
      //ER: considered using TaxableTotal etc, so as to multiply just once;  not essential, provided the rounding-to-cents is deferred  (tho for tax-included it isn't)
      var ProdID=Cart[i].ID, QP=Cart[i].QUANTITY*Cart[i].PRICEAVG, taxX=null; tax=[], taxsum=0;
      for(pref in TaxesByID) if(typeof TaxesByID[pref]!=="function") if(PrefEQ(ProdID,pref)) {taxX=TaxesByID[pref]; break;}     //is product subject to a tax-exception?
      if(taxX) {for(T=0;T<TaxRates.length;++T)tax[T]=0; for(K=taxX.length;K--;) {T=taxX[K]; tax[T]=QP*taxrate[T];}}             //if so, only taxes in taxX apply
      else     {for(T=0;T<TaxRates.length;++T)tax[T]=QP*taxrate[T];}                                                            //otherwise all taxes apply
      if(DisplayTaxIncluded){                                                           //ER: for tax-included pricing:
         for(T=0;T<TaxRates.length;++T) {tax[T]=Cents(tax[T]); taxsum+=tax[T];}  
		 
         Cart[i].PRICEAVG += taxsum / Cart[i].QUANTITY;
      }
      AddPkgQueueEntry(Cart[i].QUANTITY, new Size(Cart[i].LENGTH,Cart[i].WIDTH,Cart[i].HEIGHT), Cart[i].WEIGHT);        //ER: accumulate for package-size
      fTotal+=Cart[i].QUANTITY*Cart[i].PRICEAVG;                                        //accumulate fTotal, the subtotal before (tax)+shipping...
      for(T=0;T<TaxRates.length;++T) fTaxA[T]+=tax[T];                                  //accumulate tax-subtotals
      g_TotalQty+=Cart[i].QUANTITY;                                                     //accumulate total-quantity
   }
   ComputePackageSize(ZoneSelected);
   
    fShipping = 0;
	
   ppTotal=fTotal; ppShipping=fShipping; if(ppTotal==0) {ppTotal=moneyEps; ppShipping=Math.max(ppShipping-moneyEps,0);} //2008-02-09 kludge because PayPal chokes on zero
   for(T=0;T<TaxRates.length;++T) fTaxA[T]=Cents(fTaxA[T]);                             //ER: round to Cents separately
   fTax=0; if(!DisplayTaxIncluded) for(T=0;T<TaxRates.length;++T) fTax+=fTaxA[T];       //ER: fTax is sum of taxes, however with tax-included pricing use zero
   g_TotalCost = fTotal + fShipping + fTax;                                             //compute grand-total  ==Rename needed: fTotal->fSubTot, g_TotalCost->fTotal==
}


//------------------------------------------------------------------------
// FUNCTION: AddPaymentProcessorFieldsForOneRow
// RECEIVES: PP is the payment-processor-code;
//           i is the row-nbr, Cart[i] is the info about current row
// RETURNS: appends to global string vars  sOutPP and sDescAIO
// ER: made this a function to improve maintainability.
//------------------------------------------------------------------------
function AddPaymentProcessorFieldsForOneRow(PP, i){
   var sN="";     if(AppendItemNumToOutput) sN=""+i;                                    //ER: convert i to string once
   var SEP="\n";  if(PP=="ap"||PP=="pp") SEP="; ";                                      //2008-10-15 separator between items in the All-in-one-Description
   var ProdNAME = Cart[i].NAME;                                                         //2008-02-07 removed: +(Cart[i].ADDTLINFO ?"; "+Cart[i].ADDTLINFO :"")
   var Notes="";
   if(PP=="an"||PP=="wp"||PP=="lp"||PP=="ap")  AllInOne=true;                           //an/wp/lp/ap are always all-in-one
   if(PP=="gc")                                AllInOne=false;                          //gc+AllInOne is UNFINISHED
   if(AllInOne){                                                                        //===an/wp/lp/ap/pp+AllInOne===
      sDescAIO+= ""+Cart[i].QUANTITY +" x " + ProdNAME + " (£" + moneyFormat(Cart[i].PRICEAVG) + " each)" + (i<iNumberOrdered?SEP:"");  //format Description as: ID, NAME, Qty:QUANTITY<sep>  (an/wp/lp used to get an ending SEP)
   }
   ProdNAME+=Notes;
   if(PP=="pp" && (!AllInOne || i==iNumberOrdered)){                                    //===PayPal-cart-based OR PayPal-all-in-one AND last-item===
      var ppNAME= (AllInOne ?sDescAIO :ProdNAME);
      var ppNAME1=ppNAME.substring(0,127);                                              //ER: break into 3 strings according to PayPal limits
      var ppNAME2=ppNAME.substring(127,327);                                            //ER: break into 3 strings according to PayPal limits
      var ppNAME3=ppNAME.substring(327,527);                                            //ER: break into 3 strings according to PayPal limits
      var ppID=Cart[i].ID, ppPRICE=Cart[i].PRICEAVG, ppQUANTITY=Cart[i].QUANTITY;
      if(AllInOne) {ppID="AIO"; ppPRICE=ppTotal; ppQUANTITY=1;}                         //2008-03-09 for PayPal-all-in-one, supply the subtotal as the price
      if(AllInOne && AppendItemNumToOutput) sN=""+1;                                    //2008-03-09 for PayPal-all-in-one, use one as the row-number
      //sOutPP+=             "<input type=hidden name=\"item_number_"+sN+"\" value=\""+             ppID              + "\">";
      sOutPP+=             "<input type=hidden name=\"item_name_"  +sN+"\" value=\""+             ppNAME1           + "\">";
      sOutPP+=             "<input type=hidden name=\"amount_"     +sN+"\" value=\""+ moneyFormat(ppPRICE)          + "\">";
      sOutPP+=             "<input type=hidden name=\"quantity_"   +sN+"\" value=\""+             ppQUANTITY        + "\">";
      if(ppNAME2) sOutPP+= "<input type=hidden name=\"on0_"        +sN+"\" value=\""+             "Info2"           + "\">";
      if(ppNAME2) sOutPP+= "<input type=hidden name=\"os0_"        +sN+"\" value=\""+             ppNAME2           + "\">";
      if(ppNAME3) sOutPP+= "<input type=hidden name=\"on1_"        +sN+"\" value=\""+             "Info3"           + "\">";
      if(ppNAME3) sOutPP+= "<input type=hidden name=\"os1_"        +sN+"\" value=\""+             ppNAME3           + "\">";
   }else if(PP=="gc"){                                                                  //===Google-Checkout-cart-based===
      sOutPP+= "<input type=hidden name=\"item_name_"              +sN+"\" value=\""+             Cart[i].ID        + "\">";
      sOutPP+= "<input type=hidden name=\"item_description_"       +sN+"\" value=\""+             ProdNAME          + "\">";
      sOutPP+= "<input type=hidden name=\"item_price_"             +sN+"\" value=\""+ moneyFormat(Cart[i].PRICEAVG) + "\">";
      sOutPP+= "<input type=hidden name=\"item_quantity_"          +sN+"\" value=\""+             Cart[i].QUANTITY  + "\">";
      sOutPP+= "<input type=hidden name=\"item_currency_"          +sN+"\" value=\""+             gcCurrency        + "\">";
   }else if(PP=="cgi"){                                                                 //===CUSTOM-cgi-payment-processor===  was if(HiddenFieldsToCheckout)
      sOutPP+= "<input type=hidden name=\"" + OutputItemId         +sN+"\" value=\""+             Cart[i].ID        + "\">";
      sOutPP+= "<input type=hidden name=\"" + OutputItemQuantity   +sN+"\" value=\""+             Cart[i].QUANTITY  + "\">";
      sOutPP+= "<input type=hidden name=\"" + OutputItemPrice      +sN+"\" value=\""+ moneyFormat(Cart[i].PRICEAVG) + "\">";
      sOutPP+= "<input type=hidden name=\"" + OutputItemName       +sN+"\" value=\""+             Cart[i].NAME      + "\">";
      sOutPP+= "<input type=hidden name=\"" + OutputItemWeight     +sN+"\" value=\""+             Cart[i].WEIGHT    + "\">";
      sOutPP+= "<input type=hidden name=\"" + OutputItemLength     +sN+"\" value=\""+             Cart[i].LENGTH    + "\">";
      sOutPP+= "<input type=hidden name=\"" + OutputItemWidth      +sN+"\" value=\""+             Cart[i].WIDTH     + "\">";
      sOutPP+= "<input type=hidden name=\"" + OutputItemHeight     +sN+"\" value=\""+             Cart[i].HEIGHT    + "\">";
      //utPP+= "<input type=hidden name=\"" + OutputItemAddtlInfo  +sN+"\" value=\""+             Cart[i].ADDTLINFO + "\">";    //yanked 2008-02-07
   }
}
//------------------------------------------------------------------------
// FUNCTION: AddPaymentProcessorFieldsFinal
// RECEIVES: PP is the payment-processor-code;  global var sDescAIO is the all-in-one-description
// RETURNS: appends to global string var  sOutPP
// ER: made this a function to improve maintainability.
//------------------------------------------------------------------------
function AddPaymentProcessorFieldsFinal(PP){
   if(PP=="an"){                                        //===an:Authorize.net WebConnect===
      sOutPP += "<input type=hidden name=\"x_Version\"       value=\"3.0\">";
      sOutPP += "<input type=hidden name=\"x_Show_Form\"     value=\"PAYMENT_FORM\">";
      sOutPP += "<input type=hidden name=\"x_Description\"   value=\""+ sDescAIO + "\">";
      sOutPP += "<input type=hidden name=\"x_Amount\"        value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";
   }else if(PP=="wp"){                                  //===wp:WorldPay===
      sOutPP += "<input type=hidden name=\"desc\"            value=\""+ sDescAIO + "\">";
      sOutPP += "<input type=hidden name=\"amount\"          value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";
   }else if(PP=="lp"){                                  //===lp:LinkPoint===
      sOutPP += "<input type=hidden name=\"mode\"            value=\"fullpay\">";
      sOutPP += "<input type=hidden name=\"chargetotal\"     value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";
      sOutPP += "<input type=hidden name=\"tax\"             value=\""+ MoneySymbol + moneyFormat(fTax) + "\">";
      sOutPP += "<input type=hidden name=\"subtotal\"        value=\""+ MoneySymbol + moneyFormat(fTotal) + "\">";
      sOutPP += "<input type=hidden name=\"shipping\"        value=\""+ MoneySymbol + moneyFormat(fShipping) + "\">";
      sOutPP += "<input type=hidden name=\"desc\"            value=\""+ sDescAIO + "\">";
   }else if(PP=="ap"){                                  //===ap:AlertPay (2008-10-15)===
      sOutPP += "<input type=hidden name=\"ap_purchasetype\"    value=\""+ "item"   + "\">";
      sOutPP += "<input type=hidden name=\"ap_itemname\"        value=\""+ "cart"   + "\">";
      sOutPP += "<input type=hidden name=\"ap_description\"     value=\""+ sDescAIO + "\">";
      sOutPP += "<input type=hidden name=\"ap_quantity\"        value=\""+ "1"      + "\">";
      sOutPP += "<input type=hidden name=\"ap_amount\"          value=\""+ moneyFormat(fTotal)    + "\">";
      sOutPP += "<input type=hidden name=\"ap_shippingcharges\" value=\""+ moneyFormat(fShipping) + "\">";
      sOutPP += "<input type=hidden name=\"ap_taxamount\"       value=\""+ moneyFormat(fTax)      + "\">";
      sOutPP += "<input type=hidden name=\"ap_totalamount\"     value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";
        //field apc_1..apc_6  each limited to 100-chars
        //field ap_description -- no info available on max-length => need to test a long value...
        //field ap_discountamount - may be useful to get around negative-amount kluges?
        //NOTE: fields ap_currency, ap_merchant must be supplied in the view-cart page
   }else if(PP=="pp"){                                  //===pp:PayPal (cart-based OR all-in-one)===
      //var ShpTaxNotes="Shipping+Tax-Notes: "+ShipTable[ZoneSelected].zone+" "+sComputeShippingNote+(RegionsUsed?" "+RegionTable[RegionSelected]:"");  //==OBSOLETE
      sOutPP += "<input type=hidden name=\"cmd\"             value=\"_cart\">";                         //2008-02-05: new
      sOutPP += "<input type=hidden name=\"upload\"          value=\"1\">";                             //2008-02-05: new
      //utPP += "<input type=hidden name=\"custom\"          value=\""+ ShpTaxNotes             +"\">"; //shipping+tax-notes via "custom" don't show on emails ==OBSOLETE
      sOutPP += "<input type=hidden name=\"tax_cart\"        value=\""+ moneyFormat(fTax)       +"\">";
      sOutPP += "<input type=hidden name=\"handling_cart\"   value=\""+ moneyFormat(ppShipping) +"\">"; //2008-02-09 ppShipping for the zero-subtotal kludge
      sOutPP += "<input type=hidden name=\"no_note\"         value=\""+ "1"                     +"\">"; //use NO_NOTE until PayPal fixes NOTE/SpecialInstructions-support
   }else if(PP=="gc"){                                  //===gc:GoogleCheckout===
      if(fTax!=0){                                                              //Google forces us to supply TAX as an item...
        var sN=""+(iNumberOrdered+1);                                           //get next row-number
        sOutPP+="<input type=hidden name=\"item_name_"       +sN+"\" value=\""+ "TAX"                                                           + "\">";
        sOutPP+="<input type=hidden name=\"item_description_"+sN+"\" value=\""+ "Tax"+(RegionsUsed?" for "+RegionTable[RegionSelected]:"")      + "\">";
        sOutPP+="<input type=hidden name=\"item_price_"      +sN+"\" value=\""+ moneyFormat(fTax)                                               + "\">";
        sOutPP+="<input type=hidden name=\"item_quantity_"   +sN+"\" value=\""+ "1"                                                             + "\">";
        sOutPP+="<input type=hidden name=\"item_currency_"   +sN+"\" value=\""+ gcCurrency                                                      + "\">";
      }
      sOutPP += "<input type=hidden name=\"ship_method_price_1\"     value=\""+ moneyFormat(fShipping)                                          + "\">";
      sOutPP += "<input type=hidden name=\"ship_method_currency_1\"  value=\""+ gcCurrency                                                      + "\">";
      sOutPP += "<input type=hidden name=\"ship_method_name_1\"      value=\""+ ShipTable[ZoneSelected].zone+" "+sComputeShippingNote           + "\">";
      sOutPP += "<input type=hidden name=\"_charset_\"/>";                      //Google says this is required, though no alternatives(?)
   }else if(PP=="cgi"){                                 //===cgi:CUSTOM===      //was if(HiddenFieldsToCheckout)
      sOutPP += "<input type=hidden name=\""+OutputOrderSubtotal+"\" value=\""+ MoneySymbol + moneyFormat(fTotal)                               + "\">";
      sOutPP += "<input type=hidden name=\""+OutputOrderShipping+"\" value=\""+ MoneySymbol + moneyFormat(fShipping)                            + "\">";
      sOutPP += "<input type=hidden name=\""+OutputOrderTax     +"\" value=\""+ MoneySymbol + moneyFormat(fTax)                                 + "\">";
      sOutPP += "<input type=hidden name=\""+OutputOrderTotal   +"\" value=\""+ MoneySymbol + moneyFormat(fTotal + fShipping + fTax)            + "\">";
      sOutPP += "<input type=hidden name=\""+OutputOrderZone    +"\" value=\""+ ShipTable[ZoneSelected].zone+" "+sComputeShippingNote           + "\">";
      sOutPP += "<input type=hidden name=\""+OutputOrderRegion  +"\" value=\""+ (RegionsUsed?RegionTable[RegionSelected]:"")                    + "\">";  //ER: new
   }
   DEBUG8(sOutPP);                                      //2008-03-09 separate sOutput and sOutPP makes for nicer debugging
}
//------------------------------------------------------------------------
// FUNCTION: AddTaxSubtotalLines
// RECEIVES: INC string - is either empty-string or strINCLUDEDINTOTAL
// RETURNS: appends to global var sOutput
// ER: made this a function to improve maintainability.
//------------------------------------------------------------------------
function AddTaxSubtotalLines(INC,COL,BEG,END){  if(COL==null)COL=6;  if(BEG==null)BEG="<B>";  if(END==null)END="</B>";
   if(TaxNames.length>=2){
      for(T=0;T<TaxRates.length;++T) if(fTaxA[T]) sOutput += 
         "<TR><TD CLASS=\"noptotal\" COLSPAN="+COL+">"+BEG+strTAX+"-"+TaxNames[T]+INC+"&nbsp; "+(RegionsUsed?RegionTable[RegionSelected]:"")+END+"</TD>" +
         "<TD CLASS=\"noptotal\" ALIGN=RIGHT>"+BEG + MoneySymbol + moneyFormat(fTaxA[T]) +END+"</TD></TR>";
   }else{
      var fTaxAsum=0;  for(T=0;T<TaxRates.length;++T) fTaxAsum+=fTaxA[T];
      if(fTaxAsum) sOutput += 
         "<TR><TD CLASS=\"noptotal\" COLSPAN="+COL+">"+BEG+strTAX+INC+"&nbsp; "+(RegionsUsed?RegionTable[RegionSelected]:"")+END+"</TD>" +
         "<TD CLASS=\"noptotal\" ALIGN=RIGHT>"+BEG + MoneySymbol + moneyFormat(fTaxAsum) +END+"</TD></TR>";
   }
}


//------------------------------------------------------------------------
// FUNCTION: ManageCart
// PARAMETERS: Null
// PURPOSE: Draw current cart product table on HTML page
//------------------------------------------------------------------------
function ManageCart() {
   //ER: replaced  fWeight-->PkgAsOne.weight;  sTotal-->moneyFormat(fTotal);  sTax-->moneyFormat(fTax);  sShipping-->moneyFormat(fShipping);
   //ER: iNumberOrdered, ZoneSelected, ZoneChecked, RegionSelected, RegionChecked  are now global vars set by ReadCartComputePrices routine;
   //ER: fTotal, fShipping, fTax, fTaxA, g_TotalCost                               are now global vars set by ReadCartComputePrices routine;
   //
   var MoreState=iGetCookie("MoreState"); if(MoreState==null) MoreState= (DisplayWtColumn?1:0)*2 + (DisplaySzColumn?1:0);       //ER: MoreState, for DynamicWtSzColumns
   ReadCartComputePrices();     //ER: new
   sDescAIO="";                 //initialize the All-in-one-Description for cart-less payment-processors
   sOutPP="";
   sOutput = "<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 CLASS=\"nopcart\" WIDTH=617><TR>" +
      "<TD CLASS=\"nopheader\" width=475>"+strDLabel+"</TD>" +
      "<TD CLASS=\"nopheader\" width=64 ALIGN=CENTER>"+strQLabel+"</TD>" +
      "<TD CLASS=\"nopheader\" width=78 ALIGN=CENTER>"+strPLabel+"</TD>" +
      "<TD CLASS=\"nopheader\" width=17 ALIGN=CENTER><strong>"+strRLabel+"</strong></TD></TR>";
   if(iNumberOrdered==0)sOutput+="<TR><TD COLSPAN=6 CLASS=\"nopentry\"><CENTER><BR><strong>"+strCartEmpty+"</strong><BR><BR></CENTER></TD></TR>"; //ER: now subject to translation
   for(var i=1; i<=iNumberOrdered; ++i){
      var sCLASS="nopentry"; if(Math.round(i/2)==(i/2)) sCLASS="nopeven";               //ER: to eliminate duplication of code for even/odd background on rows
      sOutput += "<TR>";
      sOutput += "<TD><table border=0 cellpadding=0 cellspacing=0><tr><td width=45 CLASS=\"nobord\"><img src=\"/prod_images/thumb_" + Cart[i].THUMB + ".jpg\" CLASS=\"cart_thumb\"></td><td width=365 CLASS=\"nobord\"> " + Cart[i].NAME +  "</td></tr></table></TD>";
      if(DisplayChangeQty)      sOutput += "<TD ALIGN=CENTER><INPUT TYPE=TEXT NAME=Q SIZE=2 CLASS=\"qtybox\" VALUE=\"" + Cart[i].QUANTITY + "\" onChange=\"ChangeQuantity("+i+", this.value);\"></TD>";
      else                      sOutput += "<TD ALIGN=CENTER>" + Cart[i].QUANTITY + "</TD>";
      sOutput += "<TD CLASS=\"pricebg\" ALIGN=CENTER>"+ MoneySymbol + moneyFormat(Cart[i].PRICEAVG)+strEA+"</TD>";  
      sOutput += "<TD ALIGN=RIGHT> <input type=button value=\""+strRButton+"\" onClick=\"RemoveFromCart("+i+")\" class=\"nopbutton\"></TD>"; 
      sOutput += "</TR>";
      AddPaymentProcessorFieldsForOneRow(PaymentProcessor, i); 
   }
   sOutput += "<TR CLASS=\"totcol\"><TD>&nbsp;</TD>";
   sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><strong>"+strSUB+"</strong></TD>";
   sOutput += "<TD CLASS=\"pricebg\" COLSPAN=1 ALIGN=CENTER><strong>" + MoneySymbol + moneyFormat(fTotal) + "</strong></TD>";
   sOutput += "<TD>&nbsp;</TD></TR>";
   
   if(DisplayPkgAttrRow && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){   //ER: new option controls this line showing + omit if weightless & sizeless
      var MoreLessButton=(MoreState==DynamicWtSzColumns?strLButton:strMButton);         //ER: initialize according to MoreState
      var bW=MoreState&2, bw=bW^2, sW="&nbsp; " +PkgAsOne.weight+WTUNITS;               //ER: total package WEIGHT will go in WEIGHT-col, or in 1st
      var bS=MoreState&1, bs=bS^1, sS="&nbsp; " +SizeStr(PkgAsOne.size)+SZUNITS;        //ER: total package SIZE will go in SIZE-col, or in 1st
      if(DynamicWtSzColumns) {bw&=DynamicWtSzColumns; bs&=DynamicWtSzColumns;}          //ER: only show in first-col if ever shown  (Wt-only but no Dynamic-cols??)
      sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=3><strong>"+strWTSZTOT+(bW?"":sW)+(bS?"":sS)+"</strong></TD>";       //ER: moved this row, chged CLASS, etc
      sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><strong>" + (bW?sW:"") +"</strong></TD>";                            //ER: show package weight
      sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><strong>" + (bS?sS:"") +"</strong></TD>";                            //ER: show package size
      if(DynamicWtSzColumns)  sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT>&nbsp; <input type=button value=\""+MoreLessButton+"\" onClick=\"MoreLessInfo()\" class=\"nopbutton\"></TD>";      //ER: new
      else                    sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT></TD>";
      sOutput += "</TR>";
   }
   //Display the Shipping-Zone choices;  ER: Zone Defns are now table-driven; see description up front
   //ER: note: show no button as checked if user will be prompted for Zone (see the setting of ZoneChecked above)
   if(ShipTable.length>1 && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){  //ER: if more than one zone then customer must be able to pick
      sOutput += "<TR CLASS=\"delcol\"><TD CLASS=\"nopship\" COLSPAN=2 ALIGN=RIGHT><strong>"+strSHIPPINGZONE+"</strong></TD>";           //ER: was:ALIGN=CENTER  was:"UPS<BR>SHIPPING<BR>ZONE" now subject to translation
      sOutput += "<TD CLASS=\"nopship\" COLSPAN=2>";
      for(var z=0; z<ShipTable.length; z++) sOutput+= "<input type=radio name=\"ZONE\" CLASS=\"radbutt\" value=\""+z+"\"" +
         (z==ZoneChecked?" checked":"") +                                               //ER: now adding the "checked" attrib here
         " onClick=\"NewZone(this.value)\">"+ShipTable[z].zone+"<br>";                  //ER: ComputeShipping-->NewZone  in onClick
      sOutput += "</TD>"; 
		sOutput += "</TR>";
   }
   if(DisplayShippingRow && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){
	sOutput += "<TR CLASS=\"postcol\"><TD CLASS=\"noptotal\" COLSPAN=2 ALIGN=RIGHT><strong>" + strSHIP+"&nbsp; " + ShipTable[ZoneSelected].zone+"</strong>" + sComputeShippingNote +"</TD>";
	sOutput += "<TD CLASS=\"pricebg\" ALIGN=CENTER><strong>" + MoneySymbol + moneyFormat(fShipping) + "</strong></TD>";
	sOutput += "<TD>&nbsp;</TD></TR>";
   }
   //ER: have overhauled the way the Region-choices are shown & how changes are handled;  now very similar to the Zone-choices...
   //ER: note: RegionFromZoneOverrides means Region is always derived from Zone, so no user-choosing needed
   //ER: note: show no button as checked if user will be prompted for region (see the setting of RegionChecked above)
   /*if(RegionsUsed && !RegionFromZoneOverrides && iNumberOrdered){                       //ER: difference iNumberOrdered!=0 vs iNumberOrdered was a mystery...
      sOutput += "<TR><TD CLASS=\"nopship\"><B>"+strTAXABLEREGION+"</B></TD>";          //ER: was:ALIGN=CENTER
      sOutput += "<TD CLASS=\"nopship\" COLSPAN=6>";
      for(var R=0; R<RegionTable.length; R++) sOutput+= "<input type=radio name=\"TAX\" value=\""+R+"\"" +
         (R==RegionChecked?" checked":"") +                                             //ER: add the checked attrib
         " onClick=\"NewRegion(this.value)\">"+RegionTable[R]+"<br>";
      sOutput += "</TD></TR>";
   }*/
   if(DisplayTaxRow && iNumberOrdered && !DisplayTaxIncluded){                          //ER: show tax line(s) for NON-tax-included-pricing
      AddTaxSubtotalLines("");
   }
   sOutput += "<TR CLASS=\"totcol\"><TD>&nbsp;</TD>";            //ER: now show the TOTAL line even when TaxRateRegional!=0
   sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><strong>"+strTOT+"</strong></TD>";
   sOutput += "<TD CLASS=\"totbg\" ALIGN=CENTER><strong>" + MoneySymbol + moneyFormat(fTotal + fShipping + fTax) + "</strong></TD>";
   sOutput += "<TD>&nbsp;</TD></TR>";
   if(DisplayTaxRow && iNumberOrdered && DisplayTaxIncluded){                           //ER: show tax line(s) for TAX-INCLUDED-pricing (new)
      AddTaxSubtotalLines(" "+strINCLUDEDINTOTAL, 6, "<i>", "</i>");
   }
   if(DisplayTaxRow && gVat && RegionSelected<=SameCountry&&ShipTaxName!="")sOutput +=  //ER: show tax included in the shipping-charge, to customer in same country
      "<TR><TD CLASS=\"noptotal\" COLSPAN=3><i>" + ShipTaxName +"</i></TD>" + 
      "<TD CLASS=\"noptotal\" ALIGN=RIGHT>" + MoneySymbol+moneyFormat(gVat) +"</TD></TR>";
   sOutput += "</TABLE>";
   //
   //--ER: ManageCart producing PaymentProcessor-style hidden-fields is new (to enable ONE-step checkout);  the NopDesign version only offers "cgi" style here
   AddPaymentProcessorFieldsFinal(PaymentProcessor);                                    //ER: add the final (cart-wide) payment-processor fields
   document.write(sOutput+sOutPP);
   document.close();
}

//------------------------------------------------------------------------
// FUNCTION:    ValidateCart
// PARAMETERS:  Form to validate
// RETURNS:     true/false
// PURPOSE:     Validate the managecart form
//------------------------------------------------------------------------
function ValidateCart(theForm){
   if(isNaN(g_TotalCost)){
      alert(strTotalNaN);               //ER: was NoQtyPrompt
      return false;
   }
   if(g_TotalCost < MinimumOrder){
      alert(MinimumOrderPrompt);
      return false;
   }
   //ER: because of my defaults, now need to use the presence of cookies to tell whether user has made a Zone or Region selection;
   //ER: 1st test was: !RadioChecked(theForm.ZONE)
   //ER: 2nd test was: !RadioChecked(theForm.TAX)  -- actually it was: !RadioChecked(eval("theForm."+OutputOrderTax))  before simplifying OutputOrderTax-->"TAX"
   var N=iGetCookie("NumberOrdered",0);  if(N==0) return;               //skip the following if cart is empty;  only needed for the perverse use of MinimumOrder==0
   var ZoneCookie=   iGetCookie("ZoneSelected");
   var RegionCookie= iGetCookie("RegionSelected");
   if(ZoneCookie==null && (PkgAsOne.weight+PkgAsOne.size.height) && ShipTable.length>1 && ZonePrompt!="" && !(RegionCookie!=null && ZoneFromRegionOvA[RegionCookie])){
      alert(ZonePrompt);
      return false;
   }
   if(RegionCookie==null && RegionsUsed && RegionPrompt!="" && !RegionFromZoneOverrides && !(ZoneCookie!=null && RegionFromZoneOvA[ZoneCookie])){
      alert(RegionPrompt);
      return false;
   }
   return true;
}

//------------------------------------------------------------------------
// FUNCTION: CheckoutCart
// PARAMETERS: Null
// PURPOSE: Draw current cart product table on HTML page for checkout;
// NOTE: produces a simpler view of the cart, compared to ManageCart, 
// and one without controls (Remove-from-Cart etc).
//------------------------------------------------------------------------
function CheckoutCart() {
   ReadCartComputePrices();     //ER: new
   sDescAIO="";                 //initialize the all-in-one-Description for cart-less payment-processors
   sOutPP="";
   sOutput = "<TABLE ALIGN=CENTER CELLSPACING=0 CELLPADDING=0 BORDER=0 CLASS=\"nopcart\" WIDTH=617><TR>" +
         "<TD CLASS=\"nopheader\" width=475>"+strDLabel+"</TD>" +
      "<TD CLASS=\"nopheader\" width=64 ALIGN=CENTER>"+strQLabel+"</TD>" +
      "<TD CLASS=\"nopheader\" width=78 ALIGN=RIGHT>"+strPLabel+"</TD>" +
      "<TD CLASS=\"nopheader\" width=17 ALIGN=CENTER><strong>"+strRLabel+"</strong></TD></TR>";         //ER: strALabel now shown unconditionally
   for(var i=1; i<=iNumberOrdered; ++i){
      var sCLASS="nopentry"; if(Math.round(i/2)==(i/2)) sCLASS="nopeven";               //ER: to eliminate duplication of code for even/odd background on rows
      sOutput += "<TR><TD>" + Cart[i].NAME + "</TD>";
      sOutput+="<TD ALIGN=CENTER>" + Cart[i].QUANTITY + "</TD>";
      sOutput+="<TD ALIGN=RIGHT>"+MoneySymbol+moneyFormat(Cart[i].PRICEAVG)+strEA+"</TD>";                 //ER: "/ea" now subject to translation
      sOutput+="<TD ALIGN=RIGHT>"+MoneySymbol+moneyFormat(Cart[i].QUANTITY*Cart[i].PRICEAVG)+"</TD></TR>"; //ER: now shown unconditionally
      AddPaymentProcessorFieldsForOneRow(PaymentProcessor2, i);                         //ER: add payment-processor form-fields for row-i
   }
   sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=3><B>"+strSUB+"</B></TD>";
   sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal) + "</B></TD></TR>";
   if(DisplayShippingRow){
      //sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strWTSZTOT+"</B></TD>";
      //sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + fWeight + WTUNITS + "</B></TD>";
      sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=3><B>" + strSHIP+"&nbsp; "+ShipTable[ZoneSelected].zone+"</B></TD>";        //ER: removed "for"
      sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fShipping) + "</B></TD></TR>";
   }
   if(DisplayTaxRow && !DisplayTaxIncluded){                                            //ER: removed ||RegionsUsed;  now only for NON-tax-included pricing
      AddTaxSubtotalLines("", 4);
   }
   sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=3><B>"+strTOT+"</B></TD>";
   sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal + fShipping + fTax) + "</B></TD></TR>";
   if(DisplayTaxRow && DisplayTaxIncluded){                                             //ER: show tax line(s) for TAX-INCLUDED-pricing (new)
      AddTaxSubtotalLines(" "+strINCLUDEDINTOTAL, 4, "<i>", "</i>");
   }
   if(DisplayTaxRow && gVat && RegionSelected<=SameCountry&&ShipTaxName!="")sOutput +=  //ER: show tax included in the shipping-charge, to customer in same country
      "<TR><TD CLASS=\"noptotal\" COLSPAN=3><i>" + ShipTaxName +"</i></TD>" + 
      "<TD CLASS=\"noptotal\" ALIGN=RIGHT>" + MoneySymbol+moneyFormat(gVat) +"</TD></TR>";
   sOutput+= "</TABLE>";
   AddPaymentProcessorFieldsFinal(PaymentProcessor2);                                   //ER: add the final (cart-wide) payment-processor fields
   document.write(sOutput+sOutPP);
   document.close();
}

//------------------------------------------------------------------------
// FUNCTION: Cart_is_empty
// RETURNS: true/false
//------------------------------------------------------------------------
function Cart_is_empty(){
   iNumberOrdered=iGetCookie("NumberOrdered",0);                //get the nbr-rows-in-cart cookie
   return iNumberOrdered==0;
}
//------------------------------------------------------------------------
// FUNCTION: Print_total
// PARAMETERS: none
// PURPOSE: Display cost currently racked up by shopper, on the HTML page
//------------------------------------------------------------------------
function Print_total(){
   ReadCartComputePrices();
   document.write(moneyFormat(fTotal));
}
//------------------------------------------------------------------------
// FUNCTION: Print_number_items
// PARAMETER: true/false - true to get "item"/"items" appended;  use false in any non-English application
// PURPOSE: Display number of items currently racked up by shopper, on the HTML page
//------------------------------------------------------------------------
function Print_number_items(Verbose){
   ReadCartComputePrices();
   sOutput= "" + g_TotalQty;
   if(Verbose) sOutput+= (g_TotalQty==1?" item":" items");
   document.write(sOutput);
}
Print_total_products = Print_number_items;      //support the old name
//------------------------------------------------------------------------
// FUNCTION: Print_cart_summary
// PARAMETERS: strings to follow QTY singular, follow QTY plural, precede AMT;  may be omitted in an English application
// PURPOSE: Display number of items in the cart and their total cost, on the HTML page
//------------------------------------------------------------------------
function Print_cart_summary(B1,B2,C){  if(B1==null)B1=" item"; if(B2==null)B2=" items"; if(C==null)C=", at a cost of ";
   ReadCartComputePrices();
   sOutput= "" + g_TotalQty + (g_TotalQty==1?B1:B2) + C + MoneySymbol+moneyFormat(fTotal);
   document.write(sOutput);
}
//========================================================================
// END NopDesign + ER Shopping-Cart
//========================================================================
