--- loncom/interface/lonnavmaps.pm 2003/08/07 17:26:44 1.221 +++ loncom/interface/lonnavmaps.pm 2003/09/24 15:02:34 1.233 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.221 2003/08/07 17:26:44 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.233 2003/09/24 15:02:34 matthew Exp $ # # Copyright Michigan State University Board of Trustees # @@ -46,6 +46,7 @@ use strict; use Apache::Constants qw(:common :http); use Apache::loncommon(); use Apache::lonmenu(); +use Apache::lonlocal; use POSIX qw (floor strftime); use Data::Dumper; # for debugging, not always used @@ -61,19 +62,14 @@ my $resObj = "Apache::lonnavmaps::resour # Keep these mappings in sync with lonquickgrades, which uses the colors # instead of the icons. my %statusIconMap = - ( $resObj->NETWORK_FAILURE => '', - $resObj->NOTHING_SET => '', - $resObj->CORRECT => 'navmap.correct.gif', - $resObj->EXCUSED => 'navmap.correct.gif', - $resObj->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif', - $resObj->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif', - $resObj->ANSWER_OPEN => 'navmap.wrong.gif', - $resObj->OPEN_LATER => '', - $resObj->TRIES_LEFT => 'navmap.open.gif', - $resObj->INCORRECT => 'navmap.wrong.gif', - $resObj->OPEN => 'navmap.open.gif', - $resObj->ATTEMPTED => 'navmap.ellipsis.gif', - $resObj->ANSWER_SUBMITTED => 'navmap.ellipsis.gif' ); + ( + $resObj->CLOSED => '', + $resObj->OPEN => 'navmap.open.gif', + $resObj->CORRECT => 'navmap.correct.gif', + $resObj->INCORRECT => 'navmap.wrong.gif', + $resObj->ATTEMPTED => 'navmap.ellipsis.gif', + $resObj->ERROR => '' + ); my %iconAltTags = ( 'navmap.correct.gif' => 'Correct', @@ -111,9 +107,9 @@ sub real_handler { # Handle header-only request if ($r->header_only) { if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); + &Apache::loncommon::content_type($r,'text/xml'); } else { - $r->content_type('text/html'); + &Apache::loncommon::content_type($r,'text/html'); } $r->send_http_header; return OK; @@ -121,9 +117,9 @@ sub real_handler { # Send header, don't cache this page if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); + &Apache::loncommon::content_type($r,'text/xml'); } else { - $r->content_type('text/html'); + &Apache::loncommon::content_type($r,'text/html'); } &Apache::loncommon::no_cache($r); $r->send_http_header; @@ -139,7 +135,7 @@ sub real_handler { } $r->print("\n"); - $r->print("Navigate Course Contents"); + $r->print("".&mt('Navigate Course Contents').""); # ------------------------------------------------------------ Get query string &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register']); @@ -168,23 +164,17 @@ sub real_handler { # See if there's only one map in the top-level, if we don't # already have a filter... if so, automatically display it + # (older code; should use retrieveResources) if ($ENV{QUERY_STRING} !~ /filter/) { my $iterator = $navmap->getIterator(undef, undef, undef, 0); - my $depth = 1; - $iterator->next(); - my $curRes = $iterator->next(); + my $curRes; my $sequenceCount = 0; my $sequenceId; - while ($depth > 0) { - if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $iterator->END_MAP()) { $depth--; } - + while ($curRes = $iterator->next()) { if (ref($curRes) && $curRes->is_sequence()) { $sequenceCount++; $sequenceId = $curRes->map_pc(); } - - $curRes = $iterator->next(); } if ($sequenceCount == 1) { @@ -202,16 +192,11 @@ sub real_handler { $jumpToFirstHomework = 1; # Find the next homework problem that they can do. my $iterator = $navmap->getIterator(undef, undef, undef, 1); - my $depth = 1; - $iterator->next(); - my $curRes = $iterator->next(); + my $curRes; my $foundDoableProblem = 0; my $problemRes; - while ($depth > 0 && !$foundDoableProblem) { - if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $iterator->END_MAP()) { $depth--; } - + while (($curRes = $iterator->next()) && !$foundDoableProblem) { if (ref($curRes) && $curRes->is_problem()) { my $status = $curRes->status(); if ($curRes->completable()) { @@ -229,8 +214,6 @@ sub real_handler { $ENV{'form.postsymb'} = $curRes->symb(); } } - } continue { - $curRes = $iterator->next(); } # If we found no problems, print a note to that effect. @@ -239,7 +222,7 @@ sub real_handler { } } else { $r->print("" . - "Go To My First Homework Problem    "); + &mt("Go To My First Homework Problem")."    "); } my $suppressEmptySequences = 0; @@ -254,13 +237,13 @@ sub real_handler { $filterFunc = sub { my $res = shift; return $res->completable() || $res->is_map(); }; - $r->print("

