floormap.in 24.2 KB
Newer Older
1
2
3
4
5
6
7
8
#!/usr/bin/perl -w
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2004 University of Utah and the Flux Group.
# All rights reserved.
#
use English;
use Getopt::Std;
9
use File::Basename;
10

11
12
13
#
# TODO: Deal with multiple buildings? Currently defaults to MEB! 
# 
14
sub usage {
15
    print STDERR "Usage: floormap [-t] [-T] [-g] [-o <prefix>] ";
16
17
18
    print STDERR "[-s <scale>] | [-c <map_x>,<_y>] ";
    print STDERR "[-S <last_scale> -C <last_x>,<_y>] -O <last_x_off>,<_y>] ";
    print STDERR "[-f <floor>] [<building>]";
19
    print STDERR "\nor\n";
20
    print STDERR "Usage: floormap [-t] [-T] [-g] [-o <prefix>] ";
21
22
    print STDERR "[-s <scale>] | [-c <map_x>,<_y>] ";
    print STDERR "[-S <last_scale> -C <last_x>,<_y>] -O <last_x_off>,<_y>] ";
23
    print STDERR "[-e <pid,eid>] [<building>]\n";
24
25
    print STDERR "\nor\n";
    print STDERR "Usage: floormap [-k] [-o <prefix>] ";
26
27
    exit(-1);
}
28
29
30
31
32
33
34
35
36
37

# Debugging.
my $debug = 0;
sub dprint($) {
    my ($msg) = @_;
    
    print STDERR $msg
	if ($debug);
}

38
my $optlist  = "df:o:s:c:S:C:O:e:tTakgz";
39
my $notitles = 0;       # This suppresses titles surrounding the output map.
40
my $showany  = 0;	# When showing specific floor, showany is turned on.
41
my $mereuser = 1;
42
my $cleanup  = 0;
43
my $nozoom   = 0;
44
my $ghost    = 0;	#  Overlay ghost nodes from all floors onto each map.
45

46
my $building;
47
48
my $floor;
my $image;
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# These result from clicking on zoom/pan controls outside.
my $scale      = 1;	# From clicking on one of the scale buttons.
my $scale_arg  = 1;     # Scale by half-integer factors: 1=>1, 2=>1.5, 3=>2, etc.
my $map_x      = 0;	# From clicking on the (possibly scaled and offset) map.
my $map_y      = 0;

# Calculated from the scale and click coords.
my $x_offset   = 0;	# Offset on the upper-left corner of the image.
my $y_offset   = 0;
my $curr_x     = 0;	# The UNZOOMED, and hence un-offset, click point.
my $curr_y     = 0;

# We get previous scale, offsets, etc. to help interpret new click coords.
my $last_scale_arg = 1;
my $last_scale = 1;
my $last_x_off = 0;
my $last_y_off = 0;
my $last_x  = 0;  # The UNZOOMED, and hence un-offset, previous click point.
my $last_y  = 0;
my $last_notitles = 0;  # This says there were no titles around the previous images.

71
# The image produced for each floor will be this size, except when using thumbnails.
72
73
74
my $out_width  = 792;
my $out_height = 492;

75
76
77
78
# Max x,y of image to go into state file.
my $max_x = 0;
my $max_y = 0;

79
80
# Unless we suppress titles, there will be a header/trailer around each floor image.
my $head_height = 50;
81
my $head_pointsize = 32;
82
83
my $tail_height = 15;

84
85
86
87
88
89
90
91
92
93
94
95
96
# Thumbnail images are 40% as big.  Use smaller header/trailer as well.
my $thumb_head_height = 25;
my $thumb_head_pointsize = 16;
my $thumb_width = 316;
my $thumb_height = 196;
my $thumb_tail_height = 5;

# Assume non-thumbnail images last time, unless told otherwise by $last_scale == 0.
my $last_head_height = $head_height;
my $last_head_pointsize = $head_pointsize;
my $last_height = $out_height;
my $last_tail_height = $tail_height;

