Wprowadzenie

Do kogo jest adresowany ten podręcznik?

Niniejszy podręcznik jest adresowany do programistów piszących aplikacje komunikujące się z systemem MapCenter oraz do projektantów takich systemów. Stanowi on dokumentację uzupełniającą do opisu funkcji API dostarczanego z MapCenter.

Do zrozumienia treści podręcznika wymagana jest tylko podstawowa znajomość zasad programowania jednakże znajomość języka, w którym pisane są programy przykładowe będzie na pewno pomocna. Dobrze jest aby czytelnik znał takie terminy jak SOAP, WSDL, XML i HTTP.

Przykładowy kod i potrzebne narzędzia

Przykładowy kod został napisany przy użyciu środowiska Microsoft Visual C# 2005 Express Edition. Jest ono udostępniane bezpłatnie przez firmę Microsoft. Środowisko to jest potrzebne aby skompilować i uruchomić dołączone do podręcznika programy przykładowe.

Struktura systemu MapCenter

MapCenter jako serwer

System MapCenter działa jako serwer udostępniając swoje usługi innym aplikacjom. MapCenter uruchamiany jest jako serwis systemu Windows i nie posiada interfejsu komunikacji z użytkownikiem. Do zarządzania serwisem służy oddzielny program: Konfigurator MapCenter.

Aby skorzystać z możliwości oferowanych przez MapCenter należy napisać program komunikujący się z serwerem poprzez jeden z udostępnionych interfejsów. Taki program nazywa się aplikacją kliencką lub w skrócie klientem. Klient wysyła zapytania do serwera MapCenter i odbiera od niego odpowiedzi. Klient z reguły posiada interfejs użytkownika. Klientem może być samodzielna aplikacja lub skrypt uruchamiany na serwerze WWW - na przykład napisany w PHP.

Mimo, że MapCenter działa tylko w systemach Windows to ponieważ komunikuje się z aplikacjami klienckimi poprzez sieć nie ma ograniczeń co do rodzaju systemu, w którym działa klient. Może to być Windows, Linux, Unix lub nawet systemy używane w telefonach komórkowych lub palmtopach: Symbian, PalmOS lub Windows Mobile. Do współpracy z systemem MapCenter wymagana jest jedynie możliwość połączenia protokołem SOAP lub HTTP.

Interfejsy i API

System MapCenter udostępnia swoją funkcjonalność jako funkcje. Każda funkcja ma zbiór parametrów wejściowych i wyjściowych. Funkcje można traktować też jak zapytania do serwera. Parametry wejściowe funkcji są przesyłane jako zapytanie, serwer przetwarza je, a następnie odsyła parametry wyjściowe jako odpowiedź. Udostępnianych funkcji jest ponad dwieście i w miarę rozwoju systemu dołączane są nowe.

Istnieją dwie metody przesyłania zapytań do MapCenter: poprzez SOAP oraz pakiety XML protokołem HTTP.

MapCenter

Klient z reguły korzysta z jednej z metod ale nic nie stoi na przeszkodzie aby część zapytań wysyłać przez SOAP a część przez XML po HTTP.

Interfejs SOAP

SOAP jest ustandaryzowanym protokołem wymiany komunikatów XML poprzez protokół HTTP. Dzięki objęciu go standardami powstało wiele narzędzi do pracy z SOAP. Bardzo przydatny jest również dokument WSDL udostępniany przez MapCenter. Dokument ten opisuje wszystkie funkcje API dzięki czemu wiele narzędzi programistycznych automatycznie tworzy kod przesyłający zapytania SOAP.

Główną zaletą tego interfejsu jest jego standaryzacja i automatyzacja pracy programisty. Niestety istniejące luki w standardzie powodują czasami problemy na styku dwóch implementacji SOAP. Niektóre implementacje SOAP mogą też być wolniejsze niż interfejs XML.

Interfejs XML poprzez HTTP

Drugim udostępnianym interfejsem są pakiety XML przesyłane protokołem HTTP. Zawartość pakietów jest zdefiniowana w dokumentacji do API MapCenter. Dzięki temu unika się problemów z lukami w standardzie SOAP. Pełna kontrola nad strukturą pakietów XML pozwala też na - z reguły - szybsze działanie niż transmisja SOAP.

Moduły

MapCenter podzielony jest wewnętrznie na moduły - zestawy funkcji powiązanych tematycznie. W tej chwili dostępnych jest sześć modułów:

  • bazowy - podstawowe funkcje logujące do systemu, zarządzające użytkownikami, sesjami oraz udostępniające informacje o systemie
  • mapowy - funkcje geokodujące, pokazujące obraz mapy oraz przeliczające współrzędne
  • szukania - funkcje pozwalające na wyszukiwanie obiektów widocznych na mapie: miast, ulic, placów itp.
  • lokalizacji - funkcje pozwalające na umieszczanie na mapie własnych punktów, oznaczanie ich ikonami, podpisami i łączenie liniami
  • planowania trasy - funkcje znajdujące trasę między punktami mapy, generujące opis trasy oraz funkcje pomocnicze pozwalające zarządzać kategoriami kierowców i pojazdów

Nie wszystkie moduły mogą być dostępne w zakupionej wersji MapCenter. Liczba i rodzaj dostępnych modułów zależy od wykupionej licencji.

Listę dostępnych modułów można pobrać funkcją GetMapModules.

Szybki start (moduły Base i Map)

Tworzenie połączenia SOAP

Pierwszy z projektów pokazuje sposób użycia najważniejszych funkcji z modułów Base i Map.

Aby rozpocząć pracę z MapCenter należy upewnić się, że jest uruchomiony serwis MapCenter. Można to zrobić przeglądając listę serwisów systemu Windows. Następnie tworzymy nowy projekt w środowisku Microsoft C#. Kolejnym krokiem jest dołączenie usługi SOAP. Wybieramy z menu Project polecenie Add Web Reference.

Dołączanie interfejsu SOAP

Jako URL dokumentu WSDL opisującego usługę należy podać adres serwera MapCenter (czyli w tym wypadku "http://localhost:6090") z dołączoną ścieżką do dokumentu WSDL: "/wsdl/IMapCenterService". Jako nazwę dla usługi wybrano MCService ale można tu podać dowolną nazwę. Po kliknięciu na Add Reference powinniśmy w Solution Explorer odnaleźć nowy odnośnik do usługi:

Odnośnik do usługi SOAP

Dodajmy do jedynej formy w projekcie przycisk oraz - poprzez podwójne kliknięcie - procedurę obsługi. Zacznijmy od zdefiniowania dwóch zmiennych: obiektu reprezentującego serwis MapCenter oraz stringa z identyfikatorem sesji:

string strSessionID;
IMapCenterServiceservice MC;	

Identyfikator typu IMapCenterServiceservice będzie zaznaczony jako nieznany. Należy dodać dyrektywę using pozwalającą na użycie tego typu bez podawania przestrzeni nazw. Najprościej zrobić to z menu kontekstowego tak jak pokazuje rysunek:

Dodawanie namespace

Następnym krokiem jest utworzenie obiektu SOAP obsługującego komunikację z serwerem MapCenter:

MC = new IMapCenterServiceservice();
MC.Credentials = new NetworkCredential("user", "user");	

Uwierzytelnianie użytkownika MapCenter następuje na poziomie protokołu HTTP zatem przed połączeniem z serwerem należy utworzyć obiekt NetworkCredential z nazwą użytkownika i jego hasłem.

Utworzenie sesji

Dla każdego użytkownika serwer tworzy nową sesję. Pozwala to na odseparowanie użytkowników co jest istotne ze względu na parametry przechowywane w sesji przez serwer. Dzięki temu każdy użytkownik może mieć własne ustawienia mapy, własne obiekty lub własne trasy.

Sesja ma identyfikator, który jest używany w prawie wszystkich zapytaniach wysyłanych do serwera. Dlatego też identyfikator ten zapiszemy w zmiennej do późniejszego wykorzystania.

Po zakończeniu pracy z serwerem sesję należy zwolnić. Usprawnia to pracę serwera zwalniając nieużywane już zasoby.

Nieużywane sesje są zwalniane automatycznie po 30 minutach. W zależności od wykupionej licencji serwer może obsługiwać ograniczoną liczbę jednoczesnych sesji.

Funkcja CreateSessionID zwraca identyfikator sesji. Zostaje on pokazany jako tytuł okna. Poniżej znajduje się kompletny kod funkcji tworzącej sesję, wyświetlającej jej identyfikator jako tytuł okna oraz kończącej sesję i pracę z serwerem:

private void button1_Click(object sender, EventArgs e)
{
    string strSessionID;
    IMapCenterServiceservice MC;

    MC = new IMapCenterServiceservice();
    MC.Credentials = new NetworkCredential("user", "user");
    strSessionID = MC.CreateSessionID().SessionID;
   
    Text = strSessionID;

    MC.DropSession(strSessionID);
    MC = null;
}

Rysowanie mapy

Mając otwartą sesję możemy już pokusić się o pokazanie mapy. W tym celu musimy wykonać parę zapytań przygotowawczych. Ponieważ funkcje zwracające obraz mapy są bardzo ogólne trzeba przekazać im szczegółowe parametry wpływające na sposób rysowania.

Zaczynamy od dodania do formy kontrolki textBox, w której będziemy pokazywać zwrócone przez funkcje przygotowawcze informacje. Dodajemy też kontrolkę pictureBox, w której będziemy pokazywać mapę. Formę w edytorze pokazuje rysunek:

Forma główna
Gotowy projekt jest dołączony do podręcznika w katalogu MCClient_01.

