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