//
// Javascript and accompanying CSS and HTML code (in .html files) for:
// - offering user a UI to set Thai font family and size,
//   including code to detect installed Thai fonts
// - offering user a UI to choose among Thai pronunciation guides
//   which update instantly and automatically
// - automatically detecting and solving Thai bare vowel
//   display problems that happen with various Thai fonts
//   (e.g. the so-called "Vista dotted circle problem,"
//   though it also occurs under XP with some fonts)
// - presenting spectrogram images and sound players aligned
//   for proper synchronized playback
// - having a page with hundreds of Flash sound player buttons
//   with minimal CPU or memory load
//
// If you use this code, in part or in full, you must include a prominent
// notice to that effect in the end-user-visible portion of your website
// or application (or your API documentation, for libraries only) that
// includes a link to:
//
//    http://slice-of-thai.com/
//
// along with the other information (such as the copyright statement)
// required by the GNU Affero General Public License.
//
// Copyright 2008 Chris Pirazzi chris@pirazzi.net slice-of-thai.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You can find a copy of the GNU Affero General Public License
// at <http://lurkertech.com/agpl-3.0.txt> and also at
// <http://www.gnu.org/licenses/>.
//
// If you use this code, please host your own copy of this file
// rather than cross-linking to slice-of-thai.com.  The copy on
// slice-of-thai.com may be removed, renamed or modified at any
// time.
//

var user_agent = navigator.userAgent.toLowerCase();
var is_msie = (user_agent.indexOf("msie") != -1) && 
              (user_agent.indexOf("opera") == -1);

function ei(obj) { return document.getElementById(obj); }
function get_class_style(c) 
{
    for (var s = 0; s < document.styleSheets.length; s++)
    {
        if (document.styleSheets[s].rules)
        {
            for (var r = 0; r < document.styleSheets[s].rules.length; r++)
            {
                if (document.styleSheets[s].rules[r].selectorText == 
                    '.' + c)
                {
                    return document.styleSheets[s].rules[r];
                }
            }
        }
        else if (document.styleSheets[s].cssRules)
        {
            for (var r = 0; r < document.styleSheets[s].cssRules.length; r++)
            {
                if (document.styleSheets[s].cssRules[r].selectorText == 
                    '.' + c)
                    return document.styleSheets[s].cssRules[r];
            }
        }
    }
    
    return null;
}
function get_cookie(cookie_name)
{
    var results = document.cookie.match('(^|;) ?' + 
                                        cookie_name + 
                                        '=([^;]*)(;|$)');

    if (results)
        return (unescape(results[2]));
    else
        return null;
}
function set_cookie(name, value, exp_y, exp_m, exp_d, path, domain, secure)
{
    var cookie_string = name + "=" + escape ( value );

    if (exp_y)
    {
        var expires = new Date(exp_y, exp_m, exp_d);
        cookie_string += "; expires=" + expires.toGMTString();
    }

    if (path)
        cookie_string += "; path=" + escape(path);
    
    if (domain)
        cookie_string += "; domain=" + escape(domain);

    if (secure)
        cookie_string += "; secure";
    
    document.cookie = cookie_string;
}

function assert(val)
{
    if (!val)
    {
        
        alert('assertion failure');
    }
}

function is_dom_node(obj)
{
    return ('object' == typeof(obj)) && ('nextSibling' in obj);
}

// apply specified function to arg and return result
function appl(func, x)
{
    return func(x);
}

// make DOM text node
//
function telt(text)
{
    return document.createTextNode(text);
}

