import { PagemarkRect } from './PagemarkRect' ; import { Pagemark } from './Pagemark' ; import { Logger } from 'polar-shared/src/logger/Logger' ; import { Hashcodes } from 'polar-shared/src/util/Hashcodes' ; import { PagemarkType } from 'polar-shared/src/metadata/PagemarkType' ; import { PagemarkRects } from './PagemarkRects' ; import { Dictionaries } from 'polar-shared/src/util/Dictionaries' ; import { Percentage100 , Percentages , round } from 'polar-shared/src/util/Percentages' ; import { PagemarkMode } from 'polar-shared/src/metadata/PagemarkMode' ; import { DocMetas } from './DocMetas' ; import { isPresent , Preconditions } from 'polar-shared/src/Preconditions' ; import { ISODateTimeString , ISODateTimeStrings } from 'polar-shared/src/metadata/ISODateTimeStrings' ; import { Reducers } from 'polar-shared/src/util/Reducers' ; import { ReadingProgresses } from './ReadingProgresses' ; import { Provider } from 'polar-shared/src/util/Providers' ; import { HitMap } from 'polar-shared/src/util/HitMap' ; import { ReadingOverviews } from './ReadingOverviews' ; import { IPageMeta , PageNumber } from "polar-shared/src/metadata/IPageMeta" ; import { IDocMeta } from "polar-shared/src/metadata/IDocMeta" ; import { IPagemark } from "polar-shared/src/metadata/IPagemark" ; import { Numbers } from "polar-shared/src/util/Numbers" ; import { Objects } from "polar-shared/src/util/Objects" ; import { ExtendPagemark } from "polar-pagemarks-auto/src/AutoPagemarker" ; import { IPagemarkRef } from 'polar-shared/src/metadata/IPagemarkRef' ; import { IDStr } from "polar-shared/src/util/Strings" ; const log = Logger . create ( ) ; const DEFAULT_PAGEMARK_RECT = new PagemarkRect ( { left : 0 , top : 0 , width : 100 , height : 100 } ) ; export interface UpdatePagemarksForRangeOpts { readonly start ? : PageNumber ; readonly batch ? : IDStr ; } export class Pagemarks { public static sequences = { id : 0 , batch : 0 } ; public static createID ( created ) { return Hashcodes . createID ( { created , sequence : this . sequences . id ++ } ) ; } public static createExtender ( docMeta ) { return ( extendPagemark ) => { Pagemarks . updatePagemarksForRange ( docMeta , extendPagemark . page , 100 , { start : extendPagemark . origin } ) ; } ; } param docMeta param end param percentage param opts public static updatePagemarksForRange ( docMeta , end , percentage = 100 , opts = { } ) { if ( end < 1 ) { throw new Error ( "Page number must be 1 or more" ) ; } const created = ISODateTimeStrings . create ( ) ; const batch = opts . batch || Hashcodes . createID ( { created , id : this . sequences . batch ++ } ) ; const calculateStartPage = ( ) => { const range = [ ... Numbers . range ( 1 , Math . max ( 1 , end - 1 ) ) ] . reverse ( ) ; for ( const r of range ) { const pageMeta = DocMetas . getPageMeta ( docMeta , r ) ; if ( Dictionaries . size ( pageMeta . pagemarks || { } ) !== 0 ) { return r ; } } return 1 ; } ; const createPagemarkRect = ( pageNum , percentage = 100 ) : PagemarkRect | undefined => { const pageMeta = DocMetas . getPageMeta ( docMeta , pageNum ) ; const pagemarks = Object . values ( pageMeta . pagemarks || { } ) ; if ( pagemarks . length === 0 ) { return PagemarkRects . createFromRect ( { left : 0 , top : 0 , height : percentage , width : 100 } ) ; } let top = 0 ; for ( const pagemark of pagemarks ) { const newTop = pagemark . rect . top + pagemark . rect . height ; if ( newTop > top ) { top = newTop ; } } const coverage = pagemarks . map ( pagemark => pagemark . percentage ) . reduce ( Reducers . SUM , 0 ) ; if ( Math . floor ( coverage ) === 100 || top === 100 ) { return undefined ; } return PagemarkRects . createFromRect ( { left : 0 , top , height : 100 - top , width : 100 } ) ; } ; const start = opts . start || calculateStartPage ( ) ; const result = [ ] ; DocMetas . withBatchedMutations ( docMeta , ( ) => { for ( const pageNum of Numbers . range ( start , end ) ) { const rectPercentage = pageNum === end ? percentage : 100 ; const rect = createPagemarkRect ( pageNum , rectPercentage ) ; if ( rect ) { const pagemark = Pagemarks . create ( { created , rect , batch } ) ; Pagemarks . updatePagemark ( docMeta , pageNum , pagemark ) ; result . push ( { pageNum , pagemark } ) ; } } } ) ; return result ; } /**
     * Create a new pagemark with the created time, and other mandatory fields
     * added.
     *
     */ public static create ( opts = { } ) { const options = Objects . defaults ( opts , { type : PagemarkType . SINGLE_COLUMN , column : 0 , } ) ; const keyOptions = Pagemarks . createKeyOptions ( options ) ; if ( keyOptions . count === 0 ) { throw new Error ( "Must specify either rect or percentage." ) ; } if ( keyOptions . count === 1 ) { if ( keyOptions . hasPercentage ) { keyOptions . rect = PagemarkRects . createFromPercentage ( keyOptions . percentage ) ; } if ( keyOptions . hasRect ) { keyOptions . percentage = keyOptions . rect . toPercentage ( ) ; } } if ( round ( keyOptions . percentage ) !== round ( keyOptions . rect . toPercentage ( ) ) ) { const msg = "Percentage and rect are not the same" ; log . warn ( msg , keyOptions . percentage , keyOptions . rect , keyOptions . rect . toPercentage ( ) ) ; throw new Error ( msg ) ; } const created = options . created || ISODateTimeStrings . create ( ) ; const batch = options . batch || Hashcodes . createID ( { created , id : this . sequences . batch ++ } ) ; const mode = options . mode || PagemarkMode . READ ; const id = Pagemarks . createID ( created ) ; return new Pagemark ( { id , guid : id , created , type : options . type , percentage : Numbers . toFixedFloat ( keyOptions . percentage , 10 ) , column : options . column , rect : keyOptions . rect , batch , mode } ) ; } param options return { KeyPagemarkOptions } private static createKeyOptions ( options ) { const keyOptions = { count : 0 , hasPercentage : false , hasRect : false , rect : options . rect , percentage : options . percentage } ; keyOptions . hasPercentage = "percentage" in options ; keyOptions . hasRect = "rect" in options ; if ( keyOptions . hasPercentage ) { ++ keyOptions . count ; } if ( keyOptions . hasRect ) { ++ keyOptions . count ; } return keyOptions ; } /**
     *
     */ public static upgrade ( pagemarks ) { const result = { } ; Object . assign ( result , pagemarks ) ; Dictionaries . forDict ( result , ( key , pagemark ) => { if ( ! pagemark . rect ) { if ( pagemark . percentage >= 0 && pagemark . percentage <= 100 ) { pagemark . rect = PagemarkRects . createFromPercentage ( pagemark . percentage ) ; } } if ( Math . floor ( pagemark . rect . top ) === 100 ) { delete result [ key ] ; return ; } if ( ! pagemark . id ) { pagemark . id = key ; } if ( ! pagemark . mode ) { pagemark . mode = PagemarkMode . READ ; } if ( ! isPresent ( pagemark . percentage ) ) { pagemark . percentage = 0 ; } } ) ; return result ; } /**
     * Update pagemarks on the given page.
     *
     */ public static updatePagemark ( docMeta , pageNum , pagemark ) { pagemark . lastUpdated = ISODateTimeStrings . create ( ) ; this . doDocMetaMutation ( docMeta , pageNum , ( ) => { const pageMeta = DocMetas . getPageMeta ( docMeta , pageNum ) ; this . doPageMetaMutation ( pageMeta , ( ) => { pageMeta . pagemarks [ pagemark . id ] = pagemark ; } ) ; } ) ; } /**
     * Replace the pagemarks with a new pagemark with the given options
     * replaced.
     */ public static replacePagemark ( docMeta , pagemarkPtr , options ) { const pagemarksToMutate = ( ) => { const result = [ ] ; if ( pagemarkPtr . ref ) { const pageMeta = DocMetas . getPageMeta ( docMeta , pagemarkPtr . ref . pageNum ) ; result . push ( { pageMeta , id : pagemarkPtr . ref . pagemark . id } ) ; } if ( pagemarkPtr . batch ) { result . push ( ... this . pagemarksWithinBatch ( docMeta , pagemarkPtr . batch ) ) ; } return result ; } ; const pagemarkRefs = pagemarksToMutate ( ) ; DocMetas . withBatchedMutations ( docMeta , ( ) => { for ( const ref of pagemarkRefs ) { const currPagemark = ref . pageMeta . pagemarks [ ref . id ] ; const newPagemark = new Pagemark ( currPagemark ) ; if ( options . mode ) { newPagemark . mode = options . mode ; } this . doPageMetaMutation ( ref . pageMeta , ( ) => { ref . pageMeta . pagemarks [ ref . id ] = newPagemark ; } ) ; } } ) ; } param docMeta param pageNum param id public static deletePagemark ( docMeta , pageNum , id ? ) { this . doDocMetaMutation ( docMeta , pageNum , ( ) => { const pageMeta = DocMetas . getPageMeta ( docMeta , pageNum ) ; let pageMetaMutator ; if ( id ) { const primaryPagemark = pageMeta . pagemarks [ id ] ; if ( primaryPagemark ) { if ( primaryPagemark . batch ) { const pagemarksWithinBatch = this . pagemarksWithinBatch ( docMeta , primaryPagemark . batch ) ; pageMetaMutator = ( ) => { for ( const pagemarkRef of pagemarksWithinBatch ) { delete pagemarkRef . pageMeta . pagemarks [ pagemarkRef . id ] ; } } ; } else { pageMetaMutator = ( ) => delete pageMeta . pagemarks [ id ] ; } } else { log . warn ( `No pagemark found for id ${ id } for pageNum ${ pageNum }` ) ; } } else { pageMetaMutator = ( ) => Objects . clear ( pageMeta . pagemarks ) ; } this . doPageMetaMutation ( pageMeta , pageMetaMutator ) ; } ) ; } /**
     * Scan all the pagemarks finding ones with the same batch.
     */ private static pagemarksWithinBatch ( docMeta , batch ) { const result = [ ] ; const nrPages = Object . keys ( docMeta . pageMetas ) . length ; for ( let pageIdx = 1 ; pageIdx <= nrPages ; ++ pageIdx ) { const pageMeta = DocMetas . getPageMeta ( docMeta , pageIdx ) ; for ( const pagemark of Object . values ( pageMeta . pagemarks || { } ) ) { if ( pagemark . batch === batch ) { result . push ( { pageMeta , id : pagemark . id } ) ; } } } return result ; } private static doDocMetaMutation ( docMeta , pageNum , pagemarkMutator ) { Preconditions . assertPresent ( docMeta , "docMeta" ) ; Preconditions . assertPresent ( pageNum , "pageNum" ) ; DocMetas . withBatchedMutations ( docMeta , ( ) => { pagemarkMutator ( ) ; const progress = Math . floor ( DocMetas . computeProgress ( docMeta ) * 100 ) ; docMeta . docInfo . progress = progress ; docMeta . docInfo . readingPerDay = ReadingOverviews . compute ( Object . values ( docMeta . pageMetas ) ) ; } ) ; } /**
     * Mutate the pagemarks on the PageMeta and also update the readingProgress
     */ private static doPageMetaMutation ( pageMeta , pageMetaMutator ? ) { if ( ! pageMetaMutator ) { return ; } const createProgressByMode = ( ) => { const result = new HitMap ( ) ; for ( const pagemark of Object . values ( pageMeta . pagemarks ) ) { const mode = pagemark . mode || PagemarkMode . READ ; result . registerHit ( mode , pagemark . percentage ) ; } return result . toLiteralMap ( ) ; } ; const writeReadingProgress = ( preExisting ? ) => { const percentages = Object . values ( pageMeta . pagemarks ) . map ( current => current . percentage ) ; const progress = Percentages . sum ( ... percentages ) ; const progressByMode = createProgressByMode ( ) ; const readingProgress = ReadingProgresses . create ( progress , progressByMode , preExisting ) ; pageMeta . readingProgress [ readingProgress . id ] = readingProgress ; } ; const doPreExisting = Dictionaries . empty ( pageMeta . readingProgress ) && ! Dictionaries . empty ( pageMeta . pagemarks ) ; if ( doPreExisting ) { writeReadingProgress ( true ) ; } pageMetaMutator ( ) ; writeReadingProgress ( ) ; } public static computeReadingProgressStats ( docMetaProviders ) { for ( const docMetaProvider of docMetaProviders ) { const docMeta = docMetaProvider ( ) ; } } } interface PagemarkPageMetaRef { readonly pageMeta : IPageMeta ; readonly id : string ; } export interface PagemarkOptions { /**
     * The type of pagemark we're working with.
     */ type : PagemarkType ; /**
     */ rect : PagemarkRect ; /**
     */ percentage : number ; column : number ; batch ? : string ; created ? : string ; mode ? : PagemarkMode ; } /**
 * A pointer to a pagemark either by id , batch.
 */ export interface PagemarkPTR { readonly ref ? : IPagemarkRef ; readonly batch ? : string ; } export interface ReplacePagemarkOptions { readonly mode ? : PagemarkMode ; } /**
 * The key / important options when creating a Pagemark.
 */ export interface KeyPagemarkOptions { /**
     * The total number of key options.
     */ count : number ; /**
     * True when we have the percentage.
     *
     */ hasPercentage : boolean ; /**
     * True when we have the rect.
     *
     */ hasRect : boolean ; /**
     */ rect : PagemarkRect ; /**
     */ percentage : number ; } export type VOID_FUNCTION = ( ) => void ;