/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2011, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.wms.map;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.StyleSheet;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.apache.sis.internal.system.OS;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.xml.MarshallerPool;
import org.geotoolkit.client.Request;
import org.geotoolkit.client.CapabilitiesException;
import org.geotoolkit.display.canvas.RenderingContext;
import org.geotoolkit.display.SearchArea;
import org.geotoolkit.display2d.canvas.RenderingContext2D;
import org.geotoolkit.display2d.primitive.SearchAreaJ2D;
import org.geotoolkit.gui.swing.resource.MessageBundle;
import org.geotoolkit.map.CoverageMapLayer;
import org.geotoolkit.display2d.primitive.ProjectedCoverage;
import org.geotoolkit.gui.swing.render2d.control.information.presenter.AbstractInformationPresenter;
import org.geotoolkit.ogc.xml.exception.ServiceExceptionReport;
import org.geotoolkit.ogc.xml.exception.ServiceExceptionType;
import org.geotoolkit.wms.WMSCoverageReference;
import org.geotoolkit.wms.WebMapClient;
import org.geotoolkit.wms.xml.AbstractWMSCapabilities;
import org.geotoolkit.wms.xml.WMSMarshallerPool;
import org.jdesktop.swingx.JXBusyLabel;
import org.jdesktop.swingx.JXHyperlink;
import org.jdesktop.swingx.combobox.ListComboBoxModel;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
/**
* Presenter for WMS layer, this will send a getFeatureInfo query to retrieve more information.
*
* @author Johann Sorel (Geomatys)
* @author Quentin Boileau (Geomatys)
* @module
*/
public class WMSPresenter extends AbstractInformationPresenter{
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.wms.map");
public WMSPresenter() {
super(100);
}
@Override
public JComponent createComponent(final Object graphic,
final RenderingContext2D context, final SearchAreaJ2D area) {
if (!(graphic instanceof ProjectedCoverage)) {
return null;
}
final ProjectedCoverage graCoverage = (ProjectedCoverage) graphic;
final CoverageMapLayer layer = graCoverage.getLayer();
if(!(layer.getCoverageReference() instanceof WMSCoverageReference)){
return null;
}
final WMSCoverageReference reference = (WMSCoverageReference) layer.getCoverageReference();
//get the different mime types
final List<String> mimeTypes = new ArrayList<String>();
final WebMapClient server = (WebMapClient)reference.getStore();
try {
final AbstractWMSCapabilities capa = server.getCapabilities();
mimeTypes.addAll(capa.getCapability().getRequest().getGetFeatureInfo().getFormats());
} catch (CapabilitiesException ex) {
LOGGER.log(Level.WARNING, ex.getMessage(),ex);
}
final JPanel guiTopPanel = new JPanel(new BorderLayout());
final JPanel guiCenterPanel = new JPanel(new BorderLayout());
guiCenterPanel.setPreferredSize(new Dimension(350, 300));
final JLabel guiMimeLabel = new JLabel(MessageBundle.format("mimeType") +" ");
final JComboBox guiMimeTypes = new JComboBox();
guiMimeTypes.setModel(new ListComboBoxModel(mimeTypes));
guiTopPanel.add(BorderLayout.WEST, guiMimeLabel);
guiTopPanel.add(BorderLayout.CENTER, guiMimeTypes);
guiMimeTypes.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
guiCenterPanel.removeAll();
try {
final String requestMT = guiMimeTypes.getSelectedItem().toString();
final Request request = getFeatureInfo(reference, context, area, requestMT, 20);
try {
final URI uri = request.getURL().toURI();
final JPanel urlPanel = new JPanel(new BorderLayout());
final Action action = new BrowserAction(uri);
final JXHyperlink link = new JXHyperlink(action);
urlPanel.add(BorderLayout.CENTER,link);
final JButton copyToClipboardBtn = new JButton("Copy URL");
copyToClipboardBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final Clipboard clpbrd = Toolkit.getDefaultToolkit().getSystemClipboard();
clpbrd.setContents(new StringSelection(uri.toString()), null);
}
});
urlPanel.add(BorderLayout.EAST, copyToClipboardBtn);
guiCenterPanel.add(BorderLayout.NORTH,urlPanel);
} catch(Exception ex) {
LOGGER.log(Level.WARNING, "Can't build request URL.");
}
new Thread(){
@Override
public void run() {
downloadGetFeatureInfo(guiCenterPanel, request, requestMT);
}
}.start();
} catch (TransformException ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
} catch (FactoryException ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
} catch (NoninvertibleTransformException ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
}
guiCenterPanel.revalidate();
guiCenterPanel.repaint();
}
});
final JPanel guiAll = new JPanel(new BorderLayout());
guiAll.add(BorderLayout.NORTH,guiTopPanel);
guiAll.add(BorderLayout.CENTER,guiCenterPanel);
return guiAll;
}
public Request getFeatureInfo(final WMSCoverageReference reference, final RenderingContext context, final SearchArea mask,
final String infoFormat, final int featureCount)
throws TransformException, FactoryException,
NoninvertibleTransformException{
final RenderingContext2D ctx2D = (RenderingContext2D) context;
final DirectPosition center = mask.getDisplayGeometry().getCentroid();
final Request url;
url = reference.queryFeatureInfo(
ctx2D.getCanvasObjectiveBounds(),
ctx2D.getCanvasDisplayBounds().getSize(),
(int) center.getOrdinate(0),
(int) center.getOrdinate(1),
reference.getLayerNames(),
infoFormat,featureCount);
return url;
}
private static void downloadGetFeatureInfo(final JPanel contentPane, final Request request, String requestMT){
final JXBusyLabel guiBuzy = new JXBusyLabel(new Dimension(30, 30));
guiBuzy.setBusy(true);
contentPane.add(BorderLayout.CENTER,guiBuzy);
contentPane.revalidate();
contentPane.repaint();
try {
final URL url = request.getURL();
final HttpURLConnection cnx = (HttpURLConnection)url.openConnection();
final String respContentType = cnx.getContentType();
InputStream in = cnx.getInputStream();
if (!in.markSupported()) {
in = new BufferedInputStream(in);
}
Component content = null;
Dimension dim = contentPane.getPreferredSize();
try {
//maybe an error occurs
try {
in.mark(4096);
Object result = unmarshallWMSResp(in);
if (result instanceof ServiceExceptionReport) {
final ServiceExceptionReport report = (ServiceExceptionReport) result;
final StringBuilder builder = new StringBuilder("An error occurred : \n");
for (ServiceExceptionType expType : report.getServiceExceptions()) {
builder.append(expType.getMessage());
}
content = renderError(builder.toString());
}
} catch (JAXBException ex) {
//can't unmarshall -> maybe not an actual error
try {
in.reset();
} catch (IOException ioe) {
//error when reset the InputStream -> recreate a new one
in = url.openConnection().getInputStream();
}
}
if (content == null) {
//try to guess response type
if (respContentType.startsWith("image")) {
content = renderImage(in, dim);
} else if (respContentType.equals("text/html")) {
content = renderHTML(in);
} else if (respContentType.equals("text/xml")) {
content = renderXML(in);
} else {
content = renderText(in, null);
}
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
content = renderError("Error when request the FeatureInfo : \n"+ex.getMessage());
}
//can't render response
if (content == null) {
content = renderError("Can't render GetFeatureInfo response for requested URL : "+url.toString());
}
contentPane.remove(guiBuzy);
contentPane.add(BorderLayout.CENTER, new JScrollPane(content));
contentPane.setSize(dim);
contentPane.revalidate();
contentPane.repaint();
} catch (IOException ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
}
}
/**
* Unmarshall WMS service response.
* @param stream InputStream
* @return Object like
* @throws JAXBException
* @throws IOException
*/
private static Object unmarshallWMSResp(InputStream stream) throws JAXBException, IOException {
Unmarshaller unMarshaller = null;
MarshallerPool selectedPool = WMSMarshallerPool.getInstance();
try {
unMarshaller = selectedPool.acquireUnmarshaller();
return unMarshaller.unmarshal(stream);
} finally {
if (selectedPool != null && unMarshaller != null) {
selectedPool.recycle(unMarshaller);
}
}
}
/**
* Render html in a JTextPane
* @param stream InputStream
* @throws IOException
*/
private static Component renderHTML(InputStream stream) throws IOException {
String html = getStringContent(stream);
String css = null;
final Pattern style = Pattern.compile("<style>([^><]+?)</style>", Pattern.MULTILINE);
final Matcher matcher = style.matcher(html);
if (matcher.find()) {
css = matcher.group(1);
html = html.substring(0, matcher.start(0)) + html.substring(matcher.end(0));
}
final StyleSheet styles = new StyleSheet();
if (css != null) {
styles.addRule(css);
}
final JTextPane textPane = new JTextPane();
textPane.setEditable(false);
textPane.setContentType("text/html");
textPane.setStyledDocument(new HTMLDocument(styles));
textPane.setText(html);
return textPane;
}
/**
* Render xml in a JTextPane
* @param stream InputStream
* @throws IOException
*/
private static Component renderXML(InputStream stream) throws IOException {
return renderText(stream, "text/xml");
}
/**
* Render text in a JTextPane
* @param stream InputStream
* @param contentType String
* @return JTextPane
* @throws IOException
*/
private static Component renderText(InputStream stream, final String contentType) throws IOException {
final String content = getStringContent(stream);
final JTextPane textPane = new JTextPane();
textPane.setEditable(false);
if (contentType != null) {
textPane.setContentType(contentType);
}
textPane.setText(content);
return textPane;
}
/**
* Get String from an InputStream
* @param stream
* @return
* @throws IOException
*/
private static String getStringContent(InputStream stream) throws IOException {
final BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
final StringBuilder out = new StringBuilder();
final String newLine = System.getProperty("line.separator");
String line;
while ((line = reader.readLine()) != null) {
out.append(line);
out.append(newLine);
}
return out.toString();
}
/**
* Render error message in a JTextPane
* @param error String
* @return JTextPane with error
*/
private static Component renderError(final String error){
final JTextPane errorPane = new JTextPane();
Font font = errorPane.getFont();
font = font.deriveFont(Font.BOLD);
errorPane.setFont(font);
errorPane.setForeground(Color.RED);
errorPane.setText(error);
return errorPane;
}
/**
* Try to render Image in Component from URLConnection.
* @param stream InputStream
* @return JLabel with Image inside or null.
* @throws IOException
*/
private static Component renderImage(final InputStream stream, Dimension dim) throws IOException {
BufferedImage img = ImageIO.read(stream);
if (img != null) {
dim.setSize(img.getWidth(), img.getHeight());
return new JLabel(new ImageIcon(img));
}
return null;
}
/**
* Custom action that open an URI in system browser
* using first Desktop API and try with command line
* if Desktop API failed.
*/
private class BrowserAction extends AbstractAction {
URI uri;
public BrowserAction(URI uri) {
super();
this.uri = uri;
putValue(Action.NAME, uri.toString());
}
@Override
public void actionPerformed(ActionEvent e) {
if ( !browseDesktop(uri)) {
if (!openCommandLine(uri.toString())) {
LOGGER.log(Level.WARNING, "Unable to open browser for uri : "+uri.toString());
}
}
}
private boolean browseDesktop(URI uri) {
try {
if (!Desktop.isDesktopSupported()) {
return false;
}
if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
return false;
}
Desktop.getDesktop().browse(uri);
return true;
} catch (Throwable t) {
return false;
}
}
private boolean openCommandLine(String what) {
OS os = OS.current();
if (os.equals(OS.LINUX)) {
if (runCommand("gnome-open", what)) return true;
if (runCommand("kde-open", what)) return true;
if (runCommand("xdg-open", what)) return true;
}
if (os.equals(OS.MAC_OS)) {
if (runCommand("open", what)) return true;
}
if (os.equals(OS.WINDOWS)) {
if (runCommand("explorer", what)) return true;
}
return false;
}
private boolean runCommand(String command, String uri) {
String[] parts = new String[] {command, uri};
try {
Process p = Runtime.getRuntime().exec(parts);
if (p == null) return false;
try {
int retval = p.exitValue();
if (retval == 0) {
return false;
} else {
return false;
}
} catch (IllegalThreadStateException itse) {
return true;
}
} catch (IOException e) {
return false;
}
}
}
}