97
98
my $pid;
my $eid;
99
100
my @areamaps   = ();
my %baseimages = ();
101
102
103
104
105
106
my $prefix = "/tmp/floormap";

#
# Configure variables
#
my $TB		= "@prefix@";
107
my $WWWPAGE     = "@TBBASE@/shownode.php3";
108
109
110
111
112
my $ICONDIR     = "$TB/www";

# Load libraries.
use lib '@prefix@/lib';
use libdb;
113
114
115
116
117
118

# See http://www.imagemagick.org/www/perl.html .
# Also man ImageMagick, identify, display, convert, etc.
use Image::Magick;
my $images = new Image::Magick;
my $img_n  = -1;
119

120
121
# Admin people get extra info
$mereuser = 0
122
    if (TBAdmin($UID));
123

124
125
126
127
128
129
130
131
132
133
134
# Ha.
my @floortags = ();
$floortags[1] = "1st floor";
$floortags[2] = "2nd floor";   
$floortags[3] = "3rd floor";
$floortags[4] = "4th floor";
$floortags[5] = "5th floor";
$floortags[6] = "6th floor";
$floortags[7] = "7th floor";
$floortags[8] = "8th floor";
$floortags[9] = "9th floor";
135

136
137
138
139
140
#
# Turn off line buffering on output
#
$| = 1;

141
# Forward declarations.
142
143
144
sub dofloor($$);
sub writefiles($@);
sub adjustmap($$$);
145
146
sub adjust_map_y($$);
sub calc_offsets($$$$$);
147
148
149
150
151
152
153
154
155

#
# Parse command arguments. Once we return from getopts, all that should
# left are the required arguments.
#
%options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
156
157
158
if (defined($options{"a"})) {
    $showany = 1;
}
159
160
161
if (defined($options{"z"})) {
    $nozoom = 1;
}
162
163
164
if (defined($options{"d"})) {
    $debug = 1;
}
165
166
167
if (defined($options{"k"})) {
    $cleanup = 1;
}
168
169
170
if (defined($options{"t"})) {
    $notitles = 1;
}
171
172
173
if (defined($options{"T"})) {
    $last_notitles = 1;
}
174
175
176
if (defined($options{"g"})) {
    $ghost = 1;
}
177
if (defined($options{"f"})) {
178
179
    $floor   = $options{"f"};
    $showany = 1;
180
}
181
182
if (defined($options{"s"})) {
    $scale_arg = $options{"s"};
183
184
185
186
187
188
189
    if ($scale_arg > 0) {
	# Scale by half-integer factors: 1=>1, 2=>1.5, 3=>2, etc.
	$scale = ( $scale_arg + 1 ) / 2;
    }
    else {
	# Scale_arg 0 means to use the 40% size thumbnail images.
	$scale = 0.4;
190
191
192
193
194

	# Skinny down the head/tail heights as well.
	$head_height = $thumb_head_height;
	$head_pointsize = $thumb_head_pointsize;
	$tail_height = $thumb_tail_height;
195
    }
196
197
198
199
    dprint "scale arg $scale_arg, scaling factor $scale\n";
}
if (defined($options{"S"})) {
    $last_scale_arg = $options{"S"};
200
201
202
203
204
205
206
    if ($last_scale_arg > 0) {
	# Scale by half-integer factors: 1=>1, 2=>1.5, 3=>2, etc.
	$last_scale = ( $last_scale_arg + 1 ) / 2;
    }
    else {
	# Scale_arg 0 means to use the 40% size thumbnail images.
	$last_scale = 0.4;
207
208
209
210
211
212

	# Skinny down the head/tail heights as well.
	$last_head_height = $thumb_head_height;
	$last_head_pointsize = $thumb_head_pointsize;
	$last_height = $thumb_height;
	$last_tail_height = $thumb_tail_height;
213
    }
214
215
216
217
218
219
220
    dprint "last_scale arg $last_scale_arg, last scaling factor $last_scale\n";

    # Do this now so $scale is defined when reading in images.
    if (!defined($options{"s"})) {
	# If we didn't specify a new scale, keep it the same as the last time.
	$scale_arg = $last_scale_arg;
	$scale = $last_scale;
221
222
223
224
225
226

	# Skinny down the head/tail heights as well.
	$head_height = $last_head_height;
	$head_pointsize = $last_head_pointsize;
	$tail_height = $last_tail_height;

227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
	dprint "scale from last_scale_arg $last_scale_arg, last_scale $scale\n";
    }
}
if (defined($options{"c"})) {
    if ($options{"c"} =~ /([-\w]*),([-\w]*)/) {
	$map_x = $1;
	$map_y = $2;
        dprint "click point $map_x, $map_y\n";
    }
    else {
	die("*** $0:\n".
	    "    Invalid argument to -c option!\n");
    }
}
if (defined($options{"C"})) {
    if ($options{"C"} =~ /([-\w]*),([-\w]*)/) {
	$last_x = $1;
	$last_y = $2;
        dprint "last click $last_x, $last_y\n";
    }
    else {
	die("*** $0:\n".
	    "    Invalid argument to -C option!\n");
    }
}
if (defined($options{"O"})) {
    if ($options{"O"} =~ /([-\w]*),([-\w]*)/) {
	$last_x_off = $1;
	$last_y_off = $2;
        dprint "last offset $last_x_off, $last_y_off\n";
    }
    else {
	die("*** $0:\n".
	    "    Invalid argument to -C option!\n");
    }
}
263
264
if (defined($options{"o"})) {
    $prefix = $options{"o"};
265
266
267
268
    if ($cleanup) {
	unlink "${prefix}.jpg", "${prefix}.map", "${prefix}.state";
	exit(0);
    }
269
270
}

