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.event;
015
016import com.fasterxml.jackson.annotation.JsonAnyGetter;
017import com.fasterxml.jackson.annotation.JsonIgnore;
018import com.fasterxml.jackson.annotation.JsonProperty;
019import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
020import com.fasterxml.jackson.databind.annotation.JsonSerialize;
021import io.swagger.v3.oas.annotations.ExternalDocumentation;
022import io.swagger.v3.oas.annotations.media.Schema;
023import jakarta.annotation.Nullable;
024import java.lang.reflect.Field;
025import java.lang.reflect.Modifier;
026import java.net.URI;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.Date;
032import java.util.EnumSet;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.Objects;
037import java.util.Set;
038import java.util.stream.Collectors;
039import java.util.stream.Stream;
040import lombok.AllArgsConstructor;
041import lombok.Data;
042import org.gbif.api.model.common.Identifier;
043import org.gbif.api.model.common.MediaObject;
044import org.gbif.api.model.occurrence.Gadm;
045import org.gbif.api.model.occurrence.MeasurementOrFact;
046import org.gbif.api.model.occurrence.Occurrence;
047import org.gbif.api.model.occurrence.OccurrenceRelation;
048import org.gbif.api.model.occurrence.VerbatimOccurrence;
049import org.gbif.api.util.IsoDateInterval;
050import org.gbif.api.vocabulary.Continent;
051import org.gbif.api.vocabulary.Country;
052import org.gbif.api.vocabulary.EventIssue;
053import org.gbif.api.vocabulary.License;
054import org.gbif.api.vocabulary.OccurrenceIssue;
055import org.gbif.dwc.terms.DwcTerm;
056import org.gbif.dwc.terms.Term;
057import org.gbif.dwc.terms.UnknownTerm;
058
059/**
060 * Event class based on https://dwc.tdwg.org/terms/#event.
061 */
062@Data
063public class Event extends VerbatimOccurrence {
064
065  public static final String GEO_DATUM = "WGS84";
066
067  // keep names of ALL properties of this class in a set for jackson serialization, see #properties()
068  private static final Set<String> PROPERTIES = Collections.unmodifiableSet(
069    Stream.concat(
070      // we need to these json properties manually because we have a fixed getter but no field for it
071      Stream.of(DwcTerm.geodeticDatum.simpleName(), "class", "countryCode"),
072      Stream.concat(Arrays.stream(Event.class.getDeclaredFields()),
073                    Arrays.stream(VerbatimOccurrence.class.getDeclaredFields()))
074        .filter(field -> !Modifier.isStatic(field.getModifiers()))
075        .map(Field::getName)).collect(Collectors.toSet()));
076
077  public static Event fromOccurrence(Occurrence occurrence) {
078    Event event =new Event();
079    event.setKey(occurrence.getKey());
080    event.setDatasetKey(occurrence.getDatasetKey());
081    event.setPublishingOrgKey(occurrence.getPublishingOrgKey());
082    event.setNetworkKeys(occurrence.getNetworkKeys());
083    event.setDatasetCategory(occurrence.getDatasetCategory());
084    event.setInstallationKey(occurrence.getInstallationKey());
085    event.setPublishingCountry(occurrence.getPublishingCountry());
086    event.setProtocol(occurrence.getProtocol());
087    event.setLastCrawled(occurrence.getLastCrawled());
088    event.setLastParsed(occurrence.getLastParsed());
089    event.setCrawlId(occurrence.getCrawlId());
090    event.setProjectId(occurrence.getProjectId());
091    event.setProgrammeAcronym(occurrence.getProgrammeAcronym());
092    event.setHostingOrganizationKey(occurrence.getHostingOrganizationKey());
093    event.setVerbatimFields(occurrence.getVerbatimFields());
094    event.setExtensions(occurrence.getExtensions());
095    event.setDateIdentified(occurrence.getDateIdentified());
096    event.setDecimalLongitude(occurrence.getDecimalLongitude());
097    event.setDecimalLatitude(occurrence.getDecimalLatitude());
098    event.setCoordinatePrecision(occurrence.getCoordinatePrecision());
099    event.setCoordinateUncertaintyInMeters(occurrence.getCoordinateUncertaintyInMeters());
100    event.setElevation(occurrence.getElevation());
101    event.setElevationAccuracy(occurrence.getElevationAccuracy());
102    event.setDepth(occurrence.getDepth());
103    event.setDepthAccuracy(occurrence.getDepthAccuracy());
104    event.setContinent(occurrence.getContinent());
105    event.setCountry(occurrence.getCountry());
106    event.setStateProvince(occurrence.getStateProvince());
107    event.setWaterBody(occurrence.getWaterBody());
108    event.setDistanceFromCentroidInMeters(occurrence.getDistanceFromCentroidInMeters());
109    event.setYear(occurrence.getYear());
110    event.setMonth(occurrence.getMonth());
111    event.setDay(occurrence.getDay());
112    event.setEventDate(occurrence.getEventDate());
113    event.setIssues(toEventIssues(occurrence.getIssues()));
114    event.setModified(occurrence.getModified());
115    event.setLastInterpreted(occurrence.getLastInterpreted());
116    event.setReferences(occurrence.getReferences());
117    event.setLicense(occurrence.getLicense());
118    event.setOrganismQuantity(occurrence.getOrganismQuantity());
119    event.setOrganismQuantityType(occurrence.getOrganismQuantityType());
120    event.setSampleSizeUnit(occurrence.getSampleSizeUnit());
121    event.setSampleSizeValue(occurrence.getSampleSizeValue());
122    event.setRelativeOrganismQuantity(occurrence.getRelativeOrganismQuantity());
123    event.setIdentifiers(occurrence.getIdentifiers());
124    event.setMedia(occurrence.getMedia());
125    event.setFacts(occurrence.getFacts());
126    event.setRelations(occurrence.getRelations());
127    event.setGadm(occurrence.getGadm());
128    event.setDatasetID(occurrence.getDatasetID());
129    event.setDatasetName(occurrence.getDatasetName());
130    event.setPreparations(occurrence.getPreparations());
131    event.setSamplingProtocol(occurrence.getSamplingProtocol());
132    return event;
133  }
134
135  private static Set<EventIssue> toEventIssues(Collection<OccurrenceIssue> occurrenceIssues) {
136    return occurrenceIssues.stream()
137        .map(
138            occIssue -> {
139              try {
140                return EventIssue.valueOf(occIssue.name());
141              } catch (Exception ex) {
142                // we ignore the value
143              }
144              return null;
145            })
146        .filter(Objects::nonNull)
147        .collect(Collectors.toSet());
148  }
149
150  @Data
151  @AllArgsConstructor
152  public static class ParentLineage {
153    private String id;
154    private String eventType;
155  }
156
157  @Data
158  @AllArgsConstructor
159  public static class VocabularyConcept {
160    private String concept;
161    private Set<String> lineage;
162  }
163
164  private String id;
165  private String eventID;
166  private String parentEventID;
167  private Integer startDayOfYear;
168  private Integer endDayOfYear;
169  private String locationID;
170  private String eventType;
171  private List<ParentLineage> parentsLineage;
172
173  // identification
174  private Date dateIdentified;
175  // location
176  private Double decimalLongitude;
177  private Double decimalLatitude;
178
179  //coordinatePrecision and coordinateUncertaintyInMeters should be BigDecimal see POR-2795
180  private Double coordinatePrecision;
181  private Double coordinateUncertaintyInMeters;
182
183  private Double elevation;
184  private Double elevationAccuracy;
185  private Double depth;
186  private Double depthAccuracy;
187  private Continent continent;
188  @JsonSerialize(using = Country.IsoSerializer.class)
189  @JsonDeserialize(using = Country.IsoDeserializer.class)
190  private Country country;
191  private String stateProvince;
192  private String waterBody;
193  private Double distanceFromCentroidInMeters;
194
195  // recording event
196  private Integer year;
197  private Integer month;
198  private Integer day;
199  private IsoDateInterval eventDate;
200
201  private Set<EventIssue> issues = EnumSet.noneOf(EventIssue.class);
202
203  // record level
204  private Date modified;  // interpreted dc:modified, i.e. date changed in source
205  private Date lastInterpreted;
206  private URI references;
207  private License license;
208  private Double organismQuantity;
209  private String organismQuantityType;
210  private String sampleSizeUnit;
211  private Double sampleSizeValue;
212  private Double relativeOrganismQuantity;
213  private String projectTitle;
214  private String fundingAttribution;
215  private String fundingAttributionID;
216
217
218  // interpreted extension data
219  private List<Identifier> identifiers = new ArrayList<>();
220  private List<MediaObject> media = new ArrayList<>();
221  private List<MeasurementOrFact> facts = new ArrayList<>();
222  private List<OccurrenceRelation> relations = new ArrayList<>();
223  private Gadm gadm = new Gadm();
224  private String datasetID;
225  private String datasetName;
226  private String preparations;
227  private String samplingProtocol;
228  private List<Humboldt> humboldt = new ArrayList<>();
229
230  @Schema(
231    description = "The 2-letter country code (as per ISO-3166-1) of the country, territory or area in which the " +
232      "occurrence was recorded.",
233    externalDocs = @ExternalDocumentation(
234      description = "Darwin Core definition",
235      url = "https://rs.tdwg.org/dwc/terms/countryCode"
236    )
237  )
238  @Nullable
239  @JsonProperty("countryCode")
240  public Country getCountry() {
241    return country;
242  }
243
244  /**
245    * Renders the country title as a JSON property country in addition to the ISO 3166 2 letter countryCode being
246   * serialized by the regular country Java property.
247   * Made private to use it only for JSON serialization and not within Java code.
248   */
249  @Schema(
250    description = "The title (as per ISO-3166-1) of the country, territory or area in which the " +
251      "occurrence was recorded.",
252    externalDocs = @ExternalDocumentation(
253      description = "Darwin Core definition",
254      url = "https://rs.tdwg.org/dwc/terms/country"
255    )
256  )
257  @Nullable
258  @JsonProperty("country")
259  private String getCountryTitle() {
260    return country == null ? null : country.getTitle();
261  }
262
263  private void setCountryTitle(String country) {
264    // ignore, setter only to avoid JSON being written into the fields map
265  }
266
267  /**
268   * Convenience method checking if any spatial validation rule has not passed.
269   * Primarily used to indicate that the record should not be displayed on a map.
270   */
271  @JsonIgnore
272  public boolean hasSpatialIssue() {
273    for (OccurrenceIssue rule : OccurrenceIssue.GEOSPATIAL_RULES) {
274      if (issues.contains(rule)) {
275        return true;
276      }
277    }
278    return false;
279  }
280
281  /**
282   * The geodetic datum for the interpreted decimal coordinates.
283   * This is always WGS84 if there a coordinate exists as we reproject other datums into WGS84.
284   */
285  @Nullable
286  public String getGeodeticDatum() {
287    if (decimalLatitude != null) {
288      return GEO_DATUM;
289    }
290    return null;
291  }
292
293  /**
294   * This private method is only for serialization via jackson and not exposed anywhere else!
295   * It maps the verbatimField terms into properties with their simple name or qualified names for UnknownTerms.
296   */
297  @JsonAnyGetter
298  private Map<String, String> jsonVerbatimFields() {
299    Map<String, String> extendedProps = new HashMap<>();
300    for (Map.Entry<Term, String> prop : getVerbatimFields().entrySet()) {
301      Term t = prop.getKey();
302      if (t instanceof UnknownTerm || PROPERTIES.contains(t.simpleName())) {
303        extendedProps.put(t.qualifiedName(), prop.getValue());
304      } else {
305        // render all terms in controlled enumerations as simple names only - unless we have a property of that name already!
306        extendedProps.put(t.simpleName(), prop.getValue());
307      }
308    }
309    return extendedProps;
310  }
311}