diff --git a/client/src/App.css b/client/src/App.css
index 64471b1..e238a16 100644
--- a/client/src/App.css
+++ b/client/src/App.css
@@ -56,6 +56,27 @@ div {
border: 0.25rem solid transparent;
}
+.Image .FaceBox {
+ border: 1px solid red;
+ border-radius: 0.25rem;
+ position: absolute;
+}
+
+.Image .FaceBox:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+ box-shadow: 0px 0px 5px black;
+}
+
+.Image {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ background-size: contain !important;
+ background-repeat: no-repeat no-repeat !important;
+ background-position: 50% 50% !important;
+}
+
.Face:hover .Image {
border: 0.25rem solid yellow;
}
@@ -80,9 +101,6 @@ div {
box-sizing: border-box;
width: 8rem;
height: 8rem;
- background-size: contain !important;
- background-repeat: no-repeat no-repeat !important;;
- background-position: 50% 50% !important;;
}
.Cluster {
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 1f2abf1..390a3e0 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,11 +1,91 @@
-import React, { useState, useMemo, useEffect } from 'react';
+import React, { useState, useMemo, useEffect, useRef } from 'react';
import { useApi } from './useApi';
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import './App.css';
+const makeFaceBoxes = (photo: any, dimensions: any): any => {
+ const faces: FaceData[] = photo.faces;
+
+ let width: number, height: number, offsetLeft = 0, offsetTop = 0;
+
+ /* If photo is wider than viewport, it will be 100% width and < 100% height */
+ if (photo.width / photo.height > dimensions.width / dimensions.height) {
+ width = dimensions.width;
+ height = dimensions.height * photo.height / photo.width *
+ dimensions.width / dimensions.height;
+ offsetLeft = 0;
+ offsetTop = (dimensions.height - height) * 0.5;
+ } else {
+ width = dimensions.width * photo.width / photo.height *
+ dimensions.height / dimensions.width;
+ height = dimensions.height;
+ offsetLeft = (dimensions.width - width) * 0.5;
+ offsetTop = 0;
+ }
+
+ return faces.map((face: FaceData) => (
+
+ ));
+};
+
+/*
+function debounce(fn: any, ms: number) {
+ let timer: any;
+ return () => {
+ if (timer) clearTimeout(timer);
+ timer = setTimeout(() => {
+ timer = null
+ fn.apply(this as typeof Photo, arguments)
+ }, ms)
+ };
+};
+*/
+
const Photo = ({ photoId }: any) => {
const [image, setImage] = useState(undefined);
+ const ref = useRef(null);
+ const [dimensions, setDimensions] = React.useState({
+ height: window.innerHeight,
+ width: window.innerWidth
+ })
+
+ const faces = useMemo(() => {
+ if (image === undefined) {
+ return <>>;
+ }
+ return makeFaceBoxes(image, dimensions);
+ }, [image, dimensions]);
+
+ useEffect(() : any => {
+ if (!ref || !ref.current) {
+ return;
+ }
+
+ const el: Element = ref.current as Element;
+
+ const handleResize = () => {
+ setDimensions({
+ height: el.clientHeight,
+ width: el.clientWidth
+ })
+ };
+
+ const debouncedHandleResize = handleResize;//debounce(handleResize, 250);
+ debouncedHandleResize();
+ window.addEventListener('resize', debouncedHandleResize);
+ return () => {
+ window.removeEventListener('resize', debouncedHandleResize)
+ };
+ });
useEffect(() => {
if (photoId === 0) {
@@ -14,8 +94,8 @@ const Photo = ({ photoId }: any) => {
const fetchImageData = async (image: number) => {
console.log(`Loading photo ${image}`);
const res = await window.fetch(`../api/v1/photos/${image}`);
- const data = await res.json();
- setImage(data[0]);
+ const photo = await res.json();
+ setImage(photo);
};
fetchImageData(photoId);
@@ -26,9 +106,11 @@ const Photo = ({ photoId }: any) => {
}
return ();
+ background: `url(../${image.path}thumbs/scaled/${image.filename})`
+ }}>{ faces }
+ );
};
const Face = ({ faceId, onClick, title }: any) => {
@@ -181,7 +263,11 @@ type FaceData = {
displayName: string,
identityId: number,
distance: number,
- descriptors: any[]
+ descriptors: any[],
+ top: number
+ right: number,
+ bottom: number,
+ left: number,
};
type Identity = {
diff --git a/server/routes/photos.js b/server/routes/photos.js
index 0a9598c..1655e7b 100755
--- a/server/routes/photos.js
+++ b/server/routes/photos.js
@@ -1084,24 +1084,62 @@ console.log("Trying path as: " + path);
});
router.get("/:id", async (req, res) => {
+ console.log(`GET ${req.url}`);
+
const id = parseInt(req.params.id);
try {
- const results = await photoDB.sequelize.query(
+ let results;
+
+ results = await photoDB.sequelize.query(
`
- SELECT photos.*,albums.path AS path,
- faces.identityId,faces.top,faces.left,faces.right,faces.bottom
+ SELECT photos.*,albums.path AS path
FROM photos
INNER JOIN albums ON albums.id=photos.albumId
- INNER JOIN faces ON faces.photoId=photos.id
WHERE photos.id=:id
`, {
- replacements: { id }
+ replacements: { id },
+ type: photoDB.Sequelize.QueryTypes.SELECT
}
);
if (results.length === 0) {
return res.status(404);
}
- return res.status(200).json(results[0]);
+ const photo = results[0];
+ results = await photoDB.sequelize.query(
+ `
+ SELECT faces.* FROM faces
+ WHERE faces.photoId=:id
+ `, {
+ replacements: { id },
+ type: photoDB.Sequelize.QueryTypes.SELECT
+ }
+ );
+ photo.faces = results;
+ /* For each face, look up the Identity and clean up any
+ * fields we don't want to return vai the rest API */
+ await Promise.map(photo.faces, async (face) => {
+ face.faceId = face.id;
+ delete face.id;
+ delete face.descriptorId;
+ delete face.lastComparedId;
+ delete face.photoId;
+ delete face.scanVersion;
+ if (face.identityId) {
+ const results = await photoDB.sequelize.query(
+ `
+ SELECT displayName,firstName,lastName,middleName FROM identities
+ WHERE id=:id
+ `, {
+ replacements: { id: face.identityId },
+ type: photoDB.Sequelize.QueryTypes.SELECT
+ }
+ );
+ face.identity = results[0];
+ }
+ delete face.identityId;
+ });
+
+ return res.status(200).json(photo);
} catch (error) {
console.error(error);
return res.status(404).json({message: `Error connecting to DB for ${id}.`})