/* 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.proj;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.Locale;
import java.util.regex.Pattern;
import com.vividsolutions.jts.geom.Envelope;
import io.jeo.geom.Bounds;
import io.jeo.geom.GeomBuilder;
import io.jeo.proj.wkt.ProjWKTEncoder;
import io.jeo.proj.wkt.ProjWKTParser;
import org.osgeo.proj4j.CRSFactory;
import org.osgeo.proj4j.CoordinateReferenceSystem;
import org.osgeo.proj4j.CoordinateTransform;
import org.osgeo.proj4j.CoordinateTransformFactory;
import org.osgeo.proj4j.Proj4jException;
import org.osgeo.proj4j.ProjCoordinate;
import org.osgeo.proj4j.datum.Datum;
import org.osgeo.proj4j.io.Proj4FileReader;
import org.osgeo.proj4j.proj.Projection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequenceFilter;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Point;
/**
* Projection module utility class.
*
* @author Justin Deoliveira, OpenGeo
*/
public class Proj {
static Logger LOGGER = LoggerFactory.getLogger(Proj.class);
static Pattern AUTH_CODE = Pattern.compile("\\w+:\\d+", Pattern.CASE_INSENSITIVE);
static CRSFactory csFactory = new CRSFactory();
static CoordinateTransformFactory txFactory = new CoordinateTransformFactory();
static GeomBuilder gBuilder = new GeomBuilder();
/**
* The canonical geographic coordinate reference system.
*/
public static final CoordinateReferenceSystem EPSG_4326 = Proj.crs("EPSG:4326");
/**
* Google mercator
*/
public static final CoordinateReferenceSystem EPSG_900913 = Proj.crs("EPSG:900913");
/**
* Looks up a crs object base on its EPSG identifier.
* <p>
* This method is equivalent to calling <pre>crs("EPSG:" + srid)</pre>
* </p>
* @return The matching crs object, or <code>null</code> if none found.
*/
public static CoordinateReferenceSystem crs(int srid) {
return crs("EPSG:"+srid);
}
/**
* Looks up a crs object base on its identifier.
*
* @return The matching crs object, or <code>null</code> if none found.
*/
public static CoordinateReferenceSystem crs(String s) {
if (s == null || s.isEmpty()) {
return null;
}
if ("urn:ogc:def:crs:OGC:1.3:CRS84".equalsIgnoreCase(s)) {
return EPSG_4326;
}
if (!AUTH_CODE.matcher(s).matches()) {
try {
return crs(new String[]{s});
}
catch(RuntimeException e) {
try {
return new ProjWKTParser().parse(s);
}
catch(Exception e2) {
throw e;
}
}
}
if ("epsg:4326".equalsIgnoreCase(s) && EPSG_4326 != null) {
return EPSG_4326;
}
//hack for epsg:900913, we nweed to add this to proj4j
if ("epsg:900913".equalsIgnoreCase(s)) {
return EPSG_900913 != null ? EPSG_900913 : createFromExtra("epsg", "900913");
}
return csFactory.createFromName(s);
}
/**
* Creates a crs object from projection parameter definition.
*
* @param projdef The projection / proj4 parameters.
*
* @return The crs object.
*/
public static CoordinateReferenceSystem crs(String... projdef) {
if (projdef != null && projdef.length == 1) {
return csFactory.createFromParameters(null, projdef[0]);
}
return csFactory.createFromParameters(null, projdef);
}
/**
* Returns the crs of the geometry, if it exists.
*
* @param geom The geometry object.
*
* @return The crs, or null.
*/
public static CoordinateReferenceSystem crs(Geometry geom) {
Object userData = geom.getUserData();
if (userData instanceof CoordinateReferenceSystem) {
return (CoordinateReferenceSystem) userData;
}
int srid = geom.getSRID();
if (srid > 0) {
return crs(srid);
}
return null;
}
/**
* Sets the coordinate reference system of a geometry.
*
* @param g The geometry.
* @param crs The crs.
*
* @return The original geometry object.
* @see #crs(Geometry, CoordinateReferenceSystem, boolean)
*/
public static Geometry crs(Geometry g, CoordinateReferenceSystem crs) {
return crs(g, crs, true);
}
/**
* Sets the coordinate reference system of a geometry.
*
* @param g The geometry.
* @param crs The crs.
* @param overwrite Whether to overwrite an existing crs that may exist.
*/
public static Geometry crs(Geometry g, CoordinateReferenceSystem crs, boolean overwrite) {
if (g != null) {
if (g.getUserData() == null || (overwrite && g.getUserData() instanceof CoordinateReferenceSystem)) {
g.setUserData(crs);
}
if (g.getSRID() == 0 || overwrite) {
Integer srid = epsgCode(crs);
if (srid != null) {
g.setSRID(srid);
}
}
}
return g;
}
private static CoordinateReferenceSystem createFromExtra(String auth, String code) {
Proj4FileReader r = new Proj4FileReader();
InputStream in = Proj.class.getResourceAsStream("other.extra");
try {
try {
return csFactory.createFromParameters(
auth+":"+code, r.readParameters(code, in));
}
finally {
in.close();
}
}
catch(IOException e) {
LOGGER.debug(String.format(Locale.ROOT,"Failure creating crs %s:%s from extra", auth, code));
return null;
}
}
/**
* Returns the EPSG identifier for a crs object.
*
* @return The epsg identifier, or null if the CRS has no epsg code.
*/
public static Integer epsgCode(CoordinateReferenceSystem crs) {
String name = crs.getName();
if (name != null) {
String[] split = name.split(":");
if (split.length == 2 && "epsg".equalsIgnoreCase(split[0])) {
return Integer.parseInt(split[1]);
}
}
return null;
}
/**
* Returns the valid bounds of the crs object.
* <p>
* <i>Warning</i>: This method is as currently implemented is inaccurate.
* </p>
* @return An approximate bounds of validity for the crs object, or <code>null</code> if it
* can not be determined.
*/
public static Bounds bounds(CoordinateReferenceSystem crs) {
//TODO: this method is wildly inaccurate, find a better way to determine bounds
// from a projection/crs
Projection p = crs.getProjection();
if (p != null) {
CoordinateReferenceSystem geo = crs("epsg:4326");
Point p1 = gBuilder.point(p.getMinLongitudeDegrees(), p.getMinLatitudeDegrees()).toPoint();
Point p2 = gBuilder.point(p.getMaxLongitudeDegrees(), p.getMaxLatitudeDegrees()).toPoint();
p1 = reproject(p1, geo, crs);
p2 = reproject(p2, geo, crs);
return new Bounds(p1.getX(), p2.getX(), p1.getY(), p2.getY());
}
return null;
}
/**
* Reprojects a geometry object between two coordinate reference systems.
* <p>
* This method is convenience for:
* <pre><code>
* reproject(g, crs(from), crs(to));
* </code></pre>
* </p>
*
* @param g The geometry to reproject.
* @param from The source crs, as defined by {@link #crs(String)}
* @param to The target crs, as defined by {@link #crs(String)}
*
* @return The reprojected geometry.
*
* @see {@link #crs(String)}
* @see {@link #reproject(Geometry, CoordinateReferenceSystem, CoordinateReferenceSystem)}
*/
public static <T extends Geometry> T reproject(T g, String from, String to) {
return reproject(g, crs(from), crs(to));
}
/**
* Reprojects a geometry object between two coordinate reference systems.
* <p>
* In the event a transformation between the two crs objects can not be found this method throws
* {@link IllegalArgumentException}.
*
* In the event the two specified coordinate reference systems are equal this method is a
* no-op and returns the original geometry object.
* </p>
* @param g The geometry to reproject.
* @param from The source coordinate reference system.
* @param to The target coordinate reference system.
*
* @return The reprojected geometry.
*
* @throws IllegalArgumentException If no coordinate transform can be found.
*/
public static <T extends Geometry> T reproject(T g, CoordinateReferenceSystem from,
CoordinateReferenceSystem to) {
return transform(g, transform(from, to));
}
/**
* Transforms a geometry object using an explicit transformation.
*
* @param g The geometry.
* @param tx The transform to to apply.
*
* @return The transformed geometry.
*/
public static <T extends Geometry> T transform(T g, CoordinateTransform tx) {
return transform(g, tx, false);
}
/**
* Transforms a geometry object using an explicit transformation with the option to do an
* in place transformation.
* <p>
* When <tt>inPlace</tt> is set to true the geometry coordinates will be modified directly. When
* set to false a clone of the geometry is made and modified. Since cloning a geometry can be a
* very expensive operation doing an in place transform can be much more efficient but has the
* downside of modifying the geometry directly.
* </p>
*
* @param g The geometry.
* @param tx The transform to to apply.
*
* @return The transformed geometry.
*/
public static <T extends Geometry> T transform(T g, CoordinateTransform tx, boolean inPlace) {
if (tx instanceof IdentityCoordinateTransform) {
return g;
}
T h = inPlace ? g : (T) g.clone();
h.apply((CoordinateSequenceFilter) new CoordinateTransformer(tx));
return h;
}
/**
* Reprojects an envelope between two coordinate reference systems.
* <p>
* This method is convenience for:
* <pre><code>
* reproject(e, crs(from), crs(to));
* </code></pre>
* </p>
*
* @param e The envelope to reproject.
* @param from The source crs, as defined by {@link #crs(String)}
* @param to The target crs, as defined by {@link #crs(String)}
*
* @return The reprojected envelope.
*
* @see {@link #crs(String)}
* @see {@link #reproject(Envelope, CoordinateReferenceSystem, CoordinateReferenceSystem)}
*
*/
public static Bounds reproject(Envelope e, String from, String to) {
return reproject(e, crs(from), crs(to));
}
/**
* Reprojects an envelope between two coordinate reference systems.
* <p>
* In the event a transformation between the two crs objects can not be found this method throws
* {@link IllegalArgumentException}.
*
* In the event the two specified coordinate reference systems are equal this method is a
* no-op and returns the original envelope.
* </p>
* @param e The envelope to reproject.
* @param from The source coordinate reference system.
* @param to The target coordinate reference system.
*
* @return The reprojected envelope.
*
* @throws IllegalArgumentException If no coordinate transform can be found.
*/
public static Bounds reproject(Envelope e, CoordinateReferenceSystem from,
CoordinateReferenceSystem to) {
CoordinateTransform tx = transform(from, to);
if (tx instanceof IdentityCoordinateTransform) {
return new Bounds(e);
}
CoordinateTransformer txr = new CoordinateTransformer(tx);
Coordinate c1 = new Coordinate(e.getMinX(), e.getMinY());
Coordinate c2 = new Coordinate(e.getMaxX(), e.getMaxY());
txr.filter(c1);
txr.filter(c2);
return new Bounds(c1.x, c2.x, c1.y, c2.y);
}
public static CoordinateTransform transform(CoordinateReferenceSystem from,
CoordinateReferenceSystem to) {
if (from.equals(to)) {
return new IdentityCoordinateTransform();
}
CoordinateTransform tx = txFactory.createTransform(from, to);
if (tx == null) {
throw new IllegalArgumentException("Unable to find transform from " + from + " to " + to);
}
return tx;
}
private static class IdentityCoordinateTransform implements CoordinateTransform {
@Override
public CoordinateReferenceSystem getSourceCRS() {
return null;
}
@Override
public CoordinateReferenceSystem getTargetCRS() {
return null;
}
@Override
public ProjCoordinate transform(ProjCoordinate src, ProjCoordinate tgt) throws Proj4jException {
return src;
}
}
/**
* Returns the projection string for the specified CRS.
*/
public static String toString(CoordinateReferenceSystem crs) {
return crs.getParameterString();
}
/**
* Creates a crs from Well Known Text.
*
* @param wkt WKT representation of a CRS.
*/
public static CoordinateReferenceSystem fromWKT(String wkt) {
try {
return new ProjWKTParser().parse(wkt);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* Encodes a crs as Well Known Text.
*
* @param crs The coordinate reference system.
* @param format Whether to format the encoded result.
*
*/
public static String toWKT(CoordinateReferenceSystem crs, boolean format) {
return new ProjWKTEncoder().encode(crs, format);
}
/**
* Determines if two crs objects are equal.
* <p>
* Two crs objects are considered equal if {@link Datum#isEqual(Datum)} returns <tt>true</tt>
* and the two {@link CoordinateReferenceSystem#getProjection()} instances are the same type.
* </p>
*
*/
public static boolean equal(CoordinateReferenceSystem crs1, CoordinateReferenceSystem crs2) {
if (crs1 == crs2) {
return true;
}
if (crs1 == null || crs2 == null) {
return false;
}
Datum dat1 = crs1.getDatum();
Datum dat2 = crs2.getDatum();
if (!dat1.isEqual(dat2)) {
return false;
}
Projection p1 = crs1.getProjection();
Projection p2 = crs2.getProjection();
// TODO: do a better job of this
return p1.getClass().equals(p2.getClass());
}
}