Tipy a triky – mapy

Programátorům a administrátorům, kteří objevili možnosti .NETu, PowerShell nabízí možnosti, jak rychle prozkoumat neznámé API. Dnes si ukážeme, jakým způsobem bychom mohli postupovat v případě, že bychom si chtěli osahat kontrol na zobrazování map, GMap.NET - Great Maps for Windows Forms & Presentation. Pro jednoduchost si vybereme kontrol pro Windows Forms. Výsledkem pak bude malá aplikace, která nám vrátí GPS souřadnice vybraného místa.

Poznámka: Příklad bude běžet v pořádku pouze pokud bude PowerShell spuštěn v režimu STA. Při spuštění PowerShellu tedy nezapomeňte na přepínač -sta.

V průběhu se seznámíme s některými tipy na práci s enumy, [out] parametry a jak zapisovat handlery k událostem.

Kostra
Budeme potřebovat Form objekt, který bude mapový kontrol obsahovat:

Add-Type -AssemblyName System.Windows.Forms
$f = New-Object System.Windows.Forms.Form
$f.Size = New-Object System.Drawing.Size 700,500
... místo pro další kód
[void]$f.showdialog()
Programátoři zřejmě nic nového neobjevili, tento kód se opakuje pokaždé, když chceme psát WinForms aplikaci.
Dále si potřebujeme stáhnout příslušné assembly a načíst je. V našem případě půjde o tři assembly, které najdete v příloze. Samozřejmě si je můžete stáhnout i z výše uvedené adresy.

# assembly máme uložené v adresáři lib, načteme je všechny takto:
gci g:\lib\ *.dll | % { Add-Type -path $_.FullName }
Závislosti mezi assemblies
Pokud byste potřebovali najít "ty správné" assemblies, které máte načíst, čtěte dál. Jinak můžete pokračoval dál k vytváření kontrolů.

Assembly, kterou musíte načíst, najdete podle projektu. My budeme používat GMapControl. Rychle proletíme adresářovou strukturu (autor používá zásadu co třída, to nový soubor) a zjistíme, že kontrol se nachází v projektu GMap.NET.WindowsForms. Poté vyhledáme *.csproj soubor a v něm element AssemblyName. Tak jsme zjistili, že kontrol se kompiluje do assembly GMap.NET.WindowsForms.dll.
Našli jsme první assembly, kterou určitě načíst musíme. Stejně tak ale budeme pravděpodobně potřebovat i další assembly, na kterých tato závisí. Na závislosti se můžeme podívat přes Reflector, ale stejně dobrou práci zvládne i PowerShell.

PS> $assembly = [reflection.assembly]::LoadFile('g:\lib\GMap.NET.WindowsForms.dll')
PS> $assembly | fl
CodeBase : file:///g:\lib\GMap.NET.WindowsForms.dll
EntryPoint :
EscapedCodeBase : file:///g:\lib\GMap.NET.WindowsForms.dll
FullName : GMap.NET.WindowsForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b85b9027b614afef
GlobalAssemblyCache : False
HostContext : 0
ImageFileMachine :
ImageRuntimeVersion : v2.0.50727
Location : g:\lib\GMap.NET.WindowsForms.dll
ManifestModule : GMap.NET.WindowsForms.dll
MetadataToken :
PortableExecutableKind :
ReflectionOnly : False
Properties assembly jsme sice ještě nezjistili, ale zajímavá může být určitě informace o FullName. Ta se vyplatí v případě, kdy nám runtime hlásí, že nemůže načíst nějakou assembly. Je pravděpodobné, že se mu snažíme podstrčit špatnou verzi. Všimněte si také ImageRuntimeVersion – ta nám říká, že assembly byla překompilována pro runtime 2.0. Pro aplikace pod .NET 4.0 pak budete potřebovat zřejmě jinou verzi assembly. PowerShell ovšem stále běží na .NET 2.0/3.5, tedy pro naše potřeby máme verzi správnou.

Vraťme se zpátky. Když předhodíme $assembly commandletu Get-Member -membertype method, zjistíme metody, které nám nabízí. Pro stručnost už je zde nebudu uvádět. Ta správná metoda, která nám zjistí, na kterých ostatních assemblies ta naše závisí, se skrývá pod jménem …

