summaryrefslogtreecommitdiffstats
path: root/petascope/src/petascope/wcs2/server/ops/GetCoverage.java
diff options
context:
space:
mode:
Diffstat (limited to 'petascope/src/petascope/wcs2/server/ops/GetCoverage.java')
-rw-r--r--petascope/src/petascope/wcs2/server/ops/GetCoverage.java528
1 files changed, 528 insertions, 0 deletions
diff --git a/petascope/src/petascope/wcs2/server/ops/GetCoverage.java b/petascope/src/petascope/wcs2/server/ops/GetCoverage.java
new file mode 100644
index 0000000..788beec
--- /dev/null
+++ b/petascope/src/petascope/wcs2/server/ops/GetCoverage.java
@@ -0,0 +1,528 @@
+/*
+ * This file is part of PetaScope.
+ *
+ * PetaScope 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, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * PetaScope 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with PetaScope. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * For more information please see <http://www.PetaScope.org>
+ * or contact Peter Baumann via <baumann@rasdaman.com>.
+ *
+ * Copyright 2009 Jacobs University Bremen, Peter Baumann.
+ */
+package petascope.wcs2.server.ops;
+
+//~--- non-JDK imports --------------------------------------------------------
+import org.apache.commons.io.IOUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import org.xml.sax.SAXException;
+
+import petascope.ConfigManager;
+
+import petascope.wcps.server.core.DbMetadataSource;
+import petascope.wcps.server.core.Metadata;
+
+import petascope.wcs.server.exceptions.WCSException;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import java.io.IOException;
+
+import java.math.BigInteger;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import java.util.List;
+import java.util.Set;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import org.odmg.DBag;
+import org.odmg.Database;
+import org.odmg.Implementation;
+import org.odmg.ODMGException;
+import org.odmg.OQLQuery;
+import org.odmg.QueryException;
+import org.odmg.Transaction;
+import rasj.RasGMArray;
+import rasj.RasImplementation;
+import petascope.wcps.server.core.CellDomainElement;
+import petascope.wcps.server.core.DomainElement;
+import petascope.wcps.server.core.RangeElement;
+import petascope.wcps.server.core.SDU;
+import petascope.wcs.server.exceptions.InputOutputException;
+import petascope.wcs.server.exceptions.InternalComponentException;
+import petascope.wcs.server.exceptions.InvalidParameterValueException;
+import petascope.wcs.server.exceptions.InvalidServiceConfigurationException;
+import petascope.wcs.server.exceptions.NoApplicableCodeException;
+import petascope.wcs.server.exceptions.WcsRuntimeException;
+import petascope.wcs.server.exceptions.XmlNotValidException;
+import petascope.wcs2.server.templates.WcsNamespaceContext;
+
+/**
+ * Get Coverage operation for WCS 2.0
+ *
+ * @author Andrei Aiordachioaie
+ */
+public class GetCoverage implements WcsOperation {
+
+ private static Logger LOG = LoggerFactory.getLogger(GetCoverage.class);
+ private DocumentBuilder builder = null;
+ private XPathFactory xpathFactory = XPathFactory.newInstance();
+ /* Template XMLs for response types */
+ private String GetCoverageResponse;
+ private String rangeComponentTemplate;
+ /* Xml request */
+ private Document doc;
+ /* for Metadata */
+ private DbMetadataSource meta;
+ /* The new coverage domain */
+ private String lowPoint, highPoint, newAxesLabels;
+
+ public GetCoverage(DbMetadataSource metadata) throws WCSException {
+ meta = metadata;
+ GetCoverageResponse = ConfigManager.WCS2_GET_COVERAGE_TEMPLATE;
+ if (GetCoverageResponse == null) {
+ throw new InvalidServiceConfigurationException("Could not find template file.");
+ }
+
+ /* Find the RangeField template string */
+ String starttag = "<gmlwcs:rangeField", endtag = "</gmlwcs:rangeField>";
+ int start = GetCoverageResponse.indexOf(starttag);
+ int end = GetCoverageResponse.indexOf(endtag);
+ rangeComponentTemplate = GetCoverageResponse.substring(start, end + endtag.length());
+ String newTemplate = GetCoverageResponse.substring(0, start - 1) + GetCoverageResponse.substring(end + endtag.length());
+ GetCoverageResponse = newTemplate;
+
+ /* init XML parser */
+ DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
+ domFactory.setNamespaceAware(true); // never forget this!
+ try {
+ builder = domFactory.newDocumentBuilder();
+ } catch (Exception e) {
+ throw new NoApplicableCodeException("Error initializing XML parser", e);
+ }
+ }
+
+ @Override
+ public String execute(String stringXml) throws WCSException {
+ String output;
+ Metadata cov;
+
+ try {
+ doc = builder.parse(IOUtils.toInputStream(stringXml));
+ cov = readCoverageMetadata();
+ } catch (SAXException e) {
+ throw new XmlNotValidException("Could not parse input request.", e);
+ } catch (IOException e) {
+ throw new InputOutputException("Could not read input request", e);
+ } catch (XPathExpressionException e) {
+ throw new InternalComponentException("Invalid XPath expression", e);
+ }
+
+ // Analyze input and build trim/slice info structures
+ String subsetting = computeRequestSubsettingLimits(cov);
+ String coverageName = cov.getCoverageName();
+
+ // Evaluate all bands and then perform string processing to get coverage data
+ String coverageData = buildCoverageData(cov, subsetting);
+
+ // Build response xml document
+ output = buildOutputXml(coverageData, cov);
+
+ return output;
+ }
+
+ private NodeList evalXPathList(String query, Object context) throws WCSException {
+ try {
+ LOG.debug("Evaluating XPath: {}", query);
+ XPath xpath = xpathFactory.newXPath();
+
+ xpath.setNamespaceContext(new WcsNamespaceContext());
+ NodeList result = (NodeList) xpath.evaluate(query, context, XPathConstants.NODESET);
+
+ for (int i = 0; i < result.getLength(); i++) {
+ LOG.trace("Result {}: {}", i, result.item(i).getNodeValue());
+ }
+
+ return (NodeList) result;
+ } catch (XPathExpressionException e) {
+ LOG.error("Could not execute XPath expression '{}'", query);
+
+ throw new WcsRuntimeException("Could not execute XPath expression", e);
+ }
+ }
+
+ private String getCoverageName(Document doc) throws WCSException {
+ NodeList nodes = evalXPathList("//wcs:id/@gml:id", doc);
+
+ if (nodes.getLength() != 1) {
+ LOG.error("Could not retrieve coverage name from XML document.");
+
+ return null;
+ }
+
+ return nodes.item(0).getNodeValue();
+ }
+
+ private Metadata readCoverageMetadata() throws WCSException, XPathExpressionException {
+ String coverageId = getCoverageName(doc);
+
+ if (meta.existsCoverageName(coverageId)) {
+ try {
+ Metadata cov = meta.read(coverageId);
+
+ return cov;
+ } catch (Exception e) {
+ e.printStackTrace();
+
+ throw new NoApplicableCodeException("Metadata for coverage '"
+ + coverageId + "' is not valid.");
+ }
+ } else {
+ throw new InvalidParameterValueException("gml:id");
+ }
+ }
+
+ /** Computes the domain of the new coverage, and returns a string that can be
+ * used to do subsetting on the original coverage. Also computes the low, high
+ * and the axis labels for the new coverage.
+ *
+ * @param coverage
+ * @return
+ * @throws WCSException
+ */
+ private String computeRequestSubsettingLimits(Metadata coverage) throws WCSException {
+ int dims = coverage.getDimension(), i = 0;
+ String[] limits = new String[dims];
+ BigInteger[] high = new BigInteger[dims];
+ BigInteger[] low = new BigInteger[dims];
+ String[] axesLabels = new String[dims];
+ boolean[] sliced = new boolean[dims];
+ boolean[] trimmed = new boolean[dims];
+
+ Iterator<CellDomainElement> it = coverage.getCellDomainIterator();
+ Iterator<DomainElement> it2 = coverage.getDomainIterator();
+ i = 0;
+ while (it.hasNext() && it2.hasNext()) {
+ CellDomainElement cell = it.next();
+ DomainElement dom = it2.next();
+ LOG.trace(cell.toString());
+ LOG.trace(dom.toString());
+ high[i] = cell.getHi();
+ low[i] = cell.getLo();
+ axesLabels[i] = dom.getName();
+ limits[i] = low[i] + ":" + high[i];
+ sliced[i] = false;
+ trimmed[i] = false;
+ i++;
+ }
+
+ NodeList list = null;
+ int axisIndex;
+ String axis, root;
+ NodeList trims, slices;
+
+ trims = evalXPathList("//wcs:trimDimension", doc);
+ for (i = 0; i < trims.getLength(); i++) {
+ Node trim = trims.item(i);
+
+ list = evalXPathList("wcs:dimension/text()", trim);
+ axis = list.item(0).getNodeValue();
+ axisIndex = coverage.getDomainIndexByName(axis);
+ if (axisIndex == -1) {
+ throw new InvalidParameterValueException("dimension. Explanation: Unknown axis: " + axis);
+ }
+ if (trimmed[axisIndex] || sliced[axisIndex]) {
+ throw new NoApplicableCodeException("Already performed one subsetting operation on axis: " + axis);
+ }
+
+ list = evalXPathList("wcs:trimLow/text()", trim);
+ if (list.getLength() > 0) {
+ try {
+ long val = Long.parseLong(list.item(0).getNodeValue());
+ low[axisIndex] = BigInteger.valueOf(val);
+ } catch (NumberFormatException e) {
+ throw new InvalidParameterValueException("trimLow. Explanation: invalid integer number: " + list.item(0).getNodeValue());
+ }
+ }
+ list = evalXPathList("wcs:trimHigh/text()", trim);
+ if (list.getLength() > 0) {
+ try {
+ long val = Long.parseLong(list.item(0).getNodeValue());
+ high[axisIndex] = BigInteger.valueOf(val);
+ } catch (NumberFormatException e) {
+ throw new InvalidParameterValueException("trimHigh. Explanation: invalid integer number: " + list.item(0).getNodeValue());
+ }
+ }
+
+ trimmed[axisIndex] = true;
+ limits[axisIndex] = low[axisIndex] + ":" + high[axisIndex];
+ LOG.debug("New limits for axis {}: {}", axis, limits[axisIndex]);
+ }
+
+ slices = evalXPathList("//wcs:sliceDimension", doc);
+ for (i = 0; i < slices.getLength(); i++) {
+ Node slice = slices.item(i);
+
+ list = evalXPathList("wcs:dimension/text()", slice);
+ axis = list.item(0).getNodeValue();
+ axisIndex = coverage.getDomainIndexByName(axis);
+ if (axisIndex == -1) {
+ throw new InvalidParameterValueException("dimension. Explanation: Unknown axis name: " + axis);
+ }
+ if (trimmed[axisIndex] || sliced[axisIndex]) {
+ throw new NoApplicableCodeException("Already performed one subsetting operation on axis: " + axis);
+ }
+
+ list = evalXPathList("wcs:slicePoint/text()", slice);
+ if (list.getLength() > 0) {
+ try {
+ long point = Long.parseLong(list.item(0).getNodeValue());
+ low[axisIndex] = BigInteger.valueOf(point);
+ } catch (NumberFormatException e) {
+ throw new InvalidParameterValueException("slicePoint. Explanation: invalid integer number: " + list.item(0).getNodeValue());
+ }
+ high[axisIndex] = low[axisIndex];
+ limits[axisIndex] = list.item(0).getNodeValue();
+ }
+
+ sliced[axisIndex] = true;
+ LOG.debug("New limits for axis {}: {}", axis, limits[axisIndex]);
+ }
+
+ // Compute the lowest, highest point and the labels
+ lowPoint = "";
+ highPoint = "";
+ newAxesLabels = "";
+ int first = 0;
+ for (i = 0; i < dims; i++) {
+ if (sliced[i] == false) {
+ if (first == 0) {
+ lowPoint = low[i].toString();
+ highPoint = high[i].toString();
+ newAxesLabels = axesLabels[i];
+ first++;
+ } else {
+ lowPoint += " " + low[i];
+ highPoint += " " + high[i];
+ newAxesLabels += " " + axesLabels[i];
+ }
+ }
+ }
+
+ // Concatenate all limits into a single string
+ String result = limits[0];
+
+ for (i = 1; i < dims; i++) {
+ result += ", " + limits[i];
+ }
+
+ return result;
+ }
+
+ public String executeRasqlQuery(String query) throws WCSException {
+ byte[] result = null;
+
+ Implementation impl = new RasImplementation(ConfigManager.RASDAMAN_URL);
+ Database db = impl.newDatabase();
+
+ try {
+ db.open(ConfigManager.RASDAMAN_DATABASE, Database.OPEN_READ_ONLY);
+ } catch (ODMGException odmge) {
+ try {
+ db.close();
+ } catch (ODMGException e) {
+ }
+
+ throw new InternalComponentException("Could not connect to rasdaman server at " + ConfigManager.RASDAMAN_URL + ", database " + ConfigManager.RASDAMAN_DATABASE, odmge);
+ }
+
+ Transaction tr = impl.newTransaction();
+
+ tr.begin();
+ OQLQuery q = impl.newOQLQuery();
+ DBag resultSet;
+
+ try {
+ q.create(query);
+ resultSet = (DBag) q.execute();
+
+ if (resultSet != null) {
+ Iterator resultIterator = resultSet.iterator();
+
+ if (resultIterator.hasNext()) {
+ Object current = resultIterator.next();
+
+ try {
+ RasGMArray resultArray = (RasGMArray) current;
+ result = resultArray.getArray();
+ } catch (ClassCastException e) {
+ LOG.error("result=" + current.toString());
+ result = current.toString().getBytes();
+ }
+ }
+ }
+ } catch (QueryException qe) {
+ tr.commit();
+
+ try {
+ db.close();
+ } catch (ODMGException odmge) {
+ }
+
+ throw new InternalComponentException("Could not evaluate rasdaman query: '" + query + "'. Cause: " + qe.getMessage(), qe);
+ }
+
+ tr.commit();
+
+ try {
+ db.close();
+ } catch (ODMGException odmge) {
+ }
+
+ return new String(result);
+ }
+
+ /** Creates a string with the contents of the GetCoverage response XML */
+ private String buildOutputXml(String coverageData, Metadata coverage) {
+ String xml = GetCoverageResponse;
+ xml = xml.replaceAll("\\{coverageId\\}", coverage.getCoverageName() + Math.random());
+ xml = xml.replaceAll("\\{gridDimension\\}", String.valueOf(coverage.getDimension()));
+ xml = xml.replaceAll("\\{gridId\\}", "grid-" + coverage.getCoverageName());
+ // low
+ xml = xml.replaceAll("\\{low\\}", lowPoint);
+ // high
+ xml = xml.replaceAll("\\{high\\}", highPoint);
+ // axisLabels
+ xml = xml.replaceAll("\\{axisLabels\\}", newAxesLabels);
+ // coverageData
+ xml = xml.replaceAll("\\{coverageData\\}", coverageData);
+
+
+
+ // Build the range structure
+ Iterator<RangeElement> it3 = coverage.getRangeIterator();
+ int i = -1;
+ String rangeComponents = "";
+ while (it3.hasNext()) {
+ String component = rangeComponentTemplate;
+ RangeElement range = it3.next();
+ i++;
+ String rangeId = "range-" + coverage.getCoverageId() + "-" + range.getName();
+ LOG.trace(range.toString());
+
+ component = component.replaceAll("\\{rangeFieldId\\}", rangeId);
+ component = component.replaceAll("\\{fieldName\\}", range.getName());
+ component = component.replaceAll("\\{datatype\\}", DescribeCoverage.DATATYPE_URN_PREFIX + range.getType());
+
+ // Compute the null values for this range field
+ Set<String> nullVals = new HashSet<String>();
+ Iterator<String> it = coverage.getNullSetIterator();
+ while (it.hasNext()) {
+ List<String> nilVal = SDU.str2string(it.next());
+ nullVals.add(nilVal.get(i));
+ }
+ StringBuffer nullValsString = new StringBuffer();
+ it = nullVals.iterator();
+ while (it.hasNext()) {
+ nullValsString.append(" " + it.next());
+ }
+ component = component.replaceAll("\\{nilValues\\}", nullValsString.toString().substring(1));
+
+ // And add this range field to the range structure
+ rangeComponents += component;
+ }
+
+ xml = xml.replaceAll("\\{rangeFields\\}", rangeComponents);
+ xml = xml.replaceAll("\\{rangeStructureId\\}", "rangeStructure-" + coverage.getCoverageId());
+
+ xml = xml.replaceAll("\\{wcsSchemaUrl\\}", ConfigManager.WCS2_SCHEMA_URL);
+
+ return xml;
+ }
+
+ /**
+ * Retrieve the coverage data for a multi-band coverage,
+ * with particular subsetting parameters.
+ * @param coverage metadata for the coverage we want
+ * @param subsetting subsetting string, to be used in the RasQL query
+ * @return a string of space-separated values, where various bands of one
+ * pixel are comma-separated. For example, the string "1,2 3,4 5,6" can
+ * be the coverage data of a 1-by-3 coverage, with two bands
+ */
+ private String buildCoverageData(Metadata coverage, String subsetting) throws WCSException {
+ String coverageName = coverage.getCoverageName();
+ Iterator<RangeElement> it = coverage.getRangeIterator();
+ int bandcount = 0;
+ while (it.hasNext()) {
+ it.next();
+ bandcount++;
+ }
+ LOG.debug("Coverage {} has {} bands", coverageName, bandcount);
+ String[][] pixels = new String[bandcount][];
+ String currentBand = "";
+ /* For all bands of the coverage, execute a rasql query */
+ for (int band = 0; band < bandcount; band++) {
+ LOG.trace("Processing band {}", band);
+ // Construct rasql query
+ currentBand = "." + band;
+ // If this is a one-band image, then band-subsetting would result in an error
+ if (bandcount == 1) {
+ currentBand = "";
+ }
+ String rasqlQuery = "select csv(cov[" + subsetting + "]" + currentBand + ") "
+ + "from " + coverageName + " as cov";
+
+ // Execute RasQl query => coverage data
+ LOG.trace("Executing query {}", rasqlQuery);
+ String output = executeRasqlQuery(rasqlQuery);
+
+ // Remove the curly braces from the rasql output
+ LOG.trace("Removing curly braces...");
+ output = output.replaceAll("\\{", "");
+ output = output.replaceAll("\\}", "");
+
+ // Tokenize the input to get the pixel values of the current band
+ LOG.trace("Splitting values with comma...");
+ pixels[band] = output.split(",");
+
+ LOG.trace("Done processing band {}.", band);
+ }
+
+ /* Combine all bands into one single string */
+ int pixelcount = pixels[0].length;
+ StringBuilder data = new StringBuilder(pixelcount * bandcount);
+ LOG.debug("Going to combine {} pixels with {} bands...", pixelcount, bandcount);
+ for (int pix = 0; pix < pixelcount; pix++) {
+ if (pixelcount > 20 && pix % (pixelcount / 20) == 0) {
+ LOG.debug("Processing Pixel {} of " + pixelcount + " - {}%", pix, pix * 100 / (pixelcount - 1));
+ }
+ for (int b = 0; b < bandcount - 1; b++) {
+ data.append(pixels[b][pix] + ",");
+ }
+ data.append(pixels[bandcount - 1][pix]);
+ data.append(" ");
+ }
+ return data.substring(0, data.length() - 2);
+ }
+}