330 lines
8.6 KiB
HTML
Executable File
330 lines
8.6 KiB
HTML
Executable File
<html>
|
|
<script>'<base href="BASEPATH">';</script>
|
|
<script>
|
|
|
|
/*
|
|
|
|
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)";
|
|
if (!selectable) {
|
|
div.addEventListener("click", (event) => {
|
|
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 {
|
|
div.addEventListener("click", (event) => {
|
|
if (event.currentTarget.hasAttribute("disabled")) {
|
|
event.currentTarget.removeAttribute("disabled");
|
|
} else {
|
|
event.currentTarget.setAttribute("disabled", "");
|
|
}
|
|
});
|
|
}
|
|
return div;
|
|
}
|
|
|
|
function shuffle(array) {
|
|
var i = array.length, tmp, random;
|
|
while (i > 0) {
|
|
random = Math.floor(Math.random() * i);
|
|
i--;
|
|
tmp = array[i];
|
|
array[i] = array[random];
|
|
array[random] = tmp;
|
|
}
|
|
return array;
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", (event) => {
|
|
loadFace();
|
|
});
|
|
|
|
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];
|
|
|
|
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");
|
|
for (let i = 0; i < face.relatedFaces.length && i < 20; i++) {
|
|
let relation = face.relatedFaces[i];
|
|
const distance = document.createElement("div"),
|
|
facePhoto = createFace(relation.faceId, relation.photoId, true);
|
|
distance.classList.add("distance");
|
|
distance.textContent = face.distance.toFixed(2);
|
|
facePhoto.appendChild(distance);
|
|
right.appendChild(facePhoto);
|
|
}
|
|
} else {
|
|
right = document.createElement("input");
|
|
right.value = face[key];
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
identities.forEach((identity) => {
|
|
const block = document.createElement("div");
|
|
block.classList.add("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);
|
|
|
|
identity.relatedFaces.sort((a, b) => {
|
|
return a.distance - b.distance;
|
|
});
|
|
div = document.createElement("div");
|
|
div.classList.add("face-block");
|
|
for (let i = 0; i < identity.relatedFaces.length && i < 4; i++) {
|
|
const facePhoto = createFace(identity.relatedFaces[i].faceId, identity.relatedFaces[i].photoId),
|
|
distance = document.createElement("div");
|
|
distance.classList.add("distance");
|
|
distance.textContent = identity.relatedFaces[i].distance.toFixed(2);
|
|
facePhoto.appendChild(distance);
|
|
div.appendChild(facePhoto);
|
|
}
|
|
|
|
block.appendChild(div);
|
|
identitiesBlock.appendChild(block);
|
|
});
|
|
});
|
|
}
|
|
|
|
function createNewIdenityEditor() {
|
|
const block = document.createElement("div");
|
|
block.classList.add("block");
|
|
const editor = document.createElement("div");
|
|
editor.classList.add("editor");
|
|
[ "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.textContent = key;
|
|
row.appendChild(left);
|
|
row.appendChild(right);
|
|
editor.appendChild(row);
|
|
});
|
|
const button = document.createElement("button");
|
|
button.textContent = "New Identity";
|
|
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")));
|
|
});
|
|
});
|
|
editor.appendChild(button);
|
|
block.appendChild(editor);
|
|
|
|
return block;
|
|
}
|
|
|
|
</script>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.more {
|
|
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;
|
|
}
|
|
|
|
.face[disabled] {
|
|
filter: grayscale(100%);
|
|
}
|
|
|
|
.face-block {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
margin: 0.5em;
|
|
min-width: 128px;
|
|
max-width: 128px;
|
|
min-height: 128px;
|
|
max-height: 128px;
|
|
}
|
|
|
|
.face-block .face {
|
|
max-width: 64px;
|
|
max-height: 64px;
|
|
margin: 0;
|
|
}
|
|
|
|
.face {
|
|
position: relative;
|
|
max-width: 128px;
|
|
max-height: 128px;
|
|
width: 128px;
|
|
height: 128px;
|
|
background-size: contain;
|
|
background-position: center center;
|
|
display: inline-block;
|
|
border: 1px solid black;
|
|
margin: 0.5em;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.face:hover {
|
|
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.face .distance {
|
|
position: absolute;
|
|
background: black;
|
|
opacity: 0.5;
|
|
text-align: center;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
color: white;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.editor-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
padding: 0.25em 0.5em;
|
|
}
|
|
|
|
.editor-row :first-child {
|
|
width: 10em;
|
|
}
|
|
|
|
</style>
|
|
<body>
|
|
<div id="face-editor" class="block"></div>
|
|
<div id="identities" class="block"></div>
|
|
</body>
|