This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

where.is.lassul.us: add share buttons

+444 -63
+7 -55
2configs/websites/where.is.lassul.us/at.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 6 <title>lassulus was here</title> 7 7 <link rel="stylesheet" href="/leaflet/leaflet.css" /> 8 + <link rel="stylesheet" href="/maps.css" /> 8 9 <style> 9 10 html, 10 11 body { ··· 33 34 user-select: all; 34 35 color: #aaa; 35 36 } 36 - #open { 37 - display: grid; 38 - grid-template-columns: 1fr 1fr; 39 - gap: 0.5em; 40 - } 41 - #open a { 42 - display: block; 43 - padding: 0.8em; 44 - border-radius: 0.4em; 45 - text-align: center; 46 - text-decoration: none; 47 - color: #fff; 48 - background: #333; 49 - } 50 - #open a:active { 51 - background: #39f; 52 - } 53 37 #live { 54 38 display: block; 55 39 margin-top: 1em; ··· 62 46 <div id="panel"> 63 47 <p id="when"></p> 64 48 <p id="coords"></p> 65 - <div id="open"></div> 49 + <div id="open" class="open"></div> 66 50 <a id="live" href="/">live location →</a> 67 51 </div> 68 52 <script src="/leaflet/leaflet.js"></script> 53 + <script src="/maps.js"></script> 69 54 <script> 70 55 (function () { 71 56 // /at#LAT,LON[@UNIXTIME]; the fragment never reaches the server 57 + window.addEventListener("hashchange", function () { 58 + location.reload(); 59 + }); 72 60 var m = /^#(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:@(\d+))?$/.exec( 73 61 location.hash, 74 62 ); ··· 97 85 } 98 86 document.getElementById("coords").textContent = lat + ", " + lon; 99 87 100 - // iOS has no handler for geo: (iPadOS reports itself as a Mac) 101 - var ios = 102 - /iPad|iPhone|iPod/.test(navigator.userAgent) || 103 - (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); 104 - var ll = lat + "," + lon; 105 - var links = [ 106 - [ 107 - "Google Maps", 108 - "https://www.google.com/maps/search/?api=1&query=" + 109 - encodeURIComponent(ll), 110 - ], 111 - [ 112 - "Apple Maps", 113 - "https://maps.apple.com/?ll=" + ll + "&q=lassulus", 114 - ], 115 - // android: app chooser incl. OsmAnd, Organic Maps, ... 116 - ios ? null : ["other app", "geo:" + ll + "?q=" + ll + "(lassulus)"], 117 - [ 118 - "OpenStreetMap", 119 - "https://www.openstreetmap.org/?mlat=" + 120 - lat + 121 - "&mlon=" + 122 - lon + 123 - "#map=16/" + 124 - lat + 125 - "/" + 126 - lon, 127 - ], 128 - ]; 129 - var open = document.getElementById("open"); 130 - links.forEach(function (l) { 131 - if (!l) return; 132 - var a = document.createElement("a"); 133 - a.textContent = l[0]; 134 - a.href = l[1]; 135 - open.appendChild(a); 136 - }); 88 + renderMapLinks(document.getElementById("open"), lat, lon); 137 89 })(); 138 90 </script> 139 91 </body>
+5
2configs/websites/where.is.lassul.us/default.nix
··· 14 14 webroot = pkgs.runCommand "${domain}-webroot" { } '' 15 15 mkdir -p $out/leaflet 16 16 cp ${./index.html} $out/index.html 17 + cp ${./at.html} $out/at.html 18 + cp ${./maps.js} $out/maps.js 19 + cp ${./maps.css} $out/maps.css 17 20 cp ${leaflet}/dist/leaflet.js ${leaflet}/dist/leaflet.css $out/leaflet/ 18 21 cp -r ${leaflet}/dist/images $out/leaflet/images 19 22 ''; ··· 101 104 enableACME = true; 102 105 forceSSL = true; 103 106 root = webroot; 107 + # share landing page: /at#LAT,LON[@UNIXTIME] 108 + locations."= /at".tryFiles = "/at.html =404"; 104 109 locations."= /location.json".extraConfig = '' 105 110 alias ${stateDir}/location.json; 106 111 default_type application/json;
+430 -7
2configs/websites/where.is.lassul.us/index.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 6 <title>where is lassulus?</title> 7 7 <link rel="stylesheet" href="/leaflet/leaflet.css" /> 8 + <link rel="stylesheet" href="/maps.css" /> 8 9 <style> 9 10 html, 10 11 body { ··· 30 31 #info a { 31 32 color: #9cf; 32 33 } 34 + .ctl a { 35 + font-size: 18px; 36 + text-decoration: none; 37 + } 38 + .ctl a.on { 39 + background: #39f; 40 + } 41 + #compass { 42 + display: none; 43 + position: absolute; 44 + z-index: 1000; 45 + left: 50%; 46 + top: 50%; 47 + transform: translate(-50%, -50%); 48 + width: 14em; 49 + padding: 1em; 50 + border-radius: 1em; 51 + text-align: center; 52 + font: 14px/1.4 monospace; 53 + color: #eee; 54 + background: rgba(0, 0, 0, 0.75); 55 + pointer-events: none; 56 + } 57 + body.nav #compass { 58 + display: block; 59 + } 60 + #arrow { 61 + width: 10em; 62 + height: 10em; 63 + transition: transform 0.2s linear; 64 + } 65 + #dist { 66 + font-size: 2em; 67 + font-weight: bold; 68 + } 69 + #sheet { 70 + display: none; 71 + position: absolute; 72 + z-index: 1001; 73 + left: 0; 74 + right: 0; 75 + bottom: 0; 76 + padding: 1em; 77 + border-radius: 1em 1em 0 0; 78 + font: 14px/1.4 monospace; 79 + color: #eee; 80 + background: #111; 81 + } 82 + body.sheet #sheet { 83 + display: block; 84 + } 85 + #sheet-head { 86 + display: flex; 87 + justify-content: space-between; 88 + align-items: center; 89 + margin-bottom: 0.75em; 90 + } 91 + #sheet-close { 92 + border: 0; 93 + font: inherit; 94 + font-size: 1.4em; 95 + color: #eee; 96 + background: none; 97 + cursor: pointer; 98 + } 99 + #sheet-share { 100 + margin-top: 0.5em; 101 + } 33 102 </style> 34 103 </head> 35 104 <body> 36 105 <div id="map"></div> 106 + <div id="compass"> 107 + <svg id="arrow" viewBox="-50 -50 100 100"> 108 + <circle r="46" fill="none" stroke="#666" stroke-width="2" /> 109 + <path d="M0,-42 L18,20 L0,10 L-18,20 Z" fill="#e33" /> 110 + </svg> 111 + <div id="dist">…</div> 112 + <div id="dir"></div> 113 + </div> 37 114 <div id="info">loading…</div> 115 + <div id="sheet"> 116 + <div id="sheet-head"> 117 + <span>open lassulus' location in…</span> 118 + <button id="sheet-close" aria-label="close">✕</button> 119 + </div> 120 + <div id="sheet-open" class="open"></div> 121 + <div id="sheet-share" class="open"> 122 + <button id="share-link">share link…</button> 123 + <button id="copy-link">copy link</button> 124 + </div> 125 + </div> 38 126 <script src="/leaflet/leaflet.js"></script> 127 + <script src="/maps.js"></script> 39 128 <script> 40 129 (function () { 41 130 var map = L.map("map", { zoomControl: true }); ··· 46 135 }).addTo(map); 47 136 48 137 var layer = L.layerGroup().addTo(map); 138 + var meLayer = L.layerGroup().addTo(map); 49 139 var info = document.getElementById("info"); 140 + var arrow = document.getElementById("arrow"); 141 + var distEl = document.getElementById("dist"); 142 + var dirEl = document.getElementById("dir"); 143 + 144 + var target = null; // L.LatLng of lassulus' last point 145 + var targetTime = null; // ISO timestamp of that point 146 + var me = null; // L.LatLng of the viewer 147 + var meError = null; 148 + var heading = null; // device compass heading, degrees clockwise from north 149 + var compassNote = "no compass"; // why heading is null 150 + var navMode = false; 151 + var trailBounds = null; // bounds of the first trail we loaded 152 + var loaded = false; // first location.json fetched (even if empty) 153 + var meFitted = false; // initial view already includes the viewer 154 + 155 + // initial view: trail plus viewer, once each is first known 156 + function initialView() { 157 + if (!loaded) return; 158 + var b = trailBounds ? L.latLngBounds([]).extend(trailBounds) : null; 159 + if (me) { 160 + b = b ? b.extend(me) : L.latLngBounds([me]); 161 + meFitted = true; 162 + } 163 + if (b) map.fitBounds(b.pad(0.1), { maxZoom: 16 }); 164 + else map.setView([0, 0], 2); 165 + } 50 166 51 167 function ago(iso) { 52 168 var s = Math.round((Date.now() - Date.parse(iso)) / 1000); ··· 56 172 return (s / 86400).toFixed(1) + "d"; 57 173 } 58 174 175 + function fmtDist(m) { 176 + if (m < 1000) return Math.round(m) + " m"; 177 + if (m < 10000) return (m / 1000).toFixed(1) + " km"; 178 + return Math.round(m / 1000) + " km"; 179 + } 180 + 181 + // initial great-circle bearing from a to b, degrees clockwise from north 182 + function bearing(a, b) { 183 + var r = Math.PI / 180; 184 + var p1 = a.lat * r, 185 + p2 = b.lat * r, 186 + dl = (b.lng - a.lng) * r; 187 + var y = Math.sin(dl) * Math.cos(p2); 188 + var x = 189 + Math.cos(p1) * Math.sin(p2) - 190 + Math.sin(p1) * Math.cos(p2) * Math.cos(dl); 191 + return (Math.atan2(y, x) / r + 360) % 360; 192 + } 193 + 194 + var points = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; 195 + function cardinal(deg) { 196 + return points[Math.round(deg / 45) % 8]; 197 + } 198 + 199 + // rotate along the shortest path so the css transition never spins 350° 200 + var shown = 0; 201 + function rotateArrow(deg) { 202 + var d = ((((deg - shown) % 360) + 540) % 360) - 180; 203 + shown += d; 204 + arrow.style.transform = "rotate(" + shown + "deg)"; 205 + } 206 + 207 + // distance/direction between viewer and lassulus: info bar + compass 208 + function updateRel() { 209 + var rel = document.getElementById("rel"); 210 + if (!target) return; 211 + if (!me) { 212 + if (rel) 213 + rel.textContent = meError ? " · your location: " + meError : ""; 214 + distEl.textContent = "…"; 215 + dirEl.textContent = meError || "waiting for your location"; 216 + return; 217 + } 218 + var d = me.distanceTo(target); 219 + var b = bearing(me, target); 220 + if (rel) { 221 + rel.innerHTML = 222 + " · " + 223 + fmtDist(d) + 224 + " " + 225 + cardinal(b) + 226 + ' of you · <a href="https://www.openstreetmap.org/directions?route=' + 227 + me.lat + 228 + "%2C" + 229 + me.lng + 230 + "%3B" + 231 + target.lat + 232 + "%2C" + 233 + target.lng + 234 + '">route</a>'; 235 + } 236 + distEl.textContent = fmtDist(d); 237 + if (heading === null) { 238 + // no compass: arrow is relative to north (map up) 239 + rotateArrow(b); 240 + dirEl.textContent = 241 + Math.round(b) + 242 + "° " + 243 + cardinal(b) + 244 + " (" + 245 + compassNote + 246 + ", north up)"; 247 + } else { 248 + rotateArrow(b - heading); 249 + dirEl.textContent = Math.round(b) + "° " + cardinal(b); 250 + } 251 + } 252 + 59 253 // trail fades from transparent (24h ago) to solid (now); one segment per point pair 60 254 function render(data) { 61 255 layer.clearLayers(); 62 256 var pts = data.points; 63 257 if (!pts.length) { 258 + target = null; 64 259 info.textContent = "no location in the last 24h"; 65 260 return; 66 261 } ··· 83 278 ).addTo(layer); 84 279 } 85 280 var last = pts[pts.length - 1]; 281 + target = L.latLng(last.lat, last.lon); 282 + targetTime = last.t; 283 + updateSheet(); 86 284 if (last.acc) { 87 - L.circle([last.lat, last.lon], { 285 + L.circle(target, { 88 286 radius: last.acc, 89 287 color: "#39f", 90 288 weight: 1, 91 289 fillOpacity: 0.1, 92 290 }).addTo(layer); 93 291 } 94 - L.marker([last.lat, last.lon]) 292 + L.marker(target) 95 293 .addTo(layer) 96 294 .bindPopup( 97 295 "last seen " + ··· 116 314 last.lat + 117 315 "/" + 118 316 last.lon + 119 - '">osm</a>'; 317 + '">osm</a><span id="rel"></span>'; 318 + updateRel(); 120 319 return L.latLngBounds( 121 320 pts.map(function (p) { 122 321 return [p.lat, p.lon]; ··· 124 323 ); 125 324 } 126 325 127 - var first = true; 326 + function fitBoth() { 327 + if (!me || !target) return; 328 + map.fitBounds(L.latLngBounds([me, target]).pad(0.2), { 329 + maxZoom: 17, 330 + }); 331 + } 332 + 333 + // viewer position: blue dot + accuracy circle 334 + var meDot = null, 335 + meAcc = null; 336 + function onPosition(pos) { 337 + me = L.latLng(pos.coords.latitude, pos.coords.longitude); 338 + meError = null; 339 + if (!meDot) { 340 + meAcc = L.circle(me, { 341 + radius: pos.coords.accuracy, 342 + color: "#3c3", 343 + weight: 1, 344 + fillOpacity: 0.1, 345 + }).addTo(meLayer); 346 + meDot = L.circleMarker(me, { 347 + radius: 7, 348 + color: "#fff", 349 + weight: 2, 350 + fillColor: "#3c3", 351 + fillOpacity: 1, 352 + }) 353 + .addTo(meLayer) 354 + .bindPopup("you"); 355 + } else { 356 + meDot.setLatLng(me); 357 + meAcc.setLatLng(me).setRadius(pos.coords.accuracy); 358 + } 359 + if (!meFitted) initialView(); 360 + updateRel(); 361 + } 362 + function onPositionError(e) { 363 + meError = e.code === 1 ? "permission denied" : "unavailable"; 364 + updateRel(); 365 + } 366 + var watchId = null; 367 + function watchMe() { 368 + if (!navigator.geolocation) { 369 + meError = "not supported"; 370 + return; 371 + } 372 + if (watchId !== null) navigator.geolocation.clearWatch(watchId); 373 + watchId = navigator.geolocation.watchPosition( 374 + onPosition, 375 + onPositionError, 376 + { enableHighAccuracy: true, maximumAge: 10000 }, 377 + ); 378 + } 379 + 380 + // device compass. iOS exposes webkitCompassHeading; others give an 381 + // absolute alpha (counter-clockwise from north) via deviceorientationabsolute. 382 + function onOrientation(e) { 383 + var h; 384 + if (typeof e.webkitCompassHeading === "number") { 385 + h = e.webkitCompassHeading; 386 + } else if (e.absolute && e.alpha !== null) { 387 + h = 360 - e.alpha; 388 + } else { 389 + return; 390 + } 391 + var screenAngle = 392 + (screen.orientation && screen.orientation.angle) || 393 + window.orientation || 394 + 0; 395 + heading = (h + screenAngle + 360) % 360; 396 + updateRel(); 397 + } 398 + var orientationEvent = 399 + "ondeviceorientationabsolute" in window 400 + ? "deviceorientationabsolute" 401 + : "deviceorientation"; 402 + 403 + function setNav(on) { 404 + navMode = on; 405 + document.body.classList.toggle("nav", on); 406 + navBtn.classList.toggle("on", on); 407 + if (!on) { 408 + window.removeEventListener(orientationEvent, onOrientation); 409 + heading = null; 410 + compassNote = "no compass"; 411 + return; 412 + } 413 + var listen = function () { 414 + window.addEventListener(orientationEvent, onOrientation); 415 + }; 416 + // iOS 13+ (and newer Chromium): permission must be requested from 417 + // the click handler 418 + if ( 419 + window.DeviceOrientationEvent && 420 + typeof DeviceOrientationEvent.requestPermission === "function" 421 + ) { 422 + DeviceOrientationEvent.requestPermission() 423 + .then(function (state) { 424 + if (state === "granted") listen(); 425 + else compassNote = "compass " + state; 426 + updateRel(); 427 + }) 428 + .catch(function () { 429 + compassNote = "compass unavailable"; 430 + updateRel(); 431 + }); 432 + } else { 433 + listen(); 434 + } 435 + if (!me) watchMe(); 436 + fitBoth(); 437 + updateRel(); 438 + } 439 + 440 + function button(container, text, title, onClick) { 441 + var a = L.DomUtil.create("a", "", container); 442 + a.href = "#"; 443 + a.setAttribute("role", "button"); 444 + a.title = title; 445 + a.textContent = text; 446 + L.DomEvent.on(a, "click", function (e) { 447 + L.DomEvent.preventDefault(e); 448 + onClick(); 449 + }); 450 + return a; 451 + } 452 + 453 + // snapshot link to the /at page, which offers the same "open in" 454 + // choices to whoever receives it 455 + function shareUrl() { 456 + return ( 457 + location.origin + 458 + "/at#" + 459 + target.lat + 460 + "," + 461 + target.lng + 462 + "@" + 463 + Math.floor(Date.parse(targetTime) / 1000) 464 + ); 465 + } 466 + var sheetOpen = document.getElementById("sheet-open"); 467 + var shareLinkBtn = document.getElementById("share-link"); 468 + var copyLinkBtn = document.getElementById("copy-link"); 469 + // the share sheet only lists apps that accept shared text/urls, which 470 + // is why the "open in" links above exist at all 471 + if (!navigator.share) shareLinkBtn.remove(); 472 + function copyLink() { 473 + var url = shareUrl(); 474 + var ask = function () { 475 + window.prompt("copy this link", url); 476 + }; 477 + if (!navigator.clipboard) return ask(); 478 + navigator.clipboard.writeText(url).then(function () { 479 + copyLinkBtn.textContent = "copied ✓"; 480 + setTimeout(function () { 481 + copyLinkBtn.textContent = "copy link"; 482 + }, 1500); 483 + }, ask); 484 + } 485 + function shareLink() { 486 + navigator 487 + .share({ 488 + title: "where is lassulus?", 489 + text: 490 + "lassulus, last seen " + new Date(targetTime).toLocaleString(), 491 + url: shareUrl(), 492 + }) 493 + .catch(function (e) { 494 + // AbortError: user closed the share sheet 495 + if (e.name !== "AbortError") copyLink(); 496 + }); 497 + } 498 + function updateSheet() { 499 + if (target) renderMapLinks(sheetOpen, target.lat, target.lng); 500 + } 501 + function setSheet(on) { 502 + if (on && !target) return; 503 + document.body.classList.toggle("sheet", on); 504 + shareBtn.classList.toggle("on", on); 505 + if (on) updateSheet(); 506 + } 507 + shareLinkBtn.addEventListener("click", shareLink); 508 + copyLinkBtn.addEventListener("click", copyLink); 509 + document 510 + .getElementById("sheet-close") 511 + .addEventListener("click", function () { 512 + setSheet(false); 513 + }); 514 + map.on("click", function () { 515 + setSheet(false); 516 + }); 517 + 518 + var navBtn, shareBtn; 519 + var Controls = L.Control.extend({ 520 + options: { position: "topleft" }, 521 + onAdd: function () { 522 + var div = L.DomUtil.create("div", "leaflet-bar ctl"); 523 + button(div, "◎", "show me and lassulus", function () { 524 + if (!me) watchMe(); 525 + fitBoth(); 526 + }); 527 + navBtn = button( 528 + div, 529 + "➤", 530 + "compass: point to lassulus", 531 + function () { 532 + setNav(!navMode); 533 + }, 534 + ); 535 + shareBtn = button( 536 + div, 537 + "⤴", 538 + "open in maps app / share lassulus' location", 539 + function () { 540 + setSheet(!document.body.classList.contains("sheet")); 541 + }, 542 + ); 543 + L.DomEvent.disableClickPropagation(div); 544 + return div; 545 + }, 546 + }); 547 + new Controls().addTo(map); 548 + 128 549 function refresh() { 129 550 fetch("/location.json", { cache: "no-cache" }) 130 551 .then(function (r) { ··· 132 553 }) 133 554 .then(function (data) { 134 555 var bounds = render(data); 135 - if (first && bounds) { 136 - map.fitBounds(bounds.pad(0.1), { maxZoom: 16 }); 137 - first = false; 556 + if (!loaded) { 557 + loaded = true; 558 + trailBounds = bounds || null; 559 + initialView(); 138 560 } 139 561 }) 140 562 .catch(function (e) { ··· 143 565 } 144 566 refresh(); 145 567 setInterval(refresh, 60 * 1000); 568 + watchMe(); 146 569 })(); 147 570 </script> 148 571 </body>
+2 -1
2configs/websites/where.is.lassul.us/maps.js
··· 15 15 ["Apple Maps", "https://maps.apple.com/?ll=" + ll + "&q=lassulus"], 16 16 ]; 17 17 // android: app chooser incl. Google Maps, OsmAnd, Organic Maps, ... 18 - if (!ios) links.push(["choose app…", "geo:" + ll + "?q=" + ll + "(lassulus)"]); 18 + if (!ios) 19 + links.push(["choose app…", "geo:" + ll + "?q=" + ll + "(lassulus)"]); 19 20 links.push([ 20 21 "OpenStreetMap", 21 22 "https://www.openstreetmap.org/?mlat=" +