/**
* Copyright (c) Codice Foundation
* <p>
* This 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 any later version.
* <p>
* This program 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. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
**/
package org.codice.ddf.spatial.ogc.csw.catalog.common;
import org.apache.commons.lang.StringUtils;
import org.codice.ddf.libs.geo.GeoFormatException;
import org.codice.ddf.libs.geo.util.GeospatialUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
import com.vividsolutions.jts.io.WKTWriter;
/**
* Parses OWS Bounding Box geometry XML and converts it to WKT.
* <p>
* Bounding Box XML is of the form:
* <p>
* <pre>
* {@code
* <ows:BoundingBox crs="urn:x-ogc:def:crs:EPSG:6.11:4326">
* <ows:LowerCorner>60.042 13.754</ows:LowerCorner>
* <ows:UpperCorner>68.410 17.920</ows:UpperCorner>
* </ows:BoundingBox>
* }
* </pre>
*
* @author rodgersh
*/
public class BoundingBoxReader {
private static final transient Logger LOGGER = LoggerFactory.getLogger(BoundingBoxReader.class);
private static final String SPACE = " ";
private static final String DEFAULT_CRS = "urn:ogc:def:crs:EPSG:4326";
private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
private static final ThreadLocal<WKTWriter> WKT_WRITER_THREAD_LOCAL =
new ThreadLocal<WKTWriter>() {
@Override
protected WKTWriter initialValue() {
return new WKTWriter();
}
};
private HierarchicalStreamReader reader;
private CswAxisOrder cswAxisOrder;
private WKTReader wktReader;
public BoundingBoxReader(HierarchicalStreamReader reader, CswAxisOrder cswAxisOrder) {
this.reader = reader;
this.cswAxisOrder = cswAxisOrder;
wktReader = new WKTReader(GEOMETRY_FACTORY);
}
public String getWkt() throws CswException {
String wkt = null;
// reader should initially be positioned at <ows:BoundingBox> element
LOGGER.debug("Initial node name = {}", reader.getNodeName());
if (!reader.getNodeName()
.contains("BoundingBox")) {
throw new CswException(
"BoundingBoxReader.getWkt(): supplied reader does not contain a BoundingBox.");
}
String crs = reader.getAttribute("crs");
// For now, default an empty CRS to EPSG:4326
crs = StringUtils.isEmpty(crs) ? DEFAULT_CRS : crs;
LOGGER.debug("crs = {}, coordinate order = {}", crs, this.cswAxisOrder);
// Move down to the first child node of <BoundingBox>, which should
// be the <LowerCorner> tag
reader.moveDown();
// Parse LowerCorner position from bounding box
String[] lowerCornerPosition = null;
if (reader.getNodeName()
.contains("LowerCorner")) {
String value = reader.getValue();
lowerCornerPosition = getCoordinates(value);
}
// Move back up to the <BoundingBox> parent tag
reader.moveUp();
// Move down to the next child node of the <BoundingBox> tag, which
// should be the <UpperCorner> tag
reader.moveDown();
// Parse UpperCorner position from bounding box
String[] upperCornerPosition = null;
if (reader.getNodeName()
.contains("UpperCorner")) {
String value = reader.getValue();
upperCornerPosition = getCoordinates(value);
}
// If both corner positions parsed, then compute other 2 corner
// positions of the bounding box and create the WKT for the bounding box.
if (lowerCornerPosition != null && lowerCornerPosition.length == 2
&& upperCornerPosition != null && upperCornerPosition.length == 2) {
/**
* The WKT created here need to be in LON/LAT. This is the order that we store WKT in
* the metacard location field.
*/
wkt = createWkt(lowerCornerPosition, upperCornerPosition);
} else {
throw new CswException(
"BoundingBoxReader.getWkt(): could not find either LowerCorner or UpperCorner tags.");
}
// Move position back up to the parent <BoundingBox> tag, where we started
reader.moveUp();
if (!isEPSG4326(crs) || cswAxisOrder.equals(CswAxisOrder.LAT_LON)) {
wkt = convertToEPSG4326(wkt, crs);
}
LOGGER.debug("Returning WKT {}.", wkt);
return wkt;
}
/**
* We want to create WKT in LON/LAT order.
* <p>
* Points for bounding box will be computed starting with the lower corner, and proceeding in a
* clockwise rotation. So lower corner point would be point 1, upper corner point would be point
* 3, and points 2 and 4 would be computed.
*/
private String createWkt(String[] lowerCornerPosition, String[] upperCornerPosition) {
LOGGER.debug("Creating WKT in LON/LAT coordinate order.");
if (upperCornerPosition[0].equals(lowerCornerPosition[0]) && upperCornerPosition[1].equals(
lowerCornerPosition[1])) {
return "POINT (" + lowerCornerPosition[0] + SPACE + lowerCornerPosition[1] + ")";
}
return "POLYGON ((" + lowerCornerPosition[0] + SPACE + lowerCornerPosition[1] + ", "
+ upperCornerPosition[0] + SPACE + lowerCornerPosition[1] + ", "
+ upperCornerPosition[0] + SPACE + upperCornerPosition[1] + ", "
+ lowerCornerPosition[0] + SPACE + upperCornerPosition[1] + ", "
+ lowerCornerPosition[0] + SPACE + lowerCornerPosition[1] + "))";
}
/**
* @param coords The latitude and longitude coordinates (in no particular order).
* @return The coordinates in an array
*/
private String[] getCoordinates(String coords) {
return coords.split(SPACE);
}
private String convertToEPSG4326(String wkt, String crs) throws CswException {
try {
Geometry geometry = wktReader.read(wkt);
Geometry convertedGeometry = GeospatialUtil.transformToEPSG4326LonLatFormat(geometry,
crs);
if (convertedGeometry != null && withinEPSG4326Bounds(convertedGeometry)) {
return WKT_WRITER_THREAD_LOCAL.get()
.write(convertedGeometry);
} else {
throw new GeoFormatException("Converted Geometry is not set, or not within EPSG4326 bounds");
}
} catch (ParseException | GeoFormatException e) {
throw new CswException(String.format("Unable to convert %s from %s to EPSG:4326",
wkt,
crs));
}
}
private boolean isEPSG4326(String crs) {
return crs.contains("4326") || crs.contains("CRS84");
}
private boolean withinEPSG4326Bounds(Geometry geometry) {
Coordinate[] coordinates = geometry.getCoordinates();
for (Coordinate coordinate : coordinates) {
// Longitude
if (coordinate.x < -180 || coordinate.x > 180) {
return false;
}
// Latitude
if (coordinate.y < -90 || coordinate.y > 90) {
return false;
}
}
return true;
}
}