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