// vismap.js - layer/scene visibility map
// copyright 2009, Martin Rinehart
/*
This file:
classes
CharMap - convert 'VIIVVI ...' to string-encoded bitmap
Layer - one per layer, w/name, lowerName, original row index
Sixer - returns, chops off, six 'VIIVVI's at a time
support functions
click handlers
UI builders
Within all groups: alphabetical order.
Basic idea: initial display forces user to click "Get Data from Model". (Why not just do it? Mac incompatible timing issues.)
Starts by calling "refresh()" Ruby callback function. "refresh()" is called again whenever user clicks "Get Data from Model".
"refresh" gathers its data, then calls support function "rubyReturns()" with a JSON representation of the vismap.model's layers, scenes and visibilities. JavaScript creates custom UI, one row per layer and one column per scene, all checkboxes.
User fusses w/checkboxes (checked == visible). User clicks "Send Data to Model".
JavaScript calls Ruby callback "newVis()" with visibilities (in string-encoded bitmap). Ruby adjusts vismap.model appropriately.
*/
var vismap = {}; // get the rest of the data out of the global namespace
// vismap.model filled in with JSON from Ruby
vismap.layer_cbs = []; // checkboxes outside the layer names
vismap.rc_cbs = []; // main (row/col) checkboxes
vismap.scene_cbs = []; // checkboxes above the scene numbers
// "classes"
function CharMap( s, c ) {
// Called with a String ("VIIVVIIVVV...") and a char ("V"), this creates CharMap.map, a bitmap locating the chars (1001100111...). The bitmap is encoded in another string, 6 bits per char. The first three chars encode the length, in bits, of the bitmap.
var X = [ 32, 16, 8, 4, 2, 1 ];
var Base = 48;
this.matchChar = c;
this.s6 = new Sixer( s );
this.map = '';
var e;
try { // encode six bits at a time into characters
while (true ) {
this.map += encode( this.s6.next(), this.matchChar );
}
}
catch( e ) {} // the Sixer has run dry
size = encodeNumber( s.length ); // 3-char string
this.map = size + this.map;
function encode( str, c ) {
var ret = '';
var word = 0;
for ( var i = 0; i < 6; i++ ) {
if ( str.charAt(i) == c ) { word += X[i]; }
}
// alert( 'in encode(), working on ' + str + ', word= ' + word );
return encodeChar( word );
}
function encodeChar( n ) { return String.fromCharCode( n + Base ); }
function encodeNumber( n ) {
var low6 = n & 63; // 00000011111
n = n >>> 6;
var middle6 = n & 63;
var hi6 = n >>> 6;
return encodeChar( hi6 ) + encodeChar( middle6 ) + encodeChar( low6 );
}
} // end of class CharMap
// this little guy remembers where a row started, before sorting
function Layer( name, row ) {
this.name = name;
this.originalRow = row;
this.lowerName = name.toLowerCase();
}
// passed to the sort() method to compare by lowercase names
Layer.compareLower = function( a, b ) {
var x = a.lowerName, y = b.lowerName;
if ( x == y ) return 0;
return x < y ? -1 : 1;
}
// probably useless after debugging
Layer.prototype.toString = function() { return 'Layer: ' + this.name + '-' + this.originalRow; }
// Called with a string, Sixer chops off and returns 6 characters at a time from the left. The last chars are padded with blanks to length 6. When the string is empty, Sixer throws "well ran dry".
function Sixer( str ) {
this.string = str;
this.next = function() {
if ( this.string == '' ) throw "well ran dry";
return this.lpop();
}
// grab, and then chop off, six chars at a time from the left
this.lpop = function() {
var ret = this.string.substr( 0, 6 );
if ( ret.length < 6 ) {
this.string = ''; // runs well dry
return (ret + ' ').substr( 0, 6 ); // always returns 6 chars
} else {
this.string = this.string.substr( 6 ); // chops off first 6 chars
return ret;
}
} // end of Sixer.lpop()
} // end of class Sixer
// SUPPORT FUNCTIONS
// these are the checkboxes in the array
function add_checkbox( cell, i, id_suffix, handler ) {
var cb = document.createElement( 'input' );
cb.id = i + '_' + id_suffix;
cb.type = 'checkbox';
cell.appendChild( cb );
cb.onclick = handler;
}
// called this way by Ruby if the SketchUp model is broken
function error() {
return vismap.model.vis.substring(0,1) == 'E';
}
// these are the checkboxes left of the layer names
function get_layer_cbs() {
var layer, lno
for ( layer in vismap.model.layers ) {
lno = parseInt( layer );
vismap.layer_cbs[ lno ] =
document.getElementById( lno + '_layers_cb' );
}
}
// these are the checkboxes in the array
function get_rc_cbs() {
var cl, col, id, row, rw, n;
for ( row in vismap.model.layers ) {
rw = parseInt( row );
for ( col in vismap.model.scenes ) {
cl = parseInt( col );
n = rw*vismap.model.scenes.length + cl;
id = num_given_rc(rw, cl) + '_rc_cb';
vismap.rc_cbs[ n ] =
document.getElementById( id );
vismap.rc_cbs[ n ].checked = checked( rw, cl );
}
}
}
function checked( row, col ) { // is intersection visible?
var rno = vismap.model.sortedLayers[ row ].originalRow;
var cno = rno * vismap.model.scenes.length + col;
return vismap.model.vis.substring( cno, cno+1 ) == 'V';
}
// these are the checkboxes above the scene numbers
function get_scene_cbs() {
var scene, sno;
for ( scene in vismap.model.scenes ) {
sno = parseInt( scene );
vismap.scene_cbs[sno] = document.getElementById( sno + '_scenes_cb' );
}
}
// One "V" or "I" for each checkbox in the array (read left-to-right, top-to-bottom)
function getVis() { // assembles new string of 'V's and 'I's
var vis = [];
for ( lr in vismap.model.layers ) {
var lrn = parseInt( lr );
var str = '';
for ( sn in vismap.model.scenes ) {
var snn = parseInt( sn );
var cbn = num_given_rc( lrn, snn );
str += vismap.rc_cbs[ cbn ].checked ? 'V' : 'I';
}
vis[ vismap.model.sortedLayers[lrn].originalRow ] = str;
} // end of layer loop
return vis.join('');
} // end of getVis()
// prevents HTML renderer from breaking scene names on spaces
function nospace( txt ) {
return txt.replace( new RegExp(' ','g'), ' ' );
}
// index into the "vis" array for a given layer and scene #
function num_given_rc( r, c ) {
return r * vismap.model.scenes.length + c;
}
// layer and scene for a given index
function rc_given_num( num ) {
var len = vismap.model.scenes.length;
return [ Math.round(( num / len ) - 0.5), num % len ];
}
// called by "Get Data from Model" buttons
function rubyCalled( callback_name, message ) {
if ( (typeof message) == 'undefined' ) message = '';
url = 'skp:' + callback_name + '@' + message;
window.location.href = url; // the real deal
// rubyReturned( "{ layers:[ 'Layer0', 'basement', 'ground_floor', 'garage', 'second_floor', 'stairs', 'roof' ], scenes:[ 'outside, aerial', 'outside, front door', 'livingroom', 'diningroom', 'bedroom', 'garage' ], vis:'VVVVVVVVIIIVVVVVIVVVVVVVVVIVVVIIVVVIVVIIIV' }" ); // used to test in Opera
}
// called from Ruby
function rubyReturned( json ) {
// alert( json );
eval( 'vismap.model = ' + json ); // whats_in_model();
// add array of Layer objects, in vismap.model order
vismap.model.layerObjects = [];
for ( i in vismap.model.layers ) {
vismap.model.layerObjects[i] = new Layer( vismap.model.layers[i], i ); }
// create and sort array of Layer objects, sorted by lowercase name
vismap.model.sortedLayers = [];
for ( i in vismap.model.layerObjects ) {
vismap.model.sortedLayers[i] = vismap.model.layerObjects[i]; }
vismap.model.sortedLayers.sort( Layer.compareLower );
for ( i in vismap.model.sortedLayers ) {
vismap.model.sortedLayers[i].sortedRow = i; }
createUI();
if ( !error() ) {
createUIrefs();
}
}
/* debugging
whats_in_model = function() {
alert( 'layers = ' + vismap.model.layers + '\n' +
'scenes = ' + vismap.model.scenes + '\n' +
'vis = ' + vismap.model.vis );
}
*/
// CLICK HANDLERS
var btn_download = function() { // "Send Data to Model"
if ( ! vismap.model ) {
alert( 'There is no data to send.' );
return;
}
var vis = getVis();
var map = new CharMap( vis, "V" ).map;
rubyCalled( 'newVis', map );
}
var btn_refresh = function() { // "Get Data from Model"
rubyCalled( 'refresh' );
}
// on unchecking an individual box, uncheck its scene/layer boxes
var handle_click = function() {
var n = parseInt( this.id );
if ( vismap.rc_cbs[n].checked ) return;
var where = rc_given_num( n );
var row = where[0];
var col = where[1];
vismap.scene_cbs[ col ].checked = false;
vismap.layer_cbs[ row ].checked = false;
} // end of handle_click()
// Check (or uncheck) every checkbox in a row
var handle_layer_click = function( ) {
var r = parseInt( this.id );
var base = r * vismap.model.scenes.length;
var stop = base + vismap.model.scenes.length;
for ( i = base; i < stop; i++ ) {
vismap.rc_cbs[ i ].checked = vismap.layer_cbs[ r ].checked;
if ( ! vismap.rc_cbs[i].checked ) {
vismap.scene_cbs[ rc_given_num(i)[1] ].checked = false;
}
}
} // end of handle_layer_click()
// Check (or uncheck) every checkbox in a column
var handle_scene_click = function() {
var col = parseInt( this.id );
for ( i = 0; i < vismap.model.layers.length; i++ ) {
j = col + i*vismap.model.scenes.length;
vismap.rc_cbs[ j ].checked = vismap.scene_cbs[ col ].checked;
if ( ! vismap.rc_cbs[j].checked ) {
vismap.layer_cbs[ rc_given_num(j)[0] ].checked = false;
}
}
}
// WRITE THE CHECKBOX/SCENE LIST SECTIONS
function createInnerTable() { // Inner table = the blue box
var
cell, // a cell being created/populated
cl, // column (in rw/cl loops)
i, // loop counter
itbl, // the inner table
row, // a row being created/populated
rw // row (in rw/cl loops)
itbl = document.createElement( 'table' );
itbl.align = 'center';
itbl.style.backgroundColor = '#f0f0ff';
itbl.border=0;
itbl.cellPadding=3;
itbl.style.height='100%'; itbl.style.width='100%';
row = itbl.insertRow(0); // top row: scenes' checkboxes
row.insertCell(0); row.insertCell(1); // first two are empty
for ( i = 0; i < vismap.model.scenes.length; i++ ) {
cell = row.insertCell(i+2);
add_checkbox( cell, i, 'scenes_cb', handle_scene_click );
}
row = itbl.insertRow(1); // 2nd row, scene numbers
row.insertCell(0); row.insertCell(1);
for ( i = 0; i < vismap.model.scenes.length; i++ ) {
cell = row.insertCell(i+2);
cell.align='center';
cell.innerHTML = '' + (i+1) + '';
}
for ( rw = 0; rw < vismap.model.layers.length; rw++ ) {
row = itbl.insertRow(rw+2);
cell = row.insertCell(0); // layer checkbox
add_checkbox( cell, rw, 'layers_cb', handle_layer_click );
cell = row.insertCell(1); // layer name
cell.align = 'left';
cell.innerHTML = vismap.model.sortedLayers[rw].name;
for ( cl = 0; cl < vismap.model.scenes.length; cl++ ) {
cell = row.insertCell(cl+2);
add_checkbox( cell, rw*vismap.model.scenes.length + cl, 'rc_cb',
handle_click );
}
} // end of loop over layers
return itbl;
} // end of createInnerTable()
// this list goes to the right of the checkbox array
function createSceneList() {
var span = document.createElement( 'span' );
var list = '';
for ( i = 0; i < vismap.model.scenes.length; i++ ) {
list += '' + (i+1) + ' ' + nospace( vismap.model.scenes[i] )+ '
';
}
span.innerHTML = list;
return span;
}
// main line in the UI creation process
function createUI() {
var
cell, // a cell being created/populated
row // a row being created/populated
if ( error() ) { // here iff Ruby finds vismap.model invalid
document.body.innerHTML =
'
Reported ' + vismap.model.vis + '
Try Window/Model Info/Statistics Purge Unused' +
'
and then try Vismap again.'
return;
}
// Outer table has three rows:
// Ruby icons and title, built by HTML
// Inner table and scenes list, built here
// "Get" and "Send" buttons, built by HTML
// ROW 2, inner table and scene list
row = document.getElementById( 'middle_row' );
while ( row.childNodes.length ) { // empty the row
row.removeChild( row.firstChild );
}
cell = row.insertCell(0); // start with inner table
cell.align = 'center';
cell.colSpan = 6; // MSIE flakes workaround
cell.width = '90%'; // ditto
cell.appendChild( createInnerTable() );
cell = row.insertCell(1); // scene list
cell.appendChild( createSceneList() );
} // end of createUI()
// stored in vismap.layer_cbs[], vismap.rc_cbs[] and vismap.scene_cbs[]
function createUIrefs() {
get_layer_cbs();
get_rc_cbs();
get_scene_cbs();
}
// end of vismap.js