271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
if (defined($options{"e"})) {
    if ($options{"e"} =~ /([-\w]*),([-\w]*)/) {
	$pid = $1;
	$eid = $2;
    }
    else {
	die("*** $0:\n".
	    "    Invalid argument to -e option!\n");
    }

    #
    # Verify permission to view this experiment.
    #
    if ($UID && !TBAdmin($UID) &&
	! TBExptAccessCheck($UID, $pid, $eid, TB_EXPT_READINFO)) {
	die("*** $0:\n".
	    "    You do not have permission to view $pid/$eid!\n");
    }
289

290
291
292
293
294
295
296
297
298
    #
    # Optional building and floor (see above) for a specific experiment.
    #
    usage()
	if (@ARGV > 1);
    if (@ARGV) {
	$building = $ARGV[0];
    }
}
299
300
elsif (@ARGV == 1) {
    $building = $ARGV[0];
301
}
302

303
304
305
#
# Gather image data from DB.
#
306
307
308
# Scale_arg 0 means to use the 40% size thumbnail images.
my $path_col = ($scale_arg == 0 ? "thumb_path" : "image_path");
my $db_scale = max($scale_arg, 1);
309
my $query_result =
310
    DBQueryFatal("select b.building,b.title,f.floor,f.$path_col ".
311
312
		 "   from buildings as b ".
		 "left join floorimages as f on f.building=b.building ".
313
		 "where f.scale=$db_scale");
314
315
316
317
318
319
320
321
322
323
324
325
326
327

if (!$query_result->numrows) {
    die("*** $0:\n".
	"    There is no building/floor data in the DB!\n");
}

while (my ($building,$title,$floor,$image) = $query_result->fetchrow_array()) {
    ##dprint "building $building, floor $floor, image $image\n";
    if (!exists($baseimages{$building})) {
	$baseimages{$building} = {};
	$baseimages{$building}->{"title"}  = $title;
	$baseimages{$building}->{"floors"} = {};
    }

328
    $image = "$TB/www/floormap/$image"
329
330
331
332
333
334
335
336
337
338
	if (dirname($image) eq ".");
    dprint "image $image\n";

    if (! -e $image) {
	die("*** $0:\n".
	    "    $image does not exist!\n");
    }
    $baseimages{$building}->{"floors"}->{$floor} = $image;
}

