diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 00000000..242da623 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4ef48f43..6dc4ad33 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,6 @@ a unified set of frontends. ## License GPL-2.0-or-later. See [`LICENSES`](LICENSES) for the full license text and -bundled third-party license files. Bundled third-party components (Leaflet and -the Leaflet AIS tracksymbol plugin under `assets/web/vendor/`) retain their -original BSD-2-Clause license. +bundled third-party license files. Bundled third-party components retain their +original licenses: Leaflet is BSD-2-Clause, DSEG is OFL-1.1, and opus-decoder +is MIT. diff --git a/REUSE.toml b/REUSE.toml index f46d1a72..a4fb4239 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -42,3 +42,11 @@ SPDX-License-Identifier = "BSD-2-Clause" path = ["src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/dseg14-classic-latin-400-normal.woff2"] SPDX-FileCopyrightText = "2020 The DSEG Authors (https://github.com/keshikan/DSEG)" SPDX-License-Identifier = "OFL-1.1" + +# Vendored opus-decoder 0.7.11 browser build +# (https://github.com/eshaz/wasm-audio-decoders), MIT. +# SHA-256: fd73ee0a9c8a5e233c0b88234df14f46003ad89bb4ba27435bc8714db2a6dc62 +[[annotations]] +path = ["src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/opus-decoder-0.7.11.min.js"] +SPDX-FileCopyrightText = "2021-2025 Ethan Halsall" +SPDX-License-Identifier = "MIT" diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 9885f7d3..b2909005 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -645,7 +645,7 @@ function flushDeferredDecodeMapSync() { if (!decodeMapSyncPending || decodeHistoryReplayActive || !window.trx?.map?.aprsMap) return; decodeMapSyncPending = false; scheduleUiFrameJob("decode-map-maintenance", () => { - window.trx.map?.pruneMapHistory(); + window.trx.modules.map?.pruneMapHistory(); }); } @@ -1279,7 +1279,7 @@ function applyRigList(activeRigId, rigIds, displayNames) { if (typeof bmPopulateScopePicker === "function") bmPopulateScopePicker(); if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || ""); } - window.trx.map?.updateMapRigFilter(); + window.trx.modules.map?.updateMapRigFilter(); } @@ -1305,7 +1305,7 @@ async function refreshRigList() { serverRigs = rigs; serverActiveRigId = data.active_remote || null; applyRigList(data.active_remote, rigIds, displayNames); - window.trx.map?.syncAprsReceiverMarker(); + window.trx.modules.map?.syncAprsReceiverMarker(); } catch (e) { // Non-fatal: SSE/status path still drives main UI. } @@ -3143,9 +3143,9 @@ function render(update) { const grid = latLonToMaidenhead(serverLat, serverLon); locationSubtitle.textContent = `Location: ${grid}`; locationSubtitle.style.display = ""; - window.trx.map?.reverseGeocodeLocation(serverLat, serverLon, grid); + window.trx.modules.map?.reverseGeocodeLocation(serverLat, serverLon, grid); } - window.trx.map?.syncAprsReceiverMarker(); + window.trx.modules.map?.syncAprsReceiverMarker(); if (typeof update.initial_map_zoom === "number" && Number.isFinite(update.initial_map_zoom)) { initialMapZoom = Math.max(1, Math.round(update.initial_map_zoom)); } @@ -3894,7 +3894,7 @@ async function switchRigFromSelect(selectEl) { if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId); if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId); if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || ""); - window.trx.map?.syncAprsReceiverMarker(); + window.trx.modules.map?.syncAprsReceiverMarker(); // Switch this session's rig and reconnect SSE to the new rig's // state channel. try { @@ -4480,23 +4480,23 @@ function updateTabHistory(name, replaceHistory = false) { } // Initialise the Leaflet map, waiting for both Leaflet (L) and map-core.js -// (window.trx.map) if they haven't loaded yet. +// (window.trx.modules.map) if they haven't loaded yet. let _mapInitTimer = null; function _initMapWhenReady() { const loadingEl = document.getElementById("map-loading"); - if (window.trx.map && typeof L !== "undefined") { + if (window.trx.modules.map && typeof L !== "undefined") { if (_mapInitTimer) { clearInterval(_mapInitTimer); _mapInitTimer = null; } if (loadingEl) loadingEl.classList.add("is-hidden"); - window.trx.map.initAprsMap(); - window.trx.map.sizeAprsMapToViewport(); + window.trx.modules.map.initAprsMap(); + window.trx.modules.map.sizeAprsMapToViewport(); // The map panel was just made visible (display:none → ""); the browser // may not have laid it out yet, so getBoundingClientRect() can return // stale/zero dimensions. Double-rAF ensures a full layout pass has // completed before we re-measure and tell Leaflet about its real size. requestAnimationFrame(() => { requestAnimationFrame(() => { - window.trx.map.sizeAprsMapToViewport(); - if (window.trx.map.aprsMap) window.trx.map.aprsMap.invalidateSize(); + window.trx.modules.map.sizeAprsMapToViewport(); + if (window.trx.modules.map.aprsMap) window.trx.modules.map.aprsMap.invalidateSize(); }); }); return; @@ -4544,7 +4544,7 @@ function navigateToTab(name, options = {}) { _initMapWhenReady(); } if (name === "statistics") { - window.trx.map?.scheduleStatsRender(); + window.trx.modules.map?.scheduleStatsRender(); } if (name === "recorder") { refreshRecorderStatus(); @@ -4721,10 +4721,11 @@ if (headerAuthBtn) { // ── Shared namespace for lazy-loaded modules ──────────────────────────────── // Modules (map-core.js, screenshot.js) access core state and utilities via // window.trx. Modules register their own APIs as sub-namespaces -// (e.g. window.trx.map, window.trx.screenshot). -window.trx = Object.create(null); +// (e.g. window.trx.modules.map, window.trx.modules.screenshot). +const trxState = Object.create(null); +const trxModules = Object.create(null); // -- State getters (backed by core-scoped variables) -- -Object.defineProperties(window.trx, { +Object.defineProperties(trxState, { serverLat: { get() { return serverLat; }, set(v) { serverLat = v; } }, serverLon: { get() { return serverLon; }, set(v) { serverLon = v; } }, lastFreqHz: { get() { return lastFreqHz; } }, @@ -4762,7 +4763,7 @@ Object.defineProperties(window.trx, { signalOverlayGl: { get() { return signalOverlayGl; } }, }); // -- Shared utility functions -- -Object.assign(window.trx, { +const trxCore = Object.freeze({ saveSetting, loadSetting, showHint, escapeMapHtml, formatFreq, formatFreqForHumans, formatWavelength, formatBwLabel, formatUptime, formatSigStrength, formatSignal, postPath, scheduleUiFrameJob, navigateToTab, rigBadgeColor, @@ -4772,18 +4773,19 @@ Object.assign(window.trx, { currentTheme, canvasPalette, currentStyle, cssColorToRgba, rgbaWithAlpha, isBinsArray, estimateNoiseFloorDb, spectrumVisibleRange, drawSpectrum, - bandForHz: function(hz) { return window.trx.map?.bandForHz?.(hz); }, + bandForHz: function(hz) { return trxModules.map?.bandForHz?.(hz); }, markDecodeMapSyncPending, decodeHistoryMapRenderingDeferred, updateDocumentTitle, activeChannelRds, }); -Object.defineProperties(window.trx, { +Object.defineProperties(trxState, { decodeHistoryReplayActive: { get() { return decodeHistoryReplayActive; } }, decodeMapSyncPending: { get() { return decodeMapSyncPending; } }, _activeTab: { get() { return _activeTab; } }, locationSubtitle: { get() { return locationSubtitle; } }, }); +window.trx = Object.freeze({ state: trxState, core: trxCore, modules: trxModules }); // Load plugin scripts now that window.trx is populated. Dynamic scripts are // async so they must not be created before the namespace they depend on exists. @@ -4797,7 +4799,7 @@ window.addEventListener("resize", resizeHeaderSignalCanvas); // ── Map module (extracted to map-core.js, lazy-loaded) ────────────────────── // The map, statistics, and geolocation code (~3,450 lines) has been moved to // map-core.js and is loaded on demand when the Map tab is first activated. -// Core communicates with the map module via window.trx.map.* namespace. +// Core communicates with the map module via window.trx.modules.map.* namespace. // ── Geo utilities (shared with map-core.js via window.trx) ───────────────── function haversineKm(lat1, lon1, lat2, lon2) { @@ -4932,7 +4934,7 @@ document.querySelectorAll(".sub-tab-bar").forEach(_wireSubTabBar); window.addEventListener("resize", () => { const mapTab = document.getElementById("tab-map"); if (!mapTab || mapTab.style.display === "none") return; - window.trx.map?.sizeAprsMapToViewport(); + window.trx.modules.map?.sizeAprsMapToViewport(); }); // --- Signal measurement --- @@ -6102,8 +6104,8 @@ function dispatchDecodeMessage(msg, skipStats) { if (msg.type === "wefax" && window.onServerWefax) window.onServerWefax(msg); if (msg.type === "wefax_progress" && window.onServerWefaxProgress) window.onServerWefaxProgress(msg); if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") { - window.trx.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null); - window.trx.map?.scheduleStatsRender(); + window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null); + window.trx.modules.map?.scheduleStatsRender(); } } @@ -6112,10 +6114,10 @@ function dispatchDecodeBatch(batch) { // Record statistics for every message in the batch regardless of dispatch path. for (const msg of batch) { if (msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") { - window.trx.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null); + window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null); } } - window.trx.map?.scheduleStatsRender(); + window.trx.modules.map?.scheduleStatsRender(); const type = String(batch[0]?.type || ""); const uniformType = batch.every((msg) => String(msg?.type || "") === type); if (uniformType) { @@ -6200,9 +6202,9 @@ function restoreDecodeHistoryGroup(kind, messages) { // Record statistics for restored history messages. if (kind !== "lrpt_image" && kind !== "lrpt_progress" && kind !== "wefax" && kind !== "wefax_progress") { for (const msg of messages) { - window.trx.map?.statsRecordDecode(kind, msg.rig_id || msg.remote || null, msg.ts_ms || undefined); + window.trx.modules.map?.statsRecordDecode(kind, msg.rig_id || msg.remote || null, msg.ts_ms || undefined); } - window.trx.map?.scheduleStatsRender(); + window.trx.modules.map?.scheduleStatsRender(); } if (kind === "ais") { if (window.restoreAisHistory) { window.restoreAisHistory(messages); } @@ -8000,12 +8002,12 @@ window.addEventListener("keydown", (event) => { // S — spectrum screenshot (lazy-loads screenshot.js on first use) if (key === "s") { event.preventDefault(); - if (window.trx.screenshot) { - void window.trx.screenshot.captureSpectrumScreenshot(); + if (window.trx.modules.screenshot) { + void window.trx.modules.screenshot.captureSpectrumScreenshot(); } else { const s = document.createElement("script"); s.src = "/screenshot.js"; - s.onload = () => { void window.trx.screenshot?.captureSpectrumScreenshot(); }; + s.onload = () => { void window.trx.modules.screenshot?.captureSpectrumScreenshot(); }; document.body.appendChild(s); } return; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 53464377..a358b025 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -1623,7 +1623,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
Preparing recent decodes for the UI
- + diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/map-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/map-core.js index da82dc7d..2e69dd8b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/map-core.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/map-core.js @@ -3,10 +3,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Map, statistics, and geolocation module (lazy-loaded on map tab activation). -// Communicates with app.js core via window.trx namespace. +// Communicates with app.js through explicit state/core/module services. (function () { "use strict"; - const T = window.trx; + const { state: T, core: C, modules } = window.trx; // Destructure shared utility functions for convenience const { saveSetting, loadSetting, showHint, escapeMapHtml, formatFreq, formatFreqForHumans, @@ -14,7 +14,7 @@ formatUptime, latLonToMaidenhead, locatorToLatLon, haversineKm, formatDistanceKm, formatTimeAgo, currentDecodeHistoryRetentionMs, formatWavelength, bookmarkDistanceText, buildBookmarkTooltipText, - nearestBookmarkForHz } = T; + nearestBookmarkForHz } = C; function updateMapRigFilter() { const el = document.getElementById("map-rig-filter"); @@ -261,7 +261,7 @@ if (canRenderMap) { refreshAprsTrack(call, entry); } else { - T.markDecodeMapSyncPending(); + C.markDecodeMapSyncPending(); } if (!visible) { if (canRenderMap && selectedAprsTrackCall && String(selectedAprsTrackCall) === String(call)) { @@ -294,7 +294,7 @@ if (canRenderMap) { refreshAisTrack(key, entry); } else { - T.markDecodeMapSyncPending(); + C.markDecodeMapSyncPending(); } if (!visible) { if (canRenderMap && selectedAisTrackMmsi && String(selectedAisTrackMmsi) === String(key)) { @@ -337,7 +337,7 @@ entry.stations = new Set(); entry.bandMeta = new Map(); if (canRenderMap) setRetainedMapMarkerVisible(entry.marker, false); - else T.markDecodeMapSyncPending(); + else C.markDecodeMapSyncPending(); return false; } const nextStations = new Set(); @@ -352,7 +352,7 @@ ); const count = Math.max(nextDetails.size, nextStations.size || 0, 1); if (!canRenderMap) { - T.markDecodeMapSyncPending(); + C.markDecodeMapSyncPending(); return true; } ensureDecodeLocatorMarker(entry); @@ -392,7 +392,7 @@ pruneLocatorEntry(key, entry, cutoffMs); } if (!aprsMap || T.decodeHistoryReplayActive) { - T.markDecodeMapSyncPending(); + C.markDecodeMapSyncPending(); return; } rebuildDecodeContactPaths(); @@ -415,7 +415,7 @@ function locatorFilterColor(type) { const hues = locatorThemeHues(); - const lightTheme = T.currentTheme() === "light"; + const lightTheme = C.currentTheme() === "light"; const sat = lightTheme ? 66 : 76; const light = lightTheme ? 42 : 56; const hue = type === "bookmark" @@ -539,7 +539,7 @@ } function locatorThemeHues() { - const pal = T.canvasPalette(); + const pal = C.canvasPalette(); const baseHue = paletteHue(pal?.spectrumLine, 145); const waveHue = paletteHue(pal?.waveformLine, baseHue + 34); const peakHue = paletteHue(pal?.waveformPeak, baseHue - 42); @@ -560,7 +560,7 @@ function locatorBandChipColor(label) { const hues = locatorThemeHues(); - const lightTheme = T.currentTheme() === "light"; + const lightTheme = C.currentTheme() === "light"; const hue = wrapHue(hues.bandBase + locatorBandIndex(label) * 137.508); const sat = lightTheme ? 68 : 78; const light = lightTheme ? 44 : 58; @@ -606,7 +606,7 @@ const safeCount = Math.max(1, Number.isFinite(count) ? count : 1); const intensity = Math.min(1, Math.log2(safeCount + 1) / 5); const hue = locatorHueForEntry(entry); - const lightTheme = T.currentTheme() === "light"; + const lightTheme = C.currentTheme() === "light"; const strokeSat = lightTheme ? 62 : 74; const fillSat = lightTheme ? 68 : 78; const strokeLight = lightTheme ? 40 : 56; @@ -1573,7 +1573,7 @@ stageResizeObserver = new ResizeObserver(() => sizeAprsMapToViewport()); stageResizeObserver.observe(stage); } - updateMapBaseLayerForTheme(T.currentTheme()); + updateMapBaseLayerForTheme(C.currentTheme()); syncAprsReceiverMarker(); // Rebuild popup content on open (keeps age/distance/rig list fresh) @@ -2307,18 +2307,18 @@ existing.rigIds.add(msgRigId); } if (!visible) { - if (!T.decodeHistoryMapRenderingDeferred()) { + if (!C.decodeHistoryMapRenderingDeferred()) { setRetainedMapMarkerVisible(existing.marker, false); } else { - T.markDecodeMapSyncPending(); + C.markDecodeMapSyncPending(); } return; } - if (!T.decodeHistoryMapRenderingDeferred()) { + if (!C.decodeHistoryMapRenderingDeferred()) { ensureVdesMarker(key, existing); setRetainedMapMarkerVisible(existing.marker, true); } else { - T.markDecodeMapSyncPending(); + C.markDecodeMapSyncPending(); } if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) { existing.marker.setLatLng([msg.lat, msg.lon]); @@ -2334,11 +2334,11 @@ }; vdesMarkers.set(key, entry); if (!visible) return; - if (!T.decodeHistoryMapRenderingDeferred()) { + if (!C.decodeHistoryMapRenderingDeferred()) { ensureVdesMarker(key, entry); setRetainedMapMarkerVisible(entry.marker, true); } else { - T.markDecodeMapSyncPending(); + C.markDecodeMapSyncPending(); } if (aprsMap && entry.marker && !T.decodeHistoryReplayActive) { entry.marker.setPopupContent(popupHtml); @@ -2365,7 +2365,7 @@ if (T.locationSubtitle) { T.locationSubtitle.textContent = `Location: ${grid} · ${label}`; } - T.updateDocumentTitle(T.activeChannelRds()); + C.updateDocumentTitle(C.activeChannelRds()); }) .catch(() => {}); } @@ -2443,8 +2443,8 @@ } function scheduleDecodeMapMaintenance() { - if (T.decodeHistoryMapRenderingDeferred()) { - T.markDecodeMapSyncPending(); + if (C.decodeHistoryMapRenderingDeferred()) { + C.markDecodeMapSyncPending(); return; } scheduleUiFrameJob("decode-map-maintenance", () => { @@ -3461,7 +3461,7 @@ } // Register module API for core to call - window.trx.map = { + modules.map = { initAprsMap, sizeAprsMapToViewport, syncAprsReceiverMarker, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/screenshot.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/screenshot.js index f861d18d..7f57cc5a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/screenshot.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/screenshot.js @@ -257,7 +257,7 @@ } // Register module API - window.trx.screenshot = { + window.trx.modules.screenshot = { captureSpectrumScreenshot, buildSpectrumSnapshotCanvas, saveCanvasAsPng, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/opus-decoder-0.7.11.min.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/opus-decoder-0.7.11.min.js new file mode 100644 index 00000000..55f0307e --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/opus-decoder-0.7.11.min.js @@ -0,0 +1,232 @@ +/* Copyright 2021-2025 Ethan Halsall. This file is part of wasm-audio-decoders. https://github.com/eshaz/wasm-audio-decoders */ +var i,t;i=this,t=function(i,t){const e=(i,t=4294967295,e=79764919)=>{const H=new Int32Array(256);let s,h,r,n=t;for(s=0;s<256;s++){for(r=s<<24,h=8;h>0;--h)r=2147483648&r?r<<1^e:r<<1;H[s]=r}for(s=0;s>24^i[s])];return n},H=(i,t=e)=>{const H=i=>new Uint8Array(i.length/2).map((t,e)=>parseInt(i.substring(2*e,2*(e+1)),16)),s=i=>H(i)[0],h=new Map;[,8364,,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,,381,,,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,,382,376].forEach((i,t)=>h.set(i,t));const r=new Uint8Array(i.length);let n,M,o,a=!1,c=0,I=42,u=i.length>13&&"dynEncode"===i.substring(0,9),y=0;u&&(y=11,M=s(i.substring(9,y)),M<=1&&(y+=2,I=s(i.substring(11,y))),1===M&&(y+=8,o=(i=>new DataView(H(i).buffer).getInt32(0,!0))(i.substring(13,y))));const l=256-I;for(let e=y;e255){const i=h.get(n);i&&(n=i+127)}a&&(a=!1,n-=64),r[c++]=n0?n+l:n-I}else a=!0;const d=r.subarray(0,c);if(u&&1===M){const i=t(d);if(i!==o){const t="Decode failed crc32 validation";throw console.error("`simple-yenc`\n",t+"\n","Expected: "+o+"; Got: "+i+"\n","Visit https://github.com/eshaz/simple-yenc for more information"),Error(t)}}return d};function s(){const i=Uint8Array,t=Float32Array;s.t||Object.defineProperties(s,{t:{value:new WeakMap},H:{value(i,t){s.t.set(i,Promise.resolve(t))}},M:{value(i,t){let e=s.t.get(i);return e||(t?e=WebAssembly.compile(H(t)):(t=i.I,e=s.u(t).then(i=>WebAssembly.compile(i))),s.t.set(i,e)),e}},p:{value(i,e){let H=new t(e),s=0,h=0;for(;s({W:i,channelData:t,samplesDecoded:e,sampleRate:H,bitDepth:s})},T:{value(i,t,e,H,h,r){let n,M,o=[];for(n=0;n{const H=String.raw`dynEncode012804c7886d(‰›•)((()>+ˆ*§§)§ˆ,§§§§)§ˆ+§§§)§+.-()(*)-+)(ˆ.7*§)i¸¸,3§(i¸¸,3/G+.••—š¡*(,˜ŽŽ(,3‡‡‰˜‡Š‰›+)2å:-œ),§H(P*DI*H(P*@I++hH)H*r,hH(H(P*WebAssembly.instantiate(i,{})).then(({exports:H})=>{const s=new Map(Object.entries(H)),h=s.get("puff"),r=s.get("memory").buffer,n=new i(r),M=new DataView(r);let o=s.get("__heap_base");const a=t.length,c=o;o+=4,M.setInt32(c,a,!0);const I=o;o+=a,n.set(t,I);const u=o;o+=4,M.setInt32(u,n.byteLength-o,!0),h(o,u,I,c),e(n.slice(o,o+M.getInt32(u,!0)))})})}}}),Object.defineProperty(this,"I",{enumerable:!0,get:()=>this.P}),this.U=(i,t,e)=>{let H=[],s=0;for(;s{const H=this.P.A(t.BYTES_PER_ELEMENT*i);return e&&this.Y.add(H),{N:H,F:i,O:new t(this.P.S,H,i)}},this.free=()=>{this.Y.forEach(i=>{this.P.free(i)}),this.Y.clear()},this.D=i=>{const t=[],e=new Uint8Array(this.P.S);for(let H=e[i];0!==H;H=e[++i])t.push(H);return String.fromCharCode.apply(null,t)},this.V=(i,t,e,H,s,h)=>{i.push({message:t,frameLength:e,frameNumber:H,inputBytes:s,outputSamples:h})},this.instantiate=(i,t)=>(t&&s.H(i,t),this.P=new i(s).instantiate(),this.Y=new Set,this.P.ready.then(()=>this))}class h extends((()=>globalThis.Worker||t)()){constructor(i,t,e,H){s.t||new s;let h=s.t.get(e);if(!h){let i,t="text/javascript",r=`'use strict';(${""+((i,t,e)=>{let H,s,h=new Promise(i=>{s=i});self.onmessage=({data:{id:r,command:n,data:M}})=>{let o,a=h,c={id:r};"init"===n?(Object.defineProperties(i,{$:{value:t},G:{value:e},module:{value:M.module},L:{value:!0}}),H=new i(M.options),s()):"free"===n?H.free():"ready"===n?a=a.then(()=>H.ready):"reset"===n?a=a.then(()=>H.reset()):(Object.assign(c,H[n](Array.isArray(M)?M.map(i=>new Uint8Array(i)):new Uint8Array(M))),o=c.channelData?c.channelData.map(i=>i.buffer):[]),a.then(()=>self.postMessage(c,o))}})})(${e}, ${s}, ${H})`;try{i=void 0!==process.versions.node}catch{}h=i?`data:${t};base64,${Buffer.from(r).toString("base64")}`:URL.createObjectURL(new Blob([r],{type:t})),s.t.set(e,h)}super(h,{name:t}),this.B=Number.MIN_SAFE_INTEGER,this.X=new Map,this.onmessage=({data:i})=>{const{id:t,...e}=i;this.X.get(t)(e),this.X.delete(t)},new H(s).M().then(t=>{this.Z("init",{module:t,options:i})})}async Z(i,t){return new Promise(e=>{this.postMessage({command:i,id:this.B,data:t}),this.X.set(this.B++,e)})}get ready(){return this.Z("ready")}async free(){await this.Z("free").finally(()=>{this.terminate()})}async reset(){await this.Z("reset")}}const r=(i,t)=>{Object.defineProperty(i,"name",{value:t})};function n(i){function t(){}for(var e,H,s={},h=Math.cos,r=new Uint8Array(123),M=25;M>=0;--M)r[48+M]=52+M,r[65+M]=M,r[97+M]=26+M;r[43]=62,r[47]=63;var o,a,c,I,u,y,l={d:()=>(()=>{throw""})(),c(){},e(i,t){if(s[i]&&(clearTimeout(s[i].id),delete s[i]),!t)return 0;var e=setTimeout(()=>{delete s[i],y(i,performance.now())},t);return s[i]={id:e,R:t},0},a:h,f:i=>(e.length,!1),b(i){throw`exit(${i})`}};n.I||Object.defineProperty(n,"I",{get:()=>String.raw`dynEncode013bc58350c1ŸÉð|@|O€*X–Ø3R}@³ùÆöÁš†•ƒ¤½Cu­ÙFõ¶=}Vò–nою‰–X³ˆW‰~‰aGÕ5? Q= Ú†……Î…N= ‚W„ ƒeÇ¸ß3í=MÛßǹHÜjLvïêÀ:m°‰¹30‚‹>ÉX!¿b ýŽOýn +®Fj{øú¢ç=}(ݝñŸÜnŸ moǰ¥à•eò?È×µh +ØW¨;Ñ aØX­æU{¤øàÖÕÀì·ÝÌ­íø±tâÔøeVO÷PÉ´È~ÃòsX1´ÊPyK·ÿ™ŒKZçµ ›W„Ñ¡¡_‘ ‘dÍ•í+]Õ~%Yïþîñ ¾Ú·”p[òß ¶ó÷y lhnn«²ù*¶Ìéñ®ï(éÇÃRÊÆRÌ-¬ÐoÒ”Ê2z+ä$‚:ÄsxÏ#Ït!MlBqžš§%Jè„ ƒ®Ä3ó°á¸eÅ_Ö=}iTÐ͆mo †ðbMpžl–¬E›P°e,¼Æju®ix>#}N€gd™ÿÇÄbç9WZÇ·n\†Öá1ڛÈt.ú6bì:û† ào¯ú4Ÿ“Ïø ü­zå¯#ÆkŸ‡M#úÿ9›ë:üÒÝëÚà†¡d”•%0ÒekÓ×mðƸ½ùÿ⇿Ò9 ›ÄgªÕÂ%ü¸» HÁMŽ#Âqˆ²~Œèn¯ Ûa_9j= ùÿR™èæÊó%Mƒ +’¨,Àæq%98‰a9%T—˜2)´z«9gsÒ‚Ö!”ZhRõRێyÕ²ŒÝ=M<Öcº »~VFÕÖx:þrޑðnŽ£x¦8.è†$K9þ¹úSnÍ3¦ˆv…üuºsÕñ=}œ1—=M5Zʕ¦CÿSÉ2×qÒ««S†˜a»ù±(¢º]ÀmÍh÷/¥3@sns2 –%7ù-Q|nÏoöjYÛ]}à2{€EB’êÎ6ƒ>ÁbÄü +á, [DAÐuùjeN‹D‹ EëÔàZ‘ºlq˜»J /˲ájº‚ßCÃÝ1 b)øtN7Ê=}gÕ®òÍá§8²kDïû¤åÒÅóºc=M²= aË.ooèf€.QZj@bñ>qV©?æ• ƒxñˆR ô|_xw ê ˆ¸÷È¡ö"C¶œ2[¼‹ï¯¸©K†ŠmäºúZ€…¡%^°bóþ»-¼Ú¥ZIŽ(pþ…µS%øµã*Ò±8f:t¡éþ†õz¶<Äfñ*žöeP8AÓ¾/g×\С1Ï"= ¤ûGsW_¹ÔÈX Tãà‚uT™íEKPüݸ#åB¢ñ?:µßĬîFŽI8QY;X… ¶íŸ¦k{|¦éíà/fªi>´EÈoK= Š6¼l„Ör²ÿ*YǏv앭dàG{Ö,Gz½.gzSdïV%…’Š˜‰z“¥8¶²bO™¡2\nOžßV—¡S2KçK´”žUûu۝Ÿã~„f¾K)ÊÖ#´?#»ÅYƒXOøò T™CB}LUk,kÍ)]ŽíiEŸ^ÿyW)Ê»³= ô1_½úÌÞÊçýS¡‰ +»£jŒÆkžh Ð1˜G<è^àÙç?…æF¦Ç/ã«Eý6ZI¹tšl.‚aëül|„!&:MÓå?N@øËœÅf 6cì$w–,3~„17F=M𸳞°a‰õu\9æk ‡I—&>V®‡Æ/Øa´ †%¶¢],ÿ:±wäé¬áS¥FK±U¢±ÓPóŠßœ&“®ƒe€‘= ÍÍܯÏ×À@= Ø‹#‚Ðk[NCó÷À‚ÄÞŽGQRˆcnI}HGñ—\’ç­= öè^áygyDԋa©D”¦„…'ߣMߣPI¯à¨=}˜=MUÀ¸çy 9W½LšíU·ôzwzÃsÝZòŽÓ3ŸFž¸“—Á菣âƒÑ–’nՙ%‘Í¥k…á®=Mˆ×ŸGTÀÊøfëM-UF‡;ŠuåÙ#…X#‰— +nˆ©ï֟DÅñ¸q ’,Թи;h¿åñŸ#µNÓ÷¸v} *ځ‡_= ãýØb6N–=MH§'®Iƒu¯78˜–‘ªÊÝ#42p´[/ƒ¿E= =MxƒåÄV§Ým©3?;bô9fb÷Ð'¨È˜IS7ñ >¬IÍÜK tçքJ 7û6ߥA/|BÏ›=Mõª‡w¿T¼91óí•ÒèœÔ_6»Âõˆªvg2ý¡‰õ÷Ò=}Å3ýXm²¬bˆÛ}U¬0œ¸Ük~e“ +aY¬ÈƒÛ•µ¨¢–0N„ñ¬t³VÉhÖYvx§˜"ǘ5\N'ªÂ£æKlÂ?þ{m¼‰ôê±ñÔ|L„m¬t_œ2%¤E„}Ù>›ÎFûalSXø²tRªÀ8 ¥£Å˜š™|ÖW›(Ù)\3Úwåì:›oíý±Y4ì©géµC³¨Q‰\ÍõMÕ„ +!J½6ë:ËgípoÒ=}I=}ëS¯¸…ƒž™F=}ÙT__U‰¾&\íÛ ¼Âkg#ú Ú¡ÀsÏÔTžµ³ÑËߊxNÉæGç^4Ñ¡%.¦U±O[ø¼4pͺš7܆âªcÔ¦¢:oŠu¹8ÌzîXfœ¬56DC´Þ]ӆž!=MOÀՉâGÖӝ«0¤–¿‘Å2ú +éηNó“ÚÄr_Z)6y&2^F|#²ì‘36eòa¦rcLõ5Hö¦€yñÜÃMG”c~#‘!‡T ütŽ? ªÌ·«‰á¨É»ÕBms¸Û®0úBšjÛ(ƒ^ÓÅ®¤ÛF¹ÐmŒ›a)'¨ë(²ÄÞÍm²¦á¡B„á|”%«L¹a\×Ub= -f‹®Ž¢Ü¢«GÌSÔxÖXc‹Ãȋ‹ŸœŠÄh½4e#"‰'…;P >Rõo;%tÐC$g%Ä>70* *©cX— !C7€yôîËtO'j?óíÛµÁûH= I†ŽróÛ= kòwi‰J]€]þ†ßY÷{šŒŸmûpLÿÊe•LA¾À <½å…i­WÎ?o©t¡caâB¼iBüÛڈ¯:_î˜OJ7"\¥^­EPHQ”¼Cõ *{ÎÁIÅŝ$ÉVÇbo]XÆs)^‘H—²®wójÔÎE“6OÙÿŽsRͨEù9ž/²OÉQÿmåŸ«õ‡ØÆœZa;ˆÄ{ <}˜¸°Ró=MÖ²ý‰Üz´|´†pµÛá!cï‡ Æ,@zÃTŠ«¶d ³åoÙ-KŠyÁÓ4gß-²ÂŸxÊØÏ\@‡q&}eÔÂèŸ\ßÍT1Ï\kŸƒì÷Úw…é\À“Ä~<à¢e«4E¡/«Ä­aõu½=}l°Ã˜Në/J­å=}ÿ®!.N‘Š8…Ær)HÖ43¡C3Ôz/N : +Ú7!Z†‹1Þqöž)M%"O7^+TɚêaF8øc’2ú%ù3}Ô²dU(™³͏Zň±£y¤aŠf¿Ï•][Hþ”s·Ù$EpFæt4¦$V‰ú +í!hãV53øKéâ=}Nå22Úú"AÃ5£áª6ŽŽWÕæYôȂ^ AÜÍ)ýYÌúë¾a0= ºvéèŠ:‡7“Mô=MWsE²Š£²ÇL¿òÞ² —ܳJ+= 5æ+,> +¸M÷‰7jè;/_ý7ãzÈ)D÷·Ç4*È#†Ú¹"ó­!†z0 º—®“еZ*Šìx® +R>sÚýP7Š:ƒ¯ZZ=MŒͅW²Dê6Ó^Nq= fÍbN¿VþÒª£;û<cVýî’媖“6§7dËÃ-²us¶Ò¹%·iÄs4òf N…(ᨔ|õ-yZ$E)—¹ï5¶Wí]Q\·1U9†:[-zQ¼—É7÷/þžiÉõ ¿«Ob)Iµñ0 +…lÁNpʦË-+Kfø» +cù@®W¡‘Y,îuC6ZK¤é‰Ï²µI=}= óáÔÁ4´-ƒõfԔmõVÛvFÞÀç*=}éåú6™î®£³úÄßy'GD1dƒÕ±È"šx^ù1˜)DÚ'öŽ–\©^6Œ§ö_гîªZ|¾S= @$^*- 'ªÈÄb7ÎÜt7Z4©ö§–É…šU•hY{ÓÚh£ˆ ±TޗÁÉýҎRŒ‰7Ë †¡t¨‹<#Ĩ¡@EK7†Ïb3Uri)åw –=}ø»n#MMuœåÛ™Ä œ ¼ü‰÷´ÒuâÆ8¯¬œ»R6‘I¡WBòîtžnP=Má‹-0óà¤ÕƓ+¯KæÛc„âÑG½þÛÀFä7"ATMáu+mø ¼?ә^Ê?Ts÷ÁR7ñ³!åogã _ËRª«J$¼­Ž‚nÇ ‹h‘ ýõgcíÿ·ÅºÊ§~̂= @<Ûx¿a¥L¦ÃOŸ·—òi½ eûž…ÎL~tÞ/ˆ'ž…—­=M›$x@½tZŖYÖ&¼£]6o¹åۃѕv‹àMfgp£7ŒÇ6ùßkØ5˜i>9àlÊ Š>ƒÐ7w)ý;³(JKLª.z7ðfê2jÐ"£™Ÿ.SˆJ®“.çŠòìV\3(ÎÍ­Êt"´»µ¾¸[Œ +¸8Œ÷Ê+¶AúCñ&ÿøH]÷hly^Îàî ®ðgåA‰6í”KÊ7ÒäGo¿:ƒ¥G[Åþ¡«a¶GÞ¾Ž!â +=MnB»ªÖŽ1ÔIZò2»JkëÆwùºÅtªŠ’¯xÇ»×utŠ#jà:æî‹OÒ6P¿èrñË×ôm2¸ÓJ±*h«koâ}»¯£F÷§÷¸”#aƒ®—ŠÅˆ.ùÌ/³3ځ9Vϗ[³22L[ŸÓ,ö«W &$/U›Vq* fÐ;F°®/Ѧ·qo:SCÚɼñ|¨íu09é±|Þ'­ÏYÍ. 5Cm;Àúñ=MmW8ÒSŸM³¦äHmsA^mÞ¢£Â©}‚oO21*‚ÒÃʋFbäT~¿P'7ÑDØÖW®äO{ŽZ»ž A ؄;t£82Jb^“:®ÇOß­v@r*o:×>·ûxGÌt/ë*Š”F:7ÝÁwd¾º›£D´{¢= ´¾˜3EK·‚£µآŵûȟ{-VÊm_°S›³òÀmªŸQ©FlaÞñíÏW¬D©lÑñ)ðss½ÎÿVƒBt LªS+¬KB\;ªå'þmd'=}‘ÈnÊu&þÚ®4Ps±ô°ñZ÷¹„(ÌýlÜ*M»N«Ïö:[Վ.lrP޽0™w¬žƒ=Mâ×sWíQx_ó‘?CA' +Z“W-òªi±´¥²ÊMZ§æ†„dÀZÐùõ«š˜óZ—ófÉÑüÔè­\rN’ÀóV6Á69f•~6dOgD п”o>‚‘é#Y¾u?ØÞœØš-Ù±¶WE™ ¯=M^Ïè–bȽq—Úƒäqj ‘r”œ½ZmFïâjʘ4áÃ1XÒ93Oʚ´YöcÛj¥•[Èñ£@!9+ÿI™ üb•Fu#PB©ž +DÑÌYíuÃàÓ+µ"—8“q1·ìÚ¸óTü-¤‘¯ïiõæ)êI÷ïсýÙ¤†£.У‘Û P‡¬A-|¸ëu¥ätj ì¬ÔåÔ8O¥ §IØqÍB¨|D8xò)ºmÕCڒßÒ‰j&ç’þâ&}D#² –Žš½Af9«‘œ ÛБÑW‚—ï^Àþ°w4Ì÷ÿ䣁–¦›ü-yÑ‘ïc®Tú÷5YQp'ùÆ×Íçj1vaî@;åQ‘ÃÀ~¶´‡õ|Ÿ}ñh×ébÔF„4\BÂC&ÿé>SB0qç=}„¤+«mÜp®¶-[—j7ÈÁ­(’bC´=}CѼ±‰§‰ =}Êx±µn†!üÍ@je ¾Ÿ…”Új¯œq!Ró6™×üv$2ÐCÿÞ2!ý^o›äuÏtmQ»ý)ËgÏ_¦npËv õâ;þª«âÍ×;sbŸû–ûoD³auæ³e<=}ÿaSªrýô‡:kû…Üuü>YÃWa³'ÞF{À¼‚ìrag‡ÞP-n£íñN¬X—ª½Z43\#qkºýþé–;3éy”M)"N\>'?9.ۋ6c7síÒÛ)°tU9QA5zòfvèð.+þèÃÐg?€»|îb$±)|9äáà’gJ…aJù[ÿh:©rÎxò¼…ÿ*­ h5z«ÅضKÚÛ¯èß«Àú½¾]•½íDØÙ¼mñx½E¯T£E·*%eúٚ=}å>òGG7Õ.ì(ö,(lÙ³‘¶°çÿßrê§@óOcZßëÏ/m|Ñ4J­ô-@dbºX’„…­þx.I*çÃ3†Ÿ†éÙx9dÚ*zQÙP;Òö›nk·—µ×ú­¤îB@ÇW[3îÉ"­Lhó Y["nÌH8‹<^¬ =}µ«|ͱ^·+„¤¬Å/3h^”Óìçç» *ãöøŽ5a¿3uýñ”Ì0 |‡jW˜p.m˜Þ.˜MÉłw*iìe|ÂSÈÊWÒ£é܅…Lb–é4–^&©SÉ«°çn‡›xŸ¨Jg­Ä9°XXî=Mƒñ”Ò?Ñ¢öN¿=  +„AŽèWÜ õþ5¿Œ¡œ~ì "3nßÐ׿™â¨Bªœ3ÎNdֈ”b(,,âÿé®w䨏Â[ ”þ4þè|Çaán?C¦°¼èŒ{Üsîb>ûåxmîNúÿZ"Lˆß+ŽûŽŠ= ÞßO†…üwí·™¨Ÿî®Ü_‹RŠä± \Õ싱g?Òst¢œÌcÂÉ,ÊE9<ˆ{{ì20J”ù l{JDE¯= JõËƒaÿôÀ”‘bàè±Ôˆ¥hV>¡ %á:ƯîÈ%E3-8ð = öüönÑ[Q_fTp“EF€c½´ÇƒÖýc§P€ÄO€OdâŽ‹‹ +I„ÒRƒ%σ7 ¦Ãe¬ô®9¢2Ù*4 +EžòCñÔ·T†¦aG¯a*1HŽOX®Ï=}J¦¿†JæÂ mœ¯Jš1«uð¦JJw +Ät̕ñVÔkMqیě=MTÏ&ïÌÒ9‰}¨â—nz™ìŸrµ³¤ñæ4lÔ=MΓ7l¾É-¿oœÐwàâf¯RìàÅXÿ“bÃfõÈS$y^x{u€—=M~ÿõ K¸x}ϵRÌ&¸ÿÕ Læ5; ¬©‰܏¤{³«0{ÿ¹g«êD¾ÓWFäQ ù—]±koÊBÖUÁ!ìö›rH~.LHþ wq>nGžÂ›SÕᛖÁÜ´';§§SÕþïM]•Ê"p'­ÚÅÞ«‡€äÕãZöêAè(¦ 7fa›&ÁÜ´§BçoGžEñ¡SÕ±üµÁܘ£1;Þ.t±aƒJ öL¥o:“]ôáŸÉÊ"¨ªh쏭*GÑ=} †1Ù•’BW'(–ßk{­T΀¾!ñ’ i2k˼´•wô|&¤Õꇂ·,Ú=M™¡oV¡î|8H€ÔùK$¥J(mˌßÝïOísˆ¹Cÿ½a”u^‘<ɔd•ƒƒó/%Š›í²Ýœá'_dKÉ;æó}:5­®Ì?9ÈdÒ¯Gúa’}{=}‰¡bG‚23bؒ26‰Þpâë@QÃÞoÜ«™–í¸Äî5…ß:¶9¹4¨·ó5}A·~¯m¥©ªs™{?ÓüôZ”3r‘èzÔUª¬‘Œ-oO +!ú)h“;qS£Ç@²ä™ÃԊw×Õê99© ¯”œOñ^á|mæé4´™áíiñI¥\¼)±Ù=MÛȎ¦c™îõy–4Œ~1uh\é=Mòðs=Mˋh瞬ƒ¹Ã§ Œ¥ Ò$IºQfK·•ƒ|ëpÆ©{Ù3?œñê‚ÑKòV¨ôÿ½ªÝyÎ'¯4oôØäï§cXŽ fy¬q:ž <|µ…ÿŸ9š$9jÒ[ÙÌc#KHÜL4&‹g|ÎÝ&°¥ô££ŸÖþ(µØ€2u”3:¾JptŸjb˜^/X8½Þñ!°.^Î97ÚØ úÉcDÉP–0ó ï>Ì~y5ïÍ{ÿgµ'Næ¤ö¾+Ùʕîiì–üXRjàF1Ïñ* +>Œ>¦›s'ɟ™Øð§ãñ¥œAÂ쥪+8ÌÀš]RPļ£cÎü‚´éÿô‰ÚĄî?sÜ$ÿÆSÇDô¸]OyÎ-j¦*]åC½;DbM_ìO˜ÎOCKà˜»O cè ¸~)½=}4׃«Ú¯Ù2‰=}!÷´âøŠ®&Òsªï6³™ªh +¶**8º7”¨;Çÿ2Ù»ªõef^n^n^^n^^^y˜ iµÔÙ÷Ä>ÍÒ<Œ.ø²±,ŒÀ6< +.VRF é üÏ@¢®gsÖí'UQ×tF ×@¢¼@¢ŽQ‡Ü›Âûd±û‘xªÐ ÉäH§¸Ú†+ +ÍuBý* +¬E¤´a•#±¯uÌyÊQ”åÑÏzâØåjš¨sPðÉðn±Õ¼£ááH¬óGsz„‚¶5$>àAÆbŽT§¸ßäÓB÷ûÀ‚‹˅{Ž’³á…½À5ÍÞæÓ­«Éz5%Úþ„ޱc…*|ð)o÷ ·Kôàþÿ~ŒÙc0>ãvP«ŒA°K8·¡¹ÚªÑÖ·ùgƕwƂƮ,céuÎæ´ò'•ü +Èeڗð—?enojÉGþ±âRÙòõòõ 1‘~%îPozá>zwVÊuR ‘âÞR†[¯¥J%ÕSæWT_Ñ7˚Wø1*¡=MÀYµ:¥ð•ŠZ·üø¦Âõ3JÂ5ýü¤Füœ“ºQҪ֒ êBvQTŸ²ç‰)–ŽÛ +%çI¯«ù +õç°|Õ ïiۅê~b¼ZQ&ûÕN²ùC²(^Äv¥Äëk +§.øŒÇ‚ü +–Ð|Q= ˆá& !I$±bö½Âí”9îmÔ ¶IyÖg×!ļi^(¬³óŠx}5'&¶Ž2ÎgL~à"Ùϐã›^ð“ãD~šaíBÄø ãSŒ þ<@4¥è»…i䉆b#ïèUÑUhèÏ왪L[ÙQ]©â_H•i_¾°Y™îœµ7vn¶ieՌØÈF³Êåš,ŸètK™ýKÉý™BS&ØŘw»0¤Èlûͼl£[= ø5»ƒ]›a8b„ȐUÎ-,‹zDÜ«dÏäsQ +¹.Ýõã¡c…o$'ð§ é‘ÞT-¦æm&ُŒ±äÄ1ÊÉLC|($àÛC؈³W/¸â ÉÇ@âAt‡VŒˆb¸“5Àœ +Ý%‰ËÞËѼžjÀß¶ÛN¿2âaä_oÃ'°^Óå‡Þ’pòÏϒýóJHð¨6ñ â×xà»öX5¯;|J È†º©(Sèþó‹MZìÞ+à#·7¼£W˜»©6zªŽ¯w±ÆAƒHcËæï‚øÝÈ­ÐF[þ‘\…Wñ£Ò¨Éýó›ªSJ“®sºîÜwûöHÞ [8Y/‰£ä2ϾÖr÷2ûFI÷@+óWg#7,åûŸd¨Œ×ÞyµOÊ w놾B-—~ö×/X3Íè|êÊй"Ê.A ¦ ó ö‰DYêÖDJ»5èI0*U»}:ƒ]\€X]*»‘>Béúûücv= ž}#å +â˪27,7]òÛ¼ìñýWÅ8'¼ êe~»7Œ¿‚ªFËru•ÙâWë Ä«=M= ÄÏÓXI–óoN8->Yƒ/~6‚H_ƒ­Ð¥Ä𠄬ª¢c]d)Má“lúr57§k~¼÷aÃÍL}WE;ÄۊG~Mê榷Å'D^ñ<™zJµŸ,ÂÅ8ùÈ©^ζËõihê?´àž˜’¿åh÷œ÷D3ˆÔ2ëh™ð"èç°í½0•°§Ä ëPHà:Yj“u™ûŸ])²® ØXÞÉçœ]ܗ‡Œiù±Zp}ZLƒ‡¾r +Iˆñ>ˆßv¼>O¡W}kb—ꓞîô¡AIزuï֎ãíå¢Úpv¿!_ÿA¬¡!ò’Ë51@B¯~¨QÂQ¦G<‘ÜclHôµ†Ó˜Ê¶=}©”T¬3Œ*ÂDà˜"ÃsI&=Mª p"EéjÊ;їÂ}¾–¯˜,¶. Þ¦òòޏC!^ kŠLØJ.Ð%~Á:ʹš:xJ'wµ+Šz³TÊ% Â+6zjx¯4ÂgU¯¬ç¨ÀZdútÚ=M“¯^U¦r–NPÔæ„O£EÖ÷i§Â*ÄZþ^í¿ޞ£‚|þÁ|:y&{6g(<«K-•Y®ûðz©m-zQ€D¢ÄÃŖl“s(Ù$ÅK-ë3Îe´Ô‚j - ¼µËüÍTITÐ)¡âZW‘äV­ëã>Ûß#ÜÆo•3<= 2ӂ£¬è ZtFtLˆüªó#Å Ct-é>Œ¯„¤Ë$¿ªBe—x„ ¦ £õÌ!Ég©åïÍB~Å4¬}­‘ÊÕåÄÑc Ÿ©þ{¢ÆˆTêU!ҕ,Ph}ä~ôçˆéU©h½Ð=}š_0©ƒïŒþFn£k¨ÓýV‡Féýïc=Máv-÷Vn= v€¹Å†g͓¸^*5Ý$¯rÜ ÞRÄK&ÊfíåÃKÈÄA¦ò¯9iië:ٚ0´.šV f$= ™¥í. òù™ØË„ŸMIƄ«¦Ë•†‡ý0ËõW6õ“ 8 I¹]&kóNg–—ì3Û¶M²«hžc.‹ùN‚= TF%É1–=}Ô;ˆñ_•Öº6@Z² î뤋žpò—6]©rcˆºa¼o2ç¶á¡Y甎dDˆÌ±H×6íãí/f¹°֝#͹G=}ˇ¬ Q°Å‚w“õ ‡ùYY$waìÿ¿³ —ħß³º¸xù4ÐøÒ¯)Ãé*-± –øÐVö–F‚éœñ#œðj^´¬N Ã{þ0ø´-vHDšå)LN¡ñ2¿ÇIܪZ{‚Ñ ë:B6ßè>Ø=}ðaÃcJ-Á_ëâÀïþ‹Çðï ð^¿åN]ÆX¦-Ô¼^&†¢j~óFÅÜSÕß¼l bR»BØöÂÍ^‘—B¥]e^Ñ'QÿL= ó‡¤J¨yɏ õ<ÛJDá‚hGƒ³d[bñz-wWªxýÙcö…cÎPÆx|«A’XÚÎɊRLæ›3‘cÁ+ôšäŠ\0Wyâ?î@·a‡º3z¦ÅÁp³ “‹=}"  Ë= L0¨ÝK¶Úõ“pi¬Y{¸#ø³[¯zãBw¿‰øØ"å³ò (Œmpß³=MI-Ûp´Œ 69¬°Âe<JíªíXy¶)›kô—0¦8»ýmIó%SœÑ'‘Ov³]ó§S—|M òsz +ËÁ..ˆœ#ç¬A<ð<†‹£ãOàŽ'Ü\ò§"…ùxÓh*ÅPC~ +Òá‹(a?-ÍJtUÿU?…=}ó +lÖöa­ß.Êl.f#þxñü÷D²WdÀTæ7ÔÁ'r¬Yï6£ëö“šƒ&©ðÎ'ìÎð9(G©òä:·kãúñêU‡às±=}àۄ€¢Ud¸ !;PN4RÝõ EK妋í¥ë iuõ½ïÕ©?sxž&Ì˔jï=MÍè£ÏœzÞ|\Ùâü%Ó§¿Ííƒà±Ço³×Op{v„µ²Ä=}ŠÅ/Q£Ig&[UèY}ˆoòÄ©.¥––>½öÔ½Ãd‚RN:Î]åj­W'ÃYÒàßEqJòuJtB‘éd‚IvRñyÂÖe?íb{æl‘WÆû°uÙ3-ÇQîfSÿ‡K²sÉÃ$mm!¼MTª(‹·Û-Cß?-\HXÚ=M \é¤JÜô¦5ta,}Z±Í¨JÃÑX¢¨šÀÌÝÈFk@ª) ĜK®YvĂ}™t@f½l•oso{'òɌ™Î·ŠøðŸÂ§Å¶=}ÊûN…J%ú/ˆ%:pû¹=}'µ»K€'@'! åj1l?3XF–<·Úå¯nûܹjAÎFë »è®ò;1û Ç÷k{óvîžæu{°q±ûŸL“ WùXöñK™nìM¼0ügUi©X“²{¶vÒüËuÜ| XkېŸÆm’ø÷af?÷°ªB鏶@G#fˆç¶ÛêHßQ¨õÒ±Ëà¤à0§H¿ÊÑÌç* WyTf|éw›¼½2=}® + ‘ÅÃÔn­VãoM+¨±fZò|ws´+ çÄ6Kd3‹JÊl¯Éf¯}Ä^̸¸8'Ãx[V‘ ¦õÉ&+Ó\ÚÜ]©ÃIÄ^©u]Œ3œv'?—êIÁpSy„mÒÙQ#Â7s濐•¿ÕÞý€èIt£1ªÔ=}Xàv$Õn¬Æþç-µ,Qñ*À·7w ʾä…û¤z°á×K¬¿ÐtÝpŒW‘ÌđÌÂT óBGJ¦XÄÐÃk܀„•^étpï3·Mêo¤"Æ»(­ÄŠ~jr¦æàè¨Gñ‘Nß(1.©iõË=Mƒ©”ÁuµSKù¹ãÝР…~Œä¹s;©êѤ”‡rw¶O«m¾ù–钘ܡ­Ú¢”ÒÌ'"°3³ÿM{v*W¸¶]¿ÿôèxBÖÛO ï= ;n¹Îf;7:•ÝÇ1½cÞ¾¼W+Ã3ƒ¸)ÿRHñ¢Þ‘Ùô¢¨ç‡t[¥Æa£hìl, ~˝äÔ +7ª=Mû’-ÅàêÒxýað!”gGÃÿ³Ë_½)“ýŒÚÅÜ %#ay!WÔxÀÀÝåTíϤŸ¹®ø;(gU¥´(؉MȲއ†‘:•gªýü(Ï0BÝc×éË@¹¦jœ‚Îoy쯶'½‹a>õiKÂÓ÷Åc$Ããöˍ›ûÏy_UÏwø—‹{ÿm«9’? cÞT#‹Mê8=}¶Pôì_’#µýì7¼c%?gÎ}Õ›üé։ÿmNЋiF(ã q…çoxh>—Nä«Ï¤&ÈkûÈ´푅´¿ª€Õn?èÀ²ÆšÿÔxoÚi÷ÃÚ +ˆ]Ø;$3DƼ„â‡W2ڇÝ= ÃBdŠ“Q}ø’……ޱW¢Ü ZUNשð´žrr+Ëϲº$)¨†˜à>ê=M&Èt¬—Ë,º/ê|ÈÇ*½“U®î*¤ïŒßÚ=Má!$92™ÕávJ›ä +Ž†°ò5gqa:#D8¹ÈX™qøë±Ã:°:ôŒ>+˜K¨U°*‡ YnçÑ:32ùuÀ"öë%Kæ˜P”¯]vÎ%¥§Ñ×@“üþ±Y×Òd !ÀÚ_“¨éH8ÄgÖí˜Ù«´‚äñŸ«TÀW4*…½ +4Gœ?†ÊRÛÁ[ý©L@§÷MyjÐΊôzG{¯ZŽ„ÏB¼[UGjmGÑÛ†cK„ÔÜýH¢>Qì"^Ý,XÇȖ~ÿ[ ÷â‘ÑÔ«Gï—/ò[3†Ëm<¡IS?º ‡+R|+¾폽ú¯e3Æ¡½úC½zÇê@ºÌ€M7ôx÷cC9µAç_3ÎRÉ]ÃÊÃ}üò¦?ºˆD9“^zΙqü:s2k½¸|œ9½†¨´x!{hԕ_‡iû7ƒ-wýsÅõ»zz¼S R÷-²ma½üDy~½E +Y¶:q‰gWýURDÏjr¥ú>ÏüP²¨!ê=Mº_¦æŒk„ÏצÏçØmx ,17Jds!¥žá¨mòmƒý}ƒÉÍêŠÙzZ‚ïƒTòŸ%‘¼ œó—õ‡Ó”µÁQ†9Ôõ= §ò‚UB~Üáìsæi_.zWb$fc+îÏ$vD3ôì i3€u z?ªD/9k¹.r¬ó)£ê‘ðY8üš¸£"BÆÁ‹£˜Ô¹Ëd¬jhôc¬¡GÐ×5u³Ð5è\é4-››ž–ÊsàlT2”ºá¥øÉÜ6n6°˜Þbpw !×^ßdžeÛ,cg rääÃq’È·1¤Å^™o8“®â¦dæWÂßTÎaF1sºíD¶×ø’Ÿ¯ +’†Z"Ü/èœïúÝ aè}Ååãòl‚êÉAîÄ<ó˜ª}V=M“bÏ>Oɋl¥+ì'U¢ø¨Ѻ?ùZ¦ï³»ºÛI©³+Èuó҅›ÿ1,Ü–ë¨¸Ç +Y=}Dµ”¤ç:¥áTdL÷ϠG4¨‘Q¾îm1”ü-‹ôîûYV=}æñoF§É·mO„¯¯tÞx= ¥(…ô‰Ù‡¸ ðXå©Ü5‡XF!mã^½……³’'5ÁïF×¼å×eÂc­kyß<QÞ,hòo·âªãÁêº4 :¬kçÑÖñ•™ã‚NP»QÑ Xy°( ¯°•ºmö8tb[°‚–…rH]˜=}d›^ؘKäà=M §¾”Ûäˆ~ŽRîûYpã¥A–©ð©qpµî 3 (bèÜÿÜùˆêfÝÌ®F°I0H—ê²'/e¶éàÞ,Q†“:À)#Èi=}$\Ê\ÿù]̛<ÒþMÆ¢$¤ü˜o1X¿÷9ÇHLlÑkÑÌØª6uG¶>²Ï–äT‚ÝñvŒÔÀwRa]÷pxZì¹ð>ëꈤ÷ü= ÌJvÚ¬Ø AŽÄáŸÝfçë]„þúœ·ÚÇVêym3pÊÚÀDzÇ=}U"¾îÇÐhŠP8ˉ¹ž r‰8_Œû‡L.Տ”ìÑ^ªŒÐ% ¹˜òŠì¥¿4‚L[ùˆ“ɍcÛí8Ä@Ðhå1Hå™ãYf¡hê•6&‘ÈÔ¹~åAÇÿ¶‘ì™ï–­½,iu¸„+{ +Û0Çóæ8ÕãѨz3Žz?Nýhg1ç^7ȗ·= ž\ؒw‘a—V5QÅ¥Fö›ÒÂÇ©’•Q'³%cogžßgÓºÂò8_uÚ…ó.ǹS¥©†Š*¬h´ƒþ¯™¬€‘²v2z-Æw &[”ÿbIû^>;?ùΫ«ˆnTj‡ÅJÑQëóÈãÖ_öÔHeÞ®Ëtǂ¤S‘—žpnáøó¿ÌuµØ~‘YPLˆ½y4æÑ;!:«¸-ô"=}ƒÒˆ±˜§»X¨™‘Ýñ¡Ì§>ŸUâ>•Y㎜¾"çaFfe´·‹6e.Ã$NɌÔÃ_ü÷ÛßË:ücóŸ'¬ð[øé.‚àú‘/hè#Nyq*¢½ûÀá̓˜ž¦Bq!9š…;ögMgùÊ«®„ºÊ$£ûÌ̤ŸK’[Ïnv(ó„$±ã‚âÉÏ- +Œú{ÂÅ,<ÊT8ö¤Œê}æõq¤!aü:ʨ†Ôû\¹I}ëT5Æ\Ïiÿ0QEïµ*€ñK’Œh7b¾gæ'ÐÂdƒ–[§ˆhsYàm£¹KP®pýîöñ輸[u/Ú¸= Y6 ÀYu÷²Ö™ŸÖ]r[7„bmj¨'æjòà†’lMO* nj·¤ñ1(*TىZûÇIîiÁ„þ’.ÖûS6Õb6yYìÒ$©iêã‡â{Š9DÖ[™0t *4šÿÐzº0ð*µÝJ7НRbšå‘§ ‚G'Ká +è2xêy­.êä‘:kñnËÞ8ÖúuŠqhÛâ|èä(Æ5õ}v÷ƒêÍFŒ=MîX鳓mî´ü|pî´ÌõÆÅµÔ÷)jžDo…L¾6üc€Ö•tÞÔ,Æ3 eð_—‹Äšϊ.‰3ˆ,VöHDNå@ÔI؅hÈ1¾¸Šô"†§wt]CˆŽàñìßï÷\kò†§Ëkž¡—áŽÛØP^^'­Öâh䪽œ +à§÷Ê‘Z4t¯.®'c™Î÷þ!%n/ =}¤‚=}}Ú²SpŠêw½ËÖPçT¤–õßª—›U‹,2õVuPT8ÌÍ!YVÕ,,]^éyào7=M™RÇt]= ….&ùdDÒh­P\Œã¨›-FRÆx3´¿¥ûOªÄ:{·ª7A=}ÛEx¥=}VîAOúõL*þ¥ÑÚ¬,t .²ÑO·(—“ùqò×Q1 Iì(é¡U©ª_L ÷¼Ü j–mªÑm_{ŒÞQQõÔ}óköJ%¡ÚL£²ÆÓ uŠSU#zÓ*§ºl¶8BÐ"@b3ôÚzbÛvÈdӟÌg&á­ê†ühSÍw3È[©tÄn*\2¢ƒ¦à +€v6ßyŠÜ0Íw)à×莚S{Ñ.Ò¦Íæ Ùbµîрéô5) 'ý¨êëšçõª·õžJ+•mvä¯1= †9×µÄúb3®:†OaÌw†’;±QãJt= .÷g)–^=}%9Òwq9D>èÅí‚:9º_À$˜ÒÁ{cŸžÅ?°Nzí’ø~ÊY8Ä)#ˆ2b°µ#¢ix3ñȆiæ9“z¨ÿJæ“äë%<êR MÑÄQ†Ö2Ô–½aôcŠ5}t5ìø#Vñb Iò‚†j&=M¶Êwd! +–é7™[ÀÚU*l6¸ïÙ©¡NIn%ä Ì%Gù‘Ñ6¨ uo•·6Å)=}PíÎîT=M쪇¤){èÐSX&P2¿Ü ÍvHÎâ¶Á3ÇÌð¹õ•-„êÂâ©s¥‚Ÿ¾¨ò-õ£seø«OۍˆÝ·ÔnW@e¤l4¯ ÏÞ¾Qj’AޕiòÈë,òPGPñ¾ŒwfQdSu^ïÿ+‡l©„ª£¡Eõ£){æ ìððõ⡌ÿ¬ Ôé +£“’¶p™œ¾lø…w³”= ¡Km=}û5HàI§Ò–%S5x÷•i$¡;ÈwßôRxfaðZj&—h6æ‘é+4²èþ"ñ­Ð +ú¤ UÚª–ô‰Æœß ‹Ø°†;¯Ê¹¸ùʧÈÖßoÑZ!ޒôÙÙʙ˜Ã¾#·/ÙÀ#P±/·/}¯1𮶏zÉöóýaa’â% +â] +÷^ïCÙã¥þ]÷FvȊ­ÅHÒJŽ÷†Ùñs¦ÖGúìtp»²Xæßñ 3ÞFЉ}ÃéD½~ÚkíŽ]æd®ã±êµ×èÑèq1%¹ªÓåˆÌǐ-ù˜µ“Fø•I´ðìeò3ëí +#1ñ)ì”)žÞ÷ÔÊ0­ÐÊ­ÈÊЭØÊÝ÷D"¢×ûƒ>öÑ X›>>>éÍ5u²m¦œÌdlÏg=MFSôS…QÄ@ט¬Gó^Ş£y©ZIdÉܟÌý—wË}E‡¬G0Tò0RñSòÜsÁ¡¬ßâ= É÷Ù¨+­•ÒŸ_)ƒ•Mv«<§•(ÁQbùÀÇ=}Þk¨jlŠ¿ÐŸ±ðØ÷ÙjÕ²"éñžpÑ¤Š“ûر¸Ø1\’†âÙ= û ÷áùTn¯8 1˜¢:«çùdõ¦ÖÝ'dd1&"†w2u„‚ù“†$Ì$Ì%üžÕÉïÁᐝ~’E£ÄâŤÆÂ€Ú¤JÙ©¡¤î= Wxòý%=}QÚÄ­šöªÖþošpgÇ Xr*—ôMR…#ìmÕ§Íœ‡˜õp-¥ÆÑaòÿL¼šÿÑ­͖±P‘3;‘Â^šW])³Ï ã™þm¨8e»êšÈÓ=MJ­„šUðPmr ¹-=}® Äy H LúRX(,Ð:‚Ž= Õ^',Ön¶j—ØÜiPW2(ŒÒ7©<:™4Ê:2N¸ÍÊ8²Ï.ýI4·I†"k¦$ô‘q{š øâ¹f „ñež1hªQ¼[_é2Nƒ™È´jQˆ°*YÉîÍϾÿÑ +Ív@ØiúŒ!zڛ.>ÔZ³b]@ëúž“²çÝ+‡ ÑFPž†¹»áEZ¥›4œÈJ- Û£-ºzUØÛq3= Å´Ôw© P±-ÊQÊfYcÊ×Á_~%*©{غ)àŽÀ҇÷„ø'Öd¯Ý²Yíì»Ço!LÍ9ËË$+À5)„OŸ½È¯¡Éz؍ŽÊqAaûԊ“4IæÊxâ&[CÊ.¥iÂw×9iWœŽ°ÀC¸(^'‚©‚ÇÊTºL±uvý‘ÀQÉënðÈ¿œ·ö!CvH²’ÇxÎÚÉ 5ÒŸÃy¶ôÿúÍŎ- +†>ÉÿHìæ U4Ό¯þ§½ûmÁȔÁÈ&mG”x¹¨‘¾Tc²Ø +ªŠ|ð(JK¡+BN©ÅÝácÕ®ŽO©Å݈Œöd}´sã¶Ð©~Ýî#{µ/ÅÚM8özÂ)$WÜbªÀß÷Ùϲ‰$Ì4Tú2Ö©Ÿqƒ¥VµCÙ%èç/‰Å”¿ËTtºã#:}@÷Žæ(2Ö}væ}” ÑØI‰xÕ¨N”ÏxúÿIMc޶Òå—wšt‡J»Ç¢Ð>Q{‡Ôòh.ph.ù¡]ƼÔ¹&•¯áMÐy]­LÌÆ¥µÿ³·á*ùPŽwãSÛ|">4·[U8 +I‘®ð,õžï4\\A ÿb‰5+]Q—RL&5ãh£\”ÆE!ƒs1Ïı+2pӆôύ9¶ÖŒR˜¡¶m)5!^PqÏRf%\€hR'„ñ›«òSßî˓††ÍH3íÿÌ'RݯþÙJñŠÝ©–\\PVQÞ{e‡wjÍî=}ƒMå uçbÕõ·Ð?˜Âߙmìš\¡2΄‚d©E•G¥G= Ê}-ƒ¶Ùw¸R ¢zy(Ï¡Xê¿q½ö±»ýø6›v›ÞÄ7«¨¹Ü Ø=M†Õh •{çÓ]ñ1)%Ðj£TNˆqƒõœ~r¥à×õð耥®x¹BûÛèbÍ¿–‚õ-Ò¹Òæ\¤_BAý‡Q±¨4²”|×>9ŸWP®&ÊëMèØ¶&{ÑîGb¢è¦=ME…èÀ¶eî¯a=Máә€!jvñ‘#ÝÔ¦–D¨¯|;ÈRRj"éFÅ{1ÿÏr< ŽÑÄwöžå¾zÈù’Çy© ­13'&7x.uzØ" +£JßóÀ@ƒ ù~TMS°@Ù\ê= ‚óe%Ñâ§mþ÷fRôAäêtjõ3I–á‹rˆ4&¤×ùÍÜ´¼Ý|Õ¥ñ³˜çBäœ!vØéX˜x˜Õ²§DˑR<˜€=}~aXuä·%Ì Ðñ—+µ‰Á–º +%*g(X’ⱓ3÷ñX0ËOÊGË>Å=Md[[hg®1·D!v&/íjoô®bü= T?iBÔØ= ªX+óq6Eôyˆ‹ã a·–ÂXI¡—ª“bp¬š{•q¦-¤É­õ¢ÀÕgÚCD®±±¶ÑNŽ™?fÇ¢Ò —žlÙ­˜ûnHê¸!zþŠT”Š”hl¿VÖY5*Tº=M-füfè/”4;) +«€é‚áh»¯Ü’dFÎrhÈûåžJSùáÎW¾’^7‰c*Ò¯1ˆ½xÈ«µwSîÃðIðO3XˀKÉ(tì66þ<çÊñ2J /bÝ}täëåq¦¸â$à’±™­G…SC°Þý§îá.èKëí +‚a„/–Z¸ðWöI¦(|R)dzÖL²,¦Þë™J¡±âM ô{ֈ㈨Œñµ«¯Î'~7,[‡‘¸ˆÌ,:Ø'zÊ9WÊlœLA—Ä›ÊK/x¼<Æí·1äÕ:½Ö¦ }ȗ¦qÑûŸÚá~†p. +ž +-Ù0ùýÈ= H3Âm°nOzªxŸB 4$o÷·V\…nÐà‘+“wÅV¬–® âhþ šä t¶5Nþt*6FΧû¿¿}¾Ë\¥™^igi×/._'*öÃq=}Ì=M½£á9 ?LÕÓ&­s”Ä& Ø>:Áê7žWP7'z4üºÓ‹.¦0àr"˗Nͨ °Yñ!Uñ€¤¤Óƒ&¤0 ;ˆäL= ÝD¨ŒKÃ÷Ò¿ëYžF‡Šb#2¹ôñ¸ÑÑxšì¾[/Ë·Ø¡ßyƒ9? 7s*‘Ž$‚OaìÃ=}ºñËÃàφ¿ZCəà×å…}ÆÏ³ŽÃyÈ"¦ø7ã L1 äÛqhžlfó=MȨÐZ^sfú싹ž‘9ÈLsrÂÝå|›.Á%Jðó•×zƂZŠ™¶C÷òK‰IÉÕkd +Ü„¹ Ú·/v’Í6²å®v±i·4M0ñ•d*-ωÿFËÞ,X,K†îºi´å÷¹) Z6±ÊlãÙ, -G¡Y$ÒŠ+†•þÒ"‡ Y8 /©txÙ $”îæ± +ݱZîÊ'bG0}av1„¶µy©"²'ä0ô-^1‡Äl“yž"â(-7p轕þ<Õk5}Jބ¦ýé":­R톲g:»…®ðuv²Io¥ÇUÝ{èÐ!Õ"fl.šW²½'š>4ŠN¨cóxí *ÃÅöèžì¼„qi‰ï hò7Ÿ·UÚ£«Ü…™ ¬g$ÝèŒõÞ¨îÜ1cÌ(ßçÉå^cãÅ- %Q>ø$Î k†Kœÿß·µ‚Øæ¬:è¼Ãƒyz…µ.R­wÃkÈ Ú,Ñ;Õ¸»^O¥'  +ræï½|Ð:¯7Ú=}|ù¦õǃk'ð¯÷0ŀ·ÝŠêwµ˜²xï.WMyH¡Æ=M +څÏò©ì/3_˜šÖ=M ²ü|a÷²=MÄÙ¶We©ÏäN™¹?ï×Ñ^W*s€zý~†N‚櫤Ôèímܸ«óŸä³›Ä´q9òKKZ³Û]ÂMÞеuڝÓOôxIäõ„x¼H&D'#È8Æö¥ÃrɄi ª°ÅÎYGy ñÃÐN†X۳ٗªO2¨X„¦Y=MÓ"¦&Æ' Zô2’¸IÓÃw6-®›ÐŽ€¯SïÉ÷ŠÀ»-Q9ÅÛ¡TV\å>CÇØ¡òo[+lžÞéQnU¨k®ïn ³_Λ>ø 9ß-§¼?A(QD]>ýßOɪÝ:ï´hy'„ÍRÔežL ‚Ðĺ”.jÚ-j̺¤4Қ(põ§°7­>‡ˆ­·G|wÙP¦êyÉ[YÚ/›Þ5?½³wòžZ= V}Äú;5l>û(ôûíy ¬Ú}bT2W lI7&Ò´j~®ÇvbñÁskóJ¶ ‰@æޜÚÀ"ÿ± +՚›™˜±6kVñòšòpù¸ÅªTyò½ÄE&ƒÞë^“·U—­I¼HÍeÈÝ'œˆ©L½8"ýÓTƓ#…Pº‰€n…C6ÉÏàWíPšÜ}9ÅŕhoÃ'ÃR9«“žx˜®]î*§__G¦TŽtòÛ×ΕJ}Þ(QjÈü³å /œìúJ®ó·ÿy'N"ÂÚVô/%7IEô@äµ,»jlg’~=}UQÃÌÉÁ dh‘È\SPbTzÒUUÔq‰øÖ±ÈÌGÕY.N ¾h=}ÔãiÆw{ÈèLºÒ;·$—­k¥ç<”p‹®× +*¥2ÕÁbÆeý‘£³å–¢ØRŔRwîé'øa&a-þ*Hl¶ba”iç%¦/EÐX33"ݳêé|hÈp-ÆVÊ7= fþ‹j<ÚgG°¹ Êp8ÆAºšù ؼó–U7€S7hH9]G9ÑAºLAº†¾ú êB3HBºSrW7s›¬ÞüšnÏ}é얷ïQA:Q,ÑAzPfô3cvü–€žø ¥Zð>ßüfÇhÅ7 1¯ÃOEã®»gat|¸ @c¼âp³Qû¤ºßA*Næù i¸O²œ +¬ÒQP{{ˆ×ºÅ¤ê3TðÒAó@U§”â¤éóä+ƒØÞ?Ò×â0TØq©ùƘÁ^úÜùëw©g-ÜÀ\¦ùý¸K¾4Nï×ÿ.>Oª²\Åm>Â,Ò"·žÇK™U3ù² $4Öùà ÐäO:»Šȕ<ԑøƒæU=}ĨJŒ¥:€¶¨éŸÚêòϊ§”àÓÆx¸˜íˆ0ËÜܱèüx"R¹÷l±Im°¾œ•3w€AŒ­ðœÄ(îB·2‚†)7d×ÖÄê71÷·[çW.©ShêÞü6®”àù°ŸíNVRŠ‹-0S5"aÄP*{ 3[9 vcXî= ¶l›J±øÕ¥4CqèÈ´KõŒæk >âæiÚæo'ÇóӍÆö 3!7ìŒË¢Ö/õŽïæâC²,ZÝ')í)ŸŸ¹Þ!iöFÆð¹>ŽïþTTwjd–æ“äsºRøŒœ ÝI9”EyFEÜߋ¼X?«Žd Nö&gʙ6„/œ ÝÁA:ÂR§5Œm1¨|ÊјO ë»±´=}3y½>i )JFߥöÐ!í(¸×VcØS,£àe]ÅÛÓi¿cZäÀcHò«¹µc´…ª“y¾/÷Ò18Õ÷¯ìHyñ‘²Öð•ëä"ÙÈdBt 8±YªJ2Ûx6×JÈréîe)^#!¢ê;-WY„û9‹ÚS™ vM¹“Ù^4ås&j•uZ ´­¼‘3‡€Rƒø“Á&~ë\rÑPÊç'iIc €šY{ªþ]»âæãZÐÂ+~ðZpj ZþÃ×ûÁ›7îßpräÆçýÀûqEâ¾,¶éV¬t—û³¦.ø +¼æqÆQƶ—ö‘¸Ó¢°Ü‹†bKmjgøÃÖ×®²T,fIZa +]Õë´U»°F°Ý³ÑTw<hҎühÇ üE’¯ÐM˜ë> †É³Tä©lW4QJÂs©r'©>™ ÞÅòñ›2aÆâ2@gÕù‰ñdªÉƒ’#¿.ÙN ëÎr:·Û-tŒÉtÜ®½¯”qµŒƒ=Mû|…èÏï±Ù}¦½Ø©”³ZÒÐ2ˆu‚˜AjXø1øLDâU“¼‘,gFV¿—㈪–ƒèíB-ÞsXw­Nc³”™RI+ Xjr<ä-ëèn‡ÑÚå©|ˆÃi!K1yH{ob iµ)VÏR¾ çh¢Ë÷ޞ¨vg §±™Ö>·Aäނ¡ï DïìoàÈ= ·N¨¯BÆïìo‚Vìá@õvÖMAã@&æÉÀABL'?ʙ¾š)à¢Ñ ++™oëR¡ÌÑFŠ«nv5°ÏÙ%åiùEqÛëXã÷†míQ N1Xä!Hc +"°tt­bïØ~Å>)qcÔÒ­)Òqv}ßÈýÈQõç¡.’”¼º|џÿï¦@Ðo®uØ×½|:}ÐŽpܒ±·ÿ֞·Fy£(†…Uî¶K;¦3>€Y@Á•è›SYÀtۓQèyM‰©>ÕÁ֞%ù¨÷¼J^ ¯ôtBíü³m¡V! ûÚ/QŠA ¿¼&Ñ/R¿qWœ.Û¬¼ñËÊhTÁºå{†Ãâha]NÈÆPÙ9;bKŒwF[„€ñóƒ+²æe²ÊWe…Ì^2¡ã>1‰vÛÅé¶x Ø÷5ƒ¸©ð„i=MiMH!UQ5çŸR/ EÅÖm³Úöš¬ûƒF¤ÑZjå[ÌrP=M™x Ȯã—ÄO/ÕÈd ¢Ø4Wì=}‰·'~T "Ô¶°'ûóu‘3 •ý îçž&5MÉQ[7s¾¾¥©c"cë­„¼'lÕà{î¦ÿVÙè“OÈ +YAM%£r¼Oªœ6TEú ?!©›ò\Ký©1d¦ÀÙ;Pª’>kdÇjmQ=MßRD>_®ðŠ/by©§–†9ËM¸ „‰pð';hõîêe;R¤¦ÌŒù³ô @—ãÛö‡a'Ã%´#= ¡¾g»ê°+9ߗ#,³è~>1[ð@÷X eñ¸ÑšÊ+>ý»—ÃF;[aÿâ÷™wKVÍÚmÌ +? ô!¯Údiø—µ=MÄHÊAN¦É{cðþlÖÑ-3۔u¶šPö6p1Y9ŸU‚ÊöL„­£¶¿l3ŒÇ²Ãð{¼‘•Ê”'h1yZͯ8ƒú>əJH’¨If~«þy3³ð½Àj•þ= Á«t;FÊ^ R”³²Šc#“ðÓq%ˑ ±Ž¦½—çcÀÕòkwé{é˜ÿM“èŠfQ ŠßßD¥œð¶ÎSË>"ÿmå(†<…çó£âüï·B-]²ãñ€ÜGþ¾R”FÃbg®”Ý.ì¶§ÎN\y©½ŽöœZshàw0ønÄ¥X¶ßù”’ F³™Ñ´‰Â¥¸µ!ƒt\=MIÔ±:S +†¯E•*t9|§ä‘—Å›6äÃTï9(×ݫ՛ôŒËWɶ©òó#í7¦â‹HoëÚUfðd0·hWÓc0¨\0Ë%#°´¾½f?[´Öòp‚ŒÕ-¿HuH',œ4\âæ/=M]=}yÌâWi¶û>žlõÂÆq+IñA= =}5ëïx©ŸÆI $Cùó8Ÿ{’Ñ·o8õJ^¼+¶–—è"Ï\ëli)î2»B¢Ã tÁ=Mu/g~k ‚ŒY"Þü=}à42YÑ콌ÅÈ9’$sS@7ëHë…¾lQÆ +릶舿•ÁÒeÇ£'»ïïZ4ЁzsñNŽùá·ôá‰= e˸¬f&‘à‚ÔD‡IÏÂ!6[µ¨›Öýˆù«j+ad‰š´_R6ÂÏóâÏíð'”záaXƒD<Ç +¨Çä ni?P-oP桬-ÀÝóïM/Д‰-¸½Åã蓒GLÂ=MîtºôtRÏ?Ùô—yŽê1?´u +î7&­0L÷z†ãE¼.ʹN6)6?sG=M¹?BœE.WÕ³!çÃÓ„ +zÎä}"ú3Ö¢/µIê"þøÍ¢DZ (ÕÿIõ҈ٕ TIzL0fõjûNHÛû]=M£WÐÓr@G™úi<ò,¾8b™o µ¸ê ÐN؀9ޝ.eáVHˆ^cxùê<÷p“Ƽ…&ÆmQ6þý¥XÖuE»Ö&´Ù<¿>Aö4K‘Ÿˆ†®%ƒÇäwi‚õ‡“/#oF¦5B02’4¾ÞCYZ–©eYêgŒðÂú‚³GŽ€œ–[¼°þUõIkòŠsø0$­zY6ŠÒÑo<ñoå&“&›w÷Œ*Px4H<ƒÁšë rº?¯€WìW®×î3ˆ¤öÚ6•Ét´WÕ\.ô<¨ïmÑddòiV³1;JU&æžI4A5 +9Ùz.ÖÊ¿>9ÂgEb§¦Vžžá>¯ËE»£ÀÉû•ŽÃ%TY%yrßMÁæ÷%n SÑZ "¯Êš×H}Oþãmx„9:M?+ ÃpY¦*þÅ2³…æÏ«à—jð7Ó6T³Ó6Jœ“9Ü óÚþT2nꎌ€²¾·< ÆÛ!÷+æz·¿ä¡=Mô̘pPýØUb–÷_â÷RãmŒ†Î51Êï}·?þ³ógk'X//³Aþ0ËwfÌò›Yó[èu!áôÓ3$dY[¿Í)¯g$‰ÂåÈ7íHE¬ún~ÇÊB'y&LãëÅN¼ãс>¹µÓžÄ¤…50ì$’˜’9¶œî&!ò8mÌh¹Yº K/wºyÿÃ6(„— &<Œ {¥á¹«?'ç(î£ÝŠf(þ§ÝÒwD¬]sãµD—gÑEŒ¢x¬{C[Zý>>VvÆåLH€×’ÝîųgڒëÉÊX랧‹Ú?¸ƒ)Esuy¥Êu +Ä~žÙå8[ÈÛÏÆÕ>\È´©™@âñXÇGN%@ÝOþW™ÛlÇ ìu½äöÙ:ø–Y瞧pŠÜãr¬®2(‹¬Ú½õâê‚G“×—{ÈcxQ«vºx©Oÿ%ÙÁˆÆõfÈäL>ZUÀ¶JÃЗ$µ=MHÒ¨Ñ(Én7§4:¶¹²Óñg͵…_Mý'^½Ð}=M\¦„E”³-}ÉYâ£wž¹—ɇâGY\‹½«]‚‘†=M­ +çªrŸò¬&ÐÍ੗îórÁ€Aìà֊P€ ¨Ý°ÔìÑîa´¸Ïë¹è{¢°µ¥Þ2x ‹æô²â‚ú-~êì‘íXÊ%ÕÂv +ô™~)hbüXµ¨Õ4~ìÑ_ÜIϦž 닣Œ"¤—$#ç=}ø5( yW1N’a.Ðêq)àüfхüpà½fæ5®y\ = ú/+ƒ +Ï8«”âPŒ +ê\Ž.°û‚•é+<¦³™ºcËJš@Ò0“žˆÊ°zµ$&“EĶÁϘ2(³ ™dö©´kèqTßù*™ÆÊ°ó…ہeÅk»)p;§UyJ¨ëž¦Yò€OTr›E÷%´eTr›ET¯Ѧ0aZˆÞàò¸ãl²ž¨%Îñè‚4<¶Eˆ¢X‹Äëf¡Lð}ó;Ì}= Þ} ÛËðQZÏLÒ¨!@Ȧîö¤Õ5oöüoöP â·\îÅ¢”<®w}fÿ¼T¨˜T¨ÛÇ­Æà÷öÂÕïh–vPöG’6ì PP-nðþ$ôpŠû_~=MëNXØínîeÛÞµ[mª˜4ùG€Šl»@i5ï9¾lX© ëîN«6G0´÷#J¸å×ï…>RoîPî„cV¡C.°2Olèo9 ¡üGü)g€ëâ[¥=MÝÝÒ£“â†)îr|ŒtSZK†ÀpX"fìà§»¬= HUmÍÔlôa››2Ví9ŒY§¬k±W¡ñÍjéñZ¹¥Ê«ú\ɆL)zfBÞ<_^€“>œ‰Ÿ¬°p¥2= ûì„ÀàÁð„uäe«¼\{&;Ǚ'v•¢»†óÄ_¾¸—ÐAÕ%H• ;uËD¸XCï¦$;³w“DÑRùJ®ýà/Pä%²ðš‡~¾à§k)ijr$R:ŠY®¹xv6ªÖJ&Cրç}È´ÝÖ\×?¿Þt¿ð¨×´€]kôCÍnaØýš¹õúäøt8à„v{ln3û« ÓÇmƒË½×µ¡zü7e’ÏøVÙb†ø@FE§,‡î9 }~T)ɪ¸C•·-¸?zR€f;êÈaŸgăî߯m7JòË3aJˆÒúã¢9ˆ¹I¿‚fWÇçŒ{%¨ÐÌI¡1Y¯Â +½–Ý:÷M²ipšÌ²õùÇqgܖ5«w¯hI.[+¬Å£Ôá–j¯Ï´^8Oµ­øþÒ¨qÜ? X÷o5é ¢H¤èò¨@¹Ú2žPY5¯DèFþRwyŠ(Fg58ÿ'†+ DÂhÏ|†I¨EÛcD¤J'K + ËBðö:ò= ð,¼=M,yí¤l£bí—t–‘¶ìpE,›óÑ¿3œÄ$Hšr§^+5ÖóP¬0_‹V®F¦=}}ä3yЏñé%HíÓHÊ;陠Ä}›<?zêÀòp1W§0ÆuáãE¬š/E¦ÒZ8òÒêÏÚû„ïöí°¹˜‚!º6äBÔ:I*x5K,5(ØjzÚºP~q +\\=MçEW^‰³æû;)š!*ÊÁÍ¥8(7 š8ævuüÿIסÞEeo OôCúÞ¹wŒp£t-¨YŽÉљ}ˆ¾ÁJ ç¦B¸´¦þ-â5hô ëG +†RgêUjëG¼CÎ0šíǑ‚˜NUjPä-:5ړý,Žoáé-¼ðr] +ñ|â39&9¨oŒe¼“qôä nñ¯‘Ë$reú¼æ×Ó²®Q4!iÀéc¬ø—{ËtÔU3— 7-_ì,?UÎì ;p¿ž;PÖ»•õ'šcî™Ñj1çjÓ +«ÔŠ”%àõo þvìÙ!íçsŽÌ˜í" >ËfÎDL(D\ÏCcð"‹…Wà±{d¾]³FsƒHÄ´wý6= "ÛèÌOÞ'‡›Xñ,½Øä¿±QÆB ×ƒŒ„©;‚ËÏ—ã®ÞY‡ÍŠJóUýí ~t›©V–‡~ÒlÔ{Ù(¡Dk¨!§Rr‰N®X3ªÏE/;¾3bH=}‘D¸ÿUùh›ˆ¾»‚nO@äDø–•·ÎsËÒ ýV×ׁ/n-‡<¢ÝxTcɚižªm¬Nn—äÑì–Äò¿{®OÁ2¦w0cmwˆ§Jaq\c¾ð֓AQÿíÁs°N¦ôg€v2QüûAÆì‹úÑˆÕ8{æ%!¿Œ1¦æ‚±„&ßғp±„¶ÞD3ǪtV5'Ñd•/×ëÝH°5Þ.Ï ¡È®,­ÿ}þîphËãý7Ú7"ù=}ýûö\‹¡‘p;5ftñQŸ¦¯WyÖ&$Á>GÂU—®µº•Âåqöót¹˜7ÃïçZ–ñ67o@Ä:@¼™­N6­eñÀµ,ðË3= ë¾p¾+m,³Ão ý@JæAtªž¼.í}ö­©×íLs= ¨*š€R1ú¬tar!ù %þb^ø€\ Q×ÖámfÏØ¡àdÚº¦Wðœ‹]íÓ Øa¨§¬oãπ³ƒFÂZJõ˜Ö2Akæè[&õmDÞUÝ änç€ò™mÂ0 +áà‡®¥ÒDÕ=MÕí/•Š—˜³²ù+)ç+'&RIÙn0õ¨[<:ƒOòëøÍ!І¦ø±¨úÈßOˆ¸~јu>ÿ5lçFùŠD"é¤s²œ‡Ô!ŽÒNU§mRu@â÷pÿÒ™»qRNlÒ÷Qv0¡8ä¤ÿƒP.ƓÌkоògŠi™h””U‘c=MÙ= úƒÓ Š9@ý6ôB²ŽmUn(ÀÕ +ý )C!y‹'¿Wgõ#ïÜÑøÃ§«ã3Ôۃq¥m×3Wɳ÷.}Ax$«Üƒ9hÊ{ç•Õúˆ²­t]¨oo¯ +€­Ì ¿Â€§©ú,ê÷,M5)AÃN#Íp kÃm!?ê [„%¿½¶•¤¨{b>Pª…nH0%V®Öäç˜Ùv×þ0·}ô‰WOÅtƒ»ŽMÌ=MpeˆÕm¯WñÈtübš»½iØ3#sgHÙ,ºÏÇ!Êû[ñG¢s ¹ä‹fI±E]MŸeNš¸¿FÈVy4®È[ˆ.:=MÈ­G¦Ãäœ +9¯ñ‚:)ÿÓ9Šœ5M¹AR{™ë呖´—Y¸43ÓÞÉ©/YŒ¦Uµ!­Ÿ)rъH˜£;s1V8ˆ +˜ÌO*Gã0d÷@è5“¢„öDt';>ÁƒÿÝltÊÛøž ZŒ1æ¡e,+©Á Ž†×Þ ÝâjÒÚ9»DbÓåHS üìƒ ±H51˜Ž q|¶íÂ*ÑÁ^ïªØÛ’b3-ذ=}©}ԟ{?jtÛÁ5ý¶?Ðô™bXÐrhÞ8¯ðW"ˆÚéî¦ÎNûò;ÊB¶Xê/<ÿçÀk¿”ÖTgm÷ùŸÄEDö‡°“gåXë&_/™¡¥4Gö7ðÃMf9㍥G9ˑÏœ=MÀaQ™h±Xì"Öô‹ ï[•ãGŒgnâ_dU}‹Æ“°}€[íá–Àýí4‰Ú ùù@Ë¥˜š= gæý‰(p1ÇGuÆ3ìŸâQFÙôLÉè»áUJÔM‰£'ÇÔKöÖÀQŒ£õ:gxq/c/-kMpí¯ƒ–J‡…%Ö!§ï<‡n§«˜ ¨Ø•RGÿøjõqY!•˜’±¥¶°*ÔYP9h = ‹¯Þa˜]¡¹ "º¥= Ðh·šž›¯fGÙOéve^E¼çœ¡öãH1¯J‹äèØÈ0¥ Ä[‘"³ïys/֐¶¤‡nR<Ë"c€g¹UÌ­a£¼¨ç‘‚‘­Æ³fuåÖ[›—÷J‰Æ¢9|‰ƒâ@­Ó…D$Á= Úُ)üËæI sm™Où…ÈkþRòI­aëËlÑ1:Íi{¤w­²E¯Ôó…m¦7kZÚÈšY{øSeõ4'n:Ú¬(:ná: +×*BåÓè<´wCP!—„ØpæÎ¿çÎ˰%4E9{xÁU[ø©qéÒÿ­gÑwæ¾= 4\‘Ž´ÉB-Ú©Ý0ŸÓʏ¼h§HeŽÌ:£Àøˆ¨]Ÿ-B„ÕÏï2w3‚xÓ®º°ùÊêDÊÒæyó‚lÇpVÍ^µ¦ù±¿„Ów!‡6Qòý*ﻒFkª¦;ÿÛÞ¬¹š¦­ÜÖLÒ×óӅFˆ<ÙäfÒããš3|À7ñ êÒAu»ùª$Õ½ð3诃ÁO¬(µ+6ŸÝ^䒀“ÕeœMÚ\ŒåÄôä>¹š0Շïté¾)‡xêl©Èû\&kfÍ5[~pŠŠv„9¬ŠÊîRï:ȑtÅ㜬äiÑ]Šè#Pˆï_ÿéA®Þ²‘#¦ân\‰¿³âŠC´w*œW9ðpð’‘O®Wš#¡F-C(61ÅõfÃ\„¿Y´v¯•»º¥ˆÕdî ~¢;Åvʺ²¿_1ðµmˆjœW\6"h»%:ÈðäZÙ¶‹–j6%XªíŠŠËr7{OÕhUsWvÉÃVã"ƉŸsë³PÒB?Ï~²Ìƒß x£z¿t¬Û¬´ˆ ­"¯}4åàwK·´Õñ°˜•ÿJjýŸ@_ÉÓS·~Y¶‘Ö‹™&÷+AY8¦ª¹‚O4Ù¾ßRøÛüYl£Q¿þ“Ýwþô9¨¿ÐZ‡Ùª¿2j 6¿ìiîF™ˆ—6{žíT™ jΗ&^-·ÃJqjqy~l‡ÞS!ºdaâ¿bŠé[¼‰'™® NªI²ÀÌJ€•ùN›Ô ?/7-¦t‚Iç´=M†¸^\ðÚc†Ïß%|Ú8µ ¢¶,G58šJ58M u=M-ýúÁ›O0zªÓˆ»®o%ÎÿaNŠ=}ƒOï6.j*ÄѤžÚ9[q³èr.üS¦¹Äæ1”ã1Q¨š™óúˆ{ŠÆ5^¬®Ÿ”$:}—i–Q 9ØT–Iáâ¤<<¸4@ífŽyë¼EæÓ!×ðk_8*6= TŒ¨DΝ/®¯†!ƒð½-ѝVª¨´[¯U&TyðӔʚÐ=}cÆÒ:ç‹*ü£À¯k»“EÊ>õ Ñp¦éiÝ5ÓÒ¢ ¤@·Ëôe’p Á#qú®×n‹Í÷â Sk@ÑÂ?3pIÑAk¯»×ý ½6÷þ öþ°@2Õþ0±Â&D)î7d±B–M:ÚüE¼˜ +)›f=}6i9§(Iñeë¨9)›&=}6P7tëvÁûùÈz±BOâ3®Ÿ²ëh¼¸×zb±Bö”› +-")›aë.7tðv“= ëJ,"9üIC'gºYX'ÛGñ× +ž[¼àFI^֚‚Š\­òlñ äÇÚwƒ§ƒªÈ¬õ•QWF¾ßÓfá%})›¶»ä©q¢^âè¸F‡‚~aR9;È<Å3,ÉRO[P{ñf{†;ø˜=}7#PbWUDiT:”ÿi˜Hiì+;HQ™] +ßãT„´j\è´gl2X£/+SÒy\+#ÈÆ…²t„>äÏ(ô‘%†›˜–²ÞóuôëÏÈAC¡ON¾ +•p¥qTXü&ۖCæáÆhLH\°<ìñU›*ñhÜlÙWîtx焒‹ƒ‘ÜdŠ¢4ɦÑèÕ æëdwž÷”Hϒ~~äÀƒ/µ‡$ûE2à¶ KÌ©’KcfB°î»Q'ëÏà~¯¥+Þh\3€víÈOÿú¾ívUÈWí¹ÊIWsVþl .²0K™²·’&%÷?=M”S„Ý„ì^jú)rŽ) +³#eó§þ'6- βÿ²URp‹§/‘c°,Š}êùzI 5Ê\¦r(/rí ³ZPRÁñÁŸo;ë;T"¯æ*£þ8 u.1= J¢i–Y8†KmÙ:E +iÞæ½õíÿòè•ÊÁ|š°±ð0¨]ï°s!ŸÊ­Cp‚:Ýx5"( z¦æ/Ü<˜Õ–,¨9cÝM9@•4= ¬À=M=MæãHóOŸתʀ*õD¼Ï¾j<•·’WŽÈ%Bd,´?x\W”¨ÞË=M/üÔܗ÷Ía¼H*òûGm­4I8ÖR²¨—÷*NxÇÝeU¸I]îÝ[Çð@ŽfzcŒ:Ïy÷ÓgيaS炏‘žy ½DP0†D'= 1düŒ¸ø‚~€Î^.ä×z+GBû0‹}U[žé>#ï’}cÐå'—ÍØnÒe!,gª2z YΰV +u|êÂÖãmÉçñ’u1àX-ªbO”&dŸùo‘Vˆx€Íž oÊÄy­ãhúkä“çË!¯äŽÂÏ ©dŽá_µÛ†h£ôY,É¥åK4 zC\Q.aïE,#”´†é6§,(¡ú$-3÷ÀڝËz¿^j)hâù&ð4‚³œRĸ³êPÝ¥ó1ýÄ1w:= ñè>éÄï$z$Öh„[܆½KVˆ?F˜{ÕÀ¡Rö!;|ì ·¨oF›*À~Š seoupö/ü¶ìÄè’3%X@ЪàÙΐ„öFéznsÁaXgçµ¢›©"Xø){Œ ø·ÿÿÀ¦±˜¹-|¥m=Mº© 7Vêˆhxbi‡¸åà{¢:f²!T“u@iǘ4‘¡)siw¹’üèrÁEBÚ4,¶ÛÁ,{âÛótà²&HA3 +Ùòúëà$ûŒ™ŽJPªkî/Ùúo #Ô%AÔ¤Ф‚…)ùñï,CÙö%#ôBZÇ‚j—Ò´-ýÎJ-ü îM­ 8Ÿ›y'ï v‰·¦!KOôd"} . +!þbg™²ë±¨.{¦Ê„®Wx.ãü·ÇP–ÊO™¸7 “y­¥¼7ZAt³ˆÈ…y²*žS¼FT©•l—ãüÇ{F_Ÿ³ó«g“VlX*üiDÐûõ“ û&c€yÿ|PžëUÃQQ½j—ۀ½2[àa}ûêà莾Î¹_Ž ɱ Y›ZG„³X²œÐ±ðdQˆÙaEÆìiœåñ]ÿ¬eZ¾{p‚ÃuÁÈýWÀ¬}^¨ sh¢¾9ªÌØc“BUŽ’>°K±Ìw‡Y ÿ‰Ã›U뛺Ë*Ö³BùN«›²ûmE®\-Ã,sÂE¶û±B+XaάTèý;r+—û}Ñ=}9ï¸3̲§\@’Ћ6M[¿˜Û儗óHÚ¼¦">ø¿›.ä‚–}ˆ£ëW|¬:¯aŒ¾ä\À»É[¦Ü넓]u‰ C³SŒâ؅q·…Àõ¼dYÈÀ{໣ô£÷#ÆW©>Ïд§\âŒÉk{]ˋ=}Ñv“]Á~HtŒÆVDÞT»Óý›U ]“³·V«R/ÙEÎê ±å?oþü5Ñsˆ±{}Dvg‡¢Mÿ;A¨òš+j«2ŸkòtÝO  ™ëDöÄ"f,b«Òùn·fbÙûýœa'$4¸d†R‡ÖWM!'oýü µ ü…†À»ÿtgòòp¸¤³”°ø'ª6òéšPlvgû¸ßÈ;ԏÚ_³#}!ñ¸B2Zy³m©Ÿòt¼å”®Ë²íL,Ä= |{Ƹυ†à=  Oz ƒóCºšÍCšš†*¤ùŽÐ¬J Pášd¶Ì̐‹ÙPPÜ-0x:å÷÷ë(ÐfÀ¨’é÷ Çw•‘ÙL²$#Üè$jE®šQX¥6øç÷k2€æ¢vêùe£ù0£œ|¡(&çºùcóŒÙP’9õwŽàçÅë/04Qø´«%c:ÉÝÇçÇÞꙢ ¸øh]¡ºÞ6oê篅2žré‘$ãCûT€¯¤máùÐÁ&l…¢¸ }tÒĤeÙzƒû}¥žÝÀݤuÈúÖ¡ã¤é³¼‘äӝM+ÁŒ‰Çò/t<"%H¨—Å|ôòyûüpPÏ7,iٌoî¶ÇêåÁ8ÈsÃJýkò:7»ÀË@ýÌ™•[{#sA?}F{ÍD „¿‘»;LSHY(rˆPÒñ2ÿ4ÖÊ&•d̞7ág•ì¬ꀒå¥Öÿ뒵? +Äë0 ä-BH¥{$sqŸ¹'Á(úQš¶+ᔊqh³‘B­Yæ¬ìÀóë@f±Ošöš¿TG­-I”ï­~! ½FrÙ÷]ўA +“ÕËñ*´&aÄ}$Wm¶¤ý^€Ò*qN>§—b•Íï {™¤ šÈ4ͼ1)N"ÿç9ø7 îA}eò°¼»p–I8¹4 +’š„äôø†û‡5ù÷Ʋ™· níí²IÕNø±nø:ñ”ߢr¥'ÿ”læJQ˜¸3hÏÓJ°IÓTÔÓ¬ß#¾òMä=Mü =MÇ= D$B‰Ð:4åÄnZXKqj¥þ8ôR*ø¤QJS)Tí«°ÉÞ@àPÊ&•kÌ n³-ÈvÊØø,ã鐊yýnŸ‰‡5)¢éê8aÍe(wa™õa.¤}%f'bɔִß#8rˆÏÙý¡Ï:ϐ¤6Ü:¡x~f¦ôì&îÉQ™¸“êÿÑÉág™ØñB)Î5‘…m¢7/4ÎnI×÷/ëöÙÛ̞b ㇰ7'ZŠz„P^÷âãr(8(Ðöp*9’Ç/ØTTú\®:®GyÄ4«Ê„ ÄK‹=}4>f‚Ê,Raá§z¶6.³ m:øûCtìÆß§}H—Ü ëþlVqdt«yYey^ ^ñ˖?}HQ‚³Hk&œ6ãÒL’ä0Ÿ@Ü[曀º© 9Ö3°$°¤=} ‰ôûæÂ]ÔÈ¢ažqÙaˆùa$Èͧþª‹Ðւ8’YãêOLu·iBéA÷Ú[!p4Õ^•îÇ'»ï„ÃŽòdR{ƒu‹´?îôª¿H‹‰çÿš…‚?5Î/íÀœÿb#ž\ôLjÔ)ûJÌɟ[¯7šŽ ,º$ Íh‹„³œ©Ï¼#p¯”zùj D[»ÃÒk:Šÿ†%ozԋ{эלAÀv™^äÌý&§F‡ ¼ËWsæÔ5‹= Z-æÜI!0ANH¾üûö›Å¥!”¼s^x¦lŽ–éP˜õfØpʋ<"<æD‚“Tç˜Ôè  Ë°ÎŸÏßÀύ÷>𬻆ÆISopJá: +NÜx16;hi ê–ƒéßÓöO÷ÀUG›(GñÞ_kÜ,Ý·÷Q¢Ë|¨¨Aa½Ëc¤ß©:À7úå#KÈÑôƒ¦ëc%¼#[= à+Xß½œÁ+EúD +:•%RÌ^ôÙoâÓâF‹£ºÞ›p£½±Y7- +X yk~h~’†a—[Í|’|Œjd®%ï©ñ8.æ½ÃkóAÛfûB™è‚Âo÷Œ—|k0²Ìê¼J„ÝЏyùDyŸ3«÷9øí†¢®ÉWÍóµ 'íÆ!Â3ɯ¯ëîì‹Ó$W2·63/™ Ž4bJ¡éÆÈEJÙ*"¢¤§{k:» )v™­ù3‡)ônÐRªó#&$YSzhwœ&üX²W€ÚANrg?÷ÐÑF­ž­†î]÷·˜Ê“QÊîñ:¬ã7£½Ýã7ÃÃïQ=}̓‹#íÓ-çšYzِ·³¨ßM¤Ä Ûv–3©”ÖÓS$òrmd\\]› !èúјw×ð4¬|"ÿ¦vÿ“Çë¿7µ'%ª”¦nѧ™ï= %Ý% ¡ÐúêúÊʠՒ¹¿êÍ}Ö§¤$Î9ï^bfa¡¿%05º _áêÍìßc m21Ø Bëº÷§ýИm]Ê_3= ,Rÿ…àÊê½CÐMìBÙwNÜÕ¡æÍMÐòÇL·vïÌ8I0¾°… +NR鑤6òڑ$4—qÛðãjŠR¤Ð.î w‹u؎„ü˜mÇóþá¤uÖ¶¿¡#˜¿³É‘äԝWÿcBaHW6G ?ÇdBàx^ûhƬà4s5ÿ ¯¹ßlÞo%bPÛÜ[G.ËCS‚?#SM]<×¢¦ú^_c«LFŒë{G]€FŸ<^m¿…{Ë<=M=M×zêúó“²ßl6žú)¦­qN–àeÎC.DQy~­‚°8'£ª6•8"øöò ‡±®YnË_«Ü;´Üå>#4ú.‰{”˪BƒM…1}Ú®«/L8³®z;êU”û,¼v­{Q€ H+zsM¿¥ÅÍâÛ5VÎR³|ÿ‹Ä»>&’ÅÔÔ2pƒ2r]•³¾›9Xêh9ðeû µë{À4;nŽNž^~>3ÎÀOÛ5ê:aùëS@sû’iZ´ë= ‰­*(•ò§ü;Jb¦F^lÁùr7°"þv=Mé•훡£ÛÒÆÞ(¼!mDdìÌè\ÇG_O¿\i“û~ÃÈKt›Sà4dñqLì5wÂ8'S诮¸?ØM ¼ãÚcþ\_è/m&)ý#ì“SƪÚk%¢ӄ¨“j¢éÔ¯ÃVâÎȽCœâܨò›ã Ï^'Í8=M ¯•ÖÝ*>¸¸ž'6ÿW{mçžCêµaÒ") xؔzà.º]­lÚÆp‚:ëdI–3¨¥›†ö¸’‡ºë¾L6Š4+HvöÈs× 4>óX[2*o²X–þTw3NÖíáOAU¢¶ÕñE4\(-Ù4.ªTr§ÿvTÕ¤ 8ïØ%p/ޝ +ZVm‰ÝœÜ)ßlðz§:8W^s=}qMnΜ‰= [C¤¤øÎäù3ºÍ†'G:âà!OrVFžCO½Èþè’Aƒ9¶‡2Âæ–ávàOÉ0–õùÏ¢b‚‰,T x {ÊýŒšòÂôD|1¡M—󮆦ö•¦:‰×!mƒÇ.ô§ë/¯o˜sï© ÿl~Hÿê4 +N çB#Î}yg~+ +YDø* +â +Ók‡¿%©9õh Ãy.êçþçŸ/U¸†q{¨ ¼?%·Ñ§h{ØHÀÜaw¯¶þ£¸I%©4&ŸflJ°ºåeƒÙt¿Êohj2hHSå +€øzU)bÂöccI%&3-/=M ¨È«»èéöHY¼Myîîï¸å9ÊxìN(ˆƒY„¢Öt)‘5S‚ZYÒ)ÄØ›aM«Ó˜L àۂÉãd€Á>¬Î‚Èú +kÝÙjPäÙmi%©µ¯·,«xIÝjjáÀH^Óx&øžlYmq㘚YQaвoM»---ùà9~/TwÕZȲ<Øî×m’(ÚÙkÂÖK—hõ!PbÎ"µIÍ¥ 0°ºª·«©2-KbôÓJ¤/¤ ÈšŽÇ‹¶³W°zŠT~Õ +Èt㒸r”ø¨j®"H”ãòø,•µ»Ô;ïɊÑsVl[uú—o¨óÏ3,á“÷:[ö&±¹¸G6þ\ђ™Zn‰d&ñ(Z*)<1l!OŠòÚ²·èœ!©žÃ®T¯%Ì»iP°€Úˆ ‰]ŒîVy¾©A¾)¶ÕîÃH2&䴑SFYð +rŽøÈSSxèºØ“§b!m%Êõ¤NAMW(»y9Ôîÿ“Æâ2"ò¢R=Mâ…@8#%eÊè+ŽÅ°#µœÑ= –"Él^sõ¦ˆ&ÖÅ×3„5—ø¢o\¡.¿]ÔÔÐ(£*Û+ãÿh«ö‚N¼¸^@ˆÙö3qܪç‹ 1yün!€xQT““v4 .ÝÑè“Ø tj¯!7Cÿ}ÃLQݸ'y‰j—¼…¾|džÃ’§!X@\;A5êžúîü>e¿$wo.~î[œZöÒ$«o—©JêöÂ"œz:”Â[ázÕä +{»= §“+Š;æZW}ÇÒÁZ{þ$&>§›|@!/¼È( 0¥3UyêB&)$M-Ô¨T¿>Í·@Ü_sV/Fum¦g=M-“…A#k߯^{%ü1WOtC¯9µ@Wá]kHÜ +[QQC¨@_™)Û¶[ä~Á£i\yM{‰W̃¿_ƒÁÇ ;ŸÄÁ]BɽÌE´òöKç+ ĹžØ‚GðoÒ$ãڝ޾>÷˜Ø'Á“ÓƒØl\q­)/ Ä9©ÞÁJSÄôC£~Åã4îá?B5óÊYyÿýÊ?Öraèŗs¶Õ uƒ½î± ßé–T.7ÔÎb4¶ô{!¼áÜÔ$¦Òj”o¢µ#¿¬Yžc çN‚2(ï¬%àLFýâA@{x¶t&¯™ ÜØT¤&N‘æ!Cž{)1(֒jØjisÔ®³š…O-é²^ɬlߞÄÑH»ÈÌtG{įOx›ö.f­’§„覥.GòÇ"s:Dõ;€:e@wktwïy²wWÞÙw -w9Á,wÖæKx@èex¿ýxŽ!ŸxLĽxÂÚÎxíàx ùóxuúxêxOÅ2xI= By/MyšlZy£fyÅsykyÏZyú‚yÉ«yëÒºy–Ây›JÊyÓ Ñy´Ùy«)áyVêy6›òyLöúybyòÝ y³eyÏöyGŽ%y(-yAÄ6yùK=}zZ•Az_ÚEz‹IzfQNGÔ>{׌WûÖ¨‘±8í¨ó˜Èô¡ñ¨1§¶ñړ±œ‹Jma1-Ï<ÐJ\Žt=}½ ¨ëIÊ0Ôr SØI!×eî_"ØHý¼\ä —i«ÞíûÎJdyS5ΓVÃèC"Êkm=}^íc՞âžô­nÙ±¢NôùoT”³eYH-´Å×å5$WEÛ +í‡É—ý: q–f®Ó®xÒRV-ƒ +¢K@"PgEŽÝ2}ý{PÂÉæc#Þ!ì—Áx¥é·‘n³ÓÛû’«sšk¶Ù±|n½Ù²?N™CYøQ'9_c4is6†«6ÊÛzüeƒ9ÊàŠþŽ$\QQpp4bԁUL=Mâ#f¸Æá=M2£c”D¿ÐXÐï·LƒÚ•„ƒÔ®Á:ßùP͊5fÓ,–U2âo’¤ÉÊ=}ò—^ چ Jà5Ö½ƽdbß41FI¨Lהd#pŠ«ï‡ëC™âDíÞ@ y=}ÔÈìɛ¹ºüÅñ½Úºg厎¹–Àž¥7qåTx–Èò‹®u™ˆ¼öÕÔ5¥0nòùÕde©—‹Lò€À¯PÔÔey3åsãê‘¢PášÐ·÷ä&±= ÉŽÐ÷…Œ-û|„ʼ…8‹³’ü ¨Eo†O7ɏ ƒü¿qEe.¬Ú +-Å·—Þßï™ynÖÞ´jʙ&.C +ÞlÊî˜.3ŒÐƝ¢i©®«àé¦iš«¤ŽItt7[Â!ˆ®Ë˜¡ '§¿ŸŽjVågg¢lbpFfggz¡m®‚aaÔÔS#¡3d¾ÌnSg +[¶A&=M$óuØÓ¯Õ2‡â (o7âßÜËúº­·2äjγÛ×㶞$lÑù‘žÔ/€A¦ ³Bs}üü‘àß=Mæ¢e* â ^jÀ ¾ŽS²^Ô>]TÊL“‰Ý§—MþÎ öÌG€]é>MÄtÚû˜´=}¦vb«&{wLL,soÇ¢Õs¯× ºX×-“7‚Í®tÁjäl§T~6H §®Ã¡¦ O¨b¬è’OÚ1ÍX„ÖC‘Qb½x—«F¢¬ SòOœÀ\–X°‹!šI¯ŸC¼Ú¾,gC¡®Ò›=MAìkà ¢>TZƒœ ;)T’mXS3 ÙrD1G¶©8[vo©íûÑȧ q¦ÑJëá·éP*••¬±×= z–‹T¢ü ¶‹$?xIÕ®¬ßtQÕìãàTÂPá¿ÔËP›}./=Ml¡M^IïkýyK +"A²l©¸Í6%ΔçÚJõoD=M lXÑI|®Åq‘šïóVßÂ^ՏGŸ† S·šÊ¡÷6£ïòÊ]ìcÁŸÊ_4 CGG‚‹u²s,¬ ºS6²J€ö‹¯mÁ«%›©ÊU‡™[H_=M¥[&QH{0øzŠ÷5® žz—V2^s÷j2/œ‹ywÊ*È!̙ï(Æ,dxÔ¯÷Bö1ûuðvUöôV5„˜±ãF±\yõnf±°]ö„bÙ@¯ õIÃÚ(Ù¬.Œ˜ÑUþñ‡e§þ¼–èäS Õú-‘ozÍX–-ê@ŠÑrb5¨nEÇÕsädVjrœ†a3p¡üÁ‡7Ëq¡HlÖ»qÒ%Ž ìy½&Lô—¾d9N?J;}{<;í¨ñ:ñ¨ñ˜¢ñ¨ñ¨¦¦‚:§V>E–+µǚ¦^žúrtëYb4•µ‡çü{òEðôÐϓÏ^ô÷GDMEPÞ™Ã=}Á¶"Ìùtҙyƒ© ȒA šÔ­F-ÑÚëS-Ìï÷²S%ú65FQ ·…”)~õ$,çÖ«¾†„’cËøƒ3¼®Ç$¶ÕKÚøxý(PBt–ñR²˜ƒØ‰=MËhB§['èþÍpÓ èç=M9)˜ÿá‰.´¦pֈ’¡[t·e27aÍ7§W„n®r©sEšÂ«µ{×í5j Æ^¯‚áónPƕ€Êš°œ‘ô“õá‹'ÒM4ý‘ÿÅé=Mœý^šè0Oe:Ø¿²ÑðóîcºN‚NÀ5W€,)ðÏãF9hu;º;»:FWÀx·x±ªx˜Oxìxô‰‚xK&tx Ágx{]ZxgøLxí’?xð(w›Rw±…ówF¸Øwmê½w5üŠw9_UweJvÔâvvi¸ô=M¬öæöáÇb÷¼d˜÷Ä÷Ûkß÷'8ù÷î÷Ñ/÷lìBøÎQP?ü>›SAO{ü™¨ñ¨ñ¨Ȩ5jñ¨q*\-ºTë§ùSUêL±„ô}ÿPd$4ƒ5AøË-Ö* ƒ@tÕÛa( +¯ ÿN„ íͺzs…WJ$c+LîjüMoz…ñP= Õj$krŠ1"ÐвÍyð„uŸÆ²ï82NfÊ:O"z«6 Z=M²:ÊŔJ…•µNðázfAìܳcn‚-ÆlSÉb± ŸÙÙÚÁÞ&‡É:sÒêÖQk‚¸l¶-ҌpœPr­¦>6º½8(—À™Gtƒ΂sª¹×¼ÐâSr WEií$E¢UêNv.q*ª~$\*w¡´*âóuz^¥6F™ù_6¦ú vG‰·²Ú7‡çêwɐû$÷Mʺ‘\‚‚Ðÿ%WÇ 1ThÁ¹âþ<˜‡=M"¹»<Â1TLŒ¨‡ûFŽ‘1p¢Îð@”†çãÊò!d–g–'“: ¼F8Ø3ԄF—ÄG²ÞŸÚBŠ…@ÀÙG7ÖÜEy£J-îÀzˆn>Ä0D5K­M}4:‹sɀL3šÅ„ŽoZ-/³Ñùˆ7z„¯e_fÐU¤/ZÓüE‡ÂŸèÚþzþ-{zÚë\À=MhÖ¼= Gc€ê’t²Oek:öÅäªå4*•‘ê^íùÙi½<{é?<ۛwñ¨ñ¨ñ–zñ¨‘ +1Ƭùðžv”¶v97«2j€¬–À.:dÍMzc‰×8ãCqU¾7ºžóv¢g‚ž+©Wœ.‚&?-§bÍ2þ¹šoÄ= gfì0Ãm’VÓªá:å-¡ÕU½4:ž°1ª ûy–ά•wwáÀª¾qÁuçõÝþ|˜%W0s3ÈY†á&Þ¢»h>éhotÚYUÌ÷ýÓSN¹¹¥ý¿áׯp~&ÔTj’ÕŽR¨|.^Fyiìÿ\ÅÚª”œrÕò™q¸¦~éÿÑê¨'ó®IÜ٘•IÏZ®›~Ü^ºvýubq‚sÞ±þԊ½F¦’áØ®€âH⊩— þkJÑ9ü|Ё/÷s¹Æùoª‰2Óþz·”®ýÒÔ¹·,ûJ9woE[%‹;Áļ¯9Wƒ}ªK&н$”Z *K‘™E¹Œc‚ZÂ-™M@«ªI"÷²?ª +€«[vÍÚ©mènÅX*ÞÃÙVˆÙŒGrò«fÑ¥´g´Ä¬c9R=M³9óN +À1?a9Ùûã_ÌÂÎÓnrÄHsç‡gÀÀ֍#í† 7 ¡õßlõjÈuʯBT"Ðü¾Tµ¡M­[NԖQ¹¶íc«!9§)•!ËøqB¢¾—uQ ä͂wiùªyÁÊ.ôŒ˜»)²@W‰j4}×=}ÆSîòÓƝÙie!MÁ6…cô–ý7­iÐ=M#’(h —e‚• ä‚ —fã[-·@)ÐOèç*ŠÐ·¥éÝ4:3ÀŽöôÕrÎ¥ó™~#Òp÷G瀴˜hÎzŽ=MưóvkîY8µ½MV û ,Èçè¤ŁeÕS¥(҈ÿ¶oÎqæÔ¾rÖ$E¤œÍQaw¥Â㑏‘ñí7„f#qÒòŒ-ëÈçë±qj£b(v”XWÓ|þ:uSë%‚b–ØÐö+g#ê˜Óx(€›rˆÎö”·øVhY)Ò/ÛòàJµ#,# ®ªjÉâµT6éRy‰Š)ž4ÀyiÈ*b= ùZ$8Ÿš'"ì>'ÉU†;¢Aë4¸»?*|ù£XŸ11|±9mÃÜKzŸG,5K€-\¯™HœsÇã¯SÀþóØZ5¡SVÂLGF‚cÚtý&N^7³²?˜xL¦Þ€/°Ã끿 Lr™Ã3‡ˆ×S±lñÃçªÆlÑ‹)\ É¿êA_‡—^œá—}üjM5y…óœd ¨ßϨPÅ৕^ $„dš¸dà1ôÏcPTh±m©À[§ÂGîTméêά$ÃGÈæmñ9_žFTbD3UC÷²O FQS‡Q«eQëªM#WKÓsˆ]<Î0=}f±DXP#ZP3)íûÇ:C‡4‘j=}P²@]YFïùKó.› +Ú=}- +GÇ>¯— +ÞGsÑn«¶ÎÙhŒµ –%$ïƒT‚XV)¤ôàˆ†ÄàU…/7:.cƒÌveù_æ‹øcýdæÃÒÀƒhŸ8=}I¿P 2¯y âÕw±žۗYøœ¿šINrŸž”hÖt”xÙaÔW’ ¹gØù°=MÔÔׁ)õ±·†ücd§&ڇ)¼_Q”_LœÎ³=}™æT®jÉ+¥¬çìÊ ·‰G₎¡ºO.Ù­ñ§_€+hׄœpÂ-³­C"‘?ÎóvÃ4â>T°n»Š8Œø.~2ÒYú·“4Ööº÷ó/Όwc2üØ»"©_¬0÷Fz2vn£ØNÊ¢t=MŸã>‘ô1/CñÞµšó‘ŠS1þÁ‘çï řÞ6íqäJçµnìj…(¼¬ˆ²ùVõlûðf°æ±ûð‰c®uMÍ <òQ(ijѬF+máp4°– -}FIª‰®¤Á%¸N…g§À»wí£Ã(%5Jè€Æõ˜‘Öˆð“,ùAúb‘¢~Ê¥×õ“@ú7sçÿÝàPkÐ"/íiþ®=M'ُÍõ腋·†l‰Ño“3’­±7ÍZ¢ù͚ Ê:&Øf0—fù϶Ú&%ʀŽ8¼G譒•Ïú= œê†ö*Ò  æF­51””¶%Ï?§zi¶RYÌz^J…ؽ¸fŸò“/\2¥ªÐÚr7.&Ÿ-í›EtVù•²•_Vg!Æ)7§TwªÁÙež6‰Í´†øŽÞàÿLÍTq}UGͲ5QG ªÌ'ï Ì֎öž÷¿jÃËúp½ÿºëò_¹¯Lғvä³÷ê×#=M˜ˆ$[@z•»ã­œ’XŒêaÅ1Z¾;‚æt©±Ù).ب§q¹óhñ¨ò(ybJö'hJ:ø¸³y1¬b9ÉeVM]°4eSøO͎*Cxæ@ÅýäÓ¸ýoÐÈýÍ(Æ@H"¤} +õ‡Ü‡h{“g¿¨À~¿ÑxX¿u$õˆ+ãpq¬˜§i)çïQ½fï×Ve¶yˆã-Øîî‰6¤¸ÏÑo…š–•,ȄÈbýßJ @=MûHn= 6 ˆ3Q¤ľƒ²„%֕bèÐ)†"öúÑ=MUàß¾¨Íö}ØâÉ#ýÖ 2ÚNB­ŽL·¡xY[Ó-)2,–”·ÁåxZëÐÒU3+ â½³ëŽ_z«xÙ +I„2ٟ.l€÷ô×BYc" Zâ¬Fø”wj©i™î»v—êåj"‚è‘,jT¦´ü5H—óíÔ9.ð²,&™¶ô–<9£œ™ýÊ,|j‰-ÅñE)l¿.¥Æî—:=}ʃæ!¬u©ù3eê·¾òv¯Örnjy:I›÷7!zê³6º·2(÷$vi˜9z×:ý†‰r+¸z3Õ°ú4tˆ7´|f +o¨BÐ(œ=MŽ@ªœ7ˆHˆÌU+ç ÿ lXÌìáNï*½Çé#’Hax몙Šg5F²˜6Û£–š9J¢½B[¹:{3°SÇt|~Ûð®ci:UÁfÉ^Ûð³_ʂ|––Bèί ËYú&žs~o Vœœž ¢N£#ÔÜyWrÑȁ˜S—¦UƂÁ,i¯=}¹ŠÖÃIBhÛ¬Me董S©¾&1=}ÿn3‡a÷𦯱ó¯Y(:"»¹zXÜÝ)ÇFø˜KwýPbÇ+­çv)“µ›ùDh-½^.Á›(¾ª¸L©%¨ºó2J{ؑ[¹:Dÿð|+õLÇ׎\Óó\O&þëdyWi=}À~]œ2vM–逜£/iÅüƒ"“/Åþþ¢¬K²ž²¡=}=}‚b}ÓCþ„«‡¦O7´Õ½†\®‡þö*¤sú¤Mu¶ß‡žuÙè{ݐa·Z Ž¬Q=}µ¡^´ŠqYI±ü3—·ÀÐgJôe4£ØÐÊ¢ØE ž™<‘£P]¸•\QñOݘ™Lߒ€óO“Ç9"Ŭÿó?†ÒÐÛ-ªD/^†,ëíO’6ÒÜ~¹T…&›q‡A(—T} +‹”Éݛtl àWje‹W⻵@·)dëÛîMåñ…WŒA= ±e,î|Äý¹]Gç¯JRœgݟìÃzö'Ìg÷i6nLÚzÃ9\݂.z7ÚÃJ¨]ø9^=MÉr]ÔÅ__=M-G̅ˆmé’=M4…%ŸÝ_uæ[º²@ô„•‰úòƒ¥fÞ¹ õåÏÁd\hðM% ‡ €ïjóË÷^F]_Úŗ-ã҄iHÜHJí£6Á‡íH +[ â¯…ź>·¤Ÿ¥‡†]1i¼!†ìÏäR~õc=MúW|öÁŽ»¦c¨hDáôV>¤óŠ€r¸¤&NªÍÙ´=M6)æÐBد¼^= ð ñ/$Ò+ŠÒçyÍ12كŠÊÞT|ž¾T0dqÜßQ~UšáŠÖú$ àӌ¯áµs(Z…öâGÝv²6Î6ùŸÓÆÑ)'‰™¯\”ꉟ¤æ„è¥/±¡Bm¬©žLôºÔ1 +$–¡®/= ..3šº°ÒénY—Ù&Ú¢tȐ' NçtKfZõÂhä-îGÈÐ'õ€ç£Iv¸§eæ iž^=Mn”Ê“¡Kû•ÂvîúgÓ)´^ñ¨‹¡=M¾Àïäëç¿ +ýt$Ñ+ã&d”µÜ´0OÜ¥ÚÊËo³Ûëõžèȉ¦Öî= 2e‡W®æ})’´Ñ&„õ€„¸¢†%ß…iȇ¸c´²5\6¤«îø4*6^31)¡·ÂØHŠ×ø!?.æÖMó*9W)¯‹üö?‡"V¥!-iQ†³ +æ³íÉ#(7üÌøwè(kr­wZ‡!+3@93€Xé­ÎvHô +FˆÙ-Áì0÷7xzn-fÚ,bš6ñÄɧBF–PwYÝÉðzpL™DúÒ¯€›£J31&Èæ6š4;kYǼrÂ]n¦?1奍˜OGm{0•^g(¨Å$! 8uAb¸^£ +r–ýA”nSaýQ®ú^ÛÚNEbóÌâV=Mþx xF‚þ¸û÷7ë¡};É1H}"›—çC­ s©“]9¸OgQû_Id¢ì hL¥Í+Œƒ|8é'Õ?ø¯?)ûëjÔcãºI1Q] ™Yÿ4³öñ;ÒQ}tôCfƬí?¡%à‹˜Å¹mgnË܆ØG l­¸j‚:o@Ž@§ØÐS÷ˆÄfúÏ+]ÐÄsì’ÿ%Ö]WyÄsg‚Öƒ%€•òßf]”‡Œ&L@ „tTiÏ ²æmõŽŒ}xõPêÄÀ¯²·y¤¥ þg5¦Ù|Æ¡ßÌ ŸEsÔ{ÑÍò¦OtF^PQ‡Î8d¤ør€ê«âf$ o¤ãx8x[Ë(‹†%ÍtÉçnÞÔ6G$AXݽϤmÃi2Üiyd0¯‚…*߁á.9žŠ*oíÁr‡·2Ã(s´ÈÅz÷zY¢Á¼öÔ°ç0}Ñ4µEö‘Ö r5l®Ä'1Æüç\ã>ƊøièLï֘À|e¨ùRB’aEHÐ¢MßÏãØª$Äß=Mç.Œ·–¼ªnD õ´÷"T·ñy)5¹’ùÊÊ(·ÕJϘLr4jŠô*2÷5=MŠù#¯v#Ñå~¶¢z>õ8‹ŒySæ É{Úõ«:£¿!ÓrYŽØç µÐÜ ×>兘ۓ&¼¤æ¸kgz;Ö0}Ë«]WÒÁþwqIM,[x@UbœG'1ÿôÃê‚(}/Q Ç“‘ÁÛâm¿p /|¼éBÐc«Q (ë3º(z2é¹:ºoû;@՛S?¿G†S‰kž«70>³r<ÞaåUGÒ;BAi¤°óGåÅ¥Û7BëUi~vJ;ãG}@OUm½ÛXKÁnnál!kýŸKe[kWG¼ëÃì½ìÉìÔLs4F³SÆAvÌ{¾š—™„€ˆ˜Ž†–‚’Šš ÿ=M  ˜~Œû=Müj[cowmuai\dpxnvbêÛãï÷íõáé|~—zswuil= hnvúóçíáéìôøöªϤn¢ +š÷§ Õ°VrfSu5€a¢Rð¯/‰×u®9=MVñ'¶3r)w·58Ýő9Ä@ð؎!i–ò¨*Î"ªy¹84òÛí¸“‘°2ÇàV²Zx²(îòú_ øÞn†Võa•óÑØ6‰—ó¹*$&Jš:Fãs!p‡Óú±"Ì.rZø5/Ɩføœ5Õ8RZ¹7 +ä(Úð³4³Îæbg±'Îʺt°=}F6‘Wó15zöœ8½8²ùîTòèu8Řz÷µ1Éfª‡Mv˜3û¥Ôp8û®‘&7´âyîÕâŠ8»á«ôZgɐ&Zësª¶@†ƒJۛÓëâ@\S@;ýnñð±æ§ñ(ñ¨ñ¨+ò¨ !5¶½ÔTT7}÷‘‰Øu°û¼jÿ¦°%æâ‰(ØLT7}÷‘â™ÃÛRðõµ$=Màނ²‰ÆGy0™âŠÚy³«ÏÃÛRðõ¯974^‚±‡ÈGaúºM:‹ƒf‰ÍcîKúD²àç?–´=Ms¯]ü‰oÓÄ·‡= r•ÄÈÿìÎRˆ}j©ÍÔ!¢ašŽØb„' ˆkÃÂO²Ü” +o-ÛÈsƒUB4 SïgtW¦I-ºô­ç= Á(w¦=}õ¬Ð‰prŽ4×¥©ðU–„›rêÕ½¹àë®h—ùâٶ虍 +ö¡›SŠd(†îϤ!ŒÖ¬ÏGI.ä=MՎŸ°Ï”TºÃ«ÐLç}–¶ó¯!M\‰m¿„·} r”Ôˆÿðî҇…ª©Œû”Q›Q‹˜¡R„((hmӂϫÌ= “ÿjo1ËÈt•B6Óï"W<Én»¬·{´”Ù°ÿïø"‡ƒ²é̤!œUš‹¨‘¢VD()pŽ=MÚ¶O¬Ù€ÓJ²Ú0‚…6¶ +ÇG"T —¡TÑ,Bx¡®ÉÌ ynU¨sˆq‚ÖÈ 7҃’V¥ÝÞFÒ$Ÿ®mԊ䡿ëÔ³±,í#þ¨Eu½!–%óu'aÔ¸÷¨uÁ+ÓÍiñ_Æ4ÐaYñD„"KZVÆI×b ,AmÙJäµfëٓ±+÷Ä#ü°-u÷‡"†¥óU§bÌx÷©eA«ÚÝ òc.4ÌA)ò<6„ ;*ÖÄLwb Áí×z¤¬–«×‡É+ô ¤Tív'ǺϰȠ”~¯µ3¯ Eôםiá+TñKé™lt¸ô~\"“Ãt·‚˜2“Òx_ò겇 +‹Ù‹= ÑDú ^q C$X%NžÌF?tü[a+LP›€qKəklxô|y¢ÓÊT·€ˆ²“ÎX_ñâr‡{Y‹@Qžq;äX#>nnͺL!Eþ“üšï/¿ht|¡‚5j\eGÃ¥6&zf꺾ގyTTQê28ÞºŽH¼°@{ë±VñĨñ4Œñ¨6(ñ¨¡)®TòÙÑ$T˜ü¡ôO)”„ýÁµdOyGNùv„œùœÜ[:•=}Âýxý*ÔM¯Žó]'°ÎÛ¹v\,çSf’±âÎä'´fЁ€ëàƒLyҕ ((3s؃ìXBIB¾q×¹Í{ørñ+ڗ-·½¹oâ@® /]ãTªÁGà!7a³:Sˆ ÈÈ£gÖñÝA—Õ|ðh0£®ÔŠ˜%랍5OŽÆø +‘ŽïH·ØrÌe¯Q(Ûá^Ð3ºSÏpß=}…ªJ=MŽâ­ s|²…¬W"ƒ™½¤’#͇lyÑÝÁ”À“˜:»uæPÛñ1TÈVÚÂÙÀª/‚I‡Ýy Î/§‡ï7|’òŸ±#m˜mÛ,ãFjG…~.¼)wº‹Eù)•–Í —Ò(©aÁ;¹6x)z™ÉfŸ¬½P“ ÃÁ¥æmºÉÿ¤Fp±FÄæUwíŠcÆïô +[S¥#,4FÔ €h2¶ïY] gªp¢Ù"–9ÓÇØtÔvU U£Ê‚÷nÎ\”aŽíˆŸØyX=M uே$×)þ[gÁ„GÞëU¡œrGþ-kHþsçJ.ëºÁ$»;;KC}“l:厠›‡¾ïÓa¤nÈ=M¥Õào‡¤Õñsèâ´ʼ“SNìl ÿ SÅàlP–žg—'Èü“WÎìtž°)SÉ lXžw·' +>´œÙV.ƒ™t +Ø.‹™uŠ è.“™v + ø.›™wŠ!" ÷5wâÓyCg;œS»;iKKÞ£ý•6©ZÇaj¸²4¹±8 +é"0µ±øÚ˜&즸±(é8°Y±ØÊ˜" 4°Z±ʘ2 8pJ±˜.7pбҘ.¡7ÐBö-œ’™'ʘ´ífš± é7B±0¾˜¸œ’ºb±0Ƙ¸ž’:‚±0Θ¸ ’º¢±0֘¸¢’: ¼˜y À˜¹ Ęù Ș9 ̘y И91Ó"÷¦ø¶èœh¦•Y1íâøØü”×× ”Øü˜ ˜ûgÿ§ç' g§ç' [FԁíÍÔ'Ÿc†Ô‘íÑô'°{R¯}r¯’¯²¯ƒÒ¯…ò¯‡¯‰2¯‹R°r°’°‘²°“Ò°•ò°—°™2T›¼DqSœÀT±SÄdñSžÈt1SŸÌ„qT Ð”±ngüP»kGþ U´•1-Êwۈ§$.7÷ºùz:Ú. 4"ù®:Ô"Ò´nº1¹r6bÊÎ÷͚-‡ùv4âÊ/Îù=Mš5‡ú$v8ÂÂ+L™ýгEšh7HÊþ"+u7I +þ2+y7ÊJ3m9NJ3q9ÈÊ"3u9É +23y9:gD<»{;»kÖLýDZìøf<†J=MÂ/]8ƒŠ=MÒ/a8„Ê=Mâ/e8… +=Mò/i8†J/m8‡Š/q8ˆÊ"/u8‰ +2/y8 +J-Â7]:Š-Ò7a:Ê-â7e: +-ò7i:J.7m:Š.7ñ"ºú¨£z Ú.&7vz +.67zYÃJÀ3\ÙÃjÈ3^YĊÐ3= ÙĪØ3bYÅÊà3dÙÅêè3fYÆ +ð3hÙÆ*ø3jYÇJ3lÙÇj3nYȊ3pÙȪ3rYÉÊ 3tÙÉê(3vYÊ +03xÙÊ*83ºí—B@;»{K¿Å´î8–4¼QB!B.À4þ·¹lÓº‚-η= º¢-Ö·b:Â-Þ·dºâ-æ·f:-î·hº"-ö·j:B.þ·lºb.·.7.·/7/·070·1714)6?ü>}[;;;ëØ¨ñ¨ñ¨ñ¨ñ¨VxÜÈí5•&éï%U¦êò-¥¦éî9…ãݼqµcÞÁau#Þ½u•#Ý{„p‘Š ‚¸’†˜PeàՁ$àוÕ$ßڅ%¤àÖ]މz&Ήx'.‰y(® zďmaʟ=M_·ƒbƗM_Ô¥Á!Ô§ÕÔ!ÓªÅ$¡Ô¦ýߍcö÷͆fî×-†døç­†Ï3&ªø6=Mæª÷8êø4 6jìA'ìUÖ'ë +E&§ì½= ’«xҘ®X2˜¬h²˜ó¥V¨ò­¦¨ñ¹fèò©ÆÑ{@>§+{AH‡«ûB@¯ëû@<ÍãÃhÙÝÄdÉ3ÝÃiÑó]EpÇëþFtïkþDzÏ þErƒÞÉl1³^Êq!sÊm5“ÉSì¬'>Ç_”;»úõñ¨ê•Þ¥§ñôW¢ñ¨@"Ó¢ÿ$èP.Ï’ñZ¼4ôôDk r­Wkt’7"9ö= ¦î»÷nº™P>.mJ±Î}HQ?Û\ÂQWÃ{ŠDxÓ¾½GUë\¿_E#ûÀSI‹ü;MÙ'\ЕgD_ٗ•¬_M>à/Åj…m¤ÿыÍdßN’tƒ= ¾+j¤œ€KD ÁNŒSdÞJn7‹Ì€N±¿ÜàÃiÃKG\ָߎÊpü_͊v îÚ­5lZpožŠsuÅ$ؘWá՜»ôtÆ_T›ÅB-ÆF‚¸¸êsv5½n¢"’lëg|CölüœøœxœN™Ž® †i«Þøñʓx¢,í~Ñr”ôyÕ°ócøë[ú“͕׍̙Ò=MÕ é%’/ä¶o2ˆ€'?i4@¢€ÈSvÝö^Až>}Á>ÊA ?hõ©õš(­Þ"ð™²õy+ï(‘vñÆ1©Ê%5=M-E-f-Õðpdp6VŽ޵~ń˝‹xö¢y7Á»°ûC£=M|ÁKa۞á˜Â†×ÉÙRï·×ÜЄSÉz]”“usöõ¶³´6354B??A@@Æ7uÜ"Τ…!­ãã=}ҕ³öYÕ[ßPÑ[àXÙRgu{jC^ýÏ;[œó8d5ЛD3:9ä ÷ÐüWë?ç·iÉ»nEó>Ù»rF3>¼{k+R¾/OkDM¾<«¨*ªñ¨wñ¨ñ¨ñ¨6ñY¥wÒ_͇e$ŸÍˆi4O‡d ˆh0o=M‡f(¯=Mˆj8Gmc‡mgVîdÖî= –îhî^vîföîb¶îj6®[B®c®_‚®g®]b®eâ®a¢®i"®\R®dÒ®= ’®h®^r®fò®b²®j2.[J.cÊ._Š.g +.]j.eê.aª.i*.\Z.dÚ.= š.h.^z.fú.bº.j:n²é4îËg tf”•NƒkÜ¿N„oìÿ΃mäß΄qôŽƒlàώ„pðƒnèï„rø/nkÞÇno1‰õ҅AÌNôPt$ø§ස'wvþð7„&·n ’µRÓV¼F(Dšmõ‡£—§¡ilnuÒÕD–iÎdâi.E®ž’€émŽ1—hÎp!ÿÀ¦((uÔèrb)Gë²¼µE͎ë~Ȓp±ªÓØEò²L±m=Mف£÷É0”×*8aüªN8bºÎØ\Ëýo¢ávvþXÔ)­¢Éö®ËâÑ·pPåèƒÈ¥h®Ô0[Žæ/èò•x$ð| +ȵªÑ|ÆgŸ=Mi։T°傡¸í&ÒÛgŸu]lTʵ!íÚp¾ÈNn§ñkÞ襦n'ñr~~W‡²p³ŠîÙ)¦¢2"{êJ1ÜùvªÉ/Þ8"“:xTbu&Ÿ¢Ù%'n¬Ñö ¬"´)ù{HRæ p˜éñf³È•êŽ.H¥ú”Jò% Ixàò¶/³ú™Z.^µº‡AФÔwbÖRŸ/íç™ÐVí¶–Á*->Ô¹gŠ 7Àçrk‡I ì_œO9åBxÙsä¢nñ6íñ¨E¶¦¨ñ(™ñ¦¨È‰út7¶º½ªº¼æºBB »<?sQãK×SATÀûAÐ;ν—Q¥ 0› +=}tZ÷= [Õû¯û;³ÞêBb©<­¢«2>:ck~)—Cé<¬j +»·J×Z{ÒûDÑFW= ‹C‰w‡[ +M?òÐ0Ò@!‚|i¤q–#ˆg'ÞgÐQ.QŠFnvÿõ\÷,K9è\‰7g¿çIéXœޜ­:ÀDõ]Ã^]J€ËC ƒO¸…O.‰Ïì4q |qòtìaÁb‹îÓÞÊ1=  Òbëþ„9„÷JÍÃpŽÓBŽÇ臘ØÚÌÿFmg)@홼ôõ½…½ÂeL¦’œºÀwßUb”¾ùìAîhžzהÂ%è"ÿª%Ì"pKç\Œ= Ä´›Aô~¢± }:zF6Ž"¨xR sJ^!n1,·÷ÂQÒij/]ÙÆ¬r˜½zõOîǗ­½Ðf,fì–VÌøˆsæpÞr¦ü'Ó©þôÝX °Ï6²]n?>xã>µl0Ì6÷Z²)[µ\'ÁÌtBªXÀóµ?ŽS §,÷p+Ò萺¨Ÿ·˜ÞþjéÊÔ˗£ž½nq|G6ùÌV§Ä.‘#îòü/ª’º¶×²¨ìnb'Å7›r½æ1{XóR¡ŸZ­n2þ*ÊM«î@,C3l‹TÃü.ûªAܛ4‘KzW/€ÛÚÁ3ºë÷Ž[a@9&½Ö–h’”ãô\7v~ÿrH®®]½5[½¹w-§*K$?æ%uFóì_G2ÎÁÛÔ‡E×VkÓÔ(pŽsÔH‚Iby6œaÀÔHEnnnnî +ÕÕWš’¸0922hPå“c‚A3åWã܅›·G»ñm=}ñ¾D=}n “;˜¡HsQkBs>5#AÕɻҳ;®þBÓÇi»äKÛý¹BïÝb«”ûIǨÔG/ýgIQ;ÉY;'*•ÛRÇ>á³e‹fæ<â ¬CÓ¿®{vAñ7@L»Î…LX§ÄëLÀú@[©ŠZ¿!ß\®;ý‡t¥UKŽïUœTÂã ÒK6M¬²?¶Éû6>CõuÉýBMó轛’è]#¿^?^¹}‹6lĦu̓V|ÝL¾„wúÌßÙṙ >ÍñŒE“¤R¢Ô(Ãu³uŒD¢W¯ÔÄ'ºêÐ;eV;~Z»;€= »1A{{s½×ÂC%rN›ÌÛQý%†]ó|›KªÑüÌڅ+¤n½BâüޅH™T +hæNç¨1¾€œË7–sËVŽüê¦d¿õÉ>õÜI5ٖ>žWI¾ÌÂ+ÿGÞ®äY£=MƟO$uH€"pÜtŠc·Ázìø2£œ„áH´œØÑ/ó¡Ò/Ow¶ö‚Û‰FQ1'µ‹Ã6áÅÁ·RôOë<²­_¾µ°ý|”ß/öb=M†R©¡µg*5ыI6#ÜU²¸Û +@3Å[ЉûNhDîQFü*Ý?uJûØ6ë +A™§²Âœp˜[ô’s‹šFû .oCtï@ݘÚý‰# £=MÖ#ƒ‡wìîxýß½GÒÿDó|Ü}5jìÝNȰ5EÇ>ïvßøN’À‚ªfÈÆHøýˆ §åpª­( øç°ÓX–*)· ©T0œ¢Ç‚¹ÆÑB¿Ÿ¤iÅ5mtímhâ¡'»BNiÇ©¦=}{fLÐË+¡ùC}ØûÀÓ}qb³.x;ݎ<¸Ø<圢›‹ÆrEØø>¬C䇭‹ ·|ãKLQfäÕ6˜5av„TõÝýît›™Ð"¦6Ál¬¨6ñý>HG8¶  $  —=MøÄƒd»¥BGÁØÇ¼¹$=}¯ŸŠã)uYGz;žC}pd¿\|‘6(L=Mf_ï#ß«WÞ¥§fŸp@0N6‘ +Š'¤sªˆQø÷Õ¬Åq¾>.øü·¢D5³T…áhÅ8zDvO= ¯óÝyàÙåFÆÒ•çcËóŒz?Ê5cèŠ–úë.ÌðÐޛ~†O Ë|¿Ó˜Q§ v)BHXƒš6œøöDއ¥„˜ŠkÒÚa¡cÙ7—¦x©nº¡6¾}®­iÏ<ÎWhš‘2 9's"IɎ<»ïX\#F\Ë_4F[ eŠÚ ¸Šø¹2øH$žß9_xì9i 2µšã +ã/¸Äyf"Ÿ5TàyÊ0îEê²Þ—€m6ëÞè6ðÏâ§9ž¬„k=}¼àt<ž‚¦Mdžç6"p_î3, oO3p"pw÷´/(1ñøs.íA/&ÏUÃ_X™ÍpÕ1hÌ80E’Qö„ +ow÷!x.™AÜ"¢j+惡¦¶®ä“R–ow÷!xꔊpn…â#ðù +z AÍp\Ž0N˜HJê´ªØ^U +I€wu¸öôú!3å6³‹âVq£³¹´RN£%y10º:pŠ#\:J:b9Hµ7h_Ž:¢I9 _¼™µV,þ <`});var d={a:l};this.H=t=>{i.H(n,t)},this.M=()=>i.M(n),this.instantiate=()=>(this.M().then(i=>WebAssembly.instantiate(i,d)).then(i=>{const s=i.exports;var h;(i=>{o=i.i,a=i.j,c=i.k,I=i.l,u=i.m,y=i.o})(s),h=(H=s.g).buffer,e=new Uint8Array(h),new BigInt64Array(h),new BigUint64Array(h),(i=>{i.h()})(s),t()}),this.ready=new Promise(i=>{t=i}).then(()=>{this.S=H.buffer,this.A=a,this.free=u,this._=o,this.q=c,this.K=I}),this)}function M(i={}){M.W||Object.defineProperties(M,{W:{value:new Map([[-1,"OPUS_BAD_ARG: One or more invalid/out of range arguments"],[-2,"OPUS_BUFFER_TOO_SMALL: Not enough bytes allocated in the buffer"],[-3,"OPUS_INTERNAL_ERROR: An internal error was detected"],[-4,"OPUS_INVALID_PACKET: The compressed data passed is corrupted"],[-5,"OPUS_UNIMPLEMENTED: Invalid/unsupported request number"],[-6,"OPUS_INVALID_STATE: An encoder or decoder structure is invalid or already freed"],[-7,"OPUS_ALLOC_FAIL: Memory allocation has failed"]])}}),this.C=()=>new this.ii(this).instantiate(this.ti,this.ei).then(i=>{this.Hi=i,this.si=0,this.hi=0,this.ri=0,this.ni=this.Hi.v(this.Mi,Uint8Array),this.oi=this.Hi.v(this.ai*this.ci,Float32Array);const t=this.Hi.v(this.Ii,Uint8Array);t.O.set(this.ui),this.yi=this.Hi.I._(this.li,this.Ii,this.di,this.pi,t.N,this.mi,this.fi)}),Object.defineProperty(this,"ready",{enumerable:!0,get:()=>this.Ji}),this.reset=()=>(this.free(),this.C()),this.free=()=>{this.Hi.free(),this.Hi.I.K(this.yi),this.Hi.I.free(this.yi)},this.bi=i=>{if(!(i instanceof Uint8Array))throw Error("Data to decode must be Uint8Array. Instead got "+typeof i);this.ni.O.set(i);let t,e=this.Hi.I.q(this.yi,this.ni.N,i.length,this.oi.N);return e<0&&(t="libopus "+e+" "+(M.W.get(e)||"Unknown Error"),console.error(t),e=0),{outputBuffer:this.Hi.U(this.oi.O,this.ai,e),samplesDecoded:e,error:t}},this.decodeFrame=i=>{let t=[];const e=this.bi(i);return e.error&&this.Hi.V(t,e.error,i.length,this.ri,this.si,this.hi),this.ri++,this.si+=i.length,this.hi+=e.samplesDecoded,this.ii.T(t,[e.outputBuffer],this.ai,e.samplesDecoded,this.li)},this.decodeFrames=i=>{let t=[],e=[],H=0,s=0;for(;s"number"==typeof i,e=i.sampleRate,H=i.Wi,h=i.Ti,r=i.xi,o=i.gi,a=i.zi,c=i.Pi?1:0;if(H>2&&(!t(h)||!t(r)||!Array.isArray(o)))throw Error("Invalid Opus Decoder Options for multichannel decoding.");return this.li=[8e3,12e3,16e3,24e3,48e3].includes(e)?e:48e3,this.Ii=t(H)?H:2,this.di=t(h)?h:1,this.pi=t(r)?r:this.Ii-1,this.ui=o||(2===this.Ii?[0,1]:[0]),this.mi=a||0,this.fi=H<=8&&2!=H?c:0,this.Mi=3840*this.Ii,this.ci=5760,this.ai=this.fi?2:this.Ii,this.Ji=this.C(),this}class o extends h{constructor(i){super(i,"opus-decoder",M,n)}async decodeFrame(i){return this.Z("decodeFrame",i)}async decodeFrames(i){return this.Z("decodeFrames",i)}}r(M,"OpusDecoder"),r(o,"OpusDecoderWebWorker"),i.OpusDecoder=M,i.OpusDecoderWebWorker=o},"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@eshaz/web-worker")):"function"==typeof define&&define.amd?define(["exports","@eshaz/web-worker"],t):t((i="undefined"!=typeof globalThis?globalThis:i||self)["opus-decoder"]={},i.Worker); +//# sourceMappingURL=opus-decoder.min.js.map \ No newline at end of file diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs index f13b5d64..ec8a819f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs @@ -75,6 +75,13 @@ define_gz_cache!(gz_bandplan_json, status::BANDPLAN_JSON, "bandplan.json"); // Vendored DSEG14 Classic font // (binary woff2 — served directly, not through gz_cache) +// Vendored opus-decoder 0.7.11 +define_gz_cache!( + gz_opus_decoder_js, + status::OPUS_DECODER_JS, + "opus-decoder-0.7.11.min.js" +); + // Vendored Leaflet 1.9.4 define_gz_cache!(gz_leaflet_js, status::LEAFLET_JS, "leaflet.js"); define_gz_cache!(gz_leaflet_css, status::LEAFLET_CSS, "leaflet.css"); @@ -341,6 +348,12 @@ pub(crate) async fn dseg14_classic_woff2() -> impl Responder { .body(status::DSEG14_CLASSIC_WOFF2) } +#[get("/vendor/opus-decoder-0.7.11.min.js")] +pub(crate) async fn opus_decoder_js(req: HttpRequest) -> impl Responder { + let c = gz_opus_decoder_js(); + static_asset_response(&req, "application/javascript; charset=utf-8", c) +} + // --------------------------------------------------------------------------- // Vendored Leaflet 1.9.4 // --------------------------------------------------------------------------- diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs index 9ee2d992..81ce474b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs @@ -668,6 +668,8 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(assets::bandplan_json) // Vendored DSEG14 Classic font .service(assets::dseg14_classic_woff2) + // Vendored opus-decoder 0.7.11 + .service(assets::opus_decoder_js) // Vendored Leaflet 1.9.4 .service(assets::leaflet_js) .service(assets::leaflet_css) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs index b6b8d06e..23d1b8f7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs @@ -40,6 +40,9 @@ pub const BANDPLAN_JSON: &str = include_str!("../assets/web/bandplan.json"); pub const DSEG14_CLASSIC_WOFF2: &[u8] = include_bytes!("../assets/web/vendor/dseg14-classic-latin-400-normal.woff2"); +// Vendored opus-decoder 0.7.11 browser build (WebAssembly embedded in JS) +pub const OPUS_DECODER_JS: &str = include_str!("../assets/web/vendor/opus-decoder-0.7.11.min.js"); + // Vendored Leaflet 1.9.4 pub const LEAFLET_JS: &str = include_str!("../assets/web/vendor/leaflet.js"); pub const LEAFLET_CSS: &str = include_str!("../assets/web/vendor/leaflet.css"); diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 082549a1..6f191fe7 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -8,11 +8,10 @@ use std::collections::HashMap; use std::collections::{HashSet, VecDeque}; use std::net::SocketAddr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use base64::Engine as _; use bytes::Bytes; use flate2::write::GzEncoder; use flate2::Compression; @@ -36,7 +35,7 @@ use trx_core::audio::{ }; use trx_core::decode::{ AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, LrptImage, LrptProgress, - VdesMessage, WefaxMessage, WsprMessage, + WsprMessage, }; use trx_core::rig::state::{RigMode, RigState}; use trx_core::vchan::SharedVChanManager; @@ -47,21 +46,11 @@ use trx_wspr::WsprDecoder; use uuid::Uuid; use crate::config::AudioConfig; +use crate::history_policy::{ + current_timestamp_ms, enforce_capacity, lock_or_recover, prune_by_age, MAX_HISTORY_ENTRIES, +}; use trx_decode_log::DecoderLoggers; -const APRS_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const HF_APRS_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const AIS_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const VDES_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const CW_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const FT8_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const WSPR_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const LRPT_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const WEFAX_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -/// Maximum entries per decoder history queue. Prevents unbounded memory growth -/// on busy channels (e.g. AIS near a port). Oldest entries are evicted when -/// the limit is reached, independent of the time-based pruning. -const MAX_HISTORY_ENTRIES: usize = 10_000; /// Silence timeout before auto-finalising an LRPT pass (30 s without new MCUs). const LRPT_PASS_SILENCE_TIMEOUT: Duration = Duration::from_secs(30); const FT8_SAMPLE_RATE: u32 = 12_000; @@ -75,13 +64,6 @@ const DECODE_AUDIO_GATE_RMS: f32 = 2.5e-4; const AUDIO_STREAM_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(60); const AUDIO_STREAM_RECOVERY_DELAY: Duration = Duration::from_secs(1); -fn current_timestamp_ms() -> i64 { - match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { - Ok(dur) => dur.as_millis() as i64, - Err(_) => 0, - } -} - #[cfg(feature = "ft2")] fn retain_ft2_window(buf: &mut Vec) { if buf.len() > FT2_ASYNC_BUFFER_SAMPLES { @@ -360,509 +342,7 @@ fn classify_stream_error(err: &str) -> &'static str { } } -/// Per-rig decoder history store. -/// -/// Replaces the previous process-wide `OnceLock` statics so that each rig -/// instance can maintain its own independent history. Pass an -/// `Arc` into every decoder task and into the audio listener. -pub struct DecoderHistories { - pub ais: Mutex>, - pub vdes: Mutex>, - pub aprs: Mutex>, - pub hf_aprs: Mutex>, - pub cw: Mutex>, - pub ft8: Mutex>, - pub ft4: Mutex>, - pub ft2: Mutex>, - pub wspr: Mutex>, - pub lrpt: Mutex>, - pub wefax: Mutex>, - /// Approximate total entry count across all decoders, maintained - /// atomically so `estimated_total_count()` avoids 11 lock acquisitions. - total_count: AtomicUsize, -} - -/// Acquire a mutex, recovering from poisoning with a warning log. -fn lock_or_recover<'a, T>(mutex: &'a Mutex, label: &str) -> std::sync::MutexGuard<'a, T> { - mutex.lock().unwrap_or_else(|e| { - tracing::warn!( - "Mutex for {} was poisoned (prior panic); recovering with potentially inconsistent data", - label - ); - e.into_inner() - }) -} - -/// Enforce capacity limit on a history deque by evicting oldest entries. -fn enforce_capacity(deque: &mut VecDeque, max: usize) { - while deque.len() > max { - deque.pop_front(); - } -} - -/// Drop entries older than `retention` from the front of a time-tagged deque. -/// Uses `checked_sub` so an early `now` (before `retention` elapses since the -/// monotonic clock origin) is treated as "no entries are old enough to prune" -/// rather than panicking. -fn prune_by_age(deque: &mut VecDeque<(Instant, T)>, retention: Duration, now: Instant) { - let Some(cutoff) = now.checked_sub(retention) else { - return; - }; - while let Some((ts, _)) = deque.front() { - if *ts < cutoff { - deque.pop_front(); - } else { - break; - } - } -} - -impl DecoderHistories { - pub fn new() -> Arc { - Arc::new(Self { - ais: Mutex::new(VecDeque::new()), - vdes: Mutex::new(VecDeque::new()), - aprs: Mutex::new(VecDeque::new()), - hf_aprs: Mutex::new(VecDeque::new()), - cw: Mutex::new(VecDeque::new()), - ft8: Mutex::new(VecDeque::new()), - ft4: Mutex::new(VecDeque::new()), - ft2: Mutex::new(VecDeque::new()), - wspr: Mutex::new(VecDeque::new()), - lrpt: Mutex::new(VecDeque::new()), - wefax: Mutex::new(VecDeque::new()), - total_count: AtomicUsize::new(0), - }) - } - - /// Adjust the atomic total count after a record/prune/clear operation. - /// - /// Uses a CAS loop for decrements to prevent underflow wrapping the - /// counter to `usize::MAX` (which would cause a capacity-overflow panic - /// when pre-allocating the history replay blob). - fn adjust_total_count(&self, old_len: usize, new_len: usize) { - if new_len > old_len { - self.total_count - .fetch_add(new_len - old_len, Ordering::Relaxed); - } else if old_len > new_len { - let delta = old_len - new_len; - let mut current = self.total_count.load(Ordering::Relaxed); - loop { - let next = current.saturating_sub(delta); - match self.total_count.compare_exchange_weak( - current, - next, - Ordering::Relaxed, - Ordering::Relaxed, - ) { - Ok(_) => break, - Err(actual) => current = actual, - } - } - } - } - - // --- AIS --- - - fn prune_ais(history: &mut VecDeque<(Instant, AisMessage)>) { - prune_by_age(history, AIS_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_ais_message(&self, mut msg: AisMessage) { - if msg.ts_ms.is_none() { - msg.ts_ms = Some(current_timestamp_ms()); - } - let mut h = lock_or_recover(&self.ais, "ais_history"); - let before = h.len(); - h.push_back((Instant::now(), msg)); - Self::prune_ais(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_ais_history(&self) -> Vec { - let mut h = lock_or_recover(&self.ais, "ais_history"); - let before = h.len(); - Self::prune_ais(&mut h); - self.adjust_total_count(before, h.len()); - h.iter().map(|(_, msg)| msg.clone()).collect() - } - - // --- VDES --- - - fn prune_vdes(history: &mut VecDeque<(Instant, VdesMessage)>) { - prune_by_age(history, VDES_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_vdes_message(&self, mut msg: VdesMessage) { - if msg.ts_ms.is_none() { - msg.ts_ms = Some(current_timestamp_ms()); - } - let mut h = lock_or_recover(&self.vdes, "vdes_history"); - let before = h.len(); - h.push_back((Instant::now(), msg)); - Self::prune_vdes(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_vdes_history(&self) -> Vec { - let mut h = lock_or_recover(&self.vdes, "vdes_history"); - let before = h.len(); - Self::prune_vdes(&mut h); - self.adjust_total_count(before, h.len()); - h.iter().map(|(_, msg)| msg.clone()).collect() - } - - // --- APRS --- - - fn prune_aprs(history: &mut VecDeque<(Instant, AprsPacket)>) { - prune_by_age(history, APRS_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_aprs_packet(&self, mut pkt: AprsPacket) { - if !pkt.crc_ok { - return; - } - if pkt.ts_ms.is_none() { - pkt.ts_ms = Some(current_timestamp_ms()); - } - let mut h = lock_or_recover(&self.aprs, "aprs_history"); - let before = h.len(); - h.push_back((Instant::now(), pkt)); - Self::prune_aprs(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_aprs_history(&self) -> Vec { - let mut h = lock_or_recover(&self.aprs, "aprs_history"); - let before = h.len(); - Self::prune_aprs(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, pkt): &(Instant, AprsPacket)| pkt.clone()) - .collect() - } - - pub fn clear_aprs_history(&self) { - let mut h = lock_or_recover(&self.aprs, "aprs_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - // --- HF APRS --- - - fn prune_hf_aprs(history: &mut VecDeque<(Instant, AprsPacket)>) { - prune_by_age(history, HF_APRS_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_hf_aprs_packet(&self, mut pkt: AprsPacket) { - if !pkt.crc_ok { - return; - } - if pkt.ts_ms.is_none() { - pkt.ts_ms = Some(current_timestamp_ms()); - } - let mut h = lock_or_recover(&self.hf_aprs, "hf_aprs_history"); - let before = h.len(); - h.push_back((Instant::now(), pkt)); - Self::prune_hf_aprs(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_hf_aprs_history(&self) -> Vec { - let mut h = lock_or_recover(&self.hf_aprs, "hf_aprs_history"); - let before = h.len(); - Self::prune_hf_aprs(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, pkt): &(Instant, AprsPacket)| pkt.clone()) - .collect() - } - - pub fn clear_hf_aprs_history(&self) { - let mut h = lock_or_recover(&self.hf_aprs, "hf_aprs_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - // --- CW --- - - fn prune_cw(history: &mut VecDeque<(Instant, CwEvent)>) { - prune_by_age(history, CW_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_cw_event(&self, evt: CwEvent) { - let mut h = lock_or_recover(&self.cw, "cw_history"); - let before = h.len(); - h.push_back((Instant::now(), evt)); - Self::prune_cw(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_cw_history(&self) -> Vec { - let mut h = lock_or_recover(&self.cw, "cw_history"); - let before = h.len(); - Self::prune_cw(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, evt): &(Instant, CwEvent)| evt.clone()) - .collect() - } - - pub fn clear_cw_history(&self) { - let mut h = lock_or_recover(&self.cw, "cw_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - // --- FT8 --- - - fn prune_ft8(history: &mut VecDeque<(Instant, Ft8Message)>) { - prune_by_age(history, FT8_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_ft8_message(&self, msg: Ft8Message) { - let mut h = lock_or_recover(&self.ft8, "ft8_history"); - let before = h.len(); - h.push_back((Instant::now(), msg)); - Self::prune_ft8(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_ft8_history(&self) -> Vec { - let mut h = lock_or_recover(&self.ft8, "ft8_history"); - let before = h.len(); - Self::prune_ft8(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, msg): &(Instant, Ft8Message)| msg.clone()) - .collect() - } - - pub fn clear_ft8_history(&self) { - let mut h = lock_or_recover(&self.ft8, "ft8_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - // --- FT4 --- - - fn prune_ft4(history: &mut VecDeque<(Instant, Ft8Message)>) { - prune_by_age(history, FT8_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_ft4_message(&self, msg: Ft8Message) { - let mut h = lock_or_recover(&self.ft4, "ft4_history"); - let before = h.len(); - h.push_back((Instant::now(), msg)); - Self::prune_ft4(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_ft4_history(&self) -> Vec { - let mut h = lock_or_recover(&self.ft4, "ft4_history"); - let before = h.len(); - Self::prune_ft4(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, msg): &(Instant, Ft8Message)| msg.clone()) - .collect() - } - - pub fn clear_ft4_history(&self) { - let mut h = lock_or_recover(&self.ft4, "ft4_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - // --- FT2 --- - - #[cfg_attr(not(feature = "ft2"), allow(dead_code))] - fn prune_ft2(history: &mut VecDeque<(Instant, Ft8Message)>) { - prune_by_age(history, FT8_HISTORY_RETENTION, Instant::now()); - } - - #[cfg_attr(not(feature = "ft2"), allow(dead_code))] - pub fn record_ft2_message(&self, msg: Ft8Message) { - let mut h = lock_or_recover(&self.ft2, "ft2_history"); - let before = h.len(); - h.push_back((Instant::now(), msg)); - Self::prune_ft2(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - #[cfg_attr(not(feature = "ft2"), allow(dead_code))] - pub fn snapshot_ft2_history(&self) -> Vec { - let mut h = lock_or_recover(&self.ft2, "ft2_history"); - let before = h.len(); - Self::prune_ft2(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, msg): &(Instant, Ft8Message)| msg.clone()) - .collect() - } - - pub fn clear_ft2_history(&self) { - let mut h = lock_or_recover(&self.ft2, "ft2_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - // --- WSPR --- - - fn prune_wspr(history: &mut VecDeque<(Instant, WsprMessage)>) { - prune_by_age(history, WSPR_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_wspr_message(&self, msg: WsprMessage) { - let mut h = lock_or_recover(&self.wspr, "wspr_history"); - let before = h.len(); - h.push_back((Instant::now(), msg)); - Self::prune_wspr(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_wspr_history(&self) -> Vec { - let mut h = lock_or_recover(&self.wspr, "wspr_history"); - let before = h.len(); - Self::prune_wspr(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, msg): &(Instant, WsprMessage)| msg.clone()) - .collect() - } - - pub fn clear_wspr_history(&self) { - let mut h = lock_or_recover(&self.wspr, "wspr_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - // --- LRPT --- - - fn prune_lrpt(history: &mut VecDeque<(Instant, LrptImage)>) { - prune_by_age(history, LRPT_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_lrpt_image(&self, mut img: LrptImage) { - if img.ts_ms.is_none() { - img.ts_ms = Some(current_timestamp_ms()); - } - let mut h = lock_or_recover(&self.lrpt, "lrpt_history"); - let before = h.len(); - h.push_back((Instant::now(), img)); - Self::prune_lrpt(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_lrpt_history(&self) -> Vec { - let mut h = lock_or_recover(&self.lrpt, "lrpt_history"); - let before = h.len(); - Self::prune_lrpt(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, img): &(Instant, LrptImage)| img.clone()) - .collect() - } - - pub fn clear_lrpt_history(&self) { - let mut h = lock_or_recover(&self.lrpt, "lrpt_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - // --- WEFAX --- - - fn prune_wefax(history: &mut VecDeque<(Instant, WefaxMessage)>) { - prune_by_age(history, WEFAX_HISTORY_RETENTION, Instant::now()); - } - - pub fn record_wefax_message(&self, mut msg: WefaxMessage) { - if msg.ts_ms.is_none() { - msg.ts_ms = Some(current_timestamp_ms()); - } - // Strip bulk PNG data before storing in memory/persistence. - msg.png_data = None; - let mut h = lock_or_recover(&self.wefax, "wefax_history"); - let before = h.len(); - h.push_back((Instant::now(), msg)); - Self::prune_wefax(&mut h); - enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); - self.adjust_total_count(before, h.len()); - } - - pub fn snapshot_wefax_history(&self) -> Vec { - let mut h = lock_or_recover(&self.wefax, "wefax_history"); - let before = h.len(); - Self::prune_wefax(&mut h); - self.adjust_total_count(before, h.len()); - h.iter() - .map(|(_, msg)| { - let mut m = msg.clone(); - // Re-read PNG from disk so remote clients can save a local copy. - if m.png_data.is_none() { - if let Some(ref path) = m.path { - if let Ok(bytes) = std::fs::read(path) { - m.png_data = - Some(base64::engine::general_purpose::STANDARD.encode(&bytes)); - } - } - } - m - }) - .collect() - } - - pub fn clear_wefax_history(&self) { - let mut h = lock_or_recover(&self.wefax, "wefax_history"); - let before = h.len(); - h.clear(); - self.adjust_total_count(before, 0); - } - - /// Returns a quick (non-pruning) estimate of the total number of history - /// entries across all decoders, used for pre-allocating the replay blob. - /// - /// Uses an `AtomicUsize` counter maintained by record/prune/clear methods, - /// avoiding 9 separate mutex acquisitions. - pub fn estimated_total_count(&self) -> usize { - self.total_count.load(Ordering::Relaxed) - } - - /// Rebuild the aggregate count after bulk restoration bypasses the normal - /// record methods. - pub(crate) fn recalculate_total_count(&self) { - let total = lock_or_recover(&self.ais, "ais_history").len() - + lock_or_recover(&self.vdes, "vdes_history").len() - + lock_or_recover(&self.aprs, "aprs_history").len() - + lock_or_recover(&self.hf_aprs, "hf_aprs_history").len() - + lock_or_recover(&self.cw, "cw_history").len() - + lock_or_recover(&self.ft8, "ft8_history").len() - + lock_or_recover(&self.ft4, "ft4_history").len() - + lock_or_recover(&self.ft2, "ft2_history").len() - + lock_or_recover(&self.wspr, "wspr_history").len() - + lock_or_recover(&self.lrpt, "lrpt_history").len() - + lock_or_recover(&self.wefax, "wefax_history").len(); - self.total_count.store(total, Ordering::Relaxed); - } -} +pub use crate::decoder_history::DecoderHistories; /// Spawn the audio capture thread. /// diff --git a/src/trx-server/src/decoder_history.rs b/src/trx-server/src/decoder_history.rs new file mode 100644 index 00000000..c406f66a --- /dev/null +++ b/src/trx-server/src/decoder_history.rs @@ -0,0 +1,489 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +//! Per-rig storage and lifecycle operations for decoded-message histories. + +use std::collections::VecDeque; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use base64::Engine as _; +use trx_core::decode::{ + AisMessage, AprsPacket, CwEvent, Ft8Message, LrptImage, VdesMessage, WefaxMessage, WsprMessage, +}; + +use crate::history_policy::{ + current_timestamp_ms, enforce_capacity, lock_or_recover, prune_by_age, HISTORY_RETENTION, + MAX_HISTORY_ENTRIES, +}; + +/// Per-rig decoder history store. +/// +/// Replaces the previous process-wide `OnceLock` statics so that each rig +/// instance can maintain its own independent history. Pass an +/// `Arc` into every decoder task and into the audio listener. +pub struct DecoderHistories { + pub ais: Mutex>, + pub vdes: Mutex>, + pub aprs: Mutex>, + pub hf_aprs: Mutex>, + pub cw: Mutex>, + pub ft8: Mutex>, + pub ft4: Mutex>, + pub ft2: Mutex>, + pub wspr: Mutex>, + pub lrpt: Mutex>, + pub wefax: Mutex>, + /// Approximate total entry count across all decoders, maintained + /// atomically so `estimated_total_count()` avoids 11 lock acquisitions. + total_count: AtomicUsize, +} + +impl DecoderHistories { + pub fn new() -> Arc { + Arc::new(Self { + ais: Mutex::new(VecDeque::new()), + vdes: Mutex::new(VecDeque::new()), + aprs: Mutex::new(VecDeque::new()), + hf_aprs: Mutex::new(VecDeque::new()), + cw: Mutex::new(VecDeque::new()), + ft8: Mutex::new(VecDeque::new()), + ft4: Mutex::new(VecDeque::new()), + ft2: Mutex::new(VecDeque::new()), + wspr: Mutex::new(VecDeque::new()), + lrpt: Mutex::new(VecDeque::new()), + wefax: Mutex::new(VecDeque::new()), + total_count: AtomicUsize::new(0), + }) + } + + /// Adjust the atomic total count after a record/prune/clear operation. + /// + /// Uses a CAS loop for decrements to prevent underflow wrapping the + /// counter to `usize::MAX` (which would cause a capacity-overflow panic + /// when pre-allocating the history replay blob). + pub(crate) fn adjust_total_count(&self, old_len: usize, new_len: usize) { + if new_len > old_len { + self.total_count + .fetch_add(new_len - old_len, Ordering::Relaxed); + } else if old_len > new_len { + let delta = old_len - new_len; + let mut current = self.total_count.load(Ordering::Relaxed); + loop { + let next = current.saturating_sub(delta); + match self.total_count.compare_exchange_weak( + current, + next, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(actual) => current = actual, + } + } + } + } + + // --- AIS --- + + fn prune_ais(history: &mut VecDeque<(Instant, AisMessage)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_ais_message(&self, mut msg: AisMessage) { + if msg.ts_ms.is_none() { + msg.ts_ms = Some(current_timestamp_ms()); + } + let mut h = lock_or_recover(&self.ais, "ais_history"); + let before = h.len(); + h.push_back((Instant::now(), msg)); + Self::prune_ais(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_ais_history(&self) -> Vec { + let mut h = lock_or_recover(&self.ais, "ais_history"); + let before = h.len(); + Self::prune_ais(&mut h); + self.adjust_total_count(before, h.len()); + h.iter().map(|(_, msg)| msg.clone()).collect() + } + + // --- VDES --- + + fn prune_vdes(history: &mut VecDeque<(Instant, VdesMessage)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_vdes_message(&self, mut msg: VdesMessage) { + if msg.ts_ms.is_none() { + msg.ts_ms = Some(current_timestamp_ms()); + } + let mut h = lock_or_recover(&self.vdes, "vdes_history"); + let before = h.len(); + h.push_back((Instant::now(), msg)); + Self::prune_vdes(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_vdes_history(&self) -> Vec { + let mut h = lock_or_recover(&self.vdes, "vdes_history"); + let before = h.len(); + Self::prune_vdes(&mut h); + self.adjust_total_count(before, h.len()); + h.iter().map(|(_, msg)| msg.clone()).collect() + } + + // --- APRS --- + + fn prune_aprs(history: &mut VecDeque<(Instant, AprsPacket)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_aprs_packet(&self, mut pkt: AprsPacket) { + if !pkt.crc_ok { + return; + } + if pkt.ts_ms.is_none() { + pkt.ts_ms = Some(current_timestamp_ms()); + } + let mut h = lock_or_recover(&self.aprs, "aprs_history"); + let before = h.len(); + h.push_back((Instant::now(), pkt)); + Self::prune_aprs(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_aprs_history(&self) -> Vec { + let mut h = lock_or_recover(&self.aprs, "aprs_history"); + let before = h.len(); + Self::prune_aprs(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, pkt): &(Instant, AprsPacket)| pkt.clone()) + .collect() + } + + pub fn clear_aprs_history(&self) { + let mut h = lock_or_recover(&self.aprs, "aprs_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + // --- HF APRS --- + + fn prune_hf_aprs(history: &mut VecDeque<(Instant, AprsPacket)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_hf_aprs_packet(&self, mut pkt: AprsPacket) { + if !pkt.crc_ok { + return; + } + if pkt.ts_ms.is_none() { + pkt.ts_ms = Some(current_timestamp_ms()); + } + let mut h = lock_or_recover(&self.hf_aprs, "hf_aprs_history"); + let before = h.len(); + h.push_back((Instant::now(), pkt)); + Self::prune_hf_aprs(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_hf_aprs_history(&self) -> Vec { + let mut h = lock_or_recover(&self.hf_aprs, "hf_aprs_history"); + let before = h.len(); + Self::prune_hf_aprs(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, pkt): &(Instant, AprsPacket)| pkt.clone()) + .collect() + } + + pub fn clear_hf_aprs_history(&self) { + let mut h = lock_or_recover(&self.hf_aprs, "hf_aprs_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + // --- CW --- + + fn prune_cw(history: &mut VecDeque<(Instant, CwEvent)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_cw_event(&self, evt: CwEvent) { + let mut h = lock_or_recover(&self.cw, "cw_history"); + let before = h.len(); + h.push_back((Instant::now(), evt)); + Self::prune_cw(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_cw_history(&self) -> Vec { + let mut h = lock_or_recover(&self.cw, "cw_history"); + let before = h.len(); + Self::prune_cw(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, evt): &(Instant, CwEvent)| evt.clone()) + .collect() + } + + pub fn clear_cw_history(&self) { + let mut h = lock_or_recover(&self.cw, "cw_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + // --- FT8 --- + + fn prune_ft8(history: &mut VecDeque<(Instant, Ft8Message)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_ft8_message(&self, msg: Ft8Message) { + let mut h = lock_or_recover(&self.ft8, "ft8_history"); + let before = h.len(); + h.push_back((Instant::now(), msg)); + Self::prune_ft8(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_ft8_history(&self) -> Vec { + let mut h = lock_or_recover(&self.ft8, "ft8_history"); + let before = h.len(); + Self::prune_ft8(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, msg): &(Instant, Ft8Message)| msg.clone()) + .collect() + } + + pub fn clear_ft8_history(&self) { + let mut h = lock_or_recover(&self.ft8, "ft8_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + // --- FT4 --- + + fn prune_ft4(history: &mut VecDeque<(Instant, Ft8Message)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_ft4_message(&self, msg: Ft8Message) { + let mut h = lock_or_recover(&self.ft4, "ft4_history"); + let before = h.len(); + h.push_back((Instant::now(), msg)); + Self::prune_ft4(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_ft4_history(&self) -> Vec { + let mut h = lock_or_recover(&self.ft4, "ft4_history"); + let before = h.len(); + Self::prune_ft4(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, msg): &(Instant, Ft8Message)| msg.clone()) + .collect() + } + + pub fn clear_ft4_history(&self) { + let mut h = lock_or_recover(&self.ft4, "ft4_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + // --- FT2 --- + + #[cfg_attr(not(feature = "ft2"), allow(dead_code))] + fn prune_ft2(history: &mut VecDeque<(Instant, Ft8Message)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + #[cfg_attr(not(feature = "ft2"), allow(dead_code))] + pub fn record_ft2_message(&self, msg: Ft8Message) { + let mut h = lock_or_recover(&self.ft2, "ft2_history"); + let before = h.len(); + h.push_back((Instant::now(), msg)); + Self::prune_ft2(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + #[cfg_attr(not(feature = "ft2"), allow(dead_code))] + pub fn snapshot_ft2_history(&self) -> Vec { + let mut h = lock_or_recover(&self.ft2, "ft2_history"); + let before = h.len(); + Self::prune_ft2(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, msg): &(Instant, Ft8Message)| msg.clone()) + .collect() + } + + pub fn clear_ft2_history(&self) { + let mut h = lock_or_recover(&self.ft2, "ft2_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + // --- WSPR --- + + fn prune_wspr(history: &mut VecDeque<(Instant, WsprMessage)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_wspr_message(&self, msg: WsprMessage) { + let mut h = lock_or_recover(&self.wspr, "wspr_history"); + let before = h.len(); + h.push_back((Instant::now(), msg)); + Self::prune_wspr(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_wspr_history(&self) -> Vec { + let mut h = lock_or_recover(&self.wspr, "wspr_history"); + let before = h.len(); + Self::prune_wspr(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, msg): &(Instant, WsprMessage)| msg.clone()) + .collect() + } + + pub fn clear_wspr_history(&self) { + let mut h = lock_or_recover(&self.wspr, "wspr_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + // --- LRPT --- + + fn prune_lrpt(history: &mut VecDeque<(Instant, LrptImage)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_lrpt_image(&self, mut img: LrptImage) { + if img.ts_ms.is_none() { + img.ts_ms = Some(current_timestamp_ms()); + } + let mut h = lock_or_recover(&self.lrpt, "lrpt_history"); + let before = h.len(); + h.push_back((Instant::now(), img)); + Self::prune_lrpt(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_lrpt_history(&self) -> Vec { + let mut h = lock_or_recover(&self.lrpt, "lrpt_history"); + let before = h.len(); + Self::prune_lrpt(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, img): &(Instant, LrptImage)| img.clone()) + .collect() + } + + pub fn clear_lrpt_history(&self) { + let mut h = lock_or_recover(&self.lrpt, "lrpt_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + // --- WEFAX --- + + fn prune_wefax(history: &mut VecDeque<(Instant, WefaxMessage)>) { + prune_by_age(history, HISTORY_RETENTION, Instant::now()); + } + + pub fn record_wefax_message(&self, mut msg: WefaxMessage) { + if msg.ts_ms.is_none() { + msg.ts_ms = Some(current_timestamp_ms()); + } + // Strip bulk PNG data before storing in memory/persistence. + msg.png_data = None; + let mut h = lock_or_recover(&self.wefax, "wefax_history"); + let before = h.len(); + h.push_back((Instant::now(), msg)); + Self::prune_wefax(&mut h); + enforce_capacity(&mut h, MAX_HISTORY_ENTRIES); + self.adjust_total_count(before, h.len()); + } + + pub fn snapshot_wefax_history(&self) -> Vec { + let mut h = lock_or_recover(&self.wefax, "wefax_history"); + let before = h.len(); + Self::prune_wefax(&mut h); + self.adjust_total_count(before, h.len()); + h.iter() + .map(|(_, msg)| { + let mut m = msg.clone(); + // Re-read PNG from disk so remote clients can save a local copy. + if m.png_data.is_none() { + if let Some(ref path) = m.path { + if let Ok(bytes) = std::fs::read(path) { + m.png_data = + Some(base64::engine::general_purpose::STANDARD.encode(&bytes)); + } + } + } + m + }) + .collect() + } + + pub fn clear_wefax_history(&self) { + let mut h = lock_or_recover(&self.wefax, "wefax_history"); + let before = h.len(); + h.clear(); + self.adjust_total_count(before, 0); + } + + /// Returns a quick (non-pruning) estimate of the total number of history + /// entries across all decoders, used for pre-allocating the replay blob. + /// + /// Uses an `AtomicUsize` counter maintained by record/prune/clear methods, + /// avoiding 11 separate mutex acquisitions. + pub fn estimated_total_count(&self) -> usize { + self.total_count.load(Ordering::Relaxed) + } + + /// Rebuild the aggregate count after bulk restoration bypasses the normal + /// record methods. + pub(crate) fn recalculate_total_count(&self) { + let total = lock_or_recover(&self.ais, "ais_history").len() + + lock_or_recover(&self.vdes, "vdes_history").len() + + lock_or_recover(&self.aprs, "aprs_history").len() + + lock_or_recover(&self.hf_aprs, "hf_aprs_history").len() + + lock_or_recover(&self.cw, "cw_history").len() + + lock_or_recover(&self.ft8, "ft8_history").len() + + lock_or_recover(&self.ft4, "ft4_history").len() + + lock_or_recover(&self.ft2, "ft2_history").len() + + lock_or_recover(&self.wspr, "wspr_history").len() + + lock_or_recover(&self.lrpt, "lrpt_history").len() + + lock_or_recover(&self.wefax, "wefax_history").len(); + self.total_count.store(total, Ordering::Relaxed); + } +} diff --git a/src/trx-server/src/history_policy.rs b/src/trx-server/src/history_policy.rs new file mode 100644 index 00000000..5186fdd7 --- /dev/null +++ b/src/trx-server/src/history_policy.rs @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +//! Shared retention and synchronization policy for decoder histories. + +use std::collections::VecDeque; +use std::sync::{Mutex, MutexGuard}; +use std::time::{Duration, Instant}; + +pub(crate) const HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); + +/// Maximum entries per decoder history queue. Oldest entries are evicted on +/// busy channels independently of time-based pruning. +pub(crate) const MAX_HISTORY_ENTRIES: usize = 10_000; + +pub(crate) fn current_timestamp_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_millis() as i64) + .unwrap_or(0) +} + +pub(crate) fn lock_or_recover<'a, T>(mutex: &'a Mutex, label: &str) -> MutexGuard<'a, T> { + mutex.lock().unwrap_or_else(|error| { + tracing::warn!( + "Mutex for {} was poisoned (prior panic); recovering with potentially inconsistent data", + label + ); + error.into_inner() + }) +} + +pub(crate) fn enforce_capacity(deque: &mut VecDeque, max: usize) { + while deque.len() > max { + deque.pop_front(); + } +} + +/// Drop entries older than `retention` from an ordered history queue. +pub(crate) fn prune_by_age( + deque: &mut VecDeque<(Instant, T)>, + retention: Duration, + now: Instant, +) { + let Some(cutoff) = now.checked_sub(retention) else { + return; + }; + while let Some((timestamp, _)) = deque.front() { + if *timestamp < cutoff { + deque.pop_front(); + } else { + break; + } + } +} diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index d3430b77..3501e3e1 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -4,7 +4,9 @@ mod audio; mod config; +mod decoder_history; mod error; +mod history_policy; mod history_store; mod listener; mod rig_handle;