dm Server w wersji GA Eclipse BIRT
Oct 03


Prawdopodobnie tego bloga nie czyta nikt, kto nigdy nie używał Google Maps (beta ;-) ). Google od dłuższego czasu udostępnia API do Google Maps. Jako, że aplikacja jest napisana w języku javascript, tak też jest w wypadku API. Sama aplikacja jest w trakcie permanentnego rozwoju, wraz z nim zmienia się też interfejs programistyczny.

Konfiguracja i narzędzia
Jak sama nazwa wskazuje ten blog skupia się na szeroko pojętej korporacyjnej Javie, więc pokażę kilka zastosowań API w aplikacjach w tym języku. Oczywiście można sobie wyobrazić aplikację, która będzie wywoływać metody z Google Maps API bezpośrednio, ale żeby zaoszczędzić sobie pracy można wykorzystać np. otwarte rozszerzenie Apache Wicket wicket-contrib-gmap2.

Biblioteka ta jest dostępna w ramach Wicket Stuff – zbioru bibliotek nie należących do głównego projektu Apache Wicket, lecz silnie z tym frameworkiem powiązanych. W związku z czysto społecznościowym modelem rozwoju, i zmiennością samego Google Maps API, rozszerzenie w tej chwili nie ma w tej chwili stabilnego wydania. Jedną z ujemnych stron takiego modelu rozwoju jest konieczność samodzielnego zbudowania projektu ze źródeł, lub skorzystania z migawek aktualnego repozytorium.

Posłużę się podobnym schematem projektu jak w poprzednim wpisie na temat Wicketa choć tym razem z wykorzystaniem migawki rozwojowej gałęzi 1.4. W porównaniu do wersji 1.3.x zmiany związane są z pełniejszym wsparciem dla cech języka wprowadzonych w wersji 5 Javy. Pliki jar wicketa jak i projektu wicket-contrib-gmap2 są zawarte w repozytorium migawek Wicket Stuff, więc odpada konieczność samodzielnego budowania tych zależności.

Kod żródłowy
Całość projektu do zbudowania z użyciem mavena jest dostępna w tej paczce.

Klucz
Google Maps API wymaga wygenerowania klucza dla URL pod którym aplikacja będzie widoczna. Klucz można wygenerować na tej stronie, wymagane jest konto Google i zaakceptowanie warunków użycia API. Klucz będzie działał dla każdego URL (katalogu, domeny) rozpoczynającego się od ciągu użytego do jego wygenerowania. Dodatkowe informacje są zawarte na stronie na której klucz jest generowany. Pewnym ograniczeniem jest konieczność publicznego udostępnienia aplikacji korzystającej z API, chyba że zdecydujemy się na płatną wersję serwisu – Google Maps API Premier.

Klucz API (definiowany w GmapApplication.java) w przykładowym projekcie wygenerowany został dla http://localhost więc nie będzie konieczna jego zmiana dla aplikacji działającej lokalnie. API używane lokalnie traktuje klucz bardziej liberalnie, np. obsługuje aplikację uruchomioną na innym niż zadeklarowany porcie.

Przyjrzyjmy się klasie GmapApplication.java wywoływanej przy starcie aplikacji. W polu googleMapsApiKey zawarty jest klucz używany w całej aplikacji.

LiniagetMarkupSettings().setStripWicketTags(true); służy do wycięcia z wygenerowanego na podstawie szablonów HTML tagów specyficznych dla wicketa. Dodanie jej jest konieczne, by aplikacja w trybie DEVELOPMENT działała w Firefox 3. W klasie tej jest też konfigurowana domyślna strona – MapPage.

Współrzędne
Google Maps jako że jest szczególną formą mapy, ma też pewne wady map w ogólności. Podstawową jest próba odwzorowania powierzchni geoidy, którą w przybliżeniu jest ziemia w postaci płaskiej powierzchni. Twórcy map rozwiązują ten problem stosując różnego typu odwzorowania – w przypadku Google Maps jest to uproszczone odwzorowanie Mercatora. Odwzorowanie to bazuje na przekształceniu powierzchni geoidy na walec styczny do równika, i rozwinięciu powierzchni walca. Google maps dodatkowo traktuje powierzchnię ziemi jako idealną sferę, co upraszcza obliczenia na tyle by mogły one być prowadzone w API w języku javascript. Takie podejście obniża dokładność, ale głównym zadaniem Google Maps jest prezentacja ładnych, a nie wiernych map :-).

Kodowanie geograficzne – geocoding
Współrzędne na mapie są określone przez długość i szerokość geograficzną.Na podstawie takich współrzędnych działa też mechanika Google Maps. Ale w życiu codziennym takimi współrzędnymi posługujemy się bardzo rzadko, za współrzędne służy nam adres. Możliwość dość dokładnego odnajdywania miejsc z pomocą adresu w formie w jakiej używamy go na co dzień jest jedną z przyczyn sukcesu Google Maps i podobnych usług. Google udostępnił programistom taką funkcjonalność, zarówno jako konfigurowalny serwis HTTP, jak i interfejs w ramach samego API.

Pierwsza opcja jest o tyle ciekawa że można jej użyć na serwerze ( z ograniczeniami wynikającymi z licencji – ta funkcjonalność także wymaga klucza) do reprezentacji danych powiązanych geograficznie gdy cała informacja na temat lokalizacji ogranicza się do danych adresowych. Niestety dane geograficzne na temat terytorium Polski są dość gruboziarniste, czasami działają z dokładnością do ulicy, dla mniejszych miejscowości są jeszcze mniej dokładne.

W opisywanym projekcie wykorzystałem geocoding po stronie serwera – Kod jest zawarty w klasie GeoLocationClient, wykorzystuje klasę z gmap2-wicket-contrib ( Geocoder ). Ta klasa pobiera informacje w najprostrzej fomie – długości i szerokości geograficznej dla danego adresu, choć serwis ma większe możliwości, pozwala na wyciągnięcie dodatkowych informacji na temat lokalizacji w postaci pliku w formacie KML opartym na XML. Ten format ma szersze zastosowanie, w samym Google MAPs, ale też np. w aplikacji Google Earth.

Działanie przykładowego projektu
Strona po załadowaniu prezentuje mapę z polem tekstowym do wpisywania adresu. Na samej mapie wyświetlają się pinezki generowane dynamicznie. Pinezki są ładowane w momencie przesunięcia mapy i zmiany skali. Po kliknięciu w pinezkę, można zobaczyć współrzędne dla których została przyczepiona.

Jedyną stroną prezentowanego projektu jest MapPage, zawiera mapę z wyszukiwarką bazującą na wspomnianym wyżej serwisie:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
    xmlns:wicket="http://wicket.sourceforge.net/">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>Wicket Examples - GMap Panel</title>
<style type="text/css">

v\: * {
	behavior: url(#default#VML);
}
</style>
<link rel="stylesheet" type="text/css" href="style.css" />
<link wicket:id="infoWindowCss" rel="Stylesheet" type="text/css" href="infoWindow.css" />
</head>

<body>

<div wicket:id="locationPanel"></div>
<center>
<table>
    <tr>
        <td>
        <div wicket:id="topPanel" style="width: 800px; height: 600px;">topPanel</div>
        </td>
    </tr>
</table>
</center>
</body>
</html>

Jak widać szablon strony jest bardzo prosty, bardziej interesujące rzeczy dzieją się w kodzie źródłowym i javascripcie.

package pl.j2ee.wicket.gmap2.mappage;

import org.apache.wicket.PageParameters;
....
public class MapPage extends WebPage {

    private static final String ADDRESS = "Polska, Krak\u00F3w, Bociana 22";
    private static final int MARKERS_ON_X = 4;
    private static final int MARKERS_ON_Y = 4;

    private GMap2 map;

    public MapPage(final PageParameters parameters) {
        add(new StyleSheetReference("infoWindowCss", InfoWindowPanel.class, "infoWindow.css"));
        map = new GMap2("topPanel", GmapApplication.get().getGoogleMapsApiKey());
        add(map);
        setMapBaseSettings(map);
        map.add(new ClickListener() {

            private static final long serialVersionUID = 1L;

            @Override
            protected void onClick(AjaxRequestTarget target, GLatLng latLng, GOverlay overlay) {
                if (latLng != null) {
                    addMarker(map, latLng);
                }
            }
        });
        map.add(new DragEndListener() {

            private static final long serialVersionUID = 1L;

            @Override
            protected void onDragEnd(AjaxRequestTarget target) {
                regenerateMarkers();
            }
        });

        map.add(new ZoomEndListener() {

            private static final long serialVersionUID = 1L;

            @Override
            protected void onZoomEnd(AjaxRequestTarget target, int oldLevel, int newLevel) {
                regenerateMarkers();
            }
        });

        map.add(new LoadListener() {

            private static final long serialVersionUID = 1L;

            @Override
            protected void onLoad(AjaxRequestTarget target) {
                regenerateMarkers();
            }
        });
        add(new LocationPanel("locationPanel", new CompoundPropertyModel
(new AddressBean()), map));
    }

    private void setMapBaseSettings(final GMap2 map) {
        map.setDoubleClickZoomEnabled(true);
        map.setMapType(GMapType.G_HYBRID_MAP);
        map.setScrollWheelZoomEnabled(true);
        map.setDraggingEnabled(true);
        map.setMapType(GMapType.G_NORMAL_MAP);
        map.addControl(GControl.GLargeMapControl);
        map.addControl(GControl.GMapTypeControl);
        map.addControl(GControl.GScaleControl);
        String address = getStartAddress();
        GLatLng latLng = getAddressLocation(address);
        if (latLng != null) {
            map.setCenter(latLng);
        }
    }

    private GLatLng getAddressLocation(String address) {
        GeoLocatorClient geoLocatorClient = GmapApplication.get().getGeoLocatorClient();
        GLatLng latLng = geoLocatorClient.getAddressLocation(address);
        return latLng;
    }

    private String getStartAddress() {
        String address = new ResourceModel("address", ADDRESS).getObject();
        return address;
    }

    public GMarker getNewMarker(GLatLng location) {
        GMarkerOptions options = new GMarkerOptions(getPositionDescription(location));
        options.autoPan(true);
        options.bouncy(true);
        options.clickable(true);
        final GMarker result = new GMarker(location, options);

        return result;
    }

    private String getPositionDescription(GLatLng location) {
        return " latitude: " + location.getLat() + " longitude" + location.getLng();
    }

    public void regenerateMarkers() {
        map.removeAllOverlays();

        GLatLngBounds bounds = map.getBounds();
        GLatLng sw = bounds.getSW();
        GLatLng ne = bounds.getNE();
        double latSize = ne.getLat() - sw.getLat();
        double lngSize = ne.getLng() - sw.getLng();
        double latDelta = latSize / (MARKERS_ON_Y - 1);
        double lngDelta = lngSize / (MARKERS_ON_X - 1);
        for (double x = sw.getLng(); x < = ne.getLng(); x += lngDelta) {
            for (double y = sw.getLat(); y <= ne.getLat(); y += latDelta) {
                GLatLng latLng = new GLatLng(y, x);
                addMarker(map, latLng);
            }

        }
    }

    private GMarker addMarker(final GMap2 map, final GLatLng latLng) {
        final GMarker result = getNewMarker(latLng);
        map.addOverlay(result);
        result.addListener(GEvent.click, new GEventHandler() {

            private static final long serialVersionUID = 1L;

            @Override
            public void onEvent(AjaxRequestTarget target) {
                GInfoWindow infoWindow = map.getInfoWindow();
                infoWindow.open(result, new InfoWindowPanel("markerPanel", new CompoundPropertyModel(latLng)));
            }
        });
        return result;
    }
}

setMapBaseSettings służy do inicjalizacji mapy, dodaje podstawowe kontrolki. W tej metodzie również ustawiana jest też lokalizacja w której mapa startuje, na podstawie adresu.

        GLatLng latLng = getAddressLocation(address);
        if (latLng != null) {
            map.setCenter(latLng);
        }

Metoda regenerateMarkers() tworzy zestaw pinezek i dodaje je do mapy. Pinezki wcześniej dodane na mapie są usuwane. Można sobie wyobrazić, że pinezki pobierane z bazy danych na podstawie aktualnej skali i fragmentu mapy który jest właśnie wyświetlany. Ta metoda jest wywoływana z metod wywoływanych w wyniku zapytań AJAX z przeglądarki DragEndListener,ZoomEndListener,LoadListener.
Te metody są wywoływane odpowiednio po przesunięciu, zmianie skali i załadowaniu mapy.

Do pinezek podpięte jest zdarzenie AJAXowe odpalone po kliknięciu:

result.addListener(GEvent.click, new GEventHandler() {
            private static final long serialVersionUID = 1L;
            @Override
            public void onEvent(AjaxRequestTarget target) {
                GInfoWindow infoWindow = map.getInfoWindow();
                infoWindow.open(result, new InfoWindowPanel("markerPanel", new CompoundPropertyModel(latLng)));
            }
        });

Okno informacyjne jest generowane przez komponent InfoWindowPanel, jest to najzwyklejszy w świecie komponent Wicketa :-).

Wnioski
Projekt gmap2-wicket-contrib może być z powodzeniem punktem startowym dla rozwoju aplikacji korzystających z Google Maps i dodatkowych źródeł danych, zbyt dużych żeby przechowywać je po stronie klienta. Jednak ten projekt korzysta tylko z małej części możliwości Google Maps API. Skorzystanie z pozostałych wymaga dopisania nowych komponentów bezpośrednio wywołujących metody z API, te istniejące mogą być traktowane jako wzorzec i źródło pomysłów.

Podziel się z innymi:
  • Wykop
  • Digg
  • del.icio.us
  • StumbleUpon
  • Slashdot

2 Responses to “Google Maps API i Java”

  1. woro Says:

    Great Job :}

  2. Ojciec Czas Says:

    Rewelacyjny wpis. Krótko, zwięźle i na temat. Aż człowiek ma ochotę porobić coś z tymi mapkami! Dzięki!

Leave a Reply

Security Code: