1import { storageGetItem, storageSetItem, showConsentNudge } from './utils/consentStorage.js'; 2 3const HOME_STORAGE_KEY = 'auroraHomeLocation'; 4 5export const pinState = { 6 locationPins: [], 7 homePin: null, 8 lastAdd: { time: 0, lat: null, lon: null }, 9 MAX_PINS: 1 10}; 11 12export const PinManager = { 13 DEDUPE_TOLERANCE: 0.01, 14 QUICK_LOCK_MS: 700, 15 16 isNearby(lat1, lon1, lat2, lon2, tolerance = this.DEDUPE_TOLERANCE) { 17 return Math.abs(lat1 - lat2) <= tolerance && Math.abs(lon1 - lon2) <= tolerance; 18 }, 19 20 findExisting(lat, lon) { 21 return pinState.locationPins.find(pin => 22 pin?._lat !== undefined && this.isNearby(pin._lat, pin._lon, lat, lon) 23 ); 24 }, 25 26 isRecentDuplicate(lat, lon) { 27 const now = Date.now(); 28 return pinState.lastAdd.time && 29 (now - pinState.lastAdd.time) < this.QUICK_LOCK_MS && 30 this.isNearby(pinState.lastAdd.lat, pinState.lastAdd.lon, lat, lon); 31 }, 32 33 isHomeLocation(lat, lon) { 34 try { 35 const raw = storageGetItem(HOME_STORAGE_KEY); 36 if (!raw) return false; 37 const saved = JSON.parse(raw); 38 return this.isNearby(saved.lat, saved.lon, lat, lon, 0.005); 39 } catch (e) { 40 return false; 41 } 42 }, 43 44 removeAllHomePins(globe) { 45 if (pinState.homePin) { 46 globe.entities.remove(pinState.homePin); 47 pinState.homePin = null; 48 } 49 const allEntities = globe.entities.values.slice(); 50 allEntities.forEach(ent => { 51 if (ent._isHomePin) globe.entities.remove(ent); 52 }); 53 }, 54 55 enforceMaxPins(globe) { 56 while (pinState.locationPins.length > pinState.MAX_PINS) { 57 globe.entities.remove(pinState.locationPins.shift()); 58 } 59 } 60}; 61 62// --- Helper: High-Res Rounded Label Generator --- 63function createHighResLabel(text) { 64 const canvas = document.createElement('canvas'); 65 const ctx = canvas.getContext('2d'); 66 67 // Quality Multiplier (3x resolution for crispness) 68 const s = 3; 69 70 // Base Settings 71 const fontSize = 14; 72 const font = `bold ${fontSize * s}px "Inter", sans-serif`; 73 const paddingX = 20 * s; 74 const paddingY = 12 * s; 75 const radius = 12 * s; 76 const backgroundColor = 'rgba(15, 23, 42, 0.95)'; // Dark Navy #0f172a 77 78 // 1. Measure Text 79 ctx.font = font; 80 const metrics = ctx.measureText(text); 81 const textWidth = metrics.width; 82 83 // 2. Size Canvas 84 const w = textWidth + paddingX * 2; 85 const h = (30 * s) + paddingY; 86 canvas.width = w; 87 canvas.height = h; 88 89 // 3. Draw Rounded Background (Manual path for browser compatibility) 90 ctx.fillStyle = backgroundColor; 91 ctx.beginPath(); 92 ctx.moveTo(radius, 0); 93 ctx.lineTo(w - radius, 0); 94 ctx.quadraticCurveTo(w, 0, w, radius); 95 ctx.lineTo(w, h - radius);
96 ctx.quadraticCurveTo(w, h, w - radius, h); 97 ctx.lineTo(radius, h); 98 ctx.quadraticCurveTo(0, h, 0, h - radius); 99 ctx.lineTo(0, radius); 100 ctx.quadraticCurveTo(0, 0, radius, 0); 101 ctx.closePath(); 102 ctx.fill(); 103 104 // 4. Draw Text 105 ctx.font = font; 106 ctx.fillStyle = 'white'; 107 ctx.textAlign = 'center'; 108 ctx.textBaseline = 'middle'; 109 ctx.fillText(text, w / 2, h / 2 + (2 * s)); 110 111 return { canvas, scale: 1 / s }; 112} 113 114// --- Main Exported Functions --- 115 116export function addLocationPin(lat, lon, probability, globe) { 117 if (!globe && window.globe) globe = window.globe; 118 if (!globe) return null; 119 120 // 1. Redirect to Home Logic if matches 121 if (PinManager.isHomeLocation(lat, lon)) { 122 return addHomeLocationPin(lat, lon, globe, probability); 123 } 124 125 // 2. Standard Checks 126 const existing = PinManager.findExisting(lat, lon); 127 if (existing) return existing; 128 if (PinManager.isRecentDuplicate(lat, lon)) return null; 129 130 // 3. Generate High-Res Image (Standard Pin Style) 131 // We shorten the text to match the clean Home pin style 132 const { canvas, scale } = createHighResLabel(`ð ${probability}`); 133 134 // 4. Create Entity with Billboard 135 const pin = globe.entities.add({ 136 lon, lat, 137 point: { size: 14, color: 'yellow', outlineColor: 'black', outlineWidth: 2 }, // Yellow for Search 138 billboard: { image: canvas, scale, anchor: 'bottom', offset: [0, -20] }, 139 }); 140 141 pin._isLocationPin = true; 142 pin._lat = lat; 143 pin._lon = lon; 144 145 pinState.locationPins.push(pin); 146 pinState.lastAdd = { time: Date.now(), lat, lon }; 147 148 PinManager.enforceMaxPins(globe); 149 return pin; 150} 151 152/** 153 * Drop the blue "home" pin. 154 * 155 * @param {object} [options] 156 * @param {string|null} options.name Human-readable place name to persist. 157 * @param {boolean} options.persist Write the record to storage. Pass false 158 * when *restoring* an already-saved home: re-persisting on load used to 159 * overwrite the saved city name with the literal "Home", and re-fired the 160 * consent nudge on every page load for anyone who had opted out of storage. 161 */ 162export function addHomeLocationPin(lat, lon, globe, probabilityOrFunc, options = {}) { 163 const { name = null, persist = true } = options; 164 if (!globe && window.globe) globe = window.globe; 165 if (!globe) return null; 166 167 // 1. Clean slate 168 PinManager.removeAllHomePins(globe); 169 170 // 2. Resolve Probability 171 let probability = "Loading..."; 172 if (typeof probabilityOrFunc === 'function') { 173 probability = probabilityOrFunc(lat, lon); 174 } else if (probabilityOrFunc !== undefined) { 175 probability = probabilityOrFunc; 176 } 177 178 // 3. Generate High-Res Image (Home Pin Style) 179 const { canvas, scale } = createHighResLabel(`ð Home ⢠${probability}`); 180 181 // 4. Create Entity with Billboard 182 const homePin = globe.entities.add({ 183 lon, lat, 184 point: { size: 14, color: '#38bdf8', outlineColor: 'black', outlineWidth: 2 }, // Blue/Cyan for Home 185 billboard: { image: canvas, scale, anchor: 'bottom', offset: [0, -20] }, 186 }); 187 188 homePin._isHomePin = true; 189 homePin._lat = lat; 190 homePin._lon = lon; 191 pinState.homePin = homePin; 192 193 // Save state 194 if (persist) { 195 // Keep whatever name the place already had unless a new one is given â 196 // "Fairbanks, AK" is far more useful than a generic "Home" to the alert 197 // and diagnostics code that reads this record back. 198 let resolvedName = name; 199 if (!resolvedName) { 200 try { 201 const previous = JSON.parse(storageGetItem(HOME_STORAGE_KEY) || 'null'); 202 if (previous && previous.lat === lat && previous.lon === lon && previous.name) { 203 resolvedName = previous.name; 204 } 205 } catch (err) { /* corrupt record: fall through to the default */ } 206 } 207 208 const homeData = { lat, lon, name: resolvedName || 'Home' }; 209 const saved = storageSetItem(HOME_STORAGE_KEY, JSON.stringify(homeData)); 210 if (!saved) showConsentNudge("Home location set for this session only â it won't be remembered on your next visit."); 211 } 212 213 return homePin; 214} 215 216export function removeLocationPin(entity, globe) { 217 if (!entity) return; 218 globe.entities.remove(entity); 219 220 const idx = pinState.locationPins.indexOf(entity); 221 if (idx >= 0) pinState.locationPins.splice(idx, 1); 222}
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.