// you can call the function two ways:
//    elt(tag, attrs,       kids)
//    elt(tag, attrs, func, kids)
// - attrs must be an object.  it cannot be undefined.
// - in the second form, func must be defined and it must
//   be a function object.
//
// either way, this function:
// - creates DOM object obj with specified tag
// - assigns HTML attributes (not javascript properties)
//   specified by the javascript object attrs 
//   to assign onclick() handlers that are functions,
//   you must use func() instead
// - initializes obj._tips to {}
// - if kids is defined,
//   - if kids is not an array, it is replaced with [kids]
//   - foreach kid
//     - if kid is not a DOM node, it is made into a text node.
//     - otherwise kid must be a DOM node already
//     - place kid underneath new obj
//     - if kid._tips is defined, then 
//       - foreach property x of kid._tips, set obj._tips.x = kid._tips.x
// - if func is defined (and if you specify it, it must be defined), 
//   calls func(obj)
// - returns obj
// 
// you can implement "synthesized tips," which bubble up from children
// to parent, by setting fields of obj._tips like so:
//
//       elt('tr', attrs,
//           function(obj) { obj._tips.foobar = 10; },
//           kids);
//
// by default, the tips of the kids are merged into the tips
// of the parent as shown above.  if you need to combine the
// tips in another way, you can do it like so:
//
//       elt('tr', attrs
//           function(obj) 
//           {
//               obj._tips.baz = 
//                   obj.childNodes.map(function(kid) 
//                                      {
//                                          return kid._tips.baz; 
//                                      });
//           },
//           kids);
//
// you can implement "inherited tips," which trickle down from parent
// to child, by using the function get_itip() below.
//
function elt()
{
    var args = Array.prototype.slice.call(arguments);

    var tag = args.shift();
    assert('string' == typeof(tag));
    var attrs = args.shift();
    assert(undefined !== attrs &&
           'object' == typeof(attrs));
    var func = (args.length > 0 &&
                'function' == typeof(args[0])) ? args.shift() : undefined;
    assert(undefined === func ||
           'function' == typeof(func));
    var kids = args.shift();
    assert(0 === args.length);

    if (is_msie)
    {
        // horrifically awful IE -- pathetic browser, coded by blind monkeys.
        // cannot set NAME attribute except by doing it in this non-standard,
        // ridiculous way (see MSDN for NAME and for createElement):
        if (attrs.hasOwnProperty('name'))
        {
            tag = '<' + tag + ' name="' + attrs.name + '">';
        }
    }
    var obj = document.createElement(tag);
    for (var attr in attrs)
    {
        // see http://yuiblog.com/blog/2006/09/26/for-in-intrigue/
        if (!attrs.hasOwnProperty(attr))
            continue;
        
        obj.setAttribute(attr, attrs[attr], 0); // 0 for IE: case-insensitive
    }

    obj._tips = {};
    
    if (undefined !== kids)
    {
        if (!(kids instanceof Array))
        {
            kids = [ kids ];
        }
        assert(kids instanceof Array);
        for (var kid_idx=0; kid_idx < kids.length; kid_idx++)
        {
            var kid = kids[kid_idx];
            assert(undefined !== kid);
            if (!is_dom_node(kid))
            {
                assert('string' == typeof(kid) ||
                       'number' == typeof(kid));

                kid = document.createTextNode(kid);
            }
            assert('object' == typeof(kid));
            assert(is_dom_node(kid));
            obj.appendChild(kid);
            if (undefined !== kid._tips)
            {
                for (var prop in kid._tips)
                {
                    // see http://yuiblog.com/blog/2006/09/26/for-in-intrigue/
                    if (!kid._tips.hasOwnProperty(prop))
                        continue;
                    
                    obj._tips[prop] = kid._tips[prop];
                }
            }
        }
    }
    
    if (undefined !== func)
    {
        func(obj);
    }

    return obj;
}

//
// you can implement "inherited tips," which trickle down from parent
// to child, by using this function.
//
// - keep in mind that we build the DOM tree from the bottom up, so
// you can only use get_itip() once the parent DOM node has also been
// constructed and connected to the child.
//
// - also keep in mind this function is linear in the number
// of levels from obj to the DOM object with the property.
// XXX could do caching if needed.
//
// for each node from obj up its parent chain,
//   if node._tips[prop] exists, returns that
// else ASSERTION FAILS.
//
function get_itip(obj, prop)
{
    while (undefined !== obj)
    {
        if (obj.hasOwnProperty('_tips') &&
            obj._tips.hasOwnProperty(prop))
        {
            return obj._tips[prop];
        }
        
        obj = obj.parentNode;
    }
    assert(0);
}


