Colin Champion

Summary: this page contains technical documentation and javascript source code for the routemaster GPS track editor.

user doc : hints : putting routemaster on a website : technical doc : about the software : routemasterlib doc : Garmin 500 review : source : download & index

• general functions     • editing functions     • adding photos to your own instance     • creating a route index    

Routemaster may be used to edit and display GPS tracks and to display multi-track indexes. Photographs may be included. However the web display requires the user to have his or her own instance of the tool (which means in practice a 10-line HTML page calling in functions from my own libraries). Editing and downloading can be performed by anyone.

I would like to think that routemaster was self-explanatory but in case of difficulty there’s a help menu under the cogwheel button. The same menu is displayed under the load prompt.

To load a track into routemaster for editing, go to www.masterlyinactivity.com/routemaster and follow the prompts.

To see the tool in action, look at the Cleeve Hill circuit. By all means experiment with splitting the route, deleting parts of it, etc.: you can’t do any harm. You can download the result to your computer.

To see a route index go to our Cape Verde tracks. Click on any of the routes there for more information and some photos. If you click again to view its track you will see how photos are embedded at specific locations

In addition there are two functions which can only be made use of by providing a routemaster instance on your own website, namely using it to display routes on the web and including photos.

There are technical reasons (see below) why the official routemaster on www.masterlyinactivity.com can’t be used to display gps tracks held elsewhere on the web.

For an example of a route incorporating photos, see the Monte Tondo circuit we rode in Tuscany in June 2017.

Since routemaster allows you to import a route as FIT and download it as TCX it can be used as a conversion tool. This is not recommended. For a trustworthy conversion you should use a tool which aims to preserve data integrity (eg. Gpsies). routemaster preserves only those features which can be edited for navigation purposes.

• going to the startpoint of a track     • viewing photos     • changing the startpoint of a track     • rectifying post-prandial notches     • incorrectly printed photo lists     • uploading and downloading    

Hit [shift][right arrow key]. If the route has more than one segment, you may have to do this more than once.

Click on the camera icon to bring up a thumbnail. The menu gives you the options to edit the photo designator, to view some basic info, and to enlarge the photo to full size. To see full-size photos at their best, use routemaster in full-screen mode (requested under the cogwheel). When you are looking at a full-size photo, the left and right arrow keys take you to the preceding/following photos, the up and down arrow keys enlarge and reduce, and the return key takes you back to the GPS track.

If you have a (roughly) circular route and want to change its start point:

Post-prandial notches are described below. If you see an implausible notch (or less commonly, tabletop) in your altitude profile, I can’t think of anything better to do than to correct it using the Google elevation service.

Firstly use the scissors to define a segment embracing the notch. You can perform fine adjustments by invoking ‘Waypoint info’ on the first or last waypoint of a segment. When you’re happy that all the unreliable points are in the same segment, and that this segment contains as few reliable points as possible, select a waypoint in the unreliable segment, go to ‘Segment info’ and request correction from the Google elevation service. Then combine the segments again (under the cogwheel).

Occasionally, if I load a photolist through the routemaster URL, eg

http://www.masterlyinactivity.com/routemastertest/?track=../temp/fits/2017-06-20-09-40-45.fit&list=../../garfagnana/list.js

then when I download the edited track I get a misprinted photo list field, eg.

<PhotoList src="http://garfagnana/list.js" />

This is not a bug in routemaster but arguably a bug in Firefox. The cause is that the URL as I entered it is wrong – I should have written

http://www.masterlyinactivity.com/routemastertest/?track=../temp/fits/2017-06-20-09-40-45.fit&list=../garfagnana/list.js

Firefox silently corrects the error, leaving you imagining you’ve supplied the right number of ‘../’s, but you haven’t, and it’s no job of routemaster to detect and silently correct your error.

To load a track into a Garmin:

The Garmin fails to convert the files if any fields are missing which it considers obligatory, or if it encounters any formatting errors. It’s very fragile. You get no information as to why a track was rejected and there’s no documentation to tell you how to make a track it accepts.

routemaster is a trivial web page which invokes a javascript library to do all its work. If you invoke the same library from a page on your own site then you will be able to link your own tracks into it.

To do this, create a web page, eg. routemaster.html, with similar content to my own (www.masterlyinactivity.com/routemaster/index.html). This is just a few lines:

<html><head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
  <script src="http://www.masterlyinactivity.com/routemaster/resources/routemaster.js"></script>
  <script>genhead("http://www.masterlyinactivity.com/routemaster/resources",
'AIzaSyD2oAnc1Acy1tv2ov55fLUPKZl_sKoS4rQ') </script>
</head><body></body>
<script>genpage()</script>
</html>

The second parameter in the call to genhead is a Google Maps API key.

Once you have a routemaster.html page, if you visit it you should be given the upload prompt. Alternatively visit it with a parametric track name:

www.mysite.org.uk/routemaster.html/?track=rides/longride.tcx

where the part before the ‘?track=’ is the URL of your routemaster instance and the part after is the path, relative to that instance, to a TCX (or GPX) track.

You can now link to this composite URL instead of typing it into the URL bar.

None of this is within the scope of naive users; and knowledgeable ones should be aware that anything they do may cease to work if Google start charging for access to the maps API (see below). If you’re at all tempted to experiment with your own instance of routemaster, I would advise you to hold back until you’ve made yourself comfortable with pix.js.

The code for photo handling is not as user-friendly as the rest of routemaster. It was written for my own use.

Firstly you need the photos and a list in the format defined for my pix.js script. This is the hard part. Put the list in its intended location on your website.

Now load a track, passing the list location to routemaster through an extra component in the URL. If you are going to view a track uploaded from your own computer, write the following in the URL bar:

www.mysite.org.uk/routemaster.html/?list=../ruritania/list.js

If you are viewing a track already on the web, write

www.mysite.org.uk/routemaster.html/?track=rides/longride.tcx&list=../ruritania/list.js

Here the part following the ‘list=’ is the path to the photo list from your routemaster instance.

If you are uploading a track from your computer, do so now.

You should now see the track in the normal way. But if at some point you right click on the pen button, you will be prompted for the name of a photo, and if you choose a name from the list you should see its thumbnail in response. From this point on you can edit photos in much the same way as you can edit course points.

Now download the track. It will have extra entries for the photos you’ve added and also (near the end) the URL of the photo list you specified. If you use this version of the track instead of the original, you will not need to append the photo list path to the routemaster URL. You can link to the track in the same way as before and the photos will be embedded within it.

I wish I could say that this was plain sailing but I often have difficulties myself.

A problem which sometimes arises is that something in the Internet occasionally truncates www.masterlyinactivity.com to just masterlyinactivity.com, and something else then complains that an attempt to access one of these from the other violates the same origin policy.

In case of difficulty, in Firefox, open the web console (Tools > Web Developer > Web console): you may see some informative messages. Presumably other browsers offer a similar feature. Don’t use IE.

To create an index, load the first route into routemaster. Then load subsequent routes as new segments. Set a title for the index. At this stage the segments will alternate red and pink and be connected by dashed lines as if they were parts of a single route.

Click on ‘Download track as route index’ and the index file will appear on your computer. If you upload it to your own website and have a routemaster instance to serve it, you can supply a path to it after the ‘?track=’ in the routemaster URL to see it as it is meant to appear.

The index needs to be generated on your own disc but will be uploaded to the web. The index TCX file will contain links to the tracks for individual routes, but these links cannot be generated correctly because routemaster does not know your web URLs. It therefore puts the placeholder ‘$FILE$’ at various places in the index file. A global replace within an editor will change them so that the links work on the web.

It is useful for a track to link to its index. You will then be pointed to the index from the Route Info menu. The link has to be added by hand-editing the route, adding the line

    <Index href="uri"/>

amongst the extensions after the track points.

• data acquisition     • data transfer     • optimisation     • actions list     • mapping     • photos     • altitudes     • times     • downloading     • acknowledgments    

routemaster uses the relatively new HTML5 FileReader API to upload GPS tracks from the user’s disc. This is fine for editing.

The other function I aimed for with Routemaster is as a linkable display of GPS tracks. To achieve this I need to supply the track uri to Routemaster, which I achieve by appending it to the Routemaster uri after a ‘?’, eg.

www.masterlyinactivity.com/routemaster/?track=routes/montetondo.tcx

The track is then read in using the (also fairly new) ‘XMLHttpRequest’ function. Unfortunately this is subject to the internet same origin policy which makes it impossible for routemaster to display a track which isn’t hosted on www.masterlyinactivity.com. Since I have no plans to offer a track hosting service this limits the use which anyone else can make of the official instance of routemaster.

The only workaround I can see for this is to specify a Javascript format for GPS tracks. I could easily download tracks as Javascript rather than as tcx, and I think I could then get hold of a track from its uri even across domains. The idea of embedding data in executable code to circumvent access restrictions definitely has mileage.

Routemaster is a client-side web application which reads data into the browser, processes it there, and re-exports it. Nothing is sent up to masterlyinactivity.com. There are no cookies. But Google knows where you’ve been because of the mapping requests.

A track is optimised on input; that is, it is reduced to the smallest number of trackpoints to preserve its accuracy. This is done using dynamic programming. Bike route toaster uses the Ramer–Douglas–Peucker algorithm which I think is less effective; I suspect some programs simply take every nth waypoint.

Optimisation is performed on input for several reasons: so that the editing is truly WYSIWYG; because handling lots of redundant waypoints is cumbersome; and to give the user the opportunity to adjust the optimisation parameters.

Optimisation, although automatically performed, is treated as a user edit which can be undone and redone. If – immediately after loading a track – you hit ‘undo’ you go back to the unoptimised track. You will them be given an ‘Optimise’ option from the main menu (the cogwheel) and can specify your own parameters. But you are unlikely to want to do this.

To be precise the dynamic program reduces the set of waypoints to the optimum subject to two constraints. The first constraint is that the new route lies within distance tol of the one implied by the original set (interpolating linearly); the second is that the separation between two successive points is never greater than maxsep; tol and maxsep default to 10m and 95m respectively.

Optimality means having least cost where the cost is the sum of two terms: the volume of the error sleeve and a penalty for each waypoint.

The error sleeve is a rough cylinder which follows the new route and has a radius equal to the distance between new and old routes. Its volume is a figure in cubic metres.

The penalty is added once for every waypoint included. The default is 1000m3. This is quite small: it equates to an error of about 4m over a distance of 20m.

Vertical errors are given the same weight as horizontal errors, but although this is natural it has no real justification. An additional parameter (vweight) can be used to adjust the significance of altidude. It would make sense to be particularly averse to losing peaks and troughs since this affects the calculation of total ascent; but I haven’t done anything to this effect.

Given that the cost function does a good job of adjudicating between sets of waypoints it’s tempting to dispense with the tolerance altogether, or at least to make it very lax. But this might lead to a surfeit of ‘Off course’ messages. It would also make the algorithm more expensive since the tolerance (together with the maximum separation) allows hard cutoffs to be applied. The maximum separation is needed because Garmin 500s lose their breadcrumb display if a separation is >100m. (See the Garmin support forum). But I print positions with precision around 1m, so I set maxsep somewhat less than 100m to avoid it becoming greater due to rounding error.

Once a track has been optimised I try to avoid optimising it a second time, especially since – even if the parameters are the same – the results will differ: there will be an accumulation of errors. For this reason I record the fact that optimisation has been performed in the exported track. Also, if optimisation reduces the number of points by less than a factor of 2, I assume that the track has been preoptimised.

A side-effect of optimisation which took me by surprise if that if while riding along you lose confidence in the route you are following, backtrack a few tens of metres, and then realise you were right all along and resume your previous course, then the optimisation removes all record of your changes of mind.

Don’t conclude from this detailed discussion that optimisation is a critical issue. Different algorithms and different parameters will give similar performance.

In order to provide non-destructive editing I maintain an ‘actions list’ which is a specification of every editing operation which is performed. Undoing an action backsteps through this list; redoing it steps forward; performing a new action puts it at the current position and discards anything which might come after.

It isn’t always obvious what constitutes an editing action (for instance editors never treat changing a selection as an undoable action). To avoid confusion I don’t provide a blind ‘undo’ function but always prompt the user with the action he or she will undo. This provides useful reassurance and also makes it possible to put actions in the list (not only optimisation but also file load) which the user may not be conscious of having performed.

Creating a coursepoint label may require a sequence of user operations which are collapsed into a single action.

I use Google Maps v3 API. I don’t expect to extend this to other map sources.

The buttons at the bottom use the Google map.controls feature. Mostly the buttons don’t do any more than bring up menus which hang off them. These menus are google.maps.InfoWindows whose position needs to be specified as lat/long rather than relative to the controls. The calculation which converts one to the other makes me dependent on unspecified details of the map controls interface and leads to unexpected behaviour when windows are resized.

At the time I didn’t know enough HTML to generate the buttons myself. Now I think I do, but it would still be more work than taking advantage of the Google functions.

I allow photos to be included at their correct points along a route. There’s a certain amount of code to support this feature. I take advantage of the fact that I have a standard Javascript format for photo lists (eg. Garfagnana list) which is used to generate photo pages (eg. Garfagnana photos); the photo pages are generated automatically from the lists by Javascript code.

The same list is used by Routemaster. The user simply associates a photo name with a trackpoint; the list then determines the shapes of the thumbnail and of all full-size images, the title, and any caption.

To insert a photo at the current waypoint, right click on the pen button.

A difficulty I’ve encountered is that of getting the photolist into Routemaster. This may be done in either of two ways. To get started you can supply the photolist after a ‘?list=’ in the uri line, eg.

www.masterlyinactivity.com/routemaster?list=../garfagnana/list.js

(The uri is relative to routemaster, and routemaster, technically, is www.masterlyinactivity.com/routemaster/index.html.)

Then, once photos from a list have been incorporated in a track, if you save the track the uri of the photolist will be embedded in it under an ‘<Extensions>’ tag. The uri specification is no longer needed.

The photos themselves are incorporated in the track simply as their names. The image uris are generated from the names and the list uri.

Points may not have altitudes either because a route was input from a defective GPS track or because waypoints have been added or moved. The handling of these cases has been completely revised in July ’16.

Prior to this date the absence of an altitude from an input track was treated as a fatal error. When points were added or moved a request was immediately sent to the Google elevation service.

One of the changes is to permit the input of GPS tracks with missing altitudes, treating the points in question in exactly the same way as newly added points. The other is to postpone requests to the Google elevation service until there is a large enough batch to justify the transaction. But altitudes need to be filled in before a track is downloaded and the user has the the option (under the cogwheel button) of requesting an update.

The first of the new features is mostly needed to recover from faulty tracks produced by buggy versions of routemaster. The second leads to considerable savings in network traffic, but also – given Google’s pricing model – may one day bring financial savings. In any case the new code is shorter and more reliable than the old.

Rather than directly using the returned altitude of a point, I compute its difference from the nearest waypoint: this ensures consistency if there is a calibration offset. It would be better if I could use the altitudes of all the points which have been optimised away, but even if I’ve retained them I may not know which manual calibration may have been intended to be applied to them. So maybe I shouldn’t worry about this.

The original version of routemaster simply deleted the times (or rather replaced them by nominal times starting on 1 Jan 1970 – Garmins don’t like the field to be absent). This is because if you edit a track substantially (combining segments, reversing some of them, extrapolating points...) it becomes impossible to provide times which correspond to real cycling speeds. Also I’m afraid that if Garmins need the times to be present, they may behave oddly if they decrease.

However the times are useful, especially for collating photos with routes. My current policy is to replace times by nominal values if they are out of sequence, else to preserve them, interpolating and extrapolating if need be. Extrapolation uses the rider’s average speed.

When I started writing Routemaster I had no idea that the W3C had abandoned the FileWriter API. I use Eli Grey’s File Saver instead. This has a known limitation in Safari: the text you ask to download may instead be presented in a separate tab. This sounds like a Safari bug, so with any luck it will be made to go away.

Why do I only download as tcx? There’s a bewildering variety of formats (more than the names suggest: there are at least two versions of each of tcx and gpx). The differences in what they can be used for are minor but the formats differ in respects which have no bearing on their functionality. In the end no one knows what is the right format to use. I have standardised on a variety of tcx (as output by ridewithgps) because I know it does everything I need. It’s good enough for most purposes, but being proprietary doesn’t satisfy everyone.

• to do     • revisions history     • future directions     • testing     • licence    

In 2016 Google removed the ability for new pages to use the Maps API without a key. This makes it somewhat harder for users to set up their own instances of routemaster. I suspect it’s the first step towards monetising the maps API (which at present is effectively supplied as a free public service). Google have reserved the right to put ads all over the maps they supply, and that’s likely to be their first recourse. Everyone who doesn’t want ads (including me) will have to adopt a paid service; but if I don’t want ads and want other people to be able to use routemaster without them, I will need to find a way to avoid bearing the fees Google levies on them.

If the Google Maps API starts charging for ad-free use this will impose architectural changes on users. At present authentication uses the HTTP ‘referer’ which is really not fit for any purpose with financial consequences. It will need something more trustworthy, which is likely to lead to users having to provide server authentication, which means that the average citizen with web space will be excluded.

So I will watch what Google do. If it looks like I’m going to have to pay to avoid ads, I may make routemaster a subscription service, in which case I could also host routes and photos. In the meantime I’ll take it easy, but I need to change web hosts in September ’16 and may take the opportunity to introduce a simple registration mechanism.

But I’m back-pedalling on encouraging users to install their own instances, which would probably land them in difficulties within a year of so.

I’ve added a (not very permissive) licence – an unintentional omission from earlier versions.

There are scores of options and features and I can’t test them all every time I make a change; be patient if you encounter a bug (and send me an email).

It’s useful to be able to verify that I can load files from a variety of sources: these links help:

• altarezia.eu GPX    • bikehike GPX route    • bikehike GPX track    • bikehike TCX    • Garmin Connect GPX    • Garmin Connect TCX    • Ride with GPS GPX    • Ride with GPS TCX course    • Ride with GPS TCX history    • Versante Sud free track    • Modica MTB track downloaded from the web    • Edge 500 FIT track   

Also, on every revision, I should make sure that I can transfer a downloaded track to my Garmin and that it will be recognised there (I missed this check out earlier in 2016.)

routemasterlib.js (but not routemaster.js, routemasteredit.js or routemasterui.js) is supplied free of charge under an MIT licence (giving you carte blanche). routemaster.js, routemasteredit.js and routemasterui.js may be invoked freely from any non-commercial web pages (without warranty of any kind, and under the condition that the author shall not be held liable for any misadventure arising from their use) but no further licence is granted.

Since routemasterlib.js is made available for anyone to use, it has some basic documentation (following).

[The relatively restrictive terms here reflect a change in how I expect to offer routemaster to others. Originally I saw it as a piece of browser software for my own use which I could happily make available to others (although the limitations stemming from the same-origin policy put it beyond casual users). But the prospect of Google starting to levy charges makes me suspect that it will need to be a web service (that is, one which provides more than web pages and also handles authentication), which is something that cannot be shared in the same way as a piece of software. I don’t want to provide routemaster in a form which can’t be maintained so I make it available to anyone for use but not as something capable of being extended.

routemasterlib.js is treated more liberally because it is written in a way which makes it usable by anyone even if access to Google Maps is limited to web services. The same applies to pix.js and pixlib.js.]

• internal track format     • route properties object     • readtcx     • readgpx     • readfit     • writetcx     • writeoverview     • optimise     • getalts     • profilemaptype     • utilities

These functions are documented for information. Since they are available on an MIT licence you are free to use or modify them. But don’t assume that the interface will remain compatible: I consider myself free to change it. If you want to use them, make your own copy.

A track is stored internally as an array of segments (which are the routes in the case of an index). A segment comprises:

A point is a datatype object. It comprises a number of data fields, many of which are null unless the point is also a course point or has a photo associated with it.

Not yet documented (maybe not settled).

readtcx is the function to read a tcx course. The call is

readtcx(xmldoc,m,flagsel,photosel,cyclist)

where

Its return value is an object with 3 fields:

The only case in which there is more than one segment is that of index files. For indexes the global title differs from the title of each route, and each route has separate properties. For non-index files the global title is the route title and the segment properties are transferred to the routeprops variable and properties of individual segments no longer maintained.

For historical reasons routemasterlib supports some synonyms for routemaster extensions.

readgpx is the reader for gpx files.

readgpx(xmldoc,m,flagsel,cyclist)

It is the same as readtcx except for not having a photosel argument (because photos only exist in routemaster output, which is currently restricted to tcx), in not having a title field in its return value, and in the fact that the other fields are necessarily 1-long arrays.

readfit is the reader for fit files, copied from mygpsfiles.

readfit(rawdata,m)

where rawdata is an array buffer read by the FileReader readAsArrayBuffer method (which is equivalent to the XMLHttpRequest response field).

The return value is as for readgpx.

writetcx is the writer for tcx files.

writetcx(props,data)

where

The return value is a character string containing an XML representation of the route.

writeoverview writes a tcx index of gps tracks.

writeoverview(segments,title,list)

where

The return value is a character string containing an XML representation of the index.

getalts is a function which makes use of the Google maps elevation service to fill in any null altitudes. The call is:

getalts(segments,thresh,action)

segments is an array of route segments, but getalts ignores the division into segments and treats the route as a linear array of points. If the number of null altitudes is less than thresh it returns taking no action. Otherwise it groups missing altitudes into batches of at most 500 points, calling on the elevation service to supply the missing altitudes. Once the number of remaining missing altitudes is <thresh it ceases processing.

The operation of getalts is asynchronous. A request will be submitted to the elevation service as soon as you call it, but the response will come after some delay, and only then will the track values be updated. However given the way Javascript assigns objects (i.e. by address), the adjustment should be performed correctly even if you have edited the track in the meantime.

Every time a new set of altitudes is received, getalts invokes your action function (if supplied and non-null) with a single parameter, namely the number of altitudes supplied by Google in the latest call.

In order to ensure that the database altitudes are consistent with those already existing in the track (which may be subject to a calibration error) getalts performs an adjustment. It includes the last preceding point with a non-null altitude (if any) and the first following point with a non-null altitude (if any) in the request. Then the difference between your own altitude for the preceding point and the Google estimate is taken as the initial calibration error, and the final calibration error estimated likewise; and these errors are used to smoothly adjust the values returned by Google.

This isn’t ideal and may be unnecessary. It can only be beneficial for GPS tracks recorded on a device which stores barometric altitude estimates with no GPS correction. I don’t know if any such devices exist. On the other hand it’s unlikely to do much harm.

If a stretch of unknown altitudes is longer than 500, then rather than break it into substretches I select 500 points regularly along it and linearly interpolate the altitudes returned. [This probably hasn’t been tested.]

If you want to know whether the elevation service is busy or is free to accept a new request, call busyalts(), whose return value is true or false according as a request is pending.

optimise reduces the track to the smallest number of points while retaining shape. The algorithm is sketched above. The call is

optimise(data,parms)

where

parms has four fields:

The return value is a reduced array of points.

A point is not eligible for deletion from a track if either its type field is non-null or its photo field, assumed to be an array, has length >0. The pos field of a point is assumed to contain its position (as a googleLatLng) and its h field its altitude. You may pass an array of points defined however you want so long as these properties apply.

profilemaptype is the constructor for an object containing a numerical representation of an altitude profile. Invoke it as

  pro = profilemaptype(segments,) ;

where

The returned object has the following fields:

hmin may not be the true minimum but instead may be 0 if the profile will be more meaningful that way.

gencolours(n) returns an array of n character strings which will be used as track colours in an index. The colours are intended to be reasonably well spaced and not too pale or too dark. Moreover the first colour is the same regardless of n, the second is the same for all n>1, and so forth.

gencolours(u1,u2) returns the absolute uri corresponding to u2 when u2 is a uri relative to u1.

ascify(s2) returns a character string corresponding to s but with diacritics deleted (for filenames etc.).

When I bought my Garmin I knew what I wanted: I wanted a navigation aid for mountain biking which allowed me to record routes and follow other people’s shared routes, and which could be used in conjunction with paper maps and route descriptions. I did not want inbuilt maps because I didn’t expect them to be adequate. I was particularly interested in mountain biking abroad, including in countries where even paper maps are not of high standard.

After some web browsing I came to the conclusion that the Garmin 500 was closest to what I wanted, and my first impression is that I was exactly right. It has all the functions I need; it has a reasonable price; it is conveniently small; and it has good battery life. But it’s let down by the poor quality of Garmin software.

I wrote an initial review which mentioned some of the faults I’d encountered; here’s a more complete list.

And there are some design faults, often reflecting the fact that Garmin were seeking to provide a training aid rather than a navigation aid.

Finally there are some limitations which the user needs to be aware of but which are due to the nature of the device.

Are future models likely to be better? I’m afraid the opposite may be true. The market seems to be dominated by people wanting to record training times on Strava, and new models are geared at them.

When you turn the GPS on you get uncalibrated barometric readings. Meanwhile it monitors the difference between barometric and GPS estimates. When it has enough data to calibrate the barometer it starts adding the calibration term to the barometric readings. Consequently the displayed altitude varies rapidly as the calibration term comes into effect. This gives rise to the post-prandial notch you see in altitude profiles when you switch the Garmin off for a meal break.

