mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-04 13:43:34 -04:00
Merge pull request #2707 from inaturalist/mob-373-offline-suggestions-should-have-parity-with-online
Offline suggestions should have parity with online
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
export const getPredictionsForImage = jest.fn( () => Promise.resolve( { predictions: [] } ) );
|
||||
export const getPredictionsForImage = jest.fn( () => Promise.resolve( {
|
||||
predictions: [],
|
||||
commonAncestor: undefined
|
||||
} ) );
|
||||
export const getPredictionsForLocation = jest.fn( () => Promise.resolve( { predictions: [] } ) );
|
||||
export const removeLogListener = jest.fn( );
|
||||
export const resetStoredResults = jest.fn( );
|
||||
@@ -6,3 +9,8 @@ export const getCellLocation = jest.fn( location => ( {
|
||||
...location,
|
||||
elevation: 12
|
||||
} ) );
|
||||
|
||||
export const MODE = {
|
||||
BEST_BRANCH: "BEST_BRANCH",
|
||||
COMMON_ANCESTOR: "COMMON_ANCESTOR"
|
||||
};
|
||||
|
||||
@@ -3,7 +3,11 @@ import { Alert, Platform } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import RNFS from "react-native-fs";
|
||||
import type { Location } from "vision-camera-plugin-inatvision";
|
||||
import { getPredictionsForImage, getPredictionsForLocation } from "vision-camera-plugin-inatvision";
|
||||
import {
|
||||
getPredictionsForImage,
|
||||
getPredictionsForLocation,
|
||||
MODE
|
||||
} from "vision-camera-plugin-inatvision";
|
||||
|
||||
const modelFiles = {
|
||||
// The iOS model and taxonomy files always have to be referenced in the
|
||||
@@ -62,7 +66,8 @@ export const predictImage = ( uri: string, location: Location ) => {
|
||||
geomodelPath,
|
||||
location: hasLocation
|
||||
? location
|
||||
: undefined
|
||||
: undefined,
|
||||
mode: MODE.COMMON_ANCESTOR
|
||||
} );
|
||||
};
|
||||
|
||||
|
||||
@@ -78,11 +78,16 @@ const filterSuggestions = ( suggestionsToFilter, usingOfflineSuggestions, common
|
||||
};
|
||||
}
|
||||
|
||||
// online common ancestor
|
||||
// online or offline common ancestor
|
||||
if ( commonAncestor ) {
|
||||
const sortableCommonAncestor = {
|
||||
...commonAncestor,
|
||||
// temp patch to let calling code sort online common ancestor with other suggestions
|
||||
combined_score: commonAncestor.combined_score ?? commonAncestor.score
|
||||
};
|
||||
return {
|
||||
...newSuggestions,
|
||||
topSuggestion: commonAncestor,
|
||||
topSuggestion: sortableCommonAncestor,
|
||||
topSuggestionType: TOP_SUGGESTION_COMMON_ANCESTOR
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,10 +30,16 @@ const useOfflineSuggestions = (
|
||||
tryOfflineSuggestions: boolean
|
||||
}
|
||||
): {
|
||||
offlineSuggestions: OfflineSuggestion[];
|
||||
offlineSuggestions: {
|
||||
results: OfflineSuggestion[],
|
||||
commonAncestor: OfflineSuggestion | undefined
|
||||
};
|
||||
} => {
|
||||
const realm = useRealm( );
|
||||
const [offlineSuggestions, setOfflineSuggestions] = useState<OfflineSuggestion[]>( [] );
|
||||
const [offlineSuggestions, setOfflineSuggestions] = useState<{
|
||||
results: OfflineSuggestion[],
|
||||
commonAncestor: OfflineSuggestion | undefined
|
||||
}>( { results: [], commonAncestor: undefined } );
|
||||
const [error, setError] = useState( null );
|
||||
|
||||
const {
|
||||
@@ -43,10 +49,14 @@ const useOfflineSuggestions = (
|
||||
useEffect( ( ) => {
|
||||
const predictOffline = async ( ) => {
|
||||
let rawPredictions = [];
|
||||
let commonAncestor;
|
||||
try {
|
||||
const location = { latitude, longitude };
|
||||
const result = await predictImage( photoUri, location );
|
||||
rawPredictions = result.predictions;
|
||||
// Destructuring here leads to different errors from the linter.
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
commonAncestor = result.commonAncestor;
|
||||
} catch ( predictImageError ) {
|
||||
onFetchError( { isOnline: false } );
|
||||
logger.error( "Error predicting image offline", predictImageError );
|
||||
@@ -56,27 +66,39 @@ const useOfflineSuggestions = (
|
||||
// but we're offline so we only need the local list from realm
|
||||
// and don't need to fetch taxon from the API
|
||||
const iconicTaxa = realm?.objects( "Taxon" ).filtered( "isIconic = true" );
|
||||
const branchIDs = rawPredictions.map( t => t.taxon_id );
|
||||
const branchIDs = [...rawPredictions.map( t => t.taxon_id ), ...( commonAncestor
|
||||
? [commonAncestor.taxon_id]
|
||||
: [] )];
|
||||
const iconicTaxonName = iconicTaxa?.find( t => branchIDs.indexOf( t.id ) >= 0 )?.name;
|
||||
|
||||
// using the same rank level for displaying predictions in AI Camera
|
||||
// this is all temporary, since we ultimately want predictions
|
||||
// returned similarly to how we return them on web; this is returning a
|
||||
// single branch like on the AI Camera 2023-12-08
|
||||
// This function handles either regular or common ancestor predictions as input objects. I'm
|
||||
// not going to define an interface for them in the middle of refactoring and changing logic.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const formatPrediction = ( prediction: any ): OfflineSuggestion => ( {
|
||||
combined_score: prediction.combined_score,
|
||||
taxon: {
|
||||
id: Number( prediction.taxon_id ),
|
||||
name: prediction.name,
|
||||
rank_level: prediction.rank_level,
|
||||
iconic_taxon_name: iconicTaxonName
|
||||
}
|
||||
} );
|
||||
|
||||
const formattedPredictions = rawPredictions?.reverse( )
|
||||
.filter( prediction => prediction.rank_level <= 40 )
|
||||
.map( prediction => ( {
|
||||
combined_score: prediction.combined_score,
|
||||
taxon: {
|
||||
id: Number( prediction.taxon_id ),
|
||||
name: prediction.name,
|
||||
rank_level: prediction.rank_level,
|
||||
iconic_taxon_name: iconicTaxonName
|
||||
}
|
||||
} ) );
|
||||
setOfflineSuggestions( formattedPredictions );
|
||||
.map( prediction => formatPrediction( prediction ) );
|
||||
|
||||
const commonAncestorSuggestion = commonAncestor
|
||||
? formatPrediction( commonAncestor )
|
||||
: undefined;
|
||||
|
||||
const returnValue = {
|
||||
results: formattedPredictions,
|
||||
commonAncestor: commonAncestorSuggestion
|
||||
};
|
||||
|
||||
setOfflineSuggestions( returnValue );
|
||||
onFetched( { isOnline: false } );
|
||||
return formattedPredictions;
|
||||
return returnValue;
|
||||
};
|
||||
|
||||
if ( photoUri && tryOfflineSuggestions ) {
|
||||
|
||||
@@ -63,27 +63,34 @@ export const useSuggestions = ( photoUri, options ) => {
|
||||
} );
|
||||
|
||||
const usingOfflineSuggestions = tryOfflineSuggestions || (
|
||||
offlineSuggestions.length > 0
|
||||
offlineSuggestions?.results?.length > 0
|
||||
&& ( !onlineSuggestions || onlineSuggestions?.results?.length === 0 )
|
||||
);
|
||||
|
||||
const hasOnlineSuggestionResults = onlineSuggestions?.results?.length > 0;
|
||||
|
||||
const unfilteredSuggestions = hasOnlineSuggestionResults
|
||||
? onlineSuggestions.results
|
||||
: offlineSuggestions;
|
||||
const unfilteredSuggestions = useMemo(
|
||||
( ) => ( hasOnlineSuggestionResults
|
||||
? onlineSuggestions.results || []
|
||||
: offlineSuggestions.results || [] ),
|
||||
[hasOnlineSuggestionResults, onlineSuggestions, offlineSuggestions]
|
||||
);
|
||||
|
||||
const commonAncestor = hasOnlineSuggestionResults
|
||||
? onlineSuggestions?.common_ancestor
|
||||
: offlineSuggestions?.commonAncestor;
|
||||
|
||||
// since we can calculate this, there's no need to store it in state
|
||||
const suggestions = useMemo(
|
||||
( ) => filterSuggestions(
|
||||
unfilteredSuggestions,
|
||||
usingOfflineSuggestions,
|
||||
onlineSuggestions?.common_ancestor
|
||||
commonAncestor
|
||||
),
|
||||
[
|
||||
unfilteredSuggestions,
|
||||
usingOfflineSuggestions,
|
||||
onlineSuggestions?.common_ancestor
|
||||
commonAncestor
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user