Summary: pix.js comprises javascript code for displaying photo galleries which may be incorporated into webpages.

It uses a standard (XML) format for photo lists which specifies the attributes of each photo (filename stem, size, title, caption) and the sizes available for display (their scale factors, filename suffices and types).

overview : creating a picture page : URL options : local use : contact me

pix.js provides the javascript which does all the work for HTML pages displaying photos in the manner of this website. It offers the following functions:

pix.js does not prepare your input data: you have to do this yourself. This is because the tasks involved are not suitable for implementation through web browsers. However two related functions are suitable for browsers, and these are provided.

The data preparation for pix.js is fairly onerous. You need to make a version of each image in at least two sizes (thumbnail and a regular size), but I often provide 6 or 7. The XML configuration files may come across as disagreeably “tecky” to naive users.

An advantage of using pix.js is that the photos are under your own control and you can integrate them with text content (eg. route notes) or with GPS tracks (and especially with routemaster). On the other hand you need to maintain a website of your own which requires a certain amount of knowledge.

pix.js is intended for web pages, but you may access the same pages from your own disc (by file load or the file:// protocol): see below for some guidance.

You should not rely on pix.js without letting me know (see below). At present I regard it as a personal facility and make changes as the need dictates, with only moderate attention to the possible effects on hypothetical other users.

A photo gallery is specified by an XML configuration file (see below). Parallel to it (and preferably in the same directory) you need a 6-line HTML stub, which is simplicity itself and mostly boilerplate.

<html><head>
<meta charset=utf-8> 
<script src="https://www.masterlyinactivity.com/pix.js"></script>
</head><body></body>
<script>pix("list.xml")</script>
</html>

Take this line by line.

 

<html><head>

Boilerplate. Obligatory.

 

<meta charset=utf-8> 

Boilerplate: necessary if you want to use non-Ascii characters from the larger Unicode set. I recommend you always include this line: it can’t do any harm.

 

<script src="https://www.masterlyinactivity.com/pix.js"></script>

Instruction to the browser to incorporate pix.js (which in turn calls for pixlib.js).

 

</head><body></body>

Boilerplate. Obligatory.

 

<script>pix("list.xml")</script>

Invocation of the pix function: the argument is the name of the XML file defining the picture set. It is a URL, so it may be a relative or absolute path, e.g.

pix("http://www.meme.me/sillyphotos/cafepix.xml")

An early version of pix took a different argument, namely a link to your home page; this no longer works.

A subsequent version used a javascript format for picture lists, included as a <script src="..."></script> in the html header. This is no longer supported.

 

</html>

Boilerplate. Obligatory.

The URL of your page will be eg.

https://www.masterlyinactivity.com/capeverde/pix.html

By appending options after a ‘?’ you can force a different view of the pictures. To view an individual picture you use the ‘image’ attribute followed by the image name, e.g.

https://www.masterlyinactivity.com/capeverde/pix.html?image=ribeira

To limit the view to pictures in a particular category, use the ‘cat’ option:

https://www.masterlyinactivity.com/capeverde/pix.html?cat=2017

To force the display of images whose display option is ‘none’, use the ‘mode’ option with an ‘a’ in its value:

https://www.masterlyinactivity.com/capeverde/pix.html?mode=a

Or to limit the display to starred images, use a ‘*’:

https://www.masterlyinactivity.com/capeverde/pix.html?mode=*

Or, finally, to display only the hidden images (i.e. those with display="none"), use an ‘h’:

https://www.masterlyinactivity.com/capeverde/pix.html?mode=h

Inclusion of a ‘v’ in the mode tells pix.js to perform a validation rather than displaying photos.

Options can be concatenated using ampersands:

https://www.masterlyinactivity.com/capeverde/pix.html?image=ribeira&cat=2017&mode=a

When developing a picture gallery you are urged to do so on your own computer, and not to upload to your website until it’s ready. You’ll encounter resistance from your browser, which will refuse to read the local XML configuration file owing to the internet’s stupid ‘same origin policy’. Firefox didn’t complain until fairly recently (2019), but you can placate it by setting the parameter security.fileuri.strict_origin_policy to false in about:config.

Email me at colin·champion&routemaster·app, substituting full stops for the dots and an ampersat for the ampersand. I don’t guarantee to make changes but I’m happy to hear from you.

overview : creating a picture page : URL options : local use : contact me

xml : aliases : contents of the list : images : dividing images into sections : categories : layouts : the size list : sundry variables : examples

The photo list is more complicated. The following is a simple example:

<photolist>
<title>Gran Canaria mountain biking photos</title>
<alias name="l" shape="[980,700]"/>
<alias name="p" shape="[700,980]"/>
<base imagedir="images"/>

<img name="swerve" title="Rocks near the Embalse de los Hornos" shape="p"/>
<img name="ridgepath" title="Ridge path" shape="p"/>
<img name="tracey" title="Tracey on the ridge, Roque Nublo behind" shape="p"/>
<img name="precipice" title="Colin on the ridge" shape="p" 
     caption="keeping well to the right"/>
<img name="artenara" title="Colin on the ridge" shape="p"/>

<sizes>
  <size suffix="@1" scale="10"/>
  <size suffix="@0" scale="5"/>
  <size suffix="@t" scale="2" type="thumb"/>
</sizes>

</photolist>

The photo list is an XML specification of the gallery properties. Aliases can be used to avoid repetition of lengthy parameters. Note that all URLs in the photo list are (logically) relative to the list itself (ie. to the XML file), not relative to some page which makes use of the photo list. You will avoid confusion if you keep the XML parameter file and the HTML stub in the same directory.

I say that URLs are ‘logically’ relative to the list because that is how I intend them to be interpreted. However since you may include HTML snippets, and these may embed URLs in various contexts, I cannot completely enforce consistent handling. If you try to be clever, use absolute URLs.

XML is a format designed to be generated and read by computer programs; it is not ideal for human-generated input (sorry). In particular, formatting errors may be greeted by unintelligible or downright misleading error messages. XML is case-sensitive.

Information in an XML file is provided though a number of tags whose names are preceded by ‘<’ signs and closed by ‘>’ signs. Each tag is accompanied optionally by a set of attributes and optionally by a set of elements.

The attribute whose name is ‘suffix’ and whose value is ‘@1’ will be specified as

suffix="@1"

where the equals sign and the double quotes are part of the notation. The attribute is specified after the tag name but before the closing ‘>’ sign.

The elements governed by a tag comprise character strings and other tags. A tag does not need any elements, in which case it is called an ‘empty-element tag’. An empty-element tag has a solidus before its closing ‘>’. Most tags in photo lists are empty. An example is the size specification which takes the format

<size suffix="@1" scale="10"/>

‘size’ is the tag name, ‘suffix’ and ‘scale’ the attribute names, and ‘@1’ and ‘10’ the values.

If a tag has elements then it occurs in the form

<tag [attributes]>elements</tag>

i.e. there is no solidus preceding the ‘>’ which closes the first mention of the tag, but there is a second mention of the tag in which its name is preceded by a solidus.

There is an entry in the photo list for a page title. This entry has ‘title’ as its tag and the title itself as element, and often has no attributes, so it may be written

<title>Gran Canaria photos</title>

If you want to include non-Ascii characters you may either provide them as raw Unicode or give their numerical codes. The XML reference to a character whose hexadecimal code is abcd is ‘&#xabcd;’.

Aliases are used to abbreviate frequently occurring photo shapes and URL stems. The shape of an image is given as a character string in the format [w,h] where the components are width and height respectively; for instance an image might have attribute rawshape="[3264,2448]". To avoid repeating this string you may define an ‘rl’ alias for shapes as follows:

<alias name="rl" shape="[3264,2448]"/>

(The abbreviation ‘rl’ is supposed to stand for ‘raw landscape’.) You will then be able to specify the attribute as rawshape="rl".

You may also have a number of links to URLs with a common stem, and aliases can be used to abbreviate them and avoid repitition. The syntax is

<alias name="routes" 
       uri="https://www.routemaster.app/?track=https://www.masterlyinactivity.com/routemaster/routes/grancanaria"/>

(Note that the final solidus is not part of the URL but indicates that the tag has no elements. ‘uri’ is a posh name for URLs.)

Whenever an XML item has a URL as an attribute (e.g. href or gps) you may specify it in the form ‘u1,u2’ where the comma is a separator, there are no spaces, u2 is the URL relative to u1, and u1 is the name of a URL alias.

I can then specify the gps track in the photo section for our Artenara ride as

<section title="Valsequillo; the ridge path from Cruz de Tejeda to Artenara" gps="routes,artenara.tcx">

rather than as the equivalent but longer

<section title="Valsequillo; the ridge path from Cruz de Tejeda to Artenara" 
         gps="https://www.routemaster.app/?track=https://www.masterlyinactivity.com/routemaster/routes/grancanaria/artenara.tcx">

The photo list contains specifications of the images, usually divided into sections; it also contains the size list and sundry additional parameters. We look at these in turn.

Images are the main content of the XML list file. An image is specified by the empty-element tag ‘img’ with a number of attributes, listed exhaustively here.

The name is a short character string which you will use to identify a particular photo. If the name you choose is ‘swerve’ then the names of the associated image files will be (eg.) swerve@t.jpg (the thumbnail), swerve@0.jpg (the small version), and swerve@1.jpg (the larger version). These names are made up from the ‘name’, from the suffices of the various sizes, and from the ‘.jpg’ file extension.

The title is a longer string which will be shown above the photo when it is displayed, or will appear in a box if you hover over its thumbnail. Eg.

title="Rocks near the Embalse de los Hornos"

The caption is an additional string which will be shown below the photo. Eg.

caption="This is harder than it looks!"

More generally, the caption is not restricted to flat text and may be an HTML snippet. The XML notation required for this is rather clumsy. The following is an example of its use:

  <img name="traceyatsalineras" title="Tracey on the path below Salineras"
       shape="p" rawshape="rp">
    <caption><![CDATA[This is a <i>road</i> according to the map.]]></caption>
  </img>

In order to supply an HTML caption, it needs to be the text component of a ‘caption’ element rather than an attribute of the ‘img’. The text begins with the sequence

<![CDATA[

which is followed by the HTML and concluded with the sequence

]]>

All this is included between the standard

<caption>...</caption>

Beware the use of relative URLs in the HTML snippet. When I get round to it I will convert relative link hrefs into absolute URLs interpreting them properly (ie. as relative to the XML config file), but other contexts in which URLs may occur (eg. image sources) will be left intact.

This field can be used to request a vertical divider before the display of a thumbnail in the image table. (The divider is omitted if the thumbnail occurs in the first column.) It also determines the visibility of the image. Legal values are

"|" "none" "*" "|none" "|*"

The ‘|’ requests the divider. An image whose visibility is ‘none’ will not be displayed in the table unless the page has the special mode ‘a’. An image whose visibility is ‘*’ will be given special treatement: the starred images will be used to present gallery highlights in routemaster and metagalleries. The table may be limited to starred images by giving it mode ‘*’.

If an image has an overlay, it must be produced for all sizes other than thumbnail (and hithumb), and has the same name as the corresponding image except for having the “.png” extension. (If there is no raw image, you don’t need a raw overlay.) You then specify it in the following format:

<img name="route" title="Route map" shape="[1008,714]"  
       thumbshape="thl" overlay="overlay"/>

The value of the attribute (in this case the character string “overlay”) will be used in generating the image caption (which in this case will say ‘Add overlay’/‘Remove overlay’).

The shape has the form [w,h] where the components are width and height respectively. So I could write ‘shape="[980,700]"’. However I seldom do this because the photos in a collection are nearly all of the same size except for orientation. So I set up two aliases ‘l’ and ‘p’ as the default landscape and portrait shapes and then write eg. ‘shape="l"’. The example page shows how this is done. Only if an image has an unusual shape do I specify its size explicitly.

The shape is ‘heritable’ in a sense defined below.

A photo entry is given a single shape but the image will be available in different sizes – which version does the given shape apply to? The answer is that it applies to the first entry in the size list.

You may include a scale factor for raw images in the size list, thus implying that a raw image is available for every regular image, and that its size is related to the standard size. I never do this, but often provide raw images specifying their sizes individually in the list (using aliases). The format is again [w,h]. The raw shape is heritable. If the size list or the inherited property imply a raw size for an image which in fact has no raw version, you can override the implied size by the dummy field

rawshape=""

The shape of thumbnail images is usually inferred from the shape of main images. Sometimes the inferred size will not be correct (most often because the image itself is an unusual shape), so the thumbnail shape needs to be specified explicitly. The format is again [w,h]. Every image needs a thumbnail, whatever the means of its specification.

You are recommended to provide a size for hi-res thumbnails in your ‘sizes’ list (see below). If in a particular case the hi-res thumbnail does not have the scale factor specified in the ‘sizes’ field, you can provide it as a separate attribute. The format is [s], s being the scale factor relating the hi-res to the normal thumbnail. The special value of an empty character string can be used to specify that a particular thumbnail has no hi-res equivalent.

You can also use this attribute to prevent pix.js from offering a regular image as a hi-res alternative. Sometimes the thumbnail won’t just be a reduction of the regular images, and you won’t want a regular image to be treated as a substitute for it.

This value is the name of the directory containing the image. It is relative to the XML config file. The filename of the image will be constructed as

imagedir + ‘/’ + name + suffix + ‘.’ + extension

This gives the extension used in constructing the filename. The default is “jpg” (note that there is no “.”). The only time I use an “extn” attribute is for gifs.

These two values are used together. When you’re looking at an individual photo the [return] key or the ‘↵’ link will usually take you back to the tabular gllery. But if you’ve arrived at the photo from text notes, I’d like the [return] key to take you back to them at your current position. The retpage and retid field of a photo entry define the name of the HTML page and an anchor within it to comprise the link (with the ‘.html’ extension being automatically added to the page name). If a retid is provided the URL will be ‘retpage.html#retid’. If not the URL will just be ‘retpage.html’.

You do not have to supply the same retpage and retid repeatedly. Once they have been supplied for a photo entry, they apply to subsequent entries until overridden. If you want to revert to having no return address, supply an empty character string as a dummy retpage.

This tells pix.js that an image belongs to a particular category. You may limit a gallery to pictures in a category through the URL options; you can also change the view through the menu icon or the ‘m‘ key.

This completes the description of fields defining a photo entry, so we can give a fairly large example as a further illustration:

<img name="traceyrocks" title="Tracey heading for the Dog’s Leap" 
     caption="looser than she would like" shape="p" display="|*" 
     thumbshape="[140,196]" cat="2014"
     retpage="grancanaria" retid="28" imagedir="images"/>

You may provide a list of images with no division into sections, but dividing them into sections is usually neater. You do this by means of the ‘section’ tag which has the images in the section as its elements.

Remember to provide a closing ‘</section>’ after the images in the section but not to put a terminal solidus in the first mention of the section tag.

The section tag has a number of attributes. Firstly, all the heritable attributes of images are inherited from the equivalent attributes of their section (and inherited in turn by the section from the base of the list).

Secondly the section tag has a number of attributes of its own.

The section title.

This supplies a link to one or more GPS tracks associated with the section (separated by spaces if there is more than one). The URLs should be any linkable form of a GPS track. I’m used to routemaster, but you can link into Wikiloc or any other repository if you like.

If there is more than one GPS track, pix.js will attempt to show how the tracks are associated with thumbnail images for the section. If ‘|’ dividers correspond to the tracks, it can work out how to do this.

Suppose you have 8 thumbnail images in a section and two GPS tracks. Suppose that the first 2 images correspond to the first GPS track, then (after a ‘|’ divider) come another 3 images with no GPS track, and finally (after another ‘|’ divider) come the last 3 images corresponding to the second track. In order to enable pix.js to see the correspondence between thumbnails and tracks, you should provide ‘-’ as a dummy GPS track inbetween the two genuine ones. You may therefore write something like:

<section title="More rides from Valsequillo" gps="routes,artenara.tcx - routes,valsequillo.tcx">

See the routemaster page for an explanation of how to provide a URL invoking routemaster on a particular track.

The caption for a section will be displayed after the title and before the thumbnails. As with image captions, it may be an HTML snippet supplied as a caption element of the section using the ‘CDATA’ notation. It may be limited by category.

This attribute may be used to supply an HTML anchor for linking to the section.

This specifies a format for tidily laying out the contents of the section: see below.

This is auto-generated for metagalleries and should’t be used otherwise.

The ‘cat’ attribute may be used to allow partial views of a gallery. You can view the images by category or all together, according to the URL option (see above) and subsequence adjustments from the menu icon.

Our Cape Verde photos are a good example. Sometimes we want to look at the pictures for our 2016 or our 2018 visit on their own, while at other times we want to see all pictures of a particular route (eg. Route 101, which we rode on both visits).

It is not a good idea to use categories purely in order to allow a single gallery to do the job of more than one.

Layouts may be used if you want a gallery to present images for direct viewing (rather than as thumbnail links) in an attractive way. Our doors page is an example. The options may be expanded in future.

A layout must be specified as an attribute of a section, thus:

<section layout="4:40%:n">

It contains 3 fields separated by colons which may occur in any order.

The number of columns must divide the number of images displayed in the section. Unless you use one of the fancy layouts described below, the display will normally look best if all images in a row have the same height and all images in a column have the same width; but these conditions are not enforced, and sometimes breaching them gives a good effect (as with our Kangchenjunga trek).

The size indicator gives the recommended percentage of window height to be occupied by the tallest image in the first row. The available size will be chosen which most closely matches the recommendation. The default is based on the number of rows in the layout.

The flags may contain the letter ‘n’ indicating that image titles should never be shown in the gallery page, and the letter ‘t’ indicating that the display should always use thumbnail images.

The normal behaviour of pix.js is to display small regular images (it is often useful to provide especially small versions for this purpose). Thumbnails will be displayed instead if they have the same shapes and if there isn’t enough room for regular images.

A limited number of fancy layouts are implemented. They will be chosen automatically if your image shapes permit them.

The size list comprises a single entry for every image size you support. You supply a sizes tag which has no attributes but a number of elements, each with the size tag and no elements of its own but attributes defining the size. Hence you write

<sizes>
  <size .../>
  <size .../>
</sizes>

The attributes of the size tag are as follows.

This is a character string which will be appended to a photo name to construct the jpg filename. It may be empty. A non-empty example is

suffix="@1"

If we are displaying an image whose name (in the photo list) is swerve, and for which the suffix corresponding to the chosen size is ‘@1’, then the filename will be swerve@1.jpg. This may be modified by a directory path.

My practice is to reserve the empty suffix for the raw size, and to use non-empty suffices for the various non-raw sizes. You are urged not to use the empty suffix for non-raw sizes.

This is a numerical value giving the relative sizes of images with the given suffix. The only circumstances in which it may be omitted are for the suffices of types ‘raw’, ‘thumb’ and ‘hithumb’ if the relevant scalings are given separately for each image (possibly taking advantage of inheritance).

The meaning of the scale factor is as follows. The images of various sizes are assumed to have the same aspect ratio, and to have their dimensions proportional to the scale factors in the size list. The ‘shape’ assigned to each image gives the dimensions corresponding to the first size in the list. The dimensions of other sizes are scaled in proportion to the scale factors. So if an image has shape [1000,700], and the first size in the list has scale factor 18 and suffix “@3” while the second has scale factor 9 and suffix “@2”, then the jpg whose name ends “@3” will be of size [1000,700] and the jpg whose name ends “@2” will be of size [500,350].

The regular images – those which are not raws, thumbnails or high-res thumbnails – must have the same aspect ratio as each other and sizes in proportion to their scale factors. The first item in the sizes list must be a regular size. But for the special images – raws, thumbs, and hithumbs – it is possible to override the shape for an individual image through its attributes in the list. In the case of raws and hithumbs, this includes being able to say that they are not available for a particular image, even though they have an entry in the sizes list.

The scale factor for the hi-res thumbnail has a completely different meaning from the other scale factors. The other scale factors give the relative dimensions of the jpgs to each other: only their ratios are significant. The scale factor of the hi-res thumbnail is is dimension relative to the normal thumbnail. It may be fractional, and you can write it with a decimal point.

The reason the hithumb scale factor is treated differently is this. A hi-res thumbnail is not needed except when the regular thumbnail is a different shape than the sizes list implies. Therefore the cases in which a hithumb scale factor is needed are those in which the normal definition of scale factors cannot be used.

This is a character string specifying a type corresponding to the image size. For regular images it should be omitted. A single entry in the size list should have type ‘thumb’; this is the entry understood as corresponding to thumbnails.

Optionally a further entry may have type ‘raw’ understood as corresponding to the raw image. Raw images are treated the same as regular images with the following exceptions:

And you are recommended to provide a further entry in the size list ‘hithumb’ for high-resolution thumbnails. When an image is displayed at a particular size, higher resolution versions at larger sizes (but the same aspect ratio) are offered to the browser as alternatives for high-definition screens. A large regular image may be offered as an alternative to a small one. If thumbnails have the same aspect ratio as regular images, then small regular images will be offered; there is no point in providing a hi-res thumbnail in these cases. But if the thumbnail has a different aspect ratio from the regular images, then a higher resolution alternative will not be available unless it has been created for the purpose.

So, for those images whose thumbnails do not have the same aspect ratio as the regular images, you are urged to produce hi-res alternative thumbnails whose properties (suffix and scale factor relative to the normal thumbnail) you specify in the sizes list. A suitable scale factor is 2, 2.5 or 3. If all your thumbnails have the same aspect ratio as the regular images, there is no need to pay any thought to thumbnails.

An entry in the sizes list will look something like the following:

  <size suffix="@h" scale="2" type="hithumb"/>

The size in px for the heading text for photos (the caption is 4/5 the header size). Eg.

fontsize="12"

I have never used this myself, and rather regret providing it.

You may provide a title for your pix.html page either in the header of that page or in the xml photo list. The latter is recommended because it makes the title available to other software which uses the photo list (e.g. routemaster). It belongs at the top and is formatted exactly the same as an HTML title:

<title>Gran Canaria cycling photos</title>

You may provide multiple titles with categories to distinguish them, e.g.

<title>Gran Canaria cycling photos</title>
<title cat="2012">Gran Canaria cycling photos (2012)</title>
<title cat="2015">Gran Canaria cycling photos (2015)</title>

See above.

You may provide a shortcut icon for your pix.html page either in the header of that page or in the xml photo list. The sole advantage of the latter is that it allows you to specify the page content entirely from the XML rather than sharing duties between that and the HTML. The format is

<icon href="..."/>

You may specify a category if you wish.

The links are provided as single items in empty-element link tags. These entries are put into the links line at the top and bottom of a picture gallery.

The attributes of a link are as follows.

A full example may look like this:

<link name="into" href="intro17.html" cat="2017" display="|" type="origin"/>

Some text to put at the top of the page. As with image and section captions, it may be an HTML snippet.

Images inherit heritable properties from sections, and sections inherit heritable properties from the ‘base’ tag. This enables you to minimise the provision of repeated values. An example is the image directory. This will usually be the same for all images, and you can therefore specify its location once as the ‘imagedir’ attribute of the ‘base’ tag. Eg.

<base imagedir="canarypix" shape="l"/>

The image directory will then be inherited by all images, and does not need to be specified further except for any images not in ‘canarypix’. In the same way, having made ‘l’ the default shape, you have no need to specify a shape whenever the default applies.

It is unlikely that the category will be inherited from the base, but common to have all images in a section belong to the same category, so there will be no base category, a category for most sections, and categories for images only when the section does not correctly assign a category to them.

The XML photolist and the HTML stub perform a single function split across two files. The HTML page is told the name of the XML file through the pix argument, but other pages loading the XML file (eg. routemaster) may like to know the URL of the HTML file so that they can link to it. This is done through the ‘gallery’ tag, eg.

<gallery href="pix.html"/>

This tag allows you to control colours (other than link colours) for your gallery. (For link colours, see below.) Its attributes can be any interpretable HTML colour (eg. ‘black’, ‘#ff0000’), except that ‘farg’ may be any legal CSS background specification (eg. ‘url(images/carpet.gif)’), and fonts may be any legal CSS font specifications. “cat” may be added as an attribute.

Note that if you specify a style for a category, and the user moves from viewing that category to another via the menu icon, then the style will not change even if the new category has a style associated with it. I could fix this if I really considered it a bug.

The following tags have been accepted in the past but are being phased out:

  imagedir  origin  hithumb  thumbshape  pixpage

I used to provide an example of a fairly full photo list on this page, but it was forever going out of date and the number of features calling for illustration grew beyond anything that can be covered by a single example. Here are links to a few of my own photo lists. If you view them in a browser it may impose its own formatting, but if you save them to disc you can view them as normal text files.

xml : aliases : contents of the list : images : dividing images into sections : categories : layouts : the size list : sundry variables : examples

changing link colours : validation : metagalleries : thumbnail links : technical details : random thumbnails : duplicate image names : multiple photo lists : using pixlib separately : what is a srcset?

If you use the style tag to change a gallery’s colour scheme, you will want to change the link colours at the same time. This cannot be done in the same way, and depends on a rather clumsy mechanism. (Unfortunately there is no interface between Javascript and CSS link pseudoclasses, so I’m forced into convoluted measures.)

What you need to do is to insert a single Javascript instruction in your HTML stub before the invocation of pix.js. The format is as follows:

<script>var pixcols = { link:"#083090" ,  visited:"#630000" ,  active:"#404040" , 
                        mlink:"#083090" , mvisited:"#630000" , mactive:"#404040" }</script>

where the values you provide are colours for links/visited links/active links, first for the main display, then for menus. Everything you don’t specify will be left to default. They can be any HTML colours: ‘darkgoldenrod’ is fine.

Your HTML page will therefore end up looking like this:

<html><head>
<meta charset=utf-8> 
<script>var pixcols = { link:"#083090" ,  visited:"#630000" ,  active:"#404040" , 
                        mlink:"#083090" , mvisited:"#630000" , mactive:"#404040" } </script>
<script src="../pix.js"></script>
</head><body></body>
<script>pix("senales.xml")</script>
</html>

If I can find a better way of doing this, I’ll change how it works. Note that you cannot limit link colours to a particular category.

It is easy to make errors in generating the images and the list file. pix.js has a validation option, invoked by specifying a ‘v’ in the mode argument to the picture page, i.e.

https://www.masterlyinactivity.com/kumaon/pix.html?mode=v

When the picture page is invoked in this way, instead of generating a table of thumbnails it generates a list of all images and sizes implied by the list file. The images are fetched in turn. When they are loaded, if their physical size is found to agree with the size attributed by the list, the corresponding entry is deleted. If a discrepancy is found, a note is made in the list. When it has all settled down, no images should be left in the list except those whose sizes are given incorrectly and those which have not been found.

This is quite a slick procedure if you run it on your desktop image of a photo page. On the internet it may take several minutes to complete if your images are large.

A metagallery is a gallery of galleries – the thumbnail links take you to galleries rather than images. Our touring gallery is an example. It contains sundry XML tags the same as for regular galleries and images divided into sections in a special way. There is no size list. You do not produce the XML for the images yourself: instead you go a gallery and view it with a ‘g’ in its mode, eg.

https://www.masterlyinactivity.com/kumaon/pix.html?mode=g

This gives you the code to copy and paste to your XML file.

You may want to provide thumbnail links to images from a separate HTML file. For this purpose you are encouraged to use the thumb() function (or the showimg() function) in pixlib.js (whose other uses are described in below). Our Gran Canaria notes are an example of a page using it.

In the HTML page you write

<script>thumb("swerve")></script>

This expands as the full HTML

<a href=...><img src=... width=... height=... title=... srcset=... class=pix></a>

where the triple dots refer to values computed by looking at the entry for swerve in the photo list.

Alternatively you may write

<script>showimg("swerve",2.5)></script>

Instead of displaying your thumbnail, it displays your main image, scaled as the size of the first entry in your ‘sizes’ list divided by the reduction factor which is the second parameter (in this case 2.5).

To make use of either of these functions you include the following two lines in your HTML header (in this order):

<script src="https://www.masterlyinactivity.com/pixlib.js"></script>
<script>loadpix("list.xml")</script>

The first instructs it to incorporate the pixlib.js javascript. The second instructs the browser to incorporate your list file: provide the appropriate path.

You will also want to define the img.pix style appropriately, eg. in the line

<style>img.pix{border:1px solid}</style>

which may occur in the header or in a style sheet.

A neat trick occurred to me recently, which is to use a style like the following:

<style>img.pix{width:140px;height:140px;object-fit:cover;border:1px solid;padding:2px}</style>

This shows the thumbnails as cropped to square, even if there true shape is oblong. Our 2022 Gran Canaria notes illustrate the result.

If there are duplicate image names in your list and you want a thumbnail for the second with a given name, you should write (eg.)

<script>thumb("swerve",1)></script>

or

<script>showimg("swerve",2.5,1)></script>

Notice that the first image is invoked by supplying ‘0’ as an argument, the second by supplying ‘1’, and so forth.

It is also possible to specify a link for the thumbnail other than the usual full-size picture: see below.

The operation of thumb()/showimg() is more complicated than you might expect, since javascript I/O is asynchronous and when you call these functions the result of loadpix() may or may not have been delivered.

In the former case thumb() is able to generate the HTML text exactly as just described, but in the latter case it merely generates a stub, subsequently modifying the DOM through an event handler invoked when the load has completed.

You may sometimes want to show a random thumbnail for a gallery or metagallery. We do this on our home page. You call thumb() or showimg() omitting the name argument (and, obviously, with no serialno).

The result is as follows. If there are starred images in the photolist, you get a random choice from them. Otherwise you get a random choice from images whose ‘display’ attribute is not ‘none’.

The thumbnail will link to the gallery, not to the individual image.

If you combine photos from several image directories you may find you have reused the same name between them. This is not inherently problematic because the photos whose names clash must lie in different directories. The pix.js software won’t get confused.

However if you want to access these images externally (through the thumb() or showimg() functions which access an image through its name) you may need to take special measures.

Both of these functions return the first occurrence of the image name in the list unless instructed to do otherwise. The instruction to do otherwise is issued through an optional additional serialno parameter to both functions. If serialno is 0 you get the first occurrence of the name; if it is 1 you get the second; and so forth. [No compromises are made for users who can’t count from 0.]

Note that routemaster provides no mechanism for choosing between images with the same name in a list.

If you are showing just one random thumbnail from a list, you may want to show a random thumbnail from another list too. To support this, thumb() and showimg() have an additional optional argument (preceding the name, which is of course omitted for random images) which gives the number of the list to use.

So I load photo lists into our home page by the 4 lines

loadpix("galleries/touring.xml") ;
loadpix("galleries/mtb.xml") ;
loadpix("galleries/treks.xml") ;
loadpix("galleries/misc.xml") ;

and then write, eg., <script>thumb(2)</script> to get a random thumbnail from the trekking gallery (the galleries are numbered 0, 1, 2... in order of loading). I could make loadpix return the associated number if I thought there was a risk of confusion.

The full calling sequences for thumb() and showimg() are as follows:

thumb()

which generates a random thumbnail link for the gallery loaded;

thumb(listno)

which generates a random thumbnail link gallery number listno;

thumb(name)

which generates a thumbnail link for the named image;

thumb(name,serialno)

which generates a thumbnail link for the serialnoth image of the given name; and

thumb(obj)

which generates a thumbnail link according to the fields of the supplied object argument.

showimg()

which generates a random image for the gallery loaded;

showimg(name)

which generates the named image;

showimg(name,factor)

which generates the named image reduced by the given factor;

showimg(name,factor,serialno)

which generates the serialnoth image of the given name reduced by the given factor; and

showimg(obj)

which generates an image according to the fields of the supplied object argument.

When an object is supplied as a parameter, its fields are as follows:

The link target for a normal call is set up in the link’s href field. A copy is put in its non-standard imglink field. But if you specify a different link location through the linkuri field of an object argument, the location which would otherwise have been used as a link target is nonetheless supplied in the imglink field. This allows you to associate a link to the image with the thumbnail whose own link to it has been overridden by something else of your own devising.

pixlib.js contains a javascript library for displaying images. It does most of the work for pix.js but can also be used separately (and is so used by routemaster). I previously sought to document it, but I kept on changing it in ways which made the documentation obsolete and didn’t believe it had any other users so deleted the documentation.

The software for displaying galleries as tables of thumbnails is part of pix.js but is also capable of more general use. (For a while it was included in pixlib.js, but since routemaster loads pixlib.js and has no need to display galleries, I removed it to cut down on load time).

The general outline of pixlib.js should be fairly clear, but if anyone wants to use it outside pix.js they should let me know to caution me against introducing changes which invalidate their software.

Since srcsets are relatively new a few words of explanation may be useful.

When HTML started to be used it was common for page authors to specify dimensions in pixels knowing that there were 96 (or occasionally 72) to the inch. They could, in most cases, have specified dimensions in other units instead (eg inches) but if they’d done so they’d have had no control of the pixel alignment of the result. They avoided this because a pixel-wide line would look blurry on the screens of the time if not exactly aligned with a physical pixel.

Similar considerations applied to jpgs. If a jpg had an intrinsic size of 600x400 pixels then it was best to display it at that size rather than, say, as 6"x4". Respecting the true pixel dimensions ensured that the image was shown at the best possible quality and required minimal processing from the browser. If the browser was required to resize an image it had the choice of algorithms which were computationally expensive but imposed only a slight degradation in quality, or simple algorithms which introduced conspicuous artefacts. Web authors avoided the dilemma whenever they could.

When LED screens came in it became possible (and for mobile devices necessary) to make the physical pixels much smaller. Browsers then had the choice of interpreting pixel dimensions literally, which would make almost everything on the screen 3 times smaller than the author had intended, or of reinterpreting pixels as implicit sizes of about a hundredth of an inch. The latter decision imposed itself (although the sizes are actually defined in terms of angles subtended to the eye rather than as true physical dimensions).

It follows that a 600x400 image continues to be displayed at the size the author intended but that it does not exploit the screen’s high resolution capability. For existing pages that could be hardly be avoided since taking advantage of hi-res screens would require an image whose intrinsic size was more like 1800x1200, and the author hadn’t provided one.

For new pages, though, authors can make allowance for hi-res displays by providing alternative images through the srcset attribute.

In pix.js it is very natural to do this since I allow images to be supplied at a range of sizes for other reasons. I offer the large ones as hi-res srcset alternatives to the small ones; and if you want hi-res alternatives to the large ones, you can always provide still larger ones (but don’t go mad – there are only so many receptors in the eye).

This works easily for regular images, and regular images can be offered as hi-res alternatives to thumbnails if they have the same aspect ratio; but if the aspect ratios differ I have to take extra measures through the hithumb values.

The effort is rewarded though. My laptop has a ‘retina’ screen and at first I had no intention of supplying hi-res thumbnails, but at a certain point they crept in through a bug in my code. The difference in sharpness was striking to the eye so I made sure they stayed in and were properly supported for all aspect ratios.

changing link colours : validation : metagalleries : thumbnail links : random thumbnails : duplicate image names : multiple photo lists : using pixlib separately : what is a srcset?

Before I wrote pix.js I had got into the habit – starting around 2001 – of displaying individual photos on pages with navigation icons at the top left. enlarge and reduce icons toggled between two sizes whose ratios and suffices were hard-coded. The page background was a beigey colour.

Around 2013 I switched to a black background and added an optional third size. Either earlier or later I added the option of providing just a single image size. I broke out the image generation code as a separate javascript file but it was written only for my own use and was full of hard-coded assumptions.

In February 2016 I made it more generic, documented it, and made it generally available.

I made a major revision at the end of March. The (rather trifling) functional changes were to make allowance for raw images and to centre images vertically on the screen as well as horizontally.

The non-functional changes were more extensive. I switched from using a table to define the screen layout to using divs. This gives me more control (and may protect me from some of w3c’s strictures) but makes the response jerkier when the user resizes the window.

At the same time I formalised and documented the interface at a lower level, that of pixlib.js. This cleans things up and may be useful to someone sometime. I intend to use pixlib.js for the image display in routemaster.

I debugged the swipes and pinches on Tracey’s iPad.

Added the links list; deprecated the argument to pix(); added genimage(); added srcset to thumbnails; added hithumb; deprecated thumbs.js.

Bugfix to thumbnail srcsets. Added licence.

Changed the URL parms to make them google-friendlier. Added pixinfodiv to pixlib.js and i-button to pix.js. Another bugfix to thumbnail srcsets.

Added imagedir as a field in photo lists; added serialno parameter to disambiguate name clashes. Timeout rather than mouseout terminates a captionbox (I couldn’t get the mouseout to fire reliably).

Added the XML input format and the history.pushState() method of moving from one photo to another.

Added the option of multiple GPS tracks per section.

Added the categories feature and the ability to request display of hidden images. Started phasing out the javascript list format.

Removed the javascript list format. Added multiple headers. Ceased automatically adding ‘notes’ link. Used divs rather than a table for nav links.

Added overlays. Don’t generate captionbox unless the caption is too long for its position. Add ‘id’ for sections. Better info.

Added display="*" for images and cat="" for links.

Debugged the feature allowing links to be category-specific. Added newtabdiv() (previously in routemasterui).

Made GPS tracks open in a new tab and ensured cursor was a hand over thumbnails.

Added the menu button and the showimg() function. Added layouts.

At the same time I found that pix.js didn’t work correctly in the (rather old (2014)) version of Safari installed on one of my desktops. Mostly it was Safari’s fault (not handling Object.assign correctly), but there is also uncertainty about the semantics of history popstates. At any rate I hastily replaced Object.assigns of list entries by normal assignments (i.e. by reference), adding an extra field for the display value so that I don’t modify it in place. There is a risk of unanticipated consequences.

I validated pix.js in Firefox, Chrome, Opera and Safari. I should keep an eye on its performance in Safari, which is the least forgiving of the browsers available to me.

This was a thorough revision. I extended the layouts, added metagalleries and random images, tidied up some of the uglier code, and made the colour scheme customisable. I don’t have any further major changes in mind. If there’s something you think is missing, let me know.

I added “cat” as an attribute of styles.

I revised the calling sequences for thumb()/setimg(), which were getting out of hand, and added the ability to override the link target. I fixed a bug in the handling of retpage and later added z-indexes to avoid obliterating the nav icons with a layer of background colour.

A 1-line bugfix adding a z-index of 99 to the menu.

A 1-line mod allowing http images to be fetched from https via the routemaster.app fileserver.

• clone     • pixresize     • getncol     • linkp     • scrollitem     • redisplay     • display     • clickfactory     • function     • navigate     • quitimg     • retabulate     • galname     • tabulate     • setspacing     • validate     • textshape     • responsefactory     • optionparse     • pix     • render     • simulate     • tabinfodiv     • toolbox     • catclickfactory     • catbox     • gallerise     • hi     • gentable     • hoverfactory     • unhoverfactory     • addrow     • hr     • hovercraft     • unhovercraft     • dolayouts     • matches

// www.masterlyinactivity.com/software/pix.html
var body,photolist,query,thispage,thisid,here ; 
var mic = 'https://www.masterlyinactivity.com/' ;

var cols = { link: '#66aaaa' , visited: '#cc3388' , active: '#404040' ,
             mlink:'#2244cc' , mvisited:'#cc3388' , mactive:'#808080'} ; 
var style = { fg:'silver' , bg:'black' ,  mg: '#a4a4a4' , 
              mfg:'black' , mbg:'white' , farg:null , 
              font:'helvetica' , titlefont:'helvetica' , 
              title:null   , titlebg:null } ; 

var infowords = { exit: 'Exit full screen [esc key]' , 
                  enter: 'Enter full screen [f key]' ,
                  notes: 'notes' , origin: 'gallery' } ; 

if((typeof pixlib)=="undefined") document.write('<scri' + 'pt src="' +
//    mic + 
"../" + 
'pixlib.js"></scri' + 'pt>') ;

function clone(x,y) 
{ var i,z={} ; 
  if(x) for(i in x) z[i] = x[i] ; 
  if(y) for(i in y) z[i] = y[i] ; 
  return z ; 
}
var defstyle = clone(cols,style) ; 

// is link really 66aaaa or 4a8888 ?
if((typeof pixcols)!="undefined"&&pixcols)
  for(var field in cols) if(pixcols[field]) cols[field] = pixcols[field] ; 

document.write( 
    '<style>a:link{color:'+cols.link+';text-decoration:none}' + 
           'a:visited{color:'+cols.visited+';text-decoration:none}' + 
           'a:active{color:'+cols.active+';text-decoration:none}' +
           'a.m:link{color:'+cols.mlink+';text-decoration:none}' + 
           'a.m:visited{color:'+cols.mvisited+';text-decoration:none}' + 
           'a.m:active{color:'+cols.mactive+';text-decoration:none}</style>' ) ;

/* -------------------------------------------------------------------------- */

// pixresize responds to a window resize as follows:
// o. for a table view, if the number of columns can change, change it;
// o. for an image view, if the new size permits a larger or smaller image, 
//    enlarge or reduce;
// o. or if the window is expanding or contracting but hasn't changed size 
//    enough to change the desired image, anticipatively preload a larger or
//    smaller one.

function pixresize()
{ var i,match=1,ncol,hcol=here.ncol ;
  // if it's an image, then call resize
  if(here.name!=null) { genpic('resize') ; return ; }

  // if the width is unchanged and there are no layouts, just return
  if(window.innerWidth!=hcol.wwid||hcol.laysize.length!=0)
  { ncol = getncol() ; 
    if( ncol.ncol!=hcol.ncol || ncol.laysize.length!=hcol.laysize.length ) 
      match = 0 ; 
    for(i=0;i<ncol.laysize.length&&match;i++)
      if(ncol.laysize[i]!=hcol.laysize[i]) match = 0 ; 
  }
  if(match) tabulate('resize',hcol) ;
  else tabulate(scrollitem(photolist.sect),ncol) ;
}
/* ----- getncol finds the number of table columns which fit the screen ----- */

function getncol()
{ var ncol,i,j,k,ind,sect=photolist.sect,maxw,w,h,kw,sizes=photolist.sizes ; 
  var sumw,maxh,ss,ll,err,errw,lay,item ;
  var maxcol = Math.floor((window.innerWidth-17)/(photolist.maxthumb+19)) ; 
  if(maxcol<1) maxcol = 1 ;  

  for(ss=[],ncol=i=0;i<sect.length;i++) 
    if(ll=sect[i].list,lay=sect[i].layout)
  { // 17 pix for scrollbar, 19 pix for margin, border...
    maxw = window.innerWidth - 19*lay.ncol - 17 ; 
    if(lay.mustthumb)
    { if(lay.sumwt>maxw) 
      { ss.push(null) ; // special value means ignore layout
        if((k=Math.ceil(ll.length/Math.ceil(ll.length/maxcol)))>ncol) ncol = k ;
      }
      else ss.push(-1) ; 
      continue ; 
    }

    for(k=null,j=-1;j<sizes.length;j++) 
      if((j>=0&&!sizes[j].type)||(j<0&&lay.canthumb)) 
    { if(j>=0) 
      { w = lay.sumw * sizes[j].scale / sizes[0].scale ;
        h = lay.maxh * sizes[j].scale / sizes[0].scale ;
      }
      else { w = lay.sumwt ; h = lay.maxht ; } 
      err = h/window.innerHeight - lay.ht ; 
      if(err<0) err = -err ; 
      if( k==null               // no previous scale found
       || (w<kw&&kw>maxw)       // this is < prev scale found which was illegal
       || (err<kerr&&w<=maxw) ) // this is legal and better than previous scale
      { k = j ; kw = w ; kerr = err ; }
    }
    ss.push(k) ;
  }
  else if(sect[i].href)
  { if(ll.length>ncol) ncol = Math.min(ll.length,maxcol) ; }
  else if((k=Math.ceil(ll.length/Math.ceil(ll.length/maxcol)))>ncol) ncol = k ; 

  return { ncol:ncol , laysize:ss , wwid:window.innerWidth } ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------- create links line of the table page ----------------- */

function linkp()
{ var p,a,i,links=photolist.links,flag,s,ss,origin=null,style=photolist.style ; 

  if(!links||!links.length) return null ;  
  p = document.createElement("p") ; 
  p.setAttribute('style',"text-align:center;font-size:100%;margin:6px") ; 

  for(flag=i=0;i<links.length;i++)
    if( links[i].cat==undefined || links[i].cat==null || links[i].cat==query.cat
     || (links[i].cat==''&&!query.cat) )
  { if(links[i].display=='|') s = ' | ' ; else s = ' : ' ;
    ss = null ; 
    if(links[i].href)
    { a = document.createElement('a') ; 
      a.setAttribute('href',links[i].href) ; 
      if(!origin&&links[i].type=='origin')
      { origin = links[i].href ; ss = " \u21b5" ; }
    }
    else 
    { a = document.createElement('span') ; 
      a.setAttribute('style',"color:"+style.mg) ; 
    }
    a.appendChild(document.createTextNode(links[i].name)) ; 
    if(flag) p.appendChild(document.createTextNode(s)) ; 
    p.appendChild(a) ; 
    if(ss) 
    { a = document.createElement('span') ; 
      a.setAttribute('style',"font-size:90%") ; 
      a.appendChild(document.createTextNode(ss)) ; 
      p.appendChild(a) ; 
    }
    flag = 1 ; 
  }
  return [ p , origin ] ;
}
/* -------------------------------------------------------------------------- */

function scrollitem(sect)
{ var i ; 
  for(i=sect.length-1;i>0;i--) if(sect[i].scrollelement)
    if(sect[i].scrollelement.getBoundingClientRect().top<10) break ;
  return i ;
}
/* -------------------------------------------------------------------------- */

function redisplay(si,dir)
{ var sect=photolist.sect , sno=si[0] , ind=si[1] ,  item=sect[sno].list[ind] ; 
  var thisuri = thispage + '?image=' + item.name ;
  if(item.serialno>0) thisuri += '&serialno=' + item.serialno ;
  if(query.cat) thisuri += '&cat=' + query.cat ; 
  if(query.mode.length>0) thisuri += '&mode=' + query.mode ;
  if(here.ind==null) history.replaceState({ind:null,pos:scrollitem(sect)},'') ;
  history.pushState({ind:si},'',thisuri) ; 
  display(si,dir) ;
}
/* -------------------------------------------------------------------------- */

function display(si,dir)
{ var title,i,k,fetchitem,s,sect=photolist.sect ;
  var sno=si[0],ind=si[1],item=sect[sno].list[ind],retname,div ;
  var name=item.name,serialno=item.serialno,laction=null,litem=null ;
  var raction=null,ritem=null,title=sect[sno].title,retaction ;
  genmenu('del') ; 

  if(name!=here.name||serialno!=here.serialno) 
  { if(item.title) setdomtitle(item.title) ; 
    else if(photolist.title) setdomtitle(getcatval(photolist.title,query.cat)) ; 
  }
  here = { name:name , serialno:serialno , ind:si , ncol:null } ;

  if(query.mode.indexOf('n')<0) infoflag = 0 ; else infoflag = 1 ; 

  if(!dir)
  { if(sno=sect.length-1&&ind==sect[sno].list.length-1) dir = -1 ; 
    else dir = 1 ; 
  }
  function clickfactory(si,dirn) { return function() {redisplay(si,dirn);} ; }

  if( ( si = advance(photolist,here.ind,-1) ) ) 
  { laction = clickfactory(si,-1) ; litem = sect[si[0]].list[si[1]] ; }
  if( ( si = advance(photolist,here.ind,1) ) ) 
  { raction = clickfactory(si,1)  ; ritem = sect[si[0]].list[si[1]] ; }

  genpic(body,item,title,photolist.sizes,laction,litem,quitimg,raction,ritem,
         infowords,pixhelpdiv(photolist.style),dir,infoflag,photolist.style) ; 
}
/* ------------------------ navigate using the arrow keys ------------------- */

function navigate(e) 
{ var si,sect=photolist.sect ;
  if(e.keyCode==13&&genmenu('status')) { genmenu('del') ; return ; }

  if(e.keyCode==37)               // left arrow key
  { e.preventDefault() ; 
    si = advance(photolist,here.ind,-1) ; 
    if(si) redisplay(si,-1) ; 
  }
  else if(e.keyCode==39) // right arrow key
  { e.preventDefault() ;   
    si = advance(photolist,here.ind,1) ; 
    if(si) redisplay(si,1) ;
  }
  else if(e.keyCode==13&&here.ind) quitimg() ;
  else if(e.keyCode==13&&photolist.origin)  
    location.href = photolist.origin ; // return
  else if(e.keyCode==40&&here.ncol==null) 
  { e.preventDefault() ; genpic('reduce') ; }
  else if(e.keyCode==38&&here.ncol==null) 
  { e.preventDefault() ; genpic('enlarge') ; } 
  else if(e.keyCode==70&&queryfullscreen()==0) // 'f' (full screen)
  { e.preventDefault() ; enterfullscreen() ; }
  else if(e.keyCode==73||e.keyCode==77) genmenu('toggle') ; // 'i'/'m'
}
function quitimg() 
{ // console.log(here.ind[0]+'+'+here.ind[1]) ; 
  // console.log(photolist.sect[here.ind[0]].list[here.ind[1]]) ;
  if(query.mode.indexOf('n')<0) retabulate() ;
  else location.href = retlink(photolist.sect[here.ind[0]].list[here.ind[1]]) ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function retabulate() 
{ var sno = here.ind?here.ind[0]:-1 ; tabulate(sno) ; }

function galname(q)
{ var pixpage = thispage , doall ; 
  if(q.mode.indexOf('a')>=0) doall = 'a' ; 
  else if(q.mode.indexOf('*')>=0) doall = '*' ; 
  else if(q.mode.indexOf('h')>=0) doall = 'h' ; 
  else doall = null ;
  if(q.cat) 
  { pixpage += '?cat=' + q.cat ; if(doall) pixpage += '&mode=' + doall ; }
  else if(doall) pixpage += '?mode=' + doall ; 
  return pixpage ; 
}
function tabulate(sectno,ncol)
{ var i,sect=photolist.sect,caption,pfx,sfx=null,style=photolist.style,el ;
  var farging=(style&&style.farg&&style.farg!=style.bg) ; 

  function setspacing()
  { var sparew,spareh,padw,padh,marh ;
    sparew = Math.floor((window.innerWidth-tabulate.tabw)/2) ;
    if(sparew<=80) padw = Math.min(8,Math.floor(sparew/2)) ; 
    else padw = sparew - 72 ; 
    spareh = Math.floor((window.innerHeight-tabulate.tabh)/2) ;
    if(spareh>8+sparew-padw)
    { padh = spareh - (sparew-padw) ; marh = spareh - padh ; } 
    else if(spareh>0) 
    { marh = spareh ; padh = Math.min(8,Math.floor(spareh/2)) ; marh -= padh ; }
    else 
    { marh = sparew - padw - 1 ; // subtract 9 as half of scrollbar, then add 8
      // if the table ht only slightly exceeds the page ht, avoid shunting the 
      // table so much further down that the user is forced to scroll unnecessar
      if(marh<0) marh = 0 ; else if(marh>-spareh/2) marh = -spareh/2 ;
      padh = Math.min(8,Math.floor(marh/2)) ; 
      marh -= padh ; 
    } 
    tabulate.table.setAttribute('style','margin:'+marh+'px auto;padding:'+
          padh+'px '+padw+'px;background:'+style.bg) ; 
  }

  if(sectno=='resize') 
  { if(farging) setspacing() ; here.ncol.wwid = window.innerWidth ; return ; }

  genmenu('del') ; 
  genpic() ; 

  setdomtitle(photolist.pagetitle) ; 
  if(sectno==-1) item = null ; 
  else history.pushState({ind:null},'',galname(query)) ; 

  if(query&&query.mode&&(i=query.mode.indexOf('n'))>=0) 
    query.mode = query.mode.substring(0,i) + query.mode.substring(i+1) ; 
  while(body.firstChild) body.removeChild(body.firstChild) ;

  if(!ncol) ncol = getncol() ;
  here = { name:null , serialno:null , ind:null , ncol:ncol } ;

  pfx = document.createElement("div") ; 
  p = document.createElement("p") ; 
  p.setAttribute('style',"text-align:center;font-size:140%;padding:5px 0 6px;" +
                         'margin:0;color:'+style.title+';font-family:' +
                         style.titlefont+';background:' +style.titlebg) ;  
  p.appendChild(document.createTextNode(photolist.pagetitle)) ; 
  pfx.appendChild(p) ; 

  if(p=linkp()) { pfx.appendChild(p[0]) ; if(p[1]) photolist.origin = p[1] ; }
  if(p=linkp()) 
  { sfx = document.createElement("div") ; sfx.appendChild(p[0]) ; }
  caption = getcatval(photolist.caption,query.cat) ; 

  tabulate.table = gentable(pfx,sfx,caption,sect,photolist.sizes,
                            ncol,redisplay,style) ; 
  tabulate.table.setAttribute('style','margin:0 auto;padding:0;background:'+
                                      style.bg) ; 
  body.appendChild(tabulate.table) ; 
  tabulate.tabw = tabulate.table.offsetWidth ; 
  tabulate.tabh = tabulate.table.offsetHeight ; 
  if(farging) setspacing() ; 

  genmenu(body,function(){return tabinfodiv(photolist,query);},style) ;
  if(sectno>0&&sect[sectno].scrollelement)
  { i = sect[sectno].scrollelement.getBoundingClientRect().top ;
    window.scrollTo(0,i+body.scrollTop) ; 
  }
  else if(thisid)
  { el = document.getElementById(thisid) ; if(el) el.scrollIntoView() ; }
}
/* -------------------------------------------------------------------------- */

function validate()
{ var sect=photolist.sect , sizes=photolist.sizes , sty = photolist.style ; 
  var jpglink,ind,p,a,item,sfx,shape,span,img,png,sno,i,ll ; 

  function textshape(x,alias)
  { var i,shape = [ Math.floor(0.5+x[0]) , Math.floor(0.5+x[1]) ] ;
    for(i=0;i<alias.length;i++) if(alias[i].shape)
      if(alias[i].shape[0]==shape[0]&&alias[i].shape[1]==shape[1])
        return alias[i].name ; 
    return '[' + shape[0] + ',' + shape[1] + ']' ; 
  }

  function responsefactory(item,sizeno,nsizes,png) 
  { return function() 
    { var resp = item.resp , shape = resp[sizeno+1].shape ; 
      var node , i , txt , flag ;

      if(png) node = resp[sizeno+1].ovel ; 
      else node = resp[sizeno+1].el ;

      if(shape[0]==this.naturalWidth&&shape[1]==this.naturalHeight)
      { node.parentNode.removeChild(node) ; 
        if(png) resp[sizeno+1].ovel = null ; 
        else resp[sizeno+1].el = null ;
        for(flag=0,i=1;i<3+nsizes&&!flag;i++) if(resp[i]) 
          if(resp[i].el||resp[i].ovel) flag = 1 ; 
        if(!flag) 
        { node = resp[0].el ; node.parentNode.removeChild(node) ; }
      }
      else
      { txt = textshape([this.naturalWidth,this.naturalHeight],
                        photolist.alias) ;
        txt = document.createTextNode(' actually ' + txt) ;
        node.appendChild(txt) ;
      }
    } 
  }

  setdomtitle(photolist.pagetitle) ; 
  body.setAttribute('style',
                    'background:white;margin:6px 0;font-family:'+sty.font) ;
  while(body.firstChild) body.removeChild(body.firstChild) ;

  p = document.createElement("p") ; 
  p.setAttribute('style',"text-align:center;font-size:140%;margin:2px 0 6px;" +
                 "color:dimgray;") ; 
  p.appendChild(document.createTextNode(photolist.pagetitle)) ; 
  body.appendChild(p) ; 

  for(sno=0;sno<sect.length;sno++) 
  { item = sect[sno] ;
    if(!item.name) // section title
    { p = document.createElement("p") ; 
      p.setAttribute("style","font-size:110%;padding:6px 6px 4px;" +
                             "background:#4a4a4a;" +
                             "color:lightgray;margin:4px 0") ; 
      if(item.title) p.appendChild(document.createTextNode(item.title)) ; 
      else p.appendChild(document.createTextNode('[Untitled section]')) ;
      body.appendChild(p) ; 
    }
    // the rest is for images
    for(ll=item.list,i=0;i<ll.length;i++)
    { item = ll[i] ; 
      p = document.createElement("p") ; 
      p.setAttribute("style","padding:0 6px 0 6px;margin:4px;color:dimgray") ;
      p.appendChild(document.createTextNode(item.name)) ; 
      // dummy item (introductory text), 1 item per size, then thumb/hithumb
      item.resp = new Array(3+sizes.length) ; 
      item.resp[0] = { el:p , ovel:null , shape:null } ; 
      if(item.serialno>0)
        p.appendChild(document.createTextNode(' ('+item.serialno+')')) ; 

      for(png=0;png<2&&(png==0||item.overlay);png++)
        for(sizeno=0;sizeno<sizes.length+2;sizeno++)
      { if(sizeno>=sizes.length&&png) continue ;
        if(sizeno==sizes.length) 
        { shape = item.thumbshape ;
          sfx = shape[2] ; 
          jpglink = jpg(item,sizes,-1,png) ; 
        }
        else if(sizeno>sizes.length) 
        { if(!item.hithumb) continue ;
          shape = [ item.thumbshape[0]*item.hithumb[0] , 
                    item.thumbshape[1]*item.hithumb[0] ] ;
          jpglink = item.filename + item.hithumb[2] + item.extn ;
        }
        else
        { if(!(shape=imgshape(item,sizes,sizeno))) continue ; // no raw 
          sfx = sizes[sizeno].suffix ;
          jpglink = jpg(item,sizes,sizeno,png) ; 
        }
        span = document.createElement("span") ; 
        span.appendChild(document.createTextNode(' \u2666 ')) ; 

        a = document.createElement("a") ; 
        a.setAttribute('href',jpglink) ;
        a.setAttribute('target','_blank') ;
        sfx += (png?'.png':'') + ': ' + textshape(shape,photolist.alias) ;
        a.appendChild(document.createTextNode(sfx)) ; 
        span.appendChild(a) ; 
        if(png) item.resp[sizeno+1] = span ;
        else item.resp[sizeno+1] = { el:span , ovel:null , shape:shape } ; 
        p.appendChild(span) ; 
        img = new Image() ; 
        img.addEventListener('load',
                             responsefactory(item,sizeno,sizes.length,png)) ;
        img.src = jpglink ; 
      }
      body.appendChild(p) ; 
    }
  }
  p = document.createElement("p") ; 
  p.setAttribute("style","font-size:110%;padding:4px 6px;background:#4a4a4a;"+
                         "color:lightgray;margin:4px 0") ; 
  p.appendChild(document.createTextNode('\u00a0')) ; 
  body.appendChild(p) ; 
}
/* -------------------------------------------------------------------------- */

function optionparse(s)
{ var name=null,serialno=0,mode='',opt=s.split('&'),i,cat=null ;
  
  for(i=0;i<opt.length;i++)
    if(opt[i].substring(0,6)=='image=') name = opt[i].substring(6) ;
    else if(opt[i].substring(0,9)=='serialno=') serialno = opt[i].substring(9) ;
    else if(opt[i].substring(0,5)=='mode=') mode = opt[i].substring(5) ;
    else if(opt[i].substring(0,4)=='cat=') cat = opt[i].substring(4) ;
    else if(opt[i].length) alert('unrecognised option: '+opt[i]) ; 

  return { name:name , serialno:serialno , mode:mode , cat:cat } ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function pix(xmlfl)
{ var xmldoc , xhttp = new XMLHttpRequest() , parser , field , style ;

  history.replaceState({ind:null},'',location.href) ; 
  // I've forgotten what this is for. without the test on e.state it doesn't 
  // work in safari. I don't know if the check has any consequences.
  window.onpopstate = function (e)
  { if(e.state)
    { if(e.state.ind) display(e.state.ind) ; else tabulate(e.state.pos) ; }
  } ;
  if(xmlfl==undefined||xmlfl.substring(xmlfl.length-4).toLowerCase()!='.xml') 
  { alert('No XML photo list provided') ; throw '' ; }

  parser = new DOMParser() ;
  xhttp.onreadystatechange = function() 
  { if(xhttp.readyState==4) 
    { if(xhttp.status==200)
      { xmldoc = parser.parseFromString(xhttp.responseText,"application/xml") ;
        photolist = getphotolist(xmldoc,xmlfl) ; 
        render() ; 
      }
      else alert("Unable to read "+xmlfl+": error code "+xhttp.status) ;
    }
  }
  xhttp.open("GET",xmlfl,true) ;
  xhttp.send() ;
}
/* -------------------------------------------------------------------------- */

function render()
{ var ind,i,k,s,h,head=document.getElementsByTagName('head')[0],itemno,desc ; 
  var imgforce,title=photolist.title,sect,val,sel,sno,ll,sty,style ;

  // parse the url options for the current page
  thispage = location.href ;
  thisid = null ; 
  if((itemno=thispage.indexOf('?'))>=0)
  { query = optionparse(thispage.substring(itemno+1)) ; 
    thispage = thispage.substring(0,itemno) ; 
  } 
  else query = optionparse('') ; 

  ind = thispage.indexOf('#') ; 
  if(ind>=0) 
  { thisid = thispage.substring(ind+1) ; 
    thispage = thispage.substring(0,ind) ; 
  }
  if((ind=thispage.lastIndexOf('/'))>=0) thispage = thispage.substring(ind+1) ; 

  // title taking account of category
  val = getcatval(photolist.title,query.cat) ; 
  if(!val)
  { h = document.getElementsByTagName("title") ;
    if(h.length==0) alert('no title') ; else val = h[0].textContent ; 
  }
  photolist.pagetitle = val ; // should rename pagetitle to title

  // icon 
  val = getcatval(photolist.icon,query.cat) ; 
  if(val) 
  { h = document.createElement('link') ;
    h.setAttribute('rel','shortcut icon') ; 
    h.setAttribute('href',photolist.icon=val) ; 
    head.appendChild(h) ;
  }

  // origin
  val = getcatval(photolist.origin,query.cat) ; 
  if(val) photolist.origin = val ;

  // style
  style = clone(defstyle) ; 
  sty = getcatval(photolist.style,query.cat) ; 
  if(sty) for(field in style) if(sty[field]) style[field] = sty[field] ;
  for(field in cols) style[field] = cols[field] ;
  if(!style.title) style.title = style.fg ;
  if(!style.titlebg) style.titlebg = style.bg ;
  if(!style.titlefont) style.titlefont = style.font ;
  photolist.style = style ; 

  // set body style
  body = document.getElementsByTagName("body")[0] ;
  if(style.farg) s = style.farg ; else s = style.bg ; 
  body.setAttribute('style','margin:0;padding:0;font-family:'+style.font+
                            ';background:'+s+';color:'+style.fg) ; 
  
  // prepare the list
  selcat(photolist,query) ; 
  dolayouts(photolist.sect) ;
  sect = photolist.sect ; 

  // validation?
  if(query.mode.indexOf('v')>=0) return validate() ; 
  if(query.mode.indexOf('g')>=0) return gallerise() ; 

  here = { name:null , serialno:null , ind:null , 
           ncol:{ncol:0,laysize:0,wwid:0} } ;

  window.onresize = pixresize ;
  document.onkeydown = navigate ; 
  document.addEventListener('touchstart',startswipe,false) ;        
  document.addEventListener('touchmove',midswipe,false) ;        
  document.addEventListener('touchend',endswipe,false) ;

  // make up description for meta field
  for(desc=sno=0;sno<sect.length;sno++) for(ll=sect[sno],i=0;i<ll.length;i++)
  { if(desc==0) desc = 'Photos: ' ; else desc += ' | ' ; desc += ll[i].title ; }
  setdomdesc(desc) ; 

  if(query.name&&(itemno=findimage(sect,query.name,query.serialno)))
    display(itemno) ; 
  else tabulate(-1) ; 
}
/* ---------------------------------- swipes -------------------------------- */

function simulate(btn) { navigate({keyCode:btn,preventDefault:function(){}}) ; }

/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function tabinfodiv(p,q)
{ var d=document.createElement('div'),i,ll,npix,n2,s,noraw='',norig=null ; 
  var nstarred , sect=p.sect , rawsect=p.rawsect , cats=p.cats ;
  d.appendChild(toolbox(p,q)) ;

  for(norig=i=0;i<rawsect.length;i++) 
    for(ll=rawsect[i].list,j=0;j<ll.length;j++)
  { norig += 1 ; 
    if( ll[j].rawshape && ll[j].rawshape.length==2
     && ( ll[j].rawshape[0]==0||ll[j].rawshape[1]==0) ) 
      noraw += ' ' + ll[j].name ;
  }

  for(nstarred=n2=npix=i=0;i<sect.length;i++) 
  { ll = sect[i].list ;
    npix += ll.length ; 
    for(j=0;j<ll.length;j++) if(ll[j].visibility=='*') nstarred += 1 ;
    if(sect[i].href) 
    { s = sect[i].title ;
      if(s.length>=9&&s.substring(s.length-8)==' photos)')
      { s = s.substring(0,s.length-8) ; 
        j = s.lastIndexOf('(') ;
        if(j>=0&&isvalidnum(s=s.substring(j+1))) n2 += parseInt(s) ;
      }
    }
  }

  if(sect.length>1) 
  { if(n2) s = ' galleries' ; else s = ' sections' ;
    d.appendChild(document.createTextNode(sect.length+s)) ;
    d.appendChild(document.createElement('br')) ;
  }

  s = npix + ' photos' ;
  if(n2) s += ' (\u2192' + n2 + ')' ;
  else if(norig!=null&&norig>npix) s += ' displayed (out of '+norig+')'  ;
  d.appendChild(document.createTextNode(s)) ;
  d.appendChild(document.createElement('br')) ;

  if(nstarred) 
  { d.appendChild(document.createTextNode(nstarred+' starred')) ;
    d.appendChild(document.createElement('br')) ;
  }

  if(noraw!='')
  { d.appendChild(document.createTextNode('No raw shape for'+noraw)) ;
    d.appendChild(document.createElement('br')) ;
  }
  d.appendChild(pixhelpdiv(p.style)) ;
  d.appendChild(pixdocspan()) ;
  return d ; 
}
/* -------------------------------------------------------------------------- */

function toolbox(p,q) 
{ var d = document.createElement('div') , i , vis , v , s , flag = 0 ; 
  var cats = p.cats ; 

  // internal functions
  function catclickfactory(cat,opt) 
  { return function()
    { var ret,thisuri ; 
      genmenu('del') ; 
      here.ind = null ; 
      if(opt) q.mode = cat ; 
      else { q.cat = cat ; photolist.pagetitle = getcatval(p.title,cat) ; }
      q.name = null ; 
      // the shortcut icon isn’t present in the dom; changing the origin
      // would be unintuitive
      selcat(p,q) ; 
      dolayouts(p.sect) ;
      thisuri = thispage ;
      if(q.mode||q.cat)
      { thisuri += '?' ;
        if(q.cat) { thisuri += 'cat=' + q.cat ; if(q.mode) thisuri += '&' ; }
        if(q.mode) thisuri += 'mode=' + q.mode ;
      }
      history.pushState({ind:null},'',thisuri) ; 
      retabulate() ; // calls tabulate calls gentable
    }
  }
  function catbox(opt,str,cat)
  { var s = document.createElement('span') ; 
    s.setAttribute('style','color:'+p.style.mlink+';cursor:pointer') ; 
    s.onclick = catclickfactory(cat,opt) ; 
    s.appendChild(document.createTextNode(str)) ;
    return s ;
  }

  // full screen
  s = offerfullscreen( { exit: 'Exit full screen [esc key]' , 
                         enter: 'Enter full screen [f key]' } , p.style.mlink) ;
  if(s) { d.appendChild(s) ; flag = 1 ; } 

  // categories
  if(cats.length) 
  { d.appendChild(document.createTextNode('View category: ')) ;
    for(i=0;i<cats.length;i++) 
    { if(cats[i]==q.cat) d.appendChild(document.createTextNode(q.cat)) ;
      else d.appendChild(catbox(0,cats[i],cats[i])) ;
      d.appendChild(document.createTextNode(' | ')) ;
    }
    if(q.cat) d.appendChild(catbox(0,'all',null)) ;
    else d.appendChild(document.createTextNode('all')) ;
    d.appendChild(document.createElement('br')) ;
    flag = 1 ; 
  }

  // display options
  if(p.vis.star+p.vis.none+p.vis.def>1) 
  { d.appendChild(document.createTextNode('Display: ')) ;
    if(p.vis.star)
    { if(q.mode.indexOf('*')>=0) 
        d.appendChild(document.createTextNode('starred')) ;
      else d.appendChild(catbox(1,'starred','*')) ;
      d.appendChild(document.createTextNode(' | ')) ;
    }
    if(p.vis.def)
    { if(p.vis.none==0) v = 'all'; else v = 'default' ;
      if(q.mode.indexOf('*')<0&&q.mode.indexOf('a')<0&&q.mode.indexOf('h')<0) 
        d.appendChild(document.createTextNode(v)) ;
      else d.appendChild(catbox(1,v,'')) ;
      if(p.vis.none) d.appendChild(document.createTextNode(' | ')) ;
    }
    if(p.vis.none)
    { if(q.mode.indexOf('a')>=0) 
        d.appendChild(document.createTextNode('all')) ;
      else d.appendChild(catbox(1,'all','a')) ;
      d.appendChild(document.createTextNode(' | ')) ;
      if(q.mode.indexOf('h')>=0) 
        d.appendChild(document.createTextNode('hidden')) ;
      else d.appendChild(catbox(1,'hidden','h')) ;
    }
    flag = 1 ; 
  }
  if(flag) d.setAttribute('style','white-space:nowrap;border-bottom:solid 1px ' 
                       +p.style.mfg+';padding-bottom:3px;margin-bottom:3px') ;

  return d ; 
}
/* -------------------------------------------------------------------------- */

function gallerise()
{ var sect=photolist.sect , sizes=photolist.sizes , list=[] , item , title ; 
  var sno , i , k , ll , imagedir , s , u , v ;
  var hithumb=null , st="margin:0;font-family:monospace,monospace" ; 

  for(n=sno=0;sno<sect.length;sno++) 
    for(ll=sect[sno].list,n+=ll.length,i=0;i<ll.length;i++)
      if(ll[i].visibility=='*') list.push(ll[i]) ; 

  if(list.length==0) { alert('No starred images') ; return ; }

  imagedir = list[0].filename ; 
  i = imagedir.lastIndexOf('/') ; 
  if(i>=0) imagedir = imagedir.substring(0,i) ; 

  item = list[0] ; 
  s = '<section title="' + photolist.pagetitle + ' (' + n + ' photos)"' ;
  p = document.createElement('p') ; 
  p.setAttribute('style',st) ; 
  p.appendChild(document.createTextNode(s)) ; 
  p.appendChild(document.createElement('br')) ; 

  s = '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 thumbshape="' + 
          item.thumbshape[2] + '[' + item.thumbshape[0] + ',' + 
                                     item.thumbshape[1] + ']"' ;
  if(u=hi(item,sizes)) s += ' hithumb="' + u[2] + '[' + u[1] + ']"' ;
  p.appendChild(document.createTextNode(s)) ; 
  p.appendChild(document.createElement('br')) ; 

  function hi(item,sizes)
  { var v=null,j,k,qf ;
    if(item.hithumb) v = item.hithumb ;
    else if(item.shape[0]*item.thumbshape[1]==item.shape[1]*item.thumbshape[0])
    { qf = item.shape[0] / ( item.thumbshape[0]*sizes[0].scale ) ; 
      for(j=0;j<sizes.length;j++) if(!sizes[j].type)
      { k = qf * sizes[j].scale ; 
        if(k>1&&(!v||Math.abs(k-2.5)<Math.abs(qf*v[0]-2.5)))
          v = [ sizes[j].scale , 0 , sizes[j].suffix ] ;
      }
      if(v)
      { v[0] *= qf ; 
        k = v[0].toFixed(1) ; 
        if(k.charAt(k.length-1)=='0') k = k.substring(0,k.length-2) ; 
        v[1] = k ; 
      }
    }
    return v ; 
  }
  s = '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 imagedir="' + 
      reluri(location.href,item.imagedir) + '"' ;
  p.appendChild(document.createTextNode(s)) ; 
  p.appendChild(document.createElement('br')) ; 

  v = reluri(location.href,photolist.gallery) ;
  if(query.cat) v += '?cat=' + query.cat ;
  s = '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 href="' + v + '">' ;
  p.appendChild(document.createTextNode(s)) ; 
  p.appendChild(document.createElement('br')) ; 

  for(i=0;i<list.length;i++)
  { item = list[i] ;
    s = '\u00a0\u00a0\u00a0\u00a0<img name="' + item.name + '"' ;
    // check if thumbshape agrees with the section default
    for(j=0;j<3&&item.thumbshape[j]==list[0].thumbshape[j];j++) ;
    if(j<3) 
    { s += ' thumbshape="' + item.thumbshape[2] + 
          '[' + item.thumbshape[0] + ',' + item.thumbshape[1] + ']"' ;
    }
    v = hi(item,sizes) ; 
    
    if(v&&((!u)||v[1]!=u[1]||v[2]!=u[2])) 
      s += ' hithumb="' + v[2] + '[' + v[1] + ']"' ;
    else if(u&&!v) s += ' hithumb=""' ;

    if(item.imagedir!=list[0].imagedir)
    { p.appendChild(document.createTextNode(s)) ; 
      p.appendChild(document.createElement('br')) ; 
      s = '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0imagedir="' + 
          reluri(location.href,item.imagedir) + '"' ;
    }
    s += '/>' ;
    p.appendChild(document.createTextNode(s)) ; 
    p.appendChild(document.createElement('br')) ; 
  }
  p.appendChild(document.createTextNode('</section>')) ; 
  p.appendChild(document.createElement('br')) ; 
  body.appendChild(p) ; 
}
/* -------------------------------------------------------------------------- */

function gentable(pfx,sfx,caption,sect,sizes,colp,clicker,style)
{ var table,tr,td,s,ind,colno,nload,p,item,nbar,i,j,a,nim,str,layind,ll,d ; 
  var padflag,k,img,ngps,legend,sind,ncol,nrow,colour,lflag=0,lmode,gap,sno ; 
  var imgw,imgh,longdim,colwid,gmi,alist,trlist,gmilist,f,g,shortdim,docapt ;
  var laysize = colp.laysize , doprefetch = 1 ;
  var clickind,itable,alllayout ; 

  function hoverfactory(ind) { return function() { clicker(ind) ; } } 
  function unhoverfactory() 
  { return function() { this.border.color = style.bg ; } } 

  function addrow(div,captionflag,tab) 
  { if(!div) return ;
    var tr = document.createElement('tr') ;
    var td = document.createElement('td') ;
    td.setAttribute('colspan',99999) ;
    if(captionflag)
    { td.setAttribute("style","width:1px;font-size:90%;color:"+style.mg+
                              ";padding:6px 4px 8px") ; 
      td.setAttribute('align','left') ;  
      td.innerHTML = div ; 
    }
    else td.appendChild(div) ; 
    tr.appendChild(td) ; 
    if(tab) tab.appendChild(tr) ; else table.appendChild(tr) ; 
  }
  function hr()
  { var d = document.createElement('div') ; 
    d.setAttribute('style','width:100%;height:1px;background:'+style.mg+
                   ";margin-bottom:4px") ; 
    addrow(d) ; 
  }
  
  // prefetch will be performed when nimg thumbs have been loaded
  for(nimg=sno=0;sno<sect.length;sno++) if(!sect[sno].layout)
    nimg += sect[sno].list.length ;
  if(nimg==0||sect[0].layout) doprefetch = 0 ; 

  table = document.createElement('table') ;
  table.setAttribute('cellspacing','0') ; 
  table.setAttribute('cellpadding','0') ; 
  table.setAttribute('align','center') ; 

  if(pfx) addrow(pfx) ; 
  if(caption) addrow(caption,1) ;

  for(alllayout=1,sno=0;sno<sect.length;sno++)
    if(!sect[sno].layout) alllayout = 0 ; 

  for(layout=null,layind=nload=sno=0;sno<sect.length;sno++)
  { item = sect[sno] ; 
    layout = item.layout ;
    ll = item.list ; 
    if(layout) { sind = laysize[layind++] ; if(sind==null) layout = null ; }
    if(layout)
    { ncol = layout.ncol ; 
      nrow = ll.length / ncol ;
      lflag = layout.flag ; 
      itable = document.createElement('table') ;
      itable.setAttribute('cellspacing','0') ; 
      itable.setAttribute('cellpadding','0') ; 
      itable.setAttribute('align','center') ; 
      itable.setAttribute('style','margin:0px auto') ; 
      if(!lflag) colwid = new Array(ncol) ; 
    }
    else 
    { sind = -1 ; nrow = 0 ; ncol = colp.ncol ; lflag = 0 ; itable = table ; }

    hr() ; 
    tr = document.createElement('tr') ;
    td = document.createElement('td') ;
    td.setAttribute('align','left') ;  
    if(lflag) td.setAttribute('colspan',2*ncol-1) ; 
    else td.setAttribute('colspan',ncol) ;  

    if( item.title || (item.gps&&item.gps.length) || item.caption )
    { p = document.createElement("p") ; 
      p.setAttribute("style","font-size:110%;padding-top:8px;"+
                             "padding-bottom:4px;padding-left:4px") ; 
      if(item.title&&(!item.gps||!item.gps.length))
      { s = document.createTextNode(item.title) ;
        if(item.href)
        { a = document.createElement('a') ; 
          a.setAttribute('href',item.href) ; 
          a.setAttribute('style','color:'+style.fg) ; 
          a.appendChild(s) ; 
          alist = [a] ;
          s = a ; 
        }
        p.appendChild(s) ; 
      }
      else if(item.gps&&item.gps.length) // we have GPS tracks
      { if(item.title) 
          p.appendChild(document.createTextNode(item.title+' : ')) ;
        if(item.gps.length==1) // one track only
        { a = document.createElement('a') ; 
          a.setAttribute('href',item.gps[0]) ; 
          a.setAttribute('style','font-size:90%') ; 
          a.setAttribute('target','_blank') ; 
          a.appendChild(document.createTextNode('[GPS track ')) ;
          a.appendChild(newtabdiv(style.fg,style.bg)) ; 
          a.appendChild(document.createTextNode('\u202f]')) ;
          p.appendChild(a) ;
        }
        else // several tracks: need to differentiate them
        { s = document.createElement('span') ; 
          s.setAttribute('style','font-size:90%') ; 
          for(ngps=i=0;i<item.gps.length;i++)
            if(item.gps[i]!='-'&&item.gps[i]!='–') ngps += 1 ; 
          if(ngps==1) legend = '[GPS track: ' ; 
          else legend = '[GPS tracks: ' ;
          s.appendChild(document.createTextNode(legend)) ;
          for(nbar=i=0;i<ll.length;i++) if(ll[i].barred=='|') nbar += 1 ; 
          if(nbar+1>=item.gps.length&&!item.nomatch) // square icon links
            for(gpsno=0,i=0;i<ll.length;gpsno++)
          { if(item.gps[gpsno]=='-'||item.gps[gpsno]=='–')
              a = document.createElement('span') ; 
            else
            { a = document.createElement('a') ; 
              a.setAttribute('href',item.gps[gpsno]) ; 
              a.setAttribute('target','_blank') ; 
            }
            for(nim=1,i++;i<ll.length&&ll[i].barred!='|';i++) nim += 1 ;
            for(str = '',j=0;j<nim;j++)
            { str += '\u25a0' ; if(j<nim-1) str += '-' ; }
            a.appendChild(document.createTextNode(str)) ;
            s.appendChild(a) ;
            if(i<ll.length) s.appendChild(document.createTextNode(' | ')) ;
          }
          else for(i=0;i<item.gps.length;i++) // numbered links [(1), (2)...
            if(item.gps[i]!='-'&&item.gps[i]!='–')
          { a = document.createElement('a') ; 
            a.setAttribute('href',item.gps[i]) ; 
            a.appendChild(document.createTextNode(' ('+(i+1)+')')) ;
            s.appendChild(a) ;
          }
          else s.appendChild(document.createTextNode(' –')) ;
          s.appendChild(document.createTextNode(' : ')) ;
          s.appendChild(newtabdiv(style.fg,style.bg)) ; 
          s.appendChild(document.createTextNode('\u202f]')) ;
          p.appendChild(s) ;
        }
      } // end 'we have GPS tracks'
      if(item.caption) 
      { s = document.createElement('div') ; 
        s.innerHTML = item.caption ;
        s.setAttribute("style","color:"+style.mg+";font-size:85%;"+
                       "padding-top:4px;text-indent:16px;padding-left:4px") ; 
        p.appendChild(s) ; 
      }
      td.appendChild(p) ; 
    } // end check for defined title
    else td.setAttribute('height','6px') ; 
    tr.appendChild(td) ; 
    if(item.id) tr.setAttribute('id',item.id) ;
    // element to scroll to when returning from an image
    if(sno) item.scrollelement = tr ; else item.scrollelement = null ; 

    if(item.href) 
    { trlist = [tr] ; 
      gmilist = [] ; 
      if(ll.length>ncol)
      { ll = new Array(ncol) ; 
        for(nim=i=0;i<item.list.length;i++)
          if(Math.random()<(ncol-nim)/(item.list.length-i)) 
            ll[nim++] = item.list[i] ;
      }
    }
    itable.appendChild(tr) ; 

    /* ----------------------------- loop over images ----------------------- */

    for(gap=null,ind=0;ind<ll.length;ind++) 
    { item = ll[ind] ; 

      // extra padding at the bottom before a title row
      if(0==ind%ncol) 
      { tr = document.createElement('tr') ; 
        padflag = 4 ; 
        if(sno<sect.length-1&&ind+ncol>=ll.length)
        { padflag = 8 ; if(ncol>5) padflag += 2*(ncol-5) ; }
      } 
      // lmode is a per-layout parameter specifying the shape of a tessellation
      if(lflag==2&&ind==0) 
      { if(item.shape[0]>item.shape[1]) lmode = 'l' ; else lmode = 'p' ; } 

      clickind = ind ; 
      if(lflag==2&&lmode=='p'&&ind%(2*ncol)>=ncol)
      { if(ind%ncol==0) clickind = ind + (ncol-1) ; 
        else clickind = ind - 1 ; 
        item = ll[clickind] ;
      }

      // generate the image
      if(layout||doprefetch==0) img = genimage(item,sizes,sind) ; 
      else img = genimage(item,sizes,sind,function() 
                 { nload += 1 ; 
                   if(nload==nimg) preloader(sect[0].list[0],sizes)() ; 
                 } ) ;
      imgw = img.shape[0] ; 
      imgh = img.shape[1] ;
      img = img.img ; 
      if(layout&&(!lflag)&&!colwid[ind%ncol]) colwid[ind%ncol] = imgw ;

      // initialise cell dimensions now we’ve seen the first image
      // gap is a per-section size difference needed for tessellated layouts
      if(lflag&&gap==null) 
      { if(lflag==1)
        { if(sind<0) longdim = ll[ind+1].thumbshape[0] ;
          else longdim = ( ll[ind+1].shape[0] * sizes[sind].scale ) 
                                                           / sizes[0].scale ;
          if(longdim>imgw) shortdim = imgw ; 
          else { shortdim = longdim ; longdim = imgw ; }
          gap = longdim - shortdim ; 
        }
        else if(lflag==2)
        { if(imgw>imgh) { gap = imgw - imgh ; longdim = imgw ; }
          else { gap = imgh - imgw ; longdim = imgh ; }
        }
        for(i=0;i<2*ncol-1;i++)
        { d = document.createElement('col') ; 
          if(i&1) k = gap ; else k = 19 + longdim - gap ; 
          d.setAttribute('style','width:'+k+'px') ; 
          itable.appendChild(d) ;
        }
      }

      // empty cell for tessellated portrait-style layout
      if(lflag==2&&lmode=='p'&&ind%(2*ncol)==ncol) 
      { td = document.createElement('td') ;
        k = gap + (ncol-2) * (19+imgh) ; 
        td.setAttribute('style','width:'+k+'px;height:'+gap+'px') ;
        td.setAttribute('colspan',2*ncol-3) ; 
        tr.appendChild(td) ;
      }

      // generate the td and set its attributes
      td = document.createElement('td') ;
      td.setAttribute('align','center') ;  
      s = 'width:'+(19+imgw)+'px;height:'+(19+imgh)+'px;' ;
      if((!layout)&&(item.barred=='|'&&ind%ncol>0))
        s = 'border-left:1px solid '+style.mg+';' ;
      if(lflag==2)
      { if(item.shape[0]<item.shape[1]) td.setAttribute('rowspan',2) ;
        else td.setAttribute('colspan',2) ; 
      }
      else if(lflag==1)
        if(imgw==longdim) td.setAttribute('colspan',2) ;
      td.setAttribute('style',s) ;

      // set the image attributes
      if(alllayout||sect[sno].href) colour = style.bg ;  
      else if(item.visited) colour = style.visited ; 
      else colour = style.link ;
      s = 'cursor:pointer;border:1px solid ' + colour ;
      s += ';padding:4px;margin:4px 4px ' + padflag + 'px' ;
      // vertical bar: margin is t-r-b-l or t-lr-b
      if((!layout)&&(ind%ncol==0||item.barred!='|')) s += ' 5px' ;
      img.setAttribute('style',s) ; 
      if(sect[sno].href)
      { gmi = img ; 
        gmilist.push(gmi) ;
        img = document.createElement('a') ; 
        img.setAttribute('href',sect[sno].href) ; 
        img.appendChild(gmi) ; 
      }
      else
      { img.addEventListener('click',hoverfactory([sno,clickind]) ) ; 
        if(alllayout)
        { img.addEventListener('mouseout',function() 
                                { this.style.borderColor = style.bg ; } ) ; 
          if(item.visited) img.addEventListener('mouseover',function() 
                                { this.style.borderColor = style.visited ; } ) ; 
          else img.addEventListener('mouseover',function() 
                                { this.style.borderColor = style.link ; } ) ; 
        }
        if(item.title) img.setAttribute("title",item.title) ; 
      }
      td.appendChild(img) ; 
      tr.appendChild(td) ; 
      if(ind==ll.length-1||(ind+1)%ncol==0) 
      { if(sect[sno].href) trlist.push(tr) ; itable.appendChild(tr) ; }

      // empty cell for tessellated landscape-style layout
      if(lflag==2&&lmode=='l'&&ind%(2*ncol)==ncol) 
      { td = document.createElement('td') ;
        k = gap + (ncol-2) * (19+imgw) ; 
        td.setAttribute('style','width:'+k+'px;height:'+gap+'px') ;
        td.setAttribute('colspan',2*ncol-3) ; 
        tr.appendChild(td) ;
      }

      if(lflag==2&&ind%(2*ncol)==ncol)
      { itable.appendChild(tr) ; tr = document.createElement('tr') ; }

      // caption for layout
      docapt = alllayout && ind==ll.length-1 && !layout.notitle ;
      if(docapt) for(docapt=i=0;i<ll.length&&!docapt;i++)
        if(ll[i].title) docapt = 1 ; 
      if(docapt) for(i=0;i<nrow;i++)
      { for(tr=document.createElement('tr'),j=0;j<ncol;j++)
        { td = document.createElement('td') ;
          d = document.createElement('div') ;
          if(!lflag) k = colwid[j] ; 
          else if(lflag==1)
          { if((imgw==longdim&&j)||(imgw!=longdim&&!j))
            { td.setAttribute('colspan',2) ; k = longdim ; }
            else k = longdim - gap ;
          }
          else if(lflag==2)
          { if((lmode=='p'&&j!=ncol-1)||(lmode=='l'&&j!=0))
            { td.setAttribute('colspan',2) ; k = longdim ; }
            else k = longdim - gap ;
          }
          d.setAttribute('style','color:'+style.mg+';white-space:nowrap;'+
                                 'width:'+k+'px;overflow-x:hidden;'+
                                 'font-size:90%;text-align:center;') ;
          td.setAttribute('style','padding:3px 9px;') ;
          s = ll[j+ncol*i].title ;
          d.appendChild(document.createTextNode(s?s:'–')) ;
          td.appendChild(d) ;
          tr.appendChild(td) ;
        }
        itable.appendChild(tr) ; 
      }
      if(docapt)
      { d = document.createElement('div') ;
        d.setAttribute('style','width:100%;height:4px') ; 
        addrow(d,null,itable) ; 
      } // end loop for(i=0;i<nrow;i++)
    } // end loop over images

    if(sect[sno].href)
    { function hovercraft(gmilist,alist)
      { return function()
        { var i ; 
          for(i=0;i<gmilist.length;i++) 
            gmilist[i].style.borderColor = 'initial' ;
          for(i=0;i<alist.length;i++) alist[i].style = 'initial' ;
        } ;
      }
      function unhovercraft(gmilist,alist)
      { return function()
        { var i ; 
          for(i=0;i<gmilist.length;i++) 
            gmilist[i].style.borderColor = style.bg ;
          for(i=0;i<alist.length;i++) alist[i].style.color = style.fg ;
        } ;
      }
      f = hovercraft(gmilist,alist) ; 
      g = unhovercraft(gmilist,alist) ;
      for(i=0;i<trlist.length;i++) 
      { trlist[i].onmouseover = f ; trlist[i].onmouseout = g ; }
    }

    if(layout) addrow(itable) ; 
  }
  hr() ; 
  if(sfx) addrow(sfx) ; 
  return table ;
}
/* -------------------------------------------------------------------------- */

function dolayouts(r)
{ var sno,ll,layout,s,notitle,m,lcol,lrow,lht,i,c,lht,item,sumw,sumwt ; 
  var maxh,maxht,k,w,sh0,sh1,flag,w0,w1 ;

  function matches(sh0,sh1,parity)
  { if( sh0[0]==sh1[parity] && sh0[1]==sh1[1-parity] ) return 1 ; 
    else return 0  ;
  }

  for(sno=0;sno<r.length;sno++) if(layout=r[sno].layoutstr)
  { ll = r[sno].list ; 
    // parse options
    s  = layout.split(':') ;
    for(notitle=m=lcol=lrow=lht=i=0;i<s.length;i++)
      if((c=s[i].charAt(s[i].length-1))=='%')
    { lht = s[i].substring(0,s[i].length-1) ; 
      if(!isvalidnum(lht)) alert("Illegal height in layout "+layout) ; 
      lht = parseFloat(lht)/100 ; 
    }
      else if(c=='n'||c=='t') for(k=0;k<s[i].length;k++)
    { if((c=s[i].charAt(k))=='n') notitle = 1 ; 
      else if(c=='t') m = 1 ; 
      else alert('Unrecognised layout mode '+s[i]) ; 
    }
      else
    { if(isvalidnum(s[i]))
      { lcol = parseInt(s[i]) ; lrow = Math.floor(ll.length/lcol) ; }
      else 
      { c = s[i].split('x') ; 
        if(c.length!=2) alert("Uninterpreted field in layout "+layout) ; 
        if(!isvalidnum(c[0])||!isvalidnum(c[1])) 
          alert("Illegal layout "+layout) ; 
        lrow = parseInt(c[0]) ; 
        lcol = parseInt(c[1]) ; 
      }
      if(ll.length%(lrow*lcol)) 
        alert(layout+' is illegal for '+ll.length+' images') ;
    }
    lrow = ll.length / lcol ; 

    // see if we can use thumbs as normal images
    for(c=1,i=1;c&&(!m)&&i<ll.length;i++)
    { item = ll[i] ; 
      if(!item.shape) { m = 1 ; continue ; }
      if( item.thumbshape[0]*ll[0].shape[0]!=ll[0].thumbshape[0]*item.shape[0]  
       || item.thumbshape[1]*ll[1].shape[1]!=ll[1].thumbshape[1]*item.shape[1] )
        c = 0 ; 
    }

    // compute sums and maxes used for layout dimensions
    sumw = sumwt = maxh = maxht = null ; 
    maxw = new Array(lcol) ; 
    if(m||c) 
    { for(maxht=i=0;i<lcol;i++) 
      { maxw[i] = 0 ; w = ll[i].thumbshape[1] ; if(w>maxht) maxht = w ; }
      for(k=0;k<lrow;k++) for(i=0;i<lcol;i++) 
      { w = ll[i+lcol*k].thumbshape[0] ; if(w>maxw[i]) maxw[i] = w ; } 
      for(sumwt=i=0;i<lcol;i++) sumwt += maxw[i] ;
    }
    if(!m) 
    { for(maxh=i=0;i<lcol;i++) 
      { maxw[i] = 0 ; w = ll[i].shape[1] ; if(w>maxh) maxh = w ; }
      for(k=0;k<lrow;k++) for(i=0;i<lcol;i++) 
      { w = ll[i+lcol*k].shape[0] ; if(w>maxw[i]) maxw[i] = w ; } 
      for(sumw=i=0;i<lcol;i++) sumw += maxw[i] ;
    }

    // check whether we can do the tessellating layout
    flag = 0 ; 
    if(lrow==2&&lcol>=2) 
    { if(m) sh0 = ll[0].thumbshape ; else sh0 = ll[0].shape ; 
      if(sh0[0]!=sh0[1]) flag = 2 ; 
    }
    if(flag) for(i=0;i<ll.length;i+=lrow*lcol) 
    { if(m) { sh0 = ll[i].thumbshape ; sh1 = ll[0].thumbshape ; }
      else { sh0 = ll[i].shape ; sh1 = ll[0].shape ; }
      // compare this instance of layout with first instance in the section
      if((!matches(sh0,sh1,0))&&!matches(sh0,sh1,1)) { flag = 0 ; break ; }

      // now compare all shapes within the instance
      if(m) sh1 = ll[i+lcol-1].thumbshape ; else sh1 = ll[i+lcol-1].shape ;
      if(!matches(sh0,sh1,1)) { flag = 0 ; break ; }
      if(m) sh1 = ll[i+lcol].thumbshape ; else sh1 = ll[i+lcol].shape ;
      if(!matches(sh0,sh1,1)) { flag = 0 ; break ; }
      if(m) sh1 = ll[i+2*lcol-1].thumbshape ; else sh1 = ll[i+2*lcol-1].shape ;
      if(!matches(sh0,sh1,0)) { flag = 0 ; break ; }

      if(sh0[0]<sh0[1]) sh0 = [ sh0[1] , sh0[0] ] ; // landscape version
      for(k=1;k<lcol-1;k++)
      { if(m) sh1 = ll[i+k].thumbshape ; else sh1 = ll[i+k].shape ;
        if(!matches(sh0,sh1,0)) { flag = 0 ; break ; }
        if(m) sh1 = ll[i+k+lcol].thumbshape ; else sh1 = ll[i+k+lcol].shape ;
        if(!matches(sh0,sh1,0)) { flag = 0 ; break ; }
      }
    }
    // and at this point flag is 2 if we should tessellate
    if(flag==0&&lcol==2&&lrow>=2) 
    { if(m) { sh0 = ll[0].thumbshape[0] ; sh1 = ll[1].thumbshape[0] ; }
      else { sh0 = ll[0].shape[0] ; sh1 = ll[1].shape[0] ; }
      if(sh0!=sh1) for(flag=1,i=0;flag&&i<ll.length;i+=2)
      { if(m) { w0 = ll[i].thumbshape[0] ; w1 = ll[i+1].thumbshape[0] ; }
        else { w0 = ll[i].shape[0] ; w1 = ll[i+1].shape[0] ; }
        if((!(w0==sh0&&w1==sh1))&&!(w0==sh1&&w1==sh0)) flag = 0 ;
      }
    }

    // default layout row ht
    if(!lht) 
    { if(flag==2) 
      { lht = ll[0].shape[0]/(ll[0].shape[0]+ll[0].shape[1]) ; 
        if(lht<0.5) lht = 1 - lht ; 
      }
      else lht = 1/lrow ; 
      lht *= 0.95 ;
    }

    r[sno].layout = { ncol:lcol , ht:lht , mustthumb:m , canthumb:c ,
                      flag:flag , notitle:notitle , 
                      sumwt:sumwt , sumw:sumw , maxht: maxht , maxh:maxh } ;
  }
}

• enterfullscreen     • exitfullscreen     • queryfullscreen     • querycanfullscreen     • isvalidnum     • clone     • reluri     • parsetitle     • setdomtitle     • advance     • setdomdesc     • getphotolist     • geturialias     • dourialias     • genobject     • genimgobject     • getxmlsize     • retlink     • imgshape     • sparepix     • jpg     • srcset     • getsize     • genimage     • msgfactory     • function     • findimage     • preloader     • loadfactory     • thumb     • showimg     • imgind     • dothumbs     • loadpix     • genpic     • navcell     • menufactory     • resize     • rescale     • imgsize     • setimgpos     • expandcaption     • setimg     • makecaption     • uncaption     • photoinfodiv     • genmenu     • drawmenuicon     • displaymenu     • delmenu     • offerfullscreen     • getcatval     • pixhelpdiv     • addcell     • pixdocspan     • pixinfodiv     • newtabdiv     • genrect     • genpoly     • startswipe     • midswipe     • endswipe     • selcat     • keepquery

// www.masterlyinactivity.com/software/pix.html

var pixlib = 1 ; 
var xcols = { link: '#66aaaa' , visited: '#cc3388' , active: '#404040' ,
              mlink:'#2244cc' , mvisited:'#cc3388' , mactive:'#808080'} ; 
var xstyle = { fg:'silver' , bg:'black' ,  mg: '#a4a4a4' , 
               mfg:'black' , mbg:'white' , farg:null , 
               font:'helvetica' , titlefont:'helvetica' , 
               title:null   , titlebg:null } ; 
var defstyle = clone(xcols,xstyle) ; 

/* -------------------------------------------------------------------------- */

function enterfullscreen() 
{ var doo = 1 ; 
  if(document.documentElement.requestFullscreen) 
    document.documentElement.requestFullscreen() ;
  else if(document.documentElement.mozRequestFullScreen) 
    document.documentElement.mozRequestFullScreen() ;
  else if(document.documentElement.webkitRequestFullscreen) 
    document.documentElement.webkitRequestFullscreen() ;
  else if(document.documentElement.msRequestFullscreen) 
    document.documentElement.msRequestFullscreen() ;
  else doo = 0 ; 
}
function exitfullscreen() 
{ if(document.exitFullscreen) document.exitFullscreen() ;
  else if(document.mozExitFullScreen) document.mozExitFullScreen() ;
  else if(document.webkitExitFullscreen) document.webkitExitFullscreen() ;
  else if(document.msExitFullscreen) document.msExitFullscreen() ;
}
function queryfullscreen() 
{ if(document.fullScreen||document.mozFullScreen||document.webkitIsFullScreen)
    return 1 ; 
  else return 0 ;
}
function querycanfullscreen()
{ if ( document.documentElement.requestFullscreen
    || document.documentElement.mozRequestFullScreen
    || document.documentElement.webkitRequestFullscreen 
    || document.documentElement.msRequestFullscreen ) return 1 ; 
  else return 0 ;
}
function isvalidnum(x) { return !isNaN(parseFloat(x)) && isFinite(x) ; }
function clone(x,y) 
{ var i,z={} ; 
  if(x) for(i in x) z[i] = x[i] ; 
  if(y) for(i in y) z[i] = y[i] ; 
  return z ; 
}
/* ---------------------------- relative uri  ------------------------------- */

function reluri(u1,u2) 
{ if(u1==null||u1==undefined||u2==null||u2==undefined) return u2 ; 
  var u = u2.toLowerCase() ; 
  if( u.substring(0,7)=="http://" || u.substring(0,7)=="file://"
   || u.substring(0,8)=="https://" ) return u2 ;
  var last = u1.lastIndexOf('/') ; 
  if(last<0) return u2 ; 
  u1 = u1.substring(0,last) ; 

  while(u2.substring(0,3)=='../')
  { last = u1.lastIndexOf('/') ; 
    if(last<0) { if(u1) return u2.substring(3) ; else return u2 ; }
    u2 = u2.substring(3) ; 
    u1 = u1.substring(0,last) ; 
  }
  return u1 + '/' + u2 ; 
}
/* --------------------------- parse section title  ------------------------- */

function parsetitle(sect)
{ var s = sect.title , ss , j ;
  if(!sect.href||s.length<9||s.substring(s.length-8)!=' photos)') 
    return [ s , null ] ;
  s = s.substring(0,s.length-8) ; 
  j = s.lastIndexOf('(') ;
  if(j<0||!isvalidnum(ss=s.substring(j+1))) return [ s , null ] ;
  else 
  { while(j>0&&s.charAt(j-1)==' ') j -= 1 ; 
    return [ s.substring(0,j) , parseInt(ss) ] ;
  }
}
/* ------------------------------- settitle --------------------------------- */

function setdomtitle(newtitle) 
{ var h , t=document.getElementsByTagName('title') ;
  if(t.length==0)
  { h = document.getElementsByTagName('head') ;
    if(h.length==0) h = document.createElement('head') ; else h = h[0] ;
    t = document.createElement('title') ;
    h.appendChild(t) ;
  }
  else t = t[0] ; 
  while(t.childNodes.length>0) t.removeChild(t.firstChild) ; 
  if(newtitle) t.appendChild(document.createTextNode(newtitle)) ;
}
/* -------------------------------------------------------------------------- */

function advance(pl,si,dirn) 
{ var sno , ind ;
  if(si) { sno = si[0] ; ind = si[1] ; } 
  else if(dirn<0) { sno = pl.sect.length ; ind = 0 ; }
  else { sno = 0 ; ind = -1 ; }
  if(dirn<0)
  { if(ind>0) return [sno,ind-1] ; 
    else if(sno>0) return [ sno-1 , pl.sect[sno-1].list.length-1 ] ; 
    else return null ; 
  }
  else
  { if(ind<pl.sect[sno].list.length-1) return [sno,ind+1] ;
    else if(sno<pl.sect.length-1) return [sno+1,0] ; 
    else return null ;
  }
}
/* -------------------------------- setdesc --------------------------------- */

function setdomdesc(desc) 
{ if(!desc) return ; 
  var h = document.getElementsByTagName('head') ;
  if(h.length==0) h = document.createElement('head') ; else h = h[0] ;
  var m = document.createElement('meta') ;
  m.setAttribute('name','description') ; 
  m.setAttribute('content',desc) ; 
  h.appendChild(m) ;
}
/* -------------------------------------------------------------------------- */

function getphotolist(xmldoc,baseuri)
{ var tree,i,kids,j,k,item,links=[],hithumb=null,pixp=null,imagedir=null ;
  var aliases=[],ll=[],ss=[],title=[],ic=[],cat,img,orp=[],sty=[] ;
  var iname,visibility,cats=[],sect=[],sno,thind,sss,caption=[],c,field ;
  var hithind,r0,r1,doraw,R,ind,umin,umax,base={},retpage,retid ; 
  var sh={shape:null,rawshape:null,thumbshape:null} ; 
  var imgfields =  ['name','title','caption','display',
                    'retpage','retid','overlay','id'] ;
  var sectfields = ['title','caption','gps','id','layout','href'] ;
  var heritable  = ['imagedir','extn','cat',
                    'shape','rawshape','thumbshape','hithumb'] ;
  imgfields  =  imgfields.concat(heritable) ; 
  sectfields = sectfields.concat(heritable) ; 

  // extract fields
  tree = xmldoc.getElementsByTagName('photolist')[0].childNodes ;
  for(i=0;i<tree.length;i++)
  { iname = tree[i].nodeName ; 
    if(iname=='base') base = genobject(tree[i],heritable) ; 
    else if(iname=='link') 
      links.push(genobject(tree[i],['name','href','cat','display','type'])) ;
    else if(iname=='style') 
    { item = genobject(tree[i],['bg','mg','fg','font','mbg','mfg','farg',
                                  'title','titlebg','titlefont','cat']) ;
      sty.push({style:item,cat:item.cat}) ;
    }
    else if(iname=='title') 
    { c = genobject(tree[i],['cat']) ;
      if(c.cat) title.push({text:tree[i].textContent,cat:c.cat}) ;
      else title.push({text:tree[i].textContent}) ; // for Safari
    }
    else if(iname=='caption') 
    { c = genobject(tree[i],['cat']) ;
      if(c.cat) caption.push({text:tree[i].textContent,cat:c.cat}) ;
      else caption.push({text:tree[i].textContent}) ; // for Safari
    }
    else if(iname=='hithumb') hithumb = genobject(tree[i],['suffix','scale']) ;
    else if(iname=='icon') ic.push(genobject(tree[i],['href','cat'])) ;
    else if(iname=='pixpage'||iname=='gallery') 
      pixp = tree[i].getAttribute('href') ;
    else if(iname=='origin') orp.push(genobject(tree[i],['href','cat'])) ;
    else if(iname=='imagedir') imagedir = tree[i].getAttribute('href') ;
    else if(iname=='alias') 
      aliases.push(genobject(tree[i],['name','shape','uri'])) ;
    else if(iname=='sizes') 
    { for(kids=tree[i].childNodes,j=0;j<kids.length;j++)
        if(kids[j].nodeName=='size')
          ss.push(genobject(kids[j],['suffix','scale','type','fontsize'])) ;
    }
    else if(iname=='section') 
    { if(ll.length) 
      { sect.push(genimgobject(null,sectfields)) ; 
        sect[sect.length-1].list = ll ; 
      }
      sect.push(genimgobject(tree[i],sectfields)) ;
      for(ll=[],kids=tree[i].childNodes,j=0;j<kids.length;j++)
        if(kids[j].nodeName=='img') ll.push(genimgobject(kids[j],imgfields)) ; 
      sect[sect.length-1].list = ll ; 
      ll = [] ; 
    }
    else if(iname=='img') ll.push(genimgobject(tree[i],imgfields)) ;
  }
  if(ll.length) sect.push( { title:null, gps:null,    cat:null,
                             id:null,    layout:null, list:ll } ) ; 

  /* ------------------------------------------------------------------------ */

  // the following line seems to allow for an alias of an alias - can I GRO it?
  for(i=0;i<aliases.length;i++) if(aliases[i].shape)
    aliases[i].shape = getxmlsize(aliases[i].shape) ;

  // substitute GPS aliases
  for(sno=0;sno<sect.length;sno++) geturialias(sect[sno],'gps',aliases) ; 

  // fill in the fontsize field in sizes
  for(R=k=ind=0;ind<ss.length;ind++) if(!ss[ind].type) 
  { R += Math.log(ss[ind].scale) ; k += 1 ; }
  if(k) R /= k ;

  for(ind=0;ind<ss.length;ind++) if( (!ss[ind].type) && (!ss[ind].fontsize) ) 
    ss[ind].fontsize = 16 * Math.exp((Math.log(ss[ind].scale)-R)/3) ;

  for(umax=umin=null,ind=0;ind<ss.length;ind++) if(!ss[ind].type)
  { if(umin==null||ss[ind].fontsize<umin) umin = ss[ind].fontsize ;
    if(umax==null||ss[ind].fontsize>umax) umax = ss[ind].fontsize ;
  }
  for(ind=0;ind<ss.length;ind++) if(ss[ind].type=='raw'&&!ss[ind].fontsize) 
    ss[ind].fontsize = umax ; 

  // if we have a standalone hithumb, push it into the sizes
  if(hithumb) ss.push({suffix:hithumb.suffix , scale:hithumb.scale,
                       type:'hithumb' , fontsize:null }) ; 

  // find indices of thumb, hithumb in sizes
  for(hithind=thind=-1,i=1;i<ss.length;i++) 
    if(ss[i].type=='thumb') { thind = i ; r0 = ss[i].scale / ss[0].scale ; }
    else if(ss[i].type=='hithumb') hithind = i ; 

  // scale factors for thumbs and raws relative to first size
  if(thind>=0) r0 = ss[thind].scale / ss[0].scale ; 

  for(doraw=r1=ind=0;ind<ss.length;ind++) if(ss[ind].type=='raw')
  { r1 = ss[ind].scale / ss[0].scale ; doraw = 1 ; }

  if(!base.imagedir) base.imagedir = imagedir ; 

  // split out starred field, accumulate cats, fill in shapes etc
  for(retpage=retid=null,sno=0;sno<sect.length;sno++)
  { item = sect[sno] ;
    // inherit heritables
    for(j=0;j<heritable.length;j++) 
    { field = heritable[j] ; 
      if(item[field]=='') item.field = null ;
      else if(!item[field]) item[field] = base[field] ; 
    }
    item.layoutstr = item.layout ; 
    item.layout = null ; 

    for(ll=item.list,i=0;i<ll.length;i++) 
    { item = ll[i] ; 

      // retid
      if(item.retid) retid = item.retid ; 
      else if(retpage&&(item.retpage==retpage||item.retpage==null))
        item.retid = retid ; 
      // retpage
      if(item.retpage=='') item.retpage = null ; 
      else if(item.retpage) retpage = item.retpage ; 
      else item.retpage = retpage ; 

      // split display field into visibility and display
      v = null ; 
      if(item.display=='*') { item.display = null ; v = '*' ; }
      else if(item.display=='|*') { item.display = '|' ; v = '*' ; }
      else if(item.display=='none') { item.display = null ; v = 'none' ; }
      else if(item.display=='|none') { item.display = '|' ; v = 'none' ; }
      else if(item.display&&item.display!='|') 
        alert('Illegal display value “'+item.display+'” for '+item.name) ; 
      item.visibility = v ;

      // inherit heritables; process extn and imagedir->filename
      for(j=0;j<heritable.length;j++) 
      { field = heritable[j] ; 
        if(item[field]=='') item.field = null ;
        else if(!item[field]) item[field] = sect[sno][field] ; 
      }
      if(item.extn) item.extn = '.' + item.extn ; else item.extn = '.jpg' ;
      if(item.imagedir) item.filename = item.imagedir + '/' + item.name ; 
      else item.filename = item.name ; 

      // accumulate categories
      if((cat=item.cat)) 
      { for(j=0;j<cats.length&&cats[j]!=cat;j++) ;
        if(j==cats.length) cats.push(cat) ; 
      }

      // parse shape, rawshape, thumbshape and hithumb, substituting from alias
      for(j in sh) if(item[j]) 
      { item[j] = getxmlsize(item[j],aliases) ;
        if(!item[j][1]) alert('Illegal '+j+' for '+item.name) ;
      }
      if(item.hithumb)
      { item.hithumb = getxmlsize(item.hithumb,aliases) ; 
        if(item.hithumb[1]) alert('Illegal hithumb scale for '+item.name) ;
      }

      // fill in thumbshape as [w,h,sfx,fontsize] for all items
      if(!item.thumbshape)
      { if(thind<0) { alert('no thumbshape for '+item.name) ; throw '' ; }
        if(!item.shape) { alert('no shape for '+item.name) ; throw '' ; }
        for(item.thumbshape=[0,0,0,0],k=0;k<2;k++) 
          item.thumbshape[k] = Math.floor(0.5+item.shape[k]*r0) ; 
      }
      if(!item.thumbshape[2]) 
      { if(thind>=0) item.thumbshape[2] = ss[thind].suffix ;
        else { alert('no thumbshape suffix for '+item.name) ; throw '' ; }
      }
      if(!item.thumbshape[3]) item.thumbshape[3] = umin ; 

      //    fill in hithumb as [scale,"scale",sfx] whenever a regular image  
      // can't be used.
      //    the rule for srcsets will be this:
      //       o. if the item has a hithumb, then offer it; 
      //       o. else, if the thumb has the same aspect ratio as regular 
      //          images, offer all the regular images;
      //       o. else don't offer a srcset at all
      if(hithind>=0&&!item.hithumb)
        if(item.thumbshape[0]*item.shape[1]!=item.thumbshape[1]*item.shape[0]) 
          item.hithumb = [ ss[hithind].scale , 0 , ss[hithind].suffix ] ;
      // convert from fp number to char string
      if(item.hithumb)
      { k = item.hithumb[0].toFixed(1) ; 
        if(k.charAt(k.length-1)=='0') k = k.substring(0,k.length-2) ;
        item.hithumb[1] = k ; 
      }

      // rawshape
      if(r1&&!item.rawshape) for(item.rawshape=[0,0],k=0;k<2;k++) 
        item.rawshape[k] = Math.floor(0.5+item.shape[k]*r1) ; 

      for(j in {retpage:null,imagedir:null}) geturialias(item,j,aliases) ; 
      item.visited = item.overlaid = 0 ; 
      item.loaded = new Array(ss.length+1) ; 
      for(j=0;j<=ss.length;j++) item.loaded[j] = 0 ; 
    }
  } 

  for(i=0;i<links.length;i++) geturialias(links[i],'href',aliases) ; 
  geturialias(pixp,'href',aliases) ; 
  geturialias(ic,'href',aliases) ; 
  geturialias(imagedir,'href',aliases) ; 

  // at this point, the thumb/hithumb entries in sizes are no longer needed
  // ** it would be nice, at some time, to remove 'raw' too, and to remove the
  // ** filename field from items
  for(j=i=0;i<ss.length;i++) if(ss[i].type!='thumb'&&ss[i].type!='hithumb')
    ss[j++] = ss[i] ; 
  ss.length = j ; 

  if(baseuri) // this is an obligatory parameter in practice
  { if(baseuri.substr(0,5)=='http:'&&document.URL.substr(0,6)=='https:')
      baseuri = 'https://www.routemaster.app/resources/fileserver.php?' + 
                baseuri ; 
    pixp = reluri(baseuri,pixp) ;
    for(sno=0;sno<sect.length;sno++)
    { item = sect[sno] ; 
      if(item.gps) for(j=0;j<item.gps.length;j++) 
        item.gps[j] = reluri(baseuri,item.gps[j]) ; 
      for(ll=item.list,i=0;i<ll.length;i++) 
      { ll[i].filename = reluri(baseuri,ll[i].filename) ; 
        ll[i].imagedir = reluri(baseuri,ll[i].imagedir) ;
        ll[i].retpage = reluri(baseuri,ll[i].retpage) ;
      }
    }
    for(i=0;i<links.length;i++) if(links[i].href) 
      links[i].href = reluri(baseuri,links[i].href) ; 
    for(i=0;i<sty.length;i++)
    if(sty[i].farg&&(k=sty[i].farg.indexOf('url('))>=0)
    { sss = sty[i].farg.substring(k+4) ; 
      j = sss.indexOf(')') ; 
      if(j>=0) sty[i].farg = sty[i].farg.substring(0,k+4) + 
                 reluri(baseuri,sss.substring(0,j)) + sss.substring(j) ; 
    }
  }

  return { title:title , icon:ic , rawsect:sect , sect:null , sizes:ss , 
           links:links , gallery:pixp , origin:orp , caption:caption , 
           vis:null , cats:cats , maxthumb:null , alias:aliases , 
           style:sty } ;
  /* ------------------------------------------------------------------------ */

  function geturialias(item,field,aliases)
  { var i ;
    if(!item||!item[field]) return ;
    if(field!='gps') item[field] = dourialias(item[field],aliases) ; 
    else for(i=0;i<item[field].length;i++)
      item[field][i] = dourialias(item[field][i],aliases) ; 

    function dourialias(item,aliases)
    { var k,aliasno,uri ;
      k = item.indexOf(',') ; 
      if(k<=0) return item ; 
      alias = item.substring(0,k) ;
      uri = item.substring(k+1) ; 
      for(k=0;k<aliases.length&&
              (aliases[k].name!=alias||aliases[k].uri==null);k++) ;
      if(k==aliases.length) return ; 
      if(aliases[k].uri.charAt(aliases[k].uri.length-1)!='/'&&uri.charAt(0)!='/') 
        return aliases[k].uri + '/' + uri ; 
      else return aliases[k].uri + uri ; 
    }
  }
  /* ------------------------------------------------------------------------ */

  function genobject(node,l)
  { var i,j,v,r={},ind ;
    for(i=0;i<l.length&&l[i];i++) 
    { v = node.getAttribute(l[i]) ; 
      if((l[i]=='scale'||l[i]=='fontsize')&&v!=null&&v!='') v = parseFloat(v) ; 
      else if(l[i]=='gps'&&v!=null) v = v.split(' ') ; 
      if(v!=null) r[l[i]] = v ; 
    }
    return r ;
  }
  /* ------------------------------------------------------------------------ */

  function genimgobject(node,fields)
  { var img,i,kids=node.childNodes ;
    if(!node) 
    { for(img={},i=0;i<fields.length;i++) img[fields[i]] = null ; return ; }
    img = genobject(node,fields) ;
    for(i=0;i<kids.length;i++) if(kids[i].nodeName=='caption')
      if(kids[i].textContent) img.caption = kids[i].textContent ;
    return img ; 
  }
  /* ------------------------------------------------------------------------ */

  function getxmlsize(item,aliases)
  { var i,j,w,sfx ; 
    if(!item) return null ;
    i = item.indexOf('[') ; 
    if(i>=0)
    { for(j=0;j<i&&item.charAt(j)==' ';j++) ; 
      sfx = item.substring(j,i) ; 
      // advance i beyond spaces
      for(i++;i<item.length&&item.charAt(i)==' ';i++) ;
      for(j=i;j<item.length&&item.charAt(j)!=','&&item.charAt(j)!=' '
                           &&item.charAt(j)!=']';j++) ;
      if(j==item.length) { alert("badly formed shape: "+item) ; throw '' ; }
      w = item.substring(i,j) ;
      if(item.charAt(j)==']') return [ parseFloat(w) , null, sfx ] ;
      for(i=j;i<item.length&&item.charAt(i)!=',';i++) ;
      for(j=i;j<item.length&&item.charAt(j)!=']';j++) ;
      if(j==item.length) { alert("badly formed shape: "+item) ; throw '' ; }
      return [ parseInt(w) , parseInt(item.substring(i+1,j)) , sfx ] ;
    }
    if(aliases) for(i=0;i<aliases.length;i++)
      if(item==aliases[i].name) return aliases[i].shape ;
    alert("shape "+item+" not defined") ; 
    throw '' ; 
  }
}
/* -------------------------------------------------------------------------- */

function retlink(item)
{ var blink ;
  if(!item.retpage) return null ;
  blink = item.retpage+'.html' ;
  if(item.retid) blink += '#' + item.retid ;
  return blink ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ----------------------------- image functions ---------------------------- */

function imgshape(item,sizes,sizeno)
{ if(sizeno<0) return item.thumbshape ; 
  else if(sizes[sizeno].type=='raw') return item.rawshape ; 
  return [ Math.floor(0.5+item.shape[0]*sizes[sizeno].scale/sizes[0].scale) ,
           Math.floor(0.5+item.shape[1]*sizes[sizeno].scale/sizes[0].scale) ] ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ----- sparepix finds the margins left if item is displayed at usesize ---- */

function sparepix(item,sizes,sizeno)
{ var shape = imgshape(item,sizes,sizeno) ; 
  if(!shape) 
  { alert('Somehow an illegal size was passed to sparepix') ; return -1000 ; }
  var w = shape[0] , h = shape[1],r ;
  if(sizeno<0) r = shape[3] ; else r = sizes[sizeno].fontsize ;
  h += Math.floor(0.5+1.25*r) + 2 ;
  if(item.caption||item.overlay) h += Math.floor(0.5+r) + 2 ;

  r = [ window.innerHeight-50-h , window.innerHeight-h ] ;
  if(window.innerWidth-w<r[0]) r[0] = window.innerWidth-w ;
  if(window.innerWidth-w-50<r[1]) r[1] = window.innerWidth-w-50 ;
  return r ;
}
/* ---------------------- construct the jpg path name ----------------------- */

function jpg(item,sizes,sizeno,ovl)
{ var sfx ; 
  if(sizeno<0) sfx = item.thumbshape[2] ; else sfx = sizes[sizeno].suffix ; 
  return item.filename + sfx + (ovl?'.png':item.extn) ; 
}
/* ------------------- generate the srcset for an image --------------------- */

function srcset(item,sizes,sizeno,ovl,compression)
{ var i,s,hith,scale ;
  if(sizeno>=0&&sizes[sizeno].type=='raw') return '' ;
  if(!compression||compression<0) compression = 1 ; 

  if(sizeno<0) 
  { if(item.hithumb) return item.filename + item.hithumb[2] + item.extn +
                            ' ' + item.hithumb[1] + 'x' ;
    if(!item.shape) return '' ;
    if(item.thumbshape[0]*item.shape[1]!=item.thumbshape[1]*item.shape[0]) 
      return '' ;
    scale = ( sizes[0].scale * item.thumbshape[0] ) / item.shape[0] ;
  }
  else scale = sizes[sizeno].scale ;

  for(s='',i=0;i<sizes.length;i++) 
    if((!sizes[i].type)&&(sizes[i].scale>scale))
  { if(s!='') s += ', ' ;
    s += jpg(item,sizes,i,ovl) + ' ' + 
         (compression*sizes[i].scale/scale).toFixed(1) + "x" ;
  }
  return s ;
}
/* ------- getsize finds the largest image size which fits the screen ------- */

function getsize(item,sizes) 
{ var i,ibest,spare ;

  if(sizes.length==0) return -1 ; 
  for(ibest=i=-1;i<sizes.length;i++) if(i<0||!sizes[i].type)
  { spare = sparepix(item,sizes,i) ; // spare pix for landscape/portrait
    if(spare[0]>=0||spare[1]>=0) 
      if(ibest<0||sizes[i].scale>sizes[ibest].scale) ibest = i ; 
  }
  return ibest ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* --------------------------------- genimage ------------------------------- */

function genimage(item,sizes,sizeno,loadfunc,ovl) 
{ var img=document.createElement('img') , shape , s ;
  function msgfactory(name)
  { return function() 
    { var i = name.lastIndexOf('/') ; 
      i = name.substring(i+1) ; 
      console.log(i+' loaded') ;
    }
  }
  if((!sizeno)&&sizeno!=0&&sizes.length) sizeno = getsize(item,sizes) ;
  shape = imgshape(item,sizes,sizeno) ; 
  if(!shape) 
  { alert('Somehow an illegal size was passed to genimage') ; return null ; }
  img.setAttribute('width',shape[0]) ; 
  img.setAttribute('height',shape[1]) ; 
  if((s=srcset(item,sizes,sizeno,ovl))!='') img.setAttribute("srcset",s) ; 
  if(loadfunc) img.onload = loadfunc ;
//  else img.onload = msgfactory(item.filename) ; 
  img.setAttribute("src",jpg(item,sizes,sizeno,ovl)) ; 
  if(ovl) img.setAttribute("style","position:absolute;left:0;top:0") ; 
  return { img:img , shape:shape } ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------- find the list ind with a given name ----------------- */

function findimage(sect,name,serialno)
{ var sno,ind,ll,n,list,starred,i,j,v ; 

  if(!name) // return a random starred image
  { for(n=sno=0;sno<sect.length;sno++) n += sect[sno].list.length ;
    list = new Array(n) ; 
    for(starred=j=sno=0;sno<sect.length;sno++) 
      for(ll=sect[sno].list,i=0;i<ll.length;i++)
        if((v=ll[i].visibility)!='none')
    { if(starred) { if(v=='*') list[j++] = [sno,i] ; }
      else 
      { if(v=='*') { list[0] = [sno,i] ; j = starred = 1 ; }
        else list[j++] = [sno,i] ;
      }
    }
    if(j==0) { alert('no items to display') ; throw '' ; }
    return list[Math.floor(j*Math.random())] ;
  }

  if(!serialno) serialno = 0 ; 
  for(sno=0;sno<sect.length;sno++) for(ll=sect[sno].list,i=0;i<ll.length;i++)  
    if(ll[i].name==name&&ll[i].serialno==serialno) return [sno,i] ; 
  return null ;
}
function preloader(item,sizes,name)
{ var sizeno,loaded,ind ; 
  function loadfactory()
  { return function() { genimage(item,sizes,sizeno,null,1) ; } }
//  if(name) console.log(name.substring(name.lastIndexOf('/')+1)+' preloaded') ;
  if(!item) return function() { ; } ;

  sizeno = getsize(item,sizes) ;
  if(sizeno<0) ind = sizes.length ; else ind = sizeno ;
  if(item.loaded[ind]) return function() { ; } ; 
  item.loaded[ind] = 1 ; 
  if(!item.overlay) return function() 
  { // console.log('prefetching '+item.name+'('+sizeno+')') ;
    genimage(item,sizes,sizeno) ; 
  } ;
  else return function()
  { // console.log('prefetching '+item.name+'('+sizeno+') + overlay') ;
    genimage(item,sizes,sizeno,loadfactory()) ;
  }
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

var loads = [] , pending = [] , thislist = null , domparser = null ;

function thumb(p1,p2) 
{ if(typeof p1=='number') return showimg(p1) ; 
  else if(typeof p1=='string') return showimg(p1,null,p2) ; 
  else return showimg(p1) ;
}
function showimg(p1,p2,p3) 
{ var ind,s,thind,photolist,sh,i,shh,thindw,item,href,sect,a,img,title ; 
  var domopt=null ; 
  var listno=null,name=null,compression=null,serialno=null,linkuri=null ;

  if(loads.length==0) 
  { alert('No photo list – loadpix() not called') ; throw '' ; }

  if(typeof p1=='string') { name = p1 ; compression = p2 ; serialno = p3 ; }
  else if(typeof p1=='number') { listno = p1 ; compression = p2 ; }
  else if(typeof p1=='object')
  { listno = p1.listno ; 
    name = p1.name ; 
    compression = p1.reduction ; 
    serialno = p1.serialno ; 
    linkuri = p1.linkuri ; 
    domopt = p1.dom ; 
  }

  if(!compression) compression = -1 ; 
  if(listno!=0&&!listno) listno = loads.length-1 ; 

  if(listno<0||listno>=loads.length)
  { alert('list no out of range: '+listno+'/'+list.length) ; throw '' ; }
  photolist = loads[listno][1] ;

  if(!photolist) 
  { a = null ; 
    if(domopt) a = document.createElement('a') ;
    else document.write('<a id=pixpending' + pending.length+'></a>') ; 
    pending.push([name,serialno,listno,compression,linkuri,a]) ; 
    return a ; 
  }

  if(null==(ind=findimage(photolist.sect,name,serialno))) 
  { alert('Missing image: '+name+(serialno>0?('('+serialno+')'):'')) ;
    throw '' ;
  }

  href = photolist.gallery ;
  if(name) href += 
    '?image='+name + (serialno>0?('&serialno='+serialno):'') + '&mode=n' ;

  sect = photolist.sect[ind[0]] ;
  item = sect.list[ind[1]] ;
  thind = imgind(item,photolist.sizes,compression) ; 

  if(item.title) title = item.title ;
  else if(sect.title) title = parsetitle(sect)[0] ;
  else title = null ;
  s = srcset(item,photolist.sizes,thind.ind,0,thind.compression) ;

  if(domopt)
  { a = document.createElement('a') ;
    a.setAttribute('href',linkuri?linkuri:href) ; 
    a.setAttribute('imglink',href) ; 
    img = document.createElement('img') ; 
    img.setAttribute('class','pix') ; 
    img.setAttribute('src',jpg(item,photolist.sizes,thind.ind)) ; 
    img.setAttribute('width',thind.shape[0]) ; 
    img.setAttribute('height',thind.shape[1]) ; 
    if(title) img.setAttribute('title',title) ; 
    if(s) img.setAttribute('srcset',s) ; 
    a.appendChild(img) ; 
    return a ; 
  }

  href = '<a href="' + (linkuri?linkuri:href) +'" imglink="' + href +'">' + 
         '<img class=pix src="' + jpg(item,photolist.sizes,thind.ind) +
         '" width=' + thind.shape[0] + ' height=' + thind.shape[1] ;
  if(title) href += ' title="' + title + '"' ;
  if(s) href += ' srcset="' + s + '"' ; 
  href += '></a>' ; 
  document.write(href) ; 
}
/* -------------------------------------------------------------------------- */

function imgind(item,sizes,compression)
{ if(compression<0||compression==null||compression==undefined)
    return { ind:-1 , shape:item.thumbshape , compression:1 } ;
  
  var thind,j,thindw,shh ;
  var sh = [ item.shape[0]/compression , item.shape[1]/compression ] ;
  for(thind=-1,i=0;i<sizes.length;i++) if(!sizes[i].type)
  { shh = imgshape(item,sizes,i) ; 
    if(thind<0||(shh[0]>=sh[0]&&shh[0]<thindw)) 
    { thind = i ; thindw = shh[0] ; }
  }
  return { ind:thind , shape:sh , compression:thindw/sh[0] } ;
}
/* -------------------------------------------------------------------------- */

function dothumbs(listno)
{ var i,a,a,img,name,serialno,ind,thind,s,compression,item,photolist,href ;
  var sect,linkuri ; 

  for(i=0;i<pending.length;i++) if(pending[i][2]==listno)
  { name = pending[i][0] ;
    serialno = pending[i][1] ;
    compression = pending[i][3] ;
    linkuri = pending[i][4] ;
    photolist = loads[listno][1] ;
    if(null==(ind=findimage(photolist.sect,name,serialno))) 
    { alert('Missing image: '+name+(serialno>0?('('+serialno+')'):'')) ;
      continue ;
    }
    sect = photolist.sect[ind[0]] ;
    item = sect.list[ind[1]] ;
    if(pending[i][5]) a = pending[i][5] ;
    else a = document.getElementById('pixpending'+i) ; 
    if(!a) { alert('Logic error: no element pixpending'+i) ; continue ; }
    thind = imgind(item,photolist.sizes,compression) ; 

    href = photolist.gallery ;
    if(name) href += 
      '?image='+name + (serialno>0?('&serialno='+serialno):'') + '&mode=n' ;
    a.setAttribute('href',linkuri?linkuri:href) ; 
    a.setAttribute('imglink',href) ; 

    s = srcset(item,photolist.sizes,thind.ind,0,thind.compression) ;
    img = document.createElement('img') ; 
    img.setAttribute('src',jpg(item,photolist.sizes,thind.ind)) ; 
    img.setAttribute('class','pix') ; 
    img.setAttribute('width',thind.shape[0]) ; 
    img.setAttribute('height',thind.shape[1]) ; 
    if(item.title) img.setAttribute('title',item.title) ; 
    else if(sect.title) img.setAttribute('title',parsetitle(sect)[0]) ; 
    if(s) img.setAttribute('srcset',s) ; 
    a.appendChild(img) ; 
  }
}
/* -------------------------------------------------------------------------- */

function loadpix(xmlfl)
{ var i , xmldoc , xhttp = new XMLHttpRequest() ;
  if(!domparser) domparser = new DOMParser() ;
  for(i=0;i<loads.length;i++) if(loads[i][0]==xmlfl) return i ;
  xhttp.onreadystatechange = function() 
  { if(xhttp.readyState==4) 
    { if(xhttp.status==200)
      { xmldoc = 
          domparser.parseFromString(xhttp.responseText,"application/xml") ;
        for(i=0;i<loads.length&&loads[i][0]!=xmlfl;i++) ;
        if(i==loads.length) { alert('logic error') ; throw '' ; }
        loads[i][1] = getphotolist(xmldoc,xmlfl) ; 
        selcat(loads[i][1]) ; 
        dothumbs(i) ;
      }
      else alert("Unable to read "+xmlfl+": error code "+xhttp.status) ;
    }
  }
  loads.push([xmlfl,null]) ;
  xhttp.open("GET",xmlfl,true) ;
  xhttp.send() ;
  return loads.length-1 ; 
}
/* -------------------------------------------------------------------------- */

function genpic(element,item,title,sizes,laction,litem,retaction,raction,ritem,
                infowords,helpdiv,direction,infoflag,style) 
{ var i,a,udiv,ddiv,llink,rlink,fetchitem,drawparms,itemparms,name,d ;
  if(genpic.drawparms==undefined) genpic.drawparms = null ; 
  if(genpic.itemparms==undefined) genpic.itemparms = null ; 

  if(!element) 
  { genpic.itemparms = genpic.drawparms = null ; 
    if(genpic.element) for(element=genpic.element;element.firstChild;)
      element.removeChild(element.firstChild) ;
    return ; 
  }
  if(element=='uncaption') { uncaption() ; return ; }
  else if(element=='resize') { resize() ; return ; }
  else if(element=='enlarge') { resize(1) ; return ; }
  else if(element=='reduce') { resize(-1) ; return ; }
  genpic.element = element ; 
  genpic.style = defstyle ; 
  if(style) for(i in defstyle) if(style[i]) genpic.style[i] = style[i] ; 

  if(itemparms) alert('calling genpic when ' + itemparms.item.name +
                      ' is being displayed') ; 
  drawparms = genpic.drawparms ;
  var sizeno = getsize(item,sizes) ;
  item.visited = 1 ; 

  // decide which image if any to prefetch 
  fetchitem = null ; 
  if(direction<0) { if(litem) fetchitem = litem ; } 
  else if(ritem) fetchitem = ritem ; 

  genpic.itemparms = itemparms = 
              { item:          item,
                sizes:         sizes,
                sizeno:        sizeno,
                holdsize:      0,
                fetchitem:     fetchitem,
              } ;

  while(element.firstChild) element.removeChild(element.firstChild) ;

  /* -------------------- create a cell for navigation icon ----------------- */
  function navcell(action,item,dir)
  { var a,astyle='',div=document.createElement('div'),s='center' ;
    if(dir==-1) s = 'left' ; else if (dir==1) s = 'right' ;
    s = "display:inline-block;position:absolute;text-align:" + s ; 
    s += ((dir==1||dir==-1)?';font-size:20px;':';font-size:16px;') ;
    s += 'vertical-align:middle;width:16px;height:16px;' ;
    if(dir==2||dir==-2) s += 'left:16px;top:' + 8*(dir+2) + 'px' ;
    else s += 'top:16px;left:' + 16*(dir+1) + 'px' ;
    div.setAttribute("style",s) ; 

    if(!action) return div ; 
    div.style.cursor = 'pointer' ;

    a = document.createElement('span') ; 
    if(dir>0) s = "next: " + item.name + " [\u2192 key]" ; 
    else if(dir==0) s = "back to " + item + " [\u21b5 key]" ; 
    else s = "prev: " + item.name + " [\u2190 key]" ;
    a.setAttribute("title",s) ; 
    if(dir==0||item.visited) astyle = 'color:' + genpic.style.visited ;
    else astyle = 'color:' + genpic.style.link ;
    a.setAttribute("style",astyle) ; 
    if(dir>0) s = '>' ; else if(dir==0) s = '\u21b5' ; else s = '<' ;
    a.appendChild(document.createTextNode(s)) ;
    div.appendChild(a) ; 
    div.addEventListener('click',action) ; 
    return div ; 
  }
  /* ------------------------------------------------------------------------ */

  if(infoflag) name = infowords.notes ; else name = infowords.origin ; 
  d = document.createElement('div') ; 
  d.setAttribute('style','position:fixed;top:0;left:0;width:50px;height:50px;' +
                         'z-index:1;background:'+genpic.style.bg) ; 
  d.appendChild(navcell(laction,litem,-1)) ;  // '<'
  d.appendChild(udiv=navcell(null,null,-2)) ; // enlarge
  d.appendChild(navcell(raction,ritem,1)) ;   // '>'
  d.appendChild(ddiv=navcell(null,null,2)) ;  // reduce
  d.appendChild(navcell(retaction,name,0)) ;  // return
  element.appendChild(d) ;  

  genpic.drawparms = drawparms = 
              { headh:     null, 
                headf:     null, 
                caph:      null, 
                capf:      null, 
                enlink:    udiv,
                redlink:   ddiv,
                img:       null,
                ovl:       null,
                oving:     0,
                portrait:  null,
                navdiv:    d,
                bgdiv:     null,
                maindiv:   document.createElement('div'), 
                headdiv:   null,
                imgdiv:    document.createElement('div'),
                capdiv:    null,  
                capspan:   null,  
                capboffs:  null,
                capleft:   null,
                captionbox:null
              } ;

  if(genpic.style.farg&&genpic.style.farg!=genpic.style.bg) 
    drawparms.bgdiv = document.createElement('div') ;

  if(item.title)
  { drawparms.headdiv = document.createElement('div') ;
    drawparms.headdiv.appendChild(document.createTextNode(item.title)) ; 
  }

  setimg(item,sizes,sizeno,fetchitem) ; 

  if(drawparms.bgdiv) element.appendChild(drawparms.bgdiv) ; 
  drawparms.imgdiv.appendChild(drawparms.img) ;
  if(drawparms.headdiv) drawparms.maindiv.appendChild(drawparms.headdiv) ; 
  drawparms.maindiv.appendChild(drawparms.imgdiv) ; 
  if(drawparms.capdiv) drawparms.maindiv.appendChild(drawparms.capdiv) ; 

  element.appendChild(drawparms.maindiv) ; 
  genmenu(element,menufactory(item,title,sizes,infowords,
                                 retaction,helpdiv,infoflag),genpic.style) ;
          // end of genpic

  function menufactory(item,title,sizes,infowords,retaction,helpdiv,infoflag) 
  { return function() 
    { return photoinfodiv(item,title,sizes,infowords,retaction,helpdiv,infoflag) ; }
  } ;
  /* -------------- reposition image in response to a window resize --------- */

  function resize(k) 
  { var itemparms = genpic.itemparms , drawparms = genpic.drawparms ; 
    if(!itemparms) return ; 
    var item = itemparms.item , sizes = itemparms.sizes ;
    var sizeno = itemparms.sizeno ;
    if(k>0||k<0)
    { if((k=rescale(item,sizes,sizeno,k))==null) return ;
      uncaption() ; 
      itemparms.sizeno = setimg(item,sizes,k) ; // setimg draws img + navcells
      itemparms.holdsize = 1 ;  
      return ; 
    }
    var flag=0 , shape , spare , newsizeno = getsize(item,sizes) ;

    // holdsize indicates that an enlargement/reduction has been requested, and
    // shouldn't be made ineffective when the window is resized
    //    if holdsize isn't set and a new size is better, call setimg & return
    if(itemparms.holdsize==0&&newsizeno!=sizeno) 
    { setimg(item,sizes,newsizeno,itemparms.fetchitem) ; 
      itemparms.sizeno = newsizeno ;
      return ; 
    } 

    // if we get here, we have to retain size sizeno
    if(newsizeno==sizeno) itemparms.holdsize = 0 ; // because this is voluntary
    sizeno = newsizeno ; 

    // redisplay if portrait<->landscape makes a fit possible, else redraw
    if(!(shape=imgshape(item,sizes,sizeno))) 
    { alert('Somehow an illegal size was passed to resize') ; throw '' ; }
    spare = sparepix(item,sizes,sizeno) ;
    if(drawparms.portrait!=(spare[0]<spare[1])&&(spare[0]<0)!=(spare[1]<0))
      drawparms.portrait = 1-drawparms.portrait ;
    setimgpos(shape,item.caption) ;
  }
  /* ----------- rescale finds the next larger/next smaller image ----------- */

  function rescale(item,sizes,sizeno,dir)
  { var i,ind,size,cursize=imgsize(item,sizes,sizeno),bestsize ;
    function imgsize(item,sizes,sizeno)
    { var u = imgshape(item,sizes,sizeno) ; 
      if(u) return u[0] + u[1] ; else return null ; 
    }

    if(dir<0) 
    { for(bestsize=ind=null,i=-1;i<sizes.length;i++) if(i!=sizeno)
      { size = imgsize(item,sizes,i) ; 
        if(size&&size<cursize&&(bestsize==null||size>bestsize))
        { ind = i ; bestsize = size ; }
      }
      return ind ;
    }

    for(bestsize=ind=null,i=0;i<sizes.length;i++) if(i!=sizeno)
    { size = imgsize(item,sizes,i) ; 
      if(size&&size>cursize&&(bestsize==null||size<bestsize))
      { ind = i ; bestsize = size ; }
    }
    return ind ;
  }
  /* ------ setimgpos sets the image position according to window size ------ */

  // sets styles of parms.maindiv, parms.headdiv, parms.capdiv, parms.imgdiv
  // and sets event listener of parms.capspan; uses drawparms.captionbox
  //    basically it does the work for a new window size when this does not 
  // require a change of image size

  function setimgpos(shape,caption)
  { var itemparms = genpic.itemparms , drawparms = genpic.drawparms ; 
    var imgw=shape[0],imgh=shape[1],x,y,s,overflowx,overflowy,r,d ;
    var bgwidtop,bgwidside,style=genpic.style ;
    var pad = drawparms.headh + (drawparms.caph?drawparms.caph+4:0) ;
    // set space to the amount of room we've got for the image div
    var space = [ window.innerWidth , window.innerHeight-pad ] ;
    if(drawparms.portrait) space[0] -= 50 ; else space[1] -= 50 ; 

    // set imgw and imgh to the desired dimensions of the image div
    if(imgh>space[1]) imgw += 20 ; // generous inner scrollbar
    if(imgw>space[0]) imgh += 20 ; 

    // set (x,y) to the coords of the main div
    x = (window.innerWidth-imgw)/2 ;
    y = (window.innerHeight-imgh)/2 - drawparms.headh;
    if(drawparms.portrait) { if(x<50) x = 50 ; if(y<0) y = 0 ; }
    else { if(x<0) x = 0 ; if(y<50) y = 50 ; } 
    x = Math.floor(x) ; 
    y = Math.floor(y) ; 

    // now constrain imgw, imgh by the window size
    if(imgw>window.innerWidth-x) imgw = window.innerWidth - x ;
    if(imgh>window.innerHeight-y-pad) imgh = window.innerHeight-y-pad ;

    if(drawparms.bgdiv) 
    { bgwidtop = Math.floor(y/2) ; 
      if(bgwidtop>y-4) bgwidtop = y-4 ; 
      if(bgwidtop>72) bgwidtop = 72 ; else if(bgwidtop<0) bgwidtop = 0 ; 
      bgwidside = Math.floor(x/2) ; 
      if(bgwidside>x-4) bgwidside = x-4 ; 
      if(bgwidside>72) bgwidside = 72 ; else if(bgwidside<0) bgwidside = 0 ; 
      s = 'position:absolute;left:' + bgwidside + 'px;width:' +
          (window.innerWidth-2*bgwidside) + 'px;top:'+bgwidtop + 'px;height:' +
          (window.innerHeight-2*bgwidtop) + 'px;background:' + style.bg ;
      drawparms.bgdiv.setAttribute('style',s) ; 
    }

    s = 'position:absolute;left:'+x+'px;width:'+(window.innerWidth-x) +
        'px;top:'+y+'px;height:'+(pad+imgh)+'px;overflow:hidden' ;
    drawparms.maindiv.setAttribute('style',s) ; 

    if(drawparms.headh)
    { s = 'position:absolute;left:0;width:' + (window.innerWidth-x) +
          'px;top:2;height:' + (drawparms.headh-2) + 'px;overflow:hidden;' + 
          'text-overflow:ellipsis;white-space:nowrap;font-size:' +
           drawparms.headf + 'px;color:' + style.fg ;
      drawparms.headdiv.setAttribute('style',s) ; 
    }
    drawparms.capboffs = pad+imgh+y ;
    drawparms.capleft = x ; 

    function expandcaption()
    { if(drawparms.captionbox) return ; 
      if(drawparms.capspan.getBoundingClientRect().right<window.innerWidth) 
        return ;
      drawparms.captionbox = document.createElement('div') ; 
      drawparms.captionbox.setAttribute
           ( 'style','position:fixed;color:' + style.mfg + 
                     ';background:'+ style.mbg+';bottom:'+
                     (window.innerHeight-drawparms.capboffs)+';left:'+
                     drawparms.capleft+';padding:8;opacity:0.9' ) ;
//      drawparms.captionbox.appendChild(document.createTextNode(caption)) ;
      drawparms.captionbox.innerHTML = caption ;
      genpic.element.appendChild(drawparms.captionbox) ; 
      setTimeout(uncaption,1500+caption.length*20) ;
    }

    if(drawparms.capdiv!=null)
    { s = 'position:absolute;left:0;width:' + (window.innerWidth-x) +
          'px;bottom:0;height:' + drawparms.caph+'px;overflow:hidden;color:'+
          style.mg+';font-size:'+drawparms.capf+'px;text-overflow:' +
          'ellipsis;white-space:nowrap';
      drawparms.capdiv.setAttribute('style',s) ; 
      if(caption) 
        drawparms.capspan.addEventListener('mouseenter',expandcaption) ;
    }

    if(imgw<shape[0]) overflowx = 'scroll' ; else overflowx = 'hidden' ; 
    if(imgh<shape[1]) overflowy = 'scroll' ; else overflowy = 'hidden' ; 
    s = 'position:absolute;z-index:2;left:0;width:' + imgw + 'px;top:' +
        drawparms.headh + 'px;' +'height:' + imgh + 'px;overflow-x:' +
        overflowx + ';overflow-y:'+overflowy ;
    drawparms.imgdiv.setAttribute('style',s) ; 
  }
  /* --- adjust element attributes in accordance with a change of img size -- */

  // creates enlarge/reduce cells
  // sets drawparms.headh, drawparms.headf, drawparms.capf, drawparms,caph, 
  //    drawparms.portrait
  // calls genimage, assigns result to drawparms.img and appends it to 
  //    drawparms.imgdiv
  // sets drawparms.capf, drawparms.caph
  // calls setimgpos

  function setimg(item,sizes,sizeno,fetchitem)
  { var itemparms = genpic.itemparms , drawparms = genpic.drawparms ; 
    var font,spare,shape,img,ovl,s,i,capsize,astyle,resizer,a,motto ; 

    if(sizeno==null) return ;
    makecaption(item,sizeno) ;

    /* ------------------- create/reset enlarge/reduce icon ----------------- */

    for(dir=-1;dir<=1;dir+=2)
    { if(dir<0) div = drawparms.redlink ; else div = drawparms.enlink ;
      while(div.firstChild) div.removeChild(div.firstChild) ;
      resizer = dir<0 ? function(){resize(-1)} : function(){resize(1)} ;

      if(rescale(item,sizes,sizeno,dir)!=null) 
      { astyle = 'font-weight:normal;color:' + genpic.style.visited ;
        a = document.createElement('span') ; 
        motto = dir<0 ? "reduce [\u2193 key]" : "enlarge [\u2191 key]" ;
        a.setAttribute("title",motto) ; 
        a.setAttribute("style",astyle) ; 
        a.appendChild(document.createTextNode(dir<0?'\u2296':'\u2295')) ;
        div.appendChild(a) ; 
        div.addEventListener('click',resizer) ;
        div.style.cursor = 'pointer' ;
      }
      else
      { div.removeEventListener('click',resizer) ; 
        div.style.cursor = 'default' ;
      }
    }
    /* ---------------------------------------------------------------------- */

    spare = sparepix(item,sizes,sizeno) ;
    shape = imgshape(item,sizes,sizeno) ;
    if(!shape) 
    { alert('Somehow an illegal size was passed to setimg') ; throw '' ; }
    if(sizeno<0) font = item.thumbshape[3] ; 
    else font = sizes[sizeno].fontsize ;
    if(!font) 
      alert("Somehow "+item.name+' has a missing fontsize ('+sizeno+')') ;
    capsize = Math.floor(0.5+0.8*font) ;
    if(item.title) drawparms.headh = 2 + Math.floor(0.5+1.25*font) ;
    else drawparms.headh = 0 ; 
    drawparms.headf = Math.floor(0.5+font) ;
    if(item.caption) { drawparms.capf = capsize ; drawparms.caph = font ; }
    else drawparms.capf = drawparms.caph = 0 ; 

    drawparms.portrait = spare[0]<spare[1] ;

    // create the image for the new size
    img = genimage(item,sizes,sizeno,preloader(fetchitem,sizes)).img ; 
    if(drawparms.img==null) drawparms.imgdiv.appendChild(img) ;
    else drawparms.imgdiv.replaceChild(img,drawparms.img) ;
    drawparms.img = img ; 

    if(item.overlay&&drawparms.oving) 
      if(drawparms.ovl.parentNode==drawparms.imgdiv)
        drawparms.imgdiv.removeChild(drawparms.ovl) ; 
    if(item.overlay&&sizeno>=0)
    { drawparms.ovl = genimage(item,sizes,sizeno,null,1).img ; 
      if(drawparms.oving) drawparms.imgdiv.appendChild(drawparms.ovl) ;
      drawparms.capf = capsize ;
      drawparms.caph = font ; 
    }

    setimgpos(shape,item.caption) ;
    return sizeno ;

    /* ---------------------------------------------------------------------- */

    function makecaption(item,sizeno)
    { var span,drawparms=genpic.drawparms,div,s ;
      if((!item.caption)&&(sizeno<0||!item.overlay))
      { if(drawparms.capdiv) 
        { drawparms.maindiv.removeChild(drawparms.capdiv) ; uncaption() ; }
        drawparms.capdiv = drawparms.capspan = null ; 
        return ; 
      }

      div = document.createElement('div') ; 
      if(item.overlay&&sizeno>=0) 
      { div = document.createElement('div') ;
        span = document.createElement('span') ; 
        span.setAttribute('style','cursor:pointer;color:'+
                      (item.overlaid?genpic.style.visited:genpic.style.link)) ; 
        if(drawparms.oving) s = 'Remove ' ; else s = 'Add ' ; 
        span.appendChild(document.createTextNode(s+item.overlay+'.')) ; 
        div.appendChild(span) ; 
        if(item.caption) div.appendChild(document.createTextNode(' ')) ; 
        span.onclick = function()
        { var s ;
          if(!drawparms.oving) 
          { s = 'Remove ' ; drawparms.imgdiv.appendChild(drawparms.ovl) ; }
          else { s = 'Add ' ; drawparms.imgdiv.removeChild(drawparms.ovl) ; }
          while(this.childNodes.length>0) this.removeChild(this.firstChild) ; 
          this.appendChild(document.createTextNode(s+item.overlay+'.')) ; 
          drawparms.oving = 1 - drawparms.oving ;
          if(!item.overlaid) 
          { this.style.color = genpic.style.visited ; item.overlaid = 1 ; }
        }
      }

      if(item.caption) 
      { span = document.createElement('span') ;
        span.innerHTML = item.caption ; 
        span.setAttribute('style','background:'+genpic.style.bg) ; 
        div.appendChild(span) ; 
        drawparms.capspan = span ;
      }
      if(drawparms.capdiv) drawparms.maindiv.replaceChild(div,drawparms.capdiv) ; 
      else drawparms.maindiv.appendChild(div) ;
      drawparms.capdiv = div ; 
    }
  }
  /* ------------------------------------------------------------------------ */

  function uncaption()
  { if(genpic.drawparms&&genpic.drawparms.captionbox) 
    { genpic.element.removeChild(genpic.drawparms.captionbox) ; 
      genpic.drawparms.captionbox = null ; 
    }
  }
  /* ------------------------------------------------------------------------ */

  function photoinfodiv(item,title,sizes,infowords,origfunc,helpdiv,notesopt)
  { var div = document.createElement('div') , retpage = retlink(item) ; 
    var  d = document.createElement('div') , i , s , span ;

    d.setAttribute('style','border-bottom:solid 1px ' + genpic.style.mfg +
                           ';color:' + genpic.style.mfg +
                           ';padding-bottom:3px;margin-bottom:3px') ; 

    var fsoffer = offerfullscreen(infowords,genpic.style.mlink) ;
    if(fsoffer) d.appendChild(fsoffer) ;

    for(i=0;i<2;i++) // first 'return to' then 'go to'
    { if((notesopt&&i==1)||((!notesopt)&&i==0))
      { if((!origfunc)||!infowords.origin) continue ; 
        s = infowords.origin ; 
        a = document.createElement('span') ;
        a.setAttribute('style',
                       'color:'+genpic.style.mvisited+';cursor:pointer') ; 
        a.onclick = origfunc ;
      }
      if((notesopt&&i==0)||((!notesopt)&&i==1))
      { if((!retpage)||!infowords.notes) continue ; 
        s = infowords.notes ; 
        a = document.createElement('a') ;
        a.setAttribute('href',retpage) ; 
        a.setAttribute('class','m') ; 
      }
      if(i==0) s = 'Return to ' + s + " [\u21b5 key]" ; else s = 'Go to ' + s ;
      a.appendChild(document.createTextNode(s)) ;
      if(notesopt==0&&i==1)
      { a.setAttribute('target','_blank') ; 
        span = document.createElement('span') ;
        span.appendChild(a) ; 
        span.appendChild(document.createTextNode(' ')) ;
        span.appendChild(newtabdiv(genpic.style.mfg,genpic.style.mbg)) ;
        a = span ; 
      }
      d.appendChild(a) ;
      d.appendChild(document.createElement('br')) ;
    }
    div.appendChild(d) ;
    div.appendChild(pixinfodiv(item,title,sizes,genpic.style.mfg)) ; 
    div.appendChild(helpdiv) ; 
    div.appendChild(pixdocspan()) ;
    return div ; 
  }
}
/* -------------------------------------------------------------------------- */

function genmenu(element,divgen,style)
{ if(!genmenu.menudiv) genmenu.menudiv = document.createElement('div') ; 
  if(element=='display') { displaymenu() ; return ; }
  else if(element=='del') { delmenu() ; return ; }
  else if(element=='toggle') 
  { if(genmenu.div) delmenu() ; else displaymenu() ; return ; }
  else if(element=='status') return genmenu.div?1:0 ; 

  genmenu.element = element ; 
  genmenu.divgen = divgen ; 
  genmenu.style = style ; 
  genmenu.div = null ; 

  drawmenuicon(0) ;
  genmenu.menudiv.onclick = displaymenu ; 
  genmenu.element.appendChild(genmenu.menudiv) ; 
  return ; 

  function drawmenuicon(opt) 
  { var i , c = document.createElement('canvas') , ctx = c.getContext("2d") ;
    var m = genmenu.menudiv ; 
    while(m.childNodes.length) m.removeChild(m.lastChild) ;
    c.setAttribute('width','28px') ; 
    c.setAttribute('height','28px') ; 

    ctx.fillStyle = genmenu.style.bg ; 
    ctx.fillRect(0,0,28,28) ; 
    ctx.lineWidth = 3 ; 
    ctx.strokeStyle = genmenu.style.fg ;
    if(opt==0) for(i=0;i<3;i++) 
    { ctx.beginPath() ; 
      ctx.moveTo(4,6.5+7*i) ; 
      ctx.lineTo(24,6.5+7*i) ; 
      ctx.stroke() ;
    }
    else
    { ctx.beginPath() ; ctx.moveTo(4,3) ; ctx.lineTo(21,20) ; ctx.stroke() ;
      ctx.beginPath() ; ctx.moveTo(4,20) ; ctx.lineTo(21,3) ; ctx.stroke() ;
    }
    m.appendChild(c) ; 
    m.setAttribute("style","cursor:pointer;width:28px;height:28px;"+
                           "position:absolute;top:0;right:0") ;
  }

  function displaymenu()
  { if(genmenu.div) return ; 
    genmenu.div = genmenu.divgen() ; 
    genmenu.div.setAttribute('style','position:fixed;top:28px;right:4px;' +
                             'color:'+genmenu.style.mfg+';background:' +
                             genmenu.style.mbg+';padding:8px;opacity:0.9;' +
                             'z-index:99') ;
    genmenu.element.appendChild(genmenu.div) ; 
    drawmenuicon(1) ; 
    genmenu.menudiv.onclick = delmenu ; 
  }
  function delmenu()
  { if(!genmenu.div) return ; 
    genmenu.element.removeChild(genmenu.div) ; 
    genmenu.div = null ; 
    drawmenuicon(0) ; 
    genmenu.menudiv.onclick = displaymenu ; 
  }
}
/* -------------------------------------------------------------------------- */

function offerfullscreen(words,col)
{ if(!words||!querycanfullscreen()) return null ; 
  var fullword , fullfunc , s ; 
  if(!col) col = defstyle.mlink ;

  if(queryfullscreen()) 
  { if(words.exit) 
    { fullfunc = function() { exitfullscreen() ; genmenu('del') ; } ; 
      fullword = words.exit ; 
    }
  } 
  else  
  { if(words.enter)
    { fullfunc = function() { enterfullscreen() ; genmenu('del') ; } ; 
      fullword = words.enter ; 
    }
  } 
  if(!fullword) return null ;
  s = document.createElement('div') ;
  s.setAttribute('style','color:'+col+';cursor:pointer;white-space:nowrap') ;
  s.appendChild(document.createTextNode(fullword)) ;
  s.onclick = fullfunc ; 
  return s ;
}
/* -------------------------------------------------------------------------- */

function getcatval(arr,cat)
{ var val=null,k,field ; 
  if(!arr) return null ;  

  // find an attribute in arr to use when there is no category
  for(k=arr.length-1;!val&&k>=0;k--)
    if(k==0||!arr[k].cat||arr[k].cat.length==0) 
      for(field in arr[k]) if(field!='cat') val = arr[k][field] ;
  if(!cat) return val ; 

  for(k=arr.length-1;k>=0;k--) if(arr[k].cat==cat)
    for(field in arr[k]) if(field!='cat') return arr[k][field] ;
  return val ; 
}
/* -------------------------------------------------------------------------- */

function pixhelpdiv(style)
{ var tr , d=document.createElement('div') , t=document.createElement('table') ;
  if(!style) style = defstyle ; 
  d.setAttribute('style','color:'+style.mfg+';border-top:solid 1px '+
                 style.mfg+';padding-top:3px;margin-top:3px') ;

  function addcell(tr,style,content,rowspan) 
  { var td = document.createElement('td') ;
    td.setAttribute('style',style) ; 
    if(rowspan) td.setAttribute('rowspan',rowspan) ;
    td.appendChild(document.createTextNode(content)) ;
    tr.appendChild(td) ;
  }
  // the help table
  var ss='color:'+style.mfg+';font-size:80%' ;
  var sc='color:'+style.mfg+';font-size:80%;text-align:center' ;
  var sr='color:'+style.mfg+';font-size:80%;text-align:right' ;

  t = document.createElement('table') ; 
  t.setAttribute('cellspacing',0) ; 
  t.setAttribute('cellpadding',0) ; 
  tr = document.createElement('tr') ; 
  addcell(tr,sr,'swipe left') ; addcell(tr,ss,'\u00a0',99) ; 
  addcell(tr,sr,'=') ; addcell(tr,ss,'\u00a0',99) ; 
  addcell(tr,sc,'‘>’ icon') ; addcell(tr,ss,'\u00a0',99) ; 
  addcell(tr,sr,'=') ; addcell(tr,ss,'\u00a0',99) ; 
  addcell(tr,sc,'‘\u2192’ key') ; addcell(tr,ss,'\u00a0',99) ;
  addcell(tr,ss,'= next') ; 
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ; 
  addcell(tr,sr,'swipe right') ;  addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘<’ icon') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘\u2190’ key') ; addcell(tr,ss,'= prev') ; 
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ; 
  addcell(tr,sr,'swipe down') ;  addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘\u21b5’ icon') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'return key') ; addcell(tr,ss,'= back to origin') ; 
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ;  
  addcell(tr,sc,'‘\u2295’ icon') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘\u2191’ key') ; addcell(tr,ss,'= enlarge') ; 
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ;  
  addcell(tr,sc,'‘\u2296’ icon') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘\u2193’ key') ; addcell(tr,ss,'= reduce') ; 
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ; 
  addcell(tr,sr,' ') ; addcell(tr,sr,' ') ;  
  addcell(tr,sc,'‘f’ key') ; addcell(tr,ss,'= enter full screen') ; 
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ; 
  addcell(tr,sr,' ') ; addcell(tr,sr,' ') ;  
  addcell(tr,sc,'‘m’ key') ; addcell(tr,ss,'= menu/info') ; 
  t.appendChild(tr) ; 

  d.appendChild(t) ;
  return d ;
}
function pixdocspan(style)
{ var a=document.createElement('a'),span=document.createElement('div') ;
  if(!style) style = defstyle ; 
  a.setAttribute('href','http://www.masterlyinactivity.com/software/pix.html') ; 
  a.setAttribute('target','_blank') ; 
  a.setAttribute('class','m') ; 
  a.appendChild(document.createTextNode('pix.js documentation')) ;
  span.appendChild(a) ; 
  span.appendChild(document.createTextNode(' ')) ;
  span.appendChild(newtabdiv(style.mfg,style.mbg)) ;
  span.setAttribute('style','border-top:solid 1px '+
                            style.mfg+';padding-top:3px;margin-top:3px') ; 
  return span ; 
}
/* -------------------------------------------------------------------------- */

function pixinfodiv(item,sectname,sizes,col)
{ var d=document.createElement('div'),ind,r,k,shape,s ;
  if(!col) col = defstyle.mfg ; 
  d.setAttribute('style','color:'+col) ; 

  s = 'Name: ' + item.name ;
  if(item.serialno>0) s += '('+item.serialno+')' ;
  if(item.visibility=='*') s += ' \u2605' ;
  d.appendChild(document.createTextNode(s)) ;
  d.appendChild(document.createElement('br')) ;

  d.appendChild(document.createTextNode('Title: '+item.title)) ;
  d.appendChild(document.createElement('br')) ;

  if(sectname) 
  { d.appendChild(document.createTextNode('Section: '+sectname)) ;
    d.appendChild(document.createElement('br')) ;
  }

  if(item.cat)
  { d.appendChild(document.createTextNode('Category: '+item.cat)) ;
    d.appendChild(document.createElement('br')) ;
  }

  var ddash=document.createElement('div') ;
  ddash.setAttribute('style','font-size:90%;'+
         'border-top:solid 1px '+col+';padding-top:3px;margin-top:3px;') ; 
  // how many shapes?
  for(r=ind=0;ind<sizes.length;ind++) if(!sizes[ind].type) r += 1 ;
  s = 'Available in ' + r + ' size' + (r>1?'s: ':': ') ;

  // print the shapes
  for(k=ind=0;ind<sizes.length;ind++) if(!sizes[ind].type)
  { if(k>0) { if(k==r-1) s += ' and ' ; else s += ', ' ; }
    shape = imgshape(item,sizes,ind) ;
    s += shape[0] + 'x' + shape[1] ;
    k += 1 ; 
  }
  ddash.appendChild(document.createTextNode(s)) ;
  ddash.appendChild(document.createElement('br')) ;

  // print the thumb shape
  shape = item.thumbshape ;
  s = 'Thumb: ' + shape[0] + 'x' + shape[1] ;
  if(item.hithumb) s += ' (hi-res: '+ shape[0]*item.hithumb[0] + 
                                'x' + shape[1]*item.hithumb[0] + ')' ;
  ddash.appendChild(document.createTextNode(s)) ;
  ddash.appendChild(document.createElement('br')) ;

  // print the raw shape
  if(item.rawshape) for(ind=0;ind<sizes.length;ind++) if(sizes[ind].type=='raw')
  { shape = imgshape(item,sizes,ind) ;
    s = 'Raw: ' + shape[0] + 'x' + shape[1] ;
    ddash.appendChild(document.createTextNode(s)) ;
    ddash.appendChild(document.createElement('br')) ;
  }
  d.appendChild(ddash) ;
  return d ; 
}
/* -------------------------------------------------------------------------- */

function newtabdiv(fg,bg)
{ var div = document.createElement('div') ; 
  var svgns = "http://www.w3.org/2000/svg" ;  
  var lsvg = document.createElementNS(svgns,"svg") , elt;
  if(!fg) fg = 'black' ; 
  if(!bg) bg = 'white' ; 

  div.setAttribute('style','width:12px;height:12px;display:inline-block') ; 
  lsvg.setAttributeNS(null,'height',12) ; 
  lsvg.setAttributeNS(null,'width',12) ; 

  function genrect(x,y,w,h,s,rx)
  { var elt = document.createElementNS(svgns,"rect") ;
    elt.setAttributeNS(null,'x',x) ; 
    elt.setAttributeNS(null,'y',y) ; 
    elt.setAttributeNS(null,'width',w) ; 
    elt.setAttributeNS(null,'height',h) ; 
    elt.setAttributeNS(null,'style',s) ; 
    if(rx) 
    { elt.setAttributeNS(null,'rx',rx) ; elt.setAttributeNS(null,'ry',rx) ; }
    return elt ;
  }
  function genpoly(p)
  { var elt = document.createElementNS(svgns,"polygon") ;
    elt.setAttributeNS(null,'points',p) ; 
    elt.setAttributeNS(null,'style','fill:'+fg) ; 
    return elt ;
  }

  lsvg.appendChild(genrect('0.5','2.5',9,9,
                           "fill:"+bg+";stroke-width:1;stroke:"+fg,'1.5')) ; 
  lsvg.appendChild(genrect(6,0,6,6,"fill:"+bg)) ; 
  lsvg.appendChild(genpoly("6,2 6,3 7,3")) ; 
  lsvg.appendChild(genpoly("9,6 10,6 10,5")) ; 
  lsvg.appendChild(genpoly("7,0 12,0 12,5")) ; 
  lsvg.appendChild(genpoly("5.65,5.65 6.35,6.35 12,0.7 11.3,0")) ; 

  div.appendChild(lsvg) ; 
  return div ; 
}
/* ---------------------------------- swipes -------------------------------- */

var xloc=null,yloc=null,xstart=null,ystart=null,fingersep=null,startsep=null ; 
var swipetime=null ;

function startswipe(e) 
{ var x0=e.touches[0].clientX,y0=e.touches[0].clientY,x1,y1 ;
  if(e.touches.length==1) 
  { xstart = x0 ; ystart = y0 ; swipetime = new Date().getTime() ; } 
  else if(e.touches.length==2&&here.ncol==null)
  { e.preventDefault() ;        // don't let iOS take control of pinches
    x1 = e.touches[1].clientX ;
    y1 = e.touches[1].clientY ;
    startsep = Math.sqrt((x1-x0)*(x1-x0)+(y1-y0)*(y1-y0)) ;
  }
}
function midswipe(e) 
{ var x0=e.touches[0].clientX,y0=e.touches[0].clientY,x1,y1 ;
  if(e.touches.length==1) 
  { xloc = x0 ; yloc = y0 ; fingersep = startsep = null ; } 
  else if(e.touches.length==2&&here.ncol==null)
  { e.preventDefault() ; 
    x1 = e.touches[1].clientX ;
    y1 = e.touches[1].clientY ;
    fingersep = Math.sqrt((x1-x0)*(x1-x0)+(y1-y0)*(y1-y0)) ;
    xloc = yloc = xstart = ystart = swipetime = null ; 
  }
}
function endswipe(e) 
{ var v,action=null ;
  if(fingersep!=null&&startsep!=null&&here.ncol==null) ;
/* let the operating system handle pinches
  { e.preventDefault() ; 
    if(fingersep>startsep+50) action = 38 ; 
    else if(startsep>fingersep+50) action = 40 ; 
  }
*/
  else if(xloc!=null&&yloc!=null&&xstart!=null&&ystart!=null&&swipetime!=null)
  { swipetime = new Date().getTime() - swipetime ; 
    v = Math.sqrt((xloc-xstart)*(xloc-xstart)+(yloc-ystart)*(yloc-ystart)) ;
    if(v>0.65*swipetime)
    { e.preventDefault() ; 
      if(Math.abs(xloc-xstart)>100&&Math.abs(yloc-ystart)<100)
      { if(xloc>xstart) action = 37 ; else action = 39 ; }
      else if(yloc>ystart+100&&Math.abs(xloc-xstart)<100) action = 13 ; 
    }
  }
  xloc = yloc = xstart = ystart = fingersep = startsep = swipetime = null ; 
  if(action) simulate(action)
}
/* -------------------------------------------------------------------------- */

function selcat(pl,q)
{ var i,j,jj,k,r=[],gpsno,carry,rind,keepgps,ogps,ndrop,nkeep,cat,rr,item ; 
  var showhidden=null,ll,starl=[],nsort,sno,ind,maxthumb,end ;
  var sect=pl.rawsect ; 
  var disp = new Array(sect.length)  ;
  if(q) cat = q.cat ; else cat = null ; 
  pl.vis = { star:0 , none:0 , def:0 , hidden:0 } ;

  if(!q||q.mode.indexOf('v')>=0||q.mode.indexOf('a')>=0) showhidden = 'a' ; 
  else if(q.mode.indexOf('*')>=0) showhidden = '*' ;
  else if(q.mode.indexOf('h')>=0) showhidden = 'h' ;

  function keepquery(item)
  { var v , failval = 0 ;
    if(q&&q.name&&q.name==item.name) failval = 1 ; 
    // test for category
    if(cat&&((!item.cat)||item.cat!=cat)) return failval ; 
    // update pl.vis (which is whether this cat has items with a given display)
    v = item.visibility ;
    if(v=='*') pl.vis.star = 1 ; 
    else if(v=='none') pl.vis.none = 1 ; 
    else pl.vis.def = 1 ; 
    // test for visibility
    if(showhidden=='a'||failval) return 1 ; 
    if((showhidden=='*'&&v!='*')||(showhidden=='h'&&v!='none')) return 0 ; 
    else if(showhidden!='h'&&v=='none') return 0 ; 
    else return 1 ;
  }

  for(r=[],i=0;i<sect.length;i++)
  { ll = sect[i].list ; 
    for(nkeep=ndrop=carry=gpsno=0,j=0;j<ll.length;j++) 
    { if(ll[j].display=='|') gpsno += 1 ; 
      if(keepquery(ll[j])) 
      { disp[j] = { barred:((ll[j].display=='|')||carry) , gpsno:gpsno } ; 
        carry = 0 ; 
        nkeep += 1 ; 
      }
      else 
      { disp[j] = { barred:0 , gpsno:null } ; 
        carry = carry || (ll[j].display=='|') ; 
        ndrop += 1 ; 
      }
    }

    // section title - may need to compact the gps tracks
    if(nkeep>0&&showhidden!='*'&&showhidden!='h')
    { r.push(clone(sect[i])) ; 
      if(!sect[i].gps||gpsno+1!=sect[i].gps.length) r[r.length-1].nomatch = 1 ; 
      // deal with the tricky case in which the gps tracks correspond to the
      // vertical bars, and we need to see if any need to be discarded
      else for(ogps=null,r[r.length-1].gps=[],j=0;j<ll.length;j++) 
        if(disp[j].gpsno!=null&&disp[j].gpsno!=ogps)
      { ogps = disp[j].gpsno ; r[r.length-1].gps.push(sect[i].gps[ogps]) ; }
    }

    // now add the images
    for(rr=[],j=0;j<ll.length;j++) if(disp[j].gpsno!=null) 
    { rr.push(ll[j]) ; // for safari
      rr[rr.length-1].barred = 
        (disp[j].barred&&showhidden!='*'&&showhidden!='h')?'|':null ;  
    }
    if(showhidden=='*'||showhidden=='h') starl = starl.concat(rr) ; 
    else if(nkeep>0) r[r.length-1].list = rr ; 
  }

  if(showhidden=='*'||showhidden=='h') r = [ { list:starl } ] ; 

  // sort photos by name to check for dupes 
  for(nsort=sno=0;sno<r.length;sno++) nsort += r[sno].list.length ; 
  sortlist = new Array(nsort) ;
  for(nsort=sno=0;sno<r.length;sno++)
    for(ll=r[sno].list,i=0;i<ll.length;i++) 
      sortlist[nsort++] = { name:ll[i].name , sno:sno , ind:i } ;

  sortlist.sort( function(a,b)
                 { var ret = a.name.localeCompare(b.name) ; 
                   if(ret!=0) return ret ; 
                   else if(a.sno!=b.sno) return a.sno-b.sno ;
                   else return a.ind - b.ind ;
                 } ) ;

  // inject a serialno field into the list
  for(ind=0;ind<nsort;ind=end)
    for(end=ind;end<nsort&&sortlist[end].name==sortlist[ind].name;end++) 
      r[sortlist[end].sno].list[sortlist[end].ind].serialno = end - ind ; 

  // now find the max thumb dimensions
  for(pl.maxthumb=0,sno=0;sno<r.length;sno++) if(!r[sno].layout)
    for(ll=r[sno].list,i=0;i<ll.length;i++)
      if(ll[i].thumbshape[0]>pl.maxthumb) pl.maxthumb = ll[i].thumbshape[0] ; 

  pl.sect = r ; 
}
/* -------------------------------------------------------------------------- */