// Usable embedded sound players:
//
// Here are some free resources and techniques I discovered, as well as
// a bit of workaround code, for putting easy-to-use sound players
// in my web page.  I hope this information and code will be of use
// to others.
// 
// Initially I just served up bare links to .mp3/.wma/... files on my
// pages.  But this did something different, and usually unintuitive, in
// every browser, operating system, and computer I tried, which made it
// very hard for users (especially novices) to actually hear the sound.
// 
// - Sometimes, upon clicking a link, the browser would bring up Windows
// Media Player, taking 15-45 seconds before I hear anything as Windows
// Media player wasted time checking for network updates, licenses, and
// whatever the hell else it does.  
// 
// - Sometimes, the browser would bring up a "save as" dialog because it
// didn't know how to play the file.  
// 
// - Sometimes, the browser would bring up Winamp or another player which
// had stolen the MIME-type, and in some cases that player would NOT
// actually be able to play the audio at all!  
// 
// This combination of unintuitive behavior and outright failure will, I
// think, severely cut down the number of website users who will actually
// use the audio clips.
// 
// I reluctantly concluded that the only usable sound playback interface
// available to us with wide support is Flash.  Most of the Flash players
// I found are complete crap, written by non-programmer monkeys and full
// of spaghetti code, gotos and race conditions, but I managed to find
// two free players that seem usable.
// 
// One of them is just a "play button" and one has a horizontal strip
// with a "play head" and loading indicator.  You can see them both in
// use here:
// 
//  http://slice-of-thai.com/tones/
// 
// The button player is:
// 
//  http://musicplayer.sourceforge.net/
//  http://musicplayer.sourceforge.net/button/test.html
//  http://www.ic.sunysb.edu/stu/ahanley/music/help.htm
// 
// The strip player is:
// 
//  http://www.jeroenwijering.com/?item=JW_FLV_Media_Player
//  http://jeroenwijering.com/?item=Supported_Flashvars
// 
// These players will do mp3 and flv, and it's possible that they
// might do WMA as well; I haven't checked.
// 
// I should note that these players are still poorly written (source is
// available...and rather scary) but the bugs I found are minor compared
// to the other players.  For example, the button player will not let you
// stop playback the first time you play a sound, IF that sound loads
// "too quickly."  Eeeigh.
// 
// In addition to the players, there are two more pieces that you need to
// really make it work:
// 
// 1. You've probably noticed that in IE, you often have to click on a Flash
// app twice in order to actually use it.  This is due to a ridiculous
// lawsuit settlement with Eolas, but there is a workaround for this
// which avoids the second click in IE.  The workaround is called
// SWFObject and its purpose and usage is explained here:
// 
//  http://www.jeroenwijering.com/?item=Embedding_Flash
// 
// 2. I have some pages with MANY button players (say, 100 buttons).  I
// found that when I loaded these pages, the browsers would allocate
// hundreds of megabytes of memory, and in some cases the browsers would
// also consume 100% CPU, because of a horrifically bad architectural
// design on the part of Macromedia/Adobe (the Flash players poll, even
// when they are idle!).  So, the solution I came up with for this was a
// little extra bit of JavaScript that does not actually create any Flash
// players until the user clicks a decoy button that looks just like the
// player!  It is seamless for the user.  You can find that under
// set_fake_sound_button() below, and its call sites in the various .html
// files.  Initially, I was concerned that this technique would bypass
// caching (e.g. if you play sound A, then B, then back to A, that it
// would have to re-load the sound for A) but upon testing, caching
// still works!
// 
var current_sound_button_span_id;

function set_fake_sound_button(span_id, sound_url)
{
    var span = ei(span_id);
    while (null !== span.firstChild)
    {
        span.removeChild(span.firstChild);
    }

    if (undefined !== span.sound_url)
        sound_url = span.sound_url;
    else
    {
        assert(undefined !== sound_url);
        span.sound_url = sound_url;
    } 

    var root = '';
    // skip_me - XXX HACK for local testing
    root = '..';

    var img;
    if (is_msie)
    {
        // pathetic IE <= 6 cannot even display transparent PNGs
        img = document.createElement('span');
        img.style.display = 'inline-block';
        img.style.position = 'relative';
        img.style.height = '17px';
        img.style.width  = '17px';
        img.style.verticalAlign = 'baseline'; // XXX ineffective
        img.style.margin = '0px 0px 5px 0px'; // XXX this is poor subsitute
        img.style.padding = '0px 0px 0px 0px';
        img.style.border = '0px';
        img.style.filter = 
            "progid:DXImageTransform.Microsoft.AlphaImageLoader" +
            "(src='" + root + '/playbutton.png' + "');";
    }
    else
    {
        // real browser
        img = document.createElement('img');
        img.src = root + '/playbutton.png';
        img.width = 17;
        img.height = 17;
        img.border = 0;
    }
    img.onclick = function()
    {
        if (undefined !== current_sound_button_span_id)
        {
            set_fake_sound_button(current_sound_button_span_id);
        }

        // apparently the player does not require (and might not allow?)
        // proper URL escaping of slashes in the song_url.
        //
        var player_url = root + '/musicplayer_f6.swf?' +
            'song_url=' + sound_url + '&' +
            'autoplay=true&' +
            'autoload=true';

        // alert('player url is ' + player_url); // XXX HACK

        var so = 
            new SWFObject(player_url,
                          'so' + span_id,
                          '17',
                          '17',
                          '6');
        so.addVariable('movie',player_url);
        so.write(span_id);
        current_sound_button_span_id = span_id;
    };
    span.appendChild(img);
}


