Merge pull request #656 from inaturalist/obs-detail

ObsDetail : DQA
This commit is contained in:
Angie
2023-06-20 11:04:19 -07:00
committed by GitHub
12 changed files with 921 additions and 32 deletions

47
src/api/qualityMetrics.js Normal file
View File

@@ -0,0 +1,47 @@
// @flow
import inatjs from "inaturalistjs";
import handleError from "./error";
const setQualityMetric = async (
params: Object = {},
opts: Object = {}
): Promise<any> => {
try {
const response = await inatjs.observations.setQualityMetric( params, opts );
return response.results;
} catch ( e ) {
return handleError( e );
}
};
const deleteQualityMetric = async (
params: Object = {},
opts: Object = {}
): Promise<any> => {
try {
const { results } = await inatjs.observations.deleteQualityMetric( params, opts );
return results;
} catch ( e ) {
return handleError( e );
}
};
const fetchQualityMetrics = async (
params: Object = {},
opts: Object = {}
): Promise<any> => {
try {
const response = await inatjs.observations.qualityMetrics( params, opts );
return response.results;
} catch ( e ) {
return handleError( e );
}
};
export {
deleteQualityMetric,
fetchQualityMetrics,
setQualityMetric
};

View File

@@ -0,0 +1,120 @@
// @flow
import {
Body3,
INatIconButton
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import * as React from "react";
import { ActivityIndicator, useTheme } from "react-native-paper";
type Props = {
metric: string,
qualityMetrics: Object,
loadingAgree: boolean,
loadingDisagree: boolean,
loadingMetric: ?string,
setVote: Function,
removeVote: Function
}
const getUserVote = ( metric, qualityMetrics ) => {
if ( qualityMetrics ) {
const match = qualityMetrics.find( element => (
element.metric === metric && element.user_id ) );
if ( match ) {
return match.agree === true;
}
}
return null;
};
const renderVoteCount = ( status, metric, qualityMetrics ) => {
if ( !qualityMetrics ) return null;
const count = qualityMetrics
?.filter( qualityMetric => qualityMetric.agree === status && qualityMetric.metric === metric )
?.length;
if ( !count || count === 0 ) return null;
return <Body3 classname="ml-[5px]">{count}</Body3>;
};
const DQAVoteButtons = ( {
metric, qualityMetrics, loadingAgree, loadingDisagree, loadingMetric, setVote, removeVote
}: Props ): React.Node => {
const theme = useTheme( );
const userAgrees = getUserVote( metric, qualityMetrics );
const activityIndicatorOffset = "mx-[7px]";
const renderAgree = () => {
if ( loadingAgree && loadingMetric === metric ) {
return ( <ActivityIndicator size={33} className={activityIndicatorOffset} /> );
}
if ( userAgrees ) {
return (
<INatIconButton
icon="arrow-up-bold-circle"
size={33}
color={theme.colors.secondary}
onPress={() => removeVote( metric, true )}
/>
);
}
return (
<INatIconButton
icon="arrow-up-bold-circle-outline"
size={33}
onPress={() => setVote( metric, true )}
/>
);
};
const renderDisagree = () => {
if ( loadingDisagree && loadingMetric === metric ) {
return ( <ActivityIndicator size={30} className={activityIndicatorOffset} /> );
}
if ( userAgrees === null ) {
return (
<INatIconButton
icon="arrow-down-bold-circle-outline"
size={33}
onPress={() => setVote( metric, false )}
/>
);
}
if ( !userAgrees ) {
return (
<INatIconButton
icon="arrow-down-bold-circle"
size={33}
color={theme.colors.error}
onPress={() => removeVote( metric, false )}
/>
);
}
return (
<INatIconButton
icon="arrow-down-bold-circle-outline"
size={33}
onPress={() => setVote( metric, false )}
/>
);
};
return (
<View className="flex-row items-center justify-between w-[97px] space-x-[11px]">
<View className="flex-row items-center w-1/2">
{renderAgree()}
{renderVoteCount( true, metric, qualityMetrics )}
</View>
<View className="flex-row items-center w-1/2">
{renderDisagree()}
{renderVoteCount( false, metric, qualityMetrics )}
</View>
</View>
);
};
export default DQAVoteButtons;

View File

@@ -0,0 +1,415 @@
// @flow
import { useRoute } from "@react-navigation/native";
import { deleteQualityMetric, fetchQualityMetrics, setQualityMetric } from "api/qualityMetrics";
import DQAVoteButtons from "components/ObsDetails/DQAVoteButtons";
import PlaceholderText from "components/PlaceholderText";
import {
Body3,
BottomSheet,
Button,
Divider,
Heading4,
INatIcon,
List1,
List2,
ScrollViewWrapper
} from "components/SharedComponents";
import QualityGradeStatus from "components/SharedComponents/QualityGradeStatus/QualityGradeStatus";
import { View } from "components/styledComponents";
import { t } from "i18next";
import {
useEffect, useState
} from "react";
import * as React from "react";
import { useTheme } from "react-native-paper";
import useAuthenticatedMutation from "sharedHooks/useAuthenticatedMutation";
const titleOption = option => {
switch ( option ) {
case "research":
return t( "Data-quality-assessment-title-research" );
case "needs_id":
return t( "Data-quality-assessment-title-needs-id" );
default:
return t( "Data-quality-assessment-title-casual" );
}
};
const titleDescription = option => {
switch ( option ) {
case "research":
return t( "Data-quality-assessment-description-research" );
case "needs_id":
return t( "Data-quality-assessment-description-needs-id" );
default:
return t( "Data-quality-assessment-description-casual" );
}
};
const DataQualityAssessment = ( ): React.Node => {
const { params } = useRoute( );
const { observationUUID, observation, qualityGrade } = params;
const isResearchGrade = qualityGrade === "research";
const theme = useTheme( );
const sectionClass = "flex-row my-[14px] space-x-[11px]";
const voteClass = "flex-row mr-[15px] my-[7px] justify-between items-center";
const listTextClass = "flex-row space-x-[11px]";
const [qualityMetrics, setQualityMetrics] = useState( null );
const [loadingAgree, setLoadingAgree] = useState( false );
const [loadingDisagree, setLoadingDisagree] = useState( false );
const [loadingMetric, setLoadingMetric] = useState( null );
const [hideErrorSheet, setHideErrorSheet] = useState( true );
const fetchMetricsParams = {
id: observationUUID,
fields: "metric,agree,user_id",
ttl: -1
};
// destructured mutate to pass into useEffect to prevent infinite
// rerender and disabling eslint useEffect dependency rule
const { mutate } = useAuthenticatedMutation(
( qualityMetricParams, optsWithAuth ) => fetchQualityMetrics(
qualityMetricParams,
optsWithAuth
),
{
onSuccess: response => {
setLoadingMetric( null );
if ( loadingAgree ) {
setLoadingAgree( false );
}
if ( loadingDisagree ) {
setLoadingDisagree( false );
}
setQualityMetrics( response );
},
onError: () => {
setHideErrorSheet( false );
}
}
);
useEffect( ( ) => {
mutate( {
id: params.observationUUID,
fields: "metric,agree,user_id",
ttl: -1
} );
}, [mutate, params] );
const createQualityMetricMutation = useAuthenticatedMutation(
( qualityMetricParams, optsWithAuth ) => setQualityMetric( qualityMetricParams, optsWithAuth ),
{
onSuccess: () => {
// fetch updated quality metrics with updated votes
mutate( fetchMetricsParams );
},
onError: () => {
setHideErrorSheet( false );
}
}
);
const setMetricVote = ( metric, vote ) => {
const qualityMetricParams = {
id: observationUUID,
metric,
agree: vote,
ttyl: -1
};
setLoadingMetric( metric );
if ( vote ) {
setLoadingAgree( true );
} else {
setLoadingDisagree( true );
}
createQualityMetricMutation.mutate( qualityMetricParams );
};
const createRemoveQualityMetricMutation = useAuthenticatedMutation(
( qualityMetricParams, optsWithAuth ) => deleteQualityMetric(
qualityMetricParams,
optsWithAuth
),
{
onSuccess: () => {
// fetch updated quality metrics with updated votes
mutate( fetchMetricsParams );
},
onError: () => {
setHideErrorSheet( false );
}
}
);
const removeMetricVote = ( metric, vote ) => {
const qualityMetricParams = {
id: observationUUID,
metric,
ttyl: -1
};
setLoadingMetric( metric );
if ( vote ) {
setLoadingAgree( true );
} else {
setLoadingDisagree( true );
}
createRemoveQualityMetricMutation.mutate( qualityMetricParams );
};
const ifMajorityAgree = metric => {
if ( qualityMetrics ) {
const agreeCount = qualityMetrics.filter(
element => ( element.agree && element.metric === metric )
).length;
const disagreeCount = qualityMetrics.filter(
element => ( !element.agree && element.metric === metric )
).length;
return agreeCount >= disagreeCount;
}
return null;
};
const renderMetricIndicator = metric => {
const ifAgree = ifMajorityAgree( metric );
if ( ifAgree || ifAgree === null ) {
return (
<INatIcon name="checkmark-circle" size={19} color={theme.colors.secondary} /> );
}
return (
<INatIcon name="triangle-exclamation" size={19} color={theme.colors.error} />
);
};
const checkTest = metric => {
if ( observation ) {
if ( metric === "date" ) {
return observation[metric] !== null;
}
if ( metric === "location" ) {
const removedNull = observation[metric]
.filter( value => ( value !== null ) );
return removedNull.length !== 0;
}
if ( metric === "evidence" ) {
const removedEmpty = observation[metric]
.filter( value => ( Object.keys( value ).length !== 0 ) );
return removedEmpty.length !== 0;
}
if ( observation.taxon ) {
if ( metric === "id_supported" ) {
const taxonId = observation.taxon.id;
const supportedIDs = observation.identifications.filter(
identification => ( identification.taxon.id === taxonId )
).length;
return supportedIDs >= 2;
}
if ( metric === "rank" && observation.taxon.rank_level <= 10 ) {
return true;
}
}
}
return false;
};
const renderIndicator = metric => {
const ifAgree = checkTest( metric );
if ( ifAgree || ifAgree === null ) {
return (
<INatIcon name="checkmark-circle" size={19} color={theme.colors.secondary} /> );
}
return (
<INatIcon name="triangle-exclamation" size={19} color={theme.colors.error} />
);
};
return (
<>
<ScrollViewWrapper testID="DataQualityAssessment">
<View className="mx-[26px] my-[19px] space-y-[9px]">
<QualityGradeStatus
qualityGrade={qualityGrade}
color={( qualityGrade === "research" )
? theme.colors.secondary
: theme.colors.primary}
/>
<View className="flex-row space-x-[7px]">
{isResearchGrade
&& (
<INatIcon
name="checkmark-circle"
size={19}
color={theme.colors.secondary}
/>
)}
<List1 className="text-black">
{titleOption( qualityGrade )}
</List1>
</View>
<List2 className="text-black">
{titleDescription( qualityGrade )}
</List2>
</View>
<Divider />
<View className="mx-[15px]">
<View className={sectionClass}>
{renderIndicator( "date" ) }
<Body3>{t( "Data-quality-assessment-date-specified" )}</Body3>
</View>
<Divider />
<View className={sectionClass}>
{renderIndicator( "location" )}
<Body3>{t( "Data-quality-assessment-location-specified" )}</Body3>
</View>
<Divider />
<View className={sectionClass}>
{renderIndicator( "evidence" )}
<Body3>{t( "Data-quality-assessment-has-photos-or-sounds" )}</Body3>
</View>
<Divider />
<View className={sectionClass}>
{renderIndicator( "id_supported" )}
<Body3>{t( "Data-quality-assessment-id-supported-by-two-or-more" )}</Body3>
</View>
<Divider />
<View className={sectionClass}>
{renderIndicator( "rank" )}
<Body3>{t( "Data-quality-assessment-community-taxon-species-level-or-lower" )}</Body3>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "date" )}
<Body3>{t( "Data-quality-assessment-date-is-accurate" )}</Body3>
</View>
<DQAVoteButtons
metric="date"
qualityMetrics={qualityMetrics}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "location" )}
<Body3>{t( "Data-quality-assessment-location-is-accurate" )}</Body3>
</View>
<DQAVoteButtons
metric="location"
qualityMetrics={qualityMetrics}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "wild" )}
<Body3>{t( "Data-quality-assessment-organism-is-wild" )}</Body3>
</View>
<DQAVoteButtons
metric="wild"
qualityMetrics={qualityMetrics}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "evidence" )}
<Body3>{t( "Data-quality-assessment-evidence-of-organism" )}</Body3>
</View>
<DQAVoteButtons
metric="evidence"
qualityMetrics={qualityMetrics}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "recent" )}
<Body3>{t( "Data-quality-assessment-recent-evidence-of-organism" )}</Body3>
</View>
<DQAVoteButtons
metric="recent"
qualityMetrics={qualityMetrics}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
</View>
<View className="flex-row bg-lightGray px-[15px] py-[7px] mt-[20px]">
<PlaceholderText text="TODO" />
<Body3 className="shrink">
{t(
"Data-quality-assessment-can-taxon-still-be-confirmed-improved-based-on-the-evidence"
)}
</Body3>
<DQAVoteButtons
metric="needs_id"
qualityMetrics={qualityMetrics}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<View className="mt-[30px] mx-[15px] space-y-[11px]">
<Heading4>{t( "ABOUT-THE-DQA" )}</Heading4>
<List2>{t( "About-the-DQA-description" )}</List2>
</View>
</ScrollViewWrapper>
<BottomSheet
headerText={t( "ERROR-VOTING-IN-DQA" )}
hide={hideErrorSheet}
hideCloseButton
snapPoints={["25"]}
>
<View className="px-[26px] pt-[20px] flex-col space-y-[20px]">
<List2 className="text-black">{t( "Error-voting-in-DQA-description" )}</List2>
<Button
text={t( "OK" )}
onPress={() => setHideErrorSheet( true )}
/>
</View>
</BottomSheet>
</>
);
};
export default DataQualityAssessment;