Zaczynamy od pobrania listy formatów graficznych obsługiwanych przez MapCenter

TSoapGetAvailableImageFormats__mcsResult gaifRes;
gaifRes = MC.GetAvailableImageFormats();

textBox1.Text += "Image formats:\r\n";
foreach (string S in gaifRes.ImageFormats)
{
    textBox1.Text += String.Format(" {0}\r\n", S);
};	

Listę formatów pokazujemy w kontrolce textBox. Do wyświetlenia mapy wybierzemy format PNG. Znajduje się on jako pierwszy na liście. Jest to bezstratny format z dość dobrą kompresją.

Do pokazywania map najlepiej nadają się formaty z bezstratną kompresją takie jak PNG i GIF.

Następnym krokiem jest pobranie listy obsługiwanych odwzorowań. Ziemia jest kulą i nie da się jej powierzchni przedstawić na płaszczyźnie bez zniekształceń. Zawsze otrzymamy jakieś zwichrowania i błędy. Dlatego należy dobrać przekształcenie najodpowiedniejsze do naszych potrzeb. Najczęstszym wyborem będzie prawdopodobnie odwzorowanie Mercatora. Jest to walcowe odwzorowanie poprzeczne z walcem stycznym do kuli na równiku. Upraszcza to znacznie przeliczanie współrzędnych geograficznych na ekranowe choć powoduej zniekształcenia przy biegunach.

Odwzorowanie Mercatora
TSoapGetProjections__mcsResult gpRes;
gpRes = MC.GetProjections();
textBox1.Text += "Projections:\r\n";
foreach (string S in gpRes.Projections)
{
    textBox1.Text += String.Format(" {0}\r\n", S);
};	

Podobnie jak poprzednio listę odwzorowań pokazujemy na ekranie.

MapCenter dzieli informacje pokazane na mapie na warstwy. Do jednej z warstw należą drogi a do innej wyświetlane nazwy miast. Warstwy można niezależnie włączać i wyłączać. Ponieważ nie zależy nam na dopasowaniu widoku mapy, pokażemy wszystkie warstwy. Nazwy warstw pobierzemy z serwera funkcją GetDefaultLayers:

TSoapGetDefaultLayers__mcsResult gdlRes;
gdlRes = MC.GetDefaultLayers(strSessionID);
textBox1.Text += "Layers:\r\n";
foreach (string S in gdlRes.MapLayers)
{
    textBox1.Text += String.Format(" {0}\r\n", S);
};	

Wyniki funkcji przechowujemy w zmiennych gdyż będą nam one potrzebne przy wywołaniu funkcji RenderMapOnImageByPoint.

Potrzebujemy jeszcze obiektu z parametrami rysowanej mapy. Antyaliasing wygładza krawędzie linii, zaś rozdzielczość ustawiona na 96 DPI odpowiada rozdzielczości ekranu. Jeśli mapa ma być drukowana to wartość ta powinna być wyższa. Zmienna Mid przechowuje współrzędne punktu który chcemy zobaczyć. Są to 21 stopni długości geograficznej wschodniej oraz 52 stopnie i 14 minut szerokości geograficznej północnej. Współrzędne te odpowiadają mniej więcej centrum Warszawy:

TSoapRenderMapOnImageByPoint__mcsResult rmoibpRes;
TSoapTLongLatPoint Mid=new TSoapTLongLatPoint();
TSoapTImageRenderParams RenderParams=new TSoapTImageRenderParams();

Mid.Longitude=21;
Mid.Latitude=52+14.0/60;

RenderParams.Antialiasing=true;
RenderParams.DPI=96;	

Pozostaje nam tylko wywołać funkcję rysującą mapę i przesłać jej wynik do kontrolki pictureBox:

rmoibpRes=MC.RenderMapOnImageByPoint(strSessionID,
    gaifRes.ImageFormats[0], Mid, 400, 0, 0, gpRes.Projections[6], "",
    pictureBox1.Width,pictureBox1.Height,gdlRes.MapLayers,RenderParams);

pictureBox1.Image = Image.FromStream(
	new System.IO.MemoryStream(rmoibpRes.BitmapImage));

Znaczenie wszystkich parametrów jest opisane w szczegółowej dokumentacji API. Jak widać podajemy pobrane wcześniej z serwera wartości: format obrazu, odwzorowanie mapy oraz widoczne warstwy. Liczba 400 to wysokość podana w metrach, na jakiej znajduje się obserwator patrzący na powierzchnię Ziemi. Im większa ta liczba, tym wyżej jest obserwator i tym większy obszar mapy zobaczymy. Rozmiar obrazka ustawiamy tak aby pasował do rozmiaru kontrolki pictureBox.

Degeokodowanie

System MapCenter umożliwia pobieranie informacji na temat wskazanego punktu. Informacje są pobierane z różnych warstw mapy: administracyjnej, dróg, lasów itp. Część z tych warstw może być modyfikowana przez użytkownika i zawierać dynamiczne dane (zależne od sesji).

Do pobierania informacji o punkcie służy funkcja DegeocodeAtPoint. Głównym parametrem wejściowym są współrzędne geograficzne interesującego nas punktu. Do swego działania funkcja potrzebuje jednak więcej parametrów, gdyż jej wynik może zależeć od widoczności elementów mapy. Funkcja może zwracać informacje jedynie o elementach widocznych przy danych ustawieniach, nie informując o drogach lub budynkach, które w danym powiększeniu mapy byłyby ukryte. Dlatego też lista parametrów funkcji DegeocodeAtPoint trochę przypomina listę parametrów funkcji RenderMapOnImageByPoint.

Przy pobieraniu informacji o wielu punktach jednocześnie, wygodniej jest użyć funkcji Degeocode.

Przygotowaniem do wywołania funkcji degeokodującej jest pobranie domyślnych warstw zawierających informacje.

TSoapGetDegeocodeLayers__mcsResult sgdlRes;
sgdlRes = MC.GetDegeocodeLayers(strSessionID);	

Trzema istotnymi parametrami wywołania są OnlyNamedEntries, QueryRadius i UseViewVisibility. Pierwszy z nich decyduje czy pokazywać tylko informacje o obiektach z nadaną nazwą. Nie wszystkie obiekty mają nazwy, na przykład większość lasów występuje na mapie bez nazwy. Opcja ta pozwala pominąć takie obiekty co jest bardzo przydatne jeśli wynik degeokodowania pokazujemy użytkownikowi. QueryRadius określa jak daleko ma sięgać szukanie. UseViewVisibility ukrywa informacje o obiektach niewidocznych przy podanych ustawieniach mapy. W poniższym przykładzie pokazujemy wszystkie elementy mapy zawierające niepustą nazwę i znajdujące się nie dalej niż 400 metrów od środka mapy:

TSoapDegeocodeAtPoint__mcsResult sdapRes;
sdapRes = MC.DegeocodeAtPoint(strSessionID, Mid, 400, 0, 0, 
	gpRes.Projections[6], "", Mid, 400, 10, sgdlRes.DegeocodeLayers, 
	true, false);

Pozostało teraz pokazać wyniki działania funkcji użytkownikowi:

textBox1.Text += "Degeocode result:\r\n";
textBox1.Text += String.Format(" Country: {0}\r\n", sdapRes.AreaName0);
textBox1.Text += String.Format(" Adm. level 2: {0}\r\n", sdapRes.AreaName1);
textBox1.Text += String.Format(" Adm. level 3: {0}\r\n", sdapRes.AreaName2);
textBox1.Text += String.Format(" Adm. level 4: {0}\r\n", sdapRes.AreaName3);
textBox1.Text += String.Format(" ZIP: {0}\r\n", sdapRes.Zip);	

Część z warstw może zwrócić wiele wyników. W otoczeniu podanego punktu może znajdować się wiele miast lub budynków. Niektóre warstwy zwracają też dodatkowe informacje takie jak kategoria, rodzaj drogi lub odległość od punktu. W podanym przykładzie pokazujemy kategorię, nazwę i odległość obiektów z warstwy OtherMapElements:

foreach (TSoapTDegeocodeAtPointResult S in sdapRes.OtherMapElements)
{
   textBox1.Text += String.Format(" Other map elements: {0} - {1}  ({2,2:f}km)\r\n", 
       S.Category, S.Name, S.FoundLength / 1000);
};	

Projekt przykładowy dołączony do podręcznika zawiera kod pokazujący informacje z pozostałych warstw i jest on analogiczny do powyższego. Pracę z serwerem MapCenter kończymy zamykając sesję:

// zamknięcie sesji
MC.DropSession(strSessionID);	
Jeśli program ma podtrzymywać kontakt z serwerem przez długi czas bez wykonywania zapytań należy co jakiś czas wywołać funkcję KeepSession. Nieużywane sesje zwalniane są po 30 minutach zatem wywołanie KeepSession co 15 minut zapewni nam poprawny identyfikator sesji przez cały czas działania programu.

Końcowy wynik pokazuje rysunek:

Mapa Warszawy

Moduł Search

Oprócz pokazywania mapy system MapCenter zawiera wiele funkcji operujących na danych mapowych. Bardzo ważną funkcją z punktu widzenia użytkownika końcowego jest szukanie obiektów na mapie. Tymi obiektami mogą być miasta, ulice, skrzyżowania, parki itp. Szukanie jest procesem kilkuetapowym. Dodatkowo, ponieważ zwracane listy mogą być bardzo duże, zostały one podzielone na części. Serwer nie zwraca wszystkich znalezionych obiektów, a tylko kilkadziesiąt pierwszych. O kolejne obiekty z listy trzeba się upomnieć wysyłając oddzielne zapytanie.

Kod przykładowy ilustrujący proces szukania został dołączony do podręcznika jako projekt MCClient_02. Aby kod stał się bardziej czytelny wszystkie funkcje komunikujące się z systemem MapCenter zostały wydzielone do osobnej klasy o nazwie MCServerCore. Konstruktor tej klasy łączy się z serwerem tworząc sesję, a także pobiera listy formatów i warstw potrzebnych przy rysowaniu mapy:

public MCServerCore()
{
    // utworzenie sesji
    TSoapCreateSessionID__mcsResult scsRes;

    MC = new IMapCenterServiceservice();
    MC.Credentials = new NetworkCredential("user", "user");
    scsRes = MC.CreateSessionID(); Check(scsRes.SoapResult);
    SessionID = scsRes.SessionID;

    gaifRes = MC.GetAvailableImageFormats(); Check(gaifRes.SoapResult);
    gpRes = MC.GetProjections(); Check(gpRes.SoapResult);
    gdlRes = MC.GetDefaultLayers(SessionID); Check(gdlRes.SoapResult);
    rpgartRes = MC.RoutePlannerGetAvailableRoadTypes(SessionID); Check(rpgartRes.SoapResult);
}  

Klasa ma również destruktor, który anuluje sesję:

~MCServerCore()
  {
      MC.DropSession(SessionID);
  }

Każde wywołanie funkcji MapCenter jest zakończone sprawdzeniem wyniku. Funkcje te zwracają liczbę całkowitą określającą czy wywołanie zakończyło się sukcesem. Jeśli wystąpił błąd to numer ten określa jego rodzaj. Tabela z numerami błędów znajduje się w dokumentacji dołączonej do systemu. Funkcja sprawdzająca jest dość prosta, jeśli podany jako parametr wejściowy wynik zapytania jest niedodatni rzuca ona wyjątek:

public void Check(int Result)
{
    if (Result <= 0)
 {
 throw new system.applicationexception(
			string.Format("Server call failed with result: {0}", result));
 };
}	

Główna forma projektu zawiera komponent pictureBox1 pokazujący obraz mapy oraz komponent timerKeepSession, który jest odpowiedzialny za wysyłanie co 15 minut zapytania KeepSession. Zapobiega to anulowaniu sesji przez serwer w sytuacji gdy użytkownik odejdzie od komputera na dłuższy czas. Procedura obsługi zdarzenia Tick dla komponentu timerKeepSession wywołuje funkcję KeepSession klasy MCServerCore a ta z kolei wysyła zapytanie do serwera MapCenter:

private void timerKeepSession_Tick(object sender, EventArgs e)
{
    MC.KeepSession();
}

public void KeepSession()
{
    Check(MC.KeepSession(SessionID));
}

Główna forma dba o odświeżenie obrazu mapy przy każdej zmianie rozmiaru okna. Zdarzenie Resize komponentu pictureBox1 wywołuje funkcję PaintMapImage:

private void PaintMapImage()
{
    pictureBox1.Image = MC.GetMapFromPoint(Mid, Alt, pictureBox1.Width, pictureBox1.Height);
}

Jest to uproszczone wywołanie funkcji rysującej mapę. Zawiera tylko najczęściej używane parametry czyli środek mapy, wysokość obserwatora i rozmiar wyjściowego obrazu. Funkcja ta jest zdefiniowana w klasie MCServerCore:

public Image GetMapFromPoint(TSoapTLongLatPoint point, double alt, int width, int height)
{
    double Left, Right, Bottom, Top;
    Left = 0;
    Right = 0;
    Bottom = 0;
    Top = 0;
    return GetMapFromPoint(point, alt, width, height, 
		ref Left, ref Right, ref Bottom, ref Top);            
}

public Image GetMapFromPoint(TSoapTLongLatPoint point, double alt, int width, int height,
    ref double Left, ref double Right, ref double Bottom, ref double Top)
{
    TSoapRenderMapOnImageByPoint__mcsResult rmoibpRes;
    TSoapTImageRenderParams RenderParams = new TSoapTImageRenderParams();

    RenderParams.Antialiasing = true;
    RenderParams.DPI = 96;

    rmoibpRes = MC.RenderMapOnImageByPoint(SessionID,
        gaifRes.ImageFormats[DefaultImageFormat], point, alt, 0, 0,
		gpRes.Projections[DefaultProjection],
        "", width, height, gdlRes.MapLayers, RenderParams); Check(rmoibpRes.SoapResult);

    Left = rmoibpRes.BitmapLeftUpCorner.Longitude;
    Right = rmoibpRes.BitmapRightUpCorner.Longitude;
    Bottom = rmoibpRes.BitmapLeftDownCorner.Latitude;
    Top = rmoibpRes.BitmapRightDownCorner.Latitude;
    return Image.FromStream(new System.IO.MemoryStream(rmoibpRes.BitmapImage));
}
	

Jak widać funkcja ma dwie postacie, bardziej rozbudowana zwraca współrzędne rogów mapy. Pozwala to na zorientowanie się jaki obszar jest pokazywany na ekranie choć w tym przykładzie nie jest to wykorzystywane.

Obszar pokazywany na mapie nie musi być "prostokątny" (długość geograficzna lewego górnego narożnika mapy może być inna niż długość lewego dolnego narożnika, podobnie wygląda sytuacja z szerokością geograficzną). W zależności od użytego odwzorowania może to być dość skomplikowana figura. Jeśli zależy nam na prostokątnym kształcie mapy najlepiej wybrać odwzorowanie Mercatora. MapCenter

GetMapFromPoint działa analogicznie do przykładu z pierwszej części podręcznika.

Nawigację zapewnia obsługa kliknięć na mapie oraz przycisków plus i minus. Jeśli użytkownik kliknie blisko krawędzi spowoduje to przesunięcie mapy. Przyciski plus i minus służą powiększaniu i zmniejszaniu mapy.

	
private void Form1_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
    if (e.KeyCode == Keys.Add)
    {
        Alt /= 2;
        if (Alt < 400) Alt = 400;
        PaintMapImage();
    };
    if (e.KeyCode == Keys.Subtract)
    {
        Alt *= 2;
        PaintMapImage();
    }
}

Zmienna Alt przechowuje wysokość obserwatora. Wciśnięcie przycisku plus spowoduje jej dwukrotne zmniejszenie a tym samym przybliżenie mapy. Jeśli dzielenie spowodowało zbytnie zmniejszenie wysokości ustawiamy ją na wartość minimalną. Wciśnięcie przycisku minus spowoduje dwukrotne zwiększenie wysokości czyli oddalenie mapy.

Przesuwanie mapy polega na zmianie wartości zmiennej Mid określającej pozycję środka mapy. Po kliniknięciu na komponencie z obrazem mapy sprawdzamy współrzędne kliknięcia i jeśli są one bliskie krawędzi przesuwamy mapę w odpowiednią stronę. Ponieważ mapa wyświetlana jest w odwzorowaniu Mercatora, to jej krawędzie pionowe są równoległe do południków a krawędzie poziome do równoleżników. Dodatkowo szerokość i długość geograficzna zmieniają się liniowo wzdłuż krawędzi. Dzięki temu jeśli chcemy przesunąć mapę wystarczy zmienić jedną ze współrzędnych zmiennej Mid o część szerokości mapy. Szerokość (lub wysokość) mapy określimy używając zapytania ConvertScreenToMap. Zamienia ono współrzędne ekranowe na długość i szerokość geograficzną.

W naszym przypadku musimy zamienić dwa punkty: lewy górny i prawy dolny. Dlatego funkcja konwertująca z klasy MCServerCore przyjmuje dwa punkty jako parametry:

public void ConvertScreenToMap(int x1, int y1, int x2, int y2, 
    double MidLon, double MidLat, double Alt, int Width, int Height,
    ref double outlon1, ref double outlat1, ref double outlon2, ref double outlat2)
{
    TSoapTPoint[] ScreenPoints = new TSoapTPoint[2];
    ScreenPoints[0]=new TSoapTPoint();
    ScreenPoints[1]=new TSoapTPoint();
    ScreenPoints[0].X = x1;
    ScreenPoints[0].Y = y1;
    ScreenPoints[1].X = x2;
    ScreenPoints[1].Y = y2;
    
    TSoapTLongLatPoint Mid = new TSoapTLongLatPoint();
    Mid.Longitude = MidLon;
    Mid.Latitude = MidLat;

    TSoapConvertScreenToMap__mcsResult scstmRes;
    TSoapTImageRenderParams RenderParams = new TSoapTImageRenderParams();
    RenderParams.DPI = 96;

    scstmRes = MC.ConvertScreenToMap(SessionID, Mid, Alt, 0, 0,
        gpRes.Projections[DefaultProjection], "", Width, Height, RenderParams, ScreenPoints);
    Check(scstmRes.SoapResult);
    Debug.Assert(scstmRes.MapPoints.Length==2);
    outlon1 = scstmRes.MapPoints[0].Longitude;
    outlat1 = scstmRes.MapPoints[0].Latitude;
    outlon2 = scstmRes.MapPoints[1].Longitude;
    outlat2 = scstmRes.MapPoints[1].Latitude;
}

Parametry wywołania ConvertScreenToMap muszą mieć takie same wartości jak podczas rysowania mapy aby obliczone wartości zgadzały się z obrazem mapy.

