LINQ a zpracování záznamu trasy navigačního programu
Doc. Dr. Vladimír Homola, Ph.D.
Článek popisuje jednu z možností, jak běžně dostupným programovým vybavením vyhodnotit záznam trasy, který je výstupem navigačního programu. Ukazuje současně některé matematické aspekty výpočtů na eliptických plochách, přístup k datům v sešitu (workbook) a v souborech XML, a zpracování dat v relačních databázích. Používá k tomu jednak klasický VBA dostupný téměř ze všech aplikací, jednak produkt Visual Studio 2008 Express Edition uvolněný firmou Microsoft v prosinci 2007 k bezplatnému stažení. Tento produkt obsahuje dosti podstatné změny především v syntaxi jazyků Basic a C, které nyní mohou svými jazykovými konstrukcemi přistupovat přímo k datům ve čtyřech základních kategoriích: ADO, SQL, XML a obecná množina reprezentovaná rozšířením objektu dříve známého jako Kolekce. K tomu bylo zavedeno rozšíření programovacích jazyků označované jako LINQ (Language-Integrated Query).
The article discusses one of possibilities of data logger's track-log evaluation. The VBA and the release of Microsoft Visual Studio 2008 Express Edition is used to data access in workbooks, XML files, and databases. There are fairly significant changes in Basic and C-language syntax to guarantee the direct data access via ADO, SQL, XML, and common objects - all of this through the own language syntax. The Language Integrated Query (LINQ) is the new tool to realize the mentioned data access.
Termín | Termín anglicky | Význam |
Bod (trasy, GPS) | Trace point | Časová a prostorová informace o jednom bodu prostoru dodaná navigačním systémem. |
Trasa | Trace, Trace Log | Časové a prostorové informace o místech výskytu sledovaného subjektu dodané navigačním systémem = posloupnost bodů v pořadí vzrůstajícího časového atributu. |
Místo (silnice, cesty) | Point of road | Prostorová, místní a další informace o jednom místě konkrétní silnice. |
Silnice | Road | Posloupnost míst na ní v pořadí vzrůstající kilometráže. |
Cesta | Trip | Časové, prostorové a místní informace s dalšími doprovodnými údaji generalizující absolvovanou trasu. |
Civilizovaný člověk se pohybuje nějakým územím - zvláště v soukromém motorovém vozidle, byciklu a podobném - jen po komunikacích k tomu určených. Moderní civilizovaný člověk při takovém pohybu používá navigační systém, i když danou trasu absolvuje po sté. Navigační systémy kromě plánování před cestou a vlastní navigaci při cestě, umožňují o skutečně absolvované trase cesty informovat po jejím ukončení. A právě informace nikoliv o plánu, ale o skutečnosti je pro mnoho uživatelů tím daleko nejzajímavějším. Jde o to, aby v takovém případě dostal uživatel ty informace a v takové podobě, aby vyhovovaly právě jeho potřebám.
Způsob informování je dán konkrétním software konkrétního navigačního systému. Především ne každý navigační systém funguje jako data logger - zařízení s funkcí průběžného záznamu dat. Systémy, které tuto funkci mají, však nemusí mít ještě možnost exportu zaznamenané trasy - mohou záznam využívat jen pro prezentaci nebo konkrétní statistiky na tom zařízení, na kterém byly pořízeny. A konečně ty systémy, které mají možnost exportu, vytváří výstupní soubory v různých formátech s různou kvalitou informací. Společné mají to, že lze nastavit interval mezi jednotlivými záznamy - časový interval vždy (např. každých 5 vteřin), u některých i interval daný vzdáleností (např. každých 500 metrů).
Formát výstupu lze dnes vidět ve dvou základních podobách: výstup bázovaný XML a výstup jiný - přičemž ty první převládají. Jde např. o formát KML (Keyhole Markup Language), GML (Geography Markup Language), GeoXACML (Geospatial eXtensible Access Control Markup Language), GPX (GPS Exchange Format) a další. Zástupcem jiných formátů je např. NMEA, SiRF apod. V tomto případě jde spíše o protokoly přenosu dat z primárních zdrojů (GPS přijímače) do zpracovávajících systémů (navigačních a jiných). Byly často definovány výrobci nebo majoritními uživateli GPS technologií - např. NMEA (National Marine Electronics Association), Sirf (SiRF Technology Holdings, Inc.) a dalšími.
Ve všech shora zmíněných případech jde o formáty formalizované, definované a uložené většinou v elektronické podobě, u XML včetně schémat. Víceméně všechny rovněž mají možnost obsáhnout snad všechny druhy informací, které by koncového uživatele mohly potenciálně zajímat. Ovšem snad žádný navigační systém nemá možnost definovat strukturu exportovaných dat - ta je dána těmito systémy napevno a v naprosté většině obsahuje jen mírně transformovaná nebo předzpracovaná data přijímače GPS: datum, čas, zeměpisnou šířku a délku a případně nadmořskou výšku; na jejich základě lze odvodit rychlost mezi dvěma záznamy a směr pohybu.
Pro běžného uživatele data v takové kvalitě nejsou k ničemu. Běžný uživatel nepotřebuje vteřinu po vteřině vědět, že se vyskytoval na takových a takových souřadnicích. Potřebuje vědět, že tehdy a tehdy byl v Litomyšli, v tolik a tolik v Mejtě, že z Litomyšle to bylo 14,4 kilometrů které ujel za 6 min 48 sec tj. rychlostí 127 km/h. A právě řešení této situace je obsahem článku.
Poznámka: Dopřejme si jistého zevšeobecnění: v celém článku se pod označením GPS bude rozumět jakýkoliv funkční, pro navigaci použitelný družicový systém.
Navigační systémy se běžně používají především jako jednoúčelové, jediným zařízením tvořené systémy pro navigaci v automobilech, na kolech i při pěší turistice. Jsou dostatečně známé a jimi se článek zabývá jen potud, pokud jejich software umožňuje provádět záznam ujeté trasy a její export v souborovém tvaru do počítačového prostředí. Článek se zabývá konkrétně sestavou tvořenou dvěma samostatnými, obecně použitelnými komponentami: PDA a přijímač signálu GPS.
PDA: Fujitsu-Siemens LOOX-718, procesor PXA272 520 MHz, interní paměť 128 MB, MM karta 8 GB, displej 640 x 480 High Color (16 bit), Bluetooth, WiFi, Infra, USB
GPS: i-Tec BT-339, 20 kanálů, 2D/3D, citlivost -130 dBm při s/š > 39 dB, přesnost polohy 5m (2D RMS s WAAS), přesnost času < 1ms, WGS-84, Bluetooth.
Vstupní data popisované úlohy jsou dvojího druhu:
Přijímač GPS komunikuje s PDA přes jeho virtuální COM port, na straně PDA přijímá informace z GPS program iGo My way 2006 Plus firmy Nav-N-Go z května 2007. Program iGo generuje záznam o trase do souboru ve formátu GPX (viz např. www.topografix.com/GPX/1/1, funkční ke dni 30.6.2009) ve velmi jednoduché struktuře:
Obr. 1: XML schéma GPX souboru
nebo v XML zápise - příklad:
<gpx version="1.0" creator="iGO 2006" ... >
<trk>
<trkseg>
<trkpt lat="50.64061499" lon="13.82354665">
<time>2007-11-02T10:30:15Z</time>
</trkpt>
</trkseg>
<trkseg>
...
</trkseg>
...
</trk>
</gpx>
Program iGo je schopen paralelně generovat i záznam ve formátu NMEA (viz např. http://www.gpsinformation.org/dale/nmea.htm, funkční ke dni 30.6.2009). Ten obsahuje navíc informace o rychlosti, směru, nadmořské výšce a počtu družic. Protože však jde o textový soubor běžného typu comma delimited, není z hlediska zaměření tohoto článku zajímavý a nebude popisován.
Pro identifikaci míst (silnic, území) je použita tabulka relační databáze, primárně umístěná jako pojmenovaná oblast v sešitu Excelu tvořící seznam. Obsahuje informace o místech daného území (především o místech konkrétní silnice resp. silničního spoje, avšak není podmínkou). Struktura tabulky (již importovaná do databázového programu) je následující:
Sloupec | Typ | Obsah |
Sl | TEXT (6) | Identifikátor silnice |
KM | DOUBLE | Kilometráž místa na dané silnici |
ID - primární klíč | TEXT (5) | Jedinečný identifikátor - kód místa |
JM | TEXT (96) | Textové označení místa |
TP | TEXT (2) | Typ místa: křižovatka, čerpačka ... |
RD | BYTE | Rychlost do místa z místa předchozího |
EN | INTEGER | Exit number pro dálnice a rychlostní silnice |
KR | TEXT (5) | Kód místa na křížící silnici |
UZ | TEXT (1) | Indikace že místo je uzlem - znak "U" |
SM | TEXT (1) | Směr k objektu na trase rostoucí kilometráže |
PI | TEXT (1) | Point of interest - znak "x" |
LA | DOUBLE | Latitude - zeměpisná šířka |
LO | DOUBLE | Longitude - zeměpisná délka |
V popisované aplikaci jsou použita data pouze z tučně modře označených sloupců: ID, JM, LA a LO. Ostatní slouží pro použití v jiných aplikacích autora. Příklad části takové tabulky je uveden níže; jde o výňatek dat použitých ve specifikaci cílů (viz následující odstavec).
SI | KM | ID | JM | TP | RD | EN | KR | UZ | SM | PI | LA | LO |
I17 | 27,667 | VJCHR | Vjezd Chrudim | ob | 80 | x | 49,953988 | 15,751418 | ||||
I17 | 30,509 | CHTRA | Rondel u Transporty | kr | 40 | 49,952092 | 15,787743 | |||||
I17 | 31,897 | CHI37 | Chrudim \ nahoru / I37 | kr | 30 | P | 49,941104 | 15,794650 | ||||
I17 | 33,766 | CHRKA | Rondel u Kauflandu | kr | 40 | x | 49,946927 | 15,809012 | ||||
I17 | 36,807 | VJKOC | Vjezd Kočí | ob | 80 | x | 49,949043 | 15,849568 | ||||
I17 | 37,541 | VYKOC | Výjezd Kočí | ob | 50 | x | 49,951046 | 15,859152 | ||||
I17 | 41,247 | VJHTY | Vjezd Hrochův Týnec | ob | 80 | x | 49,958627 | 15,907651 | ||||
I17 | 41,982 | VYHTY | Výjezd Hrochův Týnec | ob | 50 | x | 49,961176 | 15,916942 | ||||
I17 | 43,127 | VJCAN | Vjezd Čankovice | ob | 80 | 49,962363 | 15,932706 | |||||
I17 | 43,846 | VYCAN | Výjezd Čankovice | ob | 50 | 49,965020 | 15,941221 | |||||
I17 | 48,874 | VJMES | Vjezd Městec | ob | 80 | 49,970706 | 16,006408 | |||||
I17 | 49,075 | VYMES | Výjezd Městec | ob | 50 | 49,971150 | 16,009067 | |||||
I17 | 50,232 | VJOST | Vjezd Ostrov | ob | 80 | x | 49,971926 | 16,025218 | ||||
I17 | 51,024 | VYOST | Výjezd Ostrov | ob | 50 | x | 49,971804 | 16,035982 | ||||
I17 | 52,958 | VJSTR | Vjezd Stradouň | ob | 80 | 49,969215 | 16,062156 | |||||
I17 | 53,442 | VYSTR | Výjezd Stradouň | ob | 50 | 49,969411 | 16,068736 | |||||
I17 | 57,255 | 17X35 | Zámrsk x R35 | kr | 80 | 35X17 | U | x | 49,984338 | 16,115406 | ||
R35 | 139,612 | CHV35 | Chvojenec | kr | 30 | CHVOJ | 50,109642 | 15,936816 | ||||
R35 | 144,822 | THOAG | Holice Agip | ce | 80 | L | 50,074302 | 15,981162 | ||||
R35 | 159,407 | 35X17 | R35 x I/17 | kr | 80 | 17X35 | U | P | 49,984343 | 16,115413 | ||
R35 | 162,703 | VYSMK | Vysoké Mýto Karosa | kr | 80 | x | 49,961743 | 16,145112 | ||||
R35 | 164,807 | VYSMP | Vysoké Mýto Plus | kr | 35 | 49,946169 | 16,161385 | |||||
R35 | 165,347 | TSHVM | Shell Vysoké Mýto | ce | 35 | L | x | 49,942085 | 16,165461 | |||
R35 | 168,795 | HRUVJ | Hrušová vjezd | ob | 90 | x | 49,916734 | 16,193754 | ||||
R35 | 169,736 | HRUVY | Hrušová výjezd | ob | 50 | x | 49,909948 | 16,198086 | ||||
R35 | 171,546 | OCERE | Exit / Cerekvice | kr | 90 | x | 49,899460 | 16,224847 | ||||
R35 | 177,328 | LITVJ | Litomyšl vjezd | ob | 90 | x | 49,882808 | 16,293059 | ||||
R35 | 179,738 | LITOS | Litomyšl světla centrum | kr | 40 | x | 49,867365 | 16,312915 | ||||
R35 | 180,754 | LITVY | Litomyšl výjezd | ob | 50 | 49,864188 | 16,324427 | |||||
R35 | 189,683 | SV366 | Exit na Svitavy / II366 | kr | 80 | P | 49,813816 | 16,417881 |
Data dodaná navigačním systémem jsou [délka;šířka] systému WGS-84 a údaj [datum;čas]. Interval snímkování je nastavitelný. Předpokládejme trasovanou vzdálenost o velikosti šířky celé České republiky, tj. zhruba 500 km. Předpokládejme průměrnou rychlost kolem 50 km/hod. V tom případě jde o 10 hodin plynulého záznamu. Při intervalu snímkování 1 sec jde o 36 000 záznamů. Při struktuře (textový formát) dle předchozího odstavce jde zhruba o 90 znaků (= 90 bytů) na jeden záznam. Celkový objem dat pro 10-hodinový záznam je tedy nejvýš 3,5 MB, což při gigabytových kapacitách dnešních paměťových karet je zcela přijatelné. Jde přitom o data jednotlivých míst, časově vzdálených 1 sec.
Definujme nyní, čeho chceme dosáhnout - a co tím chceme ukázat.
V úvodu kapitoly Popis problematiky jsou cíle zhruba naznačeny. Situace je následující:
Obr. 2: Schematická mapa části trasy
Jedu - úměrně dopravní situaci - po nějaké trase a navigační systém důsledně zaznamenává vždy po uplynutí stanoveného časového intervalu polohu a družicový čas. Mám připravený seznam míst na jednotlivých silnicích a po absolvování celé cesty mně zajímá skutečnost: místo od místa, kdy jsem je projel, jaká je vzdálenost jednoho od druhého, jakou rychlost lze dosáhnout mezi každými dvěma z nich. Tyto informace mně postačují např. v podobě tabulky relační databáze ať už v sešitu tabulkového procesoru nebo v klasické databázi (pozn.: vjezd a výjezd je orientován ve smyslu kilometráže dané silnice):
SIL | KMS | KMC | IDM | JMM | TMC | DST | SPD | TMP |
... | ... | ... | ... | ... | ... | ... | ... | ... |
R35 | 179,738 | 251,100 | LITOS | Litomyšl světla centrum | 11:55:01 | 0,893 | 76,5 | 0,7 |
R35 | 177,328 | 253,520 | LITVJ | Litomyšk vjezd | 11:57:33 | 2,420 | 57,3 | 2,5 |
R35 | 171,546 | 258,990 | OCERE | Exit ® Cerekvice | 12:01:08 | 5,470 | 91,6 | 3,6 |
R35 | 169,736 | 261,211 | HRUVY | Hrušová výjezd | 12:02:24 | 2,221 | 105,2 | 1,3 |
R35 | 168,795 | 262,018 | HRUVJ | Hrušová vjezd | 12:03:09 | 0,807 | 64,6 | 0,8 |
R35 | 165,347 | 265,502 | TSHVM | Shell Vysoké Mýto | 12:05:35 | 3,484 | 85,9 | 2,4 |
R35 | 162,703 | 268,395 | VYSMK | Vysoké Mýto Karosa | 12:16:03 | 2,100 | 58,2 | 2,2 |
I17 | 57,255 | 271,690 | 17X35 | Zámrsk x R35 | 12:18:45 | 3,295 | 73,2 | 2,7 |
I17 | 51,024 | 277,918 | VYOST | Výjezd Ostrov | 12:22:23 | 1,936 | 101,0 | 1,1 |
I17 | 50,232 | 278,704 | VJOST | Vjezd Ostrov | 12:23:10 | 0,786 | 60,2 | 0,8 |
I17 | 41,982 | 286,981 | VYHTY | Výjezd Hrochův Týnec | 12:29:33 | 1,150 | 94,1 | 0,7 |
I17 | 41,247 | 287,706 | VJHTY | Vjezd Hrochův Týnec | 12:30:24 | 0,725 | 51,2 | 0,8 |
I17 | 37,541 | 291,410 | VYKOC | Výjezd Kočí | 12:32:37 | 3,704 | 100,3 | 2,2 |
I17 | 36,807 | 292,159 | VJKOC | Vjezd Kočí | 12:33:22 | 0,749 | 59,9 | 0,7 |
I17 | 33,766 | 295,200 | CHRKA | Chrudim rondel u Kauflandu | 12:35:11 | 3,041 | 100,4 | 1,8 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
kde údaje v jednotlivých sloupcích znamenají
Sloupec | Typ | Obsah |
SlL | TEXT (6) | Identifikátor silnice |
KMS | DOUBLE | Kilometráž místa na dané silnici |
KMC | DOUBLE | Ujeté kilometry od začátku cestu |
IDM | TEXT (5) | Jedinečný identifikátor - kód místa |
JMM | TEXT (96) | Textové označení místa |
TMC | DATE | Okamžik průjezdu místem |
DST | DOUBLE | Vzdálenost z předchozího místa |
SPD | DOUBLE | Rychlost z předchozího místa |
TMP | DOUBLE | Čas z předchozího místa |
Cíle jsou tedy dány tabulkou ekvivalentní shora uvedeným příkladem SKUTEČNOST. Při směřování k tomuto cíli chce článek ukázat
Vzdálenost dvou míst (dále A a B) zaznamenané trasy je důležitá nejen z hlediska přesného určení celkové délky trasy i aktuální rychlosti. Kardinální význam má při stanovení nejbližšího známého místa. Případná chyba při určení vzdálenosti dvou bezprostředně po sobě zaznamenaných míst se kumuluje do výpočtu celkové vzdálenosti.
Problém v tomto případě spočívá v tom, že přijímač GPS nedodává při konstantní poloze stále stejné souřadnice. Z technických důvodů se dvě následující informace o poloze mohou lišit o jistou elementární jednotku diference. U použitého přijímače i-Tec BT-339 to činí cca 0.006" jak pro šířku, tak pro délku. Pro méně příznivý případ (tj. zeměpisnou šířku) to činí cca 18 cm. Při době stání např. 10 minut to činí v nejnepříznivějším případě 100 metrů.
Ve vzorcích sférické trigonometrie se vyskytují goniometrické funkce úhlových rozdílů dvou poloh. Obvyklá hardwarová reprezentace necelých čísel je single / real / double precision floating point (číslo v pohyblivé řádové čárce v jednoduché / zvýšené / dvojnásobné přesnosti).
V jednoduché přesnosti (model o interní délce 4 byty) se lze spolehnout na 7 platných cifer. Nejbližší přesně zobrazené číslo k 1 je tedy 0.9999999, což je cos (92"). Je-li to ovšem rozdíl úhlů dvou míst o časovém rozdílu jedné vteřiny, pak to ve směru rovnoběžky představuje rychlost 9900 km/hod! Použití tohoto číselného modelu tedy nepřichází v úvahu.
Ve zvýšené přesnosti (model o interní délce 6 bytů) se lze spolehnout na 11 platných cifer. Analogicky předchozí úvaze představuje úhlový rozdíl za jednu časovou vteřinu rychlost asi 31 km/hod. Je zřejmé, že reálné rychlosti jsou nižší, proto ani tento model nelze obecně použít.
Ve dvojnásobné přesnosti (model o interní délce 8 bytů) se lze spolehnout na 17 platných cifer. Analogicky předchozím úvahám představuje úhlový rozdíl za jednu časovou vteřinu rychlost asi 0,99 km/hod. Naopak např. při rychlosti 1 m/sec = 3.6 km/hod tento rozdíl pro méně příznivý případ (pohyb podél rovnoběžky) činí cca 1/20" = 0.05". Hodnota cos(0.05") = 0.999 999 999 999 971, což lze v daném číselném modelu zobrazit přesně.
Problém by v tomto případě mohl být s trajektorií - viz následující obrázek zatáčky. Pokud by dva následující okamžiky vzorkování padly do začátku a konce zatáčky, pak je zaznamenaná vzdálenost asi o 11% menší než skutečná.
Obr. 3: Chyba trajektorie |
|
Je zřejmé, že korektní postup výpočtu by vedl k určení vzdálenosti dvou míst na povrchu rotačního elipsoidu. Jimi a středem Země je dána rovina, jejíž průsečnice s povrchem elipsoidu je elipsa. Délka jejího eliptického oblouku vymezeného dvěma danými body je jejich hledaná vzdálenost. Tento postup ovšem vede k eliptickým integrálům, jejichž výpočet v množství rovném počtu zaznamenaných bodů trasy by enormně zpomaloval výpočet.
V geodézii je obecně přijímána v jistém - vzhledem k rozměrům elipsoidu malém - okolí daného bodu aproximace povrchu elipsoidu povrchem koule nebo dokonce rovinou. Jako ono "malé" okolí bývá přijímána hodnota 20 [km]. V popisované úloze jde o okolí nanejvýš 50 [m] - pro rychlost 180 [km/h] a interval vzorkování 1 [sec]. Aproximace kulovou plochou nebo rovinou je tedy zcela odůvodněná.
Uvažujme nejprve skutečně referenční elipsoid. Pro jednoduchost uvažujme dvě místa A a B na stejném poledníku. Budeme zjišťovat délku poledníkového oblouku s = s(A,B) a celkově směřovat k její náhradě např. přímou vzdáleností u = u(A,B) - tedy tětivou elipsy, sférickou vzdáleností apod.
Obr. 4: Eliptický, kruhový, úsečkový úsek mezi dvěma body
Parametrické rovnice elipsy jsou
a protože pro délku s oblouku křivky obecně platí
tedy konkrétně pro elipsu o poloosách a resp. b
Je tedy
což pro substituci
(bez újmy na obecnosti můžeme položit a<b) dává
Jest ovšem
neúplný eliptický integrál druhého druhu, a pro hledanou délku oblouku s(A,B) je tedy
Pro neúplný eliptický integrál druhého druhu (po integraci rozvojem na řady) platí
(1)
kde
a
a dále bylo označeno
Diskutujme určení vzdálenosti tímto exaktním postupem. To bude probíhat nějakou počítačovou aplikací. Standardní, obecně použitelné datové modely s největší přesností odpovídají definici double precision floating point a to zaručuje přesnost na max. 17 platných cifer (viz např. Excel). Z tohoto ohledu však řada (1) nepříjemně rychle konverguje. Člen
v součtu (1) se - pro k odpovídající WGS-84 - neuplatní již pro n=4:
n | P=k^2n | R=(-0,5) nad n | P*R |
1 | 4,5E-05 | -5,0E-01 | -2,2E-05 |
2 | 2,0E-09 | -1,3E-01 | -2,5E-10 |
3 | 9,0E-14 | -6,3E-02 | -5,6E-15 |
4 | 4,0E-18 | -3,9E-02 | -1,6E-19 |
5 | 1,8E-22 | -2,7E-02 | -4,9E-24 |
6 | 8,1E-27 | -2,1E-02 | -1,7E-28 |
7 | 3,6E-31 | -1,6E-02 | -5,8E-33 |
8 | 1,6E-35 | -1,3E-02 | -2,1E-37 |
9 | 7,3E-40 | -1,1E-02 | -8,0E-42 |
10 | 3,3E-44 | -9,3E-03 | -3,0E-46 |
a pro n=12 už vede k přetečení (resp. podtečení) při práci s číslem v pohyblivé řádové čárce. Pro představu: při výpočtu délky zemského kvadrantu je výsledek spočtený Excelem (resp. jinými aplikacemi užívajícími datový typ double) o 0.16% větší než skutečnost - na délku to činí 16.7 [km].
Existují některé algoritmy výpočtu neúplného eliptického integrálu druhého druhu (např. algoritmus aritmeticko - geometrického průměru) eliminující zmíněnou nevýhodu, ovšem za cenu náročnějšího výpočtu - příklad použití viz dále.
Při aproximaci kulovou plochou v blízkém okolí bodu P je vhodné zvolit takovou kulovou plochu, která má stejnou křivost - přesněji, která se v bodě P dotýká eliptické plochy a má poloměr rovný poloměru křivosti eliptické plochy v bodě dotyku. V bodě dotyku však má rotační elipsoid příčný poloměr křivosti (označovaný N) a meridiánový poloměr křivosti (označovaný M). Dále se často používá střední poloměr křivosti (označovaný R) jako geometrický průměr příčného a meridiánového poloměru:
R2 = M . N
Označíme-li a resp. b délku hlavní resp. vedlejší poloosy, e první excentricitu a W první geodetickou funkci, je
e2 = 1 - b2 / a2
W2 = 1 - e2 . sin2j
M = (1 - e2) . a / W
N = a / W
Jsou ovšem pro konkrétní elipsoid všechny poloměry křivosti i první geodetická funkce funkcí šířky:
M = M(j)
N = N(j)
R = R(j)
W = W(j)
Níže je uveden obrázek znázorňující situaci na elipsoidu, jsou rovněž uvedeny vzorce pro výpočet příčného poloměru křivosti N a převod na pravoúhlé souřadnice bodu ze souřadnic geografických [l;j].
Obr. 5: Bod na povrchu elipsoidu se souvislostmi |
|
Ověřme nyní na několika málo hodnotách přímým výpočtem jednotlivé možnosti. Víme, že jedna úhlová vteřina na poledníku je velmi zhruba 30 [m]. Rychlost 180 [km/h] je rovna 50 [m/sec]. Při intervalu vzorkování jedné časové vteřiny určitě neujedeme více než 60 metrů, což jsou zhruba dvě úhlové vteřiny. Porovnejme proto různé způsoby výpočtu vzdálenosti dvou míst A, B na stejné meridiánové elipse, jejichž rozdíl šířky jsou dvě úhlové vteřiny.
Označme u délku úsečky AB, s délku eliptického oblouku AB, P bod na elipse se šířkou rovnou průměru šířek bodů A a B. Označme dále Rs velikost průvodiče bodu Rs, N příčný poloměr křivosti v bodě P, C střed elipsy a O průsečík normály v bodě P s přímkou malé poloosy.
Obr. 6: Různé možnosti spojnice dvou bodů
Počítejme s dle matematických vztahů v odstavci Elipsoid. Počítejme s(AG) rovněž podle těchto vztahů, ale algoritmem aritmeticko - geometrického půměru. Počítejme u podle Pythagorovy věty. Počítejme s(N) jako délku oblouku kružnice s poloměrem N a středovým úhlem AOB (v barvě - modrá kružnice). Počítejme s(R) jako délku oblouku kružnice s poloměrem Rs a středovým úhlem ACB (v barvě - červená kružnice). Počítejme to vše pro šířku bodu B po řadě 10, 20 až 89 stupňů.
Výpočty byly prováděny v Excelu formulemi v jeho buňkách; pro výpočty eliptickými integrály se ovšem ve formulích volaly vlastní naprogramované funkce. Výsledek nejlépe znázorní graf:
Obr. 7: Graf délek dvouvteřinových oblouků počítaných různými metodami
Především křivka s dokumentuje evidentně chybějící další členy rozvoje dle vzorce (1) shora s jednoznačným závěrem: tento způsob prakticky použít nelze.
Další zajímavý fakt poskytuje skutečnost, že výpočet přímé vzdálenosti a obou sférických dávají prakticky totožný výsledek. Potvrzuje se tím, že v blízkém okolí bodu kulové plochy lze povrch koule považovat za rovinu?
Ovšem nejzajímavější je poslední křivka s(AG), tedy výpočet prováděný eliptickým integrálem metodou aritmeticko - geometrického průměru. Z grafu to zdaleka není patrné, ale uvažujme: dotyková koule na pólech má poloměr rovný malé poloose. Dotyková koule na rovníku má poloměr rovný velké poloose. Protože délka oblouku kružnice s poloměrem R a se středovým úhlem a je rovna R . a, měly by být poměry délek oblouků se stejným středovým úhlem na polární a rovníkové kouli ve stejném poměru jako poměr poloos. Pro WGS-84 je poměr a : b roven 1.003364. A pro křivku s(AG) je poměr délek oblouků na šířkách 90o a 00 roven 1.003326 - tedy vynikající shoda s předpokladem. Odtud je velmi blízko k tvrzení, že právě tato křivka nejvíce odpovídá skutečnosti.
Pro tři zbývající, téměř totožné křivky je tento poměr 1.0101. Porovnáme-li je s křivkou s(AG), jsou od ní rozptýleny v rozmezí ±19 [cm] na vzdálenosti 60 [m], což je ±0.3%. Nejmenší odchylka je kolem 45o zeměpisné šířky. Pro oblast ČR je odchylka 0.06%, co je 3.5 [cm] na 60 [m] délky. Ještě jinak: kdybych prolétl rychlostí 200 [km/h] přímou vzdálenost z Aše k Shellce u Čendy, pak je při (časově) jednovteřinovém snímání polohy diference 252 [m].
Je však třeba si uvědomit, že tyto diference jsou maximální a v reálném provoze jsou nejméně poloviční. Se snižující se úhlovou vzdáleností dvou bodů A, B se totiž úsečka AB čím dál víc přibližuje plášti elipsoidu.
Po tom, co bylo zjištěno v předchozím odstavci, je zcela na místě uvažovat o nahrazení povrchu elipsoidu v blízkém okolí daného bodu rovinou.
Při aproximaci rovinou v blízkém okolí bodu P je vhodné zvolit lokální tečnou rovinu (Local Tangent Plane) a zavést sekundární soustavu souřadnou celkem přirozeným způsobem: osa Z je totožná s normálou tečné roviny a tedy i elipsoidu samotného, osy X a Y leží v tečné rovině. I jejich orientace je taková, jakou lidé na povrchu Země očekávají: osa X ve směru západ - východ, osa Y ve směru jih - sever.
Následující obrázky takovou situaci znázorňují:
Obecný náhled | Znázornění v rovině | Vztahy | ||||
Obr. 8: Tečná rovina se soustavou souřadnou |
|
|
Vzdálenost dvou blízkých bodů A a B lze pak určit takto:
Obecně jsou čáry mřížky na předchozím obrázku velmi složité křivky. Vzdálenost AB však je řádově do 100 metrů a proto můžeme mřížku považovat v naznačeném okolí za pravidelnou obdélníkovou síť. Diference šířek resp. délek se pohybuje kolem jednotek úhlových vteřin. Zjistíme-li délku jednovteřinového oblouku ve směru poledníku a délku jednovteřinového oblouku ve směru rovnoběžky, pak po vyjádření diferencí délek a šířek v délkových (místo úhlových) jednotkách lze přímo aplikovat Pythagorovu větu.
Povrch elipsoidu pro uvedený účel nahraďme dotekovou koulí v bodě P s poloměrem rovným příčnému poloměru křivosti N. Jednovteřinový oblouk na poledníku pak bude mít délku p . N / (180 . 3600). Protože poloměr rovnoběžkové kružnice na šířce j je N . cos(j), je jednovteřinový oblouk na rovnoběžce roven p . N . cos(j) / (180 . 3600).
Obecnější úlohu - zjištění poloměrů rovnoběžkové kružnice a dotekové koule pro bod o dané zeměpisné šířce - řeší např. následující funkce:
Public Function PolomeryVsirceN _
(ByVal qSirkaStupnu As Double, _
Optional ByVal qA As Double = cPoloosaA, _
Optional ByVal qFr As Double = cZplosteniRec) _
As Double()
Dim V(0 To 1) As Double ' Pole pro výsledek: (0)-pro rovnoběžku (1)-pro kouli
Dim a As Double ' Velká poloosa
Dim b As Double ' Malá poloosa
Dim psi As Double ' Šířka v radiánech
Dim eps2 As Double ' eps2 = 1 - b^2/a^2
Dim CosPsi As Double ' cos (psi)
Dim Sin2psi As Double ' sin^2 (psi)
Dim W As Double ' První geodetická funkce
Dim N As Double ' Příčný poloměr křivosti
Dim rR As Double ' Poloměr rovnoběžkové kružnice
a = qA
If qA = cPoloosaA And qFr = cZplosteniRec Then
b = cPoloosaB
eps2 = cEps2
Else
b = qA * (qFr - 1#) / qFr
eps2 = 1# - (b * b) / (a * a)
End If
psi = qSirkaStupnu * cDegToRad
CosPsi = Cos(psi)
Sin2psi = 1 - CosPsi * CosPsi
W = Sqr(1 - eps2 * Sin2psi)
N = a / W
rR = N * CosPsi
V(0) = rR ' Polomér rovnoběžky v bodě
V(1) = N ' Polomér koule v bodě
PolomeryVsirceN = V
End Function
Funkci je možno použít pro jakýkoliv elipsoid; nezadají-li se druhý a třetí parametr, implicitně počítá na WGS-84. Konstanty typu cXX jsou definovány na globální úrovni následovně:
' Konstanty - WGS-84:
Public Const cPoloosaA As Double = 6378137 ' Hlavní poloosa [m]
Public Const cPoloosaB As Double = 6356752.31424518 ' Vedlejší poloosa [m]
Public Const cZplosteniRec As Double = 298.257223563 ' Reciproká hodnota zploštění
' Lze také Public Const cZplosteniRec As Double = cPoloosaA / (cPoloosaA - cPoloosaB)
Public Const cExcentricita As Double = 521854.008423385 ' Excentricita [m]
' Nelze však Public Const cExcentricita As Double = sqr(cPoloosaA*cPoloosaA -
cPoloosaB*cPoloosaB) !!
Public Const cEps As Double = 0.081819190842622 ' První (numerická) excentricita
Public Const cEps2 As Double = cEps * cEps ' Kvadrát první excentricity
' Lze také Public Const cEps2 As Double = 1# - (cPoloosaB * cPoloosaB) / (cPoloosaA *
cPoloosaA)
Public Const cPi As Double = 3.14159265358979 ' Ludolfovo číslo
Public Const cDegToRad As Double = cPi / 180# ' Pro převod stupňů na radiány
Klasické kolekce známé z Visual Studia 6 obsahovaly pouze metody Count, Add, Remove a Item pro zjištění počtu prvků, přidání a odebrání prvku, a přístup ke konkrétnímu prvku indexem nebo klíčem. Existovala sice objektová třída Dictionary poněkud rozšiřující spektrum metod (např. o zjištění existence klíče nebo řazení), avšak byla primárně určená pro použití ve skriptech a relativně pomalá.
Visual Studio 2008 přináší dosti podstatnou inovaci. Na principu kolekce ponechává obecný interface iEnumerable (rozšířený však o možnost zadat explicitně jednu objektovou třídu, jejímiž instancemi bude kontejner kolekce plněn), zavádí však dále seznam (List) instancí objektů konkrétní třídy; seznam disponuje metodami pro řazení, vyhledávání, filtrování, převody na pole a dalšími. Ukázkou dalšího "množinového" objektu je EnumerableRowCollection instancí objektů konkrétní třídy orientovaná zvláště na databázové zpracování. Na všechny tyto "množinové" objekty lze použít samozřejmě množinový iterátor (např. v Basicu reprezentovaný příkazem For Each).
Všechna tato rozšíření byla nutná díky snaze integrovat do programovacích jazyků prvky dotazovacích jazyků. Konkrétně jde o příkaz select jazyka SQL. Zdrojem v tomto příkazu je množina a výsledkem opět množina - množinou se zde rozumí kolekce nebo kterékoliv její rozšíření (některá zmiňuje předchozí text). Specielně autoři těchto inovací dosáhli toho, že zdrojem může být tabulka relační databáze, data podaná jako XML a kolekce nebo seznam jakýchkoliv uživatelem definovaných objektů - a přitom se se všemi takovými zdroji pracuje jednotným způsobem.
Tato jazyková rozšíření jsou označována jako LINQ (line-in query) a článek přináší ukázku jejich použití na konkrétním problému - popisovaném zpracování trasy navigačního programu. Protože jde o podstatné rozšíření, je LINQ věnována níže celá kapitola.
Vstupními daty popisované aplikace jsou - výše popsaná - data GPX a data XLS. Prvním úkolem programové aplikace je tedy získat tato data bod po bodu, místo po místu v programem zpracovatelné podobě. Nabízí se dvě principiální řešení: klasické (jako pole) nebo moderní (jako kolekce). Z pochopitelných důvodů bylo zvoleno řešení moderní.
Data generovaná navigačním programem mají XML (resp. GPX) formát popsaný výše. Logicky tedy je modelem dat jednoho bodu trasy objekt třídy nazvané clBodGPS.
UML schéma objektové třídy:
Zápis ve VB-2008:
Public Class clBodGPS
' Jedno místo a čas průjezdu = bod z GPS.
Public Lon As Double ' Zeměpisná délka
Public Lat As Double ' Zeměpisná šířka
Public Tim As Date ' Datum a čas průjezdu
End Class
Všechny body trasy pak tvoří kolekci
Dim Body As IEnumerable (Of clBodGPS)
ve které žádné dva prvky nemají stejnou hodnotu datového pole Tim - to vyplývá ze způsobu záznamu navigačním programem. Naplnění kolekce Body je pak obsahem následující kapitoly.
Data připravená uživatelem v sešitu Excelu a tvořící pojmenovanou oblast jsou tabulkou relační databáze. Její databázová struktura je popsána v kapitole výše. Tomu odpovídá datový model, ve kterém jeden řádek = jeden záznam tvoří instanci objektové třídy nazvané clMistoSilnice.
UML schéma objektové třídy:
Zápis ve VB-2008:
Public Class clMistoSilnice ' Jedno místo / uzel z databáze míst; tou je pojmenovaná oblast v sešitu Excelu. Public SI As String ' Kód silnice Public KM? As Double ' Kilometráž silnice Public ID As String ' Identifikace místa Public JM As String ' Jméno místa Public TP As String ' Typ místa Public RD? As Double ' Rychlost do místa Public EN? As Double ' Exit Number Public KR As String ' Křižovatka - kód místa na jiné silnici Public UZ As String ' Indikace zda jde o uzel Public SM As String ' Umístění ve směru silnice Public PI As String ' Point of Interest Public LA? As Double ' Latitude Public LO? As Double ' Longitude End Class
Poznámka: Ve Visual Studiu 2008 indikuje znak ? (otazník) přípustnost null hodnoty - tj. případ, že daný údaj není zadaný.
Všechna místa pak tvoří seznam
Dim Mista As List (Of clMistoSilnice)
Všechna místa tvoří seznam (list - na rozdíl od bodů GPX, viz výše); k místům je totiž někdy potřeba přistupovat přímo, je potřeba je mít řazené (např. podle kilometráže silnic) a to všechno seznam zajišťuje svými metodami. Naplnění kolekce Místa je pak obsahem následující kapitoly.
Jak bylo řečeno v kapitole Definice cíle (viz), postačujícím informačním výstupem o skutečně absolvované trase je tabulka relační databáze o shora uvedené struktuře. Jejím programovým zdrojem je kolekce instancí objektové třídy, kterou nazvěme např. clSkutecnost.
UML schéma této objektové třídy:
Zápis ve VB-2008:
Public Class clSkutecnost Public SIL As String ' Identifikátor silnice Public KMS As Double ' Kilometr silnice Public KMC As Double ' Kilometr cesty Public IDM As String ' Identifikátor nejbližšího místa Public JMM As String ' Textové označení nejbližšího místa Public TMC As Date ' Čas průjezdu bodem cesty Public DST As Double ' Vzdálenost předchozího a tohoto místa Public SPD As Double ' Rychlost z předchozího do tohoto místa Public TMP As Double ' Čas z předchozího do tohoto místa v [hod] Public VZD As Double ' Vzdálenost bodu průjezdu od nejbližšího místa Public DIR As Double ' Směr z bodu průjezdu k nejbližšímu místu Public LAC As Double ' Latitude bodu cesty Public LOC As Double ' Longitude bodu cesty Public LAM As Double ' Latitude nejbližšího místa Public LOM As Double ' Longitude nejbližšího místa End Class
Na rozdíl od kapitoly Definice cíle (viz), zahrnuje tato objektová třída další datová pole (tučně, v barvě modře). To, že byla zjištěna totožnost místa bodu průjezdu a místa cesty, není samozřejmě absolutní - viz následující obrázek. Proto na výstupu jsou dodány informace potřebné pro zjištění korelace mezi záznamem a konkrétním místem: jak daleko je zaznamenaný bod od nejbližšího známého místa, jaký je mezi nimi azimut, a mezi jakými vlastně souřadnicemi bylo ztotožnění deklarováno. Pokud by totiž VZD = Vzdálenost bodu průjezdu od nejbližšího místa byla neúměrně velká, pak by to znamenalo s největší pravděpodobností buď absenci skutečného místa nebo jeho nepřesné určení v tabulce MÍSTA.
Obr. 9: Situace při rozhodování mezi místy
Při zjišťování, který zaznamenaný bod trasy odpovídá kterému známému místu nezbývá než zkoumat vzájemné kombinace bodů a míst. Takovými vazebními prvky spojující kolekce Body a Místa mohou být instance objektové třídy nazvané clTRxUZ:
UML schéma objektové třídy:
Zápis ve VB-2008:
Public Class clTRxUZ Public TM As Date ' Čas bodu trasy Public ID As String ' Identifikátor místa Public VZ As Double ' Vzájemná vzdálenost bodu a místa End Class
Vzájemná vzdálenost je pak použita jako kriterium; pro dané místo se hledá bod s nejmenší vzdáleností, která navíc nepřesáhne rozumnou toleranci - např. 50 metrů (viz dále). Dvojice [bod; místo] nalezené uvedeným postupem se zkompletují již se všemi daty dostupnými z jediného místa; tím je instance objektu třídy nazvané clPrujezd:
UML schéma objektové třídy:
Zápis ve VB-2008:
Public Class clPrujezd ' Jeden bod průjezdu zkompletovaný s event. údaji nejbližšího místa. Public BodGPS As clBodGPS Public Vzdalenost As Double Public MistoSilnice As clMistoSilnice Sub New() BodGPS = Nothing Vzdalenost = -1 MistoSilnice = Nothing End Sub End Class
Vzhledem k tomu, že jedna instance této třídy obsahuje dva pointery a jednu hodnotu double (dohromady 16 bytů), je práce se seznamem takových dat velmi rychlá. Navíc, zpřístupněna ve vzrůstající posloupnosti času, tvoří datový základ požadovaných cílových informací. A zastřešením vstupních dat, mezivýsledků a cílové tabulky může být instance objektu třídy clCesta:
UML schéma objektové třídy:
Zápis ve VB-2008:
Public Class clCesta Public Body As IEnumerable(Of clBodGPS) Public Mista As List(Of clMistoSilnice) Public Prujezdy As IEnumerable(Of clTRxUZ) Public Cesta As List(Of clPrujezd) Public Skutecnost As List(Of clSkutecnost) Sub New() With Me .Body = Nothing .Mista = Nothing .Prujezdy = Nothing .Cesta = Nothing .Skutecnost = Nothing End With End Sub Public Function Info() As String Dim V As String ... Zkompletování textové informace o začátku a konci cesty ... V = V + GpsTimeDateFormat(lDatOd) + " ... " + lUzelOd + vbCrLf V = V + GpsTimeDateFormat(lDatDo) + " ... " + lUzelDo Info = V End Function End Class
LINQ je především množina vlastností ve Visual Studiu 2008, které rozšiřují výkonnost dotazů do syntaxe programovacích jazyků Studia, tj. Basicu a C. LINQ zavádí do jazyků standardní klauzule pro dotazování a aktualizaci dat, přičemž tato technologie může být rozšířena pro podporu potenciálně jakéhokoliv typu datového zdroje. Visual Studio 2008 obsahuje pro LINQ poskytovatele propojení na kolekce platformy Framework, databáze SQL serveru, objekty třídy DataSet z ADO.NET, a konečně na dokumenty XML.
Myšlenka, na které celá ta inovace stojí, je stařičká. Vidíme ji např. na (nejméně 20 let starém a pořád dobrém) jazyku xBase - jazyku pro programování databází použitém už firmou AshtonTate v databázovém programu dBase III a řadě dalších. Dodnes ho používá firma Fox (dnes již Microsoft Fox) ve svých databázových programech. Tento programovací jazyk má jednak běžné jazykové konstrukce jako jsou deklarace, příkazy, procedury a funkce atd, ale hlavně na úroveň běžných příkazů jazyka postavené příkazy SQL. U Microsoftů teď přišli na to, že i jiné jazyky lze syntakticky rozšířit o konstrukce de facto ekvivalentní nebo přímo totožné jednotlivým klauzulím SQL (viz např. [1]).
Když se ale už do toho pustili, tak to udělali poměrně důkladně. Zatímco xBase je jazyk určený především pro zpracování databází (ale lze v něm programovat i obecně), jazyky C a Basic jsou obecné programovací jazyky (ale má v nich jít programovat i databáze). Zvláště bylo nutno řešit problém kolize klíčových slov jazyků C a Basic na jedné, a jazyka SQL na druhé straně (viz hned např. SELECT). Dále: již v dřívějších verzích a databázových platformách bylo možno jazyky C a Basic zpracovávat databáze se vším všudy (a je tak možno i nyní). Využívalo se však objektů a jejich metod z příslušných připojovaných knihoven (DAO, ADO atd). Například příkaz CREATE jazyka SQL vykonala metoda Execute objektu Database z knihovny DAO, pakliže se jí dodal kompletní a syntakticky i sémanticky správný text příkazu jako textový řetězec. Jestliže tento řetězec obsahoval nesprávný text příkazu, byla chyba zjištěna až v okamžiku chodu programu. Rozšíření programovacího jazyka o konstrukce typu SQL dovolí přenést minimálně syntaktickou kontrolu do fáze překladu.
Konečně i pohled na data v databázích byl zobecněn. Databáze (rozumí se jejich relační model) jsou z datového hlediska kolekce tabulek a každá tabulka je kolekce řádků. Každý řádek je pak instancí objektu nějaké - definicí tabulky dané - datové struktury. Pro zpracování kolekcí mají však programovací jazyky už dlouho nástroje - Basic např. příkaz FOR EACH. Jestliže tedy půjde tabulku získat jako kolekci řádků (např. SQL příkazem SELECT), pak její zpracování zajistí příkaz typu
FOR EACH řádek IN tabulka
...
NEXT
Jenomže kolekce jsou obecnějším nástrojem: nemusí obsahovat jen objekty třídy "databázový řádek", ale vlastně cokoliv. Vždyť už jen pole (ARRAY) je možno nahlížet a (už dlouho) zpracovávat jako kolekci svých prvků příkazem FOR EACH. Stejně tak text XML definuje kolekce uzlů, elementů, atributů atd. Je však také pravda, že je dosti podstatný rozdíl ve způsobu získání kolekce obecných objektů, řádků databází a informací XML. Proto mezi LINQ jako takovým a konkrétní strukturou dat stojí čtyři různá propojení (obsažená ve Framework verze 3.5) podle následujícího schématu:
Obr. 10: Postavení LINQ v aplikacích (podle Microsoft Visual Studio 2008)
Určující myšlenkou komponenty .NET Framework verze 3.5, kterou nazvali LINQ to SQL, je správa dat relačních databází jako objektů umístěných na serverech SQL resp. těmito servery spravovaných. Základem jsou ovšem známé dvourozměrné tabulky, jejichž event. provázanost zajišťují společná data jednoho nebo více sloupců. To zásadně nové však je - jak bylo uvedeno v předchozím odstavci - to, že objektový model ekvivalentní datovým strukturám v databázích vytváří a zapisuje do kódu své aplikace programátor - a to prostředky svého programovacího jazyka. Stejně tak příkazy SQL vytváří přímo prostředky tohoto jazyka, nikoliv už zápisem těchto příkazů jako textových řetězců (i když i tato možnost zůstala ponechána). Evidentně tedy došlo i k syntaktickému rozšíření jazyků, ve kterých má být tento nástroj použit - ve Visual Studiu 2008 tedy jazyků Basicu a C.
Tvorba a hlavně základní syntaktická kontrola příkazů SQL přešly tedy z fáze běhu programu do fáze jeho překladu. To je bezesporu významné ulehčení práce programátora při programování a ladění programů. Na druhé straně se však - podle názoru autora tohoto článku - poněkud omezila obecnost a pružnost vývoje a použití databázové aplikace: aby totiž už překladač mohl kontrolovat text programu, musí mít databázové objekty popsány přímo v programu jako objektové třídy, a tedy poměrně staticky. Pak ovšem vývoj aplikace, která by správně reagovala na strukturální změny databází provedené mimo tuto aplikaci, je velmi složitý - ne-li v mnoha případech zcela nemožný. Lze to posoudit hned v následujícím odstavci.
Struktura tabulky je dána popisem všech jejich sloupců - to je pohled ze strany databázových systémů. Struktura tabulky je dána popisem datových polí každého řádku - to je pohled ze strany aplikačních programů. Aplikace pohlíží na každý řádek konkrétní tabulky jako na instanci objektu takové objektové třídy, jejíž datová pole (= vlastnosti, properties) jsou ekvivalentní datovým polím záznamu (řádku tabulky) databáze. Pohled databáze a pohled aplikace ztotožnili ve Visual Studiu 2008 tak, že rozšířili popis objektové třídy v aplikačním programu o možnost zadání databázových atributů a vytvořit tak vzájemné mapování mezi objekty databáze a objekty aplikačního programu.
Ukažme tento mechanismus na hypotetické tabulce PRUJEZDY, která databázově odpovídá kolekci instancí objektů shora uvedené třídy clTRxUZ:
<Table(name:="PRUJEZDY")> _ Public Class tbTRxUZ <Column(Name:="DatumCas")> Public TM As Date <column(DbType:="nvarchar(5)")> Public ID As String <Column(Name:="Vzdalenost")> Public VZ As Double End Class
Definice objektové třídy tbTRxUZ má tedy zápis běžný v syntaxi daného objektového jazyka (zde Basicu, a to v nejjednodušší podobě), doplněný o specifikaci databázových atributů. Stejně tak je možno vytvářet instance ("proměnné") dané objektové třídy a používat je běžným způsobem, např.
Dim JedenPrujezd as tbTRxUZ
Pro účely mapování do databáze je zápis definice třídy doplněn jednak o atribut <table>, jednak o atributy <column>:
V uvedeném příkladu bude tedy instance (proměnná - např. JedenPrujezd) třídy tbTRxUZ asociována s řádkem tabulky PRUJEZDY. Vlastnost ID proměnné bude asociována s hodnotou ve sloupci ID tabulky (není uveden parametr Name atributu Column). Při vytváření tabulky bude tento sloupec vytvořen jako textový s max. počtem pěti znaků. Vlastnost VZ proměnné bude asociována s hodnotou ve sloupci Vzdalenost tabulky. Při vytváření tabulky bude tento sloupec vytvořen pro hodnoty v pohyblivé řádové čárce se dvojnásobnou přesností. Analogicky vlastnost TM a sloupec DatumCas.
Jak atribut <table>, tak atribut <column> mají řadu dalších parametrů.
Ve shora uvedeném příkladu obsahuje databáze ještě tabulku MISTA. Analogicky se tedy definuje třída např. tbMistoSilnice:
<Table(name:="MISTA")> _ Public Class tbMistoSilnice <column(DbType:="nvarchar(6)")> Public SI As String <Column()> Public KM As Double <column(DbType:="nvarchar(5)" primary key)> Public ID As String <Column()> Public JM As String <Column()> Public LA As Double <Column()> Public LO As Double End Class
Při vytváření této tabulky bude pro sloupec ID vytvořen navíc primární klíč, jména sloupců budou stejná jako identifikátory datových polí objektové třídy.
Zdrojem všech databázových entit mapovaných přes databázové spojení je objekt třídy DataContext. Přestože jeho instanci lze principielně použít přímo, jeho optimální využití je dosaženo implementací v nějaké objektové třídě, která bude programovým reprezentantem databáze:
Public Class clCestovani Inherits DataContext Public PRUJEZDY As Table(Of tbTRxUZ) Public MISTA As Table(Of tbMisto) ... případně další tabulky, pokud mají být také aplikací zpracovány přímo pomocí LINQ Public Sub New(ByVal Spojeni As String) MyBase.New(Spojeni) End Sub End Class
Instance proměnné této třídy pak reprezentuje celou databázi a přes tuto proměnnou je možno s databází běžně pracovat. Ovšem největší výhodu při zpracování databáze, kterou poskytuje popisovaný LINQ, je možno použít jen pro entity, které jsou obsaženy v definici "databázové" třídy - zde clCestovani. Databáze může totiž obsahovat např. další tabulky, pokud však nejsou explicitně uvedeny v definici příslušné třídy, nelze je přímo pomocí LINQ zpracovat; jejich obsah je nutno zpracovat "postaru".
"Databázová" třída musí definovat svůj vlastní konstruktor New, kterým definuje spojení na vlastní databázi. V nejjednodušším případě volá pouze konstruktor své bázové třídy (tj. DataContext). Parametrem tohoto konstruktoru je textový řetězec, který obsahuje
Kompletní připojovací řetězec má např. tvar, který dodá následující funkce:
Function XdfConnString(ByVal qSoubor As String) As String Dim V As String = "" V = V + "Data Source=.\SQLEXPRESS;" V = V + "AttachDbFilename=""" + qSoubor + """;" V = V + "Integrated Security=True;" V = V + "Connect Timeout=30;" V = V + "User Instance=True" XdfConnString = V End Function
Parametr "Data Source" obsahuje identifikaci odvolávaného SQL serveru, pod kterou je přístupný na daném počítači (resp. v jeho systému).
Zpřístupnění např. databáze MO_KL spočívá ve vytvoření instance "databázové" třídy takto:
Dim dbCesta As New clCestovani ("C:\DATA\MO_KL.MDF")
V okamžiku vzniku instance objektu "databázové" třídy (v našem případě clAnalyzy) fyzická databáze existovat může, ale také nemusí. Objektová třída DataContext (zděděná třídou clAnalyzy) obsahuje metodu jednak pro zjištění, zda fyzická databáze existuje, jednak pro vytvoření fyzické databáze dané struktury, pokud neexistuje. Nejjednodušeji to řekne přímo příklad:
Dim dbCesta As clCestovani ... dbCesta = New clCestovani("MO_KL.MDF") If Not dbCesta.DatabaseExists Then dbCesta.CreateDatabase ...
Metoda CreateDatabase vytvoří fyzicky soubor MO_KL.MDF, tj. vlastní (samozřejmě z hlediska dat prázdnou) databázi. V ní bude vytvořena tabulka PRUJEZDY (protože její popis je obsažen v deklaraci objektové třídy clCestovani), tabulka MISTA (ze stejného důvodu) a případně další tabulky tam obsažené.
Pokud v okamžiku vzniku instance objektu "databázové" třídy (v našem případě clCestovani) fyzická databáze existuje, pak přístup do jejich dat zajistí přímo metoda New. Jestliže se tedy ví, že databáze skutečně existuje, pak po provedení příkazů
Dim dbCesta As clCestovani ... dbAnalyzy = New cclCestovani("MO_KL.MDF") ...
je možno hned začít pracovat s jejími daty.
Jak známo, operátor LEFT OUTER JOIN v SQL obsahuje všechny položky ze zdroje spojení uvedeného na LEVÉ straně, a to i tehdy, když ve zdroji na PRAVÉ straně není nalezen odpovídající záznam (v tom případě jsou datové položky čerpající z pravé strany rovny NULL). Přesněji, každá položka ze zdroje uvedeného na pravé straně, pokud nemá odpovídající položku ve zdroji uvedeného na levé straně, je vyloučena ze zpracování.
Klauzule GROUP JOIN z LINQ provádí v konečném důsledku stejnou činnost jako LEFT OUTER JOIN. Rozdíl mezi množinou dat získanou pomocí LEFT OUTER JOIN; a pomocí GROUP JOIN je tento: klauzule GROUP JOIN seskupuje výsledek z pravého zdroje dat spojení pro každou položku levého zdroje dat. V relačních databázích LEFT OUTER JOIN vrací neseskupený výsledek, v němž každá položka pocházející z levé strany zdroje dat se opakuje pro každou odpovídající položku z pravé strany, nejméně však jednou.
Výsledek získaný pomocí GROUP JOIN lze získat také jako neseskupený - tak, aby vrátil jednu položku pro každý výsledek seskupeného dotazu obdobně jako u databází. To zajistí použití metody DefaultIfEmpty seskupené kolekce. Položky levého zdroje dat jsou stále vloženy do výsledku dotazu i když nemají odpovídající položku v pravém zdroji dat, přičemž však lze programově zajistit dodání konkrétní implicitní hodnoty v případě neexistence odpovídající položky pravého zdroje dat.
Analogické postupy platí pro další dva typy propojení.
Soubor XML je v popisované problematice reprezentován souborem generovaným navigačním systémem - viz shora odstavec Data GPS - body trasy. Z jeho schématu je zřejmé, že každý element <time> je přímým následníkem elementu <trkpt> (v originále child element); naopak, každý element <trkpt> je bezprostředním předchůdcem (v originále parent element) jediného elementu <time>. Element <time> má za svůj obsah datum a čas, element <trkpt> má jako atributy zeměpisnou délku a šířku (WGS-84).
Tohoto faktu lze využít pro neuvěřitelně jednoduché přenesení obsahu XML dokumentu do kolekce objektů třídy clBodGPS: z dokumentu se vyberou všechny následnické (In lXML.Descendants) elementy, které jsou elementem <time> (Where xElm.Name.LocalName = cElmTime). Pro každý z nich se určí datum a čas převedený z jeho textového obsahu na hodnotu typu date, a dále jeho bezprostřední předchůdce (Let tmParent As XElement = xElm.Parent) - víme, že je to určitě <trkpt>. A konečně z jeho atributů lat a lon a dříve zjištěného datumu a času se zkompletuje jedna instance objektu třídy clBodGPS.
Celý popsaný postup je vytvořen jako funkce, jejímž parametrem je označení souboru a výsledkem je požadovaná kolekce zaznamenaných bodů trasy:
Public Const cElmTime As String = "time" Public Const cElmLongitude As String = "lon" Public Const cElmLatitude As String = "lat" Function SeznamBoduTrasy(ByVal qSouborGPX As String) As IEnumerable(Of clBodGPS) ' Zpracování XML - tady musím dodržet jména elementů a atributů v GPX - viz cXX shora Dim lXML As XDocument = XDocument.Load(qSouborGPX) ' lXML = GPX soubor SeznamBoduTrasy = _ From xElm As XElement In lXML.Descendants _ Where xElm.Name.LocalName = cElmTime _ Let tmParent As XElement = xElm.Parent _ Let tmCas As Date = GpxToDate(tmParent.Value) _ Order By tmCas _ Select New clBodGPS _ With { _ .Lon = GpxToDouble(tmParent.Attribute(cElmLongitude).Value), _ .Lat = GpxToDouble(tmParent.Attribute(cElmLatitude).Value), _ .Tim = tmCas _ } End Function
Uvnitř funkce jsou volány dvě jednoduché funkce pro převod textové hodnoty tvaru "2007-11-02T10:30:15Z" na hodnotu typu date (GpxToDate) a pro převod textové hodnoty tvaru "50.64061499" na hodnotu typu double:
Function GpxToDate (ByVal qValue As String) As Date Function GpxToDouble (ByVal qValue As String) As Double
Vytvoření kolekce bodů trasy pak v nějaké volající programové jednotce provede příkaz volání funkce typu
Dim BodyTrasy = SeznamBoduTrasy ("C:\DATA\OV_MO_07-11-02.GPX")
Dalším zdrojem vstupních dat popisované aplikace je pojmenovaná oblast sešitu Excelu, tvořící tabulku relační databáze; její strukturu viz shora odstavec Data XLS - místa silnic. Kolekci jejich řádků lze velmi jednoduše získat pomocí LINQ pomocí Data Adapteru pro OLE: k volající aplikaci se přes Data Adapter jako instance objektu třídy DataTable připojí pojmenovaná oblast sešitu a z ní se LINQ vybere žádané. Protože ve zdroji (= tabulka v sešitu) principielně nemusí být všechna data zadána, zařadí se na výstup jen řádky, které mají zeměpisnou šířku a délku různé od null (Where lLA.HasValue And lLO.HasValue).
Tento postup zajišťuje funkce, jejímž parametrem je označení sešitu (jako souboru) a jméno oblasti v něm, a jejímž výsledkem je požadovaná kolekce řádků:
Function RadkyZnamychMist(ByVal qJmenoSesitu As String, ByVal qJmenoOblasti As String) _ As EnumerableRowCollection(Of clMistoSilnice) Dim xtb As System.Data.DataTable = PripojTabXLS(qJmenoSesitu, qJmenoOblasti) RadkyZnamychMist = _ From exy As DataRow In xtb.AsEnumerable _ Let lLA As Double? = exy.Field(Of Double?)("LA") _ Let lLO As Double? = exy.Field(Of Double?)("LO") _ Where lLA.HasValue And lLO.HasValue _ Select New clMistoSilnice _ With { _ .SI = exy.Field(Of String)("SI"), _ .KM = exy.Field(Of Double?)("KM"), _ .ID = exy.Field(Of String)("ID"), _ .JM = exy.Field(Of String)("JM"), _ .LA = exy.Field(Of Double?)("LA"), _ .LO = exy.Field(Of Double?)("LO") _ } End Function
Z popsané funkce je volána zcela obecně použitelná další funkce autora pro přístup k datům v pojmenované oblasti sešitu:
Function PripojTabXLS(ByVal qSoubor As String, ByVal qTabulka As String) As DataTable Dim jda As OleDbDataAdapter Dim jds As New DataSet Dim jcs As String Dim jsq As String Try jcs = XlsConnString(qSoubor) jsq = "select * from " + qTabulka jda = New OleDbDataAdapter(jsq, jcs) jda.TableMappings.Add("Table", qTabulka) jds.Locale = CultureInfo.InvariantCulture jda.Fill(jds) PripojTabXLS = jds.Tables(qTabulka) Catch ex As OleDbException MsgBox("Chyba v OleDB pro XLS: " & ex.Message) PripojTabXLS = Nothing End Try End Function
Pro upřesnění je dále uvedena funkce, která dodá Connection String do zdrojů formátu Excelu:
Function XlsConnString(ByVal qSoubor As String) As String Dim V As String = "" V = V + "Provider=Microsoft.Jet.OLEDB.4.0" + ";" V = V + "Data Source=" + qSoubor + ";" V = V + "Extended Properties=""Excel 8.0;HDR=Yes;IMEX=1""" XlsConnString = V End Function
Vytvoření seznamu míst jako kolekce řádků tabulky pak v nějaké volající programové jednotce provede příkaz volání funkce typu
Dim Mista as List (Of clMistaSilnice) Mista = RadkyZnamychMist ("C:/DATA/GPSDATA.XLS", "rgMista").ToList
Jako třetí případ použití LINQ ukažme zpracování tabulek v Microsoft Database (MDB). Jak bylo popsáno v kapitole Programové modely aplikačních dat, třída clTRxUZ (přesněji kolekce těchto objektů) slouží v vytvoření kombinací typu "každý bod trasy s každým známým místem" - ale jen kombinací se vzájemnou vzdáleností menší než nějaká rozumná vzdálenost, např. 50 metrů (Where lVzd < cRozumnaVzdalenost). Takovou kolekci vytvoří funkce, jejímiž parametry jsou označení databáze a jména tabulek, do nichž byly uloženy seznamy bodů průjezdu a známých míst:
Function KombinacePrujezdyMista( _ ByVal qJmenoDatabaze As String, _ ByVal qJmenoTabulkyTracks As String, _ ByVal qJmenoTabulkyMist As String) As IEnumerable(Of clTRxUZ) Const cRozumnaVzdalenost As Double = 50# Dim ttb As System.Data.DataTable Dim utb As System.Data.DataTable ttb = PripojTabMDB(qJmenoDatabaze, qJmenoTabulkyTracks) utb = PripojTabMDB(qJmenoDatabaze, qJmenoTabulkyMist) KombinacePrujezdyMista = _ From tdr As DataRow In ttb.AsEnumerable, udr As DataRow In utb.AsEnumerable _ Let ltLA As Double = tdr.Field(Of Double)("LA") _ Let ltLO As Double = tdr.Field(Of Double)("LO") _ Let luLA As Double = udr.Field(Of Double)("LA") _ Let luLO As Double = udr.Field(Of Double)("LO") _ Let lVzd As Double = VzdalenostMist(ltLO, ltLA, luLO, luLA) _ Where lVzd < cRozumnaVzdalenost _ Select New clTRxUZ _ With { _ .TM = tdr.Field(Of Date)("TM"), _ .ID = udr.Field(Of String)("ID"), _ .VZ = lVzd _ } End Function
Z popsané funkce je volána zcela obecně použitelná další funkce autora pro přístup k datům v tabulce Microsoft databáze:
Function PripojTabMDB(ByVal qSoubor As String, ByVal qTabulka As String) As DataTable Dim jda As OleDbDataAdapter Dim jds As New DataSet Dim jcs As String Dim jsq As String Try jcs = MdbConnString(qSoubor) jsq = "select * from " + qTabulka jda = New OleDbDataAdapter(jsq, jcs) jda.TableMappings.Add("Table", qTabulka) jds.Locale = CultureInfo.InvariantCulture jda.Fill(jds) PripojTabMDB = jds.Tables(qTabulka) Catch ex As OleDbException MsgBox("Chyba v OleDB pro MDB: " & ex.Message) PripojTabMDB = Nothing End Try End Function
Pro upřesnění je dále uvedena funkce, která dodá Connection String do zdrojů formátu Microsoft databáze:
Function MdbConnString(ByVal qSoubor As String) As String Dim V As String = "" V = V + "Provider=Microsoft.Jet.OLEDB.4.0" + ";" V = V + "Data Source=" + qSoubor + ";" V = V + "Jet OLEDB:Engine Type=5" MdbConnString = V End Function
Dále je ze shora uvedené funkce volána funkce VzdalenostMist, jejíž hlavní součástí je realizace funkce PolomeryVsirceN popsaná v odstavci Rovina a obsahující dále jednoduchý přepočet na délku podle téhož odstavce.
Vytvoření seznamu kombinací bodů a míst jako kolekce řádků tabulky pak v nějaké volající programové jednotce provede příkaz volání funkce typu
Dim sqTRxUZ as IEnumerable(Of clTRxUZ) sqTRxUZ = KombinacePrujezdyMista ("C:/DATA/GPSDATA.MDB", "tbBody", "tbMista")
LINQ budované jako analogie k příkazu Select dotazovacího jazyka SQL má rovněž možnost seskupovat data zdrojů a odevzdávat kolekci dat, z nichž některá jsou výsledkem volání agregačních funkcí. Bez podrobnějšího komentáře ke kontextu uveďme příklad seskupování s agregační funkcí Min; příkaz zajistí ze všech shora vybraných kombinací bodů a míst ty nejbližší:
' Kolekce s minimálními vzdálenostmi každého průjezdu od nějakého uzlu: Dim sqTMxVZ = From Kombinace In sqTRxUz Group By Kombinace.TM _ Into Skupina = Group, MINVZ = Min(Kombinace.VZ) _ Select New clGxVz With {.GT = Skupina, .VZ = MINVZ}
Shora uvedená funkce KombinacePrujezdyMista ukazuje zároveň, jak čerpat z více datových zdrojů. Zde je plně využito toho, že v takovém případě je výsledkem kartézský součin zdrojů, tj. "každý s každým". Častější případ v praxi je však takový, že data více datových zdrojů jsou propojena nějakou vazbou. V popisované aplikaci je to na několika místech; uveďme bez komentáře ke kontextu takový příklad:
' Kolekce s minimálními vzdálenostmi konkrétního průjezdu a konkrátního uzlu: Dim lPrujezdy = From rA In sqTMxVZ Join rB In sqIDxVZ On rA.VZ Equals rB.VZ _ Select New clTRxUZ _ With {.TM = rA.GT.First.TM, .ID = rB.GT.First.ID, .VZ = rA.VZ}
V této druhé ukázce stojí za povšimnutí toto: výstupem první ukázky je kolekce, kde může existovat pro jeden bod několik míst, mají-li od bodu stejnou vzdálenost. A právě metoda First použita ve druhé ukázce zajistí, že se dále zpracuje už jen jedno místo, a sice to první.
Článek se pokusil co možná stručně popsat řešení některých aspektů zpracování dat blížící se kategorii GIS. Rozebral matematickou problematiku s ohledem na počítačové zpracování a odůvodnil náhradu povrchu rotačního elipsoidu jednodušší plochou. Výsledky pak ukázal na konkrétní aplikaci, která řeší zpracování výstupů navigačních programů. Splnil zde další z deklarovaných cílů - použití rozšíření programovacích jazyků, které zavedl Microsoft ve svém Visual Studiu. Velmi příjemně pro aplikačního programátora je tak umožněno databázovým stylem zpracovávat množiny dat stejného typu resp. objektové třídy.
Pomocí LINQ je možno přistupovat k databázovým zdrojům dvěma způsoby: jeden byl ukázán v článku, druhý způsob je využití rozšíření deklarace uživatelských typů, struktur, tříd o atribut column. Tento druhý způsob v článku popsán není; autor ho považuje za jistou slabinu celého mechanismu, protože v tom případě se pracuje s datovými zdroji, které musí být známy už v okamžiku programování a při pozdějším spuštění hotové aplikace se jen velmi obtížně mění např. umístění datového zdroje.
Celkově lze však rozšíření hodnotit jako zdařilé, za jehož pomoci lze i poměrně složité datové vztahy řešit jednoduchými úseky programu, což se článek pokusil dokumentovat.
[1] Microsoft Co.: MSDN for Visual Studio 2008 [online]. Dostupné na http://microsoft.com [cit. 13.6.2009] v sekci MSDN. Microsoft, 2007.
Rev. 07/2009