PageSourceSearch

https://clanmills.com/

html clanmills.com collected 2026-09-26 15:34:40 UTC 106,007 bytes, 2,334 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <base target="_top">
5	<meta charset="utf-8">
6	<meta name="viewport" content="width=device-width, initial-scale=1">
7	
8	<title>Mills Family - Events, Photos, Travel & Family Tree</title>
9    <link rel="icon" type="image/png" href="/favicon.ico">
10
11    <!-- Load style sheets -->
12    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
13    <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/[email protected]/dist/css/justifiedGallery.css'> 
14    <link type="text/css" rel="stylesheet" href="/js/lightGallery-2.9.0/dist/css/lightgallery-bundle.css" />
15    <link href="https://api.mapbox.com/mapbox-gl-js/v3.9.1/mapbox-gl.css" rel="stylesheet">
16    <link href='/gifs/CeraRoundProDEMORegular.otf'>
17    <link href="https://cesium.com/downloads/cesiumjs/releases/1.139/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
18    <link rel="stylesheet" href="/js/magnific-popup/magnific-popup.css">
19    <link href="/clanmills.css" rel="stylesheet">
20    
21    <!-- Load the JavaScript libraries and styles ---------- -->
22    <!-- 
22<script type="text/javascript" src='/cache/cache.js'></script>
22 -->
23    
23<script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
23
24    
24<script type="text/javascript" src='https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.js'></script>
24
25    
25<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/showdown.min.js"></script>
25
26    
26<script src="/js/magnific-popup/jquery.magnific-popup.js"></script>
26
27
28    <!--
29    Issue:  https://github.com/miromannino/Justified-Gallery/issues/372
30    Don't load the vanilla justifiedGallery.  My version supports end-of-row!
31    
31<script src='https://cdn.jsdelivr.net/npm/[email protected]/dist/js/jquery.justifiedGallery.js'></script>
31
32    -->
33    
33<script type="text/javascript" src='/js/jquery.justifiedGallery.js'></script>
33
34    
35    <!-- https://github.com/sachinchoolur/lightGallery/tags -->
36    
36<script src="/js/lightGallery-2.9.0/dist/plugins/autoplay/lg-autoplay.umd.js"></script>
36
37    
37<script src="/js/lightGallery-2.9.0/dist/plugins/comment/lg-comment.umd.js"></script>
37
38    
38<script src="/js/lightGallery-2.9.0/dist/plugins/fullscreen/lg-fullscreen.umd.js"></script>
38
39    
39<script src="/js/lightGallery-2.9.0/dist/plugins/hash/lg-hash.umd.js"></script>
39
40    
40<script src="/js/lightGallery-2.9.0/dist/plugins/pager/lg-pager.umd.js"></script>
40
41    
41<script src="/js/lightGallery-2.9.0/dist/plugins/rotate/lg-rotate.umd.js"></script>
41
42    
42<script src="/js/lightGallery-2.9.0/dist/plugins/share/lg-share.umd.js"></script>
42
43    
43<script src="/js/lightGallery-2.9.0/dist/plugins/thumbnail/lg-thumbnail.umd.js"></script>
43
44    
44<script src="/js/lightGallery-2.9.0/dist/plugins/video/lg-video.umd.js"></script>
44
45    
45<script src="/js/lightGallery-2.9.0/dist/plugins/zoom/lg-zoom.umd.js"></script>
45
46    
46<script src="/js/lightGallery-2.9.0/dist/lightgallery.umd.js"></script>
46
47    
47<script src="https://kit.fontawesome.com/4d3e0a39c2.js" crossorigin="anonymous"></script>
47
48    
48<script src="https://api.mapbox.com/mapbox-gl-js/v3.9.1/mapbox-gl.js"></script>
48
49    
49<script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
49
50    
50<script src="https://cesium.com/downloads/cesiumjs/releases/1.139/Build/Cesium/Cesium.js"></script>
50
51    
51<script src="https://cdn.jsdelivr.net/npm/jsframe.js/lib/jsframe.min.js"></script>
51
52</head>
53
54<body>
55    <!-- ----------------- Menu (/Menu.inc) ---------------- -->      
56    <table><tr><td id="Menu" style="width:190px;"/><td style="width:200px;"/>
57    <td id="Content" style="width:750px;">
58    <!-- ----------------- TODO: remove this --------------- -->      
59    <table>
60        <tr style="display:none;"><td style="float:right;">Gallery:</td><td><input id="gallery-button" type="checkbox"          onclick="toggle('gallery-div','gallery-button')"/></td></tr>
61        <tr style="display:none;"><td style="float:right;">Map:    </td><td><input id="map-button"     type="checkbox" checked  onclick="toggle('map-div'    ,'map-button'    )"/></td></tr>
62    </table>
63
64    <!-- ----------- Content Header (clock, weather etc) --- -->
65    <table class="story">
66        <tr><td colspan="2" class="padded gradient">
67        <table width="100%">
68        <tr id="clanmills" style="visibility:visible">
69          <td width="75%">
70            <h1 id='banner' style="text-align:left;margin-bottom:0px;">Mills Family<br><span style="font-size:70%;">clanmills.com</span></h1>
71            <h5 id='date'   style="border-bottom:24px solid transparent;"></h5>
72            <table width="100%" style="border:0px solid yellow;-webkit-border-horizontal-spacing:0px;border-top:2px solid yellow;border-bottom:2px solid yellow;" ><tr>
73                <td id='label'  style="border:0px solid yellow;padding:0px;vertical-align:middle;font-size:12pt;font-weight:bold;font-style:italic;"></td>
74                <td id="title"  style="vertical-align:middle;font-size:14pt;font-weight:bold;color:yellow;line-height:30px;"></td>
75                <td id="arrows" style="vertical-align:middle;font-size:14pt;font-weight:bold;color:yellow;text-align:right;"></td>
76            </tr></table>
77          </td>
78          <td width="25%" align="right"> 
79            <table>
80              <tr>
81                <td style="vertical-align:middle;padding-left:20px;"><svg id="svg-clock" style="padding-top:35;height:130px;width:130px;z-index:1;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 800 800">
82                  <circle cx="400" cy="400" r="400" fill="#fff" style="zIndex:1;"/>
83                  <use transform="matrix(-1,0,0,1,800,0)" xlink:href="#one-half"/>
84                  <g id="one-half">
85                    <g id="one-fourth">
86                        <path d="m400 40v107" stroke="#000" stroke-width="26.7"/>
87                        <g id="one-twelfth">
88                            <path d="m580 88.233-42.5 73.612" stroke="#000" stroke-width="26.7"/>
89                            <g id="one-thirtieth">
90                                <path id="one-sixtieth" d="m437.63 41.974-3.6585 34.808" stroke="#000" stroke-width="13.6" />
91                                <use transform="rotate(6 400 400)" xlink:href="#one-sixtieth"/>
92                            </g>
93                            <use transform="rotate(12 400 400)" xlink:href="#one-thirtieth"/>
94                        </g>
95                        <use transform="rotate(30 400 400)" xlink:href="#one-twelfth"/>
96                        <use transform="rotate(60 400 400)" xlink:href="#one-twelfth"/>
97                    </g>
98                    <use transform="rotate(90 400 400)" xlink:href="#one-fourth"/>
99                    </g>
100                    <path class="clock-hour-hand" id="anim-clock-hour-hand" d="m 381.925,476 h 36.15 l 5e-4,-300.03008 L 400,156.25 381.9245,175.96992 Z" transform="rotate(110.2650694444, 400, 400)" />
101                    <path class="clock-minute-hand" id="anim-clock-minute-hand" d="M 412.063,496.87456 H 387.937 L 385.249,65.68306 400,52.75 414.751,65.68306 Z" transform="rotate(243.1808333333, 400, 400)" />
102                    <path class="clock-second-hand" id="anim-clock-second-hand" fill="#a40000" d="M 397.317,63.51744 395.91962,168.4 C 374.575,170.5125 358.2,188.365 358.2,210 c 0,21.635 16.3,39 36.61214,41.47594 L 391.52847,498 h 16.94306 L 405.1868,251.47593 C 425.5,249 441.8,231.635 441.8,210 c 2e-5,-21.635 -16.375,-39.4875 -37.71971,-41.6 L 402.683,63.51744 400,60 Z M 400,190.534 c 10.888,0 19.466,8.866 19.466,19.466 0,10.6 -8.578,19.466 -19.466,19.466 -10.888,0 -19.466,-8.866 -19.466,-19.466 0,-10.6 8.578,-19.466 19.466,-19.466 z" transform="rotate(190.85, 400, 400)" />
103                </svg></td>
104        
105                <td style="vertical-align:middle;width:100px;border:0px solid transparent;padding:0;"><center><p style="width:100px;height:0px;"></p>
106                    <style>
107                    #weather-magic, .weatherwidget-io {
108                        font-family: 'Cera Round Pro DEMO', arial, helvetica, sans-serif !important;
109                    }
110                    </style>
111                    <a id="weather-magic" style="width:100px;padding:0;" class="weatherwidget-io" href="https://forecast7.com/en/51d34n0d74/camberley/" data-mode="Current" data-icons="Climacons Animated" data-label_1="Camberley" data-label_2="England" data-basecolor="transparent" data-textcolor="yellow" data-suncolor="#f98628" data-mooncolor="white" data-cloudcolor="silver" data-cloudfill="#f9f7f6" data-raincolor="#1279c1" data-snowcolor="#2acccb"></a>
112                    
112<script>
113                    !function(d,s,id){let js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src='https://weatherwidget.io/js/widget.min.js';fjs.parentNode.insertBefore(js,fjs);}}(document,'script','weatherwidget-io-js');
114                    </script>
114
115                </center></td>
116                <!--
117                <td style="vertical-align:middle;"><center>
118                    <img src="/gifs/unionjack.gif" style="border:none;" height="45"><br>
119                    <img src="/gifs/england-flag.gif" style="border:none;" height="38"><br>
120                    <img src="/gifs/scotflag.gif" style="border:none;"  height="38"><br>
121                    <img src="/gifs/3dflagsdotcom_usa_2fawm.gif" style="border:none;" height="38">
122                </center></td>
123                -->
124            </tr></table>    
125        </td></tr>
126    </table>
127    <!-- ----------------- Search -------------------------- -->      
128    <div id="search-div" style="visibility:hidden;">
129        <table id='search-controls' style="border:2px solid white;width:100%;margin-bottom:0px;">
130        <tr>
131            <th style="padding:0;vertical-align:top;">Query string:  <br><span style="color:white;font-style:italic;font-weight:normal;">Supports wildcards: <span style="color:yellow;"><b>?*</b></span>, logical <span style="color:yellow;"><b>OR AND NOT</b></span> and parentheses <span style="color:yellow;"><b>(&nbsp;)</b></span><br>Location search: <span style="color:yellow;"><b>@lon,lat</b></span>
132               (Soon:@city, @'address')
133               <br>
134            </th>
135            <th style="padding:0;vertical-align:top;">Search:<br>
136                <button border:0px solid white; class="tablink2" onclick="document.getElementById('search').value='Photos';reallySearch(true);" ><i class="fa-solid fa-image"            aria-hidden="true"></i></button>
137                <button border:0px solid white; class="tablink2" onclick="document.getElementById('search').value='Stories';reallySearch(true);"><i class="fa-solid fa-book-open-reader" aria-hidden="true"></i></button>
138                <button border:0px solid white; class="tablink2" onclick="shareSearch();"><i class="fa-solid fa-share" onclick="shareSearch();" aria-hidden="true"></i></button>
139            </th>
140            <th style="padding:0;vertical-align : top;">Limit:       <br><span style="color:white;font-style:italic;font-weight:normal;">Photos<br>/Stories</span></th>
141            <th style="padding:0;vertical-align : top;">Sort:<br><span style="color:white;font-style:italic;font-weight:normal;">Oldest<br>First</span></th>
142            <th style="padding:0;vertical-align :top;">&nbsp;Cluster:&nbsp;<br><input type="checkbox" checked id="cluster" value="cluster"  onclick="onCluster();" style="float:right;"/></th>
143            <th style="padding:0;vertical-align : top;">MapBox:<br><a onclick="toggleGlobe();"><i class="tablink mi fa-solid fa-globe"    
144                style="color:yellow;text-align:right;padding:0;padding-left:30;padding-top:2;"/></a></i></th>
145        </tr>
146        <tr>
147            <td>
148                <input style="width:95%;" id="query"/>
149            </td><td>
150                <select id="search" style="width:120px;" onchange="reallySearch()">
151                    <option value="Stories"        >Stories</option>
152                    <option value="Photos" selected>Photos</option>
153                </select>
154            </td><td>
155                <select name="limit" id="limit" onchange="reallySearch()">
156                    <option value=  "1"          >   1/5</option>
157                    <option value=  "4"          >   4/5</option>
158                    <option value= "20"          >  20/10</option>
159                    <option value= "50" selected >  50/20</option>
160                    <option value="100"          > 100/20</option>
161                    <option value="200"          > 200/40</option>
162                    <option value="500"          > 500/100</option>
163                </select>
164            </td><td>
165                <input type="checkbox" id="reverse" checked onchange="reallySearch()" value="reverse">
166            </td><td colspan="2">
167            <button id="searchButton" type="button" onclick="reallySearch(true);" style="width:100%;text-align:centre;color:black;" >Search</button>
168            </td>
169        </tr>
170        <tr></tr>
171        </table>
172        <div id="mapsBox" style="height:700px;display:none;"></div>
173    </div>
174        
175    <!-- ----------------- Story --------------------------- -->      
176    <div id="prolog" style="margin-bottom:0px;"></div>
177    <div id="cols2" style="display:none;"></div>
178    <div id="cols"  style="display:none;"></div>
179    <div id="epilog" style="display:none;"></div>
180    <div id="menu" style="display:none;"></div>
181    <!-- ----------------- Photos, Gallery and Maps  ------- -->      
182    <div  id="photo-div" style="display:block;width:790px;">
183       <table id="ribbon" style="width:100%;display:block;padding:10px;">
184         <tr style="vertical-align:middle;">
185           <td style="width:5%;text-align:right;" id="display">Display Map:</td>
186           <td style="width:20%;">
187             <button id='displayMap-button' style="width:30px;" class="tablink2" onclick="togglePhotosMap(this)"><i class="fa-solid fa-image"></i></button>
188           </td>
189           <td id="video-buttons" style="width:25%;text-align:center;vertical-align:bottom;background:transparent;display:inline">
190                <a                 class="butb fa-solid fa-backward-fast" onclick="return buttonClick(event,-3);"></a>
191                <a                 class="butb fa-solid fa-backward"      onclick="return buttonClick(event,-2);"></a>
192                <a                 class="butb fa-solid fa-backward-step" onclick="return buttonClick(event,-1);"></a>
193                <a id="stopButton" class="butb fa-solid fa-play"          onclick="return buttonClick(event, 0);"></a>
194                <a                 class="butb fa-solid fa-forward-step"  onclick="return buttonClick(event, 1);"></a>
195                <a                 class="butb fa-solid fa-forward"       onclick="return buttonClick(event, 2);"></a>
196                <a                 class="butb fa-solid fa-forward-fast"  onclick="return buttonClick(event, 3);"></a>
197                <div id="tech-buttons" style="visibility:visible;background:transparent;vertical-align:middle;padding-left:15px;display:inline;">
198                    <input style="color:yellow;" type="radio" name="radio_tech" value="gmap"    onclick="setTech(this.value);"         >GMap</input>
199                    <input style="color:yellow;" type="radio" name="radio_tech" value="leaflet" onclick="setTech(this.value);"         >Leaflet</input>
200                    <input style="color:yellow;" type="radio" name="radio_tech" value="mapbox"  onclick="setTech(this.value);" checked >MapBox</input>
201                    <input style="color:yellow;" type="radio" name="radio_tech" value="cesium"  onclick="setTech(this.value);"         >Cesium</input>
202                </div><br>   
203                <div  id="photorange" style="visibility:hidden;background:transparent;vertical-align:bottom;font-style:italic;padding-left:15px;display:inline;">
204                    <a   href="javascript:reallySearch(-9999);"            class="butb fa-solid fa-backward-fast" style="background:transparent;"></a>
205                    <a   href="javascript:reallySearch(-1);"               class="butb fa-solid fa-backward"      style="background:transparent;"></a>
206                    <span id="photorangestring">0-0 of 0</span>
207                    <a    href="javascript:reallySearch(+1);"              class="butb fa-solid fa-forward"       style="background:transparent;"></a>
208                    <a    href="javascript:reallySearch(9999);"            class="butb fa-solid fa-forward-fast"  style="background:transparent;"></a>
209                </div>
210           </td>
211         </tr>
212       </table>
213       <div id="gallery-div"   style="display:none;width:100%;height:100%;"><div id="gallery" class="gallery"></div></div>
214       <div id="map-div"       style="display:block;width:790px;height:600px;margin-bottom:30px;">
215       <div id="leaflet" style="display:none; width:790px;height:100%;z-level:0;"></div>
216       <div id='gmap'    style="display:none; width:790px;height:100%;z-level:0;"></div>
217       <div id="mapbox"  style="display:none; width:790px;height:100%;z-level:0;"></div>
218       <div id="cesium"  style="display:none; width:790px;height:100%;z-level:0;"></div>
219      </div>
220    </div>
221    <!-- ----------------- Trailer (about.inc)  ------------ -->      
222    <div id='about' style="display:block;margin-top:10px;"></div>
223    </td></tr></table>
224</table></body></html>
225
226<!-- ----------------- defaults   -------------------------- -->      
227<script src="/default.js"></script>
227
228
229<!-- ----------------- the main event  --------------------- -->      
230<script>
231
232    // Globals
233    var   tech         = defaults.tech;
234    // path is window.location.hash#path=value or default.path (defaults from defaults.js)
235    var   path         = defaults.path ;
236
237    var   Photos       =  []; // populated by readObjectsXML() 
238    var   photo        =  -1; // current photo
239    var   directoryTree     ; // See ~/bin/createDirectoryTree => cd ~/clanmills ; fs2json | jsonlint -t '' > directoryTree.json
240    
241    // JSFrame Floating Photo
242    const jsFrame = new JSFrame();
243    var   frame_        ;
244    var   W_      = 600 ; // size of jsframe
245    var   H_      = 450 ;
246    var   X_      = 800 ; // position on screen
247    var   Y_      = 300 ;
248    var   width_  = W_  ; // cached values for resizing
249    var   height_ = H_  ;
250    var   src_          ;
251    var   title_        ;
252    
253    // timer 
254    var  timer    =  0  ; // move between photos
255    var  interval = 3000;
256
257    // leaflet
258    var   lmap         ;
259    var   popup        ;
260    var   lmarkers = [];
261    var   goldIcon = new L.Icon({
262                        iconUrl     : 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-gold.png',
263                        shadowUrl   : 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
264                        iconSize    : [25, 41],
265                        iconAnchor: [12, 41],
266                        popupAnchor: [1, -34],
267                        shadowSize: [41, 41]
268          });
269    var   blueIcon = new L.Icon({
270                        iconUrl:   '/gifs/marker-icon-2x-blue.png',
271                        shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
272                        iconSize: [25, 41],
273                        iconAnchor: [12, 41],
274                        popupAnchor: [1, -34],
275                        shadowSize: [41, 41]
276          });
277    
278    // Google Maps
279    var   gmap         ;
280    var   gmarkers = [];
281    var   gpin         ; // default pin
282    var   gPin         ; // highlight pin
283    var   gpolyline= 0 ;
284
285    // Cesium
286    var   viewer;
287
288    // MapBox
289    var	  mapBox		   =  0;
290    var   mapboxMarkers    = [];
291    var   mapboxPointLayer = 'points';
292    
293    // gallery
294    var   gallery;
295    var   galleryInstance = null;
296    var   galleryHTML  = ''  ;
297    
298    // Story
299    var   converter   = new showdown.Converter();
300          converter.setOption('tables',true);
301          
302    // Event Handling
303    var  hashChangeIgnore  = false; 
304    var  preferMaps        = false; // 2026-07-14 By Default, show photos (David Hwu didn't understand the fa-image button)
305    
306    
307    // Search
308    const photorange_regexp = /^(\d+)-(\d+) of (\d+)$/;
309    var	 takenSet			;
310    var	 thePage	   = {} ; // used by searchCommand
311    var  mapsBox            ; // mapbox for search
312    var  searchCommand = '' ;
313    var  searchResult       ;
314    
315    const colors =
316    { white		 : 'rgba(255, 255, 255, 0.50)'
317    , burgundy	 : 'rgba(200,  48,  48, 0.75)'
318    , yellow	 : 'rgba(255, 255,   0, 0.75)'
319    , dodgerblue : 'rgba( 30, 144, 255, 0.60)'
320    , green      : 'rgba(  0, 255,   0, 0.60)'
321    , black      : 'rgba(  0,   0,   0, 0.50)'
322    , hollow	 : 'rgba(255, 255, 255, 0.05)'
323    }
324    
325    const paints = // displayCluster: 'yes'			  : 'no'
326    {  'circle-color'			: [ colors.burgundy   , colors.burgundy    ]  
327    ,  'circle-radius'			: [	 3				  , 3				   ]
328    ,  'circle-stroke-width'	: [	 1				  , 1				   ]
329    ,  'circle-stroke-color'	: [	 colors.white     , colors.white       ]
330    } ;
331    
332    // https://gist.github.com/chriswhong/8977c0d4e869e9eaf06b4e9fda80f3ab
333    // extend mapboxgl Marker so we can pass in an onClick handler
334    // TODO add popup for hover text
335    class ClickableMarker extends mapboxgl.Marker {
336        constructor() {
337            super();                    // Call the parent class's constructor
338            this._position = null;      // Initialize the class variable
339            this._handleClick = null;   // Initialize the handleClick variable
340        }
341    
342        // Method to set the click handler
343        onClick(handleClick) {
344            clearTimer();
345            this._handleClick = handleClick;
346            return this;
347        }
348    
349        setPosition(position) {
350            this._position = position; // Set the class variable
351            return this;
352        }
353    
354        // Method to handle map click events
355        _onMapClick(e) {
356            const targetElement = e.originalEvent.target;
357            const element = this._element;
358            if (this._handleClick && (targetElement === element || element.contains(targetElement))) {
359                clearTimer();
360                this._handleClick(element, this._position); // Pass the current position
361            }
362        }
363    }
364    
365    // https://developers.google.com/maps/documentation/javascript/adding-a-google-map 
366    async function mapInit() {
367        if ( !gmap ) {
368            (g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}
368apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
369                key: "AIzaSyA23UB_I3EaMXthbDOwoCR7yOWDVEFJlNA",
370                v: "weekly",
371                // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
372                // Add other bootstrap parameters as needed, using camel case.
373            });
374
375            //  Request the needed libraries.
376            const { Map,                  RenderingType } = await google.maps.importLibrary("maps");
377            const { AdvancedMarkerElement,PinElement    } = await google.maps.importLibrary('marker');
378
379            gmap  = new Map(document.getElementById('gmap'),
380                            {   zoom         : 12
381                            ,   center       : {lat: 51.33, lng: -0.71 }
382                            ,   mapTypeId    : 'terrain'  // 'satellite'
383                            ,   zoomControl  : true
384                            ,   mapId        : 'clanmillsMapId'
385                            ,   renderingType: RenderingType.VECTOR
386                            });
387            gpin = new PinElement({});
388            gPin = new PinElement({ background: 'yellow' , scale: 2.0});
389        }
390        mapShow();
391    }
392    
393    // Utility functions
394    function normalizePath(path) {
395        const parts = path.split('/');
396        const stack = [];
397    
398        for (let part in parts ) {
399            part = parts[part];
400            if (part === '.' || part === '') {
401                continue;
402            } else if (part === '..') {
403                if (stack.length > 0) stack.pop();
404            } else {
405                stack.push(part);
406            }
407        }
408    
409        // Join the stack and re-add the leading slash if necessary
410        return (path.startsWith('/') ? path[0] : '') + stack.join('/');
411    }
412    
413    function dateString(d,ds,ts)
414    {
415        function pad(n) {return n<10 ? '0'+n : n}
416        if ( !d ) d = new Date();
417        return d.getUTCFullYear()+ds
418             + pad(d.getUTCMonth()+1)+ds
419             + pad(d.getUTCDate())+' '
420             + pad(d.getUTCHours())+ts
421             + pad(d.getUTCMinutes())+ts
422             + pad(d.getUTCSeconds())+''
423    }
424    function exifDateString(d) {
425        if ( !d ) d = new Date();
426        return dateString(d,':',':')
427    }
428    function isoDateString(d) {
429        if ( !d ) d = new Date();
430        return dateString(d,'-',':')
431    }
432    
433    function JS(next)
434    {
435        return next ? `javascript:buildPage( '${next}')` : next;
436    }
437
438    function toggle(div,button) {
439        let ele               = document.getElementById(div) ;
440        if ( ele ) {
441            ele.style.display = ele.style.display == 'block'      ? 'none'
442                              : ele.classList.contains('tablink') ? 'block-inline'
443                              : 'block'
444                              ;
445            ele               = document.getElementById(button);
446            ele.checked      != ele.checked; 
447        }
448        document.getElementById('displayMap-button').innerHTML 
449                = document.getElementById('map-div').style.display == 'none' 
450                ? '<i class="fa-solid fa-map"></i>'
451                : '<i class="fa-solid fa-image"></i>' 
452                ;
453        document.getElementById('display').innerHTML 
454                = document.getElementById('map-div').style.display == 'none' 
455                ? 'Display Map:'
456                : 'Display Photos:' 
457                ;
458        mapsBoxResize();
459    }
460
461    function toggleMapsPhotos() {
462        mapsBoxResize();
463        if ( Photos.length && preferMaps ) showMaps(); else hideMaps();
464    }
465    
466    function hideMaps() {
467        let map_div = document.getElementById('map-div');
468        if (map_div.style.display == 'block' ) {
469            toggle('gallery-div','gallery-button');
470            toggle('map-div'    ,'map-button'    );
471        }        
472    }
473
474    function showMaps() {
475        let map_div = document.getElementById('map-div');
476        if (map_div.style.display == 'none' ) {
477            toggle('gallery-div','gallery-button');
478            toggle('map-div'    ,'map-button'    );
479        }        
480    }
481
482    function pathResolve(path,leaf) {
483        return leaf[0] == '/' ? leaf : normalizePath(`${path}/${leaf}`);
484    }
485
486    function leafname(path) {
487        path=path.replace(/\/$/, '');         // remove trailing /    (https://stackoverflow.com/questions/6680825/return-string-without-trailing-slash)
488        return path.replace(/.*\//, '');      // https://www.abeautifulsite.net/posts/javascript-functions-for-leafname-and-dirname/
489    }
490
491    function dirname(path) {
492    //  console.log('dirname',path);
493        path=path.replace(/\/$/, '');         // remove trailing /    (https://stackoverflow.com/questions/6680825/return-string-without-trailing-slash)
494        return path.match(/.*\//)[0];            // https://www.abeautifulsite.net/posts/javascript-functions-for-leafname-and-dirname/
495    }
496    
497    function capitalizeTitle(input) {
498        if (!input) return ''; // Handles empty or falsy input
499    
500        const isUpper = (char) => char >= 'A' && char <= 'Z';
501        const isDigit = (char) => char >= '0' && char <= '9';
502    
503        let result = '';
504        let prev   = '';
505    
506        for (const char of input) {
507            if (result) {
508                // Add a space for an uppercase letter or number transition in mid-string
509                if ((isUpper(char) && !isUpper(prev)) || 
510                    (isDigit(char) && !isDigit(prev))) {
511                    result += ' ';
512                }
513            }
514            result += char;
515            prev    = char;
516        }
517    
518        // Fixups for specific cases
519        result = result.replace('Py Con 2010', 'PyCon2010')
520                       .replace('Mc ', 'Mc')
521                       .replace('_ ', '_');
522    
523        return result;
524    }
525    
526    function getHashVariable(variable,location) {
527        let result = '';
528        if ( !location ) location = window.location.hash
529        let query = location.substring(1).split('#');
530        for (let q in query ) {
531            let  pair = query[q].split('=');
532            if ( pair.length > 1 && pair[0] == variable ) {
533                result = pair[1];
534            }
535        }
536        // console.log('getHashVariable',variable,location,result);
537        return result;
538    }
539
540    function objectMerge(base, overwrite) {
541        try {
542            if ( typeof overwrite ==  'string' ) overwrite = JSON.parse(overwrite);
543        } catch(e) {
544            overwrite = 0;
545        }
546        return overwrite ? { ...base, ...overwrite } : base;  
547    }
548
549    function parseGPX(options) {
550        let   geoPoints  = [];
551        let { gpx } = options;
552        if (  gpx ) {
553            const parser  = new DOMParser();
554            let   xmlDoc  = parser.parseFromString(gpx, "text/xml");
555            const trkpts  = Array.from(xmlDoc.getElementsByTagName('trkpt'));
556            trkpts.forEach(trkpt => {
557                let   lat      = trkpt.getAttribute('lat');
558                let   lng      = trkpt.getAttribute('lon');
559                geoPoints.push([ parseFloat(lng),parseFloat(lat)   ]);
560            })
561        }
562        return geoPoints;
563    }
564    
565    function onEachFeature(feature, layer) {
566        // This is where it loops through your features
567        if (feature.properties) {
568            // If there is a properties document we bind a tooltip with your property : name
569            layer.bindTooltip(feature.properties.name);
570        }
571    }
572    
573    function makeMenu(parent,menu)
574    {   
575    //  console.log('makeMenu parent',parent);
576        let titles = menu.titles;
577        let pages  = menu.pages;
578        let span='<span style="table,td { border:0px solid black;border-collapse:collapse; } td { padding:0px; }">'
579        let html = '<center>'+span+'<table>';
580        let tr   = pages.length ==10 ? 3
581             : pages.length >= 8 ? 4
582             : pages.length >= 7 ? 3
583             : pages.length == 5 ? 2
584             : pages.length >  4 ? 3
585             : pages.length == 4 ? 3
586             : 1
587             ;                     // number of cols per row
588        for ( let i=1 ; i < pages.length ; i++ ) {
589            let page   = pages[i];
590            let title  = titles[page] || capitalizeTitle(page);
591            let col    = i%tr -1;
592            html[col] += i % tr ==1 ? '<tr>' : '';
593            let path   = JS(normalizePath(`${parent}/${page}`));
594            let htm    = `<td style="text-align:right;"> ${i} :`
595                  + `</td><td><a href="${path}">` 
596                  +  title
597                  + `</a></td>`;
598        //  console.log('htm=',htm);
599            html += htm;
600            html += i % tr == 0 ? '<tr>' :'<td>&nbsp;&nbsp;</td>';
601        }
602        html += '</table></span></center>';
603        return html
604    }
605    
606    function loadGeoJsonFeatureCollection(geoPoints)
607    {
608        if ( lmap ) {
609            let geojsonFC =
610            {  type     : 'FeatureCollection'
611            , features : [
612                            { type      : 'Feature'
613                            , geometry  :   { type          : 'LineString'
614                                            , coordinates   : geoPoints
615                                            },
616                         }]
617            };
618            L.geoJSON(geojsonFC, { onEachFeature: onEachFeature }).addTo(lmap);
619        }
620    }
621
622    function loadGeoJsonStringLine(geoPoints)
623    {
624        if ( mapBox ) {
625            let geojsonSL = { type : 'geojson'
626                            , data : { type    : 'Feature'
627                                     , geometry: { type        : 'LineString'
628                                                 , coordinates : geoPoints
629                                                 }
630                                     }
631                            };
632            if ( mapBox.getLayer  (mapboxPointLayer) ) mapBox.removeLayer (mapboxPointLayer);                
633            if ( mapBox.getSource (mapboxPointLayer) ) mapBox.removeSource(mapboxPointLayer);                
634            mapBox.addSource(mapboxPointLayer, geojsonSL);
635            mapBox.addLayer(
636            { id        : mapboxPointLayer
637            , type      : 'line'
638            , source    : mapboxPointLayer
639            , layout    : { 'line-join'  : 'round'
640                        ,   'line-cap'   : 'round'
641                        }
642            , paint     : { 'line-color' : 'dodgerblue'
643                        ,   'line-width' : 2
644                        }
645            });
646        }
647    } 
648
649    function mapClear()
650    {
651        // https://stackoverflow.com/questions/14585688/clear-all-polylines-from-leaflet-lmap
652        if ( lmap ) for(i in lmap._layers) if(lmap._layers[i]._path != undefined) {
653            try {
654                lmap.removeLayer(lmap._layers[i]);
655            } catch(e) {
656                console.log("problem with " + e + lmap._layers[i]);
657            }
658            for ( let l in lmarkers ) {
659                lmarkers[l].remove();
660            }
661            lmarkers=[];
662        }
663        if ( mapBox ) {
664            let e;
665            try {
666            //  console.log('in mapClear');
667                for ( let m in mapboxMarkers ) mapboxMarkers[m].remove();
668                mapboxMarkers = [];
669                let l = mapBox.getLayer (mapboxPointLayer); if (l) mapBox.removeLayer (mapboxPointLayer);
670                let s = mapBox.getSource(mapboxPointLayer); if (s) mapBox.removeSource(mapboxPointLayer);
671            //  console.log('OK');
672            } catch (e) { console.log('mapClear',e);   }
673        }
674        if ( gmap ) {
675            // https://stackoverflow.com/questions/1544739/google-maps-api-v3-how-to-remove-all-markers
676            for (let g in gmarkers ) {
677                gmarkers[g].setMap(null);
678            }
679            gmarkers = [] ;
680            if ( gpolyline) gpolyline.setMap(null);
681            gpolyline=0;
682        }
683        Photos         = []; // clear the photos cache
684    }
685    
686    function mapAddMarker(position)
687    {
688        let lat   = position.lat;
689        let lng   = position.lng;
690        let taken = position.taken;
691        let title = position.title;
692        let data  = position.data ;
693        
694        if ( lmap ) {
695            let marker = L.marker([lat,lng],  // L.circleMarker([lat,lng],
696                    { radius        : 10
697                    , fillColor     : 'red'
698                    , color         : '#000'
699                    , weight        : 1
700                    , opacity       : 0.8
701                    , fillOpacity   : 0.5
702                    , taken  , title, data
703                    }
704                ).on('click', (event) => { 
705                        // showPhoto(position.taken)
706                        let taken = event.target.position.taken;
707                        console.log('event',event);
708                        console.log('taken',taken);
709                        showPhoto(taken)
710                });
711            marker.position = position;
712            marker.addTo(lmap);
713            lmarkers.push(marker);
714        }
715
716        if ( gmap ) {
717            let gmarker = new google.maps.marker.AdvancedMarkerElement(
718                { position     
719                , map           : gmap
720                , gmpClickable  : true
721                , title
722                })
723            gmarkers.push(gmarker);
724            gmarker.append(gpin)
725            gmarker.addListener("gmp-click", () => {
726                clearTimer();
727                showPhoto(position.taken);
728            });
729        }
730
731        if ( mapBox ) {
732        //  console.log('add marker to mapbox',lng,lat);
733            let m = new ClickableMarker()
734                    .setLngLat([lng,lat])
735                    .addTo(mapBox)
736                    .setPosition(position)
737                    .onClick(   (element,position) => { 
738                        showPhoto(position.taken)
739                    });
740            mapboxMarkers.push(m);
741            setMapboxMarkerProps(m,false);
742        }
743    }
744    
745    function mapZoom(points)
746    {
747        if ( points.length ) {
748            // bounding box of points
749            let lng    = 0;
750            let lat    = 1;
751            let minlng = points[0][lng];
752            let minlat = points[0][lat];
753            let maxlng = minlng;
754            let maxlat = minlat;
755            points.forEach(point => {
756                minlng = Math.min(minlng,point[lng]); 
757                minlat = Math.min(minlat,point[lat]); 
758                maxlng = Math.max(maxlng,point[lng]); 
759                maxlat = Math.max(maxlat,point[lat]); 
760            })
761
762            let   d    = Math.max(maxlng-minlng,maxlat-minlat);
763            lat =     (minlat+maxlat)/2.0
764            lng =     (minlng+maxlng)/2.0
765            let zoom   = d > 150    ?  2 // Empirical TODO: Compute this
766                       : d > 50     ?  3  
767                       : d < 0.025  ? 15
768                       : d < 0.1    ? 13
769                       : d < 0.2    ? 12
770                       : d < 0.5    ? 11
771                       : d < 1.0    ? 10
772                       : 8
773                       ;
774
775            if ( gmap ) {
776                gmap.setZoom(zoom);
777                gmap.setCenter({lat, lng});
778            }
779            
780            if ( lmap ) {
781                center = new L.LatLng(lat,lng) 
782                lmap.setView(center, zoom); // 11 is a guess, we should calculate the bounding box to deduce value
783            }
784            
785            if ( viewer ) {
786                let altitude = 60000;
787                if ( zoom < 6  ) altitude *= 100;
788                if ( zoom > 12 ) altitude /=  10;
789                viewer.camera.flyTo( {destination: Cesium.Cartesian3.fromDegrees(lng, lat, altitude) })   
790            }
791            
792            if ( mapBox ) {
793            //  console.log('deltas',diff(minlng,maxlng),diff(minlat,maxlat) );
794                let delta = 200/111120 ; //200m because 1degree = 60 nautical miles = 111120m 
795                if ( diff(minlng,maxlng) < delta ) { minlng -= delta; maxlng += delta; }
796                if ( diff(minlat,maxlat) < delta ) { minlat -= delta; maxlat += delta; }
797
798                let south_west = [minlng,minlat];
799                let north_east = [maxlng,maxlat];
800                mapBox.fitBounds  ( [south_west, north_east], { padding: 40 }   ) ;
801            }
802        }
803    }
804
805    function mapDisplayLine(geoPoints)
806    {
807        if ( lmap ) {
808            loadGeoJsonFeatureCollection(geoPoints);
809        }
810        
811        if ( mapBox ) {
812            mapBox.isStyleLoaded() ? loadGeoJsonStringLine(geoPoints) // ready
813                                   : mapBox.on('load', () => { loadGeoJsonStringLine(geoPoints);}  ); // defer
814        }
815
816        if ( gmap ) {
817            let path = [];
818            for ( let geoPoint in geoPoints ) {
819                geoPoint = geoPoints[geoPoint];
820                let lng  = geoPoint[0]; // geoJSON lng is first
821                let lat  = geoPoint[1];
822                path.push( new google.maps.LatLng(lat,lng) );
823            }
824            gpolyline = new google.maps.Polyline(
825                         { path 
826                         , map          : gmap
827                         , strokeColor  : 'dodgerblue'
828                         , strokeWeight :  2
829                         });
830        }
831    }
832    
833    function mapShow()
834    {
835        if ( !mapBox ) {
836            mapBox = new mapboxgl.Map(
837            {	container	: 'mapbox'
838            ,	style		: 'mapbox://styles/mapbox/streets-v9'
839            ,	projection	: 'globe'
840            });
841        
842            mapBox.addControl(new mapboxgl.NavigationControl());
843            mapBox.addControl(new mapboxgl.FullscreenControl());
844            mapBox.scrollZoom.enable();
845            mapBox.on('mouseup'  ,  () => { userInteracting = false;});
846            mapBox.on('mousedown',  () => { userInteracting = true; });
847            mapBox.on('dragstart',  () => { userInteracting = true; });
848            mapBox.on('moveend',    () => { spinGlobe();            });
849            
850            spinGlobe();
851        } 
852        
853        if ( !lmap ) {
854            lmap  = L.map('leaflet');
855            popup = new L.Popup()
856            const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
857                maxZoom: 19,
858                attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
859            }).addTo(lmap);
860        }
861        
862        if ( !viewer ) {
863            // https://cesium.com/learn/cesiumjs-learn/cesiumjs-quickstart/
864            // Your access token can be found at: https://ion.cesium.com/tokens.
865            // Replace `your_access_token` with your Cesium ion access token.
866            Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzOTQ3MjExYy04MmM2LTQzNDktODY' +
867            'wMS03OTU0ODhkMmZmYjgiLCJpZCI6MTYzNzM5LCJpYXQiOjE2OTM0NzczND
867d9.Z0Ha0htjdwTEHc4UchwiJ1IKzsvpv9hlxrsyOhYfHuw';
868        
869            // Initialize the Cesium Viewer in the HTML element with the `cesiumContainer` ID.
870            viewer = new Cesium.Viewer('cesium', {
871              terrain: Cesium.Terrain.fromWorldTerrain(),
872            });    
873            // Add Cesium OSM Buildings, a global 3D buildings layer.
874            //const buildingTileset = await Cesium.createOsmBuildingsAsync();
875            //viewer.scene.primitives.add(buildingTileset);   
876        }
877    }
878
879    function readObjectsXML(options)
880    {
881        let result; // the taken date of final object
882        mapClear();
883  
884        let xmlData    = `<objects>${options.objects}</objects>`
885        // Fetch object elements
886        const parser   = new DOMParser();
887        const xmlDoc   = parser.parseFromString(xmlData, "text/xml");
888        const objects  = Array.from(xmlDoc.getElementsByTagName('object')); 
889        let geoPoints  = [];
890        
891        galleryHTML = '';
892        for (let object in objects ) {
893            object = objects[object];
894            const data     = object.getAttribute('data'); // .replace(/=w\d*-h\d*/i,'=w600') ; // 'https://lh3.googleusercont...DV-m=w1920-h1080 => 'https://lh3.googleusercont...DV-m=w600 => '
895            let   title    = object.getAttribute('title');
896            let   endRow   = object.getAttribute('end-of-row')==1;
897            let   lat      = object.getAttribute('lat');
898            let   lng      = object.getAttribute('lon');
899            let   taken    = object.getAttribute('taken');
900            title          = title ? title : '';
901            taken          = taken ? taken : '';
902            
903            if ( taken ) {
904                result = taken;
905                lng = parseFloat(lng);
906                lat = parseFloat(lat);
907                if ( lat && lng ) {
908                    let image = new Image();
909                    let index = Photos.length;
910                    // tweak src = =w1920-h1080 to correct size
911
912                    Photos.push({ taken ,title, data,index,image
913                                , point : [lat,lng]
914                                });
915                //  console.log('data',data);
916                    image.src = data ;
917                    geoPoints.push([ lng, lat ]); // geoJSON needs lng,lat
918                    mapAddMarker({lat,lng,taken,title,data})
919                }
920            }
921
922            let   thumb    = data;
923            if ( objects.length - object > 4 && thumb.indexOf('=') != -1 && !endRow ) {
924                 let s = thumb.split('=') 
925                 if ( s[1] == 'w1920-h1080' ) thumb = s[0] + '=w240-h180';
926            }            
927            let   classRow = endRow ? "gallery-item end-of-row" : "gallery-item";
928            let   alt      = `${title}<span style='line-height:5px;font-size:60%;'><br>${taken}</span>`
929            let   html     = `<a href="${data}" data-lg-size="1920-1080" data-lg-title="${title}" class="${classRow}">` +
930                                `<img src="${thumb}" alt="${alt}"/>`+
931                             `</a>`
932        //  console.log('html',html);
933            galleryHTML += html;
934        }
935    //  console.log('galleryHTML',galleryHTML);
936
937        if ( 'gpx' in options ) geoPoints = parseGPX(options);
938
939        if ( geoPoints.length ) {
940            mapZoom(geoPoints);
941            mapDisplayLine(geoPoints);
942        }
943        toggleMapsPhotos()
944        
945        // if there are objects, show photo-div
946        document.getElementById('photo-div').style.display = objects.length ? 'block' : 'none';
947        document.getElementById('ribbon'   ).style.display = Photos.length  ? 'block' : 'none';
948        return result;
949    }  
950
951    function hideById(ids) {
952        if ( typeof ids == typeof [] ) {
953            for ( let id in ids ) 
954                hideById(ids[id]);
955        } else {
956            let ele = document.getElementById(ids);
957            if (ele) {
958                ele.style.display='none';
959                ele.innerHTML    = ''   ;
960            }
961        }
962    }
963    
964    function unhideById(ids,display) {
965        if ( typeof ids == typeof [] ) {
966            for ( let id in ids ) 
967                unhideById(ids[id],display);
968        } else {
969            let ele = document.getElementById(ids);
970            if (ele) {
971                ele.style.display= display || 'block';
972                ele.innerHTML    = ''   ;
973            }
974        }
975    }
976    
977    function modifyHTML(html, path) {
978        // Early return if html or path is not provided, or if html contains "javascript:"
979        if (!html || !path  || html.includes('javascript:') ) return html;
980    
981        // Create a DOM parser and parse the HTML
982        const parser = new DOMParser();
983        const dom    = parser.parseFromString(html, 'text/html');
984    
985        // Define an object mapping tags to their relevant attributes
986        let tagNames = { a: 'href', img: 'src' , source : 'src' };
987    
988        // Loop over each tag type and modify their attributes
989        for (let tagName in tagNames) {
990            let attrName = tagNames[tagName];
991            const tags = dom.getElementsByTagName(tagName);
992            
993            for (let tag of tags) {
994                let  attrValue = tag.getAttribute(attrName);
995                if ( attrValue && !attrValue.includes('http') ) {
996                    if (attrValue && attrValue[0] !== '/' )    {
997                        attrValue = normalizePath(`${path}/${attrValue}`);
998                        tag.setAttribute(attrName, attrValue);
999                    //  console.log(attrName + ' changed', attrValue);
1000                    }
1001        
1002                    // Specific modification for <a> tags
1003                    if (tagName === 'a' && !tag.classList.contains('popup-link')) {
1004                        // Wrap the normalized path in a JS function call
1005                        tag.setAttribute('href', `javascript:buildPage( '${attrValue}' )`);
1006                    //  console.log('href modified to JS call:', attrValue);
1007                    }
1008                }
1009            }
1010        }
1011    
1012        // Return the modified HTML as a string
1013        let result = dom.body.innerHTML;
1014    //  console.log('modifyHTML result = ', result);
1015        
1016        return result;
1017    }
1018
1019    function showById(id,html,path) {
1020        let  ele = document.getElementById(id); 
1021        if ( ele ) {
1022            ele.style.display = 'block';
1023            if ( html && path ) ele.innerHTML  = modifyHTML(html,path);
1024        }
1025    }
1026    
1027    // https://www.youtube.com/watch?v=AoBSB00vW5A&list=PL4cUxeGkcC9jx2TTZk3IGWKSbtugYdrlu&index=11
1028    async function getDataForPath(path, options) {
1029        try {
1030            let json_path  = normalizePath(`${path}/default.json`);
1031            let json       = '';
1032            let leaf       = leafname(path);
1033            let parent     = dirname(path);
1034            let response;
1035
1036            if ( ! directoryTree ) {
1037                response = await fetch('/directoryTree.json');
1038                if (response.ok) directoryTree = JSON.parse(await response.text());
1039            }
1040            let fileSystem = findDirectorySubtree(directoryTree, path)
1041
1042            // Load /Menu.inc /about.inc and directoryTree.json
1043            if (!document.getElementById('Menu').innerHTML) {
1044                response = await fetch('/Menu.inc');
1045                if (response.ok) {
1046                    document.getElementById('Menu').innerHTML = await response.text();
1047                }
1048            }
1049            if (!document.getElementById('about').innerHTML) {
1050                let about_path = '/about.inc';
1051                response = await fetch(about_path);
1052                if (response.ok) {
1053                    document.getElementById('about').innerHTML = modifyHTML(await response.text(), dirname(about_path));
1054                    document.getElementById('cyear').innerHTML = isoDateString().split('-')[0]
1055                }
1056            }
1057
1058            if (searchCommand) {
1059            //  console.log('searchCommand = ',searchCommand);
1060                response      = await fetch(searchCommand);
1061                searchCommand = '' // search complete
1062                
1063                let  what = document.getElementById('search' ).value;
1064                let  data = response.ok ? await response.text() : '';
1065                if ( data ) {
1066                //  console.log('data=',data);
1067            
1068                    // remove lines that don't start with an '/' or '<'
1069                    let lines   = data.split('\n');
1070                    data    = ''
1071                    for ( line in lines ) {
1072                        line = lines[line];
1073                        ch   = line.substring(0,1);
1074                        if  ( ch in {'<':1 , '/':1} ) {
1075                            data += line + '\n'
1076                        } else {
1077                        //  if ( line.length ) console.log('line',line)
1078                            if ( line.match(photorange_regexp) ) document.getElementById('photorangestr
1078ing').innerHTML = line;
1079                        }
1080                    }
1081                //  console.log('getDataForPath data =',data)
1082                    if ( data.length )  {
1083                        thePage['story'  ] = '';
1084                        thePage['title'  ] = 'Search Results'
1085                        if ( what == "Stories" ) {
1086                            lines = data.split('\n')
1087                            story = ''
1088                            for ( line in lines ) {
1089                                line = lines[line];
1090                                if ( line.replace(' ','').length ) {  
1091                                    story += `<a href="${line}">${line}</a><br>\n`
1092                                }
1093                            }
1094                        //  console.log('stories = ',story);
1095                            thePage['story']=story;
1096                        } else {
1097                            thePage['objects'] = data;
1098                            thePage['photos' ] = '/search'	;
1099                        }
1100                    } else {
1101                        now	 = exifDateString();
1102                        data = '<object data="/gifs/NotFound.png" title="Nothing Found" taken="' +now+ '"></object>\n';
1103                        thePage['objects'] = data;
1104                        thePage['photos' ] = '/search'	;
1105                    }
1106                }
1107            //  console.log('thePage',thePage);
1108                searchResult = objectMerge({ leaf, parent, path, fileSystem, tech: options.tech }, thePage );
1109                return searchResult ;
1110            } else {
1111                // Read default.json
1112                response = await fetch(json_path);
1113                if (response.ok) {
1114                    json = await response.text();
1115                    if (json) {
1116                        json = JSON.parse(json);
1117                        json.story   = json.story   || 'story.txt'; // Set default story = 'story.txt'
1118                        json.objects = json.objects || 'photos.txt'; // Set default photos = 'story.txt'
1119                        json.family  = normalizePath(path).split('/')[1]
1120        
1121                        // Process keys that reference files
1122                        let files = { 
1123                            story:  'txt', objects: 'txt', gpx:   'txt'   , epilog: 'txt', 
1124                            prolog: 'txt', menu:   'json', pages: 'json'
1125                        };
1126                        json['files'] = {}; // Store paths in json for debugging
1127                        for (let file in files) {
1128                            if (file in json && json[file]) {
1129                                let value = json[file];
1130                                let filepath = pathResolve(path, value);
1131                                json['files'][file] = filepath;
1132        
1133                                response = await fetch(filepath);
1134                                if (response.ok) {
1135                                    json[file] = await response.text();
1136                                    if (files[file] === 'json') {
1137                                        json[file] = JSON.parse(json[file]);
1138                                        json[file]['homepage'] = normalizePath(`${dirname(filepath)}/Homepage`);
1139                                    }
1140                                } else {
1141                                    if ( files[file] == 'txt'  ) {
1142                                        console.log(`FORGIVEN file: ${filepath} key: ${file} status: ${response.status}`)
1143                                        json[file] =  ''; // treat missing text file as the empty string
1144                                    } else {
1145                                        throw `file not found ${filepath}` ;
1146                                        //TODO display JSFrame for 5 seconds for page not found!
1147                                    }
1148                                }
1149                            }
1150                        }
1151                    }
1152                    return objectMerge({ leaf, parent, path, fileSystem, tech: options.tech }, json || {});
1153                } else throw `file not found ${json_path}`
1154            }
1155        } catch (error) {
1156            console.error(`Error fetching data for path: ${path}`, error);
1157            return {};
1158        }
1159    }
1160    
1161    function getAlbumData(path, options) {
1162        // Asynchronous operation using promises
1163        return getDataForPath(path, options)
1164            .then(data => {
1165                console.log('getAlbumData', data);
1166                return data;
1167            })
1168            .catch(error => {
1169                console.error('Error in getAlbumData:', error);
1170                return null;
1171            });
1172    }
1173    
1174    function setTech(t)
1175    {
1176        tech      = t;
1177        let techs = { gmap :'none' , leaflet : 'none' , mapbox : 'none' , cesium :'none' }
1178        techs       [ tech ] = 'block';
1179        for ( t in techs ) document.getElementById(t).style.display = techs[t] ;
1180        mapsBoxResize();
1181    }
1182    
1183    function findDirectorySubtree(node, targetPath) {
1184      // Helper function to recursively search for a path
1185      function search(node, pathComponents, currentPath) {
1186        // Return the node if all path components are matched
1187        if (pathComponents.length === 0) {
1188          return { ...node, pathname: currentPath }; // Add pathname to the node
1189        }
1190    
1191        // If the node has children, continue searching
1192        if (node.children) {
1193          const [firstPart, ...remainingParts] = pathComponents;
1194          for (const child of node.children) {
1195            if (child.name === firstPart) {
1196              const newPath = `${currentPath}/${child.name}`; // Construct the new path
1197              const result = search(child, remainingParts, newPath);
1198              if (result) {
1199                return result;
1200              }
1201            }
1202          }
1203        }
1204    
1205        // Return null if the path is not found
1206        return null;
1207      }
1208    
1209      // Split the target path into components, ignoring empty ones
1210      const pathComponents = targetPath.split('/').filter(Boolean);
1211      const initialPath = ''; // Start with an empty path
1212      return search(node, pathComponents, initialPath);
1213    }
1214
1215    async function buildPage(page)
1216    {
1217        try {
1218        //  console.log('buildPage page=',page)
1219            path = ((!page) || page == '/') ? defaults.path : page;
1220            if ( path.startsWith('/search')  ) {
1221                // document.getElementById('query').value = path;
1222                path = '/search'
1223            }
1224        //  console.log('path',path);
1225
1226            path = normalizePath(path);
1227            let options = { path, tech }
1228
1229            // asynchronously pull in the data, then use it
1230            result = await getAlbumData(path,options);
1231            if ( ! ('path' in result) ) {
1232                // 404
1233                path     = defaults.fallback;
1234                options  = objectMerge({path},options)
1235                console.log(`buildPage: no data for ${page} Will use ${path}`);
1236                result   = await getAlbumData(path,options);
1237            }
1238            options = objectMerge(options,result);
1239            
1240            // clean  up
1241            clearTimer();
1242            closeFrame();
1243            hideById(['prolog','epilog','menu'])
1244            document.getElementById('search-div'  ).style.visibility = path == '/search' ? 'visible' : 'hidden'   ;
1245            document.getElementById('search-div'  ).style.display    = path == '/search' ? 'block'   : 'none'     ;
1246            document.getElementById('photorange'  ).style.visibility = 'hidden' ; // make this visible after the search is done
1247
1248            // https://stackoverflow.com/questions/10970078/modifying-a-query-string-without-reloading-the-page
1249            let href             = window.location.href;
1250            href                 = href.substring(0,href.indexOf('#'))
1251            hashChangeIgnore     = true;
1252            window.location.href = href + `#path=${options.path}`;
1253
1254            // display tech and show
1255            setTech(options.tech);
1256            mapShow();
1257            
1258            // ---------------- Map -----------------------
1259            let taken = readObjectsXML(options)
1260            
1261            // ---------------- Story -----------------------
1262            options.story   = converter.makeHtml(options.story);
1263            
1264            let prev = '', next = '' , up = '' , label = '',  link =''  , arrows  = '' ; // empty strings
1265            // destructure options into local namespace
1266            let {  cols, fileSystem, title,  photos , maps,  strava, osmaps, pdf, leaf, epilog, prolog, date, parent, menu,family } = options;
1267            if ( ! cols ) cols = 2;
1268            if ( taken && taken.includes(' ')) date = taken.split(' ')[0]
1269            let tdate   = date; // treat all dates the same
1270            let updated = date;
1271            menu        = menu ? makeMenu(dirname(menu.homepage),menu) : '';
1272
1273            // If no title, use the leaf
1274            if ( ! title ) title   = capitalizeTitle(options.leaf);
1275            
1276            if ( photos || maps || strava || osmaps || pdf ) {
1277                let  links  = 0;
1278                if ( photos ) links++ ; if ( maps ) links ++ ; if (strava) links++ ; if ( osmaps ) links++; if ( pdf ) links++;
1279                let br = links == 4 ? '<br>' : '' ;
1280                link   = '<p style="color:yellow;margin-bottom:0px;">' +
1281                    (photos != '/search' ? 'Photos: <a id="photos" target="_blank">click here</a>.&nbsp;&nbsp;':'')+
1282                    (maps   ? 'Maps:   <a id="maps"   target="_blank">click here</a>.&nbsp;&nbsp;':'')+ br + 
1283                    (strava ? 'Strava: <a id="strava" target="_blank">click here</a>.&nbsp;&nbsp;':'')+
1284                    (osmaps ? 'OSMaps: <a id="osmaps" target="_blank">click here</a>.&nbsp;&nbsp;':'')+ br +
1285                    (pdf    ? 'PDF:    <a id="pdf"    target="_blank">click here</a>.&nbsp;&nbsp;':'')+
1286                '</p>';
1287            }                  
1288            if ( options.story && link ) link = '<hr class="hrs"/>'+link+'<hr class="hrs"/>'                    
1289
1290            // show the correct column
1291            hideById(['cols','cols2']);
1292            cols              = cols==2 ? 'cols2' : 'cols';
1293            options.story     = options.story.replace('<p>
1293','<p style="margin-block-start:0px;">') ;
1294            showById(cols,options.story + link,options.path)
1295            
1296            // ---------------- Menu and Arrows -----------------------
1297
1298            //                         |               ^
1299            // Label             TITLE V            <- | ->
1300            if ( leaf && 'pages' in options && 'pages' in options.pages && 'group' in options.pages ) {
1301                let nPages      = options.pages.pages.length -1 ;
1302                let nPage       = options.pages.pages.indexOf(leaf) ;
1303                let pageOfPages = nPage ? `<i style="font-size:80%;font-weight:normal;">&nbsp;(${nPage}/${nPages})&nbsp;</i>` : '';
1304                label           = options.pages.group + pageOfPages;
1305                if ( 'titles'  in options.pages && leaf in options.pages.titles ) with ( options.pages ) title = titles[leaf];
1306                if ( 'dates'   in options.pages )                                 with ( options.pages ) date  = leaf in dates ? dates[leaf] : dates['Homepage'];
1307
1308                // arrows
1309                up   = normalizePath(parent + '/Homepage') 
1310
1311                prev = options.pages.pages.indexOf(leaf)-1
1312                if ( prev < 0 ) prev = 0;
1313                prev = normalizePath(dirname(path) +'/'+ options.pages.pages[prev])
1314
1315                next = options.pages.pages.indexOf(leaf)+1
1316                if ( next > nPages ) next = 0;
1317                next = normalizePath(dirname(path) +'/'+ options.pages.pages[next])
1318                
1319                date =  leaf  in options.pages.dates ? options.pages.dates[leaf]
1320                     : 'Date' in options.pages       ? options.pages.Date
1321                     : '' ;
1322            }
1323            // create arrows on homepage and generate menu
1324            if ( leaf == 'Homepage' ) {
1325                menu                 = makeMenu(options.parent,options.pages);
1326                down                 = JS(normalizePath(`${options.parent}/${options.pages.pages[1]}`));
1327                title                = label + `<a id="down" class="butn fa-solid fa-arrow-down" href="${down}" style="font-size:100%;" />` ; 
1328                label                = 0    ; // cancel the label
1329                prev                 = ''
1330                next                 = ''
1331                up                   = normalizePath(dirname(parent)+'/Homepage') 
1332            } 
1333
1334            // if up/default.json does not exist, go to fallback
1335            if ( !findDirectorySubtree(directoryTree,`${up}/default.json`)) {
1336                let year = path.startsWith('/20') ? path.substring(1,5) : ''; // if it's a year
1337                up       = year ? `/Homepages/Albums/${year}` : defaults.fallback;
1338            }
1339        //  console.log('path,up',path,up);
1340            if ('options' in options && 'menu' in options.options && options.options.menu == 'prolog') { prolog = menu; menu = '' } // use in /Family/Homepage
1341           
1342            arrows =  (prev?'<a id="prev" class="butn fa-solid fa-arrow-left"  style="width:10px;font-size:100%;"/>' : '') +
1343                      (up  ?'<a id="up"   class="butn fa-solid fa-arrow-up"    style="width:10px;font-size:100%;"/>' : '') + 
1344                      (next?'<a id="next" class="butn fa-solid fa-arrow-right" style="width:10px;font-size:100%;"/>' : '') +
1345                      ''; 
1346                      
1347            // display the arrows and set hrefs
1348            document.getElementById('arrows').innerHTML = arrows ?  arrows : '' 
1349            if ( prev && document.getElementById('prev' ) ) document.getElementById('prev').setAttribute('href',JS(prev) );
1350            if ( next && document.getElementById('next' ) ) document.getElementById('next').setAttribute('href',JS(next) );
1351            if ( up   && document.getElementById('up'   ) ) document.getElementById('up'  ).setAttribute('href',JS(up  ) );
1352            
1353            let show_hide = up ? unhideById : hideById;
1354            show_hide(['down','up'],'block-inline');
1355            
1356            // ---------------- UI elements prolog, date, links etc -----------------------
1357
1358            showById('menu'  ,menu  ,options.path);
1359            showById('prolog',prolog,options.path);
1360            showById('epilog',epilog,options.path);
1361
1362            // update links
1363            if ( document.getElementById('photos') ) document.getElementById('photos').setAttribute('href',photos);
1364            if ( document.getElementById('maps'  ) ) document.getElementById('maps')  .setAttribute('href','https://www.google.com/maps/d/edit?mid='+maps+'&usp=sharing&z=14');
1365            if ( document.getElementById('strava') ) document.getElementById('strava').setAttribute('href','https://www.strava.com/activities/'+strava);
1366            if ( document.getElementById('osmaps') ) document.getElementById('osmaps').setAttribute('href','https://explore.osmaps.com/en/route/'+osmaps);
1367            if ( document.getElementById('pdf'   ) ) document.getElementById('pdf'   ).setAttribute('href',pdf);
1368            
1369            document.getElementById('banner' ).innerHTML = document.getElementById('banner').innerHTML.replace('clanmills.com',window.location.hostname);
1370            document.getElementById('date'   ).innerHTML = date  ? date                  : '' ;
1371            document.getElementById('updated').innerHTML = date  ? ( 'Updated: ' + date) : '' ;
1372            document.getElementById('label'  ).innerHTML = label ? label                 : '' ;
1373            document.getElementById('title'  ).innerHTML = title ? title                 : '' ;
1374        
1375            document.title                               = (window.location.hostname + ' - ' + title.replace('&amp;','&')).replace(/<[^>]+>/g, '');
1376
1377            // ---------------- Search -----------------------
1378
1379            if ( result.path == '/search' ) {
1380                document.getElementById('photorange'  ).style.visibility = 'visible';
1381                document.getElementById('ribbon'      ).style.display    = 'block'  ;
1382                preferMaps = false;
1383            //  console.log('show ribbon and photorange');
1384                
1385                // if the photorangestring says "1 to N of N", hide it
1386                let  match = document.getElementById('photorangestring').innerHTML.match(photorange_regexp);
1387                if ( match ) document.getElementById('photorange').style.visibility = match[1] == '1' && match [2] == match[3] ? 'hidden' : 'visible' ;
1388                // if we've loaded the /search page, options.files.story == '/search/story.txt') 
1389                if ( 'files' in options )  {
1390                    reallySearch(); // execute the search
1391                }
1392            }
1393
1394            // ---------------- Gallery -----------------------
1395
1396            // 1. destroy galleryInstance
1397            if (galleryInstance) {
1398                galleryInstance.destroy();
1399                galleryInstance = null;
1400            }
1401            
1402            // 2. wipe gallery and reset the layout plugin
1403            $('#gallery').empty().justifiedGallery('destroy');
1404            
1405            // 3. inject galleryHTML
1406            $('#gallery').append(galleryHTML);
1407            
1408            // 4. rebuild layout
1409            $("#gallery").justifiedGallery({
1410                rowHeight : 120,
1411                lastRow   : 'justify',
1412                margins   : 5
1413            })
1414            .off('jg.complete')
1415            .on ('jg.complete', () => {
1416                /*
1417                // Add event listeners for LightGallery events
1418                gallery.addEventListener('onBeforeOpen', function() {
1419                    alert('Gallery is about to open');
1420                }, false);
1421        
1422                gallery.addEventListener('onBeforeSlide', function(event) {
1423                    console.log('Slide Index:', event.detail.index);
1424                    console.log('From Touch:', event.detail.fromTouch);
1425                    console.log('From Thumbnail:', event.detail.fromThumb);
1426                }, false);
1427        
1428                gallery.addEventListener('onSlideItemLoad', function(event) {
1429                    console.log('Photo selected:', event.detail);
1430                }, false);
1431        
1432                gallery.addEventListener('onCurrentSlide', function(event) {
1433                    console.log('Current slide index:', event.detail.index);
1434                }, false);            
1435                */
1436                // 5. Re-attach LightGallery and store the instance
1437                galleryInstance = lightGallery(gallery, {
1438                    selector    : 'a',
1439                    download    : false,
1440                    plugins     : [lgThumbnail,lgRotate,lgShare,lgAutoplay], // desired plugins
1441                    thumbnail   : true,
1442
1443                    // optional settings for plugins
1444                    zoom        : true,
1445                    actualSize  : true,
1446                    autoplay    : true, // Starts the slideshow automatically when opened
1447                    pause       : 3000,    // milli seconds per slide
1448                    progressBar : true
1449                });
1450                
1451                /*
1452                let pollingInterval = setInterval(() => {
1453                    if (galleryInstance) {
1454                        console.log('Current gallery index:', galleryInstance.lgOpened ? galleryInstance.index : -1);
1455                    }
1456                }, 2000);
1457        
1458                gallery.addEventListener('onCloseAfter', () => {
1459                    clearInterval(pollingInterval);
1460                    console.log('Stopped polling gallery instance.');
1461                }, false);
1462                */
1463                mapsBoxResize();
1464            });
1465            
1466        // ---------------- error recovery -----------------------
1467        } catch (error) {
1468            console.error(`Error caught in buildPage for ${page}`, error);
1469        }
1470    }
1471    
1472    function togglePhotosMap(button)
1473    {
1474        if ( button ) preferMaps = button.innerHTML.includes('fa-map');
1475        toggle('gallery-div','gallery-button');
1476        toggle('map-div'    ,'map-button'    );
1477    }
1478    
1479    function eval_href(href) {
1480        if (href.startsWith('javascript:')) {
1481            const code = href.slice('javascript:'.length);
1482            eval (code);
1483        } else document.location = href;            
1484    }
1485
1486    function move(n) // 'fa-arrow-left' or 'fa-house' etc...
1487    {
1488    //  console.log('move',n)
1489        if ( n ) try {
1490            let   as = document.getElementsByTagName('a');
1491            for ( let a in as ) {
1492                let   A =  as[a];
1493                if (  A.classList.contains(n) ) {
1494                    eval_href(A.href);
1495                }
1496            }
1497        } catch (e) {};
1498    }
1499    
1500    function keyUp(event) {
1501        let query = 0;
1502        try {
1503            if ( !event ) event = window.event;
1504            if ( !event.keyCode ) return ;
1505            query = document.getElementById('query');
1506        } catch (e) {};
1507    
1508        listen = event.altKey || ( event.shiftKey && (  (event.keyCode >= 32 && event.keyCode <= 40) || [118,119,120].includes(event.keyCode) )  ); // option or shift + arrow
1509    //  window.alert(`listen,event.keyCode = ${listen},${event.keyCode}`);
1510        if ( event.target == query && event.keyCode == 13 ) {
1511            reallySearch() ;
1512        } else if ( listen ) switch ( event.keyCode ) {
1513            case  32 : togglePhotosMap('')    ; break; // ' '
1514            case  36 : buildPage(defaults.path)  ; break; // home 
1515            case  37 : move('fa-arrow-left' ) ; break; // <-
1516            case  39 : move('fa-arrow-right') ; break; // ->
1517            case  33 : /* drop */                      // page up
1518            case  38 : move('fa-arrow-up'   ) ; break; // ^
1519            case  40 : move('fa-arrow-down' ) ; break; // v
1520            case 118 : buttonClick(event, -2) ; break; // <<
1521            case 119 : buttonClick(event,  0) ; break; // >||
1522            case 120 : buttonClick(event,  2) ; break; // >>
1523        }
1524        
1525        if ( listen && event.target != query && document.getElementById('photorange').style.visibility == 'visible' ) switch(event.keyCode) {
1526            case  37 : reallySearch(-1) ; break; // <-
1527            case  39 : reallySearch(+1) ; break; // ->
1528        }
1529        return !listen;
1530    }
1531    
1532    function setClock() {
1533        if ( document.getElementById('svg-clock') ) {
1534            let   d = new Date();
1535            const b = true; // bClunk = only move minute hand at the top of the hour
1536            
1537            // https://commons.wikimedia.org/wiki/File:Animated_analog_SVG_clock.svg
1538            let   s = d.getSeconds() ;
1539            let   m = d.getMinutes() + (b ? 0 : s / 60) ;
1540            let   h = d.getHours()   + (m         / 60) ;
1541
1542            if (  b && s ==1 ) return ; //pause at the top
1543            
1544            s *=  6; // degrees
1545            m *=  6;
1546            h *= 30;
1547            
1548            // if (s != 6 ) // pause at the top for one second
1549            document.querySelector('.clock-second-hand').setAttribute('transform', `rotate( ${s}  , 400, 400)`); 
1550            document.querySelector('.clock-minute-hand').setAttribute('transform', `rotate( ${m}  , 400, 400)`);
1551            document.querySelector('.clock-hour-hand'  ).setAttribute('transform', `rotate( ${h}  , 400, 400)`);
1552        }
1553
1554        // console.log('**>',document.getElementById('label').offsetHeight);
1555        if ( document.getElementById('label').offsetHeight > 40 ) {
1556            let scale = ['title','label','up','down','prev','next' ];
1557            let arrows =                ['up','down','prev','next']
1558            for ( let s in scale ) {
1559                s = scale[s];
1560                try {
1561                    // console.log(s,document.getElementById(s).style['font-size'])
1562                    if ( document.getElementById(s) )
1563                        document.getElementById(s).style['font-size'] = s in arrows ? '90%' : '14px';
1564                } catch(e) {
1565                    console.log('***'+s,e);
1566                }
1567            }
1568        }
1569    }
1570    
1571    // Function to update dynamic content and set up universal popup handling
1572    function setupMagnificPopup() {
1573        // Delegate click event to the body for any .popup-link
1574        $('body').on('click', '.popup-link', function(event) {
1575            // Prevent default link behavior
1576            event.preventDefault();
1577    
1578            // Use a jQuery object to target the current element
1579            $(this).magnificPopup({
1580                type: 'image',
1581                mainClass: 'mfp-with-zoom',
1582                alignTop: true,
1583                closeOnBgClick: true,
1584                enableEscapeKey: true,
1585                showCloseBtn: true,
1586                zoom: {
1587                    enabled: true,
1588                    duration: 750,
1589                    easing: 'ease-in-out'
1590                },
1591                callbacks: {
1592                    beforeOpen: function () {
1593                        windowHeight = $(window.parent).height();
1594                        iFrameScrollPos = $(window.parent.document).scrollTop();
1595                    },
1596                    open: function () {
1597                        $('.mfp-bg').css({'background': 'transparent'});
1598                    }
1599                }
1600            }).magnificPopup('open'); // Open popup 
1601        });
1602    }
1603    
1604    $(document).ready( () => {
1605        setInterval(setClock, 1000);
1606        gallery = document.getElementById('gallery');
1607        
1608        // use the path in the URL instead of defaults['path'];
1609        if ( getHashVariable('path') ) path = getHashVariable('path');
1610
1611        mapInit().then((response) => {
1612            buildPage(path);
1613        }).catch((err) => {
1614            console.log('mapInit rejected', err);
1615        });
1616        
1617        document.getElementById('query'  ).value = defaults.search;
1618        
1619        // silence messages from the gallery
1620        function handleTouchStart(event) {}
1621        gallery.addEventListener('touchstart', handleTouchStart, { passive: true });
1622
1623        document.addEventListener("keyup", keyUp);
1624        window.addEventListener  ('hashchange', function(event) {
1625        //  console.log('hashchange hashChangeIgnore',hashChangeIgnore, event);
1626            if ( !hashChangeIgnore ) {
1627                let page = getHashVariable('path',event.newURL);
1628                if ( page == '/search&search' ) {
1629                    // Create a new URL object
1630                    // Extract the hash part and create a new URLSearchParams object
1631                    const url        = new URL(location);
1632                    const hashParams = new URLSearchParams(url.hash.substring(1));
1633                    document.getElementById('query').value = hashParams.get('query');
1634                    page = '/search'
1635                }
1636                buildPage(page);
1637            }
1638            hashChangeIgnore = false;
1639        })
1640        
1641
1642        setupMagnificPopup();
1643        showMapsBox();
1644    });
1645
1646    // Boilder plate mapboxGL code
1647    const YOUR_ACCESS_TOKEN = 'pk.eyJ1Ijoicm9iaW53bWlsbHMiLCJhIjoiY201YmFlaGxjMWFqazJvczd2eGZmdzRjcyJ9.FUZg-C8tfdcoMG6NuxBjlw';
1648    mapboxgl.accessToken = YOUR_ACCESS_TOKEN;
1649
1650    // The following values can be changed to control rotation speed:
1651    var userInteracting = false;
1652    var spinEnabled		= true;
1653    
1654    // At low zooms, complete a revolution every two minutes.
1655    const secondsPerRevolution = 120;
1656    // Above zoom level 5, do not rotate.
1657    const maxSpinZoom = 5;
1658    // Rotate at intermediate speeds between zoom levels 3 and 5.
1659    const slowSpinZoom = 3;
1660
1661    function spinGlobe() {
1662        if ( !mapBox ) return;
1663        const zoom = mapBox.getZoom();
1664        if (spinEnabled && !userInteracting && zoom < maxSpinZoom) {
1665            let distancePerSecond = 360 / secondsPerRevolution;
1666            if (zoom > slowSpinZoom) {
1667                // Slow spinning at higher zooms
1668                const zoomDif = (maxSpinZoom - zoom) / (maxSpinZoom - slowSpinZoom);
1669                distancePerSecond *= zoomDif;
1670            }
1671            const center = mapBox.getCenter();
1672            center.lng -= distancePerSecond;
1673            // Smoothly animate the map over one second.
1674            // When this animation is complete, it calls a 'moveend' event.
1675            mapBox.easeTo({ center, duration: 1000, easing: (n) => n });
1676        }
1677    }
1678
1679    //  JSFrame code
1680    function buttonClick(event,forward)
1681    {
1682        let running   = clearTimer();
1683        let bSetTimer = false;
1684        let change    = 0;
1685        try {
1686            switch ( forward ) {
1687                case -3 : showPhoto(0)              ;  break; // |<< move to first photo and stop
1688                case  3 : showPhoto(Photos.length-1);  break; // |<< move to last photo and stop
1689                
1690                case -2 : bSetTimer = true                    // << move to prev photo and run backwards
1691                case -1 : change    = -1;                     // |< move to next photo and stop
1692                break;
1693                
1694                case  2 : bSetTimer = true  ;                 // >> move to next photo and run
1695                case  1 : change    = 1     ;                 // >| move to next photo and stop
1696                break;
1697                
1698                case  0 : running = !running;                 // toggle running
1699                          if ( running ) {
1700                                change    = 1;
1701                                bSetTimer = true;
1702                          }
1703                break; 
1704            }
1705        } catch(e) {};
1706        
1707        if ( change                         ) showPhotoN(photo+change);
1708        if ( bSetTimer && Photos.length > 1 ) startTimer(change)
1709        return false;
1710    }
1711    
1712    function timerEvent(forward)
1713    {
1714        clearTimer();
1715        showPhotoN(photo+forward);
1716        startTimer(forward);
1717    }
1718    
1719    function diff (a, b) { return a > b ? a-b : b-a ; }
1720    let      count = 0; // sometimes used by console.log(....)
1721    
1722    function getFrameHTML(src,w,h)
1723    {
1724    //  We could use JSFrame to create a "popup" dialog for a 404
1725    //  result = `<h1 style="color:yellow;background-color:red;location:relate;top:50%;width:100%;height:100%;vertical-align:middle;"><center><br><br><br>Write the warning here!</center></h1>`
1726        let result = `<div style="background:transparent;z-level:100;">`
1727                   + `<img id="my_photo" src="${src}" width="${w}" `
1728                   + `style="border-width:0;z-level:101;border:0px solid yellow;width=${w};height:${h};" `
1729                   + '/></div>'
1730                   ;
1731         return result;
1732    }
1733
1734    // https://github.com/riversun/JSFrame.js.git
1735    // JSFrame.js/public/examples/v160/styling-thick-title-bar/index.html 
1736    function getOriginalStyle(frameAppearance) {
1737        frameAppearance.titleBarCaptionColorDefault = 'blue';
1738        frameAppearance.titleBarCaptionColorFocused = 'yellow';
1739        frameAppearance.titleBarCaptionTextShadow   =  null;//'0 1px 0 rgba(255,255,255,.7)';
1740        frameAppearance.titleBarColorDefault        = 'black';
1741        frameAppearance.titleBarColorFocused        = 'dodgerblue';
1742        return frameAppearance;
1743    }
1744    
1745    function closeFrame()
1746    {
1747        if ( frame_ ) {
1748            frame_.closeFrame();
1749            frame_ = 0;
1750        }
1751        photo = -1;
1752    }
1753    
1754    function createFrame(title,taken,img_src,w,h)
1755    {
1756        W_     = w;
1757        H_     = h;
1758        src_   = img_src;
1759        let A_ = w/h;       // aspect ratio
1760        
1761        if ( !frame_ ) {
1762            frame_ = jsFrame.create(
1763            {       title       : ''
1764            ,       left        : X_     
1765            ,       top         : Y_
1766            ,       width       : W_
1767            ,       height      : H_
1768            ,       movable     : true           // Enable to be moved by mouse
1769            ,       resizable   : true           // Enable to be resized by mouse
1770            ,       z_level     : 50
1771            ,       html        : '<p></p>'
1772            ,		appearance	: getOriginalStyle(jsFrame.createFrameAppearance())
1773            });
1774            // https://github.com/riversun/JSFrame.js/issues/58
1775            $("[component-id=closeButton]").unbind("click");
1776            $("[component-id=closeButton]").attr("onclick", "");
1777            frame_.on("closeButton", "click", (_frame, _evt) => {
1778                clearTimer();
1779                closeFrame();
1780            });
1781        } 
1782        frame_.setPosition(X_,Y_);
1783        frame_.setSize    (W_,H_);
1784        frame_.setHTML(getFrameHTML(src_,W_,H_));
1785
1786        frame_.setTitle(`${title} ${taken} (${ getTakenIndex(taken,Photos)+1 } of ${Photos.length})` );
1787        frame_['photo'] = getTakenIndex(taken,Photos);
1788        frame_.show();
1789    
1790        frame_.on('frame','resize', (event) => {
1791            W_ = event.target.getSize().width;
1792            H_ = W_*A_; // preserve aspect ratio
1793            frame_.setHTML(getFrameHTML(src_,W_,H_));
1794        });
1795        frame_.on('frame','move', (event) => {
1796            // console.log('frame moved.', event.target.getSize());
1797            X_  = event.target.getPosition().x;
1798            Y_  = event.target.getPosition().y;
1799        });
1800    }
1801
1802    function clearTimer()
1803    {
1804        let result = timer ? true : false;
1805   
1806        if ( timer ) {
1807            clearTimeout(timer);
1808            timer    = 0 ;
1809        }
1810        document.getElementById('stopButton').classList.replace( 'fa-pause','fa-play');
1811        return result;
1812    }
1813    
1814    function startTimer(forward) {
1815        timer = setTimeout(timerEvent, interval, forward); 
1816        document.getElementById('stopButton').classList.replace( 'fa-play','fa-pause');
1817    }
1818    
1819    function setPhoto(p) // set global photo to an in-range value;
1820    {
1821        photo = p < 0              ? Photos.length -1
1822              : p >= Photos.length ? 0 
1823              : p
1824              ;
1825        return photo;
1826    }
1827    
1828    function getTakenIndex(taken,Photos)
1829    {
1830        let    result = 0;
1831        for (   let p = 0 ; !result && p < Photos.length ; p++ ) if ( Photos[p].taken == taken ) result = p;
1832        return result;
1833    }  
1834    
1835    // AI generated function
1836    function isMarkerVisibleOnGlobe(map, markerLngLat) {
1837        const center = map.getCenter();
1838        
1839        // Convert degrees to radians
1840        const lat1 = center.lat * Math.PI / 180;
1841        const lng1 = center.lng * Math.PI / 180;
1842        const lat2 = markerLngLat.lat * Math.PI / 180;
1843        const lng2 = markerLngLat.lng * Math.PI / 180;
1844    
1845        // Calculate angular distance using the Spherical Law of Cosines
1846        const distance = Math.acos(
1847            Math.sin(lat1) * Math.sin(lat2) +
1848            Math.cos(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1)
1849        );
1850    
1851        // If distance is less than PI/6 (30 degrees), it's on the visible hemisphere
1852        return distance < (Math.PI / 6);
1853    }
1854    
1855    // AI generated function
1856    function isMarkerOnScreen(map, markerLngLat) {
1857        // 1. Check if it's on the front of the globe (function above)
1858        // 2. Check if it's within the map's current screen bounds
1859        return isMarkerVisibleOnGlobe(map, markerLngLat) && map.getBounds().contains(markerLngLat);
1860    }
1861
1862    function showPhoto(p)
1863    {
1864        photo = typeof p == 'string' ? getTakenIndex(p,Photos) : p ;
1865        if ( typeof photo != typeof 1 ) return ;
1866        
1867        with ( Photos[photo] ) {
1868            H_ = W_ * image.naturalHeight / image.naturalWidth ;
1869            createFrame(title,taken,data,W_,H_);
1870        }
1871        
1872        if ( gmap ) {
1873            for ( let m = 0 ; m < gmarkers.length; m++ ) {
1874                gmarkers[m].append(m==photo?gPin:gpin);
1875            }
1876        }
1877        if ( mapBox ) {
1878            for ( let m = 0 ; m < mapboxMarkers.length; m++ ) {
1879                setMapboxMarkerProps(mapboxMarkers[m],m==photo) ;
1880            }
1881            let center =  mapboxMarkers[photo].getLngLat();
1882
1883            if ( ! isMarkerOnScreen(mapBox,mapboxMarkers[photo].getLngLat()) ) {
1884                mapBox.easeTo({ center
1885                             , duration: 1000
1886                             , easing: (n) => n
1887                             });
1888            }
1889            // console.log('mapboxMarker',mapboxMarkers[photo]);
1890        }
1891        if ( lmap ) {
1892            // console.log('showPhoto',photo);
1893            for ( let m = 0 ; m < lmarkers.length; m++ ) {
1894                lmarkers[m].setIcon(m==photo ? goldIcon : blueIcon) ;
1895                lmarkers[m]._icon.style.opacity=1.0
1896            //  lmarkers[m]._icon.style.marginTop=-20
1897            //  console.log('icon',lmarkers[m]._icon);
1898            }
1899        }
1900    } 
1901    
1902    function showPhotoN(n)
1903    {
1904        showPhoto(setPhoto(n));
1905        // https://stackoverflow.com/questions/10970078/modifying-a-query-string-without-reloading-the-page
1906        // hashChangeIgnore = true;
1907        // let href             = window.location.href;
1908        // href                 = href.substring(0,href.indexOf('#'))
1909        // window.location.href = href + `#path=${path}&frame=${photo}`;
1910    }
1911    
1912    // https://stackoverflow.com/questions/50899452/mapbox-gl-js-change-the-color-from-the-default-marker
1913    function setMapboxMarkerProps(marker,bHighlight) {
1914        let w     = bHighlight ?        '54' :         '27';
1915        let h     = bHighlight ?        '82' :         '41';
1916        let color = bHighlight ?    'yellow' :        'red';
1917        let circle= bHighlight ? 'dodgerblue' : 'firebrick';
1918
1919        let svg = marker.getElement().querySelectorAll('svg')[0]
1920        svg.setAttribute('height' , `${h}px`);
1921        svg.setAttribute('width'  , `${h}px`);
1922        svg.setAttribute('viewbox', `0 0 ${w} ${h}`);
1923        let path = svg.querySelector('path');
1924        path.setAttribute('fill',color);
1925        svg.querySelector('circle').setAttribute('fill',circle);
1926    }
1927    
1928    // mapsBox code
1929    function selectGlobe(show) // true or false
1930    {
1931    //  console.log('selectGlobe');
1932        if ( mapsBox ) {
1933            let mb = document.getElementById('mapsBox');
1934            mb.style.display    = show  ? 'block'   : 'none'  ;
1935            if ( show ) {		
1936                let style = mapsBox.getStyle();
1937                style.sources.photos.cluster = document.getElementById('cluster').checked;
1938                mapsBox.setStyle(style);
1939                let	  index = style.sources.photos.cluster ? 0 : 1;
1940                for ( let prop in paints ) {
1941                    mapsBox.setPaintProperty('unclustered-point', prop, paints[prop][index]);
1942                }
1943            }
1944        //  console.log('displayGlobe',show?'yes':'no')
1945        }
1946    }
1947    
1948    function showGlobe(yes)
1949    {
1950        selectGlobe(yes=='yes');
1951    }
1952    
1953    function toggleGlobe()
1954    {
1955    //  console.log('toggleGlobe');
1956        if ( mapsBox ) {
1957            let mb = document.getElementById('mapsBox');
1958            showing = mb.style.display == 'block' ;
1959            selectGlobe(!showing)
1960        }
1961        mapsBoxResize();
1962    }
1963    
1964    function mapsBoxResize()
1965    {
1966        let millisecs = 30;
1967        setTimeout(() => {
1968            if ( mapsBox ) mapsBox.resize();
1969            if ( mapBox  ) mapBox.resize();
1970            if ( lmap    ) lmap.invalidateSize();
1971            if ( gmap    ) google.maps.event.trigger(gmap, 'resize');
1972            if ( viewer  ) viewer.resize();
1973        }, millisecs);
1974    }
1975    
1976    function hideGlobe()
1977    {
1978        if ( mapsBox ) {
1979            let mb = document.getElementById('mapsBox');
1980            if ( mb.style.display == 'block' ) {
1981                selectGlobe(false);
1982            }
1983        }
1984    }
1985    
1986    function onCluster()
1987    {
1988    //	console.log('onCluster');
1989        if ( mapsBox ) {
1990            let style = mapsBox.getStyle();
1991            style.sources.photos.cluster = document.getElementById('cluster').checked;
1992            mapsBox.setStyle(style);
1993    
1994            let	  index = style.sources.photos.cluster ? 0 : 1;
1995            for ( let prop in paints ) {
1996                mapsBox.setPaintProperty('unclustered-point', prop, paints[prop][index]);
1997            }
1998        }
1999    }
2000    
2001    function getLimit()
2002    {
2003        let  limit = document.getElementById("limit").value;
2004    //  the limit for 'Stories' is different.  Read HTML <option value= "50">  50/20</option> and parse text
2005        if ( document.getElementById("search").value == 'Stories' ) {
2006            let options = {}
2007            let Q = document.getElementById("limit")
2008            for ( var x = 0 ; x < Q.length ; x++ ) options[ Q[x].value ] = Q[x].text.split('/')[1];
2009            limit = options[limit];
2010        }
2011        return parseInt(limit);
2012    }
2013
2014    function getFirst(block)
2015    {   // called by getQueryString to determine the first photo in the range
2016        let photorangestring = document.getElementById('photorangestring').innerHTML ; // "1-28 of 28";
2017        let match = photorangestring.match(photorange_regexp);
2018        let from  = parseInt(match[1])
2019        let to    = parseInt(match[2])
2020        let size  = parseInt(match[3])
2021    //  console.log(`From: ${from} / To: ${to} OF ${size}.`);
2022        
2023        let limit = getLimit();
2024        let first = block ? block : 0;
2025    
2026        first                      = from-1 + first*limit;
2027        if (  first >= size) first = size - limit;
2028        if (  first < 0    ) first = 0 ;
2029        return first;
2030    }
2031    
2032    function getQueryString(block)
2033    {
2034        limit = getLimit()
2035        first = document.getElementById("search").value == 'Photos' ? getFirst(block) : 0;
2036        return	''
2037              + '?search='	+ escape(document.getElementById('search' ).value)
2038              + '&query='	+ escape(document.getElementById("query"  ).value)			  
2039              + '&reverse=' + (document.getElementById("reverse").checked?0:1)
2040              + '&limit='	+ limit
2041              + '&first='	+ first
2042              ;
2043    }
2044    
2045    function getCommand(block)
2046    {
2047        return '/cgi-bin/search.cgi' + getQueryString(block);
2048    }
2049    
2050    function shareSearch()
2051    {
2052    //  https://stackoverflow.com/questions/5817505/is-there-any-method-to-get-the-url-without-query-string
2053        let path     = window.location.href.split('&')[0]
2054        let location = path+`&`+getQueryString(0).substring(1) ;
2055        hashChangeIgnore     = true;
2056        window.location.href = location;
2057
2058//        console.log('location=',location);
2059    }
2060    
2061    function reallySearch(block)
2062    {
2063    //  console.log('reallySearch(block)',block);
2064        if ( typeof block == typeof true ) {  // boolean => hideGlobe()
2065            if ( block ) hideGlobe();
2066        }
2067        
2068        thePage['objects']=''
2069        thePage['path'   ]='/search'
2070        let what	= document.getElementById('search' ).value;
2071        let q       = document.getElementById('query'  ).value;
2072        
2073        // use the cgi search engine
2074        searchCommand = getCommand(block);
2075    //  console.log('searchCommand = ',searchCommand);
2076        buildPage(thePage.path);
2077    }
2078    
2079    function getQueryVariable(variable,defolt) {
2080        let query = window.location.search.substring(1);
2081        let vars = query.split('&');
2082        for (let i = 0; i < vars.length; i++) {
2083            let pair = vars[i].split('=');
2084            if (decodeURIComponent(pair[0]) == variable) {
2085                return decodeURIComponent(pair[1]);
2086            }
2087        }
2088        return defolt;
2089    }
2090    
2091    function setSelect(name,value)
2092    {
2093        value = value.toString();
2094        let dd = document.getElementById(name);
2095        for (let i = 0; i < dd.options.length; i++) {
2096            ddtext = dd.options[i].text;
2097            option = ddtext.split('/')
2098            option = option ? option[0] : ddtext;
2099        //	console.log('setSelect: index = ' + i + ' value = ' + value + ' dd = ' + dd.options[i].text, ' option = ' + option);
2100            if (option == value) {
2101                dd.selectedIndex = i;
2102                break;
2103            }
2104        }
2105    }
2106    
2107    // Map Search
2108    // Globals
2109    var	mapsBox		      =  0	;
2110    var openDateFilePath  = ''	;
2111    var openDateFileTaken = ''	;
2112    const pointMax		  = 101 ; // max points in a cluster that opens
2113    
2114    // The following values can be changed to control rotation speed:
2115    var usersInteracting = false;
2116    var spinsEnabled		= true;
2117    
2118    function spinsGlobe() {
2119        if ( !mapsBox ) return;
2120        const zoom = mapsBox.getZoom();
2121        if (spinsEnabled && !usersInteracting && zoom < maxSpinZoom) {
2122            let distancePerSecond = 360 / secondsPerRevolution;
2123            if (zoom > slowSpinZoom) {
2124                // Slow spinning at higher zooms
2125                const zoomDif = (maxSpinZoom - zoom) / (maxSpinZoom - slowSpinZoom);
2126                distancePerSecond *= zoomDif;
2127            }
2128            const center = mapsBox.getCenter();
2129            center.lng -= distancePerSecond;
2130            // Smoothly animate the map over one second.
2131            // When this animation is complete, it calls a 'moveend' event.
2132            mapsBox.easeTo({ center, duration: 1000, easing: (n) => n });
2133        }
2134    }
2135    
2136    function openDateFile()
2137    {
2138    //	console.log('openDataFile',openDateFilePath);
2139        let	 dateXML = readFile(openDateFilePath);
2140        if ( dateXML.length ) {
2141            loadPhotoDiv(dateXML);
2142            document.getElementById('query').value = openDateFileTaken;
2143         }
2144    }
2145    
2146    function showMapsBox() {
2147        if ( mapsBox ) return;
2148        mapsBox = new mapboxgl.Map(
2149        {	container	: 'mapsBox'
2150        ,	style		: 'mapbox://styles/mapbox/streets-v9'
2151        ,	projection	: 'globe' // 'mercator' // 'naturalEarth' 'globe' // Display the map as a globe, since satellite-v9 defaults to Mercator
2152        ,	zoom		: 2
2153        ,	center		: [30, 50] // 1000 miles east of London (30degs=cos(50)*1800nautical miles))
2154        });
2155    
2156    
2157        mapsBox.addControl(new mapboxgl.NavigationControl());
2158        mapsBox.addControl(new mapboxgl.FullscreenControl());
2159        mapsBox.scrollZoom.enable();
2160    //	mapsBox.on('style.load', () => { mapsBox.setFog({});	});
2161        mapsBox.on('mousedown', () => { usersInteracting = true; });
2162        mapsBox.on('dragstart', () => { usersInteracting = true; });
2163        mapsBox.on('moveend', () =>   { spinsGlobe(); });
2164        
2165        mapsBox.on('load', () => {
2166            mapsBox.addSource('photos', 
2167            {	type			: 'geojson'
2168            ,	data			: '/photos.geojson'
2169            ,	cluster			: document.getElementById("cluster").checked
2170            ,	clusterMaxZoom	:	10	// Max zoom to cluster points on
2171            ,	clusterRadius	:	60	// Radius of each cluster when clustering points (defaults to 50)
2172            });
2173    
2174            mapsBox.addLayer(
2175            {	 id		: 'clusters'
2176            ,	 type	: 'circle'
2177            ,	 source : 'photos'
2178            ,	 filter : ['has', 'point_count']
2179            ,	 paint	: { 'circle-color'		: [ 'step'
2180                                                  , ['get', 'point_count']
2181                                                  , colors.dodgerblue ,pointMax, colors.yellow, 750, colors.burgundy
2182                                                  ]
2183                            , 'circle-radius'  :  [ 'step', ['get', 'point_count']
2184                                                  , 20, pointMax, 30, 750, 40
2185                                                  ]
2186                }
2187            });
2188    
2189            mapsBox.addLayer(
2190            { id		: 'cluster-count'
2191            , type		: 'symbol'
2192            , source	: 'photos'
2193            , filter	: [ 'has', 'point_count']
2194            , layout	: { 'text-field'	: ['get', 'point_count_abbreviated']
2195                          , 'text-font'		: ['Cera Round Pro DEMO', 'DIN Offc Pro Medium', 'Arial Unicode MS Bold']
2196                          ,	 'text-size'	: 12
2197                          }
2198            });
2199            
2200            let displayCluster	= 'yes';
2201            let paintIndex		= displayCluster == 'yes' ? 0 : 1;
2202        //	console.log('displayCluster,paintIndex = ',displayCluster,paintIndex);
2203            mapsBox.addLayer(
2204            { id		: 'unclustered-point'
2205            , type		: 'circle'
2206            , source	: 'photos'
2207            , filter	: ['!', ['has', 'point_count']]
2208            });
2209            for ( let prop in paints ) {
2210                mapsBox.setPaintProperty('unclustered-point', prop, paints[prop][paintIndex]);
2211            }
2212    
2213            // https://stackoverflow.com/questions/70223500/mapsBox-gl-js-find-closest-address-to-clicked-point		  
2214            function mapClickFn(coordinates,point_count) {
2215            //	console.log('coords',coordinates);
2216                const lng	= coordinates.lng; const ln = parseFloat(lng).toFixed(5);
2217                const lat	= coordinates.lat; const la = parseFloat(lat).toFixed(5);
2218                let	  query = point_count ? `${point_count}@${ln},${la}` : `@${ln},${la}` ;
2219                const url	= "https://api.mapsBox.com/geocoding/v5/mapsBox.places/" + lng + "," + lat + ".json?access_token=" + YOUR_ACCESS_TOKEN + "&types=address";
2220                $.get(url, function (data) {
2221                  if (data.features.length > 0) {
2222                    const address = data.features[0].place_name;
2223                //	console.log(address);
2224                    query += ` ='${address}'` ;
2225                  }
2226                  // document.getElementById('query').value = query;
2227                });
2228            }
2229            
2230            function loadPhotosTaken(takenSet)
2231            {
2232                console.log('loadPhotosTaken',takenSet);
2233                let takens	 = [...takenSet]; // convert to list
2234                let q = ''
2235                for ( let t in takens ) {
2236                    q += q.length ? ' OR ' : '' ;
2237                    q += takens[t].replace(' ','_')                      // "1951:01:18 05:20:21" -> "1951:01:18_05:20:21 "
2238                }
2239                document.getElementById('query' ).value = q       ;
2240                document.getElementById('search').value = 'Photos';
2241            //  console.log('q=',q);
2242                reallySearch();
2243            }
2244            
2245            function loadPhotosNear(aPoint,tPhotos)
2246            {
2247                document.getElementById('search').value = 'Photos';
2248                let N   = getLimit();
2249                let lng = (''+aPoint[0]).substring(0,9)
2250                let lat = (''+aPoint[1]).substring(0,9)
2251                document.getElementById('query').value = `${N}@${lng},${lat}`
2252                reallySearch();
2253            }
2254    
2255            // inspect a cluster on click
2256            mapsBox.on('click', 'clusters', (e) => {
2257                const features = mapsBox.queryRenderedFeatures(e.point, {
2258                    layers: ['clusters']
2259                });
2260                const clusterId	  = features[0].properties.cluster_id;
2261                let	  point_count = features[0].properties.point_count;
2262            //  console.log('cluster point handler, cluster_id,point_count = ',clusterId,point_count);
2263    
2264                if (  point_count < pointMax ) {
2265                    mapClickFn(e.lngLat,point_count);
2266    
2267                    let clusterSource = mapsBox.getSource('photos') ; // /* cluster layer data source id */'photos');
2268                //	console.log('clusterSource',clusterSource);
2269                    // Get all points within cluster
2270                    clusterSource.getClusterLeaves(clusterId, point_count, 0, function(err, aFeatures)
2271                    {
2272                        takenSet = new Set();
2273                        for ( let f = 0 ;  f < aFeatures.length ; f++ ) takenSet.add(aFeatures[f].properties.t);
2274                        loadPhotosTaken(takenSet);
2275                    })
2276                } else {
2277                    // zoom in a little 
2278                    mapsBox.getSource('photos').getClusterExpansionZoom(
2279                        clusterId,
2280                        (err, zoom) => {
2281                            if (err) return;
2282    
2283                            mapsBox.easeTo({
2284                                center: features[0].geometry.coordinates,
2285                                zoom: zoom
2286                            })
2287                    })
2288                } 
2289            });
2290    
2291            mapsBox.on('click', (e) => {
2292                // only search when clustered is off
2293                if ( mapsBox.getStyle().sources.photos.cluster ) return;
2294                
2295            //	console.log('click',e);
2296                let tPhotos	 = turf.featureCollection(mapsBox.querySourceFeatures('photos'));
2297                let tClick	 = turf.point( [e.lngLat.lng,e.lngLat.lat] );
2298                loadPhotosNear([e.lngLat.lng,e.lngLat.lat],tPhotos);
2299            });
2300    
2301            mapsBox.on('click', 'unclustered-point', (e) => {
2302            //	console.log('unclustered-point handler');
2303                const coordinates = e.features[0].geometry.coordinates.slice();
2304                const taken		  = e.features[0].properties.taken;
2305            //	console.log('taken',taken);
2306                let	  data		  = 'not found' ; // == no image
2307    
2308                // Ensure that if the map is zoomed out such that
2309                if (['mercator', 'equirectangular'].includes(mapsBox.getProjection().name)) {
2310                    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
2311                        coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
2312                    }
2313                }
2314                
2315                let tPhotos	  = turf.featureCollection(mapsBox.querySourceFeatures('photos'));
2316                let tCluster  = turf.point(coordinates);
2317            //	console.log('find near cluster point click,lngLat=',e.lngLat,lngLat);
2318                loadPhotosNear([e.lngLat.lng,e.lngLat.lat],tPhotos);
2319            });
2320    
2321            mapsBox.on("contextmenu", (e) => { usersInteracting = false ; spinsGlobe(); });
2322            // cursor setting
2323            mapsBox.getCanvas().style.cursor = 'default';
2324            mapsBox.on('mouseenter', 'clusters', () => {
2325                mapsBox.getCanvas().style.cursor = 'pointer';
2326            });
2327            mapsBox.on('mouseleave', 'clusters', () => {
2328                mapsBox.getCanvas().style.cursor = '';
2329            });
2330        });
2331    
2332        spinsGlobe();
2333    }
2334</script>
vendor: 1 bytes, line 2334
2334

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.