From 25ee9e6b3f7da67bcf94eb2affd5793c67777930 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 21 Aug 2024 17:04:32 -0400
Subject: cleanup of face recognition.  some lint fixes.

---
 .../collectionFreeForm/FaceCollectionBox.tsx       | 121 +++++++++------------
 1 file changed, 49 insertions(+), 72 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index d5a2809dc..94f9a3c94 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -3,28 +3,28 @@ import { IconButton, Size } from 'browndash-components';
 import * as faceapi from 'face-api.js';
 import { FaceMatcher } from 'face-api.js';
 import 'ldrs/ring';
-import { action, computed, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import React from 'react';
-import { Utils } from '../../../../Utils';
-import { Doc, DocListCast } from '../../../../fields/Doc';
-import { DocData } from '../../../../fields/DocSymbols';
+import { setupMoveUpEvents } from '../../../../ClientUtils';
+import { Utils, emptyFunction } from '../../../../Utils';
+import { Doc, Opt } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
-import { listSpec } from '../../../../fields/Schema';
-import { Cast, ImageCast, StrCast } from '../../../../fields/Types';
+import { ImageCast } from '../../../../fields/Types';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
+import { dropActionType } from '../../../util/DropActionTypes';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { undoable } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocumentView } from '../../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../../nodes/FieldView';
+import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler';
 import './FaceCollectionBox.scss';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
-import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler';
 
 interface FaceDocumentProps {
     faceDoc: Doc;
@@ -50,49 +50,29 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps
     };
 
     protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
-        const { docDragData } = de.complete;
-        if (docDragData) {
-            const filteredDocs = docDragData.droppedDocuments.filter(doc => doc.type === DocumentType.IMG);
-            filteredDocs.forEach(doc => {
-                // If the current Face Document has no items, and the doc has more than one face descriptor, don't let the user add the document first.
-                if ((this._props.faceDoc[DocData].face_descriptors as List<List<number>>).length === 0 && (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List<List<number>>).length > 1) {
+        de.complete.docDragData?.droppedDocuments
+            ?.filter(doc => doc.type === DocumentType.IMG)
+            .forEach(imgDoc => {
+                // If the current Face Document has no faces, and the doc has more than one face descriptor, don't let the user add the document first.  Or should we just use the first face ?
+                if (FaceRecognitionHandler.FaceDocDescriptors(this._props.faceDoc).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) {
                     alert('Cannot add a document with multiple faces as the first item!');
                 } else {
-                    // Loop through the documents' face descriptors.
-                    // Choose the face with the smallest distance to add.
-                    const float32Array = (this._props.faceDoc[DocData].face_descriptors as List<List<number>>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor)));
-                    const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(StrCast(this._props.faceDoc[DocData].face_label), float32Array);
-                    const faceDescriptors: faceapi.LabeledFaceDescriptors[] = [labeledFaceDescriptor];
-
-                    const faceMatcher = new FaceMatcher(faceDescriptors, 1);
-                    let cur_lowest_distance = 1;
-                    let cur_matching_face = new List<number>();
-
-                    (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List<List<number>>).forEach(face => {
-                        // If the face has the current lowest distance, mark it as such
-                        // Once that lowest distance is found, add the face descriptor to the faceDoc, and add the associated doc
-                        const convered_32_array: Float32Array = new Float32Array(Array.from(face));
-                        const match = faceMatcher.matchDescriptor(convered_32_array);
-
-                        if (match.distance < cur_lowest_distance) {
-                            cur_lowest_distance = match.distance;
-                            cur_matching_face = face;
-                        }
-                    });
-
-                    const faceFieldKey = FaceRecognitionHandler.FaceField(doc, this._props.faceDoc);
-                    if (doc[DocData][faceFieldKey]) {
-                        Cast(doc[DocData][faceFieldKey], listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that
-                    } else {
-                        doc[DocData][faceFieldKey] = new List<List<number>>([cur_matching_face]);
-                    }
-
-                    Doc.AddDocToList(this._props.faceDoc[DocData], 'face_docList', doc);
-                    Cast(this._props.faceDoc[DocData].face_descriptors, listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that
+                    // Loop through the documents' face descriptors and choose the face in the iage with the smallest distance (most similar to the face colleciton)
+                    const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.FaceDocDescriptors(this._props.faceDoc).map(fd => new Float32Array(Array.from(fd)));
+                    const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.FaceDocLabel(this._props.faceDoc), faceDescriptorsAsFloat32Array);
+                    const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1);
+                    const { face_match } = FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).reduce(
+                        (prev, face) => {
+                            const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(face)));
+                            return match.distance < prev.dist ? { dist: match.distance, face_match: face } : prev;
+                        },
+                        { dist: 1, face_match: new List<number>() as Opt<List<number>> }
+                    );
+
+                    // assign the face in the image that's closest to the face collection to be the face that's assigned to the collection
+                    face_match && FaceRecognitionHandler.ImageDocAddFace(imgDoc, face_match, this._props.faceDoc);
                 }
             });