339
340
341
342
# Must specify a building with a floor.
if (defined($floor) && !defined($building)) {
    die("*** $0:\n".
	"    Must supply a building name!\n");
343
344
}

345
346
# Building must exist.
if (defined($building) && !exists($baseimages{$building})) {
347
348
    die("*** $0:\n".
	"    No such building: $building\n");
349
}
350
351
352
353
354

#
# If a floor specified, then do just that floor and spit it out.
#
if (defined($floor)) {
355
    if (!exists($baseimages{$building}->{"floors"}->{$floor})) {
356
357
358
	die("No such floor '$floor' in building: $building\n");
    }
    
359
360
361
    my ($floorimage, $areamap) = dofloor($building, $floor);
    $image    = $floorimage;
    @areamaps = ($areamap);
362
363
364

    $max_x = $floorimage->Get('width');
    $max_y = $floorimage->Get('height');
365
366
367
368
369
370
371
}
else {
    #
    # Need to find all the floors in this building and generate them all.
    #
    my @floors = ();

372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
    if (defined($building)) {
	foreach my $floor (sort(keys(%{ $baseimages{$building}->{"floors"} }))) {
	    my ($floorimage, $areamap) = dofloor($building, $floor);

	    push(@floors, [ $floorimage, $areamap ])
		if (defined($floorimage));
	}
    }
    else {
	#
	# XXX We need to be fancier for multiple buildings at some point!
	# Not much of a worry right now.
	#
	foreach my $building (sort(keys(%baseimages))) {
	    my @floorlist = sort(keys(%{ $baseimages{$building}->{"floors"} }));
	    
	    foreach my $floor (@floorlist) {
		my ($floorimage, $areamap) = dofloor($building, $floor);
390

391
392
393
394
		push(@floors, [ $floorimage, $areamap ])
		    if (defined($floorimage));
	    }
	}
395
396
397
398
399
400
401
    }

    #
    # Now generate a superimage from all the base images. We just line
    # them up; nothing fancy at all.
    #
    my $running_y = 0;
402
403
404
    foreach my $ref (@floors) {
	my ($floorimage, $areamap) = @$ref;
	
405
	# Move all the map references down by adjusted amount.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
406
	adjustmap($areamap, 0, $running_y);
407
408
	push(@areamaps, $areamap);
	
409
	$running_y += $floorimage->Get('height');
410
    }
411
412
413
414
415
416
417
418
419
420
    dprint "images ".($img_n+1)."\n";
    ## Append and Coalesce get this error - 
    ##    Image::Magick=ARRAY(0x84116b8) at ./floormap.in line 266.
    ## Instead, do it from the shell after writing the component images below.
    ##$err = $images->Append();
    ##warn "$err" if "$err";
}
writefiles($images->[0], @areamaps);

## Append the component images in the shell, until the Perl function is fixed.
421
dprint "Last image is $img_n.\n";
422
423
424
425
426
427
428
429
430
431
432
if ($img_n > 0) {
    my @args = ("convert", "${prefix}.jpg.*", "-append", "${prefix}.jpg");
    dprint "@args\n";
    system(@args) == 0
	or die "system @args failed: $?";

    ## Clean up the component image files.
    @args = ("/bin/sh", "-c", "rm ${prefix}.jpg.*");
    dprint "@args\n";
    system(@args) == 0
	or die "system @args failed: $?";
433
434
435
436
}
exit(0);

