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}