Hi Doug.

Please find attached the telcon time planner script, and accompanying
CSS file.

-- 
Cameron McCormack, http://mcc.id.au/
        xmpp:[EMAIL PROTECTED]  ▪  ICQ 26955922  ▪  MSN [EMAIL PROTECTED]
#!/usr/bin/perl -w

# Telcon time planner
# Copyright (C) 2007-2008 Cameron McCormack <[EMAIL PROTECTED]>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

use strict;
use CGI qw(param);
use CGI::Carp qw(fatalsToBrowser);
use Fcntl ':flock';
use Symbol;

my $locked = 0;
my $lockFile = Symbol::gensym();

sub lockIt {
  unless ($locked) {
    $locked = 1;
    open $lockFile, ">lock";
    flock $lockFile, LOCK_EX;
  }
}

sub unlockIt {
  if ($locked) {
    $locked = 0;
    close $lockFile;
  }
}

print "Content-Type: text/html\n\n";

END { unlockIt(); }
lockIt();

my $op = param('op');
my @scoreLookup = (0, 700, 850, 950, 1000);
my @dayNames = qw(Monday Tuesday Wednesday Thursday Friday Saturday Sunday);
my $prefs;
my $tzs;
my @impossibles;
my @impossibleNames;
getit();

if (!(defined $op) || $op eq 'view') {
  view();
} elsif ($op eq 'impossible') {
  impossible();
} elsif ($op eq 'edit') {
  my $who = param('who');
  die unless $who =~ /^[A-Za-z0-9_]+$/;
  top(1);
  my $p = '[';
  for (my $i = 0; $i < 7; $i++) {
    $p .= '[';
    for (my $j = 0; $j < 48; $j++) {
      $p .= (defined $prefs->{$who} ? $prefs->{$who}[$i * 48 + $j] : 0) . ',';
    }
    $p .= '],';
  }
  $p .= ']';
  my $tz = defined $tzs->{$who} ? $tzs->{$who} : 0;
  print <<EOF;
<h2>Times for $who</h2>
<p>
  Use the mouse to “paint” your preferences on the timetable, which are in
  local time.  Make sure to specify your local timezone below.
</p>
<div class='control' style='float: right; color: black'>
  <div>Select times as:</div>
  <div style='margin: 0.5em' onclick='sel(event)'>
    <div class='p' style='background: palegoldenrod; color: #666'><input id='r0' type='radio' name='paint' value='0'/><label for='r0'>Impossible</label></div>
    <div class='p' style='background: maroon; color: white'><input id='r1' type='radio' name='paint' value='1'/><label for='r1'>Bad</label></div>
    <div class='p' style='background: orange'><input id='r2' type='radio' name='paint' value='2'/><label for='r2'>Acceptable</label></div>
    <div class='p' style='background: yellow'><input id='r3' type='radio' name='paint' value='3'/><label for='r3'>Good</label></div>
    <div class='p' style='background: lime'><input id='r4' type='radio' name='paint' value='4' checked='checked'/><label for='r4'>Best</label></div>
  </div>
</div>
<form method="post" action="" enctype="multipart/form-data">
<script type="text/javascript">
var days = 'Monday Tuesday Wednesday Thursday Friday Saturday Sunday'.split(' ');
var bg = 'palegoldenrod maroon orange yellow lime'.split(' ');
var fg = '#666 white black black black'.split(' ');
var level = 'Impossible Bad Acceptable Good Best'.split(' ');
var cur = 3;
var isDown = false;
var lastD = 0;
var lastT = 0;

var prefs = $p;

function paintCell(d, t, l) {
  var explicit = typeof l != 'undefined';
  var i;
  if (explicit) {
    i = l;
  } else {
    i = prefs[d][t];
  }
  var elt = document.getElementById('c' + d + '_' + t);
  elt.firstChild.nodeValue = level[i];
  elt.style.background = bg[i];
  elt.style.color = fg[i];
  elt.style.fontWeight = explicit ? 'bold' : 'normal';
  elt.style.textDecoration = explicit ? 'underline' : 'none';
}

function target(event) {
  var t = event.srcElement || event.target;
//   var s = 't is:\\n';
//   for (var p in t) {
//     s += p + ': ' + t[p] + '\\n';
//   }
//   alert(s);
  return t.tagName.toLowerCase() == 'td' && t.className ? t : null;
}

function over(e) {
  var t = target(e);
  if (!t) return;
  var xs = t.getAttribute('id').match(/^c([0-9])_([0-9]+)/);
  if (isDown) {
    prefs[xs[1]][xs[2]] = cur;
    document.forms[0].elements['p' + xs[1] + '_' + xs[2]].value = cur;
  }
  paintCell(lastD, lastT);
  paintCell(xs[1], xs[2], cur);
  lastD = xs[1];
  lastT = xs[2];
  if ('preventDefault' in e) {
    e.preventDefault();
  }
}

function out(e) {
  var t = target(e);
  if (!t) return;
  var xs = t.getAttribute('id').match(/^c([0-9])_([0-9]+)/);
  paintCell(xs[1], xs[2]);
}

function down(e) {
  var t = target(e);
  if (!t) return;
  isDown = true;
  var xs = t.getAttribute('id').match(/^c([0-9])_([0-9]+)/);
  // println('down = true');
  prefs[xs[1]][xs[2]] = cur;
  document.forms[0].elements['p' + xs[1] + '_' + xs[2]].value = cur;
}

function up(e) {
  var t = target(e);
  if (!t) return;
}

function sel(e) {
  var t = e.srcElement || e.target;
  if (t.tagName.toLowerCase() == 'div') {
    t = t.firstChild;
  } else if (t.tagName.toLowerCase() == 'label') {
    t = t.previousSibling;
  }
  t.checked = true;
  cur = Number(t.getAttribute('id').substring(1));
}

function change(e) {
  alert('change');
  var t = e.srcElement || e.target;
  cur = Number(t.getAttribute('id').substring(1));
}

var s = '<table id="t" onmouseover="over(event)" onmouseout="out(event)" onmousedown="down(event)" onmouseup="up(event)" onselectstart="return false"><tr><td></td><th class="dh">' + days.join('</th><th class="dh">') + '</th></tr>';
for (var h = 0; h < 24; h++) {
  s += '<tr><th rowspan="2">' + h + ':00' + '</th>';
  for (var d = 0; d < 7; d++) {
    s += '<td id="c' + d + '_' + (h * 2) + '" class="a">.<input type="hidden" name="p' + d + '_' + (h * 2) + '" value="' + prefs[d][h * 2] + '"/></td>';
  }
  s += '</tr><tr>';
  for (var d = 0; d < 7; d++) {
    s += '<td id="c' + d + '_' + (h * 2 + 1) + '" class="a">.<input type="hidden" name="p' + d + '_' + (h * 2 + 1) + '" value="' + prefs[d][h * 2 + 1] + '"/></td>';
  }
  s += '</tr>';
}
s += '</table><div class="control">Timezone offset from UTC (plain number): <input name="tz" size="5" value="$tz"> | <input type="hidden" name="op" value="save"><input type="hidden" name="who" value="$who"><input type="submit" name="button" value="Save these times"> <input type="submit" name="button" value="Delete these times"></div>';
document.write(s);

function init() { }

for (var d = 0; d < 7; d++) {
  for (var t = 0; t < 48; t++) {
    paintCell(d, t);
  }
}
document.getElementById('r' + 3).checked = true;
</script>
</form>
EOF
  bottom();
} elsif ($op eq 'save') {
  my $who = param('who');
  die unless $who =~ /^[A-Za-z0-9_]+$/;
  if (param('button') =~ /^Delete/) {
    delete $prefs->{$who};
    delete $tzs->{$who};
  } else {
    for (my $i = 0; $i < 7; $i++) {
      for (my $j = 0; $j < 48; $j++) {
        $prefs->{$who}[$i * 48 + $j] = int(param("p${i}_$j"));
      }
    }
    $tzs->{$who} = int(param('tz'));
  }
  open FH, '>times';
  for my $name (keys %$prefs) {
    print FH "$name $tzs->{$name} " . join(' ', @{$prefs->{$name}}) . "\n";
  }
  close FH;
  if (!%$prefs) {
    open FH, '>best';
    close FH;
  } else {
    my @scores;
    for (my $i = 0; $i < 7; $i++) {
      for (my $j = 0; $j < 48; $j++) {
        my $score = 0;
        for my $name (keys %$prefs) {
          my $t = ($i * 48 + $j + $tzs->{$name} * 2) % (48 * 7);
          $score += $scoreLookup[$prefs->{$name}[$t]];
        }
        push @scores, [$i * 48 + $j, $score];
      }
    }
    my @pairs;
    for (my $i = 0; $i < 48 * 7; $i++) {
      for (my $j = $i + 3; $j < 48 * 7; $j++) {
        my $day1 = int($i / 48);
        my $day2 = int($j / 48);
        my $time1 = $i % 48;
        my $time2 = $j % 48;
        my $score1 = $scores[$i][1];
        my $score2 = $scores[$j][1];
        my $rank =
            ($day2 - $day1 >= 2 && $day2 - $day1 <= 5 ? 200000 : 0)
          + ($time1 == $time2 ? 1 : 0)
          + ($scores[$i][1] + $scores[$j][1]) * 10;
        push @pairs, [$i, $j, $rank]
          unless $rank == 0;
      }
    }
    @pairs = sort { $b->[2] <=> $a->[2] } @pairs;
    open FH, '>best';
    for (my $i = 0; $i <= $#pairs; $i++) {
      print FH join(' ', @{$pairs[$i]}), "\n";
    }
    close FH;
  }
  view();
}

