- }/>
+ }/>
}/>
}/>
@@ -88,3 +126,311 @@ class App extends React.Component {
}
export default withRouter(App);
+
+/*
+
+1. Query server for a face with no bound identity.
+2. Query for faces related to that face
+3. Put a UI that let's the user pick from existing identities.
+4. Select / Deselect all faces that match
+5. "Match" to bind the faces to the identity
+6. Create new Identity
+
+*/
+
+function createFace(faceId, photoId, selectable) {
+ var div = document.createElement("div");
+ div.classList.add("face");
+ div.setAttribute("photo-id", photoId);
+ div.setAttribute("face-id", faceId);
+ div.style.backgroundImage = "url(face-data/" + (faceId % 100) + "/" + faceId + "-original.png)";
+ div.addEventListener("click", (event) => {
+ if (event.shiftKey) { /* identities */
+ let faceId = parseInt(event.currentTarget.getAttribute("face-id"));
+ if (faceId) {
+ window.location.href = "identities.html?id=" + faceId;
+ } else {
+ alert("No face id mapped to face.");
+ }
+ } else if (!selectable || event.ctrlKey) { /* face-explorer */
+ let photoId = parseInt(event.currentTarget.getAttribute("photo-id"));
+ if (photoId) {
+ window.open("face-explorer.html?" + photoId, "photo-" + photoId);
+ } else {
+ alert("No photo id mapped to face.");
+ }
+ } else {
+ if (event.currentTarget.hasAttribute("disabled")) {
+ event.currentTarget.removeAttribute("disabled");
+ } else {
+ event.currentTarget.setAttribute("disabled", "");
+ }
+ }
+ });
+ return div;
+}
+
+function loadFace(id) {
+ const faceEditorBlock = document.getElementById("face-editor");
+ faceEditorBlock.innerHTML = "";
+
+ window.fetch("api/v1/faces" + (id ? "/" + id : "")).then(res => res.json()).then((faces) => {
+ if (faces.length == 0) {
+ return;
+ }
+
+ const face = faces[0];
+
+ if (face.identityId) {
+ getIdentity(face.identityId);
+ } else {
+ getIdentities(face.id);
+ }
+
+ const editor = document.createElement("div");
+ editor.classList.add("editor");
+ editor.appendChild(createFace(face.id, face.photoId));
+ let row = document.createElement("div"),
+ label = document.createElement("div"),
+ a = document.createElement("a");
+ row.classList.add("editor-row");
+ label.textContent = "View photo: ";
+ row.appendChild(label);
+ a.setAttribute("target", "photo-" + face.photoId);
+ a.href = "face-explorer.html?" + face.photoId;
+ a.textContent = "photo " + face.photoId;
+ row.appendChild(a);
+ editor.appendChild(row);
+ row = document.createElement("div");
+ row.classList.add("editor-row");
+ let message = "Unassigned related faces: ";
+ if (face.relatedFaces.length > 0) {
+ message += face.relatedFaces.length + " related (tap to deselect)";
+ } else {
+ message += "No matches found.";
+ }
+ row.textContent = message;
+ editor.appendChild(row);
+
+ row = document.createElement("div");
+ row.classList.add("editor-row");
+ row.classList.add("related-faces");
+
+ editor.appendChild(row);
+
+ let button = document.createElement("button");
+ button.textContent = "show more";
+ button.addEventListener("click", (event) => {
+ showMore(row, face.relatedFaces);
+ });
+
+ editor.appendChild(button);
+
+ showMore(row, face.relatedFaces);
+
+ faceEditorBlock.appendChild(editor);
+ });
+}
+
+function showMore(row, faces) {
+ let index = row.querySelectorAll(".face").length,
+ max = Math.min(faces.length - index, 9);
+ let more = index + max < faces.length;
+ for (let i = index; i < index + max; i++) {
+ let relation = faces[i];
+ const distance = document.createElement("div"),
+ facePhoto = createFace(relation.faceId, relation.photoId, true);
+ distance.classList.add("distance");
+ distance.textContent = relation.distance.toFixed(2);
+ facePhoto.appendChild(distance);
+ row.appendChild(facePhoto);
+ }
+ if (!more) {
+ row.parentElement.querySelector("button").style.display = "none";
+ }
+}
+
+
+function getIdentity(identityId) {
+ const identitiesBlock = document.getElementById("identities");
+ identitiesBlock.innerHTML = "";
+
+ window.fetch("api/v1/identities/" + identityId).then(res => res.json()).then((identities) => {
+ identities.forEach((identity) => {
+ const block = createIdentityBlock(identity, true),
+ button = createUseThisIdentityButton(identity);
+ block.insertBefore(button, block.firstChild);
+ identitiesBlock.appendChild(block);
+ })
+ });
+}
+
+function getIdentities(faceId) {
+ const identitiesBlock = document.getElementById("identities");
+ identitiesBlock.innerHTML = "";
+
+ const search = faceId ? "?withScore=" + faceId : "";
+ window.fetch("api/v1/identities" + search).then(res => res.json()).then((identities) => {
+ identities.sort((a, b) => {
+ if (a.lastName == b.lastName) {
+ return a.firstName.localeCompare(b.firstName);
+ }
+ return a.lastName.localeCompare(b.lastName);
+ });
+
+ identities.forEach((identity) => {
+ const block = createIdentityBlock(identity),
+ button = createUseThisIdentityButton(identity);
+ block.insertBefore(button, block.firstChild);
+ identitiesBlock.appendChild(block);
+ });
+ });
+}
+
+function createUseThisIdentityButton(identity) {
+ const button = document.createElement("button");
+ button.textContent = "Use this identity";
+ 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")));
+ });
+ });
+ return button;
+}
+
+function createIdentityBlock(identity, nolimit) {
+ const block = document.createElement("div");
+
+ block.classList.add("block");
+ 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);
+
+ identity.relatedFaces.sort((a, b) => {
+ return a.distance - b.distance;
+ });
+
+ div = document.createElement("div");
+ div.classList.add("face-block");
+ if (nolimit) {
+ div.classList.add("face-block-nolimit");
+ }
+
+ let minDistance = {
+ distance: 1
+ };
+
+ for (let i = 0; i < identity.relatedFaces.length; i++) {
+ if (!nolimit && i >= 4) {
+ break;
+ }
+ const target = identity.relatedFaces[i];
+ const facePhoto = createFace(target.faceId, target.photoId),
+ distance = document.createElement("div");
+
+ if (target.distance < minDistance.distance) {
+ minDistance.distance = target.distance;
+ minDistance.photoId = target.photoId;
+ minDistance.faceId = target.faceId;
+ }
+ distance.classList.add("distance");
+ distance.textContent = target.distance.toFixed(2);
+ facePhoto.appendChild(distance);
+ div.appendChild(facePhoto);
+ }
+
+ if (minDistance.distance < 0.45) {
+ const bestMatch = document.getElementById("best-match");
+ const distance = document.createElement("div"),
+ facePhoto = createFace(minDistance.faceId, minDistance.photoId);
+ distance.classList.add("distance");
+ distance.textContent = minDistance.distance.toFixed(2);
+ facePhoto.appendChild(distance);
+
+ bestMatch.innerHTML = "";
+ bestMatch.appendChild(facePhoto);
+ } else {
+ const bestMatch = document.getElementById("best-match");
+ bestMatch.innerHTML = "No best guess for match.";
+ }
+
+ block.appendChild(div);
+
+ return block;
+}
+
+
+function createNewIdenityEditor() {
+ const block = document.createElement("div");
+ block.classList.add("block");
+ const editor = document.createElement("div");
+ editor.classList.add("editor");
+
+ const button = document.createElement("button");
+ button.textContent = "New Identity";
+ editor.appendChild(button);
+
+ [ "lastName", "firstName", "middleName", "name" ].forEach((key) => {
+ const row = document.createElement("div"),
+ left = document.createElement("div"),
+ right = document.createElement("input");
+ row.classList.add("editor-row");
+ left.classList.add("key");
+ left.textContent = key;
+ row.appendChild(left);
+ row.appendChild(right);
+ editor.appendChild(row);
+ });
+
+ button.addEventListener("click", (event) => {
+ const rows = event.currentTarget.parentElement.querySelectorAll(".editor-row"),
+ object = {};
+ Array.prototype.forEach.call(rows, (row) => {
+ object[row.firstChild.textContent] = row.lastChild.value;
+ });
+ object.faces = [];
+ Array.prototype.forEach.call(document.body.querySelectorAll("#face-editor .face"), (face) => {
+ if (!face.hasAttribute("disabled")) {
+ object.faces.push(face.getAttribute("face-id"));
+ }
+ });
+ console.log(object);
+ window.fetch("api/v1/identities", {
+ method: "POST",
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(object)
+ }).then(res => res.json()).then((identities) => {
+ console.log("New identity: ", identities[0]);
+ const face = document.body.querySelector("#face-editor .face");
+ loadFace(parseInt(face.getAttribute("face-id")));
+ });
+ });
+ block.appendChild(editor);
+
+ return block;
+}
\ No newline at end of file
diff --git a/webpack.dev.js b/webpack.dev.js
index fc08e2e..967dcf7 100644
--- a/webpack.dev.js
+++ b/webpack.dev.js
@@ -11,7 +11,13 @@ module.exports = merge(common, {
publicPath: "http://localhost:8765/dist/",
hotOnly: true,
disableHostCheck: true,
- historyApiFallback: true
+ historyApiFallback: true,
+ proxy: {
+ "/api": "http://localhost:8123/photos",
+ "/photos/*": {
+ target: "http://localhost:8123"
+ }
+ },
},
plugins: [new webpack.HotModuleReplacementPlugin()]
});