Webes térinformatika - PaciTrip 6. rész (Új funkció: útvonalválasztó legördülő menü)

Webes térképes alkalmazás készítéséről szóló sorozatunk előző részében megoldottuk, hogy egy útvonalszakaszra vagy állomásra kattintva megjelenítsük a hozzátartozó alfanumerikus attribútum adatokat egy információ ablakban. A koncepciónk alapján a következő feladat, hogy egy legördülő menüből lehessen kiválasztani a megtekinteni kívánt utat. Eddig csupán egyetlen utazás útvonalát és állomásait láthattuk, így azonban lehetőségünk van váltani az egyes utak között. Elsőre talán nem tűnik nagy változtatásnak és ez olyan értelemben igaz is, hogy a felhasználó a felületen nem sok eltérést fog tapasztalni: az egyelőre állandóan nyitott legördülő menü elkezd majd működni. A háttérben azonban annál nagyobb változtatásokat kell eszközölnünk, de ahogy eddig is, most is lépésről lépésre fogunk haladni!

Technikai jellegű kitérő a sikeres tesztelés érdekében

A cikksorozat elkezdése óta a böngészők is fejlődtek, ezért ma már nem tudjuk olyan egyszerűen megtekinteni a saját gépünkről futtatott alkalmazást. Ennek oka, hogy a különböző típusú (~kiterjesztésű) fájlokat másként kezeli a böngésző, attól függően, hogy milyen úton-módon (protokollon) keresztül próbáljuk meg elérni. Amíg a böngésző számára leggyakoribb http vagy https protokollon keresztül próbálunk meg hozzáférni fájlokhoz, nagyon sok mindent lehetővé tesz. Azonban a saját gépünkről indítva az index.html fájlt, nem ezt a protokollt használjuk, hanem a file protokollt. Ezt használva a böngésző eléri és feldolgozza a .html, .css és .js fájlokat, a .geojson fájlokat azonban már nem! Emiatt azt láthatjuk, hogy az alkalmazásunk "üresen", geometriák nélkül nyílik meg.

Szerencsére van mód arra, hogy ezt a problémát orvosoljuk. Chrome esetében a legegyszerűbb a helyzet:

  1. Indítsd el a parancssort!
  2. Navigálj a Chrome mappájába! (pl. cd c:\program files (x86)\google\chrome\application)
  3. Indítsd el a Chrome alkalmazást az --allow-file-access-from-files flaggel kiegészítve! (chrome.exe --allow-file-access-from-files)
  4. Az így megnyíló böngészőben már engedélyezett lesz a .geojson fájl elérése is file protokollon keresztül!

Firefox esetében nem találtam ilyen flag-et, de ott is van néhány lehetséges megoldás a probléma kezelésére.

A bevált gyakorlatnak megfelelően először is fogalmazzuk meg egy (ha szükséges, összetett) mondatban!

Azt szeretnénk megvalósítani, hogy a webes térképes alkalmazásban az utazás választó gombra kattintva jelenjen meg az utazások listája, a lista egyik elemére kattintva pedig az adott utazás jelenjen meg az alkalmazásban: a neve és dátuma látszódjon a fejlécben, a neve látszódjon az utazás választó gombon, az útvonala és állomásai jelenjenek meg a térképen és az alkalmazás navigáljon abba a nézetbe, amely mutatja az utazás útvonalait és állomásait. Elvárás továbbá, hogy az összes funkció működjön az új útvonallal, ami egyelőre csak az információ ablakot jelenti, de később az időcsúszkának is alkalmazkodnia kell az új utazáshoz!

Nos, ez két mondat lett, mert nem akartam nagyon-nagyon összetett mondatot alkotni. Ám ez is csak azt mutatja, hogy lesz bőven tennivalónk! Következő lépésben szedjük kisebb részekre, így egyszerre mindig csak egy kisebb részfeladatra koncentrálhatunk és lépésről lépésre elérjük majd a kitűzött célt!

  1. "az utazás választó gombra kattintva"
  2. "jelenjen meg az utazások listája"
  3. "a lista egyik elemére kattintva"
  4. [az utazás] "neve és dátuma látszódjon a fejlécben"
  5. [az utazás] "neve látszódjon az utazás választó gombon"
  6. [az utazás] "útvonala és állomásai jelenjenek meg a térképen"
  7. "az alkalmazás navigáljon abba a nézetbe, amely mutatja az utazás útvonalait és állomásait"
  8. "az összes funkció működjön az új útvonallal", amit esetünkben így értelmezünk: "az információ ablak működjön az új útvonallal"

Ezek így (remélhetőleg) elég jól megragadható, kezelhető méretű és bonyolultságú feladatok lesznek. Viszont mielőtt nekilátunk, egy pillanatra álljunk meg és gondolkodjunk az előttünk álló feladatokon és hogy mi fog kelleni hozzá! A legtöbb esetben az utazás valamilyen adatára lesz majd szükség: név, dátum, útvonal, állomások, kezdő nézet. Mivel ezúttal több utazásról van szó, érdemes lenne ezeket egységes szerkezetben összegyűjteni minden utazás esetén, gondolva esetleg arra is, hogy mire lehet még szükség az alkalmazás továbbfejlesztésekor. Így a fenti listához még hozzáadunk egy pontot: 0. az utazások adatainak összegyűjtése, és rögtön ezzel a feladattal kezdjük a munkát.