#
437
# Do a floor. Returns the image object and an "areamap". 
438
439
440
441
#
sub dofloor($$)
{
    my ($building, $floor) = @_;
442
    my $isnew = 0;
443
444
445
446

    #
    # Grab the nodes on this floor in this building. We want to know
    # their allocation status so we know what colors to use.
447
    #
448
449
450
    my $query = "select loc.*,r.pid,r.eid,r.vname ".
		"  from location_info as loc ".
		"left join reserved as r on r.node_id=loc.node_id ".
451
452
		"where loc.building='$building' " .
		(defined($pid) ? " and r.pid='$pid' and r.eid='$eid'" : "");
453
454
455
456
457
    my $query_result = DBQueryFatal($query . " and loc.floor='$floor'");

    my $newnodes_query = "select * from new_nodes ".
		         "where building='$building'";
    my $newnodes_result = DBQueryFatal($newnodes_query . " and floor='$floor'");
458

459
460
461
462
463
464
    # When ghosting is turned on, get nodes from all floors to be overlaid.
    my ($ghost_result, $ghost_newnodes_result);
    if ($ghost) {
	$ghost_result = DBQueryFatal($query);
	$ghost_newnodes_result = DBQueryFatal($newnodes_query);
    }
465
466
467
468
469
470
471
472
473
474

    if ($mereuser) {
	if (!$query_result->numrows && !$showany) {
	    return (undef, undef);
	}
    }
    else {
	if (!$query_result->numrows && !$newnodes_result->numrows && !$showany) {
	    return (undef, undef);
	}
475
476
    }

477
478
479
480
481
482
483
484
485
486
487
488
489
    #
    # The area map is indexed by nodeid, and contains a list of the
    # x1,y1,x2,y2 (upper left, lower right) coordinates of the "hot"
    # area.  We have to wait till later to actually generate the map
    # cause the coords might need to be adjusted if creating a floor
    # as part of a building (and the floor image gets moved within a
    # bigger image).
    #
    # XXX We make no attempt to deal with overlapping icons (areamaps).
    # This will eventually lead to confusion and incorrect maps.
    # 
    my $areamap = {};

490
491
492
    #
    # Grab the base image for the floor.
    #
493
    if (! exists($baseimages{$building}->{"floors"}->{$floor})) {
494
495
496
	die("*** $0:\n".
	    "    No base image for $building:$floor!\n");
    }
497
498
499
500
501
502
503
504
505
506
507
508
509
510
    
    my $err = $images->Read($baseimages{$building}->{"floors"}->{$floor});
    if ("$err") {
        die("*** $0:\n".
            "$err\n".
            "    Could not get base ".
            $baseimages{$building}->{"floors"}->{$floor}  ."!\n");
    }
    $img_n++;
    my $baseimage = $images->[$img_n];

    # Figure out where the user clicked.  (Use the upper-left corner if no click yet.)
    # We won't have both scale and click coords, because they are separate controls.
    if (defined($options{"c"})) {
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
	if ($nozoom) {
	    #
	    # All we want is the crosshair.
	    # 
	    $curr_x = $map_x;
	    $curr_y = $map_y;
	}
	else {
	    # Back out the influence of the previously applied scale and
	    # offsets, so it is as if we are always clicking onto an image
	    # that is not scaled or offset.
	    my $image_x = $map_x;
	    my $image_y = adjust_map_y($map_y, $last_notitles);
	    dprint "recent click coords $image_x, $image_y ($map_y)\n";
	    $curr_x = int(($image_x + $last_x_off) / $last_scale);
	    $curr_y = int(($image_y + $last_y_off) / $last_scale);
	    dprint "new unzoomed center $curr_x, $curr_y\n";
	}
529
530
531
532
    } else {
	$curr_x = $last_x;
	$curr_y = $last_y;
    }
533

534
535
536
537
538
539
540
541
542
543
    # Set up for zoom (scale) and pan (offset center).  Get the offset to the
    # upper-left corner of the output image, based on the desired center point.
    my $in_width = $baseimage->Get('width');
    my $in_height = $baseimage->Get('height');
    dprint "input size $in_width, $in_height\n";
    ($x_offset, $y_offset) = 
	calc_offsets($scale, $curr_x, $curr_y, $in_width, $in_height);
    dprint "new offsets $x_offset, $y_offset\n";

    # Crop the correct rectangle out of the input image.
544
545
546
    my $this_width = min($in_width, $out_width);
    my $this_height = min($in_height, $out_height);
    $err = $baseimage->Crop(width=>$out_width, height=>$this_height, 
547
548
549
550
			    x=>$x_offset, y=>$y_offset);
    warn "$err" if "$err";

    # Draw the selected node locations and labels. 
551
552
553
    # Green dot means node is free or owned by pid/eid.
    # Red dot mean node is down.
    # Blue dot means node is allocated
554
555
556
557
    # Gold dot means node is new.
    my $DOT_RAD = 5;	# Offset from center to edge of dot.
    my $LX    = -10;    # Offset from center of dot to label.
    my $LY    = 15;
558
559
560
561
562
563
564
    foreach my $isnew (0, 1) {
	my $table;
	
	if ($isnew) {
	    next
		if ($mereuser);
	    
565
	    $table = ($ghost ? $ghost_newnodes_result : $newnodes_result);
566
567
	}
	else {
568
	    $table = ($ghost ? $ghost_result : $query_result);
569
570
571
572
	}
	
	while (my $rowref = $table->fetchrow_hashref()) {
	    my $nodeid = $rowref->{"node_id"};
573
574
	    my $x      = int($rowref->{"loc_x"} * $scale - $x_offset);
	    my $y      = int($rowref->{"loc_y"} * $scale - $y_offset);
575
576
577
578
579
	    my $rpid   = $rowref->{"pid"};
	    my $reid   = $rowref->{"eid"};
	    my $label  = $nodeid;
	    my $newid  = ($isnew ? $rowref->{"new_node_id"} : 0);

580
            my $color;
581
	    if ($isnew) {
582
		$color = 'gold'
583
584
585
586
587
		}
	    elsif ((!defined($pid) && !(defined($rpid))) ||
		   (defined($pid) && defined($rpid) && $pid eq $rpid)) {
		# Without -e option, green means node is free.
		# With -e option, green means node belongs to experiment. 
588
		$color = 'limegreen';
589
590
	    }
	    elsif ($rpid eq NODEDEAD_PID() and $reid eq NODEDEAD_EID()) {
591
		$color = 'red';
592
593
	    }
	    else {
594
		$color = 'blue';
595
	    }
596
            my $x2 = $x + $DOT_RAD;
597
598
599
600
601
602
603
604
	    if ($ghost && $rowref->{"floor"} != $floor) { 
		$err = $baseimage->Draw(stroke=>$color, strokewidth=>'1.5',
					primitive=>'circle', points=>"$x,$y $x2,$y");
	    }
	    else {
		$err = $baseimage->Draw(fill=>$color, 
					primitive=>'circle', points=>"$x,$y $x2,$y");
	    }
605
606
607
608
            warn "$err" if "$err";
            $err = $baseimage->Annotate(fill=>'black', x=>$x+$LX, y=>$y+$LY, 
					text=>"$label");
            warn "$err" if "$err";            
609
610

	    my $tmp = {};
611
612
613
614
	    $tmp->{"X1"} = $x - $DOT_RAD;
	    $tmp->{"Y1"} = $y - $DOT_RAD;
	    $tmp->{"X2"} = $x + $DOT_RAD;
	    $tmp->{"Y2"} = $y + $DOT_RAD;
615
616
	    $tmp->{"ISNEW"} = $newid;
	    $areamap->{$nodeid} = $tmp;
617
	}
618
    }
619

620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
    # Add a crosshair if the user specified a center point.
    if ($curr_x || $curr_y) {
	my $CROSS_SIZE = 12;	# Length of each arm of the crosshair.
	my $x0 = int($curr_x * $scale - $x_offset);
	my $y0 = int($curr_y * $scale - $y_offset);
	my $x1 = $x0 - $CROSS_SIZE;
	my $x2 = $x0 + $CROSS_SIZE;
	my $y1 = $y0 - $CROSS_SIZE;
	my $y2 = $y0 + $CROSS_SIZE;
	dprint "cross at $x0,$y0, lft $x1, rt $x2, top $y1, bot $y2\n";
	$err = $baseimage->Draw(stroke=>'red', primitive=>'line',
				points=>"$x1,$y0, $x2,$y0");
	warn "$err" if "$err";
	$err = $baseimage->Draw(stroke=>'red', primitive=>'line',
				points=>"$x0,$y1, $x0,$y2");
	warn "$err" if "$err";
    }

638
639
    if (!$notitles) {
	#
640
	# We want to stick in a label for the floor. To do that we need to make
641
642
	# some white space at the top by expanding the image, and moving it down.
	# 
643
644
645
646
647
	my $floor_label = $baseimages{$building}->{"title"} . 
	    " - "  . $floortags[$floor];
	dprint "head label $floor_label\n";
        $err = $baseimage->Border(fill=>'white', width=>0, height=>$head_height);
        warn "$err" if "$err";
648
        $err = $baseimage->Annotate(fill=>'black', x=>10, y=>$head_height*0.8, 
649
                                    font=>"/usr/testbed/lib/arial.ttf", 
650
				    pointsize=>$head_pointsize,
651
652
653
654
                                    text=>"$floor_label");
	warn "$err" if "$err";            

        # Border adds to both the top and the bottom.  Crop some of it away.
655
        my $y1 = $head_height + $this_height;
656
        my $y2 = $y1 + $tail_height;
657
        $err = $baseimage->Crop(width=>$this_width, height=>$y2);
658
659
660
        warn "$err" if "$err";
        # Fill in a black rectangle at the bottom for a separator.
        $err = $baseimage->Draw(fill=>'black', primitive=>'rectangle',
661
                                points=>"0,$y1, $this_width,$y2");
662
        warn "$err" if "$err";
663
664

	# Have to adjust the maps cause we just moved everything.
665
	adjustmap($areamap, 0, $head_height);
666
667
    }
    
668
669
670
    return ($baseimage, $areamap);
}