// thai script font size selection
function ts_fs_get()
{
    var sz = get_class_style('ts').style.fontSize;
    sz = sz.replace('%', '');
    sz = sz - 0;
    return sz;
}
function ts_fs_eset(sz)
{
    if (0 === sz) sz = 136;
    set_cookie('ts_fs', sz,
               undefined, undefined, undefined,
               '/', // applies to all of this website
               undefined, // this website
               undefined);
    get_class_style('ts').
         style.fontSize = sz + '%';
}
function ts_fs_set(sz)
{
    var input = ei('ts_fs_i');
    if (null !== input) input.value = sz;
    ts_fs_eset(sz);
}
function ts_fs_adj(diff)
{
    ts_fs_set(ts_fs_get() + 4*diff);
}
function ts_fs_set_default()
{
    var sz = ts_fs_get();
    var sz2 = get_cookie('ts_fs');
    if (null !== sz2) sz = sz2;
    ts_fs_set(sz);
}

// hack to test whether each font in our list is installed
// 
// this hack is a heuristic.  it renders font_test_string
// in base_font, then renders font_test_string in your font,
// with a fallback to base_font, and compares the resulting
// widths and heights.
//
// it only works if the font you are testing is not
// the same font as base_font, or an alias to base_font.
//
// font_test_string must be a string that
// can render in base_font without browser font fallback,
// otherwise the test is invalid since the base_font
// fallback font could be your font.  for example,
// if base_font does not contain Thai characters,
// then font_test_string must not contain Thai characters.
//
var font_test_string = 'font_test:ABCDEFGHIJKLMNOpqrstuvwxyz()[]{}';
var base_font = 'cursive';
var base_width;
var base_height;
var test_div;
var test_span;
function has_font(font)
{
    var body = document.getElementsByTagName("body")[0];

    if (undefined === test_div)
    {
        test_div = document.createElement("div");
        test_span = document.createElement("span");
        
        test_div.appendChild(test_span);
        test_div.style.fontFamily = base_font;
        test_span.style.fontFamily = base_font; // will change each time
        test_span.style.fontSize = '100px'; // bigger the better
        test_span.innerHTML = font_test_string;

        body.appendChild(test_div);
        base_width = test_span.offsetWidth;
        base_height = test_span.offsetHeight;
        // alert('for base ' + base_font + ', ' +
        //       'width is ' + base_width + ' and ' +
        //       'height is ' + base_height);
        body.removeChild(test_div);
    }

    body.appendChild(test_div);
    // must also specify base_font here due to Safari bug
    test_span.style.fontFamily = font + ', ' + base_font;
    var width = test_span.offsetWidth;
    var height = test_span.offsetHeight;
    // alert('for font ' + font + ', ' +
    //       'width is ' + width + ' and ' +
    //       'height is ' + height);
    body.removeChild(test_div);

    return !(width == base_width && height == base_height);
}