PS> $assembly.GetReferencedAssemblies()
Version Name
------- ----
2.0.0.0 System.Windows.Forms
2.0.0.0 mscorlib
1.5.3.3 GMap.NET.Core
2.0.0.0 System
2.0.0.0 System.Drawing
Víme tedy, že assembly GMap.NET.Core budeme potřebovat načíst také. A když se podíváme na ni …

PS> $assembly2 = [reflection.assembly]::LoadFile('g:\lib\GMap.NET.Core.dll')
PS> $assembly2.GetReferencedAssemblies()
Version Name
------- ----
...
1.0.66.0 System.Data.SQLite
Zjistili jsme závislost na System.Data.SQLite. Z tohoto důvodu jsou všechny tyto tři assembly přibaleny k tomuto článku a výše je načítáme pomocí příkazu:

gci .. | % { Add-Type .. }
Vytvoření kontrolek
Na oficiálních stránkách bohužel mnoho dokumentace není. Inspirovat se můžeme převážně jen z demo aplikace, která je mimochodem dostupná pro Windows Forms i WPF. Zkopírujeme si základní kód na inicializaci kontrolu:

$map = New-Object GMap.NET.WindowsForms.GMapControl
$map.Anchor = 'top', 'bottom', 'left', 'right'
$map.Location = New-Object System.Drawing.Point 0,0
$map.MapType = 'GoogleMap'
$map.MarkersEnabled = $true
$map.MaxZoom = 17
$map.MinZoom = 2
$map.MouseWheelZoomType = 'MousePositionAndCenter'
$map.ShowTileGridLines = $false
$map.Size = new-object System.Drawing.Size 700, 500
$map.Zoom = 16
$map.Position = new-object GMap.NET.PointLatLng 50.207, 16.849
Povšimněte si properties Anchor, MapType a MouseWheelZoomType. PowerShell nám velmi pomáhá, při práci s enumy. Pokud se snažíme přiřadit do proměnné typu enum hodnotu typu string, PowerShell ji automaticky zkonvertuje na příslušný enum. Když přiřazujeme pole stringů a daný enum má přiřazený atribut [FlagsAttribute], pak všechny stringové hodnoty jsou zkonvertovány na enumové hodnoty a následně kombinovány dohromady. Můžete si to ověřit například takto: $map.Anchor -band [system.windows.forms.anchorstyles]::right.

Abychom si mohli přepínat mezi více typy map, přidáme si ještě na formulář combo box. Ten bude reagovat na změnu indexu a nastaví příslušný typ mapy do objektu $map:

$change = new-object System.Windows.Forms.ComboBox
$change.Anchor = 'top', 'right'
$change.Location = New-Object System.Drawing.Point 615,0
$change.DropDownStyle = 'DropDownList';
$change.FormattingEnabled = $true;
$change.Items.AddRange(@('GoogleMap', 'GoogleSatellite', 'GoogleTerrain', 'GoogleHybrid'))
$change.Size = new-object System.Drawing.Size(80, 21);
$change.TabIndex = 2;
$change.add_SelectedValueChanged({ $map.MapType = $change.SelectedItem })
$change.SelectedIndex = 1
Hodnoty GoogleMap až GoogleHybrid jsou opět hodnoty enumu. Jak zjistíme dostupné hodnoty? Například takto:

PS> $map.MapType.gettype().fullname
GMap.NET.MapType
PS> [enum]::getnames([GMap.NET.MapType])
None
GoogleMap
GoogleSatellite
GoogleLabels
GoogleTerrain
GoogleHybrid
GoogleMapChina
GoogleSatelliteChina
GoogleLabelsChina
GoogleTerrainChina
GoogleHybridChina
....
K hodnotám enumu se přistupuje – pokud nemůžeme zrovna použít automatické konverze ze stringu – jako k statickým properties. Tedy jméno enumu uzavřeme do hranatých závorek a k hodnotám se dostaneme přes dvojtečkovou notaci, viz. příklad s AnchorStyles: [system.windows.forms.anchorstyles]::right

Možná jste si všimli, jakým způsobem definujeme handler pro událost SelectedValueChanged – voláme metodu se jménem add_jmeno-udalosti a jako parametr předáváme scriptblock, který se má vykonat. Handlery mívají dva parametry, které se často ale ani nevyužijí, jako v tomto případě. Pokud byste přesto potřebovali jeden z nich přečíst, níže je zobrazen handler na událost Click, kde se získává aktuální pozice myši.

