From f50dff818a37ce1cf3043bfbcbc724d87694e4a7 Mon Sep 17 00:00:00 2001 From: Torgeir Helgevold Date: Mon, 23 May 2016 22:24:15 -0400 Subject: [PATCH] docs(toh-pt6): add observables to HTTP tutorial chapter closes #1528 --- public/docs/_examples/toh-6/e2e-spec.ts | 23 ++- .../_examples/toh-6/ts/app/app.component.ts | 3 + .../toh-6/ts/app/dashboard.component.html | 2 + .../toh-6/ts/app/dashboard.component.ts | 4 +- .../toh-6/ts/app/hero-search.component.html | 11 ++ .../toh-6/ts/app/hero-search.component.ts | 53 ++++++ .../toh-6/ts/app/hero-search.service.ts | 19 ++ .../_examples/toh-6/ts/app/hero.service.ts | 4 +- .../_examples/toh-6/ts/app/rxjs-extensions.ts | 13 ++ public/docs/_examples/toh-6/ts/sample.css | 17 ++ public/docs/ts/latest/tutorial/toh-pt6.jade | 164 +++++++++++++++++- .../images/devguide/toh/toh-hero-search.png | Bin 0 -> 10942 bytes 12 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.component.html create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.component.ts create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.service.ts create mode 100644 public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts create mode 100644 public/resources/images/devguide/toh/toh-hero-search.png diff --git a/public/docs/_examples/toh-6/e2e-spec.ts b/public/docs/_examples/toh-6/e2e-spec.ts index 96277a910a..e2ab2602dc 100644 --- a/public/docs/_examples/toh-6/e2e-spec.ts +++ b/public/docs/_examples/toh-6/e2e-spec.ts @@ -23,10 +23,31 @@ describe('TOH Http Chapter', function () { addButton: element.all(by.buttonText('Add New Hero')).get(0), - heroDetail: element(by.css('my-app my-hero-detail')) + heroDetail: element(by.css('my-app my-hero-detail')), + + searchBox: element(by.css('#search-box')), + searchResults: element.all(by.css('.search-result')) }; } + it('should search for hero and navigate to details view', function() { + let page = getPageStruct(); + + return sendKeys(page.searchBox, 'Magneta').then(function () { + expect(page.searchResults.count()).toBe(1); + let hero = page.searchResults.get(0); + return hero.click(); + }) + .then(function() { + browser.waitForAngular(); + let inputEle = page.heroDetail.element(by.css('input')); + return inputEle.getAttribute('value'); + }) + .then(function(value) { + expect(value).toBe('Magneta'); + }); + }); + it('should be able to add a hero from the "Heroes" view', function(){ let page = getPageStruct(); let heroCount: webdriver.promise.Promise; diff --git a/public/docs/_examples/toh-6/ts/app/app.component.ts b/public/docs/_examples/toh-6/ts/app/app.component.ts index 2a1ff50ba3..d49c87ccbf 100644 --- a/public/docs/_examples/toh-6/ts/app/app.component.ts +++ b/public/docs/_examples/toh-6/ts/app/app.component.ts @@ -4,6 +4,9 @@ import { Component } from '@angular/core'; import { ROUTER_DIRECTIVES } from '@angular/router'; import { HeroService } from './hero.service'; +// #docregion rxjs-extensions +import './rxjs-extensions'; +// #enddocregion rxjs-extensions @Component({ selector: 'my-app', diff --git a/public/docs/_examples/toh-6/ts/app/dashboard.component.html b/public/docs/_examples/toh-6/ts/app/dashboard.component.html index 028eab6eb3..e22a2a5ebb 100644 --- a/public/docs/_examples/toh-6/ts/app/dashboard.component.html +++ b/public/docs/_examples/toh-6/ts/app/dashboard.component.html @@ -9,3 +9,5 @@ + + diff --git a/public/docs/_examples/toh-6/ts/app/dashboard.component.ts b/public/docs/_examples/toh-6/ts/app/dashboard.component.ts index 08ffecc0ea..f7b4100cee 100644 --- a/public/docs/_examples/toh-6/ts/app/dashboard.component.ts +++ b/public/docs/_examples/toh-6/ts/app/dashboard.component.ts @@ -5,11 +5,13 @@ import { Router } from '@angular/router'; import { Hero } from './hero'; import { HeroService } from './hero.service'; +import { HeroSearchComponent } from './hero-search.component'; @Component({ selector: 'my-dashboard', templateUrl: 'app/dashboard.component.html', - styleUrls: ['app/dashboard.component.css'] + styleUrls: ['app/dashboard.component.css'], + directives: [HeroSearchComponent] }) export class DashboardComponent implements OnInit { diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.html b/public/docs/_examples/toh-6/ts/app/hero-search.component.html new file mode 100644 index 0000000000..47c853746b --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.html @@ -0,0 +1,11 @@ + +
+

