From 4c20bf5ab6c40aed01d84a1bc0fe15044a06f692 Mon Sep 17 00:00:00 2001 From: Samantha Date: Mon, 15 Jun 2026 02:37:06 +0900 Subject: [PATCH] ui changes - removed mobile features --- .firebase/hosting.YnVpbGQ.cache | 40 ++-- src/lib/Firebase/messages.js | 49 +++-- src/lib/components/MapView.Svelte | 243 +----------------------- src/lib/components/SensingToggle.svelte | 63 ------ src/lib/components/SidePanel.svelte | 88 +++++---- src/lib/utils/geo.js | 43 ----- src/lib/utils/geohash.js | 22 +++ src/routes/+page.svelte | 49 +---- 8 files changed, 131 insertions(+), 466 deletions(-) delete mode 100644 src/lib/components/SensingToggle.svelte delete mode 100644 src/lib/utils/geo.js diff --git a/.firebase/hosting.YnVpbGQ.cache b/.firebase/hosting.YnVpbGQ.cache index 9dd7a65..2c188a7 100644 --- a/.firebase/hosting.YnVpbGQ.cache +++ b/.firebase/hosting.YnVpbGQ.cache @@ -1,21 +1,21 @@ -index.html,1781368618868,3d57a9fdc57223bc87524d1a469ee5d33b2d74717a5a2e5bd80af2a8e7bbea8f -_app/env.js,1781368618473,3ad3b33040ca005034c918e78bb90257fafb951a2ac1dcb9063c8b3c8af2f638 robots.txt,1780149196204,c42e958aa717ecf11e70c927ebe348b7cfdeb317d164463dde44a6245edc25bf -_app/immutable/nodes/3._gtiHFds.js,1781368617932,03f8d02b981f8b0e93287c16b1d0f68232b56de605e04e6e3064ac9559cc71b3 -_app/version.json,1781368617944,42787a03af655ace3e2d902d2d107377fd378a90a2368f8275709993b4c87d7d -_app/immutable/entry/start.yteJby6e.js,1781368617927,b7bd03d31219b336864abd516345895c21693469ce71ca1ac89886507f46c357 -_app/immutable/chunks/xihTtKlq.js,1781368617940,6dc2927621fce4ab1f29651cb0fc092849d26bcf1b4e7c03ca68912347351a5e -_app/immutable/nodes/1.BAF0rG-0.js,1781368617930,e6d2830148df3fcc69de7a81e6b2d769659f636a12abf16a81b95523c8b6a390 -_app/immutable/nodes/0.CAyX_LgE.js,1781368617929,d1d85667126849483f6b48c8109bb5f24a48655d3611533f373588c76cd2bf95 -_app/immutable/chunks/kNaey6uv.js,1781368617940,b2e326f189779bca5f499cc8b1019822fec2ee950002f2208b82d7574d863610 -_app/immutable/entry/app.DCHY1y4u.js,1781368617926,fa7e6b6448286330f3d5755bc2b2415f99c9240f3a9b1c298fcb99483f360363 -_app/immutable/chunks/dYw4s8FK.js,1781368617938,92bce86c6a19bb7e0519d637325d6b82f8da811e6574b4255feabff9bcdb90cb -_app/immutable/chunks/Da4iMNDa.js,1781368617937,bc7fe8acf9be416169b80d50ad0d021b0a540ed9c91ca752e96dcffdb47974d0 -_app/immutable/assets/3.RINX6wbk.css,1781368617943,8f9a6eebf728dfc5907cb76bd37c871d2d1f334aca214eee962e83896b47578d -_app/immutable/chunks/BqvGMMFS.js,1781368617936,2c51af2452bfec64b3f17ba6d670db004801349cf42953bd1f81c8b40f258999 -_app/immutable/chunks/1QO-eMGS.js,1781368617934,d377c0e4e478da49fbfc037b9a3cc1ce597ac2e88d99f81ac8218f111f82dcee -_app/immutable/assets/2.OyR8amje.css,1781368617943,c7a455732f6a961dd65e2dbe169fa612688277595ed10da3ba111c38a48ae110 -_app/immutable/assets/0.DB_w-Orf.css,1781368617941,65816fda749a1c0cca0e0d336ab5ce2e8072226d1011391e0fa3577e8f8d7cd2 -_app/immutable/chunks/D4ShdbHl.js,1781368617936,a5cf19027054fbc801c5e3c64857c30ba4a549222e534629580b035eb730efad -_app/immutable/nodes/2.CBoVBvZT.js,1781368617931,f5ae73581a7da49af51d4c97d6ef3b20566ad552f3ae9ff287df9b279a52adfd -_app/immutable/chunks/BSIi0fWd.js,1781368617935,81700d9f9acc30966a90b4a3305471b4deb9e1b0ef7b004283514a3a07bd80b6 +index.html,1781458582579,c238f8f4335a07ba3975ee476ef3d9dc7029dbb4da2414bebea79539edb43ad8 +_app/env.js,1781458582001,3ad3b33040ca005034c918e78bb90257fafb951a2ac1dcb9063c8b3c8af2f638 +_app/immutable/nodes/1.DRGycYkH.js,1781458581476,ddc216c00274522f0163f6e17d7a61a5f83e3e464cc4db9a443d84028c562c60 +_app/version.json,1781458581482,cbb552342fea01555352de88ca2249dc1c7967fe5631dc49cd83e85f37f1574b +_app/immutable/nodes/3.DuWWIOOm.js,1781458581477,26d857c375138b43445caa866442182f628215754f510953baf51b687ecc4262 +_app/immutable/entry/start.BqO-PbzI.js,1781458581475,d82dbd922861c4a44043e136625387f1e0c3abe17c39221d0f5d2f704c27dc75 +_app/immutable/chunks/xihTtKlq.js,1781458581481,6dc2927621fce4ab1f29651cb0fc092849d26bcf1b4e7c03ca68912347351a5e +_app/immutable/chunks/kNaey6uv.js,1781458581480,b2e326f189779bca5f499cc8b1019822fec2ee950002f2208b82d7574d863610 +_app/immutable/entry/app.BKmqOLpp.js,1781458581474,8f657f3df0b7da5e2f354e4d0daaa527dd9154560deb2debb22be3a6d449cc1a +_app/immutable/chunks/dYw4s8FK.js,1781458581480,92bce86c6a19bb7e0519d637325d6b82f8da811e6574b4255feabff9bcdb90cb +_app/immutable/chunks/BqvGMMFS.js,1781458581477,2c51af2452bfec64b3f17ba6d670db004801349cf42953bd1f81c8b40f258999 +_app/immutable/assets/2.B2G-eVjW.css,1781458581481,1c67ba19797be9f8fde6c067517d129820a6cf0d03c38e440bb6d9f9c83568a7 +_app/immutable/assets/3.RINX6wbk.css,1781458581481,8f9a6eebf728dfc5907cb76bd37c871d2d1f334aca214eee962e83896b47578d +_app/immutable/assets/0.DB_w-Orf.css,1781458581481,65816fda749a1c0cca0e0d336ab5ce2e8072226d1011391e0fa3577e8f8d7cd2 +_app/immutable/nodes/0.OUbaqqFA.js,1781458581475,d211915e16706bad6122ec7403c04f7e81e2db36ef347c39cc5dc9d54d67e6da +_app/immutable/chunks/DDp4mPSJ.js,1781458581478,02d46f2be4b0586a43d9f3a29ea25c0c44c22e444da9943f92b1fbb3f0ef4105 +_app/immutable/chunks/Gly99FG6.js,1781458581480,b075ba40b4d6f384b98ffa0ab35b245b03b81877288e357fa3bfb31b24a41e54 +_app/immutable/chunks/D4ShdbHl.js,1781458581478,a5cf19027054fbc801c5e3c64857c30ba4a549222e534629580b035eb730efad +_app/immutable/nodes/2.BnMN9e9a.js,1781458581476,4c5aeb7ba20a00990a6147dfa493edbe74409f93da27b40a312c423455c26013 +_app/immutable/chunks/Dub02Wc7.js,1781458581478,b8f2be198d8c6cab67697e1420f2a9ed824cde281d9c998f1ce056daadd84bf1 diff --git a/src/lib/Firebase/messages.js b/src/lib/Firebase/messages.js index 233ad55..256f1a4 100644 --- a/src/lib/Firebase/messages.js +++ b/src/lib/Firebase/messages.js @@ -1,6 +1,6 @@ import { collection, query, where, getDocs, addDoc, getCountFromServer } from 'firebase/firestore'; // tools for building and running db queries import { db, auth } from './config'; // database connection + anonymous auth -import { getQueryPrefix } from '$lib/utils/geohash'; // convert coordinates into geohash string +import { getNearbyGeohashCells } from '$lib/utils/geohash'; // convert coordinates into a 3x3 grid of geohash cell prefixes (handles cell-boundary edge cases - see geohash.js) import { doc, getDoc, setDoc, updateDoc, increment, serverTimestamp } from 'firebase/firestore'; import ngeohash from 'ngeohash'; @@ -16,17 +16,31 @@ import ngeohash from 'ngeohash'; const statsRef = doc(db, 'meta', 'stats'); export async function getNearbyMessages(lat, lng) { - const prefix = getQueryPrefix(lat, lng); + // query the user's geohash cell AND its 8 neighbors (3x3 grid) rather + // than a single prefix range - see getNearbyGeohashCells in geohash.js + // for why a single cell isn't reliable enough across devices. + const cells = getNearbyGeohashCells(lat, lng); - // filter by the geohash - const q = query( - collection(db, 'messages'), - where('geohash', '>=', prefix), - where('geohash', '<', prefix + 'z') + const snapshots = await Promise.all( + cells.map(prefix => getDocs(query( + collection(db, 'messages'), + where('geohash', '>=', prefix), + where('geohash', '<', prefix + 'z') + ))) ); - const snapshot = await getDocs(q); - const all = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); + // merge the 9 result sets into one list. Neighboring geohash cells never + // overlap, so each message can only appear in one snapshot - but we + // dedupe by doc id anyway as a defensive measure, in case that ever changes. + const seen = new Map(); + for (const snapshot of snapshots) { + for (const docSnap of snapshot.docs) { + if (!seen.has(docSnap.id)) { + seen.set(docSnap.id, { id: docSnap.id, ...docSnap.data() }); + } + } + } + const all = [...seen.values()]; // we filter out messages which have already expired (past their echo date) const now = Date.now(); @@ -55,16 +69,19 @@ export async function getNearbyMessages(lat, lng) { // the query. Returns true if at least one message (active or expired) has // ever existed in this area. export async function hasAnyMessagesEverNearby(lat, lng) { - const prefix = getQueryPrefix(lat, lng); + // same 3x3 cell grid as getNearbyMessages, for the same boundary reason - + // neighboring cells never overlap, so the counts can simply be summed. + const cells = getNearbyGeohashCells(lat, lng); - const q = query( - collection(db, 'messages'), - where('geohash', '>=', prefix), - where('geohash', '<', prefix + 'z') + const counts = await Promise.all( + cells.map(prefix => getCountFromServer(query( + collection(db, 'messages'), + where('geohash', '>=', prefix), + where('geohash', '<', prefix + 'z') + ))) ); - const snapshot = await getCountFromServer(q); - return snapshot.data().count > 0; + return counts.some(snapshot => snapshot.data().count > 0); } // update the echo counter diff --git a/src/lib/components/MapView.Svelte b/src/lib/components/MapView.Svelte index 94e981c..7e395e7 100644 --- a/src/lib/components/MapView.Svelte +++ b/src/lib/components/MapView.Svelte @@ -6,7 +6,6 @@ import { messagePin, locationPin } from '$lib/utils/pins.js'; // custom SVG pin generators import { mapStyles } from '$lib/utils/mapStyles.js'; // desaturated/atmospheric map styling - only works now that mapId is gone import { getTrail, addTrailPoint } from '$lib/utils/trail.js'; // private, on-device-only location history - import { distanceMeters, bearingDegrees } from '$lib/utils/geo.js'; // great-circle distance/bearing for "sensing mode" // export let latitude; // export let longitude; @@ -14,9 +13,7 @@ // firstHereMounted/firstHereVisible: "you're the first here" bubble state, // computed in +page.svelte (zero-results check + fade timing) - rendered // here so it can be positioned near the user's location marker below. - // sensingMode: combined compass-arrow + proximity-vibration mode, toggled - // by SensingToggle in +page.svelte (mobile-only - see the effect below). - let { lat, lng, firstHereMounted = false, firstHereVisible = false, sensingMode = false } = $props(); + let { lat, lng, firstHereMounted = false, firstHereVisible = false } = $props(); let mapDiv; let map = $state(null); @@ -46,39 +43,6 @@ // the toggle just lets them hide it if they'd rather not see it. let showTrail = $state(true); - // --- "sensing mode": compass arrow + proximity vibration --------------- - // `heading` is the device's current compass heading in degrees (0 = true - // north, clockwise), updated by the deviceorientation listener below. - // Defaults to 0 ("facing north") rather than null: devices/browsers - // without an orientation sensor (e.g. desktop) never fire that event, so - // heading would otherwise stay null forever and the arrow would never - // appear at all. With a 0 default, the arrow instead falls back to a - // "north-up" compass - pointing at the absolute bearing toward the - // target - and starts rotating relative to the device's actual facing - // direction the moment real orientation data arrives. - // `nearestOffScreenBearing` is the compass bearing (also 0-360, 0 = true - // north) from the user's current position to the nearest message pin - - // recomputed on the interval below. `null` only when there's no nearby - // message at all (or the map isn't ready yet), in which case the arrow - // stays hidden - there's nothing to point at. - let heading = $state(0); - let nearestOffScreenBearing = $state(null); - - // The arrow needs to point at `nearestOffScreenBearing` RELATIVE TO the - // device's current facing direction - e.g. if the pin is due north - // (bearing 0) and the device is currently facing east (heading 90), the - // arrow should point 90deg counter-clockwise (-90 / 270) from "up" on - // the screen, since "up" on the screen = wherever the device is facing. - // Subtracting heading from the target bearing gives exactly that (with - // heading's default of 0, "up" simply means "true north" until real - // orientation data arrives). `null` (nothing to point at) hides the - // arrow entirely. - let arrowRotation = $derived( - nearestOffScreenBearing !== null - ? (nearestOffScreenBearing - heading + 360) % 360 - : null - ); - // converts a { url, size } pin (from pins.js) into an Icon object for // the legacy Marker API - centers the icon on its coordinate (anchor at // size/2, size/2) the same way the old AdvancedMarkerElement content's @@ -243,167 +207,6 @@ } }) - // --- sensing mode: nearest off-screen pin ------------------------------ - // Looks at every currently-loaded nearby message and finds both (a) the - // closest one that's currently outside the map's visible bounds, and (b) - // the closest one overall. Sets nearestOffScreenBearing to the bearing - // toward (a) if one exists, otherwise falls back to (b). - // The fallback matters because getNearbyMessages's ~1.2km geohash area is - // roughly the same size as the map's default viewport - in the common - // case EVERY nearby message is already on-screen, so "off-screen only" - // would mean nearestOffScreenBearing (and therefore the arrow) is null - // almost all the time. Preferring an off-screen pin (pointing toward - // something not already visible is more useful) but falling back to the - // nearest pin overall means the arrow still has something to point at, - // instead of never appearing. Stays null only if there are no nearby - // messages at all (or the map/bounds aren't ready yet) - either way, the - // arrow hides itself via arrowRotation above. - function updateNearestOffScreenPin(userLat, userLng) { - const bounds = map?.getBounds(); - - let nearestOffScreen = null; - let nearestOffScreenDist = Infinity; - let nearestOverall = null; - let nearestOverallDist = Infinity; - - for (const message of $messagesStore) { - const dist = distanceMeters(userLat, userLng, message.lat, message.lng); - - if (dist < nearestOverallDist) { - nearestOverallDist = dist; - nearestOverall = message; - } - - const onScreen = bounds?.contains({ lat: message.lat, lng: message.lng }) ?? false; - if (!onScreen && dist < nearestOffScreenDist) { - nearestOffScreenDist = dist; - nearestOffScreen = message; - } - } - - const target = nearestOffScreen ?? nearestOverall; - - nearestOffScreenBearing = target - ? bearingDegrees(userLat, userLng, target.lat, target.lng) - : null; - } - - // --- sensing mode: proximity vibration ---------------------------------- - // Checks every nearby message against (userLat, userLng) and, for any pin - // within PROXIMITY_METERS that this session hasn't already buzzed for, - // fires a single soft pulse. `vibratedIds` is a plain (non-reactive) Set - - // it only needs to track membership for this check, not drive any UI - - // and is recreated each time sensing mode is turned on (see the effect - // below), so re-enabling the toggle lets the same pin buzz again. - const PROXIMITY_METERS = 50; - let vibratedIds = new Set(); - - function checkProximityVibration(userLat, userLng) { - // gracefully do nothing on browsers without the Vibration API - if (typeof navigator === 'undefined' || typeof navigator.vibrate !== 'function') return; - - for (const message of $messagesStore) { - if (vibratedIds.has(message.id)) continue; - - const dist = distanceMeters(userLat, userLng, message.lat, message.lng); - if (dist <= PROXIMITY_METERS) { - navigator.vibrate(100); // single soft pulse - vibratedIds.add(message.id); - } - } - } - - // --- sensing mode: lifecycle -------------------------------------------- - // Runs whenever `sensingMode` changes (toggled by SensingToggle in - // +page.svelte). While ON, this sets up everything the two features need - // and tears it all down again the moment it's turned OFF or this - // component unmounts - no listeners/timers/watchers should outlive the - // toggle being on. - $effect(() => { - if (!sensingMode) return; - - // fresh "already buzzed" set each time sensing mode is (re-)enabled - vibratedIds = new Set(); - - // Live position for sensing: separate from the `lat`/`lng` props - // (which only reflect the one-time fix used to center the map) - - // watchPosition keeps these updated as the user actually walks - // around, which is what makes "proximity" and "nearest off-screen - // pin" meaningful. Starts from the map's initial fix so there's a - // usable value immediately, before the first watch update arrives. - let userLat = Number(lat); - let userLng = Number(lng); - - // --- DeviceOrientationEvent: device compass heading ----------------- - // iOS exposes a ready-made compass heading via webkitCompassHeading - // (0 = true north, increases clockwise). Other browsers only provide - // `alpha` (rotation around the z-axis, increasing COUNTER-clockwise - // from north when the device is lying flat) - `360 - alpha` converts - // that into the same "clockwise from north" convention. This is an - // approximation (it assumes the device is held flat/upright facing - // the user) but is good enough for a rough directional arrow. - function handleOrientation(event) { - if (event.webkitCompassHeading != null) { - heading = event.webkitCompassHeading; - } else if (event.alpha != null) { - heading = (360 - event.alpha) % 360; - } - } - - if (typeof window !== 'undefined' && typeof DeviceOrientationEvent !== 'undefined') { - window.addEventListener('deviceorientation', handleOrientation); - // Chrome on Android only reports a true compass heading via - // 'deviceorientationabsolute' - plain 'deviceorientation' events - // there have `absolute: false` and `alpha` relative to whatever - // direction the device happened to be facing when it booted, not - // true north, which made the arrow point in an arbitrary (and - // sometimes never-updating) direction. iOS doesn't dispatch this - // event at all, so it just never fires there and - // webkitCompassHeading (above) continues to be used. - window.addEventListener('deviceorientationabsolute', handleOrientation); - } - // if DeviceOrientationEvent doesn't exist at all, heading just stays - // null forever - arrowRotation stays null and the arrow stays hidden, - // which is the "handle unavailable API gracefully" behavior. - - // --- periodic sensing check ------------------------------------------ - // every few seconds, recompute the nearest off-screen pin (for the - // arrow) and run the proximity-vibration check, using the latest - // watched position. - function runSensingCheck() { - updateNearestOffScreenPin(userLat, userLng); - checkProximityVibration(userLat, userLng); - } - - runSensingCheck(); - const intervalId = setInterval(runSensingCheck, 3000); - - // keep userLat/userLng current as the device moves - let watchId = null; - if (typeof navigator !== 'undefined' && navigator.geolocation?.watchPosition) { - watchId = navigator.geolocation.watchPosition( - (position) => { - userLat = position.coords.latitude; - userLng = position.coords.longitude; - }, - () => {}, // ignore watch errors - sensing just keeps using the last-known position - { enableHighAccuracy: true, maximumAge: 5000 } - ); - } - - // cleanup: tear everything down the moment sensing mode is turned off - // (or this component is destroyed) - no lingering listeners/timers. - return () => { - window.removeEventListener('deviceorientation', handleOrientation); - window.removeEventListener('deviceorientationabsolute', handleOrientation); - clearInterval(intervalId); - if (watchId !== null) { - navigator.geolocation.clearWatch(watchId); - } - heading = 0; // back to the "facing north" default (see declaration above) - nearestOffScreenBearing = null; - }; - });
@@ -433,25 +236,6 @@ no one has been here before — or at least, no one who said so.

