/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.esri.gpt.control.livedata.sos; import com.esri.gpt.framework.isodate.IsoDateFormat; import com.esri.gpt.framework.util.Val; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Calendar; import java.util.TimeZone; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import org.apache.batik.ext.awt.image.codec.png.PNGDecodeParam; import org.apache.batik.ext.awt.image.codec.png.PNGImageEncoder; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * Simple Sensor observation service bridge. Provides image stream based on * service URL, offering, and feature. */ public class SimpleSOSBridge extends HttpServlet { public static final int marginSize = 3; public static final int maxReadingsDensity = 4; private static final Color[] palette = { Color.RED, Color.GREEN, Color.BLUE, Color.ORANGE, Color.MAGENTA, Color.YELLOW, Color.PINK }; /** * Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods. * @param request servlet request * @param response servlet response * @throws ServletException if a servlet-specific error occurs * @throws IOException if an I/O error occurs */ protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { OutputStream out = response.getOutputStream(); try { SOSContext sosContext = new SOSContext(); sosContext.setWidth(Val.chkInt(request.getParameter("width"), 200)); sosContext.setHeight(Val.chkInt(request.getParameter("height"), 90)); sosContext.setUrl(request.getParameter("url")); sosContext.setMethod(Method.parse(request.getParameter("method"))); sosContext.setOfferingName(request.getParameter("offeringName")); sosContext.setResponseFormat(request.getParameter("responseFormat")); sosContext.setObservedProperty(request.getParameter("observedProperty")); sosContext.setFeatureOfInterest(request.getParameter("featureOfInterest")); sosContext.setBeginPosition(request.getParameter("beginPosition")); sosContext.setEndPosition(request.getParameter("endPosition")); response.setContentType("image/png"); createImage(sosContext, out); //SosImageProducer imageProducer = new SosImageProducer(width, height, url, method); //imageProducer.createImage(offeringName, observedProperty, featureOfInterest, beginPeriod, endPeriod, out); } catch (ParserConfigurationException ex) { throw new ServletException("Error processing request", ex); } catch (SAXException ex) { throw new ServletException("Error processing request", ex); } catch (XPathExpressionException ex) { throw new ServletException("Error processing request", ex); } finally { out.close(); } } /** * Handles the HTTP <code>GET</code> method. * @param request servlet request * @param response servlet response * @throws ServletException if a servlet-specific error occurs * @throws IOException if an I/O error occurs */ @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } /** * Handles the HTTP <code>POST</code> method. * @param request servlet request * @param response servlet response * @throws ServletException if a servlet-specific error occurs * @throws IOException if an I/O error occurs */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } /** * Returns a short description of the servlet. * @return a String containing servlet description */ @Override public String getServletInfo() { return "Simple SOS bridge"; } /** * Creates image. * @param sosContext service context * @param output output stream to create image * @throws IOException I/O exception * @throws ParserConfigurationException throws if unable to obtain XML parser * @throws SAXException if error parsing data * @throws XPathExpressionException if error invoking XPath expression */ private void createImage(SOSContext sosContext, final OutputStream output) throws IOException, ParserConfigurationException, SAXException, XPathExpressionException { ValueComponentsArray valArr = new ValueComponentsArray(); InputStream responseStream = null; try { responseStream = createInputStream(sosContext); ValueComponentsArray simpleValArr = readSimpleValues(sosContext, responseStream); valArr.addAll(simpleValArr); } finally { try { if (responseStream != null) { responseStream.close(); } } catch (Exception ef) { } } // // // submit measurements request to the server // String response = submitRequest(sosContext); // // turn response into document // Document doc = DomUtil.makeDomFromString(response, false); // // // create XPath accessories // XPathFactory xPathFactory = XPathFactory.newInstance(); // XPath xPath = xPathFactory.newXPath(); // // // read simple values // ValueComponentsArray simpleValArr = readSimpleValues(sosContext, xPath, doc); // // // read composite values // ValueComponentsArray compositeValArr = readCompositeValues(sosContext, xPath, doc); // // simpleValArr.addAll(compositeValArr); // simpleValArr.normalize(); valArr.normalize(); // create image BufferedImage image = new BufferedImage(sosContext.getWidth(), sosContext.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D gr = (Graphics2D) image.getGraphics(); // draw axes drawAxes(sosContext, gr); // draw chart int i = 0; for (String name : valArr.getNames()) { if (i>=palette.length) break; gr.setColor(palette[i]); ArrayList<Double> vls = valArr.select(name); drawChart(sosContext, gr, vls); i++; } // encode into PNG PNGDecodeParam decodeParam = new PNGDecodeParam(); decodeParam.setGenerateEncodeParam(true); PNGImageEncoder enc = new PNGImageEncoder(output, decodeParam.getEncodeParam()); enc.encode(image); gr.dispose(); } private ValueComponentsArray readSimpleValues(SOSContext sosContext, InputStream is) throws SAXException, IOException, ParserConfigurationException { ValueComponentsArray valArr = new ValueComponentsArray(); SOSParser parser = new SOSParser(valArr, sosContext); parser.parseDocument(new InputSource(is)); return valArr; } private ValueComponentsArray readSimpleValues(SOSContext sosContext, XPath xPath, Document doc) throws XPathExpressionException { ValueComponentsArray valArr = new ValueComponentsArray(); // get separator definitions String tokenSeparator = (String) xPath.evaluate("/ObservationCollection/member/Observation/featureOfInterest/FeatureCollection/featureMember/SamplingPoint[@id='" + sosContext.getFeatureOfInterest() + "']/../../../../result/DataArray/encoding/TextBlock/@tokenSeparator", doc, XPathConstants.STRING); String blockSeparator = (String) xPath.evaluate("/ObservationCollection/member/Observation/featureOfInterest/FeatureCollection/featureMember/SamplingPoint[@id='" + sosContext.getFeatureOfInterest() + "']/../../../../result/DataArray/encoding/TextBlock/@blockSeparator", doc, XPathConstants.STRING); // get values String values = (String) xPath.evaluate("/ObservationCollection/member/Observation/featureOfInterest/FeatureCollection/featureMember/SamplingPoint[@id='" + sosContext.getFeatureOfInterest() + "']/../../../../result/DataArray/values", doc, XPathConstants.STRING); // transform values String[] readings = values.split(blockSeparator); for (int i = Math.max(readings.length - ((sosContext.getWidth() - 2 * marginSize) / maxReadingsDensity), 0); i < readings.length; i++) { String tokens[] = readings[i].split(tokenSeparator); if (tokens.length == 3) { ValueComponents vc = new ValueComponents(); try { vc.put(sosContext.getObservedProperty(), Double.parseDouble(tokens[2])); } catch (NumberFormatException ex) { vc.put(sosContext.getObservedProperty(), Double.NaN); } valArr.add(vc); } } return valArr; } private Node searchForFirstQuantity(Node node) { Node n = node.getFirstChild(); while (n!=null) { if (n.getNodeName().endsWith("Quantity")) { return n; } Node q = searchForFirstQuantity(n); if (q!=null) { return q; } n = n.getNextSibling(); } return null; } private Node searchForEnclosingComposite(Node node) { if (node.getNodeName().endsWith("Composite")) { return node; } node = node.getParentNode(); if (node!=null) { return searchForEnclosingComposite(node); } return null; } private void searchForChildComposites(ArrayList<Node> childComposites, Node parentNode) { Node n = parentNode.getFirstChild(); while (n!=null) { String nodeName = n.getNodeName(); if (nodeName.endsWith("Composite")) { childComposites.add(n); } else searchForChildComposites(childComposites, n); n = n.getNextSibling(); } } private ValueComponentsArray readCompositeValues(SOSContext sosContext, XPath xPath, Document doc) { ValueComponentsArray values = new ValueComponentsArray(); try { ArrayList<Node> childComposites = new ArrayList<Node>(); Node fq = searchForFirstQuantity(doc); if (fq!=null) { fq = searchForEnclosingComposite(fq); if (fq!=null) { fq = fq.getParentNode(); if (fq!=null) { searchForChildComposites(childComposites, fq); } } } for (Node comp : childComposites) { ValueComponents vc = new ValueComponents(); Node quantity = searchForFirstQuantity(comp); while (quantity!=null) { if (quantity.getNodeName().endsWith("Quantity")) { String name = Val.chkStr(xPath.evaluate("@name", quantity)); String value = Val.chkStr(xPath.evaluate(".", quantity)); try { vc.put(name, Double.parseDouble(value)); } catch (NumberFormatException ex) { } } quantity = quantity.getNextSibling(); } values.add(vc); } } catch (XPathExpressionException ex) {} return values; } private void drawAxes(SOSContext sosContext, Graphics2D gr) { // set background to while gr.setColor(Color.WHITE); gr.fillRect(0, 0, sosContext.getWidth(), sosContext.getHeight()); // draw a simple grid gr.setColor(Color.LIGHT_GRAY); int xstep = (sosContext.getWidth() - 2 * marginSize) / 8; for (int i = xstep; i < sosContext.getWidth() - 2 * marginSize; i += xstep) { gr.drawLine(marginSize + i, sosContext.getHeight() - marginSize, marginSize + i, marginSize); } int ystep = (sosContext.getHeight() - 2 * marginSize) / 4; for (int i = ystep; i < sosContext.getHeight() - 2 * marginSize; i += ystep) { gr.drawLine(marginSize, sosContext.getHeight() - marginSize - i, sosContext.getWidth() - marginSize, sosContext.getHeight() - marginSize - i); } // draw axis gr.setColor(Color.BLACK); gr.drawLine(marginSize, sosContext.getHeight() - marginSize, marginSize, marginSize); gr.drawLine(marginSize, sosContext.getHeight() - marginSize, sosContext.getWidth() - marginSize, sosContext.getHeight() - marginSize); } /** * Draws chart. * @param sosContext SOS context * @param gr graphics * @param vals array of values */ private void drawChart(SOSContext sosContext, Graphics2D gr, ArrayList<Double> vals) { // find minimum and maximum of the value Double min = null; Double max = null; for (Double v : vals) { if (min == null || (v!=null && !v.isNaN() && v.doubleValue() < min.doubleValue())) { min = v; } if (max == null || (v!=null && !v.isNaN() && v.floatValue() > max.floatValue())) { max = v; } } // if minimum and maximum found... if (min != null && max != null) { Float delta = max.floatValue() - min.floatValue(); int clientWidth = sosContext.getWidth() - (2 * marginSize); int clientHeight = sosContext.getHeight() - (2 * marginSize); int lastX = marginSize; int lastY = marginSize; for (int i = 0; i < vals.size(); i++) { Double val = vals.get(i); if (val!=null) { int x = marginSize + (clientWidth * i) / vals.size(); int y = (int) (marginSize + ((float) clientHeight * (val.floatValue() - min.floatValue())) / delta); if (i > 0) { gr.drawLine(lastX, sosContext.getHeight() - lastY, x, sosContext.getHeight() - y); } lastX = x; lastY = y; } } } } private InputStream createInputStream(SOSContext sosContext) throws MalformedURLException, IOException { StringBuilder postData = new StringBuilder(); postData.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); postData.append("<sos:GetObservation xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.opengis.net/sos/1.0 http://schemas.opengis.net/sos/1.0.0/sosAll.xsd\" xmlns:sos=\"http://www.opengis.net/sos/1.0\" xmlns:om=\"http://www.opengis.net/om/1.0\" xmlns:gml=\"http://www.opengis.net/gml\" xmlns:ogc=\"http://www.opengis.net/ogc\" service=\"SOS\" version=\"1.0.0\">"); postData.append("<sos:offering>" + sosContext.getOfferingName() + "</sos:offering>"); String beginPosition = sosContext.getBeginPosition(); String endPosition = sosContext.getEndPosition(); if (beginPosition.length() > 0) { if (endPosition.length() == 0) { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); endPosition = (new IsoDateFormat()).format(cal.getTime()); } postData.append("<sos:eventTime>"); postData.append("<ogc:TM_During>"); postData.append("<ogc:PropertyName>om:samplingTime</ogc:PropertyName>"); postData.append("<gml:TimePeriod>"); postData.append("<gml:beginPosition>" + beginPosition + "</gml:beginPosition>"); postData.append("<gml:endPosition>" + endPosition + "</gml:endPosition>"); postData.append("</gml:TimePeriod>"); postData.append("</ogc:TM_During>"); postData.append("</sos:eventTime>"); } postData.append("<sos:observedProperty>" + sosContext.getObservedProperty() + "</sos:observedProperty>"); postData.append("<sos:responseFormat>" + sosContext.getResponseFormat() + "</sos:responseFormat>"); postData.append("<sos:resultModel>om:Observation</sos:resultModel>"); postData.append("<sos:responseMode>inline</sos:responseMode>"); postData.append("</sos:GetObservation>"); // open a connection to the targeted server URL URL = new URL(sosContext.getUrl()); HttpURLConnection httpCon = (HttpURLConnection) URL.openConnection(); httpCon.setDoInput(true); httpCon.setRequestMethod(sosContext.getMethod().name()); // set request properties httpCon.setRequestProperty("Content-Type", "text/xml"); httpCon.setRequestProperty("Content-Length", "" + postData.length()); // post data to the targeted server httpCon.setDoOutput(true); OutputStream postStream = null; try { postStream = httpCon.getOutputStream(); postStream.write(postData.toString().getBytes("UTF-8")); postStream.flush(); } finally { try { if (postStream != null) { postStream.close(); } } catch (Exception ef) { } } return httpCon.getInputStream(); } /** * Submits request to the remote SOS service. * @param sosContext service context * @return response from the server * @throws MalformedURLException if invalid URL * @throws IOException if reading/writing to the connection streams failed */ private String submitRequest(SOSContext sosContext) throws MalformedURLException, IOException { // read the response from the targeted server String responseData = ""; InputStream responseStream = null; try { responseStream = createInputStream(sosContext); responseData = readCharacters(responseStream, "UTF-8"); } finally { try { if (responseStream != null) { responseStream.close(); } } catch (Exception ef) { } } return responseData; } /** * Fully reads the characters from an input stream. * @param stream the input stream * @param charset the encoding of the input stream * @return the characters read * @throws IOException if an exception occurs */ private String readCharacters(InputStream stream, String charset) throws IOException { StringBuffer sb = new StringBuffer(); BufferedReader br = null; InputStreamReader ir = null; try { if ((charset == null) || (charset.trim().length() == 0)) { charset = "UTF-8"; } char cbuf[] = new char[2048]; int n = 0; int nLen = cbuf.length; ir = new InputStreamReader(stream, charset); br = new BufferedReader(ir); while ((n = br.read(cbuf, 0, nLen)) > 0) { sb.append(cbuf, 0, n); } } finally { try { if (br != null) { br.close(); } } catch (Exception ef) { } try { if (ir != null) { ir.close(); } } catch (Exception ef) { } } return sb.toString(); } /** * Method used to acces remote SOS service */ public static enum Method { GET, POST; /** * Parses value. * @param method method name * @return method */ public static Method parse(String method) { method = Val.chkStr(method).toUpperCase(); for (Method m : Method.values()) { if (m.name().equals(method)) { return m; } } return GET; } } }