// Using a hack similar to has_font above,
// detect whether the specified font is one
// that will automatically place a dotted
// circle underneath marks if they are not
// preceeded by a suitable consonant.
//
// DETAILS:
// 
// You've probably run into the annoying "Naked Vowel Placeholder"
// problem whereby when you attempt to render a Thai mark in isolation
// from any consonant, e.g.,
// 
//   −ี−
// 
// The rendering will work or fail (by inserting a dotted circle)
// depending on current user's font, browser, and even OS.  Some
// websites refer to this as the "Vista IE" problem, but
// it is not restricted to either Vista or IE.  For example, certain very
// nice fonts, such as the public domain Garuda font from the
// ThaiFonts-Scalable project, which I think is the most readable Thai
// font available, will always generate a dotted circle for vowels and
// marks that are not preceded by qualifying base characters, even in XP
// and even in FireFox, and even on some browsers on MacOS X.
// 
// So the real task isn't so much to detect Vista+IE but rather to detect
// the browser doing the dotted-circle insertion with whatever
// font/OS/browser is being used.
// 
// detect_dotted_circle() is a nasty but functional JavaScript hack to
// accomplish this.  I use detect_dotted_circle() in my code and then I
// automatically add a dash if the current platform is not inserting
// dotted circles (via the .ts_dc0 and .ts_dc1 CSS selectors).  
// This hack could be used entirely on the client side,
// rewriting the page depending on the result, or its result can be
// sent back to the server side (via form results or cookies or AJAX or
// ...) so as to affect server-side page generation.
// 
// The net effect is that that the user will see either dashes or dotted
// circles for a given string, depending on their font and platform, but
// as I discovered, this is the best we can do (since not all Thai fonts
// contain a dotted circle, we cannot always present a dotted circle, and
// of course we cannot always present a dash either).  
//
function detect_dotted_circle(font)
{
    var body = document.getElementsByTagName("body")[0];

    var s;

    s = document.createElement("span");
    s.style.fontFamily = font;
    s.style.fontSize = '100px'; // bigger the better

    s.innerHTML = 'ก็';
    body.appendChild(s);
    var width_with_proper_base = s.offsetWidth;
    body.removeChild(s);

    s.innerHTML = '-็';
    body.appendChild(s);
    var width_without_proper_base = s.offsetWidth;
    body.removeChild(s);
    
    // a font that allows the mark to mate with the dash
    // will give us roughly the same width, usually smaller
    // and a little bit bigger in some rare cases.
    //
    // a font that generates an automatic dotted circle
    // in this case will give us a much wider width,
    // generally twice as wide but sometimes as small
    // as just 150% bigger.
    //
    return (width_without_proper_base > 1.5 * width_with_proper_base);
}

// thai script font family selection
//
// All Thai script on this website is wrapped in the .ts CSS selector,
// and this code sets the font-family on that selector.  I do this because;
// 
// - browser-based font-setting options range from
//   completely unintuitive to completely broken,
// 
// - my Thai font selector will carefully affect only the Thai text
//   and not the English or phonemic text.
// 
// - my code will remember its setting in a cookie for my site only,
//   and not break rendering of any other site
// 
// Nothing in my code prevents users from using the browser-based
// font selectors, which always take precedence.
// 
var ts_ff_list =
[ "Arial Unicode MS", "Tahoma", "Microsoft Sans Serif", "Thonburi", "Lucida Grande", "Silom", "Ayuthaya", "Sathu", "Krungthep", "Garuda", "Kinnari", "Loma", "Norasi", "Purisa", "Sawasdee", "Tlwg Typist", "TlwgMono", "TlwgTypewriter", "Umpush", "Waree", "ST-TT-Garuda", "ST-TT-Garuda Oblique", "ST-TT-Kinnari", "ST-TT-Kinnari Oblique", "ST-TT-Loma", "ST-TT-Loma Oblique", "ST-TT-Norasi", "ST-TT-Norasi Oblique", "ST-TT-Tlwg Typist", "ST-TT-Tlwg Typist Oblique", "ST-TT-TlwgMono", "ST-TT-TlwgMono Oblique", "ST-TT-TlwgTypewriter", "ST-TT-TlwgTypewriter Oblique", "ST-TT-Umpush", "ST-TT-Umpush Light Oblique", "ST-TT-Umpush Light", "ST-TT-Umpush Oblique", "ST-TT-Waree", "ST-TT-Waree Oblique" ];
function ts_ff_set(idx)
{
    var family = ts_ff_list[idx];

    set_cookie('ts_ff', family,
               undefined, undefined, undefined,
               '/', // applies to all of this website
               undefined, // this website
               undefined);

    var families = 
        family + ', ' +
        "Garuda, Garuda, 'Arial Unicode MS', Thonburi, Tahoma, 'Lucida Grande', Arial, sans-serif";

    get_class_style('ts')
        .style.fontFamily = families;

    var radio = ei('ts_ff_rb_' + idx);
    if (null !== radio) radio.checked = true;

    if (detect_dotted_circle(family))
    {
        get_class_style('ts_dc0').style.display = 'none';
        get_class_style('ts_dc1').style.display = 'inline';
    }
    else
    {
        get_class_style('ts_dc0').style.display = 'inline';
        get_class_style('ts_dc1').style.display = 'none';
    }
}
function ts_ff_set_default()
{
    var idx;

    var tbody = ei('ts_ff_tbody');
    if (null !== tbody)
    {
        // just in case we get called multiple times
        while (null !== tbody.firstChild)
        {
            tbody.removeChild(tbody.firstChild);
        }
    }
    
    for(var i=0; i < ts_ff_list.length; i++)
    {
        var ff = ts_ff_list[i];
        if (!has_font(ff)) continue;
        
        // first one we find is the default idx
        if (undefined === idx)
            idx = i;

        if (ff.search(/garuda/i) >= 0)
        {
            var garuda_ad = ei('ts_ff_garuda');
            if (null !== garuda_ad)
                garuda_ad.style.display = 'none';
        }
        
        if (null === tbody) continue;

        // add a row for this font: nearly every line of this contains
        // a workaround for the ASTOUNDINGLY PATHETIC IE
        var tr =
            elt('tr', 
                {
                    id: 'ts_ff_tr_' + i,
                    valign: 'middle'
                }, 
                [ elt('td',
                      {
                          bgcolor: '#eeeeee'
                      },
		              function(obj)
		              {
		                  obj.noWrap = true;
		              },
                      elt('input',
                          {
                              type: 'radio',
			                  name: 'ts_ff',
                              id: 'ts_ff_rb_' + i
                          },
                          function(obj)
                          {
                              var i2 = i; // snap value of i
                              obj.onclick = function() { ts_ff_set(i2); };
                          })),
                  elt('th',
                      {
                          bgcolor: '#eeeeee'
                      }, 
		              function(obj)
		              {
		                  obj.noWrap = true;
		              },
		              ff),
                  elt('td',
                      {
                          width: '100%'
                      }, 
		              function(obj)
		              {
		                  obj.noWrap = true;
			              obj.className = 'ts';
			              obj.style.fontFamily = ff;
		              },
                      'คุณ เก็บ เสื้อ ไว้ ไหน')
                ]);
        
        tbody.appendChild(tr);
    }
    if (undefined === idx) 
    {
        // user probably has fonts fixed in browser opts
        var err = ei('ts_ff_error');
        if (null !== err)
            err.style.display = 'block';

        // might as well turn off garuda ad
        var garuda_ad = ei('ts_ff_garuda');
        if (null !== garuda_ad)
            garuda_ad.style.display = 'none';

        idx = 0; 
    }
    var cf = get_cookie('ts_ff');
    if (null !== cf)
    {
        for(var i=0; i < ts_ff_list.length; i++)
        {
            if (cf == ts_ff_list[i])
            {
                idx = i;
                break;
            }
        }
    }
    ts_ff_set(idx);
}


