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, or may be Google Maps direction pages. 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 following hints cover the simplest tasks.
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: routemasher 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 Google 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 turn information 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 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.
Bug: the Google Maps API does not return the shiftKey field of mouse clicks; therefore I have to keep track of keydowns and keyups of the shift key. This can go wrong in various ways; eg. if you hit [shift] in the process of moving to a new tab. If routemaster is treating normal clicks as shift clicks, just depress and release the shift key. Sorry.
If you want to add points by interpolation, see the next section.
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 it 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. 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, most 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. | ||
| Left | ✓ | – | ||
| Slight left | ✗ | Eg. 45° turn. | ||
| Straight | ✓ | I.e. straight on | ||
| Slight right | ✗ | Eg. 45° turn. | ||
| Right | ✓ | – | ||
| Sharp right | ✗ | Eg. 135° turn. | ||
| 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. | ||
| First Aid | ✓ | – | ||
| 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 Google 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 the Google Elevations Service.
Finally, if your altitudes have been measured accurately but are subject to calibration error, you can correct them using Google 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 Google altitudes.
If your altitudes have been measured barometrically, some form of Google 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 Google 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; hit [enlarge] to view them at full size. 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.
If you include spaces or other punctuation not suitable for URLs
Set up Google Maps to show directions from one place to another. You will get a page like this. Copy the URL from the URL bar and enter it into the URL load in Routemaster – either from the start page or by requesting ‘Load new route’ under the cogwheel menu.
The implementation isn’t as straightforward as you might hope. routemaster can’t access the Google Maps page; it has to break out the parameters of the request from the URL, and then send a similar request to the Google Directions service, whose software is (or was when I tested) separate from Google Maps, sometimes giving a different result.
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 routemasher, 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: .fit preserves the greatest amount of information; .tcx comes next (but is probably not accepted by any device which doesn’t accept .fit); and .gpx is the worst.
The main difference is in handling of turn instructions. .gpx doesn’t provide anything satisfactory at all; .fit supports a larger (and better) set than .tcx.
.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 you can do with one, but you lose the ability to associate them with icons. .gpx also allows a whole panoply of ‘symbols’, colours, and heaven knows what, but since they’re just character strings with no standard interpretation (that I know of) all you can do is echo them without using them.
Both .gpx and .tcx are hopelessly bloated: in .tcx I have to write
<Position>
<LatitudeDegrees>51.88266</LatitudeDegrees>
<LongitudeDegrees>-2.08383</LongitudeDegrees>
</Position>
when
lat="51.88266" long="-2.08383"
is all that should be needed.
.fit is compact and stores course points effectively, allowing a larger set of types than .tcx. However it is binary rather than text, opaque, almost boundless in extent, and badly documented. It should not be understood as a route format, but as a format for fusing inputs from training sensors amongst which GPS readings have no privileged status.
.rte is an 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. Therefore positioning the turn instructions does not pose any problems to the importing software.
It is also 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.
There is a completely unintelligible terminology distinguishing ‘waypoints’, ‘coursepoints’, ‘trackpoints’ etc., perhaps according to whether or not they have turn instructions, or whether or not they lie on the sequence comprising a route. I have no idea what people are talking about half the time. Unfortunately I don’t know any clear way to say that a point has a ‘turn instruction’, given that the possible values include such things as ‘first aid’ (which are not turn instructions at all). Sometimes I talk more vaguely of a ‘label’.
For my part, I’m settling on my own terminology. A waypoint is a point along a track; a track is a complete ordered sequence of waypoints. If a track is broken into parts, these parts are called segments (which are nothing to do with Strava segments).
A point which is not part of the sequence comprising a track is called a geo point (or geographical annotation). A point which has a turn instruction (or similar) is called a waymark (without ceasing to be a waypoint or geo point). There is a distinction between deleting the turn instruction (which can be done through the ‘waymark’ menu invoked from the fountain pen button) and deleting the point (using the delete or the backspace key). Adding a photo to a waypoint or waymark doesn’t turn it into anything else.
It seems that GPX “waypoints” correspond to my waymarks, and this may confuse the reader. My waypoints are GPX “trackpoints”. Since there is no difference between a way and a track, but a big difference between a point and a mark, my usage is clearly preferable. According to wikipedia “In GPS navigation, a ‘route’ is usually defined as a series of two or more waypoints”. My waypoints appear to be FIT records, and my waymarked points to be FIT coursepoints.
My understanding is that GPX “waypoints” provide labels for positions in space rather than positions along a track; “tea shop” makes sense but “turn left” doesn’t, since its correctness depends on the route you are taking through the point. And I think that FIT coursepoints, on the contrary, are meant to be understood as relating to positions along a track, but are stored as points in space. Importing software is meant to assign them to positions along the track according to their time fields, which are therefore being used for an extraneous purpose. FIT coursepoints cannot have altitudes, which creates another difficulty which importing software has to deal with.
I cannot be sure of any of this; the documentation for GPX, in particular, is very inexplicit. But you can understand why I am not keen on either format, and have defined my own.
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.
There’s a neat trick for finding locations in OSM or Google Maps. Click on the ‘Waypoint info’ button; the top line has lat and long. Select the entire string and do Edit>Copy; then paste into the search box of your online map.
Post-prandial notches are described below. 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 Google elevation service.
Firstly use the scissors to define a segment embracing the notch. You can perform fine adjustments by invoking Waypoint info on the first or last waypoint of a segment. When 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 Google 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 Google’s) and then create a route comprising a single draggable point at the specified location. The only difficulty lies in the Google interface.
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 (and
therefore cannot link to a Google Maps direction page as a Routemaster track)
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 I cannot
guarantee continued provision of a free service if they become appreciable
(which may happen if Google start charging for use of their maps API). The
method of showing data from one website in an application on
another requires a little fancy footwork and may break down in some cases.
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.
Photos to be displayed against GPS tracks need to be stored as
pix.js photo galleries. Creating such galleries requires a
certain amount of work (and of computer savviness) but is easier than it used
to be since I wrote the pixrescale software
(Aug 2024).
So the first step is to create a suitable photo gallery. An extra
detail should be attended to for images to be shown against GPS tracks. They
will be displayed at first as icons (if you click on the icon you see the
thumbnail; if you click on the thumbnail you see a full-size image).
routemaster will generate the icon from the thumbnail if no specific
icon is provided, but for bandwidth efficiency it’s best to provide icons
generated specially for the purpose. These should be 52×52 jpgs; their
suffix (usually ‘@i’) should be supplied through an entry in the ‘sizes’
field of the photolist, with ‘icon’ as the type and ‘52’ as the scale factor.
Eg.
Suitable icons are produced by the pixrescale
scripts.
Once you have a suitable gallery, upload it to the web and
you’ll be able to admire your own handiwork by visiting the corresponding
pix.html page.
The next step is to load a track in the normal way. When you want to
add a photo at a particular position, select the appropriate waypoint by
clicking near it and using the arrow keys for fine adjustment, then click on
the camera button. You will be invited to load the photo list, which can be
done from the URL or from your own computer (but the first is better).
Once the list has been loaded into Routemaster
you will be prompted to enter the name of an image from the list. The
corresponding photo will then be shown at the chosen waypoint. From this point
on you can edit photos in much the same way as you can edit course points.
Now download the track as .rte. It will have extra tags
for the photos youve added and also the URL of the photo
list you specified. (If you loaded the photo list from your own disc, you will
need to hand-edit the URL in the GPS track to point to the web version.)
In case of difficulty, in Firefox, open the web console
(Tools > Web Developer > Web console): you may see some
informative messages. In Chrome it’s View > Developer > Developer tools. Presumably other browsers offer a similar feature.
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, as is suitable for indexes (in
which you don’t want to keep as many points). You can bypass this optimisation
by shift-clicking on the pseudo-link Save track as route index. I do this for an index such as our Sperane one which is the same
size as a normal route rather than a normal index, and is itself incorporated
within an index (in this case our East Garda one).
Routes within a tour are displayed much like routes within an index; the
difference is that routes within a tour have an intrinsic order.
To create a tour, load the first route and save it as an index. Then,
using a text editor, change the line near the top from
to
Then load the modified file into routemaster and add the remaining
routes.
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
• altitude smoothing
• actions list
• TCX and GPX
• Courses and tracks
• mapping
• photos
• altitudes
• downloading
• colouring tracks
• listing hues
• assigning hues
• known problems
• acknowledgments
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). There are no cookies unless you’re logged in
but Google knows where youve been because of the mapping requests.
A track is optimised on input; that is, it is reduced to the smallest
number of trackpoints to preserve its accuracy. This is done using
dynamic programming.
Bike route toaster uses the
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.
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
1000m3. This is quite small: it equates to an error of about
4m over a distance of 20m.
Vertical errors are given the same weight as horizontal errors, but although
this is natural it has no real justification. An additional parameter
(vweight) can be used to adjust the significance of altidude. It
would make sense to be particularly averse to losing peaks and troughs since
this affects the calculation of total ascent; but I havent done
anything to this effect.
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 is needed because
Garmin 500s lose their breadcrumb display if a separation is >100m
(as was reported on the Garmin support forum when this existed). But I
print positions with precision around 1m, so I set maxsep somewhat
less than 100m to avoid it becoming greater due to rounding error.
Once a track has been optimised I try to avoid optimising it a second time,
especially since even if the parameters are the same the
results will differ: there will be an accumulation of errors. For this
reason I record the fact that optimisation has been performed in the exported
track. Also, if optimisation reduces the number of points by less than a
factor of 2, I assume that the track has been preoptimised.
A side-effect of optimisation which took me by surprise if that if
while riding along you lose confidence in the route you are following,
backtrack a few tens of metres, and then realise you were right all along and
resume your previous course, then the optimisation removes all record of your
changes of mind.
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.
The optimisation selects a smaller set of points to retain. I used formerly
to discard all the intermediate points, but now I try to improve the accuracy of
altitudes by smoothing them. If three successive points A, B and C are being
retained, then the altitude at B is reestimated by fitting a curve to all
altitudes after A and before C. The fit is weighted, with the weights for the
points governed by a Gaussian curve with standard deviation around 14m. A
linear or quadratic fit is selected according to an ad hoc criterion.
This operation is messier than might appear because I have to keep
track of the altitudes before reestimation so as to be able to revert to them
if the user hits the ‘undo’ button.
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 never 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.
Creating a coursepoint label may require a sequence of user
operations which are collapsed into a single action.
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.
I use Google Maps v3 API. I dont have any plans to extend this to other
map sources. However I like openstreetmap and would adopt it if it was easier
to do so.
The buttons at the bottom use the Google map.controls
feature. Mostly the buttons dont do any more than bring up menus
which hang off them. These menus are google.maps.InfoWindows whose
position needs to be specified as lat/long rather than relative to the
controls. The calculation which converts one to the other makes me
dependent on unspecified details of the map controls interface and leads to
unexpected behaviour when windows are resized.
At the time I didnt know enough HTML to generate the
buttons myself. Now I think I do, but it would still be more work than
taking advantage of the Google functions.
Points may not have 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.
I am not satisfied with my algorithm for shades at present: I often get too pale
a shade of pink. But I can give the matter attention at a future date.
A recurring difficulty is that I can never understand my
own code, which is largely because source-code comments are useless unless
accompanied by a diagram; so, for my own benefit, here is a graphical
description of my algorithm.
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.
There are 2 areas in which Google’s behaviour does not match
what routemaster needs.
The first concerns response to requests to open an info window. If
the request comes before the Maps API has completed its background tasks, it
has no effect. An example is if you click on an iconised image indicating a
photo at a waypoint. This brings up a thumbnail. If you now click on a
similar icon at another waypoint, the first info window is closed and a request
is sent to open another one, but the request always fails. This behaviour is
undocumented; there is no return code; I have no idea how to test for whether
background tasks have been completed or how to wait for them to complete. So
you just have to keep on clicking until you get what you want.
The second problem concerns URLs to Google Maps directions pages. I
try to get the same result from the Google directions API as has been given by
the Maps page, but the services are different and the results may not be
consistent. There are different algorithms for parsing lat/long values and for
disambiguating place names, and the via qualifier doesn’t work
(this stackoverflow question refers). So the
result may not be what it should be.
There are other tools for converting Maps directions to tracks
(e.g. mapstogpx), but I haven’t determined
whether they are more successful. Some of them use server processing which is
less visible than the Javascript sent to the browser.
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
• future directions
• testing
• licence
I attempted a fix of the shift-key problem.
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.)
In 2016 Google removed the ability for new pages to use the Maps API without
a key. I suspected at the time that it was the first step towards monetising the
maps API (which at present is effectively supplied as a free public service).
Google have reserved the right to put ads all over the maps they supply, which may be their first recourse.
But a year later nothing much has changed. I suspect that Google
were losing the ability to track users of Google maps through the increasing
popularity of ad blockers. Requiring a key allows them to analyse usage by
application. Maybe theyll take pecuniary advantage of this in the end.
[June 2019.] Wikiloc and
mygpsfiles.com have moved away from
the Google Maps API because of the prohibitive charges. I’m not being hit myself
because I don’t have enough (indeed any) users (besides myself). But I’d be
reluctant to move. Other services may stay free only for as long as it takes to
establish a base of dependent users. Those which are open source as a matter of
policy will presumably stay that way, but providing, updating and storing maps
and satellite images costs money and there’s no reason why the providers
shouldn’t charge accordingly. The open source alternatives, if they continue to
exist, may be obviously second class.
This is not to excuse Google’s prices. I suspect they overcharge
bulk customers to cross-subsidise small-scale users such
as myself. But this makes no sense. They’re a bit boxed into a corner by the
failure of microcharging to establish itself.
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
• routemaster.en.js
• routemaster.fr.js
• infocloser
• function
• resizer
• dotpath
• linepath
• arfunc
• greyout
• blackout
• bulkout
• redrawbtn
• enterFullscreen
• exitFullscreen
• findimg
• unsavedmsg
• selpoint
• distrec
• segdist
• highlight
• unhighlight
• eventually
• getbtnpos
• walkto
• keystroke
• pointstep
• shiftkey
• undraw
• redraw
• drawprofile
• predrawpoint
• drawpoint
• draw
• drawfull
• geodraw
• disconnect
• connect
• redrawconnect
• lconnect
• ldisconnect
• drawsel
• checklostedits
• genpage
• getlist
• render
• genbutton
• acfactory
• addload
• refresh
• renfactory
• wpinfo
• rellist
• dl
• cofactory1
• cofactory
• canceldl
• indexdl
• confirmeddl
• semiconfirmeddl
• reconfirmeddl
• optimaccept
• optimparms
• pen2detail
• optimmerge
• optimwork
• dgen
• posit
• vender
• unshadow
• accept
• redelta
• retitle
• respond
• restars
• routeinfo
• eyedisplay
• calwork
• manualcal
• googlecalwork
• googlecal
• googlereg
• googleadd
• help
• sidestep
• wpdelwork
• wpdel
• revsegwork
• revseg
• dupsegwork
• dupseg
• insert
• inswp
• insgeo
• draggit
• undraggit
• seginfo
• altinfo
• deltimes
• interpol
• extrapts
• combine1
• combinework
• combiner
• combine
• combinef
• combineb
• uncombine
• setalt
• delalts
• labelprompt
• editlabel
• unlabel
• photoprompt
• photomerge
• photoedit
• 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
• darken
• hexify
• hexdigit
• listhues
• extendhues
• genshades
• snipcolour
• shadeofhue
• segbounds
• getprox
• ascify
• getalts
• doelevations
• getbounds
• promoteprops
• loadtrack
• callindexify
• genarrows
• parsedesc
• parsehtml
• loaderfactory
• trackref
• pluralise
• caps
• genmarker
• imgfactory
• sqr
• phototitle
• interp
• getllpt
• readgoogle
• extendphoto
• err
• gatherpix
• thumbpix
• writeindex
• indexify
• genpixpage
• absuri
• textdiv
• highdiv
• delfactory
• updfactory
• genpicimg
• serve
• scrolltype
• scroller
• btnicon
• newcanvas
• buttons
• cloneCanvas
• buttoncell
• textcell
• appendrow
• genlink
• genindexlink
• updateacbtns
• logout
• acmenu
• logoutfactory
• doprefs
• bugreport
• acfollowon
• greyit
• emailvalidate
• pwdvalidate
• otkvalidate
• loggedin
• phpresponse
• formresponse
• blurbdiv
• rmhelpdiv
• addbull
• genpixlink
• cogwheelmenu
• addloadfactory
• dlfactory
• arfactory
• doalts
• deltimesfactory
• swapsegfactory
• seginfodiv
• optimfactory
• altinfodiv
• doclink
• walktodiv
• displayfactory
• pheditfactory
• phinfofactory
• eyemenu
• highfactory
• addcell
• listroutes
• wpinfodiv
• altfactory
• wpfactory
• kmspan
• titlediv
• titlefactory
• textpromptcleanup
• speaklang
• promptbtn
• textprompt
• submitfactory
• cancelfactory
• prefsprompt
• flagcanvas
• cclick
• pclickfactory
• defaultfactory
• labelmenu
• drawicon
• drawlabel
• genrect
• iconsel
• iconfactory
• submitfn
• genldiv
• tdivadd
• intise
• starsline
• createfunc
• starfactory
• drawxcur
• toggleprofile
• point2LatLng
• telltale
• wipetale
• zonktale
• getbus
• domadd
• domcreate
• prettynum
• shortenname
• abbreviate
• underline
• clone
• isvaliddate
• dist
• angle
• midpt
• normalise
• xmlfloat
• dehexify
• numerate
• pttype
• routetype
• function
• addlabel
• getlatlong
• deltify
• recurse
• flipseg
• getstats
• getmetastats
• diststring
• kmstring
• prettystats
• routestats
• flatten
• squash
• genclickfn
• genspan
• gentimes
• filedialogue
• unspace
• parsestats
• getprops
• gettags
• getpt
• 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
• drawpro
• choosearrows
• geotag
• getexifgps
• readexiftag
• getexifstring
• 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
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.
The call is:
and the compile line is
• checksum
• defty
• reset
• itemty
• diddy
• tetri
• main
The main page (index.php) adds a further 353 lines, and
the file server another 292. pixlib.js adds almost
2000 more.
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
<size suffix="@i" scale="52" type="icon"/>
<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>
<route type="index">
<route type="tour">
https://www.routemaster.app/?track=https://www.masterlyinactivity.com/routemaster/routes/montetondo.tcx
var segments=[],selected=null,actions=[],nactions=0,dragging=0 ;
var selmarker=null,shifted=null,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() ;
function infocloser(obj,opt)
{ var otype = obj.type ;
if(opt) obj.handle.close() ;
obj.handle = obj.type = null ;
if(otype=='highlight') unhighlight() ;
else if(otype=='phinfo') walkto(selected.segno,selected.ptno,selected.type) ;
else if(otype=='getlist'&&opt==0&&imginfo.carriedover) // exists but may be
{ imginfo.carriedover = 0 ; photoprompt() ; } // obsolete
return otype ;
}
var infowindow =
{ handle: null ,
type: null ,
closer: null ,
divvy: null ,
open: function(s,pos,type)
{ this.type = type ;
if(type=='login')
{ s.setAttribute('style','position:absolute;top:20px;right:2px;'+
'background:white;padding:4px') ;
domadd(mapdiv,s) ;
divvy = s ;
}
else if(map)
{ this.handle = new google.maps.InfoWindow({content:s,position:pos}) ;
this.handle.open(map) ;
// vv this is invoked by clicking on the 'x'
google.maps.event.addListener(this.handle,'closeclick',
function() { infocloser(infowindow,0) ; } ) ;
}
} ,
close: function() // this is invoked externally (by infowindow.close())
{ var c = this.closer ;
if(c) { this.closer = null ; c() ; return null ; }
else if(this.type=='login')
{ mapdiv.removeChild(divvy) ; divvy = null ; this.type = null ; }
else if(this.handle==null) return null ;
else return infocloser(this,1) ;
}
} ;
/* -------------------------------------------------------------------------- */
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 && segments[0].level==0 )
{ 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 dotpath(a,b)
{ this.path = [a,b] ;
this.cursor = 'default' ;
this.geodesic = true ;
this.strokeOpacity = 0 ;
this.icons = [ { icon: { path: 'M 0 0 L 1 0',strokeOpacity:1,scale:1 } ,
offset: '1px' ,
repeat: '4px'
} ] ;
this.zIndex = 0 ;
}
/* -------------------------------------------------------------------------- */
function linepath(seg,start,end,colour,width,zindex)
{ 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.clickable = (segments[0].level>0) ;
this.geodesic = true ;
this.strokeColor = colour ;
this.strokeOpacity = 1.0 ;
this.strokeWeight = width ;
if(zindex) this.zIndex = zindex ; else this.zIndex = 0 ;
if(showarrows) this.icons = seg.arrows ;
}
function arfunc(tracks,opt)
{ infowindow.close() ;
var i ;
for(i=0;i<tracks.length;i++)
tracks[i].line.setOptions({icons:opt?tracks[i].arrows:null}) ;
showarrows = 1 - showarrows ;
}
/* -------------------------------------------------------------------------- */
/* UTILITY FUNCTIONS */
/* --------------------------- button handlers ----------------------------- */
function greyout(btn) { redrawbtn(btn,1) ; }
function blackout(btn) { redrawbtn(btn,0) ; }
function bulkout(black)
{ var f ;
if(black) f = blackout ; else f = greyout ;
f(segbtn) ;
f(wpbtn) ;
if(black==0||(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) ;
if(acbtn) f(acbtn) ;
}
function redrawbtn(btn,opt) // 0<->black 1<->grey -1<->redraw as is
{ if((segments[0].level&&(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(L.unsavedchanges,len) ;
if(len<=3) for(i=0;i<len;i++)
msg += (i?',':' (') + unsavedchanges[i] + (i==len-1?')':'') ;
msg += '\n' + L.ifyouhit + ' [' + (ok?L.ok:L.leavepage) + '] ' ;
return msg + pluralise(L.willbelost,len)
}
/* --------------- selpoint: choose the clicked waypoint ------------------- */
function selpoint(event)
{ var i,j,r,scale,flag=0,pos,mindist,pts ;
var s0 = selected.segno , s1 = selected.ptno , s2 = selected.type ;
// 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(segments[0].level==0&&!flag&&shifted) // insert waypoint forwards in seg
{ if(s2) pts = segments[s0].geo ; else pts = segments[s0].pts ;
s1 = pts.length ;
if(s2)
{ pts.push(new pttype()) ;
pts[s1].setpos(pos) ;
geodraw(pts[s1],segments[s0].colour) ;
}
else
{ insert(pts,s1,1) ;
pts[s1].setpos(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(segments[0].level>0)
{ 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.
scale = selpoint.clickscale / Math.pow(2,map.getZoom()) ; // metres/pixel
if(segments[0].level==0&&flag>1) flag = 1 ;
if(mindist<50*scale) 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,shifted,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,shifted) ;
highlight.scroller = scroll.scroller ;
recurse(tracks[s0],'north',norpos) ;
infowindow.open(scroll.div,norpos.pos,'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 ; }
if(selmarker) selmarker.setMap(null) ;
infowindow.handle = selmarker = null ;
selected.segno = -1 ;
}
function eventually(f)
{ if(map&&map.getBounds()) f() ;
else google.maps.event.addListenerOnce(map,'tilesloaded',f) ;
}
/* ------------------------------- getbtnpos -------------------------------- */
function getbtnpos(btnno)
{ var bounds=map.getBounds(),sw,ne,lat,lon,λ,h,e ;
if(segments[0].level==0) h = 144 ; else { h = 48 ; if(btnno>5) btnno -= 5 ; }
sw = bounds.getSouthWest() ;
ne = bounds.getNorthEast() ;
e = ne.lng() ;
if(e<sw.lng()) e += 360 ;
λ = 64.0 / window.innerHeight ;
lat = λ*ne.lat() + (1-λ)*sw.lat() ;
λ = 0.5 + (btnno*32-h)/window.innerWidth ;
lon = λ*e + (1-λ)*sw.lng() ;
return { lat:lat , lng:lon>180?lon-360:lon } ;
}
/* --------------------------------- 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(segments[0].level) return highlight(segments[0].pts,shifted) ;
var pt = s2?segments[s0].geo[s1]:segments[s0].pts[s1] , pos = pt.pos ;
if(!flag) flag = 0 ;
map.panToBounds(new google.maps.LatLngBounds(pos,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,shifted) ;
return ;
}
infowindow.open(walktodiv(pt),pos,'walking') ;
}
/* -------------------------- keystroke handler ---------------------------- */
function keystroke(e)
{ if(!selected||infowindow.type=='addload'||infowindow.type=='getlist') return ;
if(infowindow.type=='account') 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&&segments[0].level==0) // 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(segments[0].level==0&&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(segments[0].level) { if(e.keyCode==70) enterfullscreen() ; return ; }
if(dragging) return ;
if(e.keyCode==8||e.keyCode==46) // delete/backspace
{ e.preventDefault() ;
if(!e.shiftKey) { if(segments[0].level==0) wpdel() ; }
else if(segments[0].level==0) 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(segments[0].level)
{ 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) // google maps api has no documented shiftKey field
{ shifted = val ;
if(map) map.setOptions
({draggableCursor:(val!=0&&segments[0].level==0)?'crosshair':'default'}) ;
if(val==0&&segments[0].level==0) getalts(segments,200,drawprofile) ;
}
/* --------------------- undraw & redraw segments -------------------------- */
// needs a comment explaining when to obliterate and when to undraw
function undraw(segment)
{ segment.line.setMap(null) ;
for(var i=0;i<segment.geo.length;i++) segment.geo[i].point.map = null ;
if(segment.clickhandler!=null)
{ google.maps.event.removeListener(segment.clickhandler) ;
segment.clickhandler = null ;
}
}
function redraw(i) { undraw(segments[i]) ; drawfull(segments[i]) ; }
function drawprofile()
{ var divstyle = drawpro(drawpro.pro,segments) ;
toggleprofile.pcopy = drawpro.pro ;
toggleprofile.scopy = segments ;
// cursor
if(divstyle) drawpro.pro.curdiv.setAttribute('style',divstyle) ;
drawxcur(drawpro.pro,selected) ;
}
/* ----------------------------- draw a point ------------------------------- */
function predrawpoint(colour)
{ var w = 8 , r = window.devicePixelRatio ;
var c = domcreate('canvas',null,'style','width:'+w+'px;height:'+w+'px') ;
var ctx = c.getContext('2d') ;
c.width = w * r ;
c.height = w * r ;
ctx.scale(r,r) ;
ctx.lineWidth = 2 ;
ctx.strokeStyle = colour ;
ctx.beginPath() ;
ctx.arc(w/2,w/2,w/3,0,2*Math.PI) ;
ctx.stroke() ;
var d = domcreate('div',c) ;
d.style.transform = "translate(0px," + Math.floor(w/2) + "px)" ;
return d ;
}
function drawpoint(pos,colour)
{ return new google.maps.marker.AdvancedMarkerElement
({ map , content:predrawpoint(colour) , position:pos }) ;
}
/* ----------------------------- draw segments ------------------------------ */
function draw(segment,width) // should use drawfull in place of draw
{ if(!width) width = 2 ;
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 = new google.maps.Polyline(poly) ;
}
segment.line.setMap(map) ;
if(!segment.clickhandler) segment.clickhandler =
google.maps.event.addListener(segment.line,"click",selpoint) ;
}
function drawfull(segment,width)
{ draw(segment,width) ;
if(segment.level==0) for(var i=0;i<segment.geo.length;i++)
geodraw(segment.geo[i],segment.colour) ;
}
function geodraw(pt,col)
{ pt.point = drawpoint(pt.pos,col) ;
pt.setmap(map,selpoint) ;
if(!pt.clickhandler)
pt.clickhandler = google.maps.event.addListener(pt.point,"click",selpoint) ;
}
/* ----------------------- connect and disconnect segments ------------------ */
function disconnect(segno)
{ var seg = (segments[0].type=='tour'?segments[0].pts[segno]:segments[segno]) ;
if((segments[0].level&&segments[0].type!='tour')||seg.dots==null) return ;
seg.dots.setMap(null) ;
if(seg.dothandler)
{ google.maps.event.removeListener(seg.dothandler) ; seg.dothandler = null ; }
}
function connect(i)
{ var j,seg ;
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 ;
seg[i].dots = new google.maps.Polyline(new dotpath(opos,npos)) ;
seg[i].dots.setMap(map) ;
seg[i].dothandler =
google.maps.event.addListener(seg[i].dots,"click",selpoint) ;
}
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 -------------------------- */
// note: there's no point in allowing clicking on a marker because the
// event position is always the marker position rather than the click position
function drawsel(pts,ptno,advance) // args supplied only for animated arrow
{ if(!pts&&selected.type&&selected.type=='geo')
{ pos = segments[selected.segno].geo[selected.ptno].pos ;
if(!selmarker) selmarker = new google.maps.Marker
({ position:pos, map:map, cursor:'default', icon:icons.geo , zIndex:2 }) ;
else { selmarker.setIcon(icons.geo) ; selmarker.setPosition(pos) ; }
greyout(scissorsbtn) ;
return ;
}
if(!pts) { pts = segments[selected.segno].pts ; ptno = selected.ptno ; }
var ind,clen=pts.length,pos=pts[ptno].pos,pos2,λ,a,b ;
if(clen==1)
{ if(segments.length==1) icons.arrow.rotation = 90 ;
else
{ a = selected.segno + 1 ;
if(a==segments.length) a = 0 ;
icons.arrow.rotation =
angle(pts[0].pos,segments[a].pts[0].pos)*180/Math.PI ;
}
}
else
{ if(ptno==clen-1) ind = ptno-1 ; else ind = ptno ;
icons.arrow.rotation = angle(pts[ind].pos,pts[ind+1].pos)*180/Math.PI ;
}
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) } ;
}
if(selmarker==null) selmarker = new google.maps.Marker
({ position:pos, map:map, cursor:'default', icon:icons.arrow , zIndex:2 }) ;
else // avoid unnecessary redraws
{ selmarker.setIcon(icons.arrow) ;
selmarker.setPosition(pos) ;
}
if(segments[0].level) return ;
if(drawpro.pro.m.h.length) drawxcur(drawpro.pro,selected) ;
if(ptno!=0) blackout(scissorsbtn) ; else greyout(scissorsbtn) ;
}
/* -------------------------------------------------------------------------- */
/* 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 ;
imginfo = { pending:[] , uri:null } ;
if(!genpage.focused)
{ genpage.focused = 1 ;
window.addEventListener("focus",()=>{ shifted = null ; }) ;
}
imgdiv = null ;
if(window.loaded) window.addEventListener("beforeunload",checklostedits) ;
else window.onload = function()
{ window.addEventListener("beforeunload",checklostedits) ; } ;
while(body.childNodes.length>0)
body.removeChild(body.childNodes[body.childNodes.length-1]) ;
mapdiv = domcreate('div',null,'id','map') ;
mapdiv.setAttribute('style','width:100%;height:100%;position:absolute') ;
body.appendChild(mapdiv) ;
if(response==null)
{ div = blurbdiv(buscolour) ;
div.setAttribute('style','font-family:helvetica;padding:0 4px;');
mapdiv.appendChild(div) ;
div = filedialogue("load") ;
div.setAttribute('style','margin:4px;'+
'border-top:solid 1px silver;padding-top:4px;'+
'border-bottom:solid 1px silver;padding-bottom:2px');
mapdiv.appendChild(div) ;
div = rmhelpdiv(-1) ;
div.setAttribute('style','font-family:helvetica;margin:4px;font-size:90%') ;
mapdiv.appendChild(div) ;
genpage.im = rmhelpdiv.im ;
}
else render(response,trackuri,'uri',mode,'load') ;
}
/* -------------------------------- getlist --------------------------------- */
function getlist(uri,imgtype,r1,r2)
{ if(uri.substring(uri.length-4).toLowerCase()!='.xml')
{ alert(uri+' '+L.isnotxml) ;
imginfo = { status:'failed' , uri:null } ;
return ;
}
var xhttp = new XMLHttpRequest() ;
var p = (imginfo&&imginfo.pending)?imginfo.pending:[] ;
imginfo = { status:'waiting' , type:imgtype , uri , pending:p } ;
xhttp.onreadystatechange = function()
{ var r,i,xmldoc,i,c,d ;
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) ;
alert(inject(L.unable,r)) ;
imginfo = { status:'failed' , uri:null } ;
return ;
}
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++) genmarker(p[i],1) ;
if(imgtype=='uriform') photoprompt() ;
else if(r1&&r2) r1.smallphoto = gatherpix(r2) ;
else if(r1) for(i=0;i<r1.pts.length;i++)
r1.pts[i].smallphoto = thumbpix(r1.pts[i]) ;
}
}
console.log('getting photolist '+uri.substring(1+uri.lastIndexOf('/'))) ;
xhttp.open("GET",fileserver+(imgtype=='uriform'?'?get=':'?')+uri,true) ;
xhttp.send() ;
}
/* --------------------- 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 global 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,disp,s,loadflags=null,pro ;
var extn = filename.substring(filename.length-4).toLowerCase(),newseg ;
var olen = segments.length , smt ;
if(overwrite) render.overwrite = overwrite ;
if(origin=='refresh') { ovr = 'refresh' ; origin = p4 ; }
else { ovr = render.overwrite ; loadflags = p4 ; }
genpage.im = null ;
// purge body of everything except mapdiv
if(mapparent==null||body==null)
mapparent = body = document.getElementsByTagName("body")[0] ;
infowindow.close() ;
keyhandler = keystroke ;
document.addEventListener('keyup',function(e){if(e.keyCode==16)shiftkey(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 ; // load failed or refreshing a component
r = getbounds(newseg) ;
/* ----------------------- process the photolist -------------------------- */
// if we're displaying a track/index, check its photolist and load it if nec
if(segments[0].level==0&&newseg.list!=imginfo.uri)
{ if(!imginfo.uri) getlist(newseg.list,'uri') ;
else if(!newseg.list) imginfo.carriedover = 1 ;
else abend(L.inconsistentlists+' ' + newseg.list + ' vs.' + imginfo.uri) ;
}
/* ------------------------------------------------------------------------ */
// 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(map&&(s=map.getBounds()))
{ if(ovr=='add') r = r.union(s) ; render.bounds = null ; }
else
{ if(ovr=='add'&&render.bounds) r = r.union(render.bounds) ;
render.bounds = r ;
}
flag = 0 ;
if(!map) // all this only done on first call
{ for(i=body.childNodes.length-1;i>=0;i--)
if(body.childNodes[i]!=mapdiv&&body.childNodes[i]!=telltale.div)
body.removeChild(body.childNodes[i]) ;
disp = -1 ;
if(loadflags)
{ if(loadflags.indexOf('f')>=0) disp = 0 ;
else if(loadflags.indexOf('t')>=0) disp = 1 ;
else if(loadflags.indexOf('s')>=0) disp = 2 ;
}
if(disp<0) { if(segments[0].level) disp = 0 ; else disp = 1 ; }
if(disp==0) disp = google.maps.MapTypeId.ROADMAP ;
else if(disp==1) disp = google.maps.MapTypeId.TERRAIN ;
else disp = google.maps.MapTypeId.SATELLITE ;
opts = { zoom: 22,
center: r.getCenter(),
scaleControl: true,
rotateControl: false,
streetViewControl: false,
keyboardShortcuts: false, // this is needed to keep the Google Maps
// API from hijacking the arrow keys
clickableIcons: false, // GRO POIs
mapTypeId: disp,
disableDoubleClickZoom: true,
fullscreenControl: true,
fullscreenControlOptions:
{position:google.maps.ControlPosition.BOTTOM_RIGHT},
styles: [ { featureType:"poi", stylers:[{visibility: "off"}]} ],
mapTypeControl:true,
mapTypeControlOptions:
{ style:google.maps.MapTypeControlStyle.HORIZONTAL_BAR },
mapTypeIds: [ google.maps.MapTypeId.ROADMAP,
google.maps.MapTypeId.TERRAIN,
google.maps.MapTypeId.SATELLITE
],
mapId: 'rmmap',
} ;
map = new google.maps.Map(mapdiv,opts) ;
// https://stackoverflow.com/questions/77127884/...
// ...google-maps-3-54-adding-style-to-the-map
if(disp==google.maps.MapTypeId.ROADMAP)
{ smt = new google.maps.StyledMapType(
[ { featureType: 'water',
elementType: 'all',
stylers: [ {color:'#aadaff'}, {visibility:'on'} ]
} ,
{ featureType: 'landscape',
elementType: 'all',
stylers: [ {lightness:50}, {visibility:'on'} ]
} ] ) ;
map.mapTypes.set('styled_map',smt) ;
map.setMapTypeId('styled_map') ;
}
map.setOptions({draggable:true, draggableCursor:'default'}) ;
google.maps.event.addListener(map,"click",selpoint) ;
selpoint.clickscale = (r.getSouthWest().lat()+r.getNorthEast().lat()) / 2 ;
selpoint.clickscale = 156543 * Math.cos(selpoint.clickscale*Math.PI/180) ;
// set up buttons
if(segments[0].level) eyebtn = genbutton('eye') ;
setbtn = genbutton('settings') ;
if(segments[0].level==0)
{ segbtn = genbutton('segment') ;
wpbtn = genbutton('waypoint') ;
scissorsbtn = genbutton('scissors') ;
penbtn = genbutton('pen') ;
photobtn = genbutton('camera') ;
}
undobtn = genbutton('undo') ;
redobtn = genbutton('redo') ;
dlbtn = genbutton('dl') ;
acbtn = genbutton(prefs.email?1:0) ;
if(segments[0].level==0) window.addEventListener('resize',resizer) ;
if(loadflags&&loadflags.indexOf('p')>=0) flag = 1 ;
}
// google may repeatedly add a margin: see https://stackoverflow.com/...
// ...questions/8170023/google-maps-api-3-zooms-out-on-fitbounds/41753053
if(ovr!='refresh') map.fitBounds(r,0) ;
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) ;
}
// add the markers
if(newseg.type=='segments'&&segments[0].level==0)
{ recurse(newseg,'genmarker',{map,sel:selpoint}) ;
recurse(newseg,'genlabel',{map,sel:selpoint}) ;
}
if(segments[0].level==0) // draw cursor and altitude profile
{ if(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.removeChild(pro.prodiv) ;
if(pro.curdiv) pro.curdiv.parentNode.removeChild(pro.curdiv) ;
}
drawpro.pro = pro = new profiletype(1) ;
mapparent.appendChild(pro.prodiv) ;
mapparent.appendChild(pro.curdiv) ;
pro.map = map ;
pro.active = flag ;
drawprofile() ;
getalts(segments,1,drawprofile) ;
drawsel() ;
}
else selected = { segno:-1 , ptno:-1 , type:null } ;
render.overwrite = "add" ;
if(loadflags&&loadflags.indexOf('z')>=0)
{ if(segments[0].level==0) eventually(routeinfo) ;
else eventually(eyedisplay) ;
}
}
/* ------------------------------- genbutton -------------------------------- */
function genbutton(name)
{ var u,v,b,g,k,h,gtitle=null,ktitle,a ;
u = domcreate('div') ;
u.style.backgroundColor = '#ffffff' ;
u.style.border = '2px solid #ffffff' ;
u.style.borderRadius = '3px' ;
u.style.boxShadow = '0 2px 6px rgba(0,0,0,.3)' ;
if(name=='dl'||name=='settings'||name=='cursor'||name=='eye')
u.style.cursor = 'pointer' ;
else u.style.cursor = 'default' ;
u.style.marginBottom = '12px' ;
u.style.textAlign = 'center' ;
if(name=='eye') { h = eyedisplay ; u.index = -1 ; ktitle = L.eyemenu ; }
else if(name=='settings')
{ h = routeinfo ;
u.index = 0 ;
if(segments[0].level>0) ktitle = L.controlmenu ;
else ktitle = L.routeprops ;
}
else if(name=='segment')
{ h = seginfo ; u.index = 1 ; ktitle = L.segmentprops ; }
else if(name=='waypoint')
{ h = wpinfo ; u.index = 2 ; ktitle = L.waypointprops ; }
else if(name=='scissors')
{ h = snip ;
u.index = 3 ;
ktitle = caps(L.splitsegment) + ' ' + L.atwaypoint ;
gtitle = caps(L.splitsegment) + ' ' + L.nosplitsegment ;
gtitle = L.nosplitsegment ;
}
else if(name=='pen')
{ h = labelprompt ; u.index = 4 ; ktitle = L.labelwaypoint ; }
else if(name=='camera')
{ h = photoprompt ;
u.index = 5 ;
ktitle = L.addaphoto ;
gtitle = L.noaddaphoto ;
}
else if(name=='undo')
{ h = undo ;
u.index = 6 ;
ktitle = L.undolatest ;
gtitle = L.noundolatest ;
}
else if(name=='redo')
{ h = redo ;
u.index = 7 ;
ktitle = L.redolatest ;
gtitle = L.noredolatest ;
}
else if(name=='dl')
{ h = function() { dl(0) ; } ; u.index = 8 ; ktitle = L.saveroute ; }
else // account
{ function acfactory()
{ return function()
{ infowindow.open(acmenu('account'),getbtnpos(9),'account') ; } ;
}
h = acfactory() ;
u.index = 9 ;
if(name) ktitle = L.account ;
else ktitle = L.register[1] + '/' + L.register[0] ;
}
if(u.index!=9) u.style.marginRight = '4px' ;
if(gtitle==null) gtitle = ktitle ;
v = btnicon(name) ;
g = v.grey ;
k = v.black ;
if(name!='scissors'&&name!='undo'&&name!='redo')
{ b = k ; u.title = ktitle ; u.onclick = h ; a = 1 ; }
else { b = g ; u.title = gtitle ; a = 0 ; }
u.appendChild(b) ;
map.controls[google.maps.ControlPosition.BOTTOM_CENTER].push(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')
alert(inject2(L.illegalopt,ovr,L.load)) ;
if(ovr=='load')
{ var msg = unsavedmsg(1) ; if(msg!=null) if(!confirm(msg)) return ; }
infowindow.open(filedialogue(ovr),getbtnpos(0),'addload') ;
}
function refresh(segno)
{ var s,i,stem=null ;
function renfactory(t,s)
{ return function(r) { render(r,t,'refresh',s) ; } ; } ;
infowindow.close() ;
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('/')) ;
textprompt(inject(L.waitingfor,s),null,null,'loadwait') ;
readuri(renfactory(trackuri,segno)) ;
}
/* -------------------------------------------------------------------------- */
/* 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 1 => download segments as index
// 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 str,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(segments[0].level==2) str = L.metaindex ;
else if(segments[0].level==1) str = L.index ;
else str = L.gpstrack ;
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.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 = L.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)
{ div = domcreate('div') ;
legend = L.saveas + ' ' + filename + trackextns[3] ;
function cofactory1()
{ return function() { indexdl(filename+'.rte',name,3) ; } ; } ;
div.appendChild(genclickfn(cofactory1(),legend,'hr')) ;
div.appendChild(genspan(L.combinetosave)) ;
infowindow.open(div,getbtnpos(8),'download') ;
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 = L.saveas + ' ' + filename + trackextns[i] ;
function cofactory(parm) { return function() { confirmeddl(parm) ; } ; } ;
div.appendChild(genclickfn(cofactory(filename+trackextns[i]),legend,'br')) ;
}
if(interp.length>3)
div.appendChild(genspan(L.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) div.appendChild(genspan(L.yourtimes,'br')) ;
div.appendChild(doclink('outputformats',L.formatdoc)) ;
infowindow.open(div,getbtnpos(opt?0:8),'download') ;
}
function canceldl() { infowindow.close() ; }
function indexdl(filename,name,opt,e)
{ infowindow.close() ;
var i , r , str , optparms=[] ; // shift-click bypasses optimisation
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
{ r = new routetype(segments[0].level?'metaindex':'index') ;
r.title = name ;
r.pts = new Array(segments.length) ;
r.gallery = routeprops.gallery ;
if(segments[0].level) optparms = [metaparms] ;
else if(!e||!e.shiftKey) optparms = [indparms] ;
for(i=0;i<segments.length;i++) r.pts[i] = indexify(segments[i],optparms) ;
str = writeindex(r,0) ;
}
if(str) saveAs(new Blob([str],{type:"text/plain;charset=utf-8"}),filename) ;
unsavedchanges = [] ;
}
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') ;
div.appendChild(genspan(inject(L.waitingfor,L.missingalts),'br')) ;
div.appendChild(genspan(L.dontwanttowait+' ')) ;
div.appendChild(genclickfn(canceldl,L.cancel)) ;
div.appendChild(genspan('; ')) ;
div.appendChild(genclickfn(semiconfirmeddl,L.useinterp)) ;
div.appendChild(genspan('; ')) ;
div.appendChild(genclickfn(reconfirmeddl,L.savemissing)) ;
div.appendChild(genspan('.')) ;
infowindow.open(div,getbtnpos(8),'download') ;
getalts(segments,1,drawprofile,reconfirmeddl) ;
}
function semiconfirmeddl() // turn off callback to reconfirmeddl
{ getalts(null,null,null,null) ; reconfirmeddl() ; }
function reconfirmeddl()
{ var filename = dl.pending , seg0 = 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 = seg0.pts ;
routeprops.optim = seg0.optim ;
routeprops.filename = filename ;
if((str = writegps(routeprops,seg0.pts,seg0.geo,mode,prefs.precision)))
{ 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 optimaccept(result,seg,parms,segno)
{ seg.optim = { already:0 , origlen:seg.pts.length ,
len:result.ind.length , parms:parms } ;
actions[nactions++] =
[ 'optimise' , segno , parms , seg.pts , seg.title , seg.optim , result ] ;
}
function optimparms(v,m)
{ var wp = Math.pow(10,200/v) , t=2*Math.pow(wp,0.33) ;
return {tol:t,maxsep:m,wppenalty:wp,vweight:1} ;
}
function pen2detail(pen) { return Math.floor(0.5+200/Math.log10(pen)) ; }
/* -------------------------------------------------------------------------- */
function optimmerge(seg,res)
{ var s,i,k,n=res.ind.length,s=new Array(n) ;
if(res.h) for(i=0;i<n;i++)
{ s[i] = seg.pts[res.ind[i]] ;
k = s[i].h ; s[i].h = res.h[i] ; res.h[i] = k ;
} // after optimisation res.h contains the altitudes before smoothing
else for(i=0;i<n;i++) s[i] = seg.pts[res.ind[i]] ;
deltify(s) ;
seg.pts = s ;
}
/* -------------------------------------------------------------------------- */
function optimwork(segno)
{ infowindow.close() ;
var d , slider = document.createElement('input') , seg = segments[segno];
var div = document.createElement('div') ,i , r=null , hold , box ;
var shadow=null , inp , ptsdiv , dd , newseg=null , v , maxsep , sold ;
function dgen(d,v,n)
{ while(d.childNodes.length>0)
d.removeChild(d.childNodes[d.childNodes.length-1]) ;
d.appendChild(domcreate('span',v)) ;
if(!n) n = '----' ;
ptsdiv = domcreate('div',n+' '+L.points,'style','float:right') ;
d.appendChild(ptsdiv) ;
}
greyout(setbtn) ;
bulkout(0) ;
dragging = 2 ;
recurse(seg,'obliterate') ;
ldisconnect(segno) ;
disconnect(segno) ;
shadow = new google.maps.Polyline(new linepath(seg,-1,0,'darkgrey',3,-1)) ;
shadow.setMap(map) ;
div.setAttribute('style','position:absolute;left:0;bottom:0;width:160px;'+
'z-index:999;background:white') ;
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) { deltify(s) ; seg.pts = s ; drawprofile() ; draw(seg) ; }
function vender(val,maxval)
{ var parms,line,s,n,i ;
seg.line.setMap(null) ;
for(i=0;i<sold.length;i++) sold[i].h = hold[i] ;
r = optimise(sold,optimparms(val,maxval)) ;
n = r.ind.length ;
s = new Array(n) ;
for(i=0;i<n;i++) { s[i] = sold[r.ind[i]] ; s[i].h = r.h[i] ; }
posit(s) ;
dgen(dd,val,n) ;
}
// store incoming values
sold = seg.pts ;
hold = new Array(sold.length) ;
for(i=0;i<sold.length;i++) hold[i] = sold[i].h ;
// create slider
d = document.createElement('div') ;
d.setAttribute('style','padding-bottom:4px;text-align:center') ;
slider.setAttribute('type','range') ;
slider.setAttribute('min',1) ;
slider.setAttribute('max',100) ;
slider.setAttribute('value',v) ;
slider.setAttribute('id',"optimrange") ;
slider.setAttribute('style','width:140px') ;
slider.oninput = function() { dgen(dd,this.value) ; }
slider.onmouseup = function() { v = this.value ; vender(v,maxsep) ; }
d.appendChild(slider) ;
div.appendChild(d) ;
// text response
dd = document.createElement('div') ;
dd.setAttribute('style','margin:0;padding:0 10px 4px 10px;font-size:80%') ;
dgen(dd,v,seg.pts.length) ;
div.appendChild(dd) ;
vender(v,maxsep) ;
// limit point separation?
d = document.createElement('div') ;
d.setAttribute('style',
'padding:0 0 4px 10px;font-size:80%;white-space:nowrap') ;
s = domcreate('label',L.limsep,'for','limsep') ;
d.appendChild(s) ;
box = domcreate('input',null,'type','checkbox') ;
box.setAttribute('id','limsep') ;
if(maxsep) box.setAttribute('checked',true) ;
box.onchange = function()
{ if(box.checked) maxsep = 100 ; else maxsep = 0 ; vender(v,maxsep) ; }
d.appendChild(box) ;
div.appendChild(d) ;
d = document.createElement('div') ;
d.setAttribute('style','padding-bottom:4px;text-align:center') ;
// remove the overlaid lines and reset buttons
function unshadow()
{ seg.line.setMap(null) ;
shadow.setMap(null) ;
body.removeChild(div) ;
blackout(setbtn) ;
bulkout(1) ;
dragging = 0 ;
}
// cancel button
inp = domcreate('button',caps(L.cancel)) ;
inp.style.cursor = "pointer" ;
inp.onclick = function()
{ var i ;
unshadow() ;
for(i=0;i<sold.length;i++) sold[i].h = hold[i] ;
posit(sold) ;
}
d.appendChild(inp) ;
// submit button
inp = domcreate('button',L.ok,'type','submit') ;
inp.style.cursor = "pointer" ;
function accept(response)
{ var i,s,n = r.ind.length,k ;
seg.pts = sold ;
for(i=0;i<sold.length;i++) seg.pts[i].h = hold[i] ;
optimaccept(r,seg,optimparms(v,maxsep),segno) ;
for(s=new Array(n),i=0;i<n;i++)
{ s[i] = sold[r.ind[i]] ;
k = s[i].h ; s[i].h = r.h[i] ; r.h[i] = k ;
} // now r.h contains the altitudes before smoothing
posit(s) ;
donesomething() ;
selected.ptno = 0 ;
seginfo() ;
}
inp.onclick = function()
{ unshadow() ;
if(!prefs.email||(v==prefs.detail&&(!!maxsep)==!!prefs.maxsep)) accept(0) ;
else textprompt(L.detail,[v,maxsep],accept,'optim') ;
}
d.appendChild(inp) ;
div.appendChild(d) ;
body.appendChild(div) ;
}
/* -------------------------------------------------------------------------- */
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 = L.addtitle ;
else if(opt=='desc') msg = L.adddesc ;
}
else
{ if(opt=='title') msg = caps(L.edittitle) ;
else if(opt=='desc') msg = caps(L.editdesc) ;
}
msg += ':' ;
useval = oldval ;
if(opt=='desc')
{ textprompt(msg,oldval==null?'':oldval,respond,'desc') ; return ; }
else respond(window.prompt(msg,useval==null?'':useval)) ;
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')
{ if(segno==null) routeprops.desc = newval ;
else segments[segno].desc = newval ;
}
else if(segno==null)
{ setdomtitle(routeprops.title=newval) ; routeprops.filename = null ; }
else segments[segno].title = newval ;
if(segno!=null&&opt!='desc'&&segments[segno].origin)
segments[segno].origin[0] = null ;
donesomething() ;
if(segno==null||segments[0].level) 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() ;
}
}
/* ------------------------------- restars ---------------------------------- */
function restars(oldstars,newstars,starsdiv)
{ actions[nactions++] = [ 'stars' , oldstars , newstars ] ;
donesomething() ;
starsline(routeprops.stars=newstars,1,starsdiv) ;
}
/* -------------------------------------------------------------------------- */
function routeinfo()
{ infowindow.close() ;
infowindow.open(cogwheelmenu(dragging),getbtnpos(0),'settings') ;
}
function eyedisplay()
{ infowindow.close() ; infowindow.open(eyemenu(),getbtnpos(-1),'eyemenu') ; }
/* ------------------------------- 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 ;
x = prompt(L.enteroffset) ;
if(x==null) return ;
y = parseFloat(x) ;
if(isNaN(y)) { alert(inject(L.isnan,x)) ; return ; }
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.close() ;
infowindow.open(rmhelpdiv(segments[0].level),getbtnpos(0),'help') ;
}
/* -------------------------------- 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,resp=pts[s1],clen=pts.length,mindist ;
// delete the point (but not the label/photo if we're detaching/attaching)
if(!opt) resp.setmap(null,null) ; else if(resp.point) resp.point.map = null ;
if(!s2&&clen==1) resp.point.map = null ;
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,resp.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 resp ;
}
function wpdel(opt) // opt means detach
{ 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]) ;
else if(!opt) done(['wpdel',s0,s1,s2,r]) ;
drawsel() ;
if(flag=='wpinfo') wpinfo() ;
}
/* --------------------------------- 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].dots = seg[n].dothandler = 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()
{ var s0 = selected.segno ;
infowindow.close() ;
dupsegwork(segments,s0) ;
done(['dupseg',s0]) ;
}
/* -------------------------------------------------------------------------- */
/* 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].setpos(pos) ;
if(s2) geodraw(pts[s1],segments[s0].colour) ;
else redelta(pts,s1) ;
draggit(1) ;
}
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]) ;
}
/* -------------------------------- draggit --------------------------------- */
// draggit makes the current waypoint draggable
function draggit(insparm)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type ;
var start,end,i,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) seg.line.setMap(null) ;
selmarker.setMap(null) ;
selmarker = new google.maps.Marker(
{ position: startpos,
map: map,
cursor: 'default',
icon: icons.concircle ,
draggable: true ,
zIndex: 2
} ) ;
colour = seg.colour ;
if(seg.clickhandler!=null)
{ google.maps.event.removeListener(seg.clickhandler) ;
seg.clickhandler = null ;
}
map.panToBounds(new google.maps.LatLngBounds(startpos,startpos)) ;
if(!s2)
{ draggit.seg0 = draggit.seg2 = null;
if(s1>1)
{ draggit.seg0 = new google.maps.Polyline(new linepath(seg,0,s1,colour)) ;
draggit.seg0.setMap(map) ;
}
if(s1==0) start = 0 ; else start = s1-1 ;
if(s1==len-1) end = s1+1 ; else end = s1+2 ;
draggit.seg1 = new google.maps.Polyline(new linepath(seg,start,end,colour)) ;
draggit.seg1.setMap(map) ;
if(s1<seg.pts.length-2)
{ draggit.seg2 = new google.maps.Polyline(new linepath(seg,s1+1,len,colour)) ;
draggit.seg2.setMap(map) ;
}
}
draggit.l1 = google.maps.event.addListener(selmarker,'drag',function()
{ var pos = this.getPosition() ;
pt.setpos({lat:pos.lat(),lng:pos.lng()}) ;
if(!s2)
{ draggit.seg1.setMap(null) ;
draggit.seg1 = new google.maps.Polyline(new linepath(seg,start,end,colour)) ;
draggit.seg1.setMap(map) ;
if(s1==0) { ldisconnect(s0) ; lconnect(s0) ; }
if(s1==len-1&&s0<segments.length-1) { disconnect(s0) ; connect(s0) ; }
}
} ) ;
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,s1dash,d=s2?segments[s0].geo:segments[s0].pts ;
var xpos,pos=d[s1].pos ;
google.maps.event.removeListener(draggit.l1) ;
dragging = 0 ;
d[s1].h = null ;
if(!s2)
{ if(draggit.seg0) draggit.seg0.setMap(null) ;
draggit.seg1.setMap(null) ;
if(draggit.seg2) draggit.seg2.setMap(null) ;
draw(segments[s0]) ;
getalts(segments,100,drawprofile) ;
redelta(d,s1) ;
drawprofile() ;
}
selmarker.setMap(null) ;
selmarker = null ; // force a redraw
drawsel() ;
if(draggit.inserted||dist(draggit.startpos,pos)>5)
done(['move',s0,s1,s2,draggit.startpos,pos,draggit.inserted]) ;
bulkout(1) ;
}
/* -------------------------------------------------------------------------- */
function seginfo()
{ var pos = segments[selected.segno].pts[selected.ptno].pos ;
infowindow.close() ;
infowindow.open(seginfodiv(segments,selected.segno),pos,'seginfo') ;
}
/* -------------------------------------------------------------------------- */
function altinfo()
{ var pos = segments[selected.segno].pts[selected.ptno].pos ;
infowindow.close() ;
infowindow.open(altinfodiv(segments,selected.segno),pos,'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].setpos( { 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 x,y=null,oldalt=null,pt=s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
if(edit)
{ oldalt = pt.h.toFixed(precision) ; x = prompt(L.enteralt,oldalt) ; }
else x = prompt(L.enteralt) ;
if(x==null) return ;
if(x!=''&&isNaN(y=parseFloat(x))) { alert(inject(L.isnan,x)) ; return ; }
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 labelprompt()
{ var oldlabno,label='Generic',s0=selected.segno,s1=selected.ptno ;
var s2 = selected.type , pt = s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
var oldcaption='' , i , str , flag = (infowindow.close()=='wpinfo') ;
function editlabel(caption,label)
{ if(caption==null) label = null ; else if(!caption) caption = null ;
if(label==null&&oldlabno>=0)
{ if(oldlabno>=0) label = iconic.names[oldlabno] ; else label = null ;
done(['editlabel',s0,s1,s2,oldcaption,null,label,null]) ;
pt.setlabel() ;
if(flag) wpinfo() ; else walkto(s0,s1,s2) ;
}
else if(label!=null)
{ label = iconic.names[label] ;
if(oldlabno>=0) oldlabno = iconic.names[oldlabno] ; else oldlabno = null ;
if(caption!=oldcaption||label!=oldlabno)
{ pt.setlabel(label,caption) ;
pt.setlabelmap(map,selpoint) ;
done(['editlabel',s0,s1,s2,oldcaption,caption,oldlabno,label]) ;
}
if(flag) wpinfo() ; else walkto(s0,s1,s2) ;
}
}
oldlabno = pt.label ;
if(!oldlabno) oldlabno = -1 ;
else for(oldcaption=pt.marker.title,i=iconic.names.length-1;i>=0;i--)
if(i==0||iconic.names[i]==oldlabno) { oldlabno = i ; break ; }
if(oldcaption==null||oldlabno<0) oldcaption = '' ;
if(oldlabno<0) str = L.enterlabel ; else str = L.modlabel ;
labelmenu(str,oldcaption,editlabel,oldlabno) ;
}
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.setlabel(null,null) ;
}
}
if(lab.length) done(['unlabel',lab]) ;
routeinfo() ;
}
/* -------------------------------------------------------------------------- */
function photoprompt(e)
{ var s0=selected.segno,s1=selected.ptno,s2=selected.type,pt,photo,title,k ;
if(e) e.preventDefault() ;
var flag = (infowindow.close()=='wpinfo') ;
if(imginfo.status!='ready'||imginfo.carriedover)
{ infowindow.open(filedialogue("list"),getbtnpos(5),'getlist') ; return ; }
pt = s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
photo = window.prompt(L.enterphoto,'') ;
if(photo)
{ done(['editphoto',s0,s1,s2,pt.photo.length,null,photo,null]) ;
pt.photo.push(photo) ;
if(pt.photo.length==1) genmarker(pt,0,map,selpoint) ;
else pt.photomarker.title = phototitle(pt) ;
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,save ;
var flag = (infowindow.close()=='wpinfo') ;
var pt = s2?segments[s0].geo[s1]:segments[s0].pts[s1] ;
var photo = window.prompt(L.newphoto,pt.photo[ind]) ;
if(photo==null) //cancel
{ if(flag) wpinfo() ; else walkto(s0,s1,s2) ; return ; }
if(ind) save = null ; else save = pt.photomarker.content ; // save old marker
done(['editphoto',s0,s1,s2,ind,pt.photo[ind],photo,save]) ;
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.setphotomap(null,null) ;
else
{ if(ind==0) { pt.setphotomap(null,null) ; genmarker(pt,0,map,selpoint) ; }
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) ;
keyhandler = imgwalk ;
infowindow.close() ;
if(!display.messaged)
{ display.messaged = 1 ;
if(!window.matchMedia("(hover: none)").matches)
telltale(L.returnkey+L.backtogps) ;
else telltale(L.swipedown+L.backtogps) ;
}
imgdiv = document.createElement('div') ;
imgdiv.setAttribute('style','position:fixed;width:100%;height:100%;'+
'left:0;top:0;background:black') ;
dodisplay(selected.segno,selected.ptno,ind,1) ;
mapparent.appendChild(imgdiv) ;
}
function dodisplay(s0,s1,ind,dir)
{ var item,litem=null,ritem=null,sect ;
var infowords = { exit: L.exitfs , enter: L.enterfs ,
notes: L.notes , origin: caps(L.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,'phinfo') ;
}
/* ------------------------- 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+1].dothandler = seg[s0].dothandler ;
seg[s0].dots = seg[s0].dothandler = 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 ;
seg[s0].geo[i].point.map = null ;
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==0||segments[0].type=='tour')
{ ldisconnect(segno) ; for(i=segno;i<segno+n;i++) disconnect(i) ; }
if(n>1)
{ r = new routetype('segments') ;
r.pts = new Array(n) ;
for(i=0;i<n;i++) r.pts[i] = segments[segno+i] ;
}
else if(segments[0].level==0) r = segments[segno] ;
else r = segments[0].pts[segno] ;
if(segments[0].level==0) seg = segments ; else seg = segments[0].pts ;
for(i=segno;i<segno+n;i++) recurse(seg[i],'obliterate') ;
for(i=segno;i<seg.length-n;i++) seg[i] = seg[i+n] ;
seg.length -= n ;
if(segments[0].level==0||segments[0].type=='tour') lconnect(segno) ;
if(segments[0].level==0)
{ 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() ;
}
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 ;
if(segments[0].level==0) seg = segments ; else seg = segments[0].pts ;
if(segments[0].level==0||segments[0].type=='tour') ldisconnect(segno) ;
m = seg.length ;
if(r.type=='segments') r = r.pts ; else r = [r] ;
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') ;
for(pts=seg[segno+i].pts,j=0;j<pts.length;j++)
{ if(pts[j].photo.length) genmarker(pts[j],0,map,selpoint) ;
pts[j].setmap(map,selpoint) ;
}
}
if(segments[0].level==0||segments[0].type=='tour')
{ lconnect(segno) ; for(i=0;i<n;i++) connect(segno+i) ; }
if(segments[0].level==0)
{ if(selected.segno>=segno) selected.segno += n ;
drawprofile() ;
drawsel() ;
}
}
/* ---------------------- 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)
{ if( nactions>0 && unsavedchanges.length>0 && something[0]=='editlabel'
&& actions[nactions-1][0]=='editlabel'
&& actions[nactions-1][1]==something[1]
&& actions[nactions-1][2]==something[2]
&& actions[nactions-1][3]==something[3]
&& actions[nactions-1][7] && something[7] )
// don't merge change with create/delete
{ actions[nactions-1][4] = something[5] ; // caption
actions[nactions-1][6] = something[7] ; // label
}
else { 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 , pts , props.title , props.optim , res ]
function optimswap(action,doing)
{ var s0 = action[1] , i , s , r = action[6] , k , pt , seg = segments[s0] ;
var n = r.ind.length ;
if(doing=='redo')
{ seg.optim = action[5] ;
s = new Array(n) ;
for(i=0;i<n;i++)
{ s[i] = seg.pts[r.ind[i]] ; k = r.h[i] ; r.h[i] = s[i].h ; s[i].h = k ; }
seg.pts = s ;
}
else
{ seg.optim = null ;
seg.pts = action[3] ;
for(i=0;i<n;i++)
{ pt = seg.pts[r.ind[i]] ; k = r.h[i] ; r.h[i] = pt.h ; pt.h = k ; }
}
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,save]) ;
function photoswap(x,opt)
{ var ind=x[4] , d=x[7] ;
var pt = x[3]?segments[x[1]].geo[x[2]]:segments[x[1]].pts[x[2]] ;
photomerge(pt.photo,x[6-opt],x[5+opt],ind) ;
if(d&&pt.photomarker) // swap one photo for another
{ x[7] = pt.photomarker.content ;
pt.photomarker.content = d ;
if(ind==0) phototitle(pt) ;
}
else if(d) // replace a deleted photo
{ x[7] = null ;
pt.photomarker = new google.maps.marker.AdvancedMarkerElement
({ map , content:d , position:pt.pos , title:phototitle(pt) }) ;
pt.setphotomap(map,selpoint) ;
}
else // undo an addition
{ x[7] = pt.photomarker.content ; pt.setphotomap(null,null) ; }
}
/* --------------------------------- undo ---------------------------------- */
function undo()
{ infowindow.close() ;
var opts = actionname(actions[nactions-1]) , div ;
div = domcreate('div',genclickfn(confirmedundo,L.undo+' '+opts,'br')) ;
if(nactions>1&&actions[nactions-2][0]!='load')
domadd(div,genclickfn(bulkundo,L.bulkundo)) ;
infowindow.open(div,getbtnpos(6),'undo') ;
}
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?L.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,getbtnpos(6),'undo') ;
}
/* -------------------------------------------------------------------------- */
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 ;
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) actions[ano][4] = segments[s0] ;
else actions[ano][4] = segments[0].pts[s0] ;
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
{ if(s2) pt = segments[s0].geo[s1] ; else pt = segments[s0].pts[s1] ;
pt.setlabel(actions[ano][6],actions[ano][4]) ;
pt.setlabelmap(map,selpoint) ;
}
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]) ;
pt.setlabelmap(map,selpoint) ;
}
else if(action=='edittitle') setdomtitle(routeprops.title=s0) ;
else if(action=='editdesc')
{ if(s2==null) routeprops.desc = s0 ; else segments[s2].desc = s0 ; }
else if(action=='wpdel'||action=='detach')
{ pts = s2?segments[s0].geo:segments[s0].pts ;
insert(pts,s1,1) ;
pts[s1] = actions[ano][4] ;
pts[s1].setmap(map,selpoint) ;
if(!s2) { redelta(pts,s1) ; redrawconnect(s0,s1) ; }
if(action=='detach'&&!s2)
{ segments[s0].geo.length -= 1 ; pts[s1].type = pts[s1].point.map = null ; }
else if(action=='detach') segments[s0].pts.length -= 1 ;
selected = { segno:s0 , ptno:s1 , type:s2 } ;
drawsel() ;
}
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) return ;
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,0,s2) ; }
}
/* --------------------------------- setptpos ----------------------------------- */
function setptpos(s0,s1,s2,pos)
{ var pts = s2?segments[s0].geo:segments[s0].pts ;
pts[s1].setpos(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,L.redo+' '+opts,'br')) ;
if(actions.length>nactions+1) domadd(div,genclickfn(bulkredo,L.bulkredo)) ;
infowindow.open(div,getbtnpos(7),'redo') ;
}
function redofactory(lim)
{ return function()
{ var i ; for(i=nactions;i<=lim;i++) confirmedredo(i<lim) ; }
}
function bulkredo()
{ 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;i<actions.length;i++)
{ f = redofactory(i) ;
g = genclickfn(f,i==nactions?L.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,getbtnpos(7),'redo') ;
}
/* -------------------------------------------------------------------------- */
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 ;
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) ;
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] ;
pt.setlabel(actions[nactions][7],actions[nactions][5]) ;
pt.setlabelmap(map,selpoint) ;
}
else if(action=='unlabel') for(i=0;i<s0.length;i++)
{ task = s0[i] ;
pt = segments[task[0]].pts[task[1]] ;
pt.setlabel(null,null) ;
pt.setlabelmap(map,selpoint) ;
}
else if(action=='edittitle')
{ if(s2==null) setdomtitle(routeprops.title=s1) ;
else segments[s2].title = s1 ;
}
else if(action=='editdesc')
{ if(s2==null) routeprops.desc = s1 ; else segments[s2].desc = 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) return ;
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,0,actions[nactions-1][3]) ;
}
}
/* -------------------------------- actionname ------------------------------ */
function actionname(x)
{ var i,s ;
if(x[0]=='bin')
{ if(x[3]) return L.deletex + ' ' + x[3] ;
else if(segments[0].level==0) return L.deletesegment ;
else if(segments[0].level==1) return L.deleteroute ;
else return L.deleteindex ;
}
if(x[0]=='snip') return L.splitsegment ;
if(x[0]=='editlabel') // ['editlabel',s0,s1,s2,oldcaption,caption,oldlabel,label]
{ if(!x[7]) return L.dellabel ;
else if(!x[6]) return L.labelpt ;
else return L.editlabel ;
}
if(x[0]=='unlabel') return L.removelabels ;
if(x[0]=='edittitle'||x[0]=='editindex title') return L.edittitle ;
if(x[0]=='editdesc')
{ if(x[3]==undefined||x[3]==null) return L.editdesc ;
else return inject(L.editxdesc,segments[x[3]].title) ;
}
if(x[0]=='editinfo') return L.editinfo ;
if(x[0]=='wpdel') return L.wpdel ;
if(x[0]=='move') { if(x[6]) return L.wpins ; else return L.wpdrag ; }
if(x[0]=='recal') return L.recalalts ;
if(x[0]=='insgeo') return L.insgeo ;
if(x[0]=='googlecal') return L.googlelats ;
if(x[0]=='googlereg') return L.regressalts ;
if(x[0]=='googleadd') return L.calibalts ;
if(x[0]=='setalt') return L.wpalt ;
if(x[0]=='resign') return L.wpicon ;
if(x[0]=='combine') return inject(L.combinesegments,x[2]) ;
if(x[0]=='revseg') return L.revsegment ;
if(x[0]=='dupseg') return L.dupsegment ;
if(x[0]=='optimise')
{ if(x[4]) return inject(L.optimsth,x[4]) ; else return L.optim ; }
if(x[0]=='refresh') return L.refresh ;
if(x[0]=='deltimes') return L.deltimes ;
if(x[0]=='detach') return x[3]?L.attach:L.detach ;
if(x[0]=='delalts') return L.delalts ;
if(x[0]=='editphoto')
{ if(!x[6]) return L.delphoto ;
else if(!x[5]) return L.addphoto ;
else return L.modphoto ;
}
if(x[0]=='extra') return L.interpextra ;
if(x[0]=='stars')
{ if(!x[2])
{ for(s='',i=0;i<x[1];i++) s += '\u2605' ; return inject(L.clear,s) ; }
else { for(s='',i=0;i<x[2];i++) s += '\u2605' ; return inject(L.set,s) ; }
}
if(x[0]=='swapseg') return L.swapseg ;
if(x[0]=='load')
{ if(x[3]) return inject(L.loadsth,x[3]) ; else return L.load ; }
if(x[0]=='add') return inject(L.addsth,x[3]?x[3]:L.loadnewseg)
abend(inject(L.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(L.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 darken(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)) ;
v = v <= 0.0031308 ? v*12.92 : 1.055 * Math.pow(v,1.0/2.4) - 0.055 ;
if(v>0.65) v = 255*0.65/v ; else v = 255 ;
for(i=0;i<3;i++) x[i] = Math.floor(0.5+x[i]*v) ;
return x ;
}
// convert a colour (array of 3 numbers) to hex
function hexify(x)
{ function hexdigit(y)
{ y = Math.floor(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 ;
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') ;
c[nc++] = darken(hue) ; // darken bright colours
}
}
if(lev>1)
{ l = l.concat(greycode(c,nc)) ;
if(lev==2)
{ x = l[3] ; l[3] = l[7] ; l[7] = x ; // bring orange up
l.splice(6,0,[128,128,128]) ; // insert grey
}
}
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(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 , shade = new Array(n) , sind ;
for(i=0;i<n;i++) shade[i] = shadeofhue(index.hue,i,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,dist ;
var maxdist,maxcol,shade,ssq,oldcols=new Array(n) ;
for(i=0;i<n;i++) oldcols[i] = dehexify(segments[i].colour) ;
for(i=0;i<=n;i++)
{ shade = shadeofhue(hue,i,n+1) ;
for(j=0;j<n;j++)
{ ssq = d3(shade,oldcols[j]) ;
if(j==0||ssq<dist) { dist = ssq ; newcol = shade ; }
}
if(i==0||dist>maxdist) { maxdist = dist ; maxcol = newcol ; }
}
return hexify(maxcol) ;
}
function shadeofhue(hue,i,n)
{ if(n==1) return hue ;
var q,j,shade=[0,0,0],alpha=Math.min(2-hue[1]/128,255*(n+0.5)/640) ;
q = (i+alpha)*640/(n+0.5) ;
if(q<=255) for(j=0;j<3;j++) shade[j] = hue[j] * q/255 ;
else for(q=(q-255)/510,j=0;j<3;j++) shade[j] = q*255 + (1-q)*hue[j] ;
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 ;
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 and make altcallback override doneaction
if(reqlist.length>0)
{ if(!getalts.altthresh||thresh<getalts.altthresh)
getalts.altthresh = thresh ;
getalts.pending = 0 ;
return ;
}
else if(thresh==null) { getalts.pending = 0 ; return ; }
if(!getalts.elevator) getalts.elevator = new google.maps.ElevationService ;
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) ;
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>500)
{ for(i=0;i<500;i++)
reqlist[i] = reqlist[Math.floor(i*(reqlist.length-1)/499)] ;
reqlist.length = 500 ;
}
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<=500) { 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 = altcallback = 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(501),loxpos=new Array(501),k=0;k<=500;k++)
{ loxind[k] = Math.floor(0.5+(k*(npts-1))/500) ;
loxpos[k] = lox[loxind[k]].pos ;
}
else for(loxind=new Array(npts),loxpos=new Array(npts),k=0;k<npts;k++)
{ loxind[k] = k ; loxpos[k] = lox[k].pos ; }
}
// at this point lox is an array of points whose altitudes need adjusting,
// loxpos is an array of at most 500 positions and loxind holds the indexes
// in lox of the entries in loxpos
/* ------------------------------------------------------------------------ */
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!=-3) // check response is as expected
{ if(status===google.maps.ElevationStatus.OVER_QUERY_LIMIT) alert(L.goql) ;
else if(status===google.maps.ElevationStatus.INVALID_REQUEST)
alert(L.ginv) ;
else if(status===google.maps.ElevationStatus.REQUEST_DENIED)
alert(L.gden) ;
else if(status===google.maps.ElevationStatus.UNKNOWN_ERROR)
alert(inject(L.gunk,status)) ;
else if(status!==google.maps.ElevationStatus.OK) alert(L.gcer) ;
if(status!==google.maps.ElevationStatus.OK) throw '' ;
}
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 ;
return ;
}
for(k=reqno=0;reqno<reqlist.length;reqno++,k+=n)
{ if(flag==2) n = 501 ; else n = reqlist[reqno][1] - reqlist[reqno][0] ;
if(lox[loxind[k]].h!=null&&lox[loxind[k+n-1]].h!=null)
{ d0 = lox[loxind[k]].h - results[k].elevation ;
dn = lox[loxind[k+n-1]].h - results[k+n-1].elevation ;
}
else
{ if(lox[loxind[k]].h!=null)
dn = d0 = lox[loxind[k]].h - results[k].elevation ;
else if(lox[loxind[k+n-1]].h!=null)
dn = d0 = lox[loxind[k+n-1]].h - results[k+n-1].elevation ;
else dn = d0 = 0 ;
}
for(i=0;i<n;i++) lox[loxind[k+i]].h =
results[k+i].elevation + (i*dn+((n-1)-i)*d0)/(n-1) ;
// fill in missing altitudes by interpolation
if(flag==2) for(i=0;i<n-1;i++) if(loxind[i+1]>loxind[i]+1)
{ for(D=0,k=loxind[i];k<loxind[i+1];k++) D += 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) ;
getalts(segs,thresh,action) ;
} /* end of doelevations */
/* ------------------------------------------------------------------------ */
if(thresh==-3) { doelevations(null,null,m,c) ; getalts.pending = 0 ; }
else
{ getalts.pending = 1 ;
getalts.elevator.getElevationForLocations({locations:loxpos},doelevations) ;
}
}
/* -------------------------------------------------------------------------- */
function getbounds(segment)
{ var bounds = { minlon:null } ;
recurse(segment,'getbounds',bounds) ;
return new google.maps.LatLngBounds
( new google.maps.LatLng(bounds.minlat,bounds.minlon) ,
new google.maps.LatLng(bounds.maxlat,bounds.maxlon) ) ;
}
/* -------------------------------------------------------------------------- */
function promoteprops(route,base)
{ var field , flag = 0 ;
if(route.list&&base.list&&route.list!=base.list)
abend(L.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 lname = [ ' '+L.aroute , ' '+L.anindex , ' '+L.ameta ] ;
var shortname = abbreviate(filename)[0] ;
if(ovr=='add'&&segments.length==0) abend('Logic error') ;
if(ovr=='refresh') { updseg = origintype ; origintype = 'refresh' ; }
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 = [] ;
if(seg.level==0&&segments[0].level)
{ if(!seg.optim) parms.push(defparms) ;
if(segments[0].level==2) parms.push(indparms) ;
}
if(segments[0].level==1) parms.push(indparms) ; else parms.push(metaparms) ;
return indexify(seg,parms) ;
}
// parse response
if(response.length<9||(extn!='.fit'&&response.substring(0,9)=='** Error:'))
abend(response) ;
newseg = readgps(response,filename) ;
if(!newseg||!newseg.pts.length)
{ alert(inject(L.nodata,shortname)) ; 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(L.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 )
{ alert(inject2(L.addxtoy,lname[newseg.level],lname[segments[0].level])) ;
return null ;
}
}
// check legality of load level for overwriting
if(ovr=='load')
{ if( segments.length && segments[0].level==0
&& newseg.level==1 && newseg.type=='segments' ) ;
else if(segments.length&&((segments[0].level>0)!=(newseg.level>0)))
{ alert(inject2(L.addxtoy,lname[newseg.level],lname[segments[0].level])) ;
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
if(selmarker) selmarker.setMap(null) ;
selmarker = null ;
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&&(hues[j][0]!=hue[0]||hues[j][1]!=hue[1]);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:L.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.ind.length<newseg.pts.length/2)
{ optimaccept(res,newseg,lev0parms,segments.length) ;
optimmerge(newseg,res) ;
q = pen2detail(lev0parms.wppenalty) ;
telltale(inject(L.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(L.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].hue = col[i] ;
q = 0.5 * ( 2 - col[i][1]/255 ) ;
newseg.pts[i].colour = hexify([q*col[i][0],q*col[i][1],q*col[i][2]]) ;
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.hue = col = extendhues(1,hues)[0] ;
q = 0.5 * ( 2 - col[1]/255 ) ;
newseg.colour = hexify([q*col[0],q*col[1],q*col[2]]) ;
if(newseg.level) genshades(newseg) ;
segments[0].pts.push(newseg) ;
}
else alert('logic error') ;
return newseg ;
}
/* -------------------------------------------------------------------------- */
function genarrows(seg)
{ var sep = (segments[0].type=='tour')?20:10 ;
var ind = choosearrows(seg.pts,null,sep) , icon , i , n = ind.length ;
seg.arrows = new Array(n) ;
icon = { path: "M 6 9 0 15 6 0 12 15 z",
fillColor: seg.colour,
fillOpacity: 1,
strokeColor: seg.colour,
strokeWeight: 0,
anchor: new google.maps.Point(6,6),
rotation: 0,
scale: 1,
clickable: false } ;
for(i=0;i<n;i++)
seg.arrows[i] = { icon:icon , offset:(100*ind[i].x).toFixed(8) + '%' } ;
}
/* -------------------------------- parsedesc ------------------------------- */
function parsedesc(d,val,f,origin)
{ var i,dd=d,s,tags=['i','I','b','B','s','S','u','U'],slen,sty,sind,len ;
function parsehtml(d,s,f)
{ var i,j,idash,len=s.length,e,href=null,plus,ind,str ;
for(href=null,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 ;
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
if(href)
{ if(href.charAt(0)!='+') plus = 0 ;
else { href = href.substring(1) ; if(segments[0].level==0) plus = 1 ; }
href = trackref(origin,href,plus?0:2) ;
}
function loaderfactory(href)
{ return function()
{ trackuri = href ;
readuri(function(r) { render(r,href,'urilink',null,'add');}) ;
}
}
// generate the element under the tag: create a span if an <a> cannot
// be rendered as a link
if(idash>i+3&&(plus||!href)) e = document.createElement('span') ;
else e = document.createElement(s.charAt(i+1)) ;
slen += parsehtml(e,s.substring(idash,j)) ;
if(href&&plus)
{ domadd(e,' ⊞') ;
e.setAttribute('style','cursor:pointer;color:#0000bd') ;
e.onclick = loaderfactory(href) ;
}
else if(href)
{ domadd(e,' ') ;
e.appendChild(newtabdiv()) ;
e.setAttribute('href',href) ;
e.setAttribute('target','_blank') ;
}
d.appendChild(e) ;
// process the text following the tag
slen += parsehtml(d,s.substring(j+4),f) ;
return slen ;
}
// if we get here, then we have a final top-level chunk
slen += s.length ;
if(f) { s += ' [' ; slen += 3 + L.edit.length ; }
if(s) domadd(d,s) ;
if(f) { d.appendChild(genclickfn(f,L.edit)) ; domadd(d,']') ; }
return slen ;
}
/* ------------------------------------------------------------------------ */
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)
{ dd = document.createElement('div') ;
if(s) sty = 'margin-top:'+(1+3*s)+'px;' ; else sty = '' ;
if(sind<0) sty += 'margin-bottom:3px' ;
if(sty) dd.setAttribute('style',sty) ;
}
if(sind<0) { len += parsehtml(dd,val,f) ; val = '' ; }
else
{ len += parsehtml(dd,val.substring(0,sind)) ;
val = val.substring(sind) ;
}
if(i) d.appendChild(dd) ;
}
return len ;
}
/* ---------------------------------- 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 ;
if(href&&fullcall==2&&!isgps(href)) 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 ;
}
/* -------------------------------------------------------------------------- */
function pluralise(str,n)
{ if(n==1) return str[0] ; else return inject(str[1],n) ; }
function caps(x) { return x.charAt(0).toUpperCase() + x.substring(1) ; }
/* -------------------------------------------------------------------------- */
var icons =
{ // coursepoint icons
list:
[ // Generic
{ path: "M 0.5 20.5 L 0.5 0.5 12.5 6 0.5 11.5 ",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(0.5,20.5),
xmid:5.5
} ,
// Sharp left
{ path: "M 18.5 20.5 L 16.4 7 A 1 1 0 0 0 14.8 7.5 "+
"L 10.3 12.3 12.6 14.6 "+
"5.6 13.6 4.6 6.6 6.9 8.9 15 2.8 A 3.3 3.3 0 0 1 19.8 6 z",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(18.5,20.5),
xmid:13
} ,
// Left
{ path: "M 18.5 20.5 L 16.5 11.5 A 2 2 0 0 0 14.5 9.5 "+
"L 11.5 10 11.5 13.5 "+
"6.5 7.5 11.5 1.5 11.5 5 16.5 5.5 A 3.5 3.5 0 0 1 20 9 z",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(18.5,20.5),
xmid:13
} ,
// Slight left
{ path: "M 14.5 20.5 L 12.8 12.8 A 7 7 0 0 0 10.3 9 L 7.2 7 4.4 9.8 "+
"5.9 2.3 13.4 0.8 10.6 3.6 14.5 7.9 A 5 5 0 0 1 15.9 11.3 z",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(14.5,20.5),
xmid:12
} ,
// Straight
{ path: "M 7.5 20.5 L 4.5 6.5 0.5 6.5 7.5 0.5 14.5 6.5 10 6.5 z",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(7.5,20.5),
xmid:7.5
} ,
// Slight right
{ path: "M 7.5 20.5 L 9.2 12.8 A 7 7 0 0 1 11.7 9 L 14.8 7 17.6 9.8 "+
"16.1 2.3 8.6 0.8 11.4 3.6 7.5 7.9 A 5 5 0 0 0 6.1 11.3 z",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(7.5,20.5),
xmid:10
} ,
// Right
{ path: "M 3.5 20.5 L 5.5 11.5 A 2 2 0 0 1 7.5 9.5 "+
"L 10.5 10 10.5 13.5 15.5 7.5 10.5 1.5 10.5 5 5.5 5.5 "+
"A 3.5 3.5 0 0 0 2 9 z",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(3.5,20.5),
xmid:8
} ,
// Sharp right
{ path: "M 3.5 20.5 L 5.6 7 A 1 1 0 0 1 7.2 7.5 "+
"L 11.7 12.3 9.4 14.6 "+
"16.4 13.6 17.4 6.6 15.1 8.9 7 2.8 A 3.3 3.3 0 0 0 2.2 6 z",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(3.5,20.5),
xmid:9
} ,
// Danger
{ path: "M 8.5 21.5 A 2.5 2.5 0 0 1 8.5 16.5 A 2.5 2.5 0 1 1 8.5 21.5 "+
"M 8.5 14.5 5 6 A 4 4 0 1 1 12 6 L 8.5 14.5" ,
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(8.5,21.5),
xmid:8.5
} ,
// Food
{ path: "M 0.5 0.5 L 0.5 5.5 2.5 5.5 2.5 0.5 2.5 5.5 4.5 5.5 "+
"4.5 0.5 4.5 5.5 6.5 5.5 6.5 0.5 6.5 7.5 " +
"A 2.5 2.5 0 0 1 4.25 9.95 L 5 19.5 "+
"A 1.5 1.5 0 0 1 2 19.5 L 2.75 9.95 " +
"A 2.5 2.5 0 0 1 0.5 7.5 z",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(3.5,22),
xmid:3.5
} ,
// water
{ path: "M 2.5 8.5 L 7.5 8.5 A 2 2 0 0 1 9.5 6.5 L 9.5 3.5 7.5 3.5" +
"A 1 1 0 0 1 7.5 1.5 L 13.5 1.5 A 1 1 0 0 1 13.5 3.5 " +
"L 11.5 3.5 11.5 6.5 A 2 2 0 0 1 13.5 8.5 L 16.5 8.5 " +
"A 5 5 0 0 1 21.5 13.5 L 21.5 15.5 18.5 15.5 18.5 13.5 " +
"A 2 2 0 0 0 16.5 11.5 L 13.5 11.5 A 5 5 0 0 1 7.5 11.5 "+
"L 2.5 11.5 z" +
"M 20 17.5 A 1.5 1.5 0 0 1 20 20.5 A 1.5 1.5 0 0 1 20 17.5" ,
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(20,22),
xmid:12
} ,
// summit
{ path: "M 2.5 16.5 Q 10.5 2 18.5 16.5 Z "+
"M 10.5 20.5 L 10.5 1.5 15 3.5 10.5 5.5 " ,
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(10.5,21),
xmid:10.5
} ,
// valley
{ path: "M 2.5 16.5 L 2.5 6.5 Q 10.5 23 18.5 6.5 L 18.5 16.5 Z "+
"M 10.5 20.5 L 10.5 1.5 15 3.5 10.5 5.5 ",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(10.5,21),
xmid:10.5
} ,
// first aid
{ path: "M 1.5 7.5 L 7.5 7.5 7.5 1.5 13.5 1.5 13.5 7.5 19.5 7.5 "+
"19.5 13.5 13.5 13.5 13.5 17.5 10.5 21.5 7.5 17.5 7.5 13.5 "+
"1.5 13.5 Z ",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(10.5,22),
xmid:10.5
} ,
// info
{ path: "M 9 6 A 2.5 2.5 0 0 1 9 1 A 2.5 2.5 0 1 1 9 6 M 9 21.5 "+
"A 2 2 0 0 0 7 19.5 L 3.5 19.5 3.5 19.5 3.5 17.5 5 17.5 "+
"A 1.5 1.5 0 0 0 6.5 16 L 6.5 11 A 1.5 1.5 0 0 0 5 9.5 "+
"L 3.5 9.5 3.5 7.5 11.5 7.5 11.5 16 A 1.5 1.5 0 0 0 13 17.5 "+
"L 14.5 17.5 14.5 19.5 11 19.5 A 2 2 0 0 0 9 21.5",
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(9,22),
xmid:9
} ,
// obstacle
{ path: "M 7 14.5 10 14.5 15 8.5 12 8.5 z" +
"M 1.5 17.5 3.5 17.5 3.5 14.5 1.5 14.5 z" +
"M 1.5 8.5 3.5 8.5 3.5 5.5 1.5 5.5 z" +
"M 3.5 20.5 3.5 3.5 5 0.5 6.5 3.5 6.5 20.5 z" +
"M 6.5 17.5 15.5 17.5 15.5 14.5 6.5 14.5 z" +
"M 6.5 8.5 15.5 8.5 15.5 5.5 6.5 5.5 z" +
"M 15.5 20.5 15.5 3.5 17 0.5 18.5 3.5 18.5 20.5 z" +
"M 18.5 17.5 20.5 17.5 20.5 14.5 18.5 14.5 z" +
"M 18.5 8.5 20.5 8.5 20.5 5.5 18.5 5.5 z" +
"M 10 17.5 11 21 12 17.5 z" ,
fillColor: '#FCDFFF',
fillOpacity: 0.7,
strokeColor: 'purple',
strokeWeight: 1,
anchor: new google.maps.Point(11,22),
xmid:11
}
] ,
// icon for arrow representing current waypoint
arrow:
{ path: "M 6 9 0 15 6 0 12 15 z",
fillColor: 'black',
fillOpacity: 1,
strokeColor: 'black',
strokeWeight: 0,
anchor: new google.maps.Point(6,6),
rotation: 0,
clickable: false
} ,
// icon for concentric circles representing draggable waypoints
concircle:
{ path: "M 6 0 A 6 6 0 1 0 6 12 A 6 6 0 1 0 6 0 M 6 3 " +
"A 3 3 0 1 0 6 9 A 3 3 0 1 0 6 3",
fillColor: 'black',
fillOpacity: 0,
strokeColor: 'black',
strokeWeight: 1,
strokeOpacity: 1,
anchor: new google.maps.Point(6,6),
clickable: false
} ,
// icon for geo points
geo:
{ path: "M 4.5 0 A 4.5 4.5 0 1 0 4.5 9 A 4.5 4.5 0 1 0 4.5 0 ",
fillColor: 'black',
fillOpacity: 0,
strokeColor: 'black',
strokeWeight: 1.5,
strokeOpacity: 1,
anchor: new google.maps.Point(4.5,4.5),
clickable: false
}
} ;
var oeuvres =
[ [ 'turn-slight-left' , 'Slight left' , L.slightl ] ,
[ 'turn-sharp-left' , 'Sharp left' , L.sharpl ] ,
[ 'uturn-left' , 'Left' , L.uturnl ] ,
[ 'turn-left' , 'Left' , L.turnl ] ,
[ 'ramp-left' , 'Left' , L.rampl ] ,
[ 'fork-left' , 'Slight left' , L.forkl ] ,
[ 'roundabout-left' , 'Left' , L.raboutl ] ,
[ 'turn-slight-right' , 'Slight right', L.slightr ] ,
[ 'turn-sharp-right' , 'Sharp right' , L.sharpr ] ,
[ 'uturn-right' , 'Right' , L.uturnr ] ,
[ 'turn-right' , 'Right' , L.turnr ] ,
[ 'ramp-right' , 'Right' , L.rampr ] ,
[ 'fork-right' , 'Slight right', L.forkr ] ,
[ 'roundabout-right' , 'Right' , L.raboutr ] ,
[ 'straight' , 'Straight' , L.straight ] ,
[ 'merge' , 'Generic' , L.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 ind = iconic.names.indexOf(this.label) ;
return icons.list[ind<0?0:ind] ;
} ;
pttype.prototype.setlabelmap = function(m,seller)
{ if(!this.label) m = null ;
this.selfunc = seller ;
this.map = m ;
if(!m&&!this.marker) return ;
this.marker.setMap(m) ;
if(!m&&this.clickhandler)
{ google.maps.event.removeListener(this.clickhandler) ;
this.clickhandler = this.selfunc = null ;
}
if(m&&!this.clickhandler&&seller)
this.clickhandler = this.marker.addListener('click',seller) ;
} ;
pttype.prototype.setphotomap = function(m,seller)
{ if(this.photo.length==0) m = null ;
this.selfunc = seller ;
this.map = m ;
if(!m&&!this.photomarker) return ;
if(this.photomarker) this.photomarker.map = m ;
if(this.photohandler&&!m)
{ google.maps.event.removeListener(this.photohandler) ;
this.photohandler = null ;
}
if(m&&seller&&!this.photohandler&&this.photomarker)
this.photohandler = this.photomarker.addListener('click',seller) ;
} ;
pttype.prototype.setgeomap = function(m) { if(this.point) this.point.map = m ; }
pttype.prototype.setlabel = function(l,c)
{ this.label = l ;
if(c) this.caption = c ; else this.caption = null ;
if(!l) { if(this.marker) this.setlabelmap(null,null) ; return ; }
if(!this.marker) this.marker = new google.maps.Marker
({ position:this.pos,map:null,icon:this.geticon(),title:c,zIndex:1 }) ;
else { this.marker.setIcon(this.geticon()) ; this.marker.setTitle(c) ; }
} ;
pttype.prototype.setpos = function(p)
{ if(p) this.pos = p ;
if(this.label) this.marker.setPosition(this.pos) ;
if(this.photo.length>0) this.photomarker.position = this.pos ;
if(this.point) this.point.position = p ;
} ;
pttype.prototype.setmap = function(m,seller)
{ this.setlabelmap(m,seller) ;
this.setphotomap(m,seller) ;
this.setgeomap(m)
} ;
pttype.prototype.changelabel = function(l)
{ this.label = l ; this.marker.setIcon(this.geticon()) ; } ;
pttype.prototype.clone = function()
{ var c = clone(this) ;
c.photo = clone(this.photo) ;
c.setlabel(this.label,this.caption) ; // sets marker
c.setpos(this.pos) ;
c.setmap(this.map,this.selfunc) ;
return c ;
}
/* -------------------------------------------------------------------------- */
// img = 0: haven't got imginfo yet
// img = 1: have got imginfo but not loaded the image
// img = 2: now loaded as thumbnail, need to update the photo marker
// img = 3: now loaded as icon, " " " " " "
function genmarker(pt,opt,el,selpoint)
{ 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() {genmarker(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 = domcreate('canvas',null,'style','width:'+w+'px;height:'+h+'px') ;
var ctx = c.getContext('2d') , mind , ofsx=0 , ofsy=0 , nw , nh , d ;
c.width = w * r ;
c.height = h * r ;
ctx.scale(r,r) ;
if(opt==2&&k)
{ img = el ;
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(el,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) ;
d.style.transform = "translate(-"+(w/2)+"px,"+(h/2)+"px)" ;
if(pt.photomarker) // opt >= 1
{ if(opt==0) console.log("photomarker with opt==0") ; // temp debugging
pt.photomarker.content = d ;
if(opt>1) pt.photomarker.title = phototitle(pt) ;
}
else // opt = 0
{ if(opt>0) console.log("no photomarker yet opt>0") ; // temp debugging
pt.photomarker = new google.maps.marker.AdvancedMarkerElement
({ map , content:d , position:pt.pos , title:phototitle(pt) }) ;
if(imginfo.status=='ready') genmarker(pt,1) ;
else imginfo.pending.push(pt) ;
pt.setphotomap(el,selpoint) ;
}
function sqr(h,r,ofs)
{ ctx.beginPath() ;
ctx.moveTo(ofs,ofs+r) ;
ctx.lineTo(ofs,ofs+h-r) ;
ctx.arc(ofs+r,ofs+h-r,r,-Math.PI,-Math.PI*1.5,1) ;
ctx.lineTo(ofs+h-r,ofs+h) ;
ctx.arc(ofs+h-r,ofs+h-r,r,-1.5*Math.PI,-Math.PI*2,1) ;
ctx.lineTo(ofs+h,ofs+r) ;
ctx.arc(ofs+h-r,ofs+r,r,0,-Math.PI*0.5,1) ;
ctx.lineTo(ofs+r,ofs) ;
ctx.arc(ofs+r,ofs+r,r,-Math.PI*0.5,-Math.PI,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,lamda)
{ var p = google.maps.geometry.spherical.interpolate(x,y,lamda) ;
return { lat:p.lat() , lng:p.lng() }
}
/* -------------------------------------------------------------------------- */
function getllpt(node)
{ return new pttype(getlatlong(node,'lat','lng'),null,null) ; }
/* -------------------------------------------------------------------------- */
function readgoogle(xmldoc)
{ var xmlcoords,i,j,k,r,node,end,poly,manoeuvre, props = new routetype() ;
var pts = props.pts ;
// loop over the steps
xmlcoords = xmldoc.getElementsByTagName('step') ;
for(i=0;i<xmlcoords.length;i++)
{ for(manoeuvre=poly=null,j=0;j<xmlcoords[i].childNodes.length;j++)
{ node = xmlcoords[i].childNodes[j] ;
if(i==0&&node.nodeName=='start_location') pts.push(getllpt(node)) ;
else if(node.nodeName=='end_location') end = getllpt(node) ;
else if(node.nodeName=='maneuver') manoeuvre = node.textContent ;
else if(node.nodeName=='polyline')
{ for(poly=null,k=0;(!poly)&&k<node.childNodes.length;k++)
if(node.childNodes[k].nodeName=='points')
{ poly = node.childNodes[k].textContent ;
poly = google.maps.geometry.encoding.decodePath(poly) ;
}
}
}
if(manoeuvre)
for(j=0;j<oeuvres.length;j++) if(oeuvres[j][0]==manoeuvre)
{ pts[pts.length-1].setlabel(oeuvres[j][1],oeuvres[j][2]) ; break ; }
if(poly) for(k=0;k<poly.length;k++)
pts.push(new pttype({lat:poly[k].lat(),lng:poly[k].lng()})) ;
pts.push(end) ;
}
pts = squash(pts) ;
xmlcoords = xmldoc.getElementsByTagName('summary') ;
if(xmlcoords.length)
props.title = normalise(xmlcoords[0].textContent.substring(0,15)) ;
props.desc = L.googledirs ;
xmlcoords = xmldoc.getElementsByTagName('start_address') ;
if(xmlcoords.length)
props.desc += ' from ' + normalise(xmlcoords[0].textContent) ;
xmlcoords = xmldoc.getElementsByTagName('end_address') ;
if(xmlcoords.length)
props.desc += ' to ' + normalise(xmlcoords[0].textContent) ;
xmlcoords = xmldoc.getElementsByTagName('copyrights') ;
if(xmlcoords.length)
props.desc += ' (' + normalise(xmlcoords[0].textContent) + ')' ;
return props ;
}
/* ------------------------------- 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: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)
{ 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 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) ;
r.pts[i].delta = route.pts[i].delta ;
}
for(i=0;i<optparms.length;i++)
optimmerge(r,optimise(i?r.pts:decimate(r.pts),optparms[i])) ;
}
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 genpixpage(tag,gallery)
{ return '<' + tag + ' href="' + gallery.href + '">' + gallery.title +
'</' + tag + '>' ;
}
/* -------------------------------------------------------------------------- */
function absuri(href)
{ var s = href.substring(0,7).toLowerCase() ;
return s=='http://' || s == 'file://' || (s=='https:/'&&href.charAt(7)=='/') ;
}
/* -------------------------------------------------------------------------- */
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) ;
div.appendChild(nobr) ;
}
else
{ if(title!=null) div.appendChild(b) ;
domadd(div,str) ;
if(flag==0) underline(div) ;
}
return div ;
}
/* -------------------------------------------------------------------------- */
function highdiv(segno,shifted)
{ var div,scroll,p,a,d,dwid,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.uri&&imginfo.status=='ready')
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) ;
scroll = document.createElement('div') ;
if(items.length==1) minw -= 4 ;
scroll.setAttribute('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) ; scroll.appendChild(image[i].img) ; }
div.appendChild(scroll) ;
}
else if(base.level<2&&imginfo&&imginfo.status&&imginfo.status!='ready')
{ if(imginfo.status=='waiting') s = inject(L.waitingfor,L.photolist) ;
else s = L.listnotfound ;
p = domcreate('div',genspan(s)) ;
div.appendChild(underline(p)) ;
}
div.appendChild(titlediv('title',seg.title,0,null)) ;
if(seg.stars!=null) div.appendChild(starsline(seg.stars,0)) ;
if(base.level<2)
{ d = titlediv('desc',seg.desc,null,0) ; // not linkable
if(items.length>0&&seg.desc&&seg.desc.length>=50)
{ if(minw+4<400) dwid = 400 ; else dwid = minw+4 ;
d.setAttribute('style','min-width:'+dwid+'px') ;
}
div.appendChild(d) ;
}
if(seg.stats) div.appendChild(prettystats(L.stats,seg.stats)) ;
if(seg.date) div.appendChild(textdiv(L.date,seg.date)) ;
if(base.level==2&&gallery) div.appendChild(genindexlink(gallery,L.view)) ;
if(segments[0].type=='tour')
{ var segs = segments[0].pts ;
if(segno>0) div.appendChild(genclickfn(swapsegfactory(segno-1),
inject2(L.swapwith,segs[segno-1].colour,
segs[segno-1].title+', '+L.preceding),'br')) ;
if(segno<segs.length-1) div.appendChild(genclickfn(swapsegfactory(segno),
inject2(L.swapwith,segs[segno+1].colour,
segs[segno+1].title+', '+L.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(L.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,'['+L.view1+']',1) ;
golink.setAttribute('onclick',"infowindow.close()") ;
if(seg.ttype!='html')
updlink = genclickfn(updfactory(segno),'['+caps(L.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) ; }
div.appendChild(d) ;
}
ptno = d = routeno = 0 ;
return { div: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)) ;
img.setAttribute('width',item.width) ;
img.setAttribute('height',item.height) ;
if(item.srcset)
{ item = serve(item.srcset)+ ' ' + item.scale + 'x' ;
img.setAttribute('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()
{ this.img.setAttribute('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) ;
scroll.appendChild(image[i].img) ;
}
}
}
/* -------------------------------------------------------------------------- */
function btnicon(name)
{ var i ;
if(!btnicon.list) btnicon.list = [] ;
for(i=0;i<btnicon.list.length&&btnicon.list[i][0]!=name;i++) ;
if(i==btnicon.list.length) btnicon.list.push(buttons(name)) ;
return { black:btnicon.list[i][1] , grey:btnicon.list[i][2] } ;
}
function newcanvas(ratio)
{ var c = domcreate('canvas',null,'width',24*ratio) ;
c.setAttribute('height',24*ratio) ;
c.setAttribute('style','width:24px;height:24px') ;
return c ;
}
function buttons(name)
{ var b=[name,null,null],i,j,colour,ctx,ratio=window.devicePixelRatio||1 ;
var theta,phi,psi,q=0.24,r,sign,twopi=2*Math.PI ;
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 ] ;
for(i=1;i<3;i++)
{ if(i==2) colour = '#bebebe' ; else colour = 'black' ;
b[i] = newcanvas(ratio) ;
ctx = b[i].getContext('2d') ;
ctx.strokeStyle = colour ;
ctx.fillStyle = colour ;
ctx.lineWidth = 0 ;
ctx.scale(ratio,ratio) ; // ratio is usually 1 or 2
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() ;
}
else // account name=0=>not logged in, 1=>logged in no alerts, 2=>+alerts
{ 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() ;
}
}
return b ;
}
/* -------------------------------------------------------------------------- */
function cloneCanvas(oldCanvas)
{ //create a new canvas
var newCanvas = newcanvas(window.devicePixelRatio||1) ;
var context = newCanvas.getContext('2d') ;
//apply the old canvas to the new one
context.drawImage(oldCanvas,0,0) ;
return newCanvas ;
}
function buttoncell(gif1,gif2,gif3)
{ var td=domcreate('td',null,'style','padding-bottom:4px') ;
var nobr=domcreate('nobr',null,'style','padding-right:6px') ;
var i,gifs=[gif1,gif2,gif3] ;
for(i=0;i<3&&(gifs[i]==0||gifs[i]);i++)
{ if(gifs[i]==-1) nobr.appendChild(gifs[2]) ;
else nobr.appendChild(cloneCanvas(btnicon(gifs[i]).black)) ;
domadd(nobr,' ') ;
if(i==1&&(gifs[0]==-1||gifs[1]==-1)) break ;
}
td.appendChild(nobr) ;
return td ;
}
function textcell(p1,p2)
{ var td=domcreate('td',null,'style','padding-bottom:4px') ;
td.appendChild(domcreate('nobr',p1)) ;
if(p2!=null&&p2!=undefined)
{ td.appendChild(document.createElement('br')) ;
td.appendChild(domcreate('nobr',p2)) ;
}
return td ;
}
function appendrow(td,p)
{ td.appendChild(domcreate('nobr',p)) ;
td.appendChild(document.createElement('br')) ;
}
function genlink(uri,legend,blank)
{ var a = domcreate('a',legend,'href',uri) ;
a.setAttribute('style','cursor:pointer;color:#0000bd;text-decoration:none') ;
if(blank=='close')
{ a.onclick = function() { infowindow.close() ; } ; return a ; }
if(!blank) return a ;
a.setAttribute('target','_blank') ;
var s = domcreate('span',a) ;
domadd(s,' ') ;
s.appendChild(newtabdiv()) ;
if(blank=='br') s.appendChild(document.createElement('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 = L.register[1] + '/' + L.register[0] ;
else v = L.account ;
acbtn.ui.greytitle = acbtn.blacktitle = v ;
v = btnicon(opt) ;
acbtn.blackimg = v.black ;
acbtn.greyimg = v.grey ;
redrawbtn(acbtn,-1) ;
}
else for(node=blurbdiv,i=0;i<2;i++,node=genpage) if(node.im)
{ v = cloneCanvas(btnicon(opt).black) ;
node.im.parentNode.replaceChild(v,node.im) ;
node.im = v ;
}
}
/* -------------------------------------------------------------------------- */
function logout(opt)
{ infowindow.close() ;
if(opt&&!window.confirm(L.delwarn)) return ;
var xhttp = new XMLHttpRequest() ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ if(xhttp.response.length==0) { alert(L.dberr) ; return ; }
if(xhttp.response=="**sqlerr: already") alert(L.alreadygone) ;
else if(xhttp.response!="Account deleted"&&xhttp.response!="Logged out")
{ alert(xhttp.response) ; return ; }
updateacbtns(0) ;
prefs = defprefs ;
}
}
xhttp.open("GET","resources/signup.php"+(opt?"?delact":"")) ;
xhttp.send() ;
}
/* -------------------------------------------------------------------------- */
function (opt)
{ var d = document.createElement('div') ;
function acfactory(ind)
{ return function()
{ infowindow.close() ;
infowindow.open(acfollowon(ind),opt=='account'?getbtnpos(9):0,opt) ;
} ;
} ;
function logoutfactory(ind) { return function() { logout(ind) ; } ; }
if(prefs.email)
{ d.appendChild(genspan(prefs.email,'hr')) ;
function doprefs()
{ infowindow.close() ; prefsprompt(L.prefs+':',null,null) ; }
function bugreport()
{ window.open('mailto:colin.champion@routemaster.app?subject='+L.prob) ; }
d.appendChild(genclickfn(doprefs,L.prefs,'br')) ;
d.appendChild(genclickfn(acfactory(3),L.register[3],'br')) ;
d.appendChild(genclickfn(logoutfactory(0),L.logout,'br')) ;
d.appendChild(genclickfn(logoutfactory(1),L.logout+' '+L.delact,'br')) ;
d.appendChild(genlink('mailto:colin.champion@routemaster.app?'+
'subject=routemaster problem',L.bugreport,'close')) ;
return d ;
}
d.appendChild(genclickfn(acfactory(0),L.register[0],'br')) ;
d.appendChild(genclickfn(acfactory(1),L.register[1],'br')) ;
d.appendChild(genclickfn(acfactory(2),L.register[2])) ;
return d ;
}
/* -------------------------------------------------------------------------- */
// opt is 0 for register, 1 for login,
// 2 for forgotten password, 3 for changing password
function acfollowon(opt)
{ var div=document.createElement('div'),d,i,p,formreq,optno ;
var label,inp,pl,valid,pinp,cinp=null,sinp,minp,kinp ;
var sty = 'width:' + Math.max(L.submit.length,L.cancel.length) + 'em;' ;
var styopts = [ 'color:silver' , // invalid
'background-color:#e2e2e2;cursor:pointer' , // valid
'cursor:pointer' ] ; // cancel
if(opt==3)
{ if(prefs.email) acfollowon.email = prefs.email ; else return null ; }
else if(opt<4) acfollowon.email = null ;
d = document.createElement('div') ;
d.appendChild(domcreate('b',opt>=4?L.otkey:L.register[opt])) ;
d.appendChild(document.createElement('br')) ;
if(opt==0||opt==2) domadd(d,L.emailprompt) ;
else if(opt==1) domadd(d,inject2(L.pwdprompt,L.email,L.pwd)) ;
else if(opt==3) domadd(d,L.aloneprompt) ;
else domadd(d,inject2(L.pwdprompt,L.otk,L.pwd)) ;
div.appendChild(d) ;
function greyit()
{ sinp.setAttribute('style',sty+styopts[valid==7?1:0]) ; }
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) greyit() ;
}
function pwdvalidate()
{ var ov = valid ;
if(pinp.value.length>=6&&(!cinp||pinp.value==cinp.value)) valid |= 4 ;
else valid &= ~4 ;
if(valid!=ov) greyit() ;
}
function otkvalidate()
{ var ov = valid , intval = parseInt(this.value) ;
if(intval>=100000000&&intval<1000000000) valid |= 2 ; else valid &= ~2 ;
if(valid!=ov) greyit() ;
}
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') ;
f.setAttribute('enctype','multipart/form-pts') ;
f.setAttribute('name','fileinfo') ;
div.appendChild(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 = L.email ; }
else if(i==1) { name = 'otkey' ; pl = L.otk ; }
else if(i==2) { name = 'password' ; pl = L.pwd ; }
else if(i==3) { name = 'confpwd' ; pl = L.confpwd ; }
inp = domcreate('input',null,'name',name) ;
if(i!=1) inp.setAttribute('type',i<2?'email':'password') ;
if(i>=2) inp.setAttribute('minlength',6) ;
inp.setAttribute('placeholder',pl) ;
inp.setAttribute('style','width:20em') ;
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 ; }
f.appendChild(domcreate('div',inp)) ;
}
// add the cancel/submit buttons
d = document.createElement('div') ;
for(i=0;i<2;i++)
{ inp = domcreate('input',null,'type',i?'submit':'button') ;
inp.setAttribute('name',i?'submit':'cancel') ;
inp.setAttribute('value',i?L.submit:caps(L.cancel)) ;
inp.setAttribute('style',sty+styopts[i==0?2:0]) ;
if(i==1) { sinp = inp ; inp.onclick = formresponse ; }
else inp.setAttribute('onclick','infowindow.close()') ;
d.appendChild(inp) ;
}
d.setAttribute('style','width:13em;margin:auto') ;
div.appendChild(d) ;
div.appendChild(doclink('account',L.accdoc)) ;
p = domcreate('p',L.cookies) ;
p.setAttribute('style','margin-top:2px;margin-bottom:2px;font-size:90%') ;
div.appendChild(p) ;
div.onkeydown = function(e)
{ if(e.keyCode==13||e.which==13) { e.preventDefault() ; formresponse() ; } }
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].split(' ') ,
pixhits: s[8].split(' ')
}
}
function phpresponse(event)
{ var s=event.target.response,i ;
if(formreq.status!=200) alert(L.formerr+formreq.status) ;
else if(s.substring(0,17)=="**sqlerr: already")
alert(inject(L.already,acfollowon.email)) ;
else if(s.substring(0,18)=="**sqlerr: wrongpwd") alert(L.incorrect) ;
else if(s.substring(0,18)=="**sqlerr: bademail") alert(L.bademail) ;
else if(s.substring(0,18)=="**sqlerr: badotkey") alert(L.badotkey) ;
else if(s.substring(0,20)=="**sqlerr: baduser")
alert(inject(L.baduser,acfollowon.email)) ;
else if(s.substring(0,17)=="**sqlerr: expired") alert(L.expired) ;
else if(s.substring(0,17)=="**sqlerr: toosoon")
alert(inject(L.toosoon,acfollowon.email)) ;
else if(s.substring(0,10)=="**sqlerr: ") alert(L.dberr+s.substring(9)) ;
else if(opt==1||opt==4||opt==5) loggedin(s) ;
else if(opt==0||opt==2)
{ infowindow.close() ;
infowindow.open(acfollowon(opt==0?4:5),getbtnpos(9),'account') ;
}
}
function formresponse(ev)
{ infowindow.close() ;
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 , div=document.createElement('div') ;
var url = document.location.href , i = url.indexOf('?') , bus = 'bus.png' ;
if(i>=0) url = url.substring(0,i) ;
if(url.substring(url.length-8)=='test.php') bus = 'greenbus.png' ;
var img = domcreate('img',null) ;
img.setAttribute('src',getbus(buscolour,"%23f7e9ce")) ;
img.setAttribute('width',16) ;
img.setAttribute('height',16) ;
img.setAttribute('style','margin-top:4px;position:relative;bottom:-2px') ;
div.appendChild(img) ;
div.appendChild(domcreate('b','\u00a0\u00a0'+L.rmgps)) ;
domadd(div,' '+L.rmdisplays) ;
img = cloneCanvas(btnicon(prefs.email?1:0).black) ;
d = domcreate('div',img) ;
d.setAttribute('style','position:absolute;top:0;right:2px;cursor:pointer') ;
d.onclick = function()
{ if(infowindow.type=='login') infowindow.close() ;
else infowindow.open(acmenu('login'),0,'login') ;
} ;
d.title = L.logindoc ;
body.appendChild(d) ;
blurbdiv.im = img ;
return div ;
}
/* -------------------------------------------------------------------------- */
function rmhelpdiv(mode)
{ var div=document.createElement('div'),d,t,tr,td,a,s,i,lim ;
function addbull(node,item)
{ node.appendChild(domcreate('li',L.funcs[item])) ; }
if(!mode) mode = 0 ;
t = domcreate('table',null,'style','font-size:100%') ;
t.setAttribute('cellpadding',0) ;
t.setAttribute('cellspacing',0) ;
if(mode==1)
{ tr = domcreate('tr',buttoncell('eye','settings')) ;
tr.appendChild(textcell(L.eyedoc,L.setsave)) ;
t.appendChild(tr) ;
tr = domcreate('tr',buttoncell('undo','redo')) ;
tr.appendChild(textcell(L.unre)) ;
t.appendChild(tr) ;
}
else
{ tr = domcreate('tr',buttoncell('settings','segment','waypoint')) ;
tr.appendChild(textcell(L.propsdoc[0],L.propsdoc[1])) ;
t.appendChild(tr) ;
tr = domcreate('tr',buttoncell('scissors','pen','camera')) ;
tr.appendChild(textcell(L.scisdoc[0],L.scisdoc[1])) ;
t.appendChild(tr) ;
tr = domcreate('tr',buttoncell('undo','redo','dl')) ;
tr.appendChild(textcell(L.unre+'/'+L.save)) ;
t.appendChild(tr) ;
}
/* ------------------------------------------------------------------------ */
rmhelpdiv.im = cloneCanvas(btnicon(prefs.email?1:0).black) ;
if(mode==1) tr = domcreate('tr',buttoncell('dl',-1,rmhelpdiv.im)) ;
else if(!mode) tr = domcreate('tr',buttoncell('dl',-1,rmhelpdiv.im)) ;
else
{ td = domcreate('td',rmhelpdiv.im,'style','padding-bottom:4px') ;
tr = domcreate('tr',buttoncell(-1,'eye',rmhelpdiv.im)) ;
}
if(mode<0) tr.appendChild(textcell(L.logindoc,L.eyedoc)) ;
else tr.appendChild(textcell(L.save,L.logindoc)) ;
t.appendChild(tr) ;
if(!mode)
{ tr = domcreate('tr',domcreate('td','Keyboard: ','valign','top')) ;
td = document.createElement('td') ;
d = document.createElement('div') ;
appendrow(d,'\u2190/\u2192 '+L.wpmove) ;
appendrow(d,inject2('[%0 \u2190]/[%1 \u2192] '+L.segmove,L.shift,L.shift)) ;
appendrow(d,'\u2193 '+L.centres) ;
appendrow(d,L.spacedoc,1) ;
d.setAttribute('style','margin-bottom:6px') ;
td.appendChild(d) ;
d = document.createElement('div') ;
appendrow(d,inject(L.tabdoc,L.dragfor)) ;
appendrow(d,inject(L.stabdoc,L.dragback)) ;
appendrow(d,L.deldoc) ;
appendrow(d,L.sdeldoc) ;
d.setAttribute('style','margin-bottom:6px') ;
td.appendChild(d) ;
tr.appendChild(td) ;
t.appendChild(tr) ;
tr = domcreate('tr',domcreate('td',L.mouse+' ','valign','top')) ;
td = document.createElement('td') ;
appendrow(td,L.clickdoc) ;
appendrow(td,L.sclickdoc) ;
tr.appendChild(td) ;
t.appendChild(tr) ;
}
div.appendChild(underline(domcreate('div',t))) ;
/* ------------------------------------------------------------------------ */
d = document.createElement('div') ;
if(mode<0)
{ s = 'https://www.routemaster.app/?track=https://www.masterlyinactivity.com';
a = genlink(s+'/routemaster/routes/capeverde/Caibros.gpx',L.extrack) ;
d.appendChild(a) ;
d.appendChild(document.createElement('br')) ;
a = genlink(s+'/routemaster/routes/capeverde/index.rte',L.exindex) ;
d.appendChild(a) ;
d.appendChild(document.createElement('br')) ;
a = domcreate('span',domcreate('i','Routemasher')) ;
domadd(a,' '+L.masher) ;
a = domcreate('a',a,'href','resources/masher.html') ;
a.setAttribute('style','cursor:pointer;color:#0000bd;text-decoration:none') ;
d.appendChild(a) ;
d.appendChild(document.createElement('br')) ;
}
a = genlink('https://www.routemaster.app/software/routemaster.html',
L.userman) ;
if(mode>=0) a.setAttribute('target','_blank') ;
a.setAttribute('rel','nofollow') ;
d.appendChild(a) ;
if(mode>=0) { domadd(d,' ') ; d.appendChild(newtabdiv()) ; }
else d.setAttribute('style',
'margin-bottom:2px;border-bottom:solid 1px silver;padding-bottom:2px') ;
div.appendChild(d) ;
if(mode<0)
{ d = domcreate('p',L.rmfns) ;
d.setAttribute('style','font-weight:bold;margin:3 0') ;
div.appendChild(d) ;
lim = Math.floor((L.funcs.length+1)/2) ;
d = document.createElement('div') ;
d.setAttribute('style',
'max-width:45%;padding-right:5%;display:inline-block') ;
for(i=0;i<lim;i++) addbull(d,i) ;
div.appendChild(d) ;
d = document.createElement('div') ;
d.setAttribute('style',
'max-width:45%;padding-left:5%;display:inline-block') ;
for(;i<L.funcs.length;i++) addbull(d,i) ;
div.appendChild(d) ;
}
/* ------------------------------------------------------------------------ */
d = domcreate('div',L.rmtc[0]) ;
d.appendChild(genlink('https://maps.google.com/help/terms_maps.html',
L.rmtc[1],1)) ;
d.setAttribute('style','font-size:90%;color:gray;margin-top:2px;'+
'border-top:solid 1px silver;padding-top:2px') ;
div.appendChild(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 = L.photos ;
tag = inject(L.view,tag) ;
return genlink(gallery.href,tag,sty) ;
}
function (dragopt)
{ var d = document.createElement('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)
{ return function() { arfunc(segments[0].pts,ind) ; } ; } ;
// dragging
if(dragopt) { d.appendChild(genspan(L.hitspace,'br')) ; return d ; }
if(segments[0].level)
{ // functions to add:
// (i) sort routes by date and time (in a tour)
// (ii) update all routes
// (iii) add/modify a description
d.appendChild(titlediv('title',segments[0].title,1,0)) ;
if(segments[0].level!=2&&segments[0].index)
d.appendChild(genindexlink(segments[0].index,L.viewidx)) ;
if(segments[0].level!=2&&gallery)
d.appendChild(genindexlink(gallery,L.view)) ;
if(segments[0].level==1) legend = L.addroute ; else legend = L.addindex ;
d.appendChild(genclickfn(addloadfactory("add"),legend,'br')) ;
if(segments[0].level==1)
{ d.appendChild(genclickfn(dlfactory(2),L.savemeta,'br')) ;
if(showarrows)
d.appendChild(genclickfn(arfactory(0),L.hidear,'br')) ;
else d.appendChild(genclickfn(arfactory(1),L.showar,'br')) ;
d.appendChild(genclickfn(listroutes,L.listroutes,'hr')) ;
}
d.appendChild(genclickfn(help,L.help,'br')) ;
return d ;
}
var s,i,unsaved,props,spacing,tdiv,p,rp=routeprops,loadno,pr ;
dd = document.createElement('div') ;
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.appendChild(titlediv('title',rp.title,1)) ;
// stars
starsdiv = starsline(rp.stars,1) ;
starsdiv.setAttribute('style','color:#0000bd') ;
dd.appendChild(starsdiv) ;
if(rp.srcid!=null&&rp.srcid!=rp.title)
dd.appendChild(titlediv('source',rp.srcid)) ;
// description
dd.appendChild(titlediv('desc',rp.desc,rp.origin)) ;
underline(dd) ;
d.appendChild(dd) ;
/* ------------------------------------------------------------------------ */
if(routeprops.info||routeprops.index||gallery) dd = domcreate('div') ;
// index
if(routeprops.index)
dd.appendChild(genindexlink(routeprops.index,L.viewidx)) ;
// info page
if(routeprops.info) dd.appendChild(genindexlink(routeprops.info,L.viewinfo)) ;
// gallery
if(gallery) dd.appendChild(genpixlink(gallery,'br')) ;
if(routeprops.info||routeprops.index||gallery)
{ underline(dd) ; d.appendChild(dd) ; }
/* ------------------------------------------------------------------------ */
// last added route
if(loadno>0)
{ s = '\u00a0\u00a0\u00a0' + L.lastadded + (pr?pr:L.untitled) ;
d.appendChild(genspan(s,'br')) ;
s = '\u00a0\u00a0\u00a0' ;
}
else s = '' ;
// are there any missing altitudes?
if(p.nnull&&!getalts.pending)
{ d.appendChild(genspan(pluralise(L.noalts,p.nnull)+' [')) ;
if(!getalts.altthresh||getalts.altthresh>1)
d.appendChild(genclickfn(doalts,L.findalts,']br')) ;
else d.appendChild(genspan(L.waitalts,']br','text-style:italic')) ;
}
// are points timed and are they in sequence?
if(p.outoforder==0)
{ if(p.ntimes==0) d.appendChild(genspan(L.notimes,'br')) ;
else if(p.ntimes<p.npts)
d.appendChild(genspan(pluralise(L.untimed,p.npts-p.ntimes),'br')) ;
}
else d.appendChild(genspan(L.badtimes,'br')) ;
// labels and photos
if(p.nlabels>0)
{ d.appendChild
(genspan(pluralise(L.numlabels,p.nlabels)+' ['))
d.appendChild(genclickfn(unlabel,L.remove,']br')) ;
}
if(p.pix.length>0)
d.appendChild(genspan(pluralise(L.numphotos,p.pix.length),'br')) ;
/* ------------------------------------------------------------------------ */
// unsaved changes
unsaved = unsavedchanges.length ;
if(unsaved>0)
d.appendChild(genspan(pluralise(L.unsavedchanges,unsaved),'br')) ;
// number of segments - option to combine
if(segments.length>1)
{ d.appendChild(genspan(pluralise(L.numsegments,segments.length)+' [')) ;
d.appendChild(genclickfn(combine,L.combine,']br')) ;
}
// max waypoint separation - option to interpolate
d.appendChild(genspan(inject(L.maxsep,p.maxsep.toFixed(0)),'br')) ;
if(prefs&&!prefs.maxsep) maxsep = null ; else maxsep = 100 ;
if(maxsep&&p.maxsep>=maxsep)
{ d.appendChild(genspan(L.sepwarn,'br','font-style:italic')) ;
d.appendChild(genspan('\u00a0\u00a0\u00a0[')) ;
d.appendChild(genclickfn(extrapts,caps(L.interpextra),']br')) ;
}
// operations
var dd=document.createElement('div') ;
dd.appendChild(genclickfn(addloadfactory("load"),L.loadnewroute,'br')) ;
dd.appendChild(genclickfn(addloadfactory("add"),caps(L.loadnewseg),'br')) ;
dd.appendChild(genclickfn(addloadfactory("geotag"),L.geotag,'br')) ;
dd.appendChild(genclickfn(dlfactory(1),L.saveidx,'br')) ;
// help menu
dd.appendChild(genclickfn(help,L.help,'br')) ;
dd.setAttribute('style','display:block') ;
dd.setAttribute('style',
'margin-top:2px;border-top:solid 1px silver;padding-top:2px;') ;
d.appendChild(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 d = document.createElement('div') ;
var props=segs[segno],span,prose,div,p,sty ;
d.appendChild(genspan(inject2(L.segment,segs[segno].colour+segno,
segs.length,segs[segno].pts.length),'br')) ;
if(props.title&&props.title!=routeprops.title)
d.appendChild(titlediv('title',props.title,1,segno)) ;
if(props.stars&&props.desc&&props.desc!=routeprops.desc)
d.appendChild(starsline(props.stars,0)) ;
if(props.srcid&&props.srcid!=props.title&&props.srcid!=routeprops.srcid)
d.appendChild(textdiv(L.source,props.srcid)) ;
if(props.desc&&props.desc!=routeprops.desc)
d.appendChild(titlediv('desc',props.desc,null,segno)) ; // no links
if(props.stats) d.appendChild(prettystats(L.stats,props.stats)) ;
// number of track points and optimisation
function optimfactory(segno) { return function() { optimwork(segno) ; } ; } ;
div = document.createElement('div') ;
if(props.optim&&props.optim.already)
div.appendChild(genspan(L.prevopt,'br')) ;
else if(props.optim) d.appendChild(genspan(L.optimised,'br')) ;
else div.appendChild(genclickfn(optimfactory(segno),L.optimise,'br')) ;
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.setAttribute('style',sty) ;
d.appendChild(div) ;
div = domcreate('div',d) ;
p = routestats([segs[segno]]) ;
d = genldiv(p) ;
d.setAttribute('style','display:block') ;
div.appendChild(d) ;
d = document.createElement('div') ;
if(segs.length>1)
d.appendChild(genclickfn(discard,caps(L.deletesegment),'br')) ;
else d.appendChild(genspan(caps(L.deletesegment),'br','color:silver')) ;
if(segs[segno].pts.length)
{ d.appendChild(genclickfn(revseg,caps(L.revsegment),'br')) ;
d.appendChild(genclickfn(dupseg,caps(L.dupsegment),'br')) ;
}
if(segno>0)
{ d.appendChild(genclickfn(swapsegfactory(segno-1),
inject2(L.swapwith,segs[segno-1].colour,L.preceding),'br')) ;
if(segno<segs.length-1) prose = 'br' ; else prose = 'hr' ;
d.appendChild(genclickfn(combineb,
inject2(L.combinewith,segs[segno-1].colour,L.preceding),prose)) ;
}
if(segno<segs.length-1)
{ d.appendChild(genclickfn(swapsegfactory(segno),
inject2(L.swapwith,segs[segno+1].colour,L.following),'br')) ;
d.appendChild(genclickfn(combinef,
inject2(L.combinewith,segs[segno+1].colour,L.following),'hr')) ;
}
if(p.ntimes) d.appendChild(genclickfn(deltimesfactory(segno,0),
caps(L.deltimes),'br')) ;
else d.appendChild(genspan(L.nodeltimes,'br','color:silver')) ;
if(getalts.reqlist&&getalts.reqlist.length)
d.appendChild(genspan('['+L.waitalts+']','br','color:grey')) ;
else if(p.nnull==segs[segno].pts.length) d.appendChild(
genclickfn(googlecal,L.askgoogle,'br')) ;
else d.appendChild(genclickfn(altinfo,L.adjalts,'br')) ;
d.setAttribute('style','display:block;clear:left;margin-top:2px;'+
'border-top:solid 1px silver;padding-top:2px') ;
div.appendChild(d) ;
return div ;
}
/* -------------------------------------------------------------------------- */
function altinfodiv()
{ var d = domcreate('div',genclickfn(googlereg,L.regress,'br')) ;
d.appendChild(genclickfn(googleadd,L.googleadd,'br')) ;
d.appendChild(genspan('['+L.vsgoogle+']','br',
'font-size:80%;margin-left:8px;color:grey')) ;
d.appendChild(genclickfn(googlecal,L.repalts,'br')) ;
d.appendChild(genclickfn(manualcal,L.offalts,'br')) ;
d.appendChild
(genclickfn(deltimesfactory(selected.segno,1),caps(L.delalts),'br')) ;
d.appendChild(doclink('adjusting',L.docalts)) ;
return d ;
}
function doclink(target,string)
{ var dd = document.createElement('div'),a ;
a = genlink('https://www.routemaster.app/software/routemaster.html#'+
target,string) ;
a.setAttribute('target','_blank') ;
dd.appendChild(a) ;
domadd(dd,' ') ;
dd.appendChild(newtabdiv()) ;
dd.setAttribute('style','display:block') ;
dd.setAttribute('style',
'margin-top:2px;border-top:solid 1px silver;padding-top:2px;') ;
return dd ;
}
/* -------------------------------------------------------------------------- */
function walktodiv(pt)
{ var s,d=document.createElement('div'),dd,ind,k,imgname,imgdiv ;
if(pt.label)
{ dd = document.createElement('div') ;
s = iconic.names.indexOf(pt.label) ;
if(s<0) s = L.labels[0] ; else s = L.labels[s] ;
if(pt.marker.title) s += ': ' + pt.marker.title ;
domadd(dd,s+' [') ;
dd.appendChild(genclickfn(labelprompt,L.edit,']br')) ;
if(pt.photo.length>0) underline(dd) ;
d.appendChild(dd) ;
}
for(ind=0;ind<pt.photo.length;ind++)
{ dd = document.createElement('div') ;
if(imginfo.status!='ready') k = null ; else k = findimg(pt.photo[ind]) ;
function displayfactory(ind) { return function() { display(ind) ; } ; } ;
if(k)
{ imgdiv = document.createElement('div') ;
s = genimage(imginfo.sect[k[0]].list[k[1]],-1) ;
doim(s) ;
imgdiv.appendChild(s.img) ;
if(imginfo.sect[k[0]].list[k[1]].extn=='.mp4')
imgdiv.appendChild(mp4icon()) ;
imgdiv.setAttribute('style','position:relative;cursor:pointer;width:'+
s.shape[0]+';height:'+s.shape[1]) ;
imgdiv.onclick = displayfactory(ind) ;
dd.appendChild(imgdiv) ;
dd.appendChild(document.createElement('br')) ;
}
else
{ s = L.photo + ' ' + pt.photo[ind] + ' (' ;
if(!imginfo.status) s += L.nolist ;
else if(imginfo.status=='ready')
s += inject(L.notpres,shortenname(imginfo.uri)) ;
else if(imginfo.status=='waiting') s += inject(L.notavail,imgname) ;
else s += 'imginfo.status = ' + imginfo.status ;
domadd(dd,s+ ') ') ;
}
domadd(dd,'[') ;
function pheditfactory(k) { return function() { photoedit(k) ; } ; } ;
dd.appendChild(genclickfn(pheditfactory(ind),L.edit)) ;
domadd(dd,']') ;
if(k)
{ domadd(dd,' : [') ;
function phinfofactory(k) { return function() { phinfo(k) ; } ; } ;
dd.appendChild(genclickfn(phinfofactory(k),L.info)) ;
domadd(dd,']') ;
}
d.appendChild(underline(dd)) ;
}
if(pt.photo.length>0)
{ dd = domcreate('div','[') ;
dd.appendChild(genclickfn(photoprompt,caps(L.addphoto),']br')) ;
d.appendChild(dd) ;
}
return d ;
}
/* -------------------------------------------------------------------------- */
function ()
{ var ncol,nrow,seg,len,bkg,s,sno,r,sortl,gotroutes,ind,route,maxasc ;
var t=document.createElement('table'),tr,td,i,j,k,l,item,stats ;
var 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.setAttribute('cellspacing',0) ;
t.setAttribute('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 = document.createElement('tr') ;
for(j=0;j<ncol&&(k=j+i*ncol)<n;j++)
{ ind = sortl[k].ind ;
route = base.pts[ind] ;
if(j)
{ td = domcreate('td','\u00a0\u00a0\u00a0\u00a0') ;
td.setAttribute('style','width:18px') ;
tr.appendChild(td) ;
}
td = document.createElement('td') ;
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 ;
td.setAttribute('style',csty+'width:14px;background'+bkg) ;
domadd(td,'\u00a0\u00a0\u00a0\u00a0') ;
tr.appendChild(td) ;
td = document.createElement('td') ;
function highfactory(ind)
{ return function()
{ infowindow.close() ; highlight(segments[0].pts,0,ind) ; } ;
} ;
td.appendChild(genclickfn(highfactory(ind),route.title,0,
csty+'white-space:nowrap')) ;
tr.appendChild(td) ;
function addcell(s)
{ var td = domcreate('td',null,'style',csty+'text-align:right') ;
td.appendChild(s) ;
tr.appendChild(td) ;
}
// stars
for(s='',sno=0;sno<route.stars;sno++) s += ' \u2605' ;
addcell(document.createTextNode(s)) ;
// number of routes
if(gotroutes) for(l=0;l<2;l++) // loop over 2 cols
{ if(route.level>0) s = l?L.routes:route.pts.length ; else s = ' ' ;
addcell(document.createTextNode(s)) ;
}
// distance, ascent
if(route.stats[1]>thresh) addcell(prettynum(route.stats[1]/1000,0,'km')) ;
else addcell(prettynum(route.stats[1],0,'m')) ;
addcell(prettynum(route.stats[2],maxasc>9999,'m')) ;
// date
if(segments[0].type=='tour'&&route.date)
addcell(document.createTextNode(route.date)) ;
}
t.appendChild(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 ;
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 = document.createElement('div') ;
var ratio = window.devicePixelRatio||1 , 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('\u00b0 %, ',L.nsew.charAt(0)) ;
else
s = (-lat).toFixed(5+precision) + inject('\u00b0 %, ',L.nsew.charAt(1)) ;
if(lng>=0)
s += lng.toFixed(5+precision) + inject('\u00b0 %. ',L.nsew.charAt(2)) ;
else
s += (-lng).toFixed(5+precision) + inject('\u00b0 %. ',L.nsew.charAt(3)) ;
d.appendChild(genspan(s,'br')) ;
x = new LatLon(lat,lng) ;
if(lat>49.9&&lat<62&&lng>-12&&lng<2.5&&lat-1.5*lng<75)
{ s = 10 + 2 * Math.floor((precision+1)/2.0) ;
s = L.osref + ': ' + OsGridRef.latLonToOsGrid(x).toString(s) ;
}
else s = L.utm + ': ' + x.toUtm(precision) ;
d.appendChild(genspan(s,'br')) ;
// altitude
alt = pt.h ;
function altfactory(parm)
{ return function() { setalt(parm,prefs.precision) ; } ; } ;
if(alt!=null)
{ d.appendChild(genspan(caps(L.alti)+': '+alt.toFixed(precision) + 'm [')) ;
d.appendChild(genclickfn(altfactory(1),L.edit,']br')) ;
}
else d.appendChild(genclickfn(altfactory(0),caps(L.wpalt),'br')) ;
// date and time
time = pt.t ;
if(time!=null&&time.getFullYear()>1980)
{ d.appendChild(genspan(L.date+': '+time.toDateString(),'br')) ;
d.appendChild(genspan(L.time+': '+time.toTimeString(),'br')) ;
}
// marker
if(pt.label)
{ s = pt.label ;
if(pt.marker.title) s += ': ' + pt.marker.title ;
d.appendChild(genspan(s+' [')) ;
d.appendChild(genclickfn(labelprompt,L.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 = domcreate('canvas',null,'width',12*ratio) ;
c.setAttribute('height',12*ratio) ;
c.setAttribute('style','width:12px;height:12px') ;
ctx = c.getContext("2d") ;
ctx.fillStyle = segments[s0].colour ;
ctx.strokeWidth = 0 ;
ctx.scale(ratio,ratio) ;
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)+L.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,domcreate('span',', ↑','style','padding-right:2px')) ;
domadd(s,prettynum(0.5+tasc,tasc>10000)) ;
domadd(s,domcreate('span','m','style','padding-left:2px')) ;
}
domadd(d,s) ;
}
// segment + point number
if(segments.length>1) s = inject(L.segpt,s0) + ' ' + inject(L.point,s1) ;
else s = caps(inject(L.point,s1)) ;
d.appendChild(genspan(s,'hr','font-size:80%')) ;
}
else if(segments.length>1)
{ s = inject(L.segpt,s0) ; d.appendChild(genspan(s,'hr','font-size:80%')) ; }
// edit functions (delete, drag, transfer... )
if(segments[s0].pts.length+segments[s0].geo.length>1)
d.appendChild(genclickfn(wpfactory(null),caps(L.wpdel),'br')) ;
else d.appendChild(genspan(caps(L.wpdel),'br','color:silver')) ;
d.appendChild(genclickfn(draggit,L.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)
{ d.appendChild(genclickfn(wpfactory(1),inject(L.dragaway,L.dragfor),'br')) ;
d.appendChild(genclickfn(wpfactory(-1),inject(L.dragaway,L.dragback),'br'));
}
d.appendChild(genclickfn(wpfactory(0),caps(s2?L.attach:L.detach),'br')) ;
if(url.substring(url.length-8)=='test.php')
d.appendChild(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=document.createElement('div'),i,maxl,len,origin,f ;
if(segno==undefined) segno = null ;
function titlefactory(p1,p2) { return function() { retitle(p1,p2) ; } ; } ;
if(!val)
{ if(!editable) return d ;
domadd(d,'[') ;
if(opt=='desc') s = L.adddesc ; else if(opt=='title') s = L.addtitle ;
d.appendChild(genclickfn(titlefactory(opt,segno),s)) ;
domadd(d,']') ;
return d ;
}
if(opt=='title') d.appendChild(domcreate('b',L.title+': '+val)) ;
else if(opt=='desc')
{ if(segno==null) origin = routeprops.origin ;
else origin = segments[segno].origin ;
if(editable) f = titlefactory('desc',segno) ; else f = null ;
maxl = parsedesc(d,'<b>'+L.desc+':</b> '+val,f,origin) ;
}
else
{ if(opt=='source') s = L.source ;
else alert(inject2(L.illegalopt,opt,'titlediv')) ;
d.appendChild(domcreate('b',s+': ')) ;
domadd(d,val) ;
}
if(opt!='desc'||maxl<40) d.setAttribute('style','white-space:nowrap') ;
if(editable&&opt!='desc')
{ s = domcreate('span','[','style','margin-left:1ex') ;
s.appendChild(genclickfn(titlefactory(opt,segno),L.edit)) ;
domadd(s,']') ;
d.appendChild(s) ;
}
return d ;
}
/* -------------------------------------------------------------------------- */
function textpromptcleanup() { partialcleanup() ; keyhandler = keystroke ; }
function speaklang(lang)
{ if(prefs.lang!=lang)
{ var script = document.createElement('script') ;
script.src = 'resources/routemaster.' + lang + '.js' ;
document.head.appendChild(script) ;
}
prefs.lang = lang ;
}
function promptbtn(type,label,cancelfn)
{ var button = domcreate('input',null,'type','button') , sty = 'white' ;
button.setAttribute('value',label) ;
if(type=='submit') sty = '#e2e2e2' ;
if(type!='cancel') sty += ';margin-left:4px' ;
button.setAttribute('style','background-color:'+sty) ;
button.onclick = cancelfn ;
return button ;
}
/* -------------------------------------------------------------------------- */
function textprompt(msg,oldval,action,prompttype)
{ var w,h,l,t,defw,defh,i,box,s,img,div,bigdiv,ip,span,style,c,d,i ;
var bdiv=(mapparent?mapparent:body) , tab,tr,td,lsty,returnkey,cancelfn ;
keyhandler = null ;
if(prompttype=='desc') { defw = 500 ; defh = 240 ; }
else if(prompttype=='loadwait') { defw = 320 ; defh = 20 ; }
else if(prompttype=='optim') { defw = 100 ; defh = 60 ; }
bigdiv = document.createElement('div') ;
bigdiv.setAttribute('style','position:absolute;width:100%;height:100%;'+
'top:0;left:0;background:black;opacity:0.5') ;
bdiv.appendChild(bigdiv) ;
w = Math.min(defw,window.innerWidth) ;
h = Math.min(defh,window.innerHeight) ;
l = Math.floor((window.innerWidth-w)/2) ;
t = Math.floor((window.innerHeight-h)/2) ;
div = domcreate('div',msg) ;
// margin:auto doesn't have any effect unless the width/height are specified,
// and it's best not to specify them because they depend on text spacing. yuk!
div.setAttribute('style','position:absolute;background:white;left:'+l+'px;'+
'top:'+t+'px;border:2px black solid;padding:4px;font-family:helvetica;'+
'margin:auto') ;
div.setAttribute('id','routemaster:div') ;
bigdiv.setAttribute('id','routemaster:bigdiv') ;
if(prompttype=='loadwait')
{ bdiv.appendChild(div) ; infowindow.closer = textpromptcleanup ; return ; }
div.appendChild(document.createElement('br')) ;
if(prompttype=='optim')
{ box = domcreate('input',null,'type','checkbox') ;
box.setAttribute('id','savepref') ;
box.setAttribute('value','yes') ;
ip = domcreate('div',box) ;
ip.appendChild(domcreate('label',L.savepref,'for','savepref')) ;
}
else
{ style = 'font-family:helvetica;width:'+(w-8)+'px;height:'+(h-36)+
'px;font-size:110%' ;
ip = domcreate('textarea',oldval?oldval:null,'style',style) ;
}
// handle submit/return key
function optimfactory(prompttype,box,action,oldval)
{ return function()
{ textpromptcleanup() ;
if(box.checked)
{ prefs.detail = oldval[0] ;
prefs.maxsep = oldval[1] ;
var xhttp = new XMLHttpRequest() ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ if(xhttp.response.length==0) alert(L.dberr) ;
else if(xhttp.response.substring(0,9)=="**sqlerr:")
alert(xhttp.response) ;
}
}
xhttp.open("GET","resources/signup.php?opts="+
prefs.detail+'&'+prefs.maxsep) ;
xhttp.send() ;
}
action(box.checked) ;
}
}
function submitfactory(action,ip)
{ return function() { textpromptcleanup() ; action(ip.value) ; } }
if(prompttype=='optim')
returnkey = optimfactory(prompttype,box,action,oldval) ;
else returnkey = submitfactory(action,ip) ;
div.appendChild(ip) ;
if(prompttype!='desc') ip.onkeydown = function(e)
{ if(e.keyCode==13||e.which==13)
{ e.preventDefault() ; returnkey() ; }
}
// cancel
function cancelfactory(prompttype,action)
{ return function()
{ textpromptcleanup() ; if(prompttype=='optim') action('cancel') ; }
}
cancelfn = cancelfactory(prompttype,action) ;
span = domcreate('span',promptbtn('cancel',caps(L.cancel),cancelfn)) ;
// submit
span.appendChild(promptbtn('submit',L.ok,returnkey)) ;
span.setAttribute('style','margin:auto;display:table') ;
div.appendChild(span) ;
// doc link
if(prompttype=='desc') div.appendChild(doclink('desc',L.descdoc)) ;
bdiv.appendChild(div) ;
}
/* -------------------------------------------------------------------------- */
function prefsprompt(msg,oldval,action)
{ var w,h,l,t,defw,defh,i,box,s,img,div,bigdiv,ip,span,style,c,d,i ;
var ctx , cty , bdiv=(mapparent?mapparent:body) , detail,auto,precdiv ;
var canvasdiv , lang , langs=['en','fr'] , flagdivs = [0,0] , returnkey ;
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] ;
keyhandler = null ;
defw = 360 ;
defh = 160 ;
bigdiv = document.createElement('div') ;
bigdiv.setAttribute('style','position:absolute;width:100%;height:100%;'+
'top:0;left:0;background:black;opacity:0.5') ;
bdiv.appendChild(bigdiv) ;
w = Math.min(defw,window.innerWidth) ;
h = Math.min(defh,window.innerHeight) ;
l = Math.floor((window.innerWidth-w)/2) ;
t = Math.floor((window.innerHeight-h)/2) ;
div = domcreate('div',msg) ;
// margin:auto doesn't have any effect unless the width/height are specified,
// and it's best not to specify them because they depend on text spacing. yuk!
div.setAttribute('style','position:absolute;background:white;left:'+l+'px;'+
'top:'+t+'px;border:2px black solid;padding:4px;font-family:helvetica;'+
'margin:auto') ;
div.setAttribute('id','routemaster:div') ;
bigdiv.setAttribute('id','routemaster:bigdiv') ;
div.appendChild(document.createElement('br')) ;
ip = domcreate('div') ;
// email address
email = domcreate('input',null,'type','email') ;
email.setAttribute('value',prefs.email) ;
email.setAttribute('style','width:350px') ;
ip.appendChild(domcreate('div',email)) ;
lsty = 'margin:auto;border-bottom:1px solid silver;padding-bottom:2px;' +
'margin-bottom:4px' ;
tab = domcreate('table',null,'style',lsty) ;
tab.setAttribute('cellpadding',0) ;
tab.setAttribute('cellspacing',0) ;
lsty = 'text-align:right;padding:2px 12px 2px 0' ;
// 'Language'
tr = domcreate('tr',domcreate('td',L.language,'style',lsty)) ;
// flags
if(prefs&&prefs.lang) lang = langs.indexOf(prefs.lang) ;
else lang = 0 ;
if(lang<0) lang = 0 ;
canvasdiv = domcreate('div',null,'style','position:relative') ;
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.appendChild(c) ;
// 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() ;
}
canvasdiv.appendChild(c) ;
function cclick(langno)
{ if(langno==lang) return ;
flagdivs[lang].style.borderColor = 'white' ;
lang = langno ;
flagdivs[lang].style.borderColor = 'grey' ;
}
tr.appendChild(domcreate('td',canvasdiv)) ;
tab.appendChild(tr) ;
// optimisation options: detail
td = domcreate('td',null,'style',lsty) ;
td.appendChild(domcreate('label',L.detail+':','for','detail')) ;
tr = domcreate('tr',td) ;
detail = domcreate('input',null,'type','number') ;
detail.setAttribute('min','1') ;
detail.setAttribute('max','100') ;
detail.setAttribute('id','detail') ;
detail.setAttribute('value',
(prefs&&prefs.detail)?prefs.detail:defprefs.detail) ;
detail.setAttribute('style','width:52px') ;
tr.appendChild(domcreate('td',detail)) ;
tab.appendChild(tr) ;
// auto optimise?
td = domcreate('td',null,'style',lsty) ;
td.appendChild(domcreate('label',L.optonload,'for','auto')) ;
tr = domcreate('tr',td) ;
auto = domcreate('input',null,'type','checkbox') ;
auto.setAttribute('id','auto') ;
if(!prefs||prefs.optim) auto.setAttribute('checked',true) ;
tr.appendChild(domcreate('td',auto)) ;
tab.appendChild(tr) ;
// limit separation?
td = domcreate('td',null,'style',lsty) ;
td.appendChild(domcreate('label',L.limsep,'for','limsep')) ;
tr = domcreate('tr',td) ;
box = domcreate('input',null,'type','checkbox') ;
box.setAttribute('id','limsep') ;
if(!prefs||prefs.maxsep) box.setAttribute('checked',true) ;
tr.appendChild(domcreate('td',box)) ;
tab.appendChild(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',L.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]) ;
s.setAttribute('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') ;
precdiv.appendChild(s) ;
}
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' ;
} ;
}
pclick = pclickfactory(precdiv,unitdivs) ;
precdiv.onclick = pclick ;
tr.appendChild(domcreate('td',precdiv)) ;
tab.appendChild(tr) ;
ip.appendChild(tab) ;
// handle submit/return key
function submitfactory(email,lingo,detail,auto,box)
{ return function()
{ textpromptcleanup() ;
var xhttp = new XMLHttpRequest() ;
xhttp.onreadystatechange = function()
{ if(xhttp.readyState==4)
{ if(xhttp.response.length==0) alert(L.dberr) ;
else if(xhttp.response=="**sqlerr: bademail") alert(L.bademail) ;
else if(xhttp.response=="**sqlerr: already")
alert(inject(L.already,email.value)) ;
else if(xhttp.response.substring(0,9)=="**sqlerr:")
alert(xhttp.response) ;
else prefs.email = email.value ;
}
}
speaklang(lingo) ;
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() ;
}
}
returnkey = submitfactory(email,langs[lang],detail,auto,box) ;
div.appendChild(ip) ;
ip.onkeydown = function(e)
{ if(e.keyCode==13||e.which==13) { e.preventDefault() ; returnkey() ; } }
// cancel
span = domcreate('span',promptbtn('cancel',caps(L.cancel),textpromptcleanup)) ;
span.setAttribute('style','margin:auto;display:table') ;
// default
function defaultfactory(unitdivs,auto,box,detail,pclick)
{ return function()
{ auto.checked = defprefs.optim ;
box.checked = defprefs.maxsep ;
pclick() ;
detail.value = defprefs.detail ;
}
}
deffn = defaultfactory(unitdivs,auto,box,detail,pclick) ;
span.appendChild(promptbtn('defaults',L.defaults,deffn)) ;
// submit
span.appendChild(promptbtn('submit',L.ok,returnkey)) ;
div.appendChild(span) ;
bdiv.appendChild(div) ;
}
/* -------------------------------------------------------------------------- */
function (msg,oldval,action,oldlabno)
{ var w,h,l,t,defw,defh,labelno,temp,i,missing=(oldlabno<0) ;
var div,bigdiv,ip,span,button,style,svg,labicon,lsvg,labname ;
var delopt=null , bdiv=(mapparent?mapparent:body) ;
var nlabel = iconic.names.length , btnlist = new Array(nlabel) ;
var svgns = "http://www.w3.org/2000/svg" ;
keyhandler = null ;
if(missing||!oldlabno) oldlabno = 0 ;
labelmenu.thislabno = oldlabno ;
function drawicon(iconno,scale,shift)
{ var icon = document.createElementNS(svgns,"path"),trans,x,y ;
if(!scale) scale = 1 ;
if(!shift) shift = 0 ;
x = ( 11 - icons.list[iconno].xmid ) * scale + shift;
y = ( 22 - icons.list[iconno].anchor.y ) * scale + shift ;
trans = 'matrix(' + scale.toFixed(1) + ',0,0,' + scale.toFixed(1) +
',' + x.toFixed(1) + ',' + y.toFixed(1) + ')' ;
icon.setAttributeNS(null,'d',icons.list[iconno].path) ;
icon.setAttributeNS(null,'stroke','black') ;
icon.setAttributeNS(null,'stroke-width',1) ;
icon.setAttributeNS(null,'fill','gray') ;
icon.setAttributeNS(null,'transform',trans) ;
return icon ;
}
// generate buttons for labels
function drawlabel(labelno,chosen)
{ var rect ;
function genrect(offset)
{ var rect = document.createElementNS(svgns,"rect") ;
rect.setAttributeNS(null,'x',offset) ;
rect.setAttributeNS(null,'y',offset) ;
rect.setAttributeNS(null,'width',36) ;
rect.setAttributeNS(null,'height',36) ;
rect.setAttributeNS(null,'rx',4) ;
rect.setAttributeNS(null,'ry',4) ;
return rect ;
}
svg = document.createElementNS(svgns,"svg") ;
svg.setAttributeNS(null,'height',39) ;
svg.setAttributeNS(null,'width',39) ;
// draw the button shading
rect = genrect('2.5') ;
rect.setAttributeNS(null,'style','stroke:lightgray;stroke-width:1') ;
svg.appendChild(rect) ;
// draw the button outline
rect = genrect('0.5') ;
if(chosen) rect.setAttributeNS(null,'style',
'stroke:black;stroke-width:1;fill:#e2e2e2') ;
else rect.setAttributeNS(null,'style',
'stroke:black;stroke-width:1;fill:white') ;
svg.appendChild(rect) ;
svg.appendChild(drawicon(labelno,1.5,2)) ;
return [ svg , rect ] ;
}
function iconsel(labelno)
{ if(labelno==labelmenu.thislabno) return ;
while(labname.childNodes.length)
labname.removeChild(labname.childNodes[0]) ;
domadd(labname,L.labels[labelno]) ;
temp = drawicon(labelno) ;
lsvg.replaceChild(temp,labicon) ;
btnlist[labelmenu.thislabno].setAttributeNS(null,'style',
'stroke:black;stroke-width:1;fill:white') ;
btnlist[labelno].setAttributeNS(null,'style',
'stroke:black;stroke-width:1;fill:#e2e2e2') ;
labicon = temp ;
labelmenu.thislabno = labelno ;
ip.focus() ;
ip.setSelectionRange(ip.value.length,ip.value.length) ;
}
function iconfactory(labelno) { return function() { iconsel(labelno) ; } }
defw = 41*8-2 ;
defh = 132+45 ;
bigdiv = document.createElement('div') ;
bigdiv.setAttribute('style','position:absolute;width:100%;height:100%;'+
'top:0;left:0;background:black;opacity:0.5') ;
bdiv.appendChild(bigdiv) ;
w = Math.min(defw,window.innerWidth) ;
h = Math.min(defh,window.innerHeight) ;
l = Math.floor((window.innerWidth-w)/2) ;
t = Math.floor((window.innerHeight-h)/2) ;
div = domcreate('div',msg) ;
// margin:auto doesn't have any effect unless the width/height are specified,
// and it's best not to specify them because they depend on text spacing. yuk!
div.setAttribute('style','position:absolute;background:white;left:'+l+'px;'+
'top:'+t+'px;border:2px black solid;padding:4px;font-family:helvetica;'+
'margin:auto') ;
div.setAttribute('id','routemaster:div') ;
bigdiv.setAttribute('id','routemaster:bigdiv') ;
div.appendChild(document.createElement('br')) ;
button = document.createElement('div') ;
button.setAttribute('style',
'width:22px;height:22px;display:inline-block') ;
lsvg = document.createElementNS(svgns,"svg") ;
lsvg.setAttributeNS(null,'height',22) ;
lsvg.setAttributeNS(null,'width',22) ;
labicon = drawicon(oldlabno) ; // oldlabno is index in list
lsvg.appendChild(labicon) ;
button.appendChild(lsvg) ;
div.appendChild(button) ;
ip = domcreate('input',null,'value',oldval) ;
ip.setAttribute('style','font-family:helvetica;width:140px;'+
'height:24px;margin-bottom:6px;vertical-align:top') ;
div.appendChild(ip) ;
labname = domcreate('span',L.labels[oldlabno]) ;
labname.setAttribute('style','padding-left:4px') ;
div.appendChild(labname) ;
for(labelno=0;labelno<nlabel;labelno++)
{ if(labelno==0||labelno==8) div.appendChild(document.createElement('br')) ;
button = domcreate('div',null,'title',L.labels[labelno]) ;
button.setAttribute('style','width:39px;height:45px;display:inline-block'+
((labelno==0||labelno==8)?'':';margin-left:2px')) ;
temp = drawlabel(labelno,labelno==oldlabno) ;
btnlist[labelno] = temp[1] ;
button.appendChild(temp[0]) ;
button.onclick = iconfactory(labelno) ;
div.appendChild(button) ;
}
/* ------------------------------------------------------------------------ */
// handle submit/return key
function submitfn(delopt,ip)
{ return function()
{ textpromptcleanup() ; action(delopt?null:ip.value,labelmenu.thislabno) ; }
}
// cancel
span = domcreate('span',promptbtn('cancel',caps(L.cancel),textpromptcleanup)) ;
span.setAttribute('style','margin:auto;display:table') ;
// delete
if(!missing) span.appendChild(promptbtn('del',caps(L.del),submitfn(1,ip))) ;
// submit
span.appendChild(promptbtn('submit',L.ok,submitfn(0,ip))) ;
ip.onkeydown = function(e) { if(e.keyCode==13) submitfn(0,ip)() ; }
div.appendChild(span) ;
// doc link
div.appendChild(doclink('labels',L.labeldoc)) ;
bdiv.appendChild(div) ;
ip.focus() ;
ip.setSelectionRange(0,oldval.length) ;
}
/* -------------------------------------------------------------------------- */
function genldiv(p)
{ var tdiv = document.createElement('div') ;
function tdivadd(a,s)
{ var ldiv = document.createElement('div') , i ;
for(i=0;i<5;i++)
{ domadd(ldiv,a[i]); ldiv.appendChild(document.createElement('br')) ; }
ldiv.setAttribute('style','float:left;'+s) ;
tdiv.appendChild(ldiv) ;
}
tdivadd(L.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 = document.createElement('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 = '\u2606' ; else c = '\u2605' ;
s = domcreate('span',c) ;
if(editable&&i!=nstars)
{ s.setAttribute('style','cursor:pointer') ;
function createfunc(i) { return function() { restars(nstars,i,d) ; } } ;
s.onclick = createfunc(i) ;
}
d.appendChild(s) ;
}
if(editable&&nstars!=null)
{ d.appendChild(genspan(' [')) ;
function starfactory(n) { return function() { restars(n,null,d) ; } ; } ;
d.appendChild(genclickfn(starfactory(nstars),L.clearword)) ;
d.appendChild(genspan(']')) ;
}
return d ;
}
/* -------------------------------------------------------------------------- */
function drawxcur(pro,sel)
{ var pos , i , linewid , inc , cx, cy , jlim , div , r=pro.radius , w , h ;
for(div=pro.curdiv;div.childNodes.length>0;)
div.removeChild(div.childNodes[div.childNodes.length-1]) ;
if(sel.type||!pro.m.h.length) return ;
pos = pro.m.wp2pro[sel.segno][sel.ptno] ;
pro.sel = sel ;
var c=document.createElement('canvas') , ctx=c.getContext('2d') ;
if(pro.active) { w = 620 ; h = 200 ; } else w = h = 42 ;
c.setAttribute('width',w*pro.ratio) ;
c.setAttribute('height',h*pro.ratio) ;
c.setAttribute('style','width:'+w+'px;height:'+h+'px') ;
div.appendChild(c) ;
linewid = 2*Math.floor(pro.ratio+0.5) ;
// the cursor
ctx.beginPath() ;
ctx.lineWidth = 1 ;
if(pro.active)
{ ctx.moveTo(pro.offs+pos+0.5,pro.offs) ;
ctx.lineTo(pro.offs+pos+0.5,pro.offs+pro.innerh) ;
}
else
{ jlim = Math.floor(Math.sqrt(r*r-(r-pos)*(r-pos))) ;
ctx.moveTo(pos+linewid/2+0.5,linewid/2+r-jlim) ;
ctx.lineTo(pos+linewid/2+0.5,linewid/2+r+jlim) ;
}
ctx.stroke() ;
// the circle of the 'x': first the filling
ctx.beginPath() ;
cy = r + linewid/2 ;
if(pro.active)
{ cx = pro.outerw - r - linewid/2 ;
ctx.fillStyle = 'white' ;
ctx.arc(cx,cy,r,0,2*Math.PI,false) ;
ctx.fill() ;
}
else cx = r + linewid/2 ;
// then the outline (drawn separately so that the filling shouldn't overlap)
ctx.beginPath() ;
ctx.strokeStyle = '#555' ;
ctx.lineWidth = linewid ;
ctx.arc(cx,cy,r,0,2*Math.PI,false) ;
ctx.stroke() ;
if(pro.active==0) return ;
// the two bars of the 'x'
inc = r * Math.sqrt(2) / 2 ;
ctx.beginPath() ;
ctx.moveTo(cx+inc,cy-inc) ;
ctx.lineTo(cx-inc,cy+inc) ;
ctx.stroke() ;
ctx.beginPath() ;
ctx.moveTo(cx+inc,cy+inc) ;
ctx.lineTo(cx-inc,cy-inc) ;
ctx.stroke() ;
}
/* -------------------------------------------------------------------------- */
function toggleprofile(e)
{ var flag , sel , pos = window.innerWidth - e.clientX ;
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)
{ e.latLng = point2LatLng(e.clientX,e.clientY,pro.map) ;
selpoint(e) ;
return ;
}
flag = infowindow.close() ;
if(flag=='highlight') flag = 3 ;
else if(flag=='seginfo') flag = 2 ;
else if(flag=='wpinfo') flag = 1 ;
else flag = 0 ;
pos = Math.floor(0.5+(610-pos)*pro.ratio) ;
if(pos>=pro.m.pro2wp.length) pos = pro.m.pro2wp.length-1 ;
else if(pos<0) pos = 0 ;
sel = pro.m.pro2wp[pos] ;
walkto(sel.segno,sel.ptno,sel.type,flag) ;
}
function point2LatLng(x,y,map) // by Egil (stackoverflow)
{ var topRight = map.getProjection().
fromLatLngToPoint(map.getBounds().getNorthEast()) ;
var bottomLeft = map.getProjection().
fromLatLngToPoint(map.getBounds().getSouthWest()) ;
var scale = Math.pow(2,map.getZoom());
var pt = new google.maps.Point(x/scale + bottomLeft.x, y/scale + topRight.y) ;
return map.getProjection().fromPointToLatLng(pt) ;
}
/* -------------------------------------------------------------------------- */
function telltale(msg)
{ zonktale() ;
var sty = 'background:white;position:absolute;'+
'left:0;bottom:0;padding:3px;z-index:999;font-size:150%' ;
telltale.div = domcreate('div',msg,'style',sty) ;
telltale.opacity = 10 ;
telltale.div.setAttribute('style',sty) ;
body.appendChild(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 = null ;
}
var L=
{ // routemaster
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:'Waymark' ,
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'] ,
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' ,
youneedtocombine:'You need to combine segments to save as a track.' ,
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 from Google Elevation Service' ,
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' ,
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 label:' ,
modlabel:'Modify or delete label:' ,
enterphoto:'Enter photo name:' ,
newphoto:'New photo name:' ,
exitfs:'Exit full screen [esc key]' ,
enterfs:'Enter full screen [f key]' ,
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 label' ,
labelpt:'label waypoint' ,
editlabel:'edit label' ,
removelabels:'remove labels' ,
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 Google estimates' ,
regressalts:'Google regression of altitudes' ,
calibalts:'Google additive calibration of altitudes' ,
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' ,
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' ,
googledirs:'Google directions' ,
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 %' ,
goql:'No calibration data available: over Google query limit' ,
ginv:'Invalid calibration request' ,
gden:'Calibration request denied' ,
gunk:'Unknown error reported for calibration request: %' ,
gcer:'Calibration error' ,
gcor:'elevation response does not correspond to request' ,
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' ,
// 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;' ,
'Load tracks defined by Google Maps direction pages;' ,
'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 Google elevations 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 /',
'label a waypoint / 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 waypoint 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:' ,
rmtc:[ 'By using Routemaster you agree to be bound by ' ,
'Google Maps terms of use' ] ,
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:'Ask Google to supply segment altitudes' ,
adjalts:'Adjust altitudes...' ,
regress:'Correct segment altitudes by linear regression' ,
googleadd:'Correct segment altitudes by calibration' ,
vsgoogle:'The regression and calibration are against Google estimates' ,
repalts:'Replace segment altitudes by Google estimates' ,
offalts:'Add a manual offset to segment altitudes' ,
docalts:'Documentation of altitude options' ,
edit:'Edit' ,
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 waypoint draggable' ,
dragaway:'Insert %' ,
xferwpt:'Transfer waypoint to segment %' ,
viewinfo:'View info page' ,
editurl:'Edit url' ,
addurl:'Add url of info page' ,
labeldoc:'Documentation for waypoint labels' ,
descdoc:'Documentation for route 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' ,
orweb:'or load from the web' ,
gmaps:['Google maps','data'] ,
photodoc:'Documentation for including photos' ,
urldoc:'The URL may be of a TCX/GPX/RTE/KML/FIT file or of a '+
'Google Maps page giving directions from one place to another.' ,
// index.html
nogoogle:'Unable to fetch Google Maps API.\n'+
'This may be because the Google server is not responding,\n' +
'or because of a bug somewhere,\n' + 'or because ' +
'your browser is confused (in which case restarting it may help).' ,
loadrm:'Loading %...' ,
failed:'Failed to read %: no data returned' ,
notadir:'% is not a Google directions result' ,
notenuff:'% has too few fields for a Google directions result' ,
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/CSV/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' ,
'Straight' , 'Slight right' , 'Right' , 'Sharp right' ,
'Danger' , 'Food' , 'Water' , 'Summit' ,
'Valley' , 'First Aid' , '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' ,
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 L=
{ // routemaster
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:'Liste photos 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'] ,
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' ,
youneedtocombine:'Il faut combiner des segments afin de les enregistrer en tant que trace.' ,
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 de Google Elevation Service' ,
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' ,
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 [touche ‘esc’]' ,
enterfs:'Entrer en plein écran [touche ‘f’]' ,
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 de Google' ,
regressalts:'régression Google des altitudes' ,
calibalts:'étalonnage additif Google des altitudes' ,
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' ,
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' ,
googledirs:'Google directions' ,
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 %' ,
goql:'Aucune donnée d’étalonnage disponible : on a dépassé la limite Google' ,
ginv:'Demande d’étalonnage non valide' ,
gden:'Demande d’étalonnage refusée' ,
gunk:'Erreur méconnu signalée pour la demande d’étalonnage : %' ,
gcer:'Erreur d’étalonnage' ,
gcor:'la réponse d’élévation ne correspond pas à la demande' ,
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' ,
// 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;' ,
'Importer les pistes définies par les pages de direction de Google Maps;' ,
'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 Google ;' ,
'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é /',
'étiqueter un point / 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:' ,
rmtc:[ 'En utilisant Routemaster, vous acceptez d’être lié par ' ,
'les conditions d’utilisation de Google Maps' ] ,
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:'Demander à Google de fournir les altitudes du segment' ,
adjalts:'Ajuster les altitudes...' ,
regress:'Corriger les altitudes du segment par régression linéaire' ,
googleadd:'Corriger les altitudes des segments par étalonnage' ,
vsgoogle:'La régression et l’étalonnage sont basés sur les estimations Google' ,
repalts:'Remplacer les altitudes du segment par les estimations Google' ,
offalts:'Ajouter un décalage manuel aux altitudes du segment' ,
docalts:'Documentation des options d’altitude (angl.)' ,
edit:'Modifier' ,
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 étiquettes des points (angl.)' ,
descdoc:'Documentation pour les descriptions des itinéraires (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' ,
orweb:'ou importer depuis le web' ,
gmaps:['Google maps','data'] ,
photodoc:'Documentation pour l’insertion de photos (angl.)' ,
urldoc:'L’URL peut designer un fichier TCX/GPX/KML/CSV/FIT/RTE ou '+
'un page Google Maps donnant les directions d’un endroit à un autre.' ,
// index.html
nogoogle:'Impossible de charger l’API Google Maps.\n'+
'Cela peut être dû au fait que le serveur Google ne répond pas,\n' +
'ou à cause d’un bug quelque part,\n' + 'ou parce que ' +
'votre navigateur est confus\n(auquel cas le relancer pourrait l’aider).' ,
loadrm:'Téléchargeant %...' ,
failed:'Impossible de lire % : aucune donnée renvoyée' ,
notadir:'% n’est pas un résultat de Google Maps' ,
notenuff:'% a trop peu de champs pour un résultat Google' ,
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' ,
'Tout Droit' , 'Droite légère' , 'Droite' , 'Droite serrée' ,
'Danger' , 'Nourriture' , 'Eau' , 'Sommet' ,
'Vallée' , 'Premiers secours' , '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: 'l’index' ,
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'
} ;
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 domadd(div,txt)
{ if(typeof txt=='string'||typeof txt=='number')
div.appendChild(document.createTextNode(txt)) ;
else if(txt) div.appendChild(txt) ;
}
function domcreate(type,txt,p1,p2)
{ var div = document.createElement(type) ;
if(p1&&p2) div.setAttribute(p1,p2) ;
if(txt) domadd(div,txt) ;
return div ;
}
function prettynum(num,opt,sfx) // opt means “put a space in long numbers”
{ var s , span = domcreate('span') , len ;
num = num.toFixed(0) ;
len = num.length ;
if(len>3&&opt)
{ s = domcreate('span',num.substring(0,len-3),'style','padding-right:1.5px') ;
span.appendChild(s) ;
num = num.substring(len-3) ;
}
domadd(span,num) ;
if(sfx) span.appendChild(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)
{ d.setAttribute('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)
{ x = new LatLonSpherical(x.lat,x.lng) ;
y = new LatLonSpherical(y.lat,y.lng) ;
return x.initialBearingTo(y) * Math.PI/180 ;
}
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(L.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)) ;
return y ;
}
var defparms = {tol:15,maxsep:100,wppenalty:700,vweight:1} ;
var iconic =
{ names: [ 'Generic' , 'Sharp left' , 'Left' , 'Slight left' ,
'Straight' , 'Slight right' , 'Right' , 'Sharp right' ,
'Danger' , 'Food' , 'Water' , 'Summit' ,
'Valley' , 'First Aid' , 'Info' , 'Obstacle'
] ,
tcxnames: [ 'Generic' , 'Left' , 'Left' , 'Left' ,
'Straight' , 'Right' , 'Right' , 'Right' ,
'Danger' , 'Food' , 'Water' , 'Summit' ,
'Valley' , 'First Aid' , 'Generic' , 'Generic'
] ,
fitnames: [ 0,20,6,19, 8,21,7,22, 5,4,3,1, 2,9,53,46 ] ,
} ;
/* ------------------------------- 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.marker = this.photomarker = this.label = this.t = this.selfunc = null ;
this.map = this.delta = this.type = this.point = null ;
if(isvaliddate(t)) this.t = t ; else this.t = null ;
this.photo = [] ;
this.caption = '' ;
this.clickhandler = this.photohandler = null ;
}
/* ------------------------------ 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 { 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 ;
}
// addlabel is used for a labelled point attached to a gpx/tcx track
function addlabel(pts,pt)
{ var i,mindist,near ;
for(near=null,i=0;i<pts.length;i++) if(!near||dist(pt.pos,pts[i].pos)<mindist)
{ mindist = dist(pt.pos,pts[i].pos) ; near = pts[i] ; }
if(near) near.setlabel(pt.label,pt.caption) ;
}
/* -------------------------------------------------------------------------- */
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) ;
if(seg.level==0)
{ if(opt=='arrows') 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=='genmarker')
{ for(i=0;i<len;i++)
if(pts[i].photo.length) genmarker(pts[i],0,parms.map,parms.sel) ;
for(i=0;i<gen;i++)
if(geo[i].photo.length) genmarker(geo[i],0,parms.map,parms.sel) ;
}
else if(opt=='genlabel')
{ for(i=0;i<len;i++)
if(pts[i].label) pts[i].setlabelmap(parms.map,parms.sel) ;
for(i=0;i<gen;i++)
if(geo[i].label) geo[i].setlabelmap(parms.map,parms.sel) ;
}
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')
{ for(i=0;i<len;i++) pts[i].setmap(null,null) ;
for(i=0;i<gen;i++) geo[i].setmap(null,null) ;
undraw(seg) ;
}
else alert(opt+' is an unrecognised option') ;
}
else for(i=0;i<len;i++) recurse(pts[i],opt,parms) ;
}
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)) ; domadd(div,': ') ; }
if(stats.length==3)
{ domadd(div,inject(L.numroutes,stats[0].toFixed(0))+'; ') ;
div.appendChild(prettynum(stats[1]/1000,0,'km; ↑')) ;
div.appendChild(prettynum(stats[2],stats[2]>9999,opt?'m.':'m')) ;
}
else
{ domadd(div,L.distance+' ') ;
if(stats[1]>1999)
{ s = domcreate('span',diststring(stats[1]),'style','padding-right:1.5px') ;
div.appendChild(s) ;
s = 'k' ;
}
else
{ s = domcreate('span',stats[1].toFixed(0),'style','padding-right:1.5px') ;
div.appendChild(s) ;
s = '' ;
}
if(!stats[2])
{ s += (opt?'m.':'m') ;
div.appendChild(domcreate('span',s)) ;
return div ;
}
s += 'm; ↑' + stats[2].toFixed(0) ;
div.appendChild(domcreate('span',s,'style','padding-right:1.5px')) ;
s = 'm ↓' + stats[3].toFixed(0) ;
div.appendChild(domcreate('span',s,'style','padding-right:1.5px')) ;
s = 'm; ' + L.altitude + ' ' + stats[4].toFixed(0) +
'-' + stats[5].toFixed(0) ;
div.appendChild(domcreate('span',s,'style','padding-right:1.5px')) ;
domadd(div,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=[] ;
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 ;
}
}
return { dist:dd , asc:asc , desc:des , outoforder:err ,
nnull , npts:nowpts , maxsep , nlabels ,
pix:photo , ntimes , maxalt , minalt ,
date
}
}
/* --------------------------------- flatten -------------------------------- */
function flatten(segments,geoopt)
{ var p,npoint,d,s0,s1,s2,pos,pts,opos,iter ;
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.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 genclickfn(action,legend,bropt,style)
{ var defstyle = 'cursor:pointer;color:#0000bd' ;
if(style) defstyle += ';' + style ;
var span = genspan(legend,bropt,defstyle) ;
span.onclick = action ;
return span ;
}
function genspan(legend,bropt,spanstyle)
{ var span = document.createElement(bropt=='hr'?'div':'span'),s,ind=-1 ;
if(legend) ind = legend.search('#') ;
if(ind<0) domadd(span,legend) ;
else
{ domadd(span,legend.substring(0,ind)+' ') ;
s = domcreate('span','\u00a0\u00a0\u00a0\u00a0') ;
s.setAttribute('style','background-color:'+legend.substring(ind,ind+7)) ;
span.appendChild(s) ;
domadd(span,' '+legend.substring(ind+7)) ;
}
if(spanstyle==undefined) spanstyle = '' ;
else if(spanstyle!='') spanstyle += ';' ;
if(bropt==']br') { domadd(span,']') ; bropt = 'br' ;}
if(bropt=='br') span.appendChild(document.createElement('br')) ;
else if(bropt=='hr') spanstyle +=
'margin-bottom:2px;border-bottom:solid 1px silver;padding-bottom:2px' ;
if(spanstyle!='') span.setAttribute('style',spanstyle) ;
return span ;
}
/* -------------------------------------------------------------------------- */
function 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 }
}
/* ----------------------------- file dialogue ------------------------------ */
function filedialogue(ovr)
{ var n , b , list , hits , hit , opt , extn , div = domcreate('div') ;
var input , ind , prose , listuri , d , para = domcreate('p') ;
var span,filebox,lab ;
para.setAttribute('style','font-family:helvetica;margin:0') ;
lab = domcreate('label',null,'for','filedialogue') ;
// file browser
if(ovr=="list") domadd(lab,L.loadlist+': ') ;
else
{ if(ovr=='geotag') domadd(para,L.selectjpg) ; else domadd(para,L.select) ;
input = domcreate('input',null,'type','file') ;
if(ovr=='geotag') input.setAttribute('accept','.jpg') ;
else input.setAttribute('accept','.tcx,.gpx,.kml,.fit,.rte,.csv') ;
if(ovr!='conv') input.setAttribute('multiple','multiple') ;
input.addEventListener('change',function(e)
{ if(ovr!='conv') 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 ;
var pos = geotag(readers[i].result,input.files[i].name) ;
if(pos) insgeo(pos,filename.substring(0,filename.length-4),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]) ;
}
} ) ;
para.appendChild(input) ;
div.appendChild(para) ;
if(ovr=='geotag')
{ div.appendChild(doclink('geoloc',L.geolocdoc)) ; return div ; }
else if(ovr=='conv') return div ;
para = domcreate('p',L.orweb+': ') ;
para.setAttribute('style','font-family:helvetica;margin:4 0 0') ;
}
para.appendChild(lab) ;
// url input box
filebox = domcreate('input',null,'id','filedialogue') ;
if(ovr=='list'&&imginfo.carriedover)
filebox.setAttribute('value',imginfo.uri) ;
else
{ 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 ;
}
opt = document.createElement('option') ;
opt.value = i<0?hit:hits[i] ;
if(!list) list = domcreate('datalist',null,'id','dlist') ;
list.appendChild(opt) ;
}
if(list)
{ filebox.appendChild(list) ; filebox.setAttribute('list','dlist') ; }
}
filebox.setAttribute('style','width:500px;height:24px') ;
filebox.onkeyup = function(e)
{ var s ;
if(e.keyCode==13||e.which==13)
{ infowindow.close() ;
if(ovr=='geotag') { alert(this.value) ; return ; }
if(ovr=='list')
{ if(imginfo.carriedover&&this.value==imginfo.uri)
{ imginfo.carriedover = null ; photoprompt() ; return ; }
else listuri = rellist(this.value) ;
}
else trackuri = this.value ;
if(ovr!='list')
{ s = trackuri.substring(trackuri.length-4).toLowerCase() ;
if(trackextns.indexOf(s)>=0) s = shortenname(trackuri) ;
else if( trackuri.indexOf('google')>=0
&& trackuri.indexOf('/maps/dir/')>=0 ) s = L.gmaps[0] ;
else s = L.gmaps[1] ;
textprompt(inject(L.waitingfor,s),null,null,'loadwait') ;
}
if(ovr=='list') { greyout(photobtn) ; getlist(listuri,'uriform') ; }
else
{ readuri(function(r) { render(r,trackuri,'uriform',null,ovr) ; },1) ; }
}
} ;
para.appendChild(filebox) ;
if(ovr=='list')
{ underline(para) ;
div.appendChild(para) ;
para = domcreate('p',null,'style','font-family:helvetica;margin:0') ;
para.appendChild(genlink('https://www.routemaster.app/software/' +
'routemaster.html#photos',L.photodoc,1)) ;
div.appendChild(para) ;
}
else
{ div.appendChild(para) ;
d = domcreate('div',genspan(L.urldoc,null,'font-size:80%')) ;
d.setAttribute('style','font-family:helvetica;padding-top:4px') ;
div.appendChild(d) ;
}
return div ;
}
/* -------------------------------------------------------------------------- */
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) ;
}
// route.stats =
// [ 1 , stats.dist , stats.asc , stats.desc , stats.minalt , stats.maxalt ] ;
function parsestats(stats)
{ var k ,tok ;
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 getprops(nodes,tags,xmlfile)
{ var props = new routetype() , node,i,id,field,txt,item,srcset,slen ;
for(i=0;i<nodes.length;i++)
{ node = nodes[i] ;
id = node.nodeName ;
if(id=='LongTitle') id = 'Description' ;
else if(id=='Overview') id = 'Index' ;
for(field in tags) if(tags[field]==id)
{ if(field=='opt')
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'))
}
}
else if(field=='smallphoto')
{ item = { src: node.getAttribute('src') ,
width: parseInt(node.getAttribute('width')) ,
height: parseInt(node.getAttribute('height')) ,
} ;
srcset = node.getAttribute('srcset').match(/\S+/g) ;
if(srcset.length>=2)
{ slen = srcset[1].length ;
if(srcset[1].charAt(slen-1)=='x')
{ item.srcset = srcset[0] ;
item.scale = srcset[1].substring(0,slen-1) ;
}
}
props.smallphoto.push(item) ;
}
else if(field=="list")
{ txt = node.getAttribute('src') ;
if(txt.substring(0,6)=='$FILE$') txt = null ;
}
else if(field=="index"||field=="info")
txt = { href: reluri(xmlfile,node.getAttribute('href')) ,
title:node.getAttribute('title') } ;
else if(field=='pixpage'||field=='gallery')
{ txt = node.textContent ;
txt = { href:node.getAttribute('href') , title:(txt?txt:'') } ;
}
else txt = node.textContent ;
if(field=="list") txt = reluri(xmlfile,txt) ;
if(field=='photo') props.photo = txt.match(/\S+/g) ; // normalise
else if(field=='gallery'||field=='pixpage')
{ if(field=='gallery'||!props.gallery) props.gallery = txt ; }
else if(field=='stats') props.stats = parsestats(txt) ;
else if(field!='opt'&&field!='smallphoto') props[field] = txt ;
}
}
return props ;
}
/* -------------------------------- gettags -------------------------------- */
function gettags(mode)
{ var field ;
this.info = 'Info' ;
this.time = 'Time' ;
this.extns = 'Extensions' ;
this.title = 'Caption' ;
this.opt = 'Optimised' ;
this.stars = 'Stars' ;
this.stats = 'Stats' ;
this.date = 'Date' ;
this.desc = 'Description' ;
this.list = 'PhotoList' ;
this.photo = 'Photo' ;
this.pixpage = 'PixPage' ;
this.gallery = 'Gallery' ;
this.index = 'Index' ;
this.srcid = 'SourceId' ;
this.vtime = 'ValidTime' ;
this.vel = 'ValidAlt' ;
this.no = 'No' ;
this.name = 'Name' ;
this.caption = 'Name' ;
this.truelabel = 'TrueLabel' ;
if(mode=='tcx') { this.el = 'AltitudeMeters' ; this.label = 'PointType' ; }
else
{ for(field in this) this[field] = this[field].toLowerCase() ;
if(mode=='gpx') { this.el = 'ele' ; this.label = 'type' ; }
}
this.mode = mode ;
}
/* -------------------------------------------------------------------------- */
function getpt(node,tags)
{ var lat=null,lon=null,alt=null,time=null,j,nodeno,nodes,photo=[],tag,valid ;
var pos=null,ind,validalt,caption=null,label=null,truelabel=null,dt=null ;
if(tags.mode=='gpx')
{ lat = parseFloat(node.getAttribute('lat')) ;
lon = parseFloat(node.getAttribute('lon')) ;
if(lat&&lon) pos = { lat:lat , lng:lon } ;
}
nodes = node.childNodes ;
for(validalt=valid=1,nodeno=0;nodeno<nodes.length;nodeno++)
{ node = nodes[nodeno] ;
if(node.nodeName==tags.el) alt = xmlfloat(node) ;
else if(node.nodeName==tags.time) // '1970-01-01T03:040:08Z'
time = new Date(normalise(node.textContent)) ;
else if(node.nodeName=='Position'&&tags.mode=='tcx')
pos = getlatlong(node,'LatitudeDegrees','LongitudeDegrees') ;
else if(node.nodeName==tags.extns)
for(j=0;j<node.childNodes.length;j++)
{ tag = node.childNodes[j].nodeName ;
if(tag==tags.photo) photo = node.childNodes[j].textContent.match(/\S+/g) ;
else if(tag==tags.vtime) valid = 0 ;
else if(tag==tags.vel) validalt = 0 ;
else if(tag==tags.truelabel)
truelabel = normalise(node.childNodes[j].textContent) ;
}
else if(node.nodeName==tags.label)
{ label = normalise(node.textContent) ;
if(label.substring(0,4)=='Bear') label = 'Slight' + label.substring(4) ;
}
else if(node.nodeName==tags.caption) caption = normalise(node.textContent) ;
}
if(!pos) return null ;
if(!isvalidnum(alt)) validalt = 0 ;
pt = new pttype(pos,validalt?alt:null,valid?time:null) ;
pt.photo = photo ;
if(truelabel) pt.label = truelabel ;
else if(label) pt.label = label ;
else if(caption) pt.label = 'Generic' ;
pt.caption = caption ;
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,xmlfile)
{ 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,xmlfile)
{ var nodeno,i,j,k,node,nsegment,course,r,nodes,photo=[] ;
var tags = new gettags('tcx') , loadtype = 0 , coursepts , flag ;
var rp = new routetype('segments') ;
// get global title
course = xmldoc.getElementsByTagName('TrainingCenterDatabase')[0].childNodes ;
for(rp.title=null,i=0;rp.title==null&&i<course.length;i++)
if(course[i].nodeName=='Courses')
for(nodes=course[i].childNodes,j=0;rp.title==null&&j<nodes.length;j++)
if(nodes[j].nodeName=='Name')
rp.title = normalise(nodes[j].textContent) ;
// maybe the tracks hang from courses, maybe from laps
course = xmldoc.getElementsByTagName('Track') ;
if(course.length==0) { alert(L.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
for(j=0;j<course[i].childNodes.length;j++)
if(course[i].childNodes[j].nodeName==tags.extns)
rp.pts[i] = getprops(course[i].childNodes[j].childNodes,tags,xmlfile) ;
if(rp.pts[i].indexmode) loadtype = rp.pts[i].indexmode ;
else if(!loadtype&&rp.pts[i].stats) loadtype = 1 ;
if(loadtype) { alert(L.notcxindex) ; throw '' ; }
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==null) rp.title = rp.pts[i].title ;
}
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=getpt(node,tags)))
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=getpt(course[i].childNodes[j],tags))) 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(isvaliddate(coursepts[j].t))
{ coursepts[j].setlabel(coursepts[j].label,coursepts[j].caption) ;
rp.pts[i].pts.push(coursepts[j]) ;
flag = -1 ;
}
else coursepts[k++] = coursepts[j] ;
}
coursepts.length = k ;
if(flag<0)
rp.pts[i].pts.sort(function(a,b){return a.t.getTime()-b.t.getTime();}) ;
}
// if any are left over (no time or waypoints not in order) use addlabel
for(j=0;j<coursepts.length;j++) addlabel(rp.pts[i].pts,coursepts[j]) ;
// merge coincident points
rp.pts[i].pts = squash(rp.pts[i].pts) ; // squeeze out coincident points
}
return rp ;
}
/* -------------------------------------------------------------------------- */
function readgpx(xmldoc,xmlfile)
{ var xmlcoords,i,r,rp=new routetype() , tags = new gettags('gpx') ;
// get the routemaster properties
xmlcoords = xmldoc.getElementsByTagName('gpx')[0].childNodes ;
for(i=0;i<xmlcoords.length;i++) if(xmlcoords[i].nodeName==tags.extns)
rp = getprops(xmlcoords[i].childNodes,tags,xmlfile) ;
// loop over the track points to get the coords
xmlcoords = xmldoc.getElementsByTagName('trkpt') ;
if(xmlcoords.length==0) xmlcoords = xmldoc.getElementsByTagName('rtept') ;
for(i=0;i<xmlcoords.length;i++)
if((r=getpt(xmlcoords[i],tags)))
{ rp.pts.push(r) ;
if(r.label) rp.pts[rp.pts.length-1].setlabel(r.label,r.caption) ;
}
rp.pts = squash(rp.pts) ; // squeeze out coincident points
// loop over the waypoints to get the labels
xmlcoords = xmldoc.getElementsByTagName('wpt') ;
for(i=0;i<xmlcoords.length;i++)
if((r=getpt(xmlcoords[i],tags))) addlabel(rp.pts,r) ;
// get the route name
if(rp.title==null)
{ xmlcoords = xmldoc.getElementsByTagName('name') ;
for(i=0;!rp.title&&i<xmlcoords.length;i++)
if(xmlcoords[i].childNodes.length>0)
if(xmlcoords[i].parentNode.nodeName=='trk') rp.title =
normalise(xmlcoords[i].childNodes[0].textContent).substring(0,15) ;
}
// get the route description
if(!rp.desc)
{ xmlcoords = xmldoc.getElementsByTagName('desc') ;
if(xmlcoords.length>0&&xmlcoords[0].childNodes.length>0)
rp.desc = normalise(xmlcoords[0].childNodes[0].textContent) ;
}
return rp ;
}
/* -------------------------------------------------------------------------- */
function getrtept(node)
{ var lat,lon,alt,time,nodeno,nodes,pt,ty ;
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=='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)
if(label) pt.setlabel(label,caption) ;
else if(caption) pt.setlabel('Generic',caption) ;
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 ;
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[0].length) continue ;
text = node.childNodes[0].textContent ;
if(id=='desc'&&(type=='routemaster'||type=='utf-8'))
{ 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=='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 ;
if(input.length==0) return p ;
else if(input.length<12) abend(inject(L.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(L.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(L.shortfit) ;
for(prev=0,pts=p.pts,ind=hdrlen;ind<hdrlen+datalen;)
{ flag = input[ind++] ;
if(64&flag) // definition message
{ if(flag&32) { alert(L.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:gmsgnum , lmsgnum: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:bigend } ;
defn.fields.push(item) ;
defn.size += item.size ;
}
defns[lmsgnum] = { defn:defn , gmsgnum: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(L.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)
{ val = readfitvalue(input,ind,item) ;
j = iconic.fitnames.indexOf(val) ;
label = iconic.names[j<0?0:j] ;
}
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?L.untimedrecord:L.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( desc.indexOf('\n')>=0 || desc.indexOf('&')>=0
|| desc.indexOf('>')>=0 || desc.indexOf('<')>=0 )
desc = '<![CDATA[' + desc + ']]>' ;
if(!type) 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(L.numroutes,stats[0].toFixed(0)) + '; ' +
(stats[1]/1000).toFixed(0) + 'km; ↑' + stats[2].toFixed(0) + 'm' ;
return L.distance + ' ' + kmstring(stats[1]) + '; ↑' + stats[2].toFixed(0) +
'm ↓' + stats[3].toFixed(0) + 'm; ' + L.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)
{ 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:hyp , omega:omega } ;
// see whether this breaches the max error on any intermediate point
for(legal=1,od=odh=tdist=m=0;m<i;m++,od=d,odh=dh)
{ mpos = 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.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]=clen-1,j=m-2,i=stk[0].prev;i>=0;i=backptr[i],j--) nstk[j] = i ;
if(!parms.vweight) return {ind:nstk,h:null} ;
stk = backptr = stk2 = null ;
// make smoothed altitude estimates
for(k=i=0;i<m;i++)
{ if(i) jump = 1 + nstk[i] - nstk[i-1] ; else jump = 1 ;
if(i<m-1) jump += nstk[i+1] - nstk[i] ;
if(jump>k) k = jump ;
}
h = new Array(m) ;
buf = new Array(k) ;
for(i=0;i<m;i++)
{ h[i] = ipts[nstk[i]].h ;
if(ipts[nstk[i]].h==null||ipts[nstk[i]].h==undefined) continue ;
// collect points
k = 0 ;
if(i) for(ind=nstk[i-1],n=nstk[i]-ind,j=1;j<n;j++)
{ y = ipts[ind+j].h ;
if(y!=null&&y!=undefined)
buf[k++] = { h:y , x:ipts[ind+j].delta , wt:0 } ;
}
ind = nstk[i] ;
k0 = k ;
buf[k++] = { h:ipts[ind].h , x:ipts[ind].delta , wt:0 } ;
if(i<m-1) for(ind++,n=nstk[i+1]-ind,j=0;j<n-1;j++)
{ y = ipts[ind+j].h ;
if(y!=null&&y!=undefined)
buf[k++] = { h:y , x:ipts[ind+j].delta , wt:0 } ;
}
if(k<3) continue ;
// compute weighted moments
for(y=0,j=0;j<k;j++) { x = buf[j].x ; buf[j].x = y ; y += x ; }
for(y=buf[k0].x,q=j=0;j<k;j++)
{ x = buf[j].x -= y ; buf[j].wt = Math.exp(-x*x/200) ; }
for(sxxxx=sxxy=sxxx=sxy=sxx=sy=sx=s=j=0;j<k;j++)
{ x = buf[j].x ;
y = buf[j].h ;
q = buf[j].wt ;
sxxxx += q * x * x * x * x ;
sxxy += q * x * x * y ;
sxxx += q * x * x * x ;
sxy += q * x * y ;
sxx += q * x * x ;
sy += q * y ;
sx += q * x ;
s += q ;
}
if(s<1.5||sxx<50) continue ;
else if(sxx<200||k<5) q = (sy*sxx-sx*sxy) / (s*sxx-sx*sx) ;
else
{ x = sx*sxxx - 2*sxx*sxx ;
y = sx*sxxxx - 2*sxx*sxxx ;
q = - ( y*(sxy*sxx-sy*sxxx) - x*(sxxy*sxx-sy*sxxxx) ) ;
q /= ( y*(s*sxxx -sx*sxx) - x*(s*sxxxx -sxx*sxx ) ) ;
}
h[i] = q ;
}
return {ind:nstk,h:h} ;
}
/* -------------------------------------------------------------------------- */
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)
{ 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,precision) ;
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) ;
}
/* -------------------------------------------------------------------------- */
function writerteprops(props,nesting,gallery)
{ var i , 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.level==0||props.type=='segments')&&nesting<2)
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) ;
}
// 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,precision)
{ var i , ipts = squash(xpts) ;
// 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' +
'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' + rmdoc ;
// metadata
if(title)
str += '<metadata><name>' + title + '</name></metadata>\n' +
'<trk><name>' + title + '</name>\n<trkseg>\n' ;
else str += '<trk><trkseg>\n' ;
if(desc) str += gendesc('desc',desc,null,' ') ;
// loop over points
for(i=0;i<ipts.length;i++)
{ str += ' <trkpt ' + gpxp(ipts[i].pos,precision) + '>\n' ;
if(ipts[i].h!=null&&ipts[i].h!=undefined)
str += gentag('ele',ipts[i].h.toFixed(precision),' ') ;
if(ipts[i].t!=null&&ipts[i].t!=undefined)
str += gentag('time',ipts[i].t.toISOString(),' ') ;
str += ' </trkpt>\n' ;
}
return str + '</trkseg>\n</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(nesting==0||ptype=='segments')
{ if(pt.h||pt.h==0) str += ' h=\"' + pt.h.toFixed(precision) + '\"' ;
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)
{ 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' ;
}
str += blx + ' </pt>\n' ;
}
else str += '/>\n' ;
}
else str += '/>\n' ;
}
return str + blx + '</route>\n' ;
}
/* -------------------------------------------------------------------------- */
function writetcx(title,ipts,msecs,distance,precision)
{ var clen = ipts.length , time , i , k , label , str , lab ;
if(title) title = title.substring(0,15) ; else title = L.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' +
gentag('DistanceMeters',distance[clen-1].toFixed(0),' ') +
gentag('TotalTimeSeconds',time.toFixed(0),' ') +
tcxpos('Begin',ipts[0].pos,0,' ') +
tcxpos('End' ,ipts[clen-1].pos,0,' ') +
gentag('Intensity','Active',' ') + ' </Lap>\n <Track>\n' ;
// loop over trackpoints
for(i=0;i<ipts.length;i++)
{ str += ' <Trackpoint>\n' + tcxpos('',ipts[i].pos,precision,' ') +
gentag('DistanceMeters',distance[i].toFixed(0),' ') ;
if(ipts[i].h!=null)
str += gentag('AltitudeMeters',ipts[i].h.toFixed(precision),' ') ;
str += gentag('Time',new Date(msecs[i]).toISOString(),' ') ;
str += ' <SensorState>Absent</SensorState>\n </Trackpoint>\n' ;
}
str += ' </Track>\n\n' ;
// finally loop over coursepoints
for(i=0;i<ipts.length;i++) if(ipts[i].label)
{ label = ipts[i].label ;
str += ' <CoursePoint>\n' + tcxpos('',ipts[i].pos,precision,' ') ;
k = iconic.names.indexOf(label) ;
if(k<0) lab = iconic.tcxnames[0] ; else lab = iconic.tcxnames[k] ;
str += gentag('PointType',lab,' ') ;
if(ipts[i].caption) str += gentag('Name',ipts[i].caption,' ') ;
if(ipts[i].h!=null)
str += gentag('AltitudeMeters',ipts[i].h.toFixed(precision),' ') ;
str += gentag('Time',new Date(msecs[i]).toISOString(),' ') ;
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].label==0) ncp += 1 ;
if(ipts[i].h==null||ipts[i].h==undefined) ; else nalt += 1 ;
}
var utf8encoder = new TextEncoder() ;
if(!title) title = L.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) 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) ;
j = iconic.names.indexOf(ipts[i].label) ;
fitinject(fit,n+17,j<0?0:iconic.fitnames[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 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 ;
if(pro.prodiv) for(div=pro.prodiv;div.childNodes.length>0;)
div.removeChild(div.childNodes[div.childNodes.length-1]) ;
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 = document.createElement('canvas') ;
div.appendChild(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' ;
c.setAttribute('width',pro.opixw) ;
c.setAttribute('height',pro.opixh) ;
}
else
{ divstyle += 'height:42px;width:42px' ;
c.setAttribute('width',42) ;
c.setAttribute('height',42) ;
}
div.setAttribute('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 = document.createElement('canvas') ;
cdash.setAttribute('width',maxi) ;
cdash.setAttribute('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) ;
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 , dbest , 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
// (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 ;
if(idash==i+1)
{ ind[nar++] = { x:(d+pts[i].delta/2)/total , 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 ; dbest = dpt ; }
}
if(qbest!=null) ind[nar++] = { x:dbest/total , i:ibest } ;
}
ind.length = nar ;
return ind ;
}
/* ------------------------------------ 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)
{ alert(L.badjpg+filename) ; return null ; }
for(offset=2;offset<length;offset+=2+dataview.getUint16(offset+2))
{ if(dataview.getUint8(offset)!=0xFF)
{ alert(L.badjpg+filename) ; return null ; }
if(dataview.getUint8(offset+1)==225) break ;
}
if(getexifstring(dataview,offset+4,4)!="Exif")
{ alert(L.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 { alert(L.badjpg+filename) ; return null ; }
if(dataview.getUint16(offset+2,1-bigend)!=0x002A)
{ alert(L.badjpg+filename) ; return null ; }
start = dataview.getUint32(offset+4,1-bigend) ;
if(start<0x00000008) { alert(L.badjpg+filename) ; 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) { alert(L.notgeotagged+filename) ; return null ; }
return getexifgps(dataview,offset,gpsptr,bigend) ;
}
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 ;
}
/* -------------------------------------------------------------------------- */
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;
}
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* 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);
const θ = Math.atan2(y, x);
const bearing = θ.toDegrees();
return Dms.wrap360(bearing);
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/**
* 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.c
utils: memory.h
hex filename
g++ -o hex -g -O hex.c
#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)) ;
}