View File

@@ -1,5 +1,6 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import {
Body4,
Button,
@@ -22,7 +23,8 @@ import Attribution from "./Attribution";
import checkCamelAndSnakeCase from "./helpers/checkCamelAndSnakeCase";
type Props = {
observation: Object
observation: Object,
uuid:string
}
const qualityGradeOption = option => {
@@ -51,9 +53,11 @@ const headingClass = "mt-[20px] mb-[11px] text-black";
const sectionClass = "mx-[15px] mb-[20px]";
const DetailsTab = ( { observation }: Props ): Node => {
const navigation = useNavigation( );
const application = observation?.application?.name;
const [locationKebabMenuVisible, setLocationKebabMenuVisible] = useState( false );
const qualityGrade = observation?.quality_grade;
const observationUUID = observation.uuid;
const displayQualityGradeOption = option => {
const labelClassName = ( qualityGrade === option )
@@ -94,12 +98,14 @@ const DetailsTab = ( { observation }: Props ): Node => {
/>
</KebabMenu>
</View>
<Map
obsLatitude={observation.latitude}
obsLongitude={observation.longitude}
mapHeight={230}
showMarker
/>
{ ( observation.latitude || observation.private_latitude ) && (
<Map
obsLatitude={observation.latitude}
obsLongitude={observation.longitude}
mapHeight={230}
showMarker
/>
) }
<View className={`mt-[11px] ${sectionClass}`}>
<ObservationLocation observation={observation} details />
@@ -131,7 +137,23 @@ const DetailsTab = ( { observation }: Props ): Node => {
<Body4>
{qualityGradeDescription( qualityGrade )}
</Body4>
<Button text={t( "VIEW-DATA-QUALITY-ASSESSEMENT" )} />
<Button
text={t( "VIEW-DATA-QUALITY-ASSESSEMENT" )}
onPress={() => navigation.navigate( "DataQualityAssessment", {
qualityGrade,
observationUUID,
observation: {
date: observation.observed_on,
location: [observation.latitude, observation.longitude],
evidence: [observation.observationPhotos, observation.observationSounds],
taxon: {
id: observation.taxon.id,
rank_level: observation.taxon.rank_level
},
identifications: observation.identifications
}
} )}
/>
</View>
</View>
<Divider />

View File

@@ -423,7 +423,7 @@ const ObsDetails = (): Node => {
/>
</HideView>
<HideView noInitialRender show={currentTabId === DETAILS_TAB_ID}>
<DetailsTab observation={observation} />
<DetailsTab observation={observation} uuid={uuid} />
</HideView>
{addingComment && (
<View className="flex-row items-center justify-center">

View File

@@ -3,6 +3,11 @@ ABOUT = ABOUT
About-iNaturalist = About iNaturalist
# About the Data Quality Assement
ABOUT-THE-DQA = ABOUT THE DQA
About-the-DQA-description = The Quality Grade summarizes the accuracy, precision, completeness, relevance, and appropriateness of an iNaturalist observation as biodiversity data. Some attributes are automatically determined, while others are subject to a vote by iNat users. iNaturalist shares licensed "Research Grade" observations with a number of data partners for use in science and conservation.
Accept-community-identifications = Accept community identifications
Account = Account
@@ -64,6 +69,18 @@ attribution-cc-by-nc-sa = some rights reserved (CC BY-NC-SA)
attribution-cc-by-nc-nd = some rights reserved (CC BY-NC-ND)
attribution-cc-by = some rights reserved (CC BY)
attribution-cc-by-sa = some rights reserved (CC BY-SA)
attribution-cc-by-nc = some rights reserved (CC BY-NC)
attribution-cc-by-nd = some rights reserved (CC BY-ND)
attribution-cc-by-nc-sa = some rights reserved (CC BY-NC-SA)
attribution-cc-by-nc-nd = some rights reserved (CC BY-NC-ND)
app-authorized-on-date = { $appName } (authorized on: { $date })
Applications = Applications
@@ -111,6 +128,8 @@ Content-Display = Content & Display
Copy-coordinates = Copy Coordinates
Copy-coordinates = Copy Coordinates
Couldnt-create-comment = Couldn't create comment
Couldnt-create-identification = Couldn't create identification
@@ -119,12 +138,66 @@ Couldnt-create-identification-error = Couldn't create identification { $error }
Couldnt-create-identification-unknown-error = Couldn't create identification, Unknown Error.
Couldnt-create-identification-error = Couldn't create identification { $error }
Couldnt-create-identification-unknown-error = Couldn't create identification, Unknown Error.
CREATE-AN-ACCOUNT = CREATE AN ACCOUNT
Create-an-iNaturalist-account-to-save-your-observations = Create an iNaturalist account to save your observations and contribute them to science.
CREATE-YOUR-FIRST-OBSERVATION = CREATE YOUR FIRST OBSERVATION
DATA-QUALITY = DATA QUALITY
DATA-QUALITY-ASSESSMENT = DATA QUALITY ASSESSMENT
# declares the current data quality status of the observation
Data-quality-assessment-title-research = This observation is Research Grade!
Data-quality-assessment-title-needs-id = This observation Needs ID
Data-quality-assessment-title-casual = This observation is Casual Grade
# description for different quality grades in the DQA
Data-quality-assessment-description-research = It can now be used for research and featured on other websites.
Data-quality-assessment-description-research-not-licensed = However, it is not licensed for re-use and will not be shared with data repositories that respect license choices.
Data-quality-assessment-description-needs-id = This observation has not yet met the conditions for Research Grade status:
Data-quality-assessment-description-casual = This observation has not met the conditions for Research Grade status.
# checklist test for Data Quality Assessment of Observation Details
Data-quality-assessment-date-specified = Date specified
Data-quality-assessment-location-specified = Location specified
Data-quality-assessment-has-photos-or-sounds = Has Photos or Sounds
Data-quality-assessment-id-supported-by-two-or-more = Has ID supported by two or more
Data-quality-assessment-community-taxon-species-level-or-lower = Community taxon at species level or lower
Data-quality-assessment-date-is-accurate = Date is accurate
Data-quality-assessment-location-is-accurate = Location is accurate
Data-quality-assessment-organism-is-wild = Organism is wild
Data-quality-assessment-evidence-of-organism = Evidence of organism
Data-quality-assessment-recent-evidence-of-organism = Recent evidence of an organism
Data-quality-assessment-can-taxon-still-be-confirmed-improved-based-on-the-evidence = Based on the evidence, can the Community Taxon still be improved?
Data-quality-research-description = This observation has enough identifications to be considered resarch grade
Data-quality-needs-id-description = This observation needs more identifications to reach research grade
Data-quality-casual-description = This observation needs more information verified to be considered verifiable
DATA-QUALITY = DATA QUALITY
Data-quality-research-description = This observation has enough identifications to be considered resarch grade
@@ -137,6 +210,8 @@ Date = Date
DATE = DATE
DATE = DATE
Date-added-newest-to-oldest = Date added - newest to oldest
Date-added-oldest-to-newest = Date added - oldest to newest
@@ -202,6 +277,10 @@ Error-Couldnt-Upload-Photo = Error: Couldn't Upload Photo
Error-Could-Not-Fetch-Taxon = Error: Could Not Fetch Taxon
ERROR-VOTING-IN-DQA = ERROR VOTING IN DQA
Error-voting-in-DQA-description = Your vote may not have been cast in the DQA. Check your internet connection and try again.
EVIDENCE = EVIDENCE
Explore = Explore
@@ -307,6 +386,8 @@ Location = Location
LOCATION = LOCATION
LOCATION = LOCATION
Location-accuracy-is-too-imprecise = Location accuracy is too imprecise to help identifiers. Please zoom in.
LOCATION-TOO-IMPRECISE = LOCATION TOO IMPRECISE
@@ -395,8 +476,9 @@ none = none
No-photos-found = No photos found. If this is your first time opening the app and giving permissions, try restarting the app.
# license code
no-rights-reserved-cc0 = no rights reserved (CC0)
no-rights-reserved-cc0-cc0 = no rights reserved (CC0) (CC0)
# Header for observation description on observation detail
# Header for observation description on observation detail
NOTES = NOTES
@@ -469,6 +551,11 @@ quality-grade-research = Research
quality-grade-needs-id = Needs Id
quality-grade-casual = Casual
# Quality grade options
quality-grade-research = Research
quality-grade-needs-id = Needs Id
quality-grade-casual = Casual
Rank = Rank
# The following Ranks- strings are taxonomic ranks (in taxonomic order, not alphabetical order)
@@ -589,6 +676,8 @@ SAVE-CHANGES = SAVE CHANGES
Saved-Observation = Saved observation, in queue to upload
Saved-Observation = Saved observation, in queue to upload
Search-for-a-location = Search for a location
Search-for-a-project = Search for a project
@@ -611,6 +700,8 @@ SHARE-DEBUG-LOGS = SHARE DEBUG LOGS
Share-location = Share Location
Share-location = Share Location
Sign-out = Sign out
Sign-Up = Sign Up
@@ -696,6 +787,9 @@ IMPORT-X-OBSERVATIONS = IMPORT {$count ->
# Describes whether a user made this observation from web, iOS, or Android
Uploaded-via-application = Uploaded via: { $application }
# Describes whether a user made this observation from web, iOS, or Android
Uploaded-via-application = Uploaded via: { $application }
# Shows the number of observations a user is currently uploading on my observations page
Uploading-X-Observations = Uploading {$count ->
[one] 1 Observation
@@ -728,6 +822,10 @@ VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
View-in-browser = View in Browser
VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
View-in-browser = View in Browser
Visually-search-iNaturalist-data = Visually search iNaturalists wealth of data. Search by a taxon in a location
Welcome-to-iNaturalist = Welcome to iNaturalist!
@@ -955,19 +1053,25 @@ Take-photo = Take photo
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
date-format-short = M/d/yy
# Date formatting using date-fns
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
# Date formatting using date-fns
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
datetime-format-short = M/d/yy h:mm a
# Date formatting using date-fns
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
# Date formatting using date-fns
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
date-format-long = PP
# Onboarding text on MyObservations: 0-10 observations
As-you-upload-more-observations = As you upload more observations, others in our community may be able to help you identify them!
# Onboarding text on MyObservations: 11-50 observations
# Onboarding text on MyObservations: Onboarding text on MyObservations: 11-50 observations
Observations-you-upload-to-iNaturalist = Observations you upload to iNaturalist can be used by scientists and researchers worldwide.
# Onboarding text on MyObservations: 51-100 observations
# Onboarding text on MyObservations: Onboarding text on MyObservations: 51-100 observations
You-can-search-observations-of-any-plant-or-animal = You can search observations of any plant or animal anywhere in the world with Explore!
DISCARD-MEDIA = DISCARD MEDIA?
@@ -986,16 +1090,16 @@ Failed-to-log-in = Failed to log in
# Generic error message
Something-went-wrong = Something went wrong.
# Geoprivacy sheet descriptions
# Geoprivacy sheet descriptions
Anyone-using-iNaturalist-can-see = Anyone using iNaturalist can see where this species was observed, and scientists can most easily use it for research.
The-exact-location-will-be-hidden = The exact location will be hidden publicly, and instead generalized to a larger area. (Threatened and endangered species are automatically obscured).
The-location-will-not-be-visible = The location will not be visible to others, which means it may be difficult to identify.
# Wild status sheet descriptions
# Wild status sheet descriptions
This-is-a-wild-organism = This is a wild organism and wasnt placed in this location by humans.
This-organism-was-placed-by-humans = This organism was placed in this location by humans. This applies to things like garden plants, pets, and zoo animals.
# Latitude, longitude, and accuracy on a single line
# Latitude, longitude, and accuracy on a single line on a single line
Lat-Lon-Acc = Lat: { NUMBER($latitude, maximumFractionDigits: 6) }, Lon: { NUMBER($longitude, maximumFractionDigits: 6) }, Acc: { $accuracy }
# Missing evidence sheet

View File

@@ -4,6 +4,11 @@
"val": "ABOUT"
},
"About-iNaturalist": "About iNaturalist",
"ABOUT-THE-DQA": {
"comment": "About the Data Quality Assement",
"val": "ABOUT THE DQA"
},
"About-the-DQA-description": "The Quality Grade summarizes the accuracy, precision, completeness, relevance, and appropriateness of an iNaturalist observation as biodiversity data. Some attributes are automatically determined, while others are subject to a vote by iNat users. iNaturalist shares licensed \"Research Grade\" observations with a number of data partners for use in science and conservation.",
"Accept-community-identifications": "Accept community identifications",
"Account": "Account",
"ADD-AN-ID": "ADD AN ID",
@@ -81,6 +86,34 @@
"Create-an-iNaturalist-account-to-save-your-observations": "Create an iNaturalist account to save your observations and contribute them to science.",
"CREATE-YOUR-FIRST-OBSERVATION": "CREATE YOUR FIRST OBSERVATION",
"DATA-QUALITY": "DATA QUALITY",
"DATA-QUALITY-ASSESSMENT": "DATA QUALITY ASSESSMENT",
"Data-quality-assessment-title-research": {
"comment": "declares the current data quality status of the observation ",
"val": "This observation is Research Grade!"
},
"Data-quality-assessment-title-needs-id": "This observation Needs ID",
"Data-quality-assessment-title-casual": "This observation is Casual Grade",
"Data-quality-assessment-description-research": {
"comment": "description for different quality grades in the DQA",
"val": "It can now be used for research and featured on other websites."
},
"Data-quality-assessment-description-research-not-licensed": "However, it is not licensed for re-use and will not be shared with data repositories that respect license choices.",
"Data-quality-assessment-description-needs-id": "This observation has not yet met the conditions for Research Grade status:",
"Data-quality-assessment-description-casual": "This observation has not met the conditions for Research Grade status.",
"Data-quality-assessment-date-specified": {
"comment": "checklist test for Data Quality Assessment of Observation Details ",
"val": "Date specified"
},
"Data-quality-assessment-location-specified": "Location specified",
"Data-quality-assessment-has-photos-or-sounds": "Has Photos or Sounds",
"Data-quality-assessment-id-supported-by-two-or-more": "Has ID supported by two or more",
"Data-quality-assessment-community-taxon-species-level-or-lower": "Community taxon at species level or lower",
"Data-quality-assessment-date-is-accurate": "Date is accurate",
"Data-quality-assessment-location-is-accurate": "Location is accurate",
"Data-quality-assessment-organism-is-wild": "Organism is wild",
"Data-quality-assessment-evidence-of-organism": "Evidence of organism",
"Data-quality-assessment-recent-evidence-of-organism": "Recent evidence of an organism",
"Data-quality-assessment-can-taxon-still-be-confirmed-improved-based-on-the-evidence": "Based on the evidence, can the Community Taxon still be improved?",
"Data-quality-research-description": "This observation has enough identifications to be considered resarch grade",
"Data-quality-needs-id-description": "This observation needs more identifications to reach research grade",
"Data-quality-casual-description": "This observation needs more information verified to be considered verifiable",
@@ -118,6 +151,8 @@
"Error-Couldnt-Complete-Upload": "Error: Couldn't Complete Upload",
"Error-Couldnt-Upload-Photo": "Error: Couldn't Upload Photo",
"Error-Could-Not-Fetch-Taxon": "Error: Could Not Fetch Taxon",
"ERROR-VOTING-IN-DQA": "ERROR VOTING IN DQA",
"Error-voting-in-DQA-description": "Your vote may not have been cast in the DQA. Check your internet connection and try again.",
"EVIDENCE": "EVIDENCE",
"Explore": "Explore",
"EXPLORE-OBSERVATIONS": "EXPLORE OBSERVATIONS",
@@ -248,12 +283,12 @@
"No-Location": "No Location",
"none": "none",
"No-photos-found": "No photos found. If this is your first time opening the app and giving permissions, try restarting the app.",
"no-rights-reserved-cc0": {
"no-rights-reserved-cc0-cc0": {
"comment": "license code",
"val": "no rights reserved (CC0)"
"val": "no rights reserved (CC0) (CC0)"
},
"NOTES": {
"comment": "Header for observation description on observation detail",
"comment": "Header for observation description on observation detail\nHeader for observation description on observation detail",
"val": "NOTES"
},
"Notifications": "Notifications",
@@ -660,6 +695,7 @@
"comment": "Date formatting using date-fns\nSee complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format",
"val": "M/d/yy"
},
"comment": "Date formatting using date-fns\nSee complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format",
"datetime-format-short": {
"comment": "Date formatting using date-fns\nSee complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format",
"val": "M/d/yy h:mm a"
@@ -673,11 +709,11 @@
"val": "As you upload more observations, others in our community may be able to help you identify them!"
},
"Observations-you-upload-to-iNaturalist": {
"comment": "Onboarding text on MyObservations: 11-50 observations",
"comment": "Onboarding text on MyObservations: Onboarding text on MyObservations: 11-50 observations",
"val": "Observations you upload to iNaturalist can be used by scientists and researchers worldwide."
},
"You-can-search-observations-of-any-plant-or-animal": {
"comment": "Onboarding text on MyObservations: 51-100 observations",
"comment": "Onboarding text on MyObservations: Onboarding text on MyObservations: 51-100 observations",
"val": "You can search observations of any plant or animal anywhere in the world with Explore!"
},
"DISCARD-MEDIA": "DISCARD MEDIA?",
@@ -697,18 +733,18 @@
"val": "Something went wrong."
},
"Anyone-using-iNaturalist-can-see": {
"comment": "Geoprivacy sheet descriptions",
"comment": " Geoprivacy sheet descriptions",
"val": "Anyone using iNaturalist can see where this species was observed, and scientists can most easily use it for research."
},
"The-exact-location-will-be-hidden": "The exact location will be hidden publicly, and instead generalized to a larger area. (Threatened and endangered species are automatically obscured).",
"The-location-will-not-be-visible": "The location will not be visible to others, which means it may be difficult to identify.",
"This-is-a-wild-organism": {
"comment": "Wild status sheet descriptions",
"comment": " Wild status sheet descriptions",
"val": "This is a wild organism and wasnt placed in this location by humans."
},
"This-organism-was-placed-by-humans": "This organism was placed in this location by humans. This applies to things like garden plants, pets, and zoo animals.",
"Lat-Lon-Acc": {
"comment": "Latitude, longitude, and accuracy on a single line",
"comment": "Latitude, longitude, and accuracy on a single line on a single line",
"val": "Lat: { NUMBER($latitude, maximumFractionDigits: \"6\") }, Lon: { NUMBER($longitude, maximumFractionDigits: \"6\") }, Acc: { $accuracy }"
},
"Every-observation-needs": {

View File

@@ -3,6 +3,11 @@ ABOUT = ABOUT
About-iNaturalist = About iNaturalist
# About the Data Quality Assement
ABOUT-THE-DQA = ABOUT THE DQA
About-the-DQA-description = The Quality Grade summarizes the accuracy, precision, completeness, relevance, and appropriateness of an iNaturalist observation as biodiversity data. Some attributes are automatically determined, while others are subject to a vote by iNat users. iNaturalist shares licensed "Research Grade" observations with a number of data partners for use in science and conservation.
Accept-community-identifications = Accept community identifications
Account = Account
@@ -64,6 +69,18 @@ attribution-cc-by-nc-sa = some rights reserved (CC BY-NC-SA)
attribution-cc-by-nc-nd = some rights reserved (CC BY-NC-ND)
attribution-cc-by = some rights reserved (CC BY)
attribution-cc-by-sa = some rights reserved (CC BY-SA)
attribution-cc-by-nc = some rights reserved (CC BY-NC)
attribution-cc-by-nd = some rights reserved (CC BY-ND)
attribution-cc-by-nc-sa = some rights reserved (CC BY-NC-SA)
attribution-cc-by-nc-nd = some rights reserved (CC BY-NC-ND)
app-authorized-on-date = { $appName } (authorized on: { $date })
Applications = Applications
@@ -111,6 +128,8 @@ Content-Display = Content & Display
Copy-coordinates = Copy Coordinates
Copy-coordinates = Copy Coordinates
Couldnt-create-comment = Couldn't create comment
Couldnt-create-identification = Couldn't create identification
@@ -119,12 +138,66 @@ Couldnt-create-identification-error = Couldn't create identification { $error }
Couldnt-create-identification-unknown-error = Couldn't create identification, Unknown Error.
Couldnt-create-identification-error = Couldn't create identification { $error }
Couldnt-create-identification-unknown-error = Couldn't create identification, Unknown Error.
CREATE-AN-ACCOUNT = CREATE AN ACCOUNT
Create-an-iNaturalist-account-to-save-your-observations = Create an iNaturalist account to save your observations and contribute them to science.
CREATE-YOUR-FIRST-OBSERVATION = CREATE YOUR FIRST OBSERVATION
DATA-QUALITY = DATA QUALITY
DATA-QUALITY-ASSESSMENT = DATA QUALITY ASSESSMENT
# declares the current data quality status of the observation
Data-quality-assessment-title-research = This observation is Research Grade!
Data-quality-assessment-title-needs-id = This observation Needs ID
Data-quality-assessment-title-casual = This observation is Casual Grade
# description for different quality grades in the DQA
Data-quality-assessment-description-research = It can now be used for research and featured on other websites.
Data-quality-assessment-description-research-not-licensed = However, it is not licensed for re-use and will not be shared with data repositories that respect license choices.
Data-quality-assessment-description-needs-id = This observation has not yet met the conditions for Research Grade status:
Data-quality-assessment-description-casual = This observation has not met the conditions for Research Grade status.
# checklist test for Data Quality Assessment of Observation Details
Data-quality-assessment-date-specified = Date specified
Data-quality-assessment-location-specified = Location specified
Data-quality-assessment-has-photos-or-sounds = Has Photos or Sounds
Data-quality-assessment-id-supported-by-two-or-more = Has ID supported by two or more
Data-quality-assessment-community-taxon-species-level-or-lower = Community taxon at species level or lower
Data-quality-assessment-date-is-accurate = Date is accurate
Data-quality-assessment-location-is-accurate = Location is accurate
Data-quality-assessment-organism-is-wild = Organism is wild
Data-quality-assessment-evidence-of-organism = Evidence of organism
Data-quality-assessment-recent-evidence-of-organism = Recent evidence of an organism
Data-quality-assessment-can-taxon-still-be-confirmed-improved-based-on-the-evidence = Based on the evidence, can the Community Taxon still be improved?
Data-quality-research-description = This observation has enough identifications to be considered resarch grade
Data-quality-needs-id-description = This observation needs more identifications to reach research grade
Data-quality-casual-description = This observation needs more information verified to be considered verifiable
DATA-QUALITY = DATA QUALITY
Data-quality-research-description = This observation has enough identifications to be considered resarch grade
@@ -137,6 +210,8 @@ Date = Date
DATE = DATE
DATE = DATE
Date-added-newest-to-oldest = Date added - newest to oldest
Date-added-oldest-to-newest = Date added - oldest to newest
@@ -202,6 +277,10 @@ Error-Couldnt-Upload-Photo = Error: Couldn't Upload Photo
Error-Could-Not-Fetch-Taxon = Error: Could Not Fetch Taxon
ERROR-VOTING-IN-DQA = ERROR VOTING IN DQA
Error-voting-in-DQA-description = Your vote may not have been cast in the DQA. Check your internet connection and try again.
EVIDENCE = EVIDENCE
Explore = Explore
@@ -307,6 +386,8 @@ Location = Location
LOCATION = LOCATION
LOCATION = LOCATION
Location-accuracy-is-too-imprecise = Location accuracy is too imprecise to help identifiers. Please zoom in.
LOCATION-TOO-IMPRECISE = LOCATION TOO IMPRECISE
@@ -395,8 +476,9 @@ none = none
No-photos-found = No photos found. If this is your first time opening the app and giving permissions, try restarting the app.
# license code
no-rights-reserved-cc0 = no rights reserved (CC0)
no-rights-reserved-cc0-cc0 = no rights reserved (CC0) (CC0)
# Header for observation description on observation detail
# Header for observation description on observation detail
NOTES = NOTES
@@ -469,6 +551,11 @@ quality-grade-research = Research
quality-grade-needs-id = Needs Id
quality-grade-casual = Casual
# Quality grade options
quality-grade-research = Research
quality-grade-needs-id = Needs Id
quality-grade-casual = Casual
Rank = Rank
# The following Ranks- strings are taxonomic ranks (in taxonomic order, not alphabetical order)
@@ -589,6 +676,8 @@ SAVE-CHANGES = SAVE CHANGES
Saved-Observation = Saved observation, in queue to upload
Saved-Observation = Saved observation, in queue to upload
Search-for-a-location = Search for a location
Search-for-a-project = Search for a project
@@ -611,6 +700,8 @@ SHARE-DEBUG-LOGS = SHARE DEBUG LOGS
Share-location = Share Location
Share-location = Share Location
Sign-out = Sign out
Sign-Up = Sign Up
@@ -696,6 +787,9 @@ IMPORT-X-OBSERVATIONS = IMPORT {$count ->
# Describes whether a user made this observation from web, iOS, or Android
Uploaded-via-application = Uploaded via: { $application }
# Describes whether a user made this observation from web, iOS, or Android
Uploaded-via-application = Uploaded via: { $application }
# Shows the number of observations a user is currently uploading on my observations page
Uploading-X-Observations = Uploading {$count ->
[one] 1 Observation
@@ -728,6 +822,10 @@ VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
View-in-browser = View in Browser
VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
View-in-browser = View in Browser
Visually-search-iNaturalist-data = Visually search iNaturalists wealth of data. Search by a taxon in a location
Welcome-to-iNaturalist = Welcome to iNaturalist!
@@ -955,19 +1053,25 @@ Take-photo = Take photo
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
date-format-short = M/d/yy
# Date formatting using date-fns
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
# Date formatting using date-fns
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
datetime-format-short = M/d/yy h:mm a
# Date formatting using date-fns
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
# Date formatting using date-fns
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
date-format-long = PP
# Onboarding text on MyObservations: 0-10 observations
As-you-upload-more-observations = As you upload more observations, others in our community may be able to help you identify them!
# Onboarding text on MyObservations: 11-50 observations
# Onboarding text on MyObservations: Onboarding text on MyObservations: 11-50 observations
Observations-you-upload-to-iNaturalist = Observations you upload to iNaturalist can be used by scientists and researchers worldwide.
# Onboarding text on MyObservations: 51-100 observations
# Onboarding text on MyObservations: Onboarding text on MyObservations: 51-100 observations
You-can-search-observations-of-any-plant-or-animal = You can search observations of any plant or animal anywhere in the world with Explore!
DISCARD-MEDIA = DISCARD MEDIA?
@@ -986,16 +1090,16 @@ Failed-to-log-in = Failed to log in
# Generic error message
Something-went-wrong = Something went wrong.
# Geoprivacy sheet descriptions
# Geoprivacy sheet descriptions
Anyone-using-iNaturalist-can-see = Anyone using iNaturalist can see where this species was observed, and scientists can most easily use it for research.
The-exact-location-will-be-hidden = The exact location will be hidden publicly, and instead generalized to a larger area. (Threatened and endangered species are automatically obscured).
The-location-will-not-be-visible = The location will not be visible to others, which means it may be difficult to identify.
# Wild status sheet descriptions
# Wild status sheet descriptions
This-is-a-wild-organism = This is a wild organism and wasnt placed in this location by humans.
This-organism-was-placed-by-humans = This organism was placed in this location by humans. This applies to things like garden plants, pets, and zoo animals.
# Latitude, longitude, and accuracy on a single line
# Latitude, longitude, and accuracy on a single line on a single line
Lat-Lon-Acc = Lat: { NUMBER($latitude, maximumFractionDigits: 6) }, Lon: { NUMBER($longitude, maximumFractionDigits: 6) }, Acc: { $accuracy }
# Missing evidence sheet

View File

@@ -14,6 +14,7 @@ import MediaViewer from "components/MediaViewer/MediaViewer";
import Messages from "components/Messages/Messages";
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer";
import NetworkLogging from "components/NetworkLogging";
import DataQualityAssessment from "components/ObsDetails/DataQualityAssessment";
import ObsDetails from "components/ObsDetails/ObsDetails";
import ObsEdit from "components/ObsEdit/ObsEdit";
import GroupPhotosContainer from "components/PhotoImporter/GroupPhotosContainer";
@@ -35,7 +36,8 @@ import {
hideHeader,
hideHeaderLeft,
showCustomHeader,
showHeaderLeft
showHeaderLeft,
showLongHeader
} from "navigation/navigationOptions";
import React from "react";
import { PermissionsAndroid, Platform } from "react-native";
@@ -351,6 +353,15 @@ const BottomTabs = ( ) => {
...hideHeaderLeft
}}
/>
<Tab.Screen
name="DataQualityAssessment"
component={DataQualityAssessment}
options={{
...showLongHeader,
headerTitle: t( "DATA-QUALITY-ASSESSMENT" ),
unmountOnBlur: true
}}
/>
<Tab.Screen name="Login" component={MortalLogin} options={hideHeader} />
<Tab.Screen name="SignUp" component={SignUp} options={hideHeader} />
<Tab.Screen name="ForgotPassword" component={ForgotPassword} options={hideHeader} />

View File

@@ -26,6 +26,20 @@ const showHeader: Object = {
}
};
const showLongHeader: Object = {
...baseHeaderOptions,
headerTintColor: colors.black,
// Note: left header is not supported on iOS
// so we would need to build a custom header for this:
// https://reactnavigation.org/docs/native-stack-navigator#headertitlealign
headerTitleStyle: {
fontSize: 16,
fontFamily: Platform.OS === "ios"
? "Whitney-Medium"
: "Whitney-Medium-Pro"
}
};
export const showHeaderLeft: Object = {
...showHeader,
headerLeft: ( ) => <BackButton />
@@ -53,5 +67,6 @@ export {
blankHeaderTitle,
hideHeader,
showCustomHeader,
showHeader
showHeader,
showLongHeader
};

View File

@@ -33,6 +33,7 @@ class Observation extends Realm.Object {
location: true,
longitude: true,
observation_photos: ObservationPhoto.OBSERVATION_PHOTOS_FIELDS,
observed_on: true,
place_guess: true,
quality_grade: true,
taxon: Taxon.TAXON_FIELDS,
@@ -47,6 +48,9 @@ class Observation extends Realm.Object {
captive_flag: false,
geoprivacy: "open",
owners_identification_from_vision: false,
observed_on: obs
? obs?.observed_on
: createObservedOnStringForUpload( ),
observed_on_string: obs
? obs?.observed_on_string
: createObservedOnStringForUpload( ),
@@ -187,6 +191,7 @@ class Observation extends Realm.Object {
species_guess: obs.species_guess,
description: obs.description,
observed_on_string: obs.observed_on_string,
observed_on: obs.observed_on,
place_guess: obs.place_guess,
latitude: obs.latitude,
longitude: obs.longitude,
@@ -486,6 +491,7 @@ class Observation extends Realm.Object {
observationSounds: "ObservationSound[]",
// date and/or time submitted to the server when a new obs is uploaded
observed_on_string: "string?",
observed_on: "string?",
owners_identification_from_vision: "bool?",
species_guess: "string?",
place_guess: { type: "string?", mapTo: "placeGuess" },

View File

@@ -24,9 +24,18 @@ export default {
Taxon,
User
],
schemaVersion: 33,
schemaVersion: 34,
path: `${RNFS.DocumentDirectoryPath}/db.realm`,
migration: ( oldRealm, newRealm ) => {
if ( oldRealm.schemaVersion < 34 ) {
const oldObservations = oldRealm.objects( "Observation" );
const newObservations = newRealm.objects( "Observation" );
oldObservations.keys( ).forEach( objectIndex => {
const oldObservation = oldObservations[objectIndex];
const newObservation = newObservations[objectIndex];
newObservation.observed_on = oldObservation.time_observed_at;
} );
}
if ( oldRealm.schemaVersion < 33 ) {
const oldObservations = oldRealm.objects( "Observation" );
const newObservations = newRealm.objects( "Observation" );