sub getit {
  open FH, 'times';
  $prefs = { };
  $tzs = { };
  @impossibles = (0) x (48 * 7);
  @impossibleNames;
  for (my $i = 0; $i < 48 * 7; $i++) {
    push @impossibleNames, {};
  }
  while (<FH>) {
    chomp;
    my @x = split(/ /);
    my $name = shift @x;
    my $tz = shift @x;
    $tzs->{$name} = $tz;
    $prefs->{$name} = [EMAIL PROTECTED];
    for (my $i = 0; $i < 48 * 7; $i++) {
      if ($x[$i] == 0) {
        $impossibles[($i - $tz * 2) % (48 * 7)]++;
        $impossibleNames[($i - $tz * 2) % (48 * 7)]{$name} = 1;
      }
    }
  }
  close FH;
}

sub top {
  my $onmouseup = shift;
  my $x = $onmouseup ? ' onload="init()"' : '';
  if ($onmouseup) {
    print "<html onmouseup='isDown = false'>\n";
  } else {
    print "<html>\n";
  }
  print <<EOF;
<head>
<title>Telcon time planner</title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<link rel='stylesheet' type='text/css' href='telcon.css'>
</head>
<body$x>
<h1>Telcon time planner</h1>
<p>
  This tool can help choose an appropriate telcon time according to
  group members’ preferences.  It currently assumes that it will be looking
  for two 1.5 hour slots on different days.  It will prefer to keep them
  at the same time on two days with at least one day between them.  Note
  that the preferences you enter are for the telcon starting times (i.e.
  if you specify <b>Monday 9am = Best</b> and <b>Monday 9:30am = Impossible</b>,
  the <b>9am–10:30am</b> timeslot is considered <b>Best</b>).
</p>
<p class='control'>
  View the <a href='?op=view'>current best times</a>,
  <a href='?op=impossible'>intersection of impossible times</a>,
  or edit times for
EOF
  my @names = sort keys %$prefs;
  print join(', ', map { "<a href='?op=edit;who=$_'>$_</a>" } @names);
  if (@names) {
    print ' or ';
  }
  my $else = scalar(keys %$prefs) == 0 ? '' : ' else';
  print <<EOF;
  <a href='#' onclick='someone()'>someone$else</a>.
</p>
<script type="text/javascript">
function someone() {
  var s = prompt('Who?', '');
  if (s) {
    s = s.replace(/[^A-Za-z0-9_]/g, '');
    document.location = '?op=edit;who=' + s;
  }
}
</script>
EOF
}

