diff options
Diffstat (limited to 'src/client/util/TrackMovements.ts')
-rw-r--r-- | src/client/util/TrackMovements.ts | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts new file mode 100644 index 000000000..d512e4802 --- /dev/null +++ b/src/client/util/TrackMovements.ts @@ -0,0 +1,257 @@ +import { IReactionDisposer, observable, observe, reaction } from "mobx"; +import { NumCast } from "../../fields/Types"; +import { Doc, DocListCast } from "../../fields/Doc"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { Id } from "../../fields/FieldSymbols"; + +export type Movement = { + time: number, + panX: number, + panY: number, + scale: number, + docId: string, +} + +export type Presentation = { + movements: Movement[] | null, + totalTime: number, + meta: Object | Object[], +} + +export class TrackMovements { + + private static get NULL_PRESENTATION(): Presentation { + return { movements: null, meta: {}, totalTime: -1, } + } + + // instance variables + private currentPresentation: Presentation; + private tracking: boolean; + private absoluteStart: number; + // instance variable for holding the FFViews and their disposers + private recordingFFViews: Map<string, IReactionDisposer> | null; + private tabChangeDisposeFunc: IReactionDisposer | null; + + + // create static instance and getter for global use + @observable static _instance: TrackMovements; + static get Instance(): TrackMovements { return TrackMovements._instance } + constructor() { + // init the global instance + TrackMovements._instance = this; + + // init the instance variables + this.currentPresentation = TrackMovements.NULL_PRESENTATION + this.tracking = false; + this.absoluteStart = -1; + + // used for tracking movements in the view frame + this.recordingFFViews = null; + this.tabChangeDisposeFunc = null; + } + + // little helper :) + private get nullPresentation(): boolean { + return this.currentPresentation.movements === null + } + + private addRecordingFFView(doc: Doc, key: string = doc[Id]): void { + // console.info('adding dispose func : docId', key, 'doc', doc); + + if (this.recordingFFViews === null) { console.warn('addFFView on null RecordingApi'); return; } + if (this.recordingFFViews.has(key)) { console.warn('addFFView : key already in map'); return; } + + const disposeFunc = reaction( + () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0)}), + (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovement(res.x, res.y, key, res.scale), + ); + this.recordingFFViews?.set(key, disposeFunc); + } + + private removeRecordingFFView = (key: string) => { + // console.info('removing dispose func : docId', key); + if (this.recordingFFViews === null) { console.warn('removeFFView on null RecordingApi'); return; } + this.recordingFFViews.get(key)?.(); + this.recordingFFViews.delete(key); + } + + // in the case where only one tab was changed (updates not across dashboards), set only one to true + private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => { + if (this.recordingFFViews === null) return; + + // so that the size comparisons are correct, we must filter to only the FFViews + const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform'; + const tabbedFFViews = new Set<string>(); + for (const DashDoc of tabbedDocs) { + if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]); + } + + + // new tab was added - need to add it + if (tabbedFFViews.size > this.recordingFFViews.size) { + for (const DashDoc of tabbedDocs) { + if (!this.recordingFFViews.has(DashDoc[Id])) { + if (isFFView(DashDoc)) { + this.addRecordingFFView(DashDoc); + + // only one max change, so return + if (onlyOne) return; + } + } + } + } + // tab was removed - need to remove it from recordingFFViews + else if (tabbedFFViews.size < this.recordingFFViews.size) { + for (const [key] of this.recordingFFViews) { + if (!tabbedFFViews.has(key)) { + this.removeRecordingFFView(key); + if (onlyOne) return; + } + } + } + } + + private initTabTracker = () => { + if (this.recordingFFViews === null) { + this.recordingFFViews = new Map(); + } + + // init the dispose funcs on the page + const docList = DocListCast(CollectionDockingView.Instance.props.Document.data); + this.updateRecordingFFViewsFromTabs(docList); + + // create a reaction to monitor changes in tabs + this.tabChangeDisposeFunc = + reaction(() => CollectionDockingView.Instance.props.Document.data, + (change) => { + // TODO: consider changing between dashboards + // console.info('change in tabs', change); + this.updateRecordingFFViewsFromTabs(DocListCast(change), true); + }); + } + + start = (meta?: Object) => { + this.initTabTracker(); + + // update the presentation mode + Doc.UserDoc().presentationMode = 'recording'; + + // (1a) get start date for presenation + const startDate = new Date(); + // (1b) set start timestamp to absolute timestamp + this.absoluteStart = startDate.getTime(); + + // (2) assign meta content if it exists + this.currentPresentation.meta = meta || {} + // (3) assign start date to currentPresenation + this.currentPresentation.movements = [] + // (4) set tracking true to allow trackMovements + this.tracking = true + } + + /* stops the video and returns the presentatation; if no presentation, returns undefined */ + yieldPresentation(clearData: boolean = true): Presentation | null { + // if no presentation or done tracking, return null + if (this.nullPresentation || !this.tracking) return null; + + // set the previus recording view to the play view + // this.playFFView = this.recordingFFView; + + // ensure we add the endTime now that they are done recording + const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart }; + + // reset the current presentation + clearData && this.clear(); + + // console.info('yieldPresentation', cpy); + return cpy; + } + + finish = (): void => { + // make is tracking false + this.tracking = false + // reset the RecordingApi instance + this.clear(); + } + + private clear = (): void => { + // clear the disposeFunc if we are done (not tracking) + if (!this.tracking) { + this.removeAllRecordingFFViews(); + this.tabChangeDisposeFunc?.(); + // update the presentation mode now that we are done tracking + Doc.UserDoc().presentationMode = 'none'; + + this.recordingFFViews = null; + this.tabChangeDisposeFunc = null; + } + + // clear presenation data + this.currentPresentation = TrackMovements.NULL_PRESENTATION + // clear absoluteStart + this.absoluteStart = -1 + } + + removeAllRecordingFFViews = () => { + if (this.recordingFFViews === null) { console.warn('removeAllFFViews on null RecordingApi'); return; } + + for (const [id, disposeFunc] of this.recordingFFViews) { + // console.info('calling dispose func : docId', id); + disposeFunc(); + this.recordingFFViews.delete(id); + } + } + + private trackMovement = (panX: number, panY: number, docId: string, scale: number = 0) => { + // ensure we are recording to track + if (!this.tracking) { + console.error('[recordingApi.ts] trackMovements(): tracking is false') + return; + } + // check to see if the presetation is init - if not, we are between segments + // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres) + // bacuse tracking should be false inbetween segments high key + if (this.nullPresentation) { + console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments') + return; + } + + // get the time + const time = new Date().getTime() - this.absoluteStart + // make new movement object + const movement: Movement = { time, panX, panY, scale, docId } + + // add that movement to the current presentation data's movement array + this.currentPresentation.movements && this.currentPresentation.movements.push(movement) + } + + + // method that concatenates an array of presentatations into one + public concatPresentations = (presentations: Presentation[]): Presentation => { + // these three will lead to the combined presentation + let combinedMovements: Movement[] = []; + let sumTime = 0; + let combinedMetas: any[] = []; + + presentations.forEach((presentation) => { + const { movements, totalTime, meta } = presentation; + + // update movements if they had one + if (movements) { + // add the summed time to the movements + const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } }); + // concat the movements already in the combined presentation with these new ones + combinedMovements.push(...addedTimeMovements); + } + + // update the totalTime + sumTime += totalTime; + + // concatenate the metas + combinedMetas.push(meta); + }); + + // return the combined presentation with the updated total summed time + return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas }; + } +} |