Использование Google Map в приложении на JavaFX

в 17:42, , рубрики: Google API, Google Maps, java, javafx, Maps API, метки: , , ,

Использование Google Map в приложении на JavaFX
Хочу рассказать о своем опыте использования Google Maps в приложении на JavaFX. Рассмотрим загрузку карты в приложение и вызов Google Maps JavaScript API v3 для загруженной карты из своего кода на Java.

Реализация

Сначала напишем код инициализации карты на javascript, который оформим ввиде html-страницы map.html. Далее по мере продвижения будем добавлять дополнительный код.

map.html
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <style type="text/css">
        html { height: 100% }
        body { height: 100%; margin: 0; padding: 0 }
        #map_canvas { height: 100% }
    </style>
    <script type="text/javascript"
            src="https://maps.googleapis.com/maps/api/js?key=***&sensor=false">
    </script>
    <script type="text/javascript">
        var map;
        var marker;

        function initialize() {
            var defLatLng = new google.maps.LatLng(59.95632093391832, 30.309906005859375);
            var mapOptions = {
                center: defLatLng,
                zoom: 3,
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                disableDefaultUI: true,
                panControl: false
            };
            map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);

            marker = new google.maps.Marker({
                position: defLatLng,
                map: map,
                icon: "img/Pin.png"
            });
        }
    </script>
</head>
<body onload="initialize()">
<div id="map_canvas" style="width:100%; height:100%"></div>
</body>
</html>

Теперь у нас есть html-страница с картой. Ее можно открыть в любом браузере. В JavaFX есть замечательный класс для отображения веб-контента: javafx.scene.web.WebView. Воспользуемся им для отображения написанной html-странички. Напишем класс GoogleMap, который будет представлять Google Map в нашем приложении на JavaFX. Чтобы работать с картой как с любым другим JavaFX-нодом (Scene graph node), унаследуемся от класса javafx.scene.Parent.

GoogleMap.java

public class GoogleMap extends Parent {

    public GoogleMap() {
        initMap();
        getChildren().add(webView);
    }

    private void initMap()
    {
        webView = new WebView();
        webEngine = webView.getEngine();
        webEngine.load(getClass().getResource("map.html").toExternalForm());
        ready = false;
        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>()
        {
            @Override
            public void changed(final ObservableValue<? extends Worker.State> observableValue,
                                final Worker.State oldState,
                                final Worker.State newState)
            {
                if (newState == Worker.State.SUCCEEDED)
                {
                    ready = true;
                }
            }
        });
    }

    private WebView webView;
    private WebEngine webEngine;
    private boolean ready;
}

Простого отображения карты не достаточно. Нам хочется управлять картой и отлавливать события карты. Для обращения к функциям на javascript нам потребуется экземпляр класса netscape.javascript.JSObject, который получим в методе initCommunication(), вызываемом в конструкторе класса GoogleMap. Так же в этом методе закинем в контекст javascript кода GoogleMap.this (экземпляр класса GoogleMap), чтобы иметь возможность вызывать методы класса GoogleMap из кода на javascript.

initCommunication()

    private void initCommunication() {
        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>()
        {
            @Override
            public void changed(final ObservableValue<? extends Worker.State> observableValue,
                                final Worker.State oldState,
                                final Worker.State newState)
            {
                if (newState == Worker.State.SUCCEEDED)
                {
                    doc = (JSObject) webEngine.executeScript("window");
                    doc.setMember("app", GoogleMap.this);
                }
            }
        });
    } 

    private JSObject doc;

Продемонстрируем вызов Java кода из javascript кода на примере отлавливания события клика по карте. Это событие, как ни странно, возникает при нажатии на карту и содержит географические координаты нажатия. Сначала напишем класс события:

MapEvent.java
public class MapEvent extends Event {

    public MapEvent(GoogleMap map, double lat, double lng) {
        super(map, Event.NULL_SOURCE_TARGET, Event.ANY);
        this.lat = lat;
        this.lng = lng;
    }

    public double getLat() {
        return this.lat;
    }

    public double getLng() {
        return this.lng;
    }

    private double lat;
    private double lng;
}

Теперь напишем методы класса GoogleMap, ответственные за регистрацию обработчика, прием события от javasript кода и вызов обработчика:

методы класса GoogleMap

    public void setOnMapLatLngChanged(EventHandler<MapEvent> eventHandler) {
        onMapLatLngChanged = eventHandler;
    }

    public void handle(double lat, double lng) {
        if(onMapLatLngChanged != null) {
            MapEvent event = new MapEvent(this, lat, lng);
            onMapLatLngChanged.handle(event);
        }
    }
    private EventHandler<MapEvent> onMapLatLngChanged;

Осталось только написать обработчик события карты в javascript коде и вызвать в нем метод handle(double lat, double lng) класса GoogleMap:

get_click_position(event)

function get_click_position(event){
            var location = event.latLng;
            var lat = location.lat();
            var lng = location.lng();
            app.handle(lat, lng);
 }

Теперь напишем метод класса GoogleMap для вызова функций javascript из java кода:

invokeJS(final String str)
    private void invokeJS(final String str) {
        if(ready) {
            doc.eval(str);
        }
        else {
            webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>()
            {
                @Override
                public void changed(final ObservableValue<? extends Worker.State> observableValue,
                                    final Worker.State oldState,
                                    final Worker.State newState)
                {
                    if (newState == Worker.State.SUCCEEDED)
                    {
                        doc.eval(str);
                    }
                }
            });
        }
    }

Результат

Не буду отдельно пояснять методы установки типа карты, положения центра карты, положения курсора. Все эти методы есть в полных исходниках:

GoogleMap.java

public class GoogleMap extends Parent {

    public GoogleMap() {
        initMap();
        initCommunication();
        getChildren().add(webView);
        setMarkerPosition(0,0);
        setMapCenter(0, 0);
        switchTerrain();
}

    private void initMap()
    {
        webView = new WebView();
        webEngine = webView.getEngine();
        webEngine.load(getClass().getResource("resources/map.html").toExternalForm());
        ready = false;
        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>()
        {
            @Override
            public void changed(final ObservableValue<? extends Worker.State> observableValue,
                                final Worker.State oldState,
                                final Worker.State newState)
            {
                if (newState == Worker.State.SUCCEEDED)
                {
                    ready = true;
                }
            }
        });
    }

    private void initCommunication() {
        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>()
        {
            @Override
            public void changed(final ObservableValue<? extends Worker.State> observableValue,
                                final Worker.State oldState,
                                final Worker.State newState)
            {
                if (newState == Worker.State.SUCCEEDED)
                {
                    doc = (JSObject) webEngine.executeScript("window");
                    doc.setMember("app", GoogleMap.this);
                }
            }
        });
    }

    private void invokeJS(final String str) {
        if(ready) {
            doc.eval(str);
        }
        else {
            webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>()
            {
                @Override
                public void changed(final ObservableValue<? extends Worker.State> observableValue,
                                    final Worker.State oldState,
                                    final Worker.State newState)
                {
                    if (newState == Worker.State.SUCCEEDED)
                    {
                        doc.eval(str);
                    }
                }
            });
        }
    }

    public void setOnMapLatLngChanged(EventHandler<MapEvent> eventHandler) {
        onMapLatLngChanged = eventHandler;
    }

    public void handle(double lat, double lng) {
        if(onMapLatLngChanged != null) {
            MapEvent event = new MapEvent(this, lat, lng);
            onMapLatLngChanged.handle(event);
        }
    }

    public void setMarkerPosition(double lat, double lng) {
        String sLat = Double.toString(lat);
        String sLng = Double.toString(lng);
        invokeJS("setMarkerPosition(" + sLat + ", " + sLng + ")");
    }

    public void setMapCenter(double lat, double lng) {
        String sLat = Double.toString(lat);
        String sLng = Double.toString(lng);
        invokeJS("setMapCenter(" + sLat + ", " + sLng + ")");
    }

    public void switchSatellite() {
        invokeJS("switchSatellite()");
    }

    public void switchRoadmap() {
        invokeJS("switchRoadmap()");
    }

    public void switchHybrid() {
        invokeJS("switchHybrid()");
    }

    public void switchTerrain() {
        invokeJS("switchTerrain()");
    }

    public void startJumping() {
        invokeJS("startJumping()");
    }

    public void stopJumping() {
        invokeJS("stopJumping()");
    }

    public void setHeight(double h) {
        webView.setPrefHeight(h);
    }

    public void setWidth(double w) {
        webView.setPrefWidth(w);
    }

    public ReadOnlyDoubleProperty widthProperty() {
        return webView.widthProperty();
    }

    private JSObject doc;
    private EventHandler<MapEvent> onMapLatLngChanged;
    private WebView webView;
    private WebEngine webEngine;
    private boolean ready;
}

map.html

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <style type="text/css">
        html { height: 100% }
        body { height: 100%; margin: 0; padding: 0 }
        #map_canvas { height: 100% }
    </style>
    <script type="text/javascript"
            src="https://maps.googleapis.com/maps/api/js?key=***&sensor=false">
    </script>
    <script type="text/javascript">
        var map;
        var marker;

        function get_click_position(event){
            var location = event.latLng;
            var lat = location.lat();
            var lng = location.lng();
            setMarkerPosition(lat, lng);
            app.handle(lat, lng);
        }

        function setMarkerPosition(lat, lng) {
            var clickLatLng = new google.maps.LatLng(lat, lng);
            marker.setPosition(clickLatLng);
        }

        function startJumping(){
            marker.setAnimation(google.maps.Animation.BOUNCE);
        }

        function stopJumping(){
            marker.setAnimation(google.maps.Animation.BOUNCE);
        }

        function setMapCenter(lat, lng) {
            var latlng = new google.maps.LatLng(lat, lng);
            map.setCenter(latlng);
        }

        function switchSatellite() {
            var mapOptions = {
                mapTypeId: google.maps.MapTypeId.SATELLITE
            };
            map.setOptions(mapOptions);
            setLightMarkerIcon();
        }

        function switchRoadmap() {
            var mapOptions = {
                mapTypeId: google.maps.MapTypeId.ROADMAP
            };
            map.setOptions(mapOptions);
            setDarkMarkerIcon();
        }

        function switchHybrid() {
            var mapOptions = {
                mapTypeId: google.maps.MapTypeId.HYBRID
            };
            map.setOptions(mapOptions);
            setLightMarkerIcon();
        }

        function switchTerrain() {
            var mapOptions = {
                mapTypeId: google.maps.MapTypeId.TERRAIN
            };
            map.setOptions(mapOptions);
            setDarkMarkerIcon();
        }

        function initialize() {
            var defLatLng = new google.maps.LatLng(59.95632093391832, 30.309906005859375);
            var mapOptions = {
                center: defLatLng,
                zoom: 3,
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                disableDefaultUI: true,
                panControl: false
            };
            map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);
            google.maps.event.addListener(map, 'click', get_click_position);

            marker = new google.maps.Marker({
                position: defLatLng,
                map: map,
                icon: "img/Pin.png"
            });

            app.handle(0, 0);
        }

        function setDarkMarkerIcon() {
            marker.setIcon("img/Pin.png");
        }

        function setLightMarkerIcon() {
            marker.setIcon("img/Pin_s.png");
        }

    </script>
</head>
<body onload="initialize()">
<div id="map_canvas" style="width:100%; height:100%"></div>
</body>
</html>

Как сказал Вольтер:
«Я могу быть не согласным с Вашим мнением, но я готов отдать жизнь за Ваше право высказывать его.»

Автор: tegoo

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js