sub bottom {
  print <<EOF;
</body>
</html>
EOF
}

sub impossible {
  top(0);
  print "<h2>Current intersection of Impossible times</h2>\n";
  print "<p>The times below are in UTC.  Green indicates a time everyone can attend, while varying shades of red indicates the number of <b>Impossibles</b>.</p>\n";
  print "<table><tr><td></td><th class='dh'>Monday</th><th class='dh'>Tuesday</th><th class='dh'>Wednesday</th><th class='dh'>Thursday</th><th class='dh'>Friday</th><th class='dh'>Saturday</th><th class='dh'>Sunday</th></tr>\n";
  my $max = 0;
  for my $val (@impossibles) {
    $max = $val if $val > $max;
  }
  for (my $t = 0; $t < 48; $t++) {
    print "<tr>";
    print "<th rowspan='2'>", ($t / 2), ":00</th>" if $t % 2 == 0;
    for (my $d = 0; $d < 7; $d++) {
      my $cls = $t % 2 == 0 ? 'a' : 'b';
      my $fg;
      my $bg;
      if ($impossibles[$d * 48 + $t] == 0) {
        $bg = 'lime';
        $fg = 'black';
      } else {
        $bg = 'rgb(' . int(256 * ($max - $impossibles[$d * 48 + $t]) / $max) . ',0,0)';
        $fg = 'white';
      }
      print "<td class='$cls' style='background: $bg; color: $fg'><div>" . join(', ', sort keys %{$impossibleNames[$d * 48 + $t]}) . "</div></td>";
    }
    print "</tr>\n";
  }
  print "</table>\n";
  bottom(0);
}

