sobota, 30 kwietnia 2016

JourneyPlanner #6.2 - Refaktoryzacja wyników wyszukiwania cz.2

Jest to druga część wpisu dotyczącego refaktoryzacji wyświetlania mapy i związanego z nią wyszukiwania. Dzisiaj opiszę jak zmodyfikowany został moduł Home. Bez wahania mogę powiedzieć, że zostaną wypunktowane również kolejne przeciwności losu jakie pojawiły się w wyniku tych zmian. Ale o tym na samym końcu. 


W ramach przypomnienia zamieszczę aktualną strukturę katalogów i plików wspomnianego modułu. Była ona zamieszczona pod koniec poprzedniej części:
│   config.ts
│   home.scss
│   index.ts
│
├───controllers
│       map.ts
│       searchResults.ts
│
└───templates
        index.html
        search-results.html

Pliki główne


Pliki z katalogu głównego nie zmieniły się znacząco. Plik index.ts przeszedł dość logiczne zmiany, został dodany nowy kontroler. Tak wygląda plik na obecną chwilę: 
Nie udostępniam listingu, ponieważ szkoda powierzchni strony na tak mało znaczące zmiany.
W pliku config.ts zmodyfikowałem stany, wygląda on tak:

export default function ($stateProvider:angular.ui.IStateProvider, $urlRouterProvider:angular.ui.IUrlRouterProvider) {
'ngInject';
$stateProvider.state('app.tabs.home', {
url: "/home",
views: {
'tab-home': {
template: require("./templates/index.html"),
controller: "HomeController as homeCtrl"
}
}
});
$stateProvider.state('app.search-results', {
cache: false,
url: "/search-results",
views: {
'content@app': {
template: require("./templates/search-results.html"),
controller: "SearchResultsController as searchCtrl"
}
}
});
$urlRouterProvider.otherwise("/home");
}
view raw config.ts hosted with ❤ by GitHub

Jak widać w linii nr 3 stan home jest podstanem zakładek, dzięki czemu nie jest konieczne ich pokazywanie w innych stanach nie związanych z zakładkami. Drugi stan i zarazem jego kontroler odpowiada za ukazywanie wyników wyszukiwania.

Szablony


W plikach szablonów również nie ma zbyt wielkiej filozofii. Założenie było takie aby podczas przebywania w akcji odpowiedzialnej za wyświetlanie mapy, w nagłówku widniała ikona lupy. Po kliknięciu w nią zostajemy przekierowani do stanu umozliwiającego wyszukiwanie za pomocą pola tekstowego umieszczonego w nagłówku. A więc mamy tutaj dwa stany: mapa oraz wyniki wyszukiwania. Jak widać ma to odzwierciedlenie w naszych plikach szablonów. W pliku index.html został usunięty znacznik odpowiedzialy za tytuł nagłówka, natomiast został dodany przycisk. Zaś wyniki wyszukiwania są całkowicie nowym szablonem. W nagłówku posiadają znane nam już pole tekstowe. Dodane zostało zdarzenie onkeyup aby wywoływać podpowiadanie podczas wpisywania fraz. Zaś w treści widnieje lista wyników pobierana z kontrolera.

<ion-view>
<ion-nav-buttons side="right">
<div class="item-input-inset">
<button class="button button-clear" nav-clear ng-click="homeCtrl.processSearch()">
<i class="icon ion-ios-search"></i>
</button>
</div>
</ion-nav-buttons>
<ion-content>
<div id="googleMap" data-tap-disabled="true"></div>
</ion-content>
</ion-view>
view raw index.html hosted with ❤ by GitHub
<ion-view>
<ion-nav-buttons side="right">
<div class="item-input-inset">
<label class="item-input-wrapper">
<i class="icon ion-ios-search placeholder-icon"></i>
<input type="search" ng-keyup="searchCtrl.search()">
</label>
</div>
</ion-nav-buttons>
<ion-content>
<ion-list>
<ion-item ng-repeat="item in searchCtrl.predictions">
{{item.description}}
</ion-item>
</ion-list>
</ion-content>
</ion-view>

Kontrolery


Zacznijmy od znanego nam już z poprzednich wpisów kontrolera HomeController

