https://www.kalajokilaakso.fi/wp-content/themes/hilla-theme/assets/js/location.js?version=2025121611
1/* 2 Hilla Geolocation module 3 - Requests user's location permission and fetches live coords 4 - Enriches with locality data (name, country, geoid) from FMI opendata 5 - Caches last known location in localStorage for up to 3 days 6 - Falls back to cached location if permission denied or errors occur 7 - Dispatches a CustomEvent 'ew:location' on window with { status, data, error } 8 - Exposes window.EWLocation with utility helpers 9*/ 10(function () { 11 const STORAGE_KEY = "ew_location"; 12 const MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000; // 3 days 13 const LIVE_THROTTLE_MS = 5 * 60 * 1000; // 5 minutes 14 const FMI_ENDPOINT = "https://opendata.fmi.fi/timeseries"; 15 16 const nowTs = () => Date.now(); 17 18 function isFresh(ts) { 19 if (!ts) return false; 20 try { 21 const t = typeof ts === "number" ? ts : new Date(ts).getTime(); 22 return nowTs() - t < MAX_AGE_MS; 23 } catch (e) { 24 return false; 25 } 26 } 27 28 // Generic recency checker for custom windows (e.g., 5-minute throttle) 29 function isRecent(ts, windowMs) { 30 if (!ts) return false; 31 try { 32 const t = typeof ts === "number" ? ts : new Date(ts).getTime(); 33 return nowTs() - t < windowMs; 34 } catch (e) { 35 return false; 36 } 37 } 38 39 function readCache() { 40 try { 41 const raw = localStorage.getItem(STORAGE_KEY); 42 if (!raw) return null; 43 const obj = JSON.parse(raw); 44 if (!obj || !obj.timestamp) return null; 45 if (!isFresh(obj.timestamp)) { 46 localStorage.removeItem(STORAGE_KEY); 47 return null; 48 } 49 return obj; 50 } catch (e) { 51 return null; 52 } 53 } 54 55 function writeCache(data) { 56 try { 57 const payload = { 58 lat: data.lat, 59 lon: data.lon, 60 accuracy: data.accuracy || null, 61 place: data.place || null, 62 timestamp: data.timestamp || new Date().toISOString(), 63 source: data.source || "unknown", 64 }; 65 localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); 66 return payload; 67 } catch (e) { 68 // Best-effort caching; ignore failures 69 return data; 70 } 71 } 72 73 function dispatch(status, data, error) { 74 try { 75 const detail = { status, data: data || null, error: error || null }; 76 window.dispatchEvent(new CustomEvent("ew:location", { detail })); 77 } catch (e) { 78 // no-op 79 } 80 } 81 82 async function enrichWithFmi(lat, lon, abortSignal) { 83 const q = new URLSearchParams({ 84 format: "json", 85 latlon: `${lat.toFixed(5)},${lon.toFixed(5)}`, 86 // Include geoid in response 87 param: "latlon,name,country,geoid", 88 }); 89 const url = `${FMI_ENDPOINT}?${q.toString()}`; 90 const resp = await fetch(url, { signal: abortSignal }); 91 if (!resp.ok) throw new Error(`FMI ${resp.status}`); 92 const json = await resp.json(); 93 const first = Array.isArray(json) && json.length ? json[0] : null; 94 if (!first) return null; 95 const place = { 96 name: first.name || null, 97 country: first.country || null, 98 geoid: typeof first.geoid !== "undefined" ? first.geoid : null, 99 latlon: first.latlon || null, 100 provider: "fmi-opendata", 101 }; 102 return place; 103 } 104 105 function getPosition(options) { 106 return new Promise((resolve, reject) => { 107 if (!("geolocation" in navigator)) { 108 return reject(new Error("geolocation_unavailable")); 109 } 110 navigator.geolocation.getCurrentPosition(resolve, reject, options); 111 }); 112 } 113 114 async function requestLive(abortSignal) { 115 const geoOpts = {
116 enableHighAccuracy: false, 117 timeout: 8000, 118 maximumAge: 60000, 119 }; 120 const pos = await getPosition(geoOpts); 121 const { latitude, longitude, accuracy } = pos.coords || {}; 122 const base = { 123 lat: latitude, 124 lon: longitude, 125 accuracy: accuracy, 126 timestamp: new Date().toISOString(), 127 source: "live", 128 }; 129 try { 130 // Try FMI first 131 const place1 = await enrichWithFmi(latitude, longitude, abortSignal); 132 base.place = place1 || null; 133 134 if (localStorage.getItem("geoid") !== place1.geoid) 135 localStorage.removeItem("geoid"); 136 } catch (e) { 137 // Enrichment failed; continue with bare coords 138 } 139 return writeCache(base); 140 } 141 142 async function init() { 143 if (!window.FALLBACK_GEOID) return; // Feature is not configured 144 145 // Purge stale cache proactively 146 const cached = readCache(); 147 if (cached) { 148 // Notify early with cached data while attempting live refresh 149 dispatch("cache", cached, null); 150 } 151 152 // Abort controller to prevent hanging fetches when page unloads 153 const ac = 154 typeof AbortController !== "undefined" ? new AbortController() : null; 155 const signal = ac ? ac.signal : undefined; 156 157 try { 158 // Some browsers require secure origins for geolocation 159 const isSecure = window.isSecureContext || location.protocol === "https:"; 160 if (!isSecure) throw new Error("insecure_context"); 161 162 // Throttle live location requests to at most once per 5 minutes 163 if (cached && isRecent(cached.timestamp, LIVE_THROTTLE_MS)) { 164 // We've already fetched live recently; stick with the cached value 165 state.lastKnown = cached; 166 state.status = "cache"; 167 return; 168 } 169 170 const live = await requestLive(signal); 171 dispatch("live", live, null); 172 state.lastKnown = live; 173 state.status = "live"; 174 return; 175 } catch (e) { 176 const fallback = cached && isFresh(cached.timestamp) ? cached : null; 177 if (fallback) { 178 dispatch("cache", fallback, null); 179 state.lastKnown = fallback; 180 state.status = "cache"; 181 } else { 182 dispatch("error", null, e && (e.message || String(e))); 183 state.status = "error"; 184 } 185 } finally { 186 // Best-effort abort 187 try { 188 if (ac) ac.abort(); 189 } catch (_) {} 190 } 191 } 192 193 const subscribers = new Set(); 194 const onEvent = (ev) => { 195 try { 196 const detail = ev.detail || {}; 197 subscribers.forEach((cb) => { 198 try { 199 cb(detail); 200 } catch (_) {} 201 }); 202 } catch (_) {} 203 }; 204 window.addEventListener("ew:location", onEvent); 205 206 const state = { 207 status: "init", 208 lastKnown: readCache(), 209 }; 210 211 window.EWLocation = { 212 // Returns the freshest available data; may resolve from cache immediately 213 async getLatest() { 214 const isSecure = window.isSecureContext || location.protocol === "https:"; 215 // Prefer an in-memory lastKnown if present 216 const fromState = state.lastKnown || null; 217 const cached = readCache() || fromState || null; 218 219 // If we have a cached value and it's within the throttle window, return it 220 if (cached && isRecent(cached.timestamp, LIVE_THROTTLE_MS)) { 221 return cached; 222 } 223 224 // If we can, attempt a live refresh; otherwise return whatever we have 225 if (isSecure) { 226 try { 227 const live = await requestLive(); 228 state.lastKnown = live; 229 state.status = "live"; 230 return live; 231 } catch (_) { 232 // swallow and fall back 233 } 234 } 235 236 return cached; 237 }, 238 getCached() { 239 return readCache(); 240 }, 241 subscribe(cb) { 242 if (typeof cb === "function") subscribers.add(cb); 243 return () => subscribers.delete(cb); 244 }, 245 get status() { 246 return state.status; 247 }, 248 }; 249 250 // Auto-start on load 251 try { 252 init(); 253 } catch (_) {} 254})();
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.