PageSourceSearch

https://www.kalajokilaakso.fi/wp-content/themes/hilla-theme/assets/js/location.js?version=2025121611

js kalajokilaakso.fi collected 2026-10-02 13:24:05 UTC 7,404 bytes, 254 lines download raw bytes

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.