| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.sis.metadata.iso.extent; |
| |
| import java.util.Date; |
| import java.util.List; |
| import java.util.ArrayList; |
| import javax.measure.Unit; |
| import org.opengis.geometry.Envelope; |
| import org.opengis.geometry.DirectPosition; |
| import org.opengis.temporal.TemporalPrimitive; |
| import org.opengis.metadata.extent.Extent; |
| import org.opengis.metadata.extent.VerticalExtent; |
| import org.opengis.metadata.extent.TemporalExtent; |
| import org.opengis.metadata.extent.BoundingPolygon; |
| import org.opengis.metadata.extent.GeographicExtent; |
| import org.opengis.metadata.extent.GeographicBoundingBox; |
| import org.opengis.metadata.extent.GeographicDescription; |
| import org.opengis.referencing.cs.CoordinateSystemAxis; |
| import org.opengis.referencing.cs.AxisDirection; |
| import org.opengis.referencing.crs.VerticalCRS; |
| import org.opengis.referencing.crs.GeographicCRS; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.datum.VerticalDatum; |
| import org.opengis.referencing.datum.VerticalDatumType; |
| import org.opengis.referencing.operation.TransformException; |
| import org.apache.sis.internal.metadata.ReferencingServices; |
| import org.apache.sis.metadata.InvalidMetadataException; |
| import org.apache.sis.measure.Longitude; |
| import org.apache.sis.measure.MeasurementRange; |
| import org.apache.sis.measure.Range; |
| import org.apache.sis.util.resources.Vocabulary; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.Static; |
| |
| import static java.lang.Math.*; |
| import static org.apache.sis.internal.metadata.ReferencingServices.AUTHALIC_RADIUS; |
| |
| |
| /** |
| * Convenience static methods for extracting information from {@link Extent} objects. |
| * This class provides methods for: |
| * |
| * <ul> |
| * <li>{@linkplain #getGeographicBoundingBox Fetching geographic}, |
| * {@linkplain #getVerticalRange vertical} or |
| * {@linkplain #getDate temporal components} in a convenient form.</li> |
| * <li>Computing {@linkplain #intersection intersection} of bounding boxes</li> |
| * <li>Computing {@linkplain #area area} estimations.</li> |
| * </ul> |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 0.8 |
| * |
| * @see org.apache.sis.geometry.Envelopes |
| * |
| * @since 0.3 |
| * @module |
| */ |
| public final class Extents extends Static { |
| /** |
| * Do no allow instantiation of this class. |
| */ |
| private Extents() { |
| } |
| |
| /** |
| * A geographic extent ranging from 180°W to 180°E and 90°S to 90°N. |
| * This extent has no vertical and no temporal components. |
| */ |
| public static final Extent WORLD; |
| static { |
| final DefaultGeographicBoundingBox box = new DefaultGeographicBoundingBox(-180, 180, -90, 90); |
| box.transitionTo(DefaultGeographicBoundingBox.State.FINAL); |
| final DefaultExtent world = new DefaultExtent( |
| Vocabulary.formatInternational(Vocabulary.Keys.World), box, null, null); |
| world.transitionTo(DefaultExtent.State.FINAL); |
| WORLD = world; |
| } |
| |
| /** |
| * Returns a single geographic bounding box from the specified extent. |
| * This method tries to find the bounding box in the cheapest way |
| * before to fallback on more expansive computations: |
| * |
| * <ol> |
| * <li>First, this method searches geographic elements that are instance of {@link GeographicBoundingBox}.<ul> |
| * <li>If exactly one such instance is found, then this method returns that instance directly (no copy).</li> |
| * <li>If more than one instance is found, then this method computes and returns the |
| * {@linkplain DefaultGeographicBoundingBox#add union} of all bounding boxes.</li> |
| * </ul></li> |
| * <li>If above step found no {@code GeographicBoundingBox}, then this method inspects geographic elements |
| * that are instance of {@link BoundingPolygon}, taking in account only the envelopes associated to a |
| * coordinate reference system of kind {@link GeographicCRS}. If such envelopes are found, then this |
| * method computes and returns their union.</li> |
| * <li>If above step found no polygon's envelope associated to a geographic CRS, then in last resort this |
| * method uses all polygon's envelopes regardless their coordinate reference system (provided that the |
| * CRS is not null), applying coordinate transformations if needed.</li> |
| * <li>If above step found no polygon's envelope, then this method returns {@code null}.</li> |
| * </ol> |
| * |
| * @param extent the extent to convert to a geographic bounding box, or {@code null}. |
| * @return a geographic bounding box extracted from the given extent, or {@code null} if none. |
| * |
| * @see org.apache.sis.referencing.CRS#getDomainOfValidity(CoordinateReferenceSystem) |
| */ |
| public static GeographicBoundingBox getGeographicBoundingBox(final Extent extent) { |
| GeographicBoundingBox bounds = null; |
| if (extent != null) { |
| DefaultGeographicBoundingBox modifiable = null; |
| final List<Envelope> fallbacks = new ArrayList<>(); |
| for (final GeographicExtent element : extent.getGeographicElements()) { |
| /* |
| * If a geographic bounding box can be obtained, add it to the previous boxes (if any). |
| * All exclusion boxes before the first inclusion box are ignored. |
| */ |
| if (element instanceof GeographicBoundingBox) { |
| final GeographicBoundingBox item = (GeographicBoundingBox) element; |
| if (bounds == null) { |
| /* |
| * We use DefaultGeographicBoundingBox.getInclusion(Boolean) below because |
| * add(…) method that we use cares about the case where inclusion is false. |
| */ |
| if (DefaultGeographicBoundingBox.getInclusion(item.getInclusion())) { |
| bounds = item; |
| } |
| } else { |
| if (modifiable == null) { |
| bounds = modifiable = new DefaultGeographicBoundingBox(bounds); |
| } |
| modifiable.add(item); |
| } |
| } |
| } |
| /* |
| * If we found not explicit GeographicBoundingBox element, use the information that we |
| * collected in BoundingPolygon elements. This may involve coordinate transformations. |
| */ |
| if (bounds == null) try { |
| for (final Envelope envelope : fallbacks) { |
| final DefaultGeographicBoundingBox item = new DefaultGeographicBoundingBox(); |
| item.setBounds(envelope); |
| if (bounds == null) { |
| bounds = item; |
| } else { |
| if (modifiable == null) { |
| bounds = modifiable = new DefaultGeographicBoundingBox(bounds); |
| } |
| modifiable.add(item); |
| } |
| } |
| } catch (TransformException e) { |
| throw new InvalidMetadataException(Errors.format(Errors.Keys.CanNotTransformEnvelope), e); |
| } |
| } |
| return bounds; |
| } |
| |
| /** |
| * Returns the union of chosen vertical ranges found in the given extent, or {@code null} if none. |
| * This method gives preference to heights above the Mean Sea Level when possible. |
| * Depths have negative height values: if the |
| * {@linkplain org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis#getDirection() axis direction} |
| * is toward down, then this method reverses the sign of minimum and maximum values. |
| * |
| * <h4>Multi-occurrences</h4> |
| * If the given {@code Extent} object contains more than one vertical extent, then this method |
| * performs a choice based on the vertical datum and the unit of measurement: |
| * |
| * <ul class="verbose"> |
| * <li><p><b>Choice based on vertical datum</b><br> |
| * Only the extents associated (indirectly, through their CRS) to the same non-null {@link VerticalDatumType} |
| * will be taken in account. If all datum types are null, then this method conservatively uses only the first |
| * vertical extent. Otherwise the datum type used for filtering the vertical extents is:</p> |
| * |
| * <ul> |
| * <li>{@link VerticalDatumType#GEOIDAL} or {@link VerticalDatumType#DEPTH DEPTH} if at least one extent |
| * uses those datum types. For this method, {@code DEPTH} is considered as equivalent to {@code GEOIDAL} |
| * except for the axis direction.</li> |
| * <li>Otherwise, the first non-null datum type found in iteration order.</li> |
| * </ul> |
| * |
| * <div class="note"><b>Rational:</b> like {@linkplain #getGeographicBoundingBox(Extent) geographic bounding box}, |
| * the vertical range is an approximated information; the range returned by this method does not carry any |
| * information about the vertical CRS and this method does not attempt to perform coordinate transformation. |
| * But this method is more useful if the returned ranges are close to a frequently used surface, like the |
| * Mean Sea Level. The same simplification is applied in the |
| * <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#31">{@code VerticalExtent} element of |
| * Well Known Text (WKT) format</a>, which specifies that <cite>“Vertical extent is an approximate description |
| * of location; heights are relative to an unspecified mean sea level.”</cite></div></li> |
| * |
| * <li><p><b>Choice based on units of measurement</b><br> |
| * If, after the choice based on the vertical datum described above, there is still more than one vertical |
| * extent to consider, then the next criterion checks for the units of measurement.</p> |
| * <ul> |
| * <li>If no range specify a unit of measurement, return the first range and ignore all others.</li> |
| * <li>Otherwise take the first range having a unit of measurement. Then:<ul> |
| * <li>All other ranges having an incompatible unit of measurement will be ignored.</li> |
| * <li>All other ranges having a compatible unit of measurement will be converted to |
| * the unit of the first retained range, and their union will be computed.</li> |
| * </ul></li> |
| * </ul> |
| * |
| * <div class="note"><b>Example:</b> |
| * Heights or depths are often measured using some pressure units, for example hectopascals (hPa). |
| * An {@code Extent} could contain two vertical elements: one with the height measurements in hPa, |
| * and the other element with heights transformed to metres using an empirical formula. |
| * In such case this method will select the first vertical element on the assumption that it is |
| * the "main" one that the metadata producer intended to show. Next, this method will search for |
| * other vertical elements using pressure unit. In our example there is none, but if such elements |
| * were found, this method would compute their union.</div></li> |
| * </ul> |
| * |
| * @param extent the extent to convert to a vertical measurement range, or {@code null}. |
| * @return a vertical measurement range created from the given extent, or {@code null} if none. |
| * |
| * @since 0.4 |
| */ |
| public static MeasurementRange<Double> getVerticalRange(final Extent extent) { |
| MeasurementRange<Double> range = null; |
| VerticalDatumType selectedType = null; |
| if (extent != null) { |
| for (final VerticalExtent element : extent.getVerticalElements()) { |
| double min = element.getMinimumValue(); |
| double max = element.getMaximumValue(); |
| final VerticalCRS crs = element.getVerticalCRS(); |
| VerticalDatumType type = null; |
| Unit<?> unit = null; |
| if (crs != null) { |
| final VerticalDatum datum = crs.getDatum(); |
| if (datum != null) { |
| type = datum.getVerticalDatumType(); |
| if (VerticalDatumType.DEPTH.equals(type)) { |
| type = VerticalDatumType.GEOIDAL; |
| } |
| } |
| final CoordinateSystemAxis axis = crs.getCoordinateSystem().getAxis(0); |
| unit = axis.getUnit(); |
| if (AxisDirection.DOWN.equals(axis.getDirection())) { |
| final double tmp = min; |
| min = -max; |
| max = -tmp; |
| } |
| } |
| if (range != null) { |
| /* |
| * If the new range does not specify any datum type or unit, then we do not know how to |
| * convert the values before to perform the union operation. Conservatively do nothing. |
| */ |
| if (type == null || unit == null) { |
| continue; |
| } |
| /* |
| * If the new range is not measured relative to the same kind of surface than the previous range, |
| * then we do not know how to combine those ranges. Do nothing, unless the new range is a Mean Sea |
| * Level Height in which case we forget all previous ranges and use the new one instead. |
| */ |
| if (!type.equals(selectedType)) { |
| if (!type.equals(VerticalDatumType.GEOIDAL)) { |
| continue; |
| } |
| } else if (selectedType != null) { |
| /* |
| * If previous range did not specify any unit, then unconditionally replace it by |
| * the new range since it provides more information. If both ranges specify units, |
| * then we will compute the union if we can, or ignore the new range otherwise. |
| */ |
| final Unit<?> previous = range.unit(); |
| if (previous != null) { |
| if (previous.isCompatible(unit)) { |
| range = (MeasurementRange<Double>) range.union( |
| MeasurementRange.create(min, true, max, true, unit)); |
| } |
| continue; |
| } |
| } |
| } |
| range = MeasurementRange.create(min, true, max, true, unit); |
| selectedType = type; |
| } |
| } |
| return range; |
| } |
| |
| /** |
| * Returns the union of all time ranges found in the given extent, or {@code null} if none. |
| * |
| * @param extent the extent to convert to a time range, or {@code null}. |
| * @return a time range created from the given extent, or {@code null} if none. |
| * |
| * @since 0.4 |
| */ |
| public static Range<Date> getTimeRange(final Extent extent) { |
| Date min = null; |
| Date max = null; |
| if (extent != null) { |
| for (final TemporalExtent t : extent.getTemporalElements()) { |
| final Date startTime, endTime; |
| if (t instanceof DefaultTemporalExtent) { |
| final DefaultTemporalExtent dt = (DefaultTemporalExtent) t; |
| startTime = dt.getStartTime(); // Maybe user has overridden those methods. |
| endTime = dt.getEndTime(); |
| } else { |
| final TemporalPrimitive p = t.getExtent(); |
| startTime = DefaultTemporalExtent.getTime(p, true); |
| endTime = DefaultTemporalExtent.getTime(p, false); |
| } |
| if (startTime != null && (min == null || startTime.before(min))) min = startTime; |
| if ( endTime != null && (max == null || endTime.after (max))) max = endTime; |
| } |
| } |
| if (min == null && max == null) { |
| return null; |
| } |
| return new Range<>(Date.class, min, true, max, true); |
| } |
| |
| /** |
| * Returns an instant in the {@linkplain Extent#getTemporalElements() temporal elements} of the given extent, |
| * or {@code null} if none. First, this method computes the union of all temporal elements. Then this method |
| * computes the linear interpolation between the start and end time as in the following pseudo-code: |
| * |
| * {@preformat java |
| * return new Date(startTime + (endTime - startTime) * location); |
| * } |
| * |
| * Special cases: |
| * <ul> |
| * <li>If {@code location} is 0, then this method returns the {@linkplain DefaultTemporalExtent#getStartTime() start time}.</li> |
| * <li>If {@code location} is 1, then this method returns the {@linkplain DefaultTemporalExtent#getEndTime() end time}.</li> |
| * <li>If {@code location} is 0.5, then this method returns the average of start time and end time.</li> |
| * <li>If {@code location} is outside the [0 … 1] range, then the result will be outside the temporal extent.</li> |
| * </ul> |
| * |
| * @param extent the extent from which to get an instant, or {@code null}. |
| * @param location 0 for the start time, 1 for the end time, 0.5 for the average time, or the |
| * coefficient (usually in the [0 … 1] range) for interpolating an instant. |
| * @return an instant interpolated at the given location, or {@code null} if none. |
| * |
| * @since 0.4 |
| */ |
| public static Date getDate(final Extent extent, final double location) { |
| ArgumentChecks.ensureFinite("location", location); |
| Date min = null; |
| Date max = null; |
| if (extent != null) { |
| for (final TemporalExtent t : extent.getTemporalElements()) { |
| Date startTime = null; |
| Date endTime = null; |
| if (t instanceof DefaultTemporalExtent) { |
| final DefaultTemporalExtent dt = (DefaultTemporalExtent) t; |
| if (location != 1) startTime = dt.getStartTime(); // Maybe user has overridden those methods. |
| if (location != 0) endTime = dt.getEndTime(); |
| } else { |
| final TemporalPrimitive p = t.getExtent(); |
| if (location != 1) startTime = DefaultTemporalExtent.getTime(p, true); |
| if (location != 0) endTime = DefaultTemporalExtent.getTime(p, false); |
| } |
| if (startTime != null && (min == null || startTime.before(min))) min = startTime; |
| if ( endTime != null && (max == null || endTime.after (max))) max = endTime; |
| } |
| } |
| if (min == null) return max; |
| if (max == null) return min; |
| final long startTime = min.getTime(); |
| return new Date(Math.addExact(startTime, Math.round((max.getTime() - startTime) * location))); |
| } |
| |
| /** |
| * Returns the position at the median longitude and latitude values of the given bounding box. |
| * This method does not check the {@linkplain DefaultGeographicBoundingBox#getInclusion() inclusion} status. |
| * This method takes in account bounding boxes that cross the anti-meridian. |
| * |
| * @param bbox the bounding box for which to get the median longitude and latitude values, or {@code null}. |
| * @return a median position of the given bounding box, or {@code null} if none. |
| */ |
| public static DirectPosition centroid(final GeographicBoundingBox bbox) { |
| if (bbox != null) { |
| double y = (bbox.getNorthBoundLatitude() + bbox.getSouthBoundLatitude()) / 2; |
| double x = bbox.getWestBoundLongitude(); |
| double xmax = bbox.getEastBoundLongitude(); |
| if (xmax < x) { |
| xmax += (Longitude.MAX_VALUE - Longitude.MIN_VALUE); |
| } |
| x = Longitude.normalize((x + xmax) / 2); |
| if (Double.isFinite(x) || Double.isFinite(y)) { |
| return ReferencingServices.getInstance().geographic(x, y); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns an <em>estimation</em> of the area (in square metres) of the given bounding box. |
| * Since {@code GeographicBoundingBox} provides only approximated information (for example |
| * it does not specify the datum), the value returned by this method is also approximated. |
| * |
| * <p>The current implementation performs its computation on the |
| * {@linkplain org.apache.sis.referencing.CommonCRS#SPHERE GRS 1980 Authalic Sphere}. |
| * However this may change in any future SIS version.</p> |
| * |
| * @param box the geographic bounding box for which to compute the area, or {@code null}. |
| * @return an estimation of the area in the given bounding box (m²), |
| * or {@linkplain Double#NaN NaN} if the given box was null. |
| * |
| * @since 0.4 |
| */ |
| public static double area(final GeographicBoundingBox box) { |
| if (box == null) { |
| return Double.NaN; |
| } |
| double Δλ = box.getEastBoundLongitude() - box.getWestBoundLongitude(); |
| final double span = Longitude.MAX_VALUE - Longitude.MIN_VALUE; |
| if (Δλ > span) { |
| Δλ = span; |
| } else if (Δλ < 0) { |
| if (Δλ < -span) { |
| Δλ = -span; |
| } else { |
| Δλ += span; |
| } |
| } |
| return (AUTHALIC_RADIUS * AUTHALIC_RADIUS) * toRadians(Δλ) * |
| max(0, sin(toRadians(box.getNorthBoundLatitude())) - |
| sin(toRadians(box.getSouthBoundLatitude()))); |
| } |
| |
| /** |
| * Returns the intersection of the given geographic bounding boxes. If any of the arguments is {@code null}, |
| * then this method returns the other argument (which may be null). Otherwise this method returns a box which |
| * is the intersection of the two given boxes. |
| * |
| * <p>This method never modify the given boxes, but may return directly one of the given arguments if it |
| * already represents the intersection result.</p> |
| * |
| * @param b1 the first bounding box, or {@code null}. |
| * @param b2 the second bounding box, or {@code null}. |
| * @return the intersection (may be any of the {@code b1} or {@code b2} argument if unchanged), |
| * or {@code null} if the two given boxes are null. |
| * @throws IllegalArgumentException if the {@linkplain DefaultGeographicBoundingBox#getInclusion() inclusion status} |
| * is not the same for both boxes. |
| * |
| * @see DefaultGeographicBoundingBox#intersect(GeographicBoundingBox) |
| * |
| * @since 0.4 |
| */ |
| public static GeographicBoundingBox intersection(final GeographicBoundingBox b1, final GeographicBoundingBox b2) { |
| if (b1 == null) return b2; |
| if (b2 == null || b2 == b1) return b1; |
| final DefaultGeographicBoundingBox box = new DefaultGeographicBoundingBox(b1); |
| box.intersect(b2); |
| if (box.equals(b1, ComparisonMode.BY_CONTRACT)) return b1; |
| if (box.equals(b2, ComparisonMode.BY_CONTRACT)) return b2; |
| return box; |
| } |
| |
| /** |
| * May compute an intersection between the given geographic extents. |
| * Current implementation supports only {@link GeographicBoundingBox}; |
| * all other kinds are handled as if they were {@code null}. |
| * |
| * <p>We may improve this method in future Apache SIS version, but it is not yet clear how. |
| * For example how to handle {@link GeographicDescription} or {@link BoundingPolygon}? |
| * This method should not be public before we find a better contract.</p> |
| */ |
| static GeographicExtent intersection(final GeographicExtent e1, final GeographicExtent e2) { |
| return intersection(e1 instanceof GeographicBoundingBox ? (GeographicBoundingBox) e1 : null, |
| e2 instanceof GeographicBoundingBox ? (GeographicBoundingBox) e2 : null); |
| } |
| |
| /** |
| * Returns the intersection of the given vertical extents. If any of the arguments is {@code null}, |
| * then this method returns the other argument (which may be null). Otherwise this method returns a |
| * vertical extent which is the intersection of the two given extents. |
| * |
| * <p>This method never modify the given extents, but may return directly one of the given arguments |
| * if it already represents the intersection result.</p> |
| * |
| * <h4>Advantage and inconvenient of this method</h4> |
| * This method can not intersect extents defined with different datums because height transformations |
| * generally require the geodetic positions (latitudes and longitudes) of the heights to transform. |
| * For more general transformations, it is better to convert all extent components into a single envelope, |
| * then {@linkplain org.apache.sis.geometry.Envelopes#transform(CoordinateOperation, Envelope) transform |
| * the envelope at once}. On the other hand, this {@code intersect(…)} method preserves better |
| * the {@link org.apache.sis.xml.NilReason} (if any). |
| * |
| * @param e1 the first extent, or {@code null}. |
| * @param e2 the second extent, or {@code null}. |
| * @return the intersection (may be any of the {@code e1} or {@code e2} argument if unchanged), |
| * or {@code null} if the two given extents are null. |
| * @throws IllegalArgumentException if the two extents do not use the same datum, ignoring metadata. |
| * |
| * @see DefaultVerticalExtent#intersect(VerticalExtent) |
| * |
| * @since 0.8 |
| */ |
| public static VerticalExtent intersection(final VerticalExtent e1, final VerticalExtent e2) { |
| if (e1 == null) return e2; |
| if (e2 == null || e2 == e1) return e1; |
| final DefaultVerticalExtent extent = new DefaultVerticalExtent(e1); |
| extent.intersect(e2); |
| if (extent.equals(e1, ComparisonMode.BY_CONTRACT)) return e1; |
| if (extent.equals(e2, ComparisonMode.BY_CONTRACT)) return e2; |
| return extent; |
| } |
| |
| /** |
| * Returns the intersection of the given temporal extents. If any of the arguments is {@code null}, |
| * then this method returns the other argument (which may be null). Otherwise this method returns a |
| * temporal extent which is the intersection of the two given extents. |
| * |
| * <p>This method never modify the given extents, but may return directly one of the given arguments |
| * if it already represents the intersection result.</p> |
| * |
| * @param e1 the first extent, or {@code null}. |
| * @param e2 the second extent, or {@code null}. |
| * @return the intersection (may be any of the {@code e1} or {@code e2} argument if unchanged), |
| * or {@code null} if the two given extents are null. |
| * @throws UnsupportedOperationException if no implementation of {@code TemporalFactory} has been found |
| * on the classpath. |
| * |
| * @see DefaultTemporalExtent#intersect(TemporalExtent) |
| * |
| * @since 0.8 |
| */ |
| public static TemporalExtent intersection(final TemporalExtent e1, final TemporalExtent e2) { |
| if (e1 == null) return e2; |
| if (e2 == null || e2 == e1) return e1; |
| final DefaultTemporalExtent extent = new DefaultTemporalExtent(e1); |
| extent.intersect(e2); |
| if (extent.equals(e1, ComparisonMode.BY_CONTRACT)) return e1; |
| if (extent.equals(e2, ComparisonMode.BY_CONTRACT)) return e2; |
| return extent; |
| } |
| |
| /** |
| * Returns the intersection of the given extents. If any of the arguments is {@code null}, |
| * then this method returns the other argument (which may be null). Otherwise this method |
| * returns an extent which is the intersection of all geographic, vertical and temporal |
| * elements in the two given extents. |
| * |
| * <p>This method never modify the given extents, but may return directly one of the given |
| * arguments if it already represents the intersection result.</p> |
| * |
| * @param e1 the first extent, or {@code null}. |
| * @param e2 the second extent, or {@code null}. |
| * @return the intersection (may be any of the {@code e1} or {@code e2} argument if unchanged), |
| * or {@code null} if the two given extents are null. |
| * @throws IllegalArgumentException if two elements to intersect are not compatible (e.g. mismatched |
| * {@linkplain DefaultGeographicBoundingBox#getInclusion() bounding box inclusion status} or |
| * mismatched {@linkplain DefaultVerticalExtent#getVerticalCRS() vertical datum}). |
| * @throws UnsupportedOperationException if a {@code TemporalFactory} is required but no implementation |
| * has been found on the classpath. |
| * |
| * @see DefaultExtent#intersect(Extent) |
| * |
| * @since 0.8 |
| */ |
| public static Extent intersection(final Extent e1, final Extent e2) { |
| if (e1 == null) return e2; |
| if (e2 == null || e2 == e1) return e1; |
| final DefaultExtent extent = new DefaultExtent(e1); |
| extent.intersect(e2); |
| if (extent.equals(e1, ComparisonMode.BY_CONTRACT)) return e1; |
| if (extent.equals(e2, ComparisonMode.BY_CONTRACT)) return e2; |
| return extent; |
| } |
| } |