import {SearchService} from "../../app/searchService";
export class HomeController {
maps;
errorMsg:string;
$injector: ng.auto.IInjectorService;
$cordovaGeolocation: ngCordova.IGeolocationService;
position:ngCordova.IGeoPosition;
$searchService: SearchService;
$state: angular.ui.IStateService;
$ionicHistory: IonicHistoryService;
constructor(private $injector:ng.auto.IInjectorService,
public $scope:ng.IScope,
public $state: angular.ui.IStateService,
public $ionicHistory: IonicHistoryService) {
'ngInject';
this.$injector = $injector;
this.$scope = $scope;
this.$cordovaGeolocation = this.$injector.get('$cordovaGeolocation');
this.$searchService = this.$scope.$searchService;
this.$state = $state;
this.$ionicHistory = $ionicHistory;
this.$scope.$on('$ionicView.enter', () => this.onEnter());
}
processSearch() {
this.$ionicHistory.nextViewOptions({
disableBack: true
});
this.$state.go('app.search-results');
}
onEnter() {
let targetDiv = document.getElementById("googleMap");
this.$cordovaGeolocation
.getCurrentPosition(<ngCordova.IGeolocationOptions>{timeout: 10000, enableHighAccuracy: false})
.then((position) => {
console.log("position found");
this.position = position;
console.log(position);
if (this.$searchService.$position !== undefined)
{
var latLng = this.$searchService.$position;
}
else
{
var latLng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
}
var mapOptions = {
center: latLng,
zoom: 15,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
this.maps = new google.maps.Map(targetDiv, mapOptions);
console.log('show');
}, (err) => {
console.log("unable to find location");
this.errorMsg = "Error : " + err.message;
});
}
}
view raw map.ts hosted with ❤ by GitHub

Jak widać bardzo zubożał od ostatniego razu. I słusznie! W końcu to kontroler odpowiadający za wyświetlanie mapy i niczego więcej. Wyświetla mapę i wywołuje wyszukiwanie. W przyszłości będzie trzeba dodać jedynie zdarzenie centrujące mapę w danym miejscu na podstawie wyników wyszukiwania (tak jak było to robione ostatnio). Postaram się to zrobić zdecydowanie lepiej.

Kolejny jest kontroler , który załatwia bardzo dużo rzeczy i załatwi jeszcze więcej.

export class SearchResultsController {
maps:string;
$injector: ng.auto.IInjectorService;
$cordovaGeolocation: ngCordova.IGeolocationService;
position:ngCordova.IGeoPosition;
$state: angular.ui.IState;
bounds;
predictions;
query;
constructor(private $injector:ng.auto.IInjectorService, public $scope:ng.IScope, $state: angular.ui.IState) {
'ngInject';
this.$injector = $injector;
this.$scope = $scope;
this.$state = $state;
this.$cordovaGeolocation = this.$injector.get('$cordovaGeolocation');
this.$scope.$on('$ionicView.enter', () => this.geolocate());
}
loadBounds() {
this.$cordovaGeolocation
.getCurrentPosition(<ngCordova.IGeolocationOptions>{timeout: 10000, enableHighAccuracy: false})
.then((position) => {
var geolocation = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
var circle = new google.maps.Circle({
center: geolocation,
radius: position.coords.accuracy
});
this.bounds = circle.getBounds();
}, (err) => {
console.log("unable to find location");
});
}
search() {
this.$autocompleteService = new google.maps.places.AutocompleteService();
if(event.target.value.length == 0)
{
return false;
}
let ctrl = this;
this.$autocompleteService.getPlacePredictions({ input: event.target.value, bounds: this.bounds }, function(predictions, status) {
if (status != google.maps.places.PlacesServiceStatus.OK) {
alert(status);
return;
}
ctrl.predictions = predictions;
ctrl.$scope.$apply();
})
}
onEnter() {
this.loadBounds();
}
}

loadBounds()

Po uruchomieniu kontrolera wywoływana jest akcja loadBounds(), która ma za zadanie na podstawie naszej lokalizacji ustalić granice wyszukiwania. Jest to o tyle istotne, że podczas autopodpowiadania wyszukiwarka znajduje miejsca bliższe naszym okolicom. Co za tym idzie bardziej prawdopodobne jest, że mieszkając na śląsku i wpisując "Ka" w wyszukiwarkę chodzi nam o Katowice, a nie o Kair. Wspomniania metoda tworzy okrąg o średnicy zadanej dokładnością, który ogranicza przeszukiwany obszar.

search()

Kolejną metodą jest search() wywoływany w widoku zdarzeniem keyup, o którym już wspominałem. Głowna logika działania została zaczerpnięta z przykładów zawartych w dokumentacji.
Sprawdzamy czy pole tekstowe nie jest puste, a następnie jego wartość przekazujemy do usługi wyszukiwania, z granicami uzyskanymi w metodzie loadBounds(). Uzyskane w ten sposób wyniki umieszczamy w kolekcji przetwarzanej w widoku na pozycje listy.
Ostatnia linijka
ctrl.$scope.$apply();
jest bardzo diabelnie ważna! Informuje ona framework, że nastąpiła zmiana w wartościach przestrzeni kontrolera i należy odświeżyć widoki. Bez tego mogą pojawić się problemy w odświeżaniu pozycji listy.



Końcowy rezultat bardzo mnie zadowala. Aplikacja zaczyna przypominać interfejs, który może być śmiało używany na urządzeniach mobilnych:


Przeciwności losu

Został czas na podsumowanie. Wciąż nie jestem zadowolony z wyglądu kontrolerów i łamania zasady DRY. Prawdopodobnie w przyszłości powstanie więcej usług zarządzających lokalizacją aby nie powielać kodu w przypadku np. generowania granic wyszukiwania.
W następnym poście skupię się na tym aby w wyświetlaną listę podpowiedzi tchnąć życie. I zrobić coś co już raz wykonałem czyli nanoszenie wyników na mapę. Gdy to zostanie wykonane zabiorę się za punkt trzeci priorytetu MUST czyli pogoda dla danej lokalizacji.

Brak komentarzy:

Prześlij komentarz