001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.api.model.predicate;
015
016import org.gbif.api.model.occurrence.search.OccurrenceSearchParameter;
017import org.gbif.api.util.SearchTypeValidator;
018
019import java.util.Objects;
020import java.util.StringJoiner;
021
022import javax.validation.constraints.NotNull;
023
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import com.fasterxml.jackson.annotation.JsonCreator;
028import com.fasterxml.jackson.annotation.JsonProperty;
029
030import io.swagger.v3.oas.annotations.media.Schema;
031
032/**
033 * This predicate checks if an occurrence location falls within the given WKT geometry {@code value}.
034 */
035@Schema(
036  description = "This predicate checks if an occurrence falls within the given WKT geometry."
037)
038public class WithinPredicate implements Predicate {
039
040  private static final Logger LOG = LoggerFactory.getLogger(WithinPredicate.class);
041
042  @Schema(
043    description = "The WKT geometry to test for.  Occurrences whose location is within this geometry are returned."
044  )
045  @NotNull
046  private final String geometry;
047
048  /**
049   * Builds a new within predicate for a single, simple geometry as
050   * <a href="http://en.wikipedia.org/wiki/Well-known_text">Well Known Text</a> (WKT).
051   * Multi geometries like MULTIPOLYGON are not supported and multiple predicates should be used instead.
052   * <br/>
053   * The validation implemented does a basic syntax check for the following simple geometries, but does not
054   * verify that the resulting geometries are topologically valid (see the OGC SFS specification).
055   * <ul>
056   *   <li>POINT</li>
057   *   <li>LINESTRING</li>
058   *   <li>POLYGON</li>
059   *   <li>LINEARRING</li>
060   * </ul>
061   * <strong>Unlike other predicates, this validation only logs in case of an invalid string.</strong>
062   * This is because the WKT parser has been changed over time, and some old strings are not valid according
063   * to the current parser.
064   *
065   * @param geometry
066   */
067  @JsonCreator
068  public WithinPredicate(@JsonProperty("geometry") String geometry) {
069    Objects.requireNonNull(geometry, "<geometry> may not be null");
070    try {
071      // test if it is a valid WKT
072      SearchTypeValidator.validate(OccurrenceSearchParameter.GEOMETRY, geometry);
073    } catch (IllegalArgumentException e) {
074      // Log invalid strings, but continue - the geometry parser has changed over time, and some once-valid strings
075      // are no longer considered valid.  See https://github.com/gbif/gbif-api/issues/48.
076      LOG.warn("Invalid geometry string {}: {}", geometry, e.getMessage());
077    }
078    this.geometry = geometry;
079  }
080
081  public String getGeometry() {
082    return geometry;
083  }
084
085  @Override
086  public boolean equals(Object o) {
087    if (this == o) {
088      return true;
089    }
090    if (o == null || getClass() != o.getClass()) {
091      return false;
092    }
093    WithinPredicate that = (WithinPredicate) o;
094    return Objects.equals(geometry, that.geometry);
095  }
096
097  @Override
098  public int hashCode() {
099    return Objects.hash(geometry);
100  }
101
102  @Override
103  public String toString() {
104    return new StringJoiner(", ", WithinPredicate.class.getSimpleName() + "[", "]")
105      .add("geometry='" + geometry + "'")
106      .toString();
107  }
108}