diff --git a/server/routes/faces.js b/server/routes/faces.js index c370223..013473f 100755 --- a/server/routes/faces.js +++ b/server/routes/faces.js @@ -22,7 +22,9 @@ router.put("/:id", function(req, res/*, next*/) { return res.status(400).send("Invalid request"); }); -router.delete("/:id?", function(req, res/*, next*/) { +router.delete("/:id", function(req, res/*, next*/) { + console.log(`DELETE /${req.params.id}`); + if (!req.user.maintainer) { return res.status(401).send("Unauthorized to delete photos."); } diff --git a/src/App.css b/src/App.css index fbef130..0966b0e 100755 --- a/src/App.css +++ b/src/App.css @@ -134,8 +134,7 @@ a.Link { .Body { position: absolute; - margin-top: 120px; /* .Header's two 60px chunks */ - margin-bottom: 64px; + display: flex; top: 0; bottom: 0; left: 0; @@ -147,18 +146,18 @@ a.Link { } .Body > * { + display: flex; box-sizing: border-box; } .Main { - display: inline-flex; + display: flex; position: absolute; + padding-top: 64px; + padding-bottom: 64px; top: 0; - bottom: 0; left: 0; right: 0; - background-repeat: no-repeat; - background-position: center; } @@ -216,6 +215,10 @@ body { padding: 0.5em; } +button { + margin: 0.25em; +} + #identities { display: flex; flex-wrap: wrap; @@ -229,6 +232,36 @@ body { margin: 0.25em; padding: 0.25em; box-sizing: border-box; + display: flex; + flex-direction: column; +} + +#photo-explorer { + position: relative; + display: flex; + flex-grow: 1; + background-repeat: no-repeat; + background-size: contain; + background-position: 0%; + padding: 0; + margin: 0; +} + +#photo-explorer .face-box { + position: absolute; + display: inline-block; + border: 1px solid black; + cursor: pointer; +} + +#photo-explorer #photo-info { + display: inline-block; + position: absolute; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + padding: 0.25em; } .more:hover { @@ -305,6 +338,7 @@ body { .Identities { display:flex; flex-direction:column; + flex-grow: 1; } .Identities > div:first-child { diff --git a/src/App.js b/src/App.js index 913a90e..2e45405 100755 --- a/src/App.js +++ b/src/App.js @@ -64,22 +64,22 @@ class Identities extends React.Component { componentDidMount() { let params = window.location.search ? window.location.search.replace(/^\?/, "").split("&") : [], - id; + face; for (let i in params) { let parts = params[i].split("="); switch (parts[0]) { - case "id": - id = parseInt(parts[1]); - if (id != parts[1]) { - id = undefined; + case "face": + face = parseInt(parts[1]); + if (face != parts[1]) { + face = undefined; } break; } } - loadFace(id); + loadFace(face); const newIdentity = document.getElementById("new-identity"); - newIdentity.appendChild(createNewIdenityEditor()); + newIdentity.appendChild(createNewIdentityEditor()); } render() { @@ -89,6 +89,7 @@ class Identities extends React.Component {
+
@@ -149,7 +150,11 @@ function createFace(faceId, photoId, selectable) { if (event.shiftKey) { /* identities */ let faceId = parseInt(event.currentTarget.getAttribute("face-id")); if (faceId) { - window.location.href = "identities.html?id=" + faceId; + if (event.ctrlKey) { + window.open("identities?face=" + faceId, "faceId-" + faceId); + } else { + window.location.href = "identities?face=" + faceId; + } } else { alert("No face id mapped to face."); } @@ -171,17 +176,112 @@ function createFace(faceId, photoId, selectable) { return div; } +function loadPhoto(photo) { + const tmp = document.querySelector("base"); + let base; + if (tmp) { + base = new URL(tmp.href).pathname.replace(/\/$/, "") + "/"; /* Make sure there is a trailing slash */ + } else { + base = "/"; + } + + const photoExplorerBlock = document.getElementById("photo-explorer"); + photoExplorerBlock.innerHTML = "
"; + + const url = base + photo.path + "thumbs/scaled/" + photo.filename, + taken = new Date(photo.taken), + xml = new XMLHttpRequest(), + loading = document.getElementById("loading"); + + if (loading) { + loading.textContent = "0%"; + } + + const info = photoExplorerBlock.querySelector("div"); + info.textContent = photo.path + photo.filename; + const bar = document.getElementById("bar"); + if (bar) { + bar.parentElement.removeChild(bar); + } + xml.onprogress = function (event) { + var alpha = 0; + if (event.total) { + alpha = event.loaded / event.total; + if (loading) { + loading.textContent = Math.ceil(100 * alpha) + "%"; + } + } else { + if (loading) { + loading.textContent = "0%"; + } + } + }; + + xml.onload = function(event) { + photoExplorerBlock.style.backgroundImage = "url(" + encodeURI(url).replace(/\(/g, '%28').replace(/\)/g, '%29') + ")"; + /* Give the DOM time to update before fitting boxes... */ + setTimeout(() => { + makeFaceBoxes(photo, photoExplorerBlock); + }, 250); + } + + xml.open("GET", url, true); + xml.send(); +} + +function makeFaceBoxes(photo, photoExplorer) { + if (!photo.faces || !photo.faces.length) { + return; + } + + let scale; + + /* If photo aspect ratio is higher than viewport aspect ratio, width will be scaled */ + if (photo.width / photo.height > photoExplorer.offsetWidth / photoExplorer.offsetHeight) { + scale = photoExplorer.offsetWidth / photo.width; + } else { + scale = photoExplorer.offsetHeight / photo.height; + } + + photo.faces.forEach((face) => { + const box = document.createElement("div"); + box.classList.add("face-box"); + photoExplorer.appendChild(box); + box.style.left = (photo.width * face.left * scale) + "px"; + box.style.top = (photo.height * face.top * scale) + "px"; + box.style.width = (photo.width * (face.right - face.left) * scale) + "px"; + box.style.height = (photo.height * (face.bottom - face.top) * scale) + "px"; + box.addEventListener("click", () => { + if (event.ctrlKey) { /* face-explorer */ + window.open("face-explorer.html?" + photo.id, "photo-" + photo.id); + } + if (event.shiftKey) { + window.open("identities?face=" + face.id, "faceId-" + face.id); + } else { + loadFace(face.id); + } + }); + }); +} + function loadFace(id) { const faceEditorBlock = document.getElementById("face-editor"); faceEditorBlock.innerHTML = ""; + const photoExplorerBlock = document.getElementById("photo-explorer"); + photoExplorerBlock.innerHTML = ""; + window.fetch("api/v1/faces" + (id ? "/" + id : "")).then(res => res.json()).then((faces) => { if (faces.length == 0) { return; } const face = faces[0]; - + + window.fetch("api/v1/photos/random/" + face.photoId).then(res => res.json()).then((photo) => { + loadPhoto(photo); + }); + if (face.identityId) { getIdentity(face.identityId); } else { @@ -259,7 +359,7 @@ function getIdentity(identityId) { window.fetch("api/v1/identities/" + identityId).then(res => res.json()).then((identities) => { identities.forEach((identity) => { const block = createIdentityBlock(identity, true), - button = createUseThisIdentityButton(identity); + button = createUseThisIdentityButton(identity.id); block.insertBefore(button, block.firstChild); identitiesBlock.appendChild(block); }) @@ -279,16 +379,49 @@ function getIdentities(faceId) { return a.lastName.localeCompare(b.lastName); }); + let minDistance = { + distance: 1 + }; + identities.forEach((identity) => { + identity.relatedFaces.forEach((face) => { + if (face.distance < minDistance.distance) { + minDistance.distance = face.distance; + minDistance.photoId = face.photoId; + minDistance.faceId = face.faceId; + minDistance.identity = identity; + } + }); + const block = createIdentityBlock(identity), - button = createUseThisIdentityButton(identity); + button = createUseThisIdentityButton(identity.id); block.insertBefore(button, block.firstChild); identitiesBlock.appendChild(block); }); + + + const bestMatch = document.getElementById("best-match"); + const buttonBlock = createActionButtons(); + if (minDistance.distance != 1) { + bestMatch.innerHTML = ""; + bestMatch.appendChild(document.createElement("div")); + bestMatch.firstChild.textContent = `Best match: ${minDistance.identity.name}`; + 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.appendChild(facePhoto); + buttonBlock.insertBefore(createUseThisIdentityButton(minDistance.identity.id), buttonBlock.firstChild); + } else { + bestMatch.innerHTML = "No best guess for match."; + } + bestMatch.insertBefore(buttonBlock, bestMatch.firstChild); }); } -function createUseThisIdentityButton(identity) { + +function createUseThisIdentityButton(identityId) { const button = document.createElement("button"); button.textContent = "Use this identity"; button.addEventListener("click", (event) => { @@ -300,7 +433,7 @@ function createUseThisIdentityButton(identity) { object.faces.push(face.getAttribute("face-id")); } }); - window.fetch("api/v1/identities/faces/add/" + identity.id, { + window.fetch("api/v1/identities/faces/add/" + identityId, { method: "PUT", headers: { 'Accept': 'application/json', @@ -316,12 +449,49 @@ function createUseThisIdentityButton(identity) { return button; } + +function createActionButtons() { + const buttonBlock = document.createElement("div"); + let button = document.createElement("button"); + + button = document.createElement("button"); + button.textContent = "Load random face"; + button.addEventListener("click", (event) => { + loadFace(); + }); + buttonBlock.appendChild(button); + + button = document.createElement("button"); + button.textContent = "Not a face"; + button.addEventListener("click", (event) => { + window.fetch("api/v1/face", { + method: "DELETE", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ faceId: faceId }) + }).then(res => res.json()).then(() => { + loadFace(); + }); + }); + buttonBlock.appendChild(button); + + return buttonBlock; +} + function createIdentityBlock(identity, nolimit) { const block = document.createElement("div"); block.classList.add("block"); let div = document.createElement("div"); - div.textContent = identity.name; + if (identity.lastName && identity.firstName) { + div.textContent = `${identity.lastName}, ${identity.firstName}`; + } else if (identity.lastName) { + div.textContent = identity.lastName; + } else { + div.textContent = identity.name; + } block.appendChild(div); @@ -339,10 +509,6 @@ function createIdentityBlock(identity, 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; @@ -351,44 +517,23 @@ function createIdentityBlock(identity, nolimit) { 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() { +function createNewIdentityEditor() { 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); @@ -417,7 +562,6 @@ function createNewIdenityEditor() { object.faces.push(face.getAttribute("face-id")); } }); - console.log(object); window.fetch("api/v1/identities", { method: "POST", headers: { @@ -433,8 +577,10 @@ function createNewIdenityEditor() { el.value = ""; } }); - const face = document.body.querySelector("#face-editor .face"); - loadFace(parseInt(face.getAttribute("face-id"))); + const face = document.body.querySelector("#face-editor .face"), + faceId = parseInt(face.getAttribute("face-id")); + loadFace(faceId); + getIdentities(faceId); }); }); block.appendChild(editor); diff --git a/webpack.dev.js b/webpack.dev.js index 246b207..89657c8 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -18,7 +18,7 @@ module.exports = merge(common, { target: "http://localhost:8123", bypass: function(req, res, proxyOptions) { console.log(req.url); - if (req.url == '/photos/identities') { + if (req.url.match(/^\/photos\/identities[/?]?/)) { return 'index.html'; } else { return null;