// thai pronunciation guide selection
// 
// On this site I pre-generate all the romanizations and include all of
// them in the page, so that when the user changes their romanization
// choice(s), the changes show up instantly (done via normal CSS
// hiding/showing using the CSS selectors .tpN where N is an integer
// offset).  This avoids the need to have any server-side request-time
// page generation; all pages are served static.  Initially I was worried
// about the page bloat this might cause, until I realized that the bloat
// from 8 different schemes on a text-packed page was still much less
// than the size of a single JPG image that appears on the header of my
// site!
//
var idx_to_token = [ '1', '0', '7', '2', '3', '8', '6', '10', '9', '4', '11', '5' ];
function tp_check_cookie(i)
{
    var token = idx_to_token[i];
    var v = get_cookie('tp' + token);
    var checked;
    if ('true' == v)
        checked = true;
    else if ('false' == v)
        checked = false;
    else
        checked = (0 === i); // default is first item only
    return checked;
}
function tp_ch()
{
    var got_one = false;
    for(var i=0; i < 12; i++)
    {
        var token = idx_to_token[i];
        var checkbox = ei('tp_cb_' + token);
        var checked;
        if (null === checkbox) // no ui
        {
            checked = tp_check_cookie(i);
        }
        else // get it from ui
        {
            checked = checkbox.checked;
        }
        if (checked)
        {
            get_class_style('tp' + token).style.display = 'inline';
            get_class_style('tps' + token).style.display = 
                (got_one ? 'inline' : 'none');
            got_one = true;
        }
        else
        {
            get_class_style('tp' + token).style.display = 'none';
            get_class_style('tps' + token).style.display = 'none';
        }
        set_cookie('tp' + token, checked, 
                   undefined, undefined, undefined,
                   '/', // applies to all of this website
                   undefined, // this website
                   undefined);
    }
    var none_selected_rant = ei('tp_none_selected');
    if (null !== none_selected_rant)
        none_selected_rant.style.display = (got_one ? 'none' : 'inline');
}
function set_tp_cb_default(i)
{
    var checked = tp_check_cookie(i);
    var token = idx_to_token[i];
    ei('tp_cb_' + token).checked = checked;
}


