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.vocabulary;
015
016import static org.gbif.api.vocabulary.InterpretationRemarkSeverity.ERROR;
017import static org.gbif.api.vocabulary.InterpretationRemarkSeverity.INFO;
018import static org.gbif.api.vocabulary.InterpretationRemarkSeverity.WARNING;
019
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Set;
025
026import org.apache.commons.lang3.ArrayUtils;
027
028import org.gbif.api.util.AnnotationUtils;
029import org.gbif.dwc.terms.DcTerm;
030import org.gbif.dwc.terms.DwcTerm;
031import org.gbif.dwc.terms.Term;
032
033/** An enumeration of validation rules for single occurrence records. */
034public enum OccurrenceIssue implements InterpretationRemark {
035
036  /** Coordinate is the exact 0°, 0° coordinate, often indicating a bad null coordinate. */
037  ZERO_COORDINATE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
038
039  /**
040   * Coordinate has a latitude and/or longitude value beyond the maximum (or minimum) decimal value.
041   */
042  COORDINATE_OUT_OF_RANGE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
043
044  /** Coordinate value is given in some form but GBIF is unable to interpret it. */
045  COORDINATE_INVALID(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
046
047  /** Original coordinate modified by rounding to 5 decimals. */
048  COORDINATE_ROUNDED(INFO, TermsGroup.COORDINATES_TERMS_NO_DATUM),
049
050  /** The geodetic datum given could not be interpreted. */
051  GEODETIC_DATUM_INVALID(WARNING, DwcTerm.geodeticDatum),
052
053  /**
054   * Indicating that the interpreted coordinates assume they are based on WGS84 datum as the datum
055   * was either not indicated or interpretable. See GEODETIC_DATUM_INVALID.
056   */
057  GEODETIC_DATUM_ASSUMED_WGS84(INFO, DwcTerm.geodeticDatum),
058
059  /**
060   * The original coordinate was successfully reprojected from a different geodetic datum to WGS84.
061   */
062  COORDINATE_REPROJECTED(INFO, TermsGroup.COORDINATES_TERMS),
063
064  /**
065   * The given decimal latitude and longitude could not be reprojected to WGS84 based on the
066   * provided datum.
067   */
068  COORDINATE_REPROJECTION_FAILED(WARNING, TermsGroup.COORDINATES_TERMS),
069
070  /**
071   * Indicates successful coordinate reprojection according to provided datum, but which results in
072   * a datum shift larger than 0.1 decimal degrees.
073   */
074  COORDINATE_REPROJECTION_SUSPICIOUS(WARNING, TermsGroup.COORDINATES_TERMS),
075
076  /**
077   * Indicates an invalid or very unlikely coordinate accuracy derived from precision or uncertainty
078   * in meters.
079   */
080  @Deprecated // see POR-3061
081  COORDINATE_ACCURACY_INVALID(WARNING),
082
083  /** Indicates an invalid or very unlikely coordinatePrecision */
084  COORDINATE_PRECISION_INVALID(WARNING, DwcTerm.coordinatePrecision),
085
086  /** Indicates an invalid or very unlikely dwc:uncertaintyInMeters. */
087  COORDINATE_UNCERTAINTY_METERS_INVALID(WARNING, DwcTerm.coordinateUncertaintyInMeters),
088
089  /** There is a mismatch between coordinate uncertainty in meters and coordinate precision. */
090  @Deprecated // see POR-1804
091  COORDINATE_PRECISION_UNCERTAINTY_MISMATCH(WARNING),
092
093  /** The Footprint Spatial Reference System given could not be interpreted. */
094  FOOTPRINT_SRS_INVALID(WARNING, DwcTerm.footprintSRS),
095
096  /**
097   * The Footprint Well-Known-Text conflicts with the interpreted coordinates
098   * (Decimal Latitude, Decimal Longitude etc).
099   */
100  FOOTPRINT_WKT_MISMATCH(WARNING, DwcTerm.footprintWKT),
101
102  /** The Footprint Well-Known-Text given could not be interpreted. */
103  FOOTPRINT_WKT_INVALID(WARNING, DwcTerm.footprintWKT),
104
105  /** The interpreted occurrence coordinates fall outside of the indicated country. */
106  COUNTRY_COORDINATE_MISMATCH(WARNING, TermsGroup.COORDINATES_COUNTRY_TERMS),
107
108  /** Interpreted country for dwc:country and dwc:countryCode contradict each other. */
109  COUNTRY_MISMATCH(WARNING, TermsGroup.COUNTRY_TERMS),
110
111  /** Uninterpretable country values found. */
112  COUNTRY_INVALID(WARNING, TermsGroup.COUNTRY_TERMS),
113
114  /** The interpreted country is based on the coordinates, not the verbatim string information. */
115  COUNTRY_DERIVED_FROM_COORDINATES(WARNING, TermsGroup.COORDINATES_COUNTRY_TERMS),
116
117  /** The interpreted occurrence coordinates fall outside of the indicated continent. */
118  CONTINENT_COORDINATE_MISMATCH(WARNING),
119
120  /** The interpreted continent and country do not match. */
121  CONTINENT_COUNTRY_MISMATCH(WARNING),
122
123  /** Uninterpretable continent values found. */
124  CONTINENT_INVALID(WARNING),
125
126  /** The interpreted continent is based on the country, not the verbatim string information. */
127  CONTINENT_DERIVED_FROM_COUNTRY(WARNING),
128
129  /** The interpreted continent is based on the coordinates, not the verbatim string information. */
130  CONTINENT_DERIVED_FROM_COORDINATES(WARNING),
131
132  /** Latitude and longitude appear to be swapped. */
133  PRESUMED_SWAPPED_COORDINATE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
134
135  /** Longitude appears to be negated, e.g. 32.3 instead of -32.3 */
136  PRESUMED_NEGATED_LONGITUDE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
137
138  /** Latitude appears to be negated, e.g. 32.3 instead of -32.3 */
139  PRESUMED_NEGATED_LATITUDE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
140
141  /**
142   * The recorded date specified as the eventDate string and the individual year, month, day and/or
143   * startDayOfYear, endDayOfYear are contradictory.
144   */
145  RECORDED_DATE_MISMATCH(WARNING, TermsGroup.RECORDED_DATE_TERMS),
146
147  /** A (partial) invalid date is given, such as a non-existent date, zero month, etc. */
148  RECORDED_DATE_INVALID(WARNING, TermsGroup.RECORDED_DATE_TERMS),
149
150  /**
151   * The recorded date is highly unlikely, falling either into the future or representing a very old
152   * date before 1600 thus predating modern taxonomy.
153   */
154  RECORDED_DATE_UNLIKELY(WARNING, TermsGroup.RECORDED_DATE_TERMS),
155
156  /**
157   * Matching to the taxonomic backbone can only be done using a fuzzy, non-exact match.
158   */
159  TAXON_MATCH_FUZZY(WARNING, TermsGroup.TAXONOMY_TERMS),
160
161  /**
162   * Matching to the taxonomic backbone can only be done on a higher rank and not the scientific
163   * name.
164   */
165  TAXON_MATCH_HIGHERRANK(WARNING, TermsGroup.TAXONOMY_TERMS),
166
167  /**
168   * Matching to the taxonomic backbone can only be done on a species level,
169   * but the occurrence was in fact considered a broader species aggregate/complex.
170   *
171   * @see <a href="https://github.com/gbif/portal-feedback/issues/2935">gbif/portal-feedback#2935</a>
172   */
173  TAXON_MATCH_AGGREGATE(WARNING, TermsGroup.TAXONOMY_TERMS),
174
175  /**
176   * The scientificNameID was not used when mapping the record to the GBIF backbone. This may indicate one of
177   * <ul>
178   *   <li>The ID uses a pattern not configured for use by GBIF</li>
179   *   <li>The ID did not uniquely(!) identify a concept in the checklist</li>
180   *   <li>The ID found a concept in the checklist which did not map to the backbone</li>
181   *   <li>A different ID was used, or the record names were used as no ID lookup successfully linked to the backbone</li>
182   * </ul>
183   *
184   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
185   */
186  TAXON_MATCH_SCIENTIFIC_NAME_ID_IGNORED(INFO, DwcTerm.scientificNameID),
187
188  /**
189   * The taxonConceptID was not used when mapping the record to the GBIF backbone. This may indicate one of
190   * <ul>
191   *   <li>The ID uses a pattern not configured for use by GBIF</li>
192   *   <li>The ID did not uniquely(!) identify a concept in the checklist</li>
193   *   <li>The ID found a concept in the checklist which did not map to the backbone</li>
194   *   <li>A different ID was used, or the record names were used as no ID lookup successfully linked to the backbone</li>
195   * </ul>
196   *
197   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
198   */
199  TAXON_MATCH_TAXON_CONCEPT_ID_IGNORED(INFO, DwcTerm.taxonConceptID),
200
201  /**
202   * The taxonID was not used when mapping the record to the GBIF backbone. This may indicate one of
203   *
204   * <ul>
205   *   <li>The ID uses a pattern not configured for use by GBIF</li>
206   *   <li>The ID did not uniquely(!) identify a concept in the checklist</li>
207   *   <li>The ID found a concept in the checklist which did not map to the backbone</li>
208   *   <li>A different ID was used, or the record names were used as no ID lookup successfully linked to the backbone</li>
209   * </ul>
210   *
211   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
212   */
213  TAXON_MATCH_TAXON_ID_IGNORED(INFO, DwcTerm.taxonID),
214
215  /**
216   * The scientificNameID matched a known pattern, but it was not found in the associated checklist.
217   * The backbone lookup was performed using either the names or a different ID on the record.
218   * This may indicate a poorly formatted identifier or may be caused by a newly created ID that
219   * isn't yet known in the version of the published checklist.
220   *
221   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
222   */
223  SCIENTIFIC_NAME_ID_NOT_FOUND(WARNING, DwcTerm.scientificNameID),
224
225  /**
226   * The taxonConceptID matched a known pattern, but it was not found in the associated checklist.
227   * The backbone lookup was performed using either the names or a different ID on the record.
228   * This may indicate a poorly formatted identifier or may be caused by a newly created ID that isn't yet
229   * known in the version of the published checklist.
230   *
231   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
232   */
233  TAXON_CONCEPT_ID_NOT_FOUND(WARNING, DwcTerm.taxonConceptID),
234
235  /**
236   * The taxonID matched a known pattern, but it was not found in the associated checklist.
237   * The backbone lookup was performed using either the names or a different ID on the record.
238   * This may indicate a poorly formatted identifier or may be caused by a newly created ID that isn't yet
239   * known in the version of the published checklist.
240   *
241   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
242   */
243  TAXON_ID_NOT_FOUND(WARNING, DwcTerm.taxonID),
244
245  /**
246   * The scientificName provided in the occurrence record does not precisely match the name in the
247   * registered checklist when using the scientificNameID, taxonID or taxonConceptID to look it up.
248   * Publishers are advised to check the IDs are correct, or update the formatting of the names on
249   * their records.
250   *
251   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
252   */
253  SCIENTIFIC_NAME_AND_ID_INCONSISTENT(
254      WARNING,
255      DwcTerm.scientificNameID,
256      DwcTerm.taxonID,
257      DwcTerm.taxonConceptID,
258      DwcTerm.scientificName),
259
260  /**
261   * Matching to the taxonomic backbone cannot be done because there was no match at all, or several
262   * matches with too little information to keep them apart (potentially homonyms).
263   */
264  TAXON_MATCH_NONE(WARNING, TermsGroup.TAXONOMY_TERMS),
265
266  /**
267   * The GBIF Backbone concept was found using the scientificNameID, taxonID or taxonConceptID, but
268   * it differs from what would have been found if the classification names on the record were used.
269   * This may indicate a gap in the GBIF backbone, a poor mapping between the checklist and the
270   * backbone, or a mismatch between the classification names and the declared IDs (scientificNameID
271   * or taxonConceptID) on the occurrence record itself.
272   *
273   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
274   */
275  TAXON_MATCH_NAME_AND_ID_AMBIGUOUS(WARNING, TermsGroup.TAXONOMY_TERMS),
276
277  /**
278   * Set if supplied depth is not given in the metric system, for example using feet instead of
279   * meters
280   */
281  DEPTH_NOT_METRIC(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
282
283  /** Set if depth is larger than 11,000m or negative. */
284  DEPTH_UNLIKELY(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
285
286  /** Set if supplied minimum depth > maximum depth */
287  DEPTH_MIN_MAX_SWAPPED(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
288
289  /** Set if depth is a non-numeric value */
290  DEPTH_NON_NUMERIC(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
291
292  /** Set if elevation is above the troposphere (17km) or below 11km (Mariana Trench). */
293  ELEVATION_UNLIKELY(WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
294
295  /** Set if supplied minimum elevation > maximum elevation */
296  ELEVATION_MIN_MAX_SWAPPED(
297      WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
298
299  /**
300   * Set if supplied elevation is not given in the metric system, for example using feet instead of
301   * meters
302   */
303  ELEVATION_NOT_METRIC(WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
304
305  /** Set if elevation is a non-numeric value */
306  ELEVATION_NON_NUMERIC(
307      WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
308
309  /**
310   * A (partial) invalid date is given for dc:modified, such as a nonexistent date, zero month, etc.
311   */
312  MODIFIED_DATE_INVALID(WARNING, DcTerm.modified),
313
314  /** The date given for dc:modified is in the future or predates Unix time (1970). */
315  MODIFIED_DATE_UNLIKELY(WARNING, DcTerm.modified),
316
317  /** The date given for dwc:dateIdentified is in the future or before Linnean times (1700). */
318  IDENTIFIED_DATE_UNLIKELY(WARNING, DwcTerm.dateIdentified),
319
320  /** The date given for dwc:dateIdentified is invalid and can't be interpreted at all. */
321  IDENTIFIED_DATE_INVALID(WARNING, DwcTerm.dateIdentified),
322
323  /**
324   * The given basis of record is impossible to interpret or significantly different from the
325   * recommended vocabulary.
326   */
327  BASIS_OF_RECORD_INVALID(WARNING, DwcTerm.basisOfRecord),
328
329  /**
330   * The given type status is impossible to interpret or significantly different from the
331   * recommended vocabulary.
332   */
333  TYPE_STATUS_INVALID(WARNING, DwcTerm.typeStatus),
334
335  /**
336   * The given type status contains some words that express uncertainty.
337   */
338  SUSPECTED_TYPE(WARNING, DwcTerm.typeStatus),
339
340  /** An invalid date is given for dc:created of a multimedia object. */
341  MULTIMEDIA_DATE_INVALID(WARNING),
342
343  /** An invalid URI is given for a multimedia object. */
344  MULTIMEDIA_URI_INVALID(WARNING),
345
346  /** An invalid URI is given for dc:references. */
347  REFERENCES_URI_INVALID(WARNING, DcTerm.references),
348
349  /** An error occurred during interpretation, leaving the record interpretation incomplete. */
350  INTERPRETATION_ERROR(ERROR),
351
352  /** The individual count value is not a positive integer */
353  INDIVIDUAL_COUNT_INVALID(WARNING, DwcTerm.individualCount),
354
355  /** Example: individual count value > 0, but occurrence status is absent. */
356  INDIVIDUAL_COUNT_CONFLICTS_WITH_OCCURRENCE_STATUS(WARNING, DwcTerm.individualCount),
357
358  /** Occurrence status value can't be assigned to {@link OccurrenceStatus} */
359  OCCURRENCE_STATUS_UNPARSABLE(WARNING, DwcTerm.occurrenceStatus),
360
361  /** Occurrence status was inferred from the individual count value */
362  OCCURRENCE_STATUS_INFERRED_FROM_INDIVIDUAL_COUNT(WARNING, DwcTerm.occurrenceStatus),
363
364  /** Occurrence status was inferred from basis of records */
365  OCCURRENCE_STATUS_INFERRED_FROM_BASIS_OF_RECORD(WARNING, DwcTerm.occurrenceStatus),
366
367  /** The date given for dwc:georeferencedDate is in the future or before Linnean times (1700). */
368  GEOREFERENCED_DATE_UNLIKELY(WARNING, DwcTerm.georeferencedDate),
369
370  /** The date given for dwc:georeferencedDate is invalid and can't be interpreted at all. */
371  GEOREFERENCED_DATE_INVALID(WARNING, DwcTerm.georeferencedDate),
372
373  /** The given institution matches with more than 1 GRSciColl institution. */
374  AMBIGUOUS_INSTITUTION(INFO, TermsGroup.INSTITUTION_TERMS),
375
376  /** The given collection matches with more than 1 GRSciColl collection. */
377  AMBIGUOUS_COLLECTION(INFO, TermsGroup.COLLECTION_TERMS),
378
379  /** The given institution couldn't be matched with any GRSciColl institution. */
380  INSTITUTION_MATCH_NONE(INFO, TermsGroup.INSTITUTION_TERMS),
381
382  /** The given collection couldn't be matched with any GRSciColl collection. */
383  COLLECTION_MATCH_NONE(INFO, TermsGroup.COLLECTION_TERMS),
384
385  /**
386   * The given institution was fuzzily matched to a GRSciColl institution. This can happen when
387   * either the code or the ID don't match or when the institution name is used instead of the code.
388   */
389  INSTITUTION_MATCH_FUZZY(INFO, TermsGroup.INSTITUTION_TERMS),
390
391  /**
392   * The given collection was fuzzily matched to a GRSciColl collection. This can happen when either
393   * the code or the ID don't match or when the collection name is used instead of the code.
394   */
395  COLLECTION_MATCH_FUZZY(INFO, TermsGroup.COLLECTION_TERMS),
396
397  /** The collection matched doesn't belong to the institution matched. */
398  INSTITUTION_COLLECTION_MISMATCH(
399      INFO, ArrayUtils.addAll(TermsGroup.INSTITUTION_TERMS, TermsGroup.INSTITUTION_TERMS)),
400
401  /**
402   * The given owner institution is different than the given institution. Therefore we assume it
403   * could be on loan and we don't link it to the occurrence.
404   *
405   * <p>Deprecated by {@link #DIFFERENT_OWNER_INSTITUTION}.
406   */
407  @Deprecated
408  POSSIBLY_ON_LOAN(INFO, TermsGroup.INSTITUTION_TERMS),
409
410  /**
411   * The given owner institution is different than the given institution. Therefore we assume it
412   * doesn't belong to the institution and we don't link it to the occurrence.
413   */
414  DIFFERENT_OWNER_INSTITUTION(INFO, TermsGroup.INSTITUTION_TERMS),
415
416  /** Era or erathem was inferred from a parent rank. */
417  ERA_OR_ERATHEM_INFERRED_FROM_PARENT_RANK(
418      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
419  /** Period or system was inferred from a parent rank. */
420  PERIOD_OR_SYSTEM_INFERRED_FROM_PARENT_RANK(
421      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
422  /** Epoch or series was inferred from a parent rank. */
423  EPOCH_OR_SERIES_INFERRED_FROM_PARENT_RANK(
424      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
425  /** Age or stage was inferred from a parent rank. */
426  AGE_OR_STAGE_INFERRED_FROM_PARENT_RANK(
427      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
428
429  /** The eon or eonothem provided belongs to another rank. */
430  EON_OR_EONOTHEM_RANK_MISMATCH(
431      INFO, DwcTerm.earliestEonOrLowestEonothem, DwcTerm.latestEonOrHighestEonothem),
432  /** The era or erathem provided belongs to another rank. */
433  ERA_OR_ERATHEM_RANK_MISMATCH(
434      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
435  /** The period or system provided belongs to another rank. */
436  PERIOD_OR_SYSTEM_RANK_MISMATCH(
437      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
438  /** The period or series provided belongs to another rank. */
439  EPOCH_OR_SERIES_RANK_MISMATCH(
440      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
441  /** The age or stage provided belongs to another rank. */
442  AGE_OR_STAGE_RANK_MISMATCH(
443      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
444
445  /** The earliest eon or eonothem has to be earlier than the latest. */
446  EON_OR_EONOTHEM_INVALID_RANGE(
447      INFO, DwcTerm.earliestEonOrLowestEonothem, DwcTerm.latestEonOrHighestEonothem),
448  /** The era or erathem has to be earlier than the latest. */
449  ERA_OR_ERATHEM_INVALID_RANGE(
450      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
451  /** The period or system has to be earlier than the latest. */
452  PERIOD_OR_SYSTEM_INVALID_RANGE(
453      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
454  /** The period or series has to be earlier than the latest. */
455  EPOCH_OR_SERIES_INVALID_RANGE(
456      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
457  /** The age or stage has to be earlier than the latest. */
458  AGE_OR_STAGE_INVALID_RANGE(
459      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
460
461  /** The era or erathem don't belong to the eon or eonothem. */
462  EON_OR_EONOTHEM_AND_ERA_OR_ERATHEM_MISMATCH(
463      INFO,
464      DwcTerm.earliestEonOrLowestEonothem,
465      DwcTerm.latestEonOrHighestEonothem,
466      DwcTerm.earliestEraOrLowestErathem,
467      DwcTerm.latestEraOrHighestErathem),
468
469  /** The period or system don't belong to the era or erathem. */
470  ERA_OR_ERATHEM_AND_PERIOD_OR_SYSTEM_MISMATCH(
471      INFO,
472      DwcTerm.earliestEraOrLowestErathem,
473      DwcTerm.latestEraOrHighestErathem,
474      DwcTerm.earliestPeriodOrLowestSystem,
475      DwcTerm.latestPeriodOrHighestSystem),
476
477  /** The epoch or series don't belong to the period or system. */
478  PERIOD_OR_SYSTEM_AND_EPOCH_OR_SERIES_MISMATCH(
479      INFO,
480      DwcTerm.earliestPeriodOrLowestSystem,
481      DwcTerm.latestPeriodOrHighestSystem,
482      DwcTerm.earliestEpochOrLowestSeries,
483      DwcTerm.latestEpochOrHighestSeries),
484
485  /** The age or stage don't belong to the epoch or series. */
486  EPOCH_OR_SERIES_AND_AGE_OR_STAGE_MISMATCH(
487      INFO,
488      DwcTerm.earliestEpochOrLowestSeries,
489      DwcTerm.latestEpochOrHighestSeries,
490      DwcTerm.earliestAgeOrLowestStage,
491      DwcTerm.latestAgeOrHighestStage);
492
493  /**
494   * Simple helper nested class to allow grouping of Term mostly to increase readability of this
495   * class.
496   */
497  private static class TermsGroup {
498
499    static final Term[] COORDINATES_TERMS_NO_DATUM = {
500      DwcTerm.decimalLatitude,
501      DwcTerm.decimalLongitude,
502      DwcTerm.verbatimLatitude,
503      DwcTerm.verbatimLongitude,
504      DwcTerm.verbatimCoordinates
505    };
506
507    static final Term[] COORDINATES_TERMS = {
508      DwcTerm.decimalLatitude,
509      DwcTerm.decimalLongitude,
510      DwcTerm.verbatimLatitude,
511      DwcTerm.verbatimLongitude,
512      DwcTerm.verbatimCoordinates,
513      DwcTerm.geodeticDatum
514    };
515
516    static final Term[] COUNTRY_TERMS = {DwcTerm.country, DwcTerm.countryCode};
517
518    static final Term[] COORDINATES_COUNTRY_TERMS = {
519      DwcTerm.decimalLatitude,
520      DwcTerm.decimalLongitude,
521      DwcTerm.verbatimLatitude,
522      DwcTerm.verbatimLongitude,
523      DwcTerm.verbatimCoordinates,
524      DwcTerm.geodeticDatum,
525      DwcTerm.country,
526      DwcTerm.countryCode
527    };
528
529    static final Term[] RECORDED_DATE_TERMS = {
530      DwcTerm.eventDate,
531      DwcTerm.year,
532      DwcTerm.month,
533      DwcTerm.day,
534      DwcTerm.startDayOfYear,
535      DwcTerm.endDayOfYear
536    };
537
538    static final Term[] TAXONOMY_TERMS = {
539      DwcTerm.kingdom,
540      DwcTerm.phylum,
541      DwcTerm.class_,
542      DwcTerm.order,
543      DwcTerm.family,
544      DwcTerm.genus,
545      DwcTerm.scientificName,
546      DwcTerm.scientificNameAuthorship,
547      DwcTerm.genericName,
548      DwcTerm.specificEpithet,
549      DwcTerm.infraspecificEpithet,
550      DwcTerm.scientificNameID,
551      DwcTerm.taxonConceptID,
552    };
553
554    static final Term[] INSTITUTION_TERMS = {
555      DwcTerm.institutionCode, DwcTerm.institutionID, DwcTerm.ownerInstitutionCode
556    };
557
558    static final Term[] COLLECTION_TERMS = {DwcTerm.collectionCode, DwcTerm.collectionID};
559  }
560
561  private final Set<Term> relatedTerms;
562  private final InterpretationRemarkSeverity severity;
563  private final boolean isDeprecated;
564
565  /** {@link OccurrenceIssue} not linked to any specific {@link Term}. */
566  OccurrenceIssue(InterpretationRemarkSeverity severity) {
567    this.severity = severity;
568    this.relatedTerms = Collections.emptySet();
569    this.isDeprecated = AnnotationUtils.isFieldDeprecated(OccurrenceIssue.class, this.name());
570  }
571
572  /** {@link OccurrenceIssue} linked to the provided {@link Term}. */
573  OccurrenceIssue(InterpretationRemarkSeverity severity, Term... relatedTerms) {
574    this.severity = severity;
575    this.relatedTerms = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(relatedTerms)));
576    this.isDeprecated = AnnotationUtils.isFieldDeprecated(OccurrenceIssue.class, this.name());
577  }
578
579  @Override
580  public String getId() {
581    return name();
582  }
583
584  @Override
585  public Set<Term> getRelatedTerms() {
586    return relatedTerms;
587  }
588
589  @Override
590  public InterpretationRemarkSeverity getSeverity() {
591    return severity;
592  }
593
594  @Override
595  public boolean isDeprecated() {
596    return isDeprecated;
597  }
598
599  /**
600   * All issues that indicate problems with the coordinates and thus should not be shown on maps.
601   */
602  public static final List<OccurrenceIssue> GEOSPATIAL_RULES =
603      Collections.unmodifiableList(
604          Arrays.asList(
605              ZERO_COORDINATE,
606              COORDINATE_OUT_OF_RANGE,
607              COORDINATE_INVALID,
608              COUNTRY_COORDINATE_MISMATCH,
609              PRESUMED_SWAPPED_COORDINATE,
610              PRESUMED_NEGATED_LONGITUDE,
611              PRESUMED_NEGATED_LATITUDE));
612}