Next / Previous / Contents / TCC Help System / NM Tech homepage

11. class TaxonPhotoSet: The taxonomic tree node

Each instance of this class is a container for all the photos that depict members of a specific taxon. Instances are arrange in the form of a tree. The root node is a container for the entire catalog, and represents the root taxon of birds, Class Aves.

Here is the interface. The constructor takes one argument: the taxon related to this node.

catweb
# - - - - -   c l a s s   T a x o n P h o t o S e t   - - - - -

class TaxonPhotoSet:
    """Represents all photos contained in a specific taxon.

      Exports:
        TaxonPhotoSet ( taxon ):
          [ taxon is a txny.Taxon instance ->
              return a new, empty TaxonPhotoSet for that taxon ]
        .taxon:       [ as passed to constructor; read-only ]
        .nContained:  [ number of photos in self's subtree ]
        .nReferred:   [ number of photos referred to self's taxon ]

The .addArchImage() method is used to load catalog entries into the tree. For each photo, this method is called once for each kind of bird in the photo.

Note the precondition that the specified birdId must be referred to a taxon that is either self or a descendant of self. It simplifies the logic of adding a new photo to know that the photo goes somewhere in the correct subtree. It is no burden on the caller who adds the photo to the root of the tree, since all birds are in class Aves. See Section 11.2, “TaxonPhotoSet.addArchImage(): Add one photo”.

catweb
        .addArchImage ( birdId, archImage ):
          [ birdId is referred to a taxon contained in self.taxon ->
              (birdId is a composite bird form code as an
              abbr.BirdId object) and
              (archImage is an image catalog entry as a
              archindex.ArchImage instance) ->
                self  :=  self with archImage added under birdId ]

The .walk() method walks the tree, starting at the given node, and then recursively visiting its descendants. This method is called to generate the various taxonomic headings on the index page. See Section 11.5, “TaxonPhotoSet.walk(): Walk the tree”.

catweb
        .walk():
          [ generate self, followed by the descendants of self,
            in preorder traversal ]

Because the presentation of a taxon with only one form is different than the presentation of a multi-form taxon, the .nForms() method allows the caller to determine the number of forms. See Section 11.6, “TaxonPhotoSet.nForms(): Number of contained forms”.

catweb
        .nForms():    [ returns the number of forms in self ]

The .genForms() method generates the contained FormPhotoSet objects in order. See Section 11.7, “TaxonPhotoSet.genForms(): Generate referred FormPhotoSet objects”.

catweb
        .genForms():
          [ generate the FormPhotoSet objects in self, in
            ascending order by inverted English name ]

Here are the internals of the instance. Each node maintains two counters. The .nContained attribute counts all the photos ever added to this node's subtree. The .nReferred attribute is a count of only those photos referred to this node's taxon.

Each TaxonPhotoSet instance is a container for two kinds of objects: child taxa, and FormPhotoSet instances for any photos that may be referred to this taxon.

One of the author's favorite standard Python tricks serves nicely to structure both these container relationships: we keep the contained values in a dictionary, and access them using a key that happens to sort in the desired access order.

The standard Python trick is: use the dictionary's .keys() method to extract a list of keys; sort that list; and then go through the dictionary in order using that list.

Dictionary .childMap contains the child TaxonPhotoSet elements as values. In order to access the children in taxonomic order, we use the txKey attribute of the taxon as the key, which is a string of digits that orders taxa in phylogenetic order.

catweb
      State/Internals:
        .__childMap:
          [ a dictionary whose values are the child TaxonPhotoSet
            nodes of self, and each corresponding key is the
            .txKey attribute of self's taxon ]

The desired order of form names within a taxon is alphabetical order by the inverted English name.

catweb
        .__formMap:
          [ a dictionary whose values are the FormPhotoSet
            objects holding photos referred to self.taxon,
            and each corresponding key is the inverted
            English name for that form ]
    """

11.1. TaxonPhotoSet.__init__(): Constructor

The constructor does not do much. It stores the specified taxon, zeroes the counts of contained photos, and sets up the internal data structures.

