--- loncom/interface/lonnavmaps.pm 2002/10/28 21:11:39 1.90 +++ loncom/interface/lonnavmaps.pm 2002/11/08 18:35:37 1.98 @@ -2,7 +2,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.90 2002/10/28 21:11:39 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.98 2002/11/08 18:35:37 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -820,7 +820,7 @@ sub new_handle { &Apache::loncommon::no_cache($r); $r->send_http_header; - # Initialize the nav map + # Create the nav map the nav map my $navmap = Apache::lonnavmaps::navmap->new( $ENV{"request.course.fn"}.".db", $ENV{"request.course.fn"}."_parms.db", 1, 1); @@ -837,19 +837,40 @@ sub new_handle { '')); $r->print(''); my $desc=$ENV{'course.'.$ENV{'request.course.id'}.'.description'}; - if (defined($desc)) { $r->print("

$desc

\n"); } + $r->print(''); my $date=localtime; - $r->print("

$date

\n"); - $r->rflush(); + $r->print(''); if ($navmap->{LAST_CHECK}) { - $r->print(' New discussion since '. + $r->print(''); + } else { + $r->print(''); + } + $r->print('
Key:    '. + ' New discussion since '. strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). - '
New message (click to open)

'); + '

  '. + ' New message (click to open)

'. + '

  '. + ' Discussions'. + '   New message (click to open)'. + '
'); + my $condition = 0; + if ($ENV{'form.condition'}) { + $condition = 1; + } + + if ($condition) { + $r->print('Close All Folders'); } else { - $r->print(' Discussions'. - '
New message (click to open)

'); + $r->print('Open All Folders'); } + $r->print('
 '); + $r->rflush(); + + # Now that we've displayed some stuff to the user, init the navmap + $navmap->init(); + # Check that it's defined if (!($navmap->courseMapDefined())) { $r->print('Coursemap undefined.' . @@ -926,36 +947,35 @@ sub new_handle { my $queryAdd = "postdata=" . &Apache::lonnet::escape($currenturl) . "&alreadyHere=1"; - $r->print('Show All Resources

'); - # Begin the HTML table # four cols: resource + indent, chat+feedback, icon, text string $r->print('' ."\n"); - my $condition = 0; - if ($ENV{'form.condition'}) { - $condition = 1; - } - # This needs to be updated to use symbs from the remote, # instead of uris. The changes to this and the main rendering # loop should be obvious. # Here's a simple example of the iterator. # Preprocess the map: Look for current URL, force inlined maps to display + # This currently does very little... + #my $mapEventualIterator = Apache::lonnavmaps::iterator->new($navmap, undef, undef, {}, + #undef, $condition); + my $mapIterator = $navmap->getIterator(undef, undef, {}, 1); my $found = 0; my $depth = 1; + my $currentUrlIndex = 0; # keeps track of when the current resource is found, + # so we can back up a few and put the anchor above the + # current resource $mapIterator->next(); # discard the first BEGIN_MAP my $curRes = $mapIterator->next(); + my $counter = 0; while ($depth > 0) { - if ($curRes == $mapIterator->BEGIN_MAP()) { - $depth++; - } - if ($curRes == $mapIterator->END_MAP()) { - $depth--; - } + if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } + if ($curRes == $mapIterator->END_MAP()) { $depth--; } + + if (ref($curRes)) { $counter++; } my $mapStack = $mapIterator->getStack(); if ($currenturl && !$ENV{'form.alreadyHere'} && ref($curRes) && @@ -963,6 +983,8 @@ sub new_handle { # If this is the correct resource, be sure to # show it by making sure the containing maps # are open. + + $currentUrlIndex = $counter; for my $map (@{$mapStack}) { if ($condition) { @@ -1009,6 +1031,8 @@ sub new_handle { my @backgroundColors = ("#FFFFFF", "#F6F6F6"); my $rowNum = 0; + $counter = 0; + while ($depth > 0) { # If we're in a new style course, and this is a BEGIN_MAP, END_MAP, or # map resource and the stack depth is only one, just plain ignore this resource @@ -1034,12 +1058,10 @@ sub new_handle { if ($curRes == $mapIterator->BEGIN_BRANCH()) { $isNewBranch = 1; } - if ($curRes == $mapIterator->BEGIN_MAP()) { - $depth++; - } - if ($curRes == $mapIterator->END_MAP()) { - $depth--; - } + if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } + if ($curRes == $mapIterator->END_MAP()) { $depth--; } + + if (ref($curRes)) { $counter++; } if ($depth == 1) { $deltadepth = 0; } # we're done shifting, because we're # out of the inlined map @@ -1231,9 +1253,17 @@ sub new_handle { $rowNum++; my $backgroundColor = $backgroundColors[$rowNum % scalar(@backgroundColors)]; - # FIRST COL: The resource indentation, branch icon, and name + # FIRST COL: The resource indentation, branch icon, name, and anchor $r->print("
\n"); + # anchor for current resource... - 5 is deliberate: If it's that + # high on the screen, don't bother focusing on it. Also this will + # print multiple anchors if this is an expanded multi-part problem... + # who cares? + if ($counter == $currentUrlIndex - 5) { + $r->print(''); + } + # print indentation for (my $i = 0; $i < $indentLevel - $deltalevel + $deltadepth; $i++) { $r->print($indentString); @@ -1260,6 +1290,9 @@ sub new_handle { } $r->print(" $curMarkerBegin$title$partLabel $curMarkerEnd $nonLinkedText"); + #$r->print(" TDV:" . $curRes->{DATA}->{TOP_DOWN_VAL}); # temp + #$r->print(" BUV:" . $curRes->{DATA}->{BOT_UP_VAL}); # temp + #$r->print(" DD:" . $curRes->{DATA}->{DISPLAY_DEPTH}); # temp if ($curRes->{RESOURCE_ERROR}) { $r->print(&Apache::loncommon::help_open_topic ("Navmap_Host_Down", @@ -1616,6 +1649,14 @@ sub new { $self->{PARM_HASH} = \%parmhash; $self->{HASH_TIED} = 1; + bless($self); + + return $self; +} + +sub init { + my $self = shift; + # If the course opt hash and the user opt hash should be generated, # generate them if ($self->{GENERATE_COURSE_USER_OPT}) { @@ -1720,10 +1761,6 @@ sub new { } $self->{PARM_CACHE} = {}; - - bless($self); - - return $self; } # Checks to see if coursemap is defined, matching test in old lonnavmaps @@ -1997,6 +2034,288 @@ sub END_BRANCH { return 4; } # end of sub FORWARD { return 1; } # go forward sub BACKWARD { return 2; } +sub min { + (my $a, my $b) = @_; + if ($a < $b) { return $a; } else { return $b; } +} + +sub new { + # magic invocation to create a class instance + my $proto = shift; + my $class = ref($proto) || $proto; + my $self = {}; + + $self->{NAV_MAP} = shift; + return undef unless ($self->{NAV_MAP}); + + # Handle the parameters + $self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource(); + $self->{FINISH_RESOURCE} = shift || $self->{NAV_MAP}->finishResource(); + + # If the given resources are just the ID of the resource, get the + # objects + if (!ref($self->{FIRST_RESOURCE})) { $self->{FIRST_RESOURCE} = + $self->{NAV_MAP}->getById($self->{FIRST_RESOURCE}); } + if (!ref($self->{FINISH_RESOURCE})) { $self->{FINISH_RESOURCE} = + $self->{NAV_MAP}->getById($self->{FINISH_RESOURCE}); } + + $self->{FILTER} = shift; + + # A hash, used as a set, of resource already seen + $self->{ALREADY_SEEN} = shift; + if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} }; + $self->{CONDITION} = shift; + + # Now, we need to pre-process the map, by walking forward and backward + # over the parts of the map we're going to look at. + + # The processing steps are exactly the same, except for a few small + # changes, so I bundle those up in the following list of two elements: + # (direction_to_iterate, VAL_name, next_resource_method_to_call, + # first_resource). + # This prevents writing nearly-identical code twice. + my @iterations = ( [FORWARD(), 'TOP_DOWN_VAL', 'getNext', + 'FIRST_RESOURCE'], + [BACKWARD(), 'BOT_UP_VAL', 'getPrevious', + 'FINISH_RESOURCE'] ); + + my $maxDepth = 0; # tracks max depth + + foreach my $pass (@iterations) { + my $direction = $pass->[0]; + my $valName = $pass->[1]; + my $nextResourceMethod = $pass->[2]; + my $firstResourceName = $pass->[3]; + + my $iterator = Apache::lonnavmaps::DFSiterator->new($self->{NAV_MAP}, + $self->{FIRST_RESOURCE}, + $self->{FINISH_RESOURCE}, + {}, undef, 0, $direction); + + # prime the recursion + $self->{$firstResourceName}->{DATA}->{$valName} = 0; + my $depth = 0; + $iterator->next(); + my $curRes = $iterator->next(); + while ($depth > -1) { + if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } + if ($curRes == $iterator->END_MAP()) { $depth--; } + + if (ref($curRes)) { + my $resultingVal = $curRes->{DATA}->{$valName}; + my $nextResources = $curRes->$nextResourceMethod(); + my $resourceCount = scalar(@{$nextResources}); + + if ($resourceCount == 1) { + my $current = $nextResources->[0]->{DATA}->{$valName} || 999999999; + $nextResources->[0]->{DATA}->{$valName} = min($resultingVal, $current); + } + + if ($resourceCount > 1) { + foreach my $res (@{$nextResources}) { + my $current = $res->{DATA}->{$valName} || 999999999; + $res->{DATA}->{$valName} = min($current, $resultingVal + 1); + } + } + } + if (ref($curRes) && $curRes->is_map() && $direction == FORWARD()) { + my $firstResource = $curRes->map_start(); + my $finishResource = $curRes->map_finish(); + my $newIterator = Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, + $firstResource, + $finishResource, + $self->{FILTER}, + $self->{ALREADY_SEEN}, + $self->{CONDITION}); + } + + # Assign the final val + if (ref($curRes) && $direction == BACKWARD()) { + my $finalDepth = min($curRes->{DATA}->{TOP_DOWN_VAL}, + $curRes->{DATA}->{BOT_UP_VAL}); + + $curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth; + if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;} + } + $curRes = $iterator->next(); + } + } + + # Set up some bookkeeping information. + $self->{CURRENT_DEPTH} = 0; + $self->{MAX_DEPTH} = $maxDepth; + $self->{STACK} = []; + $self->{RECURSIVE_ITERATOR_FLAG} = 0; + + for (my $i = 0; $i <= $self->{MAX_DEPTH}; $i++) { + push @{$self->{STACK}}, []; + } + + # Prime the recursion w/ the first resource + push @{$self->{STACK}->[0]}, $self->{FIRST_RESOURCE}; + $self->{ALREADY_SEEN}->{$self->{FIRST_RESOURCE}->{ID}} = 1; + + bless ($self); + + return $self; +} + +sub next { + my $self = shift; + + if ($self->{RECURSIVE_ITERATOR_FLAG}) { + # grab the next from the recursive iterator + my $next = $self->{RECURSIVE_ITERATOR}->next(); + + # is it a begin or end map? If so, update the depth + if ($next == BEGIN_MAP() ) { $self->{RECURSIVE_DEPTH}++; } + if ($next == END_MAP() ) { $self->{RECURSIVE_DEPTH}--; } + + # Are we back at depth 0? If so, stop recursing + if ($self->{RECURSIVE_DEPTH} == 0) { + $self->{RECURSIVE_ITERATOR_FLAG} = 0; + } + + return $next; + } + + if (defined($self->{FORCE_NEXT})) { + my $tmp = $self->{FORCE_NEXT}; + $self->{FORCE_NEXT} = undef; + return $tmp; + } + + # Have we not yet begun? If not, return BEGIN_MAP and + # remember we've started. + if ( !$self->{STARTED} ) { + $self->{STARTED} = 1; + return $self->BEGIN_MAP(); + } + + # Here's the guts of the iterator. + + # Find the next resource, if any. + my $found = 0; + my $i = $self->{MAX_DEPTH}; + my $newDepth; + my $here; + while ( $i >= 0 && !$found ) { + if ( scalar(@{$self->{STACK}->[$i]}) > 0 ) { + $here = $self->{HERE} = shift @{$self->{STACK}->[$i]}; + $found = 1; + $newDepth = $i; + } + $i--; + } + + # If we still didn't find anything, we're done. + if ( !$found ) { + # We need to get back down to the correct branch depth + if ( $self->{CURRENT_DEPTH} > 0 ) { + $self->{CURRENT_DEPTH}--; + return END_BRANCH(); + } else { + return END_MAP(); + } + } + + # Get to the right level + if ( $self->{CURRENT_DEPTH} > $newDepth ) { + push @{$self->{STACK}->[$newDepth]}, $here; + $self->{CURRENT_DEPTH}--; + return END_BRANCH(); + } + if ( $self->{CURRENT_DEPTH} < $newDepth) { + push @{$self->{STACK}->[$newDepth]}, $here; + $self->{CURRENT_DEPTH}++; + return BEGIN_BRANCH(); + } + + # If we made it here, we have the next resource, and we're at the + # right branch level. So let's examine the resource for where + # we can get to from here. + + # So we need to look at all the resources we can get to from here, + # categorize them if we haven't seen them, remember if we have a new + my $nextUnfiltered = $here->getNext(); + + for (@$nextUnfiltered) { + if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) { + push @{$self->{STACK}->[$_->{DATA}->{DISPLAY_DEPTH}]}, $_; + $self->{ALREADY_SEEN}->{$_->{ID}} = 1; + } + } + + # That ends the main iterator logic. Now, do we want to recurse + # down this map (if this resource is a map)? + if ($self->{HERE}->is_map() && + (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { + $self->{RECURSIVE_ITERATOR_FLAG} = 1; + my $firstResource = $self->{HERE}->map_start(); + my $finishResource = $self->{HERE}->map_finish(); + + $self->{RECURSIVE_ITERATOR} = + Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, + $finishResource, $self->{FILTER}, + $self->{ALREADY_SEEN}, $self->{CONDITION}); + } + + return $self->{HERE}; + +} + +=pod + +The other method available on the iterator is B, which returns an array populated with the current 'stack' of maps, as references to the resource objects. Example: This is useful when making the navigation map, as we need to check whether we are under a page map to see if we need to link directly to the resource, or to the page. The first elements in the array will correspond to the top of the stack (most inclusive map). + +=cut + +sub getStack { + my $self=shift; + + my @stack; + + $self->populateStack(\@stack); + + return \@stack; +} + +# Private method: Calls the iterators recursively to populate the stack. +sub populateStack { + my $self=shift; + my $stack = shift; + + push @$stack, $self->{HERE} if ($self->{HERE}); + + if ($self->{RECURSIVE_ITERATOR_FLAG}) { + $self->{RECURSIVE_ITERATOR}->populateStack($stack); + } +} + +1; + +package Apache::lonnavmaps::DFSiterator; + +# UNDOCUMENTED: This is a private library, it should not generally be used +# by the outside world. What it does is walk through the nav map in a +# depth-first fashion. This is not appropriate for most uses, but it is +# used by the main iterator for pre-processing. It also is able to isolate +# much of the complexity of the main iterator, so the main iterator is much +# simpler. +# There is no real benefit in merging the main iterator and this one into one class... +# all the logic in DFSiterator would need to be replicated, you gain no performance, +# at best, you just make one massively complicated iterator in place of two +# somewhat complicated ones. ;-) - Jeremy + +# Here are the tokens for the iterator, replicated from iterator for convenience: + +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 +sub END_BRANCH { return 4; } # end of a branch +sub FORWARD { return 1; } # go forward +sub BACKWARD { return 2; } + # Params: nav map, start resource, end resource, filter, condition, # already seen hash ref @@ -2072,8 +2391,7 @@ sub next { # Are we using a recursive iterator? If so, pull from that and # watch the depth; we want to resume our level at the correct time. - if ($self->{RECURSIVE_ITERATOR_FLAG}) - { + if ($self->{RECURSIVE_ITERATOR_FLAG}) { # grab the next from the recursive iterator my $next = $self->{RECURSIVE_ITERATOR}->next(); @@ -2137,6 +2455,8 @@ sub next { # Are we at the right depth? If not, close a branch and return # the current resource onto the branch stack + # Note: There seems to be some bugs here, so don't rely + # on this, use the real iterator instead. if (defined($self->{HERE}->{DATA}->{ITERATOR_DEPTH}) && $self->{HERE}->{DATA}->{ITERATOR_DEPTH} < $self->{BRANCH_DEPTH} ) { @@ -2205,20 +2525,14 @@ sub next { my $finishResource = $self->{HERE}->map_finish(); $self->{RECURSIVE_ITERATOR} = - Apache::lonnavmaps::iterator->new ($self->{NAV_MAP}, $firstResource, + Apache::lonnavmaps::DFSiterator->new ($self->{NAV_MAP}, $firstResource, $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, - $self->{CONDITION}); + $self->{CONDITION}, $self->{DIRECTION}); } return $self->{HERE}; } -=pod - -The other method available on the iterator is B, which returns an array populated with the current 'stack' of maps, as references to the resource objects. Example: This is useful when making the navigation map, as we need to check whether we are under a page map to see if we need to link directly to the resource, or to the page. The first elements in the array will correspond to the top of the stack (most inclusive map). - -=cut - sub getStack { my $self=shift;