671
672
673
674
675
676
677
#
# Common code to adjust a Y coordinate from an image click into a map Y coordinate.
# The image may have multiple floor maps, with optional headers and trailers.
#
sub adjust_map_y($$) {
    my ($_y, $_notitles) = @_;
    
678
679
680
    my $map_height = $last_height + 
	($_notitles ? 0 : $last_head_height + $last_tail_height);
    my $raw_map_y = ($_y % $map_height) - ($_notitles ? 0 : $last_head_height);
681
    # Avoid out-of-bound coords if the header or trailer is clicked.
682
    my $map_y = max(0, min($raw_map_y, $last_height));
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
    dprint "adjust_map_y map_height $map_height, " .
	"_y $_y, raw_map_y $raw_map_y, map_y $map_y\n";

    return $map_y;
}

# 
# Common code to calculate the image offsets from the upper-left corner, given the
# desired image scale, UNZOOMED center point, and the dimensions of the source image.
# 
# At scale 1, the offsets will always be zero because we have to show the whole image.
# At higher scales, there is room in the middle of the image where we are not up
# against the edges, so we can offset to center the clicked point on the map.  Close
# to the edges, the image edges limit the offsets.
# 
sub calc_offsets($$$$$) {
    my ($scale, $curr_x, $curr_y, $in_width, $in_height) = @_;

701
702
703
    return (0, 0)
	if ($nozoom);

704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
    my $scaled_click_x = $curr_x * $scale;
    my $scaled_click_y = $curr_y * $scale;

    # Offset limited by the scaled image size; scale 1 will always have zero offsets.
    if ($scaled_click_x > $out_width/2) {
        my $click_limit = $in_width - $out_width/2;
        if ($scaled_click_x > $click_limit) {
            dprint "right limit $click_limit\n";
            $x_offset = $in_width-$out_width;       # Against the right edge.
        }
        else {
            $x_offset = $scaled_click_x - ($out_width/2) # In the middle.
        }
    } else {
	dprint "left limit 0\n";
	my $x_offset = 0;                           # Against the left edge.
    }

    if ($scaled_click_y > $out_height/2) {
        $click_limit = $in_height - $out_height/2;
        if ($scaled_click_y > $click_limit) {
            dprint "bottom limit $click_limit\n"; 
            $y_offset = $in_height-$out_height;     # Against the bottom edge.
        }
        else {
            $y_offset = $scaled_click_y - ($out_height/2) # In the middle.
        }
    } else {
	dprint "top limit 0\n";
	my $y_offset = 0;                           # Against the top edge.
    }

    return (int($x_offset), int($y_offset));
}