catweb
# - - -   T a x o n P h o t o S e t . _ _ i n i t _ _   - - -

    def __init__ ( self, taxon ):
        """Constructor for TaxonPhotoSet"""
        self.taxon  =  taxon
        self.nContained  =  0
        self.nReferred  =  0
        self.__childMap  =  {}
        self.__formMap  =  {}

11.2. TaxonPhotoSet.addArchImage(): Add one photo

Because multiple forms of birds may appear in one photo, each photo may be added under multiple names. In that case, the .addArchImage() method is called once for each name.

Hence, this method operates on not just a catalog entry (as an ArchImage object from the archindex.py module), but also on a BirdId object (from the abbr.py module) that represents the kind of bird.

catweb
# - - -   T a x o n P h o t o S e t . a d d A r c h I m a g e   - - -

    def addArchImage ( self, birdId, archImage ):
        """Add one photo to the report under a given birdId
        """

Adding a new photo to the structure always starts at the root node, by calling its .addArchImage() method. In general, this operation is recursive. It may involve the addition of multiple new taxa to the tree, starting at the top and terminating at the taxon to which the photo is referred.

Depending on whether the given birdId is referred to self's taxon or not, there are two cases.

  • Basis case: if the birdId is referred to self's taxon, no new child nodes need to be added under this one. First, make sure that self has a FormPhotoSet for this bird name, then add the photo to that FormPhotoSet.

  • Recursive case: if the birdId is referred to a descendant of self, first insure that nodes are added to self's subtree down to the referred taxon's node, then add the photo to that node.

The first order of business is to separate the two cases. The taxon attribute of a BirdId object is the taxon to which that form is referred. The comparison function (.__cmp__()) in the Taxon class compares instances according to their txKey attribute, so we can use the ordinary Python “==” comparison to see if they are the same taxon.

For the internal methods that handle the two cases, see Section 11.3, “TaxonPhotoSet.__addReferred(): Basis case” and Section 11.4, “TaxonPhotoSet.__addContained(): Recursive case”.

catweb
        #-- 1 --
        # [ if self.taxon is the same as birdId.taxon ->
        #     self.nReferred  +:=  1
        #     self.__formMap  :=  self.__formMap with archImage added
        #         under the name from birdId
        #   else ->
        #     self.__childMap  :=  self.__childMap with new descendant
        #         nodes added as necessary down to birdId.taxon,
        #         and archImage added under the name from birdId to
        #         the node for birdId.taxon ]
        if  self.taxon == birdId.taxon:
            #-- 1.1 --
            self.__addReferred ( birdId, archImage )
            self.nReferred  +=  1
        else:
            #-- 1.2 --
            self.__addContained ( birdId, archImage )

The final step is bookkeeping: counting the number of photos in this subtree.

catweb
        #-- 2 --
        self.nContained  +=  1

11.3. TaxonPhotoSet.__addReferred(): Basis case

This method adds the new photo to the forms in self.

catweb
# - - -   T a x o n P h o t o S e t . _ _ a d d R e f e r r e d   - - -

    def __addReferred ( self, birdId, archImage ):
        """Add a catalog entry to one of the FormPhotoSets in self.

          [ (birdId is a BirdId referred to self.taxon) and
            (archImage is a catalog entry as an archindex.ArchImage) ->
              self.__formMap  :=  self.__formMap with archImage added
                  under the name from birdId ]
        """

The first step is to make sure there is a FormPhotoSet in self.__formMap under the name used by birdId. See Section 12, “class FormPhotoSet: All photos for one name”.

catweb
        #-- 1 --
        # [ formName  :=  inverted English name from birdId ]
        formName  =  birdId.engComma()

        #-- 2 --
        # [ if self.__formMap has a key formName ->
        #     formSet  :=  the corresponding value
        #   else ->
        #     self.__formMap  :=  a new FormPhotoSet instance
        #         for formName
        #     formSet  :=  that new FormPhotoSet instance ]
        try:
            formSet  =  self.__formMap[formName]
        except KeyError:
            formSet  =  FormPhotoSet ( birdId )
            self.__formMap[formName]  =  formSet

At this point, formSet is the FormPhotoSet instance to which the new photo is to be added. See Section 12.2, “FormPhotoSet.addArchImage(): Add one photo”.