private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
    double dx = 0, dy = 0, lon1 = 0, lat1 = 0, lon2 = 0, lat2 = 0;
    const double ScreenDelta = 0.25, Step = 0.33;

    if (e.Button == MouseButtons.Left)
    {
        if (e.X > (double)pictureBox1.Width * (1.0 - ScreenDelta)) dx = Step;
        else 
         if (e.X < (double)pictureBox1.Width * ScreenDelta) dx = -Step;
        if (e.Y > (double)pictureBox1.Height * (1 - ScreenDelta)) dy = Step;
        else
         if (e.Y < (double)pictureBox1.Height * ScreenDelta) dy = -Step;

        MC.ConvertScreenToMap(0, 0, pictureBox1.Width - 1, pictureBox1.Height - 1,
            Mid.Longitude, Mid.Latitude, Alt, pictureBox1.Width, pictureBox1.Height, 
            ref lon1, ref lat1, ref lon2, ref lat2);

        Mid.Longitude += (lon2 - lon1) * dx;
        Mid.Latitude += (lat2 - lat1) * dy;
        PaintMapImage();
    }
    if (e.Button == MouseButtons.Right)
    {
        MC.ConvertScreenToMap(e.X, e.Y, 0, 0,
            Mid.Longitude, Mid.Latitude, Alt, pictureBox1.Width, pictureBox1.Height,
            ref lon1, ref lat1, ref lon2, ref lat2);
        string MsgText = "";
        MsgText = MC.DegeocodeAtPoint(lon1, lat1, Alt);
        MessageBox.Show(MsgText);
    }
}

Procedura obsługi kliknięcia sprawdza, w którym miejscu na mapie kliknięto i ustawia odpowiednio wartości zmiennych dx i dy. Określają one o ile i w którym kierunku należy przesunąć punkt środkowy mapy. Następnie znajdowane są współrzędne rogów ekranu i następuje modyfikacja współrzędnych przechowywanych w zmiennej Mid. Na końcu funkcja odświeża obraz mapy.

Szukanie zostało zrealizowane w oddzielnej formie pokazanej na rysunku poniżej:

Forma szukania

Na formie umieszczona jest lista z państwami, lista z typami obiektów (ulice, place, lotniska itp.), pola edycyjne do wpisania nazw miasta i obiektu oraz dwie listy zawierające wyniki szukania. Dodatkowy przełącznik ASCII Search decyduje o tym czy przy szukaniu ignorowane są polskie znaki diakrytyczne (takie jak ąęśćż).

Przy szukaniu pewne parametry, takie jak lista znalezionych obiektów, przechowywane są w sesji dlatego też każde nowe szukanie rozpocząć należy od wyczyszczenia parametrów szukania zapytaniem SearchInitialize. Określa się przy tym czy podczas całego procesu szukania należy ignorować znaki diakrytyczne. W klasie MCServerCore zdefinowao odpowiednią funkcję:

public void SearchInitialize(bool AsciiSearch)
{
    Check(MC.SearchInitialize(SessionID, AsciiSearch)); 
}

Jest ona wywoływana po otwarciu formy szukania w procedurze obsługi zdarzenia Shown:

private void Form2_Shown(object sender, EventArgs e)
{
    MC.SearchInitialize(checkBox1.Checked);
    LoadStaticLists();
}

Funkcja LoadStaticList ładuje listy państw i typów obiektów. Listy te są niezmienne i dlatego wystarczy je załadować przy otwarciu formy lub nawet przy starcie programu. Funkcja pobiera listy do tablic typu string i wpisuje do komponentów combo:

private void LoadStaticLists()
{
    string[] Arr=null;

    MC.SearchGetCountryList(ref Arr);
    comboCountries.Items.Clear();
    foreach (string S in Arr) comboCountries.Items.Add(S);
    comboCountries.SelectedIndex = 46; // Poland

    MC.SearchGetItemKindList(ref Arr);
    comboItemKinds.Items.Clear();
    foreach (string S in Arr) comboItemKinds.Items.Add(S);
    comboItemKinds.SelectedIndex = 6; // roads and streets
}

Funkcje SearchGetCountryList i SearchGetItemKindList pochodzą z klasy MCServerCore i jak widać nie wysyłają do serwera żadnych parametrów poza identyfikatorem sesji:

public void SearchGetCountryList(ref string[] Arr)
{
    TSoapSearchGetCountryList__mcsResult ssgclRes;
    ssgclRes = MC.SearchGetCountryList(SessionID); Check(ssgclRes.SoapResult);
    Arr = ssgclRes.CountryNames;
}

public void SearchGetItemKindList(ref string[] Arr)
{
    TSoapSearchGetItemKindList__mcsResult ssgiklRes;
    ssgiklRes = MC.SearchGetItemKindList(SessionID); Check(ssgiklRes.SoapResult);
    Arr = ssgiklRes.ItemKindNames;
}	
Obie zwracane listy zawierają pusty string na pierwszej pozycji. Oznacza on ignorowanie państwa lub typu obiektu przy szukaniu. Aplikacja kliencka powinna taki pusty string zamieniać na napis bardziej czytelny dla użytkownika np.: "Dowolne państwo".

Aby wyszukać miasto użytkownik powinien wybrać nazwę państwa z listy a następnie wpisać kilka początkowych liter nazwy. Ponieważ wysłanie zapytania o listę miast i odbiór odpowiedzi może trwać dość długo (w zależności od rodzaju połączenia i obciążenia serwera), nie należy robić tego po każdym wpisaniu litery. Na formie umieszczono komponent timerCity, który zostaje aktywowany po wprowadzeniu litery w polu City. Komponent ten po upływie sekundy powoduje wysłanie zapytania szukającego do serwera.

private void textCity_TextChanged(object sender, EventArgs e)
{
    timerCity.Enabled = false; timerCity.Enabled = true;
}

private void timerCity_Tick(object sender, EventArgs e)
{
    DoCitySearch();
}

Ponieważ licznik czasu powinien być resetowany po wprowadzeniu każdej litery komponent jest wyłączany i włączany ponownie. Dzięki temu czas liczony jest zawsze od zera.

Funkcja DoCitySearch wykonuje właściwą pracę szukania miasta:

private void DoCitySearch()
{
    timerCity.Enabled = false;
    int CityCount;

    CityCount = MC.SearchSelectCities(comboCountries.SelectedIndex, 
		textCity.Text, "", "", "", "");

    string[] Cities = null;
    string[][] Adm = null;
    MC.SearchGetCityList(0, CityCount, ref Cities, ref Adm);

    listCities.Items.Clear();
    for (int I = 0; I < cities.length; i++)
 {
 string s = "";
 s = cities[i];
 foreach (string a in adm[i])
 if (a != "") s += " (" + a + ")";
 listcities.items.add(s);
 }
}	

Na samym początku funkcja wyłącza komponent timerCity, który spowodował jej wywołanie. Komponenty typu timer wysyłają zdarzenia cyklicznie a w tym wypadku potrzebne jest tylko jedno wywołanie funkcji szukającej.

Następnym krokiem jest wysłanie do serwera danych potrzebnych do stworzenia listy miast. W tym wypadku jest to numer państwa (pobrany z listy państw) oraz początkowe litery nazwy miasta. Funkcja SearchSelectCities z klasy MCServerCore wywołuje zapytanie o tej samej nazwie:

public int SearchSelectCities(int Country, string City, 
	string ZIP, string Adm1, string Adm2, string Adm3)
{
    TSoapSearchSelectCities__mcsResult ssscRes;
    ssscRes = MC.SearchSelectCities(SessionID, Country, City, ZIP, Adm1, Adm2, Adm3);
	Check(ssscRes.SoapResult);
    return ssscRes.ResultCount;
}

Lista znalezionych miast nie jest zwracana, pozostaje ona na serwerze. Zwracana jest jedynie jej długość co pozwala przygotować po stronie klienta miejsce na jej pobranie. Następnym krokiem jest pobranie części listy miast z serwera. Zapytanie SearchGetCityList oprócz nazw miast zwraca też skróty jednostek administracyjnych, do których dane miasto należy. Pokazywanie ich użytkownikowi jest przydatne gdyż istnieje wiele miast o tej samej nazwie i skróty te pozwalają je rozróżniać.

Funkcja SearchGetCityList z klasy MCServerCore pobiera dwa parametry: numer pierwszego elementu oraz liczbę elementów:

public void SearchGetCityList(int First, int Count, ref string[] Cities, 
	ref string[][] AdmNames)
{
    TSoapSearchGetCityList__mcsResult ssgclRes;
    ssgclRes = MC.SearchGetCityList(SessionID, First, Count); Check(ssgclRes.SoapResult);
    Cities = ssgclRes.CityNames;
    AdmNames = ssgclRes.CityAdmAbbrev;
}

W obecnej wersji systemu MapCenter maksymalna liczba zwracanych jednocześnie elementów listy wynosi 100. Nie należy jednak traktować jej jak stałą. Trzeba zawsze sprawdzać długość tablicy zwróconej przez serwer. W przykładowym programie żądamy pobrania całej listy ale serwer ogranicza żądanie do 100 elementów jeśli jest ona zbyt długa. W prawdziwym programie należy dociągnąć resztę elementów na żądanie użytkownika, np. wtedy gdy przewinie on suwak listy w dół. Całkowitą długość listy znamy gdyż zwracana jest ona przez funkcję SearchSelectCities.

Lista skrótów nazw jednostek administracyjnych jest dwuwymiarowa (tablica tablic). Każdy jej element zawiera klika skrótów. Kolejność skrótów odpowiada ważności jednostek, najpierw państwo potem województwo itd.

Bardzo podobnie do szukania miasta działa szukanie obiektów. Znów mamy kontrolkę edycyjną, w którą użytkownik wpisuje początkowe litery obiektu. Kontrolka po każdym wprowadzeniu litery aktywuje obiekt timerObject:

private void textObject_TextChanged(object sender, EventArgs e)
{
    timerObject.Enabled = false; timerObject.Enabled = true;
}