739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
#
# Adjust map coordinates moving everything by x,y amount. This is used
# when building an image of multiple maps.
#
sub adjustmap($$$)
{
    my ($mapref, $x, $y) = @_;

    foreach my $nodeid (keys(%{ $mapref })) {
	$mapref->{$nodeid}->{"X1"} += $x;
	$mapref->{$nodeid}->{"Y1"} += $y;
	$mapref->{$nodeid}->{"X2"} += $x;
	$mapref->{$nodeid}->{"Y2"} += $y;
    }
}

#
# Take a list of the areamaps created in the above function, and dump them
# to the output file so that the web page can grab them.
#
sub writefiles($@)
{
    my ($image, @maps) = @_;

763
764
765
766
767
768
769
    # Image.
    my $err = $images->Write("${prefix}.jpg");
    if ("$err") {
        die("*** $0:\n".
            "$err\n".
            "    Could not open ${prefix}.jpg for writing!\n");
    }
770
	
771
772
    # Areamap.
    if (!open(MAP, "> ${prefix}.map")) {
773
	unlink("${prefix}.jpg");
774
775
776
777
778
779
780
781
782
783
784
785
786
	die("*** $0:\n".
	    "    Could not open ${prefix}.map for writing!\n");
    }
    print MAP "<MAP NAME=floormap>\n";

    foreach my $mapref (@maps) {
	my %map = %{ $mapref };
	
	foreach my $nodeid (keys(%map)) {
	    my $x1 = $map{$nodeid}->{"X1"};
	    my $y1 = $map{$nodeid}->{"Y1"};
	    my $x2 = $map{$nodeid}->{"X2"};
	    my $y2 = $map{$nodeid}->{"Y2"};
787
788
789
790
	    my $isnew = $map{$nodeid}->{"ISNEW"};
	    my $link  = ($isnew ?
			 "newnode_edit.php3?id=${isnew}" :
			 "shownode.php3?node_id=${nodeid}");
791
792

	    print MAP "<AREA SHAPE=RECT COORDS=\"$x1,$y1,$x2,$y2\" ".
793
		"HREF=\"${link}\">\n\n";
794
	}
795
    }
796
797
    print MAP "</MAP>\n";
    close(MAP);
798
	
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
    # HTML items save state.  They are included into the form by PHP and returned to
    # us by HTML-get as page arguments when the map or zoom buttons are clicked on.
    if (!open(STATE, "> ${prefix}.state")) {
	unlink("${prefix}.jpg");
	die("*** $0:\n".
	    "    Could not open ${prefix}.state for writing!\n");
    }
    print STATE "  <input type=\"hidden\" name=\"last_scale\" " .
	"value=\"$scale_arg\">\n";
    print STATE "  <input type=\"hidden\" name=\"last_x_off\" " .
	"value=\"$x_offset\">\n";
    print STATE "  <input type=\"hidden\" name=\"last_y_off\" " .
	"value=\"$y_offset\">\n";
    # Note that the click point is in UNZOOMED, and hence un-offset, coordinates.
    print STATE "  <input type=\"hidden\" name=\"last_x\" " .
	"value=\"$curr_x\">\n";
    print STATE "  <input type=\"hidden\" name=\"last_y\" " .
	"value=\"$curr_y\">\n";
817
818
    print STATE "  <input type=\"hidden\" name=\"last_ghost\" " .
	"value=\"$ghost\">\n";
819
820
821
822
    if ($notitles) {
	print STATE "  <input type=\"hidden\" name=\"last_notitles\" " .
	    "value=\"$notitles\">\n";
    }
823
824
825
826
827
    print STATE "  <input type=\"hidden\" name=\"max_x\" " .
	"value=\"$max_x\">\n";
    print STATE "  <input type=\"hidden\" name=\"max_y\" " .
	"value=\"$max_y\">\n";
    
828
    close(STATE);
829
}