catweb
        #-- 3 --
        # [ formSet  :=  formSet with archImage added ]
        formSet.addArchImage ( archImage )

11.4. TaxonPhotoSet.__addContained(): Recursive case

This method is used when the form name to be added is not referred to self's taxon. It recursively adds new nodes to self's subtree down to the level where the form name is referred, at which point the basis case adds the photo at that level.

catweb
# - - -   T a x o n P h o t o S e t . _ _ a d d C o n t a i n e d   - - -

    def __addContained ( self, birdId, archImage ):
        """Recursive case: add a photo to a descendant taxon.

          [ (birdId is a BirdId referred to a descendant of
            self.taxon) and
            (archImage is a catalog entry as an archindex.ArchImage) ->
              self.__childMap  :=  self.__childMap with new descendant
                  nodes added as necessary down to birdId.taxon,
                  and archImage added under the name from birdId to
                  the node for birdId.taxon ]            
        """

Each recursive call first ensures that the tree has a node for the child of self containing the new form name, and then it adds the new catalog entry to the child.

The first task is to determine which child taxon contains the new form name. It might be an actual child of self, or it might be an ancestor further down the tree.

catweb
        #-- 1 --
        newTaxon  =  birdId.taxon

        #-- 2 --
        # [ newTaxon is a descendant of self.taxon ->
        #     childTaxon  :=  the child of self.taxon that
        #     contains newTaxon ]
        childTaxon  =  self.taxon.childContaining ( newTaxon )

Next we make sure that self.childMap has a child node for this child.

catweb
        #-- 3 --
        # [ if self.__childMap has a key for childTaxon.txKey ->
        #     childNode  :=  the corresponding value
        #   else ->
        #     self.__childMap [ childTaxon.txKey ]  :=  a new
        #         TaxonPhotoSet for childTaxon
        #     childNode  :=  that same TaxonPhotoSet ]
        try:
            childNode  =  self.__childMap [ childTaxon.txKey ]
        except KeyError:
            childNode  =  TaxonPhotoSet ( childTaxon )
            self.__childMap [ childTaxon.txKey ]  =  childNode

A recursive call to the child node's .addArchImage() method will add the new photo to the correct subtree. See Section 11.2, “TaxonPhotoSet.addArchImage(): Add one photo”.

catweb
        #-- 4 --
        # [ childNode  :=  childNode with archImage added under
        #                  birdId ]
        childNode.addArchImage ( birdId, archImage )

11.5. TaxonPhotoSet.walk(): Walk the tree

This method recursively walks the tree, yielding the current node, followed by its subtrees.

catweb
# - - -   T a x o n P h o t o S e t . w a l k   - - -

    def walk ( self ):
        """Recursive tree walker, yielding nodes in pre-order.
        """

The current node is generated first.

catweb
        #-- 1 --
        yield self

This method specifies that the children are generated in taxonomic order. Since the keys in the self.__childMap dictionary are in phylogenetic order, we extract the key set, sort it, and then recursively generate the subtrees in order by that set.

catweb
        #-- 2 --
        # [ childKeys  :=  a list containing the keys of
        #                  self.__childMap in ascendind order ]
        childKeys  =  self.__childMap.keys()
        childKeys.sort()

        #-- 3 --
        # [ childKeys is a list of keys in self.__childMap ->
        #     generate the nodes from subtrees rooted in the nodes
        #     that are values of self.__childMap, in childKeys order ]
        for  txKey in childKeys:
            childNode  =  self.__childMap[txKey]
            for  node in childNode.walk():
                yield node

Raising StopIteration terminates the generator.

catweb
        #-- 4 --
        raise StopIteration

11.6. TaxonPhotoSet.nForms(): Number of contained forms

This method returns the number of forms referred to self. Since the dictionary self.__formMap has one entry for each referred form, we need only return the cardinality of that dictionary.

catweb
# - - -   T a x o n P h o t o S e t . n F o r m s   - - -

    def nForms ( self ):
        """How many form names are referred to self?
        """
        return  len ( self.__formMap )

11.7. TaxonPhotoSet.genForms(): Generate referred FormPhotoSet objects

This method generates the FormPhotoSet objects contained in self, in ascending order by their inverted English name. The code is straightforward: extract the English names that are the keys in the .__formMap dictionary, sort them, and generate the values from the dictionary in that order.