Komponent timerObject po upływie sekundy powoduje wysłanie zapytań szukających:

private void timerObject_Tick(object sender, EventArgs e)
{
    DoObjectSearch();
}

Funkcja DoObjectSearch wygląda analogicznie do funkcji DoCitySearch:

private void DoObjectSearch()
{
    timerObject.Enabled = false;
    int ObjectCount;

    ObjectCount = MC.SearchSelectItems(
		listCities.SelectedIndex >= 0 ? listCities.SelectedIndex : 0,
		comboItemKinds.SelectedIndex >= 0 ? comboItemKinds.SelectedIndex : 0,
		textObject.Text);

    string[] Objects = null;
    MC.SearchGetItemsList(0, ObjectCount, ref Objects);

    listObjects.Items.Clear();
    foreach (string S in Objects) listObjects.Items.Add(S);
}

Najpierw wysyłane są parametry zapytania szukającego. W tym wypadku jest to numer wybranego miasta, numer typu obiektu oraz początkowe litery nazwy. Następnie pobieramy z serwera listę wyszukanych obiektów i wpisujemy do komponentu listObjects. Funkcje SearchSelectItems i SearchGetItemsList pochodzą z klasy MCServerCore:

public int SearchSelectItems(int City, int Kind, string Object)
{
    TSoapSearchSelectItems__mcsResult sssiRes;
    sssiRes = MC.SearchSelectItems(SessionID, City, Kind, Object); Check(sssiRes.SoapResult);
    return sssiRes.ResultCount;
}

public void SearchGetItemsList(int First, int Count, ref string[] Names)
{
    TSoapSearchGetItemsList__mcsResult ssgilRes;
    ssgilRes = MC.SearchGetItemsList(SessionID, First, Count); Check(ssgilRes.SoapResult);
    Names = ssgilRes.ItemNames;
}

Wyszukane miasto lub obiekt należy pokazać na mapie. Serwer przechowuje w sesji listę elementów, które należy dodatkowo wyróżnić przy rysowaniu mapy. Po wyszukaniu należy dopisać miasto lub obiekt do tej listy i odświeżyć obraz mapy. Robi to forma szukania przy jej zamykaniu:

private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
    MidPoint = null;
    if (LastActiveControl == 1)
        MC.AddCityToSelection(listCities.SelectedIndex >= 0 ? listCities.SelectedIndex : 0, ref MidPoint);
    if (LastActiveControl == 2)
        MC.AddObjectToSelection(listObjects.SelectedIndex >= 0 ? listObjects.SelectedIndex : 0, ref MidPoint);
}

public void AddCityToSelection(int City, ref TSoapTLongLatPoint MidPoint)
{
    TSoapSearchAddCityToSelection__mcsResult ssactsRes;
    ssactsRes = MC.SearchAddCityToSelection(SessionID, City); Check(ssactsRes.SoapResult);
    MidPoint = ssactsRes.MidPoint;
}

public void AddObjectToSelection(int Object, ref TSoapTLongLatPoint MidPoint)
{
    TSoapSearchAddObjectToSelection__mcsResult ssaotsRes;
    ssaotsRes = MC.SearchAddObjectToSelection(SessionID, Object); Check(ssaotsRes.SoapResult);
    MidPoint = ssaotsRes.MidPoint;
}

Ponieważ mamy dwie klasy elementów (miasta i obiekty), do zaznaczania na mapie potrzebna jest informacja, o który z nich chodziło użytkownikowi. Można przyjąć założenie, że jeśli ostatnio użytkownik korzystał z listy miast, to szukał miasta, a jeśli korzystał z listy obiektów, to szukał obiektu. Dlatego do obu list i kontrolek edycyjnych podpinamy zdarzenie zapisujące w zmiennej LastActiveControl typ szukanego elementu:

private void listCities_Enter(object sender, EventArgs e)
{
    LastActiveControl = 1;
}

private void textCity_Enter(object sender, EventArgs e)
{
    LastActiveControl = 1;
}

private void textObject_Enter(object sender, EventArgs e)
{
    LastActiveControl = 2;
}

private void listObjects_Enter(object sender, EventArgs e)
{
    LastActiveControl = 2;
}
	

Przy zamykaniu formy sprawdzamy wartość tej zmiennej i wywołujemy odpowiednią funkcję.

Oprócz podświetlenia wyszukanego elementu mapy należy też ustawić mapę tak, aby go pokazała. Funkcje dodające elementy do listy podświetlenia (AddCityToSelection i AddObjectToSelection) zwracają współrzędne punktu. Używamy tych współrzędnych aby przesunąć obraz mapy w odpowiednie miejsce:

private void searchToolStripMenuItem_Click(object sender, EventArgs e)
{
    Form2 AForm = new Form2();
    System.Windows.Forms.DialogResult Res;
    
    AForm.MC = MC;
    Res = AForm.ShowDialog();
    AForm.MC = null;

    if ((Res == System.Windows.Forms.DialogResult.OK) && (Mid != null))
    {
        Mid = AForm.MidPoint;
        PaintMapImage();
    }
}

Sposób zaznaczenia obiektu zależy od jego rodzaju. Podświetloną ulicę pokazuje poniższy rysunek:

Zaznaczone miasto

Funkcje szukające dobrze nadają się do tworzenia interfejsu bezpośrednio komunikującego się z człowiekiem. Jednak często istnieje potrzeba automatycznego zgeokodowania adresu (czyli zamiany adresu na współrzędne geograficzne). Pomocne jest wtedy zapytanie Geocode. Dla podanych danych adresowych zwraca ono zawsze tylko jeden - możliwie najlepiej dopasowany - wynik. System MapCenter stara się wykorzystać wszystkie dostępne informacje ale nie zawsze jest to możliwe. Dane wejściowe mogą zawierać np. błędnie podaną nazwę ulicy. W takim wypadku nazwa ulicy jest ignorowana i jako wynik geokodowania zwracane jest tylko miasto. Oprócz współrzędnych geograficznych serwer podaje również dokładność geokodowania, czyli które z przesłanych informacji zostały użyte przy odnajdowaniu pozycji punktu.

Dla uproszczenia funkcja Geocode w klasie MCServerCore wysyła tylko jeden punkt na raz mimo, że zapytanie Geocode może geokodować wiele punktów jednocześnie:

public int Geocode(string Country, string County, string District, string City, 
    string ZIP, string Street, string StreetNum, ref TSoapTLongLatPoint MidPoint)
{
    TSoapTGeocodePointInfo[] Input=new TSoapTGeocodePointInfo[1];
    TSoapGeocode__mcsResult sgRes;

    Input[0] = new TSoapTGeocodePointInfo();
    Input[0].Country = Country;
    Input[0].County = County;
    Input[0].District = District;
    Input[0].City = City;
    Input[0].Zip = ZIP;
    Input[0].Street = Street;
    Input[0].StreetNumber = StreetNum;
    sgRes = MC.Geocode(SessionID, false, Input);
    if (sgRes.GeocodeLevel[0] != 0)
        MidPoint = sgRes.Positions[0];
    else
        MidPoint = null;
    return sgRes.GeocodeLevel[0];
}

Projekt MCClient_02 zawiera formę Form3 służącą do podawania danych wejściowych do geokodowania:

Forma geokodowania

Dane wprowadzone przez użytkownika są wysyłane do serwera MapCenter i przetwarzane w procedurze obsługi przycisku:

private void button1_Click(object sender, EventArgs e)
{
    TSoapTLongLatPoint MidPoint=null;
    ListViewItem Item=new ListViewItem();
    int GeoLevel;
    GeoLevel = MC.Geocode(textCountry.Text, textCounty.Text, textDistrict.Text, 
        textCity.Text, textZIP.Text, textStreet.Text, textStreetNum.Text, ref MidPoint);
    Item.Text = textCountry.Text;
    Item.SubItems.Add(textCity.Text);
    Item.SubItems.Add(textStreet.Text);
    if (MidPoint != null)
    {
        Item.SubItems.Add(MidPoint.Longitude.ToString());
        Item.SubItems.Add(MidPoint.Latitude.ToString());
    }
    else
    {
        Item.SubItems.Add("---");
        Item.SubItems.Add("---");
    };

    string S = "";
    if ((GeoLevel & 1) != 0) S += "[Country]";
    if ((GeoLevel & 2) != 0) S += "[County]";
    if ((GeoLevel & 4) != 0) S += "[District]";
    if ((GeoLevel & 8) != 0) S += "[City]";
    if ((GeoLevel & 16) != 0) S += "[ZIP]";
    if ((GeoLevel & 32) != 0) S += "[Street]";
    if ((GeoLevel & 64) != 0) S += "[Num]";
    Item.SubItems.Add(S);

    listView1.Items.Add(Item);
}

Do komponentu listView1 wpisywane są kolejno geokodowane punkty. Początkowe kolumny zawierają: państwo, miasto i geokodowaną ulicę, następne współrzędne zgeokodowanego punktu (jeśli geokodowanie się nie udało kolumny zawierają "---"). Ostatnia kolumna pokazuje których informacji serwer użył do znalezienia punktu. Informacje o poziomie (dokładności) geokodowania są przesyłane jako kolejne bity w zmiennej całkowitej.

Jeśli nie wszystkie pola zostały uwzględnione przez serwer, to najczęściej oznacza to, że zawierały błędne informacje. Poziom geokodowania pozwala wyróżnić te pola i umożliwia użytkownikowi korektę błędnie wprowadzonych danych.

Moduł Localize

