Programování gadgetu pro Google Wave - pokračování
od aichi
V minulém článku jsme za den vytvořili základní Google Wave gadget a dnes za neděli ho vylepšíme tak, aby splňoval rozšířené zadání.
Pokračování:
Den druhý - neděle
Schrnutí rozšířeného zadání: pozici může zadat každý návštěvník vlny a gadget musí umět zobrazit nejbližšího uživatele a všechny uživatele. V podmětech navíc je uvedeno, že uživatelé, jejichž prohlížeč geolokaci nepodporuje, by měli mít možnost zadat pozici ručně a jelikož je geolokace občas až moc přesná, mělo by být možné pozici pozměnít.
Teoretický základ budoucího kódování
Z minula jsme si připravili úložiště pozic uživatelů, které můžeme použít beze změny, to je super. Nicméně máme problém, protože vykreslování puntíku s pozicí máme v metodě stateUpdated() a ačkoli to v minulém případě nevadí (stav se změní pouze 1x, když vlastník zadá pozici), nyní by se nám při každé změně stavu body překreslovaly přes sebe. Pro umožnění uložení pozic všem uživatelům stačí vyhodit podmínku v metodě stateUpdated() pro schování tlačítka.
Pro zobrazení všech uživatelů využijeme API Google Maps a jejich třídy google.maps.LatLngBounds, která umí vypočítat bounding box, tedy obdélník obsahující všechny zadané body, a pomocí této instance lze jednoduše vycentrovat a zazoomovat mapu, bez vlastních složitých výpočtů.
Zobrazení nejbližšího uživatele je trošku složitější, protože musíme jednak spočítat vzdálenost mezi dvěmi body (nejlépe na elipsoidu, nebo alespoň na kouli) a správně zazoomovat a vycentrovat mapu tak, aby naše pozice byla uprostřed. Výpočet vzádlenosti jsem převzal ze stránky Calculate distances, viz budoucí metoda _computeDistance(). Pro správné napozicování mapy jsem použil trik. Moje souřadnice a souřadnice nejbližšího jsem zadal do LatLonBounds a nechal mapu tento výřez zobrazit. Na mapě jsme oba, nicméně já musím být uprostřed, takže změním střed mapy na mé souřadnice a jelikož zoom je konstantní a vždy je násobkem 2 předchozího, stačí odzoomovat o jeden krok a v průhledu se zobrazí i bod kolegy.
A nakonec zadání polohy ručně je pouze o tom, že na chvíli zapneme odchytávání události onClick nad mapou, aby mohl uživatel klikem určit svojí polohu.
Let's start
Nejdříve si připravíme HTML (rozšíříme původní), trošku ostylujeme a připravíme na oživení pomocí Javascriptu. Upravíme stylopis:
<style type="text/css" media="screen">
#map {height: 300px;}
#title {margin: 10px 0 5px 0;}
#messages {font-size: 11px; height: 13px; color: #c00;}
#legend {margin-top: 3px; font-size: 12px;}
#legend img {vertical-align: bottom;}
#positionFuzzyBox{ width: 360px; background: #ccc;
position: absolute; top: 200px; left: 20px; display: none;}
</style>
a HTML:
<div id="content">
<p id="title">Zobrazení polohy uživatelů</p>
<div id="controls">
<button id="addViewerPosition" onclick="app.addViewerPosition();">Přidej moji polohu</button>
<button id="findNearest" onclick="app.findNearest();">Najdi nejbližšího</button>
<button id="showAll" onclick="app.showAll();">Zobraz všechny</button>
</div>
<div id="messages"></div>
<div id="map"></div>
<div id="legend">
<img src="http://www.czechdesign.cz/open/gb15.png" /> moje poloha,
<img src="http://www.czechdesign.cz/open/rb15.png" /> poloha zadaná přes geolocation,
<img src="http://www.czechdesign.cz/open/yb15.png" /> poloha zadaná ručně.
</div>
<div id="positionFuzzyBox">
<p>Vaše poloha může být zaměřena až na 150m, vaší vaší polohu můžeme znepřesnit.</p>
<button id="fuzzy" onclick="app.fuzzyPosition();">Znepřesnit polohu</button>
<button id="ok" onclick="app.leavePosition();">Nechat polohu jak je</button>
<button id="close" onclick="document.getElementById('positionFuzzyBox').style.display='none';">Zavřít</button>
</div>
<script type="text/javascript">
//nas obsluzny kod
</script>
</div>
Nyní se pustíme do JavaScriptu. Nejprve si ukážeme kód výše zmíněné metody _computeDistance():
//konstruktor
function Application() {
...
this.R = 6371; // km, prumer zeme
}
/**
* vypocet vzdalenosti mezi dvema body na kouli - Spherical Law of Cosines
* http://www.movable-type.co.uk/scripts/latlong.html
*/
Application.prototype._computeDistance = function(position1, position2) {
var lat1 = position1.latitude;
var lon1 = position1.longitude;
var lat2 = position2.latitude;
var lon2 = position2.longitude;
var R = this.R;
var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +
Math.cos(lat1)*Math.cos(lat2) * Math.cos(lon2-lon1)) * R;
return d; //km
}
V konstruktoru nám přibyla konstanta určující střední poloměr země. Metoda přebírá dva parametry jako literálové objekty, kde pojmenované vlastnosti jsou latitude a longitude. Zeměpisné souřadnice očekáváme ve tvaru desetinného čísla.
Nyní je nejvyšší čas na implementaci metod, které jsou navázány na tlačítka. V konstrutoru zbindujeme nové metody pro nalezení nejbližšího uživatele a zobrazení všech:
//konstruktor
function Application() {
...
this.findNearest = bind(this, this.findNearest);
this.showAll = bind(this, this.showAll);
}
Metoda pro zobrazení všech uživatelů s uloženou pozicí je jak jsem psal výše jednoduchá:
//zobrazeni vsech na mape
Application.prototype.showAll = function() {
var llb = new google.maps.LatLngBounds();
var state = this.getPositionState();
for (var i in state) {
var position = state[i];
llb.extend(new google.maps.LatLng(position.latitude, position.longitude));
}
if (!llb.isEmpty()) {
this.map.fitBounds(llb);
} else {
alert('Pro zobrazení všech, musí být uloženy alespoň dvě pozice.');
}
};
Po naplnění instance google.maps.LatLngBounds pomocí interní reprezentace souřadnice google.maps.LatLng, stačí nad mapou zavolat metodu fitBounds() a mapa se nazoomuje a vycentruje tak, aby byly všechny zadané body viditelné.
Druhá metoda, pro zobrazení nejbližšího uživatele, findNearest() je trochu složitější, neboť nejdříve musíme nalézt nejbližšího, to znamená projít náš zásobník poloh uživatelů a pro každou zjistit její vzádelost od nás. Na nejbližšího pak aplikujeme trik popsaný výše na zvýrazněných řádcích.
//na klik na tlacitko zobrazeni nejblizsiho souseda
Application.prototype.findNearest = function() {
var state = this.getPositionState();
//zjistim vlastni polohu
var myId = this.createName(this.viewer.getId());
var myPosition = state[myId];
if (myPosition) {
var distance = Infinity;
var nearest = null;
for (var i in state) {
//nejsem ja
if (myId != i) {
var position = state[i];
var d = this._computeDistance(myPosition, position);
if (d < distance) {
nearest = state[i];
distance = d;
}
}
}
//nekoho jsem nasel
if (nearest) {
var myPos = new google.maps.LatLng(myPosition.latitude,myPosition.longitude);
var nearestPos = new google.maps.LatLng(nearest.latitude,nearest.longitude);
var llb = new google.maps.LatLngBounds(myPos, nearestPos);
this.map.fitBounds(llb);
this.map.setCenter(myPos);
this.map.setZoom(this.map.getZoom() == 1 ? 1 : this.map.getZoom()-1);
}
} else {
alert('Pro zobrazení nejbližšího, musí uložit svojí pozici.');
}
};
Vylepšení zadání pozice
Metodu addViewerPosition z minulého příkladu stačí trochu rozšířit a místo vyhození chyby, pokud nedetekujeme přítomnost geolokace v prohlížeči, raději nabídneme výběru pozice na mapě:
Application.prototype.addViewerPosition = function() {
if (navigator.geolocation) {
/* firefox geolocation is available */
navigator.geolocation.getCurrentPosition(this.showFuzzyBox);
} else if (google && google.gears) {
/* gears geolocation is aviable */
var geo = google.gears.factory.create('beta.geolocation');
geo.getCurrentPosition(this.showFuzzyBox, this._showGeoLocationError);
} else {
this.dom.msg.innerHTML = 'Vyznačte svou polohu kliknutím do mapy.';
this.clickableMap = true;
}
}
Zde vidíme pouze nastavení vlastnosti clickableMap na hodnotu true. K čemu je dobrá? Jak jsem psal výše, je nutno nastavit posluchače na událsot onClick nad mapou, tedy do metody init() přibyl tento kód:
Application.prototype.init = function() {
...
this.map = new google.maps.Map(document.getElementById("map"),myOptions);
google.maps.event.addListener(this.map, 'click', this._clickedPosition);
...
}
a do konstruktoru přibyla definice vlastnosti clickableMap a zbindování metody _clickedPosition(), kterou využívá listener výše:
function Application() {
...
this.clickableMap = false;
...
this._clickedPosition = bind(this, this._clickedPosition);
}
Z kódu výše je zřejmé, že jsem se rozhodli navěsit posluchače na začátku a v něm při kliknutí do mapy testovat, zda je nastaveno this.clickableMap = true. Pokud není, metoda nebude nic dělat:
Application.prototype._clickedPosition = function(event) {
if(this.clickableMap) {
this.clickableMap = false;
this.dom.msg.innerHTML = '';
var position = {};
position.latitude = event.latLng.lat();
position.longitude = event.latLng.lng();
position.id = this.viewer.getId();
position.state = 'manual';
this.setViewerPositionState(position);
}
}
V minulém příkladu bylo ukládání pozice uživatele na mapě zabezpečeno metodou setHostPositionState(), nyní mohou ukládat pozici všichni, proto je metoda přejmenována na setViewerPositionState
Application.prototype.setViewerPositionState = function(position) {
var position = position.coords || position;
//nastaveni pozice na mape
var p = new google.maps.LatLng(position.latitude, position.longitude);
this.map.setCenter(p);
var state = this.getPositionState();
var viewer = wave.getViewer();
state[this.createName(viewer.getId())] = {id: viewer.getId(), latitude: position.latitude, longitude: position.longitude, state: (position.state) ? position.state: 'geo' };
var serializedState = wave.util.printJson(state)+'';
var delta = {};
delta[this.stateName] = serializedState;
wave.getState().submitDelta(delta);
}
Jediná odlišnost je v tom, že na nově zaměřenou polohu vycentrujeme mapu. Další malou odlišností je, že si u každé polohy pamatujeme, zda byla zadána ručně, nebo pomocí geolokace IP adresy, tudíž je můžeme na mapě zobrazovat různou barvou (viz legenda).
Znepřesnění adresy
V metodě addViewerPosition() vypsané výše si můžeme všimnout, že při úspěšném zaměření adresy pomocí geolokace IP je volána metoda showFuzzyBox() a ne setViewerPositionState(), jako v minulém příkladě. Tato metoda zobrazí uživateli informační okénko s možností znepřesnění souřadnic:
//konstruktor
function Application() {
...
this.showFuzzyBox = bind(this, this.showFuzzyBox);
}
Application.prototype.showFuzzyBox = function(position) {
this.position = position.coords || position;
this.dom.fuzzyBox.style.display = 'block';
}
V informačním okně jsou dvě tlačítka, jedno pro znepřesnění zaměření a druhé pro použití souřadnic originálních. Obě metody jsou vypsány níže:
function Application() {
...
this.fuzzyPosition = bind(this, this.fuzzyPosition);
this.leavePosition = bind(this, this.leavePosition);
}
// znepresnuji pozici
Application.prototype.fuzzyPosition = function(){
this.dom.fuzzyBox.style.display='none';
var position = {}
position.latitude = this.position.latitude + Math.random()* 0.1 * (Math.random() > 0.5 ? 1 : -1 );
position.longitude = this.position.longitude + Math.random()* 0.1 * (Math.random() > 0.5 ? 1 : -1 );
this.setViewerPositionState(position);
}
// nechavam pozici namerenou tak jak je
Application.prototype.leavePosition = function(){
this.dom.fuzzyBox.style.display='none';
this.setViewerPositionState(this.position);
}
Vlastní znepřesnění na řádcích 11 a 12 je nejjednodušší možné, tedy náhodné přičtené/odečtení čísla v rozmezí 0.01 až 0.1 od souřadnice. Znepřesnění je zhruba až 5km, tedy vyhovuje zadání, nicméně by mohlo být řešeno ruční opravou polohy.
Překopání zobrazovacích metod
Jak jsem zmínil v úvodu, aplikaci se mění stav častěji než předhozí, prtože polohy může měnit více lidí. Proto je třeba oddělit uložení stavu od jeho vykreslení tak, že jedna metoda ukládá, a zobrazení je ponecháno až na metodě, která je vyvolána událostí změny stavu vlny. Metoda stateUpdated() volaná při změně stavu doznala od minula změn a je jednodušší:
Application.prototype.stateUpdated = function() {
this.host = wave.getHost();
this.viewer = wave.getViewer();
//zobrazeni pozic vsech
this._showPositions();
this.inited = true;
}
A metoda pro zobrazeni pozic se nám trochu nafoukla:
Application.prototype._showPositions = function() {
//smazani puvodnich znacek
this.clearInfosAndMarks();
//zobrazeni vsech na mape
var state = this.getPositionState();
for (var i in state) {
var participant = wave.getParticipantById(state[i].id);
//participant muze byt ulozen, ale uz nemusi byt ve vlne
if (participant) {
var position = state[i];
//nastaveni pozice na mape
var p = new google.maps.LatLng(position.latitude, position.longitude);
//zobrazuji poprve a jeste neni doinicializovano, tak vycentruji mapu na muj bod
if (!this.inited && this.viewer.getId() == participant.getId()) {
this.map.setCenter(p);
}
//zobrazeni ikony cloveka na mape
//zelene je ten kdo se diva, cervene jsou ostatni
var markerImgUrl = 'http://www.czechdesign.cz/open/rb15.png';
if (this.viewer.getId() == participant.getId()) {
markerImgUrl = 'http://www.czechdesign.cz/open/gb15.png';
} else if (position.state && position.state == 'manual'){
markerImgUrl = 'http://www.czechdesign.cz/open/yb15.png';
}
//zobrazeni puntiku
var m1 = new google.maps.Marker({
position: p,
visible: true,
title: participant.getDisplayName(),
icon: new google.maps.MarkerImage(
markerImgUrl,
new google.maps.Size(15,15,'px', 'px'),
new google.maps.Point(0,0),
new google.maps.Point(8, 8)
),
map: this.map
});
this.marks.push(m1);
var info = new Info(participant.getDisplayName(), participant.getThumbnailUrl(), m1, this.map);
this.infos.push(info);
google.maps.event.addListener(m1, 'click', info.click);
}
}
}
Metoda kontroluje, zda uložené souřadnice jsou uživatele, který je stále ve vlně a pokud je, tak přidáme jeho značku do mapy. Obrázkem rozlišujeme vlastní pozici, a pozice z geolokace a ručně zadané. Jelikož je na mapě více bodů, musíme pro vizitku udělat vlastní instance třídy Info.
Na začátku metody ještě voláme funkci pro vymazání původních značek. Je jednodušší vymazávat staré a při aktualizaci stavu vytvářet nové, než si pro každou pamatovat jakého uživatele je a jestli je ještě validní či nikoli. Značky ukládáme do pole this.marks a instance vizitek do this.infos.
function Application() {
...
this.marks = []; //pole vsech marku
this.infos = []; //pole vsech vizitek
}
// smazani starych bodu a info bublin
Application.prototype.clearInfosAndMarks = function() {
for (var i = 0; i < this.infos.length; i++) {
this.infos[i].destroy;
this.infos[i] = null;
}
this.infos = [];
for (var i = 0; i < this.marks.length; i++) {
this.marks[i].setMap(null);
this.marks[i] = null;
}
this.marks = [];
}
A konečně třída Info pro vizitky:
/**
* @class Info
* objekt tvorici vizitku po kliku na bod
*/
function Info (name, src, mark, map) {
this.name = name;
this.src = src;
this.mark = mark;
this.map = map;
this.click = bind(this, this.click);
}
Info.prototype.click = function() {
var i = new google.maps.InfoWindow({
content: '<div><strong>'+this.name+'</strong></div><img src="'+this.src+'" />'
});
i.open(this.map, this.mark);
}
Info.prototype.destroy = function() {
for (p in this) {
this[p] = null;
}
}
Závěr
Výše popsaný kód splňuje všechny požadavky na gadget dle zadání. Myslím, že to byl dobře strávený víkend, kdy jsem se lecčemu novému naučil. Psaní gadgetů pro Wave je celkem jednoduché a nejvíce času zabralo přelouskání dokumentace. Jediné co mě mrzí je, že jsem nestil nastudovat úpravy značek pro Google Maps API. Chtěl jsem nad puntík zobrazit i malou ikonku zobrazující o koho se jedná, ale jelikož výchozí značka neumí zmenšít zobrazovaný obrázek, nechtěl jsem zobrazovat značky 96x96px.
Zdrojový kód jsem zveřejnil na GitHub. Výsledky soutěže:
http://clanky.gug.cz/2009/12/vitezem-listopadove-online-dilna-gugcz.html
Adresy zpětných odkazů pro tento příspěvek:
Trackback URL (right click and copy shortcut/link location)
12. 02. 10 18.15:14, 