catweb
# - - -   T a x o n P h o t o S e t . g e n F o r m s   - - -

    def genForms ( self ):
        """Generate self's referred forms.
        """

        #-- 1 --
        # [ keyList  :=  the keys from self.__formMap, sorted ]
        keyList  =  self.__formMap.keys()
        keyList.sort()

        #-- 2 --
        # [ keyList is a list of keys in self.__formMap ->
        #     generate the values from self.__formMap in the
        #     order specified by keyList ]
        for  key in keyList:
            yield self.__formMap[key]

        #-- 3 --
        raise StopIteration

11.8. TaxonPhotoSet.buildPage(): Construct XHTML

This method is called to render one taxon's content on the index page, and also to build all form pages (and links to them) for forms referred to this taxon.

catweb
# - - -   T a x o n P h o t o S e t . b u i l d P a g e   - - -

    def buildPage ( self, parent ):
        """Add one taxon to the index page, and build all its form pages

          [ parent is an xc.Element node ->
              parent  :=  parent with XHTML child nodes added to
                          represent self
              form pages for forms referred to self's taxon  :=
                  content displaying thumbnails and catalog info
                  for forms in self ]
        """

In one case, this method builds nothing at all: if it is a genus-level taxon that has no forms referred to it. See Section 3.1, “The index page” for remarks on this case.

catweb
        #-- 1 --
        # [ if self is a genus-level taxon with no forms referred
        #   to it ->
        #     return
        #   else -> I ]
        if  ( ( self.taxon.rank.depth ==
                self.taxon.txny.hier.genusRank().depth ) and
              ( len(self.__formMap) == 0 ) ):
            return

All the content for this taxon will be wrapped in a div element of class head-R, where R is the code for the taxonomic rank of self's taxon.

catweb
        #-- 2 --
        # [ parent  :=  parent with a div added, tagged with
        #       the heading class for self's taxonomic rank
        #   div  :=  that new div element ]
        div  =  xc.Element ( parent, "div" )
        div["class"]  =  "head-%s" % self.taxon.rank.code

Here we divide the logic into three cases.

  • If no form is referred to this taxon, the name is added directly to the div.

  • If there is only one form referred to this taxon, the name is a link to the form page.

  • If there are multiple forms referred to this taxon, the taxon's name is not a link, but it is followed by lines linking to the various form pages.

If there is a single form in this taxon, the content is the taxonomic heading as a link to the form page for that form. If there are multiple forms, the content starts with the taxonomic heading, followed by one additional div element for each form's name as a link to the corresponding form page.

catweb
        #-- 3 --
        # [ if len(self.__formMap) == 0 ->
        #     div  :=  div + (self's inverted name)
        #   else if len(self.__formMap) <= 1 ->
        #     div  :=  div + (link to form page containing self's
        #              inverted name, and the form name if
        #              different than self's inverted name
        #     form page for self.__formMap's value  :=  content
        #         displaying thumbnails and catalog info for
        #         self.__formMap's value
        #   else ->
        #     div  :=  div + (self's inverted name) +
        #         (links to form pages for forms in self.__formMap)
        #     form pages for self.__formMap values  :=  content
        #         displaying thumbnails and catalog info for
        #         values in self.__formMap ]
        if  len(self.__formMap) == 0:
            self.__webName ( div )
        elif  len(self.__formMap) == 1:
            self.__buildSingleForm ( div )
        else:
            self.__buildMultiForms ( div )

11.9. TaxonPhotoSet.__buildSingleForm(): The taxon has only one form

This method adds the content for one taxon to the index page when there is only one form referred to that taxon. It also builds the form page for that form.

catweb
# - - -   T a x o n P h o t o S e t . _ _ b u i l d S i n g l e F o r m

    def __buildSingleForm ( self, div ):
        """Build index and form content for the single-form case.

          [ (self.__formMap has only one entry) and
            (div is an xc.Element) ->
              div  :=  div + (link to form page containing self's
                       inverted name, and the form name if
                       different than self's inverted name
              form page for self.__formMap's value  :=  content
                  displaying thumbnails and catalog info for
                  self.__formMap's value ]              
        """

