import { TAU } from '../../../../common/constants' ; import { Coordinate , Distance , Pixels , PointTuple , Radian , Ratio , RingSectorConstruction , trueBearingToStandardPositionAngle , wrapToTau , } from '../../../../common/geometry' ; import { logarithm } from '../../../../common/math' ; import { Box , Font , HorizontalAlignment , PartialFont , VerticalAlignments } from '../../../../common/text_utils' ; import { integerSnap , monotonicHillClimb } from '../../../../solvers/monotonic_hill_climb' ; import { TextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator' ; import { ValueFormatter , getOppositeAlignment , isRTLString } from '../../../../utils/common' ; import { FillLabelConfig , Padding , PartitionStyle } from '../../../../utils/themes/partition' ; import { Layer } from '../../specs' ; import { QuadViewModel , RawTextGetter , RowBox , RowSet , RowSpace , ShapeTreeNode , ValueGetterFunction , } from '../types/viewmodel_types' ; import { conjunctiveConstraint , INFINITY_RADIUS , makeRowCircline } from '../utils/circline_geometry' ; import { RectangleConstruction } from './viewmodel' ; internal export function nodeId ( node ) { return `${ node . x0 }|${ node . y0 }` ; } internal export const getSectorRowGeometry = ( ringSector , cx , cy , totalRowCount , linePitch , rowIndex , fontSize , rotation ) => { const offset = ( totalRowCount / 2 ) * fontSize + fontSize / 2 - linePitch * rowIndex ; const topCircline = makeRowCircline ( cx , cy , offset , rotation , fontSize , 1 ) ; const bottomCircline = makeRowCircline ( cx , cy , offset , rotation , fontSize , - 1 ) ; const midCircline = makeRowCircline ( cx , cy , offset , rotation , 0 , 0 ) ; const valid1 = conjunctiveConstraint ( ringSector , { ... topCircline , from : 0 , to : TAU } ) [ 0 ] ; if ( ! valid1 ) return { rowAnchorX : cx , rowAnchorY : cy , maximumRowLength : 0 } ; const valid2 = conjunctiveConstraint ( ringSector , { ... bottomCircline , from : 0 , to : TAU } ) [ 0 ] ; if ( ! valid2 ) return { rowAnchorX : cx , rowAnchorY : cy , maximumRowLength : 0 } ; const from = Math . max ( valid1 . from , valid2 . from ) ; const to = Math . min ( valid1 . to , valid2 . to ) ; const midAngle = ( from + to ) / 2 ; const cheapTangent = Math . max ( 0 , to - from ) ; const rowAnchorX = midCircline . r * Math . cos ( midAngle ) + midCircline . x ; const rowAnchorY = midCircline . r * Math . sin ( midAngle ) + midCircline . y ; const maximumRowLength = cheapTangent * INFINITY_RADIUS ; return { rowAnchorX , rowAnchorY , maximumRowLength } ; } ; function getVerticalAlignment ( container , verticalAlignment , linePitch , totalRowCount , rowIndex , paddingTop , paddingBottom , fontSize , overhang ) { switch ( verticalAlignment ) { case VerticalAlignments . top : return - ( container . y0 + linePitch * rowIndex + paddingTop + fontSize * overhang ) ; case VerticalAlignments . bottom : return - ( container . y1 - linePitch * ( totalRowCount - 1 - rowIndex ) - paddingBottom - fontSize * overhang ) ; default : return - ( ( container . y0 + container . y1 ) / 2 + ( linePitch * ( rowIndex + 1 - totalRowCount ) ) / 2 ) ; } } internal export const getRectangleRowGeometry = ( container , cx , cy , totalRowCount , linePitch , rowIndex , fontSize , _rotation , verticalAlignment , padding ) => { const defaultPad = 2 ; const { top , right , bottom , left } = typeof padding === 'number' ? { top : padding , right : padding , bottom : padding , left : padding } : { ... { top : defaultPad , right : defaultPad , bottom : defaultPad , left : defaultPad } , ... padding , } ; const overhang = 0.05 ; const topPaddingAdjustment = fontSize < 6 ? 0 : Math . max ( 1 , Math . min ( 2 , fontSize / 16 ) ) ; const adjustedTop = top + topPaddingAdjustment ; if ( ( container . y1 - container . y0 - adjustedTop - bottom ) / totalRowCount < linePitch ) { return { rowAnchorX : NaN , rowAnchorY : NaN , maximumRowLength : 0 , } ; } const rowAnchorY = getVerticalAlignment ( container , verticalAlignment , linePitch , totalRowCount , rowIndex , adjustedTop , bottom , fontSize , overhang ) ; return { rowAnchorX : cx + left / 2 - right / 2 , rowAnchorY , maximumRowLength : container . x1 - container . x0 - left - right , } ; } ; function rowSetComplete ( rowSet , measuredBoxes ) { return ( measuredBoxes . length === 0 && ! rowSet . rows . some ( ( r ) => ! Number . isFinite ( r . length ) || r . rowWords . length === 0 || r . rowWords . every ( ( rw ) => rw . text . length === 0 ) ) ) ; } function identityRowSet ( ) { return { id : '' , rows : [ ] , fontSize : NaN , fillTextColor : '' , rotation : NaN , isRTL : false , verticalAlignment : VerticalAlignments . middle , horizontalAlignment : HorizontalAlignment . center , } ; } function getAllBoxes ( rawTextGetter , valueGetter , valueFormatter , sizeInvariantFontShorthand , valueFont , node ) { return rawTextGetter ( node ) . split ( ' ' ) . filter ( Boolean ) . map < Box > ( ( text ) => ( { text , ... sizeInvariantFontShorthand , isValue : false } ) ) . concat ( [ valueFormatter ( valueGetter ( node ) ) ] . filter ( Boolean ) . map < Box > ( ( text ) => ( { text , ... sizeInvariantFontShorthand , ... valueFont , isValue : true } ) ) ) ; } function getWordSpacing ( fontSize ) { return fontSize / 4 ; } type GetShapeRowGeometry < C > = ( container : C , cx : Distance , cy : Distance , targetRowCount : number , linePitch : Pixels , currentRowIndex : number , fontSize : Pixels , rotation : Radian , verticalAlignment : VerticalAlignments , padding : Padding ) => RowSpace ; type ShapeConstructor < C > = ( n : ShapeTreeNode ) => C ; type NodeWithOrigin = { node : QuadViewModel ; origin : PointTuple ; } ; function fill < C > ( shapeConstructor , getShapeRowGeometry , getRotation ) { return function fillClosure ( fillLabel , layers , measure , rawTextGetter , valueGetter , formatter , maxRowCount , leftAlign , middleAlign ) { const horizontalAlignment = leftAlign ? HorizontalAlignment . left : HorizontalAlignment . center ; return ( allFontSizes , textFillOrigin , node ) : RowSet => { const container = shapeConstructor ( node ) ; const rotation = getRotation ( node ) ; const layer = layers [ node . depth - 1 ] || { } ; const verticalAlignment = middleAlign ? VerticalAlignments . middle : node . depth < layers . length ? VerticalAlignments . bottom : VerticalAlignments . top ; const fontSizes = allFontSizes [ Math . min ( node . depth , allFontSizes . length ) - 1 ] ; const { fontStyle , fontVariant , fontFamily , fontWeight , valueFormatter , padding , clipText } = { ... fillLabel , valueFormatter : formatter , ... layer . fillLabel , ... layer . shape , } ; const valueFont = { ... fillLabel , ... fillLabel . valueFont , ... layer . fillLabel , ... layer . fillLabel ?. valueFont , } ; const sizeInvariantFont = { fontStyle , fontVariant , fontWeight , fontFamily , textColor : node . textColor , } ; const isRtlString = isRTLString ( rawTextGetter ( node ) ) ; const allBoxes = getAllBoxes ( rawTextGetter , valueGetter , valueFormatter , sizeInvariantFont , valueFont , node ) ; const [ cx , cy ] = textFillOrigin ; return { ... getRowSet ( allBoxes , maxRowCount , fontSizes , measure , rotation , verticalAlignment , horizontalAlignment , container , getShapeRowGeometry , cx , cy , padding , node , clipText , isRtlString ) , fillTextColor : node . textColor , } ; } ; } ; } function tryFontSize < C > ( measure , rotation , verticalAlignment , horizontalAlignment , container , getShapeRowGeometry , cx , cy , padding , node , boxes , maxRowCount , clipText , isRTL ) { const adjustedHorizontalAlignment = isRTL ? getOppositeAlignment ( horizontalAlignment ) : horizontalAlignment ; return function tryFontSizeFn ( initialRowSet , fontSize ) : { rowSet : RowSet ; completed : boolean ; } { let rowSet = initialRowSet ; const wordSpacing = getWordSpacing ( fontSize ) ; const allMeasuredBoxes = boxes . map < RowBox > ( ( box ) => { const { width } = measure ( box . text , box , fontSize ) ; return { width , wordBeginning : NaN , ... box , fontSize , } ; } ) ; const linePitch = fontSize ; let targetRowCount = 0 ; let measuredBoxes = allMeasuredBoxes . slice ( ) ; let innerCompleted = false ; while ( ++ targetRowCount <= maxRowCount && ! innerCompleted ) { measuredBoxes = allMeasuredBoxes . slice ( ) ; rowSet = { id : nodeId ( node ) , fontSize , fillTextColor : '' , rotation , verticalAlignment , horizontalAlignment : adjustedHorizontalAlignment , clipText , isRTL , rows : [ ... new Array ( targetRowCount ) ] . map ( ( ) => ( { rowWords : [ ] , rowAnchorX : NaN , rowAnchorY : NaN , maximumLength : NaN , length : NaN , } ) ) , container , } ; let currentRowIndex = 0 ; while ( currentRowIndex < targetRowCount ) { const currentRow = rowSet . rows [ currentRowIndex ] ; const currentRowWords = currentRow . rowWords ; const { maximumRowLength , rowAnchorX , rowAnchorY } = getShapeRowGeometry ( container , cx , cy , targetRowCount , linePitch , currentRowIndex , fontSize , rotation , verticalAlignment , padding ) ; currentRow . rowAnchorX = rowAnchorX ; currentRow . rowAnchorY = rowAnchorY ; currentRow . maximumLength = maximumRowLength ; let currentRowLength = 0 ; let rowHasRoom = true ; while ( measuredBoxes . length > 0 && rowHasRoom ) { const [ currentBox ] = measuredBoxes ; const wordBeginning = currentRowLength ; currentRowLength += currentBox . width + wordSpacing ; if ( clipText || currentRowLength <= currentRow . maximumLength ) { currentRowWords . push ( { ... currentBox , wordBeginning } ) ; currentRow . length = currentRowLength ; measuredBoxes . shift ( ) ; } else { rowHasRoom = false ; } } currentRowIndex ++ ; } innerCompleted = rowSetComplete ( rowSet , measuredBoxes ) ; } const completed = measuredBoxes . length === 0 ; return { rowSet , completed } ; } ; } function getRowSet < C > ( boxes , maxRowCount , fontSizes , measure , rotation , verticalAlignment , horizontalAlignment , container , getShapeRowGeometry , cx , cy , padding , node , clipText , isRtl ) { const tryFunction = tryFontSize ( measure , rotation , verticalAlignment , horizontalAlignment , container , getShapeRowGeometry , cx , cy , padding , node , boxes , maxRowCount , clipText , isRtl ) ; const largestIndex = fontSizes . length - 1 ; const response = ( i ) => i + ( tryFunction ( identityRowSet ( ) , fontSizes [ i ] ) . completed ? 0 : largestIndex + 1 ) ; const fontSizeIndex = monotonicHillClimb ( response , largestIndex , largestIndex , integerSnap ) ; if ( ! ( fontSizeIndex >= 0 ) ) { return identityRowSet ( ) ; } const { rowSet , completed } = tryFunction ( identityRowSet ( ) , fontSizes [ fontSizeIndex ] ) ; return { ... rowSet , rows : rowSet . rows . filter ( ( r ) => completed && Number . isFinite ( r . length ) ) } ; } internal export function inSectorRotation ( horizontalTextEnforcer , horizontalTextAngleThreshold ) { return ( node ) : Radian => { let rotation = trueBearingToStandardPositionAngle ( ( node . x0 + node . x1 ) / 2 ) ; if ( Math . abs ( node . x1 - node . x0 ) > horizontalTextAngleThreshold && horizontalTextEnforcer > 0 ) rotation *= 1 - horizontalTextEnforcer ; if ( TAU / 4 < rotation && rotation < ( 3 * TAU ) / 4 ) rotation = wrapToTau ( rotation - TAU / 2 ) ; return rotation ; } ; } type GetRotation = ( node : ShapeTreeNode ) => Radian ; internal export function fillTextLayout < C > ( shapeConstructor , getShapeRowGeometry , getRotation ) { const specificFiller = fill ( shapeConstructor , getShapeRowGeometry , getRotation ) ; return function fillTextLayoutClosure ( measure , rawTextGetter , valueGetter , valueFormatter , childNodes , style , layers , textFillOrigins , maxRowCount , leftAlign , middleAlign ) : RowSet [ ] { const allFontSizes = [ ] ; for ( let l = 0 ; l <= layers . length ; l ++ ) { const { minFontSize , maxFontSize , idealFontSizeJump } = { ... style , ... ( l < layers . length && layers [ l ] . fillLabel ) , } ; const fontSizeMagnification = maxFontSize / minFontSize ; const fontSizeJumpCount = Math . round ( logarithm ( idealFontSizeJump , fontSizeMagnification ) ) ; const realFontSizeJump = Math . pow ( fontSizeMagnification , 1 / fontSizeJumpCount ) ; const fontSizes = [ ] ; for ( let i = 0 ; i <= fontSizeJumpCount ; i ++ ) { const fontSize = Math . round ( minFontSize * Math . pow ( realFontSizeJump , i ) ) ; if ( ! fontSizes . includes ( fontSize ) ) { fontSizes . push ( fontSize ) ; } } allFontSizes . push ( fontSizes ) ; } const filler = specificFiller ( style . fillLabel , layers , measure , rawTextGetter , valueGetter , valueFormatter , maxRowCount , leftAlign , middleAlign ) ; return childNodes . map ( ( node , i ) => ( { node , origin : textFillOrigins [ i ] } ) ) . sort ( ( a , b ) => b . node . value - a . node . value ) . reduce ( ( { rowSets , fontSizes } , { node , origin } ) => { const nextRowSet = filler ( fontSizes , origin , node ) ; const { fontSize } = nextRowSet ; const layerIndex = node . depth - 1 ; return { rowSets : [ ... rowSets , nextRowSet ] , fontSizes : fontSizes . map ( ( layerFontSizes , index ) => Number . isFinite ( fontSize ) && index === layerIndex && ! layers [ layerIndex ] ?. fillLabel ?. maximizeFontSize ? layerFontSizes . filter ( ( size ) => size <= fontSize ) : layerFontSizes ) , } ; } , { rowSets : [ ] as RowSet [ ] , fontSizes : allFontSizes } ) . rowSets ; } ; }