PageSourceSearch

https://aurora-c9c22.web.app/js/pinManager.js?v=7ceda6c0cf

js aurora-c9c22.web.app collected 2026-10-03 12:17:48 UTC 7,365 bytes, 222 lines download raw bytes

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.