001/*
002 * Copyright 2014 Global Biodiversity Information Facility (GBIF)
003 * Licensed under the Apache License, Version 2.0 (the "License");
004 * you may not use this file except in compliance with the License.
005 * You may obtain a copy of the License at
006 * http://www.apache.org/licenses/LICENSE-2.0
007 * Unless required by applicable law or agreed to in writing, software
008 * distributed under the License is distributed on an "AS IS" BASIS,
009 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010 * See the License for the specific language governing permissions and
011 * limitations under the License.
012 */
013package org.gbif.api.model.occurrence;
014
015import org.gbif.api.model.common.Identifier;
016import org.gbif.api.model.common.LinneanClassification;
017import org.gbif.api.model.common.LinneanClassificationKeys;
018import org.gbif.api.model.common.MediaObject;
019import org.gbif.api.util.ClassificationUtils;
020import org.gbif.api.vocabulary.BasisOfRecord;
021import org.gbif.api.vocabulary.Continent;
022import org.gbif.api.vocabulary.Country;
023import org.gbif.api.vocabulary.EstablishmentMeans;
024import org.gbif.api.vocabulary.License;
025import org.gbif.api.vocabulary.LifeStage;
026import org.gbif.api.vocabulary.OccurrenceIssue;
027import org.gbif.api.vocabulary.Rank;
028import org.gbif.api.vocabulary.Sex;
029import org.gbif.api.vocabulary.TypeStatus;
030import org.gbif.dwc.terms.DwcTerm;
031import org.gbif.dwc.terms.Term;
032import org.gbif.dwc.terms.UnknownTerm;
033
034import java.lang.reflect.Field;
035import java.net.URI;
036import java.util.Date;
037import java.util.EnumSet;
038import java.util.LinkedHashMap;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042import javax.annotation.Nullable;
043import javax.validation.constraints.Max;
044import javax.validation.constraints.Min;
045import javax.validation.constraints.NotNull;
046
047import com.google.common.base.Function;
048import com.google.common.base.Objects;
049import com.google.common.base.Preconditions;
050import com.google.common.collect.ImmutableSet;
051import com.google.common.collect.Iterables;
052import com.google.common.collect.Lists;
053import com.google.common.collect.Maps;
054import com.google.common.collect.Sets;
055import org.codehaus.jackson.annotate.JsonAnyGetter;
056import org.codehaus.jackson.annotate.JsonIgnore;
057import org.codehaus.jackson.annotate.JsonProperty;
058import org.codehaus.jackson.map.annotate.JsonDeserialize;
059import org.codehaus.jackson.map.annotate.JsonSerialize;
060
061/**
062 * Represents an Occurrence as interpreted by GBIF, adding typed properties on top of the verbatim ones.
063 */
064public class Occurrence extends VerbatimOccurrence implements LinneanClassification, LinneanClassificationKeys {
065
066  public static final String GEO_DATUM = "WGS84";
067  // keep names of ALL properties of this class in a set for jackson serialization, see #properties()
068  private static final Set<String> PROPERTIES = ImmutableSet.copyOf(
069    Iterables.concat(
070      // we need to these json properties manually cause we have a fixed getter but no field for it
071      Lists.newArrayList(DwcTerm.geodeticDatum.simpleName(), "class", "countryCode"),
072      Iterables.transform(
073        Iterables.concat(Lists.newArrayList(Occurrence.class.getDeclaredFields()),
074                Lists.newArrayList(VerbatimOccurrence.class.getDeclaredFields())
075        ), new Function<Field, String>() {
076
077          @Nullable
078          @Override
079          public String apply(@Nullable Field f) {
080            return f.getName();
081          }
082        })
083      )
084    );
085  // occurrence fields
086  private BasisOfRecord basisOfRecord;
087  private Integer individualCount;
088  private Sex sex;
089  private LifeStage lifeStage;
090  private EstablishmentMeans establishmentMeans;
091  // taxonomy as nub keys -> LinneanClassificationKeys
092  private Integer taxonKey;
093  private Integer kingdomKey;
094  private Integer phylumKey;
095  private Integer classKey;
096  private Integer orderKey;
097  private Integer familyKey;
098  private Integer genusKey;
099  private Integer subgenusKey;
100  private Integer speciesKey;
101  // taxonomy as name strings -> LinneanClassification
102  private String scientificName;  // the interpreted name matching taxonKey
103  private String kingdom;
104  private String phylum;
105  @JsonProperty("class")
106  private String clazz;
107  private String order;
108  private String family;
109  private String genus;
110  private String subgenus;
111  private String species;
112  // atomised scientific name
113  private String genericName; // missing from DwC
114  private String specificEpithet;
115  private String infraspecificEpithet;
116  private Rank taxonRank;
117  // identification
118  private Date dateIdentified;
119  // location
120  private Double decimalLongitude;
121  private Double decimalLatitude;
122
123  //coordinatePrecision and coordinateUncertaintyInMeters should be BigDecimal see POR-2795
124  private Double coordinatePrecision;
125  private Double coordinateUncertaintyInMeters;
126  @Deprecated //see getter
127  private Double coordinateAccuracy;
128
129  private Double elevation;
130  private Double elevationAccuracy;
131  private Double depth;
132  private Double depthAccuracy;
133  private Continent continent;
134  @JsonSerialize(using = Country.IsoSerializer.class)
135  @JsonDeserialize(using = Country.IsoDeserializer.class)
136  private Country country;
137  private String stateProvince;
138  private String waterBody;
139  // recording event
140  private Integer year;
141  private Integer month;
142  private Integer day;
143  private Date eventDate;
144  private TypeStatus typeStatus;
145  // extracted from type status, but we should propose a new dwc term for this!
146  // for example: "Paratype of Taeniopteryx metequi Ricker & Ross" is status=Paratype, typifiedName=Taeniopteryx metequi
147// Ricker & Ross
148  private String typifiedName; // missing from DwC
149  private Set<OccurrenceIssue> issues = EnumSet.noneOf(OccurrenceIssue.class);
150  // record level
151  private Date modified;  // interpreted dc:modified, i.e. date changed in source
152  private Date lastInterpreted;
153  private URI references;
154  private License license;
155  // interpreted extension data
156  private List<Identifier> identifiers = Lists.newArrayList();
157  private List<MediaObject> media = Lists.newArrayList();
158  private List<FactOrMeasurment> facts = Lists.newArrayList();
159  private List<OccurrenceRelation> relations = Lists.newArrayList();
160
161  public Occurrence() {
162
163  }
164
165  /**
166   * Create occurrence instance from existing verbatim one, copying over all data.
167   */
168  public Occurrence(@Nullable VerbatimOccurrence verbatim) {
169    if (verbatim != null) {
170      setKey(verbatim.getKey());
171      setDatasetKey(verbatim.getDatasetKey());
172      setPublishingOrgKey(verbatim.getPublishingOrgKey());
173      setPublishingCountry(verbatim.getPublishingCountry());
174      setProtocol(verbatim.getProtocol());
175      setCrawlId(verbatim.getCrawlId());
176      if (verbatim.getLastCrawled() != null) {
177        setLastCrawled(new Date(verbatim.getLastCrawled().getTime()));
178      }
179      if (verbatim.getVerbatimFields() != null) {
180        getVerbatimFields().putAll(verbatim.getVerbatimFields());
181      }
182      if (verbatim.getLastParsed() != null) {
183        setLastParsed(verbatim.getLastParsed());
184      }
185      setExtensions(verbatim.getExtensions());
186    }
187  }
188
189  @Nullable
190  public BasisOfRecord getBasisOfRecord() {
191    return basisOfRecord;
192  }
193
194  public void setBasisOfRecord(BasisOfRecord basisOfRecord) {
195    this.basisOfRecord = basisOfRecord;
196  }
197
198  @Nullable
199  public Integer getIndividualCount() {
200    return individualCount;
201  }
202
203  public void setIndividualCount(Integer individualCount) {
204    this.individualCount = individualCount;
205  }
206
207  @Nullable
208  public Sex getSex() {
209    return sex;
210  }
211
212  public void setSex(Sex sex) {
213    this.sex = sex;
214  }
215
216  @Nullable
217  public LifeStage getLifeStage() {
218    return lifeStage;
219  }
220
221  public void setLifeStage(LifeStage lifeStage) {
222    this.lifeStage = lifeStage;
223  }
224
225  @Nullable
226  public EstablishmentMeans getEstablishmentMeans() {
227    return establishmentMeans;
228  }
229
230  public void setEstablishmentMeans(EstablishmentMeans establishmentMeans) {
231    this.establishmentMeans = establishmentMeans;
232  }
233
234  @Nullable
235  /**
236   * The best matching, accepted GBIF backbone name usage representing this occurrence.
237   * In case the verbatim scientific name and its classification can only be matched to a higher rank this will
238   * represent the lowest matching rank. In the worst case this could just be for example Animalia.
239   */
240  public Integer getTaxonKey() {
241    return taxonKey;
242  }
243
244  public void setTaxonKey(Integer taxonKey) {
245    this.taxonKey = taxonKey;
246  }
247
248  @Nullable
249  @Override
250  public Integer getKingdomKey() {
251    return kingdomKey;
252  }
253
254  @Override
255  public void setKingdomKey(@Nullable Integer kingdomKey) {
256    this.kingdomKey = kingdomKey;
257  }
258
259  @Nullable
260  @Override
261  public Integer getPhylumKey() {
262    return phylumKey;
263  }
264
265  @Override
266  public void setPhylumKey(@Nullable Integer phylumKey) {
267    this.phylumKey = phylumKey;
268  }
269
270  @Nullable
271  @Override
272  public Integer getClassKey() {
273    return classKey;
274  }
275
276  @Override
277  public void setClassKey(@Nullable Integer classKey) {
278    this.classKey = classKey;
279  }
280
281  @Nullable
282  @Override
283  public Integer getOrderKey() {
284    return orderKey;
285  }
286
287  @Override
288  public void setOrderKey(@Nullable Integer orderKey) {
289    this.orderKey = orderKey;
290  }
291
292  @Nullable
293  @Override
294  public Integer getFamilyKey() {
295    return familyKey;
296  }
297
298  @Override
299  public void setFamilyKey(@Nullable Integer familyKey) {
300    this.familyKey = familyKey;
301  }
302
303  @Nullable
304  @Override
305  public Integer getGenusKey() {
306    return genusKey;
307  }
308
309  @Override
310  public void setGenusKey(@Nullable Integer genusKey) {
311    this.genusKey = genusKey;
312  }
313
314  @Nullable
315  @Override
316  public Integer getSubgenusKey() {
317    return subgenusKey;
318  }
319
320  @Override
321  public void setSubgenusKey(@Nullable Integer subgenusKey) {
322    this.subgenusKey = subgenusKey;
323  }
324
325  @Nullable
326  @Override
327  public Integer getHigherRankKey(Rank rank) {
328    return ClassificationUtils.getHigherRankKey(this, rank);
329  }
330
331  /**
332   * An ordered map with entries for all higher Linnean ranks excluding the taxonKey itself.
333   * The map starts with the highest rank, e.g. the kingdom and maps the name usage key to its canonical name.
334   *
335   * @return map of higher ranks
336   */
337  @NotNull
338  @JsonIgnore
339  public LinkedHashMap<Integer, String> getHigherClassificationMap() {
340    return taxonKey == null ? ClassificationUtils.getHigherClassificationMap(this)
341      : ClassificationUtils.getHigherClassificationMap(this, taxonKey, null, null);
342  }
343
344  @Nullable
345  @Override
346  /**
347   * The accepted species for this occurrence. In case the taxonKey is of a higher rank than species (e.g. genus)
348   * speciesKey is null. In case taxonKey represents an infraspecific taxon the speciesKey points to the species
349   * the infraspecies is classified as. In case of taxonKey being a species the speciesKey is the same.
350   */
351  public Integer getSpeciesKey() {
352    return speciesKey;
353  }
354
355  @Override
356  public void setSpeciesKey(@Nullable Integer speciesKey) {
357    this.speciesKey = speciesKey;
358  }
359
360  @Nullable
361  public String getSpecificEpithet() {
362    return specificEpithet;
363  }
364
365  public void setSpecificEpithet(String specificEpithet) {
366    this.specificEpithet = specificEpithet;
367  }
368
369  @Nullable
370  public String getInfraspecificEpithet() {
371    return infraspecificEpithet;
372  }
373
374  public void setInfraspecificEpithet(String infraspecificEpithet) {
375    this.infraspecificEpithet = infraspecificEpithet;
376  }
377
378  @Nullable
379  public Rank getTaxonRank() {
380    return taxonRank;
381  }
382
383  public void setTaxonRank(Rank taxonRank) {
384    this.taxonRank = taxonRank;
385  }
386
387  @Nullable
388  /**
389   * The scientific name for taxonKey from the GBIF backbone.
390   */
391  public String getScientificName() {
392    return scientificName;
393  }
394
395  public void setScientificName(@Nullable String scientificName) {
396    this.scientificName = scientificName;
397  }
398
399  @Nullable
400  @Override
401  public String getKingdom() {
402    return kingdom;
403  }
404
405  @Override
406  public void setKingdom(@Nullable String kingdom) {
407    this.kingdom = kingdom;
408  }
409
410  @Nullable
411  @Override
412  public String getPhylum() {
413    return phylum;
414  }
415
416  @Override
417  public void setPhylum(@Nullable String phylum) {
418    this.phylum = phylum;
419  }
420
421  @Nullable
422  @Override
423  public String getClazz() {
424    return clazz;
425  }
426
427  @Override
428  public void setClazz(@Nullable String clazz) {
429    this.clazz = clazz;
430  }
431
432  @Nullable
433  @Override
434  public String getOrder() {
435    return order;
436  }
437
438  @Override
439  public void setOrder(@Nullable String order) {
440    this.order = order;
441  }
442
443  @Nullable
444  @Override
445  public String getFamily() {
446    return family;
447  }
448
449  @Override
450  public void setFamily(@Nullable String family) {
451    this.family = family;
452  }
453
454  @Nullable
455  @Override
456  public String getGenus() {
457    return genus;
458  }
459
460  @Override
461  public void setGenus(@Nullable String genus) {
462    this.genus = genus;
463  }
464
465  @Nullable
466  public String getGenericName() {
467    return genericName;
468  }
469
470  public void setGenericName(String genericName) {
471    this.genericName = genericName;
472  }
473
474  @Nullable
475  @Override
476  public String getSubgenus() {
477    return subgenus;
478  }
479
480  @Override
481  public void setSubgenus(@Nullable String subgenus) {
482    this.subgenus = subgenus;
483  }
484
485  @Nullable
486  @Override
487  public String getHigherRank(Rank rank) {
488    return ClassificationUtils.getHigherRank(this, rank);
489  }
490
491  @Nullable
492  @Override
493  /**
494   * The corresponding scientific name of the speciesKey from the GBIF backbone.
495   */
496  public String getSpecies() {
497    return species;
498  }
499
500  @Override
501  public void setSpecies(@Nullable String species) {
502    this.species = species;
503  }
504
505  @Nullable
506  public Date getDateIdentified() {
507    return dateIdentified == null ? null : new Date(dateIdentified.getTime());
508  }
509
510  public void setDateIdentified(@Nullable Date dateIdentified) {
511    this.dateIdentified = dateIdentified == null ? null : new Date(dateIdentified.getTime());
512  }
513
514  @Nullable
515  /**
516   * The decimalLongitude in decimal degrees always for the WGS84 datum. If a different geodetic datum was given the verbatim
517   * coordinates are transformed into WGS84 values.
518   */
519  public Double getDecimalLongitude() {
520    return decimalLongitude;
521  }
522
523  public void setDecimalLongitude(@Nullable Double decimalLongitude) {
524    this.decimalLongitude = decimalLongitude;
525  }
526
527  @Nullable
528  public Double getDecimalLatitude() {
529    return decimalLatitude;
530  }
531
532  public void setDecimalLatitude(@Nullable Double decimalLatitude) {
533    this.decimalLatitude = decimalLatitude;
534  }
535
536  /**
537   * The uncertainty radius for lat/lon in meters.
538   */
539  @Nullable
540  public Double getCoordinateUncertaintyInMeters() {
541    return coordinateUncertaintyInMeters;
542  }
543
544  public void setCoordinateUncertaintyInMeters(@Nullable Double coordinateUncertaintyInMeters) {
545    this.coordinateUncertaintyInMeters = coordinateUncertaintyInMeters;
546  }
547
548  @Nullable
549  public Double getCoordinatePrecision() {
550    return coordinatePrecision;
551  }
552
553  public void setCoordinatePrecision(Double coordinatePrecision) {
554    this.coordinatePrecision = coordinatePrecision;
555  }
556
557  @Nullable
558  @Deprecated
559  /**
560   * @Deprecated to be removed in the public v2 of the API (see POR-3061)
561   * The uncertainty for latitude in decimal degrees.
562   * Note that the longitude degrees have a different accuracy in degrees which changes with latitude and is largest at the poles.
563   */
564  public Double getCoordinateAccuracy() {
565    return coordinateAccuracy;
566  }
567
568  public void setCoordinateAccuracy(@Nullable Double coordinateAccuracy) {
569    this.coordinateAccuracy = coordinateAccuracy;
570  }
571
572  /**
573   * The geodetic datum for the interpreted decimal coordinates.
574   * This is always WGS84 if there a coordinate exists as we reproject other datums into WGS84.
575   */
576  @Nullable
577  public String getGeodeticDatum() {
578    if (decimalLatitude != null) {
579      return GEO_DATUM;
580    }
581    return null;
582  }
583
584  /**
585   * This private method is needed for jackson deserialization only.
586   */
587  private void setGeodeticDatum(String datum) {
588    // ignore, we have a static WGS84 value
589  }
590
591
592  @Nullable
593  /**
594   * Elevation in meters usually above sea level (altitude).
595   * </br>
596   * The elevation is calculated using the equation: (minimumElevationInMeters + maximumElevationInMeters) / 2.
597   */
598  public Double getElevation() {
599    return elevation;
600  }
601
602  public void setElevation(@Nullable Double elevation) {
603    this.elevation = elevation;
604  }
605
606  /**
607   * Elevation accuracy is the uncertainty for the elevation in meters.
608   * </br>
609   * The elevation accuracy is calculated using the equation: (maximumElevationInMeters - minimumElevationInMeters) / 2
610   */
611  @Nullable
612  public Double getElevationAccuracy() {
613    return elevationAccuracy;
614  }
615
616  public void setElevationAccuracy(@Nullable Double elevationAccuracy) {
617    this.elevationAccuracy = elevationAccuracy;
618  }
619
620  @Nullable
621  /**
622   * Depth in meters below the surface. Complimentary to elevation, the depth can be 10 meters below the surface of a
623   * lake in 1100m (=elevation).
624   * </br>
625   * The depth is calculated using the equation: (minimumDepthInMeters + maximumDepthInMeters) / 2.
626   */
627  public Double getDepth() {
628    return depth;
629  }
630
631  public void setDepth(@Nullable Double depth) {
632    this.depth = depth;
633  }
634
635  /**
636   * Depth accuracy is the uncertainty for the depth in meters.
637   * </br>
638   * The depth accuracy is calculated using the equation: (maximumDepthInMeters - minimumDepthInMeters) / 2
639   */
640  @Nullable
641  public Double getDepthAccuracy() {
642    return depthAccuracy;
643  }
644
645  public void setDepthAccuracy(@Nullable Double depthAccuracy) {
646    this.depthAccuracy = depthAccuracy;
647  }
648
649  @Nullable
650  public Continent getContinent() {
651    return continent;
652  }
653
654  public void setContinent(@Nullable Continent continent) {
655    this.continent = continent;
656  }
657
658  @Nullable
659  @JsonProperty("countryCode")
660  public Country getCountry() {
661    return country;
662  }
663
664  public void setCountry(@Nullable Country country) {
665    this.country = country;
666  }
667
668  /**
669   * Renders the country title as json property country in addition to the iso 2 letter countryCode being
670   * serialized by the regular country java property.
671   * Made private to only use it for json serialization and not within java code.
672   */
673  @Nullable
674  @JsonProperty("country")
675  private String getCountryTitle() {
676    return country == null ? null : country.getTitle();
677  }
678
679  private void setCountryTitle(String country) {
680    // ignore, setter only to avoid json being written into the fields map
681  }
682
683  @Nullable
684  public String getStateProvince() {
685    return stateProvince;
686  }
687
688  public void setStateProvince(@Nullable String stateProvince) {
689    this.stateProvince = stateProvince;
690  }
691
692  @Nullable
693  public String getWaterBody() {
694    return waterBody;
695  }
696
697  public void setWaterBody(@Nullable String waterBody) {
698    this.waterBody = waterBody;
699  }
700
701  /**
702   * The full year of the event date.
703   *
704   * @return the year of the event date
705   */
706  @Min(1500)
707  @Max(2020)
708  @Nullable
709  public Integer getYear() {
710    return year;
711  }
712
713  public void setYear(@Nullable Integer year) {
714    this.year = year;
715  }
716
717  /**
718   * The month of the year of the event date starting with zero for january following {@link Date}.
719   *
720   * @return the month of the event date
721   */
722  @Min(1)
723  @Max(12)
724  @Nullable
725  public Integer getMonth() {
726    return month;
727  }
728
729  public void setMonth(@Nullable Integer month) {
730    this.month = month;
731  }
732
733  /**
734   * The day of the month of the event date.
735   *
736   * @return the day of the event date
737   */
738  @Min(1)
739  @Max(31)
740  @Nullable
741  public Integer getDay() {
742    return day;
743  }
744
745  public void setDay(@Nullable Integer day) {
746    this.day = day;
747  }
748
749  @Nullable
750  /**
751   * The date the occurrence was recorded or collected.
752   */
753  public Date getEventDate() {
754    return eventDate == null ? null : new Date(eventDate.getTime());
755  }
756
757  public void setEventDate(@Nullable Date eventDate) {
758    this.eventDate = eventDate == null ? null : new Date(eventDate.getTime());
759  }
760
761  @Nullable
762  public TypeStatus getTypeStatus() {
763    return typeStatus;
764  }
765
766  public void setTypeStatus(@Nullable TypeStatus typeStatus) {
767    this.typeStatus = typeStatus;
768  }
769
770  @Nullable
771  /**
772   * The scientific name the type status of this specimen applies to.
773   */
774  public String getTypifiedName() {
775    return typifiedName;
776  }
777
778  public void setTypifiedName(@Nullable String typifiedName) {
779    this.typifiedName = typifiedName;
780  }
781
782  @NotNull
783  /**
784   * A set of issues found for this occurrence.
785   */
786  public Set<OccurrenceIssue> getIssues() {
787    return issues;
788  }
789
790  public void setIssues(Set<OccurrenceIssue> issues) {
791    Preconditions.checkNotNull("Issues cannot be null", issues);
792    this.issues = Sets.newEnumSet(issues, OccurrenceIssue.class);
793  }
794
795  public void addIssue(OccurrenceIssue issue) {
796    Preconditions.checkNotNull("Issue needs to be specified", issue);
797    issues.add(issue);
798  }
799
800  @Nullable
801  /**
802   * The interpreted dc:modified from the verbatim source data.
803   * Ideally indicating when a record was last modified in the source.
804   */
805  public Date getModified() {
806    return modified == null ? null : new Date(modified.getTime());
807  }
808
809  public void setModified(@Nullable Date modified) {
810    this.modified = modified == null ? null : new Date(modified.getTime());
811  }
812
813  @Nullable
814  /**
815   * The date this occurrence last went through the interpretation phase of the GBIF indexing.
816   */
817  public Date getLastInterpreted() {
818    return lastInterpreted == null ? null : new Date(lastInterpreted.getTime());
819  }
820
821  public void setLastInterpreted(@Nullable Date lastInterpreted) {
822    this.lastInterpreted = lastInterpreted == null ? null : new Date(lastInterpreted.getTime());
823  }
824
825  @Nullable
826  /**
827   * An external link to more details, the records "homepage".
828   */
829  public URI getReferences() {
830    return references;
831  }
832
833  public void setReferences(URI references) {
834    this.references = references;
835  }
836
837  /**
838   * Applied license to the occurrence record or dataset to which this record belongs to.
839   */
840  @NotNull
841  public License getLicense() {
842    return license;
843  }
844
845  public void setLicense(License license) {
846    this.license = license;
847  }
848
849  @NotNull
850  public List<Identifier> getIdentifiers() {
851    return identifiers;
852  }
853
854  public void setIdentifiers(List<Identifier> identifiers) {
855    this.identifiers = identifiers;
856  }
857
858  @NotNull
859  public List<MediaObject> getMedia() {
860    return media;
861  }
862
863  public void setMedia(List<MediaObject> media) {
864    this.media = media;
865  }
866
867  @NotNull
868  public List<FactOrMeasurment> getFacts() {
869    return facts;
870  }
871
872  public void setFacts(List<FactOrMeasurment> facts) {
873    this.facts = facts;
874  }
875
876  @NotNull
877  public List<OccurrenceRelation> getRelations() {
878    return relations;
879  }
880
881  public void setRelations(List<OccurrenceRelation> relations) {
882    this.relations = relations;
883  }
884
885
886  @JsonIgnore
887  /**
888   * Convenience method checking if any spatial validation rule has not passed.
889   * Primarily used to indicate that the record should not be displayed on a map.
890   */
891  public boolean hasSpatialIssue() {
892    for (OccurrenceIssue rule : OccurrenceIssue.GEOSPATIAL_RULES) {
893      if (issues.contains(rule)) {
894        return true;
895      }
896    }
897    return false;
898  }
899
900  @Override
901  public int hashCode() {
902    return Objects
903      .hashCode(basisOfRecord, individualCount, sex, lifeStage, establishmentMeans, taxonKey, kingdomKey, phylumKey,
904        classKey, orderKey, familyKey, genusKey, subgenusKey, speciesKey, scientificName, kingdom, phylum, clazz,
905        order, family, genus, subgenus, species, genericName, specificEpithet, infraspecificEpithet, taxonRank,
906        dateIdentified, year, month, day, eventDate, decimalLongitude, decimalLatitude, coordinatePrecision,
907        coordinateUncertaintyInMeters, elevation, elevationAccuracy, depth, depthAccuracy,
908        continent, country, stateProvince, waterBody, typeStatus, typifiedName, issues, modified,
909        lastInterpreted, references, identifiers, media, facts, relations, license);
910  }
911
912  @Override
913  public boolean equals(Object obj) {
914    if (this == obj) {
915      return true;
916    }
917    if (!(obj instanceof Occurrence)) {
918      return false;
919    }
920    if (!super.equals(obj)) {
921      return false;
922    }
923    Occurrence that = (Occurrence) obj;
924    return Objects.equal(this.basisOfRecord, that.basisOfRecord)
925      && Objects.equal(this.individualCount, that.individualCount)
926      && Objects.equal(this.sex, that.sex)
927      && Objects.equal(this.lifeStage, that.lifeStage)
928      && Objects.equal(this.establishmentMeans, that.establishmentMeans)
929      && Objects.equal(this.taxonKey, that.taxonKey)
930      && Objects.equal(this.kingdomKey, that.kingdomKey)
931      && Objects.equal(this.phylumKey, that.phylumKey)
932      && Objects.equal(this.classKey, that.classKey)
933      && Objects.equal(this.orderKey, that.orderKey)
934      && Objects.equal(this.familyKey, that.familyKey)
935      && Objects.equal(this.genusKey, that.genusKey)
936      && Objects.equal(this.subgenusKey, that.subgenusKey)
937      && Objects.equal(this.speciesKey, that.speciesKey)
938      && Objects.equal(this.scientificName, that.scientificName)
939      && Objects.equal(this.kingdom, that.kingdom)
940      && Objects.equal(this.phylum, that.phylum)
941      && Objects.equal(this.clazz, that.clazz)
942      && Objects.equal(this.order, that.order)
943      && Objects.equal(this.family, that.family)
944      && Objects.equal(this.genus, that.genus)
945      && Objects.equal(this.subgenus, that.subgenus)
946      && Objects.equal(this.species, that.species)
947      && Objects.equal(this.genericName, that.genericName)
948      && Objects.equal(this.specificEpithet, that.specificEpithet)
949      && Objects.equal(this.infraspecificEpithet, that.infraspecificEpithet)
950      && Objects.equal(this.taxonRank, that.taxonRank)
951      && Objects.equal(this.dateIdentified, that.dateIdentified)
952      && Objects.equal(this.year, that.year)
953      && Objects.equal(this.month, that.month)
954      && Objects.equal(this.day, that.day)
955      && Objects.equal(this.eventDate, that.eventDate)
956      && Objects.equal(this.decimalLongitude, that.decimalLongitude)
957      && Objects.equal(this.decimalLatitude, that.decimalLatitude)
958      && Objects.equal(this.coordinatePrecision, that.coordinatePrecision)
959      && Objects.equal(this.coordinateUncertaintyInMeters, that.coordinateUncertaintyInMeters)
960      && Objects.equal(this.elevation, that.elevation)
961      && Objects.equal(this.elevationAccuracy, that.elevationAccuracy)
962      && Objects.equal(this.depth, that.depth)
963      && Objects.equal(this.depthAccuracy, that.depthAccuracy)
964      && Objects.equal(this.continent, that.continent)
965      && Objects.equal(this.country, that.country)
966      && Objects.equal(this.stateProvince, that.stateProvince)
967      && Objects.equal(this.waterBody, that.waterBody)
968      && Objects.equal(this.typeStatus, that.typeStatus)
969      && Objects.equal(this.typifiedName, that.typifiedName)
970      && Objects.equal(this.issues, that.issues)
971      && Objects.equal(this.modified, that.modified)
972      && Objects.equal(this.lastInterpreted, that.lastInterpreted)
973      && Objects.equal(this.references, that.references)
974      && Objects.equal(this.identifiers, that.identifiers)
975      && Objects.equal(this.media, that.media)
976      && Objects.equal(this.facts, that.facts)
977      && Objects.equal(this.relations, that.relations)
978      && Objects.equal(this.license, that.license);
979  }
980
981  @Override
982  public String toString() {
983    return super.toString() + Objects.toStringHelper(this)
984      .add("basisOfRecord", basisOfRecord)
985      .add("individualCount", individualCount)
986      .add("sex", sex)
987      .add("lifeStage", lifeStage)
988      .add("establishmentMeans", establishmentMeans)
989      .add("taxonKey", taxonKey)
990      .add("kingdomKey", kingdomKey)
991      .add("phylumKey", phylumKey)
992      .add("classKey", classKey)
993      .add("orderKey", orderKey)
994      .add("familyKey", familyKey)
995      .add("genusKey", genusKey)
996      .add("subgenusKey", subgenusKey)
997      .add("speciesKey", speciesKey)
998      .add("scientificName", scientificName)
999      .add("kingdom", kingdom)
1000      .add("phylum", phylum)
1001      .add("clazz", clazz)
1002      .add("order", order)
1003      .add("family", family)
1004      .add("genus", genus)
1005      .add("subgenus", subgenus)
1006      .add("species", species)
1007      .add("genericName", genericName)
1008      .add("specificEpithet", specificEpithet)
1009      .add("infraspecificEpithet", infraspecificEpithet)
1010      .add("taxonRank", taxonRank)
1011      .add("dateIdentified", dateIdentified)
1012      .add("decimalLongitude", decimalLongitude)
1013      .add("decimalLatitude", decimalLatitude)
1014      .add("coordinatePrecision", coordinatePrecision)
1015      .add("coordinateUncertaintyInMeters", coordinateUncertaintyInMeters)
1016      .add("coordinateAccuracy", coordinateAccuracy)
1017      .add("elevation", elevation)
1018      .add("elevationAccuracy", elevationAccuracy)
1019      .add("depth", depth)
1020      .add("depthAccuracy", depthAccuracy)
1021      .add("continent", continent)
1022      .add("country", country)
1023      .add("stateProvince", stateProvince)
1024      .add("waterBody", waterBody)
1025      .add("year", year)
1026      .add("month", month)
1027      .add("day", day)
1028      .add("eventDate", eventDate)
1029      .add("typeStatus", typeStatus)
1030      .add("typifiedName", typifiedName)
1031      .add("issues", issues)
1032      .add("modified", modified)
1033      .add("lastInterpreted", lastInterpreted)
1034      .add("references", references)
1035      .add("license", license)
1036      .toString();
1037  }
1038
1039  /**
1040   * This private method is only for serialization via jackson and not exposed anywhere else!
1041   * It maps the verbatimField terms into properties with their simple name or qualified names for UnknownTerms.
1042   */
1043  @JsonAnyGetter
1044  private Map<String, String> jsonVerbatimFields() {
1045    Map<String, String> extendedProps = Maps.newHashMap();
1046    for (Map.Entry<Term, String> prop : getVerbatimFields().entrySet()) {
1047      Term t = prop.getKey();
1048      if (t instanceof UnknownTerm || PROPERTIES.contains(t.simpleName())) {
1049        extendedProps.put(t.qualifiedName(), prop.getValue());
1050      } else {
1051        // render all terms in controlled enumerations as simple names only - unless we have a property of that name already!
1052        extendedProps.put(t.simpleName(), prop.getValue());
1053      }
1054    }
1055    return extendedProps;
1056  }
1057}