Uncompleted Homework

"); + $r->print("

".&mt("Uncompleted Homework")."

"); $ENV{'form.filter'} = ''; $ENV{'form.condition'} = 1; $resource_no_folder_link = 1; } else { $r->print("" . - "Show Only Uncompleted Homework    "); + &mt("Show Only Uncompleted Homework")."    "); } # renderer call @@ -279,7 +262,7 @@ sub real_handler { # user knows there was no error. if ($renderArgs->{'counter'} == 0) { if ($showOnlyHomework) { - $r->print("

All homework is currently completed.

"); + $r->print("

".&mt("All homework is currently completed").".

"); } else { # both jumpToFirstHomework and normal use the same: course must be empty $r->print("

This course is empty.

"); } @@ -349,35 +332,35 @@ sub getDescription { my $status = $res->status($part); if ($status == $res->NETWORK_FAILURE) { - return "Having technical difficulties; please check status later"; + return &mt("Having technical difficulties; please check status later"); } if ($status == $res->NOTHING_SET) { - return "Not currently assigned."; + return &mt("Not currently assigned."); } if ($status == $res->OPEN_LATER) { return "Open " . timeToHumanString($res->opendate($part)); } if ($status == $res->OPEN) { if ($res->duedate($part)) { - return "Due " . timeToHumanString($res->duedate($part)); + return &mt("Due")." " .timeToHumanString($res->duedate($part)); } else { - return "Open, no due date"; + return &mt("Open, no due date"); } } if ($status == $res->PAST_DUE_ANSWER_LATER) { - return "Answer open " . timeToHumanString($res->answerdate($part)); + return &mt("Answer open")." " . timeToHumanString($res->answerdate($part)); } if ($status == $res->PAST_DUE_NO_ANSWER) { - return "Was due " . timeToHumanString($res->duedate($part)); + return &mt("Was due")." " . timeToHumanString($res->duedate($part)); } if ($status == $res->ANSWER_OPEN) { - return "Answer available"; + return &mt("Answer available"); } if ($status == $res->EXCUSED) { - return "Excused by instructor"; + return &mt("Excused by instructor"); } if ($status == $res->ATTEMPTED) { - return "Answer submitted, not yet graded."; + return &mt("Answer submitted, not yet graded"); } if ($status == $res->TRIES_LEFT) { my $tries = $res->tries($part); @@ -390,14 +373,14 @@ sub getDescription { } } if ($res->duedate()) { - return "Due " . timeToHumanString($res->duedate($part)) . + return &mt("Due")." " . timeToHumanString($res->duedate($part)) . " $triesString"; } else { - return "No due date $triesString"; + return &mt("No due date")." $triesString"; } } if ($status == $res->ANSWER_SUBMITTED) { - return 'Answer submitted'; + return &mt('Answer submitted'); } } @@ -447,9 +430,11 @@ sub timeToHumanString { my ($time) = @_; # zero, '0' and blank are bad times if (!$time) { - return 'never'; + return &mt('never'); } - + unless (&Apache::lonlocal::current_language()=~/^en/) { + return localtime($time); + } my $now = time(); my @time = localtime($time); @@ -673,13 +658,13 @@ can't close or open folders when this is =back -=item B: +=item * B: Whether there is discussion on the resource, email for the user, or (lumped in here) perl errors in the execution of the problem. This is the second column in the main nav map. -=item B: +=item * B: An icon for the status of a problem, with five possible states: Correct, incorrect, open, awaiting grading (for a problem where the @@ -687,11 +672,24 @@ computer's grade is suppressed, or the c essay problem), or none (not open yet, not a problem). The third column of the standard navmap. -=item B: +=item * B: A text readout of the details of the current status of the problem, such as "Due in 22 hours". The fourth column of the standard navmap. +=item * B: + +A text readout summarizing the status of the problem. If it is a +single part problem, will display "Correct", "Incorrect", +"Not yet open", "Open", "Attempted", or "Error". If there are +multiple parts, this will output a string that in HTML will show a +status of how many parts are in each status, in color coding, trying +to match the colors of the icons within reason. + +Note this only makes sense if you are I showing parts. If +C is true (see below), this column will not output +anything. + =back If you add any others please be sure to document them here. @@ -859,8 +857,7 @@ sub resource { return 0; } sub communication_status { return 1; } sub quick_status { return 2; } sub long_status { return 3; } - -# Data for render_resource +sub part_status_summary { return 4; } sub render_resource { my ($resource, $part, $params) = @_; @@ -1049,7 +1046,8 @@ sub render_quick_status { if ($resource->is_problem() && !$firstDisplayed) { - my $icon = $statusIconMap{$resource->status($part)}; + + my $icon = $statusIconMap{$resource->simpleStatus($part)}; my $alt = $iconAltTags{$icon}; if ($icon) { $result .= "$linkopen$alt$linkclose\n"; @@ -1098,8 +1096,66 @@ sub render_long_status { return $result; } +# Colors obtained by taking the icons, matching the colors, and +# possibly reducing the Value (HSV) of the color, if it's too bright +# for text, generally by one third or so. +my %statusColors = + ( + $resObj->CLOSED => '#000000', + $resObj->OPEN => '#998b13', + $resObj->CORRECT => '#26933f', + $resObj->INCORRECT => '#c48207', + $resObj->ATTEMPTED => '#a87510', + $resObj->ERROR => '#000000' + ); +my %statusStrings = + ( + $resObj->CLOSED => 'Not yet open', + $resObj->OPEN => 'Open', + $resObj->CORRECT => 'Correct', + $resObj->INCORRECT => 'Incorrect', + $resObj->ATTEMPTED => 'Attempted', + $resObj->ERROR => 'Network Error' + ); +my @statuses = ($resObj->CORRECT, $resObj->ATTEMPTED, $resObj->INCORRECT, $resObj->OPEN, $resObj->CLOSED, $resObj->ERROR); + +use Data::Dumper; +sub render_parts_summary_status { + my ($resource, $part, $params) = @_; + if (!$resource->is_problem()) { return ''; } + if ($params->{showParts}) { + return ''; + } + + my $td = "\n"; + my $endtd = "\n"; + + # If there is a single part, just show the simple status + if ($resource->singlepart()) { + my $status = $resource->simpleStatus('0'); + return $td . "" + . $statusStrings{$status} . "" . $endtd; + } + + # Now we can be sure the $part doesn't really matter. + my $statusCount = $resource->simpleStatusCount(); + my @counts; + foreach my $status(@statuses) { + # decouple display order from the simpleStatusCount order + my $slot = Apache::lonnavmaps::resource::statusToSlot($status); + if ($statusCount->[$slot]) { + push @counts, "" . $statusCount->[$slot] . ' ' + . $statusStrings{$status} . ""; + } + } + + return $td . $resource->countParts() . ' parts: ' . join (', ', @counts) . $endtd; +} + my @preparedColumns = (\&render_resource, \&render_communication_status, - \&render_quick_status, \&render_long_status); + \&render_quick_status, \&render_long_status, + \&render_parts_summary_status); sub setDefault { my ($val, $default) = @_; @@ -1185,18 +1241,13 @@ sub render { # Step three: Ensure the folders are open my $mapIterator = $navmap->getIterator(undef, undef, undef, 1); - my $depth = 1; - $mapIterator->next(); # discard the first BEGIN_MAP - my $curRes = $mapIterator->next(); + my $curRes; my $found = 0; # We only need to do this if we need to open the maps to show the # current position. This will change the counter so we can't count # for the jump marker with this loop. - while ($depth > 0 && !$found) { - if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $mapIterator->END_MAP()) { $depth--; } - + while (($curRes = $mapIterator->next()) && !$found) { if (ref($curRes) && $curRes->symb() eq $here) { my $mapStack = $mapIterator->getStack(); @@ -1210,8 +1261,6 @@ sub render { } $found = 1; } - - $curRes = $mapIterator->next(); } } @@ -1249,15 +1298,11 @@ sub render { # Note this does not take filtering or hidden into account... need # to be fixed? my $mapIterator = $navmap->getIterator(undef, undef, $filterHash, 0); - my $depth = 1; - $mapIterator->next(); - my $curRes = $mapIterator->next(); + my $curRes; my $foundJump = 0; my $counter = 0; - while ($depth > 0 && !$foundJump) { - if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $mapIterator->END_MAP()) { $depth--; } + while (($curRes = $mapIterator->next()) && !$foundJump) { if (ref($curRes)) { $counter++; } if (ref($curRes) && $jump eq $curRes->symb()) { @@ -1268,8 +1313,6 @@ sub render { $args->{'currentJumpIndex'} = $counter; $foundJump = 1; } - - $curRes = $mapIterator->next(); } my $showParts = setDefault($args->{'showParts'}, 1); @@ -1288,15 +1331,15 @@ sub render { $result.='Key:  '; if ($navmap->{LAST_CHECK}) { $result .= - ' New discussion since '. + ' '.&mt('New discussion since').' '. strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). '  '. - ' New message (click to open)

