diff --git a/frontend/identities.html b/frontend/identities.html old mode 100644 new mode 100755 index 8570e4e..ac0a887 --- a/frontend/identities.html +++ b/frontend/identities.html @@ -12,28 +12,7 @@ 6 Create new Identity */ -function loadMore(index) { - var clusterBlock = document.body.querySelector("[cluster-index='" + index + "']"); - if (!clusterBlock) { - return; - } - var faces = clusterBlock.querySelectorAll("div.face").length, i - for (i = faces; i < clusters[index].length; i++) { - if (i - faces > 10) { - return; - } - var tuple = clusters[index][i], - face = createFace(tuple[0], tuple[1]); - clusterBlock.appendChild(face); - } - if (i == clusters[index].length) { - var span = clusterBlock.querySelector("span.more"); - if (span) { - span.parentElement.removeChild(span); - } - } -} function createFace(faceId, photoId, selectable) { var div = document.createElement("div"); @@ -75,88 +54,115 @@ function shuffle(array) { } document.addEventListener("DOMContentLoaded", (event) => { - window.fetch("api/v1/faces").then(res => res.json()).then((faces) => { - const block = document.createElement("div"); - block.id = "face-editor"; - block.classList.add("block"); - - faces.forEach((face) => { - const editor = document.createElement("div"); - editor.classList.add("editor"); - editor.appendChild(createFace(face.id, face.photoId)); - for (let key in face) { - const row = document.createElement("div"), - left = document.createElement("div"); - row.classList.add("editor-row"); - left.textContent = key; - - let right; - - if (key == "relatedFaces") { - right = document.createElement("div"); - right.classList.add("related-faces"); - face.relatedFaces.forEach((face) => { - right.appendChild(createFace(face.faceId, face.photoId, true)); - }); - } else { - right = document.createElement("input"); - right.value = face[key]; - } - - row.appendChild(left); - row.appendChild(right); - editor.appendChild(row); - } - - block.appendChild(editor); - }); - document.body.appendChild(block); - - getIdentities(); - }); + loadFace(); }); -function getIdentities() { - const el = document.getElementById("identities"); - if (el) { - el.parentElement.removeChild(el); - } +function loadFace(id) { + const faceEditorBlock = document.getElementById("face-editor"); + faceEditorBlock.innerHTML = ""; - window.fetch("api/v1/identities").then(res => res.json()).then((identities) => { - const block = document.createElement("div"); - block.id = "identities"; - block.classList.add("block"); + window.fetch("api/v1/faces" + (id ? "/" + id : "")).then(res => res.json()).then((faces) => { + if (faces.length == 0) { + return; + } - identities.forEach((identity) => { - const editor = document.createElement("div"); - editor.classList.add("editor"); -// editor.appendChild(createFace(face.id, face.photoId)); - for (let key in face) { - const row = document.createElement("div"), - left = document.createElement("div"); - row.classList.add("editor-row"); - left.textContent = key; - let right; - if (key == "relatedFaces") { - right = document.createElement("div"); - face.relatedFaces.forEach((face) => { - right.appendChild(createFace(face.faceId, face.photoId)); - }); - } else { - right = document.createElement("input"); - right.value = face[key]; - } - row.appendChild(left); - row.appendChild(right); - editor.appendChild(row); + const face = faces[0]; + + getIdentities(face.id); + + const editor = document.createElement("div"); + editor.classList.add("editor"); + editor.appendChild(createFace(face.id, face.photoId)); + for (let key in face) { + const row = document.createElement("div"), + left = document.createElement("div"); + row.classList.add("editor-row"); + left.textContent = key; + + let right; + + if (key == "relatedFaces") { + right = document.createElement("div"); + right.classList.add("related-faces"); + face.relatedFaces.forEach((face) => { + right.appendChild(createFace(face.faceId, face.photoId, true)); + }); + } else { + right = document.createElement("input"); + right.value = face[key]; } - block.appendChild(editor); + row.appendChild(left); + row.appendChild(right); + editor.appendChild(row); + } + + faceEditorBlock.appendChild(editor); + }); +} + +function getIdentities(id) { + const identitiesBlock = document.getElementById("identities"); + identitiesBlock.innerHTML = ""; + + const search = id ? "?withScore=" + id : ""; + window.fetch("api/v1/identities" + search).then(res => res.json()).then((identities) => { + identitiesBlock.appendChild(createNewIdenityEditor()); + + identities.sort((a, b) => { + if (a.lastName == b.lastName) { + return a.firstName.localeCompare(b.firstName); + } + return a.lastName.localeCompare(b.lastName); }); - block.appendChild(createNewIdenityEditor()); + identities.forEach((identity) => { + const block = document.createElement("div"); + block.classList.add("block"); - document.body.appendChild(block); + const button = document.createElement("button"); + button.textContent = "Use this identity"; + block.appendChild(button); + button.addEventListener("click", (event) => { + const object = { + faces: [] + }; + Array.prototype.forEach.call(document.body.querySelectorAll("#face-editor .face"), (face) => { + if (!face.hasAttribute("disabled")) { + object.faces.push(face.getAttribute("face-id")); + } + }); + window.fetch("api/v1/identities/faces/add/" + identity.id, { + method: "PUT", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(object) + }).then(res => res.json()).then((identities) => { + console.log("Updated identity: ", identities[0]); + const face = document.body.querySelector("#face-editor .face"); + loadFace(parseInt(face.getAttribute("face-id"))); + }); + }); + + let div = document.createElement("div"); + div.textContent = identity.name; + block.appendChild(div); + div = document.createElement("div"); + div.textContent = "Related faces " + identity.relatedFaces.length; + block.appendChild(div); + + const random = Math.floor(Math.random() * identity.relatedFaces.length); + const facePhoto = createFace(identity.relatedFaces[random].faceId, identity.relatedFaces[random].photoId); + const distance = document.createElement("div"); + distance.classList.add("distance"); + distance.textContent = Math.round(100 * identity.relatedFaces[random].distance) / 100; + facePhoto.appendChild(distance); + block.appendChild(facePhoto); + + identitiesBlock.appendChild(block); + }); }); } @@ -198,8 +204,9 @@ function createNewIdenityEditor() { }, body: JSON.stringify(object) }).then(res => res.json()).then((identities) => { - console.log("Identities: ", identities); - getIdentities(); + console.log("New identity: ", identities[0]); + const face = document.body.querySelector("#face-editor .face"); + loadFace(parseInt(face.getAttribute("face-id"))); }); }); editor.appendChild(button); @@ -219,6 +226,21 @@ body { cursor: pointer; } +#identities { + display: flex; + flex-wrap: wrap; + border: 1px solid black; + padding: 0.25em; + margin: 0.25em; +} + +#identities > div { + border: 1px solid black; + margin: 0.25em; + padding: 0.25em; + box-sizing: border-box; +} + .more:hover { text-decoration: underline; } @@ -228,6 +250,7 @@ body { } .face { + position: relative; max-width: 128px; max-height: 128px; width: 128px; @@ -244,6 +267,16 @@ body { box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.5); } +.face .distance { + position: absolute; + background: black; + opacity: 0.8; + text-align: center; + left: 0; + right: 0; + bottom: 0; +} + .editor-row { display: flex; flex-direction: row; @@ -255,24 +288,7 @@ body { } -
+ + \ No newline at end of file diff --git a/server/routes/identities.js b/server/routes/identities.js old mode 100644 new mode 100755 index d173323..25c42a7 --- a/server/routes/identities.js +++ b/server/routes/identities.js @@ -10,13 +10,84 @@ require("../db/photos").then(function(db) { const router = express.Router(); +router.put("/faces/add/:id", (req, res) => { + if (!req.user.maintainer) { + return res.status(401).send("Unauthorized to modify photos."); + } + + const id = parseInt(req.params.id); + if (id != req.params.id) { + return res.status(400).send("Invalid identity id."); + } + + if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) { + return res.status(400).send("No faces supplied."); + } + + return photoDB.sequelize.query("UPDATE faces SET identityId=:identityId,identityDistance=0 " + + "WHERE id IN (:faceIds)", { + replacements: { + identityId: id, + faceIds: req.body.faces + } + }).then(() => { + const identity = { + id: id, + faces: req.body.faces + }; + return res.status(200).json([identity]); + }).catch((error) => { + console.error(error); + return res.status(500).send("Error processing request."); + }); +}); + router.post("/", (req, res) => { - console.log(req.body); - return res.status(200).json({}); + if (!req.user.maintainer) { + return res.status(401).send("Unauthorized to modify photos."); + } + const identity = { + lastName: req.body.lastName || "", + firstName: req.body.firstName || "", + middleName: req.body.middleName || "" + }; + identity.name = req.body.name || (identity.firstName + " " + identity.lastName); + + let fields = []; + for (let key in identity) { + fields.push(key); + } + + if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) { + return res.status(400).send("No faces supplied."); + } + + return photoDB.sequelize.query("INSERT INTO identities " + + "(" + fields.join(",") + ") " + + "VALUES(:" + fields.join(",:") + ")", { + replacements: identity, + }).spread(function(results, metadata) { + identity.id = metadata.lastID; + return photoDB.sequelize.query("UPDATE faces SET identityId=:identityId,identityDistance=0 " + + "WHERE id IN (:faceIds)", { + replacements: { + identityId: identity.id, + faceIds: req.body.faces + } + }).then(() => { + identity.faces = req.body.faces; + return res.status(200).json([identity]); + }); + }).catch((error) => { + console.error(error); + return res.status(500).send("Error processing request."); + }); }); router.get("/:id?", (req, res) => { let id; + console.log("here"); + if (req.params.id) { id = parseInt(req.params.id); if (id != req.params.id) { @@ -24,16 +95,65 @@ router.get("/:id?", (req, res) => { } } - const filter = id ? "WHERE id=:id" : ""; + const filter = id ? "WHERE id=:id " : ""; - return photoDB.sequelize.query(`SELECT * FROM identities ${filter}`, { + return photoDB.sequelize.query("SELECT " + + "identities.*," + + "GROUP_CONCAT(faces.id) AS relatedFaceIds," + + "GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " + + "FROM identities " + + "INNER JOIN faces ON identities.id=faces.identityId " + + filter + + "GROUP BY identities.id", { replacements: { id: id }, type: photoDB.Sequelize.QueryTypes.SELECT, raw: true - }).then((results) => { - return res.status(200).json(results); + }).then((identities) => { + console.log("asdf"); + identities.forEach((identity) => { + const relatedFaces = identity.relatedFaceIds.split(","), + relatedFacePhotos = identity.relatedFacePhotoIds.split(","); + if (relatedFaces.length != relatedFacePhotos.length) { + console.warn("Face ID to Photo ID mapping doesn't match!"); + } + delete identity.relatedFaceIds; + delete identity.relatedFacePhotoIds; + identity.relatedFaces = relatedFaces.map((faceId, index) => { + return { + faceId: faceId, + photoId: relatedFacePhotos[index] + }; + }); + }); + + if (!req.query.withScore) { + console.log("No score request."); + return identities; + } + console.log("Looking up scores."); + + return photoDB.sequelize.query("SELECT * FROM facedescriptor WHERE faceId IN (:faceIds)", { + replacements: { + faceIds: relatedFaces + }, + type: photoDB.Sequelize.QueryTypes.SELECT, + raw: true + }).then((descriptors) => { + descriptors.forEach((descriptor) => { + for (let i = 0; i < identity.relatedFaces.length; i++) { + if (identity.relatedFaces[i].faceId == descriptor.faceId) { + identity.relatedFaces[i].descriptors = descriptor.descriptors; + return; + } + } + }); + + return identities; + }); + }).then((identities) => { + return res.status(200).json(identities); }).catch((error) => { console.error(error); return res.status(500).send("Error processing request.");