{/if} - - - {#if sensingMode && arrowRotation !== null && !$mapStore.selectedMessage} - - {/if}
diff --git a/src/lib/components/SidePanel.svelte b/src/lib/components/SidePanel.svelte index 9772c33..aa7f1f2 100644 --- a/src/lib/components/SidePanel.svelte +++ b/src/lib/components/SidePanel.svelte @@ -115,42 +115,48 @@ -
- {#if message.imageUrl} - message attachment - {/if} - -

{message.text}

- - {#if decay} - {#if isLastChance} - -

this is the last chance to keep this alive

- {:else} -

left {decay.daysAgo} days ago. fading in {decay.daysLeft} days.

+ +
+
+ {#if message.imageUrl} + message attachment {/if} - {/if} - {#if showLastChancePrompt} - -
-

if this one means something, an echo will keep it here a little longer.

- -
- {/if} -
+

{message.text}

-
- - - - + {#if decay} + {#if isLastChance} + +

this is the last chance to keep this alive

+ {:else} +

left {decay.daysAgo} days ago. fading in {decay.daysLeft} days.

+ {/if} + {/if} + + {#if showLastChancePrompt} + +
+

if this one means something, an echo will keep it here a little longer.

+ +
+ {/if} +
+ +
+ + + + +
{:else} @@ -282,11 +288,23 @@ color: #999; } + /* wraps .detail-content and .actions together - lets the "last chance" + background below extend around the action buttons too, not just the + message text. flex: 1 fills the remaining height of .panel (taking over + the role .actions's margin-top: auto used to play directly inside + .panel), so .actions (still margin-top: auto, now relative to this + wrapper) stays pinned to the bottom either way. */ + .detail-wrap { + display: flex; + flex-direction: column; + flex: 1; + } + /* "last chance" treatment - a soft warm peach tint + border, distinct from the normal panel background but staying within the existing pastel/ parchment palette. A faint glow (box-shadow) adds a touch of warmth without being a jarring "alert" color like red. */ - .detail-content.last-chance { + .detail-wrap.last-chance { background: #fff6ea; border: 1px solid #f3dcb8; border-radius: 14px; @@ -302,7 +320,7 @@ } /* one-time prompt shown above the action buttons - same warm palette as - .detail-content.last-chance, slightly more saturated so it reads as the + .detail-wrap.last-chance, slightly more saturated so it reads as the "active" element within the card */ .last-chance-prompt { display: flex; diff --git a/src/lib/utils/geo.js b/src/lib/utils/geo.js deleted file mode 100644 index d22ee37..0000000 --- a/src/lib/utils/geo.js +++ /dev/null @@ -1,43 +0,0 @@ -// --- geo utilities for "sensing mode" (compass arrow + proximity vibration) - -// Both formulas are standard great-circle formulas. They're approximations -// (Earth isn't a perfect sphere), but for the distances this app cares about -// - "is this pin within ~50m" and "which direction is it in" within a single -// ~1.2km neighborhood - that error is negligible. - -const EARTH_RADIUS_M = 6371000; // mean Earth radius, in meters - -function toRad(deg) { - return (deg * Math.PI) / 180; -} - -function toDeg(rad) { - return (rad * 180) / Math.PI; -} - -// Haversine formula: great-circle distance between two lat/lng points, in -// meters. Used by the proximity-vibration check to find pins within ~50m. -export function distanceMeters(lat1, lng1, lat2, lng2) { - const dLat = toRad(lat2 - lat1); - const dLng = toRad(lng2 - lng1); - - const a = - Math.sin(dLat / 2) ** 2 + - Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; - - return EARTH_RADIUS_M * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); -} - -// Initial bearing (compass direction in degrees, 0-360, 0 = true north, -// clockwise) from point 1 to point 2. Used by the compass arrow to figure -// out which way the nearest off-screen pin is, before adjusting for the -// device's own heading. -export function bearingDegrees(lat1, lng1, lat2, lng2) { - const phi1 = toRad(lat1); - const phi2 = toRad(lat2); - const dLng = toRad(lng2 - lng1); - - const y = Math.sin(dLng) * Math.cos(phi2); - const x = Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(dLng); - - return (toDeg(Math.atan2(y, x)) + 360) % 360; -} diff --git a/src/lib/utils/geohash.js b/src/lib/utils/geohash.js index 477162d..c6557f3 100644 --- a/src/lib/utils/geohash.js +++ b/src/lib/utils/geohash.js @@ -17,4 +17,26 @@ export function encode(lat, lng) { // stored on existing messages (see encode() above) export function getQueryPrefix(lat, lng) { return ngeohash.encode(lat, lng, 6); +} + +// --- geohash cell-boundary problem ---------------------------------------- +// A single precision-6 geohash cell is only ~1.2km x 0.6km. Two devices +// standing in the same real-world spot can still land in DIFFERENT cells if +// their reported coordinates fall on opposite sides of a cell edge - this is +// very common, since desktop browsers usually resolve location via WiFi/IP +// (often off by hundreds of meters) while phones use GPS (much tighter). +// Querying only getQueryPrefix(lat, lng)'s single cell means one device's +// query can miss messages that are physically just a few meters away, on the +// other side of that edge - which is exactly the "different pin counts on +// different devices" symptom. +// +// getNearbyGeohashCells returns the user's cell PLUS its 8 surrounding cells +// (a 3x3 grid, via ngeohash.neighbors). Querying all 9 means that as long as +// both devices' reported positions are within roughly one cell-width of each +// other (which covers normal GPS/WiFi error), their 3x3 grids overlap enough +// to cover the same messages - so both devices see the same "nearby" set +// even if they're each centered in a different cell. +export function getNearbyGeohashCells(lat, lng) { + const center = getQueryPrefix(lat, lng); + return [center, ...ngeohash.neighbors(center)]; } \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 179dc2f..77ced9e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,7 +1,6 @@