package io.jeo.vector; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.prep.PreparedGeometry; import io.jeo.data.Cursor; import io.jeo.geom.Bounds; import io.jeo.geom.Geom; import io.jeo.proj.Proj; import io.jeo.util.Function; import io.jeo.util.Predicate; import org.osgeo.proj4j.CoordinateReferenceSystem; import org.osgeo.proj4j.CoordinateTransform; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; /** * Extension of Cursor for {@link Feature} objects. * */ public abstract class FeatureCursor extends Cursor<Feature> { /** * Returns an empty feature cursor. */ public static FeatureCursor empty() { return new FeatureCursor() { @Override public boolean hasNext() throws IOException { return false; } @Override public Feature next() throws IOException { return null; } @Override public void close() throws IOException { } }; } /** * Reprojects features in the stream to a specified coordinate reference system. * <p> * This method determines the source crs from objects in the underlying cursor. Use * {@link #reproject(org.osgeo.proj4j.CoordinateReferenceSystem, org.osgeo.proj4j.CoordinateReferenceSystem)} to * explicitly specify the source crs. * </p> * @param crs The crs to reproject to. * * @return The wrapped cursor. */ public FeatureCursor reproject(CoordinateReferenceSystem crs) { return reproject(null, crs); } /** * Reprojects features in the cursor between two specified coordinate reference systems. * * @param from The source crs. * @param to The destination crs. * * @return The wrapped cursor. */ public FeatureCursor reproject(CoordinateReferenceSystem from, CoordinateReferenceSystem to) { return from != null ? new TransformCursor(this, from, to) : new ReprojectCursor(this, to); } private static class TransformCursor extends FeatureCursorWrapper { CoordinateTransform tx; TransformCursor(Cursor<Feature> delegate, CoordinateReferenceSystem from, CoordinateReferenceSystem to) { super(delegate); tx = Proj.transform(from, to); } @Override public Feature next() throws IOException { return new TransformFeature(super.next(), tx); } } private static class TransformFeature extends GeometryTransformFeature { CoordinateTransform transform; TransformFeature(Feature delegate, CoordinateTransform transform) { super(delegate); this.transform = transform; } @Override protected Geometry wrap(Geometry g) { return Proj.transform(g, transform); } } private static class ReprojectCursor<T extends Feature> extends FeatureCursorWrapper { Map<String, CoordinateTransform> transforms; CoordinateReferenceSystem target; ReprojectCursor(Cursor<Feature> delegate, CoordinateReferenceSystem target) { super(delegate); this.target = Objects.requireNonNull(target, "target crs must not be null"); transforms = new HashMap<>(); } @Override public Feature next() throws IOException { return new ReprojectFeature(super.next(), target, transforms); } } private static class ReprojectFeature extends GeometryTransformFeature { CoordinateReferenceSystem target; Map<String,CoordinateTransform> transforms; public ReprojectFeature(Feature delegate, CoordinateReferenceSystem target, Map<String,CoordinateTransform> transforms) { super(delegate); this.target = target; this.transforms = transforms; } @Override protected Geometry wrap(Geometry g) { CoordinateReferenceSystem crs = Proj.crs(g); if (crs != null) { CoordinateTransform tx = transforms.get(crs.getName()); if (tx == null) { tx = Proj.transform(crs, target); transforms.put(crs.getName(), tx); } g = Proj.transform(g, tx); } return g; } } /** * Sets the projection of features in the cursor, overriding any projection that exists. * * @param crs The override projection. * * @return The wrapped cursor. */ public FeatureCursor crs(CoordinateReferenceSystem crs) { return new CrsOverrideCursor(this, crs); } private static class CrsOverrideCursor<T extends Feature> extends FeatureCursorWrapper { CoordinateReferenceSystem crs; CrsOverrideCursor(Cursor<Feature> delegate, CoordinateReferenceSystem crs) { super(delegate); this.crs = crs; } @Override public Feature next() throws IOException { Feature f = super.next(); return new GeometryTransformFeature(f) { @Override protected Geometry wrap(Geometry g) { return Proj.crs(g, crs); } }; } } /** * Returns a cursor with objects that intersect the specified bounding box. * <p> * The <tt>loose</tt> paremeter controls whether a full intersection * </p> * @param bbox The bounding box filter. * @param loose * * @return The wrapped cursor. */ public FeatureCursor intersect(final Envelope bbox, boolean loose) { Predicate<Geometry> p = new Predicate<Geometry>() { @Override public boolean test(Geometry val) { return val.getEnvelopeInternal().intersects(bbox); } }; if (!loose) { final PreparedGeometry poly = Geom.prepare(Bounds.toPolygon(bbox)); p = p.and(new Predicate<Geometry>() { @Override public boolean test(Geometry val) { return poly.intersects(val); } }); } final Predicate<Geometry> intersect = p; return wrap(filter(new Predicate<Feature>() { @Override public boolean test(Feature f) { Geometry g = f.geometry(); if (g == null) { return false; } return intersect.test(g); } })); } /** * Transforms non geometry collection objects from the specified cursor to the appropriate * geometry collection. */ public FeatureCursor multify() { return wrap(map(new Function<Feature, Feature>() { @Override public Feature apply(Feature value) { return Features.multify(value); } })); } /** * Transforms the cursor one containing features with the specified attributes. * * @param fields The fields to select. * * @return The selected cursor. */ public FeatureCursor select(final Iterable<String> fields) { return new SelectFieldsCursor(this, fields); } static class SelectFieldsCursor extends FeatureCursorWrapper { private final List<String> fields; public SelectFieldsCursor(Cursor<Feature> delegate, Iterable<String> fields) { super(delegate); this.fields = new ArrayList(); for (String f : fields) { this.fields.add(f); } } @Override public Feature next() throws IOException { Feature next = super.next(); if (next != null) { Map<String,Object> values = new LinkedHashMap<>(next.map()); values.keySet().retainAll(fields); return new MapFeature(next.id(), values); } return next; } } @Override public FeatureCursor filter(Predicate<Feature> filter) { return wrap(super.filter(filter)); } @Override public FeatureCursor limit(Integer limit) { return wrap(super.limit(limit)); } @Override public FeatureCursor skip(Integer offset) { return wrap(super.skip(offset)); } @Override public FeatureCursor buffer(int n) { return wrap(super.buffer(n)); } /** * Wraps a cursor of Feature as a FeatureCursor, if it is not an instance * already. */ public static <T extends Feature> FeatureCursor wrap(Cursor<Feature> cursor) { if (cursor instanceof FeatureCursor) { return (FeatureCursor) cursor; } return new FeatureCursorWrapper(cursor); } static class FeatureCursorWrapper extends FeatureCursor { Cursor<Feature> delegate; public FeatureCursorWrapper(Cursor<Feature> cursor) { this.delegate = cursor; } @Override public boolean hasNext() throws IOException { return delegate.hasNext(); } @Override public Feature next() throws IOException { return delegate.next(); } @Override public void rewind() { delegate.rewind(); } @Override public void close() throws IOException { delegate.close(); } } }