// 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 = '

Error Analyzing Model

' + '

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