So far as I can see this problem does not arise at the start of a ride. So the problem may be related to the missing ‘motion detected’ message after lunch: some operations which should be performed every time you power up are only performed at the beginning of a track.

You don’t get a hint of this (or of other complexities) in the supplied documentation. It’s impossible to use the unit correctly without knowing what’s going on.

• routemaster.js     • routemasteredit.js     • routemasterui.js     • routemasterlib.js    

• function     • genseg     • dotpath     • linepath     • listinfo     • interp     • bearing     • greyout     • blackout     • enterFullscreen     • exitFullscreen     • findimg     • unsavedmsg     • selpoint     • highlight     • getbtnpos     • walkto     • keystroke     • shiftkey     • undraw     • redraw     • recolour     • obliterate     • drawprofile     • draw     • disconnect     • reconnect     • connect     • redrawconnect     • drawsel     • genhead     • genpage     • getlist     • filedialogue     • render     • settitle     • setdesc     • retitle     • restars     • genbutton     • addload     • dl

var segments=[],selected,actions,nactions,sel,dragging,overviewing=0 ; 
var resuri,xmlfile=null,shifted=null,unsavedchanges=[] ; 
var body,mapdiv,pro,starsdiv=null,routeprops,map=null,clickhandle=null ; 
var scroller=null,imgdiv,imghandle,imginfo,imgind ;
var scissorsbtn,binbtn,undobtn,redobtn,penbtn,setbtn=null,dlbtn ;

var defparms = {tol:10,maxsep:95,wppenalty:1000,vweight:1} ;
var parser = new DOMParser() ;

var infowindow = 
{ handle: null , 
  type: null , 
  open: function(s,pos,type)
  { this.handle = new google.maps.InfoWindow({content:s,position:pos}) ;
    this.handle.open(map) ; 
    google.maps.event.addListener(this.handle,'closeclick',function() 
    { if(infowindow.type=='highlight')
      { redraw(selected[0]) ; 
        if(scroller!=null) { clearInterval(scroller) ; scroller = null ; }
        infowindow.handle = null ; 
      }
      else if(infowindow.type=='phinfo') walkto(selected[0],selected[1]) ;
      infowindow.type = starsdiv = null ; 
    } ) ;
    this.type = type ; 
  } , 
  close: function() 
  { if(this.handle==null) return null ; 
    var response = this.type ;
    this.handle.close() ; 
    if(response=='highlight') 
    { redraw(selected[0]) ; 
      if(scroller!=null) { clearInterval(scroller) ; scroller = null ; }
    }
    else if(response=='phinfo') walkto(selected[0],selected[1]) ; 
    this.handle = this.type = starsdiv = null ; 
    return response ; 
  } 
} ; 
/* --------------- construct a segment from an xml document ----------------- */

function genseg(a,b) 
{ this.data = a ; 
  this.props = b ; 
  this.route = this.routehandler = this.dots = this.dothandler = null ; 
  this.colour = "red" ;
}
/* -------------------------------------------------------------------------- */
/*                                CONSTRUCTORS                                */
/* -------------------------------------------------------------------------- */

function dotpath(a,b)
{ this.path = [a,b] ;
  this.cursor = 'default' ;
  this.geodesic = true ;
  this.strokeOpacity = 0 ;
  this.icons = [ { icon:   { path: 'M 0 0 L 1 0',strokeOpacity:1,scale:1 } , 
                   offset: '1px' , 
                   repeat: '4px' 
                  } ] ;
  this.zIndex = 0 ;
}
/* -------------------------------------------------------------------------- */

function linepath(s0,start,end,colour,width)
{ var i,len=(start<0?segments[s0].data.length:end-start) ; 
  if(width==undefined) width = 2 ; 
  this.path = new Array(len) ; 
  if(start<0) for(i=0;i<len;i++) this.path[i] = segments[s0].data[i].pos ;
  else for(i=0;i<len;i++) this.path[i] = segments[s0].data[start+i].pos ; 
  this.clickable = 'false' ;
  this.cursor = 'default' ;
  this.geodesic = true ;
  this.strokeColor = colour ;
  this.strokeOpacity = 1.0 ;
  this.strokeWeight = width ;
  if(width==2) this.zIndex = 0 ; else this.zIndex = 1 ; 
}
/* -------------------------------------------------------------------------- */

