/* Copyright 2013 The jeo project. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.jeo.vector; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import io.jeo.geom.Geom; import io.jeo.proj.Proj; import io.jeo.util.Function; import io.jeo.util.Optional; import io.jeo.util.Util; import org.osgeo.proj4j.CoordinateReferenceSystem; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import static io.jeo.vector.VectorQuery.all; /** * Feature utility class. * * @author Justin Deoliveira, OpenGeo */ public class Features { /** geometry factory */ static GeometryFactory gfac = new GeometryFactory(); /** * Returns the bounds of the feature object. * <p> * The bounds is computed by computing the aggregated bounds of all geometries of the feature * object. Projections are not taken into account. To handle geometries in different projections * use the {@link #boundsReprojected(Feature)} method. * </p> * @param f The feature. * * @return The bounds, or a bounds object in which {@link Envelope#isNull()} returns true. */ public static Envelope bounds(Feature f) { Envelope e = new Envelope(); for (Object obj : f.map().values()) { if (obj instanceof Geometry) { e.expandToInclude(((Geometry) obj).getEnvelopeInternal()); } } return e; } /** * Returns the bounds of the feature object, reprojecting geometries of the feature if required. * <p> * The bounds is computed by computing the aggregated bounds of all geometries of the feature * object. All geometries are reprojected to the crs returned from * <tt>f.geometry()</tt>. Therefore this method * requires that the features default geometry has a crs object. * </p> * @param f The feature. * * @return The bounds, or a bounds object in which {@link Envelope#isNull()} returns true. */ public static Envelope boundsReprojected(Feature f) { Geometry geom = f.geometry(); CoordinateReferenceSystem crs = Proj.crs(geom); if (crs == null) { throw new IllegalArgumentException("Feature default geometry has no crs"); } return boundsReprojected(f, crs); } /** * Returns the bounds of the feature object, reprojecting geometries of the feature if required. * <p> * The bounds is computed by computing the aggregated bounds of all geometries of the feature * object in the specified crs. * </p> * @param f The feature. * @param crs The target projection. * * @return The bounds, or a bounds object in which {@link Envelope#isNull()} returns true. */ public static Envelope boundsReprojected(Feature f, CoordinateReferenceSystem crs) { if (crs == null) { throw new IllegalArgumentException("crs must not be null"); } Envelope e = new Envelope(); for (Object val : f.map().values()) { if (val instanceof Geometry) { Geometry g = (Geometry) val; if (g == null) { //ignore continue; } CoordinateReferenceSystem c = Proj.crs(g); if (c != null) { g = Proj.reproject(g, c, crs); } else { // no crs, just assume it is the same reference system } e.expandToInclude(g.getEnvelopeInternal()); } } return e; } /** * Retypes a feature object to a new schema. * <p> * This method works by "pulling" the attributes defined by the fields of {@link Schema} from * the feature object. * </p> * @param feature The original feature. * @param schema The schema to retype to. * * @return The retyped feature. */ public static Feature retype(Feature feature, Schema schema) { List<Object> values = new ArrayList<Object>(); for (Field f : schema) { values.add(feature.get(f.name())); } return new ListFeature(feature.id(), schema, values); } /** * Copies values from one feature to another. * * @param from THe source feature. * @param to The target feature. * * @return The target feature. */ public static Feature copy(Feature from, Feature to) { for (Map.Entry<String, Object> kv : from.map().entrySet()) { String key = kv.getKey(); Object val = kv.getValue(); to.put(key, val); } return to; } /** * Converts non geometry collection types in the schema to appropriate collection type. * * @param schema The original schema. * * @return The transformed schema. */ public static Schema multify(Schema schema) { SchemaBuilder b = Schema.build(schema.name()); for (Field fld : schema) { if (Geometry.class.isAssignableFrom(fld.type())) { Class<? extends Geometry> t = (Class<? extends Geometry>) fld.type(); switch(Geom.Type.from(t)) { case POINT: t = MultiPoint.class; break; case LINESTRING: t = MultiLineString.class; break; case POLYGON: t = MultiPolygon.class; break; } b.field(fld.name(), t, fld.crs()); } else { b.field(fld); } } return b.schema(); } /** * Converts non collection geometry objects to associated collection type. * * @param feature The original feature. * * @return The transformed feature. */ public static Feature multify(Feature feature) { return new GeometryTransformFeature(feature) { @Override protected Geometry wrap(Geometry g) { return Geom.multi(g); } }; } /** * Returns the crs of a feature. * <p> * The crs of a feature is a crs associated with it's default geometry. * </p> * * @see Feature#geometry() * @see Proj#crs(Geometry) */ public static CoordinateReferenceSystem crs(Feature f) { return Proj.crs(f.geometry()); } /** * Returns a new feature id if the specified id is null. */ public static String id(String id) { return id != null ? id : Util.uuid(); } /** * Derives a schema for a vector dataset. * <p> * This method computes the schema by inspecting the first feature. * </p> * @param data The dataset. * * @return The optional schema. */ public static Optional<Schema> schema(final VectorDataset data) throws IOException { return data.read(all().limit(1)).first().map(new Function<Feature, Schema>() { @Override public Schema apply(Feature f) { return schema(data.name(), f); } }); } /** * Creates a schema from a feature object. * * @param name Name of the schema. * @param f The feature. * * @return The new schema. * * @see {@link SchemaBuilder#fields(Feature)} */ public static Schema schema(String name, Feature f) { return Schema.build(name).fields(f).schema(); } /** * Compares two feature objects for equality. * <p> * Equality is based on {@link Feature#id()} and contents obtained from {@link Feature#map()}. * </p> * @param f1 The first feature. * @param f2 The second feature. * * @return True if the two features are equal. */ public static boolean equals(Feature f1, Feature f2) { if (!Objects.equals(f1.id(), f2.id())) { return false; } return Objects.equals(f1.map(), f2.map()); } /** * Computes the hashcode for a feature. * <p> * To remain consistent with {@link #equals(Feature, Feature)} the hash code is computed based on * {@link Feature#id()} and {@link Feature#map()} * </p> * @param f The feature. * * @return A hashcode. */ public static int hashCode(Feature f) { return Objects.hash(f.id(), f.map()); } /** * Returns a string representation of a feature. * <p> * Implementations of {@link Feature} are encouraged to use this method to implement {@link Feature#toString()}. * </p> */ public static String toString(Feature f) { StringBuilder sb = new StringBuilder(f.id()).append("{"); Map<String,Object> map = f.map(); if (!map.isEmpty()) { for (Map.Entry<String,Object> e : map.entrySet()) { sb.append(e.getKey()).append("=").append(e.getValue()).append(", "); } sb.setLength(sb.length()-2); } return sb.append("}").toString(); } }