Moduł lokalizacji jest dość rozbudowany i umożliwia pokazywanie na mapie punktów oznaczonych sygnaturą lub ikoną, łączenie tych punktów liniami (również z zaznaczeniem kierunku) oraz dodawanie pól opisowych.

Moduł lokalizacji operuje na obiektach przechowywanych w sesji serwera. Każdy obiekt jest identyfikowany przez unikalny numer. Mimo, że numer nie jest nadawany przez serwer tylko przez aplikacje klienckie, to serwer dba o to aby dwa obiekty nie zawierały takiego samego identyfikatora, zwracając w takich przypadkach błąd DuplicatedLocalizeEntityID. Również podanie nieistniejącego w sesji identyfikatora spowoduje błąd - InvalidLocalizeEntityID. Obiekt ma również nazwę i kilka parametrów wpływających na sposób jego wyświetlania: ikonę, kolor, parametry czcionki.

Do punktów przypisane są listy pozycji. Każda pozycja zawiera współrzędne geograficzne, czas, opis oraz parametry wyświetlania podobne do tych przypisanych do obiektu. Oprócz pól stałych są jeszcze pola dodawane przez użytkownika. Dla uproszczenia przykładów ten aspekt modułu lokalizacyjnego nie został opisany.

Obiekty lokalizacyjne doskonale nadają się do reprezentowania ścieżek pobranych z systemów lokalizacji i śledzenia pojazdów. Pola dodatkowe, definiowane przez użytkownika mogą wtedy przechowywać dane pobrane z samochodu takie jak: prędkość, pozostałe paliwo czy włączenie silnika.

Do zaprezentowania lokalizacji użyjemy formy geokodującej. Po zgeokodowaniu kilku punktów mamy listę ze współrzędnymi. Możemy pokazać je na dwa sposoby:

  • jako pojedynczy obiekt z kilkoma pozycjami w ścieżce
  • jako wiele obiektów, z których każdy ma tylko jedną pozycję w ścieżce

Przed zlokalizowaniem obiektów pytamy użytkownika w jaki sposób chce je mieć pokazane na mapie. Wystarczy teraz utworzyć na serwerze obiekty lokalizacyjne odpowiadające tym punktom. Lista pozycji dla każdego z punktów będzie zawierała tylko jeden element - zgeokodowany adres.

Za dodanie obiektu lokalizacyjnego odpowiada zapytanie LocalizeObjectAdd:

public void LocalizeObjectAdd(int ID, string Name, bool Connected, int IconID)
{
    TSoapTFontParameters Font=new TSoapTFontParameters();
    Font.Name = "Arial";
    Font.Size = 10;
    Check(MC.LocalizeObjectAdd(SessionID, ID, Name, true, IconID, IconID!=-1, 0, 0x0000FF, 1, 5, false, Connected, Connected, Font));
}

Najważniejsze przekazywane parametry to identyfikator obiektu i jego nazwa. Pozostałe decydują o jego wyglądzie. W naszym przypadku obiekty będą oznaczane 5-cio pikselowym kółkiem w kolorze czerwonym (stała 0x0000FF w formacie BlueGreenRed oznacza kolor czerwony) zaś podpis pod obiektem będzie rysowany 10-cio punktową czcionką Arial.

Dodawanie pozycji do istniejących pozycji realizuje zapytanie LocalizeObjectPositionAdd:

public void LocalizeObjectPositionAdd(int ID, double Lon, double Lat, double NumField1, double NumField2, string StrField1)
{
    double[] NumFields = new double[2];
    string[] StrFields = new string[1];
    NumFields[0] = NumField1;
    NumFields[1] = NumField2;
    StrFields[0] = StrField1;
    TSoapTLongLatPoint Mid=new TSoapTLongLatPoint();
    Mid.Longitude = Lon;
    Mid.Latitude = Lat;
    TSoapTFontParameters Font=new TSoapTFontParameters();
    Font.Name = "Times New Roman";
    Font.Size = 14;

    Check(MC.LocalizeObjectPositionAdd(SessionID, ID, NumFields, StrFields, Mid, 0, "", true, true, 0, false, 0, Font));
}

Oprócz pozycji punktu przesyłane są też dwa dodatkowe pola numeryczne i jedno tekstowe. Opis tych pól należy przesłać do serwera zanim zaczniemy lokalizować punkty. Robią to funkcje LocalizeNumFieldsAdd i LocalizeStrFieldsAdd:

public void LocalizeNumFieldsAdd(string Name, bool ShowOnInfo)
{
    Check(MC.LocalizeNumFieldsAdd(SessionID, Name, ShowOnInfo));
}

public void LocalizeStrFieldsAdd(string Name, bool ShowOnInfo)
{
    Check(MC.LocalizeStrFieldsAdd(SessionID, Name, ShowOnInfo));
}

Jeśli chcemy oznaczyć obiekt ikonką to należy jej obraz i parametry przesłać na serwer MapCenter funkcją LocalizeIconAdd:

public void LocalizeIconAdd(string FileName, int ID)
{
    System.IO.FileStream IconFile=new System.IO.FileStream(FileName, System.IO.FileMode.Open);
    byte[] IconData=new byte[IconFile.Length];

    IconFile.Read(IconData, 0, (int)IconFile.Length);

    TSoapTIconProperties IconProps=new TSoapTIconProperties();
    IconProps.IconID = ID;
    IconProps.IconFormat = sgaifRes.ImageFormats[0];
    IconProps.IsTransparent = true;
    IconProps.TransparentColor = 0xFFFFFF;

    Check(MC.LocalizeIconAdd(SessionID, IconProps, IconData));
}

Parametr TransparentColor określa kolor ikony, który będzie przezroczysty. W tym wypadku wybrano kolor biały. Format ikony został wybrany z listy formatów pobranych z serwera przy inicjalizacji klasy. Identyfikator ikony to dowolna liczba, użyjemy jej przy odwołaniu do ikony gdy dodamy obiekt lokalizacyjny.

Punkty zgeokodowane i przechowywane w liście są teraz przesyłane do serwera jako obiekty lokalizacyjne. Najpierw dodawany jest obiekt lokalizacyjny, a następnie dodawana jedna pozycja. Identyfikatory obiektów ustalane są jako kolejne liczby.

