import { Logger } from "@itwin/core-bentley" ; import { CheckboxFormatPropEditorSpec , CustomFormatPropEditorSpec , CustomQuantityTypeDefinition , IModelApp , TextInputFormatPropEditorSpec , TextSelectFormatPropEditorSpec , } from "@itwin/core-frontend" ; import { CustomFormatProps , Format , FormatProps , FormatterSpec , Parser , ParserSpec , QuantityParseResult , UnitConversionSpec , UnitProps , UnitsProvider , UnitSystemKey , } from "@itwin/core-quantity" ; interface BearingFormatProps extends CustomFormatProps { readonly custom : { readonly addDirectionLabelGap : boolean ; readonly angleDirection : string ; readonly testString : string ; } ; } /** Type guard that checks the FormatProps to see if it contains the necessary props to be considered
 * BearingFormatProps. */ const isBearingFormatProps = ( item ) : item is BearingFormatProps => { return ( ( item as CustomFormatProps ) . custom !== undefined ) && ( ( item as BearingFormatProps ) . custom . addDirectionLabelGap !== undefined ) && ( ( item as BearingFormatProps ) . custom . angleDirection !== undefined ) ; } ; /** Define the default Bearing Props that will create a formatted angle value that display Degrees, Minutes, and Seconds. This includes
 * defining the default values for addDirectionLabelGap and angleDirection.
*/ const defaultBearingFormat = { composite : { includeZero : true , spacer : "" , units : [ { label : "°" , name : "Units.ARC_DEG" } , { label : "'" , name : "Units.ARC_MINUTE" } , { label : "\"" , name : "Units.ARC_SECOND" } ] } , formatTraits : [ "showUnitLabel" ] , precision : 0 , type : "Decimal" , uomSeparator : "" , custom : { addDirectionLabelGap : true , angleDirection : "clockwise" , testString : "test-string" } } ; /** Class that is the FormatterSpec for an angle.  It leaves the conversion from Radians to DMS to the base FormatterSpec and handle the adding
 * of the direction letters. */ class BearingFormatterSpec extends FormatterSpec { constructor ( name , format , conversions , persistenceUnit ) { super ( name , format , conversions , persistenceUnit ) ; } public override applyFormatting ( magnitude ) { const prefix = [ "N" , "S" , "S" , "N" ] ; const suffix = [ "E" , "E" , "W" , "W" ] ; if ( this . format . customProps ?. angleDirection === "counter-clockwise" ) { magnitude = ( Math . PI * 2 ) - magnitude ; } const isNegative = magnitude < 0 ; const positiveRad = Math . abs ( magnitude ) ; const maxRad = Math . PI * 2 ; let adjustedRad = ( positiveRad + maxRad ) % maxRad ; if ( isNegative ) adjustedRad = maxRad - adjustedRad ; let radToFormat = adjustedRad ; let quadrant = 1 ; if ( adjustedRad > Math . PI / 2 && adjustedRad <= Math . PI ) { radToFormat = Math . PI - adjustedRad ; quadrant = 2 ; } else if ( adjustedRad > Math . PI && adjustedRad <= ( 3 * Math . PI / 2 ) ) { radToFormat = adjustedRad - Math . PI ; quadrant = 3 ; } else if ( adjustedRad > ( 3 * Math . PI / 2 ) && adjustedRad < ( 2 * Math . PI ) ) { radToFormat = ( 2 * Math . PI ) - adjustedRad ; quadrant = 4 ; } const gapChar = ( this . format . customProps ?. addDirectionLabelGap ) ? " " : "" ; const formattedValue = super . applyFormatting ( radToFormat ) ; return `${ prefix [ quadrant - 1 ] }${ gapChar }${ formattedValue }${ gapChar }${ suffix [ quadrant - 1 ] }` ; } internal public static override async create ( name , format , unitsProvider , persistenceUnit ) { const conversions = await FormatterSpec . getUnitConversions ( format , unitsProvider , persistenceUnit ) ; return new BearingFormatterSpec ( name , format , conversions , persistenceUnit ) ; } } /** Class that is the ParserSpec for an angle which converts a string value into radians that represent the angle..  It leaves the parsing
 *  from string to Radians to the base ParserSpec and handles the adjustment necessary due to the direction letters being present in the string. */ class BearingParserSpec extends ParserSpec { constructor ( outUnit , format , conversions ) { super ( outUnit , format , conversions ) ; } public override parseToQuantityValue ( inString ) { let prefix ; let suffix ; let adjustedString = inString . toLocaleUpperCase ( ) . trimLeft ( ) . trimRight ( ) ; if ( adjustedString . startsWith ( "S" ) || adjustedString . startsWith ( "N" ) ) { prefix = adjustedString . slice ( 0 , 1 ) ; adjustedString = adjustedString . substr ( 1 ) ; } if ( adjustedString . endsWith ( "E" ) || adjustedString . endsWith ( "W" ) ) { suffix = adjustedString . slice ( adjustedString . length - 1 ) ; adjustedString = adjustedString . substr ( 0 , adjustedString . length - 1 ) ; adjustedString = adjustedString . trimLeft ( ) . trimRight ( ) ; } const isCCW = this . format . customProps ?. angleDirection === "counter-clockwise" ; const parsedRadians = Parser . parseToQuantityValue ( adjustedString , this . format , this . unitConversions ) ; if ( Parser . isParsedQuantity ( parsedRadians ) ) { if ( prefix === "N" && suffix === "E" ) { if ( isCCW ) parsedRadians . value = ( 2 * Math . PI ) - parsedRadians . value ; } else if ( prefix === "N" && suffix === "W" ) { if ( ! isCCW ) parsedRadians . value = ( 2 * Math . PI ) - parsedRadians . value ; } else if ( prefix === "S" && suffix === "W" ) { if ( isCCW ) parsedRadians . value = Math . PI - parsedRadians . value ; else parsedRadians . value = parsedRadians . value + Math . PI ; } else if ( prefix === "S" && suffix === "E" ) { if ( isCCW ) parsedRadians . value = Math . PI + parsedRadians . value ; else parsedRadians . value = Math . PI - parsedRadians . value ; } } return parsedRadians ; } param format param unitsProvider param outUnit public static override async create ( format , unitsProvider , outUnit ) { const conversions = await Parser . createUnitConversionSpecsForUnit ( unitsProvider , outUnit ) ; return new BearingParserSpec ( outUnit , format , conversions ) ; } } internal export class BearingQuantityType implements CustomQuantityTypeDefinition { private _key = "Bearing" ; private _type = "Bearing" ; private _persistenceUnitName = "Units.RAD" ; private _persistenceUnit : UnitProps | undefined ; private _labelKey = "SampleApp:BearingQuantityType.label" ; private _descriptionKey = "SampleApp:BearingQuantityType.description" ; private _label : string | undefined ; private _description : string | undefined ; private _formatProps = defaultBearingFormat ; public get key ( ) : string { return this . _key ; } public get type ( ) : string { return this . _type ; } public isCompatibleFormatProps ( formatProps ) { return isBearingFormatProps ( formatProps ) ; } public get formatProps ( ) : FormatProps { return this . _formatProps ; } public set formatProps ( value ) { if ( isBearingFormatProps ( value ) ) { this . _formatProps = value ; } throw new Error ( `formatProps passed to BearingQuantity setter is not a BearingFormatProps` ) ; } public get persistenceUnit ( ) : UnitProps { if ( this . _persistenceUnit ) return this . _persistenceUnit ; throw new Error ( `_persistenceUnit is not set, did you call BearingQuantityType.registerQuantityType?` ) ; } public get label ( ) : string { if ( ! this . _label ) { if ( this . _labelKey ) this . _label = IModelApp . localization . getLocalizedString ( this . _labelKey ) ; else this . _label = this . _type ; } return this . _label ? this . _label : "unknown" ; } public get description ( ) : string { if ( ! this . _description ) { if ( this . _descriptionKey ) this . _description = IModelApp . localization . getLocalizedString ( this . _descriptionKey ) ; else this . _description = this . label ; } return this . _description ? this . _description : "unknown" ; } public generateFormatterSpec = async ( formatProps , unitsProvider ) => { if ( isBearingFormatProps ( formatProps ) ) { const format = new Format ( "Bearing" ) ; await format . fromJSON ( unitsProvider , formatProps ) ; return BearingFormatterSpec . create ( format . name , format , unitsProvider , this . persistenceUnit ) ; } throw new Error ( `formatProps passed to BearingQuantity type is not a BearingFormatProps` ) ; } ; public generateParserSpec = async ( formatProps , unitsProvider ) => { if ( isBearingFormatProps ( formatProps ) ) { const format = new Format ( "Bearing" ) ; await format . fromJSON ( unitsProvider , formatProps ) ; return BearingParserSpec . create ( format , unitsProvider , this . persistenceUnit ) ; } throw new Error ( `formatProps passed to BearingQuantity type is not a BearingFormatProps` ) ; } ; public getDefaultFormatPropsBySystem = ( _requestedSystem ) => { return this . formatProps ; } ; public get secondaryPropEditorSpecs ( ) : CustomFormatPropEditorSpec [ ] { return [ { editorType : "select" , selectOptions : [ { value : "clockwise" , label : IModelApp . localization . getLocalizedString ( "SampleApp:BearingQuantityType.bearingAngleDirection.clockwise" ) } , { value : "counter-clockwise" , label : IModelApp . localization . getLocalizedString ( "SampleApp:BearingQuantityType.bearingAngleDirection.counter-clockwise" ) } , ] , label : IModelApp . localization . getLocalizedString ( "SampleApp:BearingQuantityType.bearingAngleDirection.label" ) , getString : BearingQuantityType . bearingAngleDirectionGetter , setString : BearingQuantityType . bearingAngleDirectionSetter } as TextSelectFormatPropEditorSpec , { editorType : "text" , label : "Test Text" , getString : BearingQuantityType . bearingTextGetter , setString : BearingQuantityType . bearingTextSetter } as TextInputFormatPropEditorSpec , ] ; } public get primaryPropEditorSpecs ( ) : CustomFormatPropEditorSpec [ ] { return [ { editorType : "checkbox" , label : IModelApp . localization . getLocalizedString ( "SampleApp:BearingQuantityType.bearingGap.label" ) , getBool : BearingQuantityType . bearingGapPropGetter , setBool : BearingQuantityType . bearingGapPropSetter } as CheckboxFormatPropEditorSpec , ] ; } public static async registerQuantityType ( initialProps ? ) { const quantityTypeDefinition = new BearingQuantityType ( ) ; if ( initialProps && isBearingFormatProps ( initialProps ) ) { quantityTypeDefinition . formatProps = initialProps ; } quantityTypeDefinition . _persistenceUnit = await IModelApp . quantityFormatter . findUnitByName ( quantityTypeDefinition . _persistenceUnitName ) ; const wasRegistered = await IModelApp . quantityFormatter . registerQuantityType ( quantityTypeDefinition ) ; if ( ! wasRegistered ) { Logger . logInfo ( "BearingQuantityType" , `Unable to register QuantityType [BearingQuantityType] with key '${ quantityTypeDefinition . key }'` ) ; } } private static bearingGapPropGetter ( props ) { if ( isBearingFormatProps ( props ) ) { return props . custom . addDirectionLabelGap ; } throw new Error ( `formatProps passed to bearingGapPropGetter type is not a BearingFormatProps` ) ; } private static bearingGapPropSetter ( props , isChecked ) { if ( isBearingFormatProps ( props ) ) { const customProps = { ... props . custom , addDirectionLabelGap : isChecked } ; const newProps = { ... props , custom : customProps } ; return newProps ; } throw new Error ( `formatProps passed to bearingGapPropSetter type is not a BearingFormatProps` ) ; } private static bearingAngleDirectionGetter ( props ) { if ( isBearingFormatProps ( props ) ) { return props . custom . angleDirection ; } throw new Error ( `formatProps passed to bearingAngleDirectionGetter type is not a BearingFormatProps` ) ; } private static bearingAngleDirectionSetter ( props , value ) { if ( isBearingFormatProps ( props ) ) { const customProps = { ... props . custom , angleDirection : value } ; const newProps = { ... props , custom : customProps } ; return newProps ; } throw new Error ( `formatProps passed to bearingAngleDirectionSetter type is not a BearingFormatProps` ) ; } private static bearingTextGetter ( props ) { if ( isBearingFormatProps ( props ) ) { return props . custom . testString ; } throw new Error ( `formatProps passed to bearingTextGetter type is not a BearingFormatProps` ) ; } private static bearingTextSetter ( props , value ) { if ( isBearingFormatProps ( props ) ) { const customProps = { ... props . custom , testString : value } ; const newProps = { ... props , custom : customProps } ; return newProps ; } throw new Error ( `formatProps passed to bearingTextSetter type is not a BearingFormatProps` ) ; } }