/*
This file is part of RouteConverter.
RouteConverter is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
RouteConverter is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with RouteConverter; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Copyright (C) 2007 Christian Pesch. All Rights Reserved.
*/
package slash.navigation.mapview.browser;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.web.PopupFeatures;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.util.Callback;
import slash.navigation.common.NavigationPosition;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Logger;
import static java.lang.Boolean.parseBoolean;
import static java.lang.System.currentTimeMillis;
import static javafx.application.Platform.isFxApplicationThread;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
import static javafx.concurrent.Worker.State;
import static javafx.concurrent.Worker.State.SUCCEEDED;
import static slash.common.io.Transfer.parseDouble;
import static slash.navigation.rest.HttpRequest.USER_AGENT;
/**
* Implementation for a component that displays the positions of a position list on a map
* using the JavaFX WebView.
*
* @author Christian Pesch
*/
public class JavaFX7WebViewMapView extends BrowserMapView {
private static final Logger log = Logger.getLogger(JavaFX7WebViewMapView.class.getName());
private JFXPanel panel;
private WebView webView;
static {
setImplicitExit(false);
}
public Component getComponent() {
return panel;
}
protected JFXPanel getPanel() {
return panel;
}
protected WebView getWebView() {
return webView;
}
// initialization
protected WebView createWebView() {
try {
final WebView webView = new WebView();
double browserScaleFactor = getBrowserScaleFactor();
if (browserScaleFactor != 1.0) {
// allow to compile code with Java 7; with Java 8 this would simply be
// webView.setZoom(browserScaleFactor);
try {
Method method = WebView.class.getDeclaredMethod("setZoom", double.class);
method.invoke(webView, browserScaleFactor);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
// intentionally do nothing
}
}
Group group = new Group();
group.getChildren().add(webView);
panel.setScene(new Scene(group));
panel.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
setWebViewSizeToPanelSize();
}
});
webView.getEngine().setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() {
public WebEngine call(PopupFeatures config) {
// grab the last hyperlink that has :hover pseudoclass
String url = executeScriptWithResult("extractPopupHrefs()");
if (url != null && isUrl(url)) {
mapViewCallback.startBrowser(url);
} else {
log.warning("No result from popup uri detector");
}
// prevent from opening in WebView
return null;
}
private boolean isUrl(String url) {
try {
new URL(url);
return true;
} catch (MalformedURLException e) {
return false;
}
}
});
webView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
private int startCount = 0;
public void changed(ObservableValue<? extends State> observableValue, State oldState, State newState) {
log.info("WebView changed observableValue " + observableValue + " oldState " + oldState + " newState " + newState + " thread " + Thread.currentThread());
if (newState == SUCCEEDED) {
// get out of the listener callback
new Thread(new Runnable() {
public void run() {
tryToInitialize(startCount++, currentTimeMillis());
}
}, "MapViewInitializer").start();
}
}
});
// allow to compile code with Java 7; with Java 8 this would simply be
// webView.getEngine().setUserAgent(USER_AGENT);
try {
Method method = WebEngine.class.getDeclaredMethod("setUserAgent", String.class);
method.invoke(webView.getEngine(), USER_AGENT);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
// intentionally do nothing
}
return webView;
} catch (Throwable t) {
log.severe("Cannot create WebView: " + t);
setInitializationCause(t);
return null;
}
}
/*
protected void runBrowserInteractionCallbacksAndTests(long start) {
executeScript("if (!document.getElementById('FirebugLite')){E = document['createElement' + 'NS'] && document.documentElement.namespaceURI;E = E ? document['createElement' + 'NS'](E, 'script') : document['createElement']('script');E['setAttribute']('id', 'FirebugLite');E['setAttribute']('src', 'https://getfirebug.com/' + 'firebug-lite.js' + '#startOpened');E['setAttribute']('FirebugLite', '4');(document['getElementsByTagName']('head')[0] || document['getElementsByTagName']('body')[0]).appendChild(E);E = new Image;E['setAttribute']('src', 'https://getfirebug.com/' + '#startOpened');}");
super.runBrowserInteractionCallbacksAndTests(start);
}
*/
private boolean loadWebPage() {
try {
String url = prepareWebPage();
webView.getEngine().load(url);
return true;
} catch (Throwable t) {
log.severe("Cannot load web page: " + t);
setInitializationCause(t);
return false;
}
}
protected void initializeBrowser() {
panel = new JFXPanel();
runLater(new Runnable() {
public void run() {
webView = createWebView();
if (webView == null)
return;
log.info("Using JavaFX WebView to create map view: " + JavaFX7WebViewMapView.this.getClass().getName());
initializeWebPage();
}
});
}
protected void initializeWebPage() {
log.info("Loading Google Maps API from " + getGoogleMapsServerApiUrl());
runLater(new Runnable() {
public void run() {
if (!loadWebPage())
dispose();
}
});
}
protected boolean isMapInitialized() {
String result = executeScriptWithResult("isInitialized();");
return parseBoolean(result);
}
public void resize() {
super.resize();
setWebViewSizeToPanelSize();
}
private void setWebViewSizeToPanelSize() {
Dimension size = panel.getSize();
if (webView != null) {
webView.setMinSize(size.getWidth(), size.getHeight());
webView.setMaxSize(size.getWidth(), size.getHeight());
}
}
// bounds and center
protected NavigationPosition getNorthEastBounds() {
return parsePosition("getNorthEastBounds();");
}
protected NavigationPosition getSouthWestBounds() {
return parsePosition("getSouthWestBounds();");
}
protected NavigationPosition getCurrentMapCenter() {
return parsePosition("getCenter();");
}
protected Integer getCurrentZoom() {
try {
Double zoom = parseDouble(executeScriptWithResult("getZoom();"));
if (zoom != null)
return zoom.intValue();
} catch (NumberFormatException e) {
// intentionally left empty
}
return null;
}
protected String getCallbacks() {
return executeScriptWithResult("getCallbacks();");
}
// print
public boolean isSupportsPrinting() {
return false;
}
public boolean isSupportsPrintingWithDirections() {
return false;
}
public void print(final String title, boolean withDirections) {
throw new UnsupportedOperationException("Printing not supported");
}
// script execution
protected void executeScript(final String script) {
if (webView == null || script.length() == 0)
return;
boolean debug = preferences.getBoolean(DEBUG_PREFERENCE, false);
if (debug)
log.info("Before executeScript " + script);
if (!isFxApplicationThread()) {
runLater(new Runnable() {
public void run() {
try {
webView.getEngine().executeScript(script);
} catch (Throwable t) {
log.info("Exception during runLater executeScript of " + script + ": " + t);
}
logJavaScript(script, null);
}
});
} else {
try {
webView.getEngine().executeScript(script);
} catch (Throwable t) {
log.info("Exception during executeScript of " + script + ": " + t);
}
logJavaScript(script, null);
}
}
private static final Object LOCK = new Object();
protected synchronized String executeScriptWithResult(final String script) {
if (script.length() == 0)
return null;
final boolean debug = preferences.getBoolean(DEBUG_PREFERENCE, false);
final boolean pollingCallback = !script.contains("getCallbacks");
final Object[] result = new Object[1];
if (!isFxApplicationThread()) {
final boolean[] haveResult = new boolean[]{false};
runLater(new Runnable() {
public void run() {
Object r = null;
try {
r = webView.getEngine().executeScript(script);
}
catch (Throwable t) {
log.info("Exception during runLater executeScript with result of " + script + ": " + t);
}
if (debug && pollingCallback) {
log.info("After runLater, executeScript with result " + r);
}
synchronized (LOCK) {
result[0] = r;
haveResult[0] = true;
LOCK.notifyAll();
}
}
});
synchronized (LOCK) {
while (!haveResult[0]) {
try {
LOCK.wait();
} catch (InterruptedException e) {
// intentionally left empty
}
}
}
} else {
try {
result[0] = webView.getEngine().executeScript(script);
}
catch (Throwable t) {
log.info("Exception during executeScript with result of " + script + ": " + t);
}
if (debug && pollingCallback) {
log.info("After executeScript with result " + result[0]);
}
}
if (pollingCallback) {
logJavaScript(script, result[0]);
}
return result[0] != null ? result[0].toString() : null;
}
}