/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2013 - 2016, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.coverage.grid.io.footprint;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.geotools.util.Converters;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryComponentFilter;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.operation.union.CascadedPolygonUnion;
/**
* Policy specifying how to apply an inset of the footprints
*
* @author Andrea Aime - GeoSolutions
*/
public enum FootprintInsetPolicy {
/**
* Full inset from all direction. Works best with fully overlapping granules
*/
full {
@Override
public Geometry applyInset(Geometry footprint, Geometry granuleBounds, double inset) {
if (footprint == null) {
return null;
}
return footprint.buffer(-inset);
}
},
/**
* Applies the inset only on the footprint sections that are not lying about the granule bounds
* (assuming the granules are cut in a regular grid and meant to be displayed side by side (no
* overlap)
*/
border {
@Override
public Geometry applyInset(Geometry footprint, Geometry granuleBounds, double inset) {
if (footprint != null) {
// we buffer only the portions of the footprint that are not overlapping with
// the granule bounds, and remove them from the footprint
List<LinearRing> boundRings = getRings(granuleBounds);
List<LinearRing> footprintRings = getRings(footprint);
Geometry bufferedOuterRings = buffer(boundRings, Math.max(inset / 100, 1e-9));
List<LineString> internalBorders = filterRings(footprintRings, bufferedOuterRings);
if (!internalBorders.isEmpty()) {
Geometry bufferedInternalRings = buffer(internalBorders, inset);
Geometry difference = footprint.difference(bufferedInternalRings);
footprint = collectPolygons(difference);
}
}
return footprint;
}
/**
* Collects all sub-polygons into the specified geometry and returns them either as a single
* polygon, or as a multipolygon, shaving off any other lower dimension geometry
*
* @param geometry
* @return
*/
private Geometry collectPolygons(Geometry geometry) {
if (geometry.isEmpty()) {
return geometry;
}
final List<Polygon> polygons = new ArrayList<Polygon>();
geometry.apply(new GeometryComponentFilter() {
@Override
public void filter(Geometry geom) {
if (geom instanceof Polygon && !geom.isEmpty()) {
polygons.add((Polygon) geom);
}
}
});
if (polygons.isEmpty()) {
return geometry.getFactory().createMultiPolygon(new Polygon[0]);
} else if (polygons.size() == 1) {
return polygons.get(0);
} else {
Polygon[] array = (Polygon[]) polygons.toArray(new Polygon[polygons.size()]);
return array[0].getFactory().createMultiPolygon(array);
}
}
private List<LineString> filterRings(List<LinearRing> footprintRings,
Geometry bufferedOuterRings) {
List<LineString> result = new ArrayList<LineString>();
for (LinearRing ring : footprintRings) {
Geometry difference = ring.difference(bufferedOuterRings);
if (difference != null) {
collectLines(difference, result);
}
}
return result;
}
private Geometry buffer(List<? extends Geometry> geometries, double distance) {
List<Geometry> polygons = new ArrayList<Geometry>();
for (Geometry g : geometries) {
Geometry buffered = g.buffer(distance);
polygons.add(buffered);
}
return CascadedPolygonUnion.union(polygons);
}
private List<LinearRing> getRings(Geometry bounds) {
final ArrayList<LinearRing> rings = new ArrayList<LinearRing>();
bounds.apply(new GeometryComponentFilter() {
@Override
public void filter(Geometry geom) {
if (geom instanceof LinearRing && !geom.isEmpty()) {
rings.add((LinearRing) geom);
}
}
});
return rings;
}
private void collectLines(Geometry geometry, final List<LineString> lines) {
geometry.apply(new GeometryComponentFilter() {
@Override
public void filter(Geometry geom) {
if (geom instanceof LineString && !geom.isEmpty()) {
lines.add((LineString) geom);
}
}
});
}
};
public static final String INSET_PROPERTY = "footprint_inset";
public static final String INSET_TYPE_PROPERTY = "footprint_inset_type";
public abstract Geometry applyInset(Geometry footprint, Geometry granuleBounds, double inset);
/**
* Returns the list of names for this enum
* @return
*/
public static List<String> names() {
FootprintInsetPolicy[] values = FootprintInsetPolicy.values();
List<String> names = new ArrayList<String>(values.length);
for (int i = 0; i < values.length; i++) {
names.add(values[i].name());
}
return names;
}
public static FootprintInsetPolicy getInsetPolicy(Properties properties) {
String insetTypeValue = (String) properties.get(INSET_TYPE_PROPERTY);
if (insetTypeValue == null || insetTypeValue.trim().isEmpty()) {
return FootprintInsetPolicy.border;
} else {
try {
return FootprintInsetPolicy.valueOf(insetTypeValue.trim());
} catch (Exception e) {
throw new IllegalArgumentException("Invalid inset type '" + insetTypeValue
+ "', valid values are: " + FootprintInsetPolicy.names());
}
}
}
public static double getInset(Properties properties) {
String inset = (String) properties.get(INSET_PROPERTY);
if (inset == null) {
return 0;
}
Double converted = Converters.convert(inset, Double.class);
if (converted == null) {
throw new IllegalArgumentException("Invalid inset value, should be a "
+ "floating point number, but instead it is: '" + inset + "'");
}
return converted;
}
}