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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
} | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.