To review the XHTML build in this case, refer to Section 4.2, “XHTML for the index page”. The subtree we generate here is rooted in an “a” (hyperlink) element whose href attribute points at the form page. The link text is the scientific name, followed by the inverted English name. If the inverted English name of the sole form differs from the taxon's standard English name, we add the content “(as otherName)”.

First, build the link element.

catweb
        #-- 1 --
        # [ div  :=  div + (a new "a" element)
        #   link  :=  that "a" element ]
        link  =  xc.Element ( div, "a" )

Next we extract the sole entry in self.__formMap. The .keys() method on a dictionary produces a list of keys, a singleton in our case.

catweb
        #-- 2 --
        # [ formSet  :=  the sole value in self.__formMap ]
        formKey  =  self.__formMap.keys()[0]
        formSet  =  self.__formMap[formKey]

At this point, formSet is a FormPhotoSet instance, and its .pathName() method returns the pathname to its form page (which hasn't been built yet).

catweb
        #-- 3 --
        # [ formSet is a FormPhotoSet ->
        #     link  :=  link with an href attribute added whose
        #         value is the path name to formSet's form page ]
        link [ "href" ]  =  formSet.pathName()

Add, as link text, the scientific and English names for self's taxon. Then, if the form's inverted English name is different, add the “(as otherName)” content.

catweb
        #-- 4 --
        # [ link  :=  link + (text of self's names) ]
        self.__webName ( link )

        #-- 5 --
        # [ if  self.taxon's inverted English name differs from
        #   formSet's inverted English name ->
        #     link  :=  link + "(as " + (formSet's inverted
        #               English name) +  ")"
        #   else -> I ]
        formEng  =  formSet.birdId.engComma()
        if  formEng != self.taxon.engComma:
            xc.Text ( link, "(as %s)" % formEng )

All that remains is to generate formSet's form page.

catweb
        #-- 6 --
        # [ form page for formSet  :=  content displaying
        #       thumbnails and catalog data for formSet ]
        formSet.buildPage()

11.10. TaxonPhotoSet.__webName(): Fill in scientific and English names

This method builds the XHTML for the taxon's scientific name and, if defined, its inverted English name. For design notes, see Section 4.2, “XHTML for the index page”.

catweb
# - - -   T a x o n P h o t o S e t . _ _ w e b N a m e   - - -

    def __webName ( self, parent ):
        """Add the scientific and English names to a Web page.

          [ parent is an xc.Element ->
              parent  :=  parent + (self.taxon's scientific name) +
                  (self.taxon's English name, if known) ]              
        """

The principal complication is that we must italicize genus-level and lower-rank names by wrapping them in a span class="genus" element; see Section 5, “Styling with CSS”. The depth attribute of a taxonomic rank is 0 for class, 1 for order, and so on, so we can test the rank depth of self's taxon against the depth of the genus rank to determine whether to italicize.

catweb
        #-- 1 --
        # [ selfDepth  :=  taxonomic rank depth of self.taxon
        #   genusDepth  :=  taxonomic rank depth of the genus
        #       rank of the taxonomy used by self.taxon ]
        selfDepth  =  self.taxon.rank.depth
        genusDepth  =  self.taxon.txny.hier.genusRank().depth

If this rank is genus or deeper, we now add the span element, otherwise we don't. In either case we set the variable textParent to point to the node to which the name text is to be added.

catweb
        #-- 2 --
        # [ if  selfDepth >= genusDepth ->
        #     parent  :=  parent + (a new span element with
        #                 class="genus")
        #     textParent  :=  that new span element
        #   else ->
        #     textParent  :=  parent ]
        if  selfDepth >= genusDepth:
            textParent  =  xc.Element ( parent, "span",
                                        class_="genus" )
        else:
            textParent  =  parent

Next, the scientific name is added to textParent. We add the rank name to ranks above genus level (e.g., “Family Fringillidae”), but omit it for genera and deeper because the italicization tells the reader it's a scientific name.

catweb
        #-- 3 --
        # [ textParent  :=  textParent + (self.taxon's scientific
        #       name) ]
        if  selfDepth < genusDepth:
            xc.Text ( textParent, "%s " % self.taxon.rank.name )
        xc.Text ( textParent, self.taxon.sci )

Finally, display the English name if there is one. The txny.py module sets the .eng attribute to the same as the .sci attribute by default, so in that case, don't redundantly display it. This text is added to parent, not textParent, so that it will be outside the span if present.

catweb
        #-- 4 --
        # [ if self.taxon.eng != self.taxon.sci ->
        #     parent  :=  textParent + ": " + self.taxon.engComma
        if  self.taxon.eng != self.taxon.sci:
            xc.Text ( parent, ": %s" % self.taxon.engComma )

11.11. TaxonPhotoSet.__buildMultiForms(): The taxon has multiple forms

This method generates content for one taxon on the index page for the case where more than one form name is referred to that taxon.

catweb
# - - -   T a x o n P h o t o S e t . _ _ b u i l d M u l t i F o r m s

    def __buildMultiForms ( self, parent ):
        """Build index and form content for the multi-form case.

          [ parent is an xc.Element ->
              parent  :=  parent + (self's inverted name) +
                  (links to form pages for forms in self.__formMap)
              form pages for self.__formMap values  :=  content
                  displaying thumbnails and catalog info for
                  values in self.__formMap ]
        """

First we add the form name to the parent div.

catweb
        #-- 1 --
        # [ parent  :=  parent with self's name added as text ]
        self.__webName ( parent )

The forms referred to this taxon will be displayed in ascending order by inverted English name. This is achieved using the standard Python trick of keeping instances as values in a dictionary, and using a key that sorts in the desired order—in this case, the inverted English name.

catweb
        #-- 2 --
        # [ nameList  :=  keys of self.__formMap, sorted ]
        nameList  =  self.__formMap.keys()
        nameList.sort()

For each key in nameList, the corresponding value is a FormPhotoSet instance. The generation of the index page content, and corresponding photo page, for each form is done by Section 11.12, “TaxonPhotoSet.__buildFormLine(): Build one form link and form page”.

catweb
        #-- 3 --
        # [ parent  :=  parent with divs added containing the
        #       names from self.__formMap in order by nameList,
        #       each as a link to the corresponding value from
        #       self.__formMap
        #   form pages for values in self.__formMap  :=  content
        #       from those values ]
        for  formName in nameList:
          #-- 3 body --
          # [ let  formSet == self.__formMap[formName]
          #   in
          #     parent  :=  parent with a div added containing the
          #         name from formSet and a link to formSet's form page
          #     form page for formSet  :=  content from formSet ]
          self.__buildFormLine ( parent, self.__formMap[formName] )

11.12. TaxonPhotoSet.__buildFormLine(): Build one form link and form page

This method builds the div on the index page containing the link to a form page. It also creates the corresponding form page.

catweb
# - - -   T a x o n P h o t o S e t . _ _ b u i l d F o r m L i n e

    def __buildFormLine ( self, parent, formSet ):
        """Build one form page link and the corresponding form page.

          [ (parent is an xc.Element) and
            (formSet is a FormPhotoSet) ->
              parent  :=  parent with a div added containing the
                 name from formSet and a link to formSet's form page
              form page for formSet  :=  content from formSet ]
        """

The XHTML generated here is described in Section 4.2, “XHTML for the index page”. The only child added to parent is the div element, with class="name-line",

catweb
        #-- 1 --
        # [ parent  :=  parent with a new div element added
        #   div     :=  that new div element ]
        div  =  xc.Element ( parent, "div", class_="name-line" )

The only child of the div is the link (“a”) element that points to the form page.

catweb
        #-- 2 --
        # [ div  :=  div with a new 'a' element added whose href
        #       attribute is the relative URL of formSet's page
        #   link  :=  that 'a' element ]
        link  =  xc.Element ( div, 'a', href=formSet.pathName() )

The link text is the form's inverted English name.

catweb
        #-- 3 --
        # [ link  :=  link with formSet's inverted English name
        #             added as link text ]
        xc.Text ( link, formSet.birdId.engComma() )

Having finished generating all the index page content for this form, now generate the actual form page.

catweb
        #-- 4 --
        # [ form page for formSet  :=  content from formSet ]
        formSet.buildPage()