science-ation/admin/exhibithall_sa.php

699 lines
19 KiB
PHP

<?
/*
* This file is part of the 'Science Fair In A Box' project
* SFIAB Website: http://www.sfiab.ca
*
* Copyright (C) 2010 David Grant <dave@lightbox.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, version 2.
*
* 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
?>
<?
require_once ('../common.inc.php');
require_once ('judges.inc.php'); /* for getJudgingEligibilityCode() */
require_once ('anneal.inc.php');
if ($_SERVER['SERVER_ADDR']) {
echo 'This script must be run from the command line';
exit;
}
$action = '';
switch ($argv[1]) {
case '--images':
$action = 'images';
break;
case '--pn':
$action = 'pn';
break;
}
// function TRACE() { }
// function TRACE_R() { }
function TRACE($str)
{
print ($str);
}
function TRACE_R($array)
{
print_r($array);
}
function point_rotate($x, $y, $deg)
{
/* Use - orienttaiotn because rotation is always done from dest->src */
$r = deg2rad(-$deg);
return array(round($x * cos($r) - $y * sin($r), 6), round($x * sin($r) + $y * cos($r), 6));
}
function point_translate($x, $y, $dx, $dy)
{
return array($x + $dx, $y + $dy);
}
function is_point_in_object($x, $y, $o)
{
/* Translate the point to the object origin */
list($x, $y) = point_translate($x, $y, -$o['x'], -$o['y']);
/* Rotate the point to the object's frame of reference */
list($x, $y) = point_rotate($x, $y, -$o['orientation']);
/* Is it within the object now ? */
if (abs($x) <= $o['w2'] && abs($y) <= $o['h2'])
return true;
return false;
}
function queue_new()
{
return array('head' => NULL, 'tail' => NULL);
}
function grid_path_cmp($a, $b)
{
/* This must return an integer! Strange-things(tm) happen if it doesn't */
if ($a['distance'] == $b['distance'])
return 0;
return ($a['distance'] < $b['distance']) ? -1 : 1;
}
function grid_path_check(&$i_eh, &$queue, &$loc, &$end, $ix, $iy)
{
$next_loc = &$i_eh[$ix][$iy];
// TRACE("Checking next loc($ix,$iy) ({$next_loc['x']},{$next_loc['y']})\n");
/* Don't revisit anything */
if ($next_loc['visited'] == true) {
// TRACE(" Already Visited.\n");
return false;
}
$next_loc['visited'] = true;
if (count($next_loc['ids']) != 0) {
// TRACE(" Object occupying this gridloc.\n");
/* There's something here, can't do anything */
return false;
}
$next_loc['distance'] = distance($next_loc['x'], $next_loc['y'], $end['x'], $end['y']);
$next_loc['path_length'] = $loc['path_length'] + 1;
// TRACE(" distance={$next_loc['distance']}, path_length={$next_loc['path_length']}\n");
/* Add to processing queue in order */
array_push($queue, $next_loc);
}
function grid_path($src, $dst)
{
global $exhibithall;
$i_eh = &$exhibithall[$src['exhibithall_id']];
$start = &$i_eh[$src['front_grid_ix']][$src['front_grid_iy']];
$end = &$i_eh[$dst['front_grid_ix']][$dst['front_grid_iy']];
// TRACE("Path ({$start['x']},{$start['y']}) -> ({$end['x']},{$end['y']})\n");
/* Clean out temp data */
for ($x = 0; $x < $i_eh['grid_w']; $x++) {
for ($y = 0; $y < $i_eh['grid_h']; $y++) {
$i_eh[$x][$y]['visited'] = false;
}
}
// print_r($i_eh);
/* Seed the exploration queue */
$queue = array();
$start['distance'] = distance($start['x'], $start['y'], $end['x'], $end['y']);
$start['path_length'] = 0;
$start['visited'] = true;
array_push($queue, $start);
while (1) {
if (count($queue) == 0)
break;
// print_r($queue);
/* Dequeue head */
$loc = array_shift($queue);
/* Cut it off after a long walk */
if ($loc['path_length'] > 25)
break;
// TRACE("Dequeue: ({$loc['x']},{$loc['y']})\n");
/* Is this our destionation ? */
if ($loc['x'] == $end['x'] && $loc['y'] == $end['y']) {
// TRACE("Found destination, path_length={$loc['path_length']}\n");
return $loc['path_length'];
}
/* All 4 directions */
if ($loc['ix'] > 0)
grid_path_check($i_eh, $queue, $loc, $end, $loc['ix'] - 1, $loc['iy']);
if ($loc['ix'] < $i_eh['grid_w'] - 1)
grid_path_check($i_eh, $queue, $loc, $end, $loc['ix'] + 1, $loc['iy']);
if ($loc['iy'] > 0)
grid_path_check($i_eh, $queue, $loc, $end, $loc['ix'], $loc['iy'] - 1);
if ($loc['iy'] < $i_eh['grid_h'] - 1)
grid_path_check($i_eh, $queue, $loc, $end, $loc['ix'], $loc['iy'] + 1);
usort($queue, 'grid_path_cmp');
}
// TRACE("No path found\n");
// exit;
return 100;
}
TRACE("<pre>\n");
/* Load exhibit halls */
$exhibithall = array();
$q = $pdo->prepare("SELECT * FROM exhibithall WHERE type='exhibithall'");
$q->execute();
TRACE("Loading exhibit halls...\n");
while (($r = $q->fetch(PDO::FETCH_ASSOC))) {
$r['divs'] = unserialize($r['divs']);
$r['cats'] = unserialize($r['cats']);
$exhibithall[$r['id']] = $r;
TRACE(" - {$r['name']}\n");
}
/* Load objects */
$objects = array();
$q = $pdo->prepare("SELECT * FROM exhibithall WHERE type='wall' OR type='project'");
$q->execute();
TRACE("Loading objects...\n");
while (($r = $q->fetch([PDO::FETCH_ASSOC]))) {
$r['divs'] = unserialize($r['divs']);
$r['cats'] = unserialize($r['cats']);
$objects[$r['id']] = $r;
}
TRACE(count($objects) . " objects loaded.\n");
/* Compute stuff */
foreach ($objects as $oid => $o) {
$objects[$oid]['w2'] = $o['w'] / 2;
$objects[$oid]['h2'] = $o['h'] / 2;
}
/* The grid size is the smallest object dimension */
$grid_size = 100;
foreach ($objects as $oid => $o) {
if ($grid_size > $o['w'])
$grid_size = $o['w'];
if ($grid_size > $o['h'])
$grid_size = $o['h'];
}
$grid_size /= 2;
TRACE("Grid size: {$grid_size}m\n");
// print_r($exhibithall);
// print_r($objects);
$div = array();
TRACE("Loading Project Divisions...\n");
$q = $pdo->prepare("SELECT * FROM projectdivisions WHERE year='{$config['FAIRYEAR']}' ORDER BY id");
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
$divshort[$r->id] = $r->division_shortform;
$div[$r->id] = $r->division;
TRACE(" {$r->id} - {$div[$r->id]}\n");
}
TRACE("Loading Project Age Categories...\n");
$cat = array();
$q = $pdo->prepare("SELECT * FROM projectcategories WHERE year=? ORDER BY id");
$q->execute([$config['FAIRYEAR']]);
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
$catshort[$r->id] = $r->category_shortform;
$cat[$r->id] = $r->category;
TRACE(" {$r->id} - {$r->category}\n");
}
TRACE("Loading Projects...\n");
$projects = array();
$q = $pdo->prepare("SELECT projects.* FROM projects, registrations
WHERE
projects.year=?
AND registrations.id = projects.registrations_id
" . getJudgingEligibilityCode());
$q->execute([$config['FAIRYEAR']]);
while ($p = $q->fetch(PDO::FETCH_OBJ)) {
$qq = $pdo->prepare("SELECT grade,schools_id FROM students WHERE registrations_id=?");
$qq->execute([$p->registrations_id]);
$num_students = $qq->rowCouunt();
$grade = 0;
$schools_id = 0;
while ($s = $qq->fetch(PDO::FETCH_ASSOC)) {
if ($s['grade'] > $grade) {
$grade = $s['grade'];
$schools_id = $s['schools_id'];
}
}
$projects[$p->id] = array(
'projects_id' => $p->id,
'div' => $p->projectdivisions_id,
'cat' => $p->projectcategories_id,
'grade' => $grade,
'schools_id' => $schools_id,
'req_electricity' => $p->req_electricity,
'projectnumber' => $p->projectnumber,
'floornumber' => $p->floornumber,
'num_students' => $num_students
);
}
TRACE(count($projects) . " projects loaded.\n");
if ($action == 'pn') {
TRACE("Generating Project Numbers from Floor Locations...\n");
foreach ($projects as $p) {
$c = $catshort[$p['cat']];
$d = $divshort[$p['div']];
$n = sprintf('%03d', $p['floornumber']);
$pn = "$c $n $d";
TRACE("Project {$p['projects_id']} at loc {$p['floornumber']}: $pn\n");
$q = $pdo->prepare("UPDATE projects SET projectnumber=? WHERE id=?");
$q->execute([$pn,$p['projects_id']]);
}
TRACE("Done.\n");
exit;
}
/* Assign objects to grid locations */
foreach ($exhibithall as &$i_eh) {
TRACE("Assigning objects to grid locations for {$i_eh['name']}...\n");
$ix = 0;
$i_eh['grid_w'] = 0;
$i_eh['grid_h'] = 0;
for ($x = 0; $x <= $i_eh['w']; $x += $grid_size, $ix++) {
if ($ix <= $i_eh['grid_w'])
$i_eh['grid_w'] = $ix + 1;
$iy = 0;
for ($y = 0; $y <= $i_eh['h']; $y += $grid_size, $iy++) {
if ($iy <= $i_eh['grid_h'])
$i_eh['grid_h'] = $iy + 1;
/* Initialize element if required */
if (!is_array($i_eh[$ix][$iy])) {
$i_eh[$ix][$iy] = array('x' => $x, 'ix' => $ix,
'y' => $y, 'iy' => $iy,
'ids' => array(),
'project_front' => 0);
}
/* Scan all objects */
foreach ($objects as $oid => $o) {
if ($o['exhibithall_id'] != $i_eh['id'])
continue;
if (is_point_in_object($x, $y, $o)) {
$i_eh[$ix][$iy]['ids'][] = $oid;
}
}
}
}
TRACE("Grid locations: {$i_eh['grid_w']}x{$i_eh['grid_h']}\n");
}
TRACE("Done.\n");
function distance($x1, $y1, $x2, $y2)
{
return sqrt(($x1 - $x2) * ($x1 - $x2) + ($y1 - $y2) * ($y1 - $y2));
}
TRACE("Computing gridlocation of front of projects...\n");
foreach ($objects as $oid => $o) {
if ($o['type'] != 'project')
continue;
/* Get a pointer to the exhibit hall */
$i_eh = &$exhibithall[$o['exhibithall_id']];
/* Compute the xy of the front (that's the bottom edge of the unrotate project) */
$fx = 0;
$fy = $o['h2'];
// TRACE("Front orig: ($fx,$fy)\n");
/* Rotate the point */
list($fx, $fy) = point_rotate($fx, $fy, $o['orientation']);
// TRACE("Front rotate by {$o['orientation']}: ($fx,$fy) \n");
/* Translate it to the proper position of the object */
list($fx, $fy) = point_translate($fx, $fy, $o['x'], $o['y']);
// TRACE("Front: ($fx,$fy) $grid_size\n");
/* Snap to grid offsets */
$gx = intval($fx / $grid_size); // * $grid_size;
$gy = intval($fy / $grid_size); // * $grid_size;
// TRACE("Search grid around $gx, $gy\n");
/*
* Search around that grid for a free spot, closest to $fx,$fy,
* with no objects and no object_front
*/
$smallest_d = $i_eh['w'];
$smallest_ix = 0;
$smallest_iy = 0;
$found = false;
for ($x = $gx - 1; $x <= $gx + 1; $x++) {
for ($y = $gy - 1; $y <= $gy + 1; $y++) {
// TRACE("At ($x, $y) :\n");
// print_r($i_eh[$x][$y]);
if (count($i_eh[$x][$y]['ids']))
continue;
if ($i_eh[$x][$y]['project_front'] > 0)
continue;
/* Check distance */
$d = distance($i_eh[$x][$y]['x'], $i_eh[$x][$y]['y'], $fx, $fy);
if ($d < $smallest_d) {
$smallest_d = $d;
$smallest_ix = $x;
$smallest_iy = $y;
$found = true;
}
}
}
if ($found == false) {
echo "ERROR: couldn't find project front for:\n";
print_r($o);
exit;
}
$i_eh[$smallest_ix][$smallest_iy]['project_front'] = $oid;
$objects[$oid]['front_x'] = $fx;
$objects[$oid]['front_y'] = $fy;
$objects[$oid]['front_grid_ix'] = $smallest_ix;
$objects[$oid]['front_grid_iy'] = $smallest_iy;
}
TRACE("Done.\n");
switch ($action) {
case 'images':
exhibithall_images();
exit;
}
/* Compute closest projects to each project */
$project_distance = 100 / $grid_size;
foreach ($objects as $oid => $o) {
if ($o['type'] != 'project')
continue;
TRACE("Computing paths for {$o['name']}...\n");
/* Get a pointer to the exhibit hall */
$i_eh = &$exhibithall[$o['exhibithall_id']];
/* Starting grid location */
$grid_start = &$i_eh[$o['front_grid_ix']][$o['front_grid_iy']];
/* Path to all other objects in the same exhibit hall */
foreach ($objects as $d_oid => $d_o) {
if ($d_oid == $oid)
continue;
if ($o['exhibithall_id'] != $d_o['exhibithall_id'])
continue;
$d = grid_path($o, $d_o);
$objects[$oid]['nearby_projects'][] = array('distance' => $d, 'id' => $d_oid);
if ($d < $project_distance)
$project_distance = $d;
}
/* Use the grid_path_cmp to sort the projects based on 'distance' */
usort($objects[$oid]['nearby_projects'], 'grid_path_cmp');
}
TRACE("Project Distance: {$project_distance} hops\n");
/* Compute project distances */
foreach ($objects as $oid => $o) {
if ($o['type'] != 'project')
continue;
foreach ($objects[$oid]['nearby_projects'] as &$nearby_project) {
$nearby_project['project_distance'] = $nearby_project['distance'] / $project_distance;
}
}
/* Build a list of floor objects for the annealer */
$floor_objects = array();
$x = 0;
foreach ($objects as $oid => $o) {
if ($o['type'] != 'project')
continue;
$objects[$oid]['floor_object_offset'] = $x; /* The same as the annealer bucket id */
$floor_objects[$x++] = &$objects[$oid];
}
/*
* Project floor location cost:
* - Keep divisions together
* - keep grades together
* - a project should have one of the same school nearby or adjacent, but not a lot nearby
*/
function project_cost(&$annealer, $bucket_id, $ids)
{
global $floor_objects, $projects, $exhibithall, $objects;
$cost = 0;
/* Get the floor object */
$o = &$floor_objects[$bucket_id];
/* The exhibit hall */
$eh = &$exhibithall[$o['exhibithall_id']];
if (count($ids) == 0) {
// TRACE("No items in bucket, returning 0.\n");
return 0;
}
if (count($ids) > 1) {
echo "More than one item in bucket! Bug somewhere.\n";
exit;
}
/* Get the project info */
$p = &$projects[$ids[0]];
$school_match = 0;
$div_match = 0;
$grade_match = 0;
$x = 0;
// TRACE("Cost for bucket $bucket_id...\n");
$min = $p['grade'];
$max = $p['grade'];
foreach ($o['nearby_projects'] as &$n) {
/* Get the nearby project object */
$nearby_o = &$objects[$n['id']];
// TRACE(" Scanning nearby location {$n['id']} (distance={$n['distance']})\n");
// print_r($nearby_o);
$nearby_bucket_id = $nearby_o['floor_object_offset'];
$nearby_bkt = &$annealer->bucket[$nearby_bucket_id];
// TRACE(" Bucket id: {$nearby_bucket_id} with ".count($nearby_bkt)." items\n");
if (count($nearby_bkt) == 0)
continue;
$nearby_p = &$projects[$nearby_bkt[0]];
/* Only consider closest 5 projects for school */
if ($nearby_p['schools_id'] == $p['schools_id']) {
if ($x < 5)
$school_match++;
}
/* Closest 5 projects for divs */
if ($nearby_p['div'] == $p['div']) {
if ($x < 5)
$div_match++;
}
/* Closest 10 for grade variance */
if ($x < 10) {
if ($nearby_p['grade'] < $min)
$min = $nearby_p['grade'];
if ($nearby_p['grade'] > $max)
$max = $nearby_p['grade'];
}
$x++;
if ($x == 10)
break;
}
if ($school_match == 0) {
// TRACE(" No school nearby\n");
$cost += 5;
}
if ($school_match > 2) {
// TRACE(" Too many schools bunched up\n");
$cost += 2 * ($school_match - 1);
}
if ($div_match < 2) {
// TRACE(" No divs nearby\n");
$cost += 20;
}
if ($div_match > 4) {
// TRACE(" Divs bunching up\n");
$cost += 10 * ($div_match - 4);
}
if ($max - $min > 0) {
/* Don't want bunching up grades eitehr */
// TRACE(" Grades too spread out\n");
$cost += 50 * ($max - $min);
}
/* Make sure this project is allowed in this exhibit hall */
if (!in_array($p['div'], $eh['divs']))
$cost += 1000;
if (!in_array($p['cat'], $eh['cats']))
$cost += 1000;
/* Make sure this project is allowed in this floor object too */
if (count($o['divs']) > 0 && !in_array($p['div'], $o['divs']))
$cost += 1000;
if (count($o['cats']) > 0 && !in_array($p['cat'], $o['cats']))
$cost += 1000;
/* Match electricity */
if ($p['req_electricity'] == 'yes' && $o['has_electricity'] == 'no') {
$cost += 1000;
}
// TRACE("Cost for bucket $bucket_id = $cost\n");
return $cost;
}
function project_bucket_ids($annealer, $bucket_id)
{
global $floor_objects, $objects;
$recompute_ids = array($bucket_id);
/* Get the floor object */
$o = &$floor_objects[$bucket_id];
/* Find the 10 closest projects */
$x = 0;
foreach ($o['nearby_projects'] as &$n) {
$nearby_o = &$objects[$n['id']];
$recompute_ids[] = $nearby_o['floor_object_offset'];
$x++;
if ($x == 15)
break;
}
return $recompute_ids;
}
$e = 10 * ($config['effort'] / 1000) * pow(count($projects), 1.3333);
$project_ids = array_keys($projects);
// array_splice($project_ids, 20);
$a = new annealer(count($floor_objects), 125, $e, 0.9,
project_cost, $project_ids);
$a->set_max_items_per_bucket(1);
// $a->set_delta_cost_bucket_ids_callback(project_bucket_ids);
$a->anneal();
for ($x = 0; $x < $a->num_buckets; $x++) {
$bkt = $a->bucket[$x];
if (count($bkt) > 1) {
TRACE("Assigned more than one project to bucket $x\n");
exit;
}
if (count($bkt) == 0)
continue;
/* Get the project id in this bucket */
$projects_id = array_shift($bkt);
echo "p:$projects_id, n:{$floor_objects[$x]['floornumber']}\n";
/* Get the floor object for the same bucket and floor number */
$projects[$projects_id]['floornumber'] = $floor_objects[$x]['floornumber'];
}
print_r($projects);
/* Assign floor numbers */
$q = $pdo->prepare("UPDATE projects SET floornumber=0 WHERE year=?");
$q->execute([$config['FAIRYEAR']]);
foreach ($projects as $pid => $p) {
$q = $pdo->prepare("UPDATE projects SET floornumber=? WHERE id=?");
$q->execute([$p['floornumber'],$pid]);
TRACE("Project $pid => Floor number {$p['floornumber']}\n");
}
TRACE('</pre>');
function exhibithall_images()
{
global $exhibithall, $objects, $projects;
/* Assign project IDs to objects */
foreach ($objects as $oid => $o) {
foreach ($projects as $pid => $p) {
if ($p['floornumber'] == $o['floornumber']) {
$objects[$oid]['projects_id'] = $pid;
break;
}
}
}
foreach ($exhibithall as &$i_eh) {
$i = imagecreatetruecolor($i_eh['w'] * 100, $i_eh['h'] * 100);
$c_grey = imagecolorallocate($i, 128, 128, 128);
$c_white = imagecolorallocate($i, 255, 255, 255);
$c_black = imagecolorallocate($i, 0, 0, 0);
// Fill the background with the color selected above.
imagefill($i, 0, 0, $c_white);
imagerectangle($i, 0, 0, $i_eh['w'] * 100 - 1, $i_eh['h'] * 100 - 1, $c_black);
for ($ix = 0; $ix <= $i_eh['grid_w']; $ix++) {
for ($iy = 0; $iy <= $i_eh['grid_h']; $iy++) {
$l = $i_eh[$ix][$iy];
if (count($l['ids']) > 0) {
imageellipse($i, $l['x'] * 100, $l['y'] * 100, 1, 1, $c_black);
} else {
imageellipse($i, $l['x'] * 100, $l['y'] * 100, 1, 1, $c_grey);
}
}
}
foreach ($objects as $oid => $o) {
if ($o['exhibithall_id'] != $i_eh['id'])
continue;
list($x1, $y1) = point_rotate(-$o['w2'], -$o['h2'], $o['orientation']);
list($x2, $y2) = point_rotate($o['w2'], $o['h2'], $o['orientation']);
imagerectangle($i, ($o['x'] + $x1) * 100, ($o['y'] + $y1) * 100, ($o['x'] + $x2) * 100, ($o['y'] + $y2) * 100, $c_black);
$p = $projects[$o['projects_id']];
imagestring($i, 4, $o['x'] * 100 - 30, $o['y'] * 100 - 35, "{$o['floornumber']} ({$p['projects_id']})", $c_black);
imagestring($i, 4, $o['x'] * 100 - 30, $o['y'] * 100 - 20, "gr:{$p['grade']} ", $c_black);
$d = $divshort[$p['div']];
imagestring($i, 4, $o['x'] * 100 - 30, $o['y'] * 100 - 5, "d:$d ({$p['div']})", $c_black);
imagestring($i, 4, $o['x'] * 100 - 30, $o['y'] * 100 + 10, "s:{$p['schools_id']}", $c_black);
imagestring($i, 4, $o['x'] * 100 - 30, $o['y'] * 100 + 25, "e:{$p['req_electricity']}", $c_black);
}
imagepng($i, "./eh-{$i_eh['id']}.png");
}
}
?>