sub view {
  top(0);
  open FH, 'best';
  my @best;
  my $count = 0;
  while (<FH>) {
    chomp;
    push @best, [split(/ /)];
    last if ++$count == 100;
  }
  close FH;
  print "<h2>Current best times</h2>\n";
  if ([EMAIL PROTECTED]) {
    print "<p>No times yet.</p>\n";
  } else {
    print "<p>Below are the top 100 pairs of telcon times in relevant timezones (hover over the times for UTC).  Times in sections are of equivalent goodness.</p>\n";
    my @days = qw(Mon Tue Wed Thu Fri Sat Sun);
    my @prs = qw(Impossible Bad Acceptable Good Best);
    my @who = sort keys %$prefs;
    my %rtzs;
    for (@who) {
      $rtzs{$tzs->{$_}} = 1;
    }
    my @rtzs = sort { $a <=> $b } keys %rtzs;
    print "<div>\n";
    my $alt = 0;
    my $last = -1;
    for (my $i = 0; $i <= $#best; $i++) {
      my $one = $best[$i][0];
      my $two = $best[$i][1];
      my $day1 = int($one / 48);
      my $day2 = int($two / 48);
      my $t1 = $one % 48;
      my $t2 = $two % 48;
      if ($best[$i][2] != $last) {
        $alt = ($alt + 1) % 2;
        $last = $best[$i][2];
        print "</div><div class='alt$alt'>";
      }
      my $title = "$days[$day1] " . int($t1 / 2) . ($t1 % 2 ? ':30' : ':00')
           . " and $days[$day2] " . int($t2 / 2) . ($t2 % 2 ? ':30' : ':00')
           . ' UTC';
      print "<div class='aTime'><span title='$title'>";
      print
        join(' / ',
            map { my $x = ($_ >= 0 ? "+$_" : $_);
                  my $_one = ($one + $_ * 2) % (48 * 7);
                  my $_two = ($two + $_ * 2) % (48 * 7);
                  my $_t1 = $_one % 48;
                  my $_t2 = $_two % 48;
                  my $_d1 = int($_one / 48);
                  my $_d2 = int($_two / 48);
                  "<span class='nosplit'>$days[$_d1] " . int($_t1 / 2) . ($_t1 % 2 ? ':30' : ':00')
                   . " and $days[$_d2] " . int($_t2 / 2) . ($_t2 % 2 ? ':30' : ':00')
                   . " <span class='tz'>UTC$x</span></span>" }
                @rtzs);
      print "</div>\n<div class='aTimeDesc'>\n";
      for (my $pr1 = 4; $pr1 >= 0; $pr1--) {
        for (my $pr2 = $pr1; $pr2 >= 0; $pr2--) {
          my $desc = $pr1 == $pr2 ? $prs[$pr1] : $prs[$pr1] . '/' . $prs[$pr2];
          my @matching;
          for my $name (@who) {
            my $x1 = $prefs->{$name}[($one + $tzs->{$name} * 2) % (48 * 7)];
            my $x2 = $prefs->{$name}[($two + $tzs->{$name} * 2) % (48 * 7)];
            if ($x1 == $pr1 && $x2 == $pr2 || $x1 == $pr2 && $x2 == $pr1) {
              push @matching, $name;
            }
          }
          print "<div><b>$desc</b> for " . join(', ', @matching) . "</div>\n"
            if @matching;
        }
      }
      print "</div>\n";
    }
    print "</div>\n";
  }
  bottom();
}

/*
 * Telcon time planner
 * Copyright (C) 2007-2008 Cameron McCormack <[EMAIL PROTECTED]>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

body, input {
  font-size: 11pt;
  font-family: 'Trebuchet MS', sans-serif;
}
table {
  border-collapse: collapse;
  border-bottom: 2px solid black;
}
th {
  vertical-align: top;
  border: 2px solid black;
  background: darkkhaki;
}
td, th {
  padding: 0.5em;
}
td {
  text-align: center;
  cursor: default;
  font-size: 9pt;
}
.a {
}
.a, .b {
  border: 2px solid black;
  -moz-user-select: none;
}
.b {
}
.dh {
  width: 6em;
}
.control, .controlRight {
  background: #ccf;
  padding: 0.5em;
  margin-top: 0.5em;
}
.controlRight {
  float: right;
}
.p {
  padding: 0.5em;
}
dt {
  margin-top: 1em;
}
.score {
  font-size: 90%;
  color: grey;
}
.alt0, .alt1 {
  border-top: 2px solid black;
  margin-top: 1em;
  padding-left: 1em;
  /*border-top: 1px solid #66a;
  border-left: 6px solid #66a;
  padding-left: 1em;
  padding-top: 0.5em;
  padding-bottom: 0.5em;
  margin-bottom: 0.5em;*/
  /*border-left: 4px solid #84f;*/
}
/*.alt1 {
  border-left: 4px solid #fb0;
}*/
.aTime {
  margin-top: 0.5em;
  margin-bottom: 2px;
  font-weight: bold;
}
.aTimeDesc {
  padding-left: 2em;
}
.tz {
  color: red;
  font-size: 85%;
}
.nosplit {
  white-space: nowrap;
}
.aTime > span {
  border-bottom: 1px dotted #aaa;
}

Reply via email to