private void button2_Click(object sender, EventArgs e)
{
    MC.LocalizeNumFieldsAdd("Longitude", true);
    MC.LocalizeNumFieldsAdd("Latitude", true);
    MC.LocalizeStrFieldsAdd("Geocode", true);

    if (MessageBox.Show("Do you want all points in single path?", "Localize type", 
        MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
    {
        for (int I = 0; I < listView1.Items.Count; I++)
        {
            double Lon, Lat;
            if (listView1.Items[I].SubItems[2].Text != "---")
            {
                Lon = Convert.ToDouble(listView1.Items[I].SubItems[3].Text);
                Lat = Convert.ToDouble(listView1.Items[I].SubItems[4].Text);
                MC.LocalizeObjectAdd(I, listView1.Items[I].Text + "\r\n"
                    + listView1.Items[I].SubItems[1].Text + "\r\n"
                    + listView1.Items[I].SubItems[2].Text, false, -1);
                MC.LocalizeObjectPositionAdd(I, Lon, Lat, 0, 0, "");
                MidPoint = new TSoapTLongLatPoint();
                MidPoint.Longitude = Lon;
                MidPoint.Latitude = Lat;
            };
        }
    }
    else
    {
        MC.LocalizeIconAdd("c:\\projects\\Doc\\Images\\earth.png",0);
        MC.LocalizeObjectAdd(0, "Object", true, 0);
        for (int I = 0; I < listView1.Items.Count; I++)
        {
            double Lon, Lat;
            if (listView1.Items[I].SubItems[2].Text != "---")
            {
                Lon = Convert.ToDouble(listView1.Items[I].SubItems[3].Text);
                Lat = Convert.ToDouble(listView1.Items[I].SubItems[4].Text);
                MC.LocalizeObjectPositionAdd(0, Lon, Lat, Lon, Lat, listView1.Items[I].SubItems[5].Text);
                MidPoint = new TSoapTLongLatPoint();
                MidPoint.Longitude = Lon;
                MidPoint.Latitude = Lat;
            };
        }
    }
    Close();
}
Ponieważ punkty lokalizacyjne przechowywane są w sesji przy powtórnym otwarciu formy i próbie lokalizacji otrzymamy błąd DuplicatedLocalizeEntityID. W sesji będą już bowiem obiekty o identyfikatorach 0, 1, 2 itd. Aby uniknąć tego problemu należy je usunąć z sesji zapytaniem LocalizeObjectClear.

Do każdego punktu dołączamy pozycję pobraną z listy, należy tylko sprawdzić czy faktycznie został on zgeokodowany.

Efekt końcowy pokazuje rysunek:

Lokalizacja punktów

Jeśli użytkownik wybrał pokazanie zgeokodowanych punktów jako pojedynczą ścieżkę to wykonana zostanie druga część funkcji. Dodajemy w niej również wartości do pól numerycznych i tekstowych. Na początku funkcji lokalizacyjnej dodaliśmy dwa pola numeryczne i jedno tekstowe. Opisy tych pól i ich wartości pojawią się w liście zwracanej przez funkcje degeokodujące. Zlokalizowane punkty połączone linią ze strzałką pokazuje rysunek:

Lokalizacja punktów
Wygląd obu sygnatur na powyższym rysunku różni się. Wynika to z tego, że zarówno przy dodawaniu obiektu jak i przy dodawaniu pozycji przesyłane są parametry opisujące wygląd sygnatury: identyfikator ikony, kolor punktu, typ i rozmiar czcionki. Parametrów obiektu serwer używa do rysowania ostatniej z pozycji, zaś pozostałe pozycje rysowane są korzystając z parametrów przesłanych funkcją LocalizeObjectPositionAdd.

Dane dołączone do pozycji obiektów lokalizacyjnych będą zwracane przez funkcje degeokodujące serwera MapCenter. W tym celu po kliknięciu prawym przyciskiem na mapie znajdujemy współrzędne tego punktu i wywołujemy funkcję DegeocodeAtPoint:

public string DegeocodeAtPoint(double Lon, double Lat, double Alt)
{
    TSoapTLongLatPoint Mid = new TSoapTLongLatPoint();
    Mid.Longitude = Lon;
    Mid.Latitude = Lat;

    string Res = "";
    TSoapGetDegeocodeLayers__mcsResult sgdlRes;
    sgdlRes = MC.GetDegeocodeLayers(SessionID);

    TSoapDegeocodeAtPoint__mcsResult sdapRes;
    sdapRes = MC.DegeocodeAtPoint(SessionID, Mid, Alt, 0, 0,
        gpRes.Projections[6], "", Mid, 400, 10, sgdlRes.DegeocodeLayers,
        true, false);
    Res += "Degeocode result:\r\n";
    Res += String.Format(" Country: {0}\r\n", sdapRes.AreaName0);
    Res += String.Format(" Adm. level 2: {0}\r\n", sdapRes.AreaName1);
    Res += String.Format(" Adm. level 3: {0}\r\n", sdapRes.AreaName2);
    Res += String.Format(" Adm. level 4: {0}\r\n", sdapRes.AreaName3);
    Res += String.Format(" ZIP: {0}\r\n", sdapRes.Zip);
    foreach (TSoapTDegeocodeCityElementResult S in sdapRes.City)
        Res += String.Format(" City: {0}\r\n", S.Name);
    
    foreach (TSoapTDegeocodeRoadElementResult S in sdapRes.Road)
        Res += String.Format(" Road: {0} ({1,2:f}km)\r\n", S.Name, S.FoundLength / 1000);
    foreach (TSoapTDegeocodeRoadElementResult S in sdapRes.InternationalRoad)
        Res += String.Format(" International road: {0}\r\n", S.Name);
    foreach (TSoapTDegeocodeAtPointResult S in sdapRes.Natural)
        Res += String.Format(" Natural: {0} - {1}\r\n", S.Category, S.Name);
    foreach (TSoapTDegeocodeAtPointDatabaseResult S in sdapRes.DatabaseElements)
        Res += String.Format(" Database elements: {0} - {1}\r\n", S.Category, S.Name);
    foreach (TSoapTDegeocodeAtPointResult S in sdapRes.OtherMapElements)
        Res += String.Format(" Other map elements: {0} - {1}  ({2,2:f}km)\r\n",
            S.Category, S.Name, S.FoundLength / 1000);

    return Res;

}

Jak widać jest to ten sam kod, którego użyliśmy w pierwszym z programów przykładowych. Funkcja obsługująca kliknięcie na mapie w formie głównej odpowiada za przeliczenie współrzędnych ekranowych na mapowe i pokazanie okna z informacjami. Oto jej fragment:

if (e.Button == MouseButtons.Right)
{
    MC.ConvertScreenToMap(e.X, e.Y, 0, 0,
        Mid.Longitude, Mid.Latitude, Alt, pictureBox1.Width, pictureBox1.Height,
        ref lon1, ref lat1, ref lon2, ref lat2);
    string MsgText = "";
    MsgText = MC.DegeocodeAtPoint(lon1, lat1, Alt);
    MessageBox.Show(MsgText);
}

Ponieważ zażyczyliśmy sobie pokazania informacji o obiektach w promieniu 400 metrów od punktu środkowego na ekranie zobaczymy pola numeryczne i tekstowe z obu zlokalizowanych punktów:

Lokalizacja punktów

Moduł Route

Zgeokodowane w poprzedniej części punkty mogą zostać wykorzystane również jako punkty trasy. System MapCenter posiada rozbudowaną funkcjonalność trasowania i pozwala na wyliczanie tras najszybszych, najkrótszych, najtańszych, definiowanie zestawów parametrów dla pojazdów i kierowców, postojów, opłat itp.

Wyliczanie trasy należy rozpocząć od dodania punktów pośrednich.

public void RoutePlannerEntryAdd(double Lon, double Lat)
{
    TSoapRoutePlannerEntryAdd__mcsResult srpeaRes;
    TSoapTLongLatPoint Mid=new TSoapTLongLatPoint();
    Mid.Longitude = Lon;
    Mid.Latitude = Lat;
    srpeaRes=MC.RoutePlannerEntryAdd(SessionID,Mid,0); Check(srpeaRes.SoapResult);
}

Oprócz pozycji punktu można również wyspecyfikować czas postoju w danym miejscu (w przykładzie ustawiono wartość 0 czyli brak postoju). Po dodaniu wszystkich punktów trasy należy wysłać zapytanie RoutePlannerCalculateRoute:

public void RoutePlannerCalculateRoute(int RouteType)
{
    TSoapRoutePlannerCalculateRoute__mcsResult srpcrRes;
    srpcrRes=MC.RoutePlannerCalculateRoute(SessionID,RouteType,false,false,false,false,true);
}

Interesującymi nas parametrami są: typ trasy oraz flaga decydująca czy trzymać trasę w sesji. Trasa najkrótsza może wcale nie być najszybsza gdyż może prowadzić przez drogi niższej kategorii, na których nie da się rozwinąć wyższych prędkości zaś trasa najszybsza nie musi być najtańsza gdyż może zawierać przejazdy przez drogi płatne.

Wybieramy przechowywanie trasy w sesji. Po zamknięciu okna trasa pojawi się na mapie oraz zostanie otwarty raport trasy.

private void button3_Click(object sender, EventArgs e)
{
    MidPoint = null;
    MC.RoutePlannerDriverParamsSet();
    MC.RoutePlannerVehicleParamsSet();
    MC.RoutePlannerRoadParamsSet(4, true, 50, 10); // 4 - local road
    for (int I = 0; I < listView1.Items.Count; I++)
    {
        double Lon, Lat;
        if (listView1.Items[I].SubItems[2].Text != "---")
        {
            Lon = Convert.ToDouble(listView1.Items[I].SubItems[3].Text);
            Lat = Convert.ToDouble(listView1.Items[I].SubItems[4].Text);
            MC.RoutePlannerEntryAdd(Lon,Lat);
        };
    }
    MC.RoutePlannerCalculateRoute(0);
    RouteCalculated = true;
    Close();
}

Podobnie jak to było w przypadku lokalizacji dodajemy kolejne zgeokodowane punkty sprawdzając czy zawierają one poprawne współrzędne geograficzne, a następnie prosimy serwer o policzenie trasy. Główna forma odświeża widok mapy po zamknięciu okna i policzona trasa powinna się w nim pokazać.

Na początku procedury widać wywołanie funkcji ustawiających parametry kierowcy, pojazdu i drogi. Parametry te są przechowywane w sesji i obowiązują przy liczeniu trasy. Do ustawiania tych parametrów służą funkcje RoutePlannerDriverParamsSet, RoutePlannerVehicleParamsSet i RoutePlannerRoadParamsSet:

public void RoutePlannerDriverParamsSet()
{
    TSoapTDriverParams Params=new TSoapTDriverParams();
    Params.JourneyStartTime = 8 / 24;
    Params.DayWorkTime = 8 / 24;
    Params.ContinuousWorkTime = 5 / 24;
    Params.BreakTime = 0.5 / 24;
    Params.CostPerKilometer = 5;
    Params.CostPerHour = 10;
    Check(MC.RoutePlannerDriverParamsSet(SessionID, Params));
}

public void RoutePlannerVehicleParamsSet()
{
    TSoapTVehicleParams Params = new TSoapTVehicleParams();
    Params.VehicleType = 0;
    Params.IgnoreFuel = false;
    Params.FixedCost = 10;
    Params.CostPerKilometer = 1;
    Params.CostPerHour = 1;
    Params.TollRoadPerKilometer = 2;
    Params.TankCapacity = 40;
    Params.FuelCost = 4;
    Params.RefuelTime = 0.5 / 24;
    Params.VehicleWeight = 0;
    Params.VehicleLength = 0;
    Params.VehicleHeight = 0;
    Params.VehicleWidth = 0;
    Params.VehicleCapacity = 0;
    Params.VehicleLoadCapacity = 0;
    Params.ShippingTime = 2 / 24;
    Params.BorderPassTime = 5 / 24;
    Check(MC.RoutePlannerVehicleParamsSet(SessionID, Params));
}

public void RoutePlannerRoadParamsSet(int RoadType, bool UseRoad, double Speed, double Combustion)
{
    TSoapTRoadParams[] Params = new TSoapTRoadParams[1];
    Params[0] = new TSoapTRoadParams();
    Params[0].RoadType = rpgartRes.RoadTypes[RoadType];
    Params[0].Use = UseRoad;
    Params[0].Speed = Speed;
    Params[0].Combustion = Combustion;

    Check(MC.RoutePlannerRoadParamsSet(SessionID, Params));
}

Dwie pierwsze są dość oczywiste w działaniu, dokładny opis przesyłanych parametrów dostępny jest w dokumentacji do API systemu MapCenter. Wyjaśnienia wymaga RoutePlannerRoadParamsSet. System MapCenter dzieli drogi na klasy, dla każdej z nich oddzielnie można ustawić parametry prędkości i spalania. Wyłączenie jednej z klas spowoduje, że trasa nie będzie przebiegać tymi drogami. Może się jednak zdarzyć, że nie da się jej wtedy policzyć. Lista klas dróg jest ładowana w konstruktorze klasy MCServerCore.

Jeśli z jakichś powodów nie można dojechać do punktów pośrednich trasy zapytanie RoutePlannerCalculateRoute zwróci błąd RouteNotFound. Dodatkowo pole UnreachableEntry zwrócone w wyniku zawiera numer punktu, który spowodował, że trasa nie może zostać znaleziona.

Poniższy rysunek przedstawia przykładową trasę policzoną przez system MapCenter.

Wyliczona trasa

Oprócz zaznaczenia trasy na mapie możemy pobrać z serwera opis trasy. Trasa zostaje podzielona na odcinki odpowiadające ważniejszym wydarzeniom na trasie. Wydarzeniami tymi mogą być: skręty, postoje, dotarcie do kolejnego punktu trasy itp. Opis trasy pobieramy funkcją RoutePlannerGetRouteItinerary. W naszym przykładzie opis trasy ładujemy wprost do podanej jako parametr kontrolki ListView:

public void RoutePlannerGetRouteItinerary(ListView List)
{
    TSoapRoutePlannerGetRouteItinerary__mcsResult srpgriRes;
    srpgriRes = MC.RoutePlannerGetRouteItinerary(SessionID);

    foreach (TSoapTRouteItinerary R in srpgriRes.RouteItinerary)
    {
        // type name0 name1 lon lat distance time cost
        ListViewItem Item = new ListViewItem();
        Item.Text = string.Format("{0}",R.ItineraryType);
        Item.SubItems.Add(R.ItineraryName0);
        Item.SubItems.Add(R.ItineraryName1);
        Item.SubItems.Add(string.Format("{0,2:f}", R.EntryPosition.Longitude));
        Item.SubItems.Add(string.Format("{0,2:f}", R.EntryPosition.Latitude));
        Item.SubItems.Add(string.Format("{0,2:f}", R.EntryDistance/1000));
        Item.SubItems.Add(string.Format("{0,2:f}", R.EntryTime));
        Item.SubItems.Add(string.Format("{0,2:f}", R.EntryCost));

        List.Items.Add(Item);
    }
}

Dodatkowo pobieramy też informacje ogólne o trasie. Podsumowanie to jest zwracane przez funkcję RoutePlannerGetRouteSummary i zawiera między innymi: długość trasy i jej koszt.

public string RoutePlannerGetRouteSummary()
{
    TSoapRoutePlannerGetRouteSummary__mcsResult srpgrs;
    srpgrs = MC.RoutePlannerGetRouteSummary(SessionID, false);
    Check(srpgrs.SoapResult);
    string Res="";
    Res += string.Format("Total route length: {0,2:f}km\r\n", srpgrs.TotalRouteLength / 1000);
    Res += string.Format("Total route time: {0,2:f} day(s)\r\n", srpgrs.TotalRouteTime);
    Res += string.Format("Total route cost: {0,2:f}\r\n", srpgrs.TotalRouteCost);
    Res += string.Format("Total fuel cost: {0,2:f}\r\n", srpgrs.TotalFuelCost);

    return Res;
}

Podsumowanie i opis trasy są ładowane przy otwarciu okna raportu:

private void Form4_Shown(object sender, EventArgs e)
{
    textBox1.Text = MC.RoutePlannerGetRouteSummary();
    MC.RoutePlannerGetRouteItinerary(listView1);
}

Tak wygląda kompletny opis trasy:

Raport z trasy

Kolumna Type określa rodzaj zdarzenia. Lista tych wartości jest dostępna w dokumentacji do API systemu MapCenter.

Aby policzyć kolejną trasę łączącą inne punkty należy usunąć poprzednie, wywołując zapytanie RoutePlannerEntriesClear. Pojedyncze punkty usuwa funkcja RoutePlannerEntryRemove zaś liczbę punktów w trasie można pobrać funkcją RoutePlannerEntriesGetCount.

Renderowanie kafelkowe

System MapCenter pozwala również na podział mapy na statyczne fragmenty podlegające cache'owaniu. Jest to przydatne gdy obraz mapy chcemy podzielić na niezależnie ładowane kawałki. Ponieważ kawałki są niezmienne pomiędzy sesjami to mogą być cache'owane. Niestety z tego samego powodu nie mogą zawierać żadnych dynamicznych elementów: tras, obiektów lokalizacyjnych itp.

Do renderowania kafelków używana jest funkcja RenderTiledMap. Funkcja zawsze zwraca kawałki mapy o rozmiarze 256 na 256 pikseli. Nie używa ona parametru wysokości, zamiast tego używany jest parametr Zoom. MapCenter rozpoznaje dwadzieścia poziomów powiększenia. Zerowy odpowiada całej mapie i serwer zwróci jeden kafelek. W pierwszym poziomie powiększenia, mapa dzielona jest na 4 części i serwer zwraca cztery kafelki. W drugim poziomie mapa ma 16 kafelków. W trzecim mapa jest dzielona na 64 kawałki i tyle też jest zwracanych. W pozostałych poziomach mapa dzielona jest na coraz więcej części ale zawsze zwracanych jest 64 kafelków.

Aby przeliczyć współrzędne geograficzne na numer kafelka przy zadanym powiększeniu (i odwrotnie) należy użyć poniższych funkcji:

public void XY2LL(int xtile, int xpos, int ytile, int ypos, int zoom, ref double lon, ref double lat)
{
    double x1, y1;
    x1 = xtile * 256 + xpos;
    x1 = x1 * (2 * Math.PI) / (256 << zoom);
    lon = (x1 - Math.PI) * 180 / Math.PI;
    y1 = ytile * 256 + ypos;
    y1 = y1 * (2 * Math.PI) / (256 << zoom);
    y1 = (Math.PI - y1);
    if (y1 > 0)
        y1 = 2 * Math.Atan(Math.Exp(y1)) - Math.PI / 2;
    else
        y1 = -(2 * Math.Atan(Math.Exp(-y1)) - Math.PI / 2);
    lat = y1 * 180 / Math.PI;
}

public void LL2XY(double lon, double lat, int zoom, ref double x2, ref double x3, ref double y2, ref double y3)
{
    double lon1, lat1, x1, y1;
    lon1 = lon * (Math.PI / 180);
    x1 = ((lon1 + Math.PI) / (2 * Math.PI)) * (256 << zoom);
    x2 = Math.Floor(Math.Floor(x1) / 256);
    x3 = Math.Floor(Math.Floor(x1) % 256);

    lat1 = lat * (Math.PI / 180);
    y1 = Math.Tan((Math.PI / 4) + (lat1 / 2));
    if (y1 < 0)
    {
        y1 = Math.Log(-y1);
    }
    else
    {
        y1 = Math.Log(y1);
    }
    y1 = (Math.PI - y1) * (256 << zoom) / (2 * Math.PI);
    y2 = Math.Floor(Math.Floor(y1) / 256);
    y3 = Math.Floor(Math.Floor(y1) % 256);
}

Do przygotowania paremetrów wejściowych służy nowa forma. Zawiera ona suwak ustalający powiększenie oraz pole do wpisania nazwy folderu, do którego mają trafić wygenerowane kafelki:

Parametry kafelkowania

Wygenerowane kafelki zapiszemy w podanym folderze. Funkcja RenderTiledMap została zdefiniowana w klasie MCServerCore:

public void RenderTiledMap(int PosX, int PosY, int Zoom, ref byte[][] ImageData)
{
    TSoapTTiledMapParams RenderParams = new TSoapTTiledMapParams();
    RenderParams.Antialiasing = true;

    TSoapRenderTiledMap__mcsResult srtmRes;
    srtmRes = MC.RenderTiledMap(SessionID, gaifRes.ImageFormats[DefaultImageFormat],
        PosX, PosY, Zoom, RenderParams, gtmlRes.TiledMapLayers);
    Check(srtmRes.SoapResult);
    ImageData = srtmRes.BitmapImages;
}

Po przygotowaniu parametrów wywołujemy ją w formie głównej:

private void tiledMapToolStripMenuItem_Click(object sender, EventArgs e)
{
    Form5 AForm = new Form5();
    if (AForm.ShowDialog() == DialogResult.OK)
    {
        byte[][] ImageData=null;
        double x1=0, x2=0, y1=0, y2=0;

        MC.LL2XY(Mid.Longitude, Mid.Latitude, AForm.Zoom, ref x1, ref x2, ref y1, ref y2);
        MC.RenderTiledMap((int)x1, (int)y1, AForm.Zoom, ref ImageData);
        int i = 0;
        foreach (byte[] Img in ImageData)
        {
            System.IO.FileStream aFile;
            aFile = new System.IO.FileStream(AForm.FolderPath+"\\img"+i.ToString()+".png", System.IO.FileMode.Create);
            aFile.Write(Img,0,Img.Length);
            aFile.Close();
            i++;
        }
    }
}
Ponieważ funkcja RenderTiledMap nie pokazuje dynamicznych warstw do definiowania widocznych warstw służy oddzielna funkcja o nazwie GetTiledMapLayers.

Podsumowanie

System MapCenter jest bardzo wszechstronny i zawiera funkcje pozwalające implementować systemy o bardzo różnych zastosowaniach. Nie wszystkie możliwości systemu zostały tu zaprezentowane i aby poznać pełną listę funkcji użytkownik powinien zapoznać się z dokumentacją API. Są tam również opisane znaczenia parametrów nieużywanych w przykładach z podręcznika.

W przypadku problemów, których wyjaśnienia użytkownik nie znajdzie w dokumentacji prosimy o kontakt z serwisem pod adresem emailowym serwisMapCenter@emapa.pl