function listinfo()
{ this.list = [] ; 
  this.sizes = [] ;
  this.uri = null ; 
  this.thumbind = this.scale = this.status = this.type = this.pixpage = null ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */
/*                             UTILITY FUNCTIONS                              */
/* -------------------------------------------------------------------------- */

function interp(x,y,lamda)
{ return google.maps.geometry.spherical.interpolate(x,y,lamda) ; }

function bearing(x,y)
{ return google.maps.geometry.spherical.computeHeading(x,y) ; }

/* --------------------------- button handlers  ----------------------------- */

function greyout(btn)
{ if(overviewing||btn.active==0) return 0 ; 
  btn.btn.setAttribute('src',btn.greyimg) ; 
  btn.ui.removeEventListener('click',btn.handler) ;
  if(btn==penbtn) btn.ui.removeEventListener('contextmenu',photoprompt) ;
  btn.ui.style.cursor = 'default' ; 
  btn.ui.title = btn.greytitle ; 
  btn.active = 0 ; 
  return 1 ; 
}
function blackout(btn)
{ if(overviewing||btn.active) return ; 
  btn.btn.setAttribute('src',btn.blackimg) ; 
  btn.ui.addEventListener('click',btn.handler) ;
  if(btn==penbtn) btn.ui.addEventListener('contextmenu',photoprompt) ; 
  btn.ui.style.cursor = 'pointer' ; 
  btn.ui.title = btn.blacktitle ; 
  btn.active = 1 ; 
}
/* ------------------------ enter/exit full screen -------------------------- */

// most of the code is available from pixlib
function enterFullscreen() { infowindow.close() ; enterfullscreen() ; } 

function exitFullscreen() 
{ infowindow.close() ; 
  if(document.exitFullscreen) document.exitFullscreen() ;
  else if(document.mozCancelFullScreen) document.mozCancelFullScreen() ;
  else if(document.webkitExitFullscreen) document.webkitExitFullscreen() ;
}
/* -------------------------------------------------------------------------- */

function findimg(id)
{ var i ; 
  for(i=0;i<imginfo.list.length;i++) 
    if(imginfo.list[i].name!=undefined&&imginfo.list[i].name==id) return i ; 
  return -1 ; 
}
/* ------------------- message warning of unsaved changes ------------------- */

function unsavedmsg(ok)
{ var msg , len = unsavedchanges.length , i ; 
  if(len==0) return null ; 
  msg = 'You have ' + len + ' unsaved change' + (len==1?"":'s') ; 
  if(len<=3) for(i=0;i<len;i++)
     msg += (i?',':' (') + unsavedchanges[i] + (i==len-1?')':'') ;
  msg += '\nIf you hit ' + (ok?'[OK]':'[Leave page]') ; 
  return msg + (len==1?' this change':' these changes') + ' will be lost.' ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* --------------- selpoint: choose the clicked waypoint  ------------------- */

function selpoint(event)
{ var i,j,closest,d,mindist,s0,s1 ; 
  if(dragging) return ; 
  var flag = (infowindow.close()=='wpinfo') && (shifted==0) ; 

  if(overviewing==0&&shifted)
  { s0 = selected[0] ;
    s1 = segments[s0].data.length ;
    insert(s0,s1,1) ;
    segments[s0].data[s1].setpos(event.latLng) ;
    redrawconnect(s0,s1) ;
    done(['move',s0,s1,event.latLng,event.latLng,1]) ; 
  }
  else for(s1=s0=-1,i=0;i<segments.length;i++) 
    for(j=0;j<segments[i].data.length;j++)
  { d = dist(segments[i].data[j].pos,event.latLng) ;
    if(s0<0||d<mindist) { s0 = i ; s1 = j ; mindist = d ; } 
  }
  walkto(s0,s1,flag) ; 
}
/* -------------------------- track highlighter  ---------------------------- */

function highlight()
{ var s0=selected[0],scroll,thind=null ;
  infowindow.close() ;
  undraw(s0) ; 
  draw(s0,4) ;
  if(imginfo.uri!=null) thind = thumbind(imginfo.sizes) ;
  scroll = highdiv(segments[s0].props,imginfo.list,imginfo.sizes,
                   thind,segments[s0].props.photo) ;
  scroller = scroll.scroller ;
  infowindow.open(scroll.div,northernmost(segments[s0].data),'highlight') ; 
}
/* ------------------------------- getbtnpos -------------------------------- */

function getbtnpos(btnno)
{ var bounds=map.getBounds(),sw,ne,lat,lon,lam ;
  sw = bounds.getSouthWest() ; 
  ne = bounds.getNorthEast() ; 
  lam = 52.0 / window.innerHeight ; 
  lat = lam*ne.lat() + (1-lam)*sw.lat() ; 
  lam = 0.5 + (btnno*32-96)/window.innerWidth ;
  lon = lam*ne.lng() + (1-lam)*sw.lng() ;
  return new google.maps.LatLng(lat,lon) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* --------------------------------- walkto --------------------------------- */

// draw a selection point (and possibly an info box) at [s0,s1], bringing up
// a wpinfo window if flag = 1 or a seginfo window if flag = 2 

function walkto(s0,s1,flag) 
{ var datum = segments[s0].data[s1] , pos = datum.pos ; 
  if(flag==undefined) flag = 0  ;
  selected = [ s0,s1 ] ;
  if(overviewing) return highlight() ; 
  map.panToBounds(new google.maps.LatLngBounds(pos,pos)) ; 
  drawsel(0,[s0,s1]) ; 
  if(flag||(datum.type==null&&datum.photo.length==0)) 
  { if(flag==1) wpinfo() ; 
    else if(flag==2) seginfo() ; 
    else if(flag==3) highlight() ; 
    return ; 
  }
  infowindow.open(walktodiv(datum),pos,'walking') ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------- keystroke handler  ---------------------------- */

function keystroke(e)
{ var s0=selected[0],s1=selected[1],slast,flag ;

  if(e.keyCode==17||e.keyCode==224||e.keyCode==91||e.keyCode==93) return ; 
  // [control] + 3 encodings of [command]
  if(e.keyCode==16) { shiftkey(1) ; return ; } 
  if(e.keyCode==40&&overviewing==0) 
  { map.panTo(segments[s0].data[s1].pos) ; return ; } 

  flag = infowindow.close() ;
  if(flag=='highlight') flag = 3 ; 
  else if(flag=='seginfo') flag = 2 ; 
  else if(flag=='wpinfo') flag = 1 ; 
  else flag = 0 ; 

  if(overviewing) 
  { if(e.keyCode==70) enterfullscreen() ; 
    if(flag!=3||(e.keyCode!=39&&e.keyCode!=37&&e.keyCode!=8&&e.keyCode!=46)) 
      return ; 
  }
  if(e.keyCode==13) // return 
  { if(dragging) undraggit() ; else if(s0>=0) draggit(0) ; return ; } 
  if(dragging) return ; 

  if(e.keyCode==8||e.keyCode==46) // delete/backspace
  { e.preventDefault() ; 
    if(!e.shiftKey&&(segments.length>1||segments[0].data.length>1)) wpdel() ;
    else if(e.shiftKey&&binbtn.active) discard() ; 
    return ; 
  }
  if(e.keyCode==9) { e.preventDefault() ; inswp(1) ; return ; } // tab

  if(e.keyCode==39) // forwards
  { e.preventDefault() ;
    if(e.shiftKey) { s1 = 0 ; s0 += 1 ; if(s0==segments.length) s0 = 0 ; }
    else if(s1<segments[s0].data.length-1) s1 += 1 ; 
    else { s0 += 1 ; if(s0==segments.length) s0 = 0 ; s1 = 0 ; } 
  }
  else if(e.keyCode==37) // backwards 
  { e.preventDefault() ;  
    if(e.shiftKey) { s1 = 0 ; s0 -= 1 ; if(s0<0) s0 = segments.length-1 ; }
    else if(s1>0) s1 -= 1 ; 
    else 
    { s0 -= 1 ; 
      if(s0<0) s0 = segments.length-1 ; 
      s1 = segments[s0].data.length-1 ; 
    } 
  }
  else return ; 
  walkto(s0,s1,e.shiftKey?2:flag) ;
}
/* -------------------------- shift key handler  ---------------------------- */

function shiftkey(val)
{ shifted = val ; 
  if(map!=null) map.setOptions({draggableCursor:val?'crosshair':'default'}) ;
  if(val==0&&overviewing==0) getalts(segments,200,drawprofile) ;
}
/* --------------------- undraw & redraw segments  -------------------------- */

function undraw(i) 
{ segments[i].route.setMap(null) ; 
  if(segments[i].clickhandler!=null) 
  { google.maps.event.removeListener(segments[i].clickhandler) ;
    segments[i].clickhandler = null ; 
  }
}
function redraw(i) { undraw(i) ; draw(i) ; }

function recolour(i) 
{ if(overviewing) return ; 
  else if(i&1) segments[i].route.setOptions({strokeColor:"#ff9999"}) ; 
  else segments[i].route.setOptions({strokeColor:"#ff0000"}) ; 
}
function obliterate(s0) // undraw route and all labels
{ var i ;
  for(i=0;i<segments[s0].data.length;i++) 
    segments[s0].data[i].setmap(null,null,null,null) ; 
  undraw(s0) ; 
  disconnect(s0-1) ; 
  disconnect(s0) ; 
}
function drawprofile() { drawpro(pro,segments,selected) ; } 

/* ----------------------------- draw segments ------------------------------ */

function draw(i,width)
{ var colour,poly ; 
  if(overviewing) colour = segments[i].colour ; 
  else if(i&1) colour = "#ff9999" ; 
  else colour = "#ff0000" ; 
  if(width==undefined) poly = new linepath(i,-1,0,colour) ;
  else poly = new linepath(i,-1,0,colour,width) ;
  segments[i].route = new google.maps.Polyline(poly) ;
  segments[i].route.setMap(map) ;
  if(segments[i].clickhandler==null)
    segments[i].clickhandler = 
      google.maps.event.addListener(segments[i].route,"click",selpoint) ;
}
/* ----------------------- connect and disconnect segments ------------------ */

function disconnect(i) 
{ if(overviewing||i<0||i>=segments.length-1||segments[i].dots==null) return ;
  segments[i].dots.setMap(null) ; 
  if(segments[i].dothandler!=null) 
  { google.maps.event.removeListener(segments[i].dothandler) ;
    segments[i].dothandler = null ; 
  }
}
function reconnect(i) { disconnect(i) ; connect(i) ; }

function connect(i)
{ if(overviewing||i<0||i>=segments.length-1) return ; 
  var opos = segments[i].data[segments[i].data.length-1].pos ; 
  var npos = segments[i+1].data[0].pos ;
  if(opos.equals(npos)) return ;
  segments[i].dots = new google.maps.Polyline(new dotpath(opos,npos)) ;
  segments[i].dots.setMap(map) ;
  segments[i].dothandler = 
    google.maps.event.addListener(segments[i].dots,"click",selpoint) ;
}
function redrawconnect(s0,s1) 
{ redraw(s0) ; 
  if(s1==0) reconnect(s0-1) ; 
  if(s1=segments[s0].data.length-1) reconnect(s0) ; 
}
/* ---------------------- draw the selection point -------------------------- */

// note: there's no point in allowing clicking on a marker because the 
// event position is always the marker position rather than the click position

function drawsel(opt,selection)
{ if(selection!=undefined) selected = selection ;
  var ind,clen,s0=selected[0],s1=selected[1],pos=segments[s0].data[s1].pos ;
  if(opt) drawprofile() ; 
  clen = segments[s0].data.length ; 
  if(clen==1) arrow.rotation = 90 ; 
  else
  { if(s1==clen-1) ind = s1-1 ; else ind = s1 ;
    icons.arrow.rotation = 
      bearing(segments[s0].data[ind].pos,segments[s0].data[ind+1].pos) ;
  }

  if(sel.marker==null) sel.marker = new google.maps.Marker
    ({ position:pos, map:map, cursor:'default', icon:icons.arrow , zIndex:2 }) ;
  else // avoid unnecessary redraws
  { if(icons.arrow.rotation!=sel.orientation) sel.marker.setIcon(icons.arrow) ;
    if(!pos.equals(sel.marker.getPosition())) sel.marker.setPosition(pos) ; 
  }
  sel.orientation = icons.arrow.rotation ; 
  drawxcur(pro,selected) ;

  blackout(penbtn) ;
  if(segments.length>1) blackout(binbtn) ; else greyout(binbtn) ;  
  if(s1!=0) blackout(scissorsbtn) ; else greyout(scissorsbtn) ;
}
/* -------------------------------------------------------------------------- */

function genhead(uri,key)
{ if(uri==undefined||uri==null)  
    resuri = 'http://www.masterlyinactivity.com/routemaster/resources/' ;
  else
  { resuri = uri + '/' ;
    document.write('<script src="http://maps.google.com/maps/api/js?' +
                    ((key==null||key==undefined)?'':('key='+key+'&')) +
                   'libraries=geometry"></scr' + 'ipt>') ;
  }
  document.write
  ('<script src="' + resuri + 'dms.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'vector3d.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'latlon-ellipsoidal.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'utm.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'osgridref.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'FileSaver.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'Blob.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'routemasterlib.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'routemasterui.js"></scr' + 'ipt>' +
   '<script src="' + resuri + 'routemasteredit.js"></scr' + 'ipt>' +
   '<script src="http://www.masterlyinactivity.com/pixlib.js"></scr' + 'ipt>' +
   '<style type="text/css">html, body {width: 100%; height: 100%}' +
       'body {margin:0px}a:link{color:#66aaaa}' + 
       'a:visited{color:#cc3388}a:active{color:#404040}' +
       '</style><title>Routemaster</title>' + 
   '<link rel="shortcut icon" href=' + resuri + 'bus.gif>' ) ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */
/*                FUNCTIONS TO GENERATE THE INITIAL MAP                       */
/* -------------------------------------------------------------------------- */

function genpage() 
{ var thispage=document.URL,xhttp,quotind,plusind,fit,div,x,track,ll ;
  imginfo = new listinfo() ; 
  imgdiv = null ; 

  window.onload = function() 
  { window.addEventListener("beforeunload",function(e) 
    { var msg = unsavedmsg(0) ; 
      if(msg==null) return undefined ; 
      (e || window.event).returnValue = msg ; //Gecko + IE
      return msg ; //Gecko + Webkit, Safari, Chrome etc. (msg is ignored by ffx)
    } ) ;
  } ;
  window.onfocus = function() { shiftkey(0) ; } ; 

  body = document.getElementsByTagName("body")[0] ;
  while(body.childNodes.length>0) 
    body.removeChild(body.childNodes[body.childNodes.length-1]) ;
  mapdiv = document.createElement('div') ; 
  mapdiv.setAttribute('id','map') ; 
  mapdiv.setAttribute('style','width:100%;height:100%;position:absolute') ; 
  body.appendChild(mapdiv) ;

  if((quotind=thispage.indexOf('?'))>=0)
  { track = thispage.substring(quotind+1) ; 
    if(track.substring(0,6)=='track='&&(plusind=track.indexOf('&list='))>0) 
    { ll = track.substring(plusind+6) ; track = track.substring(6,plusind) ; }
    else if(track.substring(0,5)=='list=')
    { ll = track.substring(5) ; track = null ; }
    else if(track.substring(0,6)=='track=') 
    { track = track.substring(6) ; ll = null ; }
    else if((plusind=track.indexOf('+'))>0) 
      location.href = thispage.substring(0,quotind+1) + 'track=' +
            track.substring(0,plusind) + '&list=' + track.substring(plusind+1) ; 
    else if(track.substring(track.length-3)=='.js') 
      location.href = thispage.substring(0,quotind+1) + 'list=' + track ;
    else location.href = thispage.substring(0,quotind+1) + 'track=' + track ;
      
    if(ll!=null) getlist(ll,'uri') ;
    if(track!=null)
    { fit = ( track.substring(track.length-4).toLowerCase() == '.fit' ) ;
      xhttp = new XMLHttpRequest() ;
      if(fit) xhttp.responseType = "arraybuffer" ;
      xhttp.onreadystatechange = function() 
      { if(xhttp.readyState==4) 
        { if(xhttp.status==200)
          { if(fit) render(xhttp.response,track,0,'uri') ; 
            else
            { x = parser.parseFromString(xhttp.responseText,"application/xml") ;
              render(x,track,0,'uri') ; 
            }
          }
          else alert("Unable to read "+track+": error code "+xhttp.status) ;
        }
      }
      xhttp.open("GET",track,true) ;
      xhttp.send() ;
      return ;
    }
  }

  // come here if we need to browse the filesystem for input route
  mapdiv.appendChild(filedialogue(0)) ; 
  div = blurbdiv(resuri) ;
  div.setAttribute('style','font-family:helvetica;margin:4px;'+
                           'border-top:solid 1px silver;padding-top:2px') ; 
  mapdiv.appendChild(div) ; 
  div = helpdiv(resuri,1) ; 
  div.setAttribute('style','font-family:helvetica;margin:4px;font-size:90%') ;
  mapdiv.appendChild(div) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------- getlist --------------------------------- */

function getlist(uri,imgtype) 
{ var xhttp = new XMLHttpRequest(),list=null,sizes=null,pixpage=null,i,r ; 
  var imagedir=null,thumbshape = [] ;
  imginfo.status = 'waiting' ; 
  imginfo.type = imgtype ; 

  xhttp.onreadystatechange = function() 
  { if(xhttp.readyState==4)
    { if(xhttp.status!=200)
      { alert("Unable to read "+uri+": error code "+xhttp.status) ; return ; }
      eval(xhttp.responseText) ; 
      imginfo.uri = uri ; 
      imginfo.list = list ; 
      imginfo.sizes = sizes ; 
      imginfo.pixpage = reluri(uri,pixpage) ; 
      imginfo.thumbind = thumbind(sizes) ;
      setthumbshape(list,sizes,thumbshape,reluri(uri,imagedir)) ;
      for(i=0;i<list.length;i++) if(list[i].retpage!=undefined)
        list[i].retpage = reluri(uri,list[i].retpage) ;
      imginfo.status = 'ready' ; 
    }
  }
  xhttp.open("GET",uri,true) ;
  xhttp.send() ;
}
/* ----------------------------- file dialogue ------------------------------ */

function filedialogue(overwrite)
{ var input = document.createElement('input') ; 
  var para = document.createElement('p') ; 
  para.appendChild(document.createTextNode
                             ('\u00a0\u00a0\u00a0Select TCX/GPX/FIT file: ')) ; 
  input.setAttribute('type','file') ; 
  input.setAttribute('accept','.tcx,.gpx,.fit') ; 
  input.addEventListener('change',function(e)
  { var filename = input.files[0].name , reader = new FileReader() ;
    if(filename.substring(filename.length-4).toLowerCase()=='.fit')
    { reader.onload = function(e) 
      { render(reader.result,filename,overwrite,'file') ; } 
      reader.readAsArrayBuffer(input.files[0]) ;	
    }
    else
    { reader.onload = function(e) 
      { var xmldoc = parser.parseFromString(reader.result,"application/xml") ;
        render(xmldoc,filename,overwrite,'file') ; 
      } 
      reader.readAsText(input.files[0]) ;	
    }
  } ) ;
  para.appendChild(input) ; 
  return para ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* --------------------- set up the map and buttons ------------------------- */

function render(xmldoc,filename,overwrite,origin) 
{ var i,opts,centre,lat,lon,minlon,maxlon,minlat,maxlat,newseg,s0,d,title ;
  var colours,segno ; 

  infowindow.close() ;
  document.onkeydown = keystroke ;
  document.onkeyup = function(e) { if(e.keyCode==16) shiftkey(0) ; } ;
  xmlfile = filename ;

  // read data
  i = filename.length ; 
  if(filename.substring(i-4).toLowerCase()=='.tcx') 
    newseg = readtcx(xmldoc,map,selpoint,selpoint,labelcycle) ; 
  else if(filename.substring(i-4).toLowerCase()=='.gpx') 
    newseg = readgpx(xmldoc,map,selpoint,labelcycle) ; 
  else if(filename.substring(i-4).toLowerCase()=='.fit') 
  { newseg = readfit(xmldoc,map) ; 
    title = filename.substring(0,i-4) ;
    if(origin=='uri')
    { i = title.lastIndexOf('/') ; if(i>=0) title = title.substring(i+1) ; }
    newseg.props[0].title = newseg.props[0].sourceid = title ; 
  }
  else { alert(filename+' is not TCX/GPX/FIT') ; throw '' ; }

  if(newseg.segments.length==0||newseg.segments[0].length==0) 
  { alert('no data returned') ; return ; }
  if(newseg.segments.length>1&&setbtn!=null)
  { alert('trying to add a multitrack index... not permitted') ; return ; } 

  // check and process photo list
  if(imginfo.type!='uri') for(i=0;i<newseg.segments.length;i++)
    if(newseg.props[i].list!=null)
  { if(imginfo.uri!=null&&newseg.props[i].list!=imginfo.uri)
    { alert('inconsistent photo lists: ' + imginfo.uri + ' and ' + 
            newseg.props[i].list) ; 
      return ; 
    }
    imginfo = new listinfo() ; 
    getlist(newseg.props[i].list,'tcx') ; 
  }

  if(overwrite)
  { for(i=0;i<segments.length;i++) obliterate(i) ; 
    unprofile() ; 
    if(sel.marker!=null) sel.marker.setMap(null) ;
    segments = [] ; 
    if(imginfo.type=='tcx') imginfo = new listinfo() ; 
  } 

  s0 = segments.length ; 
  if(s0==0)
  { sel = { marker:null, orientation: null } ; 
    pending = [] ; 
    xpending = [] ; 
    actions = [] ; 
    unsavedchanges = [] ; 
    nactions = dragging = 0 ; 
    pro = starsdiv = null ; 
    routeprops = new propstype() ;
  }

  if(newseg.segments.length>1) 
  { overviewing = 1 ; 
    colours = gencolours(newseg.segments.length) ; 
    routeprops.stars = -1 ; 
  } 
  if(filename.substring(filename.length-3).toLowerCase()=='tcx')
    if(routeprops.title==null&&newseg.title!=null) settitle(newseg.title) ;

  // process the new segments
  for(i=0;i<newseg.segments.length;i++)
  { newseg.props[i].source = [ filename , origin ] ; 
    if(routeprops.stars==null) routeprops.stars = newseg.props[i].stars ;
    if(routeprops.sourceid==null) 
      routeprops.sourceid = newseg.props[i].sourceid ;
    if(routeprops.overview==null) 
      routeprops.overview = newseg.props[i].overview ;
    if(routeprops.title==null||routeprops.title=='Untitled Route')
      if(newseg.props[i].title!=null) settitle(newseg.props[i].title) ; 
    segments.push(new genseg(newseg.segments[i],newseg.props[i])) ; 
    if(overviewing) segments[segments.length-1].colour = colours[i] ;
    actions[nactions++] = 
      [ 'load',s0+i,newseg.segments[i].slice(),newseg.props[i] ] ;
  }
  if(routeprops.title==null) settitle('Untitled Route') ; 

  if(!newseg.props[0].optim.already&&!overviewing) 
    optimaction(segments.length-1,defparms,0) ; 

  if(overviewing) 
  { for(d='',i=0;i<newseg.segments.length;i++) if(newseg.props[i].title!=null)
    { if(d!='') d += ' | ' ; d += newseg.props[i].title ; }
    if(d!=null) routeprops.desc = d ; 
  }
  else 
  { routeprops.desc = newseg.props[0].desc ; 
    getalts(segments,100,drawprofile) ; 
  }
  if(routeprops.desc!=null) setdesc(routeprops.desc) ;

  // find max and min lat and long - have to look at all segs, not just
  // newly loaded to avoid google's repeatedly adding a margin 
  for(maxlat=null,segno=0;segno<segments.length;segno++)
    for(i=0;i<segments[segno].data.length;i++)
  { lat = segments[segno].data[i].pos.lat() ; 
    lon = segments[segno].data[i].pos.lng() ; 
    if(maxlat==null||lon<minlon) minlon = lon ; 
    if(maxlat==null||lon>maxlon) maxlon = lon ; 
    if(maxlat==null||lat<minlat) minlat = lat ; 
    if(maxlat==null||lat>maxlat) maxlat = lat ; 
  }

  if(s0==0)
    centre = new google.maps.LatLng((minlat+maxlat)/2,(minlon+maxlon)/2) ;

  if(map==null) // all this only done on first call
  { opts = { zoom: 22,
             center: centre,
             scaleControl: true,
             rotateControl: false,
             streetViewControl: false,
             mapTypeId: overviewing?
                google.maps.MapTypeId.ROADMAP:google.maps.MapTypeId.TERRAIN,
             disableDoubleClickZoom: true,
             styles: [ { "featureType": "poi", 
                         "stylers": [{ "visibility": "off" }]
                        } ],
             mapTypeControl:true,
             mapTypeControlOptions: 
               { style:google.maps.MapTypeControlStyle.HORIZONTAL_BAR }, 
             mapTypeIds: [ google.maps.MapTypeId.ROADMAP,
                           google.maps.MapTypeId.TERRAIN,
                           google.maps.MapTypeId.SATELLITE
                         ]
           } ;

    map = new google.maps.Map(mapdiv,opts) ;
    map.setOptions({draggable:true, draggableCursor:'default'}) ;
    clickhandle = google.maps.event.addListener(map,"click",selpoint) ;

    // set up buttons
    if(overviewing==0)
    { setbtn = genbutton('settings') ;
      scissorsbtn = genbutton('scissors') ;
      binbtn = genbutton('bin') ;
      penbtn = genbutton('pen') ;
      undobtn = genbutton('undo') ;
      redobtn = genbutton('redo') ;
      dlbtn = genbutton('dl') ;
    }
  }

  map.fitBounds(new google.maps.LatLngBounds
                             (new google.maps.LatLng(minlat,minlon),
                              new google.maps.LatLng(maxlat,maxlon))) ; 

  for(segno=s0;segno<segments.length;segno++)
    for(i=0;i<segments[segno].data.length;i++) 
      segments[segno].data[i].setmap(map,selpoint,selpoint,labelcycle) ;

  if(nactions>1) donesomething() ; // specifically, done loading & optimisation
  else actions.length = nactions ; // load with no optimisation hence no undo

  if(overviewing==0)
  { pro = new profiletype(map) ; 
    body.appendChild(pro.prodiv) ; 
    body.appendChild(pro.curdiv) ; 
  }

  for(segno=s0;segno<segments.length;segno++)
  { draw(segno) ; connect(segno-1) ; }
  connect(segments.length-1) ; 

  if(s0==0) { selected = [0,0] ; if(overviewing==0) drawsel(1) ; } 
  else greyout(dlbtn) ; 
}
/* ------------------------------- settitle --------------------------------- */

function settitle(newtitle) 
{ routeprops.title = newtitle ; 
  var h = document.getElementsByTagName('title') ;
  if(h.length==0) h = document.createElement('title') ;
  else { h = h[0] ; while(h.firstChild) h.removeChild(h.firstChild) ; }
  h.appendChild(document.createTextNode(newtitle)) ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------- setdesc --------------------------------- */

function setdesc(newdesc) 
{ routeprops.desc = newdesc ; 
  var i , d='' , m = document.getElementsByTagName('meta') ;
  if(overviewing==0&&routeprops.stars!=null) for(i=0;i<=routeprops.stars;i++)
  { if(i<routeprops.stars) d += '\u2605' ; else d += ' | ' ; }
  for(i=0;i<m.length&&m[i].getAttribute('name')!='description';i++) ;
  if(i<m) { m[i].setAttribute('content',newdesc) ; return ; }
  m = document.createElement('meta') ;
  m.setAttribute('name','description') ; 
  m.setAttribute('content',d+newdesc) ; 
  document.getElementsByTagName('head')[0].appendChild(m) ;
}
/* ------------------------------- retitle ---------------------------------- */

function retitle(opt) 
{ var newval , msg , field = (opt=='title'?'title':'desc') ; 
  var oldval = routeprops[field] ;
  infowindow.close() ; 
  if(oldval==null) msg = 'Add ' + opt ; else msg = 'Modify ' + opt ;
  if(opt=='title') msg += ' (max 15 chars):' ; else msg += ':' ;
  newval = window.prompt(msg,oldval==null?'':oldval) ;
  if(newval==null) return ; 
  if(opt=='title') newval = newval.substring(0,15) ; 
  if(newval==oldval) return ; 
  if(opt=='title') settitle(newval) ; else setdesc(newval) ; 
  actions[nactions++] = [ 'edit'+opt , oldval , newval ] ; 
  donesomething() ;
  routeinfo() ; 
}
/* ------------------------------- restars ---------------------------------- */

function restars(oldstars,newstars) 
{ actions[nactions++] = [ 'stars' , oldstars , newstars ] ; 
  donesomething() ;
  starsline(routeprops.stars=newstars,1) ; 
}
/* ------------------------------- genbutton -------------------------------- */

function genbutton(name)
{ var u,v,w,b,g,k,h,div=document.createElement('div'),act,gtitle=null,ktitle ;
  u = document.createElement('div') ;
  u.style.backgroundColor = '#ffffff' ;
  u.style.border = '2px solid #ffffff' ;
  u.style.borderRadius = '3px' ;
  u.style.boxShadow = '0 2px 6px rgba(0,0,0,.3)' ;
  if(name=='dl'||name=='settings'||name=='cursor') u.style.cursor = 'pointer' ;
  else u.style.cursor = 'default' ;
  u.style.marginBottom = '12px' ;
  if(name!='dl') u.style.marginRight = '4px' ;
  u.style.textAlign = 'center' ;
  div.appendChild(u) ;

  if(name=='scissors') 
  { h = snip ; 
    div.index = 2 ; 
    ktitle = 'Split segment at current waypoint' ; 
    gtitle = 'Split segment [disabled at first waypoint in a segment]' ;
  }
  else if(name=='bin') 
  { h = discard ; 
    div.index = 3 ; 
    ktitle = 'Delete current segment' ; 
    gtitle = 'Delete segment [disabled when the route is a single segment]' ;
  }
  else if(name=='pen') 
  { h = labelprompt ; 
    div.index = 4 ; 
    ktitle = 'Add a course point [or photo if right-clicked]' ; 
  }
  else if(name=='undo') 
  { h = undo ; 
    div.index = 5 ; 
    ktitle = 'Undo latest edit' ; 
    gtitle = 'Undo [no edits performed]' ;
  }
  else if(name=='redo') 
  { h = redo ; 
    div.index = 6 ; 
    ktitle = 'Redo latest undone edit' ; 
    gtitle = 'Redo [no edits undone]' ;
  }
  else if(name=='dl') 
  { h = function() { dl(0) ; }  ; 
    div.index = 7 ; 
    ktitle = 'Download route as tcx' ; 
    gtitle = 'Download [disabled until segments are combined]' ;
  }
  else if(name=='settings') 
  { h = popup ; 
    div.index = 1 ; 
    ktitle = 'Control menu' ; 
  }

  if(gtitle==null) gtitle = ktitle ; 
  g = greybtn(resuri,name) ;
  k = blackbtn(resuri,name) ;
  if(name=='dl'||name=='settings') { b = buttonimg(k) ; u.title = ktitle ; }
  else { b = buttonimg(g) ; u.title = gtitle ; }
  u.appendChild(b) ;

  map.controls[google.maps.ControlPosition.BOTTOM_CENTER].push(div) ;

  if(name=='dl'||name=='settings') u.addEventListener('click',h) ; 
  if(name=='dl'||name=='settings') act = 1 ; else act = 0 ; 

  return { ui:u , btn:b , active:act , greyimg:g , blackimg:k , 
           greytitle:gtitle, blacktitle:ktitle , handler:h } ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function addload(overwrite)
{ var msg ; 
  infowindow.close() ; 
  if(overwrite) 
  { msg = unsavedmsg(1) ; if(msg!=null) if(!confirm(msg)) return ; }
  infowindow.open(filedialogue(overwrite),getbtnpos(0),'addload') ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ---------------------------------- dl  ----------------------------------- */

function dl(opt) 
{ var str,i,npix,s0,s1,filename ;
  infowindow.close() ; 
  if(opt==undefined||opt==0) { opt = 0 ; getalts(segments,1,drawprofile) ; }

  // filename
  i = routeprops.title.indexOf(' ') ;
  if(i<=0) filename = routeprops.title ; 
  else filename = routeprops.title.substring(0,i) ;
  if(filename==''||filename==null) filename = 'Untitled' ; 
  filename += '.tcx' ; 

  // check for photos
  for(npix=s0=0;s0<segments.length;s0++) 
    for(s1=0;s1<segments[s0].data.length;s1++) 
      npix += segments[s0].data[s1].photo.length ;

  // photo list
  if(npix>0&&imginfo.status=='ready')
  { if(imginfo.type=='tcx') routeprops.list = imginfo.uri ; // 'tcx' vice 'uri'
    else 
    { routeprops.list = document.URL ;
      if((i=routeprops.list.lastIndexOf('?'))>=0) 
        routeprops.list = routeprops.list.substring(0,i) ; 
      routeprops.list = reluri(routeprops.list,imginfo.uri) ; 
    }
  }

  if(opt) // write index and return 
  { str = writeoverview(segments,routeprops.title,routeprops.list) ;  
    saveAs(new Blob([str],{type: "text/plain;charset=utf-8"}),filename) ;
    return ;
  }

  // record optimisation 
  routeprops.optim.origlen = routeprops.optim.ndel = 0 ; 
  routeprops.optim.parms = null ; 
  for(i=0;i<nactions;i++) if(actions[i][0]=='load')
  { routeprops.optim.ndel += actions[i][3].optim.ndel ; 
    routeprops.optim.origlen += actions[i][3].optim.origlen ; 
    routeprops.optim.parms = actions[i][3].optim.parms ;
  }
  
  str = writetcx(routeprops,segments[0].data) ;  
  if(str==null) return ; 
  unsavedchanges = [] ; 
  saveAs(new Blob([str],{type: "text/plain;charset=utf-8"}),filename) ;
}

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

• optimaction     • optimprompt     • popup     • calwork     • manualcal     • googlecalwork     • googlecal     • help     • wpdelwork     • wpdel     • revsegwork     • revseg     • insert     • inswp     • draggit     • undraggit     • seginfo     • deltimes     • extrapts     • routeinfo     • combine1     • combinework     • combine     • uncombine     • wpinfo     • setalt     • labelprompt     • labelcycle     • photoprompt     • photoedit     • advance     • retreat     • prev     • backtogps     • next     • display     • dodisplay     • imgwalk     • phinfo     • snipwork     • snip     • xferwpwork     • xferwp     • binwork     • discard     • swapsegwork     • swapseg     • actiontype     • done     • donesomething     • undo     • confirmedundo     • move     • redo     • confirmedredo

/* -------------------------------------------------------------------------- */
/*                             OPTIMISATION                                   */
/* -------------------------------------------------------------------------- */

function optimaction(segno,parms,force)
{ var s = segments[segno], result = optimise(s.data,parms) , loadno ; 
  var ndel = s.data.length - result.length ; 
  for(loadno=nactions-1;loadno>=0&&actions[loadno][0]!='load';loadno--) ; 
  if(loadno<0||(force==0&&ndel<s.data.length/2)||ndel==0) return 0 ; 
  actions[loadno][3].optim.origlen = s.data.length ; 
  actions[loadno][3].optim.ndel = ndel ; 
  actions[nactions++] = [ 'optimise' , segno , parms ] ; 
  segments[segno] = new genseg(result,segments[segno].props) ;
  actions[loadno][3].optim.parms = { tol: parms.tol , 
                                     maxsep: parms.maxsep , 
                                     wppenalty: parms.wppenalty , 
                                     vweight: parms.vweight 
                                   } ; 
  return 1 ; 
}
/* -------------------------------------------------------------------------- */

function optimprompt()
{ var msg = 'Enter 4 optimisation parms (tol, maxsep, wppenalty and vweight)' ; 
  var parmstr = defparms.tol + ' ' + 
                defparms.maxsep.toFixed(0) + ' ' +
                defparms.wppenalty.toFixed(0) + ' ' +
                defparms.vweight.toFixed(1) ;
  var parms,i ; 
  infowindow.close() ;  

  for(i=0;;i++)
  { newparms = prompt(msg,parmstr) ; 
    if(newparms==null) return ;
    if(newparms=='') { parms = defparms ; break ; }
    newparms = newparms.split(' ') ;
    if(newparms.length==0) { parms = defparms ; break ; }
    parms = { tol: parseFloat(newparms[0]) , 
              maxsep: parseFloat(newparms[1]) , 
              wppenalty: parseFloat(newparms[2]) , 
              vweight: parseFloat(newparms[3]) } ; 
    if( isNaN(parms.tol)==0 && isNaN(parms.maxsep)==0 && 
        isNaN(parms.wppenalty)==0 && isNaN(parms.vweight)==0 ) break ; 
    if(i==0) msg = '*** Illegal parms ***\n' + msg ; 
  }
  if(optimaction(segments.length-1,parms,1)) 
  { donesomething() ; draw(segments.length-1) ; } 
  routeinfo() ; 
}  
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function popup()
{ infowindow.close() ;
  var d = cogwheelmenu(dragging,routeprops.overview) ;
  infowindow.open(d,getbtnpos(0),'settings') ; 
}
/* ------------------------------- calwork --------------------------------- */

function calwork(s0,y)
{ var i,s1 ; 
  for(s1=0;s1<segments[s0].data.length;s1++)
    if(segments[s0].data[s1].h!=null) segments[s0].data[s1].h += y ; 
  drawprofile() ; 
}
/* ------------------------------ manualcal --------------------------------- */

function manualcal()
{ infowindow.close() ; 
  var x,y,s0=selected[0] ;
  x = prompt('Enter offset in metres to add to altidudes:') ;
  if(x==null) return ; 
  y = parseFloat(x) ; 
  if(isNaN(y)) { alert(x+' is not a number') ; return ; }
  calwork(s0,y) ; 
  done(['recal',s0,y]) ; 
}  
/* ---------------------------- googlecalwork ------------------------------ */

function googlecalwork(s0)
{ var i,s1 ; 
  for(s1=0;s1<segments[s0].data.length;s1++) segments[s0].data[s1].h = null ; 
  getalts(segments,1,drawprofile) ;
}
/* ------------------------------ manualcal --------------------------------- */

function googlecal()
{ infowindow.close() ; 
  var i,s0=selected[0],len=segments[s0].data.length,alt=new Array(len) ;
  for(i=0;i<len;i++) alt[i] = segments[s0].data[i].h ; 
  googlecalwork(s0) ; 
  done(['googlecal',s0,alt]) ; 
}  
/* --------------------------------- help ----------------------------------- */

function help() 
{ infowindow.close() ; infowindow.open(helpdiv(resuri),getbtnpos(0),'help') ; }

/* --------------------------------- wpdel ---------------------------------- */

function wpdelwork(s0,s1)
{ var i,response=segments[s0].data[s1],clen=segments[s0].data.length ;
  response.setmap(null,null,null,null) ;
  for(i=s1;i<clen-1;i++) segments[s0].data[i] = segments[s0].data[i+1] ;
  segments[s0].data.length = clen-1 ; 
  selected = [s0,s1] ; 
  if(s1==segments[s0].data.length) selected[1] -= 1 ;  
  redrawconnect(s0,s1) ; 
  drawsel(1) ; 
  return response ;
}
function wpdel()
{ var s0=selected[0],s1=selected[1],i ;
  var flag = infowindow.close() ; 
  done(['wpdel',s0,s1,wpdelwork(s0,s1)]) ; 
  if(flag=='wpinfo') wpinfo() ; 
}
/* --------------------------------- revseg --------------------------------- */

function revsegwork(s0)
{ var i,s=segments[s0],j,x,len=s.data.length ;
  disconnect(s0-1) ; disconnect(s0) ; 
  for(i=0;i<len/2;i++)
  { j = (len-1) - i ; 
    x = s.data[i] ; s.data[i] = s.data[j] ; s.data[j] = x ; 
  }
  for(i=0;i<s.data.length;i++) 
    if(s.data[i].type=='Right') s.data[i].settype('Left') ;
    else if(s.data[i].type=='Left') s.data[i].settype('Right') ;

  if(s0==selected[0]) selected[1] = (len-1) - selected[1] ; 
  connect(s0-1) ; connect(s0) ; 
  drawsel(1) ; 
}
/* -------------------------------------------------------------------------- */

function revseg()
{ infowindow.close() ; 
  revsegwork(selected[0]) ; 
  done(['revseg',selected[0]]) ; 
}
/* -------------------------------------------------------------------------- */
/*    DRAGGING A (POSSIBLY NEWLY INSERTED) WAYPOINT IS QUITE A LOT OF WORK    */
/* -------------------------------------------------------------------------- */

function insert(s0,s1,n)
{ var i ;
  for(i=segments[s0].data.length+n-1;i>s1;i--)
    segments[s0].data[i] = segments[s0].data[i-n] ;
  for(i=0;i<n;i++) segments[s0].data[s1+i] = new datatype(null,null) ;  
}
/* --------------------------------- inswp ---------------------------------- */

function inswp(dir)
{ var s0=selected[0],s1=selected[1],bounds,del,pos,data=segments[s0].data ;
  var len = data.length ;
  if(len==1) pos = data[0].pos ;

  if(dir>=0) s1 = selected[1] += 1 ; 
  insert(s0,s1,1) ;  
  if(len==1)
  { bounds = map.getBounds() ;
    del = bounds.getNorthEast().lng() - bounds.getSouthWest().lng() ; 
    pos = new google.maps.LatLng(pos.lat(),pos.lng()+dir*del/10) ; 
  }
  else if(s1==0) pos = interp(data[2].pos,data[1].pos,1.5) ; 
  else if(s1<len) pos = interp(data[s1-1].pos,data[s1+1].pos,0.5) ;
  else pos = interp(data[s1-2].pos,data[s1-1].pos,1.5) ;
  data[s1].setpos(pos) ; 
  draggit(1) ; 
}
/* -------------------------------- draggit --------------------------------- */

// draggit makes the current waypoint draggable

var l1,l2,startpos,seg0,seg1,seg2,colour,inserted ; 

function draggit(insparm)
{ var s0=selected[0],s1=selected[1],start,end,i,len=segments[s0].data.length ;
  startpos = segments[s0].data[s1].pos ;
  inserted = insparm ; 
  infowindow.close() ; 
  greyout(scissorsbtn) ;
  greyout(binbtn) ;
  greyout(penbtn) ;
  greyout(undobtn) ;
  greyout(redobtn) ;
  greyout(dlbtn) ;
  map.panToBounds(new google.maps.LatLngBounds(startpos,startpos)) ;

  sel.marker.setMap(null) ; 
  sel.marker = new google.maps.Marker(
                  { position: segments[s0].data[s1].pos,
                    map: map,
                    cursor: 'default',
                    icon: icons.concircle ,
                    draggable: true ,
                    zIndex: 2
                  } ) ;

  if(s0&1) colour = "#ff9999" ; else colour = "#ff0000" ; 
  segments[s0].route.setMap(null) ;
  if(segments[s0].clickhandler!=null) 
  { google.maps.event.removeListener(segments[s0].clickhandler) ;
    segments[s0].clickhandler = null ; 
  }

  seg0 = seg2 = null; 
  if(s1>1)
  { seg0 = new google.maps.Polyline(new linepath(s0,0,s1,colour)) ;
    seg0.setMap(map) ;
  }

  if(s1==0) start = 0 ; else start = s1-1 ; 
  if(s1==len-1) end = s1+1 ; else end = s1+2 ; 
  seg1 = new google.maps.Polyline(new linepath(s0,start,end,colour)) ;
  seg1.setMap(map) ;

  if(s1<segments[s0].data.length-2)
  { seg2 = new google.maps.Polyline(new linepath(s0,s1+1,len,colour)) ;
    seg2.setMap(map) ;
  }

  l1 = google.maps.event.addListener(sel.marker,'drag',function()
  { segments[s0].data[s1].setpos(this.getPosition()) ; 
    seg1.setMap(null) ;
    seg1 = new google.maps.Polyline(new linepath(s0,start,end,colour)) ;
    seg1.setMap(map) ;
    if(s1==0&&s0>0) { disconnect(s0-1) ; connect(s0-1) ; }
    if(s1==len-1&&s0<segments.length-1) { disconnect(s0) ; connect(s0) ; }
  } ) ;
 
  dragging = 1 ; 
}  
/* ------------------------------- undraggit -------------------------------- */

// undraggit is invoked by [return] to terminate waypoint dragging

function undraggit()
{ var s0=selected[0],s1=selected[1],i,s1dash,pos=segments[s0].data[s1].pos ; 
  var xpos ; 
  google.maps.event.removeListener(l1) ;
  dragging = 0 ; 
  if(seg0!=null) seg0.setMap(null) ;
  seg1.setMap(null) ;
  if(seg2!=null) seg2.setMap(null) ;
  segments[s0].route = new google.maps.Polyline(new linepath(s0,-1,0,colour)) ;
  segments[s0].route.setMap(map) ;
  segments[s0].data[s1].h = null ;
  getalts(segments,100,drawprofile) ; 

  sel.marker.setMap(null) ; 
  sel.marker = null ; // force a redraw
  drawsel(1) ; 
  if(inserted||dist(startpos,pos)>5) 
    done(['move',s0,s1,startpos,pos,inserted]) ; 
  if(segments.length==1) blackout(dlbtn) ; 
}
/* -------------------------------------------------------------------------- */

function seginfo()
{ var pos = segments[selected[0]].data[selected[1]].pos ;
  infowindow.close() ;
  infowindow.open(seginfodiv(segments,selected[0]),pos,'seginfo') ; 
}
/* -------------------------------------------------------------------------- */

function deltimes()
{ var s0,s1,task=[] ;
  for(s0=0;s0<segments.length;s0++) for(s1=0;s1<segments[s0].data.length;s1++)
    if(segments[s0].data[s1].t!=null)
  { task.push([s0,s1,segments[s0].data[s1].t]) ; 
    segments[s0].data[s1].t = null ; 
  }
  infowindow.close() ;  
  done(['deltimes',task]) ;
}  
/* -------------------------------------------------------------------------- */
/*page*/
/* ------------------------- interpolate extra points ----------------------- */

function extrapts(opt)
{ var s0,s1,sep,data,n,opos,npos,i,lambda ;
  var task = [ 'extra' , selected[0] , selected[1] ] ;
  infowindow.close() ; 

  for(s0=0;s0<segments.length;s0++) 
    for(data=segments[s0].data,s1=1;s1<data.length;s1++)
      if((sep=dist(opos=data[s1-1].pos,npos=data[s1].pos))>100) 
  { n = Math.floor(sep/95) ;
    insert(s0,s1,n) ; 
    for(i=0;i<n;i++) 
    { lambda = (i+1) / (n+1) ; 
      data[s1+i].setpos(new google.maps.
                          LatLng(lambda*npos.lat()+(1-lambda)*opos.lat(),
                                 lambda*npos.lng()+(1-lambda)*opos.lng())) ;
    }
    if(selected[0]==s0&&s1<=selected[1]) selected[1] += n ; 
    task.push([s0,s1,data.slice(s1-1,s1+n+1)]) ; 
    s1 += n ;
  }

  getalts(segments,1,drawprofile) ; 
  done(task) ; 
  routeinfo() ; 
}
/* -------------------------------------------------------------------------- */
/*       ROUTEINFO IS A MENU WHICH ITSELF SUPPORTS THE COMBINE FUNCTION       */
/* -------------------------------------------------------------------------- */

function routeinfo() 
{ infowindow.close() ; 
  infowindow.open(routediv(routeprops),getbtnpos(0),'routeinfo') ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ------------------------------- combine1 --------------------------------- */

function combine1(sa,sb)
{ undraw(sb) ; 
  disconnect(sb-1) ; 
  if(selected[0]==sb) selected = [ sa , selected[1]+segments[sa].data.length ] ; 
  segments[sa].data = segments[sa].data.concat(segments[sb].data) ; 
}
function combinework()
{ var s0 ;
  for(s0=1;s0<segments.length;s0++) combine1(0,s0) ;
  segments.length = 1 ; 
  redraw(0) ; 
  drawsel(1) ; 
  drawprofile() ; 
  blackout(dlbtn) ; 
}
/* -------------------------------------------------------------------------- */

function combine()
{ var i , task = [ 'combine' , segments.length ] ;
  infowindow.close() ; 
  for(i=0;i<segments.length;i++) 
    task.push(segments[i].data.length,segments[i].props) ; 
  combinework() ; 
  done(task) ; 
}  
/* -------------------------------------------------------------------------- */

function uncombine(task)
{ var i,llen,nlen ; 

  segments.length = task[1] ;
  for(flag=0,s0=task[1]-1,i=task.length-2;i>=4;i-=2,s0--)
  { llen = segments[0].data.length ;
    segments[s0] = new genseg(segments[0].data.slice(llen-task[i]),task[i+1]) ; 
    if(flag==0&&selected[1]>=segments[0].data.length)
    { selected = [ s0 , selected[1]-segments[0].data.length ] ; flag = 1 ; } 
    draw(s0) ; 
    connect(s0) ; 
  }
  segments[0].data.length = task[2] ; 
  for(llen=s0=0;s0<segments.length;llen=nlen,s0++)
  { nlen = llen + segments[s0].data.length ;
    if(selected[1]>=llen&&selected[1]<nlen) 
    { selected = [ s0 , selected[1]-llen ] ; break ; }
  }
  redraw(0) ; 
  drawsel(1) ; 
  drawprofile() ; 
  greyout(dlbtn) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */
/*           WPINFO IS A MENU GIVING ACCESS TO THE SETALT FUNCTION            */
/* -------------------------------------------------------------------------- */

function wpinfo() 
{ infowindow.close() ; 
  infowindow.open
    (wpinfodiv(),segments[selected[0]].data[selected[1]].pos,'wpinfo') ; 
}
/* -------------------------------- setalt ---------------------------------- */

function setalt(edit)
{ infowindow.close() ; 
  var s0=selected[0],s1=selected[1],x,y=null,oldalt ; 
  oldalt = segments[s0].data[s1].h.toFixed(0) ;
  if(edit) x = prompt('Enter altitude (m):',oldalt) ;
  else x = prompt('Enter altitude (m):') ;
  if(x==null) return ; 
  if(x!=''&&isNaN(y=parseFloat(x))) { alert(x+' is not a number') ; return ; }
  if(y==null&&oldalt==null) return ; 
  if(y!=null&&Math.abs(y-oldalt)<0.1) return ; 
  segments[s0].data[s1].h = y ; 
  done(['setalt',s0,s1,oldalt,y]) ; 
  drawprofile() ; 
  wpinfo() ; 
}  
/* -------------------------------------------------------------------------- */
/*   THE LABELS ARE ACCESSED FROM THE PEN BUTTON OR BY CLICKING ON THE MAP    */
/* -------------------------------------------------------------------------- */

function labelprompt()
{ var oldcaption='',oldtype,type='Generic',s0=selected[0],s1=selected[1] ;
  var str , flag = (infowindow.close()=='wpinfo') ; 
  var datum = segments[s0].data[s1] ;
  
  oldtype = datum.type ; 
  if(oldtype!=null) oldcaption = datum.marker.title ; 
  if(oldcaption==null) oldcaption = '' ;
  if(oldtype!=null) type = oldtype ;
  if(oldtype==null) str = 'Enter' ; else str = 'Modify or delete' ; 
  var caption = window.prompt(str+' label:',oldcaption) ;

  if(caption==null) { if(flag) wpinfo() ; else walkto(s0,s1) ; return ; } 
  else if(caption=='') type = null ; 
  else caption = caption.substring(0,10) ;
  if(caption==oldcaption) 
  { if(flag) wpinfo() ; else walkto(s0,s1) ; return ; } 

  segments[s0].data[s1].setlabel(type,caption,map,selpoint,labelcycle) ; 
  done(['editlabel',s0,s1,oldcaption,caption,oldtype,type]) ; 
  if(flag) wpinfo() ; else walkto(s0,s1) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ------------------------------ labelcycle -------------------------------- */

function labelcycle()
{ var s0,s1,datum,types,caption,flag=(infowindow.close()=='wpinfo') ;  ;
  for(s0=0;s0<segments.length;s0++) for(s1=0;s1<segments[s0].data.length;s1++)
  { datum = segments[s0].data[s1] ;
    if(datum.marker!=this) continue ; 
    types = datum.labelcycle() ;
    caption = datum.marker.title ;
    selected = [s0,s1] ; 
    done(['editlabel',s0,s1,caption,caption,types[0],types[1]]) ; 
    if(flag) wpinfo() ; 
    return ;
  }
}
function photoprompt(e) 
{ var s0=selected[0],s1=selected[1] ;
  if(e!=null) e.preventDefault() ;
  var flag = (infowindow.close()=='wpinfo') ; 
  var datum = segments[s0].data[s1] ;
  var photo = window.prompt('Enter photo name:','') ;

  if(photo!=null&&photo!='') 
  { done(['editphoto',s0,s1,datum.photo.length,null,photo]) ; 
    datum.addphoto(photo,map,selpoint) ; 
    walkto(s0,s1) ;
  }
  else if(flag==1) wpinfo() ; 
  else if(flag==2) seginfo() ; 
  else if(flag==3) highlight() ; 
  else walkto(s0,s1) ; 
}
function photoedit(ind)
{ var s0=selected[0],s1=selected[1],i ;
  var flag = (infowindow.close()=='wpinfo') ; 
  var datum = segments[s0].data[s1] ;
  var photo = window.prompt('New photo name:',datum.photo[ind]) ;

  if(photo!=null&&photo!='') for(i=0;i<datum.photo.length;i++)
    if(datum.photo[i]==photo) { photo = null ; break ; }
  if(photo!=null)
  { if(photo=='') photo = null ;
    done(['editphoto',s0,s1,ind,datum.photo[ind],photo]) ; 
    datum.setphoto(ind,photo,selpoint) ; 
  }
  if(flag) wpinfo() ; else walkto(s0,s1) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ----------------------------- display photo ------------------------------ */

var lmove,rmove ; 

function advance(s0,s1,ind)
{ for(ind++;;ind++)
  { if(ind>=segments[s0].data[s1].photo.length) { ind = 0 ; s1 += 1 ; }
    if(s1==segments[s0].data.length) 
    { s0 += 1 ; if(s0==segments.length) return null ; s1 = 0 ; }
    if(ind<segments[s0].data[s1].photo.length) return [s0,s1,ind] ;
  }
}
function retreat(s0,s1,ind)
{ for(ind--;;ind--)
  { if(ind<0) { s1 -= 1 ; ind = null ; }
    if(s1<0)
    { if(s0==0) return null ; else s0 -= 1 ; 
      s1 = segments[s0].data.length-1 ; 
    }
    if(ind==null) ind = segments[s0].data[s1].photo.length - 1 ;
    if(ind>=0) return [s0,s1,ind] ;
  }
}
function prev() { dodisplay(lmove[0],lmove[1],lmove[2],-1) ; }
function backtogps() 
{ document.onkeydown = keystroke ; 
  window.removeEventListener('resize',resize) ; 
  body.removeChild(imgdiv) ; 
  walkto(selected[0],selected[1],0) ;
}
function next() { dodisplay(rmove[0],rmove[1],rmove[2],1) ; }

function display(ind)
{ document.onkeydown = imgwalk ;
  window.addEventListener('resize',resize) ; 
  infowindow.close() ; 
  imgdiv = document.createElement('div') ; 
  imgdiv.setAttribute('style','position:fixed;width:100%;height:100%;'+
                      'left:0;top:0;background:black') ;
  dodisplay(selected[0],selected[1],ind,1) ; 
  body.appendChild(imgdiv) ; 
}
function dodisplay(s0,s1,ind,dir)
{ var phind=findimg(segments[s0].data[s1].photo[ind]) , pre=null ;
  uncaption() ; 
  selected[0] = s0 ; 
  selected[1] = s1 ; 
  lmove = retreat(s0,s1,ind) ;
  rmove = advance(s0,s1,ind) ;
  if(dir<0&&lmove!=null)
    pre = findimg(segments[lmove[0]].data[lmove[1]].photo[lmove[2]]) ;
  else if(dir>=0&&rmove!=null)
    pre = findimg(segments[rmove[0]].data[rmove[1]].photo[rmove[2]]) ;
  if(pre!=null) pre = imginfo.list[pre] ;

  gendisplay(imgdiv,imginfo.list[findimg(segments[s0].data[s1].photo[ind])],
             imginfo.sizes,lmove==null?null:'javascript:prev()',
             'javascript:backtogps()',rmove==null?null:'javascript:next()',
             'GPS track',pre) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ------------------------------ image walk -------------------------------- */

function imgwalk(e)
{ e.preventDefault() ; 

  if(e.keyCode==39) { if(rmove!=null) next() ; return ; }
  else if(e.keyCode==37) { if(lmove!=null) prev() ; return ; }
  else if(e.keyCode==40) reduce() ; 
  else if(e.keyCode==38) enlarge() ; 
  else if(e.keyCode==70) enterfullscreen() ; 
  else backtogps() ;
}
/* --------------------------------- photo info ----------------------------- */

function phinfo(i) 
{ infowindow.close() ; 
  var s0=selected[0],s1=selected[1] ; 
  infowindow.open(phdiv(i),segments[s0].data[s1].pos,'phinfo') ; 
}
/* ------------------------- snip: apply scissors  -------------------------- */

function snipwork(s0,s1)
{ var i,k,newlen ; 
  undraw(s0) ; 
  segments.length += 1 ; 
  for(i=segments.length-1;i>s0+1;i--) segments[i] = segments[i-1] ; 

  segments[s0+1] =  new genseg(segments[s0].data.slice(s1),segments[s0].props) ;
  segments[s0+1].dots = segments[s0].dots ;
  segments[s0+1].dothandler = segments[s0].dothandler ;
  segments[s0].dots = segments[s0].dothandler = null ; 

  segments[s0].data.length = s1 ; 
  draw(s0) ;
  connect(s0) ; 
  draw(s0+1) ; 
  for(i=s0+2;i<segments.length;i++) recolour(i) ;
  selected = [s0+1,0] ; 
  drawsel(1) ; 
  greyout(dlbtn) ; 
}
function snip()
{ var i,s0=selected[0],s1=selected[1] ; 
  infowindow.close() ; 
  done(['snip',s0,s1]) ; 
  snipwork(s0,s1) ; 
}
/* ----------------------- xferwp: transfer waypoint  ----------------------- */

function xferwpwork(s0,s1)
{ undraw(s0) ; 
  if(s1==0)
  { undraw(s0-1) ; 
    disconnect(s0-1) ; 
    segments[s0-1].data.push(segments[s0].data[0]) ; 
    segments[s0].data = segments[s0].data.slice(1,segments[s0].data.length) ; 
    draw(s0-1) ; 
    connect(s0-1) ; 
    if(selected[0]==s0&&selected[1]==s1)
    { selected[0] -= 1 ; selected[1] = segments[s0-1].data.length-1 ; }
    else if(selected[0]==s0) selected[1] -= 1 ; 
  }
  else
  { undraw(s0+1) ; 
    disconnect(s0) ; 
    segments[s0+1].data.unshift(segments[s0].data[segments[s0].data.length-1]) ; 
    segments[s0].data.length -= 1 ; 
    draw(s0+1) ; 
    connect(s0) ; 
    if(selected[0]==s0&&selected[1]==segments[s0].data.length)
    { selected[0] += 1 ; selected[1] = 0 ; }
    else if(selected[0]==s0) selected[1] += 1 ; 
  }
  draw(s0) ; 
  drawprofile() ; 
}
function xferwp()
{ var flag = infowindow.close() ; 
  done(['xferwp',selected[0],selected[1]]) ; 
  xferwpwork(selected[0],selected[1]) ; 
  if(flag=='highlight') flag = 3 ; 
  else if(flag=='seginfo') flag = 2 ; 
  else if(flag=='wpinfo') flag = 1 ; 
  else flag = 0 ; 
  walkto(selected[0],selected[1],flag) ;
}
/* ------------------------ discard: bin a segment  ------------------------- */

function binwork(s0)
{ var i ; 
  obliterate(s0) ; 
  for(i=s0;i<segments.length-1;i++) segments[i] = segments[i+1] ; 
  segments.length -= 1 ; 
  for(i=s0;i<segments.length;i++) recolour(i) ;
  connect(s0-1) ;

  selected[1] = 0 ; 
  if(selected[0]==segments.length) selected[0] = 0 ; 
  drawsel(1) ; 
  if(segments.length==1) blackout(dlbtn) ; 
}
function discard()
{ var s0=selected[0] ; 
  infowindow.close() ;  
  done(['bin',s0,segments[s0]]) ; 
  binwork(s0) ; 
}
/* ---------------------- swapseg: swap two segments  ----------------------- */

function swapsegwork(s0)
{ var i , temp = segments[s0+1] ; 
  segments[s0+1] = segments[s0] ;
  segments[s0] = temp ; 
  for(i=s0-1;i<s0+2;i++) { reconnect(i) ; if(i>=s0) recolour(i) ; }
  drawprofile() ;
}
function swapseg(s0)
{ var flag = infowindow.close() ; 
  done(['swapseg',s0]) ; 
  swapsegwork(s0) ; 
  if(selected[0]==s0) selected[0] += 1 ; 
  else if(selected[0]==s0+1) selected[0] -= 1 ; 
  if(flag=='highlight') flag = 3 ; 
  else if(flag=='seginfo') flag = 2 ; 
  else if(flag=='wpinfo') flag = 1 ; 
  else flag = 0 ; 
  walkto(selected[0],selected[1],flag) ;
}
/* -------------------------------------------------------------------------- */

function actiontype(x)
{ if( x=='snip'||x=='combine'||x=='interpolate'
   || x=='optimise'||x=='load' ) return 0 ; else return 1 ;
}
function done(something) 
{ if( nactions>0 && unsavedchanges.length>0 && something[0]=='editlabel'
   && actions[nactions-1][0]==something[0]
   && actions[nactions-1][1]==something[1] // don't merge change with delete
   && actions[nactions-1][2]==something[2] && something[6]!=null )
  { actions[nactions-1][4] = something[4] ; // caption
    actions[nactions-1][6] = something[6] ; // type
  }
  else { actions[nactions++] = something ; donesomething() ; }
}
function donesomething()
{ actions.length = nactions ; 
  blackout(undobtn) ; 
  greyout(redobtn) ; 
  if(actiontype(actions[nactions-1][0])!=0) 
  { if(unsavedchanges.length>=3) unsavedchanges.push(null) ; 
    else unsavedchanges.push(actionname(actions[nactions-1])) ;
  }
}
/* -------------------------------------------------------------------------- */
/*page*/
/* --------------------------------- undo  ---------------------------------- */

function undo()
{ infowindow.close() ;  
  var opts = 'Undo ' + actionname(actions[nactions-1]) ;
  infowindow.open(genclickable('confirmedundo()',opts),getbtnpos(4),'undo') ; 
}
function confirmedundo()
{ var i,ano=nactions-1,action=actions[ano][0],s0=actions[ano][1],s1,caption ;
  var oldcaption,task,ind ; 
  infowindow.close() ;  

  if(action!='revseg'&&action!='interpolate'&&
     action!='deltimes'&&action!='swapseg') s1 = actions[ano][2] ;

  if(action=='bin') 
  { disconnect(s0-1) ; 
    for(i=segments.length;i>s0;i--) 
    { segments[i] = segments[i-1] ; recolour(i) ; } 
    segments[s0] = s1 ;
    for(s1=0;s1<segments[s0].data.length;s1++)
      segments[s0].data[s1].setmap(map,selpoint,selpoint,labelcycle) ; 
    draw(s0) ; 
    connect(s0-1) ; 
    connect(s0) ; 
    if(selected[0]>=s0) selected[0] += 1 ; 
    drawsel(1) ; 
    greyout(dlbtn) ; 
  }
  else if(action=='snip') // undo snip
  { selected = [ s0 , segments[s0].data.length-1 ] ; 
    undraw(s0) ; 
    combine1(s0,s0+1) ; 
    for(i=s0+1;i<segments.length-1;i++) 
    { segments[i] = segments[i+1] ; recolour(i) ; } 
    segments.length -= 1 ; 
    if(segments.length==1) blackout(dlbtn) ; 
    draw(s0) ;
    drawsel(1) ; 
  } 
  else if(action=='xferwp') // undo transfer waypoint
  { if(s1==0) xferwpwork(s0-1,segments[s0-1].data.length-1) ; 
    else xferwpwork(s0+1,0) ;
  }
  else if(action=='editlabel')  // undo create/edit/delete label
    segments[s0].data[s1].setlabel(actions[ano][5],actions[ano][3],
                                   map,selpoint,labelcycle) ;
  else if(action=='edittitle') settitle(s0) ; 
  else if(action=='editdescription') setdesc(s0) ; 
  else if(action=='wpdel')      // ['wpdel',s0,s1,wpdelwork(s0,s1)]
  { insert(s0,s1,1) ; 
    segments[s0].data[s1] = actions[ano][3] ;
    segments[s0].data[s1].setmap(map,selpoint,selpoint,labelcycle) ;
    redrawconnect(s0,s1) ;
    drawsel(1,[s0,s1]) ; 
  }
  else if(action=='move')
  { if(actions[ano][5]) wpdelwork(s0,s1) ; else move(s0,s1,actions[ano][3]) ; }
  else if(action=='recal') calwork(s0,-s1) ; 
  else if(action=='googlecal') for(i=0;i<segments[s0].data.length;i++)
    segments[s0].data[i].h = s1[i] ;
  else if(action=='setalt') segments[s0].data[s1].h = actions[ano][3] ;
  else if(action=='combine') uncombine(actions[ano]) ; 
  else if(action=='revseg') revsegwork(s0) ; 
  else if(action=='swapseg') swapsegwork(s0) ; 
  else if(action=='stars') routeprops.stars = s0 ; 
  else if(action=='deltimes') for(i=0;i<s0.length;i++) 
    segments[s0[i][0]].data[s0[i][1]].t = s0[i][2] ;
  else if(action=='optimise') // [ 'load' , s0 , data.slice() , props ]
  { for(ano--;ano>=0&&actions[ano][0]!='load';ano--) ;
    segments[s0].data = actions[ano][2] ;
    actions[ano][3].optim.ndel = 0 ; 
    redraw(s0) ;
    drawsel(1,[s0,0]) ; 
  }
  else if(action=='editphoto') 
  { ind = actions[ano][3] ;
    if(actions[ano][5]==null)      // undo delete
      for(i=segments[s0].data[s1].photo.length;i>ind;i--)
        segments[s0].data[s1].photo[i] = segments[s0].data[s1].photo[i-1] ;
    if(ind>=segments[s0].data[s1].photo.length)
      segments[s0].data[s1].addphoto(actions[ano][4],map,selpoint) ;
    else segments[s0].data[s1].setphoto(ind,actions[ano][4],selpoint) ;
  }
  else if(action=='extra') 
    for(selected=[s0,s1],i=actions[ano].length-1;i>=3;i--)
  { task = actions[ano][i]
    segments[task[0]].data.splice(task[1],task[2].length-2) ; 
  }

  nactions -= 1 ; 
  if(nactions<=1) greyout(undobtn) ; 
  blackout(redobtn) ; 
  if(actiontype(actions[nactions][0])!=0&&unsavedchanges.length>0)
    unsavedchanges.length -= 1 ;  ;
  if( action=='optimise' || action=='dltimes' || action=='stars'
   || action=='editdescription') routeinfo() ; 
  else if(action=='editphoto'||action=='editlabel') walkto(s0,s1) ;
}
/* --------------------------------- move ----------------------------------- */

function move(s0,s1,pos)
{ segments[s0].data[s1].setpos(pos) ; redrawconnect(s0,s1) ; drawsel(1) ; }

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

function redo()
{ infowindow.close() ;  
  var opts = 'Redo ' + actionname(actions[nactions]) ;
  infowindow.open(genclickable('confirmedredo()',opts),getbtnpos(5),'redo') ; 
}
function confirmedredo()
{ var i,action=actions[nactions][0],s0=actions[nactions][1],s1,caption,a,b,c ;
  var task,ind,photo ; 
  if(action!='bin'&&action!='revseg'&&action!='interpolate'&&
     action!='deltimes'&&action!='swapseg') s1 = actions[nactions][2] ;
  infowindow.close() ; 

  if(action=='bin') binwork(s0) ; 
  else if(action=='snip') snipwork(s0,s1) ; 
  else if(action=='xferwp') xferwpwork(s0,s1) ; 
  else if(action=='editlabel') // redo create/edit/delete label
    segments[s0].data[s1].setlabel(actions[nactions][6],actions[nactions][4],
                                   map,selpoint,labelcyle) ;
  else if(action=='edittitle') settitle(s1) ; 
  else if(action=='editdescription') setdesc(s1)  ; 
  else if(action=='wpdel') wpdelwork(s0,s1) ; 
  else if(action=='move')     // ['move',s0,s1,oldpos,newpos,inserted]
  { if(actions[nactions][5]) insert(s0,s1,1) ; 
    move(s0,s1,actions[nactions][4]) ; 
  }
  else if(action=='recal') calwork(s0,s1) ; 
  else if(action=='googlecal') googlecalwork(s0) ; 
  else if(action=='setalt') segments[s0].data[s1].h = actions[nactions][4] ;
  else if(action=='combine') combinework() ; 
  else if(action=='revseg') revsegwork(s0) ; 
  else if(action=='swapseg') swapsegwork(s0) ; 
  else if(action=='stars') routeprops.stars = s1 ; 
  else if(action=='deltimes') for(i=0;i<s0.length;i++) 
    segments[s0[i][0]].data[s0[i][1]].t = null ;
  else if(action=='optimise') 
  { result = optimise(segments[s0].data,actions[nactions][2]) ; 
    actions[loadno][3].optim.ndel = segments[s0].data.length - result.length ;
    segments[s0].data = result ; 
    redraw(s0) ;
    drawsel(1,[s0,0]) ; 
    routeinfo() ; 
  }
  else if(action=='editphoto') 
  { ind = actions[nactions][3] ;
    photo = actions[nactions][5] ;
    if(actions[nactions][4]==null) 
      segments[s0].data[s1].addphoto(photo,map,selpoint) ;
    else segments[s0].data[s1].setphoto(ind,photo,selpoint) ; 
  }
  else if(action=='extra') 
    for(selected=[s0,s1],i=3;i<actions[nactions].length;i++)
  { task = actions[nactions][i] ;
    a = segments[task[0]].data.slice(0,task[1]) ;
    b = task[2].slice(1,task[2].length-1) ; 
    c = segments[task[0]].data.slice(task[1]) ;
    segments[task[0]].data = a.concat(b,c) ; 
  }

  nactions += 1 ; 
  if(nactions==actions.length) greyout(redobtn) ; 
  blackout(undobtn) ; 
  if(actiontype(actions[nactions-1][0])!=0) unsavedchanges.push(action) ;
  if(action=='stars'||action=='editdescription') routeinfo() ; 
  else if(action=='editphoto'||action=='editlabel') walkto(s0,s1) ;
}
/* -------------------------------------------------------------------------- */

• underline     • textdiv     • seginfodiv     • highdiv     • scrolltype     • function     • scroller     • greybtn     • blackbtn     • buttonimg     • buttoncell     • textcell     • appendrow     • genlink     • genspan     • genclickable     • blurbdiv     • northernmost     • helpdiv     • cogwheelmenu     • walktodiv     • wpinfodiv     • titlediv     • routediv     • ldivadd     • doalts     • starsline     • createfunc     • phdiv     • actionname     • profiletype     • drawxcur     • drawpro     • toggleprofile     • point2LatLng

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

function underline(d) 
{ d.setAttribute('style',
      'margin-bottom:2px;border-bottom:solid 1px silver;padding-bottom:2px') ; 
  return d ; 
}
function textdiv(title,body,lim)
{ var div=document.createElement('div'),b,nobr,flag=0 ;
  if(lim==undefined||lim==0) lim = null ; 
  else if(lim<0) { flag = 1 ; lim = -lim ; } 

  if(title!=null) 
  { b = document.createElement('b') ;
    b.appendChild(document.createTextNode(title+': ')) ;
  }

  if(lim==null||body.length<lim)
  { nobr = document.createElement('nobr') ;
    if(title!=null) nobr.appendChild(b) ;
    nobr.appendChild(document.createTextNode(body)) ;
    div.appendChild(nobr) ;
  }
  else 
  { if(title!=null) div.appendChild(b) ; 
    div.appendChild(document.createTextNode(body)) ; 
    if(flag==0) underline(div) ;  
  }
  return div ; 
}
/* -------------------------------------------------------------------------- */

function seginfodiv(segments,segno)
{ var div=document.createElement('div'),props=segments[segno].props ; 
  var span,prose,lim,d ; 
  div.appendChild(textdiv(null,'Segment ' + segno + ' of ' +
    segments.length + ' (' + segments[segno].data.length + ' points)')) ;
  if(props.title!=null) div.appendChild(textdiv('Title',props.title)) ;
  if(props.sourceid!=null&&props.sourceid!=props.title) 
    div.appendChild(textdiv('Source',props.sourceid)) ;
  if(props.stats==null) lim = -50 ; else lim = 50 ; 
  if(props.desc!=null) div.appendChild(textdiv('Description',props.desc,lim)) ;
  if(props.stats!=null) div.appendChild(textdiv('Stats',props.stats)) ;

  div.setAttribute('style','margin-bottom:2px;'+
                   'border-bottom:solid 1px silver;padding-bottom:2px') ; 
  d = document.createElement('div') ;
  d.appendChild(div) ;
  d.appendChild(genclickable('manualcal()',
                'Add an offset to segment altitudes','br')) ; 
  d.appendChild(genclickable('googlecal()',
                'Ask Google to recalibrate segment altitudes','br')) ; 

  prose = 'Swap with preceding segment' ;
  if(segno==0) span = genspan(prose,'br') ;
  else span = genclickable('swapseg('+(segno-1)+')',prose,'br') ;
  d.appendChild(span) ;
  prose = 'Swap with following segment' ;
  if(segno==segments.length-1) span = genspan(prose,'br') ;
  else span = genclickable('swapseg('+segno+')',prose,'br') ;
  d.appendChild(span) ;
  return d ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function highdiv(props,list,sizes,sizeno,names) 
{ var div,scroll,p,a,d,dwid,nfetched=2 ;
  var items,i,ind,maxh,minw,sum,scroll,image=[null,null,null] ;
  for(items=[],i=0;i<names.length;i++) if((ind=findimage(list,names[i]))!=null)
  { scroll =  { ind:     ind,
                shape:   imgshape(list[ind],sizes,sizeno),
                top:     0 } ;
    items.push(scroll) ;
  }
  div = document.createElement('div') ;
  div.setAttribute('style','font-family:helvetica') ; 

  if(items.length>0)  
  { minw = items[0].shape[0] ;
    if(items.length>1) minw += items[items.length-1].shape[0] ;
    for(i=0;i<2&&i<items.length;i++) image[i] = new scrolltype(i) ; 

    for(maxh=i=0;i<items.length;i++)
    { if(items[i].shape[1]>maxh) maxh = items[i].shape[1] ;
      if(i)
      { sum = items[i].shape[0] + items[i-1].shape[0] ;
        if(sum<minw) minw = sum ;
      }
    }
    for(i=0;i<items.length;i++) 
      items[i].top = Math.floor(0.5+(maxh-items[i].shape[1])/2) ;

    scroll = document.createElement('div') ; 
    if(items.length==1) minw -= 4 ; 
    scroll.setAttribute('style','position:absolute;width:'+(minw+4)+'px;'+
                                'height:'+maxh+'px;overflow:hidden') ; 
    for(sum=i=0;i<2&&i<items.length;sum+=items[i].shape[0]+4,i++)
    { image[i].addimage(sum) ; scroll.appendChild(image[i].img) ; }
    div.appendChild(scroll) ;

    p = document.createElement('div') ; 
    p.setAttribute('style','width:'+(minw+4)+'px;'+'height:'+(maxh+4)+'px') ;
    div.appendChild(p) ;
  }

  if(props.title!=null) div.appendChild(textdiv('Title',props.title)) ; 
  if(props.stars!=null) div.appendChild(starsline(props.stars,0)) ;
  if(props.desc !=null) 
  { d = textdiv('Description',props.desc,50) ;
    if(items.length>0&&props.desc.length>=50) 
    { if(minw+4<400) dwid = 400 ; else dwid = minw+4 ;  
      d.setAttribute('style','width:'+dwid+'px') ; 
    }
    div.appendChild(d) ; 
  }
  if(props.stats!=null) div.appendChild(textdiv('Stats',props.stats)) ; 

  if(props.tracklink!=null)
  { nobr = document.createElement('nobr') ;
    a = document.createElement('a') ;
    a.setAttribute('style',
                   'cursor:pointer;color:#0000bd;text-decoration:none') ; 
    a.setAttribute('href',props.tracklink) ; 
    a.setAttribute('target',"_blank") ; 
    a.setAttribute('onclick',"infowindow.close()") ; 
    a.appendChild(document.createTextNode('View track')) ;
    nobr.appendChild(a) ;
    nobr.appendChild(document.createTextNode(' (opens in new tab/window)')) ;
    div.appendChild(nobr) ;
  }
  return { div:div , scroller: items.length<=2?null:setInterval(scroller,30) } ;

  function scrolltype(i)
  { this.img = this.top = this.pos = null ; 
    this.ind = i ; // index into items
    this.wid = items[i].shape[0] ;
    this.addimage = function(pos)
    { var fetch=null,ind=this.ind ; 
      if(ind<items.length-1&&nfetched<=ind)
      { nfetched += 1 ; 
        fetch = function() { genimage(list[items[ind+1].ind],sizes,sizeno) ; }
      }
      this.img = genimage(list[items[ind].ind],sizes,sizeno,fetch) ;
      this.pos = pos ; 
      this.scrollimage() ; 
    }
    this.scrollimage = function()
    { this.img.setAttribute('style',
        'position:absolute;top:'+items[this.ind].top+'px;left:'+this.pos+'px') ;
    }
  }
  function scroller()
  { var i,ind,offset ; 
    for(i=0;i<3;i++) if(image[i]!=null) image[i].pos -= 1 ; 
    if(image[0].pos+image[0].wid<=0)
    { scroll.removeChild(image[0].img) ; 
      for(i=0;i<2;i++) image[i] = image[i+1] ; 
      image[2] = null ; 
    } 
    for(i=0;i<3&&image[i]!=null;i++) image[i].scrollimage() ;
    offset = image[i-1].pos + image[i-1].wid + 4 ;
    if(offset<minw)
    { if(image[i-1].ind==items.length-1) ind = 0 ; 
      else ind = image[i-1].ind + 1 ; 
      image[i] = new scrolltype(ind) ;
      image[i].addimage(offset) ;
      scroll.appendChild(image[i].img) ; 
    }
  }
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function greybtn(uri,name)  { return uri + 'grey' + name + '.png' ; }
function blackbtn(uri,name) { return uri + 'black' + name + '.png' ; }

function buttonimg(gif)
{ var img = document.createElement('img') ;
  img.setAttribute('src',gif) ; 
  img.setAttribute('width','24') ; 
  img.setAttribute('height','24') ; 
  return img ;
}
function buttoncell(gif1,gif2) 
{ var td=document.createElement('td'),nobr=document.createElement('nobr') ;
  td.setAttribute('style','padding-bottom:4px') ; 
  nobr.appendChild(buttonimg(gif1)) ; 
  if(gif2!=null&&gif2!=undefined)
  { nobr.appendChild(document.createTextNode(' ')) ; 
    nobr.appendChild(buttonimg(gif2)) ;
  }
  td.appendChild(nobr) ; 
  return td ;
}
function textcell(p1,p2) 
{ var td=document.createElement('td'),nobr ;
  td.setAttribute('style','padding-bottom:4px') ; 
  nobr = document.createElement('nobr') ;
  nobr.appendChild(document.createTextNode(p1)) ;
  td.appendChild(nobr) ;
  if(p2!=null&&p2!=undefined)
  { td.appendChild(document.createElement('br')) ;
    nobr = document.createElement('nobr')
    nobr.appendChild(document.createTextNode(p2)) ;
    td.appendChild(nobr) ;
  }
  return td ;
}
function appendrow(td,p)
{ var nobr = document.createElement('nobr') ;
  nobr.appendChild(document.createTextNode(p)) ; 
  td.appendChild(nobr) ;
  td.appendChild(document.createElement('br')) ;
}
function genlink(uri,legend,blank)
{ var a = document.createElement('a') ;
  a.setAttribute('style','cursor:pointer;color:#0000bd;text-decoration:none') ; 
  a.setAttribute('href',uri) ; 
  if(blank!=undefined) a.setAttribute('target','_blank') ; 
  a.appendChild(document.createTextNode(legend)) ; 
  return a ;
}
function genspan(legend,bropt,spanstyle)
{ var span = document.createElement(bropt=='hr'?'div':'span') ; 
  span.appendChild(document.createTextNode(legend)) ; 
  if(spanstyle==undefined) spanstyle = '' ; 
  else if(spanstyle!='') spanstyle += ';' ; 
  if(bropt==']br') 
  { span.appendChild(document.createTextNode(']')) ; bropt = 'br' ;} 
  if(bropt=='br') span.appendChild(document.createElement('br')) ; 
  else if(bropt=='hr') spanstyle += 
    'margin-bottom:2px;border-bottom:solid 1px silver;padding-bottom:2px' ;
  if(spanstyle!='') span.setAttribute('style',spanstyle) ; 
  return span ;
}
function genclickable(action,legend,bropt,style)
{ var defstyle = 'cursor:pointer;color:#0000bd' ;
  if(style!=undefined&&style!=null) defstyle += ';' + style ; 
  var span = genspan(legend,bropt,defstyle) ; 
  span.setAttribute('onclick',action) ; 
  return span ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function blurbdiv(uri)
{ var div=document.createElement('div'),img=document.createElement('img'),b,p ;
  img.setAttribute('width',16) ; 
  img.setAttribute('height',16) ; 
  img.setAttribute('src',uri+'bus.gif') ; 
  div.appendChild(img) ;
  b = document.createElement('b') ;
  b.appendChild(document.createTextNode
                ('\u00a0\u00a0Routemaster GPS track editor:')) ;
  div.appendChild(b) ;
  div.appendChild(document.createElement('br')) ;
  p = document.createElement('div') ;
  p.appendChild(document.createTextNode
    ('The main use of routemaster is to load GPS tracks, display them, allow'+
     ' them to be edited in various ways, and to save them back to disc.')) ;
  p.setAttribute('style','padding-top:4px') ;
  div.appendChild(p) ; 
  p = document.createElement('div') ;
  p.appendChild(document.createTextNode
    ('It may also be used to display tracks on a website (see the example '+
     'track in the links below) or to display an index of tracks in an '+
     'area (for which there is also an example). However these uses require '+
     'you to host your own instance of the tool.')) ;
  p.setAttribute('style','text-indent:14px;border-bottom:solid 1px silver;'+
                         'padding-bottom:2px') ;
  div.appendChild(p) ; 
  return div ; 
}
/* -------------------------------------------------------------------------- */

function northernmost(data)
{ var i,maxlat,maxi ; 
  for(i=0;i<data.length;i++) if(i==0||data[i].pos.lat()>maxlat)
  { maxi = i ; maxlat = data[i].pos.lat() ; }
  return data[maxi].pos ;
}
/* -------------------------------------------------------------------------- */

function helpdiv(uri,noblank)
{ var div=document.createElement('div'),d,t,tr,td,a ; 
  if(noblank==undefined) noblank = 0 ; 
  t = document.createElement('table') ;
  t.setAttribute('cellpadding',0) ; 
  t.setAttribute('cellspacing',0) ; 
  t.setAttribute('style','font-size:100%') ; 

  tr = document.createElement('tr') ;
  tr.appendChild(buttoncell(blackbtn(uri,'settings'),blackbtn(uri,'dl'))) ; 
  tr.appendChild(textcell('access to help menu and to various tools and ' + 
                          'functions /','download route as .tcx')) ; 
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ;
  tr.appendChild(buttoncell(blackbtn(uri,'scissors'),blackbtn(uri,'bin'))) ; 
  tr.appendChild(textcell
    ('split the current segment at the selected point /',
     'delete the currrent segment (or use the [shift delete] '+
     'or [shift backspace] key)')) ;
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ;
  tr.appendChild(buttoncell(blackbtn(uri,'pen'))) ; 
  tr.appendChild(textcell
    ('add a labelled coursepoint at the current position (1-10chars)',
     'click on flag to edit; right-click to change symbol; '+
     'delete label to delete')) ;
  t.appendChild(tr) ; 

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

  tr = document.createElement('tr') ;
  td = document.createElement('td') ;
  td.setAttribute('valign','top') ; 
  td.appendChild(document.createTextNode('Keyboard: ')) ; 
  tr.appendChild(td) ; 

  td = document.createElement('td') ;
  appendrow(td,
            '\u2190/\u2192 move the current waypoint forwards or backwards;') ;
  appendrow(td,
       '[shift \u2190]/[shift \u2192] move it to the previous/next segment;') ;
  appendrow(td,'\u2193 centres the map on the current waypoint;') ;
  appendrow(td,'[return] makes the current waypoint draggable;') ;
  appendrow(td,'[tab] inserts a draggable waypoint;') ;
  appendrow(td,'[del], [backspace] = delete waypoint;') ;
  appendrow(td,
       '[shift del], [shift backspace] = delete segment (=bin button).') ;
  tr.appendChild(td) ; 
  t.appendChild(tr) ; 

  tr = document.createElement('tr') ;
  td = document.createElement('td') ;
  td.setAttribute('valign','top') ; 
  td.appendChild(document.createTextNode('Mouse: ')) ; 
  tr.appendChild(td) ; 

  td = document.createElement('td') ;
  appendrow(td,
         '[shift click] extends the current segment to the cursor position.') ;
  tr.appendChild(td) ; 
  t.appendChild(tr) ; 

  d = document.createElement('div') ;
  d.appendChild(t) ; 
  div.appendChild(underline(d)) ; 

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

  t = document.createElement('table') ;
  t.setAttribute('cellpadding',0) ; 
  t.setAttribute('cellspacing',0) ; 
  t.setAttribute('style','font-size:100%') ; 

  tr = document.createElement('tr') ;
  td = document.createElement('td') ;
  appendrow(td,'When viewing a route index there are no controls '+
               "but the 'f' key may be used to enter full screen.") ;
  tr.appendChild(td) ; 
  t.appendChild(tr) ; 
  d = document.createElement('div')
  d.appendChild(t) ; 
  div.appendChild(underline(d)) ; 

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

  d = document.createElement('div')

  if(noblank)
  { a = genlink('http://www.masterlyinactivity.com/routemaster/?track=routes/'+
                'Caibros.tcx','Example track to experiment with') ; 
    d.appendChild(a) ; 
    d.appendChild(document.createElement('br')) ;
    a = genlink('http://www.masterlyinactivity.com/routemaster/?track=routes/'+
                'capeverde.tcx','Example of a route index') ; 
    d.appendChild(a) ; 
    d.appendChild(document.createElement('br')) ;
  }

  a = genlink('http://www.masterlyinactivity.com/software/routemaster.html',
              'Full documentation') ; 
  if(noblank==0) a.setAttribute('target','_blank') ;
  d.appendChild(a) ; 
  if(noblank==0) 
    d.appendChild(document.createTextNode(' (opens in new tab/window)')) ; 

  div.appendChild(d) ; 
  return div ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */
/*            THE MAIN POPUP MENU AND THE FUNCTIONS IT GOVERNS                */
/* -------------------------------------------------------------------------- */

function cogwheelmenu(dragopt,ov)
{ var d = document.createElement('div') ;

  // dragging
  if(dragopt) 
  { d.appendChild(genspan('Hit [return] when you\'ve finished dragging','br')) ;
    return d ;
  }

  // route options
  d.appendChild(genclickable('routeinfo()','Route info','br')) ; 
  d.appendChild(genclickable('addload(1)','Load new route','br')) ; 
  if(ov!=null)
  { d.appendChild(genlink(ov,'View route index','blank')) ;
    d.appendChild(genspan(' (opens in new tab/window)','br')) ; 
  }
  d.appendChild(genclickable('dl(1)','Download track as route index','hr')) ; 

  // segment options
  d.appendChild(genclickable('seginfo()','Segment info','br')) ; 
  d.appendChild(genclickable('revseg()','Reverse segment','br')) ;
  d.appendChild(genclickable('addload(0)','Load route as a new segment','hr')) ;

  // waypoint options
  d.appendChild(genclickable('wpinfo()','Waypoint info','br')) ; 
  if(segments[selected[0]].data.length>1) 
    d.appendChild(genclickable('wpdel()','Delete waypoint','br')) ; 
  else d.appendChild(genspan('Delete waypoint','br','color:silver')) ;
  d.appendChild(genclickable('draggit(0)','Make waypoint draggable','br')) ; 
  d.appendChild
    (genclickable('inswp(1)','Insert draggable waypoint ahead','br')) ; 
  d.appendChild
    (genclickable('inswp(-1)','Insert draggable waypoint behind','hr')) ; 

  // tool options
  if(querycanfullscreen())
  { if(queryfullscreen()==0) d.appendChild
                (genclickable('enterFullscreen()','Enter full screen','br')) ; 
    else d.appendChild
                (genclickable('exitFullscreen()','Leave full screen','br')) ; 
  } 
  d.appendChild(genclickable('help()','Help','br')) ; 
  return d ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function walktodiv(datum) 
{ var s,d=document.createElement('div'),dd,ind,k,slashind,imgname ; 

  if(datum.type!=null)
  { dd = document.createElement('div') ;
    s = datum.type + ': ' + datum.marker.title + ' [' ;
    dd.appendChild(document.createTextNode(s)) ;
    dd.appendChild(genclickable('labelprompt()','Edit',']br')) ;
    if(datum.photo.length>0) underline(dd) ; 
    d.appendChild(dd) ; 
  }

  for(ind=0;ind<datum.photo.length;ind++)
  { dd = document.createElement('div') ;
    if(imginfo.status!='ready') k = -1 ; 
    else k=findimg(datum.photo[ind])
    if(k>=0) 
    { s = genimage(imginfo.list[k],imginfo.sizes,imginfo.thumbind) ;
      dd.appendChild(s) ; 
      dd.appendChild(document.createElement('br')) ;
    }
    else 
    { s = 'Photo: ' + datum.photo[ind] + ' (' ;
      if(imginfo.status=='null') s += 'no list provided) ' ;
      else if(imginfo.status=='ready') 
      { imgname = imginfo.uri ;
        slashind = imgname.lastIndexOf('/') ;
        if(slashind>=0) imgname = imgname.substring(slashind+1) ;
        s += 'not present in ' + imgname + ') ' ; 
      }
      else if(imginfo.status=='waiting') s += imgname + ' is not available) ' ;
      else s += 'imginfo.status = ' + imginfo.status + ') ' ; 
      dd.appendChild(document.createTextNode(s)) ;
    }
    dd.appendChild(document.createTextNode('[')) ;
    dd.appendChild(genclickable('photoedit('+ind+')','Edit')) ;
    dd.appendChild(document.createTextNode(']')) ;
    if(k>=0)
    { dd.appendChild(document.createTextNode(' : [')) ;
      dd.appendChild(genclickable('phinfo('+k+')','Info')) ;
      dd.appendChild(document.createTextNode('] : [')) ;
      dd.appendChild(genclickable('display('+ind+')','Enlarge')) ;
      dd.appendChild(document.createTextNode(']')) ;
    }
    d.appendChild(underline(dd)) ; 
  }
  if(datum.photo.length>0) 
  { dd = document.createElement('div') ;
    dd.appendChild(document.createTextNode('[')) ;
    dd.appendChild(genclickable('photoprompt(null)','Add photo',']br')) ;
    d.appendChild(dd) ; 
  }
  return d ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */
/*           WPINFO IS A MENU GIVING ACCESS TO THE SETALT FUNCTION            */
/* -------------------------------------------------------------------------- */

function wpinfodiv() 
{ var s0=selected[0],s1=selected[1],alt,nalt,s,x,lat,lng,grad,gradstr,time ;
  var datum = segments[s0].data[s1] , pos = datum.pos ;
  var s , d = document.createElement('div') ;
  var div = document.createElement('div') ; 
  underline(d) ;

  lat = pos.lat() ; 
  lng = pos.lng() ; 
  if(lat>=0) s = lat.toFixed(5) + '\u00b0 N, ' ; 
  else { lat = -lat ; s = lat.toFixed(5) + '\u00b0 S, ' ; }
  if(lng>=0) s += lng.toFixed(5) + '\u00b0 E' ; 
  else { lng = -lng ; s += lng.toFixed(5) + '\u00b0 W' ; }
  d.appendChild(genspan(s,'br')) ;
    
  x = new LatLon(lat,lng) ; 
  if(lat>49.9&&lat<62&&lng>-12&&lng<2.5&&lat-1.5*lng<75) 
    s = 'OS grid ref: ' + OsGridRef.latLonToOsGrid(x) ;
  else s = 'UTM coords = ' + x.toUtm() ; 
  d.appendChild(genspan(s,'br')) ;

  alt = segments[s0].data[s1].h ;
  if(alt!=null) 
  { d.appendChild(genspan('Altitude: ' + alt.toFixed(0) + 'm [')) ;
    d.appendChild(genclickable('setalt(1)','Edit',']br')) ;
  }
  else d.appendChild(genclickable('setalt(0)','Set altitude','br')) ;

  time = segments[s0].data[s1].t ;
  if(time!=null&&time.getFullYear()>1980) 
  { d.appendChild(genspan('Date: '+time.toDateString(),'br')) ;
    d.appendChild(genspan('Time: '+time.toTimeString(),'br')) ;
  }

  if(datum.type!=null) 
  { d.appendChild(genspan(datum.type + ': ' + datum.marker.title + ' [')) ;
    d.appendChild(genclickable('labelprompt()','Edit',']br')) ;
  }

  if(alt==null||s1==segments[s0].data.length-1) nalt = null ; 
  else 
  { nalt = segments[s0].data[s1+1].h ; 
    if(nalt!=null) x = dist(pos,segments[s0].data[s1+1].pos) ; 
  }
  if(nalt!=null) 
  { grad = 100*Math.atan2(nalt-alt,x) ; 
    gradstr = Math.abs(grad).toFixed(0) ; 
    if(gradstr=='0') d.appendChild(genspan('Flat','br')) ;
    else if(grad>0) d.appendChild(genspan('Climb '+gradstr+'%','br')) ;
    else d.appendChild(genspan('Descend '+gradstr+'%','br')) ;
  }

  div.appendChild(d) ;
  if(segments.length>1) s = 'Segment '+s0+' p' ; else s = 'P' ;
  s += 'oint ' + s1 ; 
  div.appendChild(genspan(s,'br','font-size:80%')) ;

  if((s1==0&&s0>0)||(s1==segments[s0].data.length-1&&s0<segments.length-1))
    if(segments[s0].data.length>1)
      div.appendChild(genclickable('xferwp()','Transfer waypoint to segment '+
        (s1==0?s0-1:s0+1),'br','font-size:80%')) ;
  return div ; 
}
/* -------------------------------------------------------------------------- */

function titlediv(a,b,lim)
{ var bold,d=document.createElement('div') ; 
  if(lim==null||lim==undefined||b.length<=lim) 
  { d.setAttribute('style','white-space:nowrap') ;
    d.appendChild(document.createTextNode(a+': ')) ;
    bold = document.createElement('b') ;
    bold.appendChild(document.createTextNode(b)) ;
    d.appendChild(bold) ;
    if(a!='Source') d.appendChild(document.createTextNode(' [')) ;
  }
  else if(a=='Source') d.appendChild(document.createTextNode(a+': '+b)) ;
  else d.appendChild(document.createTextNode(a+': '+b+' [')) ;

  if(a!='Source')
    d.appendChild(genclickable('retitle("'+a.toLowerCase()+'")','Edit',']br')) ;
  return d ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function routediv(rp,ov) 
{ var s0,s1,s,asc,des,oalt,alt,nlabels,nowpts,i,unsaved,props,spacing,npix ; 
  var maxsep,sep,outoforder,tlast,otime,time,ttime,ntimes,s,tdiv,ldiv ;
  var nnull , loadno , pr , dd , d = document.createElement('div') ; 

  for(loadno=nactions-1;loadno>=0&&actions[loadno][0]!='load';loadno--) ; 
  if(loadno>=0) pr = actions[loadno][3] ; else pr = null ; 
  tlast = null ;
  outoforder = dd = nnull = 0 ;
  maxsep = nlabels = npix = des = asc = nowpts = ntimes = 0 ;

  // calculate route properties 

  for(s0=0;s0<segments.length;s0++) 
  { nowpts += segments[s0].data.length ;

    for(oalt=null,s1=0;s1<segments[s0].data.length;otime=time,s1++)
    { if((alt=segments[s0].data[s1].h)==null) nnull += 1 ; 
      else
      { if(oalt!=null) 
        { if(alt>oalt) asc += alt-oalt ; else des += oalt - alt ; } 
        oalt = alt ;
      }
      if(segments[s0].data[s1].type!=null) nlabels += 1 ;
      npix += segments[s0].data[s1].photo.length ;

      time = segments[s0].data[s1].t ;
      if(time!=null) { time = time.getTime() ; ntimes += 1 ; }
      if(tlast!=null&&time!=null&&time<tlast) outoforder = 1 ; // out of order
      if(time!=null) tlast = time ;

      if(s1) 
      { sep = dist(segments[s0].data[s1-1].pos,segments[s0].data[s1].pos) ;
        dd += sep ; 
        if(sep>maxsep) maxsep = sep ; 
      }
    }
  }

  // title
  d.appendChild(titlediv('Title',rp.title,50)) ;

  // stars
  starsdiv = document.createElement('div') ; 
  starsdiv.setAttribute('style','color:#0000bd') ; 
  d.appendChild(starsline(rp.stars,1)) ;

  if(rp.sourceid!=null&&rp.sourceid!=rp.title)
    d.appendChild(titlediv('Source',rp.sourceid)) ;

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

  // description
  if(rp.desc==null) 
  { tdiv = document.createElement('div') ; 
    tdiv.appendChild(document.createTextNode('[')) ;
    s = genclickable('retitle("description")','Add description',']br') ;
    tdiv.appendChild(s) ;
  }
  else tdiv = titlediv('Description',rp.desc,50) ;
  d.appendChild(underline(tdiv)) ;

  // last added route
  if(loadno>0)
  { s = '\u00a0\u00a0\u00a0Last added route' ;
    if(pr.title==null) s += ':' ; else s += ' (' + pr.title + '):' ;
    d.appendChild(genspan(s,'br')) ;
    s = '\u00a0\u00a0\u00a0' ;
  }
  else s = '' ;

  // number of track points and optimisation
  s += 'Track points on input: ' + pr.inputlen ; 
  if(pr.optim.already)  
    d.appendChild(genspan(s+' (previously optimised)','br')) ;
  else if(pr.optim.ndel==0) 
  { d.appendChild(genspan(s+' [')) ;
    if(nactions==loadno+1) 
      d.appendChild(genclickable('optimprompt()','Optimise')) ;
    else d.appendChild(genspan('Optimise',null,'color:silver')) ; 
    d.appendChild(genspan(']','br')) ;
  }
  else d.appendChild
         (genspan(s+', optimised to '+(pr.inputlen-pr.optim.ndel),'br')) ;
  if(!pr.optim.already&&pr.inputlen-pr.optim.ndel!=nowpts) 
    d.appendChild(genspan('Now ' + nowpts + ' track points','br')) ;

  // are there any missing altitudes?
  if(nnull) 
  { d.appendChild(genspan(nnull+' points have no associated altitudes [')) ;
    if(busyalts) d.appendChild(genspan('Altitude service busy',']br',
                                       'text-style:italic')) ;
    else d.appendChild(genclickable('doalts()','Find altitudes',']br')) ;
  }

  // are points timed and are they in sequence?
  if(outoforder==0) 
  { if(ntimes==0) d.appendChild(genspan('No timings provided','br')) ;
    else 
    { if(ntimes>=nowpts) s = '[' ;
      else s = (nowpts-ntimes) + ' points have no associated timings [' ;
      d.appendChild(genspan(s)) ;
      d.appendChild(genclickable('deltimes()','Discard timings',']br')) ;
    }
  }
  else d.appendChild(genspan('Times are out of sequence (will be '+
                             'discarded on download)','br')) ;

 // labels and photos
  if(nlabels>0) d.appendChild
    (genspan(nlabels+' labelled course point'+(nlabels>1?'s':''),'br')) ; 
  if(npix>0) d.appendChild(genspan(npix+' photo'+(npix>1?'s':''),'br')) ; 

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

  // unsaved changes
  unsaved = unsavedchanges.length ; 
  if(unsaved>0) d.appendChild
    (genspan(unsaved+' unsaved change'+(unsaved>1?'s':''),'br')) ;

  // number of segments - option to combine
  if(segments.length>1) 
  { d.appendChild(genspan(segments.length + ' segments [')) ;
    d.appendChild(genclickable('combine()','Combine',']br')) ;
    d.appendChild(genspan('Note that segments must be combined before saving',
                          'br','font-style:italic')) ;
  }
  
  // max waypoint separation - option to interpolate
  d.appendChild
    (genspan('Max waypoint separation: '+maxsep.toFixed(0)+'m','br')) ;
  if(maxsep>=100) 
  { d.appendChild(genspan('Note that separations \u003e100m are illegal '+
                          'on Garmin','br','font-style:italic')) ;
    d.appendChild(genspan('\u00a0\u00a0\u00a0[')) ;
    d.appendChild(genclickable('extrapts()','Interpolate extra points',']br')) ;
  }

  // distance / ascent / descent / average speed
  tdiv = document.createElement('div') ; 
  tdiv.setAttribute('style',
    'margin-top:2px;border-top:solid 1px silver;padding-top:2px') ; 

  function ldivadd(b)
  { ldiv.appendChild(document.createTextNode(b)) ;
    ldiv.appendChild(document.createElement('br')) ;
  } ;

  ldiv = document.createElement('div') ; 
  ldiv.setAttribute('style','float:left;padding-right:8px') ; 
  ldivadd('Total distance:') ;
  ldivadd('Total ascent:') ;
  ldivadd('Total descent:') ;
  tdiv.appendChild(ldiv) ; 

  ldiv = document.createElement('div') ; 
  ldiv.setAttribute('style','float:left;text-align:right') ; 
  ldivadd((dd/1000).toFixed(3)) ;
  ldivadd(asc.toFixed(0)) ;
  ldivadd(des.toFixed(0)) ;
  tdiv.appendChild(ldiv) ; 

  ldiv = document.createElement('div') ; 
  ldiv.setAttribute('style','float:left;padding-left:2px') ; 
  ldivadd('km') ;
  ldivadd('m') ;
  ldivadd('m') ;
  tdiv.appendChild(ldiv) ; 

  d.appendChild(tdiv) ; 
  return d ; 
}
function doalts() { infowindow.close() ; getalts(segments,1,reprofile) ; }

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

function starsline(nstars,editable)
{ var i,c,s,d ;
  if(editable) 
  { if(starsdiv==null) return ; else d = starsdiv ; 
    while(d.childNodes.length>0) 
      d.removeChild(d.childNodes[d.childNodes.length-1]) ;
  }
  else d = document.createElement('div') ; 
 
  for(i=1;i<=5;i++) 
  { s = document.createElement('span') ;
    if(nstars==null||i>nstars) c = '\u2606' ; else c = '\u2605' ;
    s.appendChild(document.createTextNode(c)) ;
    if(editable&&i!=nstars) 
    { s.setAttribute('style','cursor:pointer') ; 
      function createfunc(i) { return function() { restars(nstars,i) ; } } ;
      s.onclick = createfunc(i) ; 
    }
    d.appendChild(s) ; 
  }
  if(editable&&nstars!=null) 
  { d.appendChild(genspan(' [')) ; 
    d.appendChild(genclickable('restars('+nstars+',null)','Clear')) ; 
    d.appendChild(genspan(']')) ; 
  }
  return d ; 
}
/* --------------------------------- photo info ----------------------------- */

function phdiv(i) 
{ var hind,ind , list=imginfo.list , d = pixinfodiv(list,i,imginfo.sizes) ;

  if(imginfo.pixpage!=null)
  { d.appendChild(genlink(imginfo.pixpage,'Full photo set',1)) ;
    d.appendChild(genspan(' (opens in new tab/window)','br')) ;
  }

  if(list[i].retid!=null)
  { for(hind=null,ind=0;ind<=i;ind++) 
      if(list[ind].retpage!=undefined&&list[ind].retpage!=null) 
        hind = list[ind].retpage + '.html#' + list[i].retid ; 
    if(hind!=null) 
    { d.appendChild(genlink(hind,'Route notes',1)) ;
      d.appendChild(genspan(' (opens in new tab/window)','br')) ;
    }
  }
  return d ; 
}
/* -------------------------------- actionname ------------------------------ */

function actionname(x)
{ var i,s ; 
  if(x[0]=='bin') return 'delete segment' ; 
  if(x[0]=='snip') return 'split segment' ; 
  if(x[0]=='xferwp') return 'transfer waypoint' ; 
  if(x[0]=='editlabel') 
  { if(x[4]=='') return 'delete label' ; 
    else if(x[3]=='') return 'label waypoint' ;
    else return 'edit label' ; 
  }
  if(x[0]=='edittitle') return 'edit title' ; 
  if(x[0]=='editdescription') return 'edit description' ; 
  if(x[0]=='wpdel') return 'delete waypoint' ; 
  if(x[0]=='move') 
  { if(x[5]) return 'insert waypoint' ; else return 'drag waypoint' ; }
  if(x[0]=='recal') return 'recalibrate altitudes' ; 
  if(x[0]=='googlecal') return 'ask Google to recalibrate altitudes' ; 
  if(x[0]=='setalt') return 'set waypoint altitude' ; 
  if(x[0]=='resign') return 'change label symbol' ; 
  if(x[0]=='combine') return 'combine '+x[1]+' segments' ; 
  if(x[0]=='revseg') return 'reverse segment' ; 
  if(x[0]=='optimise') return 'optimisation' ; 
  if(x[0]=='deltimes') return 'delete times' ; 
  if(x[0]=='editphoto') 
  { if(x[5]==null) return 'delete photo' ; 
    else if(x[4]==null) return 'add photo' ;
    else return 'change photo' ; 
  }
  if(x[0]=='extra') return 'interpolate extra points' ; 
  if(x[0]=='stars'&&x[2]==null)
  { s = 'clear ' ; for(i=0;i<x[1];i++) s += '\u2605' ; return s ; } 
  if(x[0]=='stars')
  { s = 'set ' ; for(i=0;i<x[2];i++) s += '\u2605' ; return s ; } 
  if(x[0]=='swapseg') return 'swap segments' ; 
  alert('Unrecognised action: '+x[0]) ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */
/*          FUNCTIONS FOR COMPUTING & DISPLAYING THE ALTITUDE PROFILE         */
/* -------------------------------------------------------------------------- */

function profiletype(map)
{ this.curhandle = this.m = null ; 
  this.map = map ; 
  this.sel = [0,0] ;
  this.ratio = window.devicePixelRatio ; 
  if(this.ratio==null||this.ratio==undefined) this.ratio = 1 ; 
  this.innerw = Math.floor(600*this.ratio) ; 
  this.innerh = Math.floor(180*this.ratio) ; 
  this.outerw = Math.floor(620*this.ratio) ; 
  this.outerh = Math.floor(200*this.ratio) ; 
  this.offs   = Math.floor((this.outerw-this.innerw)/2) ;
  this.radius = Math.floor(20*this.ratio) ; 
  this.active = 0 ; 
  this.prodiv = document.createElement('div') ;
  this.curdiv = document.createElement('div') ;
  this.curhandle = this.curdiv.addEventListener("click",toggleprofile) ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function drawxcur(pro,sel)
{ var pos , i , linewid , inc , cx, cy , jlim , div , r=pro.radius ; 
  pos = pro.m.wp2pro[sel[0]][sel[1]] ;
  pro.sel = sel ; 
  for(div=pro.curdiv;div.childNodes.length>0;) 
    div.removeChild(div.childNodes[div.childNodes.length-1]) ;
  var c=document.createElement('canvas') , ctx=c.getContext("2d") ; 
  if(pro.active)
  { c.setAttribute('width',620) ; c.setAttribute('height',200) ; }
  else { c.setAttribute('width',42) ; c.setAttribute('height',42) ; }
  div.appendChild(c) ;
  ctx.scale(1/pro.ratio,1/pro.ratio) ;
  linewid = 2*Math.floor(pro.ratio+0.5) ; 

  // the cursor
  ctx.beginPath() ; 
  ctx.lineWidth = 1 ; 
  if(pro.active)
  { ctx.moveTo(pro.offs+pos+0.5,pro.offs) ; 
    ctx.lineTo(pro.offs+pos+0.5,pro.offs+pro.innerh) ; 
  }
  else 
  { jlim = Math.floor(Math.sqrt(r*r-(r-pos)*(r-pos))) ;
    ctx.moveTo(pos+linewid/2+0.5,linewid/2+r-jlim) ; 
    ctx.lineTo(pos+linewid/2+0.5,linewid/2+r+jlim) ; 
  }
  ctx.stroke() ; 

  // the circle of the 'x': first the filling
  ctx.beginPath() ; 
  cy = r + linewid/2 ;
  if(pro.active) 
  { cx = pro.outerw - r - linewid/2 ; 
    ctx.fillStyle = 'white' ; 
    ctx.arc(cx,cy,r,0,2*Math.PI,false) ; 
    ctx.fill() ;
  }
  else cx = r + linewid/2 ; 

  // then the outline (drawn separately so that the filling shouldn't overlap)
  ctx.beginPath() ; 
  ctx.strokeStyle = '#555' ; 
  ctx.lineWidth = linewid ; 
  ctx.arc(cx,cy,r,0,2*Math.PI,false) ;
  ctx.stroke() ; 
  if(pro.active==0) return ;

  // the two bars of the 'x'
  inc = r * Math.sqrt(2) / 2 ;
  ctx.beginPath() ; 
  ctx.moveTo(cx+inc,cy-inc) ; 
  ctx.lineTo(cx-inc,cy+inc) ; 
  ctx.stroke() ; 

  ctx.beginPath() ; 
  ctx.moveTo(cx+inc,cy+inc) ; 
  ctx.lineTo(cx-inc,cy-inc) ; 
  ctx.stroke() ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

var pcopy,scopy ;

function drawpro(pro,segments,sel)
{ var ctx,i,j,k,h,ind,step,imgdata,parity,d,cdash,ctxdash,linewid,c,maxi,maxj ;
  var jlim,r=pro.radius,div,divstyle,hind ;

  pcopy = pro ; 
  scopy = segments ; 
  for(div=pro.prodiv;div.childNodes.length>0;) 
    div.removeChild(div.childNodes[div.childNodes.length-1]) ;
  c = document.createElement('canvas') ;
  div.appendChild(c) ;

  if(pro.active)
  { divstyle = 'position:absolute;height:200px;right:0;top:0;width:620px' ; 
    c.setAttribute('width',620) ; 
    c.setAttribute('height',200) ; 
    maxi = pro.innerw ;
    maxj = pro.innerh ;
  }
  else
  { divstyle = 'position:absolute;height:42px;right:0;top:0;width:42px' ;
    c.setAttribute('width',42) ; 
    c.setAttribute('height',42) ; 
    maxj = maxi = 2 * r ; 
  }

  div.setAttribute('style',divstyle) ; 
  ctx = c.getContext("2d") ; 
  ctx.scale(1/pro.ratio,1/pro.ratio) ;

  // pale grey background
  if(pro.active)
  { ctx.globalAlpha = 0.6 ; 
    ctx.fillStyle = 'lightgray' ;
    ctx.rect(0,0,pro.outerw,pro.outerh) ;
    ctx.lineWidth = 0 ; 
    ctx.fill() ; 
    ctx.font = Math.floor(0.5+10*pro.ratio)+"px Helvetica" ;
  }

  // draw a profile 
  pro.m = new profilemaptype(segments,maxi) ; 
  cdash = document.createElement('canvas') ;
  cdash.setAttribute('width',maxi) ; 
  cdash.setAttribute('height',maxj) ; 
  ctxdash = cdash.getContext("2d") ; 
  imgdata = ctxdash.createImageData(maxi,maxj) ;
  d = imgdata.data ; 

  for(i=0;i<maxi;i++) 
  { h = pro.m.h[i] ; 
    if(pro.active&&h==null) continue ;
    else if(h!=null) h = pro.m.hmin + 0.9*(pro.m.h[i]-pro.m.hmin) ; 
    if(h!=null) { h = maxj*(pro.m.hmax-h)/pro.m.hspan ; hind = Math.floor(h) ; }
    parity = pro.m.pro2wp[i][0] & 1 ;
    if(pro.active) for(j=hind;j<pro.innerh;j++)
    { ind = 4 * (i+maxi*j) ; 
      d[ind] = 255 ; 
      if(parity) d[ind+2] = d[ind+1] = 150 ; 
      if(j==hind) d[ind+3] = Math.floor(0.5+255*(hind+1-h)) ; // antialiasing
      else d[ind+3] = 255 ; 
    }
    else 
    { jlim = Math.floor(0.5+Math.sqrt(r*r-(i-r)*(i-r))) ;
      if(h==null) hind = r + jlim ; 
      j = r - jlim ; 
      if(j!=Math.floor(j)) j += 1 ; 
      for(;j<hind&&j<r+jlim;j++)
      { ind = 4 * (i+maxi*j) ; 
        d[ind+3] = d[ind+2] = d[ind+1] = d[ind] = 255 ; 
      }
      for(;j<r+jlim;j++)
      { ind = 4 * (i+maxi*j) ; 
        d[ind+3] = d[ind] = 255 ; 
        if(parity) k = 150 ; else k = 0 ; 
        if(j==hind) k = Math.floor(0.5+(h-hind)*255+(hind+1-h)*k) ;
        d[ind+2] = d[ind+1] = k ; 
      }
    }
  }
  ctxdash.putImageData(imgdata,0,0) ; 
  imgdata = null ; 
  if(pro.active==0) ctx.drawImage(cdash,1,1) ; 
  else ctx.drawImage(cdash,pro.offs,pro.offs) ;

  // lines
  if(pro.active) 
  { if(pro.m.hspan>2500) step = 1000 ; 
    else if(pro.m.hspan>1250) step = 500 ;
    else step = 100 ; 

    linewid = Math.floor(0.5+pro.ratio) ; 
    for(i=step*Math.floor(pro.m.hmin/step+1);i<pro.m.hmax;i+=step) 
    { y = pro.offs + pro.innerh*(pro.m.hmax-i)/pro.m.hspan ; 
      if(linewid&1) y = 0.5 + Math.floor(y) ; // nearest half-integer
      else y = Math.floor(0.5+y) ; 
      ctx.beginPath() ; 
      ctx.lineWidth = linewid ; 
      ctx.strokeStyle = '#555' ; 
      ctx.moveTo(pro.offs,y) ;
      ctx.lineTo(pro.innerw+pro.offs,y) ; 
      ctx.stroke() ; 
      ctx.strokeText(i,pro.innerw-pro.offs,y-2*linewid) ;
    }
  }

  // cursor
  pro.curdiv.setAttribute('style',divstyle) ; 
  drawxcur(pro,sel) ; 
}
function toggleprofile(e)
{ var flag , sel , pos = window.innerWidth - e.clientX ; 
  var pro=pcopy,segments=scopy ; 
  // pos is relative to rh edge of screen
  if((pos-20)*(pos-20)+(e.clientY-20)*(e.clientY-20)<400)
  { pro.active = 1 - pro.active ; drawpro(pro,segments,pro.sel) ; return ; } 
  else if(pro.active==0) 
  { if(mouseopt) 
    { e.latLng = point2LatLng(e.clientX,e.clientY,pro.map) ; selpoint(e) ; }
    return ; 
  }

  flag = infowindow.close() ;
  if(flag=='highlight') flag = 3 ; 
  else if(flag=='seginfo') flag = 2 ; 
  else if(flag=='wpinfo') flag = 1 ; 
  else flag = 0 ; 
  pos = Math.floor(0.5+(610-pos)*pro.ratio) ;
  if(pos>=pro.m.pro2wp.length) pos = pro.m.pro2wp.length-1 ; 
  else if(pos<0) pos = 0 ; 
  sel = pro.m.pro2wp[pos] ;
  walkto(sel[0],sel[1],flag) ;
} 
function point2LatLng(x,y,map) // by Egil (stackoverflow)
{ var topRight = map.getProjection().
                         fromLatLngToPoint(map.getBounds().getNorthEast());
  var bottomLeft = map.getProjection().
                         fromLatLngToPoint(map.getBounds().getSouthWest());
  var scale = Math.pow(2, map.getZoom());
  var pt = new google.maps.Point(x/scale + bottomLeft.x, y/scale + topRight.y);
  return map.getProjection().fromPointToLatLng(pt);
}
/* -------------------------------------------------------------------------- */

• xmlfloat     • isvaliddate     • isvalidnum     • datatype     • function     • addlabel     • propstype     • readtcx     • readgpx     • readfit     • readfitvalue     • readfitangle     • dist     • angle     • optimise     • writetcx     • addpos     • addalt     • adddist     • writeoverview     • gencolours     • reluri     • ascify     • flatten     • busyalts     • getalts     • profilemaptype

// www.masterlyinactivity.com/software/routemaster.html
//
/* The MIT License

   Copyright (c) 2016/2017, by Colin Champion
   <colin.champion@masterlyinactivity.com>

   Permission is hereby granted, free of charge, to any person obtaining
   a copy of this software and associated documentation files (the
   "Software"), to deal in the Software without restriction, including
   without limitation the rights to use, copy, modify, merge, publish,
   distribute, sublicense, and/or sell copies of the Software, and to
   permit persons to whom the Software is furnished to do so, subject to
   the following conditions:

   The above copyright notice and this permission notice shall be
   included in all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   SOFTWARE.
*/
var icons = 
{ // coursepoint icons
  flagsign:
  { path: "M 0.5 20.5  L 0.5 0.5  12.5 6  0.5 11.5  ",
    fillColor: '#FCDFFF',
    fillOpacity: 0.7,
    strokeColor: 'purple',
    strokeWeight: 1.5,
    anchor: new google.maps.Point(0.5,20.5),
  } ,
  turnleft:
  { path: "M 18.5 20.5  L 16.5 11.5  A 2 2 0 0 0 14.5 9.5  "+
          "L 11.5 10  11.5 13.5  "+
          "6.5 7.5  11.5 1.5  11.5 5  16.5 5.5  A 3.5 3.5 0 0 1 20 9   z",
    fillColor: '#FCDFFF',
    fillOpacity: 0.7,
    strokeColor: 'purple',
    strokeWeight: 1.5,
    anchor: new google.maps.Point(18.5,20.5),
  } ,
  straighton:
  { path: "M 7.5 20.5  L 4.5 6.5  0.5 6.5  7.5 0.5  14.5 6.5  10 6.5  z",
    fillColor: '#FCDFFF',
    fillOpacity: 0.7,
    strokeColor: 'purple',
    strokeWeight: 1.5,
    anchor: new google.maps.Point(7.5,20.5),
  } ,
  turnright:
  { path: "M 3.5 20.5  L 5.5 11.5  A 2 2 0 0 1 7.5 9.5  L 10.5 10  10.5 13.5  "+
          "15.5 7.5  10.5 1.5  10.5 5  5.5 5.5  A 3.5 3.5 0 0 0 2 9   z",
    fillColor: '#FCDFFF',
    fillOpacity: 0.7,
    strokeColor: 'purple',
    strokeWeight: 1.5,
    anchor: new google.maps.Point(3.5,20.5),
  } ,
  shriek:
  { path: "M 8.5 21.5 A 2.5 2.5 0 0 1 8.5 16.5  A 2.5 2.5 0 1 1 8.5 21.5  "+
          "M 8.5 14.5   4.5 5.5  A 4.5 4.5 0 1 1 12.5 5.5 L 8.5 14.5" ,
    fillColor: '#FCDFFF',
    fillOpacity: 0.7,
    strokeColor: 'purple',
    strokeWeight: 1.5,
    anchor: new google.maps.Point(8.5,21.5),
  } ,
  fork:
  { path: "M 0.5 0.5  L 0.5 5.5  2.5 5.5  2.5 0.5  2.5 5.5  4.5 5.5  4.5 0.5" +
          "  4.5 5.5  6.5 5.5   6.5 0.5   6.5 7.5  " +
          "A 2.5 2.5 0 0 1 4.25 9.95   L 5 19.5  "+
          "A 1.5 1.5 0 0 1  2 19.5   L 2.75 9.95 " + 
          "A 2.5 2.5 0 0 1 0.5 7.5   z",
    fillColor: '#FCDFFF',
    fillOpacity: 0.7,
    strokeColor: 'purple',
    strokeWeight: 1.5,
    anchor: new google.maps.Point(3.5,22),
  } ,
  // icon for arrow representing current waypoint
  arrow:
  { path: "M 6 9  0 15  6 0  12 15 z",
    fillColor: 'black',
    fillOpacity: 1,
    strokeColor: 'black',
    strokeWeight: 0,
    anchor: new google.maps.Point(6,6),
    rotation: 0,
    clickable: false 
  } ,
  // icon for concentric circles representing draggable waypoint
  concircle:
  { path: "M 6 0  A 6 6 0 1 0 6 12  A 6 6 0 1 0 6 0 M 6 3  " +
          "A 3 3 0 1 0 6  9   A 3 3 0 1 0 6  3",
    fillColor: 'black',
    fillOpacity: 0,
    strokeColor: 'black',
    strokeWeight: 1,
    strokeOpacity: 1,
    anchor: new google.maps.Point(6,6),
    clickable: false 
  } ,
  // camera icon
  camera:
  { path: "M 0.5 4   A 1.5 1.5 0 0 1 2 2.5   L  5.5 2.5   7 0.5  11 0.5   " + 
          "12.5 2.5   14 2.5   A 1.5 1.5 0 0 1  16 3   L 20 7   16 11 " +
          "A 1.5 1.5 0 0 1 15 11.5   L 2 11.5   A 1.5 1.5 0 0 1 0.5 10  z " + 
          "M 9 4  A 3 3 0 0 1 9 10   A 3 3 0 0 1 9 4 " ,  
    fillColor: '#FCDFFF',
    fillOpacity: 0.7,
    strokeColor: 'purple',
    strokeWeight: 1.5,
    anchor: new google.maps.Point(21,7),
    clickable: false 
  } 
} ;
function xmlfloat(x) { return parseFloat(x.childNodes[0].textContent) ; }

function isvaliddate(d) 
{ if(Object.prototype.toString.call(d)!=="[object Date]") return false ;
  else return !isNaN(d.getTime()) ;
}
function isvalidnum(x) { return !isNaN(parseFloat(x)) && isFinite(x) ; }

/* ------------------------------- data structure --------------------------- */

// I found the following logic quite hard to get right. A (non-null) label
// satisfies the following constraints:
// o. the marker is non-null
// o. the map may be null, and if it is null the title may also be null and the
//    icon may be arbitrary
// o. if the type is null, the map is null
// o. the map is null if and only if the clickhandler is inactive
// the same constraints apply (mutatis mutandis) to the photo, so it follows 
// that the label may have a null map and the photo non-null (and vice versa)
//    we therefore conclude that a label must be in one of 3 states:
// o. type null, map null, handlers inactive, but marker non-null
// o. type non-null, map null, handlers inactive, marker non-null
// o. type non-null, map non-null, handlers active, marker non-null
// the state in which type is non-null and map is null is applied to all 
// labels in a segment being deleted (we preserve the information in the 
// action list but don't want the label to be displayed)

function datatype(pos,h,t)
{ this.pos = pos ; 
  this.h = h ; 
  this.marker = this.photomarker = this.type = this.t = null ;
  if(t!=undefined&&t!=null&&isvaliddate(t)&&t.getTime()>365*24*3600000) 
    this.t = t ; 
  this.photo = [] ;
  this.caption = '' ;  
  this.clickhandler = this.righthandler = this.photohandler = null ; 
}
// member functions
datatype.prototype.geticon = function()
{ if(this.type=='Left')  return icons.turnleft ; 
  else if(this.type=='Straight') return icons.straighton ; 
  else if(this.type=='Right') return icons.turnright ; 
  else if(this.type=='Danger') return icons.shriek ; 
  else if(this.type=='Food') return icons.fork ; 
  else return icons.flagsign ; 
} ;
datatype.prototype.setlabelmap = function(m,seller,cyclist) 
{ if(this.type==null) m = null ; 
  if(m==null&&this.marker==null) return ;
  this.marker.setMap(m) ; 
  if(m==null&&this.clickhandler!=null)
  { google.maps.event.removeListener(this.clickhandler) ;
    google.maps.event.removeListener(this.righthandler) ;
    this.clickhandler = this.righthandler = null ; 
  }
  if(m!=null&&this.clickhandler==null)
  { if(seller!=undefined&&seller!=null)
      this.clickhandler = this.marker.addListener('click',seller) ;
    if(cyclist!=undefined&&cyclist!=null)
      this.righthandler = this.marker.addListener('rightclick',cyclist) ;
  }
} ;
datatype.prototype.setphotomap = function(m,seller) 
{ if(this.photo.length==0) m = null ; 
  if(m==null&&this.photomarker==null) return ;
  this.photomarker.setMap(m) ;
  if(m==null&&this.photohandler!=null) 
  { google.maps.event.removeListener(this.photohandler) ;
    this.photohandler = null ; 
  }
  if(m!=null&&this.photohandler==null&&seller!=undefined&&seller!=null) 
    this.photohandler = this.photomarker.addListener('click',seller) ;
} ;
datatype.prototype.setlabel = function(t,c,m,seller,cyclist) 
{ this.type = t ; 
  this.caption = c ; 
  if(t==null) 
  { if(this.marker!=null) this.setlabelmap(null,null,null) ; return ; } 
  if(this.marker==null) this.marker = new google.maps.Marker
      ({ position:this.pos,map:m,icon:this.geticon(),title:c,zIndex:1 }) ;
  else { this.marker.setIcon(this.geticon()) ; this.marker.setTitle(c) ; }
  this.setlabelmap(m,seller,cyclist) ; 
} ;
datatype.prototype.setphoto = function(ind,p,seller) 
{ var i ;
  if(p==null)
  { for(i=ind;i<this.photo.length-1;i++) this.photo[i] = this.photo[i+1] ; 
    this.photo.length -= 1 ; 
    if(this.photo.length==0&&this.photomarker!=null) 
      this.setphotomap(null,null) ; 
    return ; 
  }
  else { this.photo[ind] = p ; if(ind==0) this.photomarker.setTitle(p) ; }
} ;
datatype.prototype.addphoto = function(p,m,seller) 
{ this.photo.push(p) ; 
  if(this.photomarker==null) this.photomarker = new google.maps.Marker
      ({ position:this.pos,map:m,icon:icons.camera,title:p,zIndex:1 }) ;
  this.setphotomap(m,seller) ; 
} ;
datatype.prototype.setpos = function(p) 
{ this.pos = p ; 
  if(this.type!=null) this.marker.setPosition(p) ; 
  if(this.photo.length>0) this.photomarker.setPosition(p) ; 
} ;
datatype.prototype.setmap = function(m,flagsel,photosel,cyclist) 
{ this.setlabelmap(m,flagsel,cyclist) ; this.setphotomap(m,photosel) ; } ;

datatype.prototype.settype = function(t) 
{ this.type = t ; this.marker.setIcon(this.geticon()) ; } ;

datatype.prototype.labelcycle = function()
{ var oldtype = this.type , type ; 
  if(oldtype=='Generic') type = 'Left' ; 
  else if(oldtype=='Left') type = 'Straight' ; 
  else if(oldtype=='Straight') type = 'Right' ; 
  else if(oldtype=='Right') type = 'Danger' ; 
  else if(oldtype=='Danger') type = 'Food' ; 
  else type = 'Generic' ; 
  this.settype(type) ;
  return [ oldtype , type ] ; 
}
function addlabel(data,pos,type,caption,m,seller,cyclist) 
{ var j,ind,mindist ; 
  for(j=0;j<data.length;j++) if(j==0||dist(pos,data[j].pos)<mindist) 
  { mindist = dist(pos,data[j].pos) ; ind = j ; } 
  data[ind].setlabel(type,caption,m,seller,cyclist) ;
}
function propstype()
{ this.desc = this.title = this.list = this.inputlen = this.source = null ;
  this.stats = this.tracklink = this.overview = this.stars = null ; 
  this.sourceid = null ; 
  this.photo = [] ;
  this.optim = { already: 0, ndel: 0, origlen: 0, parms: null }
}
/* -------------------------------------------------------------------------- */

function readtcx(xmldoc,m,flagsel,photosel,cyclist)
{ var nodeno,type,lat,lon,i,j,node,alt,pos,segment,props,title,list,txt ;
  var ind,caption,data,photo,time,valid,anc,xmlnodes,nsegment,propno,names ;
  var track,trackpoint,trackno,course,coursepoint,courseno,courselen,overview ;
  var validalt,fieldnames ; 

  // loop over trackpoints 
  track = xmldoc.getElementsByTagName('Trackpoint') ;
  for(courselen=[],data=[],anc=null,trackno=0;trackno<track.length;trackno++)
  { trackpoint = track[trackno] ;
    if(trackpoint.parentNode.parentNode.nodeName=='Course')
      if(trackpoint.parentNode.parentNode!=anc) 
    { anc = trackpoint.parentNode.parentNode ; 
      courselen.push([trackno,anc]) ; 
    }
    lat = lon = alt = time = null ; 
    photo = [] ;
    for(validalt=valid=1,nodeno=0;nodeno<trackpoint.childNodes.length;nodeno++)
    { node = trackpoint.childNodes[nodeno] ;

      if(node.nodeName=='AltitudeMeters') alt = xmlfloat(node) ; 
      else if(node.nodeName=='Time') // '1970-01-01T03:040:08Z'
        time = new Date(node.childNodes[0].textContent) ; 
      else if(node.nodeName=='Position') for(j=0;j<node.childNodes.length;j++)
      { if(node.childNodes[j].nodeName=='LatitudeDegrees') 
          lat = xmlfloat(node.childNodes[j]) ; 
        else if(node.childNodes[j].nodeName=='LongitudeDegrees') 
          lon = xmlfloat(node.childNodes[j]) ;
      }
      else if(node.nodeName=='Extensions') for(j=0;j<node.childNodes.length;j++)
      { if(node.childNodes[j].nodeName=='Photo') 
          photo = node.childNodes[j].childNodes[0].textContent.split(' ') ;
        else if(node.childNodes[j].nodeName=='ValidTime') valid = 0 ;
        else if(node.childNodes[j].nodeName=='ValidAlt') validalt = 0 ;
      }
    }
    if(lat==null||lon==null) continue ; 
    if(!isvalidnum(alt)) validalt = 0 ; 
    pos = new google.maps.LatLng(lat,lon) ;
    data.push(new datatype(pos,validalt?alt:null,valid?time:null)) ; 
    for(ind=0;ind<photo.length;ind++) 
      data[data.length-1].addphoto(photo[ind],m,photosel) ;
  }
  if(track.length==0) { alert('no trackpoints') ; throw '' ; }
  if(courselen.length==0) courselen.push([0,null]) ; 
  courselen.push([track.length,null]) ; 

  // loop over coursepoints
  course = xmldoc.getElementsByTagName('CoursePoint') ;
  for(courseno=0;courseno<course.length;courseno++)
  { coursepoint = course[courseno] ;
    caption = type = lat = lon = null ;
    for(nodeno=0;nodeno<coursepoint.childNodes.length;nodeno++)
    { node = coursepoint.childNodes[nodeno] ;
      if(node.nodeName=='Name') caption = node.childNodes[0].textContent ; 
      else if(node.nodeName=='PointType') 
        type = node.childNodes[0].textContent ; 
      else if(node.nodeName=='Position') for(j=0;j<node.childNodes.length;j++)
      { if(node.childNodes[j].nodeName=='LatitudeDegrees') 
          lat = xmlfloat(node.childNodes[j]) ;
        else if(node.childNodes[j].nodeName=='LongitudeDegrees') 
          lon = xmlfloat(node.childNodes[j]) ;
      }
    }
    if(lat==null||lon==null||caption==null||type==null) 
    { alert('Badly formatted course point' ) ; throw '' ; }
    pos = new google.maps.LatLng(lat,lon) ;
    addlabel(data,pos,type,caption,m,flagsel,cyclist) ; 
  }

  for(segment=[],i=0;i<courselen.length-1;i++)
    segment.push(data.slice(courselen[i][0],courselen[i+1][0])) ;
  nsegment = segment.length ;

  // props fields
  props = new Array(nsegment) ; 
  for(i=0;i<nsegment;i++) 
  { props[i] = new propstype() ;
    props[i].inputlen = data.length ; 
    if(props[i].optim.origlen==0) props[i].optim.origlen = data.length ;
  }

  // optimised?
  xmlnodes = xmldoc.getElementsByTagName('Optimised') ;
  if(xmlnodes.length)
  { props[0].optim.already = 1 ; 
    props[0].optim.origlen = parseInt(xmlnodes[0].getAttribute('from')) ; 
    props[0].optim.ndel = 
      props[0].optim.origlen - parseInt(xmlnodes[0].getAttribute('to')) ; 
    props[0].optim.parms = 
      { tol: parseFloat(xmlnodes[0].getAttribute('tol')) ,
        maxsep: parseFloat(xmlnodes[0].getAttribute('maxsep')) ,
        wppenalty: parseFloat(xmlnodes[0].getAttribute('wppenalty')) ,
        vweight: parseFloat(xmlnodes[0].getAttribute('vweight')) 
      } ; 
    for(i=1;i<nsegment;i++) props[i].optim = props[0].optim ; 
  }

  // overview fields (different values for each segment) all in one big loop
  names = [ 'Name' ,     'LongTitle' , 'Description' , 'Stats' , 'PhotoList' ,
            'Overview' , 'Index' ,     'TrackLink' , 'Stars' ,       'Photo' ,
            'SourceId' ] ;
  fieldnames = [ 'title' ,    'desc' ,     'desc' ,      'stats' , 'list' ,
                 'overview' , 'overview' , 'tracklink' , 'stars' , 'photo' , 
                 'sourceid' ] ;
  for(title=null,propno=0;propno<names.length;propno++)
  { xmlnodes = xmldoc.getElementsByTagName(names[propno]) ;
    for(i=0;i<xmlnodes.length;i++)
    { node = xmlnodes[i] ;
      if(propno==0) anc = node.parentNode ;
      else anc = node.parentNode.parentNode ;
      if(propno==4) txt = node.getAttribute('src') ;
      else if(propno==5||propno==6||propno==7) txt = node.getAttribute('href') ;
      else txt = node.childNodes[0].textContent ;
      for(j=0;j<nsegment&&anc!=courselen[j][1];j++) ;
      if(j==nsegment)
      { if(propno==0&&title==null)
          if( node.parentNode.nodeName=='Courses' 
           || node.parentNode.nodeName=='Lap' ) title = txt ;  
        continue ; 
      }
      if(names[propno]=='Photo') props[j].photo = txt.match(/\S+/g) ;
      else props[j][fieldnames[propno]] = txt ;
    }
  }

  return { title: title , props: props , segments: segment } ;
}
/* -------------------------------------------------------------------------- */

function readgpx(xmldoc,m,flagsel,cyclist)
{ var xmlcoords,nodeno,type,lat,lon,i,node,alt,pos,caption,data,time ; 
  var props = new propstype() ; 

  // get the route name
  xmlcoords = xmldoc.getElementsByTagName('name') ;
  for(i=0;props.title==null&&i<xmlcoords.length;i++)
    if(xmlcoords[i].parentNode.nodeName!='wpt') 
      props.title = xmlcoords[i].childNodes[0].textContent.substring(0,15) ;

  // get the route description
  xmlcoords = xmldoc.getElementsByTagName('desc') ;
  if(xmlcoords.length>0&&xmlcoords[0].childNodes.length>0) 
    props.desc = xmlcoords[0].childNodes[0].textContent ;

  // loop over the track points to get the coords
  xmlcoords = xmldoc.getElementsByTagName('trkpt') ;
  if(xmlcoords.length==0) xmlcoords = xmldoc.getElementsByTagName('rtept') ;

  for(data=[],i=0;i<xmlcoords.length;i++)
  { lat = parseFloat(xmlcoords[i].getAttribute('lat')) ; 
    lon = parseFloat(xmlcoords[i].getAttribute('lon')) ; 
    pos = new google.maps.LatLng(lat,lon) ;

    for(time=alt=null,nodeno=0;nodeno<xmlcoords[i].childNodes.length;nodeno++)
    { node = xmlcoords[i].childNodes[nodeno] ;
      if(node.nodeName=='ele') alt = parseFloat(node.textContent) ; 
      else if(node.nodeName=='time')
        time = new Date(node.childNodes[0].textContent) ; 
    }
    if(!isvalidnum(alt)) alt = null ; 
    data.push(new datatype(pos,alt,time)); 
  }

  // loop over the course points to get the labels
  xmlcoords = xmldoc.getElementsByTagName('wpt') ;
  for(i=0;i<xmlcoords.length;i++)
  { caption = type = lat = lon = null ;
    lat = parseFloat(xmlcoords[i].getAttribute('lat')) ; 
    lon = parseFloat(xmlcoords[i].getAttribute('lon')) ; 
    for(nodeno=0;nodeno<xmlcoords[i].childNodes.length;nodeno++)
    { node = xmlcoords[i].childNodes[nodeno] ;
      if(node.nodeName=='name') caption = node.childNodes[0].textContent ; 
      else if(node.nodeName=='type') type = node.childNodes[0].textContent ; 
    }
    if(lat==null||lon==null||caption==null) 
    { alert('Badly formatted course point' ) ; throw '' ; }
    if(type==null) type = 'Generic' ; 
    pos = new google.maps.LatLng(lat,lon) ;
    addlabel(data,pos,type,caption,m,flagsel,cyclist) ; 
  }

  props.inputlen = data.length ; 
  return { props: [props] , segments: [data] } ;
}
/* -------------------------------------------------------------------------- */

function readfit(rawdata,m)
{ var input = new Uint8Array(rawdata) , defns=[] , props = new propstype ; 
  var ind,val,flag,tag,i,defn,lat,lon,pos,alt,time,item,nitem ;
  if(input.length<12)
  { alert("supposed FIT file has length<12 ("+input.length+")") ; throw '' ; }
  tag = String.fromCharCode(input[8]) + String.fromCharCode(input[9]) + 
        String.fromCharCode(input[10]) + String.fromCharCode(input[11]) ;
  if(tag!=".FIT") 
  { alert("supposed FIT file has format designator '"+tag+"'") ; throw '' ; }

  for(data=[],ind=input[0];ind<input.length-2;) 
  { flag = input[ind++] ; 
    if(128&flag) { alert("FIT file is compressed") ; throw '' ; }
    else if(64&flag)     // definition message
    { val = input[ind+2] + (input[ind+3]<<8) ; 
      defn = { num:val , type:15&flag , fields:[] , size:0 } ; 
      nitem = input[ind+4] ;
      for(ind+=5,i=0;i<nitem;i++,ind+=3)
      { item = { num:input[ind] , size:input[ind+1] , type:input[ind+2] } ;
        defn.fields.push(item) ; 
        defn.size += item.size ; 
      }
      defns.push(defn) ; 
    }
    else
    { for(defn=null,i=0;defn==null&&i<defns.length;i++) 
        if(defns[i].type==(15&flag)) defn = defns[i] ; 
      if(defn==null) { alert("missing definition in FIT file") ; throw '' ; }
      if(defn.num==20) 
      { lat = lon = alt = time = null ; 
        for(i=0;i<defn.fields.length;i++,ind+=item.size)
        { item = defn.fields[i] ; 
          if(item.num==0) lat = readfitangle(input,ind,item.type) ;
          else if(item.num==1) lon = readfitangle(input,ind,item.type) ;
          else if(item.num==2) 
            alt = readfitvalue(input,ind,item.type)/5 - 500  ;
          else if(item.num==253) 
            time = 1000 * ( readfitvalue(input,ind,item.type) + 631065600 ) ;
        }
        if(lat!=null&&lon!=null)
        { pos = new google.maps.LatLng(lat,lon) ;
          data.push(new datatype(pos,alt,new Date(time))) ; 
        }
      }
      else ind += defn.size ; 
    }
  }
  props.inputlen = props.origlen = data.length ; 
  return { props: [props] , segments: [data] } ;
}
/* -------------------------------------------------------------------------- */

function readfitvalue(input,ind,type)
{ var r=null ;
  if(type==1||type==2) r = input[ind] ; 
  else if(type==131||type==132) r = input[ind] + (input[ind+1]<<8) ; 
  else if(type==133||type==134) r = input[ind] + (input[ind+1]<<8) + 
                            (input[ind+2]<<16) + (input[ind+3]<<24) ; 
  if(r!=null&&(type&1)==0) r = r >>> 0 ; // type-convert to unsigned
  return r ; 
}
function readfitangle(input,ind,type)
{ return readfitvalue(input,ind,type)*180 / ((1 << 31) >>> 0) ; }

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

function dist(x,y)
{ return google.maps.geometry.spherical.computeDistanceBetween(x,y) ; }

function angle(x,y)
{ return google.maps.geometry.spherical.computeHeading(x,y)*Math.PI/180 ; }

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

function optimise(idata,parms)
{ var stk,nnstk,stk2,clen=idata.length,i,j,k,m,step=new Array(clen-1) ; 
  var opos,oalt,npos,nalt,ndatum,mpos,malt,arccentre,arctol,pathpos ; 
  var legal,theta,omega,x,y,hyp,tdist,maxtheta,mintheta,d,dh,od,odh,odash ; 
  var bearings=new Array(clen),nstk=new Array(clen),pi=Math.PI,tol=parms.tol ;
  var backptr = new Array(clen) ; 

  stk = [ { err:0 , pathpos:1 , prev:-1 } ] ;
  for(i=0;i<clen-1;i++) step[i] = dist(idata[i].pos,idata[i+1].pos) ; 

  // this is a forwards dynamic program. in stk we have a list of hypotheses
  // each of which advances a different number of points through the data, 
  // sorted increasing on how far they've advanced. at each step we take the 
  // first item from the stack and try extending to each legal successor point.
  //    note that a hypothesis whose pathpos is k is one whose last point is
  // idata[k-1].
  while(stk[0].pathpos<clen)
  { pathpos = stk[0].pathpos ;
    backptr[pathpos-1] = stk[0].prev ;
    opos = idata[pathpos-1].pos ;
    oalt = idata[pathpos-1].h ; 
    // try extending to pathpos+i
    for(arctol=null,nnstk=i=0;i<clen-pathpos;i++)
    { ndatum = idata[pathpos+i] ; 
      npos = ndatum.pos ; 
      nalt = ndatum.h ; 
      if(i==0) hyp = step[pathpos-1] ;
      else if((hyp=dist(opos,npos))>parms.maxsep) break ; 
      omega = angle(opos,npos) ; 
      // find the min and max legal bearing
      if(hyp>tol) 
      { theta = Math.asin(tol/hyp) ; 
        if(arctol==null) { arccentre = omega ; arctol = theta ; } 
        else
        { for(odash=omega-arccentre;odash>pi;odash-=2*pi) ; 
          while(odash<-pi) odash += 2*pi ;
          maxtheta = Math.min(arctol,odash+theta) ; 
          mintheta = Math.max(-arctol,odash-theta) ; 
          if(maxtheta<mintheta) break ; 
          arccentre += (maxtheta+mintheta) /2 ; 
          arctol     = (maxtheta-mintheta) /2 ;
        }
      } 
      /* -------------------------------------------------------------------- */
      /*page*/
      /* -------------------------------------------------------------------- */

      bearings[i] = { hyp:hyp , omega:omega } ; 
      // see whether this breaches the max error on any intermediate point
      for(legal=1,od=odh=tdist=m=0;m<i;m++,od=d,odh=dh)
      { mpos = idata[pathpos+m].pos ;
        malt = idata[pathpos+m].h ; 
        x = bearings[m].hyp ; 
        theta = bearings[m].omega ; 
        d = x * Math.sin(theta-omega) ; 
        dh = 0 ;
        if(d*d<tol*tol&&oalt!=null&&nalt!=null&&malt!=null)  
        { y = hyp - x*Math.cos(theta-omega) ;
          y = Math.sqrt(d*d+y*y) ; 
          dh = parms.vweight * ( malt - (oalt*y+nalt*x)/(x+y) ) ; 
        }
        if(d*d+dh*dh>tol*tol) { legal = 0 ; break ; } 
        tdist += step[pathpos-1+m] * ( d*d+d*od+od*od + dh*dh+odh*dh+odh*odh ) ;
      }
      // if we emerge with 'legal' non-zero then we may advance to pathpos+i 
      // and tdist is the sum of squared errors
      if(legal) nstk[nnstk++] = 
      ( { err:      stk[0].err + pi*tdist/3 + parms.wppenalty , 
          pathpos:  stk[0].pathpos+i+1 ,
          prev:     pathpos-1 
        } ) ; 
      if(ndatum.type!=null||ndatum.photo.length>0) break ; 
    }  // end loop over i 

    // now we have in nstk the possible extensions of stk[0] in increasing
    // order of end point, so we merge with stk[0..stk.length-1]
    for(stk2=new Array(stk.length+nnstk),i=1,k=j=0;i<stk.length||j<nnstk;)
      if(i==stk.length) stk2[k++] = nstk[j++] ; 
      else if(j==nnstk||stk[i].pathpos<nstk[j].pathpos) stk2[k++] = stk[i++] ; 
      else if(stk[i].pathpos>nstk[j].pathpos) stk2[k++] = nstk[j++] ; 
      else if(stk[i].err<nstk[j].err) { stk2[k++] = stk[i++] ; j += 1 ; } 
      else { stk2[k++] = nstk[j++] ; i += 1 ; } 
    stk = stk2.slice(0,k) ; 
  }

  // thread backwards through the pointers
  for(m=1,i=stk[0].prev;i>=0;i=backptr[i],m++) ;
  nstk = new Array(m) ; 
  for(nstk[m-1]=idata[clen-1],j=m-2,i=stk[0].prev;i>=0;i=backptr[i],j--) 
    nstk[j] = idata[i] ;
  return nstk ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ------------------------------- writetcx  -------------------------------- */

function writetcx(props,idata) 
{ var i,j,k,xmldoc,course,lap,datum,filename,track,time,routelen,nnull,sum ; 
  var trackpoint,coursepoint,str,flag,clen=idata.length,tlast,di,dk,pointdist ;
  var origlen,ndel,ano,photo,thisuri,maxsep,sep,tdist,ttime,time,otime,x,y ; 
  var distance = new Array(clen) , msecs = new Array(clen) ;
  var valid = new Array(clen) , alt = new Array(clen) ;

  for(tlast=null,nnull=maxsep=tdist=ttime=i=flag=0;i<clen;otime=time,i++) 
  { if((alt[i]=idata[i].h)==null) nnull += 1 ; 
    time = idata[i].t ;
    if(time!=null) { msecs[i] = time = time.getTime() ; valid[i] = 1 ; } 
    if(tlast!=null&&time!=null&&time<tlast) flag = 1 ; // out of order
    if(time!=null) tlast = time ;
    if(i) 
    { sep = dist(idata[i-1].pos,idata[i].pos) ;
      distance[i] = distance[i-1] + sep ; 
      if(sep>maxsep) maxsep = sep ; 
      if(time!=null&&otime!=null) { tdist += sep ; ttime += time - otime ; }
    }
    else distance[i] = 0 ; 
  }
  routelen = distance[clen-1] ;

  if(maxsep>100&&!confirm('Some gaps between waypoints are >100m.\n'+
    'This will cause problems if used for navigation in a Garmin.\n'+
    'You can hit [OK] and I will proceed anyway, or\n'+
    'you can hit [Cancel] and interpolate extra points\n'+
    '(recommended \u2013 go to Route Info under the cogwheel).')) return null ; 

  // decide what to do if some points have no altitudes
  if(nnull==clen) { alert('no points have altitudes') ; return null ; }
  if(nnull>0&&!confirm(nnull+' waypoints have no associated altitudes.\n' +
  'You can hit [OK] and I will interpolate altitudes (not guaranteed),\n'+
  'or you can hit [Cancel] and try again later when the altitudes may be '+
  'available.')) return null ; 

  if(nnull) for(pointdist=new Array(clen),i=0;i<clen;i=j)
  { for(;i<clen&&idata[i].h!=null;i++) ;          // advance to null
    if(i==clen) break ; 
    for(j=i+1;j<clen&&idata[j]==null;j++) ;       // advance to non-null
    if(i==0) { for(y=idata[j].h;i<j;i++) alt[i] = y ; continue ; } 
    if(j==clen) { for(x=idata[i-1].h;i<j;i++) alt[i] = x ; continue ; } 
    for(sum=k=0;k<=j-i;k++) 
      sum = pointdist[k] = sum + dist(idata[i+k-1].pos,idata[i+k].pos) ; 
    for(x=idata[i-1].h,y=idata[j].h,k=0;k<j-i;k++) 
      alt[i+k] = ( x*(sum-pointdist[k]) + y*pointdist[k] ) / sum ; 
  }

  // fill in missing times
  if(tdist==0||flag!=0) for(i=0;i<clen;i++) msecs[i] = distance[i] * 333 ;
  else for(i=0;i<clen;i=k)
  { for(;i<clen&&idata[i].t!=null;i++) ;      // advance to null
    if(i==clen) break ;
    for(k=i+1;k<clen&&idata[k].t==null;k++) ; // advance to non-null
    for(j=i;j<k;j++) valid[i] = 0 ;
    if(i==0) for(time=msecs[k],j=i;j<k;j++)
      msecs[j] = time - (distance[k]-distance[j])*ttime/tdist ;
    else if(k==clen) for(time=msecs[i-1],j=i;j<clen;j++)
      msecs[j] = time + (distance[j]-distance[i-1])*ttime/tdist ;
    else for(j=i,di=distance[i-1],dk=distance[k];j<k;j++) 
      msecs[j] = ( msecs[i-1]*(dk-distance[j]) + msecs[k]*(distance[j]-di) ) 
                        / (dk-di) ;
  }

  str = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n' +
        '<TrainingCenterDatabase xmlns="http://www.garmin.com/xmlschemas/' +
        'TrainingCenterDatabase/v2"\n' +
        '          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' +
        '          xsi:schemaLocation="http://www.garmin.com/' +
        'xmlschemas/TrainingCenterDatabase/v2 ' + 
        'http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd">\n' +
        '<!-- http://www.masterlyinactivity.com/software/routemaster.html -->' ;
  str += '\n  <Folders><Courses><CourseFolder Name="Courses">\n' ;
  str += '        <CourseNameRef><Id>'+props.title+'</Id></CourseNameRef>\n' ;
  str += '  </CourseFolder></Courses></Folders>\n<Courses><Course>\n' ; 
  str += '  <Name>'+props.title+'</Name>\n  <Lap>\n' + adddist(routelen) ; 
  time = (msecs[clen-1]-msecs[0]) / 1000 ;
  str += '    <TotalTimeSeconds>' + time.toFixed(0) + '</TotalTimeSeconds>\n' ;
  str += addpos('Begin',idata[0].pos) ; 
  str += addpos('End',idata[clen-1].pos) ; 
  str += '    <Intensity>Active</Intensity>\n' + '  </Lap>\n  <Track>\n' ; 

  // loop over trackpoints
  for(i=0;i<idata.length;i++) 
  { str += '  <Trackpoint>\n' + addpos('',idata[i].pos) ; 
    str += adddist(distance[i]) + addalt(idata[i].h==null?alt[i]:idata[i].h) ;
    str += '    <Time>' + new Date(msecs[i]).toISOString() + '</Time>\n' ;
    if(idata[i].photo.length>0)
    { str += '    <Extensions><Photo>' ;
      for(k=0;k<idata[i].photo.length;k++)
      { if(k) str += ' ' ; str += idata[i].photo[k] ; }
      str += '</Photo></Extensions>\n' ; 
    }
    if(valid[i]==0||idata[i].h==null) 
    { str += '    <Extensions>' ; 
      if(valid[i]==0) str += '<ValidTime>False</ValidTime>' ;
      if((valid[i]==0&&idata[i].h==null)) str += '\n                ' ;
      if(idata[i].h==null) str += '<ValidAlt>False</ValidAlt>' ;
      str += '</Extensions>\n' ; 
    }
    str += '    <SensorState>Absent</SensorState>\n  </Trackpoint>\n' ; 
  }
  str += '  </Track>\n\n' ;

  if( props.optim.ndel>0 || props.list!=null || props.stars!=null
   || props.desc!=null   || props.overview != null 
   || (props.sourceid!=null&&props.sourceid!=props.title) )
  { str += '  <Extensions>\n' ;
    if(props.optim.ndel) 
    { str += '    <Optimised from="' + props.optim.origlen +
                            '" to="' + (props.optim.origlen-props.optim.ndel) ;
      if(props.optim.parms!=null) 
        str += '" tol="' + props.optim.parms.tol.toFixed(0) +
                '" maxsep="' + props.optim.parms.maxsep.toFixed(0) +
                '" wppenalty="' + props.optim.parms.wppenalty.toFixed(0) +
                '" vweight="' + props.optim.parms.vweight.toFixed(1) ;
      str += '"/>\n'
    }
    if(props.list!=null) str += '    <PhotoList src="'+props.list+'"/>\n' ; 
    if(props.stars!=null) str += '    <Stars>'+props.stars+'</Stars>\n' ; 
    if(props.desc!=null) 
      str += '    <Description>'+props.desc+'</Description>\n' ; 
    if(props.overview!=null) 
      str += '    <Index href="' + props.overview + '"/>\n' ; 
    if(props.sourceid!=null&&props.sourceid!=props.title) 
      str += '    <SourceId>'+props.sourceid+'</SourceId>\n' ; 
    str += '  </Extensions>\n\n' ;
  }
  
  // finally loop over coursepoints
  for(i=0;i<idata.length;i++) if(idata[i].type!=null)
  { datum = idata[i] ;
    str += '  <CoursePoint><Name>'+datum.marker.title+'</Name>\n' ; 
    str += '    <PointType>'+datum.type+'</PointType>\n' ; 
    str += addpos('',idata[i].pos) + addalt(idata[i].h) ;
    time = new Date(msecs[i]) ; 
    str += '    <Time>' + time.toISOString() + '</Time>\n  </CoursePoint>\n' ;
  }
  return str + '</Course></Courses></TrainingCenterDatabase>\n' ; 
}
/* -------------------------------------------------------------------------- */

function addpos(tag,pos)
{ var str = '    <'+tag+'Position>\n      <LatitudeDegrees>' ; 
  str += pos.lat().toFixed(5)+'</LatitudeDegrees>\n      <LongitudeDegrees>' ;
  str += pos.lng().toFixed(5)+'</LongitudeDegrees>\n    </'+tag+'Position>\n' ;
  return str ;
}
function addalt(x)
{ return '    <AltitudeMeters>' + x.toFixed(0) + '</AltitudeMeters>\n' ; }
function adddist(x)
{ return '    <DistanceMeters>' + x.toFixed(0) + '</DistanceMeters>\n' ; }

/* ----------------------------- writeoverview  ----------------------------- */

function writeoverview(segments,title,list) 
{ var i,j,h,oh,maxalt,minalt,routelen,up,down,ndata,photo,segno,idata ;
  var uri ; 
  var str = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n' +
        '<!-- http://www.masterlyinactivity.com/software/routemaster.html -->' +
        '\n<!-- **** routemaster index file **** not for navigation ****' +
        ' -->\n<TrainingCenterDatabase><Courses>\n' ;

  if(title!='Untitled Route'&&title!=null) str += '<Name>'+title+'</Name>\n' ;
  str += '\n' ; 

  for(segno=0;segno<segments.length;segno++)
  { idata = segments[segno].data ;
    for(oh=maxalt=minalt=null,routelen=up=down=i=0;i<idata.length;oh=h,i++)
    { if(i) routelen += dist(idata[i-1].pos,idata[i].pos) ;
      if((h=idata[i].h)==null) continue ; 
      if(maxalt==null||h>maxalt) maxalt = h ; 
      if(minalt==null||h<minalt) minalt = h ; 
      if(h!=null&&oh!=null) { if(h>oh) up += h-oh ; else down += oh-h ; } 
    }

    for(photo=[],ndata=new Array(idata.length),i=0;i<idata.length;i++)
    { ndata[i] = new datatype(idata[i].pos,0) ;
      for(j=0;j<idata[i].photo.length;j++) photo.push(idata[i].photo[j]) ; 
    }
    idata = optimise(ndata,{tol:500,maxsep:100000,wppenalty:10000,vweight:0}) ;

    str += '<Course>\n' ;
    if(segments[segno].props.title!=null) 
      str += '  <Name>' + segments[segno].props.title + '</Name>\n' ;
    str += '  <Track>\n' ; 

    // loop over trackpoints
    for(i=0;i<idata.length;i++) 
      str += '    <Trackpoint><Position>\n      <LatitudeDegrees>' +
             idata[i].pos.lat().toFixed(4)+'</LatitudeDegrees>\n      ' +
             '<LongitudeDegrees>' + idata[i].pos.lng().toFixed(4) +
             '</LongitudeDegrees>\n    </Position></Trackpoint>\n' ; 
    str += '  </Track>\n\n  <Extensions>\n' ;

    title = segments[segno].props.stars ;
    if(title!=null) str += '    <Stars>' + title + '</Stars>\n' ; 

    title = segments[segno].props.desc ;
    if(title!=null) str += '    <Description>' + title + '</Description>\n' ; 

    uri = document.URL ;  
    if((title=segments[segno].props.source)!=null) 
    { if(title[1]=='uri') uri = reluri(uri,'?track='+title[0]) ; 
      else uri = reluri(uri,'?track=$FILE$/'+title[0]) ; 
    }
    str += '    <Stats>Distance ' + (routelen/1000).toFixed(1) + 'km;' +
           ' altitude ' + minalt.toFixed(0) + '-' + maxalt.toFixed(0) + 
           'm; \u2191' + up.toFixed(0) + 'm \u2193' + down.toFixed(0) + 
           'm</Stats>\n    <TrackLink href="' + uri + '"/>\n' ; 

    if(list!=null&&list!=undefined) 
      str += '    <PhotoList src="' + list + '"/>\n' ; 

    if(photo.length)
    { str += '    <Photo>' ;
      for(i=0;i<photo.length;i++) 
      { if(i>0&&i%6==0) str += '\n           ' ; else if(i) str += ' ' ; 
        str += photo[i] ; 
      }
      if(photo.length%6==0) str += '\n           ' ; 
      str += '</Photo>\n' ;
    }
    str += '  </Extensions>\n</Course>\n\n' ;
  }

  return str + '</Courses></TrainingCenterDatabase>\n' ; 
}
/* -------------------------------------------------------------------------- */

function gencolours(n)
{ var colours=new Array(n) ;
  var ind,density,k,a,na,i,j,m,r,g,b ;

  for(ind=0,density=1;ind<n;density*=2)
  { if(density==1) k = 3 ; else k = Math.floor(0.5+0.75*density*(1+density/2)) ;
    a = new Array(k) ;
    if(density==1) { a = [ [0,0] , [0,1] , [1,0] ] ; na = 3 ; }
    else for(na=i=0;i<=density;i++) 
    { if((i&1)==0) for(j=1;i+j<=density;j+=2) a[na++] = [ i , j ] ;
      else for(j=density-i;j>=0;j--) a[na++] = [ i , j ] ;
    }
    if(na!=k) alert('logic error') ; 
    for(k=0;(1<<k)<na;k++) ;
    for(i=0;i<(1<<k)&&ind<n;i++)
    { for(m=j=0;j<k;j++) m |= ((i>>j)&1) << (k-1-j) ;
      if(m>=na) continue ;
      r = ( density - a[m][0] - a[m][1] ) * (255/density) ;
      g = a[m][1] * (180/density) ;
      b = a[m][0] * (300/density) ;
      r = ("00"+Math.floor(0.5+r).toString(16)).substr(-2) ;
      g = ("00"+Math.floor(0.5+g).toString(16)).substr(-2) ;
      b = ("00"+Math.floor(0.5+b>255?255:b).toString(16)).substr(-2) ;
      colours[ind++] = '#' + r + g + b ; 
    }      
  }
  return colours ;
}
/* ---------------------------- relative uri  ------------------------------- */

function reluri(u1,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) return u2 ; 
    u2 = u2.substring(3) ; 
    u1 = u1.substring(0,last) ; 
  }
  return u1 + '/' + u2 ; 
}
/* -------------------------------------------------------------------------- */

function ascify(s)
{ var a = 'àáâäæãåā' , see = 'çćč' , e = 'èéêëēėę' , eye = 'îïíīįì' , l = 'ł' ;
  var n = 'ñń' , o = 'ôöòóœøōõ' , ess = 'ßśš' , u = 'ûüùúū' ;
  var sdash,i,c,C,newc ; 
  for(sdash='',i=0;i<s.length;i++)
  { C = s.charAt(i) ; 
    if(C=='‘'||C=='’'||C=='“'||C=='”'||C=='"') { sdash += "'" ; continue ; }
    if(C=='–') { sdash += '-' ; continue ; }
    if(C.charCodeAt(0)<128) { sdash += C ; continue ; }
    c = C.toLowerCase() ; 
    if(a.indexOf(c)>=0) newc = 'a' ; 
    else if(see.indexOf(c)>=0) newc = 'c' ; 
    else if(e.indexOf(c)>=0) newc = 'e' ; 
    else if(eye.indexOf(c)>=0) newc = 'i' ; 
    else if(l.indexOf(c)>=0) newc = 'l' ; 
    else if(n.indexOf(c)>=0) newc = 'n' ; 
    else if(o.indexOf(c)>=0) newc = 'o' ; 
    else if(ess.indexOf(c)>=0) newc = 's' ; 
    else if(u.indexOf(c)>=0) newc = 'u' ; 
    else newc = '*' ;
    if(c==C) sdash += newc ; else sdash += newc.toUpperCase() ; 
  }
  return sdash ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* --------------------------------- flatten -------------------------------- */

function flatten(segments)
{ var point,npoint,d,s0,s1,len,pos,opos ;

  for(npoint=s0=0;s0<segments.length;npoint+=segments[s0].data.length,s0++) ;
  point = new Array(npoint) ;
  for(d=npoint=s0=0;s0<segments.length;s0++)
    for(len=segments[s0].data.length,s1=0;s1<len;s1++,opos=pos) 
  { pos = segments[s0].data[s1].pos ;
    if(npoint) d += dist(opos,pos) ;
    point[npoint++] = 
      { pos:pos , h:segments[s0].data[s1].h , sel:[s0,s1] , d:d } ;
  }
  return point ; 
}
/* --------------------------------- getalts -------------------------------- */

var elevator=null,reqlist=[] ; 

function busyalts() { return reqlist.length>0 ; }

function getalts(segments,thresh,action)
{ var s0,s1,start,end,n,npts,flag,lox,loxpos,loxind,point,i,j,k,l,len ; 
  var reqno,ind ; 
  if(reqlist.length>0) return ;
  if(elevator==null) elevator = new google.maps.ElevationService ;

  point = flatten(segments) ; 

  for(flag=npts=i=0;i<point.length;i=j)
  { if(point[i].h!=null) { j = i+1 ; continue ; }
    for(j=i+1;j<point.length&&point[j].h==null;j++) ;
    // so now i is the first of a sequence of null altitudes and j is the 
    // non-null altitude terminating it
    if(i>0) i -= 1 ; 
    if(j<point.length) j += 1 ; 
    n = j - i ; 
    if(npts+n<=500) { npts += n ; reqlist.push([i,j]) ; }
    else if(reqlist.length>0) // unable to process correctly in this request
    { flag = 1 ; break ; }
    else { npts = n ; reqlist.push([i,j]) ; flag = 2 ; break ; }
  }
  if(flag==0&&npts<thresh) { reqlist = [] ; return ; }

  for(lox=new Array(npts),ind=reqno=0;reqno<reqlist.length;reqno++) 
    for(end=reqlist[reqno][1],k=reqlist[reqno][0];k<end;k++) 
      lox[ind++] = segments[point[k].sel[0]].data[point[k].sel[1]] ; 
  point = null ; // we've finished with it

  if(flag==2) for(loxind=new Array(501),loxpos=new Array(501),k=0;k<=500;k++)
  { loxind[k] = Math.floor(0.5+(k*(n-1))/500) ; 
    loxpos[k] = lox[loxind[k]].pos ; 
  }
  else for(loxind=new Array(npts),loxpos=new Array(npts),k=0;k<npts;k++) 
  { loxind[k] = k ;  loxpos[k] = lox[k].pos ; }

  // at this point lox is an array of points whose altitudes need adjusting, 
  // loxpos is an array of at most 500 positions and loxind holds the indexes
  // in lox of the entries in loxpos

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

  elevator.getElevationForLocations( {locations:loxpos},function(results,status)
  { // assume that the results come in sequence, ie. correspond to xpending[0]
    var d0,dn,reqno,i,j,k,l,n,err,d,D ;
    if(status===google.maps.ElevationStatus.OVER_QUERY_LIMIT)
      alert('No calibration data available: over Google query limit') ;  
    else if(status===google.maps.ElevationStatus.INVALID_REQUEST)
      alert('Invalid calibration request') ;  
    else if(status===google.maps.ElevationStatus.REQUEST_DENIED)
      alert('Calibration request denied') ;  
    else if(status===google.maps.ElevationStatus.UNKNOWN_ERROR)
      alert('Unknown error reported for calibration request:'+status) ;  
    else if(status!==google.maps.ElevationStatus.OK)
      alert('Calibration error') ;  
    if(status!==google.maps.ElevationStatus.OK) throw '' ;

    if(results.length!=loxpos.length) err = 1 ; 
    else for(err=i=0;i<loxpos.length&&err==0;i++) 
      if(dist(loxpos[i],results[i].location)>5) err= 1 ; 
    if(err) 
    { alert("elevation response does not correspond to request") ; throw '' ; }

    for(k=reqno=0;reqno<reqlist.length;reqno++,k+=n) 
    { if(flag==2) n = 501 ; else n = reqlist[reqno][1] - reqlist[reqno][0] ; 
      if(lox[loxind[k]].h!=null&&lox[loxind[k+n-1]].h!=null)
      { d0 = lox[loxind[k]].h - results[k].elevation ;
        dn = lox[loxind[k+n-1]].h - results[k+n-1].elevation ;
      }
      else
      { if(lox[loxind[k]].h!=null) 
         dn = d0 = lox[loxind[k]].h - results[k].elevation ; 
        else if(lox[loxind[k+n-1]].h!=null) 
          dn = d0 = lox[loxind[k+n-1]].h - results[k+n-1].elevation ; 
        else dn = d0 = 0 ; 
      }
      for(i=0;i<n;i++) lox[loxind[k+i]].h = 
                         results[k+i].elevation + (i*dn+((n-1)-i)*d0)/(n-1) ;
      // fill in missing altitudes by interpolation
      if(flag==2) for(i=0;i<n-1;i++) if(loxind[i+1]>loxind[i]+1)
      { for(D=0,k=loxind[i];k<loxind[i+1];k++) 
          D += dist(lox[k].pos,lox[k+1].pos) ; 
        dn = lox[loxind[i+1]].h ; 
        d0 = lox[loxind[i]].h ; 
        for(d=0,k=loxind[i];k<loxind[i+1]-1;k++) 
        { d += dist(lox[k].pos,lox[k+1].pos) ; 
          lox[k+1].h = ( d*dn+(D-d)*d0 ) / D ; 
        }
      }
    }
    reqlist = [] ; 
    if(action!=null&&action!=undefined) action(npts) ; 
    getalts(segments,thresh,action) ; 
  } ) ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */
/*          FUNCTIONS FOR COMPUTING & DISPLAYING THE ALTITUDE PROFILE         */
/* -------------------------------------------------------------------------- */

function profilemaptype(segments,nx)
{ var i,d0,d1,D,pos,oldpos,hind,htarg,oldi,lam,n,s0,s1 ; 

  var point = flatten(segments) ;
  n = point.length ; 
  for(j=0;j<n&&point[j].h==null;j++) ; 
  if(j==n) return null ;
  for(i=0;i<j;i++) point[i].h = point[j].h 
  for(i=n;i>0&&point[i-1].h==null;i--) ; 
  for(j=i;j<n;j++) point[j].h = point[i-1].h ;
  D = point[point.length-1].d ; 

  // now loop through points extracting regularly spaced altitudes
  this.h = new Array(nx) ; 
  for(oldi=hind=i=0;hind<nx;hind++)
  { htarg = (hind+0.5) * D / nx ;
    for(;i<point.length-1&&(point[i].d<htarg||point[i].h==null);i++) 
      if(point[i].h!=null) oldi = i ; 
    d1 = point[i].d ;
    d0 = point[oldi].d ;
    if(d1>d0) lam = (htarg-d0) / (d1-d0) ; else lam = 1 ; 
    if(lam>0&&lam<1&&point[i].sel[1]==0) this.h[hind] = null ; 
    else this.h[hind] = point[oldi].h + lam*(point[i].h-point[oldi].h) ; 
  }

  // a simple loop to find hmin, hmax
  for(i=0;i<nx;i++) if(this.h[i]!=null)
  { if(i==0||this.h[i]>this.hmax) this.hmax = this.h[i] ;
    if(i==0||this.h[i]<this.hmin) this.hmin = this.h[i] ;
  }
  if(this.hmin>0) 
  { if(this.hmax>3*this.hmin) this.hmin = 0 ; 
    else this.hmin *= 1 - (this.hmax/this.hmin-1)/2 ; 
  }
  this.hspan = Math.max(1,this.hmax-this.hmin) ; 

  // set up pro2wp
  this.pro2wp = new Array(nx) ; 
  for(hind=i=0;hind<nx;hind++)
  { htarg = (hind+0.5) * D / nx ;
    for(;i<point.length-1&&point[i].d<htarg;i++) ;
    this.pro2wp[hind] = point[i].sel ;
    if(i==0) continue ;
    d1 = point[i].d ;
    d0 = point[i-1].d ;
    if(d1>d0) lam = (htarg-d0) / (d1-d0) ; else lam = 1 ; 
    if(lam<0.5) this.pro2wp[hind] = point[i-1].sel ;
  }

  // set up wp2pro
  this.wp2pro = new Array(segments.length) ; 
  for(i=s0=0;s0<segments.length;s0++)
  { n = segments[s0].data.length ;
    this.wp2pro[s0] = new Array(n) ;
    for(s1=0;s1<n;s1++,i++)
      this.wp2pro[s0][s1] = Math.floor(0.5+point[i].d*nx/D) ;
  }
}
/* -------------------------------------------------------------------------- */

routemaster.js routemasteredit.js routemasterui.js routemasterlib.js
pix.js: pixlib.js