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