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>( )</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;"> Cluster: <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> </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: '© <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>. ':'')+ 1282 (maps ? 'Maps: <a id="maps" target="_blank">click here</a>. ':'')+ br + 1283 (strava ? 'Strava: <a id="strava" target="_blank">click here</a>. ':'')+ 1284 (osmaps ? 'OSMaps: <a id="osmaps" target="_blank">click here</a>. ':'')+ br + 1285 (pdf ? 'PDF: <a id="pdf" target="_blank">click here</a>. ':'')+ 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;"> (${nPage}/${nPages}) </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('&','&')).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.