Compare commits

..

No commits in common. "17fbb4e21c574e982d98746c9ea83044cc15574f" and "0043480ff877ee57a401ee2ba793f405e30b752b" have entirely different histories.

10 changed files with 455 additions and 1005 deletions

View File

@ -1,9 +0,0 @@
module.exports = {
devServer: devServerConfig => {
devServerConfig.webSocketServer = {
options: { path: process.env.PUBLIC_URL + 'ws' }
};
return devServerConfig;
}
};

1150
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,16 +14,15 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-resizable-panels": "^0.0.34", "react-resizable-panels": "^0.0.34",
"react-router-dom": "^6.6.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "export $(cat .env | xargs) ; craco start", "start": "export $(cat .env | xargs) ; react-scripts start",
"build": "export $(cat .env | xargs) ; craco build", "build": "export $(cat .env | xargs) ; react-scripts build",
"test": "export $(cat .env | xargs) ; craco test", "test": "export $(cat .env | xargs) ; react-scripts test",
"eject": "export $(cat .env | xargs) ; craco eject" "eject": "export $(cat .env | xargs) ; react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -42,8 +41,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@craco/craco": "^7.0.0"
} }
} }

View File

@ -21,7 +21,6 @@ div {
.Resizer { .Resizer {
width: 0.5rem; width: 0.5rem;
background-color: #ccc;
border: 1px solid black; border: 1px solid black;
} }
@ -37,8 +36,6 @@ div {
.Identities { .Identities {
display: flex; display: flex;
padding: 0.25rem;
gap: 0.25rem;
overflow-y: scroll; overflow-y: scroll;
flex-direction: column; flex-direction: column;
border: 1px solid green; border: 1px solid green;
@ -49,13 +46,16 @@ div {
box-sizing: border-box; box-sizing: border-box;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
border: 2px solid #444;
} }
.Face:hover { .Face:hover {
cursor: pointer; cursor: pointer;
} }
.Face .Image {
border: 0.25rem solid transparent;
}
.ClusterEditor { .ClusterEditor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -82,17 +82,12 @@ div {
background-position: 50% 50% !important; background-position: 50% 50% !important;
} }
.Active { .Face:hover .Image {
filter: brightness(1.25); border: 0.25rem solid yellow;
border-color: orange !important;
} }
.Face:hover { .Face.Selected .Image {
border-color: yellow; border: 0.25rem solid blue;
}
.Face.Selected {
border-color: blue;
} }
.Face .Title { .Face .Title {
@ -134,6 +129,5 @@ div {
.Cluster .Faces { .Cluster .Faces {
display: grid; display: grid;
gap: 0.25rem;
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
} }

View File

@ -2,17 +2,9 @@
import React, { useState, useMemo, useEffect, useRef } from 'react'; import React, { useState, useMemo, useEffect, useRef } from 'react';
import { useApi } from './useApi'; import { useApi } from './useApi';
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import {
BrowserRouter as Router,
Route,
Routes,
useParams
} from "react-router-dom";
import './App.css'; import './App.css';
const base = process.env.PUBLIC_URL; /* /identities -- set in .env */ const makeFaceBoxes = (photo: any, dimensions: any): any => {
const makeFaceBoxes = (photo: any, dimensions: any, onFaceClick: any): any => {
const faces: FaceData[] = photo.faces; const faces: FaceData[] = photo.faces;
let width: number, height: number, offsetLeft = 0, offsetTop = 0; let width: number, height: number, offsetLeft = 0, offsetTop = 0;
@ -32,29 +24,20 @@ const makeFaceBoxes = (photo: any, dimensions: any, onFaceClick: any): any => {
offsetTop = 0; offsetTop = 0;
} }
return faces.map((face: FaceData) => { return faces.map((face: FaceData) => (
const faceId = face.faceId; <div className="FaceBox"
const identityId = face.identityId; key={face.faceId}
return ( style={{
<div className="FaceBox" left: offsetLeft + Math.floor(face.left * width) + "px",
key={faceId} top: offsetTop + Math.floor(face.top * height) + "px",
data-identity-id={identityId} width: Math.floor((face.right - face.left) * width) + "px",
data-face-id={faceId} height: Math.floor((face.bottom - face.top) * height) + "px"
style={{ }}
left: offsetLeft + Math.floor(face.left * width) + "px", />
top: offsetTop + Math.floor(face.top * height) + "px", ));
width: Math.floor((face.right - face.left) * width) + "px",
height: Math.floor((face.bottom - face.top) * height) + "px"
}}
onClick={(e) => { onFaceClick(e, face) }}
onMouseEnter={(e) => { onFaceMouseEnter(e, face) }}
onMouseLeave={(e) => { onFaceMouseLeave(e, face) }}
/>
)
});
}; };
const Photo = ({ photoId, onFaceClick }: any) => { const Photo = ({ photoId }: any) => {
const [image, setImage] = useState<any>(undefined); const [image, setImage] = useState<any>(undefined);
const ref = useRef(null); const ref = useRef(null);
const [dimensions, setDimensions] = React.useState({width: 0, height: 0}); const [dimensions, setDimensions] = React.useState({width: 0, height: 0});
@ -63,8 +46,8 @@ const Photo = ({ photoId, onFaceClick }: any) => {
if (image === undefined || dimensions.height === 0) { if (image === undefined || dimensions.height === 0) {
return <></>; return <></>;
} }
return makeFaceBoxes(image, dimensions, onFaceClick); return makeFaceBoxes(image, dimensions);
}, [image, dimensions, onFaceClick]); }, [image, dimensions]);
useEffect(() => { useEffect(() => {
if (!ref.current) { if (!ref.current) {
@ -86,7 +69,7 @@ const Photo = ({ photoId, onFaceClick }: any) => {
} }
const fetchImageData = async (image: number) => { const fetchImageData = async (image: number) => {
console.log(`Loading photo ${image}`); console.log(`Loading photo ${image}`);
const res = await window.fetch(`${base}/api/v1/photos/${image}`); const res = await window.fetch(`../api/v1/photos/${image}`);
const photo = await res.json(); const photo = await res.json();
setImage(photo); setImage(photo);
}; };
@ -101,56 +84,19 @@ const Photo = ({ photoId, onFaceClick }: any) => {
return (<div className="Image" return (<div className="Image"
ref={ref} ref={ref}
style={{ style={{
background: `url("${base}/../${image.path}thumbs/scaled/${image.filename}")`.replace(/ /g, '%20') background: `url(../${image.path}thumbs/scaled/${image.filename})`.replace(/ /g, '%20')
}}>{ faces }</div> }}>{ faces }</div>
); );
}; };
const onFaceMouseEnter = (e: any, face: FaceData) => { const Face = ({ faceId, onClick, title, ...rest }: any) => {
const faceId = face.faceId;
const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)];
if (face.identity) {
const identityId = face.identity.identityId;
els.splice(0, 0,
...document.querySelectorAll(`[data-identity-id="${identityId}"]`));
}
els.forEach(el => {
el.classList.add('Active');
})
};
const onFaceMouseLeave = (e: any, face: FaceData) => {
const faceId = face.faceId;
const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)];
if (face.identity) {
const identityId = face.identity.identityId;
els.splice(0, 0,
...document.querySelectorAll(`[data-identity-id="${identityId}"]`));
}
els.forEach(el => {
el.classList.remove('Active');
})
};
const Face = ({ face, onFaceClick, title, ...rest }: any) => {
const faceId = face.faceId;
const idPath = String(faceId % 100).padStart(2, '0'); const idPath = String(faceId % 100).padStart(2, '0');
return ( return (
<div <div {...rest} onClick={(e) => { onClick(e, faceId) }}
data-face-id={face.faceId}
data-identity-id={face.identityId}
{...rest}
onClick={(e) => { onFaceClick(e, face) }}
onMouseEnter={(e) => { onFaceMouseEnter(e, face) }}
onMouseLeave={(e) => { onFaceMouseLeave(e, face) }}
className='Face'> className='Face'>
<div className='Image' <div className='Image'
style={{ style={{
background: `url("${base}/../faces/${idPath}/${faceId}.jpg")`, background: `url("/faces/${idPath}/${faceId}.jpg")`,
}}> }}>
<div className='Title'>{title}</div> <div className='Title'>{title}</div>
</div> </div>
@ -159,54 +105,47 @@ const Face = ({ face, onFaceClick, title, ...rest }: any) => {
}; };
type ClusterProps = { type ClusterProps = {
identity: IdentityData, identity: Identity,
setImage(image: number): void, setImage(image: number): void,
setSelected(selected: number[]): void, setSelected(selected: number[]): void,
setIdentity(identity: IdentityData): void setIdentity(identity: Identity): void
}; };
const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps) => { const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps) => {
const relatedFacesJSX = useMemo(() => { const relatedFacesJSX = useMemo(() => {
const faceClicked = async (e: any, face: FaceData) => { const faceClicked = async (e: any, id: any) => {
if (!identity) { if (!identity) {
return; return;
} }
const el = e.currentTarget; const el = e.currentTarget;
const face = identity.relatedFaces.find(item => item.faceId === id);
/* Control -- select / deselect single item */ if (!face) {
if (e.ctrlKey) {
el.classList.toggle('Selected');
const selected = [...el.parentElement
.querySelectorAll('.Selected')]
.map((face: any) => face.getAttribute('data-face-id'));
setSelected(selected);
return;
}
/* Shift -- select groups */
if (e.shiftKey) {
return; return;
} }
if (e.shiftKey) {
/* Default to load image */ e.stopPropagation();
e.stopPropagation(); e.preventDefault();
e.preventDefault(); setImage(face.photoId);
setImage(face.photoId); return;
}
el.classList.toggle('Selected');
const selected = [...el.parentElement
.querySelectorAll('.Selected')]
.map((face: any) => face.getAttribute('data-face-id'));
setSelected(selected);
console.log(face);
} }
if (identity === undefined) { if (identity === undefined) {
return <></>; return <></>;
} }
return identity.relatedFaces.map(face => return identity.relatedFaces.map(face =>
<div <Face
data-face-id={face.faceId}
key={face.faceId} key={face.faceId}
style={{ faceId={face.faceId}
display: "flex", onClick={faceClicked}
alignItems: "center"}}> title={face.distance}/>
<Face
face={face}
onFaceClick={faceClicked}
title={face.distance}/>
</div>
); );
}, [identity, setImage, setSelected]); }, [identity, setImage, setSelected]);
@ -286,7 +225,6 @@ type FaceData = {
firstName: string, firstName: string,
middleName: string, middleName: string,
displayName: string, displayName: string,
identity: IdentityData,
identityId: number, identityId: number,
distance: number, distance: number,
descriptors: any[], descriptors: any[],
@ -296,33 +234,40 @@ type FaceData = {
left: number, left: number,
}; };
type IdentityData = { type Identity = {
lastName: string, lastName: string,
middleName: string, middleName: string,
firstName: string, firstName: string,
descriptors: number[], descriptors: number[],
identityId: number id: number
displayName: string, displayName: string,
relatedFaces: FaceData[] relatedFaces: FaceData[]
}; };
interface IdentitiesProps { interface IdentitiesProps {
identities: IdentityData[], setIdentity(identity: Identity): void,
onFaceClick(e: any, face: FaceData): void identities: Identity[]
}; };
const Identities = ({ identities, onFaceClick } : IdentitiesProps) => { const Identities = ({ identities, setIdentity } : IdentitiesProps) => {
const identitiesJSX = useMemo(() => { const identitiesJSX = useMemo(() => {
const loadIdentity = async (id: number) => {
const res = await window.fetch(`../api/v1/identities/${id}`);
const data = await res.json();
setIdentity(data[0]);
};
return identities.map((identity) => { return identities.map((identity) => {
const face = identity.relatedFaces[0]; const face = identity.relatedFaces[0];
return ( return (
<Face key={face.faceId} <Face key={face.faceId}
face={face} data-face-id={face.faceId}
onFaceClick={onFaceClick} faceId={face.faceId}
onClick={() => loadIdentity(identity.id)}
title={identity.displayName}/> title={identity.displayName}/>
); );
}); });
}, [ identities, onFaceClick ]); }, [ setIdentity, identities ]);
return ( return (
<div className='Identities'> <div className='Identities'>
@ -340,50 +285,24 @@ const Button = ({ onClick, children }: any) => {
}; };
const App = () => { const App = () => {
const [identities, setIdentities] = useState<IdentityData[]>([]); const [identities, setIdentities] = useState<Identity[]>([]);
const { identityId, faceId } = useParams();
const [identity, setIdentity] = useState<any>(undefined); const [identity, setIdentity] = useState<any>(undefined);
const [image, setImage] = useState<number>(0); const [image, setImage] = useState<number>(0);
const { loading, data } = useApi( const { loading, data } = useApi(
`${base}/api/v1/identities` '../api/v1/identities'
); );
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
const loadIdentity = async (identityId: number) => {
try {
const res = await window.fetch(`${base}/api/v1/identities/${identityId}`);
const data = await res.json();
setIdentity(data[0]);
} catch (error) {
console.error(error);
}
};
useEffect(() => {
if (identityId !== undefined && !isNaN(+identityId)) {
loadIdentity(+identityId);
}
if (faceId !== undefined && !isNaN(+faceId)) {
setImage(+faceId);
}
}, []);
useEffect(() => { useEffect(() => {
if (data && data.length) { if (data && data.length) {
data.forEach((identity: IdentityData) => { setIdentities(data as Identity[]);
identity.relatedFaces.forEach(face => {
face.identity = identity;
});
});
setIdentities(data as IdentityData[]);
} }
}, [data]); }, [data]);
const removeSelected = async () => { const removeSelected = async () => {
try { try {
const res = await window.fetch( const res = await window.fetch(
`${base}/api/v1/identities/faces/remove/${identity.identityId}`, { `../api/v1/identities/faces/remove/${identity.id}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ faces: selected }) body: JSON.stringify({ faces: selected })
@ -403,30 +322,6 @@ const App = () => {
} }
}; };
const onFaceClick = (e: any, face: FaceData) => {
if (!face.identity) {
console.log(`Face ${face.faceId} does not have an Identity`);
return;
}
const identityId = face.identity.identityId;
console.log(face.identity);
const identitiesEl = document.querySelector('.Identities');
if (!identitiesEl) {
return;
}
const faceIdentity = identitiesEl.querySelector(
`[data-identity-id="${identityId}"]`);
if (!faceIdentity) {
return;
}
faceIdentity.scrollIntoView()
};
const identitiesOnFaceClick = (e: any, face: FaceData) => {
const identityId = face.identity.identityId;
loadIdentity(identityId);
}
return ( return (
<div className="App"> <div className="App">
<div className="Worksheet"> <div className="Worksheet">
@ -451,24 +346,13 @@ const App = () => {
<PanelResizeHandle className="Resizer"/> <PanelResizeHandle className="Resizer"/>
<Panel> <Panel>
{image === 0 && <div style={{ margin: '1rem' }}>Select image to view</div>} {image === 0 && <div style={{ margin: '1rem' }}>Select image to view</div>}
{image !== 0 && <Photo onFaceClick={onFaceClick} photoId={image}/> } {image !== 0 && <Photo photoId={image}/> }
</Panel> </Panel>
</PanelGroup> </PanelGroup>
{ !loading && <Identities { !loading && <Identities {... {identities, setIdentity }}/> }
{... { onFaceClick: identitiesOnFaceClick, identities }}/> }
</div> </div>
</div> </div>
); );
} }
const AppRouter = () => { export default App;
return (
<Router>
<Routes>
<Route element={<App />} path={`${base}/:identityId?/:faceId?`} />
</Routes>
</Router>
);
}
export default AppRouter;

View File

@ -16,7 +16,6 @@ const useApi = (_url: string, _options?: {}) : UseApi => {
if (_url === '') { if (_url === '') {
return; return;
} }
const fetchApi = async () => { const fetchApi = async () => {
console.log(`Fetching ${_url}...`); console.log(`Fetching ${_url}...`);
try { try {

View File

@ -1,8 +1,4 @@
# DEVELOPMENT -- use npm development server on port 3000 (entrypoint.sh) # DEVELOPMENT -- use npm development server on port 3000 (entrypoint.sh)
location /identities/api/v1/ {
rewrite ^/identities/api/v1/(.*)$ https://${host}/api/v1/$1 permanent;
}
location /identities { location /identities {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -13,6 +9,6 @@ location /identities {
proxy_pass_header P3P; proxy_pass_header P3P;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade"; proxy_set_header Connection "upgrade";
proxy_pass https://localhost:3000; proxy_pass https://localhost:3000;
} }

View File

@ -1,8 +1,4 @@
# PRODUCTION -- pre-built source # PRODUCTION -- pre-built source
location /identities/api/v1/ {
rewrite ^/identities/api/v1/(.*)$ https://${host}/api/v1/$1 permanent;
}
location /identities { location /identities {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;
alias /website/client/build; alias /website/client/build;

View File

@ -174,7 +174,6 @@ router.get("/:id?", async (req, res) => {
identity[key] = ''; identity[key] = '';
} }
}); });
identity.identityId = identity.id;
const relatedFaces = identity.relatedFaceIds.split(","), const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(","); relatedFacePhotos = identity.relatedFacePhotoIds.split(",");
@ -198,7 +197,6 @@ router.get("/:id?", async (req, res) => {
); );
return { return {
identityId: identity.id,
faceId, faceId,
photoId: relatedFacePhotos[index], photoId: relatedFacePhotos[index],
distance distance
@ -217,7 +215,6 @@ router.get("/:id?", async (req, res) => {
identity.relatedFaces = [ identity.relatedFaces[0] ]; identity.relatedFaces = [ identity.relatedFaces[0] ];
} }
delete identity.id;
delete identity.descriptors; delete identity.descriptors;
delete identity.relatedFaceIds; delete identity.relatedFaceIds;
delete identity.relatedFacePhotoIds; delete identity.relatedFacePhotoIds;

View File

@ -1127,8 +1127,8 @@ router.get("/:id", async (req, res) => {
if (face.identityId) { if (face.identityId) {
const results = await photoDB.sequelize.query( const results = await photoDB.sequelize.query(
` `
SELECT id AS identityId,displayName,firstName,lastName,middleName FROM identities SELECT displayName,firstName,lastName,middleName FROM identities
WHERE identityId=:id WHERE id=:id
`, { `, {
replacements: { id: face.identityId }, replacements: { id: face.identityId },
type: photoDB.Sequelize.QueryTypes.SELECT type: photoDB.Sequelize.QueryTypes.SELECT
@ -1136,6 +1136,7 @@ router.get("/:id", async (req, res) => {
); );
face.identity = results[0]; face.identity = results[0];
} }
delete face.identityId;
}); });
return res.status(200).json(photo); return res.status(200).json(photo);