simple tasks :
advanced hints :
Routemaster is an online tool to display and edit GPS tracks and to display multi-track indexes. Photographs may be included. The GPS tracks may be in TCX, GPX, RTE, KML or FIT format. You can either upload a track from your own computer or view a track stored anywhere on the web.
I would like to think that routemaster was self-explanatory but in case of difficulty theres a help menu under the cogwheel button. The same menu is displayed on the welcome page. The hints below cover the simplest tasks.
OSM routemaster has been converted from Google Maps to Leaflet/OSM. Several improvements were made at the same time (and one feature was withdrawn): see the list below.
Geolocation of images has been added: see below.
New data types: Geographical annotations are a new point type and tours are a new route type.
New offline tool: masher performs some of the simple tasks also performed by routemaster. It may be downloaded onto your own desktop for offline use, reducing your dependence on internet connectivity (and on routemaster). See below for download instructions.
Bring up routemaster, click on the Browse button, and choose a GPX, TCX, FIT, RTE or KML file from your file system. It will be displayed against a map. Various editing operations are available from the iconised buttons at the bottom of the screen and from keyboard shortcuts. Clicking on the cogwheel button and then on ‘Help’ gives you a succinct reminder of routemaster’s functions and a link to full documentation.
When the file is loaded it will be optimised (i.e. the number of points reduced to the minimum needed for accuracy). You can navigate between track points either by clicking on the route, in which case the nearest point will be selected, or by using the left and right arrow buttons to move between points.
If you don’t want the optimisation, you can click on the ‘Undo’ button (backwards arrow) immediately after loading.
If you click on the ‘Waypoint properties’ button (third from the left) you will be able to change some of the point’s properties.
If you click on the fountain pen icon you will be able to add a turn instruction or a label saying (for instance) that the point is a peak or valley. See below.
If you view a track against the satellite image you may decide that you want to adjust the position of a point. Select it and hit the space bar, making it draggable. Drag it to the position you want with the mouse, then hit space again.
To delete a single point, select it and hit the delete or backspace button. You can’t do any harm because the action can be undone.
To delete a set of points you can delete the points one by one. Alternatively you can split the track into segments (see below) so that one segment contains the points you want to delete. You can then delete the entire segment by hitting shifted delete or backspace. Then combine the remaining segments by clicking the broken cogwheel button and choosing ‘Combine with previous/next segment’.
Suppose you have a GPS track for a forest descent, and want to extend it until it reaches the nearest road.
Load the track and select satellite imagery. Point at where you want the first point of the extension, and shift-click with the mouse. Do this repeatedly until you have all the points you want.
Suppose you want to extend the track backwards, to show how you reach it from the road. Then reverse the track, extend it as before, and reverse it back.
To be specific, shift-clicking adds a new point at the end of the currently selected segment, the position of the point being the position of the click.
If you want to add points by interpolation, read on...
If you want to insert a point between two successive track points, select the earlier of them and hit the tab key. This inserts a draggable point midway. Move it with the mouse until you’re happy with its position, then hit the space bar.
You can interpolate backwards by hitting shift-tab, but this doesn’t serve much purpose.
If you want to interpolate a whole detour, split the segment, add points to the end of the earlier part (as above), and recombine.
A waypoint becomes a waymark when you add a turn instruction or similar information. To attach a turn instruction to a point along a track, select the point and hit the fountain pen button. (Note that you cannot add a turn instruction between two points along the track – interpolate first, if this is what you want.)
The information in a waymark comprises a label, drawn from a small set shown below, and a caption, which is a short explanatory text string. There may also be a description, which may be as long as you like; it uses the same mark-up as for route descriptions. The labels (‘Turn left’, ‘Danger’ etc.) correspond to icons and are usually displayed graphically. The caption may be empty. The set of permitted labels differs between TCX and FIT.
routemaster offers a subset of the FIT labels, many of which belong to the TCX set. The ones it offers are:
| Type | TCX? | Meaning/example | ||
| Generic | ✓ | Anything not covered by other available types. Almost always needs a caption. | ||
| Sharp left | ✗ | Eg. 135° turn. Similar instructions for ‘right’. | ||
| Left | ✓ | – | ||
| Slight left | ✗ | Eg. 45° turn. | ||
| Left fork | ✗ | Take the left exit at a forked junction (which may not be an exit left). | ||
| Straight | ✓ | I.e. straight on | ||
| Danger | ✓ | Eg. exposed path. Normally needs a caption. | ||
| Food | ✓ | Eg. a mountain refuge. | ||
| Water | ✓ | Eg. a water fountain. | ||
| Summit | ✓ | A high point on the route (not necessarily a true summit). | ||
| Valley | ✓ | A low point on the route. | ||
| Turn back | ✗ | Sometimes represented by a U-turn. | ||
| Viewpoint | ✗ | The FIT definition uses the Americanism ‘overlook’. | ||
| Steep incline | ✗ | I.e a steep hill, taken in ascent. | ||
| Info | ✗ | Eg. a sign board. | ||
| Obstacle | ✗ | Eg. a locked gate. Normally needs a caption. |
If you use a label absent from the TCX set and then export the course as TCX, then the type will be converted to one supported by TCX: either Left, Right, or Generic as appropriate. Make sure you provide a caption so that the meaning isn’t lost.
I formerly limited the name to 10 characters, but I’ve handled tracks from other sources with longer names and even my Garmin device didn’t throw any problems. So I now allow arbitrarily long names. On the other hand, I have recently relaxed the requirement that you have to provide at least one character since names are optional for FIT coursepoints.
There is an ‘add description’ option under the cogwheel menu. It allows some rudimentary HTML formatting. See more details below.
Splitting a track into segments is useful because it allows you to adjust part of a track as if it was an entire route (e.g. add 10m to its altitude).
Select the first point of the segment after the desired split point and hit the scissors button.
Click on the segment properties button (broken cogwheel) and click on ‘Reverse segment’.
If your route comprises a single segment, this reverses the route. If it comprises more than one segment, you can reverse them separately.
There are many options for supplying and correcting altitudes.
Firstly, if any points have no altitudes, their number will be shown in the route props box (cogwheel menu). You have the option to ‘find altitudes’. This uses the TerrainTap elevations service to estimate the difference in altitude compared with nearby points whose altitudes are known; hence the results will be consistent with those already present, even if these are miscalibrated.
There’s no need to invoke this option manually: when you attempt to save a route, it will be done automatically for you.
Secondly, if you want to change or delete an altitude, you can do so from the waypoint props box.
Thirdly, if you want to delete all altitudes in a segment, you get this option from ‘Adjust altitudes...’ in the segment props box (broken cogwheel menu).
Fourthly, if you happen to know a calibration offset for your points, you can add it by invoking an option available from ‘Adjust altitudes...’. However linear regression is likely to be better.
Fifthly, if your altitudes are not reliable (or you don’t have any), you can get values from TerrainTap.
Finally, if your altitudes have been measured accurately but are subject to calibration error, you can correct them using TerrainTap estimates. This in turn can be done in either of two ways: calibration (adding a constant offset) and linear regression. The latter makes allowance for slow shift in atmospheric pressure but may be affected more by inaccuracy in TerrainTap altitudes.
If your altitudes have been measured barometrically, some form of TerrainTap correction is likely to be the best adjustment, choosing regression for routes of some length, but preferring calibration for short tracks such as MTB downhills.
If there are erroneous altitudes in your track (owing to a device misfunction or poor satellite reception), it is best to delete them before invoking TerrainTap correction since the errors may have a distorting effect.
If you have a (roughly) circular route and want to change its start point:
Load one track into routemeaster and then add the other as a new segment (cogwheel button). Edit the two segments so that they join naturally; if you need to change their order, you can swap them from the Segment Props button (broken cogwheel). When you’re happy, combine the segments (again from Segment Props).
The routes may have differently calibrated altitudes. It’s best to make them consistent before combining them.
The times of the route you finally produce will probably be out of sequence, in which case they will be discarded when you save it. You may find it useful to delete the times in one segment (Segment Props) so that the remaining times are in sequence; this will save having them discarded.
Click on the download button. routemaster performs some repairs, recommends others, and gives you the choice of saving as GPX/TCX/FIT. (See below for their relative merits.)
If you download as FIT from Firefox you are likely to get a message asking what to do with the file – whether to save it or open it. Check the “save” option. To tell Firefox to do this automatically from now on, visit the URL of a FIT file and you will be given the same message, but with a further tick-box requesting to save automatically in future.
To see the tool in action, look at the Monte Tondo circuit we rode in Tuscany in June 2017. By all means experiment with splitting the route, deleting parts of it, etc.: you cant do any harm. You can download the result to your computer. Click photo icons to view them as thumbnails; click on the thumbnails to view full full-size images. The arrow keys navigate forwards and backwards between them, and [return] takes you back to the track.
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
To see a metaindex go to our Tyrrhenia metaindex, which works much like an index but one stage higher.
If a track is on the web, you can invoke routemaster through a URL which brings it up showing the track in question. The URL has the form
https://www.routemaster.app/?track=fullURLhere
So since Garmins sample TCX track is at
https://developer.garmin.com/downloads/connect-api/sample_file.tcx
the URL you need to type is
https://www.routemaster.app/?track=https://developer.garmin.com/downloads/connect-api/sample_file.tcx
This is more useful for HTML links than for direct typing into a browser. More details are provided below.
routemaster is an online tool which relies on online services provided by other parties, notably maps and altitude estimation. Sometimes you need to process a track without internet connectivity. For this purpose the routemasher tool may be used (which contains much of the same code). Of course, if you follow the link to it you get an internet version, but you are free to download it to your own computer (all the software is freely licenced); you may then invoke it from your browser by using the ‘File>Open...’ option.
To download masher, follow the link and invoke ‘File>Save Page as...’; you want to save as HTML only. Then go to the associated javascript library and save it in the same way. Put both masher.html and routemasterlib.js into the same directory on your desktop; navigate to masher.html using ‘File>Open...’, and put a bookmark into your toolbar for future use.
It allows you to preview GPS tracks, to reverse them, and to save them as GPX/TCX/FIT/RTE.
You may create a routemaster account and log in to it: this brings some small benefits and no appreciable drawbacks; you’re recommended to do so if you use routemaster at all often.
The benefits are these:
Once you’ve logged on, you should stay logged on for 3 months, so you won’t have to keep retyping your password. The long duration is justified by the lack of serious consequences if anyone gets onto your account. If you log on from a shared computer, don’t forget to log out when you finish.
Adding new languages is comparatively easy. If you’d like to help, take a look at the list of English prompts and at its corresponding French translation. If you can produce a similar version in a third language, I can extend routemaster’s range. At present I can only accept Romance languages and German because I need to be able to modify the translations myself.
Si mon français n’est pas parfaitement idiomatique, vous pouvez m’aider en m’envoyant par e-mail une amélioration. Il y a un lien e-mail pratique sous le bouton « compte ».
Email me at colin.champion@routemaster.app. I don’t guarantee to make changes but I’m happy to hear from you. If you have an account, there’s a friendly email link under user preferences.
If you encounter a bug, it may be that I’ve been making recent changes. It might be worth performing a hard reload ([shift][cmd]r on an Apple) to make sure you aren’t running a version whose components are out of step.
•
Just a word of advice: I would prefer you to use Firefox than Chrome. Google use their dominance to seek to impose their own objectives on the internet (eg. trying to make plain http unusable); this leads to compatibility problems. I do my best to maintain routemaster under Chrome in spite of their obstacles, but most of my testing is under Firefox.
mapData : [[46.0590010,8.6999490],[46.0589150,8.7002920],[46.0588240,8.7008460],[46.0587990,8.7012160],[46.0587780,8.7015380],[46.0587430,8.7018740],[46.0586840,8.7022120],
You should use .rte for tracks accessed through routemaster. Other formats will work up to a point, but will not retain all the information the tracks contain.
For transfer to a GPS device, you should use the best format the device accepts: the choice between .fit, .tcx and .gpx probably depends on your device.
.tcx and .fit use times to associate turn instructions with positions along a route. This is unsatisfactory because points may not have genuine times (eg. the route may have been plotted from a map), so the times are sometimes fictional. In consequence you don’t know whether a time in a .fit or .tcx track means anything or not.
.fit has the further fault that a point may have a turn instruction or an altitude, but not both; therefore, if you supply turn instructions, you have to supply points with no altitudes. These cause problems later along the line. Garmin could adopt the convention of doubling up: whenever there is a point with a turn instruction, there could be a coincident point with an altitude. Even Garmin Connect does not do this. The result is that importing software sometimes has to obtain the altitude from an elevation service, which is chargeable; so you have to spend money patching up defects in the format.
.tcx, .fit (and .rte) split a turn instruction into a ‘type’ (drawn from a small pre-specified set) and a caption, which is an arbitrary character string (perhaps limited in length). The smallness of the set allows software to associate an icon with each type, which is what you want. (It also allows a device to incorporate translations into different languages.) .gpx allows the type as well as the caption to be arbitrary character strings. This seems more flexible but in fact is the opposite: you can’t do any more with two arbitrary character strings than can with one, but you lose the ability to associate icons with them.
.gpx has a number of attributes associated with turn instructions, including <name&rt;, <desc&rt;, <type&rt;, and <sym&rt;, but they’re all character strings of arbitrary length, and nothing says what purpose they should be used for. On output, routemaster puts the caption into the <name&rt; and two slightly different versions of the label into the <sym&rt; and <type&rt;, but importing software may expect different conventions.
.rte is my own XML format along the same lines as .gpx and .tcx. It treats turn instructions in a sensible way, namely as attributes of points along a track rather than as defining a separate class of point. It is relatively compact, being about half the size of the corresponding .gpx and 5 times smaller than the corresponding .tcx.
It has a documentation page.
A possible danger with using .rte for storage is that it is supported by just one person (your humble servant). You might therefore worry that you will be unable to retrieve information embedded in .rte tracks. For this reason, you are encouraged to download the routemasher offline tool, which you can use to convert from .rte to any other format without any external dependencies. See the associated paragraph for details.
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 iconised image to bring up a thumbnail. The menu gives you the options to edit the photo designator or to view some basic info; click on the thumbnail to view a larger version. To see full-size photos at their best, use routemaster in full-screen mode (requested by an icon at bottom-right of the screen). 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.
Sometimes you want to copy/paste from routemaster. Keyboard shortcuts probably won’t work since routemaster intercepts them. The browser Edit>Copy and Edit>Paste functions should still be available. They’re useful if you’re editing a long description and want to keep a copy.
Post-prandial notches are altitude errors when a device is warming up. They affect Edge 500s when they’re switched on after a pause, presumably due to a bug. If you see an implausible notch (or less commonly, tabletop) in your altitude profile, I cant think of anything better to do than to correct it using the TerrainTap 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 youre 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 props and select ‘delete segment altitudes’. Then combine the segments again (under Route props). You can request TerrainTap to fill in the missing altitudes from the cogwheel button, or wait until you are ready to download at which point you will be prompted.
If one part of a route is superimposed on another (for instance with an out-and-back route) it can be hard to select a point on the desired stretch. But if you open the altitude profile and click on that, there is no ambiguity.
Keep a copy of the original, at least for a while. You never know what may go wrong. If the original is a .FIT file so much the better: its compact; you wont mistake it for an edited version; and routemaster keeps a track of its name when editing.
GPS units record times for each position. If you reorder the segments in a track the times will not be in sequence, and Routemaster will delete them prior to downloading for fear that they cause problems if you use the track for navigation. To avoid this you can delete some of the times yourself in such a way that the remainder are in sequence. Do this by deleting times for individual segments under the ‘segment properties’ button.
You may provide a description for your route, which will be displayed under the cogwheel button and may be edited. It will be stored in an extension field of the TCX/GPX file, but will also be accepted on input from the GPX desc field. It will not be preserved in FIT output.
You may enter the text of your description into an edit window. A limited number of HTML formatting options are available.
<a href="othertrack.gpx">some text</a>
Links should be in track reference form (described in the following paragraph). They may be preceded by a ‘+’ sign, eg.
<a href="+othertrack.gpx">some text</a>
in which case clicking on the link will lead to the specified track being added as a new segment to the track being viewed.
The quotation marks for the href may be omitted since the entire link is already a character string within the XML document. Eg.
<a href=+othertrack.gpx>some text</a>
The link may also be to a standard web page (i.e. not a GPS track), in which case it will be handled in the normal way. (I assume it checks whether the extension belongs to the set { ‘gpx’, ‘tcx’, ‘fit’, ‘rte’ }.)
When a GPS track refers to another track, it does so in ‘track reference format’. This allows 3 forms of reference.
Firstly, the entire absolute URL for viewing the track may be provided, eg.
href="https://www.routemaster.app/?track=https://www.masterlyinactivity.com/routemaster/routes/cotswolds/Notgrove.gpx"
Secondly, you can limit yourself to the URL of the track itself, eg.
href="https://www.masterlyinactivity.com/routemaster/routes/cotswolds/Notgrove.gpx"
Finally, and most conveniently, if you are referring to a track close in the directory structure to the place the reference occurs, you can use a relative URL, eg.
href="Notgrove.gpx"
Relative URLs are expanded to absolute URLs using the location of the track containing the reference. If the track is to be displayed, it is expanded to a routemaster invocation by adding the relevant part of the URL of the current browser location.
Our usual practice is to place images on a map by collating their times with those of a GPS track. We have to do this because our cameras don’t record GPS coordinates. It’s a rather clumsy process, depending on time zones and the setting of camera clocks.
If your camera records location (as all mobile phones do), then a better method is available. Load your GPS track, click on the cogwheel, and select “Position geotagged image”. You’re given a file dialogue, in which you select one (or possibly more) .jpg files. A geo point is created at the location of the geotag, generically waymarked with the image name.
What I expect you to do next is to insert a photo at the appropriate place along the GPS track, and then delete the geo point (using the del/backspace key); but if you prefer, you can retain the geo point, associating an image with it (and presumably deleting the waymark info).
This is nothing to do with routemaster, but it’s worth mentioning here. (However you might prefer to use a mapping tool such as plotaroute.) I learnt the trick from an osm forum.
Suppose I want to convert the OSM representation of the Bwlch Sirwydd footpath into a GPS track.
way(1052419938);out geom;
into the command box on the left and hit the ‘Run’ button at the top. The path number is the one I have copied.
Suppose you have a route, and want to modify it to incorporate a path you can see in the satellite image. Snip the route where you want to add the new points; select the segment preceding the new points; and [shift] click repeatedly where you want to add a point from the satellite image. You will probably want to delete some points from one of the old segments, and when you have done so you will combine the segments.
The feature just described is helpful if you want to create a route from scratch. Unfortunately you cannot start from a blank canvas; you have to import a route, add the extra segments, and then delete the original points.
When I do this, I often stitch together fragments of tracks I’ve downloaded from the internet, creating extra fragments from the satellite view, and sometimes adding others exported from mapping tools such as plotaroute.
When I get round to it I’d like to add a genuine create-from-scratch option. It would be implemented as a third load option besides file browser and URL load. You would type in the track’s location, and routemaster would look it up in a geographical database (presumably nominatim) and then create a route comprising a single draggable point at the specified location.
GPS formats allow you to associate information with a point along a track: altitude, turn instructions, photos, etc. Sometimes you want to record this information as belonging to a point close to a track but not actually on it; and you don’t want to impose an artificial dog-leg to do so. A point not belonging to a track which serves this purpose is called a “geographical annotation” or “geo point”.
To create a geo point, shift-click to extend your current track; go into “waypoint info” (the third button along the bottom) and select “detach point”. You can now edit the point in the same way as you edit a track point. If you shift-click again, then since your current selection is a geo point, the newly created point will also be one.
You have to save the track in .rte format to preserve the geographical annotations.
The format of a uri for viewing a GPS track through
routemaster is given below. If you construct a uri
in this format you can use it as a link in a web page or an email.
You cannot link to a route which has missing altitudes
because if you do so the altitudes will be recomputed every time someone
follows the link. Instead you must put the route through Routemaster so that
altitudes are included.
Even if altitudes are present, its best to use a version that
has been through at least nominal editing in routemaster, if only for the
sake of the optimisation which brings the file down to a reasonable size.
Its a good idea to add a title at the same time and to attend
to any waypoints which are clearly erroneous. If the route starts from your
garden shed and you dont want unexpected visitors, it may be
prudent to truncate the track a little.
You are strongly urged to use RTE format for tracks you put on the
web. It is more compact and retains more information than the alternatives.
You are advised not to put unoptimised tracks on the web, since viewers may not
all see them the same way,
When web tracks are slow to load this is because of their physical
size. They have to traverse the internet twice to defeat the same origin
policy and they may start from a slow server. Keeping them small will keep
your users happy.
I have done all that is possible to reduce the delay: the GPS track
is loaded as soon as its URL is available and other operations take place
in parallel.
This is a free service, in that
my web account incurs all the costs of displaying your track. These costs are trivial for now, but if the web services I use start charging me, I’ll have to
find a way to pass the charges on.
If you buy an MTB guide book there will usually be an associated set of GPS tracks obtainable in some way. Put them in a directory and load them in bulk (ie. go through the file load menu, selecting all of them). You will see a composite track in which each segment is one of the routes in the book. Go to the cogwheel menu and change the route title to something more appropriate; then hit ‘Save track as route index’. If you now load the index you have created you will see the tracks in a more attractive way.
However if you click on them you will find that the links to individual tracks do not work. Upload the directory of tracks to your website (keeping its location secret if the tracks are not your own), and do a global find and replace on the route index to correct the URLs in the TrackLinks. Now if you open the index in Routemaster you will see the index correctly and the links will work (see below).
To load a track into a Garmin:
Formatting errors in Garmins eyes include the absence of any fields
whose presence it insists on (with or without justification) as well as more
obvious errors such as unclosed tags, double quotes etc.
You get no information as to why a track was rejected and
theres no documentation to tell you how to make a track it accepts.
It’s no longer painful to type in the routemaster URL: ‘routemaster.app’ in
the URL bar should be all that’s needed. But it still has a cool bookmark icon.
If a track is on the web, you can invoke routemaster
through a URL which brings it up showing the track in question. The URL has the
form
So since Garmins sample TCX track is at
the URL you need to type is
You may specify some display options as part of the URL by adding a
string of the form “&mode=opts” where opts is a character
sequence in which the meaning of the characters is as follows:
So you might write:
Incidentally I provided the “z” option in the hope that Google would see the
route description when a track is linked, and therefore index the track
accordingly; but it doesn’t, so the option doesn’t serve its intended purpose.
.tcx, .gpx, and .rte files are editable text. If
you load one in a text
editor you will see some blurb at the beginning, then a list of points, then
some additional information at the end, some of which has been added
by Routemaster. You may want to change some of this information – indeed, for
some purposes you have to. It isn’t difficult. Just make sure the ‘<’,
‘>’ and ‘"’ delimiters are opened and closed correctly.
A feature routemaster does not offer (although some other tools do) is advance warning of labelled waypoints. The idea has occurred to me, and been suggested, but upon thought I’ve rejected it.
So I’ve come to the view that routemaster should work within the limitations of GPS devices and formats as they are rather than bypass them in a way which might make lead to compatibility problems. You can of course manually insert warnings.
See above.
routemaster supports two mechanisms for storing photos. Its native
method is that of pix.js. Images supplied through this
mechanism will be displayed against GPS tracks within routemaster:
initially as icons, but also as thumbnails and full-size images as you click
through.
The second method is consistent with the GPX notation for images,
which simply designates them as links, i.e. external documents. The names of
images may be supplied in this format, and the names (but not the images
themselves) will be shown on the GPS track; they will also be exported to
either .gpx or .rte.
The two methods are not entirely compatible. The name of an image
in the pix format will be something like “chorten” whereas under the GPX
method it would have to be “chorten.jpg”. Image names entered in the pix
method will be exported to GPX and vice versa, but it’s unlikely that any
software will be able to use them without some prior editing.
The pix method is a great deal more powerful, providing all
the metadata which an application needs to display a web image flexibly,
attractively and efficiently, but it requires more work from the user.
The first step is to create the XML file needed to specify a photo gallery,
and create versions of each image at a suitable set of sizes.
To create the photos, use your preferred image editing app (eg.
Photoshop or ffmpeg) to create an image at the largest size you will use
(eg. “chorten@5.jpg”). Then run
where the script and C programs are obtained from my
pixrescale library. The ‘5’ in the call line refers
to the suffix of the large image you have produced, and the process assumes
that you use my own set of suffices, my own size ratios, and my own
thumbnail sizes. If you want to differ from these, you might want to modify the
code (or email me).
To create the XML list you follow the instructions in the
pix.js documentation (though in practice you just modify
a previously existing list).
At this point it’s no effort to produce a photo gallery displaying
the pictures in an attractive way; this can be done while everything
is on your own computer. But routemaster needs to fetch files over
the internet, so you need to upload them to a web server to view them against
GPS tracks.
Then, in routemaster, you navigate to the position in a track
at which you wish to insert a photo. You click on the camera button and you get
a prompt for the photo list. You enter its URL. (If you are registered and
logged in, you get a history prompt, which makes it much easier.) Then you are
prompted for the image name; again you enter it, and the photo will be displayed
as an icon and you can click on it to see more. Subsequent photos need only a
single step since the same list will be used for all of them. If the track is
saved in .rte format, then the image information will be retained.
As an alternative, you can upload a photo list from your desktop,
but when you save the track as .rte, it cannot be stored by URL so you
will have a dummy entry which you need to edit when you’ve uploaded the list to
the web.
For the GPX method you don’t need a list file or different sizes of image
(or indeed an image at all, since only the names are handled). You click on
the camera button as for the pix method; when prompted for a list, you
leave the box blank and hit return. Then you enter the name when prompted for
it. The one form of entry which is certainly correct is if you enter the name
as
Since GPX images must be links, and this is certainly a valid link address,
any software which handles the GPX track correctly should display the image
(presumably as a link, i.e. a separate document). However if you enter simply
“chorten.jpg”, and you have the image file in a suitable location, then it may
be interpreted as a relative URL.
To create an index, load the first route into routemaster. Then
load subsequent routes as new segments. Set a route title which will apply to
the index. At this stage the segments will be
connected by dashed lines as if they were parts of a single route.
Click on Save track as route index (under the
cogwheel) and the index file will appear on your computer. If you load the
new track it will view as an index.
When you load the index and click on a route, you will have an
option to view the corresponding track. This relies on the URLs of the
constituent tracks being stored in the index. If, when creating the index,
you loaded them from your own disc, then routemaster will not have
known what URLs to store, and will have supplied a $FILE$
placeholder. You should use a global replace within a text editor to correct
them. The correct format is something like:
You can edit this tag to add a ‘mode’ attribute, specifying the mode with
which the track should be loaded (see url parameters).
Eg.
which requests the Upcote track to be loaded displaying its altitude
profile.
Alternatively you may replace the GPS track in the index by a web
page you want the user to go to when he or she clicks on ‘View’. You give a
‘type’ of ‘html’ to the track link to indicate that you want it to be used in
this fashion, eg.
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 lines
to a track stored in .rte format.
You may attach a title to the index, eg.
When adding a route to an index, you may add another index instead of
a single route. This may be useful if you have a cluster of routes which it makes sense to look at alongside other individual ones. The Sperane tracks in our L. Garda (east) index are an example.
You can add new tracks to an index at any time, or if you’ve changed a track on the web, you can update it as a single operation.
When you select the option to save a track as a route index, it will
be automatically optimised more aggressively since you won’t want to keep as
many points. You can bypass this optimisation by clicking on ‘uindex’ rather
than ‘index’. This makes sense if you want to bundle a small number of tracks
together so that they are shown as a group when you add the bundle to an
index. Perhaps I should rename uindexes to ‘bundles’. Perhaps one day I’ll make
a fully fledged route type out of them. At the moment they’re just indexes
created by a different means: if you add or update a component track, it will
be optimised.
Routes within a tour are displayed just like routes within an index, with
the differences that routes within a tour have an intrinsic order, and that
altitudes are retained. To create a tour, follow the same steps as for
creating an index, but click on ‘tour’ rather than ‘index’.
To create a metaindex, load the first index into routemaster. Click on
Download index as metaindex (under the cogwheel) and the index file
will appear on your computer. The new track will display as a metaindex. You can
now add further indexes to your heart’s content and save the result using the
download button.
You can add new indexes to a metaindex at any time, or if you’ve changed an index on the web, you can update it as a single operation.
The final option is to save a route as segments. This is only
possible if the route comprises more than one segment. It allows you to save
an intermediate file when editing without having to combine the segments.
• data acquisition
• data transfer
• optimisation
• actions list
• TCX and GPX
• Courses and tracks
• mapping
• photos
• altitudes
• downloading
• colouring tracks
• listing hues
• assigning hues
• listing shades
• dialogue boxes
• routemaster uses the relatively new HTML5
FileReader API to upload GPS
tracks from the users 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.
The track is then read in using the
XMLHttpRequest function.
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
routemaster.app (though tracks loaded from the web pass through
routemaster.app).
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
RamerDouglasPeucker 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. Some further background is given below.
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 segment
info menu (the broken 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
parametric. It is quite small, perhaps equating to an error of about
4m over a distance of 20m.
Vertical errors are given slightly more weight than horizontal errors. An
additional parameter (vweight) can be used to adjust the significance of
altitude. It would make sense to make vweight larger, since a 1m error
in total ascent is more important than a 1m error in distance.
Given that the cost function does a good job of adjudicating between sets of
waypoints its 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 was introduced because
Garmin 500s lose their breadcrumb display if a separation is >100m
(as was reported on the Garmin support forum when this existed).
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.
The benefit of dynamic programming is to reduce the work factor from
exponential (for a brute-force algorithm) to quadratic. Quadratic is still
expensive if there are a lot of points. To avoid excessive cost, it may be
useful to perform more than one optimisation pass, reducing the number of points
by a significant ratio each time. Also, if you don’t have a natural limit on the
legal point separation, it makes the optimisation faster (in fact linear rather
than quadratic) if you impose a limit, even if it’s larger than is likely
to make any difference. If a limit is being imposed for this purpose, it should
be a maximum distance along the route rather than as the crow flies; an
additional parameter (maxjump) has been added for this purpose.
Both of these strategies are used when a raw (unoptimised) track is
added to an index. The optimisation takes place in two passes, the first of
which is similar to the optimisation for a normal display, and the second of
which reduces to the final size. A maxjump is imposed to bound the cost of the
second iteration (though this isn’t much anyway).
Dont 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 isnt always obvious what constitutes an editing action
(for instance editors don’t treat changing a selection as an undoable action).
To avoid confusion I dont 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.
My device (a Garmin Edge 500) accepts only TCX tracks; some devices accept GPX (and maybe only GPX). routemaster outputs either format. I can validate my TCX output by using it. I can partially validate the GPX by reading it in (in routemaster and other tools) but cannot be sure that any given GPS device will handle it correctly.
If you learn of any features which don’t work properly on GPX tracks, I’d be delighted if you let me know.
Points may lack altitudes either because a route was input from a
defective GPS track or because waypoints have been added or moved. Whenever
a point is moved or added its altitude is set to null, and when enough null
altitudes have been introduced to justify a call to the elevation service a
call is made. But whenever a download is initiated routemaster tries
to fill in all missing altitudes, however small the resulting batch.
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 Ive retained them I may not know which manual calibration may have
been intended to be applied to them. So maybe I shouldnt worry about
this.
When I started writing Routemaster I had no idea that the W3C had
abandoned the
FileWriter API. I use
Eli Greys
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.
This topic has cost me more effort than the results seem to merit. When an
index is displayed I want to ensure that nearby (and possibly overlapping) tracks are shown in easily distinguishable colours, whereas it doesn’t matter if tracks which are a long way apart have similar colours.
There are two aspects to this: firstly generating a well-separated
set of hues, then assigning hues to tracks. In some cases (such as displaying a
metaindex) there is a further issue of generating a set of shades of each hue.
This section is chiefly for my own benefit. Without it I could never
understand my own code, since comments unsupported by a diagram are as
opaque as raw javascript.
I take hues from a hexagon, which is a view of the colour cube
from infinity along the line (1,1,1). (Prior to 2026 I used the triangle
whose vertices were R, G and B; this excluded some attractive secondary colours
such as orange and purple.) The vertices of the hexagon are labelled by their
indices in the array cycle and their 3-dimensional coordinates on the
colour cube.
I can impose a grid of lines parallel to the sides of the hexagon;
the points of intersection are the colours I shall choose. But since 3 lines
meet at each point, and these overdetermine its location, I construct a
non-orthogonal coordinate system using lines parallel to just two of the sides.
So the various bisections determine a position (ξ,η) specified by distances
from the edge connecting vertices 0 and 1 and the edge connecting vertices
4 and 5. These are easily converted into Cartesian coordinates
(x,y).
Points are produced in a series of generations. The first
generation is made up from lines through the vertices; the second adds lines
through the midpoints of the peripheral edges; and successive generations are
produced by repeated bisection. Each generation includes all the points in
earlier generations, but since I want early points to be chosen before later
ones, when I generate points for any generation I skip over those
which have already been listed. (This is done by ensuring that the indices of
the axes are not both even.)
In the diagram, the intersections of thick lines are the first
generation points, and the intersections of lines which are not both thick are
those added in the second generation.
In order to bound colours away from yellow and green, I discard the
part of the hexagon shaded grey; legal colours therefore come from the
unshaded hexagon (including its perimeter) in the second diagram. Nearly half
the colour space is lost in consequence.
The colour associated with a point is found from its
(x,y) coordinates. These determine the sector of the hexagon the
point lies in, where sectors are triangles formed by the two ends of an edge
and the centre of the hexagon. The colour is then found by interpolation over
the triangle. (There are two slightly different ways in which I could
interpolate parallel to an edge: either linearly along the edge or
proportionally to the angle; I do the second.)
If we don’t use all the points produced in a given generation, I
want to avoid clustering them at one end of the hexagon, so I extract them in
grey-code order. A bit of manual reordering ensures that a natural set of
colours is chosen for any size up to 8; beyond that, you have to take what the
algorithm gives.
A point we have chosen may (in spite of the
avoidance of yellows and greens) turn out to be too pale, so I compute its
brightness using an exact formula, and if it’s greater than 0·65, I
scale it crudely to bring it below that limit.
My method is to set up a proximity matrix recording how close each track is to each other track. I generate a set of colours by
subdividing the colour cube, and associate them with the tracks in order. Then (since no closed-form optimisation algorithm seems to be available) I loop through the pairs interchanging colours if it improves a certain objective function. I stop when no further changes are accepted, or when a certain number of iterations have taken place.
The proximity of two tracks is determined by comparing every line segment of the first with every line segment of the second: the metric I use is the ‘distance between two line segments’ (which, being the minimum distance rather than the average isn’t quite right).
The total work in computing proximities is therefore quadratic in the number of points. In order to keep it from getting out of hand I downsample each track by a factor of 10.
About 250 lines of code are devoted to this task, and they cost me a lot of thought without my having arrived at a very appealing algorithm; and I’m not convinced that the results are entirely satisfactory.
The shades are handled by a set of functions.
RGB colours are stored either as 3-long arrays of values between 0 and 1, or
as hex strings. Arrays of values between 0 and 255 are never used.
Google Maps offers ‘infowindows’ and Leaflet offers popups; I use neither.
Instead I have my own code for infowindows which I use for all user dialogues.
The most fundamental reason is that dialogue windows need to be
coordinated. I split them into two groups: those invoked to perform an action
(known as ‘prompts’) and those which are purely informational. When a prompt
is active it should ‘freeze’ the system, preventing other windows from being
opened. A prompt must have a ‘Cancel’ button but no ‘x’ at the top right
corner: users can decide not to proceed but cannot close the window without
making a decision.
When an informational window is open, it should close itself whenever
another dialogue window is opened. This requires the entire set of dialogue
windows to have a unified implementation.
But there is another split. Some windows are attached to
geographical locations (eg. ‘waypoint info’) whereas others are attached to
positions on the screen (those associated with control buttons). Google
infowindows and Leaflet popups support the first group only.
Prior to 2026 I used Google infowindows for all purposes. The
‘route props’ menu which was supposed to be attached to the cogwheel button
would move with the map if you dragged it. In the first half of 2026 I
corrected this by providing my own infowindow code for static buttons.
When I moved to Leaflet/OSM in the second half of 2026 I found that
I had to expand my own code to manage windows attached to geographical
locations. This allowed me to ensure consistent behaviour of the various
dialogues, but also satisfied an aesthetic purpose. I found the Leaflet popups
rather unattractive, and also intrusive since they impose their own styling
on their contents (see
this complaint).
Also the popups were prone to blinking. If I had a waypoint info popup open and
hit the right arrow key, then the popup would disappear leaving an unadorned
map, then a new popup would appear at the next position. I wanted one to
instantly replace the other.
I then expanded further, since the window in which the user chooses
a mapping layer needs to belong to the same set. (Whether and how Leaflet
makes it consistent with popups I have no idea.) This makes it possible for me
to customise the dialogue; for instance I may one day restrict some mappings to
logged-in users.
And I went one step further, since the scale ‘control’ at the
bottom left is my own (though not an infowindow). This was probably an
unnecessary step. There seems to be a Leaflet bug which prevents its own scale
control from working if the the scale control is created before the layers
control. I wrote my own scale panel to get round the bug, but I found that I had
to reposition it (I’d wanted it at bottom right), and I think that the
repositioning is all I needed to get round the bug.
I still use the Leaflet zoom control, but only after some hacking to
make it consistent with the rest of routemaster. Ideally I’ll replace it
with my own some day.
There’s one frightening potential bug, which is that if there’s some
way for the user to close a prompt window while bypassing the unfreeze step, he
or she will freeze the application itself.
https://www.routemaster.app/?track=fullURLhere
https://developer.garmin.com/downloads/connect-api/sample_file.tcx
https://www.routemaster.app/?track=https://developer.garmin.com/downloads/connect-api/sample_file.tcx
https://www.routemaster.app/?track=https://developer.garmin.com/downloads/connect-api/sample_file.tcx&mode=fz
pixdownscale.sh 5
https://www.mysite.me/images/chorten.jpg
<tracklink href="https://www.masterlyinactivity.com/routemaster/routes/cotswolds/Upcote.gpx"/>
<tracklink href="https://www.masterlyinactivity.com/routemaster/routes/cotswolds/Upcote.gpx" mode="p"/>
<tracklink type="html" href="https://www.masterlyinactivity.com/cotswolds/roughstuff.html#upcote"/>
<index>
https://www.masterlyinactivity.com/routemaster/routes/WesternUK.rte
</index>
<index title="Western Britain">
https://www.masterlyinactivity.com/routemaster/routes/WesternUK.rte
</index>
https://www.routemaster.app/?track=https://www.masterlyinactivity.com/routemaster/routes/montetondo.tcx
|
| Detail=75% |
|
| Detail=85% |
|
| Detail=90% |
|
| Detail=95% |
Optimisation reduces the number of points in a track (as recorded on a Garmin 130+) by a factor of about 20. The result, stored as .rte (a hand-editable text format), is about 15 times smaller than the compressed and uneditable .fit original. The errors introduced by optimisation are smaller than the GPS errors in the original track. This is a powerful reason to optimise.
But it is not why I incorporated optimisation into the design. Without optimisation a track would be difficult to edit. In particular, I can adjust the course of a track by dragging one or two points; if I had to drag 20 or 40 points I’d have to do something else. The arrow keys for moving between points would be almost useless if the points were only a few metres apart.
When I save tracks as an index, tour or metaindex I optimise more ruthlessly, purely to save space. One of my route indexes is 200kB; with no optimisation at all it might be 30MB.
But if you don’t like optimisation of simple tracks, you can reverse it immediately after load by clicking the ‘undo’ button; or you can turn it off completely under your user preferences.
My original reason for imposing a limit on point separation was a report by Garmin that Edge 500s worked better if the gap was never greater than 100m. They were probably making this up; anyhow not many people use Edge 500s any more. But I’ve retained the maxsep for the same reasons as I use optimisation: it improves editing. For one example, when you click on a track you expect to select a nearby point, not a point 2km away. For another, if you have 2km gaps in a track, then the altitude profile is unlikely to be accurate. But again you can turn the feature off.
So my conclusion is that for painless editing of a track, you want to avoid having points which are needlessly close to each other or uncomfortably far apart. But the best points to keep are spaced strategically rather than regularly.
Incidentally the maxsep feature speeds up the optimisation. If there was a demand to run routemaster with optimisation but without maxsep, then I would need to implement the optimisation in two rounds. (Optimising to 90% and then to 75% is faster than optimising directly to 75%, with a loss of accuracy that is probably indiscernible).
There are valid reasons not to optimise. routemaster assumes that your track is being used for navigation, but it may have other uses. It may be a scientific record, or you may just enjoy looking at it.
The small images on the left show the results of different levels of optimisation on the same GPS track of a mountain-biking descent. There is some satisfaction in dwelling on the tightness of the hairpins.
The grey track is the unoptimised original. The red dots on a yellow background are the path as displayed on OSM. The solid red line is the track as optimised.
We notice that the unoptimised track is smooth and plausible. Although there are errors of 5–10m, they are systematic within the range covered so that the shape is faithfully represented. On the other hand, the fact that the recorded track is 5–10m north of the OSM representation may reflect nothing more than GPS inaccuracy.
The second thing to notice is that 75% detail gives a fairly schematic picture of the hairpins while 95% detail is essentially exact. If you want to relive the moment at which you corrected your balance on a 1m bend, then you may prefer a high level of detail.
The last thing to notice is that even at 95% detail the track is reduced in size by almost two thirds. Unless you want a scientific record, I therefore find it hard to imagine reasons for not wanting optimisation, though in special cases you might want an unusually high level of detail.
I decided in September 2026 to move from Google Maps to Leaflet/OSM. At the time my main reason was that I felt embarrassed by the cartographic quality of Google mapping, but other experiences have justified the decision.
As I was developing the new version I found that Google had suspended my account (while I was on holiday, and with no email notification). I don’t really know why. I was using the free OSM tiles for development, but I’d used some Google tiles to try out alternative sources because I had an API key for them. There would only have been a handful of map loads. I imagine that Google detected this and considered it a breach of their terms. It caused me considerable pain because it left me with no working version; it also forced me to move away from Google’s elevations service faster than I’d intended.
I use Leaflet in place of the Google Maps API. On the whole I’m happy with it. It needed quite a lot of changes to my code, some of which are structural. Its design assumes that you will attach something to the map or remove it whereas Google assumes that you will attach something to the map or attach it to null. I carried this through into routemaster, which required some reorganisation but makes the code more logical.
As mentioned in the previous section, I took info windows out of the mapping API’s hands. This was desirable in any case, but was forced on me by some limitations in Leaflet. The only loss in adopting Leaflet is that the map is dragged in two steps: first the map moves, then the track is drawn onto the newly exposed areas. A significant improvement is having a one-click zoom control in place of Google’s new, clumsy (and short-lived) two-stage request.
Leaflet markers do not have a setTitle method, which seems to me an unfortunate omission. It’s more an obstacle to conversion than a genuine lack of functionality: you can always replace the marker with one with a different title.
I used OSM’s free tiles during development, and they’re still available for testing, but for operational use I adopt Thunderforest’s Outdoor and Atlas tiles. They’re similar to the OpenCycleMap tiles I use in the OSM viewer. There’s obviously some bug in their generation since labels get truncated at tile boundaries, but they’re attractive and contain the information I want. I found Thunderforest’s web site to be clear and well documented, and its pricing honest.
I was expecting to offer a wider range of tiles, but most tile providers are companies with catalogues of sophisticated GIS toolsets for whom tile provision is no more than a sideline. I found it time-consuming trying to get the information I needed from them. Lots of snazzy promotional videos though.
It seems that ESRI’s tiles are openly available to anonymous users, but they can’t intend this as a free service and I suspect that anyone who uses it will get blocked in some way. I couldn’t penetrate their documentation to find out how to set up an account, so I adopted Mapbox instead. I find their website easier to navigate; my only concern is that they require a credit card and don’t allow users to limit their liability in the case of unexpected demand (which could arise through a theft of credentials). I need a burner card. I make some attempt to protect my API key.
I first looked at open-elevation but it seems no longer to be operational.
Then I turned to Open Topo Data and Terrain Tap. Both are attractive services which limit requests to one per second, allowing either 100 or 512 locations per request. If a 1000-point track comes without altitudes, then asking Open Topo Data to supply them will take 10s; this is my main reason for preferring Terrain Tap. If I need to move beyond the free tier, Terrain Tap will be much cheaper.
The APIs are easy to use. Open Topo Data needs to be accessed through a proxy (I use my php file server), and in practice I use the same proxy to send requests to Terrain Tap, though this may be unnecessary. The Terrain Tap API requires lat/long pairs to be sent through the request URL, which becomes uncomfortably long; no doubt its programmers have verified that their infrastructure doesn’t complain.
It is likely that both Open Topo Data and Terrain Tap supply more accurate estimates than I was getting from Google, whose intrinsic accuracy is unlikely to be better, and who downgrade accuracy for bulk requests.
To minimise problems caused by the limit of one request per second I added some code to routemaster. If it needs to fetch more than 512 altitudes, it sleeps for a second between batches. The proxy retries up to 6 times if it gets 429’d.
In the course of the migration I converted icons from SVG to HTML canvas. Arcs are parametrised differently in the two notations, and software is needed to convert from one to the other. (I should probably use arcTon in most cases.)
The following tool, based on an entry in stackexchange, allows you to enter an SVG circular ‘A’ arc, preceded by its start point, and will display the equivalent canvas arc in the second box.
The startpoint, midpoint and endpoint of the arc will be console.logged as confirmation.
My understanding of FIT format derives from a number of sources:
In the course of learning about FIT I wrote a simple C program hex.c to walk through a FIT file, printing out definitions, records, and course points. I include it at the end of this page.
• to do • revisions history • testing • licence
Converted from Google Maps to Leaflet/OSM. See Leaflet/OSM changes above.
Complete revision of the user dialogues, all of which now use the infowindow. Some bugs fixed, but look out for new ones!
Numerous small changes to the internal logic, and to the menu boxes, all of which are intended to leave the functionality unchanged. (But in practice I fixed some bugs, and dare not hope not to have introduced any.)
I rationalised the handling of shades. Some bug fixes.
I added a basic image capability for GPX tracks. I also now use my own info windows in place of Google’s for the control buttons. Some bug fixes.
Recent changes have the consequence that GPX and TCX tracks created in old versions of routemaster, using fields which are no longer supported, may not display all the information contained within them.
I now accept point descriptions for input/output and allow them to be edited. I retain turn instructions in .gpx output, using <sym> for the label and <type> as a (slightly divergent) copy. I have added several new turn instructions and deleted First aid to make room for them. I removed the optimisation code to smooth altitudes: it could conceivably reduce accuracy, cannot increase it by more than a smidgen, while it added to the complexity. I added route descriptions to masher; this needs more attention.
I attempted a fix of the shift-key problem. A Dutch correspondent (Willy) told me of bugs in the account creation, which I hopefully fixed. I made it possible to load an index as a new track when viewing a regular track, and added a ‘Load new track’ option when viewing an index. Any of these changes may have brought in new (but hopefully smaller) bugs. I also added the ability to make the lines thicker or thinner in indexes, and added a ‘creator’ to GPX output (which is obligatory – this too is thanks to Willy). I fixed a bug in dupseg (the cloning needed more care for photos and waymarks) and in the colour generation.
I added the bulk undo/redo, and fixed a couple of bugs it brought to light. I’ll reduce the amount of clicking through another time.
I removed the option to save as segments, making the normal save button available for multi-segment tracks. I ceased disallowing adding routes to metaindexes (which seemed to work – the disallowal seemed capricious) and made a fix for bounding boxes spanning 180°.
I improved the presentation of altitude profiles for flat tracks and removed a bug in the “delete’ option of the waypoint info button.
I gave routemasher the ability to edit a title. This required a small internal change to routemaster. I implemented a better method of choosing hues in routemaster and fixed a small bug in retitling.
Separated out the eye button from the cogwheel button for indexes. (The benefit is that I can add new features to the cogwheel button without confusing the user who wants to see the table.) Fix a bug in reporting unsaved changes for indexes. Default detail is now 75.
Add image geolocation. Add report of cumulative distance and ascent to waypoint info.
Cut out a prompt from the download sequence.
Add geo points and tours; perhaps a couple of bug fixes along the way. More natural default filename (can remove the dialogue box next time).
Several minor fixes to allow better display of singleton segments. Code reorganisation splitting prefsprompt out from textprompt. Correct the examples to experiment with.
Several bugfixes/tweaks. [return] wan’t returning from images correctly (in fact this was a Google Maps bug). I added a telltale. Prompts to fill in missing altitudes were being given when they weren’t needed. Addition of turn instructions wasn’t always undone correctly. I’d ill-advisedly removed the option to delete all turn instructions. I’d lost the ability to undo an optimisation.
Added the ability to read multi-layer kml, and to get the titles and descriptions from kml files.
A sizeable update. I restructured the code so that routemasterlib.js is shared by routemaster and routemasher. I replaced the bus png by parametric svg. I added the ability to save and load files as segments (so as to reinstate the ability to read the remote tcx file correctly in the test procedure). I grey out the download button when it is not activable (sligthtly dangerous if I lose track). I moved to advanced markers with icons for photos – turnpoints should be easy.
Later in the month I encountered a bug: I’d removed the code which enables routemaster to use coursepoints in FIT files which have no lat/long. I wrote some new code for the purpose. Meanwhile, since I produced coursepoints with no positions, and they pose an extra problem for importing software, I stopped producing them.
I added a login button to the entry page and wrote routemasher, plugging it from routemaster and from this documentation.
I fixed a number of bugs, updated for the latest version of pixlib.js, and at last added a graphical presentation of gradient to the waypoint info. I added the ‘n’, ‘o’ and ‘p’ options and the ‘mode’ attribute of tracklinks. I added altitude smoothing and fairly thoroughly debugged the user preferences, adding decimetre precision as suitable for hillclimbs. I also added the forward distance in wpinfo.
I rewrote listhues to avoid hues in the green–cyan range which clash with the background green on Google maps. And I added titles to <index> tags.
I had a rude shock when I discovered that sometimes my new Garmin generated so many nearly coincident points that my optimisation algorithm appeared to freeze. I added a prefilter which squeezes out points which are too close. An optimisation algorithm which handles the case gracefully would be better.
I added a rudimentary csv input option. I can improve it if I have need.
A sizeable update has taken place. Watch out for bugs! This is mostly driven by finding out that FIT is the only file import format supported by current Garmins. I have given it full support (for output as well as input) in routemaster; I added .rte format, and restructured all the code relating to the storage of indexes.
Removed a bug from the segment duplication. Added compressed timestamp FIT files. Fixed the file server to get faster response.
Added the ability to duplicate a segment.
Added registration/login and made routemaster.app the default version.
I added the ability to incorporate HTML tags into route descriptions and the option to generate an HTML list of routes. I also added the ‘mode’ parameter to URL invocations. I made the index optimisation slightly more aggressive and allowed it to be bypassed by shift-clicking rather than through a confusing prompt.
I broke out the text strings into a separate file while merging most of the rest into a single huge routemaster.js. I fixed some bugs but probably introduced some more.
I added a link to the relevant doc to the altitude adjustment menu and to the fountain pen prompt.
I added the option to correct altitudes by linear regression against Google, and improved the description of the altitude options.
I fixed a bug in undoing a combine and another in the bulk load if the files were returned out of order. I improved the display of the altitude profile in cases when there were points without altitudes (though it could still be better) and made allowance for faulty tracks in which points have the same times.
I also made it possible to include line breaks (and < and > signs) in the description.
I made routemastercm, wrote some initial code for registration/logging in,and fixed a bug in interpolating at the end of a segment.
I added options to show/hide the index arrows.
I slightly improved the placement of arrows on index tracks. I did some optimisation of the load process, substituting HTML canvases for the button icons and rolling up the third-party scripts into a single file ‘routemasterextra.js’. In the process I noticed and corrected a bug (actually a misunderstanding) which led to the altitude profile not being as sharp as it should be on high-res screens.
Add rudimentary kml support. (I encountered a KML track on the web, and my favoured conversion utility had become cumbersome to use.) No guarantees. I also fixed a bug affecting the display when dragging a waypoint and implemented the optimisation of precomputing the distances between successive points (which are used lots of times, so may as well be computed only once). And there was another, even more minor, bug affecting the title of fit tracks.
A very minor change to pick up the latest version of pixlib.js, which provides a menu button for enlarged images.
Two changes in Oct. The first, which required some rethinking of the logic, was to allow subindexes to be included in indexes. The second is to propagate up the identity of the picture page and to link it from the cogwheel menu for routes and indexes, and from the info box for each index in metaindexes.
The first change wasn’t carried to completion: it is not yet possible to update a subindex. But that’s easy to fix.
In Nov I rewrote the squash() function to avoid use of Object.assign() because the obsolete Safari on my laptop doesn’t seem to like it. At the same time I fixed a couple of what seemed to be bugs.
The first of these was in response to a request from a user (I have users!). The second violates the tcx standard, but I’ve loaded routes successfully which had text longer than 10 chars. (It would take a genius to make problems out of this, but Garmin employ geniuses.)
There are dozens of options and features and I cant test them all every time I make a change; be patient if you encounter a bug (and send me an email).
Its useful to be able to verify that I can load files from a
variety of sources: these links help:
However these relate to the ability to load different formats, and therefore to parts of the software that seldom change nowadays.
A more significant concern is to make sure that routemaster is compatible with different browsers. I tested it in Nov ’19 in Firefox, Chrome, Opera and Safari. It only partially worked in the last of these, but my version dates from 2014 and I suspect the problem lies in the browser not being supported by current versions of the Google maps API. I removed a call to Object.assign() which I had noticed caused problems in pix.js.
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 early in 2016.)
My practice has fluctuated, occasionally making large parts of routemaster open source and then withdrawing them. It’s best for the parts which define interfaces to be shared, so now that I’ve split them off from the internals (as routemasterlib.js) they’ve gone back to being open source. (One of the drawbacks of issuing code as open source is that you place yourself under some onus to retain compatibility; also, releasing the whole of routemaster would provide people with a means of circumventing online mapping suppliers’ licence policies.) pixlib.js is likewise open source for similar reasons.
You are of course free to use routemaster as supplied, and to consult its source code for information (nothing is hidden).
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 peoples shared routes, and which could be used in conjunction with paper maps and route descriptions. I did not want inbuilt maps because I didnt 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 has several faults.
When I’d lost patience with the tendency of maps to blank out I bought an Edge 130+ as a replacement. It appealed to me because it made use of additional satellite networks and wasn’t overloaded with unwanted new features. But I’m not entirely happy.
• routemaster.js • rm.en.js • rm.fr.js
• function • union • adapt • newcanvas • getscale • displayscale • resizer • linepath • arfunc • widfunc • greyout • blackout • bulkout • redrawbtn • enterFullscreen • exitFullscreen • findimg • unsavedmsg • filedialogue • tagger • submit • hlist • btnstyle • btncol • btnbox • prompter • accept • alerter • selmarker • selpoint • distrec • segdist • highlight • unhighlight • walkto • moveto • keystroke • pointstep • shiftkey • undraw • redraw • drawprofile • drawpoint • draw • drawfull • geodraw • ungeodraw • disconnect • connect • redrawconnect • lconnect • ldisconnect • unsel • undrawsel • drawsel • circlecanvas • arrowcanvas • checklostedits • genpage • getlist • getimgtitle • render • layermenu • layerfactory • silently • genbutton • acfactory • addload • refresh • refresher • renfactory • wpinfo • rellist • dl • cofactory1 • cofactory • canceldl • indexdl • confirmeddl • semiconfirmeddl • reconfirmeddl • optimaction • optimparms • pen2detail • optimwork • dgen • posit • vender • unshadow • optimfactory • redelta • retitle • respond • setdesc • restars • routeinfo • eyedisplay • calwork • manualcal • docal • googlecalwork • googlecal • googlereg • googleadd • help • sidestep • wpdelwork • wpdel • undowpdel • revsegwork • revseg • dupsegwork • dupseg • insert • inswp • insgeo • geotag • getexifgps • readexiftag • getexifstring • draggit • dragger • undraggit • seginfo • altinfo • deltimes • interpol • extrapts • combine1 • combinework • combiner • combine • combinef • combineb • uncombine • setalt • doalt • delalts • waymarkprompt • unlabel • editlabel • undoeditlabel • photoprompt • photoadd • photomerge • photoedit • phollowon • phadvance • phretreat • next • prev • backtogps • display • dodisplay • imgwalk • simulate • phinfo • snipwork • snip • binwork • discard • undiscard • swapsegwork • swapseg • actiontype • done • donesomething • optimswap • refreshswap • photoswap • undo • undofactory • bulkundo • confirmedundo • setptpos • redo • redofactory • bulkredo • confirmedredo • actionname • distptseg • distsegseg • d3 • dsseg • segprox • assigncolours • cdif • greycode • brightness • hexify • hexdigit • listhues • extendhues • genshades • snipcolour • reshade • shadelims • shadesofhue • segbounds • getprox • ascify • getalts • doelevations • elevator • getbounds • promoteprops • loadtrack • neq • callindexify • genarrows • markarrows • parsedesc • pluralise • waymarkcanvas • genphotomark • imgfactory • sqr • phototitle • interp • getllpt • extendphoto • err • gatherpix • thumbpix • writeindex • indexify • textdiv • highdiv • delfactory • updfactory • genpicimg • serve • scrolltype • scroller • buttons • btncanvas • buttoncell • textcell • appendrow • genlink • genindexlink • updateacbtns • logoutfunc • logout • logoutclose • loggerouter • acmenu • logoutfactory • doprefs • bugreport • acfollowon • genbtns • emailvalidate • pwdvalidate • otkvalidate • loggedin • phpresponse • formresponse • blurbdiv • rmhelpdiv • addbull • genpixlink • cogwheelmenu • addloadfactory • dlfactory • arfactory • fs • doalts • deltimesfactory • swapsegfactory • seginfodiv • altinfodiv • doclink • walktodiv • displayfactory • pheditfactory • phinfofactory • photoprompter • eyemenu • highfactory • addcell • listroutes • wpinfodiv • copyfactory • altfactory • wpfactory • kmspan • titlediv • titlefactory • genclickfn • genspan • speaklang • freeze • unfreeze • unblock • waitprompt • prefsprompt • flagcanvas • cclick • pclickfactory • defaulter • waymarkdiv • drawlabel • iconsel • iconfactory • submitfn • genbox • addbox • genldiv • tdivadd • intise • starsline • createfunc • starfactory • drawxcur • toggleprofile • telltale • wipetale • zonktale
var segments=[],selected=null,actions=[],nactions=0,dragging=0 ;
var unsavedchanges=[] ;
var mapdiv,routeprops,map=null,imgdiv=null,imginfo,showarrows=1 ;
var scissorsbtn,undobtn,redobtn,penbtn,setbtn=null,dlbtn,segbtn,wpbtn ;
var acbtn=null,photobtn=null,eyebtn=null ;
var promotable = { title:1 , desc:2 , origin:3 , index:4 , srcid:5 ,
stars:6 , info:7 , gallery:8 , stats:9 , tlink:10 ,
filename:11 , tmode:12 , ttype:13 ,
photo:20 , smallphoto:21
} ;
var intparms = {tol:15,maxsep:null,wppenalty:700,vweight:0,maxjump:200} ;
var indparms = {tol:0,maxsep:null,wppenalty:100000,vweight:0,maxjump:5000} ;
var metaparms = {tol:0,maxsep:null,wppenalty:100000000,vweight:0,maxjump:10000};
var parser = new DOMParser() ;
var infowindow =
{ type: null ,
divvy: null ,
div2: null ,
pos: null ,
prompt:null ,
onclose:null ,
retval:null ,
reposition: function()
{ var sl0=0 , sl1=0 , r = this.divvy.getBoundingClientRect() ;
var w = window.innerWidth , h = window.innerHeight ;
if(r.left<0) sl0 = Math.min(10-r.left,w-r.right) ;
else if(r.right>w) sl0 = Math.max(w-r.right-10,-r.left) ;
if(r.top<0) sl1 = Math.min(10-r.top,h-r.bottom) ;
else if(r.bottom>h-12) sl1 = Math.max(h-r.bottom-22,-r.top) ;
if(sl0||sl1) map.panTo(map.containerPointToLatLng({x:w/2-sl0,y:h/2-sl1})) ;
} ,
triangle: function(dir)
{ var c = dir<0?newcanvas(12,25):newcanvas(25,12) , ctx = c.getContext('2d') ;
ctx.fillStyle = 'white' ;
if(dir>0) { ctx.lineWidth = 1 ; ctx.strokeStyle = 'rgba(0,0,0,.3)' ; }
ctx.beginPath() ;
if(dir<0) { ctx.moveTo(0,0) ; ctx.lineTo(12,12.5) ; ctx.lineTo(0,25) ; }
else { ctx.moveTo(0,dir) ; ctx.lineTo(12.5,12-dir) ; ctx.lineTo(25,dir) ; }
if(dir>0) ctx.stroke() ;
ctx.fill() ;
return c ;
} ,
cross: function()
{ var c = newcanvas(16,16) , ctx = c.getContext('2d') ;
c.onclick = infowindow.close ;
ctx.strokeStyle = '#222222' ;
ctx.lineWidth = 1.5 ;
ctx.beginPath() ;
ctx.moveTo(2,2) ;
ctx.lineTo(14,14) ;
ctx.stroke() ;
ctx.moveTo(2,14) ;
ctx.lineTo(14,2) ;
ctx.stroke() ;
return c ;
} ,
open: function(s,pos,nox,onclose)
{ var c=null,spos,y,u=0,lpos,bpos ;
this.close() ;
this.pos = pos ;
if(onclose) this.onclose = onclose ; else this.onclose = null ;
if(typeof(nox)=='string') { this.retval = nox ; nox = null ; }
else retval = null ;
// create the cross; find the position of the box
if(pos=='blurb'||pos=='layers')
{ if(!nox) c = domcreate('p',this.cross(),'style','text-align:' +
'right;margin:0;cursor:pointer;padding-bottom:4px') ;
if(pos=='blurb') spos = 'top:36px;right:0' ;
else spos = 'bottom:55px;right:56px' ;
}
else
{ if(!nox) c = domcreate('div',this.cross(),'style',
'width:16px;position:relative;left:calc(100% - 16px);'+
'cursor:pointer;padding-bottom:4px') ;
if(typeof(pos)=='number')
{ if(segbtn) y = 144 ; else { y = 48 ; if(pos>5) pos -= 5 ; }
lpos = 'left:calc(50% + ' + (pos*32-y) + 'px)' ;
bpos = 44 ;
}
else
{ map.on('move',function()
{ var p = map.latLngToContainerPoint(infowindow.pos) ;
var y = window.innerHeight - p.y ;
infowindow.div2.style.left = infowindow.divvy.style.left = p.x+'px' ;
infowindow.divvy.style.bottom = (y+12) + 'px' ;
infowindow.div2.style.bottom = y + 'px' ;
} ) ;
pos = map.latLngToContainerPoint(this.pos) ;
bpos = window.innerHeight - pos.y ;
lpos = 'left:' + pos.x + 'px' ;
}
spos = 'bottom:'+(bpos+12)+'px;'+lpos+';transform:translateX(-50%)' ;
}
// create a box containing the cross at the desired position
this.divvy = domcreate('div',c,'style','position:absolute;z-index:700;' +
'padding:7px;background:white;border-radius:5px;box-shadow:'+
'0 2px 6px rgba(0,0,0,.3);font-weight:300;font-size:13px;' + spos) ;
// add the window contents
domadd(this.divvy,s) ;
domadd(body,this.divvy) ;
// create the triangular pointer
if(pos=='blurb') { u = 12 ; spos = 'top:24px;right:2px' ; }
else if(pos=='layers') { u = -1 ; spos = 'bottom:106px;right:44px' ; }
else spos = 'bottom:'+bpos+'px;'+lpos+';transform:translateX(-50%)' ;
this.div2 = domcreate('div',this.triangle(u),'style',
'position:absolute;z-index:700;'+spos) ;
domadd(body,this.div2) ;
// pan map to fit window within it
if(typeof(this.pos)=='object') this.reposition() ;
} ,
close: function() // infowindow is invoked externally (by infowindow.close())
{ if(map) map.off('move') ;
if(infowindow.divvy)
{ body.removeChild(infowindow.divvy) ; infowindow.divvy = null ; }
if(infowindow.div2)
{ body.removeChild(infowindow.div2) ; infowindow.div2 = null ; }
if(infowindow.onclose)
{ var c = infowindow.onclose ; infowindow.onclose = null ; return c() ; }
else { c = infowindow.retval ; infowindow.retval = null ; return c ; }
}
} ;
/* -------------------------------------------------------------------------- */
function union(r,s) // union of two bounds
{ function adapt(x)
{ while(x>180) x -= 360 ; while(x<-180) x += 360 ; return x ; }
var rsw = r.getSouthWest() , rne = r.getNorthEast() ;
var ssw = s.getSouthWest() , sne = s.getNorthEast() ;
var rminlng = adapt(rsw.lng) , rmaxlng = adapt(rne.lng) ;
var sminlng = adapt(ssw.lng) , smaxlng = adapt(sne.lng) ;
if(rmaxlng<rminlng) rmaxlng += 360 ;
if(smaxlng<sminlng) smaxlng += 360 ;
var minlng = Math.min(rminlng,sminlng) ;
var maxlng = Math.max(rmaxlng,smaxlng) ;
var minlat = Math.min(rsw.lat,ssw.lat) ;
var maxlat = Math.max(rne.lat,sne.lat) ;
return L.latLngBounds({lat:minlat,lng:minlng},{lat:maxlat,lng:maxlng}) ;
}
function newcanvas(w,h)
{ var c , r = window.devicePixelRatio||1 ;
var sty = 'width:' + w + 'px;height:' + h + 'px' ;
c = domcreate('canvas',null,'width',w*r,'height',h*r,'style',sty) ;
c.getContext('2d').scale(r,r) ;
return c ;
}
// https://wiki.openstreetmap.org/wiki/Zoom_levels
function getscale() // scale in metres per pixel
{ return 40075016 * Math.abs(Math.cos(map.getCenter().lat*Math.PI/180)) /
Math.pow(2,map.getZoom()+8) ;
}
function displayscale()
{ var npix=120 , scl=getscale() , m=npix*scl , div ;
var base=Math.pow(10,Math.floor(Math.log10(m))) , r=m/base ;
if(r>=5) base *=5 ; else if(r>=2) base *= 2 ;
// base is the length in metres of the scale control
npix = Math.floor(0.5+base/scl) ;
if(base>=1000) r = Math.floor(base/1000) + '\u202fkm' ;
else r = base + '\u202fm' ;
if(!displayscale.div)
{ displayscale.div = domcreate('div',null,'style','position:absolute;' +
'z-index:700;padding:2px;background:rgba(255,255,255,0.8);' +
'left:4px;bottom:4px;border-left:2px solid black;' +
'border-right:2px solid black;border-bottom:2px solid black;' +
'font-size:90%;padding:2px 2px 0') ;
domadd(body,displayscale.div) ;
}
div = displayscale.div ;
while(div.childNodes.length>0)
div.removeChild(div.childNodes[div.childNodes.length-1]) ;
domadd(div,r) ;
div.style.width = npix - 6 ;
}
/* -------------------------------------------------------------------------- */
function resizer()
{ if(imgdiv) return ;
var isfullscreen = queryfullscreen() , fse ;
if(mapparent==null||body==null)
mapparent = body = document.getElementsByTagName("body")[0] ;
if( isfullscreen>0 && !resizer.wasfullscreen
&& mapparent==body && segbtn )
{ fse = document.fullscreenElement || document.mozFullScreenElement
|| document.webkitFullscreenElement || document.msFullscreenElement ;
if(fse&&fse!=body)
{ mapparent = fse ;
if(drawpro.pro)
{ body.removeChild(drawpro.pro.prodiv) ;
fse.appendChild(drawpro.pro.prodiv) ;
body.removeChild(drawpro.pro.curdiv) ;
fse.appendChild(drawpro.pro.curdiv) ;
}
window.removeEventListener('resize',resizer) ;
}
}
resizer.wasfullscreen = isfullscreen ;
}
/* -------------------------------------------------------------------------- */
function linepath(seg,start,end,colour,width)
{ var i , pts=seg.pts , len ;
if(!start||start<0) start = 0 ;
if(end&&end>0) len = end - start ; else len = pts.length - start ;
if(!width) width = 2 ;
this.path = new Array(len) ;
for(i=0;i<len;i++) this.path[i] = pts[start+i].pos ;
this.opts = { color:colour , weight:width , interactive:false } ;
}
function arfunc(opt)
{ if(opt) recurse(segments[0],'showarrows') ;
else recurse(segments[0],'hidearrows') ;
showarrows = 1 - showarrows ;
}
function widfunc(opt)
{ recurse(segments[0],'undraw') ;
if(!draw.ponent) draw.ponent = 0 ;
if(opt) draw.ponent += 1 ; else draw.ponent -= 1 ;
recurse(segments[0],'draw') ;
}
/* -------------------------------------------------------------------------- */
/* UTILITY FUNCTIONS */
/* --------------------------- button handlers ----------------------------- */
function greyout(btn) { if(btn) redrawbtn(btn,1) ; }
function blackout(btn) { if(btn) redrawbtn(btn,0) ; }
function bulkout(black,andset)
{ var f,btn ;
if(black) f = blackout ; else f = greyout ;
f(segbtn) ;
f(wpbtn) ;
if(black==0||(selected&&selected.ptno&&!selected.type)) f(scissorsbtn) ;
f(penbtn) ;
f(photobtn) ;
if(black==0||nactions>1) f(undobtn) ;
if(black==0||nactions<actions.length) f(redobtn) ;
f(dlbtn) ;
f(acbtn) ;
if(andset) f(setbtn) ;
btn = render.layerbtn ;
if(btn)
{ btn.ui.onclick = black?btn.handler:null ;
btn.ui.style.cursor = black?'pointer':'default' ;
}
}
function redrawbtn(btn,opt) // 0<->black 1<->grey -1<->redraw as is
{ if((!segbtn&&(btn.index==2||btn.index==4||btn.index==5))) return ;
var i ;
for(i=0;i<btn.ui.childNodes.length;i++)
{ node = btn.ui.childNodes[i] ;
if(node.tagName=='CANVAS') btn.ui.removeChild(node) ;
}
if(opt==0||(opt<0&&btn.active))
{ btn.ui.appendChild(btn.blackimg) ;
btn.ui.onclick = btn.handler ;
btn.ui.style.cursor = 'pointer' ;
btn.ui.title = btn.blacktitle ;
btn.active = 1 ;
}
else
{ btn.ui.appendChild(btn.greyimg) ;
btn.ui.onclick = null ;
btn.ui.style.cursor = 'default' ;
btn.ui.title = btn.greytitle ;
btn.active = 0 ;
}
}
/* ------------------------ 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,j,ll ;
for(i=0;i<imginfo.sect.length;i++)
for(ll=imginfo.sect[i].list,j=0;j<ll.length;j++)
if(ll[j].name==id) return [i,j] ;
return null ;
}
/* ------------------- message warning of unsaved changes ------------------- */
function unsavedmsg(ok)
{ var msg , len = unsavedchanges.length , i ;
if(len==0) return null ;
msg = pluralise(M.unsavedchanges,len) ;
if(len<=3) for(i=0;i<len;i++)
msg += (i?',':' (') + unsavedchanges[i] + (i==len-1?')':'') ;
msg += '\n' + M.ifyouhit + ' [' + (ok?M.ok:M.leavepage) + '] ' ;
return msg + pluralise(M.willbelost,len)
}
/* ----------------------------- file dialogue ------------------------------ */
function filedialogue(ovr)
{ var i,n,input,lab,extn,div,filebox,lab,para,inp,listuri,trackuri ;
var splash=(ovr=='newload') , check=(ovr=='checklist') ;
if(check) ovr = 'list' ; else if(splash) ovr = 'load' ;
para = domcreate('p',null,'style','margin:0') ;
div = domcreate('div') ;
lab = domcreate('label',null,'for','filedialogue') ;
// file browser -- no file load of list
if(ovr=='list')
domadd(div,lab,domcreate('p',M.loadlist,'style','margin:0 0 2px')) ;
else
{ if(ovr=='geotag') domadd(para,M.selectjpg) ; else domadd(para,M.select) ;
input = domcreate('input',null,'type','file') ;
if(ovr=='geotag') domset(input,'accept','.jpg') ;
else domset(input,'accept','.tcx,.gpx,.kml,.fit,.rte,.csv') ;
domset(input,'multiple','multiple') ;
input.addEventListener('change',function(e)
{ infowindow.close() ;
var filename , fileno , readers = new Array(input.files.length) ;
if(ovr!='geotag') render.overwrite = ovr ;
if(ovr=='load') zonktale() ;
// the first file loaded (not nec. fileno 0) will call 'render' with
// render.overwrite equal to ovr, but render resets render.overwrite to
// 'add' allowing subsequent files to be added
for(n=input.files.length,fileno=0;fileno<input.files.length;fileno++)
{ readers[fileno] = new FileReader() ;
filename = input.files[fileno].name ;
function readerfactory(i)
{ if(ovr=='geotag') return function(e)
{ n -= 1 ; tagger(readers[i].result,input.files[i].name,n) ; }
else return function(e)
{ render(readers[i].result,input.files[i].name,'file') ; }
}
readers[fileno].onload = readerfactory(fileno) ;
extn = filename.substring(filename.length-4).toLowerCase() ;
if(extn=='.fit'||extn=='.jpg')
readers[fileno].readAsArrayBuffer(input.files[fileno]) ;
else readers[fileno].readAsText(input.files[fileno]) ;
}
} ) ;
domadd(para,input) ;
domadd(div,para) ;
para = domcreate('p',M.orweb+': ','style','margin:4px 0') ;
}
// url input box
filedialogue.box = filebox = domcreate('input',null,'id','filedialogue',
'style','width:440px;height:24px;margin:0') ;
filebox.onkeyup = function(e) { if(e.keyCode==13||e.which==13) submit() ; } ;
hlist(filebox,ovr) ; // add history list
domadd(para,lab,filebox) ;
if(splash) // load from splash page
{ inp = domcreate('button',M.submit,'type','submit','style',btnstyle(1)) ;
inp.onclick = submit ;
domadd(para,' ',inp) ;
}
domadd(div,para) ;
/* ------------------------------------------------------------------------ */
function tagger(r,s,opt)
{ unfreeze() ;
var pos = geotag(r,s) ;
if(pos) insgeo(pos,s.substring(0,s.length-4),opt) ;
}
function submit()
{ var s ;
if(ovr=='list')
{ if(!filebox.value)
{ imginfo = { status:'nolist' } ; if(!check) photoprompt() ; return ; }
if(imginfo.carriedover&&filebox.value==imginfo.uri)
{ imginfo.carriedover = null ; if(!check) photoprompt() ; return ; }
else listuri = rellist(filebox.value) ;
}
else trackuri = filebox.value ;
unblock() ;
if(ovr!='list')
{ s = trackuri.substring(trackuri.length-4).toLowerCase() ;
if(trackextns.indexOf(s)>=0||s=='.jpg') s = shortenname(trackuri) ;
else s = M.gmaps ;
if(map||splash) waitprompt(inject(M.waitingfor,s),splash?'blurb':0) ;
}
if(check) getlist(listuri,'uricheck') ;
else if(ovr=='list') { greyout(photobtn) ; getlist(listuri,'uriform') ; }
else if(ovr=='geotag')
readuri(function(r) { tagger(r,s,0) ; },trackuri,'get') ;
else readuri(function(r) { render(r,trackuri,'uriform',null,ovr) ; },
trackuri,'get') ;
} // end of function submit()
/* ------------------------------------------------------------------------ */
if(!splash) // if load is not from splash page, i.e. is from cogwheel button
{ // cancel button
inp = domcreate('button',caps(M.cancel),'style',btnstyle()) ;
inp.onclick = unblock ;
para = btnbox(inp) ;
// submit button
inp = domcreate('button',M.submit,'type','submit','style',btnstyle(1)) ;
inp.onclick = submit ;
domadd(para,inp) ;
domadd(div,para) ;
underline(div) ;
}
div = domcreate('div',div) ;
if(ovr=='list') domadd(div,domcreate('p',
genlink('https://www.routemaster.app/software/routemaster.html#photos',
M.photodoc,1),'style','margin:0;white-space:nowrap')) ;
return div ;
}
function hlist(filebox,ovr)
{ var hits,hit,list,n,i ;
if(ovr=='list'&&imginfo.carriedover) domset(filebox,'value',imginfo.uri) ;
else if(ovr!='geotag')
{ if(ovr=='list') hits = prefs.pixhits ; else hits = prefs.gpshits ;
while(hits.length&&!hits[hits.length-1]) hits.length -= 1 ;
if(hits) n = hits.length ; else n = 0 ;
for(list=null,i=-1;i<n;i++)
{ if(i<0)
{ if(ovr=='list') hit = filedialogue.pixerr ; else hit = gpserr ;
if(!hit) continue ;
}
if(!list) list = domcreate('datalist',null,'id','dlist') ;
domadd(list,domcreate('option',null,'value',i<0?hit:hits[i])) ;
}
if(list) { domadd(filebox,list) ; domset(filebox,'list','dlist') ; }
}
}
/* -------------------------------- prompter -------------------------------- */
function btnstyle(opt)
{ return 'cursor:pointer;border:1px solid black;font-size:90%;padding:4px 8px;'+
'border-radius:3px;margin-right:'+(opt?'0;':'6px;')+
'background-color:'+btncol(opt) ;
}
function btncol(opt) { return (opt?'#dddddd':'white') ; }
function btnbox(btn)
{ return domcreate('p',btn,'style',
'text-align:center;margin:4px 0;white-space:nowrap') ;
}
// msg is the prompt
// init is its initial field
// action is the function to invoke on submit
// opt is 'desc' for a textbox description, or 'del' for a delete button
function prompter(msg,init,action,opt)
{ var para = domcreate('p',null,'style','margin:0') , filebox , inp , s ;
var div = domcreate('div',msg) , btns = btnbox() ;
// input box
if(action&&init!=-1)
{ if(opt=='desc') filebox = domcreate('textarea',init?init:null,'style',
'width:'+(Math.min(500,window.innerWidth)-8)+'px;'+
'height:200px;font-size:90%;margin-top:2px;'+
'font-family:Menlo,helvetica' ) ;
else
{ if(opt=='del') s = 'width:20em;' ; else s = 'width:15em;' ;
filebox = domcreate('input',null,'style',s+'height:24px;margin-top:2px') ;
if(init) { domset(filebox,'value',init) ; filebox.select() ; }
filebox.onkeyup = function(e)
{ if(e.keyCode==13||e.which==13) accept() ; } ;
}
domadd(para,filebox) ;
domadd(div,para) ;
}
if(action) // cancel button
{ inp = domcreate('button',caps(M.cancel),'style',btnstyle()) ;
inp.onclick = function() { unblock() ; }
domadd(btns,inp) ;
}
if(opt=='del') // delete button
{ inp = domcreate('button',caps(M.del),'style',btnstyle()) ;
inp.onclick = function() { unblock() ; action(null) ; }
domadd(btns,inp) ;
}
// submit button
inp = domcreate('button',M.ok,'type','submit','style',btnstyle(1)) ;
function accept()
{ unblock() ;
if(action) { if(init==-1) action() ; else action(filebox.value) ; }
}
inp.onclick = accept ;
domadd(btns,inp) ;
domadd(div,btns) ;
if(opt=='desc') domadd(div,doclink('desc',M.descdoc)) ;
if(action&&init!=-1) return [div,filebox] ; else return div ;
}
function alerter(msg,pos)
{ freeze() ; infowindow.open(prompter(msg),pos,1,unfreeze) ; }
/* --------------- selpoint: choose the clicked waypoint ------------------- */
function selmarker(e) { infowindow.close() ; selpoint(e) ; }
function selpoint(event)
{ var i,j,r,scale,flag=0,pos,mindist,pts ;
var s0 = selected.segno , s1 = selected.ptno , s2 = selected.type ;
shiftkey(event.originalEvent.shiftKey) ;
// this is a bit of a nightmare: if I’m displaying an image, the document
// onkeydown event may be intercepted by google's event listener and rerouted
// to selpoint, so I have to pass it on. it seems that the return key is
// treated as a click by the google onclick handler. pah!
if(dragging) return ;
pos = { lat:event.latlng.lat , lng:event.latlng.lng } ;
flag = infowindow.close() ;
if(flag=='highlight') flag = 3 ;
else if(flag=='seginfo'||flag=='altinfo') flag = 2 ;
else if(flag=='wpinfo') flag = 1 ;
if(segbtn&&!flag&&event.originalEvent.shiftKey) // insert waypoint forwards
{ if(s2) pts = segments[s0].geo ; else pts = segments[s0].pts ;
s1 = pts.length ;
if(s2)
{ pts.push(new pttype(pos)) ; geodraw(pts[s1],segments[s0].colour) ; }
else
{ insert(pts,s1,1) ;
pts[s1].pos = pos ;
redelta(segments[s0].pts,s1) ;
redrawconnect(s0,s1) ;
drawprofile() ;
}
done(['move',s0,s1,s2,pos,pos,1]) ;
walkto(s0,s1,s2,0) ;
return ;
}
// select the closest point
if(!segbtn) { r = distrec(segments[0],pos) ; s0 = r.ind ; mindist = r.dist ; }
else for(mindist=null,i=0;i<segments.length;i++)
{ if(segments[i].pts.length)
{ r = segdist(segments[i].pts,pos) ;
if(mindist==null||r.dist<mindist)
{ s0 = i ; s1 = r.ind ; s2 = null ; mindist = r.dist ; }
}
for(j=0;j<segments[i].geo.length;j++)
{ r = dist(segments[i].geo[j].pos,pos)
if(mindist==null||r<mindist)
{ s0 = i ; s1 = j ; s2 = 'geo' ; mindist = r ; }
}
}
// now we have found the shortest distance from the click to the track and
// the corresponding waypoint. require the distance to be <60 pixels,
// otherwise ignore the click.
if(!segbtn&&flag>1) flag = 1 ;
if(mindist<60*getscale()) walkto(s0,s1,s2,flag) ;
}
function distrec(seg,pos)
{ var i , r , res ;
if(seg.level==0) return segdist(seg.pts,pos) ;
for(i=0;i<seg.pts.length;i++)
{ r = distrec(seg.pts[i],pos) ;
if(i==0||r.dist<res.dist) res = { ind:i , dist:r.dist } ;
}
return res ;
}
function segdist(pts,pos) // closest approach of a segment to a point
{ var i , b = dist(pts[0].pos,pos) , res = { ind:0 , dist:b } , a , h ;
for(i=1;i<pts.length;b=a,i++)
{ a = dist(pts[i].pos,pos) ;
h = distptseg(a,b,pts[i-1].delta) ;
if(h<res.dist) res = { ind:(i-1)+(b>a) , dist:h } ;
}
return res ;
}
/* -------------------------- track highlighter ---------------------------- */
function highlight(tracks,segno)
{ if(segno||segno==0) selected.segno = segno ;
var s0=selected.segno,scroll,i,n,s , norpos = { pos:null } ;
infowindow.close() ;
recurse(tracks[s0],'undraw') ;
recurse(tracks[s0],'draw4') ;
scroll = highdiv(s0) ;
highlight.scroller = scroll.scroller ;
recurse(tracks[s0],'north',norpos) ;
infowindow.open(scroll.div,norpos.pos,null,
function() { unhighlight() ; return 'highlight' ; }) ;
}
function unhighlight()
{ var s0 = unhighlight.segno = selected.segno ;
recurse(segments[0].pts[s0],'undraw') ;
recurse(segments[0].pts[s0],'draw') ;
if(highlight.scroller)
{ clearInterval(highlight.scroller) ; highlight.scroller = null ; }
undrawsel() ;
selected.segno = -1 ;
}
/* --------------------------------- 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,s2,flag)
{ selected = { segno:s0 , ptno:s1 , type:s2 } ;
if(!segbtn) return highlight(segments[0].pts) ;
var pt = s2?segments[s0].geo[s1]:segments[s0].pts[s1] , pos = pt.pos ;
if(!flag) flag = 0 ;
moveto(pos) ;
drawsel() ;
if(flag||(!pt.label&&!pt.photo.length))
{ if(flag==1) wpinfo(prefs.precision) ;
else if(flag==2) seginfo() ;
else if(flag==3) highlight(segments[0].pts) ;
return ;
}
infowindow.open(walktodiv(pt),pos) ;
}
function moveto(pos)
{ var bounds = map.getBounds() , ρ=0.3 , ε=0.1 , x=pos.lng , y=pos.lat , s ;
var ne = bounds.getNorthEast() , sw = bounds.getSouthWest() , dx=0 , dy=0 ;
var x0=sw.lng , x1=ne.lng , y0=sw.lat , y1=ne.lat ;
if(x1>180+x0) x0 += 360 ; else if(x0>x1+180) x1 += 360 ;
if(x<(x0+x1)/2-180) x += 360 ; else if(x>(x0+x1)/2+180) x -= 360 ;
if(y>=y0&&y<=y1&&x>=x0&&x<=x1) return ;
s = y1 - y0 ;
if(y<y0+ε*s) { y = Math.min(y-ε*s,y0-ρ*s) ; dy = y - y0 }
else if(y>y1-ε*s) { y = Math.max(y+ε*s,y1+ρ*s) ; dy = y - y1 }
s = x1 - x0 ;
if(x<x0+ε*s) { x = Math.min(x-ε*s,x0-ρ*s) ; dx = x - x0 }
else if(x>x1-ε*s) { x = Math.max(x+ε*s,x1+ρ*s) ; dx = x - x1 }
map.panTo({lat:(y0+y1)/2+dy,lng:(x0+x1)/2+dx}) ;
}
/* -------------------------- keystroke handler ---------------------------- */
function keystroke(e)
{ if(!selected||infowindow.type=='addload'||infowindow.type=='getlist') return ;
if(infowindow.type=='account') return ;
if(dragging==3&&optimwork.advance)
{ if(e.keyCode==39) optimwork.advance(1) ;
else if(e.keyCode==37) optimwork.advance(-1) ;
return ;
}
if(dragging==2) return ;
var s0=selected.segno,s1=selected.ptno,s2=selected.type,slast,flag,r ;
if(e.keyCode==17||e.keyCode==224||e.keyCode==91||e.keyCode==93) return ;
// [control] + 3 encodings of [command]
if(e.keyCode==40&&segbtn) // down arrow
{ if(s2) map.panTo(segments[s0].geo[s1].pos) ;
else map.panTo(segments[s0].pts[s1].pos) ;
return ;
}
if(e.keyCode==32) // space - no infowindow.close()
{ e.preventDefault() ;
if(dragging) undraggit() ; else if(segbtn&&s0>=0) draggit(0) ;
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( flag==3 && (e.keyCode==37||e.keyCode==39)
&& !e.shiftKey && segments[0].type=='tour' )
selected.segno = unhighlight.segno ;
else if(!segbtn) { if(e.keyCode==70) enterfullscreen() ; return ; }
if(dragging) return ;
if(e.keyCode==8||e.keyCode==46) // delete/backspace
{ e.preventDefault() ;
if(!e.shiftKey) { if(segbtn) wpdel() ; }
else if(segbtn) discard() ;
else if(segments.length>1)
{ selected.segno = s0 ; discard() ; selected.segno = -1 ; }
return ;
}
if(e.keyCode==9)
{ e.preventDefault() ; inswp(e.shiftKey?-1:1) ; return ; } // tab
if(e.keyCode==39) // forwards
{ e.preventDefault() ;
if(e.shiftKey) { s1 = 0 ; s0 += 1 ; if(s0==segments.length) s0 = 0 ; }
else { r = pointstep(1) ; s0 = r.segno ; s1 = r.ptno ; s2 = r.type ; }
}
else if(e.keyCode==37) // backwards
{ e.preventDefault() ;
if(e.shiftKey) { s1 = 0 ; s0 -= 1 ; if(s0<0) s0 = segments.length-1 ; }
else { r = pointstep(-1) ; s0 = r.segno ; s1 = r.ptno ; s2 = r.type ; }
}
else return ;
walkto(s0,s1,s2,e.shiftKey?2:flag?1:0) ;
}
/* ------------------------------ pointstep -------------------------------- */
function pointstep(dir)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type , i , q , r , s ;
if(!segbtn)
{ if(segments[0].type=='tour')
{ s0 += dir ;
if(s0>=segments[0].pts.length) s0 = 0 ;
else if(s0<0) s0 = segments[0].pts.length-1 ;
}
return { segno:s0 , ptno:s1 , type:s2 } ;
}
var pos = s2 ? segments[s0].geo[s1].pos : segments[s0].pts[s1].pos ;
if(s2)
{ for(q=null,i=0;i<segments[s0].pts.length;i++)
{ r = dist(pos,segments[s0].pts[i].pos) ;
if(q==null||r<q)
{ q = r ; s = { segno:s0 , ptno:i , type:null } ; }
}
if(q!=null) return s ;
}
if(dir>0)
{ if(s2) i = 0 ; else i = s1+1 ;
if(i<segments[s0].pts.length) return { segno:s0 , ptno:i , type:null } ;
for(i=s0+1;i<segments.length;i++) if(segments[i].pts.length)
return { segno:i , ptno:0 , type:null } ;
for(i=0;i<=s0;i++) if(segments[i].pts.length)
return { segno:i , ptno:0 , type:null } ;
return { segno:s0 , ptno:s1 , type:s2 } ;
}
if(s2) i = segments[s0].pts.length-1 ; else i = s1-1 ;
if(i>=0) return { segno:s0 , ptno:i , type:null } ;
for(i=s0-1;i>=0;i--) if(segments[i].pts.length)
return { segno:i , ptno:segments[i].pts.length-1 , type:null } ;
for(i=segments.length-1;i>=s0;i--) if(segments[i].pts.length)
return { segno:i , ptno:segments[i].pts.length-1 , type:null } ;
return { segno:s0 , ptno:s1 , type:s2 } ;
}
/* -------------------------- shift key handler ---------------------------- */
function shiftkey(val)
{ if(val&&!shiftkey.active&&map) map.getContainer().style.cursor = 'crosshair' ;
else if(!val&&shiftkey.active&&map)
map.getContainer().style.cursor = 'default' ;
shiftkey.active = val ;
}
/* --------------------- undraw & redraw segments -------------------------- */
// needs a comment explaining when to obliterate and when to undraw
function undraw(segment)
{ map.removeLayer(segment.line) ;
if(showarrows&&segment.arrows) map.removeLayer(segment.arrows) ;
for(var i=0;i<segment.geo.length;i++) ungeodraw(segment.geo[i]) ;
}
function redraw(i) { undraw(segments[i]) ; drawfull(segments[i]) ; }
function drawprofile()
{ var seg , divstyle ;
if(segments[0].type=='tour'||segments[0].type=='segments')
seg = segments[0].pts ;
else seg = segments ;
divstyle = drawpro(drawpro.pro,seg) ;
toggleprofile.pcopy = drawpro.pro ;
toggleprofile.scopy = seg ;
// cursor
if(divstyle)
{ domset(drawpro.pro.curdiv,'style',divstyle) ;
drawpro.pro.curdiv.style.zIndex = drawpro.pro.prodiv.style.zIndex = 700 ;
}
drawxcur(drawpro.pro,selected) ;
}
/* ----------------------------- draw a point ------------------------------- */
function drawpoint(pos,colour)
{ var a , w = 8 , c = newcanvas(w,w) , ctx = c.getContext('2d') ;
ctx.lineWidth = 2 ;
ctx.strokeStyle = colour ;
ctx.beginPath() ;
ctx.arc(w/2,w/2,w/3,0,2*Math.PI) ;
ctx.stroke() ;
a = L.divIcon({html:c,iconSize:[w,w],className:'rm'}) ;
return L.marker(pos,{icon:a,keyboard:false}) ;
}
/* ----------------------------- draw segments ------------------------------ */
function draw(segment,width) // should use drawfull in place of draw
{ if(!width) width = 2 ;
if(!draw.sqrt) draw.sqrt = Math.sqrt(2) ;
if(draw.ponent) width *= Math.pow(draw.sqrt,draw.ponent) ;
if(segment.pts.length==1)
segment.line = drawpoint(segment.pts[0].pos,segment.colour) ;
else if(segment.pts.length>1)
{ var poly = new linepath(segment,-1,0,segment.colour,width) ;
segment.line = L.polyline(poly.path,poly.opts) ;
}
segment.line.addTo(map) ;
if(showarrows&&segment.arrows) segment.arrows.addTo(map) ;
}
function drawfull(segment,width)
{ var i,pt ;
draw(segment,width) ;
if(segment.level==0) for(i=0;i<segment.geo.length;i++)
if(pt=segment.geo[i].point,!pt.drawn)
{ pt.marker.addTo(map) ; pt.drawn = 1 ; }
}
function geodraw(pt,col)
{ if(pt.point&&pt.point.drawn) return ;
pt.point = { marker: drawpoint(pt.pos,col) , drawn:1 } ;
pt.point.marker.addTo(map) ;
}
function ungeodraw(pt)
{ if(pt.point&&pt.point.drawn)
{ map.removeLayer(pt.point.marker) ; pt.point.drawn = 0 ; }
}
/* ----------------------- connect and disconnect segments ------------------ */
function disconnect(segno)
{ var seg = (segments[0].type=='tour'?segments[0].pts[segno]:segments[segno]) ;
if((!segbtn&&segments[0].type!='tour')||seg.dots==null) return ;
seg.dots.remove() ;
}
function connect(i)
{ var j,seg,opts ;
if(segments[0].type=='tour') seg = segments[0].pts ;
else { seg = segments ; if(seg[0].level||seg[i].pts.length==0) return ; }
for(j=i+1;j<seg.length&&seg[j].pts.length==0;j++) ;
if(i<0||j==seg.length) return ;
var opos = seg[i].pts[seg[i].pts.length-1].pos ;
var npos = seg[j].pts[0].pos ;
opts = { color:'black' , weight:2 , interactive:false ,
dashArray:'1 4' , dashOffset:'3' } ;
seg[i].dots = L.polyline([opos,npos],opts) ;
seg[i].dots.addTo(map) ;
}
function redrawconnect(s0,s1)
{ var i ;
if(segments[s0].pts.length) redraw(s0) ;
if(s1==0) { ldisconnect(s0) ; lconnect(s0) ; }
if(s1==segments[s0].pts.length-1) { disconnect(s0) ; connect(s0) ; }
}
function lconnect(segno)
{ var i , seg = (segments[0].type=='tour'?segments[0].pts:segments) ;
for(i=segno-1;i>=0&&seg[i].pts.length==0;i--) ;
if(i>=0) connect(i) ;
}
function ldisconnect(segno)
{ var i , seg = (segments[0].type=='tour'?segments[0].pts:segments) ;
for(i=segno-1;i>=0&&seg[i].pts.length==0;i--) ;
if(i>=0) disconnect(i) ;
}
/* ---------------------- draw the selection point -------------------------- */
function unsel() { if(drawsel.marker) map.removeLayer(drawsel.marker) ; }
function undrawsel() { unsel() ; drawsel.marker = null ; }
function drawsel(pts,ptno,advance) // args supplied only for animated arrow
{ var icon,opt=0,seg,pt,parms ;
undrawsel() ;
if(pts=='drag') opt = 1 ;
else if(!pts&&selected.type&&selected.type=='geo') opt = 2 ;
if(pts=='drag'||!pts)
{ seg = segments[selected.segno] ;
if(selected.type&&selected.type=='geo') pts = seg.geo ; else pts = seg.pts ;
ptno = selected.ptno ;
}
pt = pts[ptno] ;
if(opt)
{ parms = { html:circlecanvas(opt==1) , iconSize:[14,14] , className:'rm' } ;
icon = L.divIcon(parms) ;
parms = { icon , keyboard:false , draggable:opt==1 , autoPan:opt==1 } ;
drawsel.marker = L.marker(pt.pos,parms) ;
drawsel.marker.addTo(map) ;
if(opt==2) greyout(scissorsbtn) ;
return ;
}
var ind,clen=pts.length,pos=pt.pos,pos2,λ,a,b,θ=0 ;
if(clen>1)
{ if(ptno==clen-1) ind = ptno-1 ; else ind = ptno ;
θ = angle(pts[ind].pos,pts[ind+1].pos) ;
}
if(advance&&ptno<clen-1)
{ pos2 = pts[ptno+1].pos ;
λ = advance / Math.max(0.1,pts[ptno].delta) ;
a = pos.lat ;
b = pos.lng ;
pos = { lat:a+λ*(pos2.lat-a) , lng:b+λ*(pos2.lng-b) } ;
}
a = L.divIcon({html:arrowcanvas('black',θ),iconSize:[20,20],className:'rm'}) ;
drawsel.marker = L.marker(pos,{icon:a,keyboard:false}) ;
drawsel.marker.addTo(map) ;
if(segbtn)
{ if(drawpro.pro.m.h.length) drawxcur(drawpro.pro,selected) ;
if(ptno!=0) blackout(scissorsbtn) ; else greyout(scissorsbtn) ;
}
}
function circlecanvas(opt)
{ var c = newcanvas(14,14) , ctx = c.getContext('2d') ;
ctx.strokeStyle = 'black' ;
ctx.lineWidth = 1 ;
ctx.beginPath() ;
ctx.arc(7,7,6,0,2*Math.PI) ;
ctx.stroke() ;
ctx.beginPath() ;
ctx.arc(7,7,3,0,2*Math.PI) ;
if(opt) // crosshairs
{ ctx.moveTo(1,7) ; ctx.lineTo(13,7) ; ctx.moveTo(7,1) ; ctx.lineTo(7,13) ; }
ctx.stroke() ;
return c ;
}
function arrowcanvas(colour,θ) // θ = compass bearing (clockwise from north)
{ var s = Math.sin(θ) , c = Math.cos(θ) , a = 3*Math.sqrt(2) ;
var C = newcanvas(20,20) , ctx = C.getContext('2d') ;
ctx.fillStyle = colour ;
ctx.beginPath() ;
ctx.moveTo(10+6*s,10-6*c) ;
ctx.lineTo(10-3*s-a*(s+c),10+3*c-a*s+a*c) ;
ctx.lineTo(10-3*s,10+3*c) ;
ctx.lineTo(10-3*s-a*(s-c),10+3*c+a*s+a*c) ;
ctx.fill() ;
return C ;
}
/* -------------------------------------------------------------------------- */
/* FUNCTIONS TO GENERATE THE INITIAL MAP */
/* -------------------------------------------------------------------------- */
function checklostedits(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)
}
/* -------------------------------------------------------------------------- */
function genpage(response,trackuri,mode,buscolour)
{ var div,xmldoc,i,url,xhttp ;
// once-only initialisation
if(window.loaded) window.addEventListener("beforeunload",checklostedits) ;
else window.onload = function()
{ window.addEventListener("beforeunload",checklostedits) ; } ;
keyhandler = keystroke ;
document.addEventListener('keyup',
function(e) { if(e.keyCode==16) shiftkey(0) ; } ) ;
document.addEventListener('keydown',
function(e) { if(e.keyCode==16) shiftkey(1) ; } ) ;
if(!mapparent||!body)
mapparent = body = document.getElementsByTagName("body")[0] ;
while(body.childNodes.length>0)
body.removeChild(body.childNodes[body.childNodes.length-1]) ;
url = document.location.href ;
i = url.indexOf('?') ;
if(i>=0) url = url.substring(0,i) ;
render.layers =
[ { name:"osmlandscape" , tiles:null } ,
{ name:"osmplain" , tiles:null } ,
{ name:"mapboxsatellite" , tiles:null }
] ;
if(url.substring(url.length-8)=='test.php') render.layers.push
( { name:"osmstandard",
tiles:L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{ maxZoom:19,
attribution:'© <a href="http://www.openstreetmap.org/' +
'copyright" target="_blank">OpenStreetMap</a>'
} )
} ) ;
xhttp = new XMLHttpRequest() ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ var r = xhttp.response.split(' ') , s ;
for(i=0;i<render.layers.length;i++)
if(render.layers[i].name!="osmstandard")
{ if(render.layers[i].name=="mapboxsatellite")
{ s = 'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x' +
'.jpg90?access_token=' + r[1] ;
render.layers[i].tiles = L.tileLayer(s,
{ maxZoom:22,
attribution:
'© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> ' +
'© <a href="https://www.maxar.com/">Maxar</a>'
} )
}
else
{ s = '//api.thunderforest.com/'+(i==0?'landscape':'atlas') +
'/{z}/{x}/{y}.png?apikey=' + r[0] ;
render.layers[i].tiles = L.tileLayer(s,
{ maxZoom:22,
attribution:'<a href="https://www.thunderforest.com/" ' +
'target="_blank">© Thunderforest</a> ' +
'<a href="https://www.openstreetmap.org/copyright" ' +
'target="_blank">© OpenStreetMap contributors</a>'
} )
}
}
// generate a page: either splash or display a route
if(response==null)
{ div = blurbdiv(buscolour) ;
domset(div,'style','font-family:helvetica;padding:0 4px;');
domadd(body,div) ;
div = filedialogue("newload") ;
domset(div,'style','margin:4px;border-top:solid 1px silver;padding-' +
'top:4px;border-bottom:solid 1px silver;padding-bottom:2px') ;
domadd(body,div) ;
div = rmhelpdiv(-1) ;
domset(div,'style','font-family:helvetica;margin:4px;font-size:90%') ;
domadd(body,div) ;
}
else render(response,trackuri,'uri',mode,'load') ;
}
}
xhttp.open("GET",fileserver+'?tokens') ;
xhttp.send() ;
}
/* -------------------------------- getlist --------------------------------- */
function getlist(uri,imgtype,r1,r2)
{ if(uri.substring(uri.length-4).toLowerCase()!='.xml')
{ imginfo = { status:'failed' } ;
blackout(photobtn) ;
return alerter(uri+' '+M.isnotxml,5) ;
}
var xhttp = new XMLHttpRequest() ;
var p = (imginfo&&imginfo.pending)?imginfo.pending:[] , pfx ;
imginfo = { status:'waiting' , type:imgtype , uri , pending:p } ;
if(r1&&r2) getlist.pending = [ r1 , r2 ] ;
xhttp.onreadystatechange = function()
{ var r,i,xmldoc,i ;
if(xhttp.readyState==4)
{ if(photobtn) blackout(photobtn) ;
if(xhttp.responseText.length==0)
{ filedialogue.pixerr = r = uri ;
i = r.lastIndexOf('/') ;
if(i>=0) r = r.substring(i+1) ;
imginfo = { status:'failed' , uri:null } ;
return alerter(inject(M.unable,r),5) ;
}
filedialogue.pixerr = null ;
if(prefs.pixhits&&0>prefs.pixhits.indexOf(uri))
prefs.pixhits = addtolist(prefs.pixhits,uri) ;
// was parselist
xmldoc = parser.parseFromString(xhttp.responseText,"application/xml") ;
imginfo = getphotolist(xmldoc,uri) ;
selcat(imginfo) ;
imginfo.status = 'ready' ;
imginfo.carriedover = 0 ;
imginfo.type = imgtype ;
imginfo.uri = uri ;
imginfo.gallery = reluri(uri,imginfo.gallery) ;
imginfo.title = getcatval(imginfo.title) ;
imginfo.title = ( imginfo.title?imginfo.title:'' ) ;
imginfo.pending = [] ;
for(i=0;i<p.length;i++) genphotomark(p[i],1) ;
if(imgtype=='uricheck') return ;
else if(imgtype=='uriform') photoprompt() ;
else if(getlist.pending)
{ for(i=0;i<getlist.pending.length;i+=2)
getlist.pending[i].smallphoto = gatherpix(getlist.pending[i+1]) ;
getlist.pending = [] ;
}
}
}
if(imgtype=='uriform'||imgtype=='uricheck') pfx = '?get=' ; else pfx = '?' ;
console.log('getting photolist '+uri.substring(1+uri.lastIndexOf('/'))) ;
xhttp.open("GET",fileserver+pfx+uri,true) ;
xhttp.send() ;
}
/* ------------------------------ getimgtitle ------------------------------- */
function getimgtitle(name)
{ if(!imginfo||!imginfo.status||imginfo.status!='ready') return null ;
var ind = findimage(imginfo.sect,name) ;
if(!ind) return null ;
return imginfo.sect[ind[0]].list[ind[1]].title ;
}
/* --------------------- set up the map and buttons ------------------------- */
// if s0 is 0 then ovr (overwrite option) is always 1
// note that render is invoked repeatedly for a bulk read, so it doesn't need
// to do a bulk render: it renders ONE track/index/metaindex as returned by
// loadtrack
// overwrite needs to be a static variable because render is invoked repeatedly
// in the case of a bulk load. before the load the global variable is set to
// whatever value is suitable for the first file, and it is then set to 0 for
// subsequent loads.
function render(response,filename,origin,p4,overwrite)
{ var i,pts,opts,segno,proactive,r,flag,ovr,s,loadflags=null,pro,nbtn,w ;
var newseg,lcontrol,smt,dobuttons,btn ;
var extn = filename.substring(filename.length-4).toLowerCase() ;
var olen=segments.length , olev=olen?segments[0].level:0 ;
if(overwrite) render.overwrite = overwrite ;
if(origin=='refresh') { ovr = 'refresh' ; origin = p4 ; }
else { ovr = render.overwrite ; loadflags = p4 ; }
if(ovr=='load') { render.asked = 0 ; imginfo = { pending:[] , uri:null } ; }
if(map) unfreeze() ;
infowindow.close() ;
genpage.im = null ;
// purge body of everything except mapdiv
if(map&&segments[0].type=='segments') olev = 0 ;
xmlfile = filename ;
// if filename is a relative url, convert it to absolute
if((ovr=='refresh'||origin.substring(0,3)=='uri')&&!absuri(filename))
{ i = document.location.href.indexOf('?') ;
if(i<0) filename = reluri(document.location.href,filename) ;
else filename = reluri(document.location.href.substring(0,i),filename) ;
}
newseg = loadtrack(response,filename,ovr,origin,prefs,loadflags) ;
if(!newseg) return null ; // load failed or refreshing a component
r = getbounds(newseg) ;
/* ------------------------------------------------------------------------ */
// update uri
if( ovr=='load'&&(origin=='uriform'||origin=='urilink')
&& (trackextns.indexOf(extn)>=0) )
{ i = thispage.indexOf('?') ;
if(i<0) i = thispage.length ;
thispage = document.URL = thispage.substring(0,i) + '?track=' + filename ;
history.pushState(null,null,thispage) ;
}
// extend bounds of new tracks to include old ones if kept
if(ovr=='refresh') ; // don’t bother if refreshing
else if(map&&(s=map.getBounds()))
{ if(ovr=='add') r = union(r,s) ; render.bounds = null ; }
else
{ if(ovr=='add'&&render.bounds) r = union(r,render.bounds) ;
render.bounds = r ;
}
flag = dobuttons = 0 ;
// slightly convoluted logic: we want to load the first track before setting
// up the map so as to give it bounds, but loadtrack calls telltale which
// cannot operate until we’ve removed obsolete elements from body, so we
// delay its operation (by setting telltale.hold) and then invoke it when we
// are ready, setting map to 1 as a sign that all is clear.
if(!map) // all this only done on first call
{ while(body.childNodes.length>0)
body.removeChild(body.childNodes[body.childNodes.length-1]) ;
mapdiv = domcreate('div',null,'id','map',
'style','width:100%;height:100%;position:absolute') ;
domadd(body,mapdiv) ;
if(telltale.hold) telltale(telltale.hold,1) ; // force display
opts = { keyboard:false , doubleClickZoom:false , zoomControl:false ,
closePopupOnClick:false , preferCanvas:true } ;
map = L.map('map',opts) ;
// when I add the leaflet scale control, it stops working after an
// infowindow has been opened
map.on('zoomend',displayscale) ;
L.control.zoom({position:'bottomright'}).addTo(map) ;
// Prevent URL hover preview on zoom controls
btn = document.querySelector('.leaflet-control-zoom-in') ;
if(btn) { btn.removeAttribute('href') ; btn.style.cursor = 'pointer' ; }
btn = document.querySelector('.leaflet-control-zoom-out') ;
if(btn) { btn.removeAttribute('href') ; btn.style.cursor = 'pointer' ; }
if(render.layers.length==3) render.curlayer = 0 ;
else render.curlayer = render.layers.length - 1 ;
if(loadflags)
{ if(loadflags.indexOf('f')>=0) render.curlayer = 1 ;
else if(loadflags.indexOf('t')>=0) render.curlayer = 0 ;
else if(loadflags.indexOf('s')>=0) render.curlayer = 2 ;
}
render.layers[render.curlayer].tiles.addTo(map) ;
map.on('click',selpoint) ;
map.getContainer().style.cursor = 'default' ;
dobuttons = 2 ;
if(segments[0].level==0||segments[0].type=='segments')
window.addEventListener('resize',resizer) ;
if(loadflags&&loadflags.indexOf('p')>=0) flag = 1 ;
}
// replacing a regular track by an index/metaindex or vice versa
else if( ovr=='load' && (newseg.level>0&&newseg.type!='segments')!=(olev>0) )
{ dobuttons = 1 ;
if(render.btndiv&&render.btndiv.parentNode)
render.btndiv.parentNode.removeChild(render.btndiv) ;
}
if(ovr!='refresh') map.fitBounds(r) ;
if(dobuttons) // set up buttons
{ if(segments[0].level==0||segments[0].type=='segments') nbtn = 10 ;
else nbtn = 6 ;
w = 32*nbtn - 4 ;
render.btndiv = domcreate('div',null,'style','position:absolute;bottom:0;' +
'left:calc(50% - '+(w/2)+'px);z-index:700;width:'+w+'px;height:40px') ;
render.laydiv = domcreate('div',null,'style','position:absolute;' +
'bottom:100px;right:10px;z-index:700') ;
if(segments[0].level==0||segments[0].type=='segments') eyebtn = null ;
else eyebtn = genbutton('eye',render.btndiv) ;
setbtn = genbutton('settings',render.btndiv) ;
if(segments[0].level==0||segments[0].type=='segments')
{ segbtn = genbutton('segment',render.btndiv) ;
wpbtn = genbutton('waypoint',render.btndiv) ;
scissorsbtn = genbutton('scissors',render.btndiv) ;
penbtn = genbutton('pen',render.btndiv) ;
photobtn = genbutton('camera',render.btndiv) ;
}
else segbtn = wpbtn = scissorsbtn = penbtn = photobtn = null ;
undobtn = genbutton('undo',render.btndiv) ;
redobtn = genbutton('redo',render.btndiv) ;
dlbtn = genbutton('dl',render.btndiv) ;
acbtn = genbutton(prefs.email?1:0,render.btndiv) ;
domadd(body,render.btndiv) ;
}
if(dobuttons==2)
{ render.layerbtn = genbutton('layers',render.laydiv) ;
domadd(body,render.laydiv) ;
displayscale() ;
}
donesomething() ; // ie loaded and optimised, but we can't record the fact
// until we've defined the buttons
/* ------------------------ draw the new points --------------------------- */
recurse(newseg,'draw') ;
recurse(newseg,'geodraw') ;
if(newseg.type=='segments')
{ i = segments.length - newseg.pts.length ;
lconnect(i) ;
for(;i<segments.length-1;i++) connect(i) ;
}
else if(segments[0].type=='tour')
{ i = segments[0].pts.length ;
if(newseg.type=='tour') i -= newseg.pts.length ; else i -= 1 ;
lconnect(i) ;
for(;i<segments[0].pts.length-1;i++) connect(i) ;
}
/* ----------------------- process the photolist -------------------------- */
// if there are photos and no photo list, prompt in case the user wants one
if( !newseg.list && !render.asked && !imginfo.uri && extn=='.rte'
&& (newseg.level==0||newseg.type=='segments') && recurse(newseg,'npix') )
{ freeze() ;
infowindow.open(filedialogue("checklist"),5,1,unfreeze) ;
render.asked = 1 ;
}
// if we're displaying a track/index, check its photolist and load it if nec
else if(segbtn && newseg.list!=imginfo.uri) // a lev0 track & lists differ
{ if(!imginfo.uri) getlist(newseg.list,'uri') ; // list was absent, now presnt
else if(!newseg.list) { if(ovr=='load') imginfo.carriedover = 1 ; }
else abend(M.inconsistentlists+' ' + newseg.list + ' vs.' + imginfo.uri) ;
}
/* -------------------------- add the markers ----------------------------- */
if(newseg.type=='segments'&&segments[0].level==0)
{ recurse(newseg,'genphotomark') ; recurse(newseg,'genwaymark') ; }
/* ------------------------ draw altitude profile ------------------------- */
if(!segbtn) selected = { segno:-1 , ptno:-1 , type:null } ;
if(segbtn||segments[0].type=='tour')
{ if(segbtn&&ovr=='load')
selected = {segno:0 , ptno:0 , type:segments[0].pts.length?null:'geo' } ;
if(drawpro.pro)
{ pro = drawpro.pro ;
if(pro.active) flag = 1 ;
if(pro.prodiv&&pro.prodiv.parentNode)
pro.prodiv.parentNode.removeChild(pro.prodiv) ;
if(pro.curdiv&&pro.curdiv.parentNode)
pro.curdiv.parentNode.removeChild(pro.curdiv) ;
}
drawpro.pro = pro = new profiletype(1) ;
domadd(mapparent,pro.prodiv,pro.curdiv) ;
pro.map = map ;
pro.active = flag ;
drawprofile() ;
if(segments[0].level) drawprofile() ;
else getalts(segments,1,drawprofile) ;
if(segbtn) drawsel() ;
}
else if(drawpro&&drawpro.pro)
{ delpro(drawpro.pro) ;
if(drawpro.pro.curdiv) body.removeChild(drawpro.pro.curdiv) ;
}
render.overwrite = "add" ;
if(loadflags&&loadflags.indexOf('z')>=0)
{ if(segbtn) routeinfo() ; else eyedisplay() ; }
return newseg.title || filename ;
}
/* ------------------------------- layermenu -------------------------------- */
function ()
{ var div=domcreate('div'),i,p,inp,lab,name,s ;
for(i=0;i<render.layers.length;i++)
{ name = M[render.layers[i].name] ;
s = i==render.curlayer?'':'cursor:pointer' ;
inp = domcreate('input',null,'type','radio','name','layers','id',name) ;
if(i==render.curlayer) domset(inp,'style',s) ;
inp.checked = i==render.curlayer ;
p = domcreate('div',inp,'style','white-space:nowrap;margin:2px 0;'+s) ;
domadd(p,domcreate('label',name,'for',name,'style',s)) ;
if(i!=render.curlayer) p.onclick = layerfactory(i) ;
domadd(div,p) ;
}
infowindow.open(div,'layers') ;
function submit(layerno)
{ infowindow.close() ;
map.removeLayer(render.layers[render.curlayer].tiles) ;
render.curlayer = layerno ;
render.layers[render.curlayer].tiles.addTo(map) ;
}
function layerfactory(layerno) { return function() { submit(layerno) ; } }
}
/* ------------------------------- genbutton -------------------------------- */
function silently(f)
{ return function(e) { if(e) e.stopPropagation() ; f() ; } }
function genbutton(name,div)
{ var u,b,g=null,k,h,gtitle=null,ktitle,a ;
u = domcreate('div',null,'style','display:inline-block') ;
u.style.backgroundColor = '#ffffff' ;
if(name=='layers')
{ u.style.border = '2px solid #999999' ;
u.style.borderRadius = '5px' ;
}
else
{ u.style.border = '2px solid #ffffff' ;
u.style.boxShadow = '0 2px 6px rgba(0,0,0,.3)' ;
u.style.borderRadius = '3px' ;
}
u.style.textAlign = 'center' ;
if(name=='eye') { h = eyedisplay ; u.index = -1 ; ktitle = M.eyemenu ; }
else if(name=='settings')
{ h = routeinfo ;
u.index = 0 ;
if(!segbtn) ktitle = M.controlmenu ; else ktitle = M.routeprops ;
}
else if(name=='segment')
{ h = seginfo ; u.index = 1 ; ktitle = M.segmentprops ; }
else if(name=='waypoint')
{ h = wpinfo ; u.index = 2 ; ktitle = M.waypointprops ; }
else if(name=='scissors')
{ h = snip ;
u.index = 3 ;
ktitle = caps(M.splitsegment) + ' ' + M.atwaypoint ;
gtitle = caps(M.splitsegment) + ' ' + M.nosplitsegment ;
gtitle = M.nosplitsegment ;
}
else if(name=='pen')
{ h = function() { waymarkprompt() ; } ;
u.index = 4 ;
ktitle = M.labelwaypoint ;
}
else if(name=='camera')
{ h = photoprompt ;
u.index = 5 ;
ktitle = M.addaphoto ;
gtitle = M.noaddaphoto ;
}
else if(name=='undo')
{ h = undo ;
u.index = 6 ;
ktitle = M.undolatest ;
gtitle = M.noundolatest ;
}
else if(name=='redo')
{ h = redo ;
u.index = 7 ;
ktitle = M.redolatest ;
gtitle = M.noredolatest ;
}
else if(name=='dl')
{ h = function() { dl(0) ; } ; u.index = 8 ; ktitle = M.saveroute ; }
else if(typeof(name)=='number') // account
{ function acfactory()
{ return function() { infowindow.open(acmenu('account'),9) ; } ; }
h = acfactory() ;
u.index = 9 ;
if(name) ktitle = M.account ;
else ktitle = M.register[1] + '/' + M.register[0] ;
}
else if(name=='layers') { h = layermenu ; ktitle = M.layermenu ; }
if(u.index!=9&&name!='layers') u.style.marginRight = '4px' ;
if(gtitle==null) gtitle = ktitle ;
// the icon is 24x24, the border is 2px, rt margin is 4px
k = btncanvas(name,'black',24) ;
if(name=='layers') g = k ; else g = btncanvas(name,'grey',24) ;
if(name!='scissors'&&name!='undo'&&name!='redo')
{ b = k ; u.title = ktitle ; u.onclick = silently(h) ; a = 1 ; }
else { b = g ; u.title = gtitle ; a = 0 ; }
if(a) u.style.cursor = 'pointer' ; else u.style.cursor = 'default' ;
u.appendChild(b) ;
domadd(div,u) ;
return { ui:u , greyimg:g , blackimg:k , active:a ,
greytitle:gtitle, blacktitle:ktitle , handler:h } ;
}
/* -------------------------------------------------------------------------- */
function addload(ovr)
{ infowindow.close() ;
if(ovr!='load'&&ovr!='add'&&ovr!='geotag')
return alerter(inject2(M.illegalopt,ovr,M.load),0) ;
if(ovr=='load')
{ var msg = unsavedmsg(1) ; if(msg!=null) if(!confirm(msg)) return ; }
freeze() ;
infowindow.open(filedialogue(ovr),0,1,unfreeze) ;
}
function refresh(segno)
{ infowindow.close() ;
freeze() ;
if(segno||segno==0) refresher(segno) ; else refresher(0,1) ;
}
function refresher(segno,carryon)
{ var s,i,stem=null,segst,segend ;
function renfactory(t,s,c)
{ return function(r)
{ var z=null ;
if(r) z = render(r,t,'refresh',s) ;
if(!z) telltale(inject(M.unable,segments[0].pts[segno].title)) ;
if(carryon&&segno<segments[0].pts.length-1) refresher(segno+1,1) ;
else unfreeze() ;
} ;
} ;
seg = segments[0].pts[segno] ;
s = seg.tlink ;
i = s.indexOf('?') ;
if(i>=0) { stem = s.substring(0,i) ; s = s.substring(i+1) ; }
i = s.indexOf('track=') ;
if(i>=0) s = s.substring(i+6) ;
i = s.indexOf('&') ;
if(i>=0) s = s.substring(0,i) ;
trackuri = reluri(stem,s) ;
s = s.substring(1+s.lastIndexOf('/')) ;
waitprompt(inject(M.waitingfor,s),0) ;
readuri(renfactory(trackuri,segno),trackuri) ;
}
/* -------------------------------------------------------------------------- */
/* WPINFO IS A MENU GIVING ACCESS TO THE SETALT FUNCTION */
/* -------------------------------------------------------------------------- */
function wpinfo()
{ infowindow.close() ;
var s0=selected.segno , s1=selected.ptno , s2=selected.type , pos ;
if(s2) pos = segments[s0].geo[s1].pos ; else pos = segments[s0].pts[s1].pos ;
infowindow.open(wpinfodiv(prefs.precision),pos,'wpinfo') ;
}
/* ---------------------------------- dl ----------------------------------- */
function rellist(list)
{ var i , x = document.URL , str ;
if((i=x.lastIndexOf('?'))>=0) x = x.substring(0,i) ;
return reluri(x,list) ;
}
// opt 'index'/'tour'/'uindex' => as implied
// opt 2 => download index as metaindex
// opt 3 => download segments as segments (never invoked in this way)
// opt null => download track/index/metaindex as is
function dl(opt,e)
{ var i,npix,s0,s1,name,extn,filename=null,legend,ooo,time,tlast,interp=[] ;
var flag , lim , routename ;
infowindow.close() ;
if(e) e.preventDefault() ;
// save as segments if appropriate
if(!opt&&segments[0].level==0&&segments.length>1) opt = 3 ;
if(!opt&&segments[0].level<1) // ie. it's a normal track
{ interp = interpol() ;
if(interp.length>3) done(interp) ;
getalts(segments,1,drawprofile) ;
}
// filename
if(segments[0].level) routename = abbreviate(segments[0].filename) ;
else routename = abbreviate(routeprops.filename) ;
flag = filename = null ;
if(!opt||opt==3)
{ if( routename[0] && routename[1]!='.fit'
&& trackextns.indexOf(routename[1])>=0 ) filename = routename[0] ;
else if(!routename&&segments[0].level>0)
{ if(segments[0].filename) filename = abbreviate(segments[0].filename)[0] ;
else filename = ascify(segments[0].title) ;
}
else if(routeprops.title) { filename = routeprops.title ; flag = 1 ; }
}
if(flag)
{ filename = filename.replace(/[\'\"]/gi,'') ; // remove ascii quotes
filename = filename.split(' ') ;
if(opt) lim = 25 ; else lim = 15 ;
for(i=1;i<filename.length&&filename[0].length+filename[i].length<lim;i++)
filename[0] += filename[i] ;
filename = ascify(filename[0]) ;
}
if(!filename) filename = M.untitled ;
// check for photos
for(npix=s0=0;s0<segments.length;s0++)
for(s1=0;s1<segments[s0].pts.length;s1++)
npix += segments[s0].pts[s1].photo.length ;
// photo list
if(npix>0&&imginfo.status=='ready')
{ if(imginfo.type=='uri') routeprops.list = imginfo.uri ;
else if(imginfo.type=='uriform') routeprops.list = rellist(imginfo.uri) ;
else routeprops.list = '$FILE$/' + imginfo.uri ;
}
// save segments
if(opt==3)
{ legend = M.saveas + ' ' + filename + trackextns[3] ;
function cofactory1()
{ return function() { indexdl(filename+'.rte',name,3) ; } ; } ;
div = domcreate('div',genclickfn(cofactory1(),legend,'hr')) ;
domadd(div,M.combinetosave) ;
infowindow.open(div,8) ;
return ;
}
// save an index as itself or a route as an index
else if(opt) { indexdl(filename+'.rte',name,opt,e) ; return ; }
else if(segments[0].level>0)
{ routeprops.filename = filename + '.rte' ;
indexdl(routeprops.filename,name) ;
return ;
}
// if we get here it's a download of a normal track
div = domcreate('div') ;
for(i=0;i<4;i++)
{ legend = M.saveas + ' ' + filename + trackextns[i] ;
function cofactory(parm) { return function() { confirmeddl(parm) ; } ; } ;
domadd(div,genclickfn(cofactory(filename+trackextns[i]),legend,'br')) ;
}
if(interp.length>3)
domadd(div,genspan(M.gapsfixed,'br','font-style:italic')) ;
for(tlast=null,ooo=i=0;i<segments[0].pts.length;i++)
{ time = segments[0].pts[i].t ;
if(tlast!=null&&time!=null&&time<tlast) ooo = 1 ; // out of order
if(time!=null) tlast = time ;
}
if(ooo) domadd(div,M.yourtimes,domcreate('br')) ;
domadd(div,doclink('outputformats',M.formatdoc)) ;
infowindow.open(div,opt?0:8) ;
}
function canceldl() { infowindow.close() ; }
/* --------------------------------- indexdl -------------------------------- */
function indexdl(filename,name,opt,e)
{ infowindow.close() ;
var i , r , str , optparms , type ;
if(!opt) str = writeindex(segments[0],0) ;
else if(opt==3)
{ routeprops.level = 1 ;
routeprops.type = 'segments' ;
routeprops.pts = segments ;
str = writeindex(routeprops,0) ;
}
else
{ if(segments[0].level) type = 'metaindex' ;
else if(opt=='uindex') type = 'index' ;
else type = opt ;
r = new routetype(type) ;
r.title = name ;
r.pts = new Array(segments.length) ;
r.gallery = routeprops.gallery ;
if(segments[0].level) optparms = metaparms ;
else
{ optparms = { tol:0,maxsep:null,wppenalty:indparms.wppenalty,
vweight:0,maxjump:indparms.maxjump } ;
if(opt=='tour') optparms.vweight = 3 ;
}
for(i=0;i<segments.length;i++)
r.pts[i] = indexify(segments[i],[optparms],null,opt) ;
str = writeindex(r,0) ;
}
if(str) saveAs(new Blob([str],{type:"text/plain;charset=utf-8"}),filename) ;
unsavedchanges = [] ;
}
/* ------------------------------- confirmeddl ------------------------------ */
function confirmeddl(filename)
{ var nnull,i,div ;
dl.pending = filename ;
for(nnull=i=0;nnull==0&&i<segments[0].pts.length;i++)
if(segments[0].pts[i].h==null) nnull = 1 ;
if(nnull==0) { reconfirmeddl() ; return ; }
infowindow.close() ;
div = domcreate('div',inject(M.waitingfor,M.missingalts)) ;
domadd(div,domcreate('br'),M.dontwanttowait+' ') ;
domadd(div,genclickfn(canceldl,M.cancel),'; ') ;
domadd(div,genclickfn(semiconfirmeddl,M.useinterp),'; ') ;
domadd(div,genclickfn(reconfirmeddl,M.savemissing),'.') ;
infowindow.open(div,8) ;
getalts(segments,1,drawprofile,reconfirmeddl) ;
}
function semiconfirmeddl() // turn off callback to reconfirmeddl
{ getalts(null,null,null,null) ; reconfirmeddl() ; }
/* -------------------------------- reconfirmeddl --------------------------- */
function reconfirmeddl()
{ var filename = dl.pending , s = segments[0] ;
if(filename==null) return ;
var i,str,mode,len=filename.length ;//filename is null
infowindow.close() ;
mode = filename.substring(len-4) ;
// record optimisation
routeprops.pts = s.pts ;
routeprops.optim = s.optim ;
routeprops.filename = filename ;
if((str=writegps(routeprops,s.pts,s.geo,mode,prefs.precision,getimgtitle)))
{ unsavedchanges = [] ;
if(mode==".fit") mode = "application/octet-stream" ;
else mode = "text/plain;charset=utf-8" ;
saveAs(new Blob([str],{type:mode}),filename) ;
}
dl.pending = null ;
}
/* -------------------------------------------------------------------------- */
/* OPTIMISATION */
/* -------------------------------------------------------------------------- */
function optimaction(oldpts,parms,segno,newlen)
{ var optim = { already:0 , origlen:oldpts.length , len:newlen , parms } ;
actions[nactions++] = [ 'optimise' , segno , parms , oldpts , optim ] ;
return optim ;
}
function optimparms(v,m)
{ var wp = 100000 * Math.pow(1-v/100,4) , t=2*Math.pow(wp,0.33) ;
return {tol:t,maxsep:m,wppenalty:wp,vweight:3} ;
}
function pen2detail(pen)
{ return Math.floor(0.5+100*(1-Math.pow(pen/100000,1/4))) ; }
/* -------------------------------------------------------------------------- */
function optimwork(segno) // this does the interactive optimisation slider
{ infowindow.close() ;
var d , slider , poly , seg = segments[segno] , div ,i , r=null , box , btns ;
var shadow=null , inp , dd , newseg=null , v , maxsep , sold , dels ;
function dgen(d,v,n) // generates the slider message saying how many points
{ while(d.childNodes.length>0)
d.removeChild(d.childNodes[d.childNodes.length-1]) ;
domadd(d,domcreate('span',v)) ;
if(!n) n = '----' ;
domadd(d,domcreate('div',n+' '+M.points,'style','float:right')) ;
}
bulkout(0,1) ;
recurse(seg,'obliterate') ;
ldisconnect(segno) ;
disconnect(segno) ;
poly = new linepath(seg,-1,0,'darkgrey',3) ;
shadow = L.polyline(poly.path,poly.opts) ;
shadow.addTo(map) ;
if(prefs&&prefs.detail) v = prefs.detail ;
else v = pen2detail(defparms.wppenalty) ;
if(v<1) v = 1 ; else if(v>100) v = 100 ;
if(prefs&&!prefs.maxsep) maxsep = null ; else maxsep = 100 ;
function posit(s)
{ var i,pt ;
deltify(s) ; // this clobbers sold[i].delta
seg.pts = s ;
drawprofile() ;
draw(seg) ;
for(i=0;i<seg.pts.length;i++) if(pt=seg.pts[i],pt.label||pt.photo.length)
{ if(pt.label) pt.setlabel(pt.label,pt.caption,pt.desc) ; pt.setmap() ; }
}
function vender(val,maxval)
{ map.removeLayer(seg.line) ;
for(i=0;i<sold.length;i++) sold[i].delta = dels[i] ;
r = optimise(sold,optimparms(val,maxval),val) ;
posit(r) ;
dgen(dd,val,r.length) ;
}
// store incoming values
sold = seg.pts ;
dels = new Array(sold.length) ;
for(i=0;i<sold.length;i++) dels[i] = sold[i].delta ;
// create slider
slider = domcreate('input',null,'type','range','min',0,'max',100) ;
domset(slider,'value',v,'id',"optimrange",'style','width:140px') ;
slider.onmouseup = function()
{ v = this.value ;
dgen(dd,v) ;
setTimeout(function() { vender(v,maxsep) ; }) ;
}
div = domcreate('div',
domcreate('div',slider,'style','padding-bottom:4px;text-align:center')) ;
// text response
dd = domcreate('div',null,'style',
'margin:0;padding:0 10px 4px 10px;font-size:80%') ;
dgen(dd,v,seg.pts.length) ;
domadd(div,dd) ;
vender(v,maxsep) ;
optimwork.advance = function(dir)
{ slider.value = Math.max(0,Math.min(100,Number(slider.value)+dir)) ;
dgen(dd,slider.value) ;
setTimeout(function() { vender(slider.value,maxsep) ; }) ;
}
// limit point separation?
d = domcreate('div',domcreate('label',M.limsep,'for','limsep'),'style',
'padding:0 0 4px 10px;white-space:nowrap') ;
box = domcreate('input',null,'type','checkbox','id','limsep') ;
if(maxsep) domset(box,'checked',true) ;
box.onchange = function()
{ if(box.checked) maxsep = 100 ; else maxsep = 0 ;
dgen(dd,v) ;
setTimeout(function() { vender(v,maxsep) ; }) ;
}
domadd(d,box) ;
domadd(div,d) ;
// remove the overlaid lines and reset buttons
function unshadow()
{ map.removeLayer(seg.line) ;
map.removeLayer(shadow) ;
infowindow.close() ;
bulkout(1,1) ;
dragging = 0 ;
optimwork.advance = null ;
}
// cancel button
inp = domcreate('button',caps(M.cancel),'style',btnstyle()) ;
inp.onclick = function() { unshadow() ; posit(sold) ; }
btns = btnbox(inp) ;
// submit button
inp = domcreate('button',M.ok,'type','submit','style',btnstyle(1)) ;
/* ------------------------------------------------------------------------ */
inp.onclick = function()
{ var ip,box ;
optimaction(sold,optimparms(v,maxsep),segno,r.length) ;
donesomething() ;
selected.ptno = 0 ;
if(!prefs.email||(v==prefs.detail&&(!!maxsep)==!!prefs.maxsep))
{ unshadow() ; posit(r) ; drawsel() ; return ; }
div = domcreate('div',domcreate('div',M.detail)) ;
box = domcreate('input',null,'type','checkbox','id','savepref') ;
ip = domcreate('div',box) ;
domadd(ip,domcreate('label',M.savepref,'for','savepref')) ;
div.appendChild(ip) ;
// handle submit/return key
function optimfactory()
{ return function()
{ unshadow() ;
posit(r) ;
drawsel() ;
if(box.checked)
{ prefs.detail = v ;
prefs.maxsep = maxsep ;
var xhttp = new XMLHttpRequest() ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ if(xhttp.response.length==0) alerter(M.dberr,1) ;
else if(xhttp.response.substring(0,9)=="**sqlerr:")
alerter(xhttp.response,1) ;
}
}
xhttp.open("GET","resources/signup.php?opts="+
prefs.detail+'&'+prefs.maxsep) ;
xhttp.send() ;
}
}
}
btn = domcreate('button',M.ok,'type','submit','style',btnstyle(1)) ;
btn.onclick = optimfactory() ;
domadd(div,domcreate('p',btn,'style','text-align:center')) ;
freeze() ;
infowindow.open(div,1,1,unfreeze) ;
}
/* ------------------------------------------------------------------------ */
domadd(btns,inp) ;
domadd(div,btns) ;
infowindow.open(div,1,1,unfreeze) ;
dragging = 3 ; // special value to allow use of arrow keys
}
/* -------------------------------------------------------------------------- */
function redelta(pts,s1)
{ if(s1>0) pts[s1-1].delta = dist(pts[s1-1].pos,pts[s1].pos) ;
if(s1<pts.length-1) pts[s1].delta = dist(pts[s1].pos,pts[s1+1].pos) ;
else pts[s1].delta = null ;
}
/* ------------------------------- retitle ---------------------------------- */
function retitle(opt,segno)
{ var newval , oldval , useval , msg , s0 , s1 , i , ind , retpage , item ;
var filename = (segno&&segments[segno].origin)
?segments[segno].origin[0]:null ;
if(segno!=null&&segno!=undefined) oldval = segments[segno][opt] ;
else
{ if(segments[0].level>0) oldval = segments[0][opt] ;
else oldval = routeprops[opt] ;
segno = null ;
}
infowindow.close() ;
if(oldval==null)
{ if(opt=='title') msg = M.addtitle ; else if(opt=='desc') msg = M.adddesc ; }
else
{ if(opt=='title') msg = caps(M.edittitle) ;
else if(opt=='desc') msg = caps(M.editdesc) ;
}
msg += ':' ;
useval = oldval ;
if(opt=='desc') i = prompter(msg,oldval==null?'':oldval,respond,'desc') ;
else i = prompter(msg,useval==null?'':useval,respond) ;
freeze() ;
infowindow.open(i[0],0,1,unfreeze) ;
i[1].focus() ;
function respond(newval)
{ var list ;
if(newval==null||newval==oldval) return ;
if(newval==''&&opt!='desc') return ;
if(oldval&&newval&&segno==null&&opt=='desc')
{ for(list=[],i=0;i<segments.length;i++)
if(segments[i].desc==oldval) list.push(i) ;
for(i=0;i<list.length;i++) segments[list[i]].desc = null ;
}
actions[nactions++] = [ 'edit'+opt , oldval , newval , segno ,list ] ;
if(opt=='desc') setdesc(segno,newval) ;
else if(segno==null)
{ setdomtitle(routeprops.title=newval) ; routeprops.filename = null ; }
else if(segments[0].level&&segno==0)
setdomtitle(segments[segno].title=newval) ;
else segments[segno].title = newval ;
if(segno!=null&&opt!='desc'&&segments[segno].origin)
segments[segno].origin[0] = null ;
donesomething() ;
if(segno==null||!segbtn) routeinfo() ;
// doesn't work for desc for unknown reasons. it seems that the InfoWindow
// open() has no effect, but there are no diagnostics to explain why.
// logically it ought to return a status code.
else seginfo() ;
}
}
function setdesc(segno,desc)
{ if(segments[0].level) segments[0].desc = desc ;
else if(segno==null) routeprops.desc = desc ;
else segments[segno].desc = desc ;
}
/* ------------------------------- restars ---------------------------------- */
function restars(oldstars,newstars,starsdiv)
{ actions[nactions++] = [ 'stars' , oldstars , newstars ] ;
donesomething() ;
starsline(routeprops.stars=newstars,1,starsdiv) ;
}
/* -------------------------------------------------------------------------- */
function routeinfo() { infowindow.open(cogwheelmenu(dragging),0) ; }
function eyedisplay() { infowindow.open(eyemenu(),-1) ; }
/* ------------------------------- calwork --------------------------------- */
function calwork(seg,y)
{ var i,s1 ;
for(s1=0;s1<seg.pts.length;s1++) if(seg.pts[s1].h!=null) seg.pts[s1].h += y ;
drawprofile() ;
}
/* ------------------------------ manualcal --------------------------------- */
function manualcal()
{ infowindow.close() ;
var x,y,s0=selected.segno, r = prompter(M.enteroffset,null,docal) ;
freeze() ;
infowindow.open(r[0],1,null,unfreeze) ;
r[1].focus() ;
function docal(x)
{ y = parseFloat(x) ;
if(isNaN(y)) return alerter(inject(M.isnan,x),1) ;
else { calwork(segments[s0],y) ; done(['recal',s0,y]) ; }
}
}
/* ---------------------------- googlecalwork ------------------------------ */
function googlecalwork(s0)
{ var i,s1 ;
for(s1=0;s1<segments[s0].pts.length;s1++) segments[s0].pts[s1].h = null ;
getalts([segments[s0]],1,drawprofile) ;
}
/* ------------------------------ googlecal --------------------------------- */
function googlecal()
{ infowindow.close() ;
var i,s0=selected.segno,len=segments[s0].pts.length,alt=new Array(len) ;
for(i=0;i<len;i++) alt[i] = segments[s0].pts[i].h ;
googlecalwork(s0) ;
done(['googlecal',s0,alt]) ;
}
/* ------------------------------ googlereg --------------------------------- */
function googlereg()
{ var s0 = selected.segno ;
infowindow.close() ;
getalts([segments[s0]],-1,drawprofile,s0) ;
}
function googleadd()
{ var s0 = selected.segno ;
infowindow.close() ;
getalts([segments[s0]],-2,drawprofile,s0) ;
}
/* --------------------------------- help ----------------------------------- */
function help() { infowindow.open(rmhelpdiv(segments[0].level),0) ; }
/* -------------------------------- sidestep -------------------------------- */
function sidestep(seg,s0,s1,pos)
{ var i,mindist
for(mindist=null,i=0;i<seg[s0].pts.length;i++)
if(mindist==null||dist(seg[s0].pts[i].pos,pos)<mindist)
{ mindist = dist(seg[s0].pts[i].pos,pos) ;
selected = { segno:s0 , ptno:i , type:null } ;
}
for(i=0;i<seg[s0].geo.length;i++) if(i!=s1)
if(mindist==null||dist(seg[s0].geo[i].pos,pos)<mindist)
{ mindist = dist(seg[s0].geo[i].pos,pos) ;
selected = { segno:s0 , ptno:i , type:'geo' } ;
}
}
/* --------------------------------- wpdel ---------------------------------- */
function wpdelwork(seg,s0,s1,s2,opt)
{ var i,pts=s2?seg[s0].geo:seg[s0].pts,pt=pts[s1],clen=pts.length,mindist ;
if(!opt) ungeodraw(pt) ; // no op if not geo
if(pt.photo.length) pt.unmapphoto() ;
if(pt.label) pt.unmaplabel() ;
for(i=s1;i<clen-1;i++) pts[i] = pts[i+1] ;
pts.length = clen-1 ;
// if a deletion (rather than detachment) determine new selection point
if(s2&&!opt) sidestep(seg,s0,s1,pt.pos) ;
else if(!opt)
{ if(!pts.length) selected = { segno:s0 , ptno:0 , type:'geo' } ;
else selected = { segno:s0 , ptno:s1==pts.length?s1-1:s1 , type:null } ;
}
if(!s2) // if we’re deleting/detaching a waypoint
{ if(s1>0) redelta(pts,s1-1) ; else if(pts.length) redelta(pts,s1) ;
if(pts.length) redrawconnect(s0,s1?s1-1:0) ; // was (s0,s1) before 10 Jun 25
else { ldisconnect(s0) ; disconnect(s0) ; lconnect(s0) ; }
drawprofile() ;
}
return pt ;
}
function wpdel(opt) // opt means detach/attach; opt==2 is used in redo
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type,i,r ;
var seg = segments[s0] ;
if(!opt&&seg.pts.length+seg.geo.length==1) return discard() ;
var flag = infowindow.close() ;
r = wpdelwork(segments,s0,s1,s2,opt) ;
if(s2&&opt)
{ seg.pts.push(r) ;
if(seg.pts.length>1) redelta(seg.pts,seg.pts.length-1) ;
redrawconnect(s0,seg.pts.length-1) ;
drawprofile() ;
selected = { segno:s0 , ptno:seg.pts.length-1 , type:null } ;
}
if(!s2&&opt)
{ seg.geo.push(r) ;
geodraw(r,seg.colour) ;
selected = { segno:s0 , ptno:seg.geo.length-1 , type:'geo' } ;
}
if(opt==1) done(['detach',s0,s1,s2,r]) ; // don't add an action for a redo
else if(!opt) done(['wpdel',s0,s1,s2,r]) ;
drawsel() ;
if(flag=='wpinfo') wpinfo() ;
}
function undowpdel(action,s0,s1,s2,pt)
{ var pts = s2?segments[s0].geo:segments[s0].pts ;
insert(pts,s1,1) ;
pts[s1] = pt ;
pt.setmap() ; // this does a geodraw if needed
if(!s2) { redelta(pts,s1) ; redrawconnect(s0,s1) ; }
if(action=='detach') //undo an attach if s2 is 'geo' or a detach if s2 is null
{ if(s2) { segments[s0].pts.length -= 1 ; redraw(s0) ; }
else { segments[s0].geo.length -= 1 ; ungeodraw(pt) ; }
}
selected = { segno:s0 , ptno:s1 , type:s2 } ;
drawsel() ;
}
/* --------------------------------- revseg --------------------------------- */
function revsegwork(seg,s0)
{ var d=seg[s0].pts,len=d.length,lim,i ;
ldisconnect(s0) ;
disconnect(s0) ;
flipseg(d) ;
if(s0==selected.segno) selected.ptno = (len-1) - selected.ptno ;
lconnect(s0) ;
connect(s0) ;
drawsel() ;
drawprofile() ;
}
/* -------------------------------------------------------------------------- */
function revseg()
{ infowindow.close() ;
revsegwork(segments,selected.segno) ;
done(['revseg',selected.segno]) ;
}
/* --------------------------------- dupseg --------------------------------- */
function dupsegwork(seg,s0)
{ var i,newcol=snipcolour(seg[s0].hue),n=seg.length ;
seg[n] = seg[s0].clone() ;
seg[n].pts = new Array(seg[s0].pts.length) ;
for(i=0;i<seg[s0].pts.length;i++)
{ seg[n].pts[i] = seg[s0].pts[i].clone() ;
seg[n].pts[i].photo = [] ;
seg[n].pts[i].photomarker = seg[n].pts[i].marker = null ;
if(seg[s0].pts[i].label||seg[s0].pts[i].caption)
seg[n].pts[i].setlabel(seg[s0].pts[i].label,seg[s0].pts[i].caption,
seg[s0].pts[i].desc) ;
}
recurse(seg[n],'genwaymark') ;
seg[n].dots = null ;
seg[n].geo = [] ;
seg[n].hue = seg[s0].hue ;
seg[n].colour = newcol ;
draw(seg[n]) ;
lconnect(n) ;
if(selected.segno==s0) selected.segno = n ;
drawpro.pro.active = 1 ;
drawprofile() ;
drawsel() ;
}
/* -------------------------------------------------------------------------- */
function dupseg()
{ infowindow.close() ;
dupsegwork(segments,selected.segno) ;
done(['dupseg',selected.segno]) ;
}
/* -------------------------------------------------------------------------- */
/* DRAGGING A (POSSIBLY NEWLY INSERTED) WAYPOINT IS QUITE A LOT OF WORK */
/* -------------------------------------------------------------------------- */
function insert(pts,s1,n)
{ var i ;
for(i=pts.length+n-1;i>s1;i--) pts[i] = pts[i-n] ;
for(i=0;i<n;i++) pts[s1+i] = new pttype(null,null) ;
}
/* --------------------------------- inswp ---------------------------------- */
function inswp(dir)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
var bounds,del,pos,pts=s2?segments[s0].geo:segments[s0].pts ;
var len = pts.length ;
if(len==1||s2) pos = pts[s1].pos ;
if(s2) s1 = selected.ptno = pts.length ;
else if(dir>=0) s1 = ( selected.ptno += 1 ) ;
insert(pts,s1,1) ;
if(len==1||s2)
{ bounds = map.getBounds() ;
del = bounds.getNorthEast().lng() - bounds.getSouthWest().lng() ;
pos = { lat:pos.lat , lng:pos.lng+dir*del/10 } ;
}
else if(s1==0) pos = interp(pts[2].pos,pts[1].pos,1.5) ;
else if(s1<len) pos = interp(pts[s1-1].pos,pts[s1+1].pos,0.5) ;
else pos = interp(pts[s1-2].pos,pts[s1-1].pos,1.5) ;
pts[s1].pos = pos ;
if(s2) geodraw(pts[s1],segments[s0].colour) ;
else redelta(pts,s1) ;
draggit(1) ;
}
/* ------------------------------------ insgeo ------------------------------ */
function insgeo(pos,name,opt)
{ var s0=selected.segno , pts=segments[s0].geo , len=pts.length ;
pts[len] = new pttype(pos) ;
pts[len].setlabel('Generic',name) ;
geodraw(pts[len],segments[s0].colour) ;
if(!opt)
{ map.panTo(pos) ;
selected = { segno:selected.segno , ptno:len , type:'geo' } ;
walkto(selected.segno,len,'geo') ;
}
if(opt!=null) done(['insgeo',selected.segno,len,pos,name]) ;
}
/* ------------------------------------ geotag ------------------------------ */
function geotag(response,filename)
{ var dataview = new DataView(response) , length = response.byteLength ;
var bigend , tags , tag , exif , gps , start , i , n , ind , gpsptr ;
if(dataview.getUint8(0)!=0xFF||dataview.getUint8(1)!= 0xD8)
{ alerter(M.badjpg+filename,0) ; return null ; }
for(offset=2;offset<length;offset+=2+dataview.getUint16(offset+2))
{ if(dataview.getUint8(offset)!=0xFF)
{ alerter(M.badjpg+filename,0) ; return null ; }
if(dataview.getUint8(offset+1)==225) break ;
}
if(getexifstring(dataview,offset+4,4)!="Exif")
{ alerter(M.badjpg+filename) ; return null ; }
offset += 10 ;
// test for TIFF validity and endianness
if(dataview.getUint16(offset)==0x4949) bigend = 0 ;
else if(dataview.getUint16(offset)==0x4D4D) bigend = 1 ;
else { alerter(M.badjpg+filename,0) ; return null ; }
if(dataview.getUint16(offset+2,1-bigend)!=0x002A)
{ alerter(M.badjpg+filename,0) ; return null ; }
start = dataview.getUint32(offset+4,1-bigend) ;
if(start<0x00000008) { alerter(M.badjpg+filename,0) ; return null ; }
n = dataview.getUint16(offset+start,!bigend) ;
for(gpsptr=null,i=0;i<n&&!gpsptr;i++)
{ k = i*12 + offset + start + 2 ;
if(dataview.getUint16(k,!bigend)==0x8825)
gpsptr = readexiftag(dataview,k,offset,offset+start,bigend) ;
}
if(gpsptr==null) { alerter(M.notgeotagged+filename,0) ; return null ; }
return getexifgps(dataview,offset,gpsptr,bigend) ;
}
/* -------------------------------- getexifgps ------------------------------ */
function getexifgps(dataview,offset,start,bigend)
{ var n = dataview.getUint16(offset+start,!bigend) , key , k , i ;
var vals=[null,null,null,null],negs=[0,'S',0,'W'],r={lat:null,lng:null} ;
for(i=0;i<n;i++)
{ k = i*12 + offset + start + 2 ;
key = dataview.getUint16(k,!bigend) - 1 ;
if(key>=0&&key<4)
vals[key] = readexiftag(dataview,k,offset,offset+start,bigend) ;
}
for(i=1;i<4;i+=2)
{ if(vals[i]==null||vals[i].length!=3) return null ;
k = vals[i][0] + (vals[i][1]+vals[i][2]/60)/60 ;
key = vals[i-1] ;
if( key && ( (typeof(key)=='number'&&key<0)
|| (typeof(key)=='string'&&key.charAt(0)==negs[i]) ) ) k = -k ;
if(i==1) r.lat = k ; else r.lng = k ;
}
return r ;
}
/* ------------------------------- readexiftag ------------------------------ */
function readexiftag(data,k,tiffoffs,diroffs,bigend)
{ var n = data.getUint32(k+4,!bigend) , offset,vals,i,num,den,
valoffs = data.getUint32(k+8,!bigend)+tiffoffs ;
switch(data.getUint16(k+2,!bigend))
{ case 1: // byte, 8-bit unsigned int
case 7: // undefined, 8-bit byte, value depending on field
offset = n>4 ? valoffs : (k+8) ;
for(vals=new Array(n),i=0;i<n;i++) vals[i] = data.getUint8(offset+i) ;
if(n==1) return vals[0] ; else return vals ;
case 2: // ascii, 8-bit byte
offset = n>4 ? valoffs : (k+8) ;
return getexifstring(data,offset,n-1) ;
case 3: // short, 16 bit int
offset = n>2 ? valoffs : (k+8) ;
for(vals=new Array(n),i=0;i<n;i++)
vals[i] = data.getUint16(offset+2*i,!bigend) ;
if(n==1) return vals[0] ; else return vals ;
case 4: // long, 32 bit int
offset = (n>1) ? valoffs : (k+8) ;
for(vals=new Array(n),i=0;i<n;i++)
vals[i] = data.getUint32(offset+4*i,!bigend) ;
if(n==1) return vals[0] ; else return vals ;
case 5: // rational = two long values, first is num, second is den
for(vals=new Array(n),i=0;i<n;i++)
{ num = data.getUint32(valoffs+8*i,!bigend) ;
den = data.getUint32(valoffs+4+8*i,!bigend);
vals[i] = new Number(num/den) ;
vals[i].num = num ;
vals[i].den = den ;
}
if(n==1) return vals[0] ; else return vals ;
case 9: // slong, 32 bit signed int
offset = (n>1) ? valoffs : (k+8) ;
for(vals=new Array(n),i=0;i<n;i++)
vals[i] = data.getInt32(offset+4*i,!bigend) ;
if(n==1) return vals[0] ; else return vals ;
case 10: // signed rational, two slongs, first is num, second is den
for(vals=new Array(n),i=0;i<n;i++)
vals[i] = data.getInt32(valoffs+8*i,!bigend) /
data.getInt32(valoffs+4+8*i,!bigend) ;
if(n==1) return vals[0] ; else return vals ;
}
}
function getexifstring(buffer,start,length)
{ var outstr , n ;
for(outstr='',n=start;n<start+length;n++)
outstr += String.fromCharCode(buffer.getUint8(n)) ;
return outstr;
}
/* -------------------------------- draggit --------------------------------- */
// draggit makes the current waypoint draggable
function draggit(insparm)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
var start,end,i,poly , seg=segments[s0] , pts=s2?seg.geo:seg.pts ;
var len=pts.length , startpos , colour , pt = pts[s1] ;
draggit.startpos = startpos = pt.pos ;
draggit.inserted = insparm?1:0 ;
infowindow.close() ;
bulkout(0) ;
if(!s2) map.removeLayer(seg.line) ;
drawsel('drag') ;
colour = seg.colour ;
moveto(startpos) ;
if(!s2)
{ draggit.seg0 = draggit.seg2 = null;
if(s1>1)
{ poly = new linepath(seg,0,s1,colour) ;
draggit.seg0 = L.polyline(poly.path,poly.opts) ;
draggit.seg0.addTo(map) ;
}
if(s1==0) start = 0 ; else start = s1-1 ;
if(s1==len-1) end = s1+1 ; else end = s1+2 ;
poly = new linepath(seg,start,end,colour) ;
draggit.seg1 = L.polyline(poly.path,poly.opts) ;
draggit.seg1.addTo(map) ;
if(s1<seg.pts.length-2)
{ poly = new linepath(seg,s1+1,len,colour) ;
draggit.seg2 = L.polyline(poly.path,poly.opts) ;
draggit.seg2.addTo(map) ;
}
}
function dragger(e)
{ var marker = e.target , pos = marker.getLatLng() ;
pt.pos = pos ;
if(!s2)
{ map.removeLayer(draggit.seg1) ;
poly = new linepath(seg,start,end,colour) ;
draggit.seg1 = L.polyline(poly.path,poly.opts) ;
draggit.seg1.addTo(map) ;
if(s1==0) { ldisconnect(s0) ; lconnect(s0) ; }
if(s1==len-1&&s0<segments.length-1) { disconnect(s0) ; connect(s0) ; }
}
}
drawsel.marker.on("drag",dragger) ;
dragging = 1 ;
}
/* ------------------------------- undraggit -------------------------------- */
// undraggit is invoked by [space] to terminate waypoint dragging
function undraggit()
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
var i,pts=s2?segments[s0].geo:segments[s0].pts , pt = pts[s1] ;
var pos=pt.pos , movepix = dist(draggit.startpos,pos) / getscale() ;
if(movepix<2) pt.pos = draggit.startpos ; else pt.h = null ;
dragging = 0 ;
if(!s2)
{ if(draggit.seg0) map.removeLayer(draggit.seg0) ;
map.removeLayer(draggit.seg1) ;
if(draggit.seg2) map.removeLayer(draggit.seg2) ;
draw(segments[s0]) ;
getalts(segments,200,drawprofile) ;
redelta(pts,s1) ;
drawprofile() ;
}
drawsel() ;
if(draggit.inserted||movepix>=2)
done(['move',s0,s1,s2,draggit.startpos,pos,draggit.inserted]) ;
bulkout(1) ;
}
/* -------------------------------------------------------------------------- */
function seginfo()
{ infowindow.open(seginfodiv(segments,selected.segno),1,'seginfo') ; }
function altinfo()
{ infowindow.open(altinfodiv(segments,selected.segno),1,'altinfo') ; }
/* -------------------------------------------------------------------------- */
function deltimes(s0)
{ var s1,task=[] ;
for(s1=0;s1<segments[s0].pts.length;s1++) if(segments[s0].pts[s1].t!=null)
{ task.push([s1,segments[s0].pts[s1].t,0]) ; segments[s0].pts[s1].t = null ; }
for(s1=0;s1<segments[s0].geo.length;s1++) if(segments[s0].geo[s1].t!=null)
{ task.push([s1,segments[s0].geo[s1].t,1]) ; segments[s0].geo[s1].t = null ; }
infowindow.close() ;
done(['deltimes',s0,task]) ;
}
/* ------------------------- interpolate extra points ----------------------- */
function interpol()
{ var s0,s1,pts,n,opos,npos,i,lambda ;
var task = [ 'extra' , selected.segno , selected.ptno ] ;
for(s0=0;s0<segments.length;s0++)
for(pts=segments[s0].pts,s1=1;s1<pts.length;s1++)
if(pts[s1-1].delta>100)
{ opos = pts[s1-1].pos ;
npos = pts[s1].pos ;
n = Math.floor(pts[s1-1].delta/95) ;
insert(segments[s0].pts,s1,n) ;
for(i=0;i<n;i++)
{ lambda = (i+1) / (n+1) ;
pts[s1+i].pos = { lat:lambda*npos.lat+(1-lambda)*opos.lat ,
lng:lambda*npos.lng+(1-lambda)*opos.lng } ;
}
for(i=0;i<=n;i++) redelta(pts,s1+i) ;
if(selected.segno==s0&&s1<=selected.ptno) selected.ptno += n ;
task.push([s0,s1,pts.slice(s1-1,s1+n+1)]) ;
s1 += n ;
}
return task ;
}
function extrapts(opt)
{ infowindow.close() ;
var task = interpol() ;
getalts(segments,1,drawprofile) ;
done(task) ;
if(opt==1) dl(0) ; else routeinfo() ;
}
/* ------------------------------- combine1 --------------------------------- */
function combine1(seg,sa,sb)
{ var n=seg[sa].pts.length,ngeo=seg[sa].geo.length ;
undraw(seg[sb]) ;
if(sb>0) ldisconnect(sb) ;
if(selected.segno==sb)
{ if(selected.type)
selected = { segno:sa , ptno:selected.ptno+ngeo , type:'geo' } ;
else selected = { segno:sa , ptno:selected.ptno+n , type:null } ;
}
seg[sa].pts = seg[sa].pts.concat(seg[sb].pts) ;
seg[sa].geo = seg[sa].geo.concat(seg[sb].geo) ;
if(n&&seg[sa].pts.length>n) redelta(seg[sa].pts,n) ;
}
function combinework(seg,base,n)
{ var s0,i ;
disconnect(base+n-1) ;
for(s0=base+1;s0<base+n;s0++) combine1(seg,base,s0) ;
for(s0=base+n;s0<seg.length;s0++) seg[s0-(n-1)] = seg[s0] ;
seg.length -= n-1 ;
if(selected.segno>base) for(i=base;i<selected.segno;i++)
{ selected.segno -= 1 ;
if(selected.type) selected.ptno += seg[i].geo.length ;
else selected.ptno += seg[i].pts.length ;
}
redraw(base) ;
connect(base) ;
drawprofile() ;
drawsel() ;
}
/* -------------------------------------------------------------------------- */
function combiner(seg,base,n)
{ var i , task = [ 'combine' , base , n ] ;
infowindow.close() ;
for(i=base;i<base+n;i++)
task.push(seg[i].pts.length,seg[i].geo.length, // is item 2 (seg[i]) used?
seg[i],seg[i].hue,seg[i].colour) ;
combinework(seg,base,n) ;
done(task) ;
}
function combine() { combiner(segments,0,segments.length) ; }
function combinef() { combiner(segments,selected.segno,2) ; }
function combineb() { combiner(segments,selected.segno-1,2) ; }
/* -------------------------------------------------------------------------- */
function uncombine(seg,task)
{ var i,j,s0,llen,nlen,base=task[1],n=task[2] ;
seg.length += n-1 ;
for(s0=seg.length-1;s0-(n-1)>base;s0--) seg[s0] = seg[s0-(n-1)] ;
disconnect(base) ;
for(s0=n-1,i=task.length-5;i>=5;i-=5,s0--)
{ // the waypoints
llen = seg[base].pts.length - task[i] ;
seg[base+s0] = new routetype() ;
seg[base+s0].pts = seg[base].pts.slice(llen) ;
seg[base+s0].pts[seg[base+s0].pts.length-1].delta = null ;
seg[base+s0].hue = task[i+3] ;
seg[base+s0].colour = task[i+4] ;
draw(seg[base+s0]) ;
connect(base+s0) ;
seg[base].pts.length = llen ;
// the geopoints
llen = seg[base].geo.length - task[i+1] ;
seg[base+s0].geo = seg[base].geo.slice(llen) ;
seg[base].geo.length = llen ;
for(j=0;j<seg[base+s0].geo.length;j++)
{ seg[base+s0].geo[j].setmap(map,selpoint) ;
geodraw(seg[base+s0].geo[j],seg[base+s0].colour) ;
}
}
seg[base].pts.length = task[3] ;
seg[base].pts[task[3]-1].delta = null ;
connect(base) ;
redraw(base) ;
if(selected.segno>base) selected.segno += n-1 ;
while(selected.ptno>=(n=seg[selected.segno].pts.length))
{ selected.ptno -= n ; selected.segno += 1 ; }
drawprofile() ;
drawsel() ;
}
/* -------------------------------- setalt ---------------------------------- */
function setalt(edit,precision)
{ infowindow.close() ;
var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
var r,y=null,oldalt=null,pt=s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
if(edit) oldalt = pt.h.toFixed(precision) ;
r = prompter(M.enteralt,oldalt,doalt) ;
freeze() ;
infowindow.open(r[0],pt.pos,1,unfreeze) ;
r[1].focus() ;
function doalt(x)
{ if(x&&isNaN(y=parseFloat(x))) return alerter(inject(M.isnan,x),pt.pos) ;
if(y==null&&oldalt==null) return ;
if(y!=null&&Math.abs(y-oldalt)<Math.pow(0.1,precision)/2) return ;
done(['setalt',s0,s1,s2,pt.h,y]) ;
pt.h = y ;
if(!s2) drawprofile() ;
wpinfo() ;
}
}
function delalts(s0)
{ var s1,task=[] ;
for(s1=0;s1<segments[s0].pts.length;s1++) if(segments[s0].pts[s1].h!=null)
{ task.push([s1,segments[s0].pts[s1].h]) ; segments[s0].pts[s1].h = null ; }
infowindow.close() ;
done(['delalts',s0,task]) ;
drawprofile() ;
}
/* -------------------------------------------------------------------------- */
/* THE LABELS ARE ACCESSED FROM THE PEN BUTTON OR BY CLICKING ON THE MAP */
/* -------------------------------------------------------------------------- */
function waymarkprompt(opt)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
var pt=s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
var menu = waymarkdiv(pt,infowindow.close()=='wpinfo') ;
freeze() ;
infowindow.open(menu[0],opt?pt.pos:4,1,unfreeze) ;
menu[1].focus() ;
}
function unlabel()
{ var s0,s1,lab=[],pt ;
for(s0=0;s0<segments.length;s0++) for(s1=0;s1<segments[s0].pts.length;s1++)
{ pt = segments[s0].pts[s1] ;
if(pt.label)
{ lab.push([ s0 , s1 , pt.label , pt.caption , pt.desc ]) ;
pt.unsetlabel() ;
}
}
if(lab.length) done(['unlabel',lab]) ;
routeinfo() ;
}
function editlabel(caption,label,desc,oldcaption,oldlabno,olddesc,flag)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
var pt=s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
if(caption==null) desc = label = null ; // ‘delete’ button chosen
else if(!caption) caption = null ;
if(label==null&&oldlabno>=0) // delete the label
{ if(oldlabno>=0) label = waymarks[oldlabno].name ; else label = null ;
done(['editlabel',s0,s1,s2,oldcaption,null,label,null,olddesc,null]) ;
pt.unsetlabel() ;
if(flag) wpinfo() ; else walkto(s0,s1,s2) ;
}
else if(label!=null) // modify the label
{ label = waymarks[label].name ;
if(oldlabno>=0) oldlabno = waymarks[oldlabno].name ; else oldlabno = null ;
if(caption!=oldcaption||label!=oldlabno||desc!=olddesc)
{ pt.setlabel(label,caption,desc) ;
pt.setlabelmap() ;
done(['editlabel',s0,s1,s2,
oldcaption,caption,oldlabno,label,olddesc,desc]) ;
}
if(flag) wpinfo() ; else walkto(s0,s1,s2) ;
}
}
function undoeditlabel(s0,s1,s2,oldlabno,oldcaption,olddesc)
{ var pt ;
if(s2) pt = segments[s0].geo[s1] ; else pt = segments[s0].pts[s1] ;
if(oldlabno==null) pt.unsetlabel() ;
else { pt.setlabel(oldlabno,oldcaption,olddesc) ; pt.setlabelmap() ; }
}
/* -------------------------------------------------------------------------- */
function photoprompt(e)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type,pt,k,pos ;
pt = s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
if(typeof(e)=='number') pos = pt.pos ;
else { pos = 5 ; if(e) e.preventDefault() ; }
var flag = (infowindow.close()=='wpinfo') ;
if((imginfo.status!='ready'&&imginfo.status!='nolist')||imginfo.carriedover)
{ freeze() ;
infowindow.open(filedialogue("list"),5,1,unfreeze) ;
return ;
}
k = prompter(M.enterphoto,null,photoadd) ;
freeze() ;
infowindow.open(k[0],pos,1,unfreeze) ;
k[1].focus() ;
function photoadd(photo)
{ if(photo)
{ done(['editphoto',s0,s1,s2,pt.photo.length,null,photo]) ;
if(pt.photo.length) pt.unmapphoto() ;
pt.photo.push(photo) ;
genphotomark(pt,0) ;
walkto(s0,s1,s2) ;
return ;
}
if(flag==1) wpinfo() ;
else if(flag==2) seginfo() ;
else if(flag==3) highlight(segments[0].pts) ;
else walkto(s0,s1,s2) ;
}
}
/* -------------------------------------------------------------------------- */
function photomerge(p,oldp,newp,ind) // put newp in place of oldp at ind
{ var i ; // all that matters about oldp is if null
if(!oldp) for(i=p.length;i>ind;i--) p[i] = p[i-1] ;
if(newp) p[ind] = newp ;
else { for(i=ind;i<p.length-1;i++) p[i] = p[i+1] ; p.length -= 1 ; }
}
function photoedit(ind)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type,i,k ;
var flag = (infowindow.close()=='wpinfo') ;
var pt = s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
k = prompter(L.newphoto,pt.photo[ind],phollowon,'del') ;
freeze() ;
infowindow.open(k[0],pt.pos,1,unfreeze) ;
k[1].focus() ;
function phollowon(photo)
{ done(['editphoto',s0,s1,s2,ind,pt.photo[ind],photo]) ;
if(photo&&pt.photo[ind]==photo)
{ if(flag) wpinfo() ; else walkto(s0,s1,s2) ; return ; }
photomerge(pt.photo,pt.photo[ind],photo,ind) ;
if(pt.photo.length==0) pt.unmapphoto() ;
else
{ if(ind==0) { pt.unmapphoto() ; genphotomark(pt,0) ; }
else pt.photomarker.title = phototitle(pt) ;
}
if(flag) wpinfo() ; else walkto(s0,s1,s2) ;
}
}
/* ----------------------------- display photo ------------------------------ */
var lmove,rmove ;
function phadvance(s0,s1,ind)
{ var i ;
for(ind++;;ind++)
{ if(ind>=segments[s0].pts[s1].photo.length) { ind = 0 ; s1 += 1 ; }
if(s1==segments[s0].pts.length)
{ s0 += 1 ; if(s0==segments.length) return null ; else s1 = 0 ; }
if(ind<segments[s0].pts[s1].photo.length)
if((i=findimg(segments[s0].pts[s1].photo[ind]))) return [s0,s1,ind,i] ;
}
}
function phretreat(s0,s1,ind)
{ var i ;
for(ind--;;ind--)
{ if(ind<0) { s1 -= 1 ; ind = -1 ; }
if(s1<0)
{ if(s0==0) return null ; else s0 -= 1 ;
s1 = segments[s0].pts.length-1 ;
}
if(ind<0) ind = segments[s0].pts[s1].photo.length - 1 ;
if(ind>=0&&(i=findimg(segments[s0].pts[s1].photo[ind])))
return [s0,s1,ind,i] ;
}
}
function next() { dodisplay(rmove[0],rmove[1],rmove[2],1) ; }
function prev() { dodisplay(lmove[0],lmove[1],lmove[2],-1) ; }
function backtogps()
{ keyhandler = keystroke ;
window.removeEventListener('resize',function(){genpic('resize');}) ;
window.removeEventListener('touchstart',startswipe,false) ;
window.removeEventListener('touchmove',midswipe,false) ;
window.removeEventListener('touchend',endswipe,false) ;
genmenu('del') ;
genpic() ;
mapparent.removeChild(imgdiv) ;
imgdiv = null ;
walkto(selected.segno,selected.ptno,selected.type,0) ;
}
function display(ind)
{ window.addEventListener('resize',function(){genpic('resize');}) ;
window.addEventListener('touchstart',startswipe,false) ;
window.addEventListener('touchmove',midswipe,false) ;
window.addEventListener('touchend',endswipe,false) ;
infowindow.close() ;
keyhandler = imgwalk ;
if(!display.messaged)
{ display.messaged = 1 ;
if(!window.matchMedia("(hover: none)").matches)
telltale(M.returnkey+M.backtogps) ;
else telltale(M.swipedown+M.backtogps) ;
}
imgdiv = domcreate('div',null,
'style','position:fixed;width:100%;height:100%;'+
'z-index:1000;left:0;top:0;background:black') ;
dodisplay(selected.segno,selected.ptno,ind,1) ;
domadd(body,imgdiv) ;
}
function dodisplay(s0,s1,ind,dir)
{ var item,litem=null,ritem=null,sect ;
var infowords = { exit: M.exitfs , enter: M.enterfs ,
notes: M.notes , origin: caps(M.gpstrack) } ;
genpic('uncaption') ;
selected = { segno:s0 , ptno:s1 } ;
lmove = phretreat(s0,s1,ind) ;
if(lmove) litem = imginfo.sect[lmove[3][0]].list[lmove[3][1]] ;
rmove = phadvance(s0,s1,ind) ;
if(rmove) ritem = imginfo.sect[rmove[3][0]].list[rmove[3][1]] ;
item = findimg(segments[s0].pts[s1].photo[ind]) ;
sect = imginfo.sect[item[0]] ;
item = sect.list[item[1]] ;
genpic(imgdiv,item,sect.texttitle,
lmove?prev:null,litem,backtogps,rmove?next:null,ritem,
infowords,pixhelpdiv(),dir,0) ;
}
/* ------------------------------ image walk -------------------------------- */
function imgwalk(e)
{ if(e.keyCode==70) enterfullscreen() ; else simulate(e.keyCode) ; }
function simulate(btn)
{ if(btn==39) { if(rmove) next() ; return ; }
else if(btn==37) { if(lmove) prev() ; return ; }
else if(btn==32) genpic('spacebar') ;
else if(btn==38) genpic('enlarge') ;
else if(btn==40) genpic('reduce') ;
else if(btn==77) genmenu('toggle') ;
else backtogps() ;
}
/* --------------------------------- photo info ----------------------------- */
function phinfo(i) // pixinfodiv is in pixlib
{ infowindow.close() ;
var s0 = selected.segno , s1 = selected.ptno , pos = segments[s0].pts[s1].pos;
var item = imginfo.sect[i[0]].list[i[1]] , sname = imginfo.sect[i[0]].name ;
infowindow.open(pixinfodiv(item,sname),pos,null,
function() { walkto(selected.segno,selected.ptno,selected.type) ; }) ;
}
/* ------------------------- snip: apply scissors -------------------------- */
function snipwork(seg,s0,s1,s2)
{ var i,k,newcol=snipcolour(seg[s0].hue),d0,d1,pos ;
undraw(seg[s0]) ;
seg.length += 1 ;
for(i=seg.length-1;i>s0+1;i--) seg[i] = seg[i-1] ;
seg[s0+1] = seg[s0].clone() ;
seg[s0+1].pts = seg[s0].pts.slice(s1) ;
seg[s0+1].geo = [] ;
seg[s0+1].dots = seg[s0].dots ;
seg[s0].dots = null ;
seg[s0+1].hue = seg[s0].hue ;
seg[s0+1].colour = newcol ;
seg[s0].pts.length = s1 ;
seg[s0].pts[s1-1].delta = null ;
draw(seg[s0]) ;
connect(s0) ;
draw(seg[s0+1]) ;
selected = null ;
// partition geo pts according to proximity
for(i=0;i<seg[s0].geo.length;i++)
{ pos = seg[s0].geo[i].pos ;
d0 = segdist(seg[s0].pts,pos).dist ;
d1 = segdist(seg[s0+1].pts,pos).dist ;
ungeodraw(seg[s0].geo[i]) ;
if(d1<d0)
{ geodraw(seg[s0].geo[i],newcol) ;
if(s2&&i==s1)
selected = { segno:s0+1 , ptno:seg[s0+1].geo.length , type:'geo' } ;
seg[s0+1].geo.push(seg[s0].geo[i]) ;
seg[s0].geo[i] = null ;
}
else geodraw(seg[s0].geo[i],seg[s0].colour) ;
}
for(k=i=0;i<seg[s0].geo.length;i++) if(seg[s0].geo[i])
seg[s0].geo[k++] = seg[s0].geo[i] ;
seg[s0].geo.length = k ;
if(!selected) selected = { segno:s0+1 , ptno:0 , type:null } ;
drawprofile() ;
drawsel() ;
}
function snip()
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
infowindow.close() ;
done(['snip',s0,s1,s2]) ;
snipwork(segments,s0,s1,s2) ;
}
/* ------------------------ discard: bin n segments ------------------------- */
function binwork(segno,n)
{ var i,r,seg ;
if(!n) n = 1 ;
if(!segments[0].level) seg = segments ; else seg = segments[0].pts ;
if(segno+n>seg.length) alert('Logic error') ;
if(!segments[0].level||segments[0].type=='tour'||segments[0].type=='segments')
{ ldisconnect(segno) ; for(i=segno;i<segno+n;i++) disconnect(i) ; }
for(r=new Array(n),i=0;i<n;i++)
{ r[i] = seg[segno+i] ; recurse(r[i],'obliterate') ; }
for(i=segno;i<seg.length-n;i++) seg[i] = seg[i+n] ;
seg.length -= n ;
if(!segments[0].level||segments[0].type=='tour'||segments[0].type=='segments')
lconnect(segno) ;
if(!segments[0].level)
{ selected.ptno = 0 ;
selected.type = null ;
if(selected.segno>=segments.length)
{ selected.segno = segments.length-1 ;
selected.ptno = segments[selected.segno].pts.length - 1 ;
}
if(!segments[selected.segno].pts.length)
{ selected.ptno = 0 ; selected.type = 'geo' ; }
drawprofile() ;
drawsel() ;
}
else if(segments[0].type=='tour'||segments[0].type=='segments')
drawprofile() ;
return r ;
}
function discard()
{ var seg = segments[0].level>0?segments[0].pts:segments , name=null ;
if(seg.length<=1) return ;
var segno = selected.segno ; // selected.segno is reset by infowindow.close()
if(segno>=0)
{ infowindow.close() ;
if(segments[0].level>0) name = segments[0].pts[segno].title ;
done(['bin',segno,binwork(segno,1),name]) ;
}
}
function undiscard(segno,r)
{ var i,j,seg,m,n,pts,pt,rt ;
if(!segments[0].level) seg = segments ; else seg = segments[0].pts ;
if(!segments[0].level||segments[0].type=='tour'||segments[0].type=='segments')
ldisconnect(segno) ;
m = seg.length ;
n = r.length ;
seg.length += n ;
for(i=m-segno-1;i>=0;i--) seg[segno+i+n] = seg[segno+i] ;
for(i=0;i<n;i++)
{ seg[segno+i] = r[i] ;
recurse(seg[segno+i],'draw') ;
recurse(seg[segno+i],'geodraw') ;
if(!segments[0].level||segments[0].type=='segments')
{ for(pts=r[i].pts,j=0;j<pts.length;j++) pts[j].setmap() ;
for(pts=r[i].geo,j=0;j<pts.length;j++) pts[j].setmap() ;
}
}
if(!segments[0].level||segments[0].type=='tour'||segments[0].type=='segments')
{ lconnect(segno) ;
for(i=0;i<n;i++) connect(segno+i) ;
drawprofile() ;
}
if(!segments[0].level)
{ if(selected.segno>=segno) selected.segno += n ; drawsel() ; }
if(!segments[0].level||segments[0].type=='tour'||segments[0].type=='segments')
drawprofile() ;
}
/* ---------------------- swapseg: swap two segments ----------------------- */
function swapsegwork(seg,s0)
{ var i , temp = seg[s0+1] ;
seg[s0+1] = seg[s0] ;
seg[s0] = temp ;
ldisconnect(s0) ; lconnect(s0) ;
for(i=0;i<2;i++) { disconnect(s0+i) ; connect(s0+i) ; }
if(segments[0].level==0) drawprofile() ;
}
function swapseg(s0)
{ var flag = infowindow.close() ;
var pts = segments[0].type=='tour'?segments[0].pts:segments ;
done(['swapseg',s0]) ;
swapsegwork(pts,s0) ;
if(segments[0].type=='tour') return ;
if(selected.segno==s0) selected.segno += 1 ;
else if(selected.segno==s0+1) selected.segno -= 1 ;
if(flag=='highlight') flag = 3 ;
else if(flag=='seginfo') flag = 2 ;
else if(flag=='wpinfo') flag = 1 ;
else flag = 0 ;
walkto(selected.segno,selected.ptno,selected.type,flag) ;
}
/* -------------------------------------------------------------------------- */
function actiontype(x,opt)
{ if(x=='interpolate'||x=='optimise'||(x=='load'&&opt==0)||(x=='add'&&opt==0))
return 0 ;
else return 1 ;
}
// ['editlabel',s0,s1,s2,oldcaption,caption,oldlabel,label]
function done(something) { actions[nactions++] = something ; donesomething() ; }
function donesomething()
{ actions.length = nactions ;
if(nactions>1) blackout(undobtn) ;
greyout(redobtn) ;
if(actiontype(actions[nactions-1][0],actions[nactions-1][1])!=0)
{ if(unsavedchanges.length>=3) unsavedchanges.push(null) ;
else unsavedchanges.push(actionname(actions[nactions-1])) ;
}
}
// [ 'optimise' , s0+i , defparms , oldpts , props.optim ]
function optimswap(action,doing) // swap action[3] with segments[s0].pts
{ var s0 = action[1] , s , seg = segments[s0] ;
if(doing=='redo') seg.optim = action[4] ; else seg.optim = null ;
s = seg.pts ;
seg.pts = action[3] ;
action[3] = s ;
deltify(seg.pts) ;
redraw(s0) ;
drawprofile() ;
if(s0==selected.segno) { selected.ptno = 0 ; drawsel() ; }
seginfo() ;
}
// [ 'refresh' , segno , route ] ;
function refreshswap(action)
{ var segno = action[1] , r = segments[0].pts[segno] ;
recurse(r,'obliterate') ;
segments[0].pts[segno] = action[2] ;
action[2] = r ;
recurse(segments[0].pts[segno],'draw') ;
}
/* -------------------------------------------------------------------------- */
// done(['editphoto',s0,s1,s2,ind,oldphoto,newphoto]) ;
function photoswap(x,opt)
{ var s0 = x[1] , s1 = x[2] , s2 = x[3] , ind=x[4] ;
var pt = s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
photomerge(pt.photo,x[6-opt],x[5+opt],ind) ;
if(pt.photomarker) pt.unmapphoto() ;
if(pt.photo.length) genphotomark(pt,0) ;
}
/* --------------------------------- undo ---------------------------------- */
function undo()
{ infowindow.close() ;
var opts = actionname(actions[nactions-1]) , div ;
div = domcreate('div',genclickfn(confirmedundo,M.undo+' '+opts,'br')) ;
if(nactions>1&&actions[nactions-2][0]!='load')
domadd(div,genclickfn(bulkundo,M.bulkundo+'...')) ;
infowindow.open(div,6) ;
}
function undofactory(lim)
{ return function()
{ var i ; for(i=nactions-1;i>=lim;i--) confirmedundo(i>lim) ; }
}
function bulkundo()
{ infowindow.close() ;
var t=domcreate('table',null,'style','font-size:90%') , tr , td , i , f , g ;
t.setAttribute('cellpadding',0) ;
t.setAttribute('cellspacing',0) ;
for(i=nactions-1;i>=0&&actions[i][0]!='load';i--)
{ f = undofactory(i) ;
g = genclickfn(f,i==nactions-1?M.undo:'+',null,'padding-right:10px') ;
tr = domcreate('tr',domcreate('td',g,'style','text-align:right')) ;
g = actionname(actions[i]) ;
domadd(tr,domcreate('td',genclickfn(f,actionname(actions[i])))) ;
domadd(t,tr) ;
}
infowindow.open(t,6) ;
}
/* -------------------------------------------------------------------------- */
function confirmedundo(flag)
{ var i,ano=nactions-1,action=actions[ano][0],s0=actions[ano][1],s1,caption ;
var oldcaption,task,ind,d,d2,pt,s2,pts,pos,seg ;
infowindow.close() ;
if( action!='revseg' && action!='dupseg' && action!='interpolate'
&& action!='swapseg' && action!='unlabel' )
s1 = actions[ano][2] ;
s2 = actions[ano][3] ;
if(action=='bin') undiscard(s0,s1) ;
else if(action=='add') // [ 'add', s0 , n , loadprops.title , null ]
{ if(segments[0].level==0) seg = segments[s0] ;
else seg = segments[0].pts[s0] ;
actions[ano][4] = [seg] ;
binwork(s0,s1) ;
}
else if(action=='refresh') refreshswap(actions[ano]) ;
else if(action=='snip') // undo snip
{ selected = { segno:s0 , ptno:segments[s0].pts.length-1 } ;
undraw(segments[s0]) ;
combine1(segments,s0,s0+1) ;
for(i=s0+1;i<segments.length-1;i++) segments[i] = segments[i+1] ;
segments.length -= 1 ;
drawprofile() ;
drawfull(segments[s0]) ;
drawsel() ;
}
else if(action=='editlabel') // undo create/edit/delete label
undoeditlabel(s0,s1,s2,actions[ano][6],actions[ano][4],actions[ano][8]) ;
else if(action=='unlabel') for(i=0;i<s0.length;i++)
{ task = s0[i] ;
pt = segments[task[0]].pts[task[1]] ;
pt.setlabel(task[2],task[3],task[4]) ;
pt.setlabelmap() ;
}
else if(action=='edittitle') setdomtitle(routeprops.title=s0) ;
else if(action=='editdesc') setdesc(s2,s0) ;
else if(action=='wpdel'||action=='detach')
undowpdel(action,s0,s1,s2,actions[ano][4]) ;
else if(action=='move')
{ if(actions[ano][6]) wpdelwork(segments,s0,s1,s2,0) ;
else setptpos(s0,s1,s2,actions[ano][4]) ;
drawsel() ;
}
else if(action=='insgeo')
{ pos = segments[s0].geo[s1].pos ;
wpdelwork(segments,s0,s1,'geo',0) ;
sidestep(segments,s0,-1,pos)
drawsel() ;
}
else if(action=='recal') calwork(segments[s0],-s1) ;
else if(action=='googlecal') for(i=0;i<segments[s0].pts.length;i++)
segments[s0].pts[i].h = s1[i] ;
else if(action=='googlereg'||action=='googleadd')
getalts([segments[s0]],-3,drawprofile,s0,-s1,-s2) ;
else if(action=='setalt')
{ if(s2) segments[s0].geo[s1].h = actions[ano][4] ;
else { segments[s0].pts[s1].h = actions[ano][4] ; drawprofile() ; }
}
else if(action=='combine') uncombine(segments,actions[ano]) ;
else if(action=='revseg') revsegwork(segments,s0) ;
else if(action=='dupseg')
{ if(selected.segno==segments.length-1) selected.segno = s0 ;
recurse(segments[segments.length-1],'obliterate') ;
ldisconnect(segments.length-1) ;
segments.length -= 1 ;
drawprofile() ;
}
else if(action=='swapseg')
{ if(segments[0].type=='tour') pts = segments[0].pts ; else pts = segments ;
swapsegwork(pts,s0) ;
}
else if(action=='stars') routeprops.stars = s0 ;
else if(action=='deltimes') for(i=0;i<s1.length;i++)
{ if(s1[i][2]) segments[s0].geo[s1[i][0]].t = s1[i][1] ;
else segments[s0].pts[s1[i][0]].t = s1[i][1] ;
}
else if(action=='delalts')
{ for(i=0;i<s1.length;i++) segments[s0].pts[s1[i][0]].h = s1[i][1] ;
drawprofile() ;
}
else if(action=='optimise') optimswap(actions[ano],'undo') ;
else if(action=='editphoto') photoswap(actions[ano],0) ;
else if(action=='extra')
for(selected={segno:s0,ptno:s1},i=actions[ano].length-1;i>=3;i--)
{ task = actions[ano][i] ;
d = segments[task[0]].pts ;
d.splice(task[1],task[2].length-2) ;
redelta(d,task[1]) ;
}
nactions -= 1 ;
if(nactions==0||actions[nactions-1][0]=='load') greyout(undobtn) ;
blackout(redobtn) ;
if(actiontype(actions[nactions][0])!=0&&unsavedchanges.length>0)
unsavedchanges.length -= 1 ;
if(flag&&typeof(flag)=='number') return ; // object if not bulk undo
if(action=='dltimes'||action=='stars') routeinfo() ;
else if(action=='optimise') seginfo() ;
else if(action=='editphoto'||action=='editlabel'||action=='setalt')
walkto(s0,s1,s2) ;
else if(action=='editdesc')
{ if(s2==null) routeinfo() ; else highlight(segments[0].pts,s2) ; }
}
/* ------------------------------- setptpos --------------------------------- */
function setptpos(s0,s1,s2,pos)
{ var pts = s2?segments[s0].geo:segments[s0].pts ;
pts[s1].pos = pos ;
if(!s2) { redelta(segments[s0].pts,s1) ; redrawconnect(s0,s1) ; }
drawsel() ;
}
/* --------------------------------- redo ---------------------------------- */
function redo()
{ infowindow.close() ;
var opts = actionname(actions[nactions]) , div ;
div = domcreate('div',genclickfn(confirmedredo,M.redo+' '+opts,'br')) ;
if(actions.length>nactions+1) domadd(div,genclickfn(bulkredo,M.bulkredo)) ;
infowindow.open(div,7) ;
}
function redofactory(lim)
{ return function()
{ var i ; for(i=nactions;i<=lim;i++) confirmedredo(i<lim) ; }
}
function bulkredo()
{ infowindow.close() ;
var t , tr , td , i , f , g ;
t = domcreate('table',null,'style','font-size:90%',
'cellpadding',0,'cellspacing',0) ;
for(i=nactions;i<actions.length;i++)
{ f = redofactory(i) ;
g = genclickfn(f,i==nactions?M.redo:'+',null,'padding-right:10px') ;
tr = domcreate('tr',domcreate('td',g,'style','text-align:right')) ;
g = actionname(actions[i]) ;
domadd(tr,domcreate('td',genclickfn(f,actionname(actions[i])))) ;
domadd(t,tr) ;
}
infowindow.open(t,7) ;
}
/* -------------------------------------------------------------------------- */
function confirmedredo(flag)
{ var i,action=actions[nactions][0],s0=actions[nactions][1],s1,caption,a,b,c ;
var task,ind,photo,pt,s2,pts,seg,labno ;
if( action!='revseg' && action!='dupseg' && action!='interpolate'
&& action!='swapseg' && action!='unlabel')
s1 = actions[nactions][2] ;
s2 = actions[nactions][3] ;
infowindow.close() ;
if(action=='bin') binwork(s0,s1.length) ;
else if(action=='add')
{ undiscard(s0,actions[nactions][4]) ; actions[nactions][4] = null ; }
else if(action=='refresh') refreshswap(actions[nactions]) ;
else if(action=='snip') snipwork(segments,s0,s1) ;
else if(action=='editlabel') // redo create/edit/delete label
{ if(s2) pt = segments[s0].geo[s1] ; else pt = segments[s0].pts[s1] ;
labno = actions[nactions][7] ;
if(labno==null) pt.unsetlabel() ;
else
{ pt.setlabel(labno,actions[nactions][5],actions[nactions][9]) ;
pt.setlabelmap() ;
}
}
else if(action=='unlabel') for(i=0;i<s0.length;i++)
{ task = s0[i] ;
pt = segments[task[0]].pts[task[1]] ;
pt.unsetlabel() ;
}
else if(action=='edittitle')
{ if(s2==null) setdomtitle(routeprops.title=s1) ;
else segments[s2].title = s1 ;
}
else if(action=='editdesc') setdesc(s2,s1) ;
else if(action=='wpdel') { wpdelwork(segments,s0,s1,s2,0) ; drawsel() ; }
else if(action=='detach')
{ selected = { segno:s0 , ptno:s1 , type:s2 } ; wpdel(2) ; }
else if(action=='move') // ['move',s0,s1,s2,oldpos,newpos,inserted]
{ seg = segments[s0] ;
if(s2) pts = seg.geo ; else pts = seg.pts ;
if(actions[nactions][6]) insert(pts,s1,1) ;
setptpos(s0,s1,s2,actions[nactions][5]) ;
if(s2) geodraw(pts[s1],seg.colour) ;
if(actions[nactions][5]) selected = { segno:s0 , ptno:s1 , type:s2 } ;
}
else if(action=='insgeo')
{ selected = { segno:s0 , ptno:s1 , type:'geo' } ;
insgeo(s2,actions[nactions][4],null) ; // don’t add an action
}
else if(action=='recal') calwork(segments[s0],s1) ;
else if(action=='googlecal') googlecalwork(s0) ;
else if(action=='googlereg'||action=='googleadd')
getalts([segments[s0]],-3,drawprofile,s0,s1,s2) ;
else if(action=='setalt')
{ if(s2) segments[s0].geo[s1].h = actions[nactions][4] ;
else segments[s0].pts[s1].h = actions[nactions][4] ;
}
else if(action=='combine') combinework(segments,s0,s1) ;
else if(action=='revseg') revsegwork(segments,s0) ;
else if(action=='dupseg') dupsegwork(segments,s0) ;
else if(action=='swapseg')
{ if(segments[0].type=='tour') pts = segments[0].pts ; else pts = segments ;
swapsegwork(pts,s0) ;
}
else if(action=='stars') routeprops.stars = s1 ;
else if(action=='deltimes') for(i=0;i<s1.length;i++)
{ if(s1[i][2]) segments[s0].geo[s1[i][0]].t = null ;
else segments[s0].pts[s1[i][0]].t = null ;
}
else if(action=='delalts')
{ for(i=0;i<s1.length;i++) segments[s0].pts[s1[i][0]].h = null ;
drawprofile() ;
}
else if(action=='optimise') optimswap(actions[nactions],'redo') ;
else if(action=='editphoto') photoswap(actions[nactions],1) ;
else if(action=='extra')
for(selected={segno:s0,ptno:s1},i=3;i<actions[nactions].length;i++)
{ task = actions[nactions][i] ;
a = segments[task[0]].pts.slice(0,task[1]) ;
b = task[2].slice(1,task[2].length-1) ;
c = segments[task[0]].pts.slice(task[1]) ;
segments[task[0]].pts = a.concat(b,c) ;
redelta(segments[task[0]].pts,task[1]) ;
redelta(segments[task[0]].pts,task[1]+task[2].length-1) ;
}
nactions += 1 ;
if(nactions==actions.length) greyout(redobtn) ;
blackout(undobtn) ;
if(actiontype(actions[nactions-1][0])!=0) unsavedchanges.push(action) ;
if(flag&&typeof(flag)=='number') return ; // object if not bulk undo
if(action=='dltimes'||action=='stars') routeinfo() ;
else if(action=='optimise') seginfo() ;
else if( action=='editphoto' || action=='editlabel' || action=='setalt'
|| (action=='move'&&actions[nactions-1][6]) ) walkto(s0,s1,s2) ;
else if(action=='editdesc')
{ if(actions[nactions-1][3]==null) routeinfo() ;
else highlight(segments[0].pts,actions[nactions-1][3]) ;
}
}
/* -------------------------------- actionname ------------------------------ */
function actionname(x)
{ var i,s ;
if(x[0]=='bin')
{ if(x[3]) return M.deletex + ' ' + x[3] ;
else if(segments[0].level==0) return M.deletesegment ;
else if(segments[0].level==1) return M.deleteroute ;
else return M.deleteindex ;
}
if(x[0]=='snip') return M.splitsegment ;
// ['editlabel',s0,s1,s2,oldcaption,caption,oldlabel,label]
if(x[0]=='editlabel')
{ if(!x[7]) return M.dellabel ;
else if(!x[6]) return M.labelpt ;
else return M.editlabel ;
}
if(x[0]=='unlabel') return M.removelabels ;
if(x[0]=='edittitle'||x[0]=='editindex title') return M.edittitle ;
if(x[0]=='editdesc')
{ if(x[3]==undefined||x[3]==null) return M.editdesc ;
else return inject(M.editxdesc,segments[x[3]].title) ;
}
if(x[0]=='editinfo') return M.editinfo ;
if(x[0]=='wpdel') return M.wpdel ;
if(x[0]=='move') { if(x[6]) return M.wpins ; else return M.wpdrag ; }
if(x[0]=='recal') return M.recalalts ;
if(x[0]=='insgeo') return M.insgeo ;
if(x[0]=='googlecal') return M.googlelats ;
if(x[0]=='googlereg') return M.regressalts ;
if(x[0]=='googleadd') return M.calibalts ;
if(x[0]=='setalt') return M.wpalt ;
if(x[0]=='resign') return M.wpicon ;
if(x[0]=='combine') return inject(M.combinesegments,x[2]) ;
if(x[0]=='revseg') return M.revsegment ;
if(x[0]=='dupseg') return M.dupsegment ;
if(x[0]=='optimise') return M.optim ;
if(x[0]=='refresh') return M.refresh ;
if(x[0]=='deltimes') return M.deltimes ;
if(x[0]=='detach') return x[3]?M.attach:M.detach ;
if(x[0]=='delalts') return M.delalts ;
if(x[0]=='editphoto')
{ if(!x[6]) return M.delphoto ;
else if(!x[5]) return M.addphoto ;
else return M.modphoto ;
}
if(x[0]=='extra') return M.interpextra ;
if(x[0]=='stars')
{ if(!x[2])
{ for(s='',i=0;i<x[1];i++) s += '★' ; return inject(M.clear,s) ; }
else { for(s='',i=0;i<x[2];i++) s += '★' ; return inject(M.set,s) ; }
}
if(x[0]=='swapseg') return M.swapseg ;
if(x[0]=='load')
{ if(x[3]) return inject(M.loadsth,x[3]) ; else return M.load ; }
if(x[0]=='add') return inject(M.addsth,x[3]?x[3]:M.loadnewseg)
abend(inject(M.unrecogaction,x[0])) ;
}
/* -------------------------------------------------------------------------- */
// distance of a point from a segment
function distptseg(d01,d02,d12) // p0 is the point, p1-p2 the segment
{ if(d01<=0||d02<=0) return 0 ; else if(d12<=0) return d01 ;
var u = (d01*d01+d12*d12-d02*d02) / (2*d01*d12) ; // u = cos(theta)
if(u<=0) return d01 ; else if(d01*u>=d12) return d02 ;
else if(Math.abs(u)>=1) return 0 ;
else return d01 * Math.sqrt(1-u*u) ;
}
// distance between two segments
function distsegseg(d0,d00,d01,d1,d10,d11)
{ if(d0<=0) return distptseg(d00,d01,d1) ;
else if(d1<=0) return distptseg(d00,d10,d0) ;
var A,B, C = ( d10*d10 + d01*d01 - d00*d00 - d11*d11 ) / (2*d0*d1) ;
if(Math.abs(C)>=1) return d00 ; // segments are parallel
A = (d00*d00-d10*d10)/d0 ;
B = (d00*d00-d01*d01)/d1 ;
if( Math.abs(A+(B+d1+d0*C)*C)<=d0*(1-C*C)
&& Math.abs(B+(A+d0+d1*C)*C)<=d1*(1-C*C) ) return 0 ; // segments cross
return Math.min( distptseg(d00,d01,d1) , distptseg(d10,d11,d1) ,
distptseg(d00,d10,d0) , distptseg(d01,d11,d0) ) ;
}
/* -------------------------------------------------------------------------- */
function d3(x,y)
{ return (x[0]-y[0])*(x[0]-y[0]) + (x[1]-y[1])*(x[1]-y[1]) +
(x[2]-y[2])*(x[2]-y[2]) ;
}
// downsample a segment by a factor of 10
function dsseg(pts)
{ var j , k , points , n = pts.length , npts = 1 + Math.floor(n/10) ;
if(npts<=3)
{ if(n==0) abend(M.emptyseg) ;
points = [ 0 , 0 , 0 ] ;
for(j=0;j<3;j++)
points[j] = { pos:pts[Math.floor(j*(n-1)/2)].pos , dist:null } ;
for(j=0;j<2;j++) points[j].dist = dist(points[j].pos,points[j+1].pos) ;
return points ;
}
points = new Array(npts) ;
for(j=0;j<npts;j++)
{ k = Math.floor(0.5+(n-1)*j/(npts-1)) ;
points[j] = { pos: pts[k].pos , dist:null } ;
if(j) points[j-1].dist = dist(points[j-1].pos,points[j].pos) ;
}
return points ;
}
// compute proximity of two downsampled segments
function segprox(segi,segj)
{ var qnum,qden,i,j,d00,d01,d10,d11,d0,d1 ;
for(qnum=qden=0,i=0;i<segi.length-1;i++)
{ d00 = dist(segi[i].pos,segj[0].pos) ;
for(j=0;j<segj.length-1;d00=d01,j++)
{ d01 = dist(segi[i].pos,segj[j+1].pos) ;
d10 = dist(segi[i+1].pos,segj[j].pos) ;
d11 = dist(segi[i+1].pos,segj[j+1].pos) ;
d0 = segi[i].dist ;
d1 = segj[j].dist ;
qnum += d0 * d1 / ( 1 + Math.sqrt(distsegseg(d0,d00,d01,d1,d10,d11)) ) ;
qden += d0 * d1 ;
}
}
return [qnum,qden] ;
}
/* -------------------------------------------------------------------------- */
// twiddle choice of colours from c to maximise separation of n points
function assigncolours(c,prox)
{ var boosted,maxboost,iter,q,boost,maxj,i,j,k ;
var nset = c.length , n = prox.length ;
if(n==1) return [ c[0] ] ;
var cind = new Array(nset) , rind = new Array(n) ;
function cdif(i,j) { return Math.sqrt(d3(c[i],c[j])) ; }
// set up initial assignment and twiddle it
for(i=0;i<nset;i++) cind[i] = null ;
for(i=0;i<n;i++)
{ rind[i] = Math.floor(0.5+i*(nset-1)/(n-1)) ; cind[rind[i]] = i ; }
for(boosted=2,iter=0;boosted>1&&iter<10;iter++) for(boosted=i=0;i<n;i++)
{ for(maxboost=0,j=i+1;j<n;j++)
{ for(boost=k=0;k<n;k++) if(k!=i&&k!=j)
{ q = cdif(rind[k],rind[i]) - cdif(rind[k],rind[j]) ;
boost += (prox[j][k]-prox[i][k]) * q ;
}
if(boost>maxboost) { maxboost = boost ; maxj = [ j , 0 ] ; }
}
for(j=0;j<nset;j++) if(cind[j]==null)
{ for(boost=k=0;k<n;k++) if(k!=i)
boost += prox[i][k]*(cdif(rind[k],j)-cdif(rind[k],rind[i])) ;
if(boost>maxboost) { maxboost = boost ; maxj = [ j , 1 ] ; }
}
if(maxboost>0)
{ if(maxboost>boosted) boosted = maxboost ;
j = maxj[0] ;
k = rind[i] ;
if(maxj[1]>0) { rind[i] = j ; cind[k] = null ; }
else { rind[i] = rind[j] ; rind[j] = k ; cind[k] = j ; }
cind[rind[i]] = i ;
}
}
for(i=0;i<n;i++) rind[i] = c[rind[i]] ;
rind.length = n ;
return rind ;
}
/* -------------------------------------------------------------------------- */
function greycode(a,n)
{ var b = new Array(n) , m , j , i , wid , nb ;
for(wid=0;(1<<wid)<n;wid++) ; // find width of bitfield needed
for(nb=i=0;i<(1<<wid);i++)
{ for(m=j=0;j<wid;j++) m |= ((i>>j)&1)<<(wid-1-j) ; // bit reverse
if(m<n) b[nb++] = a[m] ;
}
if(nb!=n) console.log("logic error") ;
return b ;
}
// Returns greyscale "brightness" (0-1) of the given 0-255 RGB values
// https://stackoverflow.com/questions/596216/formula-to-determine...
// ...-perceived-brightness-of-rgb-color
function brightness(x)
{ var i , v , k = [0.212655,0.715158,0.072187] ;
for(v=i=0;i<3;i++) v += k[i]*
(x[i] <= 0.04045 ? x[i]/12.92 : Math.pow((x[i]+0.055)/1.055,2.4)) ;
return v <= 0.0031308 ? v*12.92 : 1.055 * Math.pow(v,1.0/2.4) - 0.055 ;
}
// convert a colour (array of 3 numbers in the range [0,1]) to hex
function hexify(x)
{ function hexdigit(y)
{ y = Math.floor(255*y+0.5) ; return ('00'+y.toString(16)).substr(-2) ; }
return '#' + hexdigit(x[0]) + hexdigit(x[1]) + hexdigit(x[2]) ;
}
function listhues(n)
{ var i,j,k,lev,l,nl,c,nc,clen,λ,μ,ξ,η,jst,jend,x,y,θ,sect,hue,q,lims ;
var π=Math.PI,sq=Math.sqrt(3) ;
var cycle = [ [1,0,0],[1,0,1] , [0,0,1],[0,1,1] , [0,1,0],[1,1,0] , [1,0,0] ];
for(l=[],lev=1,nl=0;;lev*=2)
{ for(clen=3*(lev+1)*(lev+1),c=new Array(clen),nc=i=0;i<=1.25*lev;i++)
{ ξ = (sq/2)*i / lev ;
if(i<=lev) { jst = lev-i ; jend = 2*lev ; }
else { jst = 0 ; jend = 3*lev-i ; }
for(j=jst;j<=jend;j++) if(lev==1||((j&1)||(i&1))) if(i<=5*lev/4&&j>=lev/2)
{ η = (sq/2)*j / lev ;
x = η - sq/2 ;
y = 1 - 2*ξ/sq - x/sq ;
if(Math.abs(x)<0.001&&Math.abs(y)<0.001) hue = [0.5,0.5,0.5] ;
else
{ θ = ( 5*π/2 - Math.atan2(y,x) ) % (2*π) ;
sect = Math.floor(θ/(π/3)) ;
θ = θ % (π/3) ;
μ = Math.sqrt(x*x+y*y) / ((sq/2)/Math.sin(θ+π/3)) ;
λ = θ / (π/3) ;
for(hue=[0,0,0],k=0;k<3;k++)
{ q = (1-μ)/2 + μ*( (1-λ)*cycle[sect][k] + λ*cycle[1+sect][k] ) ;
hue[k] = q>1?1:(q<0?0:q) ;
}
}
if(nc==clen) console.log('logic error') ;
lims = shadelims(hue) ;
if(lims[0]>1) hue = reshade(hue,lims[0]) ;
else if(lims[1]<1) hue = reshade(hue,lims[1]) ;
c[nc++] = hue ; // coerce to legal brightness
}
}
if(lev>1)
{ l = l.concat(greycode(c,nc)) ;
if(lev==2) { x = l[3] ; l[3] = l[7] ; l[7] = x ; } // grey <-> orange
}
else l = [c[0],c[3],c[1],c[2]] ; // red, blue, magenta, grey
if(l.length>=n) return l.slice(0,n) ;
}
}
// extendhues generates n hues fairly distant from those supplied in hues
function extendhues(n,hues)
{ var nhues=hues.length,c=listhues(n+nhues),i,j,mindist,minc,dist,h ;
// for each hue previously in hues, we delete the closest match in c
for(i=0;i<nhues;i++)
{ for(minc=null,j=0;j<c.length;j++) if(c[j])
{ dist = d3(c[j],hues[i]) ;
if(minc==null||dist<mindist) { mindist = dist ; minc = j ; }
}
c[minc] = null ;
}
h = new Array(n) ;
for(i=j=0;j<c.length&&i<n;j++) if(c[j]) h[i++] = c[j] ;
return h ;
}
/* -------------------------------------------------------------------------- */
function genshades(index)
{ var i , n = index.pts.length , sind ;
var shade = shadesofhue(index.hue,n) ;
index.shades = [ hexify(shade[0]) , hexify(shade[n-1]) ] ; // gradient
sind = assigncolours(shade,getprox(index)) ;
for(i=0;i<n;i++) recurse(index.pts[i],'colour',sind[i]) ;
}
/* -------------------------------------------------------------------------- */
function snipcolour(hue)
{ var alpha,i,j,k,n=segments.length,newcol,maxdist,maxcol,ssq,dist ;
var oldcols=new Array(n) , shade = shadesofhue(hue,n+1) ;
for(i=0;i<n;i++) oldcols[i] = dehexify(segments[i].colour) ;
for(i=0;i<=n;i++)
{ for(j=0;j<n;j++)
{ ssq = d3(shade[i],oldcols[j]) ;
if(j==0||ssq<dist) { dist = ssq ; newcol = shade[i] ; }
}
if(i==0||dist>maxdist) { maxdist = dist ; maxcol = newcol ; }
}
return hexify(maxcol) ;
}
// reshade computes the shade of hue with scale factor λ, where λ=0 gives black;
// λ=2 gives white, and λ=1 gives the hue itself. RGB vals in the range [0,1]
function reshade(hue,λ)
{ var i,x=[0,0,0],μ ;
if(λ>1) { μ = λ-1 ; λ = 2-λ ; } else μ = 0 ;
for(i=0;i<3;i++) x[i] = λ*hue[i] + μ ;
return x ;
}
// shadelims finds approximate limits for scale factors, applied to the given
// hue, for which the brightnesses are the hard-coded limits (curr 0.2 and 0.7).
function shadelims(hue)
{ var lim = [ 0.2,0.7 ] , bright = new Array(21) , i , lo=10 , hi=10 ;
bright[10] = brightness(hue) ;
// work down to an i for which bright[i]<lim[0]
for(i=9;i>=0&&bright[i+1]>lim[0];i--)
bright[lo=i] = brightness(reshade(hue,i/10)) ;
for(i=11;i<=20&&bright[i-1]<lim[1];i++)
bright[hi=i] = brightness(reshade(hue,i/10)) ;
// find the least i at which bright[i] exceeds lim[1]
for(i=lo;i<=hi&&bright[i]<lim[1];i++) ;
// interpolate linearly
lim[1] = ( (i-1) + (lim[1]-bright[i-1])/(bright[i]-bright[i-1]) ) / 10 ;
for(i=hi;i>=lo&&bright[i]>lim[0];i--) ;
lim[0] = ( i + (lim[0]-bright[i])/(bright[i+1]-bright[i]) ) / 10 ;
return lim ;
}
// shadesofhue computes n scale factors for reshading hue roughly equally
// spaced to fill the region between the legal limits, and with the original
// hue always included.
function shadesofhue(hue,n)
{ var shade = new Array(n) , i , i0 ;
if(n==1) { shade[0] = [ hue[0],hue[1],hue[2] ] ; return shade ; }
var lim = shadelims(hue) ;
if(lim[0]>1) lim[0] = 1 ;
if(lim[1]<1) lim[1] = 1 ;
i0 = Math.floor(0.5+(n-1)*(1-lim[0])/(lim[1]-lim[0])) ;
shade[i0] = [ hue[0],hue[1],hue[2] ] ;
if(i0>0) for(i=0;i<i0;i++) shade[i] = reshade(hue,lim[0]+i*(1-lim[0])/i0) ;
if(i0<n-1) for(i=i0+1;i<n;i++)
shade[i] = reshade(hue,1+(i-i0)*(lim[1]-1)/(n-i0-1)) ;
return shade ;
}
/* -------------------------------------------------------------------------- */
function segbounds(bounds,route)
{ var i,lat,lon,it,pts ;
// find maxima and minima
for(pts=route.pts,it=0;it<2;it++,pts=route.geo) for(i=0;i<pts.length;i++)
{ lat = pts[i].pos.lat ;
lon = pts[i].pos.lng ;
if(bounds.minlon==null)
{ bounds.minlon = bounds.maxlon = lon ;
bounds.minlat = bounds.maxlat = lat ;
}
else
{ if(lon<bounds.minlon) bounds.minlon = lon ;
else if(lon>bounds.maxlon) bounds.maxlon = lon ;
if(lat<bounds.minlat) bounds.minlat = lat ;
else if(lat>bounds.maxlat) bounds.maxlat = lat ;
}
}
}
/* -------------------------------------------------------------------------- */
function getprox(index)
{ var n=index.pts.length,dspts=new Array(n),i,j,k,i0,j0 ;
var prox=new Array(n) ;
for(i=0;i<n;i++)
{ dspts[i] = [] ;
recurse(index.pts[i],'listsegs',dspts[i]) ;
prox[i] = new Array(n) ;
}
for(i0=0;i0<n-1;i0++) for(j0=i0+1;j0<n;j0++)
{ kk = [0,0] ; // numerator/denominator
for(i=0;i<dspts[i0].length;i++) for(j=0;j<dspts[j0].length;j++)
{ k = segprox(dspts[i0][i],dspts[j0][j]) ;
kk[0] += k[0] ;
kk[1] += k[1] ;
}
prox[j0][i0] = prox[i0][j0] = (kk[0]/kk[1]) * Math.pow(kk[1],0.1) ;
}
return prox ;
}
/* -------------------------------------------------------------------------- */
function ascify(s)
{ var a = 'àáâäãåā' , see = 'çćč' , e = 'èéêëēėę' , eye = 'îïíīįì' , l = 'ł' ;
var n = 'ñń' , o = 'ôöòóœøōõ' , ess = 'śš' , esss = 'ß' , u = 'ûüùúū' ;
var ae = 'æ' , oe = 'œ'
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(ae.indexOf(c)>=0) newc = 'ae' ;
else if(oe.indexOf(c)>=0) newc = 'oe' ;
else if(esss.indexOf(c)>=0) newc = 'ss' ;
else if(u.indexOf(c)>=0) newc = 'u' ;
else newc = '*' ;
if(c==C) sdash += newc ; else sdash += newc.toUpperCase() ;
}
return sdash ;
}
/* --------------------------------- getalts -------------------------------- */
// action is drawprofile, ie. update profile whenever results are obtained
// getalts.action is occasionally reconfirmdl, ie. go on to next step of
// saving the track
function getalts(segs,thresh,action,doneaction,m,c)
{ var s0,start,end,n,pts,npts,flag,lox,loxpos,loxind,p,i,j,k,l,len,sel ;
var reqno,ind,reqlist,dotimes,MAX=512,MAXREGRESS=200 ;
if(!getalts.reqlist) getalts.reqlist = [] ;
reqlist = getalts.reqlist ;
if(thresh<0) s0 = doneaction ;
else if(doneaction!=undefined) getalts.action = doneaction ; // so null is ok
// if getalts is called while alts are still being got, all we do is tighten
// the threshold
if(reqlist.length>0)
{ if(!getalts.altthresh||thresh<getalts.altthresh)
getalts.altthresh = thresh ;
getalts.pending = 0 ;
return ;
}
// this can only occur when invoked from semiconfirmeddl
else if(thresh==null) { getalts.pending = 0 ; return ; }
if(getalts.altthresh&&getalts.altthresh<thresh) thresh = getalts.altthresh ;
p = flatten(segs,1) ;
if(thresh<0) // ie. we're doing a linear regression
{ reqlist = getalts.reqlist = new Array(p.length) ;
flag = dotimes = 0 ;
for(n=j=i=0;i<p.length;i++) if(p[i].h)
{ if(p[i].t) n += 1 ; reqlist[j++] = i ; }
reqlist.length = j ;
if(n>=reqlist.length/2&&n>=2) for(dotimes=1,j=i=0;i<reqlist.length;i++)
if(p[reqlist[i]].t) reqlist[j++] = reqlist[i] ;
reqlist.length = j ;
if(reqlist.length>MAXREGRESS)
{ for(i=0;i<MAXREGRESS;i++)
reqlist[i] = reqlist[Math.floor(i*(reqlist.length-1)/(MAXREGRESS-1))] ;
reqlist.length = MAXREGRESS ;
}
for(loxpos=new Array(reqlist.length),i=0;i<reqlist.length;i++)
loxpos[i] = p[reqlist[i]].pos ;
}
else
{ for(flag=npts=i=0;i<p.length;i=j)
{ if(p[i].h!=null) { j = i+1 ; continue ; }
for(j=i+1;j<p.length&&p[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<p.length) j += 1 ;
n = j - i ;
if(npts+n<=MAX) { npts += n ; reqlist.push([i,j]) ; }
else if(reqlist.length) // 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)
{ getalts.reqlist = [] ;
getalts.altthresh = null ;
if(getalts.action) { getalts.action() ; getalts.action = null ; }
getalts.pending = 0 ;
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++)
{ sel = p[k].sel ;
if(sel.type) pts = segs[sel.segno].geo ; else pts = segs[sel.segno].pts ;
lox[ind++] = pts[sel.ptno] ;
}
p = null ; // we've finished with it
if(flag==2) for(loxind=new Array(MAX),loxpos=new Array(MAX),k=0;k<MAX;k++)
{ loxind[k] = Math.floor(0.5+(k*(npts-1))/(MAX-1)) ;
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 MAX positions and loxind holds the indexes
// in lox of the entries in loxpos
/* ------------------------------------------------------------------------ */
function doelevations(results,status,m,c)
{ // assume that the results come in sequence, ie. correspond to xpending[0]
var d0,d1,dn,reqno,i,j,k,l,n,d,D,sx,sy,sxx,sxy,sy,x,y,j0,j1 ;
if(thresh>=0&&results.length!=lox.length)
{ x = M.gunk+(status?(': '+status):'') ; alerter(x,0) ; return ; }
if(thresh<0)
{ // set sel as the independent variable x for regression
for(n=i=0;i<p.length;i++) if(p[i].t) n += 1 ;
if(dotimes)
{ // set sel to time whenever available, to null else
for(i=0;i<p.length;i++)
if(p[i].t) p[i].sel = p[i].t.getTime() ;
else p[i].sel = null ;
// linearly extrapolate times at ends when absent
for(j0=0;!p[j0].sel;j0++) ;
for(j1=p.length-1;!p[j1].sel;j1--) ;
d0 = p[j0].d ;
d1 = p[j1].d ;
for(i=0;i<p.length;i++)
if(i==j0) i = j1 ;
else
{ d = p[i].d ;
p[i].sel = ( (d1-d)*p[j0].sel + (d-d0)*p[j1].sel ) / ( d1-d0 ) ;
}
// linearly interpolate times when absent
for(j1=j0+1;!p[j1].sel;j1++) ;
for(i=0;i<p.length;i++) if(!p[i].sel)
{ if(reqlist[j1]<i)
{ for(j=j1+1;j<p.length&&!p[j].sel;j++) ;
if(j<p.length) { j0 = j1 ; j1 = j ; }
}
d0 = p[j0].d ;
d = p[i].d ;
d1 = p[j1].d ;
p[i].sel = ( (d1-d)*p[j0].sel + (d-d0)*p[j1].sel ) / ( d1-d0 ) ;
}
}
else for(i=0;i<p.length;i++) p[i].sel = p[i].d ;
// regress
if(thresh==-1)
{ for(sx=sy=sxx=sxy=i=0;i<reqlist.length;i++)
{ j = reqlist[i] ;
y = ( results[i].elevation -= p[j].h ) ; // google correction
sy += y ;
sx += ( x = p[j].sel ) ;
sxy += x * y ;
sxx += x * x ;
}
m = (sxy-sx*sy/reqlist.length) / (sxx-sx*sx/reqlist.length) ;
c = (sy-m*sx) / reqlist.length ;
}
else if(thresh==-2)
{ for(sy=m=i=0;i<reqlist.length;i++)
sy += ( results[i].elevation -= p[reqlist[i]].h ) ;
c = sy / reqlist.length ;
}
for(i=0;i<p.length;i++)
{ y = segs[0].pts[i].h ;
if(y!=null) segs[0].pts[i].h = y + m*p[i].sel + c ;
}
getalts.reqlist = [] ;
if(thresh!=-3) done([thresh==-2?'googleadd':'googlereg',s0,m,c]) ;
if(action) action(npts) ;
getalts.pending = 0 ;
telltale(M.gotaltitudes) ;
return ;
}
for(k=reqno=0;reqno<reqlist.length;reqno++,k+=n)
{ if(flag==2) n = MAX ; 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 += lox[k].delta ;
dn = lox[loxind[i+1]].h ;
d0 = lox[loxind[i]].h ;
for(d=0,k=loxind[i];k<loxind[i+1]-1;k++)
{ d += lox[k].delta ;
lox[k+1].h = ( d*dn+(D-d)*d0 ) / D ;
}
}
}
getalts.reqlist = [] ;
if(action) action(npts) ;
if(flag) setTimeout(getalts,1000,segs,thresh,action) ;
else telltale(M.gotaltitudes) ;
} /* end of doelevations */
/* ------------------------------------------------------------------------ */
if(thresh==-3) { doelevations(null,null,m,c) ; getalts.pending = 0 ; }
else { getalts.pending = 1 ; elevator(loxpos,doelevations) ; }
}
/* -------------------------------------------------------------------------- */
function elevator(lox,action) // terrain tap elevator
{ var i,xhttp,s = 'locations=' ;
for(i=0;i<lox.length;i++) s += lox[i].lat.toFixed(6) + ',' +
lox[i].lng.toFixed(6) + ((i==lox.length-1)?'':'|') ;
xhttp = new XMLHttpRequest() ;
xhttp.open('POST',fileserver+'?terraintap') ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ var r ;
if(!xhttp.responseText) action([],'') ;
else { r = JSON.parse(xhttp.responseText) ; action(r.results,r.status) ; }
}
} ;
xhttp.send(s) ;
}
/*
function elevator(lox,action) // open topo data elevator
{ var i,xhttp,s = '{ "locations": "' ;
for(i=0;i<lox.length;i++) s += lox[i].lat.toFixed(8) + ',' +
lox[i].lng.toFixed(8) + ((i==lox.length-1)?'" }':' | ') ;
xhttp = new XMLHttpRequest() ;
xhttp.open('POST',fileserver+'?getalts') ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ var r ;
if(!xhttp.responseText) action([],'') ;
else
{ r = JSON.parse(xhttp.responseText) ; action(r.results,r.status) ; }
}
} ;
xhttp.send(s) ;
}
function elevator(lox,action) // open-elevation elevator
{ var i,xhttp,s = '{ "locations": [ ' ;
for(i=0;i<lox.length;i++)
s += '{ "latitude": '+lox[i].lat.toFixed(8)+' , ' +
'"longitude": '+lox[i].lng.toFixed(8)+' } ' +
((i==lox.length-1)?'] }':', ') ;
xhttp = new XMLHttpRequest() ;
xhttp.open('POST','https://api.open-elevation.com/api/v1/lookup') ;
xhttp.setRequestHeader('Accept','application/json') ;
xhttp.setRequestHeader('Content-Type','application/json') ;
// xhttp.responseType = 'text' ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4) action(JSON.parse(xhttp.responseText).results) ; } ;
xhttp.send(s) ;
}
*/
/* -------------------------------------------------------------------------- */
function getbounds(segment)
{ var bounds = { minlon:null } ;
recurse(segment,'getbounds',bounds) ;
return L.latLngBounds( { lat:bounds.minlat , lng:bounds.minlon } ,
{ lat:bounds.maxlat , lng:bounds.maxlon } ) ;
}
/* -------------------------------------------------------------------------- */
function promoteprops(route,base)
{ var field , flag = 0 ;
if(route.list&&base.list&&route.list!=base.list)
abend(M.inconsistentlists + ' ' + route.list + ' vice ' + base.list) ;
for(field in promotable)
if( ( promotable[field]<20&&route[field]&&!base[field] )
|| ( promotable[field]>=20&&route[field].length&&!base[field].length ) )
{ flag |= 1 << promotable[field] ; base[field] = route[field] ; }
return flag ;
}
/* -------------------- load a track and update state ----------------------- */
// note: if more than one file is being loaded, loadtrack is called once for
// each file, so it does not need to deal with multiple input files (which might
// be a mixture of tracks and indexes)
function loadtrack(response,filename,ovr,origintype,prefs,loadflags)
{ var xmldoc,newseg,i,j,k,routeno,rind,oldroute,col,hues=[],hue,q,flag,updseg ;
var opts,res,n,s,lev0parms,dooptim,pts ;
var extn = filename.substring(filename.length-4).toLowerCase() ;
var shortname = abbreviate(filename)[0] ;
if(ovr=='add'&&segments.length==0) abend('Logic error') ;
if(ovr=='refresh') { updseg = origintype ; origintype = 'refresh' ; }
function neq(a,b) { return a[0]!=b[0] || a[1]!=b[1] || a[2]!=b[2] ; }
function callindexify(seg)
{ // the set of optimisations to perform depends on whether we are loading a
// track or an index, whether (if a track) it has already been optimised,
// and whether we are adding it to an index or to a metaindex.
var parms = [] , indextype=null ;
if(seg.level==0&&segments[0].level)
{ if(!seg.optim) parms.push(defparms) ;
if(segments[0].level==2) parms.push(indparms) ;
indextype = segments[0].type ;
}
if(segments[0].level==1) parms.push(indparms) ; else parms.push(metaparms) ;
return indexify(seg,parms,null,indextype) ;
}
// parse response
if(response.length<9||(extn!='.fit'&&response.substring(0,9)=='** Error:'))
abend(filename+'→'+response) ;
newseg = readgps(response,filename) ;
if(!newseg||!newseg.pts.length)
{ alerter(inject(M.nodata,shortname),0) ; return null ; }
recurse(newseg,'deltas') ; // compute deltas
newseg.origin = [ filename , origintype ] ;
newseg.filename = filename ;
// don't allow missing altitudes in url load or in tracks added to indexes
if(origintype=='uri'||(segments.length>0&&segments[0].level>0))
if(testflag==0&&newseg.level==0) for(k=0;k<2;k++)
for(pts=k?newseg.geo:newseg.pts,i=0;i<pts.length;i++)
if(pts[i].h==null||pts[i].h==undefined) abend(M.missingurialt) ;
// check legality of load level for addition
if(ovr=='add')
{ if(segments[0].level==0&&newseg.level==1&&newseg.type=='segments') ;
else if( newseg.level>segments[0].level || newseg.level==2
|| newseg.type=='segments')
{ s = inject2(M.addxtoy,newseg.type,segments[0].type) ;
alerter(s,0) ;
return null ;
}
}
/* ------------------------------------------------------------------------ */
if(ovr=='load') // initialise: don't do this earlier in case the read fails
{ if(imginfo.uri) imginfo.carriedover = 1 ;
// if a photolist has been loaded, it is not unloaded when a new track
// overwrites the original, since it may still be useful: if a list is not
// incorporated in the new track, the status of the old list is set to
// 'questionable' and the user is prompted whether to keep it when he or she
// comes to add an image
undrawsel() ;
actions = [] ;
unsavedchanges = [] ;
nactions = dragging = 0 ;
routeprops = new routetype() ;
for(i=0;i<segments.length;i++)
{ recurse(segments[i],'obliterate') ; disconnect(i) ; }
segments = [] ;
hues = [] ;
}
else
{ if(segments.length&&segments[0].level>0) newseg.tlink = filename ;
for(routeno=0;routeno<segments.length;routeno++) // enumerate the hues
{ if(segments[routeno].level) k = segments[routeno].pts.length ;
else { k = 1 ; hue = segments[routeno].hue ; }
for(i=0;i<k;i++)
{ if(segments[routeno].level) hue = segments[routeno].pts[i].hue ;
for(j=0;j<hues.length&&neq(hues[j],hue);j++) ;
if(j==hues.length) hues.push(hue) ;
}
}
}
/* ------------------------------------------------------------------------ */
// refreshing a track or index in an index or metaindex
if(ovr=='refresh')
{ oldroute = segments[0].pts[updseg] ;
recurse(oldroute,'obliterate') ;
newseg = callindexify(newseg) ;
segments[0].pts[updseg] = newseg ;
newseg.tmode = oldroute.tmode ;
if(newseg.level) { newseg.hue = oldroute.hue ; genshades(newseg) ; }
else newseg.colour = oldroute.colour ;
if(segments[0].level==1&&newseg.level==0) recurse(newseg,'arrows') ;
actions[nactions++] = [ 'refresh' , updseg , oldroute ] ;
}
// loading an index or metaindex
else if(newseg.level&&newseg.type!='segments'&&ovr!='add')
{ setdomtitle(newseg.title?newseg.title:M.untitledroute) ;
segments = [newseg] ;
// assign colours
hues = extendhues(newseg.pts.length,[]) ; // hues is list of [r,g,b] triples
rind = assigncolours(hues,getprox(newseg)) ; // rind is a subset of hues
for(i=0;i<newseg.pts.length;i++)
{ newseg.pts[i].colour = hexify(newseg.pts[i].hue=rind[i]) ;
if(newseg.pts[i].level) genshades(newseg.pts[i]) ;
}
if(newseg.level==1) recurse(newseg,'arrows') ;
actions[nactions++] = [ 'load' , 0 , newseg.title , null ] ;
}
// loading a new track or adding it as a new segment
else if( (newseg.level==0||newseg.type=='segments')
&& (segments.length==0||segments[0].level==0) )
{ if(newseg.type=='segments') n = newseg.pts.length ; else n = 1 ;
actions[nactions++] = [ ovr , segments.length , n , newseg.title , null ] ;
if(newseg.optim||newseg.type=='segments') dooptim = 0 ;
else if(loadflags&&loadflags.indexOf('n')>=0) dooptim = 0 ;
else if(loadflags&&loadflags.indexOf('o')>=0) dooptim = 1 ;
else if(prefs&&!prefs.optim) dooptim = 0 ;
else dooptim = 1 ;
if(dooptim)
{ if(prefs) lev0parms = optimparms(prefs.detail,prefs.maxsep) ;
else lev0parms = defparms ;
res = optimise(newseg.pts,lev0parms) ;
if(res.length<newseg.pts.length/2)
{ newseg.optim = optimaction(newseg.pts,lev0parms,
segments.length,res.length) ;
deltify(res) ;
newseg.pts = res ;
q = pen2detail(lev0parms.wppenalty) ;
telltale(inject(M.optimdetail,q.toFixed(0))) ;
}
}
if(newseg.list&&newseg.list!=imginfo.uri) getlist(newseg.list,'uri') ;
flag = promoteprops(newseg,routeprops) ;
if(flag&(1<<promotable.title)) setdomtitle(newseg.title) ;
else if(ovr=='load') setdomtitle(M.untitledroute) ;
if(newseg.type!='segments')
{ s = clone(newseg) ;
s.type = 'segments' ;
s.level = 1 ;
s.pts = [newseg] ;
newseg = s;
}
col = extendhues(n,hues) ;
for(i=0;i<n;i++)
{ newseg.pts[i].colour = hexify(newseg.pts[i].hue=col[i]) ;
segments.push(newseg.pts[i]) ;
}
}
// adding a track or index to an index or metaindex
else if(segments.length&&segments[0].level&&ovr=='add')
{ actions[nactions++] = [ ovr,segments[0].pts.length,1,newseg.title,null ] ;
newseg = callindexify(newseg) ;
newseg.colour = hexify(newseg.hue=extendhues(1,hues)[0]) ;
if(newseg.level) genshades(newseg) ;
segments[0].pts.push(newseg) ;
}
else return alerter('logic error',0) ;
return newseg ;
}
/* -------------------------------------------------------------------------- */
function genarrows(seg)
{ var sep = (segments[0].type=='tour')?20:10 , C , ctx , θ , c , s , icon , i ;
var pts = seg.pts , ind = choosearrows(pts,null,sep) , n = ind.length , k ;
var arrows = new Array(n) ;
for(i=0;i<n;i++)
{ k = ind[i] ;
θ = angle(pts[k].pos,pts[k+1].pos) ;
c = Math.cos(θ) ;
s = Math.sin(θ) ;
C = newcanvas(16,16) ;
ctx = C.getContext('2d') ;
ctx.fillStyle = seg.colour ;
ctx.beginPath() ;
ctx.moveTo(8+6*s,8-6*c) ;
ctx.lineTo(8-9*s+6*c,8+9*c+6*s) ;
ctx.lineTo(8-3*s,8+3*c) ;
ctx.lineTo(8-9*s-6*c,8+9*c-6*s) ;
ctx.closePath() ;
ctx.fill() ;
arrows[i] = { pos:midpt(pts[k].pos,pts[k+1].pos) ,
icon:L.divIcon({html:C,iconSize:[16,16],className:'rm'})
} ;
}
return arrows ;
}
function markarrows(arrows)
{ var i , opts , n = arrows.length ;
for(i=0;i<n;i++) // the arrow is just underneath the track
{ opts = { icon:arrows[i].icon , keyboard:false , zIndexOffset:-201} ;
arrows[i] = L.marker(arrows[i].pos,opts) ;
}
return L.layerGroup(arrows) ;
}
/* -------------------------------- parsedesc ------------------------------- */
function parsedesc(d,val,f,origin)
{ var i,dd=d,s,sty,sind,len,mode ;
if(!segbtn) mode = 'index' ; else mode = null ;
for(i=val.length;i>0&&(val.charAt(i-1)=='\n'||val.charAt(i-1)==' ');i--) ;
val = val.substring(0,i) ;
for(len=i=0;val.length;i++)
{ for(s=0;s<val.length&&(val.charAt(s)==' '||val.charAt(s)=='\n');s++) ;
if(s==val.length) break ; // all white space
for(s=0;s<val.length&&val.charAt(s)=='\n';s++) ;
val = val.substring(s) ;
sind = val.indexOf('\n') ;
if(i)
{ if(s) sty = 'margin-top:'+(1+3*s)+'px;' ; else sty = '' ;
if(sind<0) sty += 'margin-bottom:3px' ;
dd = domcreate('div',null,sty?'style':null,sty) ;
}
if(sind<0) { len += innerRML(dd,val,f,origin,mode) ; val = '' ; }
else
{ len += innerRML(dd,val.substring(0,sind),null,origin,mode) ;
val = val.substring(sind) ;
}
if(i) domadd(d,dd) ;
}
return len ;
}
/* -------------------------------------------------------------------------- */
function pluralise(str,n)
{ if(n==1) return str[0] ; else return inject(str[1],n) ; }
/* -------------------------------------------------------------------------- */
function waymarkcanvas(ind,scolor,fcolor,scale,xshift,desc,offs)
{ var c , r = window.devicePixelRatio||1 , ctx , sty , q ;
if(!scale) scale = 1 ;
w = scale * 24 ;
if(w!=Math.floor(w)) w = Math.floor(w+1) ;
sty = 'width:' + w + 'px;height:' + w + 'px' ;
w = scale * 24 * r ;
if(w!=Math.floor(w)) w = Math.floor(w+1) ;
c = domcreate('canvas',null,'width',w,'height',w,'style',sty) ;
ctx = c.getContext('2d') ;
ctx.scale(r*scale,r*scale) ;
if(xshift) ctx.translate(xshift,0) ;
ctx.fillStyle = fcolor ;
ctx.strokeStyle = scolor ;
ctx.lineWidth = 1 ;
ctx.beginPath() ;
waymarks[ind].draw(ctx) ;
ctx.stroke() ;
ctx.fill() ;
if(desc)
{ if(!offs) offs = 0 ;
offs += waymarks[ind].anchor[0] ;
q = Math.sqrt(3) ;
ctx.strokeStyle = '#771177' ;
ctx.lineWidth = 2 ;
ctx.beginPath() ;
ctx.moveTo(offs-1,10.5+q) ;
ctx.arc(offs,10.5+4/q,2/q,Math.PI*(1.5-1/3),Math.PI*(1.5+1/3)) ;
ctx.lineTo(offs+5,22.5-q) ;
ctx.arc(offs+4,22.5-2/q,2/q,Math.PI*(1.5+1/3),Math.PI*2.5) ;
ctx.lineTo(offs-4,22.5) ;
ctx.arc(offs-4,22.5-2/q,2/q,Math.PI*0.5,Math.PI*7/6) ;
ctx.closePath() ;
ctx.stroke() ;
}
return c ;
}
var oeuvres =
[ [ 'turn-slight-left' , 'Slight left' , M.slightl ] ,
[ 'turn-sharp-left' , 'Sharp left' , M.sharpl ] ,
[ 'uturn-left' , 'Left' , M.uturnl ] ,
[ 'turn-left' , 'Left' , M.turnl ] ,
[ 'ramp-left' , 'Left' , M.rampl ] ,
[ 'fork-left' , 'Left fork' , M.forkl ] ,
[ 'roundabout-left' , 'Left' , M.raboutl ] ,
[ 'turn-slight-right' , 'Slight right', M.slightr ] ,
[ 'turn-sharp-right' , 'Sharp right' , M.sharpr ] ,
[ 'uturn-right' , 'Right' , M.uturnr ] ,
[ 'turn-right' , 'Right' , M.turnr ] ,
[ 'ramp-right' , 'Right' , M.rampr ] ,
[ 'fork-right' , 'Right fork' , M.forkr ] ,
[ 'roundabout-right' , 'Right' , M.raboutr ] ,
[ 'straight' , 'Straight' , M.straight ] ,
[ 'merge' , 'Generic' , M.merge ] ] ;
/* ------------------------------- pts 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 label 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. label null, map null, handlers inactive, but marker non-null
// o. label non-null, map null, handlers inactive, marker non-null
// o. label non-null, map non-null, handlers active, marker non-null
// the state in which label 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)
// member functions
pttype.prototype.geticon = function()
{ var i,icon,offs ;
for(i=0;i<waymarks.length&&waymarks[i].name!=this.label;i++) ;
if(i==waymarks.length) i = 0 ;
icon = waymarkcanvas(i,'purple','#ddaaee',null,null,this.desc,0) ;
return L.divIcon({ html:icon , iconSize:[24,24] , className:'rm' ,
iconAnchor:waymarks[i].anchor }) ;
} ;
pttype.prototype.unmaplabel = function()
{ if(this.label&&this.labelmarker&&this.labelmarker.drawn)
{ map.removeLayer(this.labelmarker.marker) ; this.labelmarker.drawn = 0 ; }
} ;
pttype.prototype.setlabelmap = function()
{ if(!this.labelmarker.drawn)
{ this.labelmarker.marker.addTo(map) ;
this.labelmarker.drawn = 1 ;
this.labelmarker.marker.on('click',selmarker) ;
}
} ;
pttype.prototype.unmapphoto = function()
{ if(this.photomarker.drawn)
{ map.removeLayer(this.photomarker.marker) ; this.photomarker.drawn = 0 ; }
} ;
pttype.prototype.setphotomap = function()
{ if(!this.photomarker.drawn)
{ this.photomarker.marker.addTo(map) ;
this.photomarker.drawn = 1 ;
this.photomarker.marker.on('click',selmarker) ;
}
} ;
pttype.prototype.unsetlabel = function()
{ this.unmaplabel() ; this.label = this.caption = this.desc = null ; }
pttype.prototype.setlabel = function(l,c,d)
{ var opts ;
this.label = l ;
if(c) this.caption = c ; else this.caption = null ;
if(d) this.desc = d ; else this.desc = null ;
this.icon = this.geticon() ;
if(this.labelmarker) this.unmaplabel() ;
opts = { icon:this.icon , title:c , keyboard:false } ;
this.labelmarker = { marker:L.marker(this.pos,opts) , drawn:0 } ;
} ;
pttype.prototype.setmap = function()
{ if(this.label&&this.labelmarker) this.setlabelmap() ;
if(this.photo&&this.photomarker) this.setphotomap() ;
if(this.point&&!this.point.drawn)
{ this.point.marker.addTo(map) ; this.point.drawn = 1 ; }
} ;
pttype.prototype.unmap = function()
{ if(this.label&&this.labelmarker) this.unmaplabel() ;
if(this.photo&&this.photomarker) this.unmapphoto() ;
if(this.point&&this.point.marker) ungeodraw(this) ;
}
pttype.prototype.clone = function()
{ var c = clone(this) ;
c.photo = clone(this.photo) ;
c.setlabel(this.label,this.caption,this.desc) ; // sets marker
c.pos = { lat:this.pos.lat , lng:this.pos.lng } ;
c.setmap() ;
return c ;
}
/* -------------------------------------------------------------------------- */
// opt = 0: haven't got imginfo yet
// opt = 1: have got imginfo but not loaded the image
// opt = 2: now loaded as thumbnail, need to update the photo marker
// opt = 3: now loaded as icon, " " " " " "
function genphotomark(pt,opt,img)
{ var w=36,h=30,name=pt.photo[0],k=null,item,icon, r = window.devicePixelRatio ;
if(!opt) pt.photomarker = null ; // make sure it isn't there
if(imginfo.status=='ready')
{ k = findimg(name) ; if(k) item = imginfo.sect[k[0]].list[k[1]] ; }
if(opt==1&&k)
{ function imgfactory(pt,o,img)
{ return function() {genphotomark(pt,o,img);} }
img = domcreate('img') ;
icon = iconjpg(item,r*26) ;
if(icon&&icon.scale>=26*Math.min(r,2))
{ img.onload = imgfactory(pt,3,img) ; img.src = icon.url ; }
else { img.onload = imgfactory(pt,2,img) ; img.src = jpg(item,-1) ; }
return ;
}
var c = newcanvas(w,h) , ctx = c.getContext('2d') ;
var mind , ofsx=0 , ofsy=0 , nw , nh , d , opts ;
if(opt==2&&k)
{ nw = img.naturalWidth?img.naturalWidth:item.thumbshape[0] ;
nh = img.naturalHeight?img.naturalHeight:item.thumbshape[1] ;
if(nw<nh) { mind = nw ; ofsy = Math.floor((nh-nw)/2) ; }
else { mind = nh ; ofsx = Math.floor((nw-nh)/2) ; }
ctx.drawImage(img,ofsx,ofsy,mind,mind,2,2,h-4,h-4) ;
}
else if(opt==3) ctx.drawImage(img,2,2,h-4,h-4) ;
ctx.lineWidth = 0 ;
ctx.fillStyle = 'purple' ;
ctx.moveTo(h,h/2-2) ;
ctx.lineTo(w,h/2) ;
ctx.lineTo(h,h/2+2) ;
ctx.fill() ;
ctx.lineWidth = 2 ;
ctx.strokeStyle = 'purple' ;
sqr(h-2,2,1) ;
if(opt==1&&!k)
{ ctx.lineWidth = 2 ;
ctx.strokeStyle = 'purple' ;
ctx.beginPath() ;
ctx.moveTo(1,h-1) ;
ctx.lineTo(h-1,1) ;
ctx.stroke() ;
}
else
{ ctx.lineWidth = 1 ;
ctx.strokeStyle = 'white' ;
sqr(h-3,1.5,1.5) ;
}
d = domcreate('div',c) ;
icon = L.divIcon( { html:d , iconSize:[36,30] ,
iconAnchor:[36,15] , className:'rm' } ) ;
opts = { icon , keyboard:false , title:phototitle(pt) , zIndexOffset:25 } ;
if(pt.photomarker) pt.unmapphoto() ; // opt >= 1
pt.photomarker = { marker:L.marker(pt.pos,opts) , drawn:0 } ;
pt.setphotomap() ;
if(opt==0)
{ if(imginfo.status=='ready') genphotomark(pt,1) ;
else if(imginfo.status!='nolist') imginfo.pending.push(pt) ;
}
function sqr(h,r,ofs)
{ var π = Math.PI ;
ctx.beginPath() ;
ctx.moveTo(ofs,ofs+r) ;
ctx.lineTo(ofs,ofs+h-r) ;
ctx.arc(ofs+r,ofs+h-r,r,-π,-3*π/2,1) ;
ctx.lineTo(ofs+h-r,ofs+h) ;
ctx.arc(ofs+h-r,ofs+h-r,r,-3*π/2,-2*π,1) ;
ctx.lineTo(ofs+h,ofs+r) ;
ctx.arc(ofs+h-r,ofs+r,r,0,-π/2,1) ;
ctx.lineTo(ofs+r,ofs) ;
ctx.arc(ofs+r,ofs+r,r,-π/2,-π,1) ;
ctx.stroke() ;
}
}
/* -------------------------------------------------------------------------- */
function phototitle(pt)
{ var k,title ;
title = pt.photo[0] ;
if( imginfo.status=='ready' && (k=findimg(title)) )
title = imginfo.sect[k[0]].list[k[1]].texttitle ;
if(pt.photo.length>1) title += ' (+' + (pt.photo.length-1) + ')' ;
return title ;
}
/* -------------------------------------------------------------------------- */
function interp(x,y,λ)
{ var y0=x.lat,y1=y.lat,x0=x.lng,x1=y.lng,p ;
if(x1>180+x0) x0 += 360 ; else if(x0>x1+180) x1 += 360 ;
p = x0 + λ*(x1-x0) ;
if(p>180) p -= 360 ;
return { lat:y0+λ*(y1-y0) , lng:p }
}
/* -------------------------------------------------------------------------- */
function getllpt(node)
{ return new pttype(getlatlong(node,'lat','lng'),null,null) ; }
/* ------------------------------- gatherpix ------------------------------- */
function extendphoto(photo,done,name)
{ var item,k,kbest,img,scale,imgscale,src,sizes ;
function err(x) { return Math.abs(x-2.5) ; }
var ind = findimage(imginfo.sect,name) ;
if(!ind) return ;
item = imginfo.sect[ind[0]].list[ind[1]] ;
for(k=0;k<done.length&&done[k]!=ind;k++) ;
if(k<done.length) return ;
src = item.filename + item.thumbshape[2] + '.jpg' ;
img = { src , width:item.thumbshape[0] , height:item.thumbshape[1] ,
stars:item.visibility=='*'?'*':null } ;
if(item.hithumb&&item.hithumb.length)
{ for(imgscale=k=0;k<item.hithumb.length;k++)
if(!imgscale||err(item.hithumb[k].scale)<err(imgscale))
{ kbest = k ; imgscale = item.hithumb[k].scale ; }
img.srcset = item.filename + item.hithumb[kbest].suffix + '.jpg' ;
img.scale = imgscale ;
}
else
{ sizes = item.sizes ;
scale = ( sizes[0].scale * item.thumbshape[0] ) / item.shape[0] ;
for(imgscale=null,k=0;k<sizes.length;k++)
if((!sizes[k].type)&&(sizes[k].scale>scale))
if(!imgscale||sizes[k].scale<imgscale)
{ img.srcset = sizes[k].suffix ; imgscale = sizes[k].scale ; }
if(img.srcset)
{ img.srcset = item.filename + img.srcset + '.jpg' ;
img.scale = (imgscale/scale).toFixed(1) ;
}
}
photo.push(img) ;
done.push(ind) ;
}
function gatherpix(route) // construct small photos from images
{ if(!imginfo.status) return [] ;
var photo,i,j,done ;
for(done=[],photo=[],i=0;i<route.pts.length;i++)
if(route.pts[i].photo&&route.pts[i].photo.length)
for(j=0;j<route.pts[i].photo.length;j++)
extendphoto(photo,done,route.pts[i].photo[j]) ;
return photo ;
}
function thumbpix(route) // construct small photos from images
{ if(!imginfo.status) return [] ;
var photo=[],j,done ;
if(route.photo&&route.photo.length) for(done=[],j=0;j<route.photo.length;j++)
extendphoto(photo,done,route.photo[j]) ;
return photo ;
}
/* ------------------------------- writeindex ------------------------------ */
function writeindex(index,nesting)
{ var i,str='',blx='',type=index.type ;
for(i=0;i<nesting;i++) blx += ' ' ;
if(nesting==0)
str = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n' +
'<!-- https://www.routemaster.app/software/routemaster.html -->\n' ;
str += blx + '<route type="'+type+'">\n' ;
str += writerteprops(index,nesting,index.gallery) ;
for(i=0;i<index.pts.length;i++)
{ if(index.pts[i].level) str += writeindex(index.pts[i],nesting+1) ;
else str += writerte(index.pts[i],index.pts[i].pts,index.pts[i].geo,
type=='segments'?prefs.precision:0,nesting+1,type) ;
}
return str + blx + '</route>\n' ;
}
/* -------------------------------- indexify ------------------------------- */
function indexify(route,optparms,reclev,indextype)
{ var origin,i,j , r = new routetype() , stats , n = route.pts.length ;
r.pts = new Array(n) ;
// processing the top-level route - get photos and stats before optimisation
if(!reclev)
{ for(field in route) if(typeof(route[field])!='object')
r[field] = route[field] ;
// if the loaded file has pix images, convert to smallphotos
if(route.list&&route.level==0)
{ if(imginfo.uri!=route.list) // can be satisfied when adding to an index
getlist(route.list,'uri',r,route) ; // does a deferred gatherpix
else if(imginfo.status=='waiting') getlist.pending.push(r,route) ;
else if(imginfo.status!='failed') r.smallphoto = gatherpix(route) ;
}
if(!route.level) // reformat the stats for a route incorporated in an index
{ stats = routestats([route]) ;
r.date = stats.date ;
r.stats = [ 1 , stats.dist , stats.asc , stats.desc ,
stats.minalt , stats.maxalt ] ;
}
origin = route.origin ;
if( origin && origin[1]
&& (origin[1].substring(0,3)=='uri'||origin[1]=='refresh') )
r.tlink = origin[0] ;
else if(origin&&origin[0]) r.tlink = '$FILE$/' + origin[0] ;
else r.tlink = '$FILE$' ;
}
else
{ for(i in {title:0,stats:0,stars:0,level:0,type:null}) r[i] = route[i] ;
if(reclev==1) for(i=0;i<route.smallphoto.length;i++)
if(route.smallphoto[i].stars) r.smallphoto.push(route.smallphoto[i]) ;
}
if(route.level) for(i=0;i<n;i++)
r.pts[i] = indexify(route.pts[i],optparms,reclev?reclev+1:1) ;
else
{ for(i=0;i<n;i++)
{ r.pts[i] = new pttype(route.pts[i].pos) ;
if(indextype=='tour') r.pts[i].h = route.pts[i].h ;
r.pts[i].delta = route.pts[i].delta ;
}
if(indextype!='uindex') for(i=0;i<optparms.length;i++)
{ r.pts = optimise(i?r.pts:decimate(r.pts),optparms[i]) ; deltify(r.pts) ; }
}
if(!reclev&&route.level)
{ for(i=0;i<n;i++) for(j=0;j<r.pts[i].smallphoto.length;j++)
r.smallphoto.push(r.pts[i].smallphoto[j]) ;
getmetastats(r) ;
}
return r ;
}
/* -------------------------------------------------------------------------- */
function textdiv(title,str,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 = domcreate('b',title+': ') ;
if(lim==null||str.length<lim)
{ nobr = domcreate('nobr',title!=null?b:null) ;
domadd(nobr,str) ;
domadd(div,nobr) ;
}
else
{ if(title!=null) domadd(div,b) ;
domadd(div,str) ;
if(flag==0) underline(div) ;
}
return div ;
}
/* -------------------------------------------------------------------------- */
function highdiv(segno)
{ var div,scroll,p,a,d,s,ptno,nseg,word,routeno,base=segments[0],col ;
var link , nfetched = 2 , sect = imginfo.sect , url = document.location.href ;
var seg = base.pts[segno] , gallery = seg.gallery , dellink,golink,updlink ;
var items=[],i,j,k,ind,maxh,minw,sum,scroll,npts,image=[null,null,null] ;
i = url.indexOf('?') ;
if(i>=0) url = url.substring(0,i) ;
if(base.level) items = seg.smallphoto ;
else if(imginfo.status=='ready'&&imginfo.uri)
for(i=0;i<seg.photo.length;i++) if((ind=findimage(sect,seg.photo[i])))
{ for(j=0;j<items.length&&items[j].ind!=ind;j++) ;
if(j==items.length)
{ s = sect[ind[0]].list[ind[1]].thumbshape ;
items.push({ ind:ind , width:s[0] , height:s[1] , top:0 }) ;
}
}
div = domcreate('div',null,'style','font-family:helvetica') ;
if(items.length>0)
{ minw = items[0].width ;
if(items.length>1) minw += items[items.length-1].width ;
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].height>maxh) maxh = items[i].height ;
if(i)
{ sum = items[i].width + items[i-1].width ; if(sum<minw) minw = sum ; }
}
for(i=0;i<items.length;i++)
items[i].top = Math.floor(0.5+(maxh-items[i].height)/2) ;
if(items.length==1) minw -= 4 ;
scroll = domcreate('div',null,'style','position:relative;width:'+(minw+4)+
'px;height:'+(maxh+4)+'px;overflow:hidden;'+
'left:calc((100% - '+((minw+4)+'px')+')/2)') ;
for(sum=i=0;i<2&&i<items.length;sum+=items[i].width+4,i++)
{ image[i].addimage(sum) ; domadd(scroll,image[i].img) ; }
domadd(div,scroll) ;
}
else if(base.level<2&&imginfo&&imginfo.status&&imginfo.status!='ready')
{ if(imginfo.status=='waiting') s = inject(M.waitingfor,M.photolist) ;
else s = M.listnotfound ;
domadd(div,underline(domcreate('div',s))) ;
}
domadd(div,titlediv('title',seg.title,0,null)) ;
if(seg.stars!=null) domadd(div,starsline(seg.stars,0)) ;
if(base.level<2)
{ d = titlediv('desc',seg.desc,null,0) ; // not linkable
if(seg.desc&&seg.desc.length>=50)
{ // ↓↓ 18 is a magic number to avoid narrowly columnar text
k = Math.min( 18*Math.sqrt(seg.desc.length) , 0.6*window.innerWidth ) ;
if(items.length>0&&minw+4>k) k = minw + 4 ;
domset(d,'style','min-width:'+k+'px') ;
}
domadd(div,d) ;
}
if(seg.stats) domadd(div,prettystats(M.stats,seg.stats)) ;
if(seg.date) domadd(div,textdiv(M.date,seg.date)) ;
if(base.level==2&&gallery) domadd(div,genindexlink(gallery,M.view)) ;
if(segments[0].type=='tour')
{ var segs = segments[0].pts ;
if(segno>0) domadd(div,genclickfn(swapsegfactory(segno-1),
inject2(M.swapwith,segs[segno-1].colour,
segs[segno-1].title+', '+M.preceding),'br')) ;
if(segno<segs.length-1) domadd(div,genclickfn(swapsegfactory(segno),
inject2(M.swapwith,segs[segno+1].colour,
segs[segno+1].title+', '+M.following),'br')) ;
}
dellink = updlink = golink = col = null ;
// route colour
if(url.substring(url.length-8)=='test.php') col = '[col='+seg.colour+']' ;
// delete route
function delfactory(i)
{ return function() { selected.segno = i ; discard() ; } ; } ;
if(base.pts.length>1)
dellink = genclickfn(delfactory(segno),'['+caps(M.del)+']') ;
// go to route / update route
function updfactory(i) { return function() { refresh(i) ; } ; } ;
if(seg.tlink&&seg.tlink.substring(0,6)!='$FILE$')
{ if(seg.ttype=='html') link = seg.tlink ;
else link = trackref(base.origin,seg.tlink,1) ;
golink = genlink(link,'['+M.view1+']',1) ;
domset(golink,'onclick',"infowindow.close()") ;
if(seg.ttype!='html')
updlink = genclickfn(updfactory(segno),'['+caps(M.refresh)+']') ;
}
if(dellink||golink||col)
{ d = domcreate('div') ;
if(golink) { domadd(d,golink) ; k = 1 ; } else k = 0 ;
if(dellink) { if(k) domadd(d,' : ') ; domadd(d,dellink) ; k = 1 ; }
if(updlink) { if(k) domadd(d,' : ') ; domadd(d,updlink) ; k = 1 ; }
if(col) { if(k) domadd(d,' : ') ; domadd(d,col) ; }
domadd(div,d) ;
}
ptno = d = routeno = 0 ;
return { div , scroller:setInterval(scroller,30) } ;
function genpicimg(item,loadfunc)
{ var ind,img ;
function serve(x)
{ if(x.substr(0,5)=='http:'&&document.URL.substr(0,6)=='https:')
return fileserver + '?' + x ;
else return x ;
}
if(!base.level)
{ ind = item.ind ;
img = genimage(sect[ind[0]].list[ind[1]],-1,{loadfunc:loadfunc}) ;
return doim(img).img ;
}
img = domcreate('img',null,'src',serve(item.src),
'width',item.width,'height',item.height) ;
if(item.srcset)
{ item = serve(item.srcset)+ ' ' + item.scale + 'x' ;
domset(img,'srcset',item) ;
}
if(loadfunc) img.onload = loadfunc ;
return img ;
}
function scrolltype(i)
{ this.img = this.top = this.pos = null ;
this.ind = i ; // index into items
this.wid = items[i].width ;
this.addimage = function(pos)
{ var fetch=null,ind=this.ind ;
if(ind<items.length-1&&nfetched<=ind)
{ nfetched += 1 ; fetch = function() { genpicimg(items[ind+1],null) ; } }
this.img = genpicimg(items[ind],fetch) ;
this.pos = pos ;
this.scrollimage() ;
}
this.scrollimage = function()
{ domset(this.img,'style',
'position:absolute;top:'+items[this.ind].top+'px;left:'+this.pos+'px') ;
}
}
function scroller()
{ var i,ind,offset,ipts ;
if(base.level==1)
{ if(seg.level==1) ipts = seg.pts[routeno].pts ; else ipts = seg.pts ;
drawsel(ipts,ptno,d>0?d:null) ;
for(d+=40;ptno<ipts.length-1;d-=offset,ptno++)
{ offset = ipts[ptno].delta ; if(d<offset) break ; }
if(ptno==ipts.length-1)
{ routeno += 1 ;
if(seg.level==1) if(routeno>=seg.pts.length) routeno = 0 ;
ptno = 0 ;
d = -400 ;
}
}
if(items.length<=2) return ;
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) ;
domadd(scroll,image[i].img) ;
}
}
}
/* -------------------------------------------------------------------------- */
function buttons(name)
{ return [ name,btncanvas(name,'black',24),btncanvas(name,'grey',24) ] ; }
function btncanvas(name,colour,size)
{ var i,j,ctx,c , r = window.devicePixelRatio||1 ;
var theta,phi,psi,q=0.24,sign,twopi=2*Math.PI ;
var sty = 'width:' + size + 'px;height:' + size + 'px' ;
var eyeparms = // svg bezier path for an eye in a 100x100 box
[ 50.000,78.706, 34.898,78.706, 19.759,69.703, 5.000,51.948,
4.061,50.818, 4.061,49.181, 5.000,48.052,
19.759,30.297, 34.898,21.295, 50.000,21.295,
65.100,21.295, 80.241,30.297, 95.000,48.052,
95.939,49.181, 95.939,50.818, 95.000,51.948,
80.241,69.702, 65.100,78.706, 50.000,78.706 ] ;
if(name=='layers') sty += ';padding:3px' ;
c = domcreate('canvas',null,'width',size*r,'height',size*r,'style',sty) ;
ctx = c.getContext('2d') ;
ctx.scale(r*size/24,r*size/24) ;
if(colour!='black') colour = '#bebebe' ;
ctx.strokeStyle = colour ;
ctx.fillStyle = colour ;
ctx.lineWidth = 0 ;
if(name=='settings'||name=='segment')
{ ctx.lineWidth = 4 ;
ctx.beginPath() ;
ctx.fillStyle = 'white' ;
ctx.arc(12,12,6,0,twopi,false) ;
ctx.stroke() ;
ctx.lineWidth = 0 ;
ctx.fillStyle = colour ;
phi = twopi / 12 ;
psi = twopi / 32 ;
r = 11 / Math.cos(psi) ;
for(j=0;j<6;j++)
{ theta = j * twopi / 6 ;
ctx.beginPath() ;
ctx.moveTo(12+7*Math.sin(theta-phi),12+7*Math.cos(theta-phi)) ;
ctx.lineTo(12+r*Math.sin(theta-psi),12+r*Math.cos(theta-psi)) ;
ctx.lineTo(12+r*Math.sin(theta+psi),12+r*Math.cos(theta+psi)) ;
ctx.lineTo(12+7*Math.sin(theta+phi),12+7*Math.cos(theta+phi)) ;
ctx.fill() ;
}
if(name=='segment')
{ ctx.fillStyle = 'white' ;
ctx.beginPath() ;
ctx.moveTo(12,12) ;
ctx.lineTo(12,24) ;
ctx.lineTo(24,24) ;
ctx.lineTo(24,6) ;
ctx.fill() ;
}
}
else if(name=='waypoint')
{ ctx.beginPath() ;
ctx.moveTo(12,2) ;
ctx.lineTo(3,23) ;
ctx.lineTo(12,13) ;
ctx.lineTo(21,23) ;
ctx.fill() ;
}
else if(name=='scissors') for(j=0;j<2;j++)
{ if(j) sign = -1 ; else sign = 1 ;
ctx.fillStyle = colour ;
ctx.beginPath() ;
ctx.moveTo(12-5*sign,1) ;
ctx.lineTo(12+3*sign,15) ;
ctx.lineTo(12+1.5*sign,18) ;
ctx.lineTo(12-6.6*sign,2.5) ;
ctx.fill() ;
ctx.beginPath() ;
ctx.ellipse(12+5.5*sign,18.5, 3.5,5, -sign*twopi/12, 0,twopi) ;
ctx.fill() ;
ctx.fillStyle = 'white' ;
ctx.beginPath() ;
ctx.ellipse(12+5.5*sign,18.5, 2,3, -sign*twopi/12, 0,twopi) ;
ctx.fill() ;
}
else if(name=='pen')
{ ctx.beginPath() ;
ctx.moveTo(16,1) ;
ctx.lineTo(23,8) ;
ctx.lineTo(17,12) ;
ctx.lineTo(12,7) ;
ctx.fill() ;
ctx.beginPath() ;
ctx.moveTo(12,8) ;
ctx.lineTo(16,13) ;
ctx.arcTo(15,16,16,20,12) ;
ctx.lineTo(2,24) ;
ctx.lineTo(1.3,23.3) ;
ctx.lineTo(8.3,16.15) ;
ctx.lineTo(7.7,15.85) ;
ctx.fill() ;
ctx.beginPath() ;
ctx.moveTo(12,8) ;
ctx.arcTo(9,9,5,8,12) ;
ctx.lineTo(0,22) ;
ctx.lineTo(0.7,22.7) ;
ctx.lineTo(7.7,15.85) ;
ctx.fill() ;
ctx.fillStyle = 'white' ;
ctx.beginPath() ;
ctx.arc(9,15,1.5,0,twopi) ;
ctx.fill() ;
}
else if(name=='camera')
{ ctx.beginPath() ;
ctx.moveTo(1,18) ;
ctx.lineTo(1,8) ;
ctx.lineTo(2,8) ;
ctx.lineTo(3,6) ;
ctx.lineTo(7,6) ;
ctx.lineTo(8,8) ;
ctx.lineTo(9,8) ;
ctx.lineTo(12,5) ;
ctx.lineTo(15,5) ;
ctx.lineTo(19,8) ;
ctx.lineTo(19,8) ;
ctx.lineTo(20,7) ;
ctx.lineTo(21,7) ;
ctx.lineTo(22,8) ;
ctx.lineTo(23,8) ;
ctx.lineTo(23,18) ;
ctx.lineTo(22,19) ;
ctx.lineTo(2,19) ;
ctx.fill() ;
ctx.fillStyle = 'white' ;
ctx.lineWidth = 3 ;
ctx.beginPath() ;
ctx.arc(13.5,14,4.5,0,twopi) ;
ctx.stroke() ;
ctx.fill() ;
}
else if(name=='undo'||name=='redo')
{ if(name=='undo') sign = 1 ; else sign = -1 ;
// the arrowhead
ctx.beginPath() ;
ctx.moveTo(12-sign*10,10) ;
ctx.lineTo(12-sign*3,3) ;
ctx.lineTo(12-sign*3,17) ;
ctx.fill() ;
// upper elliptical arc
ctx.beginPath() ;
ctx.ellipse(12-sign*3,20, 12,13,0, (sign>0?0:-twopi/2),-twopi/4, sign>0) ;
ctx.fill() ;
// fill in triangle below
ctx.beginPath() ;
ctx.moveTo(12-sign*3,7) ;
ctx.lineTo(12+sign*3,13) ;
ctx.lineTo(12+sign*9,20) ;
ctx.lineTo(12-sign*3,20) ;
ctx.fill() ;
// lower elliptical arc
ctx.fillStyle = 'white' ;
ctx.beginPath() ;
ctx.ellipse(12-sign*3,20, 12,7,0, (sign>0?0:-twopi/2),-twopi/4, sign>0) ;
ctx.fill() ;
// unfill triangle below
ctx.beginPath() ;
ctx.moveTo(12-sign*3,13) ;
ctx.lineTo(12+sign*3,16) ;
ctx.lineTo(12+sign*9,20) ;
ctx.lineTo(12-sign*3,20) ;
ctx.fill() ;
}
else if(name=='eye')
{ ctx.beginPath() ;
ctx.lineWidth = 3 ;
ctx.moveTo(q*eyeparms[0],q*eyeparms[1]) ;
for(i=0;i<6;i++)
ctx.bezierCurveTo(q*eyeparms[2+6*i],q*eyeparms[3+6*i],
q*eyeparms[4+6*i],q*eyeparms[5+6*i],
q*eyeparms[6+6*i],q*eyeparms[7+6*i]) ;
ctx.stroke() ;
ctx.beginPath() ;
ctx.arc(q*50,q*50,q*17,0,twopi) ;
ctx.fill() ;
}
else if(name=='dl')
{ ctx.beginPath() ;
ctx.moveTo(12,19) ;
ctx.lineTo(4,11) ;
ctx.lineTo(8,11) ;
ctx.lineTo(8,4) ;
ctx.arcTo(8,2,10,2,2) ;
ctx.lineTo(14,2) ;
ctx.arcTo(16,2,16,4,2) ;
ctx.lineTo(16,11) ;
ctx.lineTo(20,11) ;
ctx.fill() ;
ctx.beginPath() ;
ctx.lineWidth = 2 ;
ctx.moveTo(2,18) ;
ctx.lineTo(2,20) ;
ctx.arcTo(2,22,4,22,2) ;
ctx.lineTo(20,22) ;
ctx.arcTo(22,22,22,20,2) ;
ctx.lineTo(22,18) ;
ctx.stroke() ;
}
// account name=0=>not logged in, 1=>logged in no alerts, 2=>+alerts
else if(typeof(name)=='number')
{ if(!name) ctx.fillStyle = 'white' ;
ctx.lineWidth = 2 ;
ctx.beginPath() ;
ctx.arc(12,7,5,0,twopi) ;
ctx.fill() ;
ctx.stroke() ;
ctx.beginPath() ;
ctx.moveTo(2,22) ;
ctx.lineTo(2,18) ;
ctx.arcTo(2,14,6,14,4) ;
ctx.lineTo(18,14) ;
ctx.arcTo(22,14,22,18,4) ;
ctx.lineTo(22,22) ;
ctx.closePath() ;
ctx.fill() ;
ctx.stroke() ;
}
else if(name=='layers')
{ var grey=['#aaaaaa','#b8b8b8','#cccccc','#d8d8d8','#eeeeee'] ;
for(i=16;i>=8;i-=4) // bottom layer, then middle layer, then top
{ ctx.beginPath() ; // draw the edge of the tile
ctx.fillStyle = '#888888' ;
ctx.moveTo(0,i) ;
ctx.lineTo(0,i+1) ;
ctx.lineTo(12,i+1+6) ;
ctx.lineTo(24,i+1) ;
ctx.lineTo(24,i) ;
ctx.lineTo(12,i+6) ;
ctx.fill() ;
ctx.beginPath() ; // draw the tile
ctx.fillStyle = grey[(16-i)/2] ;
ctx.moveTo(0,i) ;
ctx.lineTo(12,i+6) ;
ctx.lineTo(24,i) ;
ctx.lineTo(12,i-6) ;
ctx.closePath() ;
ctx.fill() ;
if(i==16) continue ;
ctx.beginPath() ; // draw the bleedthrough of the lower tile
ctx.fillStyle = grey[(16-i)/2-1] ;
ctx.moveTo(4,i+2) ;
ctx.lineTo(12,i+6) ;
ctx.lineTo(20,i+2) ;
ctx.lineTo(12,i-2) ;
ctx.closePath() ;
ctx.fill() ;
if(i==12) continue ;
ctx.beginPath() ; // draw the bleedthrough of the lower tile
ctx.fillStyle = grey[(16-i)/2-2] ;
ctx.moveTo(8,i+4) ;
ctx.lineTo(12,i+6) ;
ctx.lineTo(16,i+4) ;
ctx.lineTo(12,i+2) ;
ctx.closePath() ;
ctx.fill() ;
}
}
return c ;
}
/* -------------------------------------------------------------------------- */
function buttoncell(gif1,gif2,gif3)
{ var nobr=domcreate('nobr',null,'style','padding-right:6px') ;
var i,gifs=[gif1,gif2,gif3],c,g ;
for(i=0;i<3&&(gifs[i]==0||gifs[i]);i++)
{ if(gifs[i]<0) g = gifs[2] ; else g = gifs[i] ;
c = btncanvas(g,'black',24) ;
domadd(nobr,c,' ') ;
if(g==0||g==1) rmhelpdiv.im = c ;
if(i==1&&(gifs[0]==-1||gifs[1]==-1)) break ;
}
return domcreate('td',nobr,'style','padding-bottom:4px') ;
}
function textcell(p1,p2)
{ var td=domcreate('td',domcreate('nobr',p1),'style','padding-bottom:4px') ;
if(p2!=null&&p2!=undefined)
domadd(td,domcreate('br'),domcreate('nobr',p2)) ;
return td ;
}
function appendrow(td,p) { domadd(td,domcreate('nobr',p),domcreate('br')) ; }
function genlink(uri,legend,blank)
{ var a = domcreate('a',legend,'href',uri,'style',
'cursor:pointer;color:#0000bd;text-decoration:none') ;
if(blank=='close')
{ a.onclick = function() { infowindow.close() ; } ; return a ; }
if(!blank) return a ;
domset(a,'target','_blank') ;
var s = domcreate('span',a) ;
domadd(s,' ',newtabdiv()) ;
if(blank=='br') domadd(s,domcreate('br')) ;
return s ;
}
function genindexlink(index,legend)
{ var s = trackref(routeprops.origin,index.href,1) ;
if(index.title) legend += ' ‘' + index.title + '’' ;
return genlink(s,legend,'br') ;
}
/* -------------------------------------------------------------------------- */
function updateacbtns(opt)
{ var i,v,node ;
if(acbtn)
{ if(opt==0) v = M.register[1] + '/' + M.register[0] ; else v = M.account ;
acbtn.ui.greytitle = acbtn.blacktitle = v ;
acbtn.blackimg = btncanvas(opt,'black',24) ;
acbtn.greyimg = btncanvas(opt,'grey',24) ;
redrawbtn(acbtn,-1) ;
}
else for(node=blurbdiv,i=0;i<2;i++,node=rmhelpdiv) if(node.im)
{ v = btncanvas(opt,'black',24) ;
node.im.parentNode.replaceChild(v,node.im) ;
node.im = v ;
}
}
/* -------------------------------------------------------------------------- */
function logoutfunc(opt,pos)
{ var xhttp = new XMLHttpRequest() ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ if(xhttp.response.length==0) return alerter(M.dberr,pos) ;
if(xhttp.response=="**sqlerr: already")
return alerter(M.alreadygone,pos) ;
else if(xhttp.response!="Account deleted"&&xhttp.response!="Logged out")
return alerter(xhttp.response,pos) ;
updateacbtns(0) ;
prefs = defprefs ;
}
}
xhttp.open("GET","resources/signup.php"+(opt?"?delact":"")) ;
xhttp.send() ;
}
function logout(pos) { infowindow.close() ; logoutfunc(0,pos) ; }
function logoutclose(pos)
{ function loggerouter() { logoutfunc(1,pos) ; }
freeze() ;
infowindow.open(prompter(M.delwarn,-1,loggerouter),pos,1,unfreeze) ;
}
/* -------------------------------------------------------------------------- */
function (opt) // opt is 'login' if called from map, 'blurb' from splash
{ var d = domcreate('div') , pos = (opt=='blurb')?'blurb':9 ;
function acfactory(ind)
{ return function()
{ freeze() ; infowindow.open(acfollowon(ind,opt),pos,1,unfreeze) ; } ;
}
function logoutfactory(opt,pos)
{ if(opt) return function() { logoutclose(pos) ; }
else return function() { logout(pos) ; }
}
if(prefs.email)
{ domadd(d,genspan(prefs.email,'hr')) ;
function doprefs()
{ infowindow.close() ; prefsprompt(M.prefs+':',null,null,pos) ; }
function bugreport()
{ window.open('mailto:colin.champion@routemaster.app?subject='+M.prob) ; }
if(opt=='blurb') opt = 'login' ;
domadd(d,genclickfn(doprefs,M.prefs,'br')) ;
domadd(d,genclickfn(acfactory(3),M.register[3],'br')) ;
domadd(d,genclickfn(logoutfactory(0,pos),M.logout,'br')) ;
domadd(d,genclickfn(logoutfactory(1,pos),M.logout+' '+M.delact,'br')) ;
domadd(d,genlink('mailto:colin.champion@routemaster.app?'+
'subject=routemaster problem',M.bugreport,'close')) ;
return d ;
}
domadd(d,genclickfn(acfactory(0),M.register[0],'br')) ;
domadd(d,genclickfn(acfactory(1),M.register[1],'br')) ;
domadd(d,genclickfn(acfactory(2),M.register[2])) ;
return d ;
}
/* -------------------------------------------------------------------------- */
// opt is 0 for register, 1 for login,
// 2 for forgotten password, 3 for changing password
function acfollowon(opt,logopt)
{ var div,d,i,formreq,optno,btns ;
var label,inp,pl,valid,pinp,cinp=null,sinp,minp,kinp ;
if(opt==3)
{ if(prefs.email) acfollowon.email = prefs.email ; else return null ; }
else if(opt<4) acfollowon.email = null ;
if(opt>=4)
{ d = domcreate('div',acfollowon.email+':') ;
domadd(d,domcreate('br'),M.otkey) ;
}
else d = domcreate('div',M.register[opt]) ;
domadd(d,domcreate('br')) ;
if(opt==0||opt==2) domadd(d,M.emailprompt) ;
else if(opt==1) domadd(d,inject2(M.pwdprompt,M.email,M.pwd)) ;
else if(opt==3) domadd(d,M.aloneprompt) ;
else domadd(d,inject2(M.pwdprompt,M.otk,M.pwd)) ;
function genbtns()
{ var i,node,inp ;
for(i=btns.childNodes.length-1;i>=0;i--)
btns.removeChild(btns.childNodes[i]) ;
inp = domcreate('button',caps(M.cancel),'style',btnstyle(valid<7)) ;
inp.onclick = unblock ;
domadd(btns,inp) ;
if(valid==7)
{ inp = domcreate('button',M.submit,'style',btnstyle(1)) ;
inp.onclick = function() { formresponse() ; unblock() ; }
domadd(btns,inp) ;
}
}
function emailvalidate()
{ var i = this.value.indexOf('@') , ov = valid , j=-1 ;
if(i>1) j = this.value.substring(i+2).indexOf('.') ;
if(j>=0&&i+2+j<this.value.length-1) valid |= 1 ; else valid &= ~1 ;
if(valid!=ov) genbtns() ;
}
function pwdvalidate()
{ var ov = valid ;
if(pinp.value.length>=6&&(!cinp||pinp.value==cinp.value)) valid |= 4 ;
else valid &= ~4 ;
if(valid!=ov) genbtns() ;
}
function otkvalidate()
{ var ov = valid , intval = parseInt(this.value) ;
if(intval>=100000000&&intval<1000000000) valid |= 2 ; else valid &= ~2 ;
if(valid!=ov) genbtns() ;
}
if(opt==0||opt==2) opts = [0] ; // send username alone to get otkey
else if(opt==1) opts = [0,2] ; // login: send username and password
else if(opt==3) opts = [2,3] ; // update password: request pwd + confirmation
else if(opt==4||opt==5) opts = [1,2,3] ; // send otkey and password
var f = domcreate('form',null,'method','POST','name','fileinfo') ;
domset(f,'style','margin-bottom:0','enctype','multipart/form-pts') ;
div = domcreate('div',d) ;
domadd(div,f) ;
// build up the html form
for(valid=7,optno=0;optno<opts.length;optno++)
{ i = opts[optno] ;
valid &= ~ (1<<i) ;
if(i==0) { name = 'email' ; pl = M.email ; }
else if(i==1) { name = 'otkey' ; pl = M.otk ; }
else if(i==2) { name = 'password' ; pl = M.pwd ; }
else if(i==3) { name = 'confpwd' ; pl = M.confpwd ; }
inp = domcreate('input',null,'name',name,'placeholder',pl) ;
domset(inp,'style','width:20em;margin-top:2px') ;
if(i!=1) domset(inp,'type',i<2?'email':'password') ;
if(i>=2) domset(inp,'minlength',6) ;
if(i==0) { minp = inp ; inp.oninput = emailvalidate ; }
else if(i==1) { kinp = inp ; inp.oninput = otkvalidate ; }
else if(i==2) { pinp = inp ; inp.oninput = pwdvalidate ; }
else { cinp = inp ; inp.oninput = pwdvalidate ; }
domadd(f,domcreate('div',inp)) ;
}
// add the cancel/submit buttons
btns = btnbox() ;
genbtns() ;
domadd(div,btns,doclink('account',M.accdoc),domcreate('p',M.cookies,'style',
'margin-top:2px;margin-bottom:2px;font-size:90%')) ;
div.onkeyup = function(e)
{ if(e.keyCode==13||e.which==13)
{ e.preventDefault() ; if(valid==7) formresponse() ; unblock() ; }
}
return div ;
// the submit button invokes formresponse which sends the form, and when a
// reply is returned it triggers phpresponse
function loggedin(s)
{ updateacbtns(1) ;
s = s.split('|') ;
speaklang(s[2]) ;
prefs = { email: s[0] ,
instance: parseInt(s[1]) ,
lang: s[2] ,
detail: parseInt(s[3]) ,
optim: parseInt(s[4]) ,
maxsep: parseInt(s[5]) ,
precision: parseInt(s[6]) ,
gpshits: s[7]?s[7].split(' '):[] ,
pixhits: s[8]?s[8].split(' '):[]
}
if(logopt=='blurb'&&filedialogue.box) hlist(filedialogue.box,'load') ;
}
function phpresponse(event)
{ var s=event.target.response,i,d,pos=(logopt=='blurb')?'blurb':9,p='prompt' ;
if(formreq.status!=200) return alerter(M.formerr+formreq.status,pos) ;
else if(s.substring(0,17)=="**sqlerr: already")
return alerter(inject(M.already,acfollowon.email),pos) ;
else if(s.substring(0,18)=="**sqlerr: wrongpwd")
return alerter(M.incorrect,pos) ;
else if(s.substring(0,18)=="**sqlerr: bademail")
return alerter(M.bademail+": "+s.substring(19),pos) ;
else if(s.substring(0,18)=="**sqlerr: badotkey")
return alerter(M.badotkey+": "+s.substring(19),pos) ;
else if(s.substring(0,20)=="**sqlerr: baduser")
return alerter(inject(M.baduser,acfollowon.email),pos) ;
else if(s.substring(0,17)=="**sqlerr: expired")
return alerter(M.expired,pos) ;
else if(s.substring(0,17)=="**sqlerr: toosoon")
return alerter(inject(M.toosoon,acfollowon.email),pos) ;
else if(s.substring(0,10)=="**sqlerr: ")
return alerter(M.dberr+s.substring(9),pos) ;
else if(opt==1||opt==4||opt==5) loggedin(s) ;
else if(opt==0||opt==2)
infowindow.open(acfollowon(opt==0?4:5,logopt),pos) ;
}
function formresponse(ev)
{ var formdata = new FormData(f) ;
formdata.append("option",opt) ;
if(opts[0]==0) acfollowon.email = minp.value ; // pull out email
else formdata.append("email",acfollowon.email) ;
formreq = new XMLHttpRequest() ;
formreq.open("POST","resources/signup.php") ;
formreq.onload = phpresponse ;
formreq.send(formdata) ;
if(ev) ev.preventDefault() ;
} ;
}
/* -------------------------------------------------------------------------- */
function blurbdiv(buscolour)
{ var d, url = document.location.href , i = url.indexOf('?') ;
if(i>=0) url = url.substring(0,i) ;
var img = domcreate('img',null) ;
domset(img,'src',getbus(buscolour,"%23f7e9ce"),'width',16,'height',16) ;
domset(img,'style','margin-top:4px;position:relative;bottom:-2px') ;
var div = domcreate('div',img) ;
domadd(div,domcreate('b','\u00a0\u00a0'+M.rmgps),' '+M.rmdisplays) ;
img = btncanvas(prefs.email?1:0,'black',24) ;
d = domcreate('div',img,'style',
'position:absolute;top:0;right:2px;cursor:pointer') ;
d.onclick = function()
{ if(infowindow.type=='login') infowindow.close() ;
else infowindow.open(acmenu('blurb'),'blurb') ;
} ;
d.title = M.logindoc ;
domadd(body,d) ;
blurbdiv.im = img ;
return div ;
}
/* -------------------------------------------------------------------------- */
function rmhelpdiv(mode)
{ var div=domcreate('div'),d,t,tr,td,a,s,i,lim ;
function addbull(node,item)
{ node.appendChild(domcreate('li',M.funcs[item])) ; }
if(!mode) mode = 0 ;
t = domcreate('table',null,'style','font-size:100%',
'cellpadding',0,'cellspacing',0) ;
if(mode==1)
{ tr = domcreate('tr',buttoncell('eye','settings')) ;
domadd(tr,textcell(M.eyedoc,M.setsave)) ;
domadd(t,tr) ;
tr = domcreate('tr',buttoncell('undo','redo')) ;
domadd(tr,textcell(M.unre)) ;
domadd(t,tr) ;
}
else
{ tr = domcreate('tr',buttoncell('settings','segment','waypoint')) ;
domadd(tr,textcell(M.propsdoc[0],M.propsdoc[1])) ;
domadd(t,tr) ;
tr = domcreate('tr',buttoncell('scissors','pen','camera')) ;
domadd(tr,textcell(M.scisdoc[0],M.scisdoc[1])) ;
domadd(t,tr) ;
tr = domcreate('tr',buttoncell('undo','redo','dl')) ;
domadd(tr,textcell(M.unre+'/'+M.save)) ;
domadd(t,tr) ;
}
/* ------------------------------------------------------------------------ */
if(mode==1) tr = domcreate('tr',buttoncell('dl',-1,prefs.email?1:0)) ;
else if(!mode) tr = domcreate('tr',buttoncell('dl',-1,prefs.email?1:0)) ;
else tr = domcreate('tr',buttoncell(-1,'eye',prefs.email?1:0)) ;
if(mode<0) domadd(tr,textcell(M.logindoc,M.eyedoc)) ;
else domadd(tr,textcell(M.save,M.logindoc)) ;
domadd(t,tr) ;
if(!mode)
{ tr = domcreate('tr',domcreate('td','Keyboard: ','valign','top')) ;
d = domcreate('div',null,'style','margin-bottom:6px') ;
appendrow(d,'←/→ '+M.wpmove) ;
appendrow(d,inject2('[%0 ←]/[%1 →] '+M.segmove,M.shift,M.shift)) ;
appendrow(d,'↓ '+M.centres) ;
appendrow(d,M.spacedoc,1) ;
td = domcreate('td',d) ;
d = domcreate('div',null,'style','margin-bottom:6px') ;
appendrow(d,inject(M.tabdoc,M.dragfor)) ;
appendrow(d,inject(M.stabdoc,M.dragback)) ;
appendrow(d,M.deldoc) ;
appendrow(d,M.sdeldoc) ;
domadd(td,d) ;
domadd(tr,td) ;
domadd(t,tr) ;
tr = domcreate('tr',domcreate('td',M.mouse+' ','valign','top')) ;
td = domcreate('td') ;
appendrow(td,M.clickdoc) ;
appendrow(td,M.sclickdoc) ;
domadd(tr,td) ;
domadd(t,tr) ;
}
domadd(div,underline(domcreate('div',t))) ;
/* ------------------------------------------------------------------------ */
d = domcreate('div') ;
if(mode<0)
{ s = 'https://www.routemaster.app/?track=https://www.masterlyinactivity.com';
a = genlink(s+'/routemaster/routes/capeverde/Caibros.gpx',M.extrack) ;
domadd(d,a,domcreate('br')) ;
a = genlink(s+'/routemaster/routes/capeverde/index.rte',M.exindex) ;
domadd(d,a,domcreate('br')) ;
a = domcreate('span',domcreate('i','Masher')) ;
domadd(a,' '+M.masher) ;
a = domcreate('a',a,'href','resources/masher.html',
'style','cursor:pointer;color:#0000bd;text-decoration:none') ;
domadd(d,a,domcreate('br')) ;
}
a = genlink('https://www.routemaster.app/software/routemaster.html',
M.userman) ;
if(mode>=0) domset(a,'target','_blank') ;
domset(a,'rel','nofollow') ;
domadd(d,a) ;
if(mode>=0) domadd(d,' ',newtabdiv()) ;
else domset(d,'style',
'margin-bottom:2px;border-bottom:solid 1px silver;padding-bottom:2px') ;
domadd(div,d) ;
if(mode<0)
{ d = domcreate('p',M.rmfns,'style','font-weight:bold;margin:3 0') ;
domadd(div,d) ;
d = domcreate('div',null,'style',
'max-width:45%;padding-right:5%;display:inline-block') ;
lim = Math.floor((M.funcs.length+1)/2) ;
for(i=0;i<lim;i++) addbull(d,i) ;
domadd(div,d) ;
d = domcreate('div',null,'style',
'max-width:45%;padding-left:5%;display:inline-block') ;
for(;i<M.funcs.length;i++) addbull(d,i) ;
domadd(div,d) ;
}
return div ;
}
/* -------------------------------------------------------------------------- */
/* THE route props MENU AND THE FUNCTIONS IT GOVERNS */
/* -------------------------------------------------------------------------- */
function genpixlink(gallery,sty)
{ var tag ;
if(gallery.title) tag = '‘' + gallery.title + '’' ; else tag = M.photos ;
tag = inject(M.view,tag) ;
return genlink(gallery.href,tag,sty) ;
}
function (dragopt)
{ var d = domcreate('div') , dd , gallery=getgallery() , legend ;
var starsdiv , maxsep ;
function addloadfactory(ind) { return function() { addload(ind) ; } ; } ;
function dlfactory(ind) { return function(e) { dl(ind,e) ; } ; } ;
function arfactory(ind)
{ if(ind<2) return function() { arfunc(ind) ; }
else return function() { widfunc(ind-2) ; } ;
} ;
function fs(opt)
{ if(opt) return function() { infowindow.close() ; enterfullscreen() ; }
else return function() { infowindow.close() ; exitfullscreen() ; }
}
// dragging
if(dragopt) return domadd(d,M.hitspace,domcreate('br')) ;
if(!segbtn)
{ // function to add: sort routes by date and time (in a tour)
domadd(d,titlediv('title',segments[0].title,1,0)) ;
domadd(d,titlediv('desc',segments[0].desc,1)) ;
if(segments[0].level!=2&&segments[0].index)
domadd(d,genindexlink(segments[0].index,M.viewidx)) ;
if(segments[0].level!=2&&gallery) domadd(d,genindexlink(gallery,M.view)) ;
domadd(d,domcreate('div',
genclickfn(addloadfactory("load"),M.loadnewroute,'br'),
'style','margin-top:2px;border-top:solid 1px silver;padding-top:2px')) ;
if(segments[0].level==1) legend = M.addroute ; else legend = M.addindex ;
domadd(d,genclickfn(addloadfactory("add"),legend,'br')) ;
if(segments[0].level==1)
domadd(d,genclickfn(dlfactory(2),M.savemeta,'br')) ;
domadd(d,genclickfn(refresh,M.bulkupdate,'hr')) ;
if(segments[0].level==1)
{ if(showarrows) domadd(d,genclickfn(arfactory(0),M.hidear,'br')) ;
else domadd(d,genclickfn(arfactory(1),M.showar,'br')) ;
}
domadd(d,genclickfn(arfactory(3),M.thicker),' / ') ;
domadd(d,genclickfn(arfactory(2),M.thinner,'br')) ;
domadd(d,genclickfn(listroutes,M.listroutes,'hr')) ;
if(querycanfullscreen()) // full screen
{ if(queryfullscreen()) domadd(d,genclickfn(fs(0),M.exitfs,'br')) ;
else domadd(d,genclickfn(fs(1),M.enterfs,'br')) ;
}
domadd(d,genclickfn(help,M.help,'br')) ;
return d ;
}
var s,i,unsaved,props,spacing,tdiv,p,rp=routeprops,loadno,pr ;
for(loadno=nactions-1;
loadno>=0&&actions[loadno][0]!='load'&&actions[loadno][0]!='add';
loadno--) ;
if(loadno>=0) pr = actions[loadno][3] ; else pr = null ;
// calculate route properties
p = routestats(segments) ;
// title
dd = domcreate('div',titlediv('title',rp.title,1)) ;
// stars
starsdiv = starsline(rp.stars,1) ;
domset(starsdiv,'style','color:#0000bd') ;
domadd(dd,starsdiv) ;
if(rp.srcid!=null&&rp.srcid!=rp.title)
domadd(dd,titlediv('source',rp.srcid)) ;
// description
domadd(dd,titlediv('desc',rp.desc,rp.origin)) ;
domadd(d,underline(dd)) ;
/* ------------------------------------------------------------------------ */
if(routeprops.info||routeprops.index||gallery) dd = domcreate('div') ;
// index
if(routeprops.index) domadd(dd,genindexlink(routeprops.index,M.viewidx)) ;
// info page
if(routeprops.info) domadd(dd,genindexlink(routeprops.info,M.viewinfo)) ;
// gallery
if(gallery) domadd(dd,genpixlink(gallery,'br')) ;
if(routeprops.info||routeprops.index||gallery) d.appendChild(underline(dd)) ;
/* ------------------------------------------------------------------------ */
// last added route
if(loadno>0)
{ s = '\u00a0\u00a0\u00a0' + M.lastadded + (pr?pr:M.untitled) ;
domadd(d,s,domcreate('br')) ;
s = '\u00a0\u00a0\u00a0' ;
}
else s = '' ;
// are there any missing altitudes?
if(p.nnull&&!getalts.pending)
{ domadd(d,pluralise(M.noalts,p.nnull)+' [') ;
if(!getalts.altthresh||getalts.altthresh>1)
domadd(d,genclickfn(doalts,M.findalts,']br')) ;
else domadd(d,genspan(M.waitalts,']br','text-style:italic')) ;
}
// are points timed and are they in sequence?
if(p.outoforder==0)
{ if(p.ntimes==0) domadd(d,M.notimes,domcreate('br')) ;
else if(p.ntimes<p.npts)
domadd(d,pluralise(M.untimed,p.npts-p.ntimes),domcreate('br')) ;
}
else domadd(d,M.badtimes,domcreate('br')) ;
// labels and photos
if(p.nlabels>0)
{ domadd(d,pluralise(M.numlabels,p.nlabels)+' [')
domadd(d,genclickfn(unlabel,M.remove,']br')) ;
}
if(p.pix.length>0)
domadd(d,pluralise(M.numphotos,p.pix.length),domcreate('br')) ;
/* ------------------------------------------------------------------------ */
// unsaved changes
unsaved = unsavedchanges.length ;
if(unsaved>0) domadd(d,pluralise(M.unsavedchanges,unsaved),domcreate('br')) ;
// number of segments - option to combine
if(segments.length>1)
{ domadd(d,pluralise(M.numsegments,segments.length)+' [') ;
domadd(d,genclickfn(combine,M.combine,']br')) ;
}
// max waypoint separation - option to interpolate
domadd(d,inject(M.maxsep,p.maxsep.toFixed(0)),domcreate('br')) ;
if(prefs&&!prefs.maxsep) maxsep = null ; else maxsep = 100 ;
if(maxsep&&p.maxsep>=maxsep)
{ domadd(d,genspan(M.sepwarn,'br','font-style:italic'),'\u00a0\u00a0\u00a0[');
domadd(d,genclickfn(extrapts,caps(M.interpextra),']br')) ;
}
// operations
var dd = domcreate('div',null,'style',
'margin-top:2px;border-top:solid 1px silver;padding-top:2px;') ;
domadd(dd,genclickfn(addloadfactory("load"),M.loadnewroute,'br')) ;
domadd(dd,genclickfn(addloadfactory("add"),caps(M.loadnewseg),'br')) ;
domadd(dd,genclickfn(addloadfactory("geotag"),M.geotag,'br'),M.saveas,' ') ;
domadd(dd,genclickfn(dlfactory('index'),M.index),'\u2009/\u2009') ;
domadd(dd,genclickfn(dlfactory('tour'),M.tour),'\u2009/\u2009') ;
domadd(dd,genclickfn(dlfactory('uindex'),'u'+M.index,'br')) ;
domadd(d,dd) ;
// full screen
dd = domcreate('div',null,'style',
'margin-top:2px;border-top:solid 1px silver;padding-top:2px;') ;
if(querycanfullscreen())
{ if(queryfullscreen()) domadd(dd,genclickfn(fs(0),M.exitfs,'br')) ;
else domadd(dd,genclickfn(fs(1),M.enterfs,'br')) ;
}
// help menu
domadd(dd,genclickfn(help,M.help,'br')) ;
domadd(d,dd) ;
return d ;
}
function doalts() { infowindow.close() ; getalts(segments,1,drawprofile) ; }
/* -------------------------------------------------------------------------- */
function deltimesfactory(segno,opt)
{ if(opt) return function() { delalts(segno) ; } ;
else return function() { deltimes(segno) ; } ;
} ;
function swapsegfactory(ind) { return function() { swapseg(ind) ; } ; } ;
function seginfodiv(segs,segno)
{ var props=segs[segno],span,prose,div,p,sty,d,div ;
d = domcreate('div',genspan(inject2(M.segment,segs[segno].colour+segno,
segs.length,segs[segno].pts.length),'br')) ;
if(props.title&&props.title!=routeprops.title)
domadd(d,titlediv('title',props.title,1,segno)) ;
if(props.stars&&props.desc&&props.desc!=routeprops.desc)
domadd(d,starsline(props.stars,0)) ;
if(props.srcid&&props.srcid!=props.title&&props.srcid!=routeprops.srcid)
domadd(d,textdiv(M.source,props.srcid)) ;
if(props.desc&&props.desc!=routeprops.desc)
domadd(d,titlediv('desc',props.desc,null,segno)) ; // no links
if(props.stats) domadd(d,prettystats(M.stats,props.stats)) ;
// number of track points and optimisation
function optimfactory(segno) { return function() { optimwork(segno) ; } ; } ;
sty = 'display:block;margin-bottom:2px;'+
'border-bottom:solid 1px silver;padding-bottom:2px' ;
if(props.desc&&!props.stats) sty += ';margin-top:2px;'+
'border-top:solid 1px silver;padding-top:2px' ;
div = domcreate('div',null,'style',sty) ;
if(props.optim&&props.optim.already) domadd(div,M.prevopt,domcreate('br')) ;
else if(props.optim) domadd(d,M.optimised,domcreate('br')) ;
else domadd(div,genclickfn(optimfactory(segno),M.optimise,'br')) ;
domadd(d,div) ;
div = domcreate('div',d) ;
p = routestats([segs[segno]]) ;
d = genldiv(p) ;
domset(d,'style','display:block') ;
domadd(div,d) ;
d = domcreate('div',null,'style','display:block;clear:left;margin-top:2px;'+
'border-top:solid 1px silver;padding-top:2px') ;
if(segs.length>1) domadd(d,genclickfn(discard,caps(M.deletesegment),'br')) ;
else domadd(d,genspan(caps(M.deletesegment),'br','color:silver')) ;
if(segs[segno].pts.length)
{ domadd(d,genclickfn(revseg,caps(M.revsegment),'br')) ;
domadd(d,genclickfn(dupseg,caps(M.dupsegment),'br')) ;
}
if(segno>0)
{ domadd(d,genclickfn(swapsegfactory(segno-1),
inject2(M.swapwith,segs[segno-1].colour,M.preceding),'br')) ;
if(segno<segs.length-1) prose = 'br' ; else prose = 'hr' ;
domadd(d,genclickfn(combineb,
inject2(M.combinewith,segs[segno-1].colour,M.preceding),prose)) ;
}
if(segno<segs.length-1)
{ domadd(d,genclickfn(swapsegfactory(segno),
inject2(M.swapwith,segs[segno+1].colour,M.following),'br')) ;
domadd(d,genclickfn(combinef,
inject2(M.combinewith,segs[segno+1].colour,M.following),'hr')) ;
}
if(p.ntimes)
domadd(d,genclickfn(deltimesfactory(segno,0),caps(M.deltimes),'br')) ;
else domadd(d,genspan(M.nodeltimes,'br','color:silver')) ;
if(getalts.reqlist&&getalts.reqlist.length)
domadd(d,genspan('['+M.waitalts+']','br','color:grey')) ;
else if(p.nnull==segs[segno].pts.length)
domadd(d,genclickfn(googlecal,M.askgoogle,'br')) ;
else domadd(d,genclickfn(altinfo,M.adjalts,'br')) ;
domadd(div,d) ;
return div ;
}
/* -------------------------------------------------------------------------- */
function altinfodiv()
{ var d = domcreate('div',genclickfn(googlereg,M.regress,'br')) ;
domadd(d,genclickfn(googleadd,M.googleadd,'br')) ;
domadd(d,genclickfn(googlecal,M.repalts,'br')) ;
domadd(d,genspan('['+M.vsgoogle+']','br',
'font-size:80%;margin-left:8px;color:grey')) ;
domadd(d,genclickfn(manualcal,M.offalts,'br')) ;
domadd(d,genclickfn(deltimesfactory(selected.segno,1),caps(M.delalts),'br')) ;
domadd(d,doclink('adjusting',M.docalts)) ;
return d ;
}
function doclink(target,string,flag)
{ var a = genlink('https://www.routemaster.app/software/routemaster.html#'+
target,string) ;
domset(a,'target','_blank') ;
var dd = domcreate('div',a) ;
domadd(dd,' ',newtabdiv()) ;
if(!flag) domset(dd,'style',
'margin-top:2px;border-top:solid 1px silver;padding-top:2px;') ;
return dd ;
}
/* -------------------------------------------------------------------------- */
function walktodiv(pt)
{ var s,d=domcreate('div'),dd,ind,k,imgname,imgdiv ;
if(pt.label)
{ dd = domcreate('div',domcreate('b',pt.label)) ;
domadd(dd,pt.caption?(': '+pt.caption+' ['):' [') ;
domadd(dd,genclickfn(function(){waymarkprompt(1);},M.edit,']br')) ;
domadd(d,dd) ;
if(pt.desc) domadd(d,titlediv('ptdesc',pt.desc,1,selected.segno)) ;
}
for(ind=0;ind<pt.photo.length;ind++)
{ dd = domcreate('div') ;
if(ind==0&&(pt.label||pt.desc)) dd.setAttribute('style',
'margin-top:2px;border-top:solid 1px silver;padding-top:2px') ;
if(imginfo.status!='ready') k = null ; else k = findimg(pt.photo[ind]) ;
function displayfactory(ind) { return function() { display(ind) ; } ; } ;
if(k)
{ s = genimage(imginfo.sect[k[0]].list[k[1]],-1) ;
doim(s) ;
imgdiv = domcreate('div',s.img,'style','position:relative;cursor:'+
'pointer;width:'+s.shape[0]+';height:'+s.shape[1]) ;
if(imginfo.sect[k[0]].list[k[1]].extn=='.mp4')
domadd(imgdiv,mp4icon()) ;
imgdiv.onclick = displayfactory(ind) ;
domadd(dd,imgdiv,domcreate('br')) ;
}
else
{ s = M.photo + ' ' + pt.photo[ind] + ' (' ;
if(!imginfo.status||imginfo.status=='nolist') s += M.nolist ;
else if(imginfo.status=='ready')
s += inject(M.notpres,shortenname(imginfo.uri)) ;
else if(imginfo.status=='waiting') s += inject(M.notavail,imgname) ;
else s += 'imginfo.status = ' + imginfo.status ;
domadd(dd,domcreate('nobr',s+') ')) ;
}
function pheditfactory(k) { return function() { photoedit(k) ; } ; } ;
domadd(dd,'[',genclickfn(pheditfactory(ind),M.edit),']') ;
if(k)
{ function phinfofactory(k) { return function() { phinfo(k) ; } ; } ;
domadd(dd,' : [',genclickfn(phinfofactory(k),M.info),']') ;
}
domadd(d,underline(dd)) ;
}
if(pt.photo.length>0)
{ dd = domcreate('div','[') ;
function photoprompter() { photoprompt(1) ; }
domadd(dd,genclickfn(photoprompter,caps(M.addphoto),']br')) ;
domadd(d,dd) ;
}
return d ;
}
/* -------------------------------------------------------------------------- */
function ()
{ var ncol,nrow,seg,len,bkg,s,sno,r,sortl,gotroutes,ind,route,maxasc ;
var t,tr,td,i,j,k,l,item,stats,maxlen,minlen,thresh ;
var csty = 'padding:2px 4px;white-space:nowrap;font-size:80%;' ;
var base = segments[0] , n = base.pts.length ;
if(segments[0].type=='tour') ncol = 1 ;
else ncol = Math.floor(Math.sqrt(n/4.5)) ;
t = domcreate('table',null,'cellspacing',0,'cellpadding',0) ;
if(ncol<1) ncol = 1 ;
nrow = Math.floor((n+ncol-1)/ncol) ;
for(sortl=new Array(n),maxasc=gotroutes=i=0;i<n;i++)
{ route = base.pts[i] ;
if(route.level>0) gotroutes = 1 ;
sortl[i] = { ind:i , title: route.title } ;
if(route.stats[2]>maxasc) maxasc = route.stats[2] ;
if(i==0||route.stats[1]>maxlen) maxlen = route.stats[1] ;
if(i==0||route.stats[1]<minlen) minlen = route.stats[1] ;
}
if(segments[0].type!='tour')
sortl.sort(function(a,b) { return a.title>b.title ; }) ;
if(maxlen<9999) thresh = 10000 ; // always print dist in m if all routes short
else thresh = 2000 ; // only print in m for short routes if there are long 1s
for(i=0;i<nrow;i++)
{ tr = domcreate('tr') ;
for(j=0;j<ncol&&(k=j+i*ncol)<n;j++)
{ ind = sortl[k].ind ;
route = base.pts[ind] ;
if(j) domadd(tr,domcreate('td','\u00a0\u00a0\u00a0\u00a0',
'style','width:18px')) ;
if(route.pts.length>1&&route.shades.length)
bkg = '-image: linear-gradient(to bottom right,' +
route.shades[0] + ', ' + route.shades[1] + ')' ;
else bkg = ':' + route.colour ;
domadd(tr,domcreate('td','\u00a0\u00a0\u00a0\u00a0',
'style',csty+'width:14px;background'+bkg)) ;
function highfactory(ind)
{ return function()
{ infowindow.close() ; highlight(segments[0].pts,ind) ; } ;
} ;
domadd(tr,domcreate('td',genclickfn(highfactory(ind),route.title,0,
csty+'white-space:nowrap'))) ;
function addcell(tr,s)
{ domadd(tr,domcreate('td',s,'style',csty+'text-align:right')) ; }
// stars
for(s='',sno=0;sno<route.stars;sno++) s += ' ★' ;
addcell(tr,s) ;
// number of routes
if(gotroutes) for(l=0;l<2;l++) // loop over 2 cols
{ if(route.level>0) s = l?M.routes:route.pts.length ; else s = ' ' ;
addcell(tr,s) ;
}
// distance, ascent
if(route.stats[1]>thresh)
addcell(tr,prettynum(route.stats[1]/1000,0,'km')) ;
else addcell(tr,prettynum(route.stats[1],0,'m')) ;
addcell(tr,prettynum(route.stats[2],maxasc>9999,'m')) ;
// date
if(segments[0].type=='tour'&&route.date) addcell(tr,route.date) ;
}
domadd(t,tr) ;
}
return t ;
}
/* -------------------------------------------------------------------------- */
function listroutes()
{ var index = segments[0] , n=index.pts.length,props,s='<a href="',sortl,i,l ;
for(sortl=new Array(n),i=0;i<n;i++)
sortl[i] = { ind:i , title: index.pts[i].title } ;
sortl.sort(function(a,b) { return a.title>b.title ; }) ;
s += trackref(routeprops.origin,null,1) + '&mode=z">index</a>\n' ;
for(i=0;i<n;i++)
{ props = index.pts[sortl[i].ind] ;
if(props.ttype=='html') l = props.tlink ;
else l = trackref(routeprops.origin,props.tlink,1) + '&mode=z' ;
s += ' : <a href="' + l + '">' + props.title + '</a>' ;
}
s += '\n' ;
saveAs(new Blob([s],{type: "text/plain;charset=utf-8"}),'list.html') ;
}
/* -------------------------------------------------------------------------- */
/* WPINFO IS A MENU GIVING ACCESS TO THE SETALT FUNCTION */
/* -------------------------------------------------------------------------- */
function wpinfodiv(precision)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type?selected.type:null ;
var alt,s,x,lat,lng,time,c,ctx,grad,i,h,hh,tdist,tasc,cspan ;
var pts=s2?segments[s0].geo:segments[s0].pts , pt = pts[s1] ;
var pos = pt.pos , npt=null , step , dd ;
var s , dt , dh , nalt = null , d , url = document.location.href ;
i = url.indexOf('?') ;
if(i>=0) url = url.substring(0,i) ;
if(!s2&&s1<segments[s0].pts.length-1)
{ npt = segments[s0].pts[s1+1] ; step = pt.delta ; nalt = npt.h ; }
if(!s2&&s1) for(h=pts[0].h,tasc=(h==null?null:0),tdist=i=0;i<s1;i++,h=hh)
{ tdist += pts[i].delta ;
if(tasc!=null)
{ if((hh=pts[i+1].h)==null) tasc = null ; else if(hh>h) tasc += hh - h ; }
}
// position
lat = pos.lat ;
lng = pos.lng ;
if(lat>=0) s = lat.toFixed(5+precision) + inject('° %, ',M.nsew.charAt(0)) ;
else s = (-lat).toFixed(5+precision) + inject('° %, ',M.nsew.charAt(1)) ;
if(lng>=0) s += lng.toFixed(5+precision) + inject('° %',M.nsew.charAt(2)) ;
else s += (-lng).toFixed(5+precision) + inject('° %',M.nsew.charAt(3)) ;
function copyfactory(s)
{ return function()
{ navigator.clipboard.writeText(s) ;
cspan.replaceWith(document.createTextNode(M.copied)) ;
} ;
}
d = domcreate('nobr',s+'.') ;
cspan = genclickfn(copyfactory(s),M.copy) ;
d = domcreate('div',domadd(d,' [',cspan,']')) ;
x = new LatLon(lat,lng) ;
if(lat>49.9&&lat<62&&lng>-12&&lng<2.5&&lat-1.5*lng<75)
{ s = 10 + 2 * Math.floor((precision+1)/2.0) ;
s = M.osref + ': ' + OsGridRef.latLonToOsGrid(x).toString(s) ;
}
else s = M.utm + ': ' + x.toUtm(precision) ;
domadd(d,domcreate('br'),domcreate('nobr',s),domcreate('br')) ;
// altitude
alt = pt.h ;
function altfactory(parm)
{ return function() { setalt(parm,prefs.precision) ; } ; } ;
if(alt!=null)
{ domadd(d,caps(M.alti)+': '+alt.toFixed(precision) + 'm [') ;
domadd(d,genclickfn(altfactory(1),M.edit,']br')) ;
}
else domadd(d,genclickfn(altfactory(0),caps(M.wpalt),'br')) ;
// date and time
time = pt.t ;
if(time!=null&&time.getFullYear()>1980)
{ domadd(d,M.date+': '+time.toDateString(),domcreate('br')) ;
s = M.time + ': ' + time.toTimeString() ;
domadd(d,domcreate('nobr',s),domcreate('br')) ;
}
// marker
if(pt.label)
{ s = pt.label ;
if(pt.labelmarker.title) s += ': ' + pt.labelmarker.title ;
domadd(d,s+' [',genclickfn(waymarkprompt,M.edit,']br')) ;
}
// gradient + speed
if(!s2)
{ grad = '' ;
if(npt&&pt.t&&npt.t) dt = npt.t.getTime() - time.getTime() ;
else dt = null ;
if(alt!=null&&nalt!=null) dh = nalt - alt ; else dh = null ;
if(dh!=null&&step>0)
{ c = newcanvas(12,12) , ctx = c.getContext("2d") ;
ctx.fillStyle = segments[s0].colour ;
ctx.strokeWidth = 0 ;
ctx.beginPath() ;
if(4*Math.abs(alt-nalt)<step)
{ ctx.moveTo(0,6*(1+4*dh/step)) ;
ctx.lineTo(12,6*(1-4*dh/step)) ;
ctx.lineTo(12,12) ;
ctx.lineTo(0,12) ;
}
else
{ ctx.moveTo(6*(1-step/(4*dh)),12) ;
ctx.lineTo(6*(1+step/(4*dh)),0) ;
if(dh>0) { ctx.lineTo(12,0) ; ctx.lineTo(12,12) ; }
else { ctx.lineTo(0,0) ; ctx.lineTo(0,12) ; }
}
ctx.fill() ;
grad = (100*dh/step).toFixed(0) + '%' ;
}
if(grad!=''||dt>0)
{ s = domcreate('div',grad,'style','padding-right:5px;'+
'display:inline-block;width:34px;text-align:right') ;
dd = domcreate('div',s) ;
if(grad!='') domadd(dd,c) ;
if(npt&&pt.t&&npt.t) if((x=npt.t.getTime()-time.getTime())>0)
domadd(dd,' '+(3600*step/x).toFixed(1)+M.kmh) ;
domadd(dd,' (→'+step.toFixed(precision)+'m)') ;
domadd(d,dd) ;
}
// distance and ascent to point
if(s1&&tdist>0)
{ s = domcreate('span',kmspan(tdist)) ;
if(tasc!=null)
{ domadd(s,genspan(', ↑',null,'padding-right:2px')) ;
domadd(s,prettynum(0.5+tasc,tasc>10000)) ;
domadd(s,genspan('m',null,'padding-left:2px')) ;
}
domadd(d,s) ;
}
// segment + point number
if(segments.length>1) s = inject(M.segpt,s0) + ' ' + inject(M.point,s1) ;
else s = caps(inject(M.point,s1)) ;
domadd(d,genspan(s,'hr','font-size:80%')) ;
}
else if(segments.length>1)
domadd(d,genspan(inject(M.segpt,s0),'hr','font-size:80%')) ;
// edit functions (delete, drag, transfer... )
if(segments[s0].pts.length+segments[s0].geo.length>1)
domadd(d,genclickfn(wpfactory(null),caps(M.wpdel),'br')) ;
else domadd(d,genspan(caps(M.wpdel),'br','color:silver')) ;
domadd(d,genclickfn(draggit,M.mkdrag,'br')) ;
function wpfactory(ind)
{ if(ind) return function() { inswp(ind) ; } ;
else if(ind==0) return function() { wpdel(1) ; } ;
else return function() { wpdel() ; } ;
} ;
if(!s2)
{ domadd(d,genclickfn(wpfactory(1),inject(M.dragaway,M.dragfor),'br')) ;
domadd(d,genclickfn(wpfactory(-1),inject(M.dragaway,M.dragback),'br'));
}
domadd(d,genclickfn(wpfactory(0),caps(s2?M.attach:M.detach),'br')) ;
if(url.substring(url.length-8)=='test.php')
domadd(d,genclickfn(function(){console.log(pt);},'Log to console','br')) ;
return d ;
}
function kmspan(val)
{ var s , pad = 'padding-right:2px' ;
if(val>4999)
{ s = domcreate('span',diststring(val)) ;
domadd(s,domcreate('span','km','style','padding-left:2px')) ;
}
else s = domcreate('span',prettynum(val+0.5,1,'m')) ;
return s ;
}
/* -------------------------------------------------------------------------- */
function titlediv(opt,val,editable,segno)
{ var s,d=domcreate('div'),i,maxl,len,origin,f,k ;
var flag = (infowindow.close()=='wpinfo') ;
if(segno==undefined) segno = null ;
function titlefactory(p1,p2)
{ if(p1=='ptdesc')
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
var pt=s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
return function()
{ var menu = waymarkdiv(pt,0) ;
freeze() ;
infowindow.open(menu[0],4,1,unfreeze) ;
menu[1].focus() ;
} ;
}
else return function() { infowindow.close() ; retitle(p1,p2) ; } ;
} ;
if(!val)
{ if(!editable) return d ;
if(opt=='desc') s = M.adddesc ; else if(opt=='title') s = M.addtitle ;
domadd(d,'[',genclickfn(titlefactory(opt,segno),s),']') ;
return d ;
}
if(opt=='title') domadd(d,domcreate('b',M.title+': '+val)) ;
else if(opt=='desc'||opt=='ptdesc')
{ if(segno==null&&!segments[0].level) origin = routeprops.origin ;
else if(segno==null) origin = segments[0].origin
else origin = segments[segno].origin ;
if(editable) f = titlefactory(opt,segno) ; else f = null ;
maxl = parsedesc(d,'<b>'+M.desc+':</b> '+val,f,origin) ;
}
else if(opt=='source') domadd(d,domcreate('b',M.source+': '),val) ;
else return alerter(inject2(M.illegalopt,opt,'titlediv'),0) ;
if((opt!='desc'&&opt!='ptdesc')||maxl<40)
domset(d,'style','white-space:nowrap') ;
else if(opt=='ptdesc')
{ k = Math.min( 30*Math.sqrt(maxl) , 0.6*window.innerWidth ) ;
if(k>50) domset(d,'style','max-width:'+Math.floor(k+0.5)+'px') ;
}
if(editable&&(opt!='desc'&&opt!='ptdesc'))
{ s = domcreate('span','[','style','margin-left:1ex') ;
domadd(s,genclickfn(titlefactory(opt,segno),M.edit),']') ;
domadd(d,s) ;
}
return d ;
}
/* -------------------------------------------------------------------------- */
function genclickfn(action,legend,bropt,style)
{ var defstyle = 'cursor:pointer;color:#0000bd' ;
if(style) defstyle += ';' + style ;
var span = genspan(legend,bropt,defstyle) ;
span.onclick = function() { action() ; } ; // onclick = action passes e
return span ;
}
function genspan(legend,bropt,spanstyle)
{ var span = domcreate(bropt=='hr'?'div':'span'),s,ind=-1 ;
if(legend) ind = legend.search('#') ;
if(ind<0) domadd(span,legend) ;
else // a colour name (this should’t be in genspan)
{ domadd(span,legend.substring(0,ind)+' ') ;
domadd(span,domcreate('span','\u00a0\u00a0\u00a0\u00a0',
'style','background-color:'+legend.substring(ind,ind+7))) ;
domadd(span,' '+legend.substring(ind+7)) ;
}
if(spanstyle==undefined) spanstyle = '' ;
else if(spanstyle!='') spanstyle += ';' ;
if(bropt==']br') { domadd(span,']') ; bropt = 'br' ; }
if(bropt=='br') domadd(span,domcreate('br')) ;
else if(bropt=='hr') spanstyle +=
'margin-bottom:2px;border-bottom:solid 1px silver;padding-bottom:2px' ;
if(spanstyle!='') domset(span,'style',spanstyle) ;
return span ;
}
/* -------------------------------------------------------------------------- */
function speaklang(lang)
{ if(prefs.lang!=lang)
{ var script = domcreate('script') ;
script.src = 'resources/rm.' + lang + '.js' ;
document.head.appendChild(script) ;
}
prefs.lang = lang ;
}
function freeze() { dragging = 2 ; keyhandler = null ; bulkout(0,1) ; }
function unfreeze() { keyhandler = keystroke ; dragging = 0 ; bulkout(1,1) ; }
function unblock() { unfreeze() ; infowindow.close() ; }
function waitprompt(msg,pos)
{ infowindow.close() ;
var div = domcreate('div',msg) ;
var inp = domcreate('button',M.ok,'type','submit','style',btnstyle(1)) ;
inp.onclick = unfreeze ;
domadd(div,btnbox(inp)) ;
freeze() ;
infowindow.open(div,pos?pos:0,null,unfreeze) ;
}
/* -------------------------------------------------------------------------- */
function prefsprompt(msg,oldval,action,pos)
{ var w,h,l,t,defw,defh,i,box,s,img,div,ip,span,style,c,d,i,btns,inp ;
var ctx , cty , detail,auto,precdiv ;
var canvasdiv , lang , langs=['en','fr'] , flagdivs = [0,0] ;
var tab,tr,td,lsty,email,pclick,deffn ;
var unflag = 'data:image/gif;base64,R0lGODlhJAAYAKIAAEGP3jqL3T6O3lec4m2\n' +
'p5pK/7LnW8/X5/SwAAAAAJAAYAAADqgi63P4wykmrvUvojaVWQ0iEihB\n' +
'0TDAUQDgQI1kMJxoQRhG/Y/4ahNpFMDAAeUjD4WcYCDCBgk9EJRRY0oK\n' +
'wEj2KckbdVzsESGEw5fVw0KVnz4txhMsqDzGcARC3nO9oai56fENZVgd\n' +
'7d15Se1B6I2JSiXRzWxRPjTs/lUaFUEVzLgMtek2XFipGOVetq0EoCyq\n' +
'NQKYzqB1PIgEqJH2xGRo1vB/AxsfIyRIJADs=' ;
var units = ['m','dm','cm','mm'] , unitdivs = [0,0,0,0] ;
div = domcreate('div',domcreate('p',msg,'style','margin:0')) ;
ip = domcreate('div') ;
// email address
email = domcreate('input',domcreate('div',email),'type','email',
'value',prefs.email,'style','width:350px') ;
lsty = 'margin:auto;border-bottom:1px solid silver;padding-bottom:2px;' ;
tab = domcreate('table',null,'style',lsty,'cellpadding',0,'cellspacing',0) ;
// 'Language'
lsty = 'text-align:right;padding:2px 12px 2px 0' ;
tr = domcreate('tr',domcreate('td',M.language,'style',lsty)) ;
// flags
if(prefs&&prefs.lang) lang = langs.indexOf(prefs.lang) ;
else lang = 0 ;
if(lang<0) lang = 0 ;
function flagcanvas(i)
{ var c = domcreate('canvas',null,'title',langs[i]) ;
var csty = "width:18px;height:12px;cursor:pointer;border:2px solid " ;
if(i==lang) csty += "grey" ; else csty += "white" ;
c.width = 36 ;
c.height = 24 ;
c.setAttribute('style',csty) ;
c.onclick = function() { cclick(i) ; }
return flagdivs[i] = c ;
}
// english
c = flagcanvas(0) ;
ctx = c.getContext("2d") ;
img = new Image(36,24) ;
img.onload = function() { ctx.drawImage(img,0,0) ; } ;
img.src = unflag ;
canvasdiv = domcreate('div',c,'style','position:relative') ;
// french
c = flagcanvas(1) ;
cty = c.getContext("2d") ;
cty.strokeWidth = 0 ;
for(i=0;i<3;i++) // tricolore
{ cty.fillStyle = ["#0055A4","white","#EF4135"][i] ;
cty.beginPath() ;
cty.rect(12*i,0,12,24) ;
cty.fill() ;
}
domadd(canvasdiv,c) ;
function cclick(langno)
{ if(langno==lang) return ;
flagdivs[lang].style.borderColor = 'white' ;
lang = langno ;
flagdivs[lang].style.borderColor = 'grey' ;
}
domadd(tr,domcreate('td',canvasdiv)) ;
domadd(tab,tr) ;
// optimisation options: detail
tr = domcreate('tr',domcreate('td',
domcreate('label',M.detail+':','for','detail'),'style',lsty)) ;
detail = domcreate('input',null,'type','number','min','1','max','100') ;
domset(detail,'id','detail','style','width:52px','value',
(prefs&&prefs.detail)?prefs.detail:defprefs.detail) ;
domadd(tr,domcreate('td',detail)) ;
domadd(tab,tr) ;
// auto optimise?
tr = domcreate('tr',domcreate('td',
domcreate('label',M.optonload,'for','auto'),'style',lsty)) ;
auto = domcreate('input',null,'type','checkbox','id','auto') ;
if(!prefs||prefs.optim) domset(auto,'checked',true) ;
domadd(tr,domcreate('td',auto)) ;
domadd(tab,tr) ;
// limit separation?
tr = domcreate('tr',domcreate('td',
domcreate('label',M.limsep,'for','limsep'),'style',lsty)) ;
box = domcreate('input',null,'type','checkbox','id','limsep') ;
if(!prefs||prefs.maxsep) domset(box,'checked',true) ;
domadd(tr,domcreate('td',box)) ;
domadd(tab,tr) ;
// track precision
if(prefs.precision&&prefs.precision>0&&prefs.precision<units.length)
prefsprompt.precision = prefs.precision ;
else prefsprompt.precision = 0 ;
tr = domcreate('tr',domcreate('td',M.precision,'style',lsty)) ;
precdiv = domcreate('div',null,'style','position:relative;cursor:pointer') ;
// this would be simpler if each box had its own onclick
for(i=0;i<units.length;i++)
{ unitdivs[i] = s = domcreate('div',units[i],'style',
'display:inline-block;text-align:center;width:29px;' +
'left:'+(31*i)+'px;overflow:hidden;border:2px solid') ;
s.style.borderColor = (i==prefsprompt.precision?'grey':'white') ;
domadd(precdiv,s) ;
}
pclick = pclickfactory(precdiv,unitdivs) ;
precdiv.onclick = pclick ;
domadd(tr,domcreate('td',precdiv)) ;
domadd(tab,tr) ;
domadd(ip,tab) ;
domadd(div,ip) ;
ip.onkeydown = function(e)
{ if(e.keyCode==13||e.which==13) { e.preventDefault() ; submit() ; } }
/* ------------------------------------------------------------------------ */
function pclickfactory(precdiv,unitdivs)
{ return function(e)
{ var i,prec ;
if(e) i = e.clientX - precdiv.getBoundingClientRect().left ; else i = 0 ;
prec = Math.floor(i/31) ;
if(prec==prefsprompt.precision) return ;
unitdivs[prefsprompt.precision].style.borderColor = 'white' ;
prefsprompt.precision = prec ;
unitdivs[prec].style.borderColor = 'grey' ;
} ;
}
// handle submit/return key
function submit()
{ unblock() ;
var xhttp = new XMLHttpRequest() ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ if(xhttp.response.length==0) alerter(M.dberr,pos) ;
else if(xhttp.response=="**sqlerr: bademail") eralert(M.bademail,pos) ;
else if(xhttp.response=="**sqlerr: already")
alerter(inject(M.already,email.value),pos) ;
else if(xhttp.response.substring(0,9)=="**sqlerr:")
alerter(xhttp.response,pos) ;
else prefs.email = email.value ;
}
}
if(lang<0||lang>=langs.length) lang = 0 ;
speaklang(langs[lang]) ;
prefs.detail = detail.value ;
prefs.optim = auto.checked?1:0 ;
prefs.maxsep = box.checked?100:0 ;
prefs.precision = prefsprompt.precision ;
xhttp.open("GET","resources/signup.php?opts="+email.value+'&'+
prefs.lang+'&'+prefs.detail+'&'+prefs.optim+'&'+
prefs.maxsep+'&'+prefs.precision) ;
xhttp.send() ;
}
function defaulter()
{ unblock() ;
auto.checked = defprefs.optim ;
box.checked = defprefs.maxsep ;
detail.value = defprefs.detail ;
lang = langs.indexOf(defprefs.lang) ;
submit() ;
}
/* ------------------------------------------------------------------------ */
// cancel button
inp = domcreate('button',caps(M.cancel),'style',btnstyle()) ;
inp.onclick = unblock ;
btns = btnbox(inp) ;
// defaults button
inp = domcreate('button',caps(M.defaults),'style',btnstyle()) ;
inp.onclick = defaulter ;
domadd(btns,inp) ;
// submit button
inp = domcreate('button',M.submit,'type','submit','style',btnstyle(1)) ;
inp.onclick = submit ;
domadd(btns,inp) ;
domadd(div,btns) ;
freeze() ;
infowindow.open(div,pos,1,unfreeze) ;
}
/* -------------------------------------------------------------------------- */
function waymarkdiv(pt,flag)
{ var labelno,textbox=null,labno,oldlabno,div,ip,span,button,d,tr,row ;
var labname,marg,k,btn,btns,icon,icondiv ;
var nlabel = waymarks.length , btnlist = new Array(nlabel) ;
var oldcaption = pt.caption , oldlabel=pt.label , olddesc = pt.desc ;
for(labno=nlabel-1;labno>=0&&waymarks[labno].name!=oldlabel;labno--) ;
if(labno>=0) oldlabno = labno ; else { labno = 0 ; oldlabno = -1 ; }
/* ------------------------------------------------------------------------ */
// generate buttons for labels
function drawlabel(labelno,chosen)
{ var w = waymarks[labelno] ;
var x = (w.xoffs||w.xoffs==0)?w.xoffs:12-w.anchor[0] ;
var icon = waymarkcanvas(labelno,'black','grey',1.5,x) ;
var div = domcreate('div',null,'style',
'width:39px;height:39px;position:relative') ;
domadd(div,domcreate('div',null,'style','width:36px;height:36px;' +
'position:absolute;top:1px;left:1px;' +
'border-right:3px solid darkgray;' +
'border-bottom:3px solid darkgray;'+
'border-radius:4px')) ;
icon = domcreate('div',icon,'style','width:36px;height:36px;' +
'border:1px solid black;border-radius:4px;' +
'position:absolute;top:0;left:0;' +
'background:'+(chosen?'#e2e2e2':'white')) ;
domadd(div,icon) ;
return { div , icon } ;
}
function iconsel(labelind)
{ if(labelind==labno) return ;
while(labname.childNodes.length)
labname.removeChild(labname.childNodes[0]) ;
domadd(labname,M.labels[labelind]) ;
var newicon = waymarkcanvas(labelind,'black','grey') ;
icondiv.replaceChild(newicon,icon) ;
icon = newicon ;
btnlist[labno].style.background = 'white' ;
btnlist[labelind].style.background = '#e2e2e2' ;
labno = labelind ;
ip.focus() ;
ip.setSelectionRange(ip.value.length,ip.value.length) ;
}
function iconfactory(labelno) { return function() { iconsel(labelno) ; } }
// handle submit/return key
function submitfn(delopt,ip)
{ return function()
{ unblock() ;
var caption ;
if(delopt) labno = caption = null ;
else if(ip.value) caption = ip.value ;
else caption = '' ;
editlabel(caption,labno,textbox?textbox.value:null,
oldcaption,oldlabno,olddesc,flag) ;
}
}
/* ------------------------------------------------------------------------ */
// “Modify or delete waymark”
div = domcreate('div',pt.label?M.modlabel:M.enterlabel) ;
domadd(div,domcreate('br')) ;
icon = waymarkcanvas(labno,'black','grey') ;
icondiv = domcreate('div',icon,'style',
'width:22px;height:22px;padding-right:2px') ;
tr = domcreate('tr',icondiv) ;
ip = domcreate('input',null,'value',oldcaption,
'style','width:200px;height:24px') ;
domadd(tr,domcreate('td',ip)) ;
labname = domcreate('span',M.labels[labno]) ;
domadd(tr,domcreate('td',labname,'style','padding-left:4px')) ;
domadd(div,domcreate('table',tr,'cellspacing',0,'cellpadding',0,
'style','margin-bottom:8px;margin-top:4px')) ;
for(labelno=0;labelno<nlabel;labelno++,k++)
{ if(labelno==0||labelno==nlabel/2)
{ k = marg = 0 ;
row = domcreate('div',null,'style','white-space:nowrap') ;
}
else if(k==1||k==5||k==6) marg = 8 ;
else if(labelno) marg = 2 ;
button = domcreate('div',null,'title',M.labels[labelno],'style',
'width:42px;height:45px;display:inline-block;' +
'margin-left:'+marg+'px') ;
d = drawlabel(labelno,labelno==labno) ;
btnlist[labelno] = d.icon ;
domadd(button,d.div) ;
button.onclick = iconfactory(labelno) ;
domadd(row,button) ;
if(labelno==nlabel/2-1||labelno==nlabel-1) domadd(div,row) ;
}
/* ------------------------------------------------------------------------ */
function genbox()
{ var d = domcreate('div',domcreate('div',caps(M.editdesc),
'style','padding-bottom:4px')) ;
var w = 41*(nlabel/2)+20 ;
textbox = domcreate('textarea',olddesc?olddesc:null,'style','width:'+
(w-8)+'px;height:200px;font-size:100%;font-family:Menlo,helvetica') ;
domadd(d,textbox) ;
return [d,textbox] ;
}
function addbox()
{ var box = genbox() ;
d.replaceWith(box[0]) ;
domadd(d,doclink('desc',M.descdoc,1)) ; // flag says no overline
infowindow.reposition() ;
box[1].focus() ;
}
if(olddesc) d = genbox()[0] ;
else d = domcreate('div',genclickfn(addbox,M.adddesc),
'style','height:13px;padding-bottom:4px') ;
domadd(div,d) ;
/* ------------------------------------------------------------------------ */
// cancel
btn = domcreate('input',null,'type','button','value',caps(M.cancel),
'style',btnstyle()) ;
btn.onclick = unblock ;
btns = btnbox(btn) ;
// delete
if(oldlabel)
{ btn = domcreate('input',null,'type','button','value',caps(M.del),
'style',btnstyle()) ;
btn.onclick = submitfn(1,ip) ;
domadd(btns,btn) ;
}
// submit
btn = domcreate('input',null,'type','button','value',caps(M.ok),
'style',btnstyle(1)) ;
btn.onclick = submitfn(0,ip) ;
domadd(btns,btn) ;
ip.onkeydown = function(e) { if(e.keyCode==13) submitfn(0,ip)() ; }
domadd(div,btns) ;
// doc link
domadd(div,domcreate('div',doclink('labels',M.labeldoc))) ;
if(olddesc) domadd(div,doclink('desc',M.descdoc,1)) ;
if(oldcaption&&oldcaption.length) ip.setSelectionRange(0,oldcaption.length) ;
return [div,ip] ;
}
/* -------------------------------------------------------------------------- */
function genldiv(p)
{ var tdiv = domcreate('div') ;
function tdivadd(a,s)
{ var ldiv = domcreate('div',null,'style','float:left;'+s) , i ;
for(i=0;i<5;i++) { domadd(ldiv,a[i],domcreate('br')) ; }
domadd(tdiv,ldiv) ;
}
tdivadd(M.tabvals,'padding-right:8px') ;
function intise(x)
{ if(x==null||x==undefined) return '–' ; else return x.toFixed(0) ; }
tdivadd([(p.dist/1000).toFixed(3),intise(p.asc),intise(p.desc),
intise(p.maxalt),intise(p.minalt)],'text-align:right') ;
tdivadd(['km','m','m','m','m'],'padding-left:2px') ;
return tdiv ;
}
/* -------------------------------------------------------------------------- */
function starsline(nstars,editable,d)
{ var i,c,s,d ;
if(!d) d = domcreate('div') ;
else if(editable) while(d.childNodes.length>0)
d.removeChild(d.childNodes[d.childNodes.length-1]) ;
for(i=1;i<=5;i++)
{ if(nstars==null||i>nstars) c = '☆' ; else c = '★' ;
s = domcreate('span',c) ;
if(editable&&i!=nstars)
{ domset(s,'style','cursor:pointer') ;
function createfunc(i) { return function() { restars(nstars,i,d) ; } } ;
s.onclick = createfunc(i) ;
}
domadd(d,s) ;
}
if(editable&&nstars!=null)
{ domadd(d,' [',genclickfn(starfactory(nstars),M.clearword),']') ;
function starfactory(n) { return function() { restars(n,null,d) ; } ; } ;
}
return d ;
}
/* -------------------------------------------------------------------------- */
function drawxcur(pro,sel)
{ var pos , i , linewid , inc , cx, cy , jlim , div , r=pro.radius , w , h ;
var c , ctx ;
for(div=pro.curdiv;div.childNodes.length>0;)
div.removeChild(div.childNodes[div.childNodes.length-1]) ;
if(sel.type||!pro.m.h.length) return ;
pro.sel = sel ;
if(pro.active) { w = 620 ; h = 200 ; } else w = h = 42 ;
c = domcreate('canvas',null,'width',w*pro.ratio,'height',h*pro.ratio,
'style','width:'+w+'px;height:'+h+'px') ;
domadd(div,c) ;
ctx = c.getContext('2d') ;
linewid = 2*Math.floor(pro.ratio+0.5) ;
// the cursor
if(segments[0].type!='tour')
{ pos = pro.m.wp2pro[sel.segno][sel.ptno] ;
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 = 1.5*pro.ratio ;
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() ;
}
/* -------------------------------------------------------------------------- */
function toggleprofile(e)
{ var flag , sel , pos = window.innerWidth - e.clientX , ee ;
if(dragging) return ;
var pro=toggleprofile.pcopy,segments=toggleprofile.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 ; drawprofile() ; return ; }
else if(pro.active==0)
{ selpoint({ originalEvent:e ,
latlng:map.containerPointToLatLng([e.clientX,e.clientY])
}) ;
return ;
}
else if(segments[0].type=='tour') 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.segno,sel.ptno,sel.type,flag) ;
}
/* -------------------------------------------------------------------------- */
function telltale(msg,force)
{ if(!map&&!force) { telltale.hold = msg ; return ; }
zonktale() ;
var sty = 'background:white;position:absolute;z-index:1000;'+
'left:0;bottom:0;padding:3px;z-index:999;font-size:140%' ;
telltale.div = domcreate('div',msg,'style',sty) ;
telltale.opacity = 10 ;
telltale.div.setAttribute('style',sty) ;
domadd(body,telltale.div) ;
setTimeout(wipetale,3000) ;
}
function wipetale()
{ if(!telltale.div) return ;
telltale.opacity -= 1 ;
if(telltale.opacity<=0) zonktale() ;
else
{ telltale.div.style.opacity = telltale.opacity/10 ;
setTimeout(wipetale,100) ;
}
}
function zonktale()
{ if(telltale.div) telltale.div.parentNode.removeChild(telltale.div) ;
telltale.div = telltale.hold = null ;
}
var M=
{ // routemaster
osmlandscape:'OSM landscape' ,
osmplain:'OSM plain' ,
esrisatellite:'ESRI satellite' ,
mapboxsatellite:'Mapbox satellite' ,
osmstandard:'OSM standard' ,
unsavedchanges:['1 unsaved change','% unsaved changes'] ,
ifyouhit:'If you hit' ,
ok:'OK' ,
leavepage:'Leave page' ,
willbelost:['this change will be lost.','these changes will be lost.'] ,
isnotxml:'is not an XML file' ,
unable:'Unable to read %' ,
inconsistentlists:'Inconsistent photo lists:' ,
untitledroute:'Untitled route' ,
eyemenu:'List routes' ,
controlmenu:'Control menu' ,
routeprops:'Route properties' ,
segmentprops:'Segment properties' ,
waypointprops:'Waypoint properties' ,
atwaypoint:'at current waypoint' ,
nosplitsegment:'[disabled at first waypoint in a segment]' ,
labelwaypoint:'Mark selected waypoint' ,
addaphoto:'Add a photo' ,
noaddaphoto:'[disabled while waiting for photo list]' ,
undolatest:'Undo latest edit' ,
noundolatest:'Undo [no edits performed]' ,
redolatest:'Redo latest undone edit' ,
noredolatest:'Redo [no edits undone]' ,
saveroute:'Save route' ,
account:'Account' ,
register:['Create an account','Login','Forgot password?','Change password'] ,
layermenu:'Mapping' ,
otkey:'Your confirmation key has been emailed to you (check your spam folder).' ,
illegalopt:'Illegal option %0 for %1' ,
waitingfor:'Waiting for %' ,
aphotolist:'a photo list... try again later' ,
untitled:'Untitled' ,
saveindexas:'Save %index as' ,
saveXas:'Save % as' ,
saveas:'Save as' ,
interpextra:'interpolate extra points' ,
yourtimes:'Note: your times are out of sequence and\nwill not be retained in .fit or .tcx' ,
missingalts:'missing altitudes' ,
dontwanttowait:'If you don’t want to wait you can' ,
cancel:'cancel' ,
del:'delete' ,
defaults:'Defaults' ,
useinterp:'use interpolated altitudes' ,
savemissing:'or save with missing altitudes' ,
gotaltitudes:'Altitudes obtained from TerrainTap' ,
addtitle:'Add title' ,
adddesc:'Add description' ,
addinfo:'Add info' ,
editindex:'Modify index title' ,
enteroffset:'Enter offset in metres to add to altitudes:' ,
isnan:'% is not a number' ,
enteralt:'Enter altitude (m):' ,
enterlabel:'Enter waymark:' ,
modlabel:'Modify or delete waymark:' ,
enterphoto:'Enter photo name:' ,
newphoto:'New photo name:' ,
exitfs:'Exit full screen' ,
enterfs:'Enter full screen' ,
notes:'notes' ,
gpstrack:'GPS track' ,
cantundoload:'Unable to undo load while waiting for %' ,
deletex:'delete' ,
deletesegment:'delete segment' ,
deleteroute:'delete route' ,
deleteindex:'delete index' ,
splitsegment:'split segment' ,
xferwaypoint:'transfer waypoint' ,
dellabel:'delete waymark' ,
labelpt:'mark waypoint' ,
editlabel:'edit waymark' ,
removelabels:'remove waymarks' ,
edittitle:'edit title' ,
editdesc:'edit description' ,
editxdesc:'edit ‘%’ description' ,
editinfo:'edit url of info page' ,
wpdel:'delete waypoint' ,
wpins:'insert point' ,
insgeo:'geolocate image' ,
wpdrag:'drag point' ,
recalalts:'manual recalibration of altitudes' ,
googlelats:'replace altitudes by estimates from a terrain model' ,
regressalts:'regress altitudes against a terrain model' ,
calibalts:'calibrate altitudes using a terrain model' ,
wpalt:'set altitude' ,
wpicon:'change icon' ,
combinesegments:'combine % segments' ,
revsegment:'reverse segment' ,
dupsegment:'duplicate segment' ,
optimsth:'optimise %' ,
optim:'optimise' ,
refresh:'update' ,
deltimes:'delete segment times' ,
delalts:'delete segment altitudes' ,
delphoto:'delete photo' ,
addphoto:'add photo' ,
modphoto:'change photo' ,
clear:'clear %' ,
set:'set %' ,
swapseg:'swap segments' ,
loadsth:'load %' ,
load:'load' ,
addsth:'add %' ,
unrecogaction:'Unrecognised action: %' ,
undo:'Undo' ,
redo:'Redo' ,
bulkundo:'Bulk undo' ,
bulkredo:'Bulk redo' ,
bulkupdate:'Bulk update' ,
slightl:'bear L' ,
sharpl:'sharp L' ,
uturnl:'u-turn L' ,
turnl:'turn L' ,
rampl:'ramp L' ,
forkl:'fork L' ,
raboutl:'roundabout L' ,
slightr:'bear R' ,
sharpr:'sharp R' ,
uturnr:'u-turn R' ,
turnr:'turn R' ,
rampr:'ramp R' ,
forkr:'fork R' ,
raboutr:'roundabout R' ,
straight:'straight on' ,
merge:'merge' ,
notracks:'No tracks' ,
shortfit:'supposed FIT file is too short' ,
unfitfit:'supposed FIT file has format designator ‘%’' ,
missfit:'Missing definition in FIT file.\n“FIT File Viewer” may be able to patch it up.' ,
routes:'routes' ,
numroutes: '% routes' ,
alti:'altitude' ,
cantmeta:'Unable to save index while waiting for photolist' ,
emptyseg:'Empty segment' ,
logicerr:'Logic error in %' ,
gunk:'Elevation service request failed' ,
aroute:'a route' ,
anindex:'an index' ,
ameta:'a metaindex' ,
nodata:'No data returned' ,
addto0:'Somehow you are adding a track to an empty set' ,
addxtoy:'Adding %0 to %1' ,
missingurialt:'Missing altitudes not supported in uri load' ,
missingidxalt:'Missing altitudes not allowed for tracks added to an index' ,
updmulti:'Trying to update a track from a multi-track file' ,
photos:'photos' ,
thicker:'Thicker lines' ,
thinner:'thinner lines' ,
// routemasterui
photolist:'photolist' ,
listnotfound:'Photolist not found' ,
title:'Title' ,
indextitle:'Index title' ,
desc:'Description' ,
stats:'Stats' ,
date:'Date' ,
colour:'Colour' ,
npoints:'%0 (%1 points)' ,
delroute:['Delete route','Delete routes'] ,
updroute:['Update route','Update routes'] ,
updnotposs:'Update of file from disc not possible' ,
updsub:'Update of subindex not implemented' ,
track:['track','tracks'] ,
emailprompt:'Enter your email address:' ,
aloneprompt:'Enter your password:' ,
pwdprompt:'Enter your %0 and %1:' ,
email:'email address' ,
pwd:'password' ,
confpwd:'confirm password' ,
otk:'one-time key' ,
submit:'Submit' ,
rmgps:'Routemaster GPS track editor' ,
rmdisplays:'displays GPS tracks (including embedded photos),' +
' allowing them to be edited and saved to disc.' ,
funcs:[ 'Load gpx, tcx, and fit tracks;' ,
'Display against OSM maps or Mapbox satellite imagery;' ,
'Optimise the track (i.e. remove redundant waypoints);' ,
'Add/delete/move points;' ,
'Divide routes into segments;' ,
'Delete/reorder/reverse segments;' ,
'Combine routes;' ,
'View and modify route/segment/waypoint properties;' ,
'Add turn instructions;' ,
'Add photos;' ,
'Adjust altitudes manually or using the TerrainTap service;' ,
'View an altitude profile;' ,
'Organise routes in tours, indexes and metaindexes;' ,
'Visualise the route in full screen;' ,
'Undo and redo editing operations;' ,
'Save as tcx/gpx/fit/rte.' ] ,
eyedoc:'show routes as a table' ,
setsave:'settings' ,
unre:'undo/redo' ,
logindoc:'register/login/prefs/logout' ,
propsdoc:['view and modify route/segment/waypoint properties',
'(help menu is under route properties)'] ,
scisdoc:['split the current segment at the selected point /',
'make a waymark / add a photo'] ,
save:'save' ,
delbksp:'[shift del], [shift backspace] = delete route.' ,
wpmove:'move to the previous/next waypoint;' ,
segmove:'move to the previous/next segment;' ,
shift:'shift' ,
centres:'centres the map on the current waypoint;' ,
spacedoc:'[space] makes the current point draggable or terminates dragging.' ,
dragfor:'a draggable waypoint forwards' ,
dragback:'a draggable waypoint backwards' ,
detach:'detach point' ,
attach:'convert to waypoint' ,
tabdoc:'[tab] inserts %;' ,
stabdoc:'[shift tab] inserts a draggable point backwards;' ,
deldoc:'[del], [backspace] = delete point;' ,
sdeldoc:'[shift del], [shift backspace] = delete segment.' ,
mouse:'Mouse:' ,
clickdoc:'[click] selects the waypoint closest to the cursor position;' ,
sclickdoc:'[shift click] extends the current segment to the cursor position.' ,
masher:'tool for offline processing' ,
extrack:'Example track to experiment with' ,
exindex:'Example of a route index' ,
userman:'User manual' ,
rmfns:'Routemaster functions:' ,
hitspace:'Hit [space] when you’ve finished dragging' ,
addroute:'Add route' ,
addindex:'Add index or route' ,
savemeta:'Save index as metaindex' ,
hidear:'Hide arrows' ,
showar:'Show arrows' ,
listroutes:'Save list of routes' ,
help:'Help' ,
viewidx:'View index' ,
view:'View gallery' ,
lastadded:'Last added route: ' ,
noalts:['1 point has no associated altitude',
'% points have no associated altitudes'] ,
findalts:'Find altitudes' ,
waitalts:'Awaiting results from elevation service' ,
notimes:'No timings provided' ,
untimed:['1 point has no timing','% points have no timings'] ,
badtimes:'Times are out of sequence (and will be discarded on file save)' ,
numlabels:['1 waymark','% waymarks'] ,
remove:'Remove' ,
numphotos:['1 photo','% photos'] ,
numsegments:['1 segment','% segments'] ,
combine:'Combine' ,
sepwarn:'Note that separations >100m may cause problems on Garmin' ,
loadnewroute:'Load new route' ,
loadnewseg:'add route as new segment' ,
geotag:'Position geotagged image' ,
saveidx:'Save track as route index' ,
combinetosave:'Combine segments to save in other formats' ,
segment:'Segment %0 of %1 (%2 points)' ,
source:'Source' ,
prevopt:'Previously optimised' ,
optimised:'Optimised' ,
optimise:'Optimise' ,
optimdetail:'Optimised (detail=%)' ,
swapwith:'Swap with%0(%1)' ,
combinewith:'Combine with%0(%1)' ,
preceding:'preceding' ,
following:'following' ,
nodeltimes:'Delete times (no times present)' ,
askgoogle:'Estimate altitudes from a terrain model' ,
adjalts:'Adjust altitudes...' ,
regress:'Correct segment altitudes by linear regression' ,
googleadd:'Correct segment altitudes by calibration' ,
vsgoogle:'The preceding methods use the TerrainTap model' ,
repalts:'Replace segment altitudes by estimates' ,
offalts:'Add a manual offset to segment altitudes' ,
docalts:'Documentation of altitude options' ,
edit:'Edit' ,
copy:'Copy' ,
copied:'Copied' ,
photo:'Photo:' ,
nolist:'no list provided' ,
notpres:'not present in %' ,
notavail:'% is not available' ,
info:'Info' ,
enlarge:'Enlarge' ,
nsew:'NSEW' ,
osref:'OS grid ref' ,
utm:'UTM coords' ,
time:'Time' ,
flat:'Flat' ,
climb:'Climb' ,
descend:'Descend' ,
kmh:'km/h' ,
segpt:'Segment %' ,
point:'point %' ,
mkdrag:'Make point draggable' ,
dragaway:'Insert %' ,
xferwpt:'Transfer waypoint to segment %' ,
viewinfo:'View info page' ,
editurl:'Edit url' ,
addurl:'Add url of info page' ,
labeldoc:'Documentation for waymarks' ,
descdoc:'Documentation for descriptions' ,
geolocdoc:'Documentation for geolocating images' ,
accdoc:'Reasons for creating a account' ,
tabvals:['Total distance:','Total ascent:','Total descent:',
'Maximum altitude:','Minimum altitude:'] ,
clearword:'Clear' ,
loadlist:'Load photo list from the web (or leave blank)' ,
orweb:'or load from the web' ,
gmaps:'data' ,
photodoc:'Documentation for including photos' ,
// index.html
loadrm:'Loading %...' ,
failed:'Failed to read %: no data returned' ,
noviewbox:'** Error: % does not have a viewbox field' ,
badend:'** Error: % does not end with a data field' ,
nodata:'** Error: % has no data points' ,
badtrack:'Invalid track URL (must be .gpx, .tcx, .rte, .kml or .fit)' ,
badopt:'Unrecognised option: ' ,
badjpg:'Invalid jpg file: ' ,
notgeotagged:'No geotag in jpg file: ' ,
distance: 'Distance' ,
altitude: 'altitude' ,
// subsequent additions
illegalfield:'Illegal % field' ,
select:'Select TCX/GPX/RTE/KML/FIT file(s): ' ,
selectjpg:'Select JPG file: ' ,
sqlerr:'SQL error:' ,
cookies:'Registering with routemaster indicates consent '+
'to the use of cookies to store session data.' ,
already:'Email address % already registered' ,
formerr:'Unable to communicate with server: error=' ,
incorrect:'Incorrect password' ,
badotkey:'Incorrect one-time key' ,
expired:'One-time key expired' ,
baduser:'% is not a registered user' ,
toosoon:'There is already an unexpired one-time key for %' ,
dberr:'Unable to connect to database:' ,
bademail:'Illegal email address' ,
logout:'Log out' ,
delact:'and close account' ,
bugreport:'Report a bug/request help' ,
delwarn:'Close account?\nIf you proceed, all preferences will be lost.' ,
prefs:'Update preferences' ,
labels: [ 'Generic' , 'Sharp left' , 'Left' , 'Slight left' , 'Left fork' ,
'Straight' , 'Right fork' , 'Slight right' , 'Right' , 'Sharp right',
'Danger' , 'Food' , 'Water' , 'Summit' , 'Valley' ,
'Turn back' , 'Viewpoint' , 'Steep incline' , 'Info' , 'Obstacle'
] ,
points:'points' ,
detail:'Optimisation detail' ,
savepref:'Save as preference?' ,
language:'Language:' ,
maxsep:'Maximum waypoint separation: %m' ,
limsep:'Limit point separation?' ,
optonload:'Optimise on load?' ,
precision:'GPS track precision:' ,
prob:'routemaster problem' ,
alreadygone:'You are already logged out' ,
devdata:'.fit files containing developer fields not accepted' ,
gapsfixed:'Additional waypoints have been interpolated.' ,
formatdoc:'Documentation for track formats' ,
cantplace:'Unable to place coursepoint: ' ,
untimedrecord: 'Record encountered with no timestamp' ,
untimedcoursept: 'Course point encountered with no timestamp' ,
index: 'index' ,
tour: 'tour' ,
metaindex: 'metaindex' ,
nameforX: 'Enter name for % (without extension)' ,
view1: 'View' ,
outoforder: 'Your points are out of order, so turn instructions cannot be placed.',
notcxindex:'tcx indexes are no longer supported by routemaster – use .rte instead.' ,
multitrack:'multitrack route: only the first track will be retained.' ,
swipedown: 'Swipe down: ' ,
returnkey: 'Return key: ' ,
backtogps: 'back to GPS'
} ;
var M=
{ // routemaster
osmlandscape:'OSM relief' ,
osmplain:'OSM simple' ,
esrisatellite:'ESRI satellite' ,
mapboxsatellite:'Mapbox satellite' ,
osmstandard:'OSM standard' ,
unsavedchanges:['1 modification non enregistrée',
'% modifications non enregistrées'] ,
ifyouhit:'Si vous poussez' ,
ok:'OK' ,
leavepage:'Quitter la page' ,
willbelost:['cette modification sera perdu.',
'ces modifications seront perdues.'] ,
isnotxml:'n’est pas un fichier XML' ,
unable:'Impossible de lire %' ,
inconsistentlists:'Listes photo incohérentes:' ,
untitledroute:'Sans titre' ,
eyemenu:'Lister les itinéraires' ,
controlmenu:'Menu principal' ,
routeprops:'Attributs de l’itinéraire' ,
segmentprops:'Attributs du segment' ,
waypointprops:'Attributs du point' ,
atwaypoint:'au point actuel' ,
nosplitsegment:'[désactivé au premier point d’un segment]' ,
labelwaypoint:'Balise' ,
addaphoto:'Ajouter une photo' ,
noaddaphoto:'[désactivé en attendant la liste photos]' ,
undolatest:'Annuler la dernière modification' ,
noundolatest:'Annuler [aucune modification effectuée]' ,
redolatest:'Refaire la dernière modification annulée' ,
noredolatest:'Refaire [aucune modification annulée]' ,
saveroute:'Enregistrer l’itinéraire' ,
account:'Compte' ,
register:['Créer un compte','Connexion',
'Mot de passe oublié?','Changer le mot de passe'] ,
layermenu:'Cartographie' ,
otkey:'Votre clé de confirmation vous a été envoyée par e-mail (vérifiez '+
'votre dossier spam).' ,
illegalopt:'Option illégale %0 pour %1' ,
waitingfor:'En attendant %' ,
aphotolist:'une liste photos... réessayez plus tard' ,
untitled:'Sans titre' ,
saveindexas:'Enregistrer %index en tant que' ,
saveXas:'Enregistrer % en tant que' ,
saveas:'Enregistrer en tant que' ,
interpextra:'interpoler les points de passage supplémentaires' ,
yourtimes:'Remarque : vos données de temps sont hors séquence\net ne seront pas conservées en .fit ou .tcx' ,
missingalts:'altitudes manquantes' ,
dontwanttowait:'Si vous ne voulez pas attendre, vous pouvez' ,
cancel:'annuler' ,
del:'supprimer' ,
defaults:'Réinitialiser' ,
useinterp:'utiliser les altitudes interpolées' ,
savemissing:'ou enregistrer avec les altitudes manquantes' ,
gotaltitudes:'Altitudes obtenues à partir de TerrainTap' ,
addtitle:'Ajouter un titre' ,
adddesc:'Ajouter une description' ,
addinfo:'Ajouter des informations' ,
editindex:'Modifier le titre de l’index' ,
enteroffset:'Entrez le décalage en mètres à ajouter aux altitudes:' ,
isnan:'% n’est pas un nombre' ,
enteralt:'Entrez l’altitude (m):' ,
enterlabel:'Entrez l’étiquette:' ,
modlabel:'Modifier ou supprimer l’étiquette:' ,
enterphoto:'Entrez le nom de la photo :' ,
newphoto:'Nom de la nouvelle photo :' ,
exitfs:'Quitter le plein écran' ,
enterfs:'Entrer en plein écran' ,
notes:'notes' ,
gpstrack:'trace GPS' ,
cantundoload:'Impossible d’annuler le chargement en attendant %' ,
deletex:'supprimer' ,
deletesegment:'supprimer le segment' ,
deleteroute:'supprimer l’itinéraire' ,
deleteindex:'supprimer l’index' ,
splitsegment:'diviser le segment' ,
xferwaypoint:'transferrer le point' ,
dellabel:'supprimer l’étiquette' ,
labelpt:'étiqueter le point' ,
editlabel:'modifier le point' ,
removelabels:'supprimer les étiquettes' ,
edittitle:'modifier le titre' ,
editdesc:'modifier la description' ,
editxdesc:'modifier la description de ‘%’' ,
editinfo:'modifier l’url de la page info' ,
wpdel:'supprimer le point' ,
wpins:'insérer un point' ,
insgeo:'géolocaliser image' ,
wpdrag:'faire glisser le point' ,
recalalts:'recalibration manuelle des altitudes' ,
googlelats:'remplacer les altitudes par les estimations d’un modèle de terrain' ,
regressalts:'régresser les altitudes avec un modèle de terrain' ,
calibalts:'étalonner les altitudes avec un modèle de terrain' ,
wpalt:'définir l’altitude' ,
wpicon:'changer l’icône' ,
combinesegments:'combiner % segments' ,
revsegment:'inverser le segment' ,
dupsegment:'dupliquer le segment' ,
optimsth:'optimiser %' ,
optim:'optimiser' ,
refresh:'remettre à neuf' ,
deltimes:'supprimer les données de temps' ,
delalts:'supprimer les altitudes des segments' ,
delphoto:'supprimer la photo' ,
addphoto:'ajouter une photo' ,
modphoto:'changer de photo' ,
clear:'effacer %' ,
set:'définir %' ,
swapseg:'échanger des segments' ,
loadsth:'importer %' ,
load:'importer' ,
addsth:'ajouter %' ,
unrecogaction:'Action non reconnue: %' ,
undo:'Annuler' ,
redo:'Rétablir' ,
bulkundo:'Annuler en masse' ,
bulkredo:'Rétablir en masse' ,
bulkupdate:'Mise à jour en masse' ,
slightl:'léger G' ,
sharpl:'serré G' ,
uturnl:'U à G' ,
turnl:'virage G' ,
rampl:'rampe à G' ,
forkl:'embr à G' ,
raboutl:'rond-pt G' ,
slightr:'léger D' ,
sharpr:'serré D' ,
uturnr:'U à D' ,
turnr:'virage D' ,
rampr:'rampe à D' ,
forkr:'embr à D' ,
raboutr:'rond-pt D' ,
straight:'tout droit' ,
merge:'fusionner' ,
notracks:'Pas de traces' ,
shortfit:'Fichier FIT prétendu est trop court' ,
unfitfit:'Fichier FIT prétendu a ‘%’ pour indicateur de format' ,
missfit:'Définition manquante dans le fichier FIT.\nPeut-être que “FIT File Viewer” le refistolerait' ,
routes:'itinéraires' ,
numroutes: '% itinéraires' ,
alti:'altitude' ,
cantmeta:'Impossible d’enregistrer le’index en attendant la liste photos' ,
emptyseg:'Segment vide' ,
logicerr:'Erreur logique dans %' ,
gunk:'Demande d’altitudes échouée' ,
aroute:'une itinéraire' ,
anindex:'un index' ,
ameta:'un métaindex' ,
nodata:'Aucune donnée renvoyée' ,
addto0:'D’une manière ou d’une autre, vous essayez d’ajouter une trace à un ensemble vide' ,
addxtoy:'Vous essayez d’ajouter %0 à %1' ,
missingurialt:'Altitudes manquantes non permises dans un import URL' ,
missingidxalt:'Altitudes manquantes non permises pour les traces ajoutées à un index' ,
updmulti:'Tentative de remplacer une trace à partir d’un fichier multitrace' ,
photos:'photos' ,
thicker:'Lignes plus épaisses' ,
thinner:'lignes plus fines' ,
// routemasterui
photolist:'liste photos' ,
listnotfound:'Liste photos introuvable' ,
title:'Titre' ,
indextitle:'Titre d’index' ,
desc:'Description' ,
stats:'Chifres' ,
date:'Date' ,
colour:'Couleur' ,
npoints:'%0 (%1 points de passage)' ,
delroute:['Supprimer l’itinéraire','Supprimer les itinéraires'] ,
updroute:['Remplacer l’itinéraire','Remplacer les itinéraires'] ,
updnotposs:'Remplacement du fichier à partir du disque impossible' ,
updsub:'Remplacement du sous-index non implémentée' ,
track:['trace','traces'] ,
emailprompt:'Entrez votre adresse e-mail :' ,
aloneprompt:'Entrez votre mot de passe :' ,
pwdprompt:'Entrez vos %0 et %1 :' ,
email:'adresse e-mail' ,
pwd:'mot de passe' ,
confpwd:'confirmer le mot de passe' ,
otk:'clé unique' ,
submit:'Envoyer' ,
rmgps:'Routemaster éditeur de traces GPS ' ,
rmdisplays:'affiche les traces GPS (y compris les photos intégrées),' +
' leur permettant d’être édités et sauvegardés sur disque.' ,
funcs:[ 'Importer les pistes gpx, tcx et fit;' ,
'Afficher sur des cartes OSM ou des images satellites Mapbox;' ,
'Optimiser la trace (c’est-à-dire supprimer les points de passage superflus) ;' ,
'Ajouter/supprimer/déplacer les points ;' ,
'Diviser les itinéraires en segments ;' ,
'Supprimer/réorganiser/inverser les segments ;' ,
'Fusionner les itinéraires ;' ,
'Afficher et modifier les propriétés de l’itinéraire/segment/point ;' ,
'Étiquetér les points (par exemple, ‘Virage à G’) ;' ,
'Ajouter des photos ;' ,
'Ajuster les altitudes manuellement ou en utilisant le service d’altitudes « TerrainTap » ;' ,
'Afficher un profil d’altitude ;' ,
'Organiser les itinéraires en circuits, index et métaindex ;' ,
'Afficher l’itinéraire en plein écran ;' ,
'Annuler et rétablir les modifications ;' ,
'Sauvegarder en fichier tcx/gpx/fit/rte.' ] ,
eyedoc:'vue d’ensemble' ,
setsave:'contrôles' ,
unre:'annuler/rétablir' ,
logindoc:'s’abonner/connexion/prefs/déconnexion' ,
propsdoc:['afficher et modifier les propriétés de l’itinéraire/segment/point',
'(le menu d’aide est sous les propriétés de l’itinéraire)'] ,
scisdoc:['diviser le segment au point sélectionné /',
'faire une balise / ajouter une photo'] ,
save:'sauvegarder' ,
delbksp:'[↑ suppr], [↑ retour arrière] = supprimer l’itinéraire.' ,
wpmove:'passer au point de passage précédent/suivant;' ,
segmove:'passer au segment précédent/suivant;' ,
shift:'↑' ,
centres:'centre la carte sur le point actuel;' ,
spacedoc:'[espace] rend le point actuel glissable ou termine le glissement.' ,
dragfor:'vers l’avant un point glissable' ,
dragback:'vers l’arrière un point glissable' ,
detach:'détacher le point' ,
attach:'convertir en point de passage' ,
tabdoc:'[tab] insèrer % ;' ,
stabdoc:'[↑ tab] insèrer % ;' ,
deldoc:'[suppr], [retour arrière] = supprimer le point ;' ,
sdeldoc:'[↑ suppr], [↑ retour arrière] = supprimer le segment.' ,
mouse:'Souris :' ,
clickdoc:'[clique] sélectionne le point le plus proche de la position du curseur;' ,
sclickdoc:'[↑ clique] étend le segment actuel à la position du curseur.' ,
masher:'outil hors ligne' ,
extrack:'Exemple d’une trace à expérimenter' ,
exindex:'Exemple d’un index des itinéraires' ,
userman:'Manuel d’utilisation' ,
rmfns:'Fonctions de routemaster:' ,
hitspace:'Appuyez sur [espace] lorsque vous avez fini de faire glisser' ,
addroute:'Ajouter un itinéraire' ,
addindex:'Ajouter un index/un itinéraire' ,
savemeta:'Sauvegarder l’index en tant que métaindex' ,
hidear:'Masquer les chevrons' ,
showar:'Afficher les chevrons' ,
listroutes:'Sauvegarder une liste des itinéraires' ,
help:'Aide' ,
viewidx:'Afficher l’index' ,
view:'Afficher la galerie' ,
lastadded:'Dernière itinéraire ajoutée : ' ,
noalts:['1 point de passage n’a pas d’altitude associée',
'% points de passage n’ont pas d’altitudes associées'] ,
findalts:'Déterminer les altitudes' ,
waitalts:'En attente des résultats du service d’altitudes' ,
notimes:'Aucune donnée de temps fourni' ,
untimed:['1 point n’a pas une donnée de temps','% points n’ont pas de données de temps'] ,
badtimes:'Les données de temps sont hors séquence (et seront ignorées quand vous sauvegardez le fichier)' ,
numlabels:['1 balise','% balises'] ,
remove:'Supprimer' ,
numphotos:['1 photo','% photos'] ,
numsegments:['1 segment','% segments'] ,
combine:'Fusionner' ,
sepwarn:'Notez que les séparations >100m peuvent causer des problèmes sur les appareils Garmin' ,
loadnewroute:'Importer un nouvel itinéraire' ,
loadnewseg:'ajouter un itinéraire comme segment nouveau' ,
geotag:'Localiser image géotaguée' ,
saveidx:'Sauvegarder la trace en tant que index des itinéraires' ,
combinetosave:'Combiner les segments pour sauvegarder en autres formats' ,
segment:'Segment %0 de %1 (%2 points)' ,
source:'Origine' ,
prevopt:'Précédemment optimisé' ,
optimised:'Optimisé' ,
optimise:'Optimiser' ,
optimdetail:'Optimisé (détail=%)' ,
swapwith:'Échanger avec%0(%1)' ,
combinewith:'Fusionner avec%0(%1)' ,
preceding:'précédent' ,
following:'suivant' ,
nodeltimes:'Supprimer les données de temps (manquantes)' ,
askgoogle:'Estimer les altitudes du segment à partir d’un modèle de terrain' ,
adjalts:'Ajuster les altitudes...' ,
regress:'Corriger les altitudes du segment par régression linéaire' ,
googleadd:'Corriger les altitudes des segments par étalonnage additif' ,
vsgoogle:'Les méthodes précédentes utilisent le modèle de « TerrainTap ».' ,
repalts:'Remplacer les altitudes du segment par les estimations' ,
offalts:'Ajouter un décalage manuel aux altitudes du segment' ,
docalts:'Documentation des options d’altitude (angl.)' ,
edit:'Modifier' ,
copy:'Copier' ,
copied:'Copié' ,
photo:'Photo:' ,
nolist:'aucune liste fournie' ,
notpres:'manquant de %' ,
notavail:'% n’est pas disponible' ,
info:'Infos' ,
enlarge:'Agrandir' ,
nsew:'NSEO' ,
osref:'ref grille OS' ,
utm:'coords UTM' ,
time:'Heure' ,
flat:'Plat' ,
climb:'Ascension' ,
descend:'Descente' ,
kmh:'km/h' ,
segpt:'Segment %' ,
point:'point %' ,
mkdrag:'Faire le point glissable' ,
dragaway:'Insérer %' ,
xferwpt:'Transférer le point de passage vers le segment %' ,
viewinfo:'Afficher la page infos' ,
editurl:'Modifier URL' ,
addurl:'Ajouter URL d’une page infos' ,
labeldoc:'Documentation pour les balises (angl.)' ,
descdoc:'Documentation pour les descriptions (angl.)' ,
geolocdoc:'Documentation pour la géolocation des images (angl.)' ,
accdoc:'Raisons pour créer un compte' ,
tabvals:['Distance totale :','Dénivelé total :','Descente totale :',
'Altitude maximum :','Altitude minimum :'] ,
clearword:'Effacer' ,
loadlist:'Importer la liste de photos depuis le Web (ou laisser vide)' ,
orweb:'ou importer depuis le web' ,
gmaps:'fichier' ,
photodoc:'Documentation pour l’insertion de photos (angl.)' ,
// index.html
loadrm:'Téléchargeant %...' ,
failed:'Impossible de lire % : aucune donnée renvoyée' ,
noviewbox:'** Error: % n’a pas un champs ‘viewbox’' ,
badend:'** Erreur : % ne se termine pas par un champ de données' ,
nodata:'** Erreur : % n’a pas de points' ,
badtrack:'URL de trace non valide (doit être .gpx, .tcx, .rte, .kml ou .fit)' ,
badopt:'Option non reconnue: ' ,
badjpg:'Fichier jpg invalide : ' ,
notgeotagged:'Fichier jpg sans géotagging : ' ,
distance: 'Distance' ,
altitude: 'altitude' ,
// subsequent additions
illegalfield:'Champ % illégal' ,
select:'Sélectionnez un ou plusieurs fichiers TCX/GPX/RTE/KML/FIT : ' ,
selectjpg:'Sélectionnez un fichier JPG : ' ,
sqlerr:'Erreur SQL :' ,
cookies:'Création d’un compte routemaster indique le consentement' +
'à l’utilisation de cookies pour maintenir les données de session.' ,
already:'Adresse e-mail % déjà enregistrée' ,
formerr:'Impossible de communiquer avec le serveur : erreur=' ,
incorrect:'Mot de passe incorrect' ,
badotkey:'Clé unique incorrect' ,
expired:'Clé unique expirée' ,
baduser:'% n’est pas un compte reconnu' ,
toosoon:'Il existe déjà une clé unique non expirée pour %' ,
dberr:'Impossible de se connecter à la base de données : ' ,
bademail:'Adresse e-mail illégale' ,
logout:'Déconnexion' ,
delact:'et clôture de compte' ,
bugreport:'Signaler un bug/demander de l’aide' ,
delwarn:'Fermer le compte ?\nSi vous continuez, tous vos paramètres seront perdus.' ,
prefs:'Mettre à jour les préférences' ,
labels: [ 'Générique' , 'Gauche serrée' , 'Gauche' , 'Gauche légère' , 'Fourche gauche' ,
'Tout Droit' , 'Fourche droite' , 'Droite légère' , 'Droite' , 'Droite serrée' ,
'Danger' , 'Nourriture' , 'Eau' , 'Sommet' , 'Vallée' ,
'Rebrousser' , 'Point de vue' , 'Grimpée raide' , 'Infos' , 'Obstacle'
] ,
points:'points' ,
detail:'Détail d’optimisation' ,
savepref:'Enregistrer comme préférence ?' ,
language:'Langue:' ,
maxsep:'Séparation maximale des points de passage : %m' ,
limsep:'Limiter la séparation ?' ,
optonload:'Optimiser au chargement ?' ,
precision:'Précision de la trace GPS :' ,
prob:'problème routemaster' ,
alreadygone:'Vous êtes déjà déconnecté' ,
devdata:'Les fichiers .fit avec les champs ‘developer data’ ne sont pas acceptés' ,
gapsfixed:'Quelques points supplémentaires ont été interpolés.' ,
formatdoc:'Documentation pour les formats de trace (angl.)' ,
cantplace:'Impossible de placer un point de passage : ' ,
untimedrecord: 'Record rencontré sans horodatage' ,
untimedcoursept: 'Point de parcours rencontré sans horodatage' ,
index: 'index' ,
tour: 'tour ' ,
metaindex: 'le métaindex' ,
nameforX: 'Entrez un nom pour % (sans extension)' ,
view1: 'Afficher' ,
notcxindex:'Les index tcx ne sont plus supportés par routemaster – plutôt utiliser .rte.' ,
multitrack:'Itinéraire multitrace : seule la première trace sera conservée.' ,
swipedown: 'Glisser vers le bas : ' ,
returnkey: 'Touche retour : ' ,
backtogps: 'retour à GPS'
} ;
• getbus • prettynum • shortenname • abbreviate • underline • clone • isvaliddate • dist • angle • midpt • normalise • xmlfloat • dehexify • numerate • function • caps • pttype • correctlabel • routetype • getlatlong • deltify • recurse • flipseg • getstats • getmetastats • diststring • kmstring • prettystats • routestats • flatten • squash • gentimes • parsestats • unspace • getgpxpt • gettcxpt • readcsv • readkml • readtcx • readgpx • getrtept • getrteprops • readrte • readfitvalue • readfitangle • readfit • maketext • getgallery • gentag • gentypedtag • gendesc • genindex • formatstats • genimgtag • genurl • genoptim • geninfotag • tcxpos • gpxp • decimate • optimise • readgps • writegps • writerteprops • writegpx • writerte • writetcx • writefit • fitinject • fitinject2 • fitinject4 • fitinjectangle • checksum • profilemaptype • profiletype • delpro • drawpro • choosearrows • dexmlify • absuri • trackref • innerRML • loaderfactory • catch • BlobBuilder • Blob • separator • parse • toDms • toLat • toLon • toBrng • fromLocale • toLocale • compassPoint • wrap90 • wrap180 • wrap360 • Vector3d • LatLon • Utm • OsGridRef • constructor • lat • latitude • lon • lng • longitude • parse • distanceTo • initialBearingTo • equals
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* routemaster interface and utility functions (c) Colin Champion 2014-2026 */ /* MIT Licence */ /* www.routemaster.app/software/routemaster.html */ /* This file also contains code by Eli Grey and Chris Veness, issued under */ /* the same licence, but with copyright vested in them rather than me. */ /* See below for details. */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ var rmdoc = '<!-- https://www.routemaster.app/software/routemaster.html -->\n' ; function getbus(col1,col2) { var i , j , s , c , svgbus = 'data:image/svg+xml;charset=utf-8,'+ '<svg xmlns="http://www.w3.org/2000/svg" '+ 'xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32">'+ '<title>bus</title>'+ '<path d="M 0 29 L 32 29 L 32 20 L 3 20 A 3 3 0 0 0 0 23 Z" '+ 'style="stroke-width:0" fill="@1"/>'+ '<path d="M 2 13 L 32 13 L 32 0 L 8 0 A 6 6 0 0 0 2 6 L 2 11 A 2 2 0 0 0 4 13 z" '+ 'style="stroke-width:0" fill="@1"/>'+ '<path d="M 5 7 L 32 7 L 32 2 L 9 2 A 4 4 0 0 0 5 6 z" '+ 'style="stroke-width:0" fill="white"/>'+ '<path d="M 4 11 A 1 1 0 0 0 5 12 L 32 12 L 32 8 L 5 8 A 1 1 0 0 0 4 9 z" '+ 'style="stroke-width:0" fill="@2"/>'+ '<circle r="5" cx="6" cy="27" style="stroke-width:0" fill="black"/>'+ '<circle r="2.5" cx="6" cy="27" style="stroke-width:0" fill="%23f7e9ce"/>'+ '<line x1="5.5" y1="13" x2="5.5" y2="20" style="stroke:@1;stroke-width:1"/>'+ '<line x1="8" y1="13" x2="8" y2="20" style="stroke:@1;stroke-width:1"/>'+ '<line x1="13.75" y1="13" x2="13.75" y2="20" style="stroke:@1;stroke-width:2.5"/>'+ '<line x1="21.5" y1="13" x2="21.5" y2="20" style="stroke:@1;stroke-width:1"/>'+ '<line x1="15" y1="15.5" x2="32" y2="15.5" style="stroke:@1;stroke-width:1"/>'+ '<line x1="6" y1="18.5" x2="8" y2="18.5" style="stroke:@1;stroke-width:1"/>'+ '<line x1="4" y1="3.5" x2="32" y2="3.5" style="stroke:@1;stroke-width:1"/>'+ '<line x1="11.5" y1="1" x2="11.5" y2="7" style="stroke:@1;stroke-width:1"/>'+ '<line x1="19.5" y1="1" x2="19.5" y2="7" style="stroke:@1;stroke-width:1"/>'+ '<line x1="27.5" y1="1" x2="27.5" y2="7" style="stroke:@1;stroke-width:1"/></svg>' ; for(s='',i=0;;i=j+2) { j = svgbus.indexOf('@',i) ; if(j<0) return s + svgbus.substring(i) ; c = svgbus.charAt(j+1) ; if(c=='1') c = col1 ; else c = col2 ; s += svgbus.substring(i,j) + c ; } } function prettynum(num,opt,sfx) // opt means “put a space in long numbers” { var span = domcreate('span') , len ; num = num.toFixed(0) ; len = num.length ; if(len>3&&opt) { domadd(span,domcreate('span',num.substring(0,len-3), 'style','padding-right:1.5px')) ; num = num.substring(len-3) ; } domadd(span,num) ; if(sfx) domadd(span,domcreate('span',sfx,'style','padding-left:1.5px')) ; return span ; } function shortenname(uri) { if(!uri) return uri ; var i = uri.lastIndexOf('/') ; if(i<0) return uri ; else return uri.substring(1+i) ; } function abbreviate(name) { if(!name) return [ name,null ] ; name = shortenname(name) ; var len=name.length , extn=len>0?name.substring(len-4).toLowerCase():null ; if(trackextns.indexOf(extn)>=0) return [ name.substring(0,len-4) , extn ] ; else return [ name , extn ] ; } function underline(d) { domset(d,'style', 'margin-bottom:2px;border-bottom:solid 1px silver;padding-bottom:2px') ; return d ; } function clone(x) { var i,y={} ; for(i in x) y[i] = x[i] ; return y ; } function isvaliddate(d) { if(!d) return false ; if(Object.prototype.toString.call(d)!=="[object Date]") return false ; else return !isNaN(d.getTime()) ; } function dist(x,y) { x = new LatLonSpherical(x.lat,x.lng) ; y = new LatLonSpherical(y.lat,y.lng) ; return x.distanceTo(y) ; } function angle(x,y) // clockwise from N in radians { x = new LatLonSpherical(x.lat,x.lng) ; y = new LatLonSpherical(y.lat,y.lng) ; return x.initialBearingTo(y) ; } function midpt(x,y) // should deal with wraparound { return { lat:(x.lat+y.lat)/2 , lng:(x.lng+y.lng)/2 } ; } function normalise(txt) { if(!txt) return txt ; var s = txt.replace(/\s/g,' ') , i , j , str , c ; for(i=0;i<s.length&&s.charAt(i)==' ';i++) ; for(j=s.length;j>=i&&s.charAt(j-1)==' ';j--) ; s = s.substring(i,j) ; return s ; } function xmlfloat(x) { return parseFloat(x.textContent) ; } function dehexify(x) { var i,y=[0,0,0] ; function numerate(x) { return '0123456789abcdef'.search(x.toLowerCase()) ; } if(x.charAt(0)!='#') { alert(inject(M.logicerr,'dehexify')) ; return y ; } for(i=0;i<3;i++) y[i] = ( numerate(x.charAt(2*i+2))+16*numerate(x.charAt(2*i+1)) ) / 255 ; return y ; } var defparms = {tol:15,maxsep:100,wppenalty:700,vweight:3} ; var waymarks = [ { name: 'Generic' , tcxname: 'Generic' , fitname: 0 , draw:function(ctx) { ctx.moveTo(6.5, 21.5) ; ctx.lineTo(6.5, 1.5) ; ctx.lineTo(18.5, 7) ; ctx.lineTo(6.5, 12.5) ; } , anchor: [6.5,21.5] , xoffs: 2 } , { name: 'Sharp left' , tcxname: 'Left' , fitname: 20 , draw: function(ctx) { ctx.moveTo(18.5, 20.5) ; ctx.lineTo(16.4, 7) ; ctx.arc(15.76,7.77 , 1 , -0.8798,3.4156,1) ; ctx.lineTo(10.3, 12.3) ; ctx.lineTo(12.6, 14.6) ; ctx.lineTo(5.6, 13.6) ; ctx.lineTo(4.6, 6.6) ; ctx.lineTo(6.9, 8.9) ; ctx.lineTo(15, 2.8) ; ctx.arc(16.51,5.73 , 3.3 , -2.0463,0.0807) ; ctx.closePath() ; } , anchor: [18.5,20.5] , xoffs: -1 } , { name: 'Left' , tcxname: 'Left' , fitname: 6 , draw: function(ctx) { ctx.moveTo(14.5, 20.5) ; ctx.lineTo(12.5, 11.5) ; ctx.arc(10.5,11.5 , 2 , 0,4.7124,1) ; ctx.lineTo(7.5, 10) ; ctx.lineTo(7.5, 13.5) ; ctx.lineTo(2.5, 7.5) ; ctx.lineTo(7.5, 1.5) ; ctx.lineTo(7.5, 5) ; ctx.lineTo(12.5, 5.5) ; ctx.arc(12.5,9 , 3.5 , -1.5708,0) ; ctx.closePath() ; } , anchor: [14.5,20.5] , xoffs: 3 } , { name: 'Slight left' , tcxname: 'Left' , fitname: 19 , draw: function(ctx) { ctx.beginPath() ; ctx.moveTo(14.5, 20.5) ; ctx.lineTo(12.8, 12.8) ; ctx.arc(6.02,14.54 , 7 , -0.251,5.3704,1) ; ctx.lineTo(7.2, 7) ; ctx.lineTo(4.4, 9.8) ; ctx.lineTo(5.9, 2.3) ; ctx.lineTo(13.4, 0.8) ; ctx.lineTo(10.6, 3.6) ; ctx.arc(10.9,11.37 , 5 , -0.7671,-0.0141) ; ctx.closePath() ; } , anchor: [14.5,20.5] , xoffs: -1 } , { name: 'Left fork' , tcxname: 'Left' , fitname: 16 , draw: function(ctx) { ctx.moveTo(14.5, 20.5) ; ctx.lineTo(12.8, 12.8) ; ctx.arc(6.02,14.54 , 7 , -0.251,5.3704,1) ; ctx.lineTo(7.2, 7) ; ctx.lineTo(4.4, 9.8) ; ctx.lineTo(5.9, 2.3) ; ctx.lineTo(13.4, 0.8) ; ctx.lineTo(10.6, 3.6) ; ctx.lineTo(14.5, 7.9) ; ctx.lineTo(18.5, 1.2) ; ctx.lineTo(20.5, 2.4) ; ctx.lineTo(17.7, 7.5) ; ctx.arc(36.13,15.26 , 20 , -2.743,3.3055,1) ; ctx.closePath() ; } , anchor: [14.5,20.5] , xoffs: -1 } , { name: 'Straight' , tcxname: 'Straight' , fitname: 8 , draw: function(ctx) { ctx.moveTo(12.5,20.5) ; ctx.lineTo(10,6.5) ; ctx.lineTo(6,6.5) ; ctx.lineTo(12.5,1.5) ; ctx.lineTo(19,6.5) ; ctx.lineTo(15,6.5) ; ctx.closePath() ; } , anchor: [12.5,20.5] } , { name: 'Right fork' , tcxname: 'Right' , fitname: 17 , draw: function(ctx) { ctx.moveTo(12.5, 20.5) ; ctx.lineTo(14.2, 12.8) ; ctx.arc(20.98,14.54 , 7 , -2.8906,-2.2288) ; ctx.lineTo(19.8, 7) ; ctx.lineTo(22.6, 9.8) ; ctx.lineTo(21.1, 2.3) ; ctx.lineTo(13.6, 0.8) ; ctx.lineTo(16.4, 3.6) ; ctx.lineTo(12.5, 7.9) ; ctx.lineTo(8.5, 1.2) ; ctx.lineTo(6.5, 2.4) ; ctx.lineTo(9.3, 7.5) ; ctx.arc(-9.13,15.26 , 20 , -0.3986,-0.1639) ; ctx.closePath() ; } , anchor: [12.5,20.5] , xoffs: -4 } , { name: 'Slight right' , tcxname: 'Right' , fitname: 21 , draw: function(ctx) { ctx.moveTo(7.5, 20.5) ; ctx.lineTo(9.2, 12.8) ; ctx.arc(15.98,14.54 , 7 , -2.8906,-2.2288) ; ctx.lineTo(14.8, 7) ; ctx.lineTo(17.6, 9.8) ; ctx.lineTo(16.1, 2.3) ; ctx.lineTo(8.6, 0.8) ; ctx.lineTo(11.4, 3.6) ; ctx.lineTo(7.5, 7.9) ; ctx.arc(11.1,11.37 , 5 , -2.3745,3.1557,1) ; ctx.closePath() ; } , anchor: [7.5,20.5] , xoffs: 1 } , { name: 'Right' , tcxname: 'Right' , fitname: 7 , draw: function(ctx) { ctx.moveTo(8.5, 20.5) ; ctx.lineTo(10.5, 11.5) ; ctx.arc(12.5,11.5 , 2 , 3.1416,4.7124) ; ctx.lineTo(15.5, 10) ; ctx.lineTo(15.5, 13.5) ; ctx.lineTo(20.5, 7.5) ; ctx.lineTo(15.5, 1.5) ; ctx.lineTo(15.5, 5) ; ctx.lineTo(10.5, 5.5) ; ctx.arc(10.5,9 , 3.5 , -1.5708,3.1416,1) ; ctx.closePath() ; } , anchor: [8.5,20.5] , xoffs: 0 } , { name: 'Sharp right' , tcxname: 'Right' , fitname: 22 , draw: function(ctx) { ctx.moveTo(8.5, 20.5) ; ctx.lineTo(10.6, 7) ; ctx.arc(11.24,7.77 , 1 , -2.2618,-0.274) ; ctx.lineTo(16.7, 12.3) ; ctx.lineTo(14.4, 14.6) ; ctx.lineTo(21.4, 13.6) ; ctx.lineTo(22.4, 6.6) ; ctx.lineTo(20.1, 8.9) ; ctx.lineTo(12, 2.8) ; ctx.arc(10.49,5.73 , 3.3 , -1.0953,3.0609,1) ; ctx.closePath() ; } , anchor: [8.5,20.5] , xoffs: -1 } , { name: 'Danger' , tcxname: 'Danger' , fitname: 5 , draw: function(ctx) { var θ = Math.atan2(17,7) , π = Math.PI ; ctx.moveTo(15,20) ; ctx.arc(12.5,20,2.5,0,2*π) ; ctx.moveTo(12.5,15.5) ; ctx.lineTo(9,7) ; ctx.arc(12.5,7-49/34,7*Math.sqrt(49/(17*17)+1)/2,π/2+θ,5*π/2-θ) ; ctx.closePath() ; } , anchor: [12.5,21.5] } , { name: 'Food' , tcxname: 'Food' , fitname: 4 , draw: function(ctx) { var θ = Math.acos(0.65/3.05) , π = Math.PI ; ctx.moveTo(6.5, 1.5) ; ctx.lineTo(6.5, 8.5) ; ctx.lineTo(8.5, 8.5) ; ctx.lineTo(8.5, 1.5) ; ctx.lineTo(8.5, 8.5) ; ctx.lineTo(10.5, 8.5) ; ctx.lineTo(10.5, 1.5) ; ctx.lineTo(10.5, 8.5) ; ctx.lineTo(12.5, 8.5) ; ctx.lineTo(12.5, 1.5) ; ctx.lineTo(12.5, 8.5) ; ctx.arc(9.45,8.5,3.05,0,θ) ; ctx.lineTo(10.5,21.5) ; ctx.arc(9.5,21.5,1,0,π) ; ctx.lineTo(9,11.5) ; ctx.arc(9.55,8.5,3.05,π-θ,π) ; ctx.closePath() ; } , anchor: [9.5,23] , xoffs:3 } , { name: 'Water' , tcxname: 'Water' , fitname: 3 , draw: function(ctx) { var π = Math.PI ; ctx.moveTo(0.5,8.5) ; ctx.lineTo(5.5,8.5) ; ctx.arc(7.5,8.5,2,π,3*π/2) ; ctx.lineTo(7.5, 3.5) ; ctx.lineTo(5.5, 3.5) ; ctx.arc(5.5,2.5,1,π/2,3*π/2) ; ctx.lineTo(11.5, 1.5) ; ctx.arc(11.5,2.5,1,3*π/2,5*π/2) ; ctx.lineTo(9.5, 3.5) ; ctx.lineTo(9.5, 6.5) ; ctx.arc(9.5,8.5,2,3*π/2,2*π) ; ctx.lineTo(14.5, 8.5) ; ctx.arc(14.5,13.5,5,3*π/2,2*π) ; ctx.lineTo(19.5, 15.5) ; ctx.lineTo(16.5, 15.5) ; ctx.lineTo(16.5, 13.5) ; ctx.arc(14.5,13.5,2,2*π,3*π/2,true) ; ctx.lineTo(11.5, 11.5) ; ctx.arc(8.5,8.5,3*Math.sqrt(2),π/4,3*π/4) ; ctx.lineTo(0.5, 11.5) ; ctx.closePath() ; ctx.moveTo(18,20.5) ; ctx.arc(18,19,1.5,0,2*Math.PI) ; } , anchor: [18,22] , xoffs:2 } , { name: 'Summit' , tcxname: 'Summit' , fitname: 1 , draw: function(ctx) { ctx.moveTo(4.5, 17.5) ; ctx.bezierCurveTo(9.83, 7.83, 15.17, 7.83, 20.5, 17.5) ; ctx.closePath() ; ctx.moveTo(12.5, 21.5) ; ctx.lineTo(12.5, 2.5) ; ctx.lineTo(17, 4.5) ; ctx.lineTo(12.5, 6.5) ; } , anchor: [12.5,21] } , { name: 'Valley' , tcxname: 'Valley' , fitname: 2 , draw: function(ctx) { ctx.moveTo(4.5, 17.5) ; ctx.lineTo(4.5, 7.5) ; ctx.bezierCurveTo(9.83, 18.5, 15.17, 18.5, 20.5, 7.5) ; ctx.lineTo(20.5, 17.5) ; ctx.closePath() ; ctx.moveTo(12.5, 21.5) ; ctx.lineTo(12.5, 2.5) ; ctx.lineTo(17, 4.5) ; ctx.lineTo(12.5, 6.5) ; } , anchor: [12.5,22] } , { name: 'Turn back' , tcxname: 'Generic' , fitname: 23 , draw: function(ctx) { ctx.moveTo(12.5,2.5) ; ctx.lineTo(10,16.5) ; ctx.lineTo(6,16.5) ; ctx.lineTo(12.5,21.5) ; ctx.lineTo(19,16.5) ; ctx.lineTo(15,16.5) ; ctx.closePath() ; } , anchor: [12.5,22.5] } , { name: 'Viewpoint' , tcxname: 'Generic' , fitname: 38 , draw: function(ctx) { var i , π = Math.PI , θ , c , s ; ctx.arc(11,19,2.5,0,2*π) ; for(i=0;i<4;i++) { θ = π/4 - i*π/6 ; s = Math.sin(θ-π/30) ; c = Math.cos(θ-π/30) ; ctx.moveTo(11+5*s,19-5*c) ; ctx.lineTo(11+13.5*s,19-13.5*c) ; s = Math.sin(θ+π/30) ; c = Math.cos(θ+π/30) ; ctx.lineTo(11+13.5*s,19-13.5*c) ; ctx.lineTo(11+5*s,19-5*c) ; ctx.closePath() ; } } , anchor: [11,23] } , { name: 'Steep incline' , tcxname: 'Generic' , fitname: 43 , draw: function(ctx) { ctx.moveTo(4,20) ; ctx.lineTo(18,6) ; ctx.lineTo(18,20) ; ctx.lineTo(13,20) ; ctx.lineTo(11,22) ; ctx.lineTo(9,20) ; ctx.closePath() ; } , anchor: [11,23] } , { name: 'Info' , tcxname: 'Generic' , fitname: 53 , draw: function(ctx) { var π = Math.PI ; ctx.arc(9,4.5,2.5,0,2*π) ; ctx.moveTo(9,22.5) ; ctx.lineTo(7,20) ; ctx.lineTo(3.5,20) ; ctx.lineTo(3.5,18.5) ; ctx.lineTo(5,18.5) ; ctx.arc(5,17,1.5,π/2,0,1) ; ctx.lineTo(6.5,12) ; ctx.arc(5,12,1.5,0,-π/2,1) ; ctx.lineTo(3.5,10.5) ; ctx.lineTo(3.5,8.5) ; ctx.lineTo(11.5,8.5) ; ctx.lineTo(11.5,17) ; ctx.arc(13,17,1.5,π,π/2,1) ; ctx.lineTo(14.5,18.5) ; ctx.lineTo(14.5,20.5) ; ctx.lineTo(11,20.5) ; ctx.closePath() ; } , anchor: [9,23] } , { name: 'Obstacle' , tcxname: 'Generic' , fitname: 46 , draw: function(ctx) { ctx.moveTo(7, 15.5) ; ctx.lineTo(10, 15.5) ; ctx.lineTo(15, 9.5) ; ctx.lineTo(12, 9.5) ; ctx.closePath() ; ctx.moveTo(1.5, 18.5) ; ctx.lineTo(3.5, 18.5) ; ctx.lineTo(3.5, 15.5) ; ctx.lineTo(1.5, 15.5) ; ctx.closePath() ; ctx.moveTo(1.5, 8.5) ; ctx.lineTo(3.5, 8.5) ; ctx.lineTo(3.5, 6.5) ; ctx.lineTo(1.5, 6.5) ; ctx.closePath() ; ctx.moveTo(3.5, 21.5) ; ctx.lineTo(3.5, 4.5) ; ctx.lineTo(5, 1.5) ; ctx.lineTo(6.5, 4.5) ; ctx.lineTo(6.5, 21.5) ; ctx.closePath() ; ctx.moveTo(6.5, 18.5) ; ctx.lineTo(15.5, 18.5) ; ctx.lineTo(15.5, 15.5) ; ctx.lineTo(6.5, 15.5) ; ctx.closePath() ; ctx.moveTo(6.5, 9.5) ; ctx.lineTo(15.5, 9.5) ; ctx.lineTo(15.5, 6.5) ; ctx.lineTo(6.5, 6.5) ; ctx.closePath() ; ctx.moveTo(15.5, 21.5) ; ctx.lineTo(15.5, 4.5) ; ctx.lineTo(17, 1.5) ; ctx.lineTo(18.5, 4.5) ; ctx.lineTo(18.5, 21.5) ; ctx.closePath() ; ctx.moveTo(18.5, 18.5) ; ctx.lineTo(20.5, 18.5) ; ctx.lineTo(20.5, 15.5) ; ctx.lineTo(18.5, 15.5) ; ctx.closePath() ; ctx.moveTo(18.5, 9.5) ; ctx.lineTo(20.5, 9.5) ; ctx.lineTo(20.5, 6.5) ; ctx.lineTo(18.5, 6.5) ; ctx.closePath() ; ctx.moveTo(10, 18.5) ; ctx.lineTo(11, 22) ; ctx.lineTo(12, 18.5) ; ctx.closePath() ; } , anchor: [11,23] } , ] ; var iconic = { gpxnames: [ 'Left_slight' , 'Right_slight' , 'Left_sharp' , 'Right_sharp' , 'Left_fork' , 'Right_fork' , 'Stay_left' , 'Stay_right' , 'U-turn' , 'U turn' ] , gpxtrans: [ 'Slight left' , 'Slight right' , 'Sharp left' , 'Sharp right' , 'Left fork' , 'Right fork' , 'Slight left' , 'Slight right' , 'Turn back' , 'Turn back' ] , } ; function caps(x) { if(x) return x.charAt(0).toUpperCase() + x.substring(1) ; else return x ; } /* ------------------------------- pts 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 label 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. label null, map null, handlers inactive, but marker non-null // o. label non-null, map null, handlers inactive, marker non-null // o. label non-null, map non-null, handlers active, marker non-null // the state in which label 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 pttype(pos,h,t) { if(!pos) pos = null ; if(h==undefined) h = null ; // altitude (m) if(t==undefined) t = null ; // a Date object this.pos = pos ; // a google maps pos this.h = h ; this.labelmarker = this.photomarker = this.label = this.t = null ; this.selfunc = this.caption = this.desc = null ; this.map = this.delta = this.type = this.point = this.fitlabel = null ; if(isvaliddate(t)) this.t = t ; else this.t = null ; this.photo = [] ; this.clickhandler = this.photohandler = null ; } function correctlabel(lab,caption,t) // t = output type { var ind,label=caps(lab),k ; if(!label) { if(caption) label = 'Generic' ; return { label , caption } ; } for(ind=waymarks.length-1;ind>=0&&waymarks[ind].name!=label;ind--) ; if(ind<0) { ind = iconic.gpxnames.indexOf(label) ; if(ind>=0) label = iconic.gpxtrans[ind] ; else if(label) label = 'Generic' ; } // at this point ind is -ve if label was absent from both names and gpxnames if(t=='tcx'&&ind>0) { label = waymarks[ind].tcxname ; if(label!=waymarks[ind].name) ind = -1 ; } else if(t=='gpx') { k = iconic.gpxtrans.indexOf(label) ; if(k>=0) label = iconic.gpxnames[k] ; } if(ind<0&&!caption) caption = caps(lab) ; return { label , caption } ; } /* ------------------------------ props structure --------------------------- */ function routetype(type) { this.desc = this.title = this.list = this.filename = this.stats = null ; this.tlink = this.index = this.stars = this.srcid = this.origin = null ; this.date = this.optim = this.info = this.arrows = this.bounds = null ; this.line = this.dots = this.dothandler = this.indexmode = this.tmode = null ; this.ttype = null ; this.colour = this.hue = "#ff0000" ; this.photo = [] ; this.smallphoto = [] ; this.pts = [] ; this.geo = [] ; this.shades = [] ; if(type=='metaindex') { this.type = type ; this.level = 2 ; } else if(type=='index'||type=='tour'||type=='segments') { this.type = type ; this.level = 1 ; } else if(type=='uindex') { this.type = 'index' ; this.level = 1 ; } else { this.type = null ; this.level = 0 ; } } routetype.prototype.clone = function() { var c = clone(this) , i ; c.photo = clone(this.photo) ; c.smallphoto = new Array(this.smallphoto.length) ; for(i=0;i<this.smallphoto.length;i++) c.smallphoto[i] = clone(this.smallphoto[i]) ; return c ; } /* -------------------------------------------------------------------------- */ function getlatlong(node,latstr,lonstr) { var lat=null,lon=null,nodes=node.childNodes,nodeno ; for(nodeno=0;nodeno<nodes.length;nodeno++) { node = nodes[nodeno] ; if(node.nodeName==latstr) lat = xmlfloat(node) ; else if(node.nodeName==lonstr) lon = xmlfloat(node) ; } if(lat==null||lon==null) return null ; else return { lat:lat , lng:lon } ; } function deltify(pts) { var i,len=pts.length ; for(pts[len-1].delta=null,i=0;i<len-1;i++) pts[i].delta = dist(pts[i].pos,pts[i+1].pos) ; } function recurse(seg,opt,parms) { var i , pts = seg.pts , len = pts.length , geo = seg.geo , gen = geo.length ; var col = (opt=='colour'?hexify(parms):null) , pt ; if(opt=='npix'&&!parms) { render.npix = 0 ; parms = 1 ; } // zeroise at top if(seg.level==0) { if(opt=='arrows') seg.arrows = markarrows(genarrows(seg)) ; else if(opt=='north') { for(i=0;i<len;i++) { if(!parms.pos||pts[i].pos.lat>parms.pos.lat) parms.pos = pts[i].pos ; } for(i=0;i<gen;i++) { if(!parms.pos||geo[i].pos.lat>parms.pos.lat) parms.pos = geo[i].pos ; } } else if(opt=='draw4') draw(seg,4,1) ; else if(opt=='draw') draw(seg) ; else if(opt=='geodraw') for(i=0;i<seg.geo.length;i++) geodraw(seg.geo[i],seg.colour) ; else if(opt=='setmap') { for(i=0;i<len;i++) pts[i].setmap(parms.map,parms.sel) ; for(i=0;i<gen;i++) geo[i].setmap(parms.map,parms.sel) ; } else if(opt=='genphotomark') { for(i=0;i<len;i++) if(pts[i].photo.length) genphotomark(pts[i],0) ; for(i=0;i<gen;i++) if(geo[i].photo.length) genphotomark(geo[i],0) ; } else if(opt=='npix') { for(i=0;i<len;i++) render.npix += pts[i].photo.length ; for(i=0;i<gen;i++) render.npix += geo[i].photo.length ; } else if(opt=='genwaymark') { for(i=0;i<len+gen;i++) { if(i<len) pt = pts[i] ; else pt = geo[i] ; if(pt.label) { pt.setlabel(pt.label,pt.caption,pt.desc) ; pt.setlabelmap() ; } } } else if(opt=='undraw') undraw(seg) ; else if(opt=='deltas') deltify(pts) ; else if(opt=='getbounds') segbounds(parms,seg) ; else if(opt=='listsegs') parms.push(dsseg(pts)) ; else if(opt=='colour') { seg.hue = parms ; seg.colour = col ; } else if(opt=='obliterate') // undraws segment and all its points { for(i=0;i<len;i++) pts[i].unmap() ; for(i=0;i<gen;i++) geo[i].unmap() ; undraw(seg) ; } else if(opt=='hidearrows') { if(seg.arrows) map.removeLayer(seg.arrows) ; } else if(opt=='showarrows') { if(seg.arrows) seg.arrows.addTo(map) ; } else alert(opt+' is an unrecognised option') ; } else for(i=0;i<len;i++) recurse(pts[i],opt,parms) ; if(opt=='npix') return render.npix ; } function flipseg(pts) { var i,j,x,len=pts.length,lim ; for(lim=Math.floor(len/2),i=0;i<lim;i++) { j = (len-1)-i ; x = pts[i] ; pts[i] = pts[j] ; pts[j] = x ; } for(i=0;i<len-1;i++) pts[i].delta = pts[i+1].delta ; pts[len-1].delta = null ; } /* -------------------------------- getstats ------------------------------- */ function getstats(route) { var stats = routestats([route]) ; route.photo = stats.pix ; route.date = stats.date ; route.stats = [ 1 , stats.dist , stats.asc , stats.desc , stats.minalt , stats.maxalt ] ; } function getmetastats(route) { var i,s1,s2,n,x,s ; for(x=s2=s1=n=i=0;i<route.pts.length;i++) { s1 += route.pts[i].stats[1] ; s2 += route.pts[i].stats[2] ; if((s=route.pts[i].stars)) { x += s*s ; n += 1 ; } } route.stats = [ route.pts.length , s1 , s2 ] ; if(n) route.stars = Math.floor(0.5+Math.sqrt(x/n)) ; else route.stars = null ; } function diststring(val) { var d = (val/100).toFixed(0) , n = d.length ; return d.substring(0,n-1) + '.' + d.charAt(n-1) ; } function kmstring(val) { if(val>9999) return diststring(val) + 'km' ; else return val.toFixed(0) + 'm' ; } function prettystats(title,stats,opt) // opt requests a closing full stop { var div = domcreate('div') , s ; if(title) domadd(div,domcreate('b',title),': ') ; if(stats.length==3) { domadd(div,inject(M.numroutes,stats[0].toFixed(0))+'; ') ; domadd(div,prettynum(stats[1]/1000,0,'km; ↑')) ; domadd(div,prettynum(stats[2],stats[2]>9999,opt?'m.':'m')) ; } else { domadd(div,M.distance+' ') ; if(stats[1]>1999) { domadd(div,domcreate('span',diststring(stats[1]), 'style','padding-right:1.5px')) ; s = 'k' ; } else { domadd(div,domcreate('span',stats[1].toFixed(0), 'style','padding-right:1.5px')) ; s = '' ; } if(!stats[2]) return domadd(div,domcreate('span',s+(opt?'m.':'m'))) ; s += 'm; ↑' + stats[2].toFixed(0) ; domadd(div,domcreate('span',s,'style','padding-right:1.5px')) ; s = 'm ↓' + stats[3].toFixed(0) ; domadd(div,domcreate('span',s,'style','padding-right:1.5px')) ; s = 'm; ' + M.altitude + ' ' + stats[4].toFixed(0) + '-' + stats[5].toFixed(0) ; domadd(div,domcreate('span',s,'style','padding-right:1.5px'),opt?'m.':'m') ; } return div ; } // the arg to routestats is an array of items each of which has a pts field function routestats(segments) { var tlast=null,err=0,dd=0,nnull=0,maxsep=0,nlabels=0,des=0,asc=0,ntimes=0 ; var nowpts=0,s0,s1,oalt,alt,otime,time,sep,minalt=null,maxalt=null,date=null ; var photo=[],ngeo ; for(s0=0;s0<segments.length;nowpts+=segments[s0].pts.length,s0++) for(oalt=null,s1=0;s1<segments[s0].pts.length;otime=time,s1++) { if((alt=segments[s0].pts[s1].h)==null) nnull += 1 ; else { if(oalt!=null) { if(alt>oalt) asc += alt-oalt ; else des += oalt - alt ; } oalt = alt ; if(maxalt==null||alt>maxalt) maxalt = alt ; if(minalt==null||alt<minalt) minalt = alt ; } if(segments[s0].pts[s1].label) nlabels += 1 ; photo = photo.concat(segments[s0].pts[s1].photo) ; time = segments[s0].pts[s1].t ; if(time!=null) { if(date==null) date = time.toDateString() ; time = time.getTime() ; ntimes += 1 ; } if(tlast!=null&&time!=null&&time<tlast) err = 1 ; // out of order if(time!=null) tlast = time ; if(s1) { dd += ( sep = segments[s0].pts[s1-1].delta ) ; if(sep>maxsep) maxsep = sep ; } } for(ngeo=s0=0;s0<segments.length;ngeo+=segments[s0].geo.length,s0++) for(s1=0;s1<segments[s0].geo.length;s1++) photo = photo.concat(segments[s0].geo[s1].photo) ; return { dist:dd , asc , desc:des , outoforder:err , nnull , npts:nowpts , maxsep , nlabels , pix:photo , ntimes , maxalt , minalt , date , ngeo } } /* --------------------------------- flatten -------------------------------- */ function flatten(segments,geoopt) { var p,npoint,d,s0,s1,s2,pos,pts,opos,iter ; if(segments[0].type=='tour'||segments[0].type=='segments') segments = segments[0].pts ; for(npoint=s0=0;s0<segments.length;npoint+=segments[s0].pts.length,s0++) ; if(geoopt) for(s0=0;s0<segments.length;npoint+=segments[s0].geo.length,s0++) ; p = new Array(npoint) ; for(d=npoint=s0=0;s0<segments.length;opos=pos,s0++) for(iter=0;iter<(geoopt?2:1);iter++) { s2 = iter?'geo':null ; pts = iter?segments[s0].geo:segments[s0].pts ; for(s1=0;s1<pts.length;s1++) { pos = pts[s1].pos ; if(geoopt) d = null ; else if(s1>0) d += pts[s1-1].delta ; else if(npoint) d += dist(opos,pos) ; p[npoint++] = { pos , h:pts[s1].h , t:pts[s1].t , sel:{segno:s0,ptno:s1,type:s2} , d } ; } } return p ; } /* -------------------------------------------------------------------------- */ // this function collapses almost coincident points onto a single point. // it is invoked on input (perhaps out of the fear that exact coincidence will // cause the optimisation to blow up?) and again on output (perhaps from a fear // that the user will have interpolated so many points that other applications // get confused). // rewritten nov '19 (a) to avoid object.assign for safari, (b) to collapse // more than 2 nearly coincident points. at the same time i removed the call // to setpos (which seemed incorrect on output), writing a setsegpos() // function to do the job on input; and I made the arg to setpos() optional. function squash(pts) { var i,idash,j,k,pt, ilen=pts.length , npts = new Array(ilen) ; // arithmetic precision of lat/long at the equator is 1.1m (5dp) for(j=i=0;i<ilen;i=idash) { for(idash=i+1;idash<ilen&&dist(pts[idash].pos,pts[i].pos)<1.2;idash++) ; if(idash==i+1) { npts[j++] = pts[i] ; continue ; } pt = clone(pts[i]) ; for(nh=h=0,pt.photo=[],k=i;k<idash;k++) { if(pts[k].photo.length) { pt.photo = pt.photo.concat(pts[k].photo) ; pt.photomarker = pts[k].photomarker ; } if(pts[k].label) { pt.label = pts[k].label ; pt.caption = pts[k].caption ; pt.desc = pts[k].desc ; pt.marker = pts[k].marker ; } if(pts[k].h&&pt.h==null) pt.h = pts[k].h ; if(pts[k].t&&!pt.t) pt.t = pts[k].t ; } npts[j++] = pt ; } npts.length = j ; return npts ; } /* -------------------------------------------------------------------------- */ function gentimes(ipts) { var maxspeed=3600.0/50 ; var i,j,k,date,tlast,tdist,ttime,flag,i,otime,sep,di,dk,h,seen ; var msecs = new Array(ipts.length) , distance = new Array(ipts.length) ; for(seen=date=tlast=null,tdist=ttime=i=flag=0;i<ipts.length;otime=time,i++) { if(ipts[i].t) time = ipts[i].t ; else time = null ; if(time) { if(!date) date = time.toDateString() ; msecs[i] = time = time.getTime() ; seen = 1 ; } else msecs[i] = null ; if(tlast!=null&&time!=null&&time<tlast) flag = 1 ; // out of order if(time!=null) tlast = time ; if(i) { sep = ipts[i-1].delta ; distance[i] = distance[i-1] + sep ; if(time!=null&&otime!=null) { tdist += sep ; ttime += time - otime ; } } else distance[i] = 0 ; } // fill in missing times if(seen==0||tdist==0||flag!=0) for(msecs[0]=0,hspeed=3600.0/10,vspeed=3600/0.4,i=1;i<ipts.length;i++) { h = ipts[i-1].delta*hspeed ; if(ipts[i-1].h!=null&&ipts[i].h!=null) h += (ipts[i].h-ipts[i-1].h)*vspeed ; if(h<ipts[i-1].delta*maxspeed) h = ipts[i-1].delta*maxspeed ; msecs[i] = msecs[i-1] + h ; } else for(date=null,i=0;i<ipts.length;i=k) { for(;i<ipts.length&&ipts[i].t!=null;i++) ; // advance to null if(i==ipts.length) break ; for(k=i+1;k<ipts.length&&ipts[k].t==null;k++) ; // advance to non-null if(i==0) for(time=msecs[k],j=i;j<k;j++) msecs[j] = time - (distance[k]-distance[j])*ttime/tdist ; else if(k==ipts.length) for(time=msecs[i-1],j=i;j<ipts.length;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) ; } return { t:msecs , d:distance } } /* -------------------------------------------------------------------------- */ // route.stats = // [ 1 , stats.dist , stats.asc , stats.desc , stats.minalt , stats.maxalt ] ; function parsestats(stats) { var k ,tok ; function unspace(str) { var k,r='',c ; for(k=0;k<str.length;k++) { c = str.charAt(k) ; if(c!=' '&&c!='m'&&c!='k'&&c!='\u202f') r += str.charAt(k) ; } return parseFloat(r) ; } if(stats.charAt(0)>=0&&stats.charAt(0)<=9) // "11 routes; 339km; ↑11 859m" { var r = [ null , null , null ] ; k = stats.indexOf(' ') ; // ^ r[0] = parseInt(stats.substring(0,k)) ; stats = stats.substring(k+1) ; // "routes; 339km; ↑11 859m" k = stats.indexOf(' ') ; // ^ stats = stats.substring(k+1) ; // "339km; ↑11 859m" k = stats.indexOf(';') ; // ^ r[1] = 1000 * unspace(stats.substring(0,k)) ; // "339km" k = stats.indexOf('↑') ; // ^ if(k>=0) r[2] = unspace(stats.substring(k+1)) ; // "11 859m" return r ; } else // "Distance 7.2km; altitude 1559-1766m; ↑408m ↓322m" { var r = [ 1 , null , null , null , null , null ] ; k = stats.indexOf(' ') ; // ^ stats = stats.substring(k+1) ; // "7.2km; altitude 1559-1766m; ↑408m ↓322m" k = stats.indexOf(';') ; // ^ if(stats.substring(k-2,k)=='km') r[1] = 1000 * unspace(stats.substring(0,k-2)) ; // "7.2km" else r[1] = unspace(stats.substring(0,k-1)) ; // "7200m" k = stats.indexOf('↑') ; // ^ if(k<0) return r ; tok = stats.substring(k+1) ; r[2] = unspace(tok.substring(0,tok.indexOf('m'))) ; k = stats.indexOf('↓') ; // ^ tok = stats.substring(k+1) ; r[3] = unspace(tok.substring(0,tok.indexOf('m'))) ; k = stats.indexOf('-') ; // ^ if(k<0) k = stats.indexOf('–') ; // ^ tok = stats.substring(k+1) ; r[5] = unspace(tok.substring(0,tok.indexOf('m'))) ; stats = stats.substring(0,k) ; k = stats.lastIndexOf(' ') ; // ^ r[4] = unspace(stats.substring(k+1)) ; return r ; } } /* -------------------------------------------------------------------------- */ function getgpxpt(node) { var lat=null,lon=null,alt=null,time=null,j,nodeno,nodes,photo=[],href,extn ; var pos=null,ind,caption=null,label=null,desc=null,val,sym=null,pt ; var imgextns = [ 'jpeg' , 'jpg' , 'png' , 'gif' , 'webp' , 'mp4' , 'mpeg' ] ; lat = parseFloat(node.getAttribute('lat')) ; lon = parseFloat(node.getAttribute('lon')) ; if(lat&&lon) pos = { lat:lat , lng:lon } ; else return null ; nodes = node.childNodes ; for(nodeno=0;nodeno<nodes.length;nodeno++) { node = nodes[nodeno] ; if(node.nodeName=='ele') alt = xmlfloat(node) ; else if(node.nodeName=='time') // '1970-01-01T03:040:08Z' time = new Date(normalise(node.textContent)) ; else if(node.nodeName=='link') { href = normalise(node.getAttribute('href')) ; j = href.lastIndexOf('.') ; if(j>=0&&imgextns.indexOf(href.substring(j+1))>=0) photo.push(href) ; } else if(node.nodeName=='type'||node.nodeName=='sym') { val = caps(normalise(node.textContent)) ; for(ind=waymarks.length-1;ind>=0&&waymarks[ind].name!=val;ind--) ; if(ind<0) { if((ind=iconic.gpxnames.indexOf(val))>=0) val = iconic.gpxtrans[ind] ; else val = 'Generic' ; } if(node.nodeName=='type') label = val ; else sym = val ; } else if(node.nodeName=='name') caption = normalise(node.textContent) ; else if(node.nodeName=='desc') desc = dexmlify(normalise(node.textContent)) ; } pt = new pttype(pos,alt,time) ; pt.photo = photo ; if(sym) label = sym ; val = correctlabel((desc&&!label)?'Generic':label,caption,'rte') ; pt.label = val.label ; pt.caption = val.caption ; pt.desc = desc ; return pt ; } /* -------------------------------------------------------------------------- */ function gettcxpt(node) { var lat=null,lon=null,alt=null,time=null,nodeno,nodes=node.childNodes ; var pos=null,caption=null,label=null,desc=null,val,pt,i ; for(nodeno=0;nodeno<nodes.length;nodeno++) { node = nodes[nodeno] ; if(node.nodeName=='AltitudeMeters') alt = xmlfloat(node) ; else if(node.nodeName=='Time') // '1970-01-01T03:040:08Z' time = new Date(normalise(node.textContent)) ; else if(node.nodeName=='Position') pos = getlatlong(node,'LatitudeDegrees','LongitudeDegrees') ; else if(node.nodeName=='PointType') { label = caps(normalise(node.textContent)) ; for(i=waymarks.length-1;i>=0&&waymarks[i].name!=label;i--) ; if(i<0) label = 'Generic' ; } else if(node.nodeName=='Name') caption = normalise(node.textContent) ; else if(node.nodeName=='Notes') desc = dexmlify(normalise(node.textContent)) ; } if(!pos) return null ; pt = new pttype(pos,alt,time) ; val = correctlabel((desc&&!label)?'Generic':label,caption,'rte') ; pt.label = val.label ; pt.caption = val.caption ; pt.desc = desc ; return pt ; } /* -------------------------------------------------------------------------- */ // https://stackoverflow.com/questions/17374893/how-to-extract-... // ...floating-numbers-from-strings-in-javascript function readcsv(str) { var i,n,p=new routetype() , regex = /[+-]?\d+(\.\d+)?/g ; var x = str.match(regex).map(function(v) { return parseFloat(v) ; }) ; n = Math.floor(x.length/2) ; p.pts = new Array(n) ; for(i=0;i<n;i++) p.pts[i] = { lat:x[2*i] , lng:x[2*i+1] } ; return p ; } /* -------------------------------------------------------------------------- */ function readkml(xmldoc) { var xmlcoords,xmlpts,i,lat,lon,h,route=new routetype('segments'),p,layerno ; // get the route title xmlcoords = xmldoc.getElementsByTagName('name') ; if(xmlcoords.length>0&&xmlcoords[0].childNodes.length>0) route.title = normalise(xmlcoords[0].childNodes[0].textContent) ; // get the route description xmlcoords = xmldoc.getElementsByTagName('description') ; if(xmlcoords.length>0&&xmlcoords[0].childNodes.length>0) route.desc = normalise(xmlcoords[0].childNodes[0].textContent) ; // loop over the track points to get the coords xmlcoords = xmldoc.getElementsByTagName('coordinates') ; for(layerno=0;layerno<xmlcoords.length;layerno++) { xmlpts = xmlcoords[layerno].textContent.split(/[,\s]+/).filter(Boolean) ; p = new Array(xmlpts.length/3) ; for(i=0;i<p.length;i++) { lat = parseFloat(xmlpts[3*i+1]) ; lon = parseFloat(xmlpts[3*i]) ; h = parseFloat(xmlpts[3*i+2]) ; if(!h) h = null ; p[i] = new pttype({lat:lat,lng:lon},h) ; } route.pts[layerno] = new routetype() ; route.pts[layerno].pts = p ; } return route ; } /* -------------------------------------------------------------------------- */ function readtcx(xmldoc) { var nodeno,i,j,k,node,nsegment,course,r,nodes,photo=[],coursepts , flag ; var rp = new routetype('segments') ; // get global title & desc course = xmldoc.getElementsByTagName('TrainingCenterDatabase')[0].childNodes ; for(i=0;i<course.length;i++) if(course[i].nodeName=='Courses') for(nodes=course[i].childNodes,j=0;j<nodes.length;j++) { if(nodes[j].nodeName=='Name') rp.title = normalise(nodes[j].textContent) ; else if(nodes[j].nodeName=='Notes') rp.desc = normalise(nodes[j].textContent) ; } // maybe the tracks hang from courses, maybe from laps course = xmldoc.getElementsByTagName('Track') ; if(course.length==0) { alert(M.notracks) ; throw '' ; } // loop over courses course = xmldoc.getElementsByTagName(course[0].parentNode.nodeName) ; nsegment = course.length ; rp.pts = new Array(nsegment) ; for(i=0;i<nsegment;i++) rp.pts[i] = new routetype() ; for(i=0;i<nsegment;i++) { // get properties rp.pts[i].pts = new Array() ; // get title and trackpoints for(j=0;j<course[i].childNodes.length;j++) if(course[i].childNodes[j].nodeName=='Name') { rp.pts[i].title = normalise(course[i].childNodes[j].textContent) ; if(!rp.title) rp.title = rp.pts[i].title ; } else if(course[i].childNodes[j].nodeName=='Notes') { rp.pts[i].desc = normalise(course[i].childNodes[j].textContent) ; if(!rp.desc) rp.desc = rp.pts[i].desc ; } else if(course[i].childNodes[j].nodeName=='Track') { for(k=0;k<course[i].childNodes[j].childNodes.length;k++) { node = course[i].childNodes[j].childNodes[k] ; if(node.nodeName=='Trackpoint'&&(r=gettcxpt(node))) rp.pts[i].pts.push(r) ; } } // get coursepoints coursepts = new Array() ; for(j=0;j<course[i].childNodes.length;j++) if(course[i].childNodes[j].nodeName=='CoursePoint') if(r=gettcxpt(course[i].childNodes[j])) coursepts.push(r) ; // are the waypoints in order? if(isvaliddate(rp.pts[i].pts[0].t)) flag = 0 ; else flag = 1 ; for(j=1;flag==0&&j<rp.pts[i].pts.length;j++) if( !isvaliddate(rp.pts[i].pts[j].t) || rp.pts[i].pts[j].t.getTime()<rp.pts[i].pts[j-1].t.getTime() ) flag = 1 ; if(flag==0) // j.e. if all points have times and the times are in order { // put the course points in position for(k=j=0;j<coursepts.length;j++) if(pt=coursepts[j],isvaliddate(pt.t)) { pt.setlabel(pt.label,pt.caption,pt.desc) ; rp.pts[i].pts.push(pt) ; flag = -1 ; } if(flag<0) rp.pts[i].pts.sort(function(a,b){return a.t.getTime()-b.t.getTime();}) ; } // merge coincident points rp.pts[i].pts = squash(rp.pts[i].pts) ; // squeeze out coincident points } if(rp.desc) rp.desc = dexmlify(rp.desc) ; return rp ; } /* -------------------------------------------------------------------------- */ function readgpx(xmldoc) { var xmlcoords,i,j,r,rp=new routetype() , kids ; // loop over the track points to get the coords xmlcoords = xmldoc.getElementsByTagName('trkpt') ; if(!xmlcoords.length) xmlcoords = xmldoc.getElementsByTagName('rtept') ; for(i=0;i<xmlcoords.length;i++) if((r=getgpxpt(xmlcoords[i]))) { if(r.label) r.setlabel(r.label,r.caption) ; rp.pts.push(r) ; } rp.pts = squash(rp.pts) ; // squeeze out coincident points // loop over the waypoints to get the geo points xmlcoords = xmldoc.getElementsByTagName('wpt') ; for(i=0;i<xmlcoords.length;i++) if(r=getgpxpt(xmlcoords[i])) { if(r.label) r.setlabel(r.label,r.caption) ; rp.geo.push(r) ; } // loop over gpx and tracks to get the title and description for(i=0;i<2&&(!rp.title||!rp.desc);i++) { xmlcoords = xmldoc.getElementsByTagName(i?'trk':'gpx') ; if(xmlcoords.length) for(kids = xmlcoords[0].childNodes,j=0;j<kids.length;j++) if(kids[j].nodeName=='name'&&!rp.title) rp.title = normalise(kids[j].textContent) ; else if(kids[j].nodeName=='desc'&&!rp.desc) rp.desc = dexmlify(normalise(kids[j].textContent)) ; } return rp ; } /* -------------------------------------------------------------------------- */ function getrtept(node) { var lat,lon,alt,time,nodeno,nodes,pt,ty,val ; var pos=null,caption=null,label=null,dt=null ; lat = parseFloat(node.getAttribute('lat')) ; lon = parseFloat(node.getAttribute('lon')) ; if(lat&&lon) pos = { lat:lat , lng:lon } ; else return null ; nodes = node.childNodes ; if(alt=node.getAttribute('h')) alt = parseFloat(alt) ; if(dt=node.getAttribute('dt')) dt = parseFloat(dt) ; if(time=node.getAttribute('t')) time = new Date(time) ; pt = new pttype(pos,alt,time) ; label = normalise(node.getAttribute('label')) ; // simplify white space caption = normalise(node.getAttribute('caption')) ; ty = normalise(node.getAttribute('type')) ; for(nodeno=0;nodeno<nodes.length;nodeno++) if(nodes[nodeno].nodeName=='img') if(nodes[nodeno].getAttribute('type')=='pix') pt.photo.push(normalise(nodes[nodeno].textContent)) ; for(nodeno=0;nodeno<nodes.length;nodeno++) if(nodes[nodeno].nodeName=='desc') pt.desc = normalise(nodes[nodeno].textContent) ; for(nodeno=0;nodeno<nodes.length;nodeno++) if(nodes[nodeno].nodeName=='mark') { label = normalise(nodes[nodeno].getAttribute('label')) ; caption = normalise(nodes[nodeno].getAttribute('caption')) ; } // it would be better if I simply stored label/caption in the point, and left // it for a subsequent process to set up a marker (as I do for photos) val = correctlabel((pt.desc&&!label)?'Generic':label,caption,'rte') ; if(val) pt.setlabel(val.label,val.caption,pt.desc) ; return { pt , dt , geo:(ty=='geo'?1:0)} ; } /* -------------------------------------------------------------------------- */ function getrteprops(nodes,xmlfile,type) { var props = new routetype(type) , node,i,id,txt,item,type,scale ; for(i=0;i<nodes.length;i++) if(nodes[i].nodeType==1) // an element { node = nodes[i] ; id = node.tagName ; if(id=='pt'||id=='route') continue ; if(id=='photolist') alert("Deprecated ‘photolist’ encountered.") ; type = node.getAttribute('type') ; if(id=='optimised') { if(type=='routemaster') props.optim = { already: 1 , origlen: parseInt(node.getAttribute('from')) , len: parseInt(node.getAttribute('to')) , parms: { tol: parseFloat(node.getAttribute('tol')) , maxsep: parseFloat(node.getAttribute('maxsep')) , wppenalty: parseFloat(node.getAttribute('wppenalty')) , vweight: parseFloat(node.getAttribute('vweight')) } } continue ; } if(!node.childNodes||!node.childNodes.length||!node.childNodes[0].length) continue ; text = node.childNodes[0].textContent ; if(id=='desc'&&(type=='routemaster'||type=='utf-8')) { if(type=='utf-8') text = dexmlify(text) ; props.desc = text ; continue ; } text = normalise(text) ; if( id=="imgdef"||id=="photolist"||id=="index"||id=="tracklink" || id=="info"||id=="gallery" ) text = reluri(xmlfile,text) ; if(id=='name') props.title = text ; else if(id=='date') props.date = text ; else if(id=='index'||id=='gallery'||(id=='info'&&type=='routemaster')) props[id] = { href:text , title:normalise(node.getAttribute('title')) } ; else if(id=='tracklink') { props.tlink = text ; props.tmode = normalise(node.getAttribute('mode')) ; props.ttype = type ; } else if(id=='origin'&&(type=='uri'||type=='file')) props.origin = [ text , type ] ; else if(id=='stars'&&type=='1-5') props.stars = parseInt(text) ; else if(id=='stats') props.stats = parsestats(text) ; else if(id=='img'&&type=='pix') photo = photo.concat(text.match(/\S+/g)) ; else if(id=='img'&&(!type||type=='url')) { text = text.match(/\S+/g) ; item = { src: reluri(xmlfile,text[0]) , width: parseInt(node.getAttribute('width')) , height: parseInt(node.getAttribute('height')) , stars: normalise(node.getAttribute('stars'))=='*'?'*':null , srcset: null , scale:null } ; if(text.length>1&&(scale=normalise(node.getAttribute('scale')))) { item.srcset = reluri(xmlfile,text[1]) ; item.scale = scale ; } props.smallphoto.push(item) ; } else if((id=="imgdef"||id=='photolist')&&type=='pix') props.list = text ; else if(id=='srcid'&&type=='utf-8') props.srcid = text ; } return props ; } /* -------------------------------------------------------------------------- */ function readrte(node,xmlfile) { var coords,i,route,t,r,prevt,props,type ; coords = node.childNodes ; type = normalise(node.getAttribute('type')) ; route = getrteprops(coords,xmlfile,type) ; if(route.level==0) { for(prevt=i=0;i<coords.length;i++) if(coords[i].nodeType==1) if(coords[i].tagName=='pt') if(r=getrtept(coords[i])) { t = r.pt.t ; if(t==null&&prevt&&r.dt) r.pt.t = new Date(prevt+r.dt*1000) ; if(r.pt.t==null) prevt = null ; else prevt = r.pt.t.getTime() ; if(r.geo||type=='geo') route.geo.push(r.pt) ; else route.pts.push(r.pt) ; } route.pts = squash(route.pts) ; // squeeze out coincident points } else for(i=0;i<coords.length;i++) if(coords[i].nodeType==1) if(coords[i].tagName=='route') route.pts.push(readrte(coords[i],xmlfile)) ; return route ; } /* -------------------------------------------------------------------------- */ function readfitvalue(input,ind,defitem) { var r=null,format=defitem.format&31,bigend=defitem.bigend ; if(format<3) { r = input[ind] ; if(format==1) { if(r==0x7f) r = null ; } else if(r==0xff) r = null ; } else if(format==3||format==4) { if(bigend) r = (input[ind]<<8) | input[ind+1] ; else r = input[ind] | (input[ind+1]<<8) ; if(format==3) { if(r==0x7fff) r = null ; } else if(r==0xffff) r = null ; } else if(format==5||format==6) { if(bigend) r = (input[ind]<<24) | (input[ind+1]<<16) | (input[ind+2]<<8) | input[ind+3] ; else r = input[ind] | (input[ind+1]<<8) | (input[ind+2]<<16) | (input[ind+3]<<24) ; if(format==5) { if(r==0x7fffffff) r = null ; } else if(r==0xffffffff) r = null ; } if(r!=null&&(format&1)==0) r = r >>> 0 ; // type-convert to unsigned return r ; } function readfitangle(input,ind,defitem) { var r = readfitvalue(input,ind,defitem) ; if(r==null) return null ; else return r * 90.0 / (1<<30) ; } /* -------------------------------------------------------------------------- */ function readfit(rawdata) { var input = new Uint8Array(rawdata) , p = new routetype() ; var ind,flag,tag,i,j,defn,lat,lon,pos,alt,time,item,nitem,prev,bigend ; var gmsgnum,fitlabel ; if(input.length==0) return p ; else if(input.length<12) abend(inject(M.shortfit,input.length)) ; tag = String.fromCharCode(input[8]) + String.fromCharCode(input[9]) + String.fromCharCode(input[10]) + String.fromCharCode(input[11]) ; if(tag!=".FIT") abend(inject(M.unfitfit,tag)) ; // error var utf8decoder = new TextDecoder() , defns = new Array(16) ; var lmsgnum,meaning,label,d,pts,caption,hdrlen,datalen,dt,idash ; function maketext(x,ind,n) { var text = x.slice(ind,ind+n) , k = text.indexOf(0) ; if(k>=0) text = text.slice(0,k) ; return utf8decoder.decode(text) ; } hdrlen = input[0] ; datalen = readfitvalue(input,4,{format:6,bigend:0}) ; if(input.length<hdrlen+datalen) abend(M.shortfit) ; for(prev=0,pts=p.pts,ind=hdrlen;ind<hdrlen+datalen;) { flag = input[ind++] ; if(64&flag) // definition message { if(flag&32) { alert(M.devdata) ; return null ; } lmsgnum = flag & 15 ; bigend = input[ind+1] ; if(bigend) gmsgnum = (input[ind+2]<<8) | input[ind+3] ; else gmsgnum = input[ind+2] | (input[ind+3]<<8) ; nitem = input[ind+4] ; defn = { gmsgnum , lmsgnum , fields:[] , size:0 } ; for(ind+=5,i=0;i<nitem;i++,ind+=3) { item = { meaning:input[ind] , size:input[ind+1] , format:input[ind+2] , bigend } ; defn.fields.push(item) ; defn.size += item.size ; } defns[lmsgnum] = { defn , gmsgnum } ; } else // pts message { if(128&flag) // compressed timestamp { time = (prev&~31) | (flag&31) ; if(time<prev) time += 32 ; lmsgnum = 3 & (flag>>5) ; } else { time = null ; lmsgnum = 15 & flag ; } if(!defns[lmsgnum]) abend(M.missfit) ; // error gmsgnum = defns[lmsgnum].gmsgnum ; defn = defns[lmsgnum].defn ; if(gmsgnum==31) for(i=0;i<defn.fields.length;i++,ind+=item.size) // course { item = defn.fields[i] ; if(item.meaning==5) p.title = maketext(input,ind,item.size) ; } else if(gmsgnum==20||gmsgnum==32) // record or coursepoint { lat = lon = alt = label = caption = pos = null ; for(i=0;i<defn.fields.length;i++,ind+=item.size) { item = defn.fields[i] ; meaning = -1 ; if(gmsgnum==20) { if(item.meaning<3||item.meaning==253) meaning = item.meaning ; } else if(item.meaning==1) meaning = 253 ; else if(item.meaning==2||item.meaning==3) meaning = item.meaning - 2 ; else if(item.meaning==5||item.meaning==6) meaning = item.meaning ; if(meaning==0) lat = readfitangle(input,ind,item) ; else if(meaning==1) lon = readfitangle(input,ind,item) ; else if(meaning==2) { alt = readfitvalue(input,ind,item) ; if(alt) alt = alt/5 - 500 ; else alt = null ; } else if(meaning==5) { fitlabel = readfitvalue(input,ind,item) ; for(j=waymarks.length-1;j>=0&&waymarks[j].fitname!=fitlabel;j--) ; label = waymarks[j<0?0:j].name ; } else if(meaning==6) caption = maketext(input,ind,item.size) ; else if(meaning==253) { prev = readfitvalue(input,ind,item) ; time = 1000 * (prev+631065600) ; } } if(time==null) alert(gmsgnum==20?M.untimedrecord:M.untimedcoursept) ; if(lat!=null&&lon!=null) pos = { lat:lat , lng:lon } ; if(label||(gmsgnum==20&&pos)) { d = new pttype(pos,alt,new Date(time)) ; if(label) d.setlabel(label,caption) ; pts.push(d) ; } } else ind += defn.size ; } } // put the course points in position pts.sort(function(a,b){return a.t.getTime()-b.t.getTime();}) ; // now, if any points lack positions, fill them in for(i=0;i<pts.length;i++) if(!pts[i].pos) { time = pts[i].t.getTime() ; for(ind=-1,j=i-1;j>=0&&!pts[j].pos;j--) ; if(j>=0) ind = j ; for(j=i+1;j<pts.length&&!pts[j].pos;j++) ; if(j<pts.length) if( ind<0 || Math.abs(pts[ind].t.getTime()-time)> Math.abs(pts[j].t.getTime()-time) ) ind = j ; if(ind>=0) pts[i].pos = { lat:pts[ind].pos.lat , lng:pts[ind].pos.lng } ; else alert("no times and some missing positions") ; } // merge coincident points p.pts = squash(pts) ; return p ; } /* -------------------------------------------------------------------------- */ function getgallery() { var pp = null ; if(routeprops&&routeprops.gallery) pp = { href:routeprops.gallery.href , title:routeprops.gallery.title } ; if(imginfo&&imginfo.gallery) pp = { href:imginfo.gallery , title:imginfo.title } ; return pp ; } function gentag(tag,val,sp) { return sp + '<' + tag + '>' + xmlify(val) + '</' + tag + '>\n' ; } function gentypedtag(tag,val,attr,aval,sp) { if(!attr) attr = "type" ; if(!aval) aval = "routemaster" ; val = xmlify(val) ; aval = xmlify(aval) ; var len = 2*tag.length + aval.length + val.length + sp.length + attr.length ; var str = sp + '<' + tag + ' ' + attr + '="' + aval + '">' if(len<70) return str + val + '</' + tag + '>\n' ; else return str + '\n ' + sp + val + '\n' + sp + '</' + tag + '>\n' ; } function gendesc(tag,desc,type,sp) { if(type=='gpx') { if(!gendesc.parser) gendesc.parser = new DOMParser() ; desc = gendesc.parser.parseFromString(desc,'text/html') ; desc = desc.body.textContent || "" ; } if( desc.indexOf('\n')>=0 || desc.indexOf('&')>=0 || desc.indexOf('>')>=0 || desc.indexOf('<')>=0 ) desc = '<![CDATA[' + desc + ']]>' ; if(!type||type=='gpx') return sp + '<'+tag+'>' + desc + '</'+tag+'>\n' ; else return sp + '<'+tag+' type=\"'+type+'\">' + desc + '</'+tag+'>\n' ; } function genindex(val,sp) { var str , len = 10+val.href.length+sp.length ; if(val.title) len += 10 + val.title.length ; str = sp + '<index' ; if(val.title) str += ' title="' + xmlify(val.title) + '"' ; if(len<70) return str + '>' + xmlify(val.href) + '</index>\n' ; else return str + '>\n ' + sp + xmlify(val.href) + '\n' + sp + '</index>\n'; } function formatstats(stats) { if(stats.length==3) return inject(M.numroutes,stats[0].toFixed(0)) + '; ' + (stats[1]/1000).toFixed(0) + 'km; ↑' + stats[2].toFixed(0) + 'm' ; return M.distance + ' ' + kmstring(stats[1]) + '; ↑' + stats[2].toFixed(0) + 'm ↓' + stats[3].toFixed(0) + 'm; ' + M.altitude + ' ' + stats[4].toFixed(0) + '-' + stats[5].toFixed(0) + 'm' ; } function genimgtag(img,sp) { var str = sp + '<img width="' + img.width + '" height="' + img.height + '"' ; if(img.stars) str += ' stars="' + img.stars + '"' ; if(img.scale) str += ' scale="' + img.scale + '"' ; str += '>\n ' + sp + img.src + '\n' ; if(img.srcset) str += ' ' + sp + img.srcset + '\n' ; return str + sp + '</img>\n' ; } function genurl(tag,val,attr,aval,sp) { var len,ltag=tag ; if(attr&&aval) ltag += ' ' + attr + '="' + aval + '"' ; len = tag.length + ltag.length + val.length + sp.length ; if(len<70) return sp + '<' + ltag + '>' + xmlify(val) + '</' + tag + '>\n' ; else return sp + '<' + ltag + '>\n ' + sp + xmlify(val) + '\n' + sp + '</' + tag + '>\n' ; } function genoptim(tag,optim,sp) { var str = sp + '<' + tag + ' type="routemaster" from="' + optim.origlen + '" to="' + optim.len + '"'; if(optim.parms) { str += '\n ' ; if(optim.parms.tol) str += 'tol="' + optim.parms.tol.toFixed(0) + '"' ; if(optim.parms.maxsep) str += ' maxsep="' + optim.parms.maxsep.toFixed(0) + '"' ; if(optim.parms.wppenalty) str += ' wppenalty="' + optim.parms.wppenalty.toFixed(0) + '"' ; if(optim.parms.vweight) str += ' vweight="' + optim.parms.vweight.toFixed(1) + '"' ; } return str + '/>\n' ; } function geninfotag(info,sp) { var len = 30 + sp.length , title , str = sp + '<info type="routemaster"' ; if(info.title) { title = xmlify(info.title) str += ' title="' + title + '"' ; len += 10 + title.length ; } if(len<70) return str + '>' + info.href + '</info>\n' ; else return str + '>\n ' + sp + info.href + '\n' + sp + '</info>\n' ; } function tcxpos(pfx,pos,prec,sp) { return sp + '<' + pfx + 'Position>\n' + gentag('LatitudeDegrees', pos.lat.toFixed(5+prec),sp+' ') + gentag('LongitudeDegrees',pos.lng.toFixed(5+prec),sp+' ') + sp + '</' + pfx + 'Position>\n' ; } function gpxp(pos,prec) { return 'lat="' + pos.lat.toFixed(5+prec) + '" lon="' + pos.lng.toFixed(5+prec) + '"' ; } /* -------------------------------------------------------------------------- */ function decimate(pts) { var i,j,k,d,r = new Array(pts.length) ; for(r[0]=pts[0],k=1,i=0;i<pts.length-1;i=j,k++) { for(d=pts[i].delta,j=i+1;j<pts.length-1;d+=pts[j].delta,j++) if(pts[j].label||pts[j].photo.length||dist(pts[i].pos,pts[j].pos)>2) break ; r[k] = pts[j] ; // j is the first point significantly different than i r[k-1].delta = d ; } r[k-1].delta = null ; r.length = k ; return r ; } /* -------------------------------------------------------------------------- */ function optimise(ipts,parms,val) { var clen = ipts.length ; var stk,nnstk,stk2,i,j,k,m,pi=Math.PI,tol=parms.tol,e,h,buf,n,k0,q ; var opos,oalt,npos,nalt,npt,mpos,malt,arccentre,arctol,pathpos,jump ; var s,sx,sy,sxx,sxy,sxxx,sxxy,sxxxx,u ; var legal,theta,omega,x,y,hyp,tdist,maxtheta,mintheta,d,dh,od,odh,odash ; var bearings=new Array(clen),nstk=new Array(clen),backptr=new Array(clen) ; stk = [ { err:0 , pathpos:1 , prev:-1 } ] ; // 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 pts, // 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 // ipts[k-1]. while(stk[0].pathpos<clen) { pathpos = stk[0].pathpos ; backptr[pathpos-1] = stk[0].prev ; opos = ipts[pathpos-1].pos ; oalt = ipts[pathpos-1].h ; // try extending to pathpos+i for(arctol=null,jump=nnstk=i=0;i<clen-pathpos;i++) { npt = ipts[pathpos+i] ; npos = npt.pos ; nalt = npt.h ; jump += ipts[pathpos+i-1].delta ; if( i>0 && parms.maxjump && jump>parms.maxjump ) break ; if(i==0) hyp = ipts[pathpos-1].delta ; else { hyp = dist(opos,npos) ; if(parms.maxsep&&hyp>parms.maxsep*0.99999) break ; } omega = angle(opos,npos) ; // find the min and max legal bearing if(tol&&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 ; } } /* -------------------------------------------------------------------- */ bearings[i] = { hyp , 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 = ipts[pathpos+m].pos ; malt = ipts[pathpos+m].h ; x = bearings[m].hyp ; theta = bearings[m].omega ; d = x * Math.sin(theta-omega) ; dh = 0 ; if(tol&&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(tol&&d*d+dh*dh>tol*tol) { legal = 0 ; break ; } tdist += ipts[pathpos-1+m].delta * ( 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(npt.label||npt.desc||npt.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]=ipts[clen-1],j=m-2,i=stk[0].prev;i>=0;i=backptr[i],j--) nstk[j] = ipts[i] ; return nstk ; } /* -------------------------------------------------------------------------- */ function readgps(response,filename) { var xmldoc , dotind = filename.lastIndexOf('.') , seg ; var extn = filename.substring(dotind).toLowerCase() ; if(extn!='.fit'&&extn!='.csv') { if(!readgps.parser) readgps.parser = new DOMParser() ; xmldoc = readgps.parser.parseFromString(response,"application/xml") ; } // input formats if(extn=='.fit') { seg = readfit(response) ; seg.srcid = abbreviate(filename)[0] ; if(!seg.title) seg.title = seg.srcid ; return seg ; } else if(extn=='.rte') return readrte(xmldoc.documentElement,filename) ; else if(extn=='.gpx') return readgpx(xmldoc,filename) ; else if(extn=='.kml') return readkml(xmldoc,filename) ; else if(extn=='.tcx') { seg = readtcx(xmldoc,filename) ; if(seg.pts.length>1) return seg ; else return seg.pts[0] ; } else if(extn=='.csv') { seg = readcsv(xmldoc) ; seg.title = abbreviate(filename)[0] ; return seg ; } else return readgoogle(xmldoc) ; } function writegps(props,pts,geo,fmt,precision,namer) { var ms ; if(fmt=='.fit'||fmt=='.tcx') ms = gentimes(pts) ; if(fmt=='.rte') return writerte(props,pts,geo,precision,0) ; else if(fmt=='.gpx') return writegpx(props.title,props.desc,pts,geo,precision,namer) ; else if(fmt=='.fit') return writefit(props.title,pts,ms.t) ; else if(fmt=='.tcx') return writetcx(props.title,pts,ms.t,ms.d,precision,props.desc) ; } /* -------------------------------------------------------------------------- */ function writerteprops(props,nesting,gallery) { var i , ty , str = '' , sp = ' ' ; // top-level routes have nested props for(i=0;i<nesting;i++) sp += ' ' ; if(props.title) str += gentag('name',props.title,sp) ; // fields which belong to routes but not to indexes if(props.level==0&&nesting==1) // dates are properties of routes in indexes if(props.date) str += gentag('date',props.date,sp) ; // desc is a prop of a route alone or in index, or of a route of type segments if(props.desc) str += gendesc('desc',props.desc,"routemaster",sp) ; // stars are props of routes, either alone or in indexes, or indexes in metas if(props.level<2&&nesting<2) if(props.stars) str += gentypedtag('stars',props.stars.toString(),0,"1-5",sp) ; // galleries and index links occur only at top level if(nesting==0) { if(props.index) str += genindex(props.index,sp) ; if(gallery&&gallery!='/dev/null') str += gentypedtag('gallery',gallery.href,'title',gallery.title,sp) ; if(props.origin&&props.origin[0]&&props.origin.length==2) { ty = props.origin[1] ; if(ty=='uriform'||ty=='refresh') ty = 'uri' ; str += gentypedtag('origin',props.origin[0],'type',ty,sp) ; } } // small photos, stats and tlinks only occur at depth 1 if(nesting==1) { if(props.stats) str += gentypedtag('stats',formatstats(props.stats),0,0,sp) ; if(props.smallphoto&&props.smallphoto.length) for(i=0;i<props.smallphoto.length;i++) str += genimgtag(props.smallphoto[i],sp) ; if(props.tlink) { if(props.ttype) str += genurl('tracklink',props.tlink,'type',props.ttype,sp) ; else str += genurl('tracklink',props.tlink,'mode',props.tmode,sp) ; } } // the following properties only occur in bare routes if(nesting==0&&(props.level==0||props.type=='segments')) { if(props.list) str += gentypedtag('imgdef',props.list,0,'pix',sp) ; if(props.info) str += geninfotag(props.info,sp) ; if(props.srcid&&props.srcid!=props.title) str += gentypedtag('srcid',props.srcid,'type','utf-8',sp) ; } // optimisation occurs in bare routes and within routes of type 'segments' if((nesting==0&&props.level==0)||(nesting==1&&props.type=='segments')) if(props.optim) str += genoptim('optimised',props.optim,sp) ; return str ; } /* -------------------------------------------------------------------------- */ function writegpx(title,desc,xpts,xgeo,precision,namer) { var i , j , ipts = squash(xpts) , val , str , iter , ptlab , sp , sp2 ; // header str = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n' + '<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1"\n' + 'creator="Routemaster - https://www.routemaster.app/software/'+ 'routemaster.html"\n' + 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' + 'xsi:schemaLocation="http://www.topografix.com/GPX/1/1 ' + 'http://www.topografix.com/GPX/1/1/gpx.xsd">\n' ; // metadata if(title) str += '<metadata><name>' + title + '</name></metadata>\n' ; str += '<trk>' ; if(title) str += '<name>' + title + '</name>\n' ; if(desc) str += gendesc('desc',desc,'gpx',' ') ; // loop over points for(iter=0;iter<2;iter++) { if(iter) { if(ipts.length) str += ' <trkseg>\n' ; else break ; } if(iter==0) { pts = xgeo ; ptlab = 'w' ; sp = ' ' ; } else { pts = ipts ; ptlab = 'trk' ; sp = ' ' ; } sp2 = ' ' + sp ; for(i=0;i<pts.length;i++) { str += sp + '<' + ptlab + 'pt ' + gpxp(pts[i].pos,precision) + '>\n' ; if(pts[i].h!=null&&pts[i].h!=undefined) str += gentag('ele',pts[i].h.toFixed(precision),sp2) ; if(pts[i].t) str += gentag('time',pts[i].t.toISOString(),sp2) ; val = correctlabel(pts[i].label,pts[i].caption,'gpx') ; if(val.label) { if(val.caption) str += gentag('name',val.caption,sp2) ; if(val.desc) str += gendesc('desc',val.desc,'gpx',sp2) ; } for(j=0;j<pts[i].photo.length;j++) { if(namer) title = namer(pts[i].photo[j]) ; else title = null ; if(title) { str += sp + ' <link href="' + pts[i].photo[j] + '">\n' ; str += gentag('text',title,sp+' ') + sp + ' </link>\n' ; } else str += sp + ' <link href="' + pts[i].photo[j] + '"/>\n' ; } if(val.label) str += gentag('sym',val.label,sp2) + gentag('type',val.label,sp2) ; str += sp + '</' + ptlab + 'pt>\n' ; } } if(ipts.length) str += ' </trkseg>\n' return str + '</trk>\n</gpx>\n' ; } /* -------------------------------------------------------------------------- */ function writerte(props,ipts,gpts,precision,nesting,ptype) { var i,j,dt,len,tlast,str='',gal=null , blx = '' , rstr , pt ; for(i=0;i<nesting;i++) blx += ' ' ; // header if(blx=='') { str = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n' + rmdoc ; gal = getgallery() ; } // props if(ipts.length==0) rstr = '<route type="geo">\n' ; else rstr = '<route>\n' ; str += blx + rstr + writerteprops(props,nesting,gal) ; // route points for(tlast=-1,i=0;i<ipts.length+gpts.length;i++) { rstr = ' <pt ' ; if(i<ipts.length) pt = ipts[i] ; else { pt = gpts[i-ipts.length] ; if(ipts.length) rstr = ' <pt type="geo" ' ; } str += blx + rstr + gpxp(pt.pos,precision) ; if(pt.h||pt.h==0) str += ' h=\"' + pt.h.toFixed(precision) + '\"' ; if(nesting==0||ptype=='segments') { if(pt.t) { if(i<ipts.length&&tlast>=0&&i<tlast+10&&ipts[i-1].t) { dt = pt.t.getTime() - ipts[i-1].t.getTime() ; dt = (dt/1000).toFixed(3) ; for(j=dt.length;j>0&&dt.charAt(j-1)=='0';j--) ; if(j>0&&dt.charAt(j-1)=='.') j-- ; if(j<dt.length) dt = dt.slice(0,j) ; str += ' dt=\"' + dt + '\"' ; } else { str += ' t=\"' + pt.t.toISOString() + '\"' ; tlast = i ; } } else tlast = -1 ; if(pt.photo.length||pt.label||pt.caption||pt.desc) { str += '>\n' ; for(j=0;j<pt.photo.length;j++) str += gentypedtag('img',pt.photo[j],'type','pix',blx+' ') ; if(pt.label||pt.caption) { str += blx + ' <mark' ; if(pt.label) str += ' label="' + pt.label + '"' ; if(pt.caption) str += ' caption="' + xmlify(pt.caption) + '"' ; str += '/>\n' ; } if(pt.desc) str += gendesc('desc',pt.desc,"routemaster",' ') ; str += blx + ' </pt>\n' ; } else str += '/>\n' ; } else str += '/>\n' ; } return str + blx + '</route>\n' ; } /* -------------------------------------------------------------------------- */ // this may be overcomplicated because I formerly tried to encode lots of // non-tcx data in a tcx file. I ceased to do so when I moved to .rte, but // there may be code which is a relic of my previous practice. function writetcx(title,ipts,msecs,distance,precision,desc) { var clen = ipts.length , time , i , k , ind , label , caption , str , val ; if(title) title = title.substring(0,15) ; else title = M.untitledroute ; // header 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' + rmdoc ; // metadata time = (msecs[clen-1]-msecs[0]) / 1000 ; str += ' <Folders><Courses><CourseFolder Name="Courses">\n' + ' <CourseNameRef><Id>'+title+'</Id></CourseNameRef>\n' + ' </CourseFolder></Courses></Folders>\n<Courses><Course>\n' + gentag('Name',title,' ') + ' <Lap>\n' ; str += gentag('TotalTimeSeconds',time.toFixed(0),' ') + gentag('DistanceMeters',distance[clen-1].toFixed(0),' ') + tcxpos('Begin',ipts[0].pos,0,' ') + tcxpos('End' ,ipts[clen-1].pos,0,' ') + gentag('Intensity','Active',' ') + ' </Lap>\n' ; str += ' <Track>\n' ; // loop over trackpoints for(i=0;i<ipts.length;i++) { str += ' <Trackpoint>\n' ; str += gentag('Time',new Date(msecs[i]).toISOString(),' ') ; str += tcxpos('',ipts[i].pos,precision,' ') if(ipts[i].h!=null) str += gentag('AltitudeMeters',ipts[i].h.toFixed(precision),' ') ; str += ' </Trackpoint>\n' ; } str += ' </Track>\n\n' ; if(desc) str += gendesc('Notes',desc,'gpx',' ') ; // finally loop over coursepoints for(i=0;i<ipts.length;i++) if(ipts[i].label) { str += ' <CoursePoint>\n' ; val = correctlabel(ipts[i].label,ipts[i].caption,'tcx') ; str += gentag('Name',val.caption?val.caption:' ',' ') ; str += gentag('Time',new Date(msecs[i]).toISOString(),' ') ; str += tcxpos('',ipts[i].pos,precision,' ') ; if(ipts[i].h!=null) str += gentag('AltitudeMeters',ipts[i].h.toFixed(precision),' ') ; str += gentag('PointType',val.label,' ') ; if(ipts[i].desc) str += gendesc('Notes',ipts[i].desc,'gpx',' ') ; str += ' </CoursePoint>\n' ; } return str + '</Course></Courses></TrainingCenterDatabase>\n' ; } /* -------------------------------------------------------------------------- */ function writefit(title,ipts,msecs) { var clen=ipts.length,i,j,k,ncp,n,offs,dist,goth,nalt ; for(nalt=ncp=i=0;i<clen;i++) { if(ipts[i].label||ipts[i].fitlabel!=null) ncp += 1 ; if(ipts[i].h==null||ipts[i].h==undefined) ; else nalt += 1 ; } var utf8encoder = new TextEncoder() ; if(!title) title = M.untitledroute ; var name = utf8encoder.encode(title) , tlen = name.length ; var dlen = 17*clen + 2*nalt + 34*ncp ; var fitlen = 199 + tlen + dlen ; var fit = new Uint8Array(fitlen) ; var t0 = new Date() , seconds = Math.floor(t0.getTime()/1000) - 631065600 ; // check msecs offs = msecs[0] - 1000 * 631065600 ; if(offs<0) for(i=0;i<clen;i++) msecs[i] -= offs ; for(i=0;i<clen;i++) msecs[i] = Math.floor(msecs[i]/1000) - 631065600 ; /* ----------------------------- file_id ---------------------------------- */ // hdr:0:big:gma:gmb:nfields fitinject(fit,14, 0x40,0,0,0,0,7) ; // definition header // meaning:size:format fitinject(fit,20,0,1,0) ; // format of file type fitinject(fit,23,1,2,0x84) ; // format of manufacturer fitinject(fit,26,2,2,0x84) ; // format of product fitinject(fit,29,4,4,0x86) ; // format of serialno fitinject(fit,32,3,4,0x8c) ; // format of creation time fitinject(fit,35,5,2,0x84) ; // format of number fitinject(fit,38,8,16,7) ; // format of product name fitinject(fit,41, 0, 6, 0,0, 0,0) ; // lmsgnum=0, course=6, manuf, product fitinject4(fit,47,seconds) ; // use time as serialno fitinject4(fit,51,seconds) ; // time fitinject2(fit,55,1) ; // number '1' fitinject(fit,57,0x72,0x6F,0x75,0x74,0x65) ; // 'route' fitinject(fit,62,0x6D,0x61,0x73,0x74,0x65,0x72) ; // 'master' fitinject(fit,68,0x2E,0x61,0x70,0x70,0) ; //'.app' n = 73 ; /* ------------------------------ course ---------------------------------- */ // hdr:0:big:gma:gmb:nfields fitinject(fit,n, 0x41,0,0,31,0,1) ; // definition header fitinject(fit,n+6,5,tlen+1,7) ; // format of course name fitinject(fit,n+9,1) ; for(n+=10,i=0;i<tlen;i++,n++) fitinject(fit,n,name[i]) ; fitinject(fit,n,0) ; // null terminated n += 1 ; /* -------------------------------- lap ----------------------------------- */ // hdr:0:big:gma:gmb:nfields fitinject(fit,n, 0x42,0,0,19,0,2) ; // definition header fitinject(fit,n+6,2,4,0x86) ; // format of start time fitinject(fit,n+9,253,4,0x86) ; // format of timestamp fitinject(fit,n+12,2) ; // local msg num fitinject4(fit,n+13,msecs[0]) ; // start time fitinject4(fit,n+17,msecs[0]) ; // timestamp n += 21 ; /* ---------------------------- event start ------------------------------- */ // hdr:0:big:gma:gmb:nfields fitinject(fit,n, 0x43,0,0,21,0,3) ; // definition header fitinject(fit,n+6,253,4,0x86) ; // format of timestamp fitinject(fit,n+9,0,1,0) ; // format of event fitinject(fit,n+12,1,1,0) ; // format of eventtype fitinject(fit,n+15,3) ; // local msg num fitinject4(fit,n+16,msecs[0]) ; // timestamp fitinject(fit,n+20,0,0) ; // two zero bytes n += 22 ; /* --------------- definitions for records and course points -------------- */ // hdr:0:big:gma:gmb:nfields fitinject(fit,n, 0x44,0,0,20,0,5) ; // definition header for record fitinject(fit,n+6,253,4,0x86) ; // format of timestamp fitinject(fit,n+9,0,4,0x85) ; // format of lat fitinject(fit,n+12,1,4,0x85) ; // format of long fitinject(fit,n+15,2,2,0x84) ; // format of altitude fitinject(fit,n+18,5,4,0x86) ; // format of distance n += 21 ; fitinject(fit,n, 0x45,0,0,32,0,6) ; // definition header for coursepoint fitinject(fit,n+6,1,4,0x86) ; // format of timestamp fitinject(fit,n+9,2,4,0x85) ; // format of lat fitinject(fit,n+12,3,4,0x85) ; // format of long fitinject(fit,n+15,4,4,0x86) ; // format of distance fitinject(fit,n+18,5,1,0) ; // format of coursepoint type fitinject(fit,n+21,6,16,7) ; // format of coursept name (16 bytes) n += 24 ; fitinject(fit,n, 0x46,0,0,20,0,4) ; // definition for record (no altitude) fitinject(fit,n+6,253,4,0x86) ; // format of timestamp fitinject(fit,n+9,0,4,0x85) ; // format of lat fitinject(fit,n+12,1,4,0x85) ; // format of long fitinject(fit,n+15,5,4,0x86) ; // format of distance n += 18 ; /* ------------------------------ records --------------------------------- */ for(dist=i=0;i<clen;dist+=ipts[i].delta,i++) { if(ipts[i].h==null||ipts[i].h==undefined) goth = 0 ; else goth = 1 ; fitinject(fit,n,goth?4:6) ; // header for record: local msg type = 4/6 // for a record with/without an altitude fitinject4(fit,n+1,msecs[i]) ; fitinjectangle(fit,n+5,ipts[i].pos.lat) ; fitinjectangle(fit,n+9,ipts[i].pos.lng) ; n += 13 ; if(goth) { fitinject2(fit,n,(ipts[i].h+500)*5) ; n += 2 ; } fitinject4(fit,n,dist*100) ; n += 4 ; if(!ipts[i].label&&ipts[i].fitlabel==null) continue ; fitinject(fit,n,5) ; // header for course point: local msg type = 5 fitinject4(fit,n+1,msecs[i]) ; fitinjectangle(fit,n+5,ipts[i].pos.lat) ; fitinjectangle(fit,n+9,ipts[i].pos.lng) ; fitinject4(fit,n+13,dist*100) ; if(ipts[i].fitlabel!=null) j = ipts[i].fitlabel ; else { for(j=waymarks.length-1;j>=0&&waymarks[j].name!=ipts[i].label;j--) ; j = j<0?0:waymarks[j].fitname ; } fitinject(fit,n+17,j) ; if(ipts[i].caption) { name = utf8encoder.encode(ipts[i].caption) ; k = name.length ; } else k = 0 ; for(j=0;j<k&&j<15;j++) fitinject(fit,n+18+j,name[j]) ; for(;j<16;j++) fitinject(fit,n+18+j,0) ; n += 34 ; } /* ----------------------------- event end -------------------------------- */ // hdr:0:big:gma:gmb:nfields fitinject(fit,n,3) ; // local msg num fitinject4(fit,n+1,msecs[clen-1]) ; // timestamp fitinject(fit,n+5,0,9) ; // event 0; 9 means end n += 7 ; /* ------------------------------ header ---------------------------------- */ fitinject(fit,0,14,0x10,0x7a,0x52) ; // protocol version number fitinject(fit,8,0x2e,0x46,0x49,0x54) ; // ".FIT" fitinject4(fit,4,n-14) ; // length of pts component of file fitinject2(fit,12,checksum(fit,0,12)) ; fitinject2(fit,n,checksum(fit,14,n-14)) ; n += 2 ; if(n!=fitlen) alert("miscounted fit length") ; return fit ; } function fitinject(buf,pos,p0,p1,p2,p3,p4,p5) { buf[pos] = p0 ; if(p1||p1==0) buf[pos+1] = p1 ; else return ; if(p2||p2==0) buf[pos+2] = p2 ; else return ; if(p3||p3==0) buf[pos+3] = p3 ; else return ; if(p4||p4==0) buf[pos+4] = p4 ; else return ; if(p5||p5==0) buf[pos+5] = p5 ; } function fitinject2(buf,pos,x) { fitinject(buf,pos,x&0xff) ; fitinject(buf,pos+1,(x>>8)&0xff) ; } function fitinject4(buf,pos,x) { fitinject(buf,pos,x&0xff) ; fitinject(buf,pos+1,(x>>8)&0xff) ; fitinject(buf,pos+2,(x>>16)&0xff) ; fitinject(buf,pos+3,(x>>24)&0xff) ; } function fitinjectangle(buf,pos,x) { fitinject4(buf,pos,x*(1<<30)/90.0) ; } function checksum(x,start,n) { var crc_table = [ 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400 ] ; var i,crc,tmp ; for(crc=0,i=start;i<start+n;i++) { // compute checksum of lower four bits of byte tmp = crc_table[crc & 0xF] ; crc = (crc >> 4) & 0x0FFF ; crc = crc ^ tmp ^ crc_table[x[i] & 0xF] ; // now compute checksum of upper four bits of byte tmp = crc_table[crc & 0xF] ; crc = (crc >> 4) & 0x0FFF ; crc = crc ^ tmp ^ crc_table[(x[i] >> 4) & 0xF] ; } return crc ; } /* -------------------------------------------------------------------------- */ /* FUNCTIONS FOR COMPUTING & DISPLAYING THE ALTITUDE PROFILE */ /* -------------------------------------------------------------------------- */ function profilemaptype(segments,nx) { var i,d0,d1,D,pos,oldpos,hind,htarg,lam,n,s0,s1,eps,nvalid,delta ; // if I were to dispense with the call to flatten I could reduce the work var p = flatten(segments) ; n = p.length ; for(nvalid=i=0;i<n&&nvalid<2;i++) if(p[i].h!=null) nvalid += 1 ; if(nvalid<2) { this.h = [] ; return ; } this.d = D = p[n-1].d ; eps = D / nx ; // now loop through points extracting regularly spaced altitudes this.h = new Array(nx) ; for(hind=i=0;hind<nx;hind++) { htarg = (hind+0.5) * eps ; for(;i<p.length-1&&p[i].d<htarg;i++) ; if(i==0) { this.h[hind] = p[i].h ; continue ; } else if(i==p.length) { this.h[hind] = p[i-1].h ; continue ; } d0 = p[i-1].d ; d1 = p[i].d ; if(p[i].sel.ptno==0&&htarg>d0+eps/2&&htarg<d1-eps/2) { this.h[hind] = undefined ; continue ; } if(p[i].sel.ptno==0) { this.h[hind] = p[i].h ; continue ; } if(d1>d0) lam = (htarg-d0) / (d1-d0) ; else lam = 1 ; if(p[i-1].h==null) { if(lam>=0.5) this.h[hind] = p[i].h ; else this.h[hind] = null ; } else if(p[i].h==null) { if(lam<0.5) this.h[hind] = p[i-1].h ; else this.h[hind] = null ; } else this.h[hind] = p[i-1].h + lam*(p[i].h-p[i-1].h) ; } // a simple loop to find hmin, hmax for(this.hmax=this.hmin=null,i=0;i<nx;i++) if(this.h[i]||this.h[i]==0) { if(this.hmax==null||this.h[i]>this.hmax) this.hmax = this.h[i] ; if(this.hmin==null||this.h[i]<this.hmin) this.hmin = this.h[i] ; } // don't put hmin too low if(this.hmin>0) { if(this.hmax>3*this.hmin) this.hmin = 0 ; else this.hmin *= 1 - (this.hmax/this.hmin-1)/2 ; } // enforce a span of at least 100m if(this.hmax<this.hmin+100) { delta = (100-(this.hmax-this.hmin))/2 ; if(this.hmin<0) this.hmin -= delta/10 ; else if(this.hmin<delta-10) this.hmin = -10 ; else this.hmin -= delta ; this.hmax = this.hmin + 100 ; } // set up pro2wp this.pro2wp = new Array(nx) ; for(hind=i=0;hind<nx;hind++) { htarg = (hind+0.5) * D / nx ; for(;i<p.length-1&&p[i].d<htarg;i++) ; this.pro2wp[hind] = p[i].sel ; if(i==0) continue ; d1 = p[i].d ; d0 = p[i-1].d ; if(d1>d0) lam = (htarg-d0) / (d1-d0) ; else lam = 1 ; if(lam<0.5) this.pro2wp[hind] = p[i-1].sel ; } // set up wp2pro this.wp2pro = new Array(segments.length) ; for(i=s0=0;s0<segments.length;s0++) { n = segments[s0].pts.length ; this.wp2pro[s0] = new Array(n) ; for(s1=0;s1<n;s1++,i++) this.wp2pro[s0][s1] = Math.floor(0.5+p[i].d*nx/D) ; } } /* -------------------------------------------------------------------------- */ function profiletype(opt) { var id,node ; this.curhandle = this.m = null ; this.sel = { segno:0 , ptno:0 , type:null } ; this.ratio = window.devicePixelRatio ; if(!this.ratio) this.ratio = 1 ; if(opt<0) { this.ipixw = this.opixw = 300 ; this.ipixh = this.opixh = 120 ; this.active = -1 ; } else { this.ipixw = 600 ; this.ipixh = 180 ; this.opixw = 620 ; this.opixh = 200 ; this.active = 0 ; } this.innerw = Math.floor(this.ipixw*this.ratio) ; this.innerh = Math.floor(this.ipixh*this.ratio) ; this.outerw = Math.floor(this.opixw*this.ratio) ; this.outerh = Math.floor(this.opixh*this.ratio) ; this.offs = Math.floor((this.outerw-this.innerw)/2) ; this.radius = Math.floor(20*this.ratio) ; this.prodiv = domcreate('div',null,'id','prodiv') ; if(opt==1) { this.curdiv = domcreate('div',null,'id','curdiv') ; this.curhandle = this.curdiv.addEventListener("click",toggleprofile) ; } this.release = function() { for(id='prodiv';id!=null;id=(id=='prodiv'?'curdiv':null)) { node = document.getElementById(id) ; if(node) node.parentNode.removeChild(node) ; } this.m = null ; } } /* -------------------------------------------------------------------------- */ function delpro(pro) { var div ; if(pro.prodiv) for(div=pro.prodiv;div.childNodes.length>0;) div.removeChild(div.childNodes[div.childNodes.length-1]) ; else div = domcreate('div') ; return div ; } function drawpro(pro,segments) { var ctx,i,j,k,h,ind,step,imgdata,d,cdash,ctxdash,c,maxi,maxj,col,hspan ; var jlim,r=pro.radius,div,divstyle,hind,x,y,inc,segno,defh,div ; var div = delpro(pro) ; if(pro.active) { maxi = pro.innerw ; maxj = pro.innerh ; } else maxj = maxi = 2 * r ; pro.m = new profilemaptype(segments,maxi) ; if(!pro.m.h.length) return null ; c = domcreate('canvas') ; domadd(div,c) ; if(pro.active<0) divstyle = 'display:inline-block;' ; else divstyle = 'position:absolute;right:0;top:0;' ; if(pro.active) { divstyle += 'height:'+pro.opixh+'px;width:'+pro.opixw+'px' ; domset(c,'width',pro.opixw,'height',pro.opixh) ; } else { divstyle += 'height:42px;width:42px' ; domset(c,'width',42,'height',42) ; } domset(div,'style',divstyle) ; ctx = c.getContext("2d") ; ctx.scale(1/pro.ratio,1/pro.ratio) ; // pale grey background if(pro.active<0) { ctx.globalAlpha = 1 ; ctx.font = Math.floor(0.5+8*pro.ratio)+"px Helvetica" ; } else if(pro.active) { ctx.globalAlpha = 0.8 ; ctx.fillStyle = '#f0f0f0' ; 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 cdash = domcreate('canvas',null,'width',maxi,'height',maxj) ; ctxdash = cdash.getContext("2d") ; imgdata = ctxdash.createImageData(maxi,maxj) ; d = imgdata.data ; hspan = pro.m.hmax - pro.m.hmin ; for(i=0;i<maxi;i++) { h = pro.m.h[i] ; if(h===undefined&&pro.active) continue ; if(h===null) { if(i>0&&pro.m.h[i-1]!==null) { for(j=i-1;j>=0&&(pro.m.h[j]===null||pro.m.h[j]===undefined);j--) ; if(j>=0) defh = pro.m.h[j] ; else defh = null ; for(j=i+1;j<maxi&&(pro.m.h[j]===null||pro.m.h[j]===undefined);j++) ; if(j<maxi) { if(defh===null) defh = pro.m.h[j] ; else defh = ( pro.m.h[j] + defh ) / 2 ; } // defh is the altitude corresponding to the horizontal line defh = (defh+pro.m.hmin)/2 ; } hind = Math.floor(maxj*(pro.m.hmax-defh)/hspan) ; } else if(h!==undefined) { h = maxj*(pro.m.hmax-h)/hspan ; hind = Math.floor(h) ; } segno = pro.m.pro2wp[i].segno ; col = dehexify(segments[segno].colour) ; for(j=0;j<3;j++) col[j] = Math.floor(0.5+255*col[j]) ; if(h===null&&pro.active) for(j=hind-1;j<hind+1&&j<pro.innerh;j++) { if(j>=0) { ind = 4 * (i+maxi*j) ; for(k=0;k<3;k++) d[ind+k] = col[k] ; d[ind+3] = 255 ; } } else if(pro.active) for(j=hind;j<pro.innerh;j++) { ind = 4 * (i+maxi*j) ; for(k=0;k<3;k++) d[ind+k] = col[k] ; 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))) ; j = r - jlim ; if(j!=Math.floor(j)) j += 1 ; for(;(h==null||h==undefined||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) ; if(j==hind) { k = Math.floor(0.5+(h-hind)*255+(hind+1-h)*k) ; for(k=0;k<4;k++) d[ind+k] = k ; } else { for(k=0;k<3;k++) d[ind+k] = col[k] ; d[ind+3] = 255 ; } } if(h===null) for(j=hind-1;j<hind+1;j++) if(j>=r-jlim&&j<r+jlim) { ind = 4 * (i+maxi*j) ; for(k=0;k<3;k++) d[ind+k] = col[k] ; d[ind+3] = 255 ; } } } 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.hmax>pro.m.hmin+2500) step = 1000 ; else if(pro.m.hmax>pro.m.hmin+1250) step = 500 ; else step = 100 ; if(pro.active<0) step *= 2 ; 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)/hspan ; y = 0.5 + Math.floor(y) ; // nearest half-integer ctx.beginPath() ; ctx.lineWidth = 1 ; ctx.strokeStyle = '#555' ; ctx.moveTo(pro.offs,y) ; ctx.lineTo(pro.innerw+pro.offs,y) ; ctx.stroke() ; ctx.strokeStyle = 'black' ; ctx.strokeText(i,pro.innerw-28*pro.ratio,y-2*pro.ratio) ; } if(pro.m.d>50000) { step = 10000 ; inc = 5 ; } else if(pro.m.d>25000) { step = 5000 ; inc = 2 ; } else if(pro.m.d>5000) { step = 1000 ; inc = 5 ; } else if(pro.m.d>2500) { step = 500 ; inc = 2 ; } else { step = 100 ; inc = 10 ; } for(i=1;i*step<pro.m.d;i++) { x = pro.offs + pro.innerw*i*step/pro.m.d ; x = 0.5 + Math.floor(x) ; // nearest half-integer ctx.beginPath() ; ctx.lineWidth = 1 ; ctx.strokeStyle = '#555' ; ctx.moveTo(x,pro.offs+pro.innerh) ; ctx.lineTo(x,pro.offs+0.95*pro.innerh) ; ctx.stroke() ; ctx.strokeStyle = 'black' ; if(i%inc==0) ctx.strokeText((i*step)/1000,x-8,pro.offs+0.93*pro.innerh) ; } } return divstyle ; } /* -------------------------------------------------------------------------- */ function choosearrows(pts,nar,tgt) { var n=pts.length , total , i , narrow , d , j , ind , idash , ddash ; var nar , fom , seglen , arno , θ , ϕ , ii , dd , ibest , dpt ; var dj , score ; for(total=i=0;i<n-1;i++) total += pts[i].delta ; if(nar) narrow = nar ; else { if(tgt) tgt *= 1000 ; else tgt = 10000 ; narrow = Math.floor(0.5+total/tgt) ; // one per 10km if(!narrow&&total>0.4*tgt) narrow = 1 ;// but at least 1 if the route is 4km } if(narrow>n-1) narrow = n-1 ; if(!narrow||n<2) return [] ; seglen = total / narrow ; ind = new Array(narrow) ; // compute a score for an arrow at every halfway pt from i to i+1 score = new Array(n-1) ; for(i=0;i<n-1;i++) score[i] = null ; for(dd=ii=d=i=0;i<n-1;d+=pts[i].delta,i++) { θ = angle(pts[i].pos,pts[i+1].pos) ; pt = midpt(pts[i].pos,pts[i+1].pos) ; dpt = d + pts[i].delta/2 ; // compute a figure of merit as the largest divergence for(dj=dd,j=ii;j<n&&(dj<dpt+seglen/2||j==i+1);dj+=pts[j].delta,j++) if(dj<dpt-seglen/2&&j<i) { ii = j+1 ; dd = dj+pts[j].delta ; } // skip else { ϕ = angle(pts[j].pos,pt) - θ ; if(j>i) ϕ += Math.PI ; while(ϕ>Math.PI) ϕ -= 2*Math.PI ; while(ϕ<-Math.PI) ϕ += 2*Math.PI ; // more divergence is accepted at a greater range, so divide it by distnce fom = Math.abs(ϕ) * seglen / Math.abs(dj-dpt) ; if(score[i]==null||fom>score[i]) score[i] = fom ; } } // choose an arrow for the chunk from i to idash (i.e. roughly equispaced) // (we could do better by choosing narrow arrows by dynamic programming) for(nar=d=arno=i=0;i<n;arno++,i=idash,d=ddash) { tgt = ( total / narrow ) * (arno+1) ; for(ddash=d,idash=i;idash<n;ddash+=pts[idash].delta,idash++) if(ddash+pts[idash].delta/2>tgt) break ; // i is the first pt of the sequence, i' the first after it if(idash==i) continue ; for(qbest=null,dd=d,ii=i;ii<idash&&ii<n-1;dd+=pts[ii].delta,ii++) if(score[ii]!=null) { dpt = dd + pts[ii].delta/2 ; fom = score[ii] / ( 0.001 + (dpt-d)*(ddash-dpt) ) ; if(qbest==null||fom<qbest) { qbest = fom ; ibest = ii ; } } if(qbest!=null) ind[nar++] = ibest ; } ind.length = nar ; return ind ; } /* -------------------------------------------------------------------------- */ // https://medium.com/@trinad536/replacing-html-tags-with-named-... // ...entities-in-javascript-225be346f6c9 function dexmlify(str) { return str.replace(/<\/?[^>]+(>|$)/g, function(match) { var entities = { "<": "<", ">": ">", "&": "&", "\"": """, "'": "'" } ; return entities[match] || match ; } ) ; } /* -------------------------------------------------------------------------- */ function absuri(href) { if(!href||href.length<7) return false ; var s = href.substring(0,7).toLowerCase() ; return s=='http://' || s == 'file://' || (s=='https:/'&&href.charAt(7)=='/') ; } /* ---------------------------------- trackref ------------------------------ */ // a track has been specified as href in a route loaded from origin: we want an // absolute uri for it, prefixed by the routemaster invocation iff fullcall // fullcall is 0 for additive links, 2 for non-additive links in route // descriptions, and 1 in all other cases function trackref(origin,href,fullcall) { var ind=-1,str,extn ; if(href&&fullcall==2) { extn = href.substring(href.length-4).toLowerCase() ; if(trackextns.indexOf(extn)<0) return href ; // link is not to a gps trk } if(!href||!absuri(href)) { if( !origin || !origin[1] || (origin[1].substring(0,3)!='uri'&&origin[1]!='refresh') ) return null ; if(href) href = reluri(origin[0],href) ; else href = origin[0] ; } else ind = href.indexOf('?track=') ; if(fullcall&&ind<0) { ind = document.location.href.indexOf('?') ; if(ind<0) str = document.location.href ; else str = document.location.href.substring(0,ind) ; href = str + '?track=' + href ; } else if(!fullcall&&ind>=0) { href = href.substring(ind+7) ; ind = href.indexOf('&') ; if(ind>=0) href = href.substring(0,ind) ; } return href ; } /* ---------------------------------- innerRML ------------------------------ */ function innerRML(d,s,f,origin,mode) { var i,j,idash,len=s.length,slen,e,href=null,plus,ind,str,sp,dolink,span ; var tags = ['i','I','b','B','s','S','u','U'] ; for(slen=i=0;i<len-7;i++) { if(s.charAt(i)!='<') continue ; if( ( s.charAt(i+2)!='>'||tags.indexOf(s.charAt(i+1))<0) && ( s.charAt(i+2)!=' '||(s.charAt(i+1)!='a'&&s.charAt(i+1)!='A') ) ) continue ; href = null ; if(s.charAt(i+1)!='a'&&s.charAt(i+1)!='A') idash = i+3 ; else { for(idash=i+3;idash<len-4&&s.charAt(idash)==' ';idash++) ; if(idash==len-4||s.substring(idash,idash+5).toLowerCase()!='href=') continue ; for(idash+=5,j=idash;j<len-4&&s.charAt(j)!='>';j++) ; if(j==len-4) continue ; if(s.charAt(idash)=='"') { if(j-1<=idash||s.charAt(j-1)!='"') continue ; href = s.substring(idash+1,j-1) ; } else href = s.substring(idash,j) ; idash = j+1 ; } for(j=idash;j<len-3;j++) if( s.charAt(j)=='<' && s.charAt(j+1)=='/' && s.charAt(j+3)=='>' && s.charAt(j+2).toLowerCase()==s.charAt(i+1).toLowerCase()) break ; if(j==len-3||j==idash) continue ; // add text preceding the tag if(i) { domadd(d,s.substring(0,i)) ; slen += i ; } // parse the link plus = 0 ; if(href&&href.charAt(0)=='+') { href = href.substring(1) ; if(!mode) plus = 1 ; } dolink = href?1:0 ; if( mode=='masher' && !absuri(href) && ( !origin || !origin.length || origin.length<2 || ( origin[1]!='uri'&&origin[1]!='uriform') ) ) dolink = 0 ; if(dolink) href = trackref(origin,href,(plus||mode=='masher')?0:2) ; function loaderfactory(href) { return function() { readuri(function(r){ render(r,href,'urilink',null,'add');},href) ; } } // generate the element under the tag: create a span if an <a> cannot // be rendered as a link if(idash>i+3&&(!dolink||plus)) e = domcreate('span') ; else e = domcreate(s.charAt(i+1)) ; slen += innerRML(e,s.substring(idash,j),null,origin,mode) ; if(href&&plus) { domadd(e,' ⊞') ; domset(e,'style','cursor:pointer;color:#0000bd') ; e.onclick = loaderfactory(href) ; } else if(href&&!dolink) domadd(e,domcreate('span',' ['+href+'] ','style','color:darkgray')) ; else if(href) { domadd(e,' ',newtabdiv()) ; domset(e,'href',href,'target','_blank','style','color:#0000bd') ; } domadd(d,e) ; // process the text following the tag slen += innerRML(d,s.substring(j+4),f,origin,mode) ; return slen ; } // if we get here, then we have a final top-level chunk slen += s.length ; if(f) { slen += 3 + M.edit.length ; span = domcreate('span','['+M.edit+']', 'style','cursor:pointer;color:#0000bd') ; span.onclick = f ; domadd(d,s+' ',span) ; } else domadd(d,s) ; return slen ; } /* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */ /* FileSaver.js * A saveAs() FileSaver implementation. * 1.1.20151003 * * By Eli Grey, http://eligrey.com * License: MIT * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md */ /*global self */ /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ var saveAs = saveAs || (function(view) { "use strict"; // IE <10 is explicitly unsupported if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { return; } var doc = view.document // only get URL when necessary in case Blob.js hasn't overridden it yet , get_URL = function() { return view.URL || view.webkitURL || view; } , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") , can_use_save_link = "download" in save_link , click = function(node) { var event = new MouseEvent("click"); node.dispatchEvent(event); } , is_safari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent) , webkit_req_fs = view.webkitRequestFileSystem , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem , throw_outside = function(ex) { (view.setImmediate || view.setTimeout)(function() { throw ex; }, 0); } , force_saveable_type = "application/octet-stream" , fs_min_size = 0 // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and // https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047 // for the reasoning behind the timeout and revocation flow , arbitrary_revoke_timeout = 500 // in ms , revoke = function(file) { var revoker = function() { if (typeof file === "string") { // file is an object URL get_URL().revokeObjectURL(file); } else { // file is a File file.remove(); } }; if (view.chrome) { revoker(); } else { setTimeout(revoker, arbitrary_revoke_timeout); } } , dispatch = function(filesaver, event_types, event) { event_types = [].concat(event_types); var i = event_types.length; while (i--) { var listener = filesaver["on" + event_types[i]]; if (typeof listener === "function") { try { listener.call(filesaver, event || filesaver); } catch (ex) { throw_outside(ex); } } } } , auto_bom = function(blob) { // prepend BOM for UTF-8 XML and text/* types (including HTML) if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { return new Blob(["\ufeff", blob], {type: blob.type}); } return blob; } , FileSaver = function(blob, name, no_auto_bom) { if (!no_auto_bom) { blob = auto_bom(blob); } // First try a.download, then web filesystem, then object URLs var filesaver = this , type = blob.type , blob_changed = false , object_url , target_view , dispatch_all = function() { dispatch(filesaver, "writestart progress write writeend".split(" ")); } // on any filesys errors revert to saving with object URLs , fs_error = function() { if (target_view && is_safari && typeof FileReader !== "undefined") { // Safari doesn't allow downloading of blob urls var reader = new FileReader(); reader.onloadend = function() { var base64Data = reader.result; target_view.location.href = "data:attachment/file" + base64Data.slice(base64Data.search(/[,;]/)); filesaver.readyState = filesaver.DONE; dispatch_all(); }; reader.readAsDataURL(blob); filesaver.readyState = filesaver.INIT; return; } // don't create more object URLs than needed if (blob_changed || !object_url) { object_url = get_URL().createObjectURL(blob); } if (target_view) { target_view.location.href = object_url; } else { var new_tab = view.open(object_url, "_blank"); if (new_tab == undefined && is_safari) { //Apple do not allow window.open, see http://bit.ly/1kZffRI view.location.href = object_url } } filesaver.readyState = filesaver.DONE; dispatch_all(); revoke(object_url); } , abortable = function(func) { return function() { if (filesaver.readyState !== filesaver.DONE) { return func.apply(this, arguments); } }; } , create_if_not_found = {create: true, exclusive: false} , slice ; filesaver.readyState = filesaver.INIT; if (!name) { name = "download"; } if (can_use_save_link) { object_url = get_URL().createObjectURL(blob); save_link.href = object_url; save_link.download = name; setTimeout(function() { click(save_link); dispatch_all(); revoke(object_url); filesaver.readyState = filesaver.DONE; }); return; } // Object and web filesystem URLs have a problem saving in Google Chrome when // viewed in a tab, so I force save with application/octet-stream // http://code.google.com/p/chromium/issues/detail?id=91158 // Update: Google errantly closed 91158, I submitted it again: // https://code.google.com/p/chromium/issues/detail?id=389642 if (view.chrome && type && type !== force_saveable_type) { slice = blob.slice || blob.webkitSlice; blob = slice.call(blob, 0, blob.size, force_saveable_type); blob_changed = true; } // Since I can't be sure that the guessed media type will trigger a download // in WebKit, I append .download to the filename. // https://bugs.webkit.org/show_bug.cgi?id=65440 if (webkit_req_fs && name !== "download") { name += ".download"; } if (type === force_saveable_type || webkit_req_fs) { target_view = view; } if (!req_fs) { fs_error(); return; } fs_min_size += blob.size; req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { var save = function() { dir.getFile(name, create_if_not_found, abortable(function(file) { file.createWriter(abortable(function(writer) { writer.onwriteend = function(event) { target_view.location.href = file.toURL(); filesaver.readyState = filesaver.DONE; dispatch(filesaver, "writeend", event); revoke(file); }; writer.onerror = function() { var error = writer.error; if (error.code !== error.ABORT_ERR) { fs_error(); } }; "writestart progress write abort".split(" ").forEach(function(event) { writer["on" + event] = filesaver["on" + event]; }); writer.write(blob); filesaver.abort = function() { writer.abort(); filesaver.readyState = filesaver.DONE; }; filesaver.readyState = filesaver.WRITING; }), fs_error); }), fs_error); }; dir.getFile(name, {create: false}, abortable(function(file) { // delete file if it already exists file.remove(); save(); }), abortable(function(ex) { if (ex.code === ex.NOT_FOUND_ERR) { save(); } else { fs_error(); } })); }), fs_error); }), fs_error); } , FS_proto = FileSaver.prototype , saveAs = function(blob, name, no_auto_bom) { return new FileSaver(blob, name, no_auto_bom); } ; // IE 10+ (native saveAs) if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { return function(blob, name, no_auto_bom) { if (!no_auto_bom) { blob = auto_bom(blob); } return navigator.msSaveOrOpenBlob(blob, name || "download"); }; } FS_proto.abort = function() { var filesaver = this; filesaver.readyState = filesaver.DONE; dispatch(filesaver, "abort"); }; FS_proto.readyState = FS_proto.INIT = 0; FS_proto.WRITING = 1; FS_proto.DONE = 2; FS_proto.error = FS_proto.onwritestart = FS_proto.onprogress = FS_proto.onwrite = FS_proto.onabort = FS_proto.onerror = FS_proto.onwriteend = null; return saveAs; }( typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content )); // `self` is undefined in Firefox for Android content script context // while `this` is nsIContentFrameMessageManager // with an attribute `content` that corresponds to the window if (typeof module !== "undefined" && module.exports) { module.exports.saveAs = saveAs; } else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { define([], function() { return saveAs; }); } /* Blob.js * A Blob implementation. * 2014-07-24 * * By Eli Grey, http://eligrey.com * By Devin Samarin, https://github.com/dsamarin * License: MIT * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md */ /*global self, unescape */ /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, plusplus: true */ /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ (function (view) { "use strict"; view.URL = view.URL || view.webkitURL; if (view.Blob && view.URL) { try { new Blob; return; } catch (e) {} } // Internally we use a BlobBuilder implementation to base Blob off of // in order to support older browsers that only have BlobBuilder var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { var get_class = function(object) { return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; } , FakeBlobBuilder = function BlobBuilder() { this.data = []; } , FakeBlob = function Blob(data, type, encoding) { this.data = data; this.size = data.length; this.type = type; this.encoding = encoding; } , FBB_proto = FakeBlobBuilder.prototype , FB_proto = FakeBlob.prototype , FileReaderSync = view.FileReaderSync , FileException = function(type) { this.code = this[this.name = type]; } , file_ex_codes = ( "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" ).split(" ") , file_ex_code = file_ex_codes.length , real_URL = view.URL || view.webkitURL || view , real_create_object_URL = real_URL.createObjectURL , real_revoke_object_URL = real_URL.revokeObjectURL , URL = real_URL , btoa = view.btoa , atob = view.atob , ArrayBuffer = view.ArrayBuffer , Uint8Array = view.Uint8Array , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/ ; FakeBlob.fake = FB_proto.fake = true; while (file_ex_code--) { FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; } // Polyfill URL if (!real_URL.createObjectURL) { URL = view.URL = function(uri) { var uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a") , uri_origin ; uri_info.href = uri; if (!("origin" in uri_info)) { if (uri_info.protocol.toLowerCase() === "data:") { uri_info.origin = null; } else { uri_origin = uri.match(origin); uri_info.origin = uri_origin && uri_origin[1]; } } return uri_info; }; } URL.createObjectURL = function(blob) { var type = blob.type , data_URI_header ; if (type === null) { type = "application/octet-stream"; } if (blob instanceof FakeBlob) { data_URI_header = "data:" + type; if (blob.encoding === "base64") { return data_URI_header + ";base64," + blob.data; } else if (blob.encoding === "URI") { return data_URI_header + "," + decodeURIComponent(blob.data); } if (btoa) { return data_URI_header + ";base64," + btoa(blob.data); } else { return data_URI_header + "," + encodeURIComponent(blob.data); } } else if (real_create_object_URL) { return real_create_object_URL.call(real_URL, blob); } }; URL.revokeObjectURL = function(object_URL) { if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { real_revoke_object_URL.call(real_URL, object_URL); } }; FBB_proto.append = function(data/*, endings*/) { var bb = this.data; // decode data to a binary string if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { var str = "" , buf = new Uint8Array(data) , i = 0 , buf_len = buf.length ; for (; i < buf_len; i++) { str += String.fromCharCode(buf[i]); } bb.push(str); } else if (get_class(data) === "Blob" || get_class(data) === "File") { if (FileReaderSync) { var fr = new FileReaderSync; bb.push(fr.readAsBinaryString(data)); } else { // async FileReader won't work as BlobBuilder is sync throw new FileException("NOT_READABLE_ERR"); } } else if (data instanceof FakeBlob) { if (data.encoding === "base64" && atob) { bb.push(atob(data.data)); } else if (data.encoding === "URI") { bb.push(decodeURIComponent(data.data)); } else if (data.encoding === "raw") { bb.push(data.data); } } else { if (typeof data !== "string") { data += ""; // convert unsupported types to strings } // decode UTF-16 to binary string bb.push(unescape(encodeURIComponent(data))); } }; FBB_proto.getBlob = function(type) { if (!arguments.length) { type = null; } return new FakeBlob(this.data.join(""), type, "raw"); }; FBB_proto.toString = function() { return "[object BlobBuilder]"; }; FB_proto.slice = function(start, end, type) { var args = arguments.length; if (args < 3) { type = null; } return new FakeBlob( this.data.slice(start, args > 1 ? end : this.data.length) , type , this.encoding ); }; FB_proto.toString = function() { return "[object Blob]"; }; FB_proto.close = function() { this.size = 0; delete this.data; }; return FakeBlobBuilder; }(view)); view.Blob = function(blobParts, options) { var type = options ? (options.type || "") : ""; var builder = new BlobBuilder(); if (blobParts) { for (var i = 0, len = blobParts.length; i < len; i++) { if (Uint8Array && blobParts[i] instanceof Uint8Array) { builder.append(blobParts[i].buffer); } else { builder.append(blobParts[i]); } } } var blob = builder.getBlob(type); if (!blob.slice && blob.webkitSlice) { blob.slice = blob.webkitSlice; } return blob; }; var getPrototypeOf = Object.getPrototypeOf || function(object) { return object.__proto__; }; view.Blob.prototype = getPrototypeOf(new view.Blob()); }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Geodesy representation conversion functions (c) Chris Veness 2002-2020 */ /* MIT Licence */ /* www.movable-type.co.uk/scripts/latlong.html */ /* www.movable-type.co.uk/scripts/js/geodesy/geodesy-library.html#dms */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* eslint no-irregular-whitespace: [2, { skipComments: true }] */ /** * Latitude/longitude points may be represented as decimal degrees, or subdivided into sexagesimal * minutes and seconds. This module provides methods for parsing and representing degrees / minutes * / seconds. * * @module dms */ /* Degree-minutes-seconds (& cardinal directions) separator character */ let dmsSeparator = '\u202f'; // U+202F = 'narrow no-break space' /** * Functions for parsing and representing degrees / minutes / seconds. */ class Dms { // note Unicode Degree = U+00B0. Prime = U+2032, Double prime = U+2033 /** * Separator character to be used to separate degrees, minutes, seconds, and cardinal directions. * * Default separator is U+202F ‘narrow no-break space’. * * To change this (e.g. to empty string or full space), set Dms.separator prior to invoking * formatting. * * @example * import LatLon, { Dms } from '/js/geodesy/latlon-spherical.js'; * const p = new LatLon(51.2, 0.33).toString('dms'); // 51° 12′ 00″ N, 000° 19′ 48″ E * Dms.separator = ''; // no separator * const pʹ = new LatLon(51.2, 0.33).toString('dms'); // 51°12′00″N, 000°19′48″E */ static get separator() { return dmsSeparator; } static set separator(char) { dmsSeparator = char; } /** * Parses string representing degrees/minutes/seconds into numeric degrees. * * This is very flexible on formats, allowing signed decimal degrees, or deg-min-sec optionally * suffixed by compass direction (NSEW); a variety of separators are accepted. Examples -3.62, * '3 37 12W', '3°37′12″W'. * * Thousands/decimal separators must be comma/dot; use Dms.fromLocale to convert locale-specific * thousands/decimal separators. * * @param {string|number} dms - Degrees or deg/min/sec in variety of formats. * @returns {number} Degrees as decimal number. * * @example * const lat = Dms.parse('51° 28′ 40.37″ N'); * const lon = Dms.parse('000° 00′ 05.29″ W'); * const p1 = new LatLon(lat, lon); // 51.4779°N, 000.0015°W */ static parse(dms) { // check for signed decimal degrees without NSEW, if so return it directly if (!isNaN(parseFloat(dms)) && isFinite(dms)) return Number(dms); // strip off any sign or compass dir'n & split out separate d/m/s const dmsParts = String(dms).trim().replace(/^-/, '').replace(/[NSEW]$/i, '').split(/[^0-9.,]+/); if (dmsParts[dmsParts.length-1]=='') dmsParts.splice(dmsParts.length-1); // from trailing symbol if (dmsParts == '') return NaN; // and convert to decimal degrees... let deg = null; switch (dmsParts.length) { case 3: // interpret 3-part result as d/m/s deg = dmsParts[0]/1 + dmsParts[1]/60 + dmsParts[2]/3600; break; case 2: // interpret 2-part result as d/m deg = dmsParts[0]/1 + dmsParts[1]/60; break; case 1: // just d (possibly decimal) or non-separated dddmmss deg = dmsParts[0]; // check for fixed-width unseparated format eg 0033709W //if (/[NS]/i.test(dmsParts)) deg = '0' + deg; // - normalise N/S to 3-digit degrees //if (/[0-9]{7}/.test(deg)) deg = deg.slice(0,3)/1 + deg.slice(3,5)/60 + deg.slice(5)/3600; break; default: return NaN; } if (/^-|[WS]$/i.test(dms.trim())) deg = -deg; // take '-', west and south as -ve return Number(deg); } /** * Converts decimal degrees to deg/min/sec format * - degree, prime, double-prime symbols are added, but sign is discarded, though no compass * direction is added. * - degrees are zero-padded to 3 digits; for degrees latitude, use .slice(1) to remove leading * zero. * * @private * @param {number} deg - Degrees to be formatted as specified. * @param {string} [format=d] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec. * @param {number} [dp=4|2|0] - Number of decimal places to use – default 4 for d, 2 for dm, 0 for dms. * @returns {string} Degrees formatted as deg/min/secs according to specified format. */ static toDms(deg, format='d', dp=undefined) { if (isNaN(deg)) return null; // give up here if we can't make a number from deg if (typeof deg == 'string' && deg.trim() == '') return null; if (typeof deg == 'boolean') return null; if (deg == Infinity) return null; if (deg == null) return null; // default values if (dp === undefined) { switch (format) { case 'd': case 'deg': dp = 4; break; case 'dm': case 'deg+min': dp = 2; break; case 'dms': case 'deg+min+sec': dp = 0; break; default: format = 'd'; dp = 4; break; // be forgiving on invalid format } } deg = Math.abs(deg); // (unsigned result ready for appending compass dir'n) let dms = null, d = null, m = null, s = null; switch (format) { default: // invalid format spec! case 'd': case 'deg': d = deg.toFixed(dp); // round/right-pad degrees if (d<100) d = '0' + d; // left-pad with leading zeros (note may include decimals) if (d<10) d = '0' + d; dms = d + '°'; break; case 'dm': case 'deg+min': d = Math.floor(deg); // get component deg m = ((deg*60) % 60).toFixed(dp); // get component min & round/right-pad if (m == 60) { m = (0).toFixed(dp); d++; } // check for rounding up d = ('000'+d).slice(-3); // left-pad with leading zeros if (m<10) m = '0' + m; // left-pad with leading zeros (note may include decimals) dms = d + '°'+Dms.separator + m + '′'; break; case 'dms': case 'deg+min+sec': d = Math.floor(deg); // get component deg m = Math.floor((deg*3600)/60) % 60; // get component min s = (deg*3600 % 60).toFixed(dp); // get component sec & round/right-pad if (s == 60) { s = (0).toFixed(dp); m++; } // check for rounding up if (m == 60) { m = 0; d++; } // check for rounding up d = ('000'+d).slice(-3); // left-pad with leading zeros m = ('00'+m).slice(-2); // left-pad with leading zeros if (s<10) s = '0' + s; // left-pad with leading zeros (note may include decimals) dms = d + '°'+Dms.separator + m + '′'+Dms.separator + s + '″'; break; } return dms; } /** * Converts numeric degrees to deg/min/sec latitude (2-digit degrees, suffixed with N/S). * * @param {number} deg - Degrees to be formatted as specified. * @param {string} [format=d] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec. * @param {number} [dp=4|2|0] - Number of decimal places to use – default 4 for d, 2 for dm, 0 for dms. * @returns {string} Degrees formatted as deg/min/secs according to specified format. * * @example * const lat = Dms.toLat(-3.62, 'dms'); // 3°37′12″S */ static toLat(deg, format, dp) { const lat = Dms.toDms(Dms.wrap90(deg), format, dp); return lat===null ? '–' : lat.slice(1) + Dms.separator + (deg<0 ? 'S' : 'N'); // knock off initial '0' for lat! } /** * Convert numeric degrees to deg/min/sec longitude (3-digit degrees, suffixed with E/W). * * @param {number} deg - Degrees to be formatted as specified. * @param {string} [format=d] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec. * @param {number} [dp=4|2|0] - Number of decimal places to use – default 4 for d, 2 for dm, 0 for dms. * @returns {string} Degrees formatted as deg/min/secs according to specified format. * * @example * const lon = Dms.toLon(-3.62, 'dms'); // 3°37′12″W */ static toLon(deg, format, dp) { const lon = Dms.toDms(Dms.wrap180(deg), format, dp); return lon===null ? '–' : lon + Dms.separator + (deg<0 ? 'W' : 'E'); } /** * Converts numeric degrees to deg/min/sec as a bearing (0°..360°). * * @param {number} deg - Degrees to be formatted as specified. * @param {string} [format=d] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec. * @param {number} [dp=4|2|0] - Number of decimal places to use – default 4 for d, 2 for dm, 0 for dms. * @returns {string} Degrees formatted as deg/min/secs according to specified format. * * @example * const lon = Dms.toBrng(-3.62, 'dms'); // 356°22′48″ */ static toBrng(deg, format, dp) { const brng = Dms.toDms(Dms.wrap360(deg), format, dp); return brng===null ? '–' : brng.replace('360', '0'); // just in case rounding took us up to 360°! } /** * Converts DMS string from locale thousands/decimal separators to JavaScript comma/dot separators * for subsequent parsing. * * Both thousands and decimal separators must be followed by a numeric character, to facilitate * parsing of single lat/long string (in which whitespace must be left after the comma separator). * * @param {string} str - Degrees/minutes/seconds formatted with locale separators. * @returns {string} Degrees/minutes/seconds formatted with standard Javascript separators. * * @example * const lat = Dms.fromLocale('51°28′40,12″N'); // '51°28′40.12″N' in France * const p = new LatLon(Dms.fromLocale('51°28′40,37″N, 000°00′05,29″W'); // '51.4779°N, 000.0015°W' in France */ static fromLocale(str) { const locale = (123456.789).toLocaleString(); const separator = { thousands: locale.slice(3, 4), decimal: locale.slice(7, 8) }; return str.replace(separator.thousands, '⁜').replace(separator.decimal, '.').replace('⁜', ','); } /** * Converts DMS string from JavaScript comma/dot thousands/decimal separators to locale separators. * * Can also be used to format standard numbers such as distances. * * @param {string} str - Degrees/minutes/seconds formatted with standard Javascript separators. * @returns {string} Degrees/minutes/seconds formatted with locale separators. * * @example * const Dms.toLocale('123,456.789'); // '123.456,789' in France * const Dms.toLocale('51°28′40.12″N, 000°00′05.31″W'); // '51°28′40,12″N, 000°00′05,31″W' in France */ static toLocale(str) { const locale = (123456.789).toLocaleString(); const separator = { thousands: locale.slice(3, 4), decimal: locale.slice(7, 8) }; return str.replace(/,([0-9])/, '⁜$1').replace('.', separator.decimal).replace('⁜', separator.thousands); } /** * Returns compass point (to given precision) for supplied bearing. * * @param {number} bearing - Bearing in degrees from north. * @param {number} [precision=3] - Precision (1:cardinal / 2:intercardinal / 3:secondary-intercardinal). * @returns {string} Compass point for supplied bearing. * * @example * const point = Dms.compassPoint(24); // point = 'NNE' * const point = Dms.compassPoint(24, 1); // point = 'N' */ static compassPoint(bearing, precision=3) { if (![ 1, 2, 3 ].includes(Number(precision))) throw new RangeError(`invalid precision ‘${precision}’`); // note precision could be extended to 4 for quarter-winds (eg NbNW), but I think they are little used bearing = Dms.wrap360(bearing); // normalise to range 0..360° const cardinals = [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ]; const n = 4 * 2**(precision-1); // no of compass points at req’d precision (1=>4, 2=>8, 3=>16) const cardinal = cardinals[Math.round(bearing*n/360)%n * 16/n]; return cardinal; } /** * Constrain degrees to range -90..+90 (for latitude); e.g. -91 => -89, 91 => 89. * * @private * @param {number} degrees * @returns degrees within range -90..+90. */ static wrap90(degrees) { if (-90<=degrees && degrees<=90) return degrees; // avoid rounding due to arithmetic ops if within range // latitude wrapping requires a triangle wave function; a general triangle wave is // f(x) = 4a/p ⋅ | (x-p/4)%p - p/2 | - a // where a = amplitude, p = period, % = modulo; however, JavaScript '%' is a remainder operator // not a modulo operator - for modulo, replace 'x%n' with '((x%n)+n)%n' const x = degrees, a = 90, p = 360; return 4*a/p * Math.abs((((x-p/4)%p)+p)%p - p/2) - a; } /** * Constrain degrees to range -180..+180 (for longitude); e.g. -181 => 179, 181 => -179. * * @private * @param {number} degrees * @returns degrees within range -180..+180. */ static wrap180(degrees) { if (-180<=degrees && degrees<=180) return degrees; // avoid rounding due to arithmetic ops if within range // longitude wrapping requires a sawtooth wave function; a general sawtooth wave is // f(x) = (2ax/p - p/2) % p - a // where a = amplitude, p = period, % = modulo; however, JavaScript '%' is a remainder operator // not a modulo operator - for modulo, replace 'x%n' with '((x%n)+n)%n' const x = degrees, a = 180, p = 360; return (((2*a*x/p - p/2)%p)+p)%p - a; } /** * Constrain degrees to range 0..360 (for bearings); e.g. -1 => 359, 361 => 1. * * @private * @param {number} degrees * @returns degrees within range 0..360. */ static wrap360(degrees) { if (0<=degrees && degrees<360) return degrees; // avoid rounding due to arithmetic ops if within range // bearing wrapping requires a sawtooth wave function with a vertical offset equal to the // amplitude and a corresponding phase shift; this changes the general sawtooth wave function from // f(x) = (2ax/p - p/2) % p - a // to // f(x) = (2ax/p) % p // where a = amplitude, p = period, % = modulo; however, JavaScript '%' is a remainder operator // not a modulo operator - for modulo, replace 'x%n' with '((x%n)+n)%n' const x = degrees, a = 180, p = 360; return (((2*a*x/p)%p)+p)%p; } } // Extend Number object with methods to convert between degrees & radians Number.prototype.toRadians = function() { return this * Math.PI / 180; }; Number.prototype.toDegrees = function() { return this * 180 / Math.PI; }; /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Vector handling functions (c) Chris Veness 2011-2016 */ /* MIT Licence */ /* www.movable-type.co.uk/scripts/geodesy/docs/module-vector3d.html */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 'use strict'; /** * Library of 3-d vector manipulation routines. * * In a geodesy context, these vectors may be used to represent: * - n-vector representing a normal to point on Earth's surface * - earth-centered, earth fixed vector (≡ Gade’s ‘p-vector’) * - great circle normal to vector (on spherical earth model) * - motion vector on Earth's surface * - etc * * Functions return vectors as return results, so that operations can be chained. * @example var v = v1.cross(v2).dot(v3) // ≡ v1×v2⋅v3 * * @module vector3d */ /** * Creates a 3-d vector. * * The vector may be normalised, or use x/y/z values for eg height relative to the sphere or * ellipsoid, distance from earth centre, etc. * * @constructor * @param {number} x - X component of vector. * @param {number} y - Y component of vector. * @param {number} z - Z component of vector. */ function Vector3d(x, y, z) { // allow instantiation without 'new' if (!(this instanceof Vector3d)) return new Vector3d(x, y, z); this.x = Number(x); this.y = Number(y); this.z = Number(z); } /** * Adds supplied vector to ‘this’ vector. * * @param {Vector3d} v - Vector to be added to this vector. * @returns {Vector3d} Vector representing sum of this and v. */ Vector3d.prototype.plus = function(v) { if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object'); return new Vector3d(this.x + v.x, this.y + v.y, this.z + v.z); }; /** * Subtracts supplied vector from ‘this’ vector. * * @param {Vector3d} v - Vector to be subtracted from this vector. * @returns {Vector3d} Vector representing difference between this and v. */ Vector3d.prototype.minus = function(v) { if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object'); return new Vector3d(this.x - v.x, this.y - v.y, this.z - v.z); }; /** * Multiplies ‘this’ vector by a scalar value. * * @param {number} x - Factor to multiply this vector by. * @returns {Vector3d} Vector scaled by x. */ Vector3d.prototype.times = function(x) { x = Number(x); return new Vector3d(this.x * x, this.y * x, this.z * x); }; /** * Divides ‘this’ vector by a scalar value. * * @param {number} x - Factor to divide this vector by. * @returns {Vector3d} Vector divided by x. */ Vector3d.prototype.dividedBy = function(x) { x = Number(x); return new Vector3d(this.x / x, this.y / x, this.z / x); }; /** * Multiplies ‘this’ vector by the supplied vector using dot (scalar) product. * * @param {Vector3d} v - Vector to be dotted with this vector. * @returns {number} Dot product of ‘this’ and v. */ Vector3d.prototype.dot = function(v) { if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object'); return this.x*v.x + this.y*v.y + this.z*v.z; }; /** * Multiplies ‘this’ vector by the supplied vector using cross (vector) product. * * @param {Vector3d} v - Vector to be crossed with this vector. * @returns {Vector3d} Cross product of ‘this’ and v. */ Vector3d.prototype.cross = function(v) { if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object'); var x = this.y*v.z - this.z*v.y; var y = this.z*v.x - this.x*v.z; var z = this.x*v.y - this.y*v.x; return new Vector3d(x, y, z); }; /** * Negates a vector to point in the opposite direction * * @returns {Vector3d} Negated vector. */ Vector3d.prototype.negate = function() { return new Vector3d(-this.x, -this.y, -this.z); }; /** * Length (magnitude or norm) of ‘this’ vector * * @returns {number} Magnitude of this vector. */ Vector3d.prototype.length = function() { return Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z); }; /** * Normalizes a vector to its unit vector * – if the vector is already unit or is zero magnitude, this is a no-op. * * @returns {Vector3d} Normalised version of this vector. */ Vector3d.prototype.unit = function() { var norm = this.length(); if (norm == 1) return this; if (norm == 0) return this; var x = this.x/norm; var y = this.y/norm; var z = this.z/norm; return new Vector3d(x, y, z); }; /** * Calculates the angle between ‘this’ vector and supplied vector. * * @param {Vector3d} v * @param {Vector3d} [n] - Plane normal: if supplied, angle is -π..+π, signed +ve if this->v is * clockwise looking along n, -ve in opposite direction (if not supplied, angle is always 0..π). * @returns {number} Angle (in radians) between this vector and supplied vector. */ Vector3d.prototype.angleTo = function(v, n) { if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object'); if (!(n instanceof Vector3d || n == undefined)) throw new TypeError('n is not Vector3d object'); var sign = n==undefined ? 1 : Math.sign(this.cross(v).dot(n)); var sinθ = this.cross(v).length() * sign; var cosθ = this.dot(v); return Math.atan2(sinθ, cosθ); }; /** * Rotates ‘this’ point around an axis by a specified angle. * * @param {Vector3d} axis - The axis being rotated around. * @param {number} theta - The angle of rotation (in radians). * @returns {Vector3d} The rotated point. */ Vector3d.prototype.rotateAround = function(axis, theta) { if (!(axis instanceof Vector3d)) throw new TypeError('axis is not Vector3d object'); // en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle // en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix var p1 = this.unit(); var p = [ p1.x, p1.y, p1.z ]; // the point being rotated var a = axis.unit(); // the axis being rotated around var s = Math.sin(theta); var c = Math.cos(theta); // quaternion-derived rotation matrix var q = [ [ a.x*a.x*(1-c) + c, a.x*a.y*(1-c) - a.z*s, a.x*a.z*(1-c) + a.y*s ], [ a.y*a.x*(1-c) + a.z*s, a.y*a.y*(1-c) + c, a.y*a.z*(1-c) - a.x*s ], [ a.z*a.x*(1-c) - a.y*s, a.z*a.y*(1-c) + a.x*s, a.z*a.z*(1-c) + c ], ]; // multiply q × p var qp = [ 0, 0, 0 ]; for (var i=0; i<3; i++) { for (var j=0; j<3; j++) { qp[i] += q[i][j] * p[j]; } } var p2 = new Vector3d(qp[0], qp[1], qp[2]); return p2; // qv en.wikipedia.org/wiki/Rodrigues'_rotation_formula... }; /** * String representation of vector. * * @param {number} [precision=3] - Number of decimal places to be used. * @returns {string} Vector represented as [x,y,z]. */ Vector3d.prototype.toString = function(precision) { var p = (precision === undefined) ? 3 : Number(precision); var str = '[' + this.x.toFixed(p) + ',' + this.y.toFixed(p) + ',' + this.z.toFixed(p) + ']'; return str; }; /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** Polyfill Math.sign for old browsers / IE */ if (Math.sign === undefined) { Math.sign = function(x) { x = +x; // convert to a number if (x === 0 || isNaN(x)) return x; return x > 0 ? 1 : -1; }; } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ if (typeof module != 'undefined' && module.exports) module.exports = Vector3d; // ≡ export default Vector3d /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Geodesy tools for an ellipsoidal earth model (c) Chris Veness 2005-2016 */ /* MIT Licence */ /* www.movable-type.co.uk/scripts/latlong-convert-coords.html */ /* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-ellipsoidal.html */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 'use strict'; if (typeof module!='undefined' && module.exports) var Vector3d = require('./vector3d.js'); // ≡ import Vector3d from 'vector3d.js' /** * Library of geodesy functions for operations on an ellipsoidal earth model. * * Includes ellipsoid parameters and datums for different coordinate systems, and methods for * converting between them and to cartesian coordinates. * * q.v. Ordnance Survey ‘A guide to coordinate systems in Great Britain’ Section 6 * www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf. * * @module latlon-ellipsoidal * @requires dms */ /** * Creates lat/lon (polar) point with latitude & longitude values, on a specified datum. * * @constructor * @param {number} lat - Geodetic latitude in degrees. * @param {number} lon - Longitude in degrees. * @param {LatLon.datum} [datum=WGS84] - Datum this point is defined within. * * @example * var p1 = new LatLon(51.4778, -0.0016, LatLon.datum.WGS84); */ function LatLon(lat, lon, datum) { // allow instantiation without 'new' if (!(this instanceof LatLon)) return new LatLon(lat, lon, datum); if (datum === undefined) datum = LatLon.datum.WGS84; this.lat = Number(lat); this.lon = Number(lon); this.datum = datum; } /** * Ellipsoid parameters; major axis (a), minor axis (b), and flattening (f) for each ellipsoid. */ LatLon.ellipsoid = { WGS84: { a: 6378137, b: 6356752.314245, f: 1/298.257223563 }, Airy1830: { a: 6377563.396, b: 6356256.909, f: 1/299.3249646 }, AiryModified: { a: 6377340.189, b: 6356034.448, f: 1/299.3249646 }, Bessel1841: { a: 6377397.155, b: 6356078.962818, f: 1/299.1528128 }, Clarke1866: { a: 6378206.4, b: 6356583.8, f: 1/294.978698214 }, Clarke1880IGN: { a: 6378249.2, b: 6356515.0, f: 1/293.466021294 }, GRS80: { a: 6378137, b: 6356752.314140, f: 1/298.257222101 }, Intl1924: { a: 6378388, b: 6356911.946, f: 1/297 }, // aka Hayford WGS72: { a: 6378135, b: 6356750.5, f: 1/298.26 }, }; /** * Datums; with associated ellipsoid, and Helmert transform parameters to convert from WGS 84 into * given datum. * * Note that precision of various datums will vary, and WGS-84 (original) is not defined to be * accurate to better than ±1 metre. No transformation should be assumed to be accurate to better * than a meter; for many datums somewhat less. */ LatLon.datum = { // transforms: t in metres, s in ppm, r in arcseconds tx ty tz s rx ry rz ED50: { ellipsoid: LatLon.ellipsoid.Intl1924, transform: [ 89.5, 93.8, 123.1, -1.2, 0.0, 0.0, 0.156 ] }, Irl1975: { ellipsoid: LatLon.ellipsoid.AiryModified, transform: [ -482.530, 130.596, -564.557, -8.150, -1.042, -0.214, -0.631 ] }, NAD27: { ellipsoid: LatLon.ellipsoid.Clarke1866, transform: [ 8, -160, -176, 0, 0, 0, 0 ] }, NAD83: { ellipsoid: LatLon.ellipsoid.GRS80, transform: [ 1.004, -1.910, -0.515, -0.0015, 0.0267, 0.00034, 0.011 ] }, NTF: { ellipsoid: LatLon.ellipsoid.Clarke1880IGN, transform: [ 168, 60, -320, 0, 0, 0, 0 ] }, OSGB36: { ellipsoid: LatLon.ellipsoid.Airy1830, transform: [ -446.448, 125.157, -542.060, 20.4894, -0.1502, -0.2470, -0.8421 ] }, Potsdam: { ellipsoid: LatLon.ellipsoid.Bessel1841, transform: [ -582, -105, -414, -8.3, 1.04, 0.35, -3.08 ] }, TokyoJapan: { ellipsoid: LatLon.ellipsoid.Bessel1841, transform: [ 148, -507, -685, 0, 0, 0, 0 ] }, WGS72: { ellipsoid: LatLon.ellipsoid.WGS72, transform: [ 0, 0, -4.5, -0.22, 0, 0, 0.554 ] }, WGS84: { ellipsoid: LatLon.ellipsoid.WGS84, transform: [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] }, }; /* sources: * - ED50: www.gov.uk/guidance/oil-and-gas-petroleum-operations-notices#pon-4 * - Irl1975: www.osi.ie/wp-content/uploads/2015/05/transformations_booklet.pdf * ... note: many sources have opposite sign to rotations - to be checked! * - NAD27: en.wikipedia.org/wiki/Helmert_transformation * - NAD83: (2009); www.uvm.edu/giv/resources/WGS84_NAD83.pdf * ... note: functionally ≡ WGS84 - if you *really* need to convert WGS84<->NAD83, you need more knowledge than this! * - NTF: Nouvelle Triangulation Francaise geodesie.ign.fr/contenu/fichiers/Changement_systeme_geodesique.pdf * - OSGB36: www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf * - Potsdam: kartoweb.itc.nl/geometrics/Coordinate%20transformations/coordtrans.html * - TokyoJapan: www.geocachingtoolbox.com?page=datumEllipsoidDetails * - WGS72: www.icao.int/safety/pbn/documentation/eurocontrol/eurocontrol wgs 84 implementation manual.pdf * * more transform parameters are available from earth-info.nga.mil/GandG/coordsys/datums/NATO_DT.pdf, * www.fieldenmaps.info/cconv/web/cconv_params.js */ /** * Converts ‘this’ lat/lon coordinate to new coordinate system. * * @param {LatLon.datum} toDatum - Datum this coordinate is to be converted to. * @returns {LatLon} This point converted to new datum. * * @example * var pWGS84 = new LatLon(51.4778, -0.0016, LatLon.datum.WGS84); * var pOSGB = pWGS84.convertDatum(LatLon.datum.OSGB36); // 51.4773°N, 000.0000°E */ LatLon.prototype.convertDatum = function(toDatum) { var oldLatLon = this; var transform = null; if (oldLatLon.datum == LatLon.datum.WGS84) { // converting from WGS 84 transform = toDatum.transform; } if (toDatum == LatLon.datum.WGS84) { // converting to WGS 84; use inverse transform (don't overwrite original!) transform = []; for (var p=0; p<7; p++) transform[p] = -oldLatLon.datum.transform[p]; } if (transform == null) { // neither this.datum nor toDatum are WGS84: convert this to WGS84 first oldLatLon = this.convertDatum(LatLon.datum.WGS84); transform = toDatum.transform; } var oldCartesian = oldLatLon.toCartesian(); // convert polar to cartesian... var newCartesian = oldCartesian.applyTransform(transform); // ...apply transform... var newLatLon = newCartesian.toLatLonE(toDatum); // ...and convert cartesian to polar return newLatLon; }; /** * Converts ‘this’ point from (geodetic) latitude/longitude coordinates to (geocentric) cartesian * (x/y/z) coordinates. * * @returns {Vector3d} Vector pointing to lat/lon point, with x, y, z in metres from earth centre. */ LatLon.prototype.toCartesian = function() { var φ = this.lat.toRadians(), λ = this.lon.toRadians(); var h = 0; // height above ellipsoid - not currently used var a = this.datum.ellipsoid.a, f = this.datum.ellipsoid.f; var sinφ = Math.sin(φ), cosφ = Math.cos(φ); var sinλ = Math.sin(λ), cosλ = Math.cos(λ); var eSq = 2*f - f*f; // 1st eccentricity squared ≡ (a²-b²)/a² var ν = a / Math.sqrt(1 - eSq*sinφ*sinφ); // radius of curvature in prime vertical var x = (ν+h) * cosφ * cosλ; var y = (ν+h) * cosφ * sinλ; var z = (ν*(1-eSq)+h) * sinφ; var point = new Vector3d(x, y, z); return point; }; /** * Converts ‘this’ (geocentric) cartesian (x/y/z) point to (ellipsoidal geodetic) latitude/longitude * coordinates on specified datum. * * Uses Bowring’s (1985) formulation for μm precision in concise form. * * @param {LatLon.datum.transform} datum - Datum to use when converting point. */ Vector3d.prototype.toLatLonE = function(datum) { var x = this.x, y = this.y, z = this.z; var a = datum.ellipsoid.a, b = datum.ellipsoid.b, f = datum.ellipsoid.f; var e2 = 2*f - f*f; // 1st eccentricity squared ≡ (a²-b²)/a² var ε2 = e2 / (1-e2); // 2nd eccentricity squared ≡ (a²-b²)/b² var p = Math.sqrt(x*x + y*y); // distance from minor axis var R = Math.sqrt(p*p + z*z); // polar radius // parametric latitude (Bowring eqn 17, replacing tanβ = z·a / p·b) var tanβ = (b*z)/(a*p) * (1+ε2*b/R); var sinβ = tanβ / Math.sqrt(1+tanβ*tanβ); var cosβ = sinβ / tanβ; // geodetic latitude (Bowring eqn 18: tanφ = z+ε²bsin³β / p−e²cos³β) var φ = isNaN(cosβ) ? 0 : Math.atan2(z + ε2*b*sinβ*sinβ*sinβ, p - e2*a*cosβ*cosβ*cosβ); // longitude var λ = Math.atan2(y, x); // height above ellipsoid (Bowring eqn 7) [not currently used] var sinφ = Math.sin(φ), cosφ = Math.cos(φ); var ν = a/Math.sqrt(1-e2*sinφ*sinφ); // length of the normal terminated by the minor axis var h = p*cosφ + z*sinφ - (a*a/ν); var point = new LatLon(φ.toDegrees(), λ.toDegrees(), datum); return point; }; /** * Applies Helmert transform to ‘this’ point using transform parameters t. * * @private * @param {number[]} t - Transform to apply to this point. * @returns {Vector3} Transformed point. */ Vector3d.prototype.applyTransform = function(t) { // this point var x1 = this.x, y1 = this.y, z1 = this.z; // transform parameters var tx = t[0]; // x-shift var ty = t[1]; // y-shift var tz = t[2]; // z-shift var s1 = t[3]/1e6 + 1; // scale: normalise parts-per-million to (s+1) var rx = (t[4]/3600).toRadians(); // x-rotation: normalise arcseconds to radians var ry = (t[5]/3600).toRadians(); // y-rotation: normalise arcseconds to radians var rz = (t[6]/3600).toRadians(); // z-rotation: normalise arcseconds to radians // apply transform var x2 = tx + x1*s1 - y1*rz + z1*ry; var y2 = ty + x1*rz + y1*s1 - z1*rx; var z2 = tz - x1*ry + y1*rx + z1*s1; return new Vector3d(x2, y2, z2); }; /** * Returns a string representation of ‘this’ point, formatted as degrees, degrees+minutes, or * degrees+minutes+seconds. * * @param {string} [format=dms] - Format point as 'd', 'dm', 'dms'. * @param {number} [dp=0|2|4] - Number of decimal places to use - default 0 for dms, 2 for dm, 4 for d. * @returns {string} Comma-separated latitude/longitude. */ LatLon.prototype.toString = function(format, dp) { return Dms.toLat(this.lat, format, dp) + ', ' + Dms.toLon(this.lon, format, dp); }; /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** Extend Number object with method to convert numeric degrees to radians */ if (Number.prototype.toRadians === undefined) { Number.prototype.toRadians = function() { return this * Math.PI / 180; }; } /** Extend Number object with method to convert radians to numeric (signed) degrees */ if (Number.prototype.toDegrees === undefined) { Number.prototype.toDegrees = function() { return this * 180 / Math.PI; }; } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* UTM / WGS-84 Conversion Functions (c) Chris Veness 2014-2017 */ /* MIT Licence */ /* www.movable-type.co.uk/scripts/latlong-utm-mgrs.html */ /* www.movable-type.co.uk/scripts/geodesy/docs/module-utm.html */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * Convert between Universal Transverse Mercator coordinates and WGS 84 latitude/longitude points. * * Method based on Karney 2011 ‘Transverse Mercator with an accuracy of a few nanometers’, * building on Krüger 1912 ‘Konforme Abbildung des Erdellipsoids in der Ebene’. * * @module utm * @requires latlon-ellipsoidal */ /** * Creates a Utm coordinate object. * * @constructor * @param {number} zone - UTM 6° longitudinal zone (1..60 covering 180°W..180°E). * @param {string} hemisphere - N for northern hemisphere, S for southern hemisphere. * @param {number} easting - Easting in metres from false easting (-500km from central meridian). * @param {number} northing - Northing in metres from equator (N) or from false northing -10,000km (S). * @param {LatLon.datum} [datum=WGS84] - Datum UTM coordinate is based on. * @param {number} [convergence] - Meridian convergence (bearing of grid north clockwise from true * north), in degrees * @param {number} [scale] - Grid scale factor * @throws {Error} Invalid UTM coordinate * * @example * var utmCoord = new Utm(31, 'N', 448251, 5411932); */ function Utm(zone, hemisphere, easting, northing, datum, convergence, scale) { if (!(this instanceof Utm)) { // allow instantiation without 'new' return new Utm(zone, hemisphere, easting, northing, datum, convergence, scale); } if (datum === undefined) datum = LatLon.datum.WGS84; // default if not supplied if (convergence === undefined) convergence = null; // default if not supplied if (scale === undefined) scale = null; // default if not supplied if (!(1<=zone && zone<=60)) throw new Error('Invalid UTM zone '+zone); if (!hemisphere.match(/[NS]/i)) throw new Error('Invalid UTM hemisphere '+hemisphere); // range-check easting/northing (with 40km overlap between zones) - is this worthwhile? //if (!(120e3<=easting && easting<=880e3)) throw new Error('Invalid UTM easting '+ easting); //if (!(0<=northing && northing<=10000e3)) throw new Error('Invalid UTM northing '+ northing); this.zone = Number(zone); this.hemisphere = hemisphere.toUpperCase(); this.easting = Number(easting); this.northing = Number(northing); this.datum = datum; this.convergence = convergence===null ? null : Number(convergence); this.scale = scale===null ? null : Number(scale); } /** * Converts latitude/longitude to UTM coordinate. * * Implements Karney’s method, using Krüger series to order n^6, giving results accurate to 5nm for * distances up to 3900km from the central meridian. * * @returns {Utm} UTM coordinate. * @throws {Error} If point not valid, if point outside latitude range. * * @example * var latlong = new LatLon(48.8582, 2.2945); * var utmCoord = latlong.toUtm(); // utmCoord.toString(): '31 N 448252 5411933' */ LatLon.prototype.toUtm = function() { if (isNaN(this.lat) || isNaN(this.lon)) throw new Error('Invalid point'); if (!(-80<=this.lat && this.lat<=84)) throw new Error('Outside UTM limits'); var falseEasting = 500e3, falseNorthing = 10000e3; var zone = Math.floor((this.lon+180)/6) + 1; // longitudinal zone var λ0 = ((zone-1)*6 - 180 + 3).toRadians(); // longitude of central meridian // ---- handle Norway/Svalbard exceptions // grid zones are 8° tall; 0°N is offset 10 into latitude bands array var mgrsLatBands = 'CDEFGHJKLMNPQRSTUVWXX'; // X is repeated for 80-84°N var latBand = mgrsLatBands.charAt(Math.floor(this.lat/8+10)); // adjust zone & central meridian for Norway if (zone==31 && latBand=='V' && this.lon>= 3) { zone++; λ0 += (6).toRadians(); } // adjust zone & central meridian for Svalbard if (zone==32 && latBand=='X' && this.lon< 9) { zone--; λ0 -= (6).toRadians(); } if (zone==32 && latBand=='X' && this.lon>= 9) { zone++; λ0 += (6).toRadians(); } if (zone==34 && latBand=='X' && this.lon< 21) { zone--; λ0 -= (6).toRadians(); } if (zone==34 && latBand=='X' && this.lon>=21) { zone++; λ0 += (6).toRadians(); } if (zone==36 && latBand=='X' && this.lon< 33) { zone--; λ0 -= (6).toRadians(); } if (zone==36 && latBand=='X' && this.lon>=33) { zone++; λ0 += (6).toRadians(); } var φ = this.lat.toRadians(); // latitude ± from equator var λ = this.lon.toRadians() - λ0; // longitude ± from central meridian var a = this.datum.ellipsoid.a, f = this.datum.ellipsoid.f; // WGS 84: a = 6378137, b = 6356752.314245, f = 1/298.257223563; var k0 = 0.9996; // UTM scale on the central meridian // ---- easting, northing: Karney 2011 Eq 7-14, 29, 35: var e = Math.sqrt(f*(2-f)); // eccentricity var n = f / (2 - f); // 3rd flattening var n2 = n*n, n3 = n*n2, n4 = n*n3, n5 = n*n4, n6 = n*n5; // TODO: compare Horner-form accuracy? var cosλ = Math.cos(λ), sinλ = Math.sin(λ), tanλ = Math.tan(λ); var τ = Math.tan(φ); // τ ≡ tanφ, τʹ ≡ tanφʹ; prime (ʹ) indicates angles on the conformal sphere var σ = Math.sinh(e*Math.atanh(e*τ/Math.sqrt(1+τ*τ))); var τʹ = τ*Math.sqrt(1+σ*σ) - σ*Math.sqrt(1+τ*τ); var ξʹ = Math.atan2(τʹ, cosλ); var ηʹ = Math.asinh(sinλ / Math.sqrt(τʹ*τʹ + cosλ*cosλ)); var A = a/(1+n) * (1 + 1/4*n2 + 1/64*n4 + 1/256*n6); // 2πA is the circumference of a meridian var α = [ null, // note α is one-based array (6th order Krüger expressions) 1/2*n - 2/3*n2 + 5/16*n3 + 41/180*n4 - 127/288*n5 + 7891/37800*n6, 13/48*n2 - 3/5*n3 + 557/1440*n4 + 281/630*n5 - 1983433/1935360*n6, 61/240*n3 - 103/140*n4 + 15061/26880*n5 + 167603/181440*n6, 49561/161280*n4 - 179/168*n5 + 6601661/7257600*n6, 34729/80640*n5 - 3418889/1995840*n6, 212378941/319334400*n6 ]; var ξ = ξʹ; for (var j=1; j<=6; j++) ξ += α[j] * Math.sin(2*j*ξʹ) * Math.cosh(2*j*ηʹ); var η = ηʹ; for (var j=1; j<=6; j++) η += α[j] * Math.cos(2*j*ξʹ) * Math.sinh(2*j*ηʹ); var x = k0 * A * η; var y = k0 * A * ξ; // ---- convergence: Karney 2011 Eq 23, 24 var pʹ = 1; for (var j=1; j<=6; j++) pʹ += 2*j*α[j] * Math.cos(2*j*ξʹ) * Math.cosh(2*j*ηʹ); var qʹ = 0; for (var j=1; j<=6; j++) qʹ += 2*j*α[j] * Math.sin(2*j*ξʹ) * Math.sinh(2*j*ηʹ); var γʹ = Math.atan(τʹ / Math.sqrt(1+τʹ*τʹ)*tanλ); var γʺ = Math.atan2(qʹ, pʹ); var γ = γʹ + γʺ; // ---- scale: Karney 2011 Eq 25 var sinφ = Math.sin(φ); var kʹ = Math.sqrt(1 - e*e*sinφ*sinφ) * Math.sqrt(1 + τ*τ) / Math.sqrt(τʹ*τʹ + cosλ*cosλ); var kʺ = A / a * Math.sqrt(pʹ*pʹ + qʹ*qʹ); var k = k0 * kʹ * kʺ; // ------------ // shift x/y to false origins x = x + falseEasting; // make x relative to false easting if (y < 0) y = y + falseNorthing; // make y in southern hemisphere relative to false northing // round to reasonable precision x = Number(x.toFixed(6)); // nm precision y = Number(y.toFixed(6)); // nm precision var convergence = Number(γ.toDegrees().toFixed(9)); var scale = Number(k.toFixed(12)); var h = this.lat>=0 ? 'N' : 'S'; // hemisphere return new Utm(zone, h, x, y, this.datum, convergence, scale); }; /** * Returns a string representation of a UTM coordinate. * * To distinguish from MGRS grid zone designators, a space is left between the zone and the * hemisphere. * * Note that UTM coordinates get rounded, not truncated (unlike MGRS grid references). * * @param {number} [digits=0] - Number of digits to appear after the decimal point (3 ≡ mm). * @returns {string} A string representation of the coordinate. * * @example * var utm = Utm.parse('31 N 448251 5411932').toString(4); // 31 N 448251.0000 5411932.0000 */ Utm.prototype.toString = function(digits) { digits = Number(digits||0); // default 0 if not supplied var z = this.zone<10 ? '0'+this.zone : this.zone; // leading zero var h = this.hemisphere; var e = this.easting; var n = this.northing; if (isNaN(z) || !h.match(/[NS]/) || isNaN(e) || isNaN(n)) return ''; return z+' '+h+' '+e.toFixed(digits)+' '+n.toFixed(digits); }; /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** Polyfill Math.sinh for old browsers / IE */ if (Math.sinh === undefined) { Math.sinh = function(x) { return (Math.exp(x) - Math.exp(-x)) / 2; }; } /** Polyfill Math.cosh for old browsers / IE */ if (Math.cosh === undefined) { Math.cosh = function(x) { return (Math.exp(x) + Math.exp(-x)) / 2; }; } /** Polyfill Math.tanh for old browsers / IE */ if (Math.tanh === undefined) { Math.tanh = function(x) { return (Math.exp(x) - Math.exp(-x)) / (Math.exp(x) + Math.exp(-x)); }; } /** Polyfill Math.asinh for old browsers / IE */ if (Math.asinh === undefined) { Math.asinh = function(x) { return Math.log(x + Math.sqrt(1 + x*x)); }; } /** Polyfill Math.atanh for old browsers / IE */ if (Math.atanh === undefined) { Math.atanh = function(x) { return Math.log((1+x) / (1-x)) / 2; }; } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Ordnance Survey Grid Reference functions (c) Chris Veness 2005-2017 */ /* MIT Licence */ /* www.movable-type.co.uk/scripts/latlong-gridref.html */ /* www.movable-type.co.uk/scripts/geodesy/docs/module-osgridref.html */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * Convert OS grid references to/from OSGB latitude/longitude points. * * Formulation implemented here due to Thomas, Redfearn, etc is as published by OS, but is inferior * to Krüger as used by e.g. Karney 2011. * * www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf. * * @module osgridref * @requires latlon-ellipsoidal */ /* * Converted 2015 to work with WGS84 by default, OSGB36 as option; * www.ordnancesurvey.co.uk/blog/2014/12/confirmation-on-changes-to-latitude-and-longitude */ /** * Creates an OsGridRef object. * * @constructor * @param {number} easting - Easting in metres from OS false origin. * @param {number} northing - Northing in metres from OS false origin. * * @example * var grid = new OsGridRef(651409, 313177); */ function OsGridRef(easting, northing) { // allow instantiation without 'new' if (!(this instanceof OsGridRef)) return new OsGridRef(easting, northing); this.easting = Number(easting); this.northing = Number(northing); } /** * Converts latitude/longitude to Ordnance Survey grid reference easting/northing coordinate. * * Note formulation implemented here due to Thomas, Redfearn, etc is as published by OS, but is * inferior to Krüger as used by e.g. Karney 2011. * * @param {LatLon} point - latitude/longitude. * @returns {OsGridRef} OS Grid Reference easting/northing. * * @example * var p = new LatLon(52.65798, 1.71605); * var grid = OsGridRef.latLonToOsGrid(p); // grid.toString(): TG 51409 13177 * // for conversion of (historical) OSGB36 latitude/longitude point: * var p = new LatLon(52.65757, 1.71791, LatLon.datum.OSGB36); */ OsGridRef.latLonToOsGrid = function(point) { if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); // if necessary convert to OSGB36 first if (point.datum != LatLon.datum.OSGB36) point = point.convertDatum(LatLon.datum.OSGB36); var φ = point.lat.toRadians(); var λ = point.lon.toRadians(); var a = 6377563.396, b = 6356256.909; // Airy 1830 major & minor semi-axes var F0 = 0.9996012717; // NatGrid scale factor on central meridian var φ0 = (49).toRadians(), λ0 = (-2).toRadians(); // NatGrid true origin is 49°N,2°W var N0 = -100000, E0 = 400000; // northing & easting of true origin, metres var e2 = 1 - (b*b)/(a*a); // eccentricity squared var n = (a-b)/(a+b), n2 = n*n, n3 = n*n*n; // n, n², n³ var cosφ = Math.cos(φ), sinφ = Math.sin(φ); var ν = a*F0/Math.sqrt(1-e2*sinφ*sinφ); // nu = transverse radius of curvature var ρ = a*F0*(1-e2)/Math.pow(1-e2*sinφ*sinφ, 1.5); // rho = meridional radius of curvature var η2 = ν/ρ-1; // eta = ? var Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (φ-φ0); var Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(φ-φ0) * Math.cos(φ+φ0); var Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(φ-φ0)) * Math.cos(2*(φ+φ0)); var Md = (35/24)*n3 * Math.sin(3*(φ-φ0)) * Math.cos(3*(φ+φ0)); var M = b * F0 * (Ma - Mb + Mc - Md); // meridional arc var cos3φ = cosφ*cosφ*cosφ; var cos5φ = cos3φ*cosφ*cosφ; var tan2φ = Math.tan(φ)*Math.tan(φ); var tan4φ = tan2φ*tan2φ; var I = M + N0; var II = (ν/2)*sinφ*cosφ; var III = (ν/24)*sinφ*cos3φ*(5-tan2φ+9*η2); var IIIA = (ν/720)*sinφ*cos5φ*(61-58*tan2φ+tan4φ); var IV = ν*cosφ; var V = (ν/6)*cos3φ*(ν/ρ-tan2φ); var VI = (ν/120) * cos5φ * (5 - 18*tan2φ + tan4φ + 14*η2 - 58*tan2φ*η2); var Δλ = λ-λ0; var Δλ2 = Δλ*Δλ, Δλ3 = Δλ2*Δλ, Δλ4 = Δλ3*Δλ, Δλ5 = Δλ4*Δλ, Δλ6 = Δλ5*Δλ; var N = I + II*Δλ2 + III*Δλ4 + IIIA*Δλ6; var E = E0 + IV*Δλ + V*Δλ3 + VI*Δλ5; N = Number(N.toFixed(3)); // round to mm precision E = Number(E.toFixed(3)); return new OsGridRef(E, N); // gets truncated to SW corner of 1m grid square }; /** * Converts ‘this’ numeric grid reference to standard OS grid reference. * * @param {number} [digits=10] - Precision of returned grid reference (10 digits = metres); * digits=0 will return grid reference in numeric format. * @returns {string} This grid reference in standard format. * * @example * var ref = new OsGridRef(651409, 313177).toString(); // TG 51409 13177 */ OsGridRef.prototype.toString = function(digits) { digits = (digits === undefined) ? 10 : Number(digits); if (isNaN(digits) || digits%2!=0 || digits>16) throw new RangeError('Invalid precision ‘'+digits+'’'); var e = this.easting; var n = this.northing; if (isNaN(e) || isNaN(n)) throw new Error('Invalid grid reference'); // use digits = 0 to return numeric format (in metres, allowing for decimals & for northing > 1e6) if (digits == 0) { var eInt = Math.floor(e), eDec = e - eInt; var nInt = Math.floor(n), nDec = n - nInt; var ePad = ('000000'+eInt).slice(-6) + (eDec>0 ? eDec.toFixed(3).slice(1) : ''); var nPad = (nInt<1e6 ? ('000000'+nInt).slice(-6) : nInt) + (nDec>0 ? nDec.toFixed(3).slice(1) : ''); return ePad + ',' + nPad; } // get the 100km-grid indices var e100k = Math.floor(e/100000), n100k = Math.floor(n/100000); if (e100k<0 || e100k>6 || n100k<0 || n100k>12) return ''; // translate those into numeric equivalents of the grid letters var l1 = (19-n100k) - (19-n100k)%5 + Math.floor((e100k+10)/5); var l2 = (19-n100k)*5%25 + e100k%5; // compensate for skipped 'I' and build grid letter-pairs if (l1 > 7) l1++; if (l2 > 7) l2++; var letterPair = String.fromCharCode(l1+'A'.charCodeAt(0), l2+'A'.charCodeAt(0)); // strip 100km-grid indices from easting & northing, and reduce precision e = Math.floor((e%100000)/Math.pow(10, 5-digits/2)); n = Math.floor((n%100000)/Math.pow(10, 5-digits/2)); // pad eastings & northings with leading zeros (just in case, allow up to 16-digit (mm) refs) e = ('00000000'+e).slice(-digits/2); n = ('00000000'+n).slice(-digits/2); return letterPair + ' ' + e + ' ' + n; }; /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Latitude/longitude spherical geodesy tools (c) Chris Veness 2002-2022 */ /* MIT Licence */ /* www.movable-type.co.uk/scripts/latlong.html */ /* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-spherical */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ const π = Math.PI; /** * Library of geodesy functions for operations on a spherical earth model. * * Includes distances, bearings, destinations, etc, for both great circle paths and rhumb lines, * and other related functions. * * All calculations are done using simple spherical trigonometric formulae. * * @module latlon-spherical */ // note greek letters (e.g. φ, λ, θ) are used for angles in radians to distinguish from angles in // degrees (e.g. lat, lon, brng) /* LatLonSpherical - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * Latitude/longitude points on a spherical model earth, and methods for calculating distances, * bearings, destinations, etc on (orthodromic) great-circle paths and (loxodromic) rhumb lines. */ class LatLonSpherical { /** * Creates a latitude/longitude point on the earth’s surface, using a spherical model earth. * * @param {number} lat - Latitude (in degrees). * @param {number} lon - Longitude (in degrees). * @throws {TypeError} Invalid lat/lon. * * @example * import LatLon from '/js/geodesy/latlon-spherical.js'; * const p = new LatLon(52.205, 0.119); */ constructor(lat, lon) { if (isNaN(lat)) throw new TypeError(`invalid lat ‘${lat}’`); if (isNaN(lon)) throw new TypeError(`invalid lon ‘${lon}’`); this._lat = Dms.wrap90(Number(lat)); this._lon = Dms.wrap180(Number(lon)); } /** * Latitude in degrees north from equator (including aliases lat, latitude): can be set as * numeric or hexagesimal (deg-min-sec); returned as numeric. */ get lat() { return this._lat; } get latitude() { return this._lat; } set lat(lat) { this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(lat)); if (isNaN(this._lat)) throw new TypeError(`invalid lat ‘${lat}’`); } set latitude(lat) { this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(lat)); if (isNaN(this._lat)) throw new TypeError(`invalid latitude ‘${lat}’`); } /** * Longitude in degrees east from international reference meridian (including aliases lon, lng, * longitude): can be set as numeric or hexagesimal (deg-min-sec); returned as numeric. */ get lon() { return this._lon; } get lng() { return this._lon; } get longitude() { return this._lon; } set lon(lon) { this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon)); if (isNaN(this._lon)) throw new TypeError(`invalid lon ‘${lon}’`); } set lng(lon) { this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon)); if (isNaN(this._lon)) throw new TypeError(`invalid lng ‘${lon}’`); } set longitude(lon) { this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon)); if (isNaN(this._lon)) throw new TypeError(`invalid longitude ‘${lon}’`); } /** * Parses a latitude/longitude point from a variety of formats. * * Latitude & longitude (in degrees) can be supplied as two separate parameters, as a single * comma-separated lat/lon string, or as a single object with { lat, lon } or GeoJSON properties. * * The latitude/longitude values may be numeric or strings; they may be signed decimal or * deg-min-sec (hexagesimal) suffixed by compass direction (NSEW); a variety of separators are * accepted. Examples -3.62, '3 37 12W', '3°37′12″W'. * * Thousands/decimal separators must be comma/dot; use Dms.fromLocale to convert locale-specific * thousands/decimal separators. * * @param {number|string|Object} lat|latlon - Latitude (in degrees) or comma-separated lat/lon or lat/lon object. * @param {number|string} [lon] - Longitude (in degrees). * @returns {LatLon} Latitude/longitude point. * @throws {TypeError} Invalid point. * * @example * const p1 = LatLon.parse(52.205, 0.119); // numeric pair (≡ new LatLon) * const p2 = LatLon.parse('52.205', '0.119'); // numeric string pair (≡ new LatLon) * const p3 = LatLon.parse('52.205, 0.119'); // single string numerics * const p4 = LatLon.parse('52°12′18.0″N', '000°07′08.4″E'); // DMS pair * const p5 = LatLon.parse('52°12′18.0″N, 000°07′08.4″E'); // single string DMS * const p6 = LatLon.parse({ lat: 52.205, lon: 0.119 }); // { lat, lon } object numeric * const p7 = LatLon.parse({ lat: '52°12′18.0″N', lng: '000°07′08.4″E' }); // { lat, lng } object DMS * const p8 = LatLon.parse({ type: 'Point', coordinates: [ 0.119, 52.205] }); // GeoJSON */ static parse(...args) { if (args.length == 0) throw new TypeError('invalid (empty) point'); if (args[0]===null || args[1]===null) throw new TypeError('invalid (null) point'); let lat=undefined, lon=undefined; if (args.length == 2) { // regular (lat, lon) arguments [ lat, lon ] = args; lat = Dms.wrap90(Dms.parse(lat)); lon = Dms.wrap180(Dms.parse(lon)); if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args.toString()}’`); } if (args.length == 1 && typeof args[0] == 'string') { // single comma-separated lat,lon string [ lat, lon ] = args[0].split(','); lat = Dms.wrap90(Dms.parse(lat)); lon = Dms.wrap180(Dms.parse(lon)); if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args[0]}’`); } if (args.length == 1 && typeof args[0] == 'object') { // single { lat, lon } object const ll = args[0]; if (ll.type == 'Point' && Array.isArray(ll.coordinates)) { // GeoJSON [ lon, lat ] = ll.coordinates; } else { // regular { lat, lon } object if (ll.latitude != undefined) lat = ll.latitude; if (ll.lat != undefined) lat = ll.lat; if (ll.longitude != undefined) lon = ll.longitude; if (ll.lng != undefined) lon = ll.lng; if (ll.lon != undefined) lon = ll.lon; lat = Dms.wrap90(Dms.parse(lat)); lon = Dms.wrap180(Dms.parse(lon)); } if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${JSON.stringify(args[0])}’`); } if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args.toString()}’`); return new LatLonSpherical(lat, lon); } /** * Returns the distance along the surface of the earth from ‘this’ point to destination point. * * Uses haversine formula: a = sin²(Δφ/2) + cosφ1·cosφ2 · sin²(Δλ/2); d = 2 · atan2(√a, √(a-1)). * * @param {LatLon} point - Latitude/longitude of destination point. * @param {number} [radius=6371e3] - Radius of earth (defaults to mean radius in metres). * @returns {number} Distance between this point and destination point, in same units as radius. * @throws {TypeError} Invalid radius. * * @example * const p1 = new LatLon(52.205, 0.119); * const p2 = new LatLon(48.857, 2.351); * const d = p1.distanceTo(p2); // 404.3×10³ m * const m = p1.distanceTo(p2, 3959); // 251.2 miles */ distanceTo(point, radius=6371e3) { if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms if (isNaN(radius)) throw new TypeError(`invalid radius ‘${radius}’`); // a = sin²(Δφ/2) + cos(φ1)⋅cos(φ2)⋅sin²(Δλ/2) // δ = 2·atan2(√(a), √(1−a)) // see mathforum.org/library/drmath/view/51879.html for derivation const R = radius; const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians(); const φ2 = point.lat.toRadians(), λ2 = point.lon.toRadians(); const Δφ = φ2 - φ1; const Δλ = λ2 - λ1; const a = Math.sin(Δφ/2)*Math.sin(Δφ/2) + Math.cos(φ1)*Math.cos(φ2) * Math.sin(Δλ/2)*Math.sin(Δλ/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); const d = R * c; return d; } /** * Returns the initial bearing from ‘this’ point to destination point. * * @param {LatLon} point - Latitude/longitude of destination point. * @returns {number} Initial bearing in degrees from north (0°..360°). * * @example * const p1 = new LatLon(52.205, 0.119); * const p2 = new LatLon(48.857, 2.351); * const b1 = p1.initialBearingTo(p2); // 156.2° */ initialBearingTo(point) { if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms if (this.equals(point)) return NaN; // coincident points // tanθ = sinΔλ⋅cosφ2 / cosφ1⋅sinφ2 − sinφ1⋅cosφ2⋅cosΔλ // see mathforum.org/library/drmath/view/55417.html for derivation const φ1 = this.lat.toRadians(); const φ2 = point.lat.toRadians(); const Δλ = (point.lon - this.lon).toRadians(); const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ); const y = Math.sin(Δλ) * Math.cos(φ2); return Math.atan2(y, x); } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * Checks if another point is equal to ‘this’ point. * * @param {LatLon} point - Point to be compared against this point. * @returns {bool} True if points have identical latitude and longitude values. * * @example * const p1 = new LatLon(52.205, 0.119); * const p2 = new LatLon(52.205, 0.119); * const equal = p1.equals(p2); // true */ equals(point) { if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms if (Math.abs(this.lat - point.lat) > Number.EPSILON) return false; if (Math.abs(this.lon - point.lon) > Number.EPSILON) return false; return true; } } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
hex is a C program I wrote to walk through a FIT file, printing out the definitions, records, and coursepoints. It’s not very nicely formatted, but provides the basis of the fit-reading code I wrote for routemaster.
hex.c utils: memory.h
The call is:
hex filename
and the compile line is
g++ -o hex -g -O hex.c
• checksum • defty • reset • itemty • diddy • tetri • main
#include "memory.h" #include <math.h> int checksum(unsigned char *x,int n) { static int crc_table[16] = { 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400 } ; int i,crc,tmp ; for(crc=i=0;i<n;i++) { // compute checksum of lower four bits of byte tmp = crc_table[crc & 0xF] ; crc = (crc >> 4) & 0x0FFF ; crc = crc ^ tmp ^ crc_table[x[i] & 0xF] ; // now compute checksum of upper four bits of byte tmp = crc_table[crc & 0xF] ; crc = (crc >> 4) & 0x0FFF ; crc = crc ^ tmp ^ crc_table[(x[i] >> 4) & 0xF] ; } return crc ; } struct defty { unsigned char size,gmsgnum,nitem,maxitem ; defty() { size = nitem = maxitem = 0 ; gmsgnum = 255 ; } void reset(int i,int j) { size = 0 ; gmsgnum = i ; maxitem = max(nitem,j) ; nitem = j ; } } ; struct itemty { unsigned char meaning,size,format,bigend ; itemty() { meaning = size = format = bigend = 0 ; } itemty(int i,int j,int k,int l) { meaning = i ; size = j ; format = k ; bigend = l ; } } ; int diddy(unsigned char *x,unsigned char bigend) { if(bigend) return (x[0]<<8) | x[1] ; else return x[0] | (x[1]<<8) ; } int tetri(unsigned char *x,unsigned char bigend) { if(bigend) return (diddy(x,1)<<16) | diddy(x+2,1) ; else return diddy(x,0) | (diddy(x+2,0)<<16) ; } genvector(unsigned char,ucharvector) ; genvector(itemty,itemtyvector) ; /* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */ int main(int argc,char **argv) { int i,k,n,itemno,val,meaning,hdrlen,datalen ; unsigned char flag,nitem,gmsgnum,lmsgnum,ilen,compass,bigend ; double qval ; defty def[16] ; itemty *item[16],it ; char *type, *types[] = {"Generic","Summit","Valley","Water","Food","Danger", "Left","Right","Straight","First Aid",0,0,0,0,0,0, "Bear left","Bear right",0, "Bear left","Sharp Left","Bear right","Sharp right"} ; unsigned char itemlen[32] = { 1,1,1,2,2,4,4,0 , 4,8,1,2,4,0,8,8, 8,3,3,3,3,3,3,3 , 3,3,3,3,3,3,3,3 } ; for(i=0;i<16;i++) { item[i] = 0 ; def[i] = defty() ; } FILE *ifl = fopenread(argv[1]) ; unsigned char *buf = ucharvector(14) ; n = fread(buf,1,14,ifl) ; if(n<14) throw up("%s is only %d bytes long",argv[1],n) ; hdrlen = buf[0] ; datalen = tetri(buf+4,0) ; printf("header begins:") ; for(i=0;i<12;i++) printf(" %x",buf[i]) ; printf("\n") ; if(hdrlen==14&&(buf[12]||buf[13])) printf("checksum: stored=%04x, computed=%04x\n", buf[12]+(buf[13]<<8),checksum(buf,12)) ; buf = ucharvector(buf,hdrlen+datalen) ; n += fread(buf+hdrlen,1,datalen+2,ifl) ; for(i=hdrlen;i<hdrlen+datalen;) { flag = buf[i] ; lmsgnum = 15 & flag ; printf("*** flag=%02x, lmsgnum=%d ***\n",flag,lmsgnum) ; if(flag&0x40) { if(i+6>hdrlen+datalen) throw up("definition extends beyond end of file") ; bigend = buf[i+2] ; gmsgnum = diddy(buf+i+3,bigend) ; nitem = buf[i+5] ; printf("[byte %d]: defn: ",i) ; for(k=i;k<i+6;k++) printf("%02x ",buf[k]) ; printf("| hdr:res:big:gma:gmb:nitem\n") ; for(itemno=0;itemno<nitem;itemno++,k+=3) printf(" %02x %02x %02x\n",buf[k],buf[k+1],buf[k+2]) ; printf("[lmsgnum=%d,gmsgnum=%d,nitem=%d]: ",lmsgnum,gmsgnum,nitem) ; if(i+6+3*nitem>hdrlen+datalen) throw up("definition extends beyond end of file") ; if(nitem>def[lmsgnum].maxitem) item[lmsgnum] = itemtyvector(item[lmsgnum],nitem) ; def[lmsgnum].reset(gmsgnum,nitem) ; for(itemno=0;itemno<nitem;itemno++) { k = 6 + i + 3*itemno ; def[lmsgnum].size += buf[k+1] ; item[lmsgnum][itemno] = itemty(buf[k],buf[k+1],buf[k+2],bigend) ; printf("(meaning=%d,size=%d,format=%x) ",buf[k],buf[k+1],buf[k+2]) ; } printf(": len=%d\n",def[lmsgnum].size) ; i += 6 + 3*nitem ; } else if(flag&0x80) throw up("compressed timestamp") ; else { gmsgnum = def[lmsgnum].gmsgnum ; if(gmsgnum==255) { printf("[byte %d]: <lmsgnum=%d,len=%d, gmsgnum=?>\n", i,lmsgnum,def[lmsgnum].size) ; throw up("no defn for lmsgnum %d",lmsgnum) ; } printf("[byte %d]: <lmsgnum=%d,len=%d, gmsgnum=%d>\n", i,lmsgnum,def[lmsgnum].size,gmsgnum) ; if(gmsgnum==0||gmsgnum==20||gmsgnum==21||gmsgnum==32||gmsgnum==31) for(k=i+1,itemno=0;itemno<def[lmsgnum].nitem;itemno++,k+=it.size) { it = item[lmsgnum][itemno] ; ilen = itemlen[it.format&31] ; if(gmsgnum==31) // course { if(it.meaning==5) { printf("> coursename = \"") ; for(val=0;val<it.size&&buf[k+val];val++) printf("%c",buf[k+val]) ; printf("\"\n") ; } continue ; } meaning = -1 ; if(gmsgnum==20) { if(it.meaning<3||it.meaning==253) meaning = it.meaning ; } else if(it.meaning==2||it.meaning==3) meaning = it.meaning - 2 ; else if(it.meaning==1) meaning = 253 ; else if(it.meaning==5||it.meaning==6) meaning = it.meaning ; if(meaning==6) { for(val=0;val<it.size&&buf[k+val];val++) printf("%c",buf[k+val]) ; printf(" ") ; } else if(ilen==1||ilen==2||ilen==4) { if(ilen==1) val = buf[k] ; else if(ilen==2) val = diddy(buf+k,it.bigend) ; else val = tetri(buf+k,it.bigend) ; if(gmsgnum==0||gmsgnum==21) printf("(meaning %d,size %d,val %x) ",it.meaning,ilen,val) ; else if(meaning==0||meaning==1) { qval = val / ((1<<30)/90.0) ; if(meaning==0) { if(qval>=0) compass = 'n' ; else compass = 's' ; } else { if(qval>=0) compass = 'e' ; else compass = 'w' ; } printf("%.6f%c ",fabs(qval),compass) ; } else if(meaning==2) printf("%dm ",(val/5-500)) ; else if(meaning==5) { if(val>=0&&val<23) type = types[val] ; else if(val==46) type = "Obstacle" ; else if(val==53) type = "Info" ; else type = 0 ; if(type==0) type = types[0] ; printf("*%s ",type) ; } else if(meaning==253) printf("%ds ",val) ; else printf("(%d,%d) ",it.meaning,val) ; } else printf("[%d] ",ilen) ; } if(gmsgnum==0||gmsgnum==20||gmsgnum==21||gmsgnum==32) printf("\n") ; i += 1 + def[lmsgnum].size ; } } if(n==hdrlen+datalen+2) printf("checksum: stored=%04x, computed=%04x\n", buf[hdrlen+datalen]+(buf[hdrlen+datalen+1]<<8), checksum(buf+hdrlen,datalen)) ; }
The main page (index.php) adds a further 353 lines, and the file server another 292. pixlib.js adds almost 2000 more.