@@ -4,6 +4,8 @@ import { AACTree, AACPage, AACButton } from '../../core/treeStructure';
44import * as fs from 'fs' ;
55import * as path from 'path' ;
66import { execSync } from 'child_process' ;
7+ import Database from 'better-sqlite3' ;
8+ import { dotNetTicksToDate } from '../../utils/dotnetTicks' ;
79
810function normalizeZipPath ( p : string ) : string {
911 const unified = p . replace ( / \\ / g, '/' ) ;
@@ -144,6 +146,17 @@ export interface Grid3VocabularyPath {
144146 gridsetPath : string ;
145147}
146148
149+ export interface Grid3HistoryEntry {
150+ id : string ;
151+ content : string ;
152+ occurrences : Array < {
153+ timestamp : Date ;
154+ latitude ?: number | null ;
155+ longitude ?: number | null ;
156+ } > ;
157+ rawXml ?: string ;
158+ }
159+
147160/**
148161 * Get the Windows Common Documents folder path from registry
149162 * Falls back to default path if registry access fails
@@ -314,3 +327,81 @@ export function findGrid3UserHistory(userName: string, langCode?: string): strin
314327
315328 return match ?. historyDbPath ?? null ;
316329}
330+
331+ function parseGrid3ContentXml ( xmlContent : string ) : string {
332+ const regex = / < r > (?: < ! \[ C D A T A \[ ) ? ( .* ?) (?: \] \] > ) ? < \/ r > / gis;
333+ const parts : string [ ] = [ ] ;
334+ let match : RegExpExecArray | null ;
335+ while ( ( match = regex . exec ( xmlContent ) ) !== null ) {
336+ parts . push ( match [ 1 ] ) ;
337+ }
338+ if ( parts . length > 0 ) {
339+ return parts . join ( '' ) ;
340+ }
341+ return xmlContent . replace ( / < [ ^ > ] + > / g, '' ) . trim ( ) ;
342+ }
343+
344+ export function readGrid3History ( historyDbPath : string ) : Grid3HistoryEntry [ ] {
345+ if ( ! fs . existsSync ( historyDbPath ) ) return [ ] ;
346+
347+ const db = new Database ( historyDbPath , { readonly : true } ) ;
348+ const rows = db
349+ . prepare (
350+ `
351+ SELECT p.Id as PhraseId,
352+ p.Text as TextValue,
353+ p.Content as ContentXml,
354+ ph.Timestamp as TickValue,
355+ ph.Latitude as Latitude,
356+ ph.Longitude as Longitude
357+ FROM PhraseHistory ph
358+ INNER JOIN Phrases p ON p.Id = ph.PhraseId
359+ WHERE ph.Timestamp <> 0
360+ ORDER BY ph.Timestamp ASC
361+ `
362+ )
363+ . all ( ) as Array < {
364+ PhraseId : number ;
365+ TextValue ?: string ;
366+ ContentXml ?: string ;
367+ TickValue ?: number | bigint ;
368+ Latitude ?: number ;
369+ Longitude ?: number ;
370+ } > ;
371+
372+ const events = new Map < number , Grid3HistoryEntry > ( ) ;
373+
374+ for ( const row of rows ) {
375+ const phraseId : number = row . PhraseId ;
376+ const contentText = parseGrid3ContentXml ( String ( row . ContentXml ?? row . TextValue ?? '' ) ) ;
377+ const entry =
378+ events . get ( phraseId ) ??
379+ ( {
380+ id : `grid:${ phraseId } ` ,
381+ content : contentText ,
382+ occurrences : [ ] ,
383+ rawXml : row . ContentXml ,
384+ } as Grid3HistoryEntry ) ;
385+
386+ entry . occurrences . push ( {
387+ timestamp : dotNetTicksToDate ( BigInt ( row . TickValue ?? 0 ) ) ,
388+ latitude : row . Latitude ?? null ,
389+ longitude : row . Longitude ?? null ,
390+ } ) ;
391+
392+ events . set ( phraseId , entry ) ;
393+ }
394+
395+ return Array . from ( events . values ( ) ) ;
396+ }
397+
398+ export function readGrid3HistoryForUser ( userName : string , langCode ?: string ) : Grid3HistoryEntry [ ] {
399+ const dbPath = findGrid3UserHistory ( userName , langCode ) ;
400+ if ( ! dbPath ) return [ ] ;
401+ return readGrid3History ( dbPath ) ;
402+ }
403+
404+ export function readAllGrid3History ( ) : Grid3HistoryEntry [ ] {
405+ const paths = findGrid3HistoryDatabases ( ) ;
406+ return paths . flatMap ( ( p ) => readGrid3History ( p ) ) ;
407+ }
0 commit comments