Hero Search

+ +
+
+ {{hero.name}} +
+
+
diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts new file mode 100644 index 0000000000..2b4d155046 --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts @@ -0,0 +1,53 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +import { HeroSearchService } from './hero-search.service'; +import { Hero } from './hero'; + +@Component({ + selector: 'hero-search', + templateUrl: 'app/hero-search.component.html', + providers: [HeroSearchService] +}) +export class HeroSearchComponent implements OnInit { + // #docregion subject + search = new Subject(); + // #enddocregion subject + // #docregion search + heroes: Observable; + // #enddocregion search + + constructor( + private heroSearchService: HeroSearchService, + private router: Router) {} + + + // #docregion search + ngOnInit() { + this.heroes = this.search + .asObservable() // "cast" as Observable + .debounceTime(300) // wait for 300ms pause in events + .distinctUntilChanged() // ignore if next search term is same as previous + .switchMap(term => term // switch to new observable each time + // return the http search observable + ? this.heroSearchService.search(term) + // or the observable of empty heroes if no search term + : Observable.of([])) + + .catch(error => { + // Todo: real error handling + console.log(error); + return Observable.throw(error); + }); + } + // #enddocregion search + + gotoDetail(hero: Hero) { + let link = ['/detail', hero.id]; + this.router.navigate(link); + } +} diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts new file mode 100644 index 0000000000..42018e3526 --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts @@ -0,0 +1,19 @@ +// #docregion +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroSearchService { + + constructor(private http: Http) {} + + // #docregion observable-search + search(term: string) { + return this.http + .get(`app/heroes/?name=${term}+`) + .map((r: Response) => r.json().data as Hero[]); + } + // #enddocregion observable-search +} diff --git a/public/docs/_examples/toh-6/ts/app/hero.service.ts b/public/docs/_examples/toh-6/ts/app/hero.service.ts index 8abbcc2778..04012768be 100644 --- a/public/docs/_examples/toh-6/ts/app/hero.service.ts +++ b/public/docs/_examples/toh-6/ts/app/hero.service.ts @@ -17,13 +17,13 @@ export class HeroService { constructor(private http: Http) { } - getHeroes(): Promise { + getHeroes() { return this.http.get(this.heroesUrl) // #docregion to-promise .toPromise() // #enddocregion to-promise // #docregion to-data - .then(response => response.json().data) + .then(response => response.json().data as Hero[]) // #enddocregion to-data // #docregion catch .catch(this.handleError); diff --git a/public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts b/public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts new file mode 100644 index 0000000000..a0facfe03e --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts @@ -0,0 +1,13 @@ +// #docregion +// Observable class extensions +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/throw'; + +// Observable operators +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/switchMap'; diff --git a/public/docs/_examples/toh-6/ts/sample.css b/public/docs/_examples/toh-6/ts/sample.css index 0c99008d2d..a5ac5b4d70 100644 --- a/public/docs/_examples/toh-6/ts/sample.css +++ b/public/docs/_examples/toh-6/ts/sample.css @@ -5,3 +5,20 @@ button.delete-button{ background-color: gray !important; color:white; } + +.search-result{ + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + width:195px; + height: 20px; + padding: 5px; + background-color: white; + cursor: pointer; +} + +#search-box{ + width: 200px; + height: 20px; +} + diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index 5f65c5a421..0b00284f5c 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -131,9 +131,10 @@ block get-heroes-details :marked The Angular `http.get` returns an RxJS `Observable`. *Observables* are a powerful way to manage asynchronous data flows. - We'll learn about `Observables` *later*. + We'll learn about [Observables](#observables) later in this chapter. - For *now* we get back on familiar ground by immediately converting that `Observable` to a `Promise` using the `toPromise` operator. + For *now* we get back on familiar ground by immediately by + converting that `Observable` to a `Promise` using the `toPromise` operator. +makeExample('toh-6/ts/app/hero.service.ts', 'to-promise')(format=".") :marked Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ... not out of the box. @@ -358,6 +359,146 @@ block review figure.image-display img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editting w/ HTTP") +:marked + ## Observables + + Each `Http` method returns an `Observable` of HTTP `Response` objects. + + Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller. + In this section we learn to return the `Observable` directly and discuss when and why that might be + a good thing to do. + + ### Background + An *observable* is a stream of events that we can process with array-like operators. + + Angular core has basic support for observables. We developers augment that support with + operators and extensions from the [RxJS Observables](http://reactivex.io/rxjs/) library. + We'll see how shortly. + + Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`. + That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller. + + Converting to a promise is often a good choice. We typically ask `http` to fetch a single chunk of data. + When we receive the data, we're done. + A single result in the form of a promise is easy for the calling component to consume + and it helps that promises are widely understood by JavaScript programmers. + + But requests aren't always "one and done". We may start one request, + then cancel it, and make a different request ... before the server has responded to the first request. + Such a _request-cancel-new-request_ sequence is difficult to implement with *promises*. + It's easy with *observables* as we'll see. + + ### Search-by-name + We're going to add a *hero search* feature to the Tour of Heroes. + As the user types a name into a search box, we'll make repeated http requests for heroes filtered by that name. + + We start by creating `HeroSearchService` that sends search queries to our server's web api. + ++makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".") + +:marked + The `http.get` call in `HeroSearchService` is similar to the `http.get` call in the `HeroService`. + The notable difference: we no longer call `toPromise`. + We simply return the *observable* instead. + + ### HeroSearchComponent + Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`. + + The component template is simple - just a textbox and a list of matching search results. ++makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html') +:marked + As the user types in the search box, a *keyup* event binding calls `search.next` with the new search box value. + + The component's data bound `search` property returns a `Subject`. + A `Subject` is a producer of an _observable_ event stream. + Each call to `search.next` puts a new string into this subject's _observable_ stream. + + The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there. + + But `heroes` is an `Observable` of heroes, not an array of heroes. + The `*ngFor` can't do anything with that until we flow it through the `AsyncPipe` (`heroes | async`). + The `AsyncPipe` subscribes to the observable and produces the array of heroes to `*ngFor`. + + Time to create the `HeroSearchComponent` class and metadata. ++makeExample('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts') +:marked + Scroll down to where we create the `search` subject. ++makeExample('toh-6/ts/app/hero-search.component.ts', 'subject') +:marked + We're binding to that `search` subject in our template. + The user is sending it a stream of strings, the filter criteria for the name search. + + A `Subject` is also an `Observable`. + We're going to access that `Observable` and append operators to it that turn the stream + of strings into a stream of `Hero[]` arrays. + + Each user keystroke could result in a new http request returning a new Observable array of heroes. + + This could be a very chatty, taxing our server resources and burning up our cellular network data plan. + Fortunately we can chain `Observable` operators to reduce the request flow + and still get timely results. Here's how: + ++makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".") +:marked + * The `asObservable` operator casts the `Subject` as an `Observable` of filter strings. + + * `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds + before passing along the latest string. We'll never make requests more frequently than 300ms. + + * `distinctUntilChanged` ensures that we only send a request if the filter text changed. + There's no point in repeating a request for the same search term. + + * `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet. + It discards previous search observables, returning only the latest search service observable. + +.l-sub-section + :marked + The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) + (formerly known as "flatMapLatest") is very clever. + + Every qualifying key event can trigger an http call. + Even with a 300ms pause between requests, we could have multiple http requests in flight + and they may not return in the order sent. + + `switchMap` preserves the original request order while returning + only the observable from the most recent http call. + Results from prior calls will be discarded. + + We also short-circuit the http call and return an observable containing an empty array + if the search text is empty. +:marked + * `catch` intercepts a failed observable. + Our simple example prints the error to the console; a real life application should do better. + Then it re-throws the failed observable so that downstream processes know it failed. + The `AsyncPipe` in the template is downstream. It sees the failure and ignores it. + + ### Import RxJS operators + The RxJS operators are not available in Angular's base `Observable` implementation. + We have to extend `Observable` by *importing* them. + + We could extend `Observable` with just the operators we need here by + including the pertinent `import` statements at the top of this file. + +.l-sub-section + :marked + Many authorities say we should do just that. +:marked + We take a different approach in this example. + We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file. + ++makeExample('toh-6/ts/app/rxjs-extensions.ts', null, 'app/rxjs-extensions.ts')(format=".") +:marked + We load them all at once by importing `rxjs-extensions` in `AppComponent`. + ++makeExample('toh-6/ts/app/app.component.ts', 'rxjs-extensions', 'app/app/app.component.ts')(format=".") +:marked + Finally, we add the `HeroSearchComponent` to the bottom of the `DashboardComponent`. + Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles. + At some point it might look like this. + +figure.image-display + img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") + .l-main-section :marked ## Application structure and code @@ -381,6 +522,10 @@ block filetree .file hero-detail.component.css .file hero-detail.component.html .file hero-detail.component.ts + .file hero-search.component.html + .file hero-search.component.ts + .file hero-search.service.ts + .file rxjs-operators.ts .file hero.service.ts .file heroes.component.css .file heroes.component.html @@ -407,7 +552,8 @@ block filetree - We extended HeroService to support post, put and delete calls. - We updated our components to allow adding, editing and deleting of heroes. - We configured an in-memory web API. - + - We learned how to use Observables. + Below is a summary of the files we changed and added. block file-summary @@ -430,3 +576,15 @@ block file-summary in-memory-data.service.ts, sample.css` ) + + +makeTabs( + `toh-6/ts/app/hero-search.service.ts, + toh-6/ts/app/hero-search.component.ts, + toh-6/ts/app/hero-search.component.html, + toh-6/ts/app/rxjs-operators.ts`, + null, + `hero-search.service.ts, + hero-search.component.ts, + hero-search.service.html, + rxjs-operators.ts` +) diff --git a/public/resources/images/devguide/toh/toh-hero-search.png b/public/resources/images/devguide/toh/toh-hero-search.png new file mode 100644 index 0000000000000000000000000000000000000000..f09fd45d7effd039bec1813174d823fd7a1fd204 GIT binary patch literal 10942 zcmdUVbySMVO`+aAvyVm{wyz6GIByaZ2-us=I%-+v_=1tVoQN_oh#z8|v!&g^RHb8x!qTcHu zOw^}L^bc$_H1x1%N=ka_N=l4+e%{W{Je<(bI5Qn=Z6B-ia1Pno+1d_`b93SN1sWtI zq!`$C_rb^EeT@CK1B}J_R#xkjMC*N#Bw_7?a8NnlkEk(?^FP6}Z$E|oB^RxK?PtO! z*?I{@57cn;6~w?`!OAEqG$<^@b=*T+k{o^nVUxeYW3+7PfT8uvqB|y&qSJvK*U%eh znSK&7z97yY?+2KW5O3o-YN9Ox)QLW0CKM6aSw=1nQzdfAf5sZ(O`w5C&h|liR^SHT zG9%m7G20|6`K9E`2C@q%sPX);h(`H?_*?lo`SXP+`OO6P1;uk=Ca)q%CHE+d$erH0 zyy6rGD2W>PNXWI6HnVYXNQU{J9+If*2QeQr<3C*j++~=}wDlO3 zy#1UQMR>t&aL7!|WOm;3LV$7Z@1G8z{umMm90t<6<;Fq9KH|cQ{)7epOC1`4;}k>9e3iw2%O6 zK$2j90*(P|2A+uCU42yqIcyO~Jdz!GChpU;K)ss}#=!^HFAeoo`c;hg8QlOprr^naD%k=fl zll(rq40f-(b*2E(=#&KoKerwA_E*EMsyhbs#%KS02sUkEeZ-#RNUw?>;Xa_qS6ZzV3`tOnD3KXq#E! z?WDgsfDa*BUdL4&c)WYkec?2pQ&O|4TN}bJbfTp7d=*oZLWMW2!ZOwVn>3-oo-W+> zGdzWI0isfbzgMqa`aQotnZrMb-YkB%T5Ewm#Y#MW;`8e1e4l)avOnfW4C^8erl#T| z@$|)UzEgwxXaSn&)1D~4`j3RKX72+Co<>k|jhi28`4Heage^jBT({2{WFzG=qnGR- z+1o9dM$e~mGvFyP5ZXHLa`nD>jAryXc_eMP$x&X`>0J?3kcpxnHNaYGrQ*bHab9{O z9AP9YP6`~M4CJQ~NhQC!b`pxIPLly5q=^4+*<$q-a@Wucx2p}SVcA=)5c%!b`XaN>DbW|oFLgx4 zPo(=kx*V498U0p((4OlGBQ5v?HgI^;+bRQrzw)d6HQhWPDVD9K{iT=sd8`1F`S=J) z{d4;{pKbNm^4#I|C6>w0e0agLi>c=)i^)$0Og!kgyB>x4UJI>+oW}5wyd%ENI>jD$TfT&zyVNQZs|q;K45oyqzTr+&bYJ2pHuEyZqWYn+s|3rh?W{xUyi(z zgk9WRT1U1ed$pOa`$G~tciF>< zfjUwd6@&qg*@{UGtDpIj%l_O1$W(kHGhgI2DOVoxn3*9DW(#HxPgg+y@wPW5?CwzJ zjGcVIIeH{YKW16xn`g`~M-I$`$&A*-M;o17VdTKcytVl*5$U1q7CITy<@Q_cpcfJ3 z)8(R{l=^50+W6TgzjH1#8urauu`~H`(~EcR_7w8rtD16zU=f^~w>gV+Gmrt@q_D!R z>%WgRzprJtAECjBrlf^6JRindn;{PGB<3Yja~ypPn@h^qF8H2mx2s7=O!~o#N5%wO ztqrfax1&#^xo>Cgy}ZwrY%y;gGyNKTW|j}vi7V)Nhd%V>a7+_3k6SbT=E^DBSff0& z17aiIG@X@E5dJIYB#JOSPc%c@+qT$byKjRmh@zO0C%0EZ06@m9_t&?k!AxJ2*&g9e zW{vnY1!Zb^G%ITX9ElOQT&jXB`SCx$B<(PsraNi~4FE2zH(zwUe$K!ycmAwpJM?6z z9eJiV43QLgL0IvPv_C1}f{TZ5%)X7sFtq;7?mMzYRU0CES?*5H0Q3^X#ng7^DI{B) z3y;T&%WEh>s87bs#B?JX*M_lGFJMiUb?*{du9@RtX_P>ijt{`Ph6{_$GFwvdodAju zrD3K41Q7}6UbmmEJ|n1cLAMC4=`WFj6_tV*@oysp5n z`1S#QG-pzq{N`H^LRaqGh+(Y*(UmQ3(|pJ?{9UKax7|K~m%1O*xPI2-8)BaQyVagg zFsvo=bx7wbB7KxXcUY)M2Lq1gU>=;QP-1g#f_whG5whN2re5QioiHm$7TJZS#eSZ~ zFxRQV_C{xBMIyQu5lL|F)xTO6=TWe%o#@Qv#}C(X3S0-Rh84Yh7P6qpYprYNE6a;0 z>$K*ZR_tQ{~Qb|t@l3hId04@(By0v{`_qcWM(kq<8_Raax6a;aqG zh_N6bRWTBKg%j3Br&3CQ%E3R-9-9t>gmKW>eJV0`Ur<6N8R5VW7}Zv6EK{7QOq{A% z@ZAo07>P#GN>J*TlOkV1WD5|(dFB=W|H>$oPRz0P?ODVfUB+04;<2$vr8pmpk2H9y z2yGdU<8<|8?(O|8yU*1<`u%N7eMafau@=|B#WjIwcOhmg%~6)AGi$ZjG`X9<*!zT( z7$V?G zE`n5SBS3Sn;0Nn`XInHar`d8+=mJ%pXXiE>rgHa<(z4U{Bs}*f0)jX5tiKPshWyL# zG0D4^O3tj3z~aXbn)1~5cUL79_k8zMYjO8jUJ<8NtvOv$-raZ9gor<+5x;68U<|tV zI3MnjV6T^VPo8%ECci(|_1a30XbfBV3cZ~K{ltY9kWdCiGF(P549fjcDrCzJM^-3M zrh40-eswuZ?`EU;vs8|pp+&S7I9qa^tw>_2^q;#T4YW5L~5YsWbTvL1IH?HCU-L!{< zFuXN#m)vs!65yk!ik(MX>+bLG!w(_?%iR#KXlz0Wtq{Gudjs36Y`8YgKuYL0Mjuxo zZ%cBgw*+(81gL`^`j+YRhxQI_3L1-E8ft6Of@NKRX+#fzS=h|uF-j;ue%1eYt)Zra z>7hjmfTQY`+}j1>*=Fiud0HwB4S7whVtgn3{%EN#j*9EEq;A<8^o&V*wT`-PsqZ(geb?!Bxdn31Q8fe|y1*Wp-T8G> zw?u|hi=H=ht0wo?Y3#H z`e9?~GcWs~S65eV?Cp!>Dv?ChC0G;Aj1i@5wg{Y_lEt*ZA&McJF-QMjTe}i!_RfCf z+}vQbXWw=OQl8iWSpkCs*B< z(fD_~7>a+zttjNsL@*}%Zb5Kt(@?xFTdgad1)&$%(F9K@d28Zb4+BaDZW@xi!m>`K zThmJeI+<45g0V2R@&s01*TK6aqlf2(0FMEe=WyzNc8-u*Nk(I8mH05G5xb~W=NZ3| z>w);d4SXDAfvx|t9n-fw&4Klz@Pzr=toeIaV%)3_%*CH3`-1vBeDRqho=n9=DWl9( zx=du&ncE`GmfI&fPBMmu2}2<>rz`Ds3re0#p2}-@y$Nex+ z=Y1Xp10qID!{4n9QpZ5APflLg{g>%WtdKUQ&&hFNlkMBw>B9E&qHEu?SJhmpRV8bK zNE3kX`>*8Zy&NWsuF#{$Pa3$oUSgzsy@dKKMh^rr#BuPl1pg3?TB5gVdqJSmbh7Z{ z=WG;ko*>)-c$g@Ehg~?{yL+fA1vbO2XgrJuxfOYdjb-*KlBe)mwR@>*oCQYj9ERO!O2x4S53sS&ZXe~fG&L2A z+!thJcA+V1^2s}A^;L@YJLcJ6tFgxR5EK!Y&(`n{n?YY{RQ?$O31dPriSmc4PLx|7 zZJ#NEu=&35sJSaflu((2)hQ~g*P>s$^w62S)YKNSeAipPz2Afm0p_*@49*Nx6;emf zj$7$mPB)VA@Q@nA=-ni8_y87P4eCwZ)~=|3;jrk10yWI_Z-2*b!Kk==mXBh*nNoK4 zlI8P<9G;iG`9Sf@2Kgm9pBahsWibSE>bqxr-+_EUIPp1CjzO=nfks9k`#0|N0ZrlB zfsJ39q4bWG#-s`DlUrHs7*wf^q;4F<5&?^2MzZD>s%%#|x$8nIr5`|Z5nxS`Zta-q z0NTr7rt00Ff!;sNvU;5|@h0@O3TXx-YiQ^O#Q|V2e1-!3tZRJt6~bqBuctue%TMZ# z(b!XS=kW1l!ypfh#C`VVF*?7Bqcj4c+`YH_GBS|!p9aVz?=KoZvvZKREK`iY!#!xC zRhG>KbF{I8*q1s|l~E2!aw^%p>&EdJg}mOOm=gQJ>^MVd0Fu|i&?G~FGYc-i#)T(U z;=n}Pcj3}v8)yN>t@p-lbIL?wCQW?*Jpq?xc>740&nvtu4tgw>k#GY}O3V^1%F)vKjS zC~{+)fADwWj`G(GZ*<(rgpLrJkkEb+#?NnKhA}9-^<)*^;w>b0EWMzqMLv5)phNZh zz5hZ}Jx9?WVkcUd^0Uon)M>NOH1_2 z<0f5q=o%F?gr$Mc6J}o#e)X|rqe|}X*;!>YLlp|48Z;wpquR7@r(-Ri#Ez!Z2if|i zI|RrFd9Zbss%lS`hjfHqVq)SLIviaUpBy+wnRGpS9#VCsJnTWOp2QkMoAO6zakNXu za`R0}WR!eekl}7c0r05<&G^eeJEprq$uDh*W4#!TLKTz^G}|4^G-PGo@hdeo%TFo4 zk1f_onCBr9KHI~tn(CJaCFEFq(11yDxmeY2p^yXfpEnr{h6w&BghYafJKJmii@!k0 z^rz{~1gBCgdLjT-CDVz*4E z=up(((66S`TRz1F9=70E@ z2pCUAW!<)~@pTTsD`ga#;*bu@2D^Fz-H?>(e|SF|k3= z961V`d!B8h(sUfNUiMctDa*qCEvZ283Z#2%$_ui#gbNdC9-W|(GNKL&!}7mi_`_ma zU*`SQ-t7$x=5smNR<7+5Sna`(XHv3KF}_u4`#5CM=BsPCOK@Ei&jHtJdFrieAQ1_d ztkovsH@NDokfe8$3OI1?ZM4Z;f3`S#yY!V}^(;n=LvWkIP8n9$9lQj?X{Ca4=;@7N{PG z5FYE^PCx%EIPoGm-#q^f^RsEyAZpkhigs(i;Itka;J@FeSF~eW5ixJ)_p_rNm=4(< zAQ=lbRh!O08a9cqQg(}Lecwz}9*HG9X0bnrl28eS%nr^;*3jI4a>U5s%r%+y+3wpH z_SA0Y)y(T={YEZu8vveu68k4=dErBgk6oBv+^?n*p_&H50EgL==*3l&VWAk!t*Vi( zKO^pY=UXZA)=IAUi+;MVv_EWu6N?fZnMHt^f{EgOuk)I}=1Ax8Es=U&YY1z*D)t=K zJy&=-gZFvbc`E6GGj{W=JPKyWIpp5-_!d%S&!n|>M@-}POJouw%SV66y>ST^bKtxf zf7{|rWNN`4oI`5gG+-yp#fqs$+iUwV`_NFISt7T67N(-G zEn1C$S)zbBk0BmY&Pm#UBa454eNa8K!w3$Qf9}5lY0uMt5Dc4TeCI_V&cCh7cDepPmT zhA3s`J`DDWC(m6t^YFzWImRp22nOK}suj;w#%=M@ul9WO0i82X+V;#Zs~2B%8qz7d zX2qVZU2&85yXb+lX)dHe~)~ZQ@)B;+h#^ zL(Dcj7d!fG(1|qT>C33^AGy`4!5ZqavUWft;EOV~m#4$r0E5f7h(O=qfYOmuy&!YC ziIm6NWG0evYq8_kkIMO2)a2P3CKC14pJw(YYy@Jh-}c7mWopBoxa%3cx=@SBJ!RH@ zo>jY_s~11p}0;s(++k&+zFuGizAOaw2|L6MVF#}e0}@#Kvk{tUDt z)i^q#UkSV9GbmprDGxgsYueg*VX~fCz8Z7oz|V6vsn1ID+A%ONga~#@T z?LJ+`gU3~WnU|TvQP~I< zj$VH&dO!ccJ3?&q)S~^v0f*ZjDl~Ygmi`6475$VzHJB}aJ^jcdTrzcW0f&A}+{dA# ze(pVUauVmeUGSJlWWbkX0~Ijvn-lI4B~P#}yP&iBr5>&+ZpGwwyD zqHj~$kPe%2<7zfza6FAp5#h!VF?k7hdYzS50Ril8=A&DHbzuQB(!dzjwk^AgdrLzg zKBljwGvQq$$GuH~qM?i#eL%@(MXdo{ss;Jj<9#IhL*?SKS=x!@LmxYo5p}Fw^o!RW z4Gr05NL%Y|ro<`fdLcV$zK||jBzH2z*@EBQUouBk>y19Enn7iSQYN|flSw5rA0Kxs-^H?F~sKGMfifr8>zLk%0@4?(gGqV1T)ck-4@Ng z9OuMh8CutgE~QbC^U{6o0=(Z7_wn)dbp3?Lx(< z2j;3uD)JQVUNT4RWX#D7tP|yMQCuM^XIJk?RqAiHS6|G?ls#+vI+p zOg=#`tFn9~&$Hgw^gD=~0)MvSWhv3`lNkb3o%uqkl!-p5YEajjnngo@B=+nAQuqq; zb=sRO3dRM@%VP^JI@~Jg&^0m1RnvgPbINR@0*(9$NQUD@wXo@$7UuhS`cX9`buBB^ zZ-u8~&wMs<;tDtQ$H~vqL2SeBS(mQ31HH{iV|HyE<9gnp4H%8Jfs%1NI#_m8Fl3;Q zhRl4v2iKt?YxT7m5w7qa`Sz!)ixKxB2sm?1LgTAO72ZgabX~o#J@5sj^k5tEk@fO* zY4E$AIjEo0^yX96gErqd@(=y~+eH!iIXUUJO)$e1J1=ft3vm-CMRikB=Mh8Q58FqT zJ01=)G>$5kVXTC46beAmY8@1kVd-F3zlXY0R^Ef2rGWyK z+B>HPXBBiEe*R3GGBNXeY4*YYxByT2yM!CM0_E74$P!UORlwl)Lg%C*Dpd zs;+OKL&1mq9Y_ntsOf`FbX<0bjiRk}i1)C`%vT%a7p!Y#X5-gJ<* z#|QL+GWwF(Fb6y})( z7b)+9X+uTZ9uZZ0gX#bGk)KxZT(L)J>w?M}$R6b1l^)p^we;DmNL1{a)m& zg$pZWss4cVXre0DK4xI18V1*P(MXMy2ajxjvl!>Fd!v-yN7vxDXO7yBS(vDF6^2q# zWib!=yIs@?*Jko}R&kwe&nkDnz{Bcvn9UqfHz08;YvK(1q2{38ga-ah?qTipOS*{k z^L)Agkyj0S3c|eX%JJ*R#pB$VeSmr{o@9!$S0#D&Ih|E|kVbK>Y+L+Nj&^^gRuw^4 zw}BCL9B%}`Gx|zA^LLk6l95I~`j?LItn;GLR<))wwJ!L0`{efJ-*ZzkrZap(SvC=+ z>KP7W^_a)bNN$!XA|OFJ+~Rr&KqR26AbZH~D?A zu8%1FazmK(A^>}e`5peMY)0hI3M$;_mu5og`{*F-h0>|?7+|wFxC!2sde6kjZ-jGB zIYQsI=LJKor(-BN1Vk73DBjsC zFzoJiDnFI^qULUI`9w=G~$_iDj{-t(A|Mb81~rf8c> zQFm7D^ZZG=d|e@Z{M3_s{SAW8f~eSOLT`O+Irbg&b8O1qE|Zt zk^F-pHFuvSQE({%t3;biO7AMgdO*T-Aav+>k=4c#m`3x!yDj0pN_D`VsU!N6cr+Ck z_9%E7PrcU19!2~f5>+!mhg&FCLNU3!)LuK$m|2V(cKoDAZG{whHV~<}iTW|dcH*Ny zYJ^fqyMPc+svlC4tE}VDg^o(g&sXNZk?Dy#Eq)Kg~+c8jRntR5#^RZuXRbCFeVHrD7v@*b5uM^7=fECjvRT zlu#wrQgr&o7}-FMD3~s5@4jKL3^TwcKf(DZdb9~=S=&VY0F8X75&tAYP{HyO_p`GN z?Vt+bDstlP(>|7w-l+3$a>^}>3Zhi-&UXKisMkY^>i)+$qY3QX860(!FFqv=pm;H( zbQC|QwX7g&Dz}^nx1dZl?j~jO|DrA_Y+yDNH682z?MRPQQLKUbd%v?VAi`=&pKot& z_%dUTwW)Oyh4^23`P)@rbI;(AC%I}E2-CdJ1lMU58`YGcSQ2OXQgk3Y@<3|Z6Fuw= zQ%nqnH91VAtfV=;$*C8r-;E_S8I=XAM9V2~`)m*k8J>n@@jQ@j?_YuI%!-?fS z0SVN=WUi0cO+m|Ax4r1}NlfFPFffSZT^%pT3C4Vn+fN|?*(S?c`&^8E@#6c2k0eo1 z32PhKb3pBTboL>s>>rhKY>++++7H(6t~$e!uz8E#UsO<333kt%>!$t&RB<4F$Z}*8 z5_r|#vXl?=F=vhTCned76Iee~i5EzdR~{i#lz!~>QKB+(>Fs2+h_;|`JLZWWQy9X; zmyKd%TU{4_3iF(Gy6MoAA>x^*u>r;2Ll~F8ljeW9zT;B-v>%Ur&?~@_D}@|qr5TN_ zcMmz|d{C-s=h&+&u%+I&q+`E^aN@)%qk6Z(KNX16H?e01`pSQ|^klKfq`&vacSWxy z=U|M(;J?Kh8rX^uB&_574{ru?M@g|XoIZaJoh)%8%A*QY0hw9)JzW?6d>?PbFB`fo zdylEE}?TQu~>v!&;8iORuxGnWls z(?U^K@r#^)R*UVSMje%CmnDXPeAP zyVwKY%iC5leoWeo&&VkzODFs#u!GOCUleWj(Ig97=|s2meE(AI2U@ofz?j8{Rqi;g zC;0{@2Nekkcvb#Y12g+kQ4vS--{5jL+Ez4F2e5b+FjtLcd_Ta`W>_|>0-`TtM!R}MFLG=>;?bnx)FqdG)K`GcZu)c*oK^=Wkg literal 0 HcmV?d00001