diff options
Diffstat (limited to 'petascope/src/petascope/wcs2/server/ops/GetCoverage.java')
-rw-r--r-- | petascope/src/petascope/wcs2/server/ops/GetCoverage.java | 528 |
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); + } +} |