import flatten from 'lodash/flatten' ; import map from 'lodash/map' ; import xorWith from 'lodash/xorWith' ; import uniqWith from 'lodash/uniqWith' ; import uniqBy from 'lodash/uniqBy' ; import sum from 'lodash/sum' ; import join from 'lodash/join' ; import isPlainObject from 'lodash/isPlainObject' ; import countBy from 'lodash/countBy' ; import maxBy from 'lodash/maxBy' ; import groupBy from 'lodash/groupBy' ; import keyBy from 'lodash/keyBy' ; import some from 'lodash/some' ; import isUndefined from 'lodash/isUndefined' ; import { createVectorFromPoints , createAngleFromVectors , createPerpendicular , getSegmentLength , calculateAngle , radiansToDegrees , } from 'utils/math' ; function getSymbolForAngle ( line1 , line2 ) { const A = line1 . components [ 0 ] ; const B = line1 . components [ 1 ] ; const C = line2 . components [ 0 ] ; const D = line2 . components [ 1 ] ; if ( A . symbol === C . symbol || B . symbol === D . symbol ) { const uniqArray = uniqBy ( [ B , C , D , A ] , p => p . symbol ) ; return map ( uniqArray , c => c . symbol ) . join ( '' ) ; } else { return map ( [ line1 , line2 ] , c => c . symbol ) . join ( ',' ) ; } } const defaultMapAngle = ( line1 , line2 ) => createAngleFromVectors ( line1 , line2 ) ; const defaultCalculateAngle = ( ) => ( ) => ( angle ) => radiansToDegrees ( calculateAngle ( angle ) ) ; const defaultMapLine = ( A , B ) => createVectorFromPoints ( A , B ) ; const defaultMapDistance = ( A , line ) => createPerpendicular ( line , A ) ; const defaultCalculateLine = ( ) => ( ) => ( segment ) => getSegmentLength ( segment ) ; const defaultCalculateSum = ( ... values ) => ( ) => ( ) => sum ( values ) ; /**
 * Creates an object conforming to the Angle interface based on 2 lines
 */ export function angleBetweenLines ( lineA , lineB , name ? , symbol ? , unit = 'degree' , imageType = 'ceph_lateral' ) { return { type : 'angle' , symbol : symbol || getSymbolForAngle ( lineA , lineB ) , unit , name , components : [ lineA , lineB ] , map : defaultMapAngle , calculate : defaultCalculateAngle , imageType , } ; } /**
 * Creates an object conforming to the Angle interface based on 3 points
 */ export function angleBetweenPoints ( A , B , C , name ? , unit = 'degree' ) { return angleBetweenLines ( line ( B , A ) , line ( B , C ) , name , undefined , unit ) ; } export function point ( symbol , name ? , description ? , imageType = 'ceph_lateral' ) { return { type : 'point' , name , symbol , description , components : [ ] , imageType , } ; } /**
 * Creates an object conforming to the Line interface connecting two points
 */ export function line ( A , B , name ? , symbol ? , imageType = 'ceph_lateral' ) { return { type : 'line' , name , symbol : symbol || `${ A . symbol }-${ B . symbol }` , components : [ A , B ] , map : defaultMapLine , imageType , } ; } ; export function distance ( A , line , name ? , symbol ? , unit = 'mm' , imageType = 'ceph_lateral' ) { return { type : 'distance' , name , unit , symbol : symbol || `${ A . symbol }-${ line . symbol }` , components : [ A , line ] , map : defaultMapDistance , calculate : defaultCalculateLine , imageType , } ; } ; export function angularSum ( components , name , symbol ? , imageType = 'ceph_lateral' ) { return { type : 'sum' , name , unit : components [ 0 ] . unit , symbol : symbol || join ( map ( components , c => c . symbol ) , '+' ) , components , calculate : defaultCalculateSum , imageType , } ; } /**
 * Creates a new landmark that can be set on the specified image type
 * by copying properties of a landmark defined for a different image type
 * and replacing its components with ones applicable to the new image type.
 * The new landmark has the same symbol and name of the reused one.
 */ export const reuseLandmarkForImageType = ( imageType ) => ( landmark ) : CephLandmark => ( { ... landmark , imageType , components : map ( landmark . components , reuseLandmarkForImageType ( imageType ) ) , } ) ; export function areEqualSteps ( l1 , l2 ) { if ( l1 . symbol === l2 . symbol ) { return true ; } if ( l1 . type !== l2 . type ) { return false ; } if ( l1 . components . length === 0 ) { return false ; } if ( l1 . components . length !== l2 . components . length ) { return false ; } return ( xorWith ( l1 . components , l2 . components , areEqualSteps ) . length === 0 ) ; } ; export function areEqualSymbols ( l1 , l2 ) { return l1 . symbol === l2 . symbol ; } ; export function getStepsForLandmarks ( landmarks , removeEqualSteps = true ) { return uniqWith ( flatten ( map ( landmarks , ( landmark ) => { if ( ! landmark ) { console . warn ( 'Got unexpected value in getStepsForLandmarks. ' + 'Expected a Cephalo.Landmark, got ' + landmark ) ; return [ ] ; } return [ ... getStepsForLandmarks ( landmark . components , removeEqualSteps ) , landmark , ] ; } ) ) , removeEqualSteps === true ? areEqualSteps : areEqualSymbols ) ; } ; export function getStepsForAnalysis < T extends ImageType > ( analysis , deduplicateVectors = true ) { return getStepsForLandmarks ( analysis . components . map ( c => c . landmark ) , deduplicateVectors ) ; } ; export function flipVector ( vector ) { return line ( vector . components [ 1 ] , vector . components [ 0 ] ) ; } ; export function isCephPoint ( object ) { return isPlainObject ( object ) && object . type === 'point' ; } ; export function isCephLine ( object ) { return isPlainObject ( object ) && object . type === 'line' ; } ; export function isCephAngle ( object ) { return isPlainObject ( object ) && object . type === 'angle' ; } ; /**
 * Tries mapping a CephaloLandmark.
 * Returns the GeoObject the landmark maps to.
 * Returns undefined if the landmark is not mappable.
 */ export function tryMap ( landmark , manualObjects ) { const manual = manualObjects [ landmark . symbol ] ; if ( typeof manual !== 'undefined' ) { return manual ; } else if ( typeof landmark . map === 'function' ) { return landmark . map ( ... map ( landmark . components , c => tryMap ( c , manualObjects ) ) ) ; } return undefined ; } ; /**
 * Tries calculating the value of a landmark on a cephalometric radiograph.
 * Returns the calculated value as specified in the landmark.calculate method.
 * Returns undefined if the landmark cannot be calculated.
 */ export function tryCalculate ( landmark , manualObjects , values ) { const manualValue = values [ landmark . symbol ] ; if ( typeof manualValue !== 'undefined' ) { return manualValue ; } else if ( typeof landmark . calculate === 'function' ) { return landmark . calculate ( ... map ( landmark . components , c => tryCalculate ( c , manualObjects , values ) ) ) ( ... map ( landmark . components , c => tryMap ( c , manualObjects ) ) ) ( tryMap ( landmark , manualObjects ) ) ; } return undefined ; } ; const categoryMap = { growthPattern : 'Growth Pattern' , lowerIncisorInclination : 'Lower incisor inclination' , upperIncisorInclination : 'Upper incisor inclination' , mandible : 'Mandible' , maxilla : 'Maxilla' , mandibularRotation : 'Mandibular rotation' , skeletalBite : 'Skeletal bite' , skeletalPattern : 'Skeletal pattern' , skeletalProfile : 'Skeletal profile' , chin : 'Chin prominence' , lowerLipProminence : 'Lower lip prominence' , upperLipProminence : 'Upper lip prominence' , overbite : 'Overbite' , overjet : 'Overjet' , } ; const indicationMap = { labial : 'Labial' , class1 : 'Class 1' , class2 : 'Class 2' , class3 : 'Class 3' , clockwise : 'Clockwise' , closed : 'Closed' , concave : 'Concave' , convex : 'Convex' , counterclockwise : 'Counter-clockwise' , horizontal : 'Horizontal' , vertical : 'Vertical' , lingual : 'Lingual' , normal : 'Normal' , open : 'Open' , palatal : 'Palatal' , prognathic : 'Prognathic' , prominent : 'Prominent' , recessive : 'Recessive' , retrognathic : 'Retrognathic' , tendency_for_class3 : 'Class 3 Tendency' , decreased : 'Decreased' , increased : 'Increased' , negative : 'Negative' , resessive : 'Recessive' , } ; const severityMap = { low : 'Slight' , medium : 'Medium' , high : 'Severe' , none : 'None' , } ; export const getDisplayNameForCategory = ( category ) => categoryMap [ category ] ; export const getDisplayNameForIndication = ( indication ) => indicationMap [ indication ] ; export const getDisplayNameForSeverity = ( severity ) => severityMap [ severity ] ; /**
 * Determines whether a step in a cephalometric analysis can be
 * automatically mapped to a geometrical object
 */ export function isStepAutomatic ( step ) { return typeof step . map === 'function' ; } ; /** Determines whether a step in a cephalometric analysis needs to be performed by the user  */ export const isStepManual = ( step ) => ! isStepAutomatic ( step ) ; /** Determines whether a step in a cephalometric analysis can be represented a geometrical object  */ export const isStepMappable = ( _ ) => true ; /** Determines whether a step in a cephalometric analysis can be computed as a numerical value */ export function isStepComputable ( step ) { return typeof step . calculate === 'function' ; } ; /**
 * Default implementation of Analysis.interpret.
 * Returns the falttened interpretation of each of this analysis components
 * grouped by category and resolves indication and severity with the default
 * resolving strategy.
 */ export const defaultInterpretAnalysis = ( components ) : InterpretAnalysis < Category > => { return ( values , _ ) => { const results = flatten ( map ( components , ( { landmark : { symbol , interpret } , max , min , mean } ) => { const value = values [ symbol ] ; if ( typeof interpret === 'function' && typeof value === 'number' ) { return map ( interpret ( value , min , max , mean ) , r => ( { ... r , symbol } ) ) ; } else { return [ ] ; } } ) ) ; return map ( groupBy ( results , r => r . category ) , ( group , category ) => ( { category , indication : resolveIndication ( group ) , severity : resolveSeverity ( group ) , relevantComponents : map ( group , ( ( { symbol , value , mean , max , min } ) => ( { symbol , value , mean , max , min , } ) ) ) , } ) ) ; } ; } ; /**
 * Default implementation of CephLandmark.interpret.
 * Maps a value to one of three possible indications
 * based on the mean, maximum and minimum values.
 * For example, given a category of skeletalPattern and the ranges
 * ['class3', 'class1', 'class2'], the interpretation function should
 * indicate a Class 3 skeletal pattern given a value of -1 for ANB and
 * a mean of 2, min of 0 and maximum of 4.
 */ export function defaultInterpetLandmark < T extends Category > ( category , ranges ) { return ( value , min , max , mean ) => { let indication = ranges [ 1 ] ; let severity = 'none' ; if ( value > max ) { indication = ranges [ 2 ] ; } else if ( value < min ) { indication = ranges [ 0 ] ; } return [ { category , indication , severity , value , mean , max , min , } ] ; } ; } ; /**
 * Creates a landmark interpretation function that calls
 * any number of interpretation functions and returns a array
 * of interpretations composed of flattening the results of
 * each function call.
 */ export function composeInterpretation < C extends Category > ( ... args ) { return ( value , max , min , mean ) => { return flatten ( map ( args , fn => fn ( value , max , min , mean ) ) ) ; } ; } ; /**
 * Tries to get the most reasonable indication given contradicting
 * interpretations of the evaluated value of a landmark by returning the
 * most occurring indication.
 */ export function resolveIndication < C extends Category > ( results ) { const counts = countBy ( results , r => r . indication ) ; const pairs = map ( counts , ( value , indication ) => ( { value , indication , } ) ) ; const max = maxBy ( pairs , ( { value } ) => value ) ; return max . indication ; } ; /**
 * Default strategy for resolving conflicting severity values.
 * Tries to get the most reasonable severity value given contradicting
 * interpretations of the evaluated value of a landmark by returning the
 * most occurring severity value.
 */ export function resolveSeverity < C extends Category > ( results ) { const counts = countBy ( results , r => r . severity ) ; const pairs = map ( counts , ( value , severity ) => ( { value , severity } ) ) ; const max = maxBy ( pairs , ( { value } ) => value ) ; return max . severity ; } ; export const indexAnalysisResults = < C extends Category > ( results ) => { return keyBy ( results , 'category' ) as IndexedAnalysisInterpretation ; } ; /**
 * Given a **topoligcally sorted** list of cephalometric landmarks
 * and a record of manually set landmarks, this function tries
 * to calculate and map each given landmark taking into account
 * the previous steps.
 */ export const mapAndCalculateSteps = ( steps , manualLandmarks ) => { const objects = { ... manualLandmarks } ; const values = { } ; for ( const step of steps ) { const mapped = map ( step . components , c => objects [ c . symbol ] ) ; if ( some ( mapped , isUndefined ) ) { if ( __DEBUG__ ) { const unmapped = step . components . map ( ( { symbol } ) => symbol ) . filter ( symbol => isUndefined ( objects [ symbol ] ) ) ; console . warn ( `Every sub component must be mapped in order to map ${ step . symbol }. ` + `The following sub components were not mapped: ${ unmapped . join ( ', ' ) }` ) ; } } else { const calculated = map ( step . components , c => values [ c . symbol ] ) ; if ( typeof step . map === 'function' ) { objects [ step . symbol ] = step . map ( ... mapped ) ; } if ( typeof step . calculate === 'function' ) { values [ step . symbol ] = step . calculate ( ... calculated ) ( ... mapped ) ( objects [ step . symbol ] ) ; } } } return { values , objects } ; } ;