Můžeme si vyzkoušet, jestli bylo naše dosavadní snažení k něčemu dobré. Kontroly vložíme do kolekce a zobrazíme formulář.

$f.Controls.Add($change)
$f.Controls.Add($map)
[void]$f.showdialog()
Naši malou aplikaci teď můžeme spustit. Rolováním kolečka myši měníme zoom. Klikem pravým tlačítkem a tažením pak posouváme výřez.

Vidíme, že vcelku zadarmo jsme získali aplikaci, která je schopna bez prohlížeče zobrazovat mapy a dokonce je cachovat. Vnitřně totiž používá SQLite, kde si uchovává stažené náhledy a funguje tak i v případě, že zrovna není dostupné síťové připojení.

Přidání křížku
Pokud chceme ještě přidat na mapu značku, kterou bychom zacílili na námi vybraný bod, potřebujeme přidat vrstvu a do ní značku zasadit:

$overlay = New-Object GMap.NET.WindowsForms.GMapOverlay $map, "point"
$map.Overlays.Add($overlay)
$marker = New-Object GMap.NET.WindowsForms.Markers.GMapMarkerCross($map.Position)
$overlay.Markers.Add($marker)
Na mapě se nám tak bude zobrazovat červený křížek. Při kliku chceme, aby se křízek přesunul na místo, kam jsme kliknuli. To docílíme opět zavěšením na události Click.

$map.add_Click({
param($s, $e)
$marker.Position = $map.FromLocalToLatLng($e.X, $e.y)
$map.Tag = $marker.Position.Lat,$marker.Position.Lng
})
Všimněte si, že tentokráte jsme použili scriptblock s parametry. Druhý parametr obsahuje umístění myši v okamžiku kliknutí. Podle nich nastavíme červenému křížku nové souřadnice a uložíme je do property $map.Tag.

Poté, co zavřeme formulář, budeme mít v property Tag poslední kliknuté souřadnice. Přečteme je tedy a vrátíme jako výsledek skriptu. Poslední řádek skriptu tedy bude:

$map.Tag
Nastavení počátečních souřadnic
Ve skriptu máme nyní počáteční souřadnice nastavené napevno. My ovšem máme i možnost si souřadnice vyhledat. Níže uvedený kód vložte kdekoliv za inicalizaci mapového kontrolu a před zobrazení formuláře:

[GMap.NET.GeoCoderStatusCode]$status = 'G_GEO_SUCCESS'
$loc = [GMap.NET.GMaps]::Instance.GetLatLngFromGeocoder("Kralický sněžník", [ref] $status);
if ($status -eq 'G_GEO_SUCCESS') {
Write-Host "Nalezen Kralický Sněžník, souřadnice: $($loc.Lat), $($loc.Lng)"
$lat,$lng = $loc.lat,$loc.Lng
$map.Position = new-object GMap.NET.PointLatLng $lat, $lng
} else {
$lat,$lng = 49.6034664,17.254005
}
Zde si povšimněte použití metody GetLatLngFromGeocoder. Druhý parametr by měl být předáván jako [out]. Jenže PowerShell nic takového nezná. V PowerShellu se oba případy ([ref] i [out]) předávají jako [ref]. Příklad můžete vidět například na http://wiki.poshcode.org.

Někdy se nám může hodit, když známe, jak zapsat typovou proměnnou: [GMap.NET.GeoCoderStatusCode]$status. Takovým způsobem určíme typ proměnné $status, který zaručuje, že do proměnné vždy můžeme uložit pouze hodnotu typu [GMap.NET.GeoCoderStatusCode]. Opět platí, že pokud se snažíme do ní uložit stringovou hodnotu, je tato hodnota konvertována a v případě úspěchu uložena. Můžeme si to ukázat na tomto příkladu:

[GMap.NET.GeoCoderStatusCode]$status = 'G_GEO_SUCCESS'
$status = 'test' #chyba
Cannot convert value "test" to type "GMap.NET.GeoCoderStatusCode" due to invalid enumeration values.....
$status = 'G_GEO_TOO_MANY_QUERIES' #uspěje

Povedlo se?
S trochou štěstí jste se dostali až k opravdu funkčnímu kódu a dozvěděli se pár drobných triků, které vám mohou pomoci programovat téměř jako v C#. Opravdu jen téměř. Nezapomínejme totiž, že PowerShell je primárně skriptovací jazyk, který rozhodně nemá ambice stát se plnohodnotným objektově orientovaným jazykem.