'. + ' '.&mt('New message (click to open)').'

'. ''; } else { $result .= '  '. - ' Discussions'. - '   New message (click to open)'. + ' '.&mt('Discussions').''. + '   '.&mt('New message (click to open)'). ''; } @@ -1307,11 +1350,11 @@ sub render { if ($condition) { $result.="Close All Folders"; + "\">".&mt('Close All Folders').""; } else { $result.="Open All Folders"; + "\">".&mt('Open All Folders').""; } $result .= "

\n"; } @@ -1347,7 +1390,7 @@ sub render { $it->{FIRST_RESOURCE}, $it->{FINISH_RESOURCE}, {}, undef, 1); - $depth = 0; + my $depth = 0; $dfsit->next(); my $curRes = $dfsit->next(); while ($depth > -1) { @@ -1379,9 +1422,6 @@ sub render { my $displayedJumpMarker = 0; # Set up iteration. - $depth = 1; - $it->next(); # discard initial BEGIN_MAP - $curRes = $it->next(); my $now = time(); my $in24Hours = $now + 24 * 60 * 60; my $rownum = 0; @@ -1389,10 +1429,8 @@ sub render { # export "here" marker information $args->{'here'} = $here; - while ($depth > 0) { - if ($curRes == $it->BEGIN_MAP()) { $depth++; } - if ($curRes == $it->END_MAP()) { $depth--; } - + $args->{'indentLevel'} = -1; # first BEGIN_MAP takes this to 0 + while ($curRes = $it->next()) { # Maintain indentation level. if ($curRes == $it->BEGIN_MAP() || $curRes == $it->BEGIN_BRANCH() ) { @@ -1545,8 +1583,6 @@ sub render { $r->rflush(); } } continue { - $curRes = $it->next(); - if ($r) { # If we have the connection, make sure the user is still connected my $c = $r->connection; @@ -1942,7 +1978,7 @@ sub getById { sub getBySymb { my $self = shift; my $symb = shift; - my ($mapUrl, $id, $filename) = split (/___/, $symb); + my ($mapUrl, $id, $filename) = &Apache::lonnet::decode_symb($symb); my $map = $self->getResourceByUrl($mapUrl); return $self->getById($map->map_pc() . '.' . $id); } @@ -2018,7 +2054,7 @@ sub parmval_real { unless ($symb) { return ''; } my $result=''; - my ($mapname,$id,$fn)=split(/\_\_\_/,$symb); + my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); # ----------------------------------------------------- Cascading lookup scheme my $rwhat=$what; @@ -2184,18 +2220,9 @@ sub retrieveResources { my @resources = (); # Run down the iterator and collect the resources. - my $depth = 1; - $it->next(); - my $curRes = $it->next(); - - while ($depth > 0) { - if ($curRes == $it->BEGIN_MAP()) { - $depth++; - } - if ($curRes == $it->END_MAP()) { - $depth--; - } - + my $curRes; + + while ($curRes = $it->next()) { if (ref($curRes)) { if (!&$filterFunc($curRes)) { next; @@ -2208,8 +2235,6 @@ sub retrieveResources { } } - } continue { - $curRes = $it->next(); } return @resources; @@ -2285,6 +2310,13 @@ new branch. The possible tokens are: =over 4 +=item * B: + +The iterator has returned all that it's going to. Further calls to the +iterator will just produce more of these. This is a "false" value, and +is the only false value the iterator which will be returned, so it can +be used as a loop sentinel. + =item * B: A new map is being recursed into. This is returned I the map @@ -2315,12 +2347,33 @@ consisting entirely of empty resources e ending resource, will cause a lot of BRANCH_STARTs and BRANCH_ENDs, but only one resource will be returned. +=head2 Normal Usage + +Normal usage of the iterator object is to do the following: + + my $it = $navmap->getIterator([your params here]); + my $curRes; + while ($curRes = $it->next()) { + [your logic here] + } + +Note that inside of the loop, it's frequently useful to check if +"$curRes" is a reference or not with the reference function; only +resource objects will be references, and any non-references will +be the tokens described above. + +Also note there is some old code floating around that trys to track +the depth of the iterator to see when it's done; do not copy that +code. It is difficult to get right and harder to understand then +this. They should be migrated to this new style. + =back =cut # Here are the tokens for the iterator: +sub END_ITERATOR { return 0; } sub BEGIN_MAP { return 1; } # begining of a new map sub END_MAP { return 2; } # end of the map sub BEGIN_BRANCH { return 3; } # beginning of a branch @@ -2406,13 +2459,13 @@ sub new { # prime the recursion $self->{$firstResourceName}->{DATA}->{$valName} = 0; - my $depth = 0; - $iterator->next(); + $iterator->next(); my $curRes = $iterator->next(); - while ($depth > -1) { - if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $iterator->END_MAP()) { $depth--; } - + my $depth = 1; + while ($depth > 0) { + if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } + if ($curRes == $iterator->END_MAP()) { $depth--; } + if (ref($curRes)) { # If there's only one resource, this will save it # we have to filter empty resources from consideration here, @@ -2446,8 +2499,8 @@ sub new { $curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth; if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;} } - } continue { - $curRes = $iterator->next(); + + $curRes = $iterator->next(); } } @@ -2468,6 +2521,7 @@ sub new { $self->{MAX_DEPTH} = $maxDepth; $self->{STACK} = []; $self->{RECURSIVE_ITERATOR_FLAG} = 0; + $self->{FINISHED} = 0; # When true, the iterator has finished for (my $i = 0; $i <= $self->{MAX_DEPTH}; $i++) { push @{$self->{STACK}}, []; @@ -2485,6 +2539,10 @@ sub new { sub next { my $self = shift; + if ($self->{FINISHED}) { + return END_ITERATOR(); + } + # If we want to return the top-level map object, and haven't yet, # do so. if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0}) { @@ -2544,6 +2602,7 @@ sub next { $self->{CURRENT_DEPTH}--; return END_BRANCH(); } else { + $self->{FINISHED} = 1; return END_MAP(); } } @@ -3040,9 +3099,9 @@ sub symb { my $self=shift; (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; my $symbSrc = &Apache::lonnet::declutter($self->src()); - return &Apache::lonnet::declutter( - $self->navHash('map_id_'.$first)) + my $symb = &Apache::lonnet::declutter($self->navHash('map_id_'.$first)) . '___' . $second . '___' . $symbSrc; + return &Apache::lonnet::symbclean($symb); } sub title { my $self=shift; @@ -3478,6 +3537,11 @@ sub multipart { return $self->countParts() > 1; } +sub singlepart { + my $self = shift; + return $self->countParts() == 1; +} + sub responseType { my $self = shift; my $part = shift; @@ -3565,8 +3629,7 @@ sub extractParts { my @otherChunks = @partChunks[$i+1..$#partChunks]; my $responseId = join('_', @otherChunks); push @{$responseIdHash{$partIdSoFar}}, $responseId; - $responseTypeHash{$partIdSoFar} = $responseType; - last; + push @{$responseTypeHash{$partIdSoFar}}, $responseType; } } } @@ -3881,6 +3944,7 @@ sub status { # dimension and 5 entries on the other, which we want to colorize, # plus network failure and "no date data at all". + #if ($self->{RESOURCE_ERROR}) { return NETWORK_FAILURE; } if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; } my $suppressFeedback = lc($self->parmval("problemstatus", $part)) eq 'no'; @@ -3927,7 +3991,7 @@ sub status { if ($completionStatus == INCORRECT || $completionStatus == INCORRECT_BY_OVERRIDE) { # and there are TRIES LEFT: if ($self->tries($part) < $self->maxtries($part) || !$self->maxtries($part)) { - return TRIES_LEFT; + return $suppressFeedback ? ANSWER_SUBMITTED : TRIES_LEFT; } return $suppressFeedback ? ANSWER_SUBMITTED : INCORRECT; # otherwise, return orange; student can't fix this } @@ -3936,6 +4000,96 @@ sub status { return OPEN; } +sub CLOSED { return 23; } +sub ERROR { return 24; } + +=pod + +B + +Convenience method B provides a "simple status" for the resource. +"Simple status" corresponds to "which icon is shown on the +Navmaps". There are six "simple" statuses: + +=over 4 + +=item * B: The problem is currently closed. (No icon shown.) + +=item * B: The problem is open and unattempted. + +=item * B: The problem is correct for any reason. + +=item * B: The problem is incorrect and can still be +completed successfully. + +=item * B: The problem has been attempted, but the student +does not know if they are correct. (The ellipsis icon.) + +=item * B: There is an error retrieving information about this +problem. + +=back + +=cut + +# This hash maps the composite status to this simple status, and +# can be used directly, if you like +my %compositeToSimple = + ( + NETWORK_FAILURE() => ERROR, + NOTHING_SET() => CLOSED, + CORRECT() => CORRECT, + EXCUSED() => CORRECT, + PAST_DUE_NO_ANSWER() => INCORRECT, + PAST_DUE_ANSWER_LATER() => INCORRECT, + ANSWER_OPEN() => INCORRECT, + OPEN_LATER() => CLOSED, + TRIES_LEFT() => OPEN, + INCORRECT() => INCORRECT, + OPEN() => OPEN, + ATTEMPTED() => ATTEMPTED, + ANSWER_SUBMITTED() => ATTEMPTED + ); + +sub simpleStatus { + my $self = shift; + my $part = shift; + my $status = $self->status($part); + return $compositeToSimple{$status}; +} + +=pod + +B will return an array reference containing, in +this order, the number of OPEN, CLOSED, CORRECT, INCORRECT, ATTEMPTED, +and ERROR parts the given problem has. + +=cut + +# This maps the status to the slot we want to increment +my %statusToSlotMap = + ( + OPEN() => 0, + CLOSED() => 1, + CORRECT() => 2, + INCORRECT() => 3, + ATTEMPTED() => 4, + ERROR() => 5 + ); + +sub statusToSlot { return $statusToSlotMap{shift()}; } + +sub simpleStatusCount { + my $self = shift; + + my @counts = (0, 0, 0, 0, 0, 0, 0); + foreach my $part (@{$self->parts()}) { + $counts[$statusToSlotMap{$self->simpleStatus($part)}]++; + } + + return \@counts; +} + =pod B