-            return false;
-        }
         return false;
     }
 
@@ -108,23 +88,16 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps
      * Deletes a Face Document.
      */
     deleteFaceDocument = undoable(() => {
-        if (Doc.ActiveDashboard) {
-            Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'faceDocuments', this._props.faceDoc);
-        }
-    }, 'remove face');
+        FaceRecognitionHandler.DeleteFaceDoc(this._props.faceDoc);
+    }, 'delete face');
 
     /**
      * Deletes a document from a Face Document's associated docs list.
      * @param doc
      */
-    @action
-    deleteAssociatedDoc = (doc: Doc) => {
-        this._props.faceDoc[DocData].face_descriptors = new List<List<number>>(
-            (this._props.faceDoc[DocData].face_descriptors as List<List<number>>).filter(fd => !(doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] as List<List<number>>).includes(fd))
-        );
-        doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] = new List<List<number>>();
-        Doc.RemoveDocFromList(this._props.faceDoc[DocData], 'face_docList', doc);
-    };
+    deleteAssociatedDoc = undoable((imgDoc: Doc) => {
+        FaceRecognitionHandler.FaceDocRemoveImageDocFace(imgDoc, this._props.faceDoc);
+    }, 'remove doc from face');
 
     render() {
         return (
@@ -133,7 +106,7 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps
                     <IconButton tooltip="Delete Face From Collection" onPointerDown={this.deleteFaceDocument} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
                 </div>
                 <div className="face-document-top">
-                    <h1>{StrCast(this._props.faceDoc[DocData].face_label)}</h1>
+                    <h1>{FaceRecognitionHandler.FaceDocLabel(this._props.faceDoc)}</h1>
                 </div>
                 <IconButton
                     tooltip="See image information"
@@ -144,10 +117,24 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps
                 />
                 {this._displayImages ? (
                     <div className="face-document-image-container">
-                        {DocListCast(this._props.faceDoc[DocData].face_docList).map(doc => {
+                        {FaceRecognitionHandler.FaceDocFaces(this._props.faceDoc).map(doc => {
                             const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
                             return (
-                                <div className="image-wrapper" key={Utils.GenerateGuid()}>
+                                <div
+                                    className="image-wrapper"
+                                    key={Utils.GenerateGuid()}
+                                    onPointerDown={e =>
+                                        setupMoveUpEvents(
+                                            this,
+                                            e,
+                                            () => {
+                                                DragManager.StartDocumentDrag([e.target as HTMLElement], new DragManager.DocumentDragData([doc], dropActionType.embed), e.clientX, e.clientY);
+                                                return true;
+                                            },
+                                            emptyFunction,
+                                            emptyFunction
+                                        )
+                                    }>
                                     <img onClick={() => DocumentView.showDocument(doc, { willZoomCentered: true })} style={{ maxWidth: '60px', margin: '10px' }} src={`${name}_o.${type}`} />
                                     <div className="remove-item">
                                         <IconButton tooltip={'Remove Doc From Face Collection'} onPointerDown={() => this.deleteAssociatedDoc(doc)} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
@@ -168,25 +155,15 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return FieldView.LayoutString(FaceCollectionBox, fieldKey);
     }
 
-    public static Instance: FaceCollectionBox;
-
-    @computed get currentDocs() {
-        if (Doc.ActiveDashboard) {
-            return DocListCast(Doc.ActiveDashboard[DocData].faceDocuments);
-        }
-        return [];
-    }
-
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
-        FaceCollectionBox.Instance = this;
     }
 
     render() {
         return (
             <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}>
-                {this.currentDocs.map(doc => (
+                {FaceRecognitionHandler.FaceDocuments().map(doc => (
                     <FaceDocumentItem key={doc[Id]} faceDoc={doc} />
                 ))}
             </div>
-- 
cgit v1.2.3-70-g09d2

