From ab33759bd298a9efed2d12fd42ed654c25f959ed Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 13 Dec 2024 12:49:26 +0100
Subject: [PATCH 1/3] better organized export page
---
rowers/models.py | 70 ++++++++
rowers/templates/rower_exportsettings.html | 199 ++++++++++++++-------
rowers/views/statements.py | 8 +-
rowers/views/userviews.py | 28 ++-
4 files changed, 240 insertions(+), 65 deletions(-)
diff --git a/rowers/models.py b/rowers/models.py
index f4715081..3ac826ff 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -4561,6 +4561,76 @@ class RowerExportForm(ModelForm):
'intervals_auto_import',
'intervals_auto_export',
]
+
+class RowerExportFormStrava(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'stravaexportas',
+ 'strava_auto_export',
+ 'strava_auto_import',
+ 'strava_auto_delete',
+ ]
+
+class RowerExportFormIntervals(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'intervals_auto_import',
+ 'intervals_auto_export',
+ ]
+
+class RowerExportFormGarmin(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'garminactivity',
+ ]
+
+class RowerExportFormPolar(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'polar_auto_import',
+ ]
+
+class RowerExportFormConcept2(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'c2_auto_export',
+ 'c2_auto_import',
+ ]
+
+class RowerExportFormSportTracks(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'sporttracks_auto_export',
+ ]
+
+class RowerExportFormTrainingPeaks(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'trainingpeaks_auto_export',
+ 'rp3_auto_import',
+ ]
+
+class RowerExportFormRP3(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'rp3_auto_import',
+ ]
+
+class RowerExportFormNK(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'nk_auto_import'
+ ]
+
# Simple form to set rower's Functional Threshold Power
class SimpleRowerPowerForm(ModelForm):
diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html
index b6aae035..9625a472 100644
--- a/rowers/templates/rower_exportsettings.html
+++ b/rowers/templates/rower_exportsettings.html
@@ -7,8 +7,10 @@
{% block main %}
Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}
+
-{% if rower.garmintoken and rower.garmintoken != '' %}
-
- You are connected to Garmin. Switching off Garmin Connect sync is on the
- Account settings
- page. Look for the "Rowsandall" app.
-
-{% endif %}
-
- Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
- automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
- want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
- to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
- you may have to set your activity to Run or Ride while rowing.
-
-
- Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
- a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
- check box.
-
+
+ Click on the icons to establish the connection or to renew the authorization.
+
-
+
+ API Key
+ {{ apikey }}
+
+ Regenerate
+
+ This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
+
+
+ {% if form.errors %}
+
+
+ Please correct the error{{ form.errors|pluralize }} below.
+
+
+ {% endif %}
+
+ Polar
+
+
+
+
+ Concept2
+
+
+
+
+ NK
+
+
+
+
+ SportTracks
+
+
+
+
+ TrainingPeaks
+
+
+
+ Rojabo
+
+
+
+ Intervals.icu
+
+
+
+
+ Garmin Connect
+
+
+
+
+ Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
+ automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
+ want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
+ to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
+ you may have to set your activity to Run or Ride while rowing.
+
+ {% if rower.garmintoken and rower.garmintoken != '' %}
+
+ You are connected to Garmin. Switching off Garmin Connect sync is on the
+ Account settings
+ page. Look for the "Rowsandall" app.
+
+ {% endif %}
+
+
+ Strava
+
+ {{ forms.strava.as_p }}
+
+
+ Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
+ a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
+ check box.
+
+
+
+
+ TrainingPeaks
+
+
+
+
+ RP3
+
+
+
{% if grants %}
+
Applications
+
+ These applications have access to your Rowsandall data.
+
@@ -99,35 +197,12 @@
{% endfor %}
+
{% endif %}
- API Key
- {{ apikey }}
-
- Regenerate
-
- This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
-
- Click on one of the icons below to connect to the service of your
- choice or to renew the authorization.
-
-
-
-
-
-
-
-
-
-
-
+
+
{% endblock %}
diff --git a/rowers/views/statements.py b/rowers/views/statements.py
index 01771ce6..09ac2fa1 100644
--- a/rowers/views/statements.py
+++ b/rowers/views/statements.py
@@ -179,7 +179,13 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo
IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert,
Condition, StaticChartRowerForm, FollowerForm,
VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
- StepEditorForm, iDokladToken )
+ StepEditorForm, iDokladToken,
+ RowerExportFormStrava, RowerExportFormPolar,
+ RowerExportFormSportTracks, RowerExportFormTrainingPeaks,
+ RowerExportFormConcept2, RowerExportFormGarmin,
+ RowerExportFormIntervals, RowerExportFormRP3,
+ RowerExportFormNK,
+ )
from rowers.models import (
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
get_course_timezone, BaseConditionFormSet,
diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py
index ddd4fe73..d2d22e25 100644
--- a/rowers/views/userviews.py
+++ b/rowers/views/userviews.py
@@ -457,19 +457,42 @@ def rower_exportsettings_view(request, userid=0):
'polar_auto_import': 'polartoken',
'c2_auto_export': 'c2token',
'c2_auto_import': 'c2token',
- 'runkeeper_auto_export': 'runkeepertoken',
'sporttracks_auto_export': 'sporttrackstoken',
'strava_auto_export': 'stravatoken',
'strava_auto_import': 'stravatoken',
'strava_auto_delete': 'stravatoken',
'trainingpeaks_auto_export': 'tptoken',
'rp3_auto_import': 'rp3token',
- 'nk_auto_import': 'nktoken'
+ 'nk_auto_import': 'nktoken',
+ 'intervals_auto_export': 'intervals_token',
}
r = getrequestrowercoachee(request, userid=userid)
+
+ forms = {
+ 'polar': RowerExportFormPolar(instance=r),
+ 'c2': RowerExportFormConcept2(instance=r),
+ 'sporttracks': RowerExportFormSportTracks(instance=r),
+ 'strava': RowerExportFormStrava(instance=r),
+ 'trainingpeaks': RowerExportFormTrainingPeaks(instance=r),
+ 'rp3': RowerExportFormRP3(instance=r),
+ 'intervals': RowerExportFormIntervals(instance=r),
+ 'nk': RowerExportFormNK(instance=r),
+ 'garmin': RowerExportFormGarmin(instance=r),
+ }
if request.method == 'POST':
form = RowerExportForm(request.POST)
+ forms = {
+ 'polar': RowerExportFormPolar(request.POST, instance=r),
+ 'c2': RowerExportFormConcept2(request.POST, instance=r),
+ 'sporttracks': RowerExportFormSportTracks(request.POST, instance=r),
+ 'strava': RowerExportFormStrava(request.POST, instance=r),
+ 'trainingpeaks': RowerExportFormTrainingPeaks(request.POST, instance=r),
+ 'rp3': RowerExportFormRP3(request.POST, instance=r),
+ 'intervals': RowerExportFormIntervals(request.POST, instance=r),
+ 'nk': RowerExportFormNK(request.POST, instance=r),
+ 'garmin': RowerExportFormGarmin(request.POST, instance=r),
+ }
if form.is_valid():
cd = form.cleaned_data
if r.rowerplan == 'basic': # pragma: no cover
@@ -528,6 +551,7 @@ def rower_exportsettings_view(request, userid=0):
return render(request, 'rower_exportsettings.html',
{'form': form,
+ 'forms': forms,
'rower': r,
'breadcrumbs': breadcrumbs,
'grants': grants,
From a7d48d711f7875b5ea3a6ce13503fda3306e067c Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 13 Dec 2024 13:15:48 +0100
Subject: [PATCH 2/3] adding resampling on demand, untested
---
rowers/integrations/intervals.py | 13 ++++++++++-
rowers/models.py | 2 ++
rowers/templates/rower_exportsettings.html | 26 ++++++++++-----------
rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4000 bytes
4 files changed, 27 insertions(+), 14 deletions(-)
diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py
index 4e0ac680..1cdcbfb0 100644
--- a/rowers/integrations/intervals.py
+++ b/rowers/integrations/intervals.py
@@ -101,7 +101,16 @@ class IntervalsIntegration(SyncIntegration):
def createworkoutdata(self, w, *args, **kwargs) -> str:
dozip = kwargs.get('dozip', True)
- filename = w.csvfilename
+ # resample if wanted by user, not tested
+ if w.user.intervals_resample_to_1s:
+ datadf, id, msgs = dataprep.resample(
+ w.id, w.user, w, overwrite=False
+ )
+ w_resampled = Workout.objects.get(id=id)
+ filename = w_resampled.csvfilename
+ else:
+ w_resampled = None
+ filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
@@ -128,6 +137,8 @@ class IntervalsIntegration(SyncIntegration):
except TypeError:
newnotes = 'from'+w.workoutsource+' via rowsandall.com'
+ if w.user.intervals_resample_to_1s and w_resampled:
+ w_resampled.delete()
row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype])
if dozip:
gzfilename = tcxfilename + '.gz'
diff --git a/rowers/models.py b/rowers/models.py
index 3ac826ff..cd2500f7 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -1174,6 +1174,7 @@ class Rower(models.Model):
c2_auto_import = models.BooleanField(default=False)
intervals_auto_export = models.BooleanField(default=False)
intervals_auto_import = models.BooleanField(default=False)
+ intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
sporttrackstoken = models.CharField(
default='', max_length=200, blank=True, null=True)
sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True)
@@ -4578,6 +4579,7 @@ class RowerExportFormIntervals(ModelForm):
fields = [
'intervals_auto_import',
'intervals_auto_export',
+ 'intervals_resample_to_1s',
]
class RowerExportFormGarmin(ModelForm):
diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html
index 9625a472..0d7d3858 100644
--- a/rowers/templates/rower_exportsettings.html
+++ b/rowers/templates/rower_exportsettings.html
@@ -64,13 +64,13 @@
{% endif %}
- Polar
+ Intervals.icu
-
+
Concept2
@@ -103,20 +103,20 @@
+
+ Polar
+
+
+
Rojabo
-
- Intervals.icu
-
-
-
Garmin Connect
diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz
index f15cf38957d0b2a3b4cba860a3eabe8396f81f14..3a4eca337f44e3c892acabb79b59b31fb7ee2684 100644
GIT binary patch
literal 4000
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
F0RYzQKaKzZ
From dfad4865c908abb4b8b87c6a25d8d698b307c21f Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Sat, 14 Dec 2024 13:58:23 +0100
Subject: [PATCH 3/3] ready for merge back
---
rowers/dataprep.py | 4 +-
rowers/forms.py | 20 +++++---
rowers/integrations/intervals.py | 1 +
rowers/models.py | 2 +-
rowers/templates/rower_exportsettings.html | 53 +++++++++------------
rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes
rowers/uploads.py | 36 +++++++-------
rowers/views/userviews.py | 1 +
rowers/views/workoutviews.py | 11 -----
static/img/intervals_logo_with_name.png | Bin 0 -> 18456 bytes
10 files changed, 61 insertions(+), 67 deletions(-)
create mode 100644 static/img/intervals_logo_with_name.png
diff --git a/rowers/dataprep.py b/rowers/dataprep.py
index 827a9717..28bf84be 100644
--- a/rowers/dataprep.py
+++ b/rowers/dataprep.py
@@ -369,7 +369,7 @@ def workout_summary_to_df(
return df
-def resample(id, r, parent, overwrite='copy'):
+def resample(id, r, parent, overwrite=False):
data, row = getrowdata_db(id=id)
messages = []
@@ -393,7 +393,7 @@ def resample(id, r, parent, overwrite='copy'):
data['pace'] = data['pace'] / 1000.
data['time'] = data['time'] / 1000.
- if overwrite == 'overwrite':
+ if overwrite == True:
# remove CP data
try:
cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id)
diff --git a/rowers/forms.py b/rowers/forms.py
index 0ebc5fb3..506f900f 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -67,13 +67,12 @@ class FlexibleDecimalField(forms.DecimalField):
class ResampleForm(forms.Form):
- resamplechoices = (
- ('overwrite', 'Overwrite Workout'),
- ('copy', 'Create a Duplicate Workout')
- )
-
+ # add resamplechoice field, the result is a True or False boolean, labels are "overwrite" and "create copy"
resamplechoice = forms.ChoiceField(
- initial='copy', choices=resamplechoices, label='Copy behavior')
+ required=True,
+ choices=((True, 'overwrite'), (False, 'create copy')),
+ label='Resample choice',
+ widget=forms.RadioSelect)
class TrainingZonesForm(forms.Form):
@@ -582,6 +581,11 @@ class UploadOptionsForm(forms.Form):
races = VirtualRace.objects.filter(
registration_closure__gt=timezone.now())
+ # set upload_to_X based on r.X_auto_export
+ for field in ['C2', 'Strava', 'SportTracks', 'TrainingPeaks', 'Intervals']:
+ if getattr(r, field.lower()+'_auto_export') and r.rowerplan in ['pro', 'plan','coach']:
+ self.fields['upload_to_'+field].initial = True
+
registrations = IndoorVirtualRaceResult.objects.filter(
race__in=races,
userid=r.id)
@@ -665,6 +669,10 @@ class TeamUploadOptionsForm(forms.Form):
upload_to_TrainingPeaks = forms.BooleanField(initial=False,
required=False,
label='Export to TrainingPeaks')
+
+ upload_to_Intervals = forms.BooleanField(initial=False,
+ required=False,
+ label='Export to TrainingPeaks')
# do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)')
makeprivate = forms.BooleanField(initial=False, required=False,
label='Make Workout Private')
diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py
index 1cdcbfb0..7db883ae 100644
--- a/rowers/integrations/intervals.py
+++ b/rowers/integrations/intervals.py
@@ -17,6 +17,7 @@ import os
from uuid import uuid4
from django.utils import timezone
from datetime import timedelta
+import rowers.dataprep as dataprep
from rowsandall_app.settings import (
INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL
diff --git a/rowers/models.py b/rowers/models.py
index cd2500f7..047d8d0a 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -4561,6 +4561,7 @@ class RowerExportForm(ModelForm):
'rp3_auto_import',
'intervals_auto_import',
'intervals_auto_export',
+ 'intervals_resample_to_1s'
]
class RowerExportFormStrava(ModelForm):
@@ -4616,7 +4617,6 @@ class RowerExportFormTrainingPeaks(ModelForm):
model = Rower
fields = [
'trainingpeaks_auto_export',
- 'rp3_auto_import',
]
class RowerExportFormRP3(ModelForm):
diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html
index 0d7d3858..13418d15 100644
--- a/rowers/templates/rower_exportsettings.html
+++ b/rowers/templates/rower_exportsettings.html
@@ -64,13 +64,12 @@
{% endif %}
- Intervals.icu
+ NK
-
+
Concept2
@@ -81,12 +80,27 @@
- NK
+ RP3
-
+
+
+
+ Rojabo
+
+
+
+ Intervals.icu
+
+
SportTracks
@@ -102,6 +116,8 @@
{{ forms.trainingpeaks.as_table }}
+
Polar
@@ -112,11 +128,6 @@
-
- Rojabo
-
-
Garmin Connect
@@ -153,24 +164,6 @@
-
- TrainingPeaks
-
-
-
-
- RP3
-
-
-
{% if grants %}
Applications
diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz
index 3a4eca337f44e3c892acabb79b59b31fb7ee2684..dd7306f84921784d4337eb7cf60575abe39ca49f 100644
GIT binary patch
delta 257
zcmV+c0sj7=AD<+lIJJZMCm_J{BTJ63+
zczBUGo?U#{_gR^Kd%F^qPsbfRc-_A`ySVb2
zJUBl;SsitK`stT1#^t%cI$8Ser`M~KmzO{HxAZ=yL$5rX4_lsg|4En3&Vs9OpTFL1
zwN9J8eA;ij;quXbasGoB>81~VU7jqv%`$PwpFTXgEBAHqq+1_9esuWrv&?4u=WhEi
z=`DWt`t*-wx-)=#0K5MW7FWL5J^AfpI=r(*y;`T|@GzlVUOY-q2op#5N
H#DD<+W`c@3
delta 258
zcmV+d0sa1;AD|xxABzYG+aFx92dNH!vHLS{=(;6*cijJQu-F}NXLqKLzcGKl{IuGA
zfAH`kaXh>Du$`N`ZufR2DxZ!!c<{P^b#`&(
zHFmSi+Z(A&*5Q0xx9Fkp2i>P(ua5c2dhPx
I_QZez0LCki$p8QV
diff --git a/rowers/uploads.py b/rowers/uploads.py
index 71f07553..fae902d5 100644
--- a/rowers/uploads.py
+++ b/rowers/uploads.py
@@ -245,23 +245,6 @@ def do_sync(w, options, quick=False):
dologging('c2_log.log','Error C2')
pass
- if do_strava_export: # pragma: no cover
- strava_integration = StravaIntegration(w.user.user)
- try:
- id = strava_integration.workout_export(w)
- dologging(
- 'strava_export_log.log',
- 'exporting workout {id} as {type}'.format(
- id=w.id,
- type=w.workouttype,
- )
- )
- except NoTokenError: # pragma: no cover
- id = 0
- message = "Please connect to Strava first"
- except Exception as e:
- dologging('stravalog.log', e)
-
if do_icu_export:
intervals_integration = IntervalsIntegration(w.user.user)
try:
@@ -334,4 +317,23 @@ def do_sync(w, options, quick=False):
dologging('tp_export.log','No Token Error')
return 0
+ # we do Strava last.
+ if do_strava_export: # pragma: no cover
+ strava_integration = StravaIntegration(w.user.user)
+ try:
+ id = strava_integration.workout_export(w)
+ dologging(
+ 'strava_export_log.log',
+ 'exporting workout {id} as {type}'.format(
+ id=w.id,
+ type=w.workouttype,
+ )
+ )
+ except NoTokenError: # pragma: no cover
+ id = 0
+ message = "Please connect to Strava first"
+ except Exception as e:
+ dologging('stravalog.log', e)
+
+
return 1
diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py
index d2d22e25..cf004032 100644
--- a/rowers/views/userviews.py
+++ b/rowers/views/userviews.py
@@ -465,6 +465,7 @@ def rower_exportsettings_view(request, userid=0):
'rp3_auto_import': 'rp3token',
'nk_auto_import': 'nktoken',
'intervals_auto_export': 'intervals_token',
+ 'intervals_resample_to_1s': 'intervals_token',
}
r = getrequestrowercoachee(request, userid=userid)
diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py
index 14112726..0412c7ff 100644
--- a/rowers/views/workoutviews.py
+++ b/rowers/views/workoutviews.py
@@ -5609,17 +5609,6 @@ def workout_upload_view(request,
return response
else:
if not is_ajax:
- if r.c2_auto_export and ispromember(r.user): # pragma: no cover
- uploadoptions['upload_to_C2'] = True
-
- if r.strava_auto_export and ispromember(r.user): # pragma: no cover
- uploadoptions['upload_to_Strava'] = True
-
- if r.sporttracks_auto_export and ispromember(r.user): # pragma: no cover
- uploadoptions['upload_to_SportTracks'] = True
-
- if r.trainingpeaks_auto_export and ispromember(r.user): # pragma: no cover
- uploadoptions['upload_to_TrainingPeaks'] = True
form = DocumentsForm(initial=docformoptions)
optionsform = UploadOptionsForm(initial=uploadoptions,
diff --git a/static/img/intervals_logo_with_name.png b/static/img/intervals_logo_with_name.png
new file mode 100644
index 0000000000000000000000000000000000000000..b448dd4e6a12f8d5d9b8888035ce6ea20d3f3f26
GIT binary patch
literal 18456
zcmZ^~1zeoF(m1?8k;3Bc?(XjH?ykk%-Q8(%FHnlR7uVwMRw(Z7eA{!+z3+MN_x^W(
z&u%uEnPeo%Y%&w2q#y|oivtS)0N|yi#8dzPuw78u3{$!HySg{t5VAT>&
z&)^gaY|??K`_S5y#(0U8GNA$B(~xG
zL;^En{t`1DUMg>?tkStDebPzSb2=1|=cL0JaNMJBlcx`x$;6)(CXIussMS%Bg0$4l
zCcS4_Lw4z8VBxbYz#LfolU0ksrf0{-Ey(3q6LwI^2x)=vxqD_ogu1zu5%}!mFSeV*
zR;gwZ!}~I+HR-csNn>NQ@(HQWs*ko_Zyj@`OI8!;{hE+iUT+
z4c6fx
z_4nJ!n$+VG-mcI@EPUFzF%Bh@ZzdR=88~qviV$?qa0()T`=yv070%c^I!*cm^>=Pp
z3g^BD*QQ7t{w(`Xrk@bI`~7lw5FD&h?vV=hb1o}M!M7N22nhxGci^6Y-5>n`k#*R4
zR)~P;;nuE>FxoG>3Xc@$C{Rd2DETCY5c)h-nrg7G_Q1ALt$k+`v5HhR041YMJOU+sfflK-Payjieqr8OrY0>tE{u
z8f}=?YT+!P9mm3V51P)_jMn1T^47*JaoBP;5%Na&ZL>M0a(U=s#fOacp#O5;LfDzu
z_I8A5N$`a1AlN`Sk5KH6xv;tX{UG{K|3L6UGA(NtTeqyy!fuq`Xok(6UWiri3a6bt>
zk+gJ{6tt9G(Q{F=8Ow6$iMRsMej;bm@i6AF;xHjPb@W2Ct$6M)>j9bpiUEQFO_>km
zrsO3vB*lEi&c!=2oS(zLeEdoAlg>Q;ism=XZ`GgopC#3)Xqsq@X|QQ<)e*kyDSK5#
ztFkNKs{j6uQF^BSBY#CvSQ$u72b)$GUzgyV#3D4HBLCgi8mBScJ9I@HUs+Y%MJYyE
zw@jnhT|!<$bXv~RHZxH}gjb?n_PL_!>x9~Vxr35}(z)_`KC^6}Y)G|o(R8Vk
z{;VdezDtZn{hld(Lu%LpUnRLtdjo?t{AA&TbJjqPI@|G51@uZyQ&a*ICz5*U2M^%ssn|`p@mPo0VOW(+Wt`qda!eLj-*6l_&)D9K
zOsr9C>Nbj&cU!-QQ?Ig3Hf-D^VK0x((v4OuoHTJSWEsP-4&=Gd8Aq)H-dJAx|$y&qXhx_3gLC@6Aoyl(o;RbxwuLdDG{M$Dx1koU886{w_blJ=HyY?w{;t9H`!K=--?UXbsQ>P!LfW$#(NMA!LnK
znI?STfvy6cz>q+SL#adY!#BaCerOKwCJrLj6(tD6iHH`->J!4^wY6E=ah$!VxG>x|
z-Vd8B&%CGW2C6VvJ*wDoO9lQ)t4a1csN+hHGRuA$HQLQmz6BkgLs1p
z{f$nMfPqYOKiUl1`-)%<@=9w%yOuViTcOT?w;uL>_Cp(gU7PwQ*VA^l+G8C!SJ*4K
zr+|z1f=;DcB^6JHAFEH9gwuks{wFI&UB*M<4&oh!tC(JVJ&w`(^%o?%SVL(8hmT+2K+huhlGd`(0D@?*8hbM=dJ#8u{L|LM$WUG!FT
zD!v3S$N7UwhK|>_pY3I}I_=(~w-GrVIsEM%?(B01b7+gc8}v)-?Lz^Is+H1qj|L|j
zYd=#{VQJwp1TN;@DuT4xt5go^)x-Co|UBe^%yG&c{;W(#z_Svb#N+h
z?$~Bz1}Oaw
z-;wi~Fa?YK*3q*xMMI|_&POym*4OOcY~&a5tMgvQFPl^J74+pfvmF}NTw9k{_?w??
z6FyBo>IyV7tXph$x>MdL+^-+B|G39kOS!%B&fCP?2zVvB?^)`5kxZ8qPxwNZCOGY{
z^4$J-9>+qtoFS0cd8|m9CvdWV*Y!gBoWKYHIXOJ}6Z#hFICqs_kbjrc^?Kk16M}Jo
zaVQ+_UG1^{>C19CYJk_Pn;)6I*;eE3?L$_a1N$a{=l(^NADi#Z$aCzyDPgpLwLjlO
z)aCbUmy`XTT5>~2Lz>Ra0J~R=o6~8}!H1S~(rvjWa4NCwT{FN29v~185%8O0MsO_e
z=o=+u<`Z-}*+rgb*?uk>B>)GBbsD2~1;7*$}j+E
zFcbg;s00Rj1HfdxRR1S`?0(|%z
zHx=~#=amS0Kve(rtz_kCZfj@k>flVo%D@c(urRT(@-T7ou&@y^as4T9u>$}Q$>9KK
z5C#h9>COlLH$|}Be29OS0cM~!fUv5lv^3~j)zsPC+}_2~!PP;8Y7|uQ(NRj<1pvSx
z|MP%JtB_p)03XJz)U;f+i~mPoe_H;ZXlADW>B`Z~+4e6%n3*z~+nU>%+q=4e@R%9@!#{{U
zzCR=95p}S2a8`9RHZ>Pu=KG78e^&jEB>n~cmsx3vS%E0JfTSkC#>MtG(to%7pP1VJ
zgUQUq^iRxxwEQ=wx{JB9sDs@fCjK&k|7hkfTM)MWui8JH{ZXc^)gK$-u{E~;#82W$
zZ)R>`>}KmqBJe*p%*XgIxc{R1SMC2M`}auy?~(9o|AqO__J5%MF$W$Ma~B6&w?8(lZg1r(z{1D)e^viC
z7D%9u&gL$EDG{WIzfk^J_uue}&Q>7#8~^F%kDdGl|IfPrhS&O^VF@s^fsEuYjDObt
zH-`5AiSf_ce_)uI^0>H}Seu*vm-7G8ysEhq<3DuD$N2w;{+H_iG5`OyxcsGh(CQ)p
z3zGZ4S1JM6CWHMz0Dx0hT1;5Y6YNwU+E;C0A&)KD)*I$yfoRb^1yR0=FKl!)q;O^!
z$wLJn91TsO2{bMrk}n#vm;jhEof>-L1{oFA3@VC<)cJ`2dwtz#DzD4=`iZ@J-`eK+
z*$zyr1^6SwCT=M2HEI5FsbRNPQp!M*)ip{KwDO$46M0j4bzi8hPm-%?~H7
z!sOqvqlz(EAbv8K)z+=~YUFBm#}=EHlsrH})&)93z{SFt2C;wDRvKTr!ijSF-s5Id
z{jn_N!OJ#}LD#oFxWOznech*Tt&LE(e$zYXiI6h?l2A8#jL;Y6-MInU&GD6**Mg4q
z2;fGfzh@c;w1tpZLiwL6TI&Tad44ios>e&~$W!`rJZQ8#`
zZ#BZG$t>ESsG3^PWnBJ#Ppcua0yB2uR@;#N&$NmSoCRh8Tckdn#F$tM(rLq1Y37M<
zI?}^gEy1*lXzC~#g|HZlEFnp7AYTdpXG-FI$o?hP&_Hdz_uhFWZgZ61!nI&|WJ*`2
z2|f5S{Q^rmy5XIexN+UKUPl)P)ew_p>^{CXQ&N9c8tdGa%vfTHJIin{3vJa;3LLv${t|!wnaHoel#|8jaOU)SyZCOMm)|D%ez)o%
z?d9dA%8!JT3mX-)R5lv@(DXX;&HwKmRlz3
zMlBg8`Z_xn%Z^LPn)>D0HH(y|X$WMoFq<0)hEgi8!6-MZ
z-4Gg41^tez^|jMyQePYzt~Z$DqPXesM$F?3+8799>2!_Mc*-l&qUy34DjJ$AF?BO+
z6tFzu{Dy|P0ST&&WmlYnkkab6B*NhUZ|yaj2mJHETZVa@wgo|_&!{0CwSt>YUhe|ry;>0ASkGo
zJ4ig@Grc-&M
zia%TNOqOOzH5xcP?H65(RM1)=2V*3?%f0sDWa9_#+0EhXX-rH^-|>8TbGc@<0d@1b
zdzP3qC3rNj)pIdu#5`k}N18j0ufom-*TS|0C0yI?v{rO^IpcavSV5nZ_jr~t3+`hC
zX_6c#t66V)jYNik!-)OhIA#@+wCTCL*Pm^zN+e%4jiJHg6iRctUFDD}y29oD#XapC
zAnI^4MOW=4aAS;7$Z4#5+>{V`K|4zn>Df1Tic~ctw|}KGPV^T>P&v|3asyYbnN
zYN^Hw?NjSznq?}`9@6~{*)iQ8gOH!Kn6J8#ann~;gt~f<91VL-nYd#etqx}Tv
zMk~->@nE@7F^6YhX68-17aJXYz{J#4JBP
z8XfM!|K+$p3iTtUV(!h*NL_usww6}9{ey~N=6zjRvB<6=tJaI^8^^p2L=?31@)+crCd*v?(NeU19V9<%gxwOO*TuNX7M
zCvbZ1b+PO1ORwz31n+r~@5|*-RL6s&;H#@#o`Bz;>Wd_y-_7mGay>LUnk6n0N{IjU
zIA?@`-|f8N#X$<8y+QWIMV5Wrcck0d4z!E=&44asx$~I$a!tveC2<$0@l1BpNxti`
zKBI|xwof7=MqZN*RSA4HGvk{bX=~Flm$%{^c*u2+N6vYY%xDXG17d_Apo?T$qEuZInHvRxw}&0Si;0sO
zG0zJLc({%f=7&xzq0-}Sj5O-uEU7Wg$)KDP8A9)a0_jBqpVJ7PYxxWoa#UVVWKs>j
za3FSah(Co4b24h_!udrf?)C7TNYmB@v;Dr6^lo8RsXe|rbr?t4xJZ?CVWtALuqWmI
z&5(r-GQLix!@Bn^*`B>!B)F67=QYfnjlo
z;$m)2f<)_E1+c7p#yK}%Uax+Sx-pwtLvZf}fCME7HJktH#!h&U9~Q#ndI!(UC6mSF
zM3RQBYCJbRJKYuVo*>T|ocDOWKZztpD`D&-!oXSr#EQ(;81tk?V$B>}QZM~*>WP2f
zmd>1%-!r||>Qa5QRQnZqvAD2q4IjFs{zE+@Dc<~n$)Fk|PNC8{_W$J{c*k3OD5C!MPW53552E4MvY>q=K@?C}!C{$5M%=5u~^HBe#D9$66Rq!s2Gv9tYw{??lc?P
z=+G1AVjlX9$-_LyZ3=~Fd#TB8RsSKd>Gkz~lSj%x0hSr5FP7}-cF|BX_e@{mD4I9o
zTs(@ZzfywfmlA(H2RRRDS#%*PLX;T8nCAhix<~efGScz1GXcG&vg!H8fEoR(NQ)o8
z<>GSM`$|cTF`xi%H0oP67=B)Pj?y>FHS;a4R~UA^rM91UK06$Fo))?aCexTb-5iQ;
zQ>ZU?OL_=r19yfJ7=y)jHpa%}6kzRomOg}o=7{FD$A;Jbqkg;l?Of_8>-T()she>n
zJEA0R1PPv`{+TcQ7ge|it3f;z8W8l*^ObG{zC&%YN@yX>(VSJl+n4dZy+uqr9{@QX38we}EAs1>!^%hL_>6Z0;H
zk6|gtKKkfqlu4UJGC-7?R-DvYP8TwbrQ>R*##0Fj$Pv5r<`;7JyA0y<_Md+#1wM1l
zd=mX2>1{L*F>ga9^Eo-$BvUvTNO5v#gGUy-=S}9+L>)J
zxUr@qrNw}a6z9oLh>%{P$SeLfJ8Uv4%z&3m$#E1+`cv}DGr#-wW=Ia)iOSnKmB_4_
zj;o)v8A=Ik&g5QC?t^i-9tCx5m;9w=iZr_6++FZ#w4g(YMZE1Z8!=8TO&i;i0&PDG
zy{@1tnlKNHn5^rRcMI;WGvpaybevhwZK#Tnu|I!OZoh%z4_|WnC}QGx{;jyMA&+q
zfobg1^5=uE3002ltQ?D|V*(Emg#PKa{_gweCRdEa!cp#$*k%D>&w-+6`KJt-w3=1E
z_W|$Ec}k7HZI?kzL*w$gS4^Hb8uul}Oy=V5+VIUni10~;YMkKi8jyL(RYKqZk3JGkdlIV3}OfoxWc;mer)XN3SKx>hr>Qc?#{*<
zYX7d<$kIVfFepFG_~Rydscx~XFchw0bfiwlWfQZ-1eB@J|S~yx-^VUgH
zJJd7k6Fb=3I%qm86VQ(HF4=Hf(5QgTGuL^M(SG(V&%X{^Xhk#cW~2rf0Jn`HjhNmL
zf(O-BX2PUoKk2^iwwN?fCn`WURL3}T6I-kwa^A9opsrbOWG{YT#M@hr?0Ij#2e0o$
z>DuCB$^H2t5dCR1GViA>L{g(@j+a$spA`UwfMCMXU7XdxCS2~2s3{j>nypzh1WP9c
zl4cH-)yUXRzhRu;9nqvF-fxux3Gw;v)CShK@3gNnFG@M+&bULck~p|2o@HIsJ2~c^
zU#|h5NSZT$hakLIOco!eg~|xzvm_*7vr0(8s|3R~6jMOiAxDyyAGZt8!#7Ff
zU(+_0UcQ5l_!<=@X{Ph1Ff?nsv9|Aj3+3opL6KYHGJY7GHz|O(;hkcF{%kO2E*?Pe
z3B7org6Oe@TkF#LATW-?^{5*C~KnW
zSraSATJV`E>{Hgg0kMH5Qu@g|GASxP=x^yt9D7$mf9&aO(8tpOqdtf3K(OA!o$bDe
z>);!{gO?h5P*TNOf?DU#h2k+S%Q@(!PWu$$vl$o=lNOjhJk`HXRGq60xOg0
zkJprNUOoxw>CDKZf}UbhtPRQ}aFNb2-hCx{ipCDArD=Rqu>ce9?Fec0Fh~nvRINC)
zMMha_a5~tPAp){f7#9%0Go7g)MkR}c=dYK|<7TZX3)r^+Emo&C3l$?#uasEzai#Pf!Jb&M>})BG7>
zZnQ`nmeoh3!YG_Yz#({DRbw45vFbTp;`sUZf&%UxfFlI|`DeF^j($S8Ty^Kuk=B>n>sVnT
zk?J$w_^+USr$Y2@{90(Ty(~41M_pZCs9)^NeEh0Iy@n^jW1^L4?Y~`{dt>inP)wWp
z43m+mLU7E)^n=CK;N`H&&7|BXvNDWS($)Q=#t6QBPb_6rHs4&OZdy?7gwt>&AU2O?
zm=^Usloci-BLxK?wr@*nX{2W~?dZAhYbCj!JYNh)iDdyqMW<@LV2k56xS?EWF%svu
zo@P=tw?s7PvS`i*HHbSbU8Zids+`cv%aQ?{wMZz#RC0&g@6oX+@;6XYD$XIJu`
zhUhe9KPpiSbJuFVKBO*w({0XG-NE~aiHcU
zIXn5GcEv%zG+bQWNlfgTOLROFN>Kab#fLk4`(V}G+vo)56dfWOse~9AWPEz3-I|aN
z>nMZ@S&XW7^`CHg%Q8KwOs*@OIs~q3iPLyTswJc`@+z*LbC)iNOzZbvN$J9izXp`K
z(uckldTI{zIG$i?PCJJq_*%g+hi)4Iph_WiEO*|QVz76sxe&KsaulRkk`J*99jeU-
z=QzeR3^5y%Vu3x0jR_4xK%Twvf&$;TKw7-`$fJ{JuHWBFX{lT+#cmJ!qk?4H>m6ac
zeBsMk{h6*gr@ALEBM$@MM<0%KhazQ(!<(3T!FTrXy?p$KVq4}=;*!W{xgIFfxdIAJ
zE$7o87h%lN$$!>Ez%hI6xPLH0bCZ)tJh+q5qXE>XNI7YD25I33nJI-b
zc2w8{sW`336sj&i_l5}K@+kA%D=`vtXE3?2YHfRhi7{WbJ`!)|xAbilA}l`|x)X8f
z7AHq^ZrIc|R;IwHFs#qhXGX4k7u^tReCpUi_&+InmVc(~vdgy43u;mQJce_s8B0K$PgzLeb?JVX^2=#pMW4wS3MYmR#nsZbXi`8~Nr4_9
zubCn{>n4o#Op96?xYcb1E(>x+ir*devn<*x(Q#FL)e4AxjljS9jj!~MXcb`Q!D7~H
zK+qWm;CTnLW;bYer+*LA936`q0_OjWb?g4jXcCV3RgHbdM|Agz2kXRT;WC{&p~kD&I$!_Qa{epT?*SN$`ve*WX#WxL0@
z#)2R*F>oN(y9t5pmD?h^fpy8UQM<&CzvDM{+h*K_!fi`^`T4L4F2oNe+x7=Ei^)Mu
zLtkdN(Q|K4+2@8i(eVKOyWaxEVG3@trhWJjHWHd`eho3IfyBX(ipRGKjI0=hFh9`VYb&!4r&R~^_I>m__it`VI#A}7(8
zcmX63YtaDr!)dZ$cQ4-RR4C+m^9Z$J1&VTMyQE&euHD>62C2s)OGg-gHPchL_p
z=-mh^0bUqu
zugisPq%g!jDBL_7^lP2{+XbJo&t0PHFS$Nvt#U1%amuDDh3->C@S}YwAQxQC8I$&>
zF1nMoz)?(eG)?11C(X)kG>ct}MO=T28dBva3OOK`)0(4&L|whzjJLF;P*=kg~^W$HfNe
zUlf9HCCc&^@pdtiX+q>F_e2B*!`JwDbh#g1DfYRGLEC`p8CMSZ$uD1JWsy80;8DH+
z1H%mE@G{fxR$Jp@{sIQRl2~(m#e`UaTbnG7cD8pAI(|Jm_UzNv8h(Vf+WB3yx!B#=
z@JmQ7+V_XNVyOmWanIFqv{@CS>F;bB$r3y8zrQj)GFs48ZymLKaj`-yZd@e3-I4!<
zX7LL$;ANn0!7z8*u-AZBIHT?-vQUKL6JnObaHmT8Ji6%!q;!t$oao&l8(*5SM8-!Q
z>ZP8eoD9}a*2BMY$&8AwYr%&2a#1l3l6JnEJ*;;U%6hbX0hs31n?P=!2Xd2&)pi-*
zeezf9;IN)6{m|L!5Gr^wE|Vpj&T5SREV1T;F3Pb8-`O&>ovU#7JI!!Hi1#eydDN1L
z(gj6c^8p<(l(R|wV%@L31gM@U6-WH7xLDoYUk$Rd%r}W6M_MVTe^Pj+H}SfLROM-g
zP`--PDjW|B9rT}P+t$sw8UF)oQRnzwON%{R8Zor7OZ3Hro%TdiPKjKPRw^3SPN^n`
z>e#v)){|*xQ8nE)xY+0+T~X3AP-~QUhCyM}3=;ay%&f)2Pp@}_M{2`|8@%?{V~Nx|NjW`y^X`pFe7!PU(^
zKqOxfbR*j1oGq5~pAe2a*H(yM^8sWgW+$#kw}^7B(!TN&
z%;1Hj1+-Nk*O^_1-B7gRJP(pk9VtX2@Su}8(n;v$XMEMLv)wSBZx(4`(g(5iG4v^@41C9~_%Qb4!4;OaSKAk=
z-X+_7sv0}!`|JBLA<7tx554&?+eQcg=go_yta%og7K0+^Ca=qCG)`;7<(FR#nT(N!OzBm=7j$W(~YgHoFnnn6kZ&5TF+YA}7<
zk8tNyOb5MVTn-*}B=E=GF4t+slHVChs|$ZbQ&ZCq(TogRkbj!(ah)ZCr3}yd8QtwD
zt2$AC)qyv5CtEpEwt>1g=H6S8DM{f-02b-5&+==)TS>=Z)O=V?G!k#KyLg3p7lU4>
zLgLe8#l2g}T_)n#6ppE`F;u{?@ksL5irY6_)dm4jeQ^914^
zKhmVZNFuMt*$eRz$9P-k!hM?WSC5dPD*T0Q>wCv$?I3dXXdcfKSoNUWV}w{tTyH&q
z{@E7bmy5V}M_aeZT6c^poQK(PWwJ_7;&G|DS~RD|=lZK)yjg8vNlta*aDPL=^DQTq
zmX|`4Ij&e(QAvIUA#u|l(=N^>^HOOc`KVyxST7I9ev%Pnz_*$iU@}8=!b~ji?zx6i
z`&8K@6S(~I8m`B6GHBbR#Ah!%3M{Q8tFrjHP4^xR
z87kU2(85X;bgJ)N_LHTXWj0zJ2Dc{>{%}WK72!MqldPtVwm_B#CR+7P4GfvJ#O`Wq
z$~0L;;F)QR17|rxThO#b-QtJw54Y?Y{`9=Ubbac|^1iLc(+Vj<;QIupvEVLY!{-=z
zSt)0u&<{abc<1_t_Gajk-L)g-ORHWuiZ2Qf@KW0TV(hId7|#^lB?a)dxGiI-vi)eE
zs<3twi-wBYo4-jgab8H1z%kFOa
z3x9XD4eD+fe$Y1CWA~KH3`P3-S7qPmRBLX=<40beJuCN<+O(xZ4~zO6^K$KpRQ!sA
z-H$1uf&Hshd~0#7w8uL5mIbOM$jtXhcBqzlA3LMMVpH7_y-yn#vr$s~-(T-VYoxoa
zm>A!njSPtq1^Onh5R6}lOJ+OMs*1@6gIxGS+-nTcwd2wZp)Bf{~5$v`aT*OvMSB2O1uqwB(@V=(jOv?SqcumzIV;&DPT
z9|Ll|ZkqD44;{Ts_(0=X54gp`J64HhDZu>`iVXBH^Wf2Z`LiX_K#5#N9ct>PNV$BH
zfrc!0A)9`7k2ebEi`5AP5
za!U_8vWCJ23TIdp3q9-#Q!bKjV3d(`N+i1#eGPjEB6iRy3|VedqEp!1%I0>NbIT>_
z1RWVK*W{EItHq}>L5Jhbx4Bt(E^-t(-e>Ooda_$<`>+`E=_9HA@4?v<9*1fRbkwx?
zb`lty-ZlLja(&4UOeDGcSl-;BVrn%OelN~2?+6obf+0c4G^)L+-ceHy*u)yD>d^dG
z!=$ZTf=wJ0+@2RiArlM7=`6AB0^47AuuKEXV4rsO@k~F9Zp~^nGJ=0yk6b2Cn-=jU
z2y1LzPH%NpXZY=0ij&fwt@4h)@rArl&oMwcb-(N!W%}XedU7(dfBrKRM2WoXZ~O~y
zcjNv@VQ~*Q2jmCCyLf1=IU8PNeY*4zwNRx1zKECCo
zTSgZAz`GXpkg8nyY_25UB_$U%a0=W|>}P6wu*h!jD;F*6yF{uuoooov5+HKUYL(LH
z!N@~J-0CC9yN-pY$8juIbwu+otM5qL^uk@xBPv(^!1H)&Judu-9il#b_Y1A2LU-oW
zvEa)^-?ub6Z5A17LcdU^IpyfOk6+=zw|*bdVGUzXc_Ts@QoPdgEO5En!L1K2A7VgZ6fX7XR
zCH?jV)RQQu?Wrb2jw{St`|(gbe)}~jl4z`VWlahh-UV;kc@b>BPYehZwp-gULdeQN
ziswVcfZg}Dx+w7eaxH#gsIMsoqe8^td9hX8!NzJarUi%WP@%$yt!6BLzbWHXh0s@}
z2RCnNQX|f*k6lYWg#GfZdaFIE2=zXJ|6wbHJHJ+|D2wvR=uL!_#Mh(OdTQ(8`?uV(
z1cy18#-Si~n}v>8b3gu`(!4x2s~N}@22_PlC}I-sz2FSuG)IQP?M-A$je2RQ}GatZILd!?iuj;dRH*vtn{s@H8$%<7)
zdmyt-AVcUlON8m5aC6Mql>xkyAu0Pi)-Cqe+V);na+$w_y7!FHKK83g!9sv@6*n-OkW-
zYZVB_!AVc1lA??`--(r##gu(26giRB@*yWy32$MjNr4C5Oa2=hrB#~%F4oZiVKa|7
zr_Yf8w<|9aRAV_UpF%qAaC-15KaHhuwX=`-cK9f_>haNBoZQ{rV-)FWuuvNIr+i+&
zgGVOz2DBqmkZ0gI*DlK3(o=oLf`9QHmVCxaO7-SWfr`CSE+u$#y2%#_
zL%L72tDu6)O2N$Z(~6#?n&WFx2UUA>WO(^6rfc}G*gtp?aJog>*pVPMNs#?sC>@M`
zG|N7JPzGh;iigWZk@~o2j*hCKZ)@Xvr0eM(QLk#LKviq`hA{CrtR*e1GP#$;HN?+k
z_^RhXlW>b)x|kn9_8|t08Ljnx?(6FtsVSl#Kl5_yKRF{~oXFX32&Lvq=T)ddllP(4
z1s&xnWfg=FR+GvWCyR89=gC;*SKP^WN7595!iaFF4i!Zo9Q7v
zig&yc&3VrfXq523a?*g3#NY5lY6`98To~7*bgf&v@5`3xgVwZAIF{FBsp+we8(qt;
zJO+hGyc*-@Y>zn;8DuPRe+Mp9GixRIMy7@L_9TW~7Iu7z>wB8?WyhJy>&j0J$3!d<
zykGv5ms3Vkw-54Lzvfaj=cEF{a1Zi^s6W^s%v{J%rAGNTZ~FTbSQO%*^pfNR)w1^y
z%m$Q^N1Qt}k^zqg0yN}{l7-+f#K5id=xgRJBiVas&>F>4*q}5P5xbLpI-Brfg>P99
zlfz`*Dc9RQK3U7oc)FpuxBLlZPD`8K*(Pf;s8+PiX&?2u9Wn@`-=7R67KNewi2@vz
zC^$okm9`Trl9dacP${*kQJL~ir#zUW6)oC~HbW`1BAWmgNy3XRQR>dh0z
z$hBrAAPLr1HSidg242#8TAou_xLY2w-I%IoJoE`QViG{DSxka&EGTon-QYfFvY;E<;2EC;{5Du)&u>;rzhvmfzC;M?(pCIk?e^x0h-GZVBRd;x
z7I~IQNayvaDrvhuFe#tJI_dJmm7W5Jm>+r%B`y*auj%MKoYypDcOxrw5Dvx0fR9S}
z6FkpFtdUcU^u@l{V}w_1NG~4TA^SilV;Hs1|y(&-dr=<<;Al$JJd;
z{m*Hk_q#ki+nJ39uC6!I9TBt_@h^cv+k{o=KQf&P*w+%nkx8S%Zj92n*RrbKK^OZT
zn2wea*Elaj<4oFAp^#6L-KO|eBTB-pr|ms~-)U2W*TWo|csWj6V9hai@OeW$Y~kp(
z7r|N6zDKKJv%FFV#Ci!(dOZ7Y*Fh-0dPN}u&E4{_3Uu@)?#$d>Pgu(i`ErN~eInK{
zt_@rgqzk^`l8*izKn<2gOoXPLB^!l1ckkELE%W?~Go^g)_7@b2Zn^A++|!QCm))na
zP-5dl@1Mzqe^s^n;QF25@n7eUH#a1-)e&X}Eb2N=q(X!F!nyw7lE&
zKoPS4$`0`q1kc1`VgLFkp@5UkrbvL@%_Ym~TXBV(Nu!dNJs9mvA2|mZgAKf~BvxB&
zJ{j5H$p-VVVBgzYZb||%a3}38wy_Vf$La~iC=UYau{vF`%gFH2m
z_#Io>1F?glP7^cDhie7`IjWM;|31bU1IyRk?v7y8uQ!@WwW
zxOg#yUe8FPqh$I7se169Ps!>PTZod2KfYgU))t~t>gi{z-oTK8&|RVf$NtTkCJ;Px
zfta?y$$z|Lo0=8VS0g#K0PQrX1|Z56({-9m~<3UWo
z`;*N;!vAQd^EXP1Gx8LDT~l~8mcl9YIRQQr#hPI^k8EQXB^?avNNoA5AwBND5=-==
zg^DeP_cDtZ9<1kD9}NvISv8v95?>n=^5OSJug*iYyd{%O
zzpgmAcnRb5^?!>u?SpKhs1_*DbY7MgduP}CZC{SAp7uQF=GxlN5$_K=m-}YSHPp@R
z0`3FZ`tFQTGc>Vzu;?j1C~EU5+5FFD>Xg4vZ_U3ny)IZhd%`O98wKs2ayw4EY>!Eg
zvHhQPQwy^F!jt9Bp$TVsJ)-xO`5oJoe_Tw)_TjanGfyjDc}crY$j^GocR9G@bP+gt
zIec|L@V3XO{Qmpv@ey0|JdeHlTH2HP)UJ0$rhRhGLXX{MCwrA@eyyJ}bvihu#I-lv
zmfW=SsN`yMpVJYWKW+rAl0TEUucY_IJw@}pfcxK{)*ajTW&O!rkPSlj9Sm-j2ZhVI1BX9_(w;p~KY@tZ$RowK(3_|u%*vje8h
z%;j4*DLWxYqQ|P$~XIG@F$!7=bd41r?9ozkP
zr*3~85K|e~XnAVwbbP0l+XkK(i=GA
literal 0
HcmV?d00001