All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.geotools.renderer.crs.ProjectionHandler Maven / Gradle / Ivy

Go to download

The main module contains the GeoTools public interfaces that are used by other GeoTools modules (and GeoTools applications). Where possible we make use industry standard terms as provided by OGC and ISO standards. The formal GeoTools public api consists of gt-metadata, jts and the gt-main module. The main module contains the default implementations that are available provided to other GeoTools modules using our factory system. Factories are obtained from an appropriate FactoryFinder, giving applications a chance configure the factory used using the Factory Hints facilities. FilterFactory ff = CommonFactoryFinder.getFilterFactory(); Expression expr = ff.add( expression1, expression2 ); If you find yourself using implementation specific classes chances are you doing it wrong: Expression expr = new AddImpl( expression1, expressiom2 );

There is a newer version: 24.2-oss84-1
Show newest version
/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2002-2015, 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.renderer.crs;

import static org.geotools.referencing.crs.DefaultGeographicCRS.WGS84;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.transform.ConcatenatedTransform;
import org.geotools.referencing.operation.transform.GeocentricTransform;
import org.locationtech.jts.densify.Densifier;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
import org.locationtech.jts.precision.EnhancedPrecisionOp;
import org.locationtech.jts.precision.GeometryPrecisionReducer;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;

/**
 * A class that can perform transformations on geometries to handle the singularity of the rendering
 * CRS, deal with geometries that are crossing the dateline, and eventually wrap them around to
 * produce a seamless continuous map effect.
 *
 * 

This basic implementation will cut the geometries that get outside of the area of validity of * the projection (as provided by the constructor) * *

WARNING: this API is not finalized and is meant to be used by StreamingRenderer only * * @author Andrea Aime - OpenGeo */ public class ProjectionHandler { public static final String ADVANCED_PROJECTION_DENSIFY = "advancedProjectionDensify"; protected static final double EPS = 1e-6; protected static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(ProjectionHandler.class); protected ReferencedEnvelope renderingEnvelope; protected final ReferencedEnvelope validAreaBounds; protected final Geometry validArea; protected final PreparedGeometry validaAreaTester; protected final CoordinateReferenceSystem sourceCRS; protected final CoordinateReferenceSystem targetCRS; protected double datelineX = Double.NaN; protected double targetHalfCircle = Double.NaN; protected boolean queryAcrossDateline; protected SingleCRS geometryCRS; protected boolean noReprojection; protected double densify = 0.0; Map projectionParameters; /** * Initializes a projection handler * * @param sourceCRS The source CRS * @param validAreaBounds The valid area (used to cut geometries that go beyond it) * @param renderingEnvelope The target rendering area and target CRS */ public ProjectionHandler( CoordinateReferenceSystem sourceCRS, Envelope validAreaBounds, ReferencedEnvelope renderingEnvelope) throws FactoryException { this.renderingEnvelope = renderingEnvelope; this.sourceCRS = CRS.getHorizontalCRS(sourceCRS); this.targetCRS = renderingEnvelope.getCoordinateReferenceSystem(); this.validAreaBounds = validAreaBounds != null ? new ReferencedEnvelope(validAreaBounds, DefaultGeographicCRS.WGS84) : null; this.validArea = null; this.validaAreaTester = null; // query across dateline only in case of reprojection, Oracle won't use the spatial index // with two or-ed bboxes and fixing the issue at the store level requires more // time/resources than we presently have this.queryAcrossDateline = !CRS.equalsIgnoreMetadata( sourceCRS, renderingEnvelope.getCoordinateReferenceSystem()); checkReprojection(); } /** * Initializes a projection handler * * @param sourceCRS The source CRS * @param validArea The valid area (used to cut geometries that go beyond it) * @param renderingEnvelope The target rendering area and target CRS */ public ProjectionHandler( CoordinateReferenceSystem sourceCRS, Geometry validArea, ReferencedEnvelope renderingEnvelope) throws FactoryException { if (validArea.isRectangle()) { this.renderingEnvelope = renderingEnvelope; this.sourceCRS = sourceCRS; this.targetCRS = renderingEnvelope.getCoordinateReferenceSystem(); this.validAreaBounds = new ReferencedEnvelope( validArea.getEnvelopeInternal(), DefaultGeographicCRS.WGS84); this.validArea = null; this.validaAreaTester = null; } else { this.renderingEnvelope = renderingEnvelope; this.sourceCRS = sourceCRS; this.targetCRS = renderingEnvelope.getCoordinateReferenceSystem(); this.validAreaBounds = new ReferencedEnvelope( validArea.getEnvelopeInternal(), DefaultGeographicCRS.WGS84); this.validArea = validArea; this.validaAreaTester = PreparedGeometryFactory.prepare(validArea); } checkReprojection(); } /** * Set one of the supported projection parameters: - advancedProjectionDensify (double) if > 0 * enables densification on preprocessing with the given distance between points. */ public void setProjectionParameters(Map projectionParameters) { if (projectionParameters.containsKey(ADVANCED_PROJECTION_DENSIFY)) { densify = (Double) projectionParameters.get(ADVANCED_PROJECTION_DENSIFY); } this.projectionParameters = projectionParameters; } private void checkReprojection() throws FactoryException { geometryCRS = CRS.getHorizontalCRS(sourceCRS); CoordinateReferenceSystem renderingCRS = renderingEnvelope.getCoordinateReferenceSystem(); try { noReprojection = !CRS.isTransformationRequired(geometryCRS, renderingCRS); } catch (Exception e) { LOGGER.log( Level.FINE, "Failed to determine is reprojection is required, assumming it is", e); noReprojection = false; } } /** Returns the current rendering envelope */ public ReferencedEnvelope getRenderingEnvelope() { return renderingEnvelope; } public CoordinateReferenceSystem getSourceCRS() { return this.sourceCRS; } /** * Returns a set of envelopes that will be used to query the data given the specified rendering * envelope and the current query envelope */ public List getQueryEnvelopes() throws TransformException, FactoryException { CoordinateReferenceSystem renderingCRS = renderingEnvelope.getCoordinateReferenceSystem(); if (!queryAcrossDateline) { ReferencedEnvelope envelope = transformEnvelope(renderingEnvelope, sourceCRS); if (envelope == null) return Collections.emptyList(); return Collections.singletonList(envelope); } if (renderingCRS instanceof GeographicCRS && !CRS.equalsIgnoreMetadata(renderingCRS, WGS84)) { // special case, if we just transform the coordinates are going to be wrapped by the // referencing // subsystem directly ReferencedEnvelope re = renderingEnvelope; List envelopes = new ArrayList(); addTransformedEnvelope(re, envelopes); if (CRS.getAxisOrder(renderingCRS) == CRS.AxisOrder.NORTH_EAST) { if (re.getMinY() >= -180.0 && re.getMaxY() <= 180) { return envelopes; } // We need to split reprojected envelope and normalize it. To be lenient with // situations in which the data is just broken (people saying 4326 just because they // have no idea at all) we don't actually split, but add elements adjustEnvelope(re, envelopes, true); } else { if (re.getMinX() >= -180.0 && re.getMaxX() <= 180) { ReferencedEnvelope envelope = transformEnvelope(renderingEnvelope, sourceCRS); if (envelope == null) return Collections.emptyList(); return Collections.singletonList(envelope); } // We need to split reprojected envelope and normalize it. To be lenient with // situations in which the data is just broken (people saying 4326 just because they // have no idea at all) we don't actually split, but add elements adjustEnvelope(re, envelopes, true); } mergeEnvelopes(envelopes); return envelopes; } else { if (!Double.isNaN(datelineX) && renderingEnvelope.getMinX() < datelineX && renderingEnvelope.getMaxX() > datelineX && renderingEnvelope.getWidth() < targetHalfCircle) { double minX = renderingEnvelope.getMinX(); double minY = renderingEnvelope.getMinY(); double maxX = renderingEnvelope.getMaxX(); double maxY = renderingEnvelope.getMaxY(); ReferencedEnvelope re1 = new ReferencedEnvelope(minX, datelineX - EPS, minY, maxY, renderingCRS); List result = new ArrayList(); ReferencedEnvelope tx1 = transformEnvelope(re1, WGS84); if (tx1 != null) { tx1.expandToInclude(180, tx1.getMinY()); addTransformedEnvelope(tx1, result); } ReferencedEnvelope re2 = new ReferencedEnvelope(datelineX + EPS, maxX, minY, maxY, renderingCRS); ReferencedEnvelope tx2 = transformEnvelope(re2, WGS84); if (tx2 != null) { if (tx2.getMinX() > 180) { tx2.translate(-360, 0); } tx2.expandToInclude(-180, tx1.getMinY()); addTransformedEnvelope(tx2, result); } mergeEnvelopes(result); return result; } else { return getSourceEnvelopes(renderingEnvelope); } } } private void addTransformedEnvelope(ReferencedEnvelope re, List envelopes) throws TransformException, FactoryException { ReferencedEnvelope transformed = transformEnvelope(re, sourceCRS); if (transformed != null) { envelopes.add(transformed); } } protected List getSourceEnvelopes(ReferencedEnvelope renderingEnvelope) throws TransformException, FactoryException { // check if we are crossing the dateline ReferencedEnvelope re = transformEnvelope(renderingEnvelope, WGS84); if (re == null) { return Collections.emptyList(); } if (re.getMinX() >= -180.0 && re.getMaxX() <= 180) { final ReferencedEnvelope result = transformEnvelope(renderingEnvelope, sourceCRS); if (result != null) { return Collections.singletonList(result); } else { return Collections.emptyList(); } } // We need to split reprojected envelope and normalize it. To be lenient with // situations in which the data is just broken (people saying 4326 just because they // have no idea at all) we don't actually split, but add elements List envelopes = new ArrayList(); envelopes.add(re); adjustEnvelope(re, envelopes, false); mergeEnvelopes(envelopes); reprojectEnvelopes(sourceCRS, envelopes); return envelopes.stream().filter(e -> e != null).collect(Collectors.toList()); } /** * Adjust the envelope by taking into account dateline wrapping as well as multiple spans of the * whole world extent. When transform flag is true, the envelopes will be transformed before * being returned */ private void adjustEnvelope( ReferencedEnvelope re, List envelopes, boolean transform) throws TransformException, FactoryException { CoordinateReferenceSystem crs = re.getCoordinateReferenceSystem(); boolean isLatLon = CRS.getAxisOrder(crs) == CRS.AxisOrder.NORTH_EAST; double minX = isLatLon ? re.getMinY() : re.getMinX(); double maxX = isLatLon ? re.getMaxY() : re.getMaxX(); double minY = isLatLon ? re.getMinX() : re.getMinY(); double maxY = isLatLon ? re.getMaxX() : re.getMaxY(); double extent = maxX - minX; List envelopesToBeAdded = new ArrayList<>(); if (extent > 360) { // at least one whole world use case -> requested data covers the full world: // let's set a -180,180 bbox. // the wrapping projectionHandler and the gridCoverageReaders // will do proper clones / intersections afterwards minX = -180; maxX = 180; // Create a whole world envelope taking into account latLon/lonLat ReferencedEnvelope envelope = new ReferencedEnvelope( isLatLon ? minY : minX, isLatLon ? maxY : maxX, isLatLon ? minX : minY, isLatLon ? maxX : maxY, crs); envelopesToBeAdded.add(envelope); } else { // Note that the extent won't be > 360 at this point // let's do some adjustments to "shift" the request around -180, 180 interval: // we basically add or subtract 360° N times // 1) let's count how many halfCircles (a 180° span) we are away from the zero // Using half circles allow to understand if there is a dateline cross // 2) add/subtract 360° N times to move forward/backward the request, also // keeping into account whether we are crossing the dateline or not, // by using (halfCircles % 2). // An odd number of halfCircles means dateline crossing. // An even number of halfCircles means no dateline crossing. // i.e. minX = 371 -> halfCircles = 2 -> no dateline crossing (371° = 11°) // i.e. minX = 908 -> halfCircles = 5 -> dateline crossing (908° = 188°) // 3) add/subtract the original extent to get the other value of the interval // in order to move the whole window (Note that the extent won't be > 360° // since we are inside the "else") int halfCircles = 0; if (minX < -180) { halfCircles = (int) ((Math.abs(minX) / 180)); minX += (360 * ((halfCircles / 2) + (halfCircles % 2))); maxX = minX + extent; } else if (minX > 180) { halfCircles = (int) (minX / 180); minX -= (360 * ((halfCircles / 2) + (halfCircles % 2))); maxX = minX + extent; } else if (maxX < -180) { halfCircles = (int) (Math.abs(maxX) / 180); maxX += (360 * ((halfCircles / 2) + (halfCircles % 2))); minX = maxX - extent; } else if (maxX > 180) { halfCircles = (int) (Math.abs(maxX) / 180); maxX -= (360 * ((halfCircles / 2) + (halfCircles % 2))); minX = maxX - extent; } if ((int) (minX / 180) < (int) (maxX / 180)) { // Dateline crossing check. // Examples of [min, max] and how this IF will work // a case like [-91, 91] will be 0 < 0 -> False: no Dateline cross // a case like [-1, 181] will be 0 < 1 -> True: Dateline cross. // a case like [-181, 1] will be -1 < 0 -> True: Dateline cross. // Need to use 2 separate envelopes when crossing the dateline. // Let's prepare 8 coordinates, 4 for each side of the dateline // (left and right), keeping also into account latLon vs lonLat double coord1L, coord2L, coord3L, coord4L; double coord1R, coord2R, coord3R, coord4R; if (minX < -180) { coord1L = isLatLon ? minY : -180; coord2L = isLatLon ? maxY : Math.min(maxX, 180); coord3L = isLatLon ? -180 : minY; coord4L = isLatLon ? Math.min(maxX, 180) : maxY; coord1R = isLatLon ? minY : minX + 360; coord2R = isLatLon ? maxY : 180; coord3R = isLatLon ? minX + 360 : minY; coord4R = isLatLon ? 180 : maxY; } else { // maxX will be greater than 180 since we crossed the dateline // so we need to put it back of a -360 factor coord1L = isLatLon ? minY : -180; coord2L = isLatLon ? maxY : maxX - 360; coord3L = isLatLon ? -180 : minY; coord4L = isLatLon ? maxX - 360 : maxY; coord1R = isLatLon ? minY : minX; coord2R = isLatLon ? maxY : 180; coord3R = isLatLon ? minX : minY; coord4R = isLatLon ? 180 : maxY; } envelopesToBeAdded.add( new ReferencedEnvelope(coord1L, coord2L, coord3L, coord4L, crs)); envelopesToBeAdded.add( new ReferencedEnvelope(coord1R, coord2R, coord3R, coord4R, crs)); } else { // No dateline has been crossed. One envelope would be enough envelopesToBeAdded.add( new ReferencedEnvelope( isLatLon ? minY : minX, isLatLon ? maxY : maxX, isLatLon ? minX : minY, isLatLon ? maxX : maxY, crs)); } } for (ReferencedEnvelope env : envelopesToBeAdded) { if (transform) { addTransformedEnvelope(env, envelopes); } else { envelopes.add(env); } } } /** * Reprojects the given envelope to the target CRS, taking into account the ProjectionHandler * constraints (valid area bounds, etc.). * * @param envelope envelope to reproject * @param targetCRS target CRS * @return reprojected envelope */ public ReferencedEnvelope getProjectedEnvelope( ReferencedEnvelope envelope, CoordinateReferenceSystem targetCRS) throws TransformException, FactoryException { return transformEnvelope(envelope, targetCRS); } protected ReferencedEnvelope transformEnvelope( ReferencedEnvelope envelope, CoordinateReferenceSystem targetCRS) throws TransformException, FactoryException { if (CRS.equalsIgnoreMetadata(envelope.getCoordinateReferenceSystem(), targetCRS)) { return envelope; } try { if (validAreaBounds != null) { ReferencedEnvelope validAreaInTargetCRS = validAreaBounds.transform(envelope.getCoordinateReferenceSystem(), true); envelope = envelope.intersection(validAreaInTargetCRS); if (envelope.isEmpty()) { return null; } } ReferencedEnvelope transformed = envelope.transform(targetCRS, true, 10); ProjectionHandler handler = ProjectionHandlerFinder.getHandler( new ReferencedEnvelope(targetCRS), DefaultGeographicCRS.WGS84, true, projectionParameters); // does the target CRS have a strict notion of what's possible in terms of // valid coordinate ranges? if (handler == null || handler instanceof WrappingProjectionHandler) { return transformed; } // if so, cut final ReferencedEnvelope validAreaBounds = handler.getValidAreaBounds(); ReferencedEnvelope validArea = validAreaBounds.transform(targetCRS, true); ReferencedEnvelope reduced = transformed.intersection(validArea); if (reduced.isNull()) { return null; } else { return reduced; } } catch (Exception e) { LOGGER.fine( "Failed to reproject the envelope " + envelope + " to " + targetCRS + " trying an area restriction"); ReferencedEnvelope envWGS84 = envelope.transform(DefaultGeographicCRS.WGS84, true); // do we have restrictions on the target CRS? ProjectionHandler handler = ProjectionHandlerFinder.getHandler( new ReferencedEnvelope(targetCRS), DefaultGeographicCRS.WGS84, false); if (handler != null && handler.validAreaBounds != null) { ReferencedEnvelope validAreaBounds = handler.validAreaBounds; envWGS84 = envWGS84.intersection(validAreaBounds); } // let's see if we can restrict the area we're reprojecting back using a projection // handler for the source CRS handler = ProjectionHandlerFinder.getHandler( envelope, envelope.getCoordinateReferenceSystem(), false); if (handler != null && handler.validAreaBounds != null) { ReferencedEnvelope validAreaBounds = handler.validAreaBounds; envWGS84 = envWGS84.intersection(validAreaBounds); } // try to reproject if (envWGS84.isNull()) { return null; } else { try { return ReferencedEnvelope.reference(envWGS84).transform(targetCRS, true); } catch (Exception e2) { LOGGER.fine( "Failed to reproject the restricted envelope " + envWGS84 + " to " + targetCRS); } } // ok, let's see if we have an area of validity then GeographicBoundingBox bbox = CRS.getGeographicBoundingBox(targetCRS); if (bbox != null) { ReferencedEnvelope restriction = new ReferencedEnvelope( bbox.getEastBoundLongitude(), bbox.getWestBoundLongitude(), bbox.getSouthBoundLatitude(), bbox.getNorthBoundLatitude(), DefaultGeographicCRS.WGS84); Envelope intersection = envWGS84.intersection(restriction); if (intersection.isNull()) { return null; } else { try { return ReferencedEnvelope.reference(intersection) .transform(targetCRS, true); } catch (Exception e2) { LOGGER.fine( "Failed to reproject the restricted envelope " + intersection + " to " + targetCRS); } } } throw new TransformException( "All attemptsto reproject the envelope " + envelope + " to " + targetCRS + " failed"); } } protected void reprojectEnvelopes( CoordinateReferenceSystem queryCRS, List envelopes) throws TransformException, FactoryException { // reproject the surviving envelopes for (int i = 0; i < envelopes.size(); i++) { final ReferencedEnvelope envelope = transformEnvelope(envelopes.get(i), queryCRS); if (envelope != null) { envelopes.set(i, envelope); } } } protected void mergeEnvelopes(List envelopes) { // the envelopes generated might overlap, check and merge if necessary, we // don't want the data backend to deal with ORs against the spatial index // unless necessary boolean merged = true; while (merged && envelopes.size() > 1) { merged = false; for (int i = 0; i < envelopes.size() - 1; i++) { ReferencedEnvelope curr = envelopes.get(i); for (int j = i + 1; j < envelopes.size(); ) { ReferencedEnvelope next = envelopes.get(j); if (curr.intersects((Envelope) next)) { curr.expandToInclude(next); envelopes.remove(j); merged = true; } else { j++; } } } } } /** Returns true if the geometry needs special handling */ public boolean requiresProcessing(Geometry geometry) { // if there is no valid area, no cutting is required if (validAreaBounds == null) return false; // if not reprojection is going on, we don't need to cut if (noReprojection) { return false; } return true; } /** * Pre processes the geometry, e.g. cuts it, splits it, etc. in its native srs. May return null * if the geometry is not to be drawn */ public Geometry preProcess(Geometry geometry) throws TransformException, FactoryException { // if there is no valid area, no cutting is required either if (validAreaBounds == null) return densify(geometry); // if not reprojection is going on, we don't need to cut if (noReprojection) { return densify(geometry); } Geometry mask; // fast path for the rectangular case, more complex one for the // non rectangular one ReferencedEnvelope ge = new ReferencedEnvelope(geometry.getEnvelopeInternal(), geometryCRS); ReferencedEnvelope geWGS84 = ge.transform(WGS84, true); // if the size of the envelope is less than 1 meter (1e-6 in degrees) expand it a bit // to make intersection tests work geWGS84.expandBy(EPS); if (validArea == null) { // if the geometry is within the valid area for this projection // just skip expensive cutting if (validAreaBounds.contains((Envelope) geWGS84)) { return densify(geometry); } // we need to cut, first thing, we intersect the geometry envelope // and the valid area in WGS84, which is a neutral, everything can // be turned into it, and then turn back the intersection into // the origin SRS ReferencedEnvelope envIntWgs84 = new ReferencedEnvelope(validAreaBounds.intersection(geWGS84), WGS84); // if the intersection is empty the geometry is completely outside of the valid area, // skip it if (envIntWgs84.getHeight() <= 0 || envIntWgs84.getWidth() <= 0) { // valid area is crossing dateline? if (validAreaBounds.contains( 180, (validAreaBounds.getMinY() + validAreaBounds.getMaxY()) / 2)) { ReferencedEnvelope translated = new ReferencedEnvelope(validAreaBounds); translated.translate(-360, 0); if (translated.contains((Envelope) geWGS84)) { return densify(geometry); } envIntWgs84 = translated.intersection(geWGS84); } else if (validAreaBounds.contains( -180, (validAreaBounds.getMinY() + validAreaBounds.getMaxY()) / 2)) { ReferencedEnvelope translated = new ReferencedEnvelope(validAreaBounds); translated.translate(360, 0); if (translated.contains((Envelope) geWGS84)) { return densify(geometry); } envIntWgs84 = translated.intersection(geWGS84); } if (envIntWgs84.getHeight() <= 0 || envIntWgs84.getWidth() <= 0) { return null; } } ReferencedEnvelope envInt = envIntWgs84.transform(geometryCRS, true); mask = JTS.toGeometry((Envelope) envInt); } else { // if the geometry is within the valid area for this projection // just skip expensive cutting if (validaAreaTester.contains(JTS.toGeometry(geWGS84))) { return densify(geometry); } // we need to cut, first thing, we intersect the geometry envelope // and the valid area in WGS84, which is a neutral, everything can // be turned into it, and then turn back the intersection into // the origin SRS ReferencedEnvelope envIntWgs84 = new ReferencedEnvelope(validAreaBounds.intersection(geWGS84), WGS84); // if the intersection is empty the geometry is completely outside of the valid area, // skip it if (envIntWgs84.isEmpty()) { return null; } Polygon polyIntWgs84 = JTS.toGeometry(envIntWgs84); Geometry maskWgs84 = intersect(validArea, polyIntWgs84, geometryCRS); if (maskWgs84 == null || maskWgs84.isEmpty()) { return null; } mask = JTS.transform(maskWgs84, CRS.findMathTransform(WGS84, geometryCRS)); } return densify(intersect(geometry, mask, geometryCRS)); } /** * Densifies the given geometry using the current densification configuration. * *

It returns the original geometry if densification is not enabled. */ protected Geometry densify(Geometry geometry) { if (geometry != null && densify > 0.0) { try { geometry = Densifier.densify(geometry, densify); } catch (Throwable t) { LOGGER.warning("Cannot densify geometry"); } } return geometry; } protected Geometry intersect( Geometry geometry, Geometry mask, CoordinateReferenceSystem geometryCRS) { // this seems to cause issues to JTS, reduce to // single geometry when possible (http://jira.codehaus.org/browse/GEOS-6570) if (geometry instanceof GeometryCollection) { int numGeometries = geometry.getNumGeometries(); if (numGeometries == 1) { geometry = geometry.getGeometryN(0); } else { // go piecewise, the JTS intersection can be pretty fragile in these cases // and take a lot of time List elements = new ArrayList<>(); String geometryType = numGeometries > 0 ? geometry.getGeometryN(0).getGeometryType() : null; for (int i = 0; i < numGeometries; i++) { Geometry g = geometry.getGeometryN(i); if (g.getEnvelopeInternal().intersects(mask.getEnvelopeInternal())) { Geometry intersected = intersect(g, mask, geometryCRS); if (intersected != null) { if (intersected.getGeometryType().equals(geometryType)) { elements.add(intersected); } else if (intersected instanceof GeometryCollection) { addGeometries( elements, (GeometryCollection) intersected, geometryType); } } } } if (elements.size() == 0) { return null; } if (geometry instanceof MultiPoint) { Point[] array = elements.toArray(new Point[elements.size()]); return geometry.getFactory().createMultiPoint(array); } else if (geometry instanceof MultiLineString) { LineString[] array = elements.toArray(new LineString[elements.size()]); return geometry.getFactory().createMultiLineString(array); } else if (geometry instanceof MultiPolygon) { Polygon[] array = elements.toArray(new Polygon[elements.size()]); return geometry.getFactory().createMultiPolygon(array); } else { Geometry[] array = elements.toArray(new Geometry[elements.size()]); return geometry.getFactory().createGeometryCollection(array); } } } Geometry result = null; try { result = geometry.intersection(mask); } catch (Exception e1) { // try a precision reduction approach, starting from mm and scaling up to km double precision; if (CRS.getProjectedCRS(geometryCRS) != null) { precision = 1e-3; } else { precision = 1e-3 / 100000; // 1 degree roughly 100km } // from mm to km for (int i = 0; i < 6; i++) { GeometryPrecisionReducer reducer = new GeometryPrecisionReducer(new PrecisionModel(1 / precision)); Geometry reduced = reducer.reduce(geometry); try { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log( Level.FINE, "Failed to intersect the geometry with the projection area of " + "validity mask, trying a precision reduction approach with a precision of " + precision); } result = reduced.intersection(mask); break; } catch (Exception e3) { precision *= 10; } } if (result == null) { LOGGER.log( Level.WARNING, "Failed to intersect the geometry with the projection area of " + "validity mask, returning the original geometry: " + geometry); result = geometry; } } // workaround for a JTS bug, sometimes it returns empty results // even if the two geometries are indeed intersecting if (result.isEmpty() && geometry.intersects(mask)) { try { result = EnhancedPrecisionOp.intersection(geometry, mask); } catch (Exception e2) { result = geometry; } } // clean up lower dimensional elements GeometryDimensionCollector collector = new GeometryDimensionCollector(geometry.getDimension()); result.apply(collector); result = collector.collect(); // handle in special way empty intersections if (result == null || result.isEmpty()) { return null; } else { return result; } } /** Can modify/wrap the transform to handle specific projection issues */ public MathTransform getRenderingTransform(MathTransform mt) throws FactoryException { List elements = new ArrayList(); accumulateTransforms(mt, elements); List wrapped = new ArrayList(); List datumShiftChain = null; boolean datumShiftDetected = false; for (MathTransform element : elements) { if (datumShiftChain != null) { datumShiftChain.add(element); if (element.getClass() .getName() .equals(GeocentricTransform.class.getName() + "$Inverse")) { datumShiftDetected = true; MathTransform combined = concatenateTransforms(datumShiftChain); GeographicOffsetWrapper wrapper = new GeographicOffsetWrapper(combined); wrapped.add(wrapper); datumShiftChain = null; } } else if (element instanceof GeocentricTransform) { datumShiftChain = new ArrayList(); datumShiftChain.add(element); } else { wrapped.add(element); } } if (datumShiftDetected) { if (datumShiftChain != null) { wrapped.addAll(datumShiftChain); } return concatenateTransforms(wrapped); } else { return mt; } } protected MathTransform concatenateTransforms(List datumShiftChain) { if (datumShiftChain.size() == 1) { return datumShiftChain.get(0); } else { MathTransform mt = ConcatenatedTransform.create(datumShiftChain.get(0), datumShiftChain.get(1)); for (int i = 2; i < datumShiftChain.size(); i++) { MathTransform curr = datumShiftChain.get(i); mt = ConcatenatedTransform.create(mt, curr); } return mt; } } protected void accumulateTransforms(MathTransform mt, List elements) { if (mt instanceof ConcatenatedTransform) { ConcatenatedTransform ct = (ConcatenatedTransform) mt; accumulateTransforms(ct.transform1, elements); accumulateTransforms(ct.transform2, elements); } else { elements.add(mt); } } /** * Processes the geometry already projected to the target SRS. May return null if the geometry * is not to be drawn. * * @param mt optional reverse transformation to facilitate unwrapping */ public Geometry postProcess(MathTransform mt, Geometry geometry) { return geometry; } /** * Returns the area where the transformation from source to target is valid, expressed in the * source coordinate reference system, or null if there is no limit */ public ReferencedEnvelope getValidAreaBounds() { return validAreaBounds; } protected void setCentralMeridian(double centralMeridian) { // compute the earth half circle in target CRS coordinates try { CoordinateReferenceSystem targetCRS = renderingEnvelope.getCoordinateReferenceSystem(); MathTransform mt = CRS.findMathTransform(WGS84, targetCRS, true); double[] src = new double[] {centralMeridian, 0, 180 + centralMeridian, 0}; double[] dst = new double[4]; mt.transform(src, 0, dst, 0, 2); if (CRS.getAxisOrder(targetCRS) == CRS.AxisOrder.NORTH_EAST) { targetHalfCircle = Math.abs(dst[3] - dst[1]); } else { targetHalfCircle = Math.abs(dst[2] - dst[0]); } if (targetHalfCircle <= 0) { throw new RuntimeException("Computed Earth half circle is 0, what is going on?"); } } catch (Exception e) { throw new RuntimeException( "Unexpected error computing the Earth half circle in the current projection", e); } computeDatelineX(); } protected void computeDatelineX() { // compute the x of the dateline in the rendering CRS try { double[] ordinates = new double[] {180, -80, 180, 80}; MathTransform mt = CRS.findMathTransform( DefaultGeographicCRS.WGS84, renderingEnvelope.getCoordinateReferenceSystem()); mt.transform(ordinates, 0, ordinates, 0, 2); datelineX = ordinates[0]; } catch (Exception e) { // should never happen... throw new RuntimeException(e); } } /** * Private method for adding to the input List only the {@link Geometry} objects of the input * {@link GeometryCollection} which belongs to the defined geometryType */ protected void addGeometries( List geoms, GeometryCollection collection, String geometryType) { // Check if the list exists if (geoms == null) { return; } // Check the Geometry type if (geometryType == null || geometryType.isEmpty()) { return; } // Check the collection if (collection == null || collection.getNumGeometries() <= 0) { return; } // Get the number of Geometries int numGeometries = collection.getNumGeometries(); // Cycle on the Geometries for (int i = 0; i < numGeometries; i++) { // get the Geometry Geometry geo = collection.getGeometryN(i); // If it belongs to the correct Geometry type, it is added to the Liats if (geo.getGeometryType().equals(geometryType)) { geoms.add(geo); // Otherwise if it is a collection we try to iterate on it (recursion) } else if (geo instanceof GeometryCollection) { addGeometries(geoms, (GeometryCollection) geo, geometryType); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy