PageSourceSearch

https://canalplan.org.uk/can_js/common.js?ver=9.45.11

js canalplan.org.uk collected 2026-10-01 10:15:44 UTC 43,491 bytes, 1,223 lines download raw bytes

1/* This file is loaded by every important page, so this is a good
2 * place to put the automatic error reporting function.  We trap the
3 * error and send back a "carry on" status, but then - after a short
4 * delay to let things carry on nicely - report it */
5
6//REMOVE BEFORE RELEASE
7//window.onerror = function(msg, url, line, column, err) {
8//    return Report_An_Error(msg, url, line, column, err);
9//};
10
11/*
12window.onunhandledrejection = function(evt) {
13    evt.preventDefault();
14    // catch for apple which throws these all over the place
15    //    if(evt.reason == 'TypeError: Load failed' && navigator.userAgent.indexOf('AppleWebKit/605.1.15' != -1))
16    // April 2024 - all sorts of promise errors from Safari.  Kill them all!
17    if(navigator.userAgent.indexOf('AppleWebKit/605.1.15' != -1))
18        return false;
19    return Report_An_Error("Error in promise - "+evt.reason, evt.srcElement.location.toString(), 0, 0, evt);
20}
21*/
22var js_query = document.currentScript.src.substring(document.currentScript.src.indexOf('?'));
23
24function $$(id) {
25    return document.getElementById(id);
26}
27
28// useful for empty callbacks
29function do_nothing() {
30}
31
32//*** Last updated August 2021 
33//*** POLYFILLS
34//*** for Safari 10
35if (!Object.values) Object.values = o=>Object.keys(o).map(k=>o[k]);
36
37//** for Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Mobile Safari/537.36 Edge/15.15254
38
39// Add a getElementsByClassName function if the browser doesn't have one
40// Limitation: only works with one class name
41// Copyright: Eike Send http://eike.se/nd
42// License: MIT License
43
44if (!document.getElementsByClassName) {
45  document.getElementsByClassName = function(search) {
46    var d = document, elements, pattern, i, results = [];
47    if (d.querySelectorAll) { // IE8
48      return d.querySelectorAll("." + search);
49    }
50    if (d.evaluate) { // IE6, IE7
51      pattern = ".//*[contains(concat(' ', @class, ' '), ' " + search + " ')]";
52      elements = d.evaluate(pattern, d, null, 0, null);
53      while ((i = elements.iterateNext())) {
54        results.push(i);
55      }
56    } else {
57      elements = d.getElementsByTagName("*");
58      pattern = new RegExp("(^|\\s)" + search + "(\\s|$)");
59      for (let i = 0; i < elements.length; i++) {
60        if ( pattern.test(elements[i].className) ) {
61          results.push(elements[i]);
62        }
63      }
64    }
65    return results;
66  }
67}
68
69//*** end of POLYFILLS
70
71// a global variable for us to keep useful stuff in when debugging subtle bugs
72if(typeof(cp_debug_data)=='undefined')
73    var cp_debug_data = [];
74
75var errors_reported = {};
76function Report_An_Error(msg, url, line, column, err) {
77    if(errors_reported.any)
78        return false;  // simple check to prevent 1000s from one loop
79    // sometimes my code wants to die silently (often to avoid other bugs)
80    if(msg.indexOf('Ignore this error') != -1)
81        return true;
82    // something (browser plug ins?  advert?) can inject buggy code into a page
83    // this will appear in page (not JS file) and in line 1
84    if(url.indexOf('.js') == -1 && line == 1)
85        return false;
86    // trap for something very odd (hundreds of messages, different code, all
87    // same error (including line and number)
88    if(msg.indexOf('userAgent')!=-1 && line==1 && column==293)
89        return false;
90    if(typeof cp_debug_data == 'undefined')
91        cp_debug_data = 'URL is "'+url+'"'
92    if(!url)
93        return false;   // some engines give us no useful information
94    if(url.indexOf("bootstrap.min.js") != -1)
95        return;         // can't fix bugs in bootstrap!
96    let myurl = new URL(url); // sometimes we have our domain in ad query string
97    if(myurl.hostname.indexOf('canalplan') == -1)
98        return true;   // we can't fix bugs in other people's code
99    let agent = navigator.userAgent;
100    let dodgybots = [
101        "Mediapartners-Google",  // runs JS without having allowed it to load
102        "Yeti/1.1",              // likewise
103        "Applebot/0.1",          // and another
104        "Linespider/1.1",        // a new one like the above
105        "YandexBot",             // clicks things it shouldn't see
106        "Googlebot",
107        "Adsbot-Google",         // FFS Google - fix this
108        "bingbot"                // problems with screen size and CSS
109    ];
110    for(let i=0; i<dodgybots.length; i++) {
111        if(agent.indexOf(dodgybots[i]) != -1)
112            return true;
113    }
114    let ancient_browsers = [
115        {pattern:/Chrome\/([0-9.]+)/, version:56},
116        {pattern:/Firefox\/([0-9.]+)/, version:60},
117    ];
118    for(let i=0; i<ancient_browsers.length; i++) {
119        let hits = ancient_browsers[i].pattern.exec(agent);
120        if(hits && parseInt(hits[1]) <= ancient_browsers[i].version) {
121            Canalplan_Alert("ancient-browser");
122            return true;
123        }
124    }
125    // errors that we can do nothing about, so there is no point in reporting
126    let just_ignore = [
127        // internal thing in Bootstrap, entirely random
128        {type:'error', match:" a is not a function"},
129        // maybe adblocked, but can't do anything
130        {type:'error',match:"PayPal"},
131        // a browser extension that breaks things
132        {type:'trace',match:"hookGeo@"},
133        // Nov 24 - another
134        {type:'trace',match:'/content/gps.js'},
135        // Nov 25 - strange distributed fetch of photo pages with JS error
136        {type:'url',match:'/can_js/min/jquery-3.4.1.min.js'},
137        // AWS can't fake a browser properly
138        {type:'error',match:"Incorrect locale"}
139    ];
140    for(let i=0; i<just_ignore.length; i++) {
141        switch(just_ignore[i].type) {
142        case 'error':
143            if(msg.indexOf(just_ignore[i].match) != -1)
144                return true;
145            break;
146        case 'trace':
147            if(err.stack && err.stack.indexOf(just_ignore[i].match) != -1)
148                return true;
149            break;
150        case 'url':
151            if(url.indexOf(just_ignore[i].match) != -1)
152                return true;
153            break;
154        default:
155            throw 'Unknown type for just_ignore in error trapping'
156        }
157    }
158    
159    // finally, we are going to actually report this!
160    var rep = {line:line,
161               column: column,
162               url:url,
163               error_text:msg,
164               trace:err.stack,
165               page:document.location,
166               debug:JSON.stringify(cp_debug_data),
167               agent: navigator.userAgent
168              };
169//    if(typeof re.trace == 'undefined') // if we have no stack trace, debugging
170  //      return true;                   // is just about impossible
171    if(typeof cp_info != 'undefined') {
172        rep.cp_info = JSON.stringify(cp_info);
173        rep.page_code = cp_info.page_code;
174        rep.build_number = cp_info.build;
175    }
176    setTimeout(function(){
177        Report_Error(rep,'JE');
178    }, 2000);
179    errors_reported.any = true;
180    // return true;  // prevents error and allows continue.  pain to have when developing
181    return false;
182};
183
184// very simple as we don't do anything with the return
185
186function Report_Error(vals, prefix) {
187    var fd = new FormData();
188    fd.append("mode","JavaScript_Error");
189    Object.keys(vals).forEach(function(k) {
190        fd.append(k, vals[k]);
191    });
192    Async_Post("/cgi-bin/callback.cgi", fd, function(resp) {
193        // do nothing
194    });
195}
196
197function WAS_Report_Error(vals, prefix) {
198    var xmlhttp = new XMLHttpRequest();
199    var message = "";
200    for(var name in vals) {
201        if (vals.hasOwnProperty(name)) {
202            if(message)
203                message += '&';
204            message += name+'='+vals[name];
205        }
206    }
207    // it's too long to send as a GET - also means we don't need to encode
208    xmlhttp.open("POST","../cgi-bin/callback.cgi?mode="+prefix,true);
209    //Send the proper header information along with the request
210    //    xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
211    xmlhttp.send(message);
212    return true;
213}
214
215function hasClass(ele,cls) {
216    return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
217}
218
219function addClass(ele,cls) {
220    if (!this.hasClass(ele,cls)) ele.className += " "+cls;
221}
222
223function removeClass(ele,cls) {
224    if (hasClass(ele,cls)) {
225        var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
226        ele.className=ele.className.replace(reg,' ').replace(/\s+/g,' ').replace(/^\s|\s$/,'');
227    }
228}
229
230function NowBusy() {
231    if(hasClass(document.body,'wait')) {
232        return false;
233    }
234    addClass(document.body,'wait');
235    return true;
236}
237
238function NotBusy() {
239    removeClass(document.body,'wait');
240}
241
242function Callback_To_CGI(mode,what,value,ondone) {
243    var params = "mode="+mode+"&what="+what+"&value="+value
244    Async_Text("../cgi-bin/callback.cgi?"+params, function (txt) {
245        if(ondone)
246            ondone(txt);
247    });
248}
249
250function Reload_Page() {
251    //    window.location.href = window.location.href;
252    location.reload(true);
253}
254
255function Master_Startup(opts) {
256    NotBusy();
257    // if we have PHP options they override the CS ones
258    if(opts.php_vars.indexOf("{") == 0) {
259        var newopts = JSON.parse(opts.php_vars);
260        if(newopts != null)   // parsing didn't fail
261            opts = newopts;
262    }
263    if(opts.call_on_load) {
264        setTimeout(function() {
265            Call_On_Load(opts.call_on_load, opts.call_on_load_params);
266        }, 0);
267    }
268    // run all these functions asynchronously 
269    setTimeout(function() {
270        Display_Adverts();
271    },0);
272    if(typeof Logon_Error_Check == 'function') {
273        setTimeout(function() {
274            Logon_Error_Check();
275        }, 0);
276    }
277    if(opts.geocode && opts.geocode != 'no') {
278        setTimeout(function() {
279            Start_Geolocation(opts.geocode);
280        }, 0);
281//    } else {
282//        setTimeout(function() {
283//            Start_Geolocation("log");
284//        }, 0);
285    }
286    if(typeof Logon_Box == 'function') {
287        setTimeout(function() {
288            Logon_Box(null);
289        }, 0);
290    }
291    setTimeout(function() {
292        Show_Navigational_Photos();
293    },0);
294    setTimeout(function() {
295        Add_Amenity_Popups();
296    },0);
297    Any_Messages();
298}
299
300function Call_On_Load(tocallname, params) {
301    let tocall = window[tocallname];   // get function from name
302    if(typeof tocall != 'function') {   // still loading JS
303        setTimeout(function() {
304            Call_On_Load(tocallname, params);
305        }, 100);
306        return;
307    }
308    tocall.apply(null, params);
309}
310
311function Popup(contentsId, focusId, options=[]) {
312    var div = $$('popup_div');
313    if(div == null) {
314        var div = document.createElement('div');
315        div.style.display = "none";
316        div.id = 'popup_div';
317        document.body.appendChild(div); 
318    }
319    if(options.style)  // this means the calculations below will work
320        div.className = options.style;
321    else
322        div.className = "popUp";
323    // create out of sight
324    div.style.left = "-2000px";
325    div.style.top = "0px";
326    let header = '';
327    if(options.noheader)
328        ;
329    else
330        header = '<div style="text-align:right; font-size:70%"><span class="linklike" onclick="Kill_Popup(\'any\')">Close</span></div>';
331    if(options.fetch) {  // contents come from server
332        if(options.fetch == 'html')
333            pth = "/render/html?page="+options.page;
334        else
335            pth = "/render/"+options.fetch;
336        if(options.fetchopt)
337            pth += '&'+options.fetchopt;
338        Async_Text(pth, function(res) {
339            // if lots of hot links, this prevents multiple results
340            if(options.unique_content)
341                div.innerHTML = header;
342            div.innerHTML += res;
343            Popup_After_Load(div, contentsId, focusId, options);
344        });
345    } else {
346        div.innerHTML = header;
347        if(typeof contentsId == 'string') {
348            var id = $$(contentsId);
349            if(id == null)
350                throw "No block '"+contentsId+"' for popup";
351        } else
352            var id = contentsId;
353        var innards = id.innerHTML.replace(/INACTIVE_/g,'');
354        if(options && options.format)
355            innards = options.format(innards,options.passthrough);
356        div.innerHTML += innards;
357        Popup_After_Load(div, contentsId, focusId, options);
358    }
359}
360
361function Popup_After_Load(div, contentsId, focusId, options) {
362    div.style.display = "block";
363    // if anchoring popup to an element
364    if(options && options.anchor) {
365        var anchor = options.anchor;
366        var x =0, y = 0;               // work out where to show it
367        y = anchor.offsetHeight;
368        while(anchor.offsetParent) {
369            x += anchor.offsetLeft;
370            y += anchor.offsetTop;
371            anchor = anchor.offsetParent;
372        }
373        var mm = div;
374        var mx = 0;
375        while(div.parentNode && div.parentNode.offsetLeft) {
376            mx += div.parentNode.offsetLeft;
377            div = div.parentNode;
378        }
379        x -= mx;
380        div.style.left = x+"px";
381        div.style.top = y+"px";
382    } else if(options && options.anchortomouse) {
383        var x,y;
384        let mouse = options.anchortomouse;  // just for clarity in expressions
385        let divbox = div.getBoundingClientRect();
386        let anchorbox = contentsId.getBoundingClientRect();
387        if(mouse.clientX < window.innerWidth/2) {  // left half of screen
388            x = mouse.clientX;
389        } else {   // right half of screen
390            x = mouse.clientX - divbox.width;
391        }
392        // we use the two mouse Y coordinates to work out what offset
393        // the screen has!
394        let yoff = mouse.layerY - mouse.clientY;
395        if(mouse.clientY < window.innerHeight/2) { // top half of screen
396            // vertical has a small offset to move off the text
397            y = anchorbox.bottom + yoff + 10;
398        } else {   // bottom half of screen
399            y = anchorbox.bottom + yoff - divbox.height - 10;
400        }
401        div.style.left = x+'px';
402        div.style.top  = y+'px';
403    } else {
404        // rather naughty - rely on specific selector name
405        if(contentsId == 'selector_popup') {  // selectors start at the
406            // left, rest centred
407            div.style.left = "10%";
408            div.style.top = "10%";
409            div.style.width="80%";
410        } else {
411            div.style.left = ((window.innerWidth - div.offsetWidth)/2) + 'px';
412            if(typeof($) != 'undefined')
413                div.style.top = ($(window).scrollTop()+(window.innerHeight - div.offsetHeight)/2) + 'px';
414        }
415    }
416    div.style.zIndex = 500;
417    if(focusId && focusId != "")
418        $$(focusId).focus();
419    if(options && options.nogrey) {
420    } else {
421        greyOut(true);
422    }
423    popupis = contentsId;
424    if(options && options.callback) {
425        options.callback();
426    }
427    if(options && options.reloadonclose) {
428        div.canalplan_reloadonclose = true;
429    }
430    if(options && options.truncateurl) {
431        div.canalplan_truncateurl = true;
432    }
433    if(options && options.onfinish)
434        options.onfinish();
435}
436
437window.Kill_Popup =function(what) {
438    if(what != 'any' && typeof popupis != 'undefined')
439        if(what != popupis)      // prevents wrong box getting killed
440            return;
441    var div = $$('popup_div');
442    if(div) {
443        div.innerHTML = '';
444        div.style.display = 'none';
445        greyOut(false);
446        if(div.canalplan_reloadonclose) {
447            if(div.canalplan_truncateurl) {
448                window.location = window.location.pathname + window.location.hash;
449            } else {
450                window.location.reload(true);
451            }
452        }      
453    }
454}
455
456function greyOut(vis, options) {
457    // Pass true to gray out screen, false to ungray
458    // options are optional.  This is a JSON object with the following (optional) properties
459    // opacity:0-100         // Lower number = less grayout higher = more of a blackout 
460    // zindex: #             // HTML elements with a higher zindex appear on top of the gray out
461    // bgcolor: (#xxxxxx)    // Standard RGB Hex color code
462    // grayOut(true, {'zindex':'50', 'bgcolor':'#0000FF', 'opacity':'70'});
463    // Because options is JSON opacity/zindex/bgcolor are all optional and can appear
464    // in any order.  Pass only the properties you need to set.
465    var options = options || {}; 
466    var zindex = options.zindex || 50;
467    var opacity = options.opacity || 70;
468    var opaque = (opacity / 100);
469    var bgcolor = options.bgcolor || '#000000';
470    var dark=document.getElementById('darkenScreenObject');
471    if (!dark) {
472        // The dark layer doesn't exist, it's never been created.  So we'll
473        // create it here and apply some basic styles.
474        // If you are getting errors in IE see: http://support.microsoft.com/default.aspx/kb/927917
475        var tbody = document.getElementsByTagName("body")[0];
476        var tnode = document.createElement('div');           // Create the layer.
477        tnode.style.position='absolute';                 // Position absolutely
478        tnode.style.top='0px';                           // In the top
479        tnode.style.left='0px';                          // Left corner of the page
480        tnode.style.overflow='hidden';                   // Try to avoid making scroll bars            
481        tnode.style.display='none';                      // Start out Hidden
482        tnode.id='darkenScreenObject';                   // Name it so we can find it later
483        tbody.appendChild(tnode);                            // Add it to the web page
484        dark=document.getElementById('darkenScreenObject');  // Get the object.
485    }
486    if (vis) {
487        // Calculate the page width and height 
488        /* I don't know what is going wrong here, but I'm only getting the height of the header bar in Firefox.  So fake it */
489        if( document.body && ( document.body.scrollWidth || document.body.scrollHeight ) ) {
490            var pageWidth = document.body.scrollWidth+'px';
491            var pageHeight = document.body.scrollHeight+'px';
492            if(document.body.offsetHeight < 200)
493                pageHeight = '100%';
494        } else if( document.body.offsetWidth ) {
495            var pageWidth = document.body.offsetWidth+'px';
496            var pageHeight = document.body.offsetHeight+'px';
497            if(document.body.offsetHeight < 200)
498                pageHeight = '100%';
499        } else {
500            var pageWidth='100%';
501            var pageHeight='100%';
502        }   
503        //set the shader to cover the entire page and make it visible.
504        dark.style.width= pageWidth;
505        dark.style.height= pageHeight;
506        dark.style.opacity=opaque;                      
507        dark.style.MozOpacity=opaque;                   
508        dark.style.filter='alpha(opacity='+opacity+')'; 
509        dark.style.zIndex=zindex;        
510        dark.style.backgroundColor=bgcolor;  
511        dark.style.display='block';                          
512    } else {
513        dark.style.display='none';
514    }
515}
516
517// additional function causes proper closure creation and so differing tips
518function Set_Tips(bx,tit) {
519    bx.onmouseover = function() {Tip(tit,FONTSIZE,'1em');};
520    bx.onmouseout = function() {UnTip();};
521}
522
523// navnote and other boxes for places, features and waterways
524// rather messy because of a lot of historic decisions
525
526function Add_New_Info(item,elname,iswhat) {
527    var totoggle = true;
528    if(cp_info.whatami == 'waterway') {
529        elname = 'wway_'+elname+'_box';
530        totoggle = false;
531    }
532    if(cp_info.whatami == 'feature') {
533        elname = 'struct_'+elname+'_box';
534    }
535    $$(elname).style.display = 'block';
536    $$(elname).scrollIntoView(true);
537    if(totoggle)
538        Toggle_Parent_Menu(item);
539    Canalplan_Alert("created-"+iswhat);
540}
541
542function Add_Navnote(item) {
543    var elname = 'navnote';
544    if(cp_info.whatami == 'place')
545        elname = 'plugin_box_navnotes';
546    Add_New_Info(item,elname,'navnote');  //ALERT-MESSAGE: created-navnote
547}
548
549function Add_Details(item) {
550    var elname = 'detail';
551    if(cp_info.whatami == 'place')
552        elname = 'place_detail_box';
553    Add_New_Info(item,elname,'detail');  //ALERT-MESSAGE: created-detail
554}
555
556function Kill_Navnote(item) {
557    if(cp_info.whatami == 'place')
558        Kill_Navnote_Action('pl');
559    if(cp_info.whatami == 'waterway')
560        Kill_Navnote_Action('ww');
561    if(cp_info.whatami == 'feature')
562        Kill_Navnote_Action('st');
563}
564
565//**FIXME - this shouldn't be in "common.js"
566function Add_Photo(id) {
567    if(id) {
568        if(cp_info.userid) {
569            document.location = "../cgi-bin/add_photo.cgi?id="+id;
570        } else {        // false or null
571            Canalplan_Alert("logged-on-for-photo"); 
572        }
573    } else {
574        Popup('selector_popup','forpopup');
575        Place_Selected_Callback = function(pn) {
576            Kill_Popup('any');
577            if(cp_info.userid) {
578                document.location = "../cgi-bin/add_photo.cgi?id="+pn.id;
579            } else {        // false or null
580                Canalplan_Alert("logged-on-for-photo"); 
581            }
582        };
583        Selector_Start('place');
584    }
585}
586
587function getQueryVariable(variable) {
588    var query = window.location.search.substring(1);
589    var vars = query.split("&");
590    for (var i=0;i<vars.length;i++) {
591        var pair = vars[i].split("=");
592        if(pair[0] == variable){return pair[1];}
593    }
594    return false;
595}
596
597/* Geolocator - January 2010, complete re-write Oct 2012.  Tweak for
598 * new sessions June 2013.  Moved into common May 2015 for boat tracking
599
600 To use geolocation add "geolocate = js-function-name" to the 'head'
601 section in config.hdf.
602
603 That function will be passed a single object as follows:
604 actual{latitude,longitude} - the actual point on the globe
605 determined and the nearest canalplan place with all the usual
606 fields - look at api.can for details of the structure returned
607*/
608
609function Start_Geolocation(geo_callback = 'no') {
610    //**** NEXT THREE (FOUR) LINES FOR TESTING WITH NO WIRELESS OR HTTPS
611//    var position = {coords:{latitude:51.95,longitude:-2.1674,accuracy:1235.06007890}};
612//    Async_JSON('/api/nearest_place?lat='+position.coords.latitude+'&lng='+position.coords.longitude,Geo_With_Location,{sc:"browser",fn:geo_callback,raw:position.coords});
613//    return;
614//    console.log("Restore later!");
615    //**** REAL CODE HERE
616    // Does the browser do things?
617//    console.log("Starting");
618    if (navigator.geolocation) {
619        var fail = function(err) {
620            if(typeof geo_callback == 'function') {
621                geo_callback({mode:'error',error:err});
622            }
623        };
624        navigator.geolocation.getCurrentPosition(function(position) {
625            if(typeof cp_info == 'undefined')
626                cp_info = [];
627            if(typeof position.coords != 'undefined') {
628                cp_info.latitude = position.coords.latitude;
629                cp_info.longitude = position.coords.longitude;
630                if(typeof geo_callback == 'function')
631                    Async_JSON('/api/nearest_place?lat='+position.coords.latitude+'&lng='+position.coords.longitude,Geo_With_Location,{sc:"browser",fn:geo_callback,raw:position.coords});
632                else {
633                    Async_Text('../boats/location.php?browser=Y&user='+cp_info.userid+'&latitude='+position.coords.latitude+'&longitude='+position.coords.longitude+'&accuracy='+position.coords.accuracy,do_nothing);
634                    var dummy = setTimeout(function(){Start_Geolocation(geo_callback)},5*60*1000);
635                }
636            }
637        },fail,{timeout:10000});  
638    } else {   // no browser geolocation at all
639        if(typeof geo_callback == 'function') {
640            geo_callback({mode:'error',error:'nobrowsergeo'});
641        }
642    }
643}
644
645function Geo_With_Location(geodata,inf) {
646    if(geodata.mode == 'success') {
647        geodata.source = inf.sc;
648        geodata.rawdata = inf.raw;
649        inf.fn(geodata);
650    }
651}
652
653// pulls a message block etc
654function Dynamic_HTML(divid, filename, tocall, passthrough) {
655    var area = $$(divid);
656    if(area == null) {
657        var wksp = document.createElement('div');
658        wksp.style.display = 'none';
659        wksp.id = divid;
660        document.body.appendChild(wksp);
661        area = $$(divid);
662    }
663    if(filename.indexOf('/') == -1)
664        filename = "/html/"+filename;
665    Async_Text(filename, function(txt) {
666        area.innerHTML = txt;
667        if(tocall)
668            tocall(passthrough);
669    });
670}
671
672function Show_Help_Page(pageid) {
673    window.open('/cgi-bin/wiki.cgi?pageid=help:'+encodeURIComponent(pageid),'canalplan_help');
674}
675
676function HTML_To_Text(ht) {
677    // translate HTML entities etc
678    var wkdiv = document.createElement('div');
679    wkdiv.innerHTML = ht;
680    return wkdiv.innerText;
681}
682
683/* NB - in a rather nasty fudge the error code can contain (generated
684   by the server code) additional parameters (like place codes) needed
685   to display the message.  Ensure you don't break this if extending
686   the call to message.cgi in any way.
687
688   options - 
689   - buttons is a list of button names (see
690   data/XX_params.txt) that will be displayed.
691
692   - callback is the *name* (text) of a function that will be
693   called with the index of the button clicked.
694
695   - cbparms - if any - will be passed as a second parameter
696   to callback (meaningless otherwise). Again, should be a
697   string
698
699   - substitution=a list of texts that will be pushed into
700   the message replacing [[index]]
701
702   - reloadonclose - once "close" is clicked, the page will reload
703   - truncateurl - use with the above: removes any query parameters
704*/
705function Canalplan_Alert(code, options) {
706    cp_debug_data.push({what:"Alerting", code: code, options:options});
707    if(!cp_info.language)
708        throw("No language set");
709    // if "code" contains HTML it's a bug at the server end
710    // set sensible defaults
711    if(options == null) options = {};
712    if(options.buttons == null) options.buttons = ['OK'];
713    // now do stuff
714    var q = document.getElementById('workspace');
715    if(q == null) {
716        var wksp = document.createElement('div');
717        wksp.style.display = 'none';
718        wksp.id = 'workspace';
719        document.body.appendChild(wksp);
720        q = wksp;
721    }
722    Async_Text("/cgi-bin/message.cgi?lang="+cp_info.language+"&what="+code, function(txt) {
723        cp_debug_data.push({what:"Message",value:txt});
724        var parts = txt.split("$$");
725        if(options.substitutions)
726            var restxt = Apply_Alert_Substitutions(parts[1],options.substitutions);
727        else
728            var restxt = parts[1];
729        if(parts[0].indexOf('{') != -1) {
730            var parms = JSON.parse(parts[0]);
731        } else {
732            throw "No parameter block in text:"+txt
733        }
734        q.innerHTML = '<div class="AlertText">'+restxt+'</div>';
735        q.innerHTML+='<div class="AlertText">'+Alert_Buttons(options.buttons, parms.buttons, options)+'</div>';
736        Popup('workspace','workspace',{style:"AlertMessage",noheader:true,reloadonclose:options.reloadonclose,truncateurl:options.truncateurl});
737    });
738}
739
740function Alert_Buttons(blist, bdefn, options) {
741    var i;
742    var rets = '';
743    for(let i=0; i<blist.length; i++) {
744        rets += '<button class="'+bdefn[blist[i]].class+' btn extra-btn" type="button" onclick="';
745        if(options.nokill)
746            ; // do nothing
747        else
748            rets += 'Kill_Popup(\'any\');';
749        if(options.callback) {
750            rets += ' '+options.callback+'('+i
751            if(options.cbparms) {
752                rets += ",'"+options.cbparms+"'";
753            }
754            rets += ');';
755        }
756        rets += '">'+bdefn[blist[i]].text+'</button>&nbsp;'
757    }
758    return rets;
759}
760
761function Cause_Bug_Report(text) {
762    Async_Text("/cgi-bin/showstuff.cgi?mode=die_for_bug&msg="+encodeURIComponent(text), function() {
763        Canalplan_Alert('bug-reported&reason='+encodeURIComponent(text));
764    });
765}
766
767function Apply_Alert_Substitutions(txt, sublist) {
768    for(let i=0;i<sublist.length;i++) {
769        var ss = '[['+i+']]';
770        txt = txt.replace(ss,sublist[i]);
771    }
772    return txt;
773}
774
775function Log_Social_Share(context) {
776    // very minimal as don't want any action
777    Async_Text("/cgi-bin/showstuff.cgi?mode=log_social_share&share="+encodeURIComponent(context.share)+"&whatpage="+encodeURIComponent(context.url), do_nothing);
778}
779
780// Called at start-up to check for error and info messages
781function Any_Messages() {
782    if(typeof(cp_info.error_message) != "undefined" && cp_info.error_message != "") {
783        Canalplan_Alert(cp_info.error_message);
784    } else {   // don't bother them with two messages
785        let nonag = false;
786        if(typeof cp_info.last_amenity_edit == 'undefined')
787            nonag = true;   // page doesn't have info
788        let dte = new Date(cp_info.last_amenity_edit);
789        let today = new Date();
790        let oneday = 60*60*24*1000;
791        // start nagging after 5 days
792        if(Math.floor((today.getTime() - dte.getTime()) / oneday) < 5)
793            nonag = true;
794        if(!nonag) {  // not every single time!
795            if(Math.random() > .85)
796                Canalplan_Alert("amenity-nag");
797        }
798    }
799}
800
801// A bit of a wrapper for "alert" handling markers of various sorts
802function Show_Message_Alert(msg, options) {
803    if(typeof options == 'undefined')
804        options = [];
805    if(msg.charAt(0) == '!') {  // failed - remove marker
806        msg = msg.slice(1);
807        options.reloadonclose = false;
808    }
809    if(msg.charAt(0) == '@') {   // reload flag
810        msg = msg.slice(1);
811        options.reloadonclose = true;
812    }
813  Canalplan_Alert(msg, options);
814} 
815
816// Advert code - to be run on all pages
817function Display_Adverts() {
818    var el = document.getElementsByClassName('cpadvert');
819    for(var i=0; i<el.length; i++) {
820        if(typeof el[i].id == 'string' && el[i].id.substring(0,3) == 'ad_') {
821            Apply_Advert(el[i], el[i].id.substr(3));
822        }
823    }
824}
825
826function Apply_Advert(el, adkey) {
827    Async_Text("/api/showad?id="+encodeURIComponent(adkey), function(txt) {
828        el.innerHTML = txt;
829        el.style.display = 'block';
830    });
831}
832
833function JSON_Callback(j_callback, response, passthrough) {
834    response=response.replace('\n','')
835    response=response.replace('\r','')
836    if(response) {
837        // the files can end in commas, making them easy to
838        // concatenate.  If so, remove for our purposes
839        if(response.slice(-1) == ',')
840            response = response.slice(0,-1);
841        // suppose that the main website isn't available - in that case
842        // we simply do nothing - better to fail quietly
843        if(response.indexOf("<!DOCTYPE") != -1 && response.indexOf("not available") != -1)
844            return;
845        cp_debug_data.push({what:"JSON parse", txt: response, passthrough: passthrough});
846	var data = JSON.parse(response);
847	j_callback(data, passthrough);
848    }
849}
850
851function Add_Random_Anticache(path) {
852    let dt = new Date();
853    let randomish = dt.getTime();  // lots of nanoseconds
854    if(path.indexOf('?') != -1)    // already has a QS
855        return path +'&decache='+randomish;
856    return path + '?decache='+randomish;
857} 
858
859function Add_Anti_Hack(path) {
860    if(path.indexOf('?') != -1)    // already has a QS
861        return path +'&legit=yes';
862    return path + '?legit=yes';
863} 
864
865function Async_JSON(path, a_callback, passthrough) {
866    path = Add_Random_Anticache(path);
867    path = Add_Anti_Hack(path);
868    cp_debug_data.push({what:"Async_JSON", path: path});
869    var xmlhttp = new XMLHttpRequest();
870    xmlhttp.onreadystatechange=function() {
871	if (xmlhttp.readyState==4) {
872	    if (xmlhttp.status==200) {
873//                cp_debug_data.push({what:"async", from: path, read:xmlhttp.responseText});
874                JSON_Callback(a_callback, xmlhttp.responseText, passthrough);
875	    }
876	}
877    };
878    xmlhttp.open("GET",path,true);
879    xmlhttp.send(null);
880}
881
882// used when you don't want to parse the results
883
884function Async_Text(path, funct, passthrough) {
885    path = Add_Random_Anticache(path);
886    path = Add_Anti_Hack(path);
887    cp_debug_data.push({what:"Async_Text", path: path});
888    var xmlhttp = new XMLHttpRequest();
889    xmlhttp.onreadystatechange=function() {
890        if (xmlhttp.readyState==4) {
891            if (xmlhttp.status==200 && funct) {
892                funct(xmlhttp.responseText, passthrough);
893            }  
894        }
895    };
896    xmlhttp.open("GET",path,true);
897    xmlhttp.send(null);
898}
899
900// used to post formdata
901function Async_Post(path, formd, funct, passthrough) {
902    var xmlhttp = new XMLHttpRequest();
903    xmlhttp.onreadystatechange=function() {
904        if (xmlhttp.readyState==4) {
905            if (xmlhttp.status==200) {
906                JSON_Callback(funct, xmlhttp.responseText, passthrough);
907            }  
908        }
909    };
910    xmlhttp.open("POST",path,true);
911    xmlhttp.send(formd);
912}
913
914function Show_Navigational_Photos() {
915    var el = document.getElementsByClassName('navigational_photo');
916    for(let i=0; i<el.length; i++) {
917        var plid = el[i].getAttribute('data-placeid');
918        Async_JSON('/api/guide_photo?plid='+plid, Load_Navigational_Photo, {el: el[i]});
919    }
920}
921
922function Load_Navigational_Photo(dat, parms) {
923    if(dat.url == '') 
924        return;
925    var img = document.createElement('img');
926    img.src = dat.url;
927    img.className = "shadowed smallshadow";
928    parms.el.appendChild(img);
929}
930
931function Show_Hide_Item(id, showas) {
932    var it = $$(id);
933    if(it) {
934        if(it.style.display == 'none')
935            it.style.display = showas;
936        else
937            it.style.display = 'none';
938    }
939}
940
941function Show_Hidden_Menu_Items(ison) {
942    let i = 0;
943    let a = document.getElementsByTagName("div");
944    while (element = a[i++]) {
945        if(element.className.indexOf('useronly') != -1) {
946            if(ison)
947                element.style.display = 'block';
948            else
949                element.style.display = 'none';
950        }
951    }
952}
953
954// track the screen size, and call things when it changes...
955function Check_Screen_Size(swid) {
956    var v = swid.matches;
957    if(typeof screensize_callback_list != 'undefined') {
958        screensize_callback_list.forEach(function(x) {
959            x(v);
960        });
961    }
962}
963
964var swid = window.matchMedia('(min-width: 1024px)');
965swid.addListener(Check_Screen_Size);
966
967// call it once everything is ready
968window.addEventListener('DOMContentLoaded', () => {
969    Check_Screen_Size(swid);
970});
971
972// dynamically load and call another JS file
973var alreadyloaded = {};
974function Load_And_Run_JS(url, callback_function, callback_params, options) {
975    // it can be conditional on a function not existing (useful if sometimes
976    // you will have loaded the JS file in the header)
977    if(options && options.unlessexists) {
978        if(typeof options.unlessexists == 'string') {
979            if(typeof window[options.unlessexists] == 'function') {
980                callback_function(callback_params);
981                return;
982            }
983        } else
984            throw "options.unlessexists must be a string, not:"+typeof(options.unlessexists);
985    }           
986    var furl = url;
987    // if there isn't one, add a query-string to prevent over-caching
988    if(url.indexOf('?') == -1 && js_query.indexOf('?') != -1)
989        furl = url + js_query;   // js_query created at top of this file
990    if(!alreadyloaded[url]) {  // never seen this file before
991        // give it some storage and queue the function
992        alreadyloaded[url] = {loaded:false, queue:[]};
993        alreadyloaded[url].queue.push({cbf:callback_function, cbp:callback_params});
994        var scriptTag = document.createElement('script');
995        scriptTag.src = furl;
996        scriptTag.onload = function() {
997            for(let i=0; i<alreadyloaded[url].queue.length; i++) {
998                alreadyloaded[url].queue[i].cbf(alreadyloaded[url].queue[i].cbp);
999            }
1000            alreadyloaded[url].loaded = true;
1001        };
1002        document.head.appendChild(scriptTag);        
1003        return;
1004    }
1005    if(!alreadyloaded[url].loaded) { // not loaded yet, so add to queue
1006        alreadyloaded[url].queue.push({cbf:callback_function, cbp:callback_params});
1007        // in the extreme case that load completed while pushing
1008        if(alreadyloaded[url].loaded)
1009            callback_function(callback_params);
1010    } else {                        // it was loaded before we got here
1011        callback_function(callback_params);
1012    }
1013}
1014
1015// no callback as will action when loaded
1016function Load_Stylesheet(url) {
1017    if(!alreadyloaded[url]) {
1018        var styleTag = document.createElement('link');
1019        styleTag.href = url;
1020        styleTag.type = 'text/css';
1021        styleTag.rel = 'stylesheet';
1022        document.head.appendChild(styleTag);
1023        alreadyloaded[url] = true;
1024    }
1025}
1026
1027// ads or hides the "explorer" icon on input
1028function Changed_Input(bx) {
1029    var sicon = 'search_'+bx;
1030    var content = $$(bx).value;
1031    var hint = $$('helpfor_'+bx);
1032    content = content.replace(/\s/g, "");   // remove all spaces
1033    if(content) {
1034        $$(sicon).style.visibility = 'visible';
1035        if(hint) hint.style.display = 'block';
1036    } else {
1037        $$(sicon).style.visibility = 'hidden';
1038        if(hint) hint.style.display = 'none';
1039    }
1040}
1041
1042function Toggle_On_Checkbox(el, id) {
1043    if(el.checked)
1044        $$(id).classList.remove('hidden');
1045    else
1046        $$(id).classList.add('hidden');
1047}
1048
1049// Pop ups for amenities
1050var amenity_globals = {popuplive: false,
1051                       stickypopup: null
1052                      };
1053
1054function Add_Amenity_Popups() {  // add the effect to all boxes with the class
1055    var el = document.getElementsByClassName('amenity');
1056    if(el.length == 0) return;   // bail out immediately if none on page
1057    for(let i=0; i<el.length; i++) {
1058        el[i].addEventListener("mouseover", (e) => {
1059            if(amenity_globals.hoverpopup || amenity_globals.stickypopup !== null)
1060                return;
1061            amenity_globals.hoverpopup = true;
1062            Amenity_Popup(e, false);
1063            
1064        });
1065        el[i].addEventListener("mouseout", () => {
1066            if(amenity_globals.stickypopup !== null)
1067                return;
1068            Kill_Popup('any');
1069            amenity_globals.hoverpopup = false;
1070        });
1071        el[i].addEventListener("click", (e) => {
1072            // clear any hover
1073            if(amenity_globals.hoverpopup) {
1074                Kill_Popup('any');
1075                amenity_globals.hoverpopup = false;
1076            }
1077            if(amenity_globals.stickypopup !== i) {
1078                Amenity_Popup(e, true);
1079                amenity_globals.stickypopup = i;
1080
1081            } else {  // clicked again on same one
1082                amenity_globals.stickypopup = null;
1083                Kill_Popup('any');
1084            }
1085        });
1086    }
1087}
1088
1089function Show_Single_Amenity(e) {
1090    Amenity_Popup(e);
1091}
1092
1093function Show_Amenity_From_List(e) {
1094    if(amenity_globals.expanded_item) {
1095        let exi = $$(amenity_globals.expanded_item);
1096        if(exi)
1097            exi.classList.add('hidden');
1098    }
1099    let id = 'amdetails_'+e.target.dataset.amenity_key;
1100    $$(id).classList.remove('hidden');
1101    amenity_globals.expanded_item = id;
1102}
1103
1104function Amenity_Popup(e, withclose) {
1105    let cmd = (parseInt(e.target.dataset.amenity_count) > 1) ? 'amenity_list' : 'amenity';
1106    let key = e.target.dataset.amenity_key;
1107    if(e.target.dataset.naptan_key) {
1108        cmd = (parseInt(e.target.dataset.amenity_count) > 1) ? 'naptan_list' : 'naptan';
1109        key = e.target.dataset.naptan_key;
1110    }
1111    Popup(e.target, null,
1112          {fetch: cmd,
1113           fetchopt: 'key='+key,
1114           nogrey: true,
1115           noheader: !withclose,
1116           anchortomouse: e,
1117           unique_content: true,
1118           onfinish: function() {
1119               // if by the time we've rendered the item we have left,
1120               // then we immediately kill it
1121               if(amenity_globals.hoverpopup == false && amenity_globals.stickypopup == null)
1122                   Kill_Popup('any');
1123           }
1124          });
1125}
1126
1127// for "recent places" on gazetteer page and in menus
1128function Toggle_Recents(el, searchbox) {
1129    if(typeof el == 'string')   // allows literal or "this" in calls from pages
1130        el = $$(el);
1131    if($$('gaz_recents') == null) {
1132        Async_JSON("/api/recents_and_favourites", function (data) {
1133            let ddown = document.createElement('div');
1134            ddown.id = 'gaz_recents';
1135            ddown.classList.add('recent-place-list','hidden');
1136            Add_Dropdown_Lines(ddown, data, 'recent', el, searchbox);
1137            Add_Dropdown_Lines(ddown, data, 'favourite', el, searchbox);
1138            document.body.appendChild(ddown);
1139            Toggle_Recent_Display(el);
1140        });
1141    } else {
1142        Toggle_Recent_Display(el);
1143    }
1144}
1145
1146function Add_Dropdown_Lines(ddown, data, forwhat, el, searchbox) {
1147    let dhead = document.createElement('div');
1148    dhead.classList.add('recent_header');
1149    if(data[forwhat].length)
1150        dhead.innerHTML = `${forwhat} places`;
1151    else
1152        dhead.innerHTML = 'No recent places';
1153    ddown.appendChild(dhead);
1154    for(let i of data[forwhat]) {
1155        let line = document.createElement('span');
1156        let icon = document.createElement('i');
1157        if(forwhat == 'recent') {
1158            icon.classList.add('fa','fa-heart-o');
1159        } else {
1160            icon.classList.add('fa','fa-heart');
1161            icon.style.color = 'red';
1162        }
1163        icon.onclick = function() {
1164            Swap_Recent_And_Favourite(forwhat,i,searchbox,el)
1165        };
1166        line.appendChild(icon);
1167        let item = document.createElement('span');
1168        item.classList.add('indexmenu');
1169        item.innerHTML = '&thinsp;'+i;
1170        item.onclick = function(){Set_Search_From_Recent(searchbox,item,el)};
1171        line.appendChild(item);
1172        ddown.appendChild(line);
1173        ddown.appendChild(document.createElement('br'));
1174    }
1175}
1176
1177function Toggle_Recent_Display(el) {
1178    if(el.dataset.showing == 'off') {
1179        el.dataset.showing = 'on';
1180        el.classList.remove('fa-toggle-down');
1181        el.classList.add('fa-toggle-up');
1182        let ddown = $$('gaz_recents');
1183        ddown.style.left = el.offsetLeft+16+'px';
1184        ddown.style.top = el.offsetTop+18+'px';
1185        $$('gaz_recents').classList.remove('hidden');
1186    } else {
1187        el.dataset.showing = 'off'
1188        el.classList.remove('fa-toggle-up');
1189        el.classList.add('fa-toggle-down');
1190        let ddown = $$('gaz_recents');
1191        $$('gaz_recents').classList.add('hidden');
1192    }
1193}
1194
1195function Set_Search_From_Recent(sb, item,toggleel) {
1196    let target = $$(sb);
1197    if(target.value)  // already has something - give it a comma
1198        target.value += ','
1199    target.value += item.innerHTML;
1200    Toggle_Recent_Display(toggleel);
1201}
1202
1203function Update_Favourites(text, add) {
1204    let mode = add ? 'add' : 'remove';
1205    Async_Text("/api/update_favourite?do="+mode+'&pl='+encodeURIComponent(text), function (res) {
1206    });
1207}
1208
1209function Swap_Recent_And_Favourite(mode, pl, searchbox, el) {
1210    // we do this at the server and then reload
1211    if(mode=='recent') {
1212        Update_Favourites(pl, true);
1213    } else {
1214        Update_Favourites(pl, false);
1215    }
1216    // hide existing dropdown
1217    Toggle_Recent_Display(el);
1218    // remove existing dropdown
1219    $$('gaz_recents').remove();
1220    // and now redisplay
1221    Toggle_Recents(el,searchbox);
1222}
1223   

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.