0. Az utazások adatainak összegyűjtése

A utazások adatait egy-egy Javascript objektumban fogjuk eltárolni. A Javascript objektumban kulcs-érték párok találhatók, vesszővel elválasztva, az objektum kezdetét és végét pedig kapcsos zárójelek jelzik. A Javascript objektumok ugyanúgy tárolhatók változókban, mint a számok, szövegek és logikai értékek. Egy-egy utazásról az alábbi adatokat szeretnénk tárolni:

  • name (név): szöveges adat, amely tartalmazza az utazás nevét és hogy mettől meddig tartott. Ez az információ kerül majd a fejlécbe.
  • shortName (rövid név): szöveges adat, amely az utazás nevét tartalmazza. Ez az információ kerül majd az utazás választó gombra, ha az utazást kiválasztják.
  • days (az utazás napjai): tömbben tárolt szöveges adatok, amelyek az utazás napjainak felsorolása éééé.hh.nn. formátumban. Ezt egyelőre nem használjuk de az időcsúszka számára hasznos lesz. (Kicsit előredolgozunk.)
  • initView (az utazás kezdő térképi nézete): két részből álló tömb: először a nézet középpontjának koordinátáit tartalmazza egy tömbben, utána pedig nézet zoom szintjét. Ezt az információt használjuk a térképi nézet beállításához.
  • stopsGeoJSON (az állomások téradata): az állomások téradat fájljának relatív elérési útját tartalmazza. Ezt a fájlt hívjuk majd be a térképre, amikor új állomás réteget készítünk.
  • routeGeoJSON (az útvonal téradata): az útvonal téradat fájljának relatív elérési útját tartalmazza. Ezt a fájlt hívjuk majd be a térképre, amikor új útvonal réteget készítünk.

A NordTrip objektuma például így néz ki:

let nordtrip : {
    name : 'NordTrip - 2018.06.15-29.',
    shortName : 'Nordtrip',
    days : ['2018.06.15.', '2018.06.16.', '2018.06.17.', '2018.06.18.', '2018.06.19.', '2018.06.20.', '2018.06.21.', '2018.06.22.', '2018.06.23.', '2018.06.24.', '2018.06.25.', '2018.06.26.', '2018.06.27.', '2018.06.28.', '2018.06.29.'],
    initView: [[16.074259, 64.260138], 5],
    stopsGeoJSON : 'nordtrip/stops.geojson',
    routeGeoJSON : 'nordtrip/routes.geojson'
};

Talán feltűnt, hogy eddig a var kulcsszóval deklaráltunk változókat, most viszont a let kifejezést használom. A Javascript nyelvben jelenleg mindkettő használható, a var a régebbi, a let (és a const) újabb(ak). A két fő különbség a var és a let között:

  1. A var segítségével deklarált változó function szinten érhető el, míg a let segítségével deklarált változó csak block szinten (a block itt kapcsos zárójelek {} közötti kódrészletet jelenti) érhető el, így pontosabban lehatárolható, melyik változót hol is szeretnénk használni.
  2. A var segítségével létrehozott változót azelőtt szeretnénk elérni, hogy deklaráltuk volna, akkor undefined értéket kapunk, let deklarálás előtti használata esetén azonban hivatkozási hibát (ReferenceError), aminek számos előnye van, például segít, hogy a hibánkat a megfelelő helyen keressük.

Visszatérve az utazásokra, több megoldási lehetőség áll előttünk. Tárolhatjuk mindegyik utazás objektumát egy-egy külön változóban, vagy (és én ezt a megoldást használom) tárolhatjuk mindegyiket egyetlen objektumban (trips), ahol a kulcs lényegében az, ami a változó neve is lenne, a kulcshoz tartozó érték pedig az előbbi objektum.

$(document).ready(function(){
    
    // Utazások adatai
    const trips = {
        nordtrip : {
            name : 'NordTrip - 2018.06.15-29.',
            shortName : 'Nordtrip',
            days : ['2018.06.15.', '2018.06.16.', '2018.06.17.', '2018.06.18.', '2018.06.19.', '2018.06.20.', '2018.06.21.', '2018.06.22.', '2018.06.23.', '2018.06.24.', '2018.06.25.', '2018.06.26.', '2018.06.27.', '2018.06.28.', '2018.06.29.'],
            initView: [[16.074259, 64.260138], 5],
            stopsGeoJSON : 'nordtrip/stops.geojson',
            routeGeoJSON : 'nordtrip/routes.geojson'
        },
        swisstrip : {
            name : 'SwissTrip - 2017.08.14-24.',
            shortName : 'SwissTrip',
            days : ['2017.08.14.', '2017.08.15.', '2017.08.16.', '2017.08.17.', '2017.08.18.', '2017.08.19.', '2017.08.20.', '2017.08.21.', '2017.08.22.','2017.08.23.', '2017.08.24.'],
            initView: [[12.589709, 47.526508], 7],
            stopsGeoJSON : 'swisstrip/stops.geojson',
            routeGeoJSON : 'swisstrip/routes.geojson'       
        },
        iretrip : {
            name : 'IreTrip - 2016.10.24-31.',
            shortName : 'IreTrip',
            days : ['2016.10.24.','2016.10.25.','2016.10.26.', '2016.10.27.', '2016.10.28.', '2016.10.29.', '2016.10.30.','2016.10.31.'],
            initView: [[-7.865367, 53.5], 7],
            stopsGeoJSON : 'iretrip/stops.geojson',
            routeGeoJSON : 'iretrip/routes.geojson'
        },
        eurotrip : {
            name : 'EuroTrip - 2015.08.17-27.',
            shortName : 'EuroTrip',
            days : ['2015.08.17.', '2015.08.18.', '2015.08.19.', '2015.08.20.', '2015.08.21.', '2015.08.22.', '2015.08.23.', '2015.08.24.', '2015.08.25.', '2015.08.26.', '2015.08.27.'],
            initView: [[10.978365, 49.502767], 6.5],
            stopsGeoJSON : 'eurotrip/stops.geojson',
            routeGeoJSON : 'eurotrip/routes.geojson'
        }
    };

Ebben az esetben még csak nem is let kifejezést használtam, hanem a már futólag említett const kifejezést. E kettő között csupán annyi a különbség, hogy a let segítségével deklarált változónak az értéke később megváltoztatható, a const segítségével deklarált azonban nem!

Ez azonban nem azt jelenti, hogy valóban konstanst (állandót) hoznánk létre. A Javascript logikája szerint ha a változó egy objektumra mutat, és mi megváltoztatjuk az objektum egyik kulcsához tartozó értéket, attól az objektum még ugyanaz marad. Vagyis ha const segítségével deklarálunk egy változót, amelyben egy objektumot tárolunk, akkor nem társíthatunk a változóhoz egy másik objektumot (sem számot, szöveget, stb.), viszont az objektum tulajdonságait megváltoztathatjuk. A var, let és const különbségeiről részletesebben olvashatsz Tyler McGinnis cikkében. (Bátran keress a témára, rengeteg cikket fogsz találni, több átolvasása után nekem ez tetszett a legjobban, mert kellően részletes és példákkal alátámasztva, jól elmagyarázza a lényeget.)

A korábbi kódot ebből a szempontból átnéztem és módosítottam, a var kifejezéseket ahol csak lehet let vagy const kifejezésekkel helyettesítettem.

Az utazásokat tartalmazó objektumot rögtön a kód elején, a $(document).ready() sora után helyezzük el, hogy a kód további részében már elérhető legyen.

Az utazásokat Javascript objektum helyett tárolhatjuk egy JSON fájlban is, amely struktúrájában nagyon hasonlít a Javascript objektumra, azonban felépítését és formázását tekintve sokkal szigorúbb. Ha egy külső fájlban tároljuk az adatokat, akkor a map.js fájlunk elején be kell olvastatnunk (pl. egy .ajax() hívással) azt, hogy elérhető legyen. Ennek a megoldásnak előnye, hogy külön választjuk az adatot és az azt feldolgozó logikát, így ha csak adatot kell módosítanunk, akkor véletlenül sem módosíthatjuk a működést biztosító kódot. Továbbá ezzel teszünk egy lépést afelé a korszerűbb megoldás felé, hogy az adatokat adatbázisban tároljuk és azokat csak szolgáltatásokon (service-ken) keresztül érhetjük el. Ha a service megfelelő struktúrában szolgáltatja az adatokat, akkor a feldolgozó kódon nem is kell változtatni ahhoz, hogy az alkalmazás továbbra is működőképes maradjon.

1+2. Kattintás eseményfigyelő és eseménykezelő hozzárendelése az utazás választó gombhoz

Jelenleg mind az utazás választó gomb, mind az utazások listája "bele van égetve" a kódba. Habár ez ebben a formában is működőképes, egy újabb utazás hozzáadásával nagyon sok helyen kellene módosítanunk a kódot (hozzáadni egy újabb elemet a listához, megváltoztatni a gomb feliratát, stb.), tehát több a hibázási lehetőség. Ezért olyan megoldást kell kidolgoznunk, amely az utazások adatai (a trips változóban tárolt objektum) alapján építi fel a felhasználói felületet. Ezért az index.html fájlból törölni fogjuk a #tripSelector div tartalmát, miután a map.js fájlban készítünk egy function-t (createTripSelector), amely

  1. összeállítja az utazás választó gomb HTML kódját és beleteszi a #tripSelector div-be,
  2. a trips objektum alapján összeállítja az utatások listájának HTML kódját és beleteszi a #tripSelector div-be,
  3. kattintás eseményfigyelőt rendel az utazás választó gombhoz,
  4. eseménykezelőt rendel az előző eseményfigyelőhöz.

1.1. Utazás választó gomb dinamikus létrehozása

A gomb HTML kódja lényegében ott van az index.html-ben, onnan csak ki kell másolnunk.

<button class = "tripSelectorButton" value = "nordtrip">NordTrip</button>

Ebben a kódban azonban cserélnünk kell a value attribútum értékét (nordtrip), valamint a gomb szövegét (NordTrip), hogy az mindig a trips objektum első elemét mutassa. Ez egyelőre szintén a NordTrip, de később ezt az objektumot bővíteni fogjuk és akkor automatikusan igazodni fog ehhez az alkalmazás. Ehhez szükségünk lesz a trips objektumban található kulcsok tömbjére. Ehhez az Object.keys() funkciót fogjuk használni, amely egy tömbben visszaadja egy objektum kulcsait. Ezt rögtön a trips deklarálása után helyezzük el. Rögtön ezután pedig egy másik változóba (actualTrip) kiválasztjuk az első kulcsot a tömbből a 0 index segítségével.

// Utazások adatai
const trips = {...
};

const tripKeys = Object.keys(trips);
let actualTrip = tripKeys[0];

Ezeket a változókat felhasználva módosítjuk a gomb HTML kódját. A kódot egyelőre szövegként állítjuk össze, és az .append() function segítségével adjuk hozzá a #tripSelector div-hez. A value attribútum értéke az első utazás (actualTrip) kulcsa lesz, a gomb szövege pedig az első utazás rövid neve (shortName), amelyet a kulcsokat használva tudunk kiolvasni az objektumból, hasonlóan ahhoz, ahogy egy tömb elemeihez hozzáférhetünk az indexek segítségével. A trips[actualTrip] visszadja az első utazás objektumát, esetünkben a nordtrip-ét, a trips[actualTrip]['shortName'] pedig az első utazás objektumának shortName kulcsához tartozó értéket, ami most a "NordTrip" lesz.

function createTripSelector(){
    $('#tripSelector').append('<button class = "tripSelectorButton" value = "' + actualTrip + '">' + trips[actualTrip]['shortName'] + '</button>');
}

1.2. Utazások listájának dinamikus létrehozása

Ebben az esetben is szövegként fűzzük össze a HTML kódot. Annak érdekében, hogy pontosan annyi lista elem jöjjön létre, ahány utazás van a trips objektumban, egy for ciklussal iterálunk végig a tripKeys tömbön és a gombhoz hasonlóan elhelyezzük attribútumként az utazás objektumának kulcsát, szövegként pedig az utazás rövid nevét.

function createTripSelector(){
    $('#tripSelector').append('<button class = "tripSelectorButton" value = "' + actualTrip + '">' + trips[actualTrip]['shortName'] + '</button>');
    
    for(let key of tripKeys){
        $('#tripSelector').append('<div class = "tripSelectorDropdownItem" value = "' + key + '">' + trips[key]['shortName'] + '</div>');
    }
}

1.3. Kattintás eseményfigyelő hozzárendelése az utazás választó gombhoz 

Mivel biztosak lehetünk benne, hogy a gomb ekkor már létezik, a .click() function segítségével hozzárendelünk egy eseményfigyelőt, de az eseménykezelő function magját egyelőre üresen hagyjuk.

function createTripSelector(){
    $('#tripSelector').append('<button class = "tripSelectorButton" value = "' + actualTrip + '">' + trips[actualTrip]['shortName'] + '</button>');
    
    for(let key of tripKeys){
        $('#tripSelector').append('<div class = "tripSelectorDropdownItem" value = "' + key + '">' + trips[key]['shortName'] + '</div>');
    }
    
    $('.tripSelectorButton').click(function(){
        // utazás választó gombra kattintás esemény kezelése
    });
}

1.4. Eseménykezelő hozzárendelése

Ideje végiggondolnunk, hogy egészen pontosan milyen működést várunk az utazás választó gombra kattintás esetén? Magamnak valahogy így fogalmaztam meg: ha az utazások listája nem látszik, akkor a kattintás hatására jelenjen meg a lista, ha pedig látszik a lista, a gombra kattintva tűnjön el. Az alkalmazásban pedig mindezt úgy valósítanám meg, hogy egy változóban (tripListItems) eltárolom az utazások listájának elemeit, amelyet a jQuery selector $('.tripSelectorDropdownItem') segítségével találhatok meg. Egy if-else logikai vizsgálattal megnézem, hogy ezek az elemek láthatók-e, amihez az .is() function-t és a visible pseudo selectort hívom segítségül. Ha a tripListItems.is(':visible') értéke true, akkor a lista elemek éppen látszanak, ha false, akkor viszont nem. A listaelemek megjelenítéséhez a .show(), elrejtésükhöz a .hide() function-t használok, az előbbi megjelenti, utóbbi elrejti az elemeket.

function createTripSelector(){
    $('#tripSelector').append('<button class = "tripSelectorButton" value = "' + actualTrip + '">' + trips[actualTrip]['shortName'] + '</button>');
    
    for(let key of tripKeys){
        $('#tripSelector').append('<div class = "tripSelectorDropdownItem" value = "' + key + '">' + trips[key]['shortName'] + '</div>');
    }
    
    $('.tripSelectorButton').click(function(){
        let tripListItems = $('.tripSelectorDropdownItem');
        if(tripListItems.is(':visible')){
            tripListItems.hide();
        }
        else{
            tripListItems.show();
        }
    });
}

A map.js fájlban még egy módosítást kell tennünk: meg kell hívnunk a createTripSelector() function-t. Ezt egyelőre az actualTrip változó deklarálása után teszem meg.

...
let actualTrip = tripKeys[0];

// Utazás választó dinamikus felépítése
createTripSelector();

// Hozzuk létre az állomások stílusát!

A style.css-ben az utazások listájának elemeit (tripSelectorDropdownItem class) el kell tüntetnünk, hogy az alkalmazás indulásakor még ne látszódjanak. Ezért a class-ra vonatkozó megjelenítési szabályokat kiegészítjük még eggyel: display: none;. Annak érdekében pedig, hogy a gomb fölé mozgatva az egeret a kurzor megváltozzon és ezzel jelezze, hogy kattintható az elem, a tripSelectorButton class szabályait kiegészítjük a cursor: pointer; szabállyal.

.tripSelectorButton{
    display: block;
    min-width: 10em;
    height: 2em;
    background-color: var(--color-v04);
    font-size: var(--font-size-v01);
    border-radius: var(--border-radius-v02);
    cursor: pointer;
}

.tripSelectorDropdownItem{
    cursor: pointer;
    padding: .1em .5em;
    text-align: center;
    background-color: var(--color-v02);
    display: none;
}

1.5. A "beégetett" utazás lista és utazás választó gomb törlése

Mivel a kódunk most már dinamikusan elkészíti az utazás választó gombot és a listát, már nincs szükség az index.html-ben leírt, statikus listára. Ha ezen a ponton megnézzük az alkalmazást, akkor két gomb látszik majd. Az első gomb az index.html statikus kódja miatt, a második gomb a Javascript logika miatt, amit az előbb írtunk hozzá. (A listák azért nem látszanak, mert a CSS-ben már elrejtettük azokat.)

PaciTrip - Két utazás választó gomb látszik

Nekünk viszont csak egy gombra és egy listára van szükségünk, így az index.html-ben ezt a részt:

<!-- Trip Selector -->
<div id = "tripSelector">
    <button class = "tripSelectorButton" value = "nordtrip">NordTrip</button>
    <div class = "tripSelectorDropdownItem" value = "nordtrip">NordTrip</div>
    <div class = "tripSelectorDropdownItem" value = "swisstrip">SwissTrip</div>
    <div class = "tripSelectorDropdownItem" value = "iretrip">IreTrip</div>
    <div class = "tripSelectorDropdownItem" value = "eurotrip">EuroTrip</div>
</div>

átírjuk így:

<!-- Trip Selector -->
<div id = "tripSelector"></div>

3. Kattintás eseményfigyelő rendelése az utazás választó lista elemeihez

Ismét a .click() function-t alkalmazzuk, és a selector sem lesz ismeretlen ($('.tripSelectorDropdownItem')), hiszen azt is használtuk már, amikor az utazás választó gomb eseménykezelőjét építettük fel. Az eseménykezelő function magját egyelőre üresen hagyjuk.

function createTripSelector(){
    $('#tripSelector').append('<button class = "tripSelectorButton" value = "' + actualTrip + '">' + trips[actualTrip]['shortName'] + '</button>');
    
    for(let key of tripKeys){
        $('#tripSelector').append('<div class = "tripSelectorDropdownItem" value = "' + key + '">' + trips[key]['shortName'] + '</div>');
    }
    
    $('.tripSelectorButton').click(function(){
        let tripListItems = $('.tripSelectorDropdownItem');
        if(tripListItems.is(':visible')){
            tripListItems.hide();
        }
        else{
            tripListItems.show();
        }
    });
    
    $('.tripSelectorDropdownItem').click(function(){
        // utazás lista elemeire kattintás esemény kezelése
    });
}

4. Eseménykezelő - név és dátum megjelenítése a fejlécben

Az utazások listájának bármelyik elemére kattintva több dolognak is történnie kell, ebből az első, hogy az utazás hosszú nevét, amely tartalmazza az utazás nevét és hogy mettől meddig tartott, megjelenik a fejlécben. Ehhez az alkalmazás vázának összeállítása során létrehoztunk egy div-et, amelynek a subTitle class-t adtuk. A .text() function segítségével fogjuk ennek a div-nek a tartalmát megváltoztatni: a kiválasztott utazás objektumából a "name" kulccsal kiolvasott szöveget írjuk majd bele. De honnan tudjuk, hogy melyik a kiválasztott utazás? Kiolvassuk a kattintott elem ($(this)) value attribútumából, ahova korábban mi magunk írtuk bele. Ehhez az .attr() function-t használjuk, a kiolvasott értéket pedig eltároljuk az actualTrip változóban, mivel innentől kezdve ez lesz az aktuális utazás és így más funkciók is hozzáférhetnek ehhez az információhoz.

...
$('.tripSelectorDropdownItem').click(function(){
    // utazás lista elemeire kattintás esemény kezelése
    const actualTrip = $(this).attr('value');
    $('.subTitle').text(trips[actualTrip]['name']);
});
...

5. Eseménykezelő - név megjelenítése az utazás választó gombon

Az előzőhöz nagyon hasonló módon fogunk eljárni, amikor az utazás választó gomb szövegét változtatjuk meg: a .text() function-t használjuk, ám ezúttal a kiválasztott utazás rövid nevét (shortName) fogjuk beírni. A gombnak azonban nem csak szövege, hanem értéke (value attribútum, illetve property) is van, így ha következetesek akarunk lenni, azt is megváltoztatjuk. Ehhez a .val() function-t használjuk, amely az űrlapon is használt elemek (a button ilyen) egy rövidített megfelelője az .attr('value')-nak.

...
$('.tripSelectorDropdownItem').click(function(){
    // utazás lista elemeire kattintás esemény kezelése
    const actualTrip = $(this).attr('value');
    $('.subTitle').text(trips[actualTrip]['name']);
    $('.tripSelectorButton').text(trips[actualTrip]['shortName']);
    $('.tripSelectorButton').val(actualTrip);
});
...

Ha most megnézzük az alkalmazást, akkor azt fogjuk látni, hogy a legördülő menüben az egyes utazásokra kattintva a trips objektumban tárolt adatokkal tölti fel a fejlécet és a gomb feliratát is, azonban a kattintás után a lista ott marad! Persze lehet ez is a kívánt működés, hiszen magára a gombra kattintva eltüntethető a lista, én mégis azt gondolom, hogy ha a felhasználó kiválaszt egy újabb utazást, akkor előbb meg szeretné nézni annak a részleteit, nem rögtön egy másikat választani, így a lista átmenetileg csak útban lenne. Ezért célszerű lenne eltüntetni a listát, miután kiválasztotta az új utazást, ezért egészítsük ki még ezzel az utasítással az eseménykezelőt! (A selector és a .hide() function is ismerős lehet az utazás választó gomb eseménykezelőjéből.)

PaciTrip - A lista még látszik az utazás kiválasztása után is

...
$('.tripSelectorDropdownItem').click(function(){
    // utazás lista elemeire kattintás esemény kezelése
    const actualTrip = $(this).attr('value');
    $('.subTitle').text(trips[actualTrip]['name']);
    $('.tripSelectorButton').text(trips[actualTrip]['shortName']);
    $('.tripSelectorButton').val(actualTrip);
    $('.tripSelectorDropdownItem').hide();
});
...

6. Eseménykezelő - útvonal és állomás megjelenítése a térképen

Nagyon sok mindennel megvagyunk már, most azonban ismét jön néhány komolyabb változtatás a már meglévő kódban. A jelenlegi verzióban mind az útvonal, mind az állomás rétege statikus, olyan értelemben, hogy az adatforrás elérési útjának "beégettük" a 'data/nordtrip/routes.geojson', valamint a 'data/nordtrip/stops.geojson' értékeket. Ennél azonban itt is egy fokkal dinamikusabb megoldásra van szükségünk, ezért ahelyett, hogy rögtön egy-egy változóban tárolnánk el a két réteget, a változókat először globális szinten (rögtön a kódunk elején, hogy minden function elérhesse) deklaráljuk, de egyelőre nem adunk nekik értéket, és írunk egy function-t (setLayers), amely értéket társít hozzájuk a bemeneti paraméterként kapott utazás adatai alapján.

A globális változókat egyetlen let kifejezés után, vesszővel felsorolva is deklarálhatjuk, én a kódban a $(document).ready() után helyezem.

$(document).ready(function(){
    
    // Globális változók
    let stopsLayer, routesLayer;
    
    // Utazások adatai

A setLayers function számára két bemeneti paramétert adunk át: az útvonal és az állomások GeoJSON fájljainak elérési útját. A function belseje pedig alig fog különbözni az eddigi réteg létrehozástól, csupán az url paraméter értékét változtatjuk meg arra, amit a function bemeneti paraméterként kapott (elé fűzve még a 'data/' könyvtárat). Vagyis a gyakorlatban ezek helyett a sorok helyett a setLayers function-t és annak hívását fogjuk írni:

let stopsLayer = new ol.layer.Vector({
    title: 'Állomások',
    style: stopsStyle,
    source: new ol.source.Vector({
        projection : 'EPSG:3857',
        url: 'data/nordtrip/stops.geojson',
        format : new ol.format.GeoJSON()
    })
});

let routesLayer = new ol.layer.Vector({
    title : 'Útvonal',
    style: routesStyle,
    source : new ol.source.Vector({
        projection : 'EPSG:3857',
        url : 'data/nordtrip/routes.geojson',
        format : new ol.format.GeoJSON()
    })
});

A setLayer function így néz ki:

function setLayers(stopsGeoJSON, routeGeoJSON){
    routesLayer = new ol.layer.Vector({
        title : 'Útvonal',
        style: routesStyle,
        source : new ol.source.Vector({
            projection : 'EPSG:3857',
            url : 'data/' + routeGeoJSON,
            format : new ol.format.GeoJSON()
        })
    });

    stopsLayer = new ol.layer.Vector({
        title: 'Állomások',
        style: stopsStyle,
        source: new ol.source.Vector({
            projection : 'EPSG:3857',
            url: 'data/' + stopsGeoJSON,
            format : new ol.format.GeoJSON()
        })
    });
}

Magát a function-t nyugodtan írjuk a kód végére a többi function mellé, azonban szükség lesz rá, hogy meg is hívjuk ott, ahol korábban a két változót deklaráltuk, hogy az alkalmazás számára ugyanúgy létezzen a két réteg, amikor majd a térképhez szeretné adni őket.

// Hozzuk létre az útvonal stílusát!
...

// Hozzuk létre az állomások és az útvonal rétegét!
setLayers(trips[actualTrip].stopsGeoJSON, trips[actualTrip].routeGeoJSON);

// Hozzuk létre az OpenStreetMap alaptérképi réteget

Bizonyára kiszúrtad, hogy két különböző módon is használom a Javascript objektum kulcsait az értékek elérésére. Az egyik megoldás, hogy a változó neve után, amelyben a Javascript objektumot tárolom, szögletes zárójelek közé, szövegként írom be a kulcs nevét. Pl. a trips változóban a nordtrip kulcshoz tartozó érték esetében ez így néz ki: trips['nordtrip']. A másik lehetőség, hogy az objektumot tartalmazó változó neve után egy pontot teszek, majd rögtön a kulcs nevét, ami az előző példánál maradva így néz ki: trips.nordtrip . Mind a kettő megközelítés helyes, azonban a másodikat csak akkor tudom használni, ha ismerem a kulcs pontos nevét, változónevet nem lehet ponttal hozzáfűzni. Amikor a kulcs neve egy változóból érkezik, akkor csak az első megközelítés használható. Tegyük fel, hogy az actualTrip változóban tárolom a kiválasztott utazáshoz tartozó kulcsot, ami más és más szöveg lehet attól függően, hogy melyik elemre kattintott a felhasználó (lehet 'nordtrip', 'iretrip', 'swisstrip', stb.). Ha a ponttal való összefűzést próbálnám meg, vagyis trips.actualTrip, akkor sajnos nem működne. Ez a kifejezés azt feltételezi, hogy van a trips változóban tárolt objektumnak van egy actualTrip nevű kulcsa. A szögletes zárójeles megoldásnál azonban először behelyettesítésre kerül a változó értéke, vagyis a trips[actualTrip] (figyelj, itt nincs aposztróf, mint a trips['nordtrip'] esetében!) értelmezése úgy történik, hogy előbb kiolvassa az actualTrip változóból az éppen aktuális értéket (legyen mondjuk 'eurotrip') és azt behelyettesítve próbálja végrehajtani, pl. trips['eurotrip']. A két megoldás egyébként kombinálható is, ahogy a fenti példában látható, tehát a kiválasztott útvonal nevét lekérhetem így is: trips[actualTrip]['name'] és így is: trips[actualTrip].name .

Ezzel azonban még csak az alkalmazás indulásakor megjelenő (a trips objektumban első) utazás rétegeinek megjelenítését tettük dinamikusabbá, az eseménykezelőben még nem dolgoztunk vele. Az eseménykezelőnek azt az esetet kell kezelnie, hogy a térképen van egy útvonal és egy állomás réteg, ami az alkalmazás indulásakor került rá (vagy már váltottunk az utazások között és a kiválasztott utazás rétegeit látjuk). Ezért először ezeket a rétegeket el kell távolítani. Ehhez az OpenLayers map objektumámak removeLayer() funkcióját fogjuk használni, amely paraméterként az eltávolítani kívánt réteget várja, ami a stopsLayer és a routesLayer változókban található. Ezután futtatjuk az imént készített setLayers() function-t a kiválasztott utazás GeoJSON fájljainak elérési útjait átadva, így a stopsLayer és routesLayer változókban már az új utazás rétegei kerülnek. Ezután pedig ezeket a rétegeket ismét hozzá kell adnunk a térképhez, hogy meg is jelenjenek rajta. Ehhez az OpenLayers map objektumának addLayer() funkcióját fogjuk használni, paraméterként a már új rétegeket tartalmazó változókat adva neki.

...
$('.tripSelectorDropdownItem').click(function(){
    // utazás lista elemeire kattintás esemény kezelése
    const actualTrip = $(this).attr('value');
    $('.subTitle').text(trips[actualTrip]['name']);
    $('.tripSelectorButton').text(trips[actualTrip]['shortName']);
    $('.tripSelectorButton').val(actualTrip);
    $('.tripSelectorDropdownItem').hide();
    map.removeLayer(stopsLayer);
    map.removeLayer(routesLayer);
    setLayers(trips[actualTrip].stopsGeoJSON, trips[actualTrip].routeGeoJSON);
    map.addLayer(routesLayer);
    map.addLayer(stopsLayer);
});
...

Ha ezen a ponton teszteljük az alkalmazást, azt tapasztaljuk, hogy tudunk váltani az utazások között. Kiválasztva egy másik utazást, a régi geometriái eltűnnek, és megjelennek az új úthoz tartozók, ám ezeket csak akkor látjuk, ha a megfelelő területre mozgatjuk a térképi nézetet.

PaciTrip - Az IreTrip után a EuroTrip-et választva a geometria megjelenik, de a térképi nézet még nem változik 

7. Eseménykezelő - a térkép igazítása az új útvonal geometriáihoz

Annyi maradt hátra, hogy az új utazás kiválasztása után a megfelelő helyre is navigáljon a térkép. Ehhez ismét dinamizálnunk kell egy kicsit a korábbi kódunkat, vagyis készítünk egy function-t (setView), amelyet két helyen fogunk meghívni: az alkalmazás indításakor, pontosan ott, ahol most a view változóban tároljuk el a kezdeti nézet objektumát, és az új utazás kiválasztásához kapcsolódó eseménykezelőben, ahol az új utazás nézetével felülírjuk a korábbit. Először is a view változót is globálissá tesszük, vagyis a stopsLayer és routesLayer után ez is bekerül a felsorolásba.

// Globális változók
let stopsLayer, routesLayer, view;

// Utazások adatai

Ezután pedig elkészítjük a function-t, amely egyetlen paramétert (tripView) vár, egy olyan tömböt, amelyet az utazások initView paraméterében megadtunk: az első eleme (tripView[0]) egy WGS84 koordinátapárt tartalmazó tömb, a második (tripView[1]) a zoomszint. A function magja nagyon hasonló lesz ahhoz, ahogy eddig létrehoztuk a view objektumot, csupán a statikus értékeit cseréljük a paraméterből érkezőkre:

function setView(tripView){
    view = new ol.View({
        center : ol.proj.transform(tripView[0], 'EPSG:4326', 'EPSG:3857'),
        zoom: tripView[1]
    });
}

A function elkészítése után pedig két helyen hívjuk meg, először ott, ahol eddig változóban tároltuk el, vagyis ehelyett:

let view = new ol.View({
    center : ol.proj.transform([16.074259, 64.260138], 'EPSG:4326', 'EPSG:3857'),
    zoom: 5
});

ezt írjuk:

// Hozzuk létre a mértéklécet (ScaleLine)
...

setView(trips[actualTrip].initView);

// Hozzunk létre egy fedvényt (Overlay) a térképhez

A setLayer második hívására pedig az eseménykezelőben kerül sor. A function felülírja a view változó tartalmát, de a térképi nézet ettől még nem változik meg! Ehhez az OpenLayers map objektum setView() funkcióját kell használnunk, amelynek paraméterként átadjuk a megváltozott tartalmú view változót, s így az új utazás rétegeinek megjelenítése után az alkalmazás oda is navigáljon (mozogjon, nagyítson-kicsinyítsen), ahol az új rétegek találhatók. 

...
$('.tripSelectorDropdownItem').click(function(){
    const actualTrip = $(this).attr('value');
    $('.subTitle').text(trips[actualTrip]['name']);
    $('.tripSelectorButton').text(trips[actualTrip]['shortName']);
    $('.tripSelectorButton').val(actualTrip);
    $('.tripSelectorDropdownItem').hide();
    map.removeLayer(stopsLayer);
    map.removeLayer(routesLayer);
    setLayers(trips[actualTrip].stopsGeoJSON, trips[actualTrip].routeGeoJSON);
    map.addLayer(routesLayer);
    map.addLayer(stopsLayer);
    setView(trips[actualTrip].initView);
    map.setView(view);
});
...

8. Információ ablak funkció módosítása 

Az információ ablakot úgy építettük fel, hogy az útvonal és az állomás rétegre épüljön, ilyen rétegek pedig minden utazás esetén vannak az alkalmazásban, így a funkció kellően általános ahhoz, hogy működőképes maradhatna akkor is, ha megváltoztatjuk az utazást. Ehhez mindössze csak annyi kell, hogy minden útvonal és állomás réteg azonos adatszerkezettel rendelkezzen, vagyis azonos oszlopnevek és oszloptípusok legyenek bennük, hogy az információ ablak tartalma is helyesen álljon össze. Szerencsére az utazások adatainak előkészítésénél mindig azt az eljárást követtem, amelyet korábban leírtunk, így ezzel sincs gond. Sajnos azonban az állomás információ ablakának tartalmát felépítő funkcióba fixen beleírtuk a nordtrip mappát a kép elérési útjába (src = "img/nordtrip/' + props.pic + '"). Ezért a buildStopsPopup funkciót egy ponton módosítjuk és a kép elérési útjába az aktuális utazás kulcsát fogjuk beleírni (src = "img/' + actualTrip + '/' + props.pic + '"), mivel a hozzá tartozó képeket is egy ilyen nevű mappában tároljuk (így a képek is rendezettebben tárolhatók és kezelhetők). A módosított kód így néz ki:

function buildStopsPopup(feature){
    const coordinate = feature.getGeometry().getCoordinates();
    const props = feature.getProperties();
    
    const contentHtml = '<div class = "popupHtml">' +
                         props.day + '. nap ' + props.stop + '. állomás: ' + '<br/>' + 
                         props.place + '<br/>' +
                         '<img class = "popupImage" src = "img/' + actualTrip + '/' + props.pic + '"/><br/>' +
                         props.desc + '<br/>' +
                         '</div>';
                         
    overlay.setPosition(coordinate);
    
    $("#popup-content").html(contentHtml);
}

PaciTrip - Az utazás választás legördülő menüből működik

Készen vagyunk! Működik az alkalmazásban az utazás választó gomb és a legördülő menü. Ami még az eredeti elképzelésünk szerint hátra van: egy időcsúszka segítségével lehessen áttekinteni az út egyes napjait, valamint a webes térképes alkalmazás publikálása egy szerverre. S bár eredetileg nem szerepelt a koncepcióban, de szeretném bemutatni azt is, hogyan lehet egy új utazást hozzáadni az alkalmazáshoz.

Ahogy bővül és fejlődik a webalkalmazás, a HTML, a CSS és a JS is változni fog még. Elsősorban új elemeket, szabályokat és funkciókat adunk majd hozzá, de előfordulhat az is, hogy a most leírtakból kell majd törölnünk, vagy legalább egy részét megváltoztatnunk.

A PaciTrip webes térképes alkalmazás már elkészült állományait megtalálod GitHub oldalunkon!

Tekintsd meg a PaciTrip webes térképes alkalmazást aktuális állapotában! (Az aktuális állapot eltérhet a fent leírt változattól!)

Ha adatbázis-építés, adatelemzés, vizualizáció, térinformatika témában segítségre van szükséged, írj nekünk az Ez az e-mail-cím a szpemrobotok elleni védelem alatt áll. Megtekintéséhez engedélyeznie kell a JavaScript használatát. címre és megpróbálunk egy tippel, ötlettel segíteni!

© 2017 AdatTérKép