/*
* Copyright (C) 2011 Andrea Schweer
*
* This file is part of the Digital Parrot.
*
* The Digital Parrot is free software; you can redistribute it and/or modify
* it under the terms of the Eclipse Public License as published by the Eclipse
* Foundation or its Agreement Steward, either version 1.0 of the License, or
* (at your option) any later version.
*
* The Digital Parrot 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 Eclipse Public License for
* more details.
*
* You should have received a copy of the Eclipse Public License along with the
* Digital Parrot. If not, see http://www.eclipse.org/legal/epl-v10.html.
*
*/
package net.schweerelos.parrot.ui;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import net.schweerelos.parrot.model.CenteredThing;
import net.schweerelos.parrot.model.NoSuchNodeWrapperException;
import net.schweerelos.parrot.model.NodeWrapper;
import net.schweerelos.parrot.model.ParrotModel;
import net.schweerelos.parrot.util.LatLonBounds;
import net.schweerelos.parrot.util.LatLonBounds.InvalidStringException;
import com.webrenderer.swing.BrowserFactory;
import com.webrenderer.swing.IMozillaBrowserCanvas;
import com.webrenderer.swing.ProxySetting;
import com.webrenderer.swing.event.JavascriptEvent;
import com.webrenderer.swing.event.JavascriptListener;
import com.webrenderer.swing.event.NetworkAdapter;
import com.webrenderer.swing.event.NetworkEvent;
@SuppressWarnings("serial")
public class MapBrowser extends JPanel {
private List<MapBrowserListener> listeners;
private IMozillaBrowserCanvas browser;
private int zoomLevel;
private LatLonBounds mapBounds;
private ParrotModel model;
private LatLonBounds lastBoundsFromMap;
private Timer timer;
public MapBrowser(String licenseType, String licenseData) {
listeners = new ArrayList<MapBrowserListener>();
setLayout(new BorderLayout());
BrowserFactory.setLicenseData(licenseType, licenseData);
browser = BrowserFactory.spawnMozilla();
setupProxy();
browser.enableDefaultContextMenu(false);
browser.setJavascriptEnabled(true);
browser.addJavascriptListener(new JavascriptListener() {
@Override
public void onJavascriptStatusChange(JavascriptEvent e) {
if (!isShowing()) {
return;
}
String status = e.getJavascriptStatus();
if (status.startsWith("boundsChanged")) {
LatLonBounds newBounds;
try {
newBounds = LatLonBounds.fromString(status);
if (!newBounds.equals(mapBounds)) {
maybeBoundsChanged(newBounds);
}
} catch (InvalidStringException e1) {
e1.printStackTrace();
}
} else if (status.startsWith("zoomChanged")) {
try {
int newZoomLevel = Integer
.parseInt(status.split("=")[1]);
if (newZoomLevel != zoomLevel) {
zoomLevel = newZoomLevel;
fireZoomed(zoomLevel);
}
} catch (NumberFormatException nfe) {
nfe.printStackTrace();
}
} else if (status.startsWith("markerClicked")) {
String markerID = status.split("=")[1];
try {
NodeWrapper wrapper = model
.getNodeWrapperForString(markerID);
fireMarkerClicked(wrapper);
} catch (NoSuchNodeWrapperException e1) {
// ignore
}
}
}
@Override
public void onJavascriptMessage(JavascriptEvent e) {
// ignore
}
@Override
public void onJavascriptDialog(JavascriptEvent e) {
// ignore
}
});
browser.addNetworkListener(new NetworkAdapter() {
@Override
public void onDocumentComplete(NetworkEvent ne) {
if (mapBounds != null || lastBoundsFromMap != null) {
return;
}
// if there are no bounds yet, get them from the document
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
String javaScript = "map.getBounds();";
String boundsString = browser.executeScriptWithReturn(javaScript);
try {
mapBounds = LatLonBounds.fromString(boundsString);
} catch (InvalidStringException e) {
// ignore
e.printStackTrace();
}
}
});
}
});
String fileName = constructMapHTML();
browser.loadURL(fileName);
add(browser.getComponent(), BorderLayout.CENTER);
setPreferredSize(new Dimension(320, 300));
timer = new Timer(500, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
mapBounds = lastBoundsFromMap;
firePanned(mapBounds);
}
});
timer.setRepeats(false);
}
private void setupProxy() {
// http://java.sun.com/javase/6/docs/technotes/guides/net/properties.html
String proxyHost = System.getProperty("http.proxyHost");
if (proxyHost != null && !proxyHost.equals("")) {
// this is the default anyway when proxyHost is specified
int port = 80;
try {
String proxyPort = System.getProperty("http.proxyPort");
port = Integer.parseInt(proxyPort);
} catch (NumberFormatException nfe) {
// ignore
}
browser.setProxyProtocol(new ProxySetting(ProxySetting.PROTOCOL_HTTP,
proxyHost,
port));
// try to get auth info -- this is non-standard
String username = System.getProperty("http.proxyUser", "");
String password = System.getProperty("http.proxyPass", "");
if (!username.equals("") && !password.equals("")) {
browser.setProxyAuthentication(username, password);
}
browser.enableProxy();
}
}
private String constructMapHTML() {
InputStream templateFile = this.getClass().getResourceAsStream("/maps/gmap-template.html");
File outputFile = new File(System.getProperty("user.home") + File.separator + ".digital-parrot" + File.separator + "gmap.html");
String html = "";
Scanner scanner = null;
try {
scanner = new Scanner(templateFile);
html = scanner.useDelimiter("\\A").next();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
// center on Hamilton initially
html = html.replace("MAPCENTER",
"new google.maps.LatLng(-37.783333, 175.283333)");
// zoom level is an integer
html = html.replace("ZOOMLEVEL", "6");
try {
if (!outputFile.getParentFile().canWrite()) {
outputFile.getParentFile().mkdirs();
}
BufferedWriter output = new BufferedWriter(new FileWriter(
outputFile));
output.write(html.toString());
output.close();
} catch (IOException ex) {
Logger.getLogger(MapBrowser.class.getName()).log(Level.SEVERE,
null, ex);
}
return outputFile.getAbsolutePath();
}
public void focusMapOn(CenteredThing<NodeWrapper> thing) {
String location = String.format("new google.maps.LatLng(%f, %f)", thing
.getLat(), thing.getLon());
String javaScript = "map.panTo(" + location + ");";
browser.executeScript(javaScript);
javaScript = String.format("map.setZoom(%d);", thing.getPrecision()
.toZoomLevel());
browser.executeScript(javaScript);
}
public void setModel(ParrotModel model) {
this.model = model;
List<CenteredThing<NodeWrapper>> allLocatedThings = model
.getLocatedThings().getAll();
int i = 0;
for (CenteredThing<NodeWrapper> locatedThing : allLocatedThings) {
createMarker(i, locatedThing);
i++;
}
}
private void createMarker(int i, CenteredThing<NodeWrapper> locatedThing) {
String javaScript = String.format(
"var marker%d = new google.maps.Marker({ "
+ "position: new google.maps.LatLng(%f, %f), "
+ "map: map, " + "title: '%s', "
+ "clickable: true, " + "draggable: false" + "});",
i, locatedThing.getLat(), locatedThing.getLon(),
locatedThing.getLabel());
browser.executeScript(javaScript);
javaScript = String.format(
"google.maps.event.addListener(marker%d, 'click', function() {"
+ " window.status = 'markerClicked, marker=%s';"
+ " });", i, locatedThing.getValue()
.getOntResource().getURI());
browser.executeScript(javaScript);
}
private synchronized void maybeBoundsChanged(LatLonBounds newBounds) {
lastBoundsFromMap = newBounds;
if (timer.isRunning()) {
timer.restart();
} else {
timer.start();
}
}
/* listener support start */
public synchronized void addMapBrowserListener(
MapBrowserListener mapBrowserListener) {
listeners.add(mapBrowserListener);
}
public synchronized void removeMapBrowserListener(
MapBrowserListener mapBrowserListener) {
listeners.remove(mapBrowserListener);
}
private void firePanned(LatLonBounds newBounds) {
List<MapBrowserListener> listeners;
synchronized (this) {
listeners = Collections.synchronizedList(this.listeners);
}
synchronized (listeners) {
for (MapBrowserListener listener : listeners) {
try {
listener.panned(newBounds);
} catch (RuntimeException re) {
re.printStackTrace();
this.listeners.remove(listener);
}
}
}
}
private void fireMarkerClicked(NodeWrapper wrapper) {
List<MapBrowserListener> listeners;
synchronized (this) {
listeners = Collections.synchronizedList(this.listeners);
}
synchronized (listeners) {
for (MapBrowserListener listener : listeners) {
try {
listener.markerClicked(wrapper);
} catch (RuntimeException re) {
re.printStackTrace();
this.listeners.remove(listener);
}
}
}
}
private void fireZoomed(int zoom) {
List<MapBrowserListener> listeners;
synchronized (this) {
listeners = Collections.synchronizedList(this.listeners);
}
synchronized (listeners) {
for (MapBrowserListener listener : listeners) {
try {
listener.zoomed(zoom);
} catch (RuntimeException re) {
re.printStackTrace();
this.listeners.remove(listener);
}
}
}
}
/* end listener support */
public LatLonBounds getMapBounds() {
return mapBounds;
}
}