diff --git a/css/empty-view.css b/css/empty-view.css new file mode 100644 index 00000000..5b728058 --- /dev/null +++ b/css/empty-view.css @@ -0,0 +1,18 @@ +#empty-view { + display: none; + align-items: center; + background-color: #e1e1e1; + padding: 10px; + position: fixed; + z-index: 1; + height: 30px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 5px; + border: 1px solid #000; +} + +#empty-view p { + margin-left: 10px; +} diff --git a/css/information.css b/css/information.css index 6f02a227..e6ebef1e 100644 --- a/css/information.css +++ b/css/information.css @@ -37,3 +37,40 @@ .info-modal-content { width: 100%; } + +#information-buttons { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + margin-top: 10px; +} + +.information-button { + background-color: #fff; + padding: 5px; + border-radius: 5px; + border: 1px solid black; + margin: 0 5px; +} + +.information-button:hover { + cursor: pointer; + background-color: #c5c5c5; +} + +#information-button { + background-color: #c5c5c5; +} + +#view-information-button { + display: none; +} + +#information-button { + display: none; +} + +#view-information-content { + display: none; +} diff --git a/css/switch-deploy.css b/css/switch-deploy.css new file mode 100644 index 00000000..dea8a671 --- /dev/null +++ b/css/switch-deploy.css @@ -0,0 +1,32 @@ +#switch-deploy { + position: fixed; + left: 10px; + bottom: 10px; + display: flex; + flex-direction: row; + align-items: center; + z-index: 1; + background-color: #fff; + padding: 5px; + border-radius: 5px; + border: 1px solid #000; +} + +#switch-deploy-button { + cursor: pointer; + background-color: #fff; + border: 1px solid #000; + padding: 5px; + border-radius: 5px; + font-family: sans-serif; + font-size: 14px; +} + +#switch-deploy-button:hover { + background-color: #c5c5c5; + cursor: pointer; +} + +#switch-deploy-text { + margin: 0 7px 0 0; +} diff --git a/css/views.css b/css/views.css new file mode 100644 index 00000000..86e4c0c8 --- /dev/null +++ b/css/views.css @@ -0,0 +1,61 @@ +#available-views { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: flex-start; + align-items: center; + padding: 8px; + max-height: 90px; + overflow-y: auto; +} + +.view-button { + background-color: #f1f1f1; + border: 1px solid #000; + border-radius: 5px; + padding: 8px; + margin: 8px; + cursor: pointer; + height: fit-content; +} + +#views { + display: none; + flex-direction: column; + position: fixed; + top: 25%; + left: 10px; + width: fit-content; + height: 50%; + background-color: #e1e1e1; + padding: 15px; + border: 1px solid #000; + border-radius: 5px; +} + +#view-selector { + display: flex; + flex-direction: column; + justify-content: flex-start; + overflow-y: auto; + overflow-x: hidden; + width: fit-content; +} + +.view-selector-menu::-webkit-scrollbar { + width: 7px; +} + +.view-selector-menu::-webkit-scrollbar-track { + background: #e1e1e1; + border-radius: 5px; +} + +.view-selector-menu::-webkit-scrollbar-thumb { + background: #afafaf; + border-radius: 5px; +} + +.view-selector-menu::-webkit-scrollbar-thumb:hover { + background: #858585; +} diff --git a/img/blue-info.svg b/img/blue-info.svg new file mode 100644 index 00000000..760cf319 --- /dev/null +++ b/img/blue-info.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/index.html b/index.html index 47939161..c8222b41 100644 --- a/index.html +++ b/index.html @@ -16,6 +16,9 @@ + + + @@ -33,14 +36,27 @@
-

Example input file (right click to save): p8_ee_ZH_ecm240.edm4hep.json +

Example input files (right click to save): +

+
+

@@ -95,65 +111,94 @@
eedE -

Welcome to eede, an EDM4hep Event Data Explorer. Learn more about eede on the wiki. - Want to learn more about EDM4hep? Check out the EDM4hep website. -

-

- Found a bug or have a feature request? Open a new issue. -

-

Contact:

- -

Version: 0.10

+
+ + +
+
+
+

Welcome to eede, an EDM4hep Event Data Explorer. Learn more about eede on the wiki. + Want to learn more about EDM4hep? Check out the EDM4hep website. +

+

+ Found a bug or have a feature request? Open a new issue. +

+

Contact:

+
    +
  • Juraj Smiesko: + + + +
  • +
  • Thomas Madlener: + + +
  • +
  • Braulio Rivas: + + +
  • +
+

Version: 0.20

+
+
+

+
+
+
+
+

Select a view:

+
+
+ +
+ Empty view +

This view has no elements

+
+ +
+

Switch to

+ +
+ - + + + \ No newline at end of file diff --git a/js/draw.js b/js/draw.js index 65159e75..555d8173 100644 --- a/js/draw.js +++ b/js/draw.js @@ -1,39 +1,58 @@ import { canvas, ctx } from "./main.js"; +import { updateCanvas } from "./lib/graphic-primitives.js"; -export function drawAll(ctx, loadedObjects) { - ctx.clearRect(0, 0, canvas.width, canvas.height); +function draw(objects) { + const datatypes = objects.datatypes; + const associations = objects.associations; - for (const elements of Object.values(loadedObjects)) { + for (const collection of Object.values(associations)) { + for (const association of collection) { + association.draw(ctx); + } + } + + for (const elements of Object.values(datatypes)) { const { collection, oneToMany, oneToOne } = elements; for (const links of Object.values(oneToMany)) { - for (const link of links) link.draw(ctx); + for (const link of links) { + link.draw(ctx); + } } - for (const link of Object.values(oneToOne)) link.draw(ctx); + for (const links of Object.values(oneToOne)) { + for (const link of links) { + link.draw(ctx); + } + } - for (const object of collection) object.draw(ctx); + for (const object of collection) { + object.draw(ctx); + } } } +export function drawAll(loadedObjects) { + emptyCanvas(); + draw(loadedObjects); +} + export function drawVisible(visibleObjects) { + emptyVisibleCanvas(); + draw(visibleObjects); +} + +export function emptyCanvas() { + updateCanvas(ctx, 0, 0, canvas.width, canvas.height); +} + +function emptyVisibleCanvas() { const boundigClientRect = canvas.getBoundingClientRect(); - ctx.clearRect( + updateCanvas( + ctx, 0 - boundigClientRect.x, 0 - boundigClientRect.y, window.innerWidth, window.innerHeight ); - - for (const elements of Object.values(visibleObjects)) { - const { collection, oneToMany, oneToOne } = elements; - - for (const links of Object.values(oneToMany)) { - for (const link of links) link.draw(ctx); - } - - for (const link of Object.values(oneToOne)) link.draw(ctx); - - for (const object of collection) object.draw(ctx); - } } diff --git a/js/event-number.js b/js/event-number.js new file mode 100644 index 00000000..83b773ad --- /dev/null +++ b/js/event-number.js @@ -0,0 +1,73 @@ +import { loadObjects } from "./types/load.js"; +import { copyObject } from "./lib/copy.js"; +import { jsonData, selectedObjectTypes } from "./main.js"; +import { objectTypes } from "./types/objects.js"; +import { drawCurrentView, saveScrollLocation } from "./views/views.js"; + +const eventNumber = document.getElementById("selected-event"); +const previousEvent = document.getElementById("previous-event"); +const nextEvent = document.getElementById("next-event"); + +const currentEvent = {}; + +const eventCollection = {}; +const currentObjects = {}; + +function updateEventNumber() { + if (eventNumber.firstChild) { + eventNumber.removeChild(eventNumber.firstChild); + } + eventNumber.appendChild( + document.createTextNode(`Event: ${currentEvent.event}`) + ); +} + +function loadSelectedEvent() { + if (eventCollection[currentEvent.event] === undefined) { + const objects = loadObjects( + jsonData.data, + currentEvent.event, + selectedObjectTypes.types + ); + + eventCollection[currentEvent.event] = objects; + + for (const [key, value] of Object.entries( + eventCollection[currentEvent.event].datatypes + )) { + const classType = objectTypes[key]; + const collection = value.collection; + classType.setup(collection); + } + copyObject(objects, currentObjects); + } else { + copyObject(eventCollection[currentEvent.event], currentObjects); + } +} + +export function renderEvent(eventNumber) { + saveScrollLocation(); + currentEvent.event = eventNumber; + loadSelectedEvent(); + updateEventNumber(); + drawCurrentView(); +} + +previousEvent.addEventListener("click", () => { + const newEventNum = `${parseInt(currentEvent.event) - 1}`; + renderEvent(newEventNum); +}); +nextEvent.addEventListener("click", () => { + const newEventNum = `${parseInt(currentEvent.event) + 1}`; + renderEvent(newEventNum); +}); +eventNumber.addEventListener("click", () => { + const eventSelectorMenu = document.getElementById("event-selector-menu"); + if (eventSelectorMenu.style.display === "flex") { + eventSelectorMenu.style.display = "none"; + } else { + eventSelectorMenu.style.display = "flex"; + } +}); + +export { currentObjects, currentEvent }; diff --git a/js/events.js b/js/events.js index 660ec57b..8eb2e169 100644 --- a/js/events.js +++ b/js/events.js @@ -10,7 +10,7 @@ const mouseDown = function (event, visibleObjects, dragTools) { dragTools.prevMouseX = mouseX; dragTools.prevMouseY = mouseY; - for (const { collection } of Object.values(visibleObjects)) { + for (const { collection } of Object.values(visibleObjects.datatypes)) { for (const object of collection) { if (object.isHere(mouseX, mouseY)) { dragTools.draggedObject = object; @@ -30,7 +30,7 @@ const mouseUp = function (event, currentObjects, dragTools) { dragTools.isDragging = false; // console.time("drawAll"); - drawAll(ctx, currentObjects); + drawAll(currentObjects); // console.timeEnd("drawAll"); }; @@ -44,15 +44,38 @@ const mouseOut = function (event, dragTools) { }; const mouseMove = function (event, visibleObjects, dragTools) { - if (!dragTools.isDragging) { - return; - } - event.preventDefault(); - const boundigClientRect = canvas.getBoundingClientRect(); const mouseX = parseInt(event.clientX - boundigClientRect.x); const mouseY = parseInt(event.clientY - boundigClientRect.y); + const allObjects = Object.values(visibleObjects.datatypes) + .map((datatype) => datatype.collection) + .flat(); + + let someHovered = false; + for (const object of allObjects) { + if (object.isHere(mouseX, mouseY)) { + if (dragTools.hoveredObject !== object) { + dragTools.hoveredObject = object; + drawVisible(visibleObjects); + object.showObjectTip(ctx); + } + someHovered = true; + document.body.style.cursor = "pointer"; + break; + } + } + + if (!someHovered) { + document.body.style.cursor = "default"; + dragTools.hoveredObject = null; + drawVisible(visibleObjects); + } + + if (!dragTools.isDragging) { + return; + } + const dx = mouseX - dragTools.prevMouseX; const dy = mouseY - dragTools.prevMouseY; @@ -60,9 +83,7 @@ const mouseMove = function (event, visibleObjects, dragTools) { draggedObject.x += dx; draggedObject.y += dy; - // console.time("drawVisible"); drawVisible(visibleObjects); - // console.timeEnd("drawVisible"); dragTools.prevMouseX = mouseX; dragTools.prevMouseY = mouseY; @@ -71,10 +92,14 @@ const mouseMove = function (event, visibleObjects, dragTools) { const getVisible = function (loadedObjects, visibleObjects) { const boundigClientRect = canvas.getBoundingClientRect(); - for (const [objectType, elements] of Object.entries(loadedObjects)) { + visibleObjects.datatypes = {}; + visibleObjects.associations = {}; + for (const [objectType, elements] of Object.entries( + loadedObjects.datatypes ?? {} + )) { const { collection, oneToMany, oneToOne } = elements; - visibleObjects[objectType] = { + visibleObjects.datatypes[objectType] = { collection: [], oneToMany: {}, oneToOne: {}, @@ -89,12 +114,12 @@ const getVisible = function (loadedObjects, visibleObjects) { window.innerHeight ) ) { - visibleObjects[objectType].collection.push(object); + visibleObjects.datatypes[objectType].collection.push(object); } } for (const [name, links] of Object.entries(oneToMany)) { - visibleObjects[objectType].oneToMany[name] = []; + visibleObjects.datatypes[objectType].oneToMany[name] = []; for (const link of links) { if ( @@ -105,13 +130,13 @@ const getVisible = function (loadedObjects, visibleObjects) { window.innerHeight ) ) { - visibleObjects[objectType].oneToMany[name].push(link); + visibleObjects.datatypes[objectType].oneToMany[name].push(link); } } } for (const [name, links] of Object.entries(oneToOne)) { - visibleObjects[objectType].oneToOne[name] = null; + visibleObjects.datatypes[objectType].oneToOne[name] = []; for (const link of links) { if ( @@ -122,11 +147,30 @@ const getVisible = function (loadedObjects, visibleObjects) { window.innerHeight ) ) { - visibleObjects[objectType].oneToOne[name] = link; + visibleObjects.datatypes[objectType].oneToOne[name].push(link); } } } } + + for (const [name, links] of Object.entries( + loadedObjects.associations ?? {} + )) { + visibleObjects.associations[name] = []; + + for (const link of links) { + if ( + link.isVisible( + 0 - boundigClientRect.x, + 0 - boundigClientRect.y, + window.innerWidth, + window.innerHeight + ) + ) { + visibleObjects.associations[name].push(link); + } + } + } }; const onScroll = function (currentObjects, visibleObjects) { diff --git a/js/filter/mcparticle.js b/js/filter/mcparticle.js new file mode 100644 index 00000000..f29bb0bb --- /dev/null +++ b/js/filter/mcparticle.js @@ -0,0 +1,47 @@ +import { PdgToggle } from "../menu/show-pdg.js"; +import { + bits, + genStatus, + renderRangeParameters, + parametersRange, + renderGenSim, + start, + getWidthFilterContent, +} from "../menu/filter/filter.js"; +import { drawAll } from "../draw.js"; + +const filter = document.getElementById("filter"); +const filters = document.getElementById("filters"); +const manipulationTools = document.getElementsByClassName("manipulation-tool"); + +export function setupMCParticleFilter( + viewObjects, + viewCurrentObjects, + viewVisibleObjects +) { + for (const tool of manipulationTools) { + tool.style.display = "flex"; + } + const mcObjects = + viewCurrentObjects.datatypes["edm4hep::MCParticle"].collection; + genStatus.reset(); + mcObjects.forEach((mcObject) => { + genStatus.add(mcObject.generatorStatus); + }); + genStatus.setCheckBoxes(); + filters.replaceChildren(); + + renderRangeParameters(parametersRange); + renderGenSim(bits, genStatus); + + const width = getWidthFilterContent(); + filter.style.width = width; + const pdgToggle = new PdgToggle("show-pdg"); + pdgToggle.init(() => { + pdgToggle.toggle(viewCurrentObjects, () => { + drawAll(viewCurrentObjects); + }); + }); + + start(viewObjects, viewCurrentObjects, viewVisibleObjects); +} diff --git a/js/filter/nofilter.js b/js/filter/nofilter.js new file mode 100644 index 00000000..0e41a3fc --- /dev/null +++ b/js/filter/nofilter.js @@ -0,0 +1,7 @@ +export function setupNoFilter() { + const manipulationTools = + document.getElementsByClassName("manipulation-tool"); + for (const tool of manipulationTools) { + tool.style.display = "none"; + } +} diff --git a/js/graphic-primitives.js b/js/graphic-primitives.js deleted file mode 100644 index e9aec198..00000000 --- a/js/graphic-primitives.js +++ /dev/null @@ -1,59 +0,0 @@ -export function drawLine(ctx, startX, startY, endX, endY, color) { - ctx.save(); - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(startX, startY); - ctx.lineTo(endX, endY); - ctx.stroke(); - ctx.restore(); -} - -export function drawCross(ctx, x, y, color = "#F00") { - const crossLenght = 6; - ctx.save(); - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(x - crossLenght, y - crossLenght); - ctx.lineTo(x + crossLenght, y + crossLenght); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(x + crossLenght, y - crossLenght); - ctx.lineTo(x - crossLenght, y + crossLenght); - ctx.stroke(); - ctx.restore(); -} - -export function drawRoundedRect(ctx, x, y, width, height, fillColor) { - ctx.save(); - - ctx.fillStyle = fillColor; - ctx.beginPath(); - ctx.roundRect(x, y, width, height, 15); - ctx.fill(); - - ctx.strokeStyle = "black"; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.roundRect(x, y, width, height, 15); - ctx.stroke(); - ctx.restore(); -} - -export function drawTex(ctx, x, y, texImg, maxWidth) { - let scale = (maxWidth * 0.9) / texImg.naturalWidth; - if (scale > 2) { - scale = 2; - } - const tempHeight = texImg.naturalHeight * scale; - const tempWidth = texImg.naturalWidth * scale; - - ctx.save(); - ctx.drawImage( - texImg, - x - tempWidth / 2, - y - tempHeight / 2, - tempWidth, - tempHeight - ); - ctx.restore(); -} diff --git a/js/information.js b/js/information.js index d26168cc..d0972c08 100644 --- a/js/information.js +++ b/js/information.js @@ -2,6 +2,8 @@ const infoIcon = document.getElementById("information-icon"); const closeIcon = document.getElementById("close-information"); const copyToClipboardButtons = document.getElementsByClassName("copy-email-button"); +const informationButton = document.getElementById("information-button"); +const viewButton = document.getElementById("view-information-button"); Array.from(copyToClipboardButtons).forEach((button) => { button.addEventListener("click", () => { @@ -56,3 +58,63 @@ window.addEventListener("click", (event) => { hideModal(); } }); + +function chooseButton(id) { + const buttons = document.getElementsByClassName("information-button"); + Array.from(buttons).forEach((button) => { + if (button.id === id) { + button.style.backgroundColor = "#c5c5c5"; + } else { + button.style.backgroundColor = "#ffffff"; + } + }); +} + +function showOption(id) { + const informationOptions = document.getElementById("information-options"); + const children = informationOptions.children; + Array.from(children).forEach((child) => { + if (child.id === id) { + child.style.display = "block"; + } else { + child.style.display = "none"; + } + }); +} + +export function selectInformationSection() { + chooseButton("information-button"); + showOption("information-content"); +} + +export function selectViewInformation() { + chooseButton("view-information-button"); + showOption("view-information-content"); +} + +informationButton.addEventListener("click", () => { + selectInformationSection(); +}); + +viewButton.addEventListener("click", () => { + selectViewInformation(); +}); + +export function showViewInformation(title, description) { + if (viewButton.style.display !== "block") { + viewButton.style.display = "block"; + } + + const viewTitle = document.getElementById("view-title-info"); + viewTitle.innerText = `Learn more about ${title} view`; + + const viewDescription = document.getElementById("view-description-info"); + viewDescription.replaceChildren(); + const newElement = document.createElement("div"); + newElement.innerHTML = description; + viewDescription.appendChild(newElement.firstChild); +} + +export function hideViewInformation() { + viewButton.style.display = "none"; +} diff --git a/js/lib/copy.js b/js/lib/copy.js index f87cbc03..c650058c 100644 --- a/js/lib/copy.js +++ b/js/lib/copy.js @@ -1,25 +1,33 @@ -export function copyObject(objToCopy, updatedObject) { - for (const [key, value] of Object.entries(objToCopy)) { - updatedObject[key] = value; +export function copyObject(source, destiny) { + for (const [key, value] of Object.entries(source)) { + destiny[key] = value; } } -export function emptyCopyObject(objToCopy, updatedObject) { - for (const [objectType, elements] of Object.entries(objToCopy)) { +export function emptyCopyObject(source, destiny) { + destiny.datatypes = {}; + + for (const [objectType, elements] of Object.entries(source.datatypes)) { const { _, oneToMany, oneToOne } = elements; - updatedObject[objectType] = { + destiny.datatypes[objectType] = { collection: [], oneToMany: {}, oneToOne: {}, }; for (const name in oneToMany) { - updatedObject[objectType].oneToMany[name] = []; + destiny.datatypes[objectType].oneToMany[name] = []; } for (const name in oneToOne) { - updatedObject[objectType].oneToOne[name] = null; + destiny.datatypes[objectType].oneToOne[name] = []; } } + + destiny.associations = {}; + + for (const key in source.associations) { + destiny.associations[key] = []; + } } diff --git a/js/lib/empty-object.js b/js/lib/empty-object.js new file mode 100644 index 00000000..8b24aecd --- /dev/null +++ b/js/lib/empty-object.js @@ -0,0 +1,32 @@ +const updateEmpty = (empty, length) => { + if (length === 0) { + empty.value = empty.value && true; + } else { + empty.value = false; + } +}; + +export function checkEmptyObject(obj) { + const datatypes = obj.datatypes; + const associations = obj.associations; + + let empty = { value: true }; + + Object.values(datatypes).forEach((datatype) => { + updateEmpty(empty, datatype.collection.length); + + Object.values(datatype.oneToMany).forEach((oneToMany) => { + updateEmpty(empty, oneToMany.length); + }); + + Object.values(datatype.oneToOne).forEach((oneToOne) => { + updateEmpty(empty, oneToOne.length); + }); + }); + + Object.values(associations).forEach((association) => { + updateEmpty(empty, association.length); + }); + + return empty.value; +} diff --git a/js/lib/getName.js b/js/lib/getName.js index edfbc1f5..599fb792 100644 --- a/js/lib/getName.js +++ b/js/lib/getName.js @@ -1,4 +1,4 @@ -import { mappings } from "../../data/particles.js"; +import { mappings } from "../../mappings/particles.js"; export function getName(pdg) { const particle = mappings[pdg]; diff --git a/js/lib/graphic-primitives.js b/js/lib/graphic-primitives.js new file mode 100644 index 00000000..1318b7cb --- /dev/null +++ b/js/lib/graphic-primitives.js @@ -0,0 +1,152 @@ +export function drawRoundedRect(ctx, x, y, width, height, fillColor, radius) { + ctx.save(); + + ctx.fillStyle = fillColor; + ctx.beginPath(); + ctx.roundRect(x, y, width, height, radius); + ctx.fill(); + + ctx.strokeStyle = "black"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.roundRect(x, y, width, height, radius); + ctx.stroke(); + ctx.restore(); +} + +export function drawTex(ctx, x, y, texImg, maxWidth) { + let scale = (maxWidth * 0.9) / texImg.naturalWidth; + if (scale > 2) { + scale = 2; + } + const tempHeight = texImg.naturalHeight * scale; + const tempWidth = texImg.naturalWidth * scale; + + ctx.save(); + ctx.drawImage( + texImg, + x - tempWidth / 2, + y - tempHeight / 2, + tempWidth, + tempHeight + ); + ctx.restore(); +} + +export function drawTextLines(ctx, lines, boxCenterX, y, n) { + ctx.save(); + ctx.font = "16px sans-serif"; + for (const [i, lineText] of lines.entries()) { + ctx.fillText( + lineText, + boxCenterX - ctx.measureText(lineText).width / 2, + y + i * n + ); + } + ctx.restore(); +} + +export function drawBezierLink(ctx, link) { + const boxFrom = link.from; + const boxTo = link.to; + + const fromX = boxFrom.x + boxFrom.width / 2; + const fromY = boxFrom.y + boxFrom.height; + const toX = boxTo.x + boxTo.width / 2; + const toY = boxTo.y; + + if (toY > fromY) { + var cpFromY = (toY - fromY) / 2 + fromY; + var cpToY = cpFromY; + } else { + cpFromY = (fromY - toY) / 2 + fromY; + cpToY = toY - (fromY - toY) / 2; + } + + if (toX > fromX) { + var cpFromX = (toX - fromX) / 4 + fromX; + var cpToX = (3 * (toX - fromX)) / 4 + fromX; + } else { + cpFromX = (3 * (fromX - toX)) / 4 + toX; + cpToX = (fromX - toX) / 4 + toX; + } + + ctx.save(); + ctx.strokeStyle = link.color; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(fromX + link.xShift, fromY); + ctx.bezierCurveTo( + cpFromX + link.xShift, + cpFromY, + cpToX + link.xShift, + cpToY, + toX + link.xShift, + toY + ); + ctx.stroke(); + ctx.restore(); +} + +export function drawStraightLink(ctx, link) { + const boxFrom = link.from; + const boxTo = link.to; + + const fromX = boxFrom.x + boxFrom.width / 2; + const fromY = boxFrom.y + boxFrom.height / 2; + + const toX = boxTo.x + boxTo.width / 2; + const toY = boxTo.y + boxTo.height / 2; + + ctx.save(); + ctx.strokeStyle = link.color; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(fromX, fromY); + ctx.lineTo(toX, toY); + ctx.stroke(); + ctx.restore(); +} + +export function updateCanvas(ctx, x, y, width, height) { + ctx.clearRect(x, y, width, height); +} + +export function drawObjectHeader(ctx, object) { + ctx.save(); + ctx.font = "bold 16px sans-serif"; + const text = object.constructor.name; + const boxCenterX = object.x + object.width / 2; + const textWidth = ctx.measureText(text).width; + const x = boxCenterX - ctx.measureText(text).width / 2; + const topY = object.y + 20; + + if (textWidth > object.width) { + const lines = text.split(/(?=[A-Z])/); + for (const [i, lineText] of lines.entries()) { + ctx.fillText( + lineText, + boxCenterX - ctx.measureText(lineText).width / 2, + topY + i * 20 + ); + } + } else { + ctx.fillText(text, x, topY); + } + ctx.restore(); +} + +export function drawObjectInfoTip(ctx, x, y, ...args) { + ctx.save(); + ctx.font = "bold 12px sans-serif"; + const lines = args.length; + const height = 20 * lines; + const maxWidth = Math.max(...args.map((arg) => ctx.measureText(arg).width)); + ctx.fillStyle = "rgba(225, 225, 225, 1)"; + ctx.fillRect(x, y, maxWidth + 10, height + 10); + ctx.fillStyle = "black"; + for (const [i, arg] of args.entries()) { + ctx.fillText(arg, x + 5, y + 20 + i * 20); + } + ctx.restore(); +} diff --git a/js/lib/html-string.js b/js/lib/html-string.js new file mode 100644 index 00000000..2d8e2eaf --- /dev/null +++ b/js/lib/html-string.js @@ -0,0 +1,3 @@ +export function spanWithColor(text, color) { + return `${text}`; +} diff --git a/js/tools.js b/js/lib/messages.js similarity index 61% rename from js/tools.js rename to js/lib/messages.js index 4b24f73e..495bb3b9 100644 --- a/js/tools.js +++ b/js/lib/messages.js @@ -11,3 +11,13 @@ export function errorMsg(msg) { msgDiv.style.color = "red"; msgDiv.innerHTML = "

ERROR: " + msg + "

"; } + +export function emptyViewMessage() { + const msgDiv = document.getElementById("empty-view"); + msgDiv.style.display = "flex"; +} + +export function hideEmptyViewMessage() { + const msgDiv = document.getElementById("empty-view"); + msgDiv.style.display = "none"; +} diff --git a/js/lib/parseCharge.js b/js/lib/parseCharge.js new file mode 100644 index 00000000..306a2039 --- /dev/null +++ b/js/lib/parseCharge.js @@ -0,0 +1,18 @@ +export function parseCharge(charge) { + if (Math.abs(charge) < 1.0 && charge != 0) { + if (Math.round(charge * 1000) === 667) { + return "q = 2/3 e"; + } + if (Math.round(charge * 1000) === -667) { + return "q = -2/3 e"; + } + if (Math.round(charge * 1000) === 333) { + return "q = 1/3 e"; + } + if (Math.round(charge * 1000) === -333) { + return "q = -1/3 e"; + } + } else { + return "q = " + charge + " e"; + } +} diff --git a/js/main.js b/js/main.js index 56926690..7c6f05d7 100644 --- a/js/main.js +++ b/js/main.js @@ -1,10 +1,8 @@ -import { errorMsg } from "./tools.js"; -import { PdgToggle } from "./menu/show-pdg.js"; -import { drawAll } from "./draw.js"; -import { getWidthFilterContent } from "./menu/filter/filter.js"; -import { mouseDown, mouseUp, mouseOut, mouseMove, onScroll } from "./events.js"; -import { showEventSwitcher, loadSelectedEvent } from "./menu/event-number.js"; -import { renderEvent } from "./menu/event-number.js"; +import { errorMsg } from "./lib/messages.js"; +import { renderEvent } from "./event-number.js"; +import { setView, getView } from "./views/views.js"; +import { views } from "./views/views-dictionary.js"; +import { selectViewInformation } from "./information.js"; const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); @@ -14,43 +12,44 @@ canvas.height = window.innerHeight; const jsonData = {}; -const dragTools = { - draggedObject: null, - isDragging: false, - prevMouseX: 0, - prevMouseY: 0, +const selectedObjectTypes = { + types: [ + "edm4hep::MCParticle", + "edm4hep::ReconstructedParticle", + "edm4hep::MCRecoParticleAssociation", + "edm4hep::MCRecoTrackParticleAssociation", + "edm4hep::MCRecoClusterParticleAssociation", + "edm4hep::Cluster", + "edm4hep::Track", + "edm4hep::Vertex", + "edm4hep::ParticleID", + ], }; -const loadedObjects = {}; +function hideInputModal() { + const modal = document.getElementById("input-modal"); -const currentObjects = {}; + modal.style.display = "none"; +} -const visibleObjects = {}; +function showEventSwitcher() { + const eventSwitcher = document.getElementById("event-switcher"); -const selectedObjectTypes = { - types: ["edm4hep::MCParticle"], -}; + eventSwitcher.style.display = "flex"; +} -canvas.onmousedown = (event) => { - mouseDown(event, visibleObjects, dragTools); -}; -canvas.onmouseup = (event) => { - mouseUp(event, currentObjects, dragTools); -}; -canvas.onmouseout = (event) => { - mouseOut(event, dragTools); -}; -canvas.onmousemove = (event) => { - mouseMove(event, visibleObjects, dragTools); -}; -window.onscroll = () => { - onScroll(currentObjects, visibleObjects); -}; +function showViewsMenu() { + const viewsMenu = document.getElementById("views"); + const aboutButton = document.getElementById("information-button"); -function hideInputModal() { - const modal = document.getElementById("input-modal"); + viewsMenu.style.display = "flex"; + aboutButton.style.display = "block"; +} - modal.style.display = "none"; +function hideDeploySwitch() { + const deploySwitch = document.getElementById("switch-deploy"); + + deploySwitch.style.display = "none"; } document.getElementById("input-file").addEventListener("change", (event) => { @@ -97,6 +96,27 @@ document.getElementById("input-file").addEventListener("change", (event) => { eventSelectorMenu.style.display = "none"; }); }); + + const availableViews = document.getElementById("available-views"); + availableViews.replaceChildren(); + const buttons = []; + for (const key in views) { + const button = document.createElement("button"); + button.appendChild(document.createTextNode(key)); + button.className = "view-button"; + button.onclick = (event) => { + event.preventDefault(); + setView(key); + for (const otherButton of buttons) { + if (otherButton !== button) { + otherButton.style.backgroundColor = "#f1f1f1"; + } + } + button.style.backgroundColor = "#c5c5c5"; + }; + buttons.push(button); + availableViews.appendChild(button); + } }); reader.readAsText(file); break; @@ -113,28 +133,19 @@ document return; } + if (getView() === undefined) { + errorMsg("No view selected!"); + return; + } + const eventNum = document.getElementById("event-number").value; hideInputModal(); - showEventSwitcher(eventNum); - loadSelectedEvent(); - - const width = getWidthFilterContent(); - filter.style.width = width; - const pdgToggle = new PdgToggle("show-pdg"); - pdgToggle.init(() => { - pdgToggle.toggle(currentObjects, () => { - drawAll(ctx, currentObjects); - }); - }); + showEventSwitcher(); + showViewsMenu(); + renderEvent(eventNum); + selectViewInformation(); + hideDeploySwitch(); }); -export { - canvas, - ctx, - loadedObjects, - currentObjects, - visibleObjects, - jsonData, - selectedObjectTypes, -}; +export { canvas, ctx, jsonData, selectedObjectTypes }; diff --git a/js/menu/event-number.js b/js/menu/event-number.js deleted file mode 100644 index 273a817e..00000000 --- a/js/menu/event-number.js +++ /dev/null @@ -1,135 +0,0 @@ -import { loadObjects } from "../types/load.js"; -import { copyObject } from "../lib/copy.js"; -import { - loadedObjects, - currentObjects, - visibleObjects, - canvas, - ctx, -} from "../main.js"; -import { getVisible } from "../events.js"; -import { - bits, - genStatus, - renderRangeParameters, - parametersRange, - renderGenSim, -} from "./filter/filter.js"; -import { drawAll } from "../draw.js"; -import { objectTypes } from "../types/objects.js"; -import { jsonData, selectedObjectTypes } from "../main.js"; - -const filters = document.getElementById("filters"); -const eventSwitcher = document.getElementById("event-switcher"); -const eventNumber = document.getElementById("selected-event"); -const previousEvent = document.getElementById("previous-event"); -const nextEvent = document.getElementById("next-event"); -const manipulationTools = document.getElementsByClassName("manipulation-tool"); - -let currentEvent; - -const scrollLocation = {}; - -function updateEventNumber(newEventNumber) { - if (eventNumber.firstChild) { - eventNumber.removeChild(eventNumber.firstChild); - } - eventNumber.appendChild(document.createTextNode(`Event: ${newEventNumber}`)); -} - -function start(currentObjects, visibleObjects) { - for (const [key, value] of Object.entries(currentObjects)) { - const classType = objectTypes[key]; - const collection = value.collection; - classType.setup(collection, canvas); - } - - drawAll(ctx, currentObjects); - - getVisible(currentObjects, visibleObjects); -} - -export function renderEvent(eventNumber) { - const data = jsonData.data[`Event ${eventNumber}`]; - - scrollLocation[currentEvent] = { - x: window.scrollX, - y: window.scrollY, - }; - - if (data === undefined) { - return; - } else { - currentEvent = eventNumber; - loadSelectedEvent(jsonData, selectedObjectTypes.types, eventNumber); - updateEventNumber(eventNumber); - } -} - -export function showEventSwitcher(initialValue) { - eventSwitcher.style.display = "flex"; - updateEventNumber(initialValue); - currentEvent = initialValue; -} - -export function loadSelectedEvent() { - const objects = loadObjects( - jsonData.data, - currentEvent, - selectedObjectTypes.types - ); - - copyObject(objects, loadedObjects); - copyObject(objects, currentObjects); - - const length = Object.values(loadedObjects) - .map((obj) => obj.collection.length) - .reduce((a, b) => a + b, 0); - - if (length === 0) { - errorMsg("Event does not contain any objects!"); - return; - } - - start(currentObjects, visibleObjects); - if (scrollLocation[currentEvent] === undefined) { - scrollLocation[currentEvent] = { - x: (canvas.width - window.innerWidth) / 2, - y: 0, - }; - } - - window.scroll(scrollLocation[currentEvent].x, scrollLocation[currentEvent].y); - - for (const tool of manipulationTools) { - tool.style.display = "flex"; - } - - const mcObjects = loadedObjects["edm4hep::MCParticle"].collection; - genStatus.reset(); - mcObjects.forEach((mcObject) => { - genStatus.add(mcObject.generatorStatus); - }); - genStatus.setCheckBoxes(); - filters.replaceChildren(); - - renderRangeParameters(parametersRange); - renderGenSim(bits, genStatus); -} - -previousEvent.addEventListener("click", () => { - const newEventNum = `${parseInt(currentEvent) - 1}`; - renderEvent(newEventNum); -}); -nextEvent.addEventListener("click", () => { - const newEventNum = `${parseInt(currentEvent) + 1}`; - renderEvent(newEventNum); -}); -eventNumber.addEventListener("click", () => { - const eventSelectorMenu = document.getElementById("event-selector-menu"); - if (eventSelectorMenu.style.display === "flex") { - eventSelectorMenu.style.display = "none"; - } else { - eventSelectorMenu.style.display = "flex"; - } -}); diff --git a/js/menu/filter/filter.js b/js/menu/filter/filter.js index a26e577c..abcc5807 100644 --- a/js/menu/filter/filter.js +++ b/js/menu/filter/filter.js @@ -1,16 +1,11 @@ import { drawAll } from "../../draw.js"; -import { - ctx, - loadedObjects, - currentObjects, - visibleObjects, -} from "../../main.js"; import { CheckboxBuilder, BitFieldBuilder } from "./builders.js"; import { Range, Checkbox, buildCriteriaFunction } from "./parameters.js"; import { reconnect } from "./reconnect.js"; import { getVisible } from "../../events.js"; import { units } from "../../types/units.js"; import { copyObject } from "../../lib/copy.js"; +import { SimStatusBitFieldDisplayValues } from "../../../mappings/sim-status.js"; const filterButton = document.getElementById("filter-button"); const openFilter = document.getElementById("open-filter"); @@ -22,20 +17,6 @@ const reset = document.getElementById("filter-reset"); let active = false; -filterButton.addEventListener("click", () => { - active = !active; - - if (active) { - openFilter.style.display = "none"; - closeFilter.style.display = "block"; - filterContent.style.display = "flex"; - } else { - openFilter.style.display = "block"; - closeFilter.style.display = "none"; - filterContent.style.display = "none"; - } -}); - export function renderRangeParameters(rangeParameters) { const rangeFilters = document.createElement("div"); rangeFilters.id = "range-filters"; @@ -80,17 +61,6 @@ let parametersRange = units.sort((a, b) => parametersRange = parametersRange.map((parameter) => new Range(parameter)); -const SimStatusBitFieldDisplayValues = { - 23: "Overlay", - 24: "Stopped", - 25: "LeftDetector", - 26: "DecayedInCalorimeter", - 27: "DecayedInTracker", - 28: "VertexIsNotEndpointOfParent", - 29: "Backscatter", - 30: "CreatedInSimulation", -}; - const bits = new BitFieldBuilder( "simulatorStatus", "Simulation status", @@ -115,7 +85,7 @@ function applyFilter(loadedObjects, currentObjects, visibleObjects) { copyObject(filteredObjects, currentObjects); - drawAll(ctx, currentObjects); + drawAll(currentObjects); getVisible(currentObjects, visibleObjects); } @@ -123,7 +93,7 @@ function applyFilter(loadedObjects, currentObjects, visibleObjects) { function removeFilter(loadedObjects, currentObjects, visibleObjects) { copyObject(loadedObjects, currentObjects); - drawAll(ctx, currentObjects); + drawAll(currentObjects); getVisible(currentObjects, visibleObjects); @@ -133,18 +103,34 @@ function removeFilter(loadedObjects, currentObjects, visibleObjects) { renderGenSim(bits, genStatus); } -apply.addEventListener("click", () => - applyFilter(loadedObjects, currentObjects, visibleObjects) -); +export function start(loadedObjects, currentObjects, visibleObjects) { + filterButton.addEventListener("click", () => { + active = !active; + + if (active) { + openFilter.style.display = "none"; + closeFilter.style.display = "block"; + filterContent.style.display = "flex"; + } else { + openFilter.style.display = "block"; + closeFilter.style.display = "none"; + filterContent.style.display = "none"; + } + }); -document.addEventListener("keydown", (event) => { - if (event.key === "Enter" && active) { - applyFilter(loadedObjects, currentObjects, visibleObjects); - } -}); + apply.addEventListener("click", () => + applyFilter(loadedObjects, currentObjects, visibleObjects) + ); -reset.addEventListener("click", () => - removeFilter(loadedObjects, currentObjects, visibleObjects) -); + document.addEventListener("keydown", (event) => { + if (event.key === "Enter" && active) { + applyFilter(loadedObjects, currentObjects, visibleObjects); + } + }); + + reset.addEventListener("click", () => + removeFilter(loadedObjects, currentObjects, visibleObjects) + ); +} export { bits, genStatus, parametersRange }; diff --git a/js/menu/filter/reconnect.js b/js/menu/filter/reconnect.js index e33e4eee..28ccac22 100644 --- a/js/menu/filter/reconnect.js +++ b/js/menu/filter/reconnect.js @@ -6,11 +6,11 @@ export function reconnect(criteriaFunction, loadedObjects) { emptyCopyObject(loadedObjects, filteredObjects); - for (const [key, value] of Object.entries(loadedObjects)) { - const filterFunction = objectTypes[key].filter; + const filterFunction = objectTypes["edm4hep::MCParticle"].filter; - filterFunction(value, filteredObjects, criteriaFunction); - } + const mcParticles = loadedObjects.datatypes["edm4hep::MCParticle"]; + + filterFunction(mcParticles, filteredObjects.datatypes, criteriaFunction); return filteredObjects; } diff --git a/js/menu/show-pdg.js b/js/menu/show-pdg.js index a5333e59..131bbd2e 100644 --- a/js/menu/show-pdg.js +++ b/js/menu/show-pdg.js @@ -1,5 +1,4 @@ import { Toggle } from "./toggle.js"; -import { selectedObjectTypes } from "../main.js"; export class PdgToggle extends Toggle { constructor(id) { @@ -7,23 +6,17 @@ export class PdgToggle extends Toggle { } toggle(currentObjects, redraw) { - const validObjects = selectedObjectTypes.types; - + const collection = + currentObjects.datatypes["edm4hep::MCParticle"].collection; if (this.isSliderActive) { - for (const objectType of validObjects) { - const collection = currentObjects[objectType].collection; - if (collection[0].PDG === undefined) return; - for (const object of collection) { - object.updateTexImg(`${object.PDG}`); - } + if (collection[0].PDG === undefined) return; + for (const object of collection) { + object.updateTexImg(`${object.PDG}`); } } else { - for (const objectType of validObjects) { - const collection = currentObjects[objectType].collection; - if (collection[0].PDG === undefined) return; - for (const object of collection) { - object.updateTexImg(`${object.name}`); - } + if (collection[0].PDG === undefined) return; + for (const object of collection) { + object.updateTexImg(`${object.name}`); } } diff --git a/js/switch-deploy.js b/js/switch-deploy.js new file mode 100644 index 00000000..24fb9009 --- /dev/null +++ b/js/switch-deploy.js @@ -0,0 +1,18 @@ +const button = document.getElementById("switch-deploy-button"); + +button.addEventListener("click", () => { + const currentUrl = window.location.href; + + if (currentUrl.includes("/release")) { + window.location.href = currentUrl.replace("/release", "/main"); + } else { + window.location.href = "https://key4hep.github.io/eede/release/index.html"; + } +}); + +const url = window.location.href; +if (url.includes("/release")) { + button.innerText = "Develop"; +} else { + button.innerText = "Release"; +} diff --git a/js/types/dynamic.js b/js/types/dynamic.js deleted file mode 100644 index fa250270..00000000 --- a/js/types/dynamic.js +++ /dev/null @@ -1,56 +0,0 @@ -import { linkTypes } from "./links.js"; - -export function loadMembers(object, data, membersToLoad) { - for (const member of membersToLoad) { - const name = member.name; - if (data[name] === undefined) continue; // load up to date data - object[name] = data[name]; - } -} - -export function loadOneToOneRelations( - object, - data, - relationsToLoad = [], - oneToOne, - objects -) { - object.oneToOneRelations = {}; - for (const relation of relationsToLoad) { - const name = relation.name; - const relationData = data[name]; - if (relationData === undefined) continue; - - const toObject = objects[relationData.index]; - const linkType = linkTypes[name]; - const link = new linkType(object, toObject); - - oneToOne[name].push(link); - object.oneToOneRelations[name] = link; - } -} - -export function loadOneToManyRelations( - object, - data, - relationsToLoad = [], - oneToMany, - objects -) { - object.oneToManyRelations = {}; - for (const relation of relationsToLoad) { - const name = relation.name; - const relationData = data[name]; - - if (relationData === undefined) continue; - object.oneToManyRelations[name] = []; - - for (const relationElement of relationData) { - const toObject = objects[relationElement.index]; - const linkType = linkTypes[name]; - const link = new linkType(object, toObject); - oneToMany[name].push(link); - object.oneToManyRelations[name].push(link); - } - } -} diff --git a/js/types/edmobject.js b/js/types/edmobject.js deleted file mode 100644 index 52332c34..00000000 --- a/js/types/edmobject.js +++ /dev/null @@ -1,8 +0,0 @@ -export class EDMObject { - constructor(id) { - this.id = id; - } - - draw() {} - // more methods common to all particles -} diff --git a/js/types/links.js b/js/types/links.js index 5f2914d1..5a7c7391 100644 --- a/js/types/links.js +++ b/js/types/links.js @@ -1,15 +1,18 @@ -export const colors = { - "daughters": "#00AA00", +import { drawBezierLink, drawStraightLink } from "../lib/graphic-primitives.js"; + +const colors = { "parents": "#AA0000", - // more if needed + "daughters": "#00AA00", + "mcreco": "#0000AA", + "tracks": "#AAAA00", + "clusters": "#00AAAA", + "particles": "#AA00AA", + "mcclusters": "#D8F1A0", + "mctracks": "#fe5e41", + "vertex": "#593746", }; -export function generateRandomColor() { - return "#" + ((0xffffff * Math.random()) << 0).toString(16).padStart(6, "0"); -} - export class Link { - // we may create a specific class for each type if needed constructor(from, to) { this.from = from; this.to = to; @@ -18,64 +21,7 @@ export class Link { } draw(ctx) { - const boxFrom = this.from; - const boxTo = this.to; - - const fromX = boxFrom.x + boxFrom.width / 2; - const fromY = boxFrom.y + boxFrom.height; - const toX = boxTo.x + boxTo.width / 2; - const toY = boxTo.y; - - if (toY > fromY) { - var cpFromY = (toY - fromY) / 2 + fromY; - var cpToY = cpFromY; - } else { - cpFromY = (fromY - toY) / 2 + fromY; - cpToY = toY - (fromY - toY) / 2; - } - - if (toX > fromX) { - var cpFromX = (toX - fromX) / 4 + fromX; - var cpToX = (3 * (toX - fromX)) / 4 + fromX; - } else { - cpFromX = (3 * (fromX - toX)) / 4 + toX; - cpToX = (fromX - toX) / 4 + toX; - } - - /* - drawCross(ctx, fromX, fromY, "blue"); - drawCross(ctx, toX, toY, "green"); - drawCross(ctx, cpFromX, cpFromY, "yellow"); - drawLine(ctx, fromX, fromY, cpFromX, cpFromY, "yellow") - drawCross(ctx, cpToX, cpToY, "orange"); - drawLine(ctx, toX, toY, cpToX, cpToY, "orange") - */ - - ctx.save(); - ctx.strokeStyle = this.color; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(fromX + this.xShift, fromY); - ctx.bezierCurveTo( - cpFromX + this.xShift, - cpFromY, - cpToX + this.xShift, - cpToY, - toX + this.xShift, - toY - ); - ctx.stroke(); - ctx.restore(); - - /* - ctx.save(); - ctx.font = "14px sans-serif"; - ctx.fillStyle = this.color; - const idText = "ID: " + this.id; - ctx.fillText(idText, - cpToX, cpToY); - ctx.restore(); - */ + drawBezierLink(ctx, this); } isVisible(x, y, width, height) { @@ -108,7 +54,7 @@ export class Link { } } -export class ParentLink extends Link { +class ParentLink extends Link { constructor(from, to) { super(to, from); this.color = colors["parents"]; @@ -118,7 +64,7 @@ export class ParentLink extends Link { } } -export class DaughterLink extends Link { +class DaughterLink extends Link { constructor(from, to) { super(from, to); this.color = colors["daughters"]; @@ -128,10 +74,80 @@ export class DaughterLink extends Link { } } +class MCRecoParticleAssociation extends Link { + constructor(from, to, weight) { + super(from, to); + this.color = colors["mcreco"]; + this.weight = weight; + } + + draw(ctx) { + drawStraightLink(ctx, this); + } +} + +class Particles extends Link { + constructor(from, to) { + super(from, to); + this.color = colors["particles"]; + } +} + +class Clusters extends Link { + constructor(from, to) { + super(from, to); + this.color = colors["clusters"]; + } +} + +class Tracks extends Link { + constructor(from, to) { + super(from, to); + this.color = colors["tracks"]; + } +} + +class Vertex extends Link { + constructor(from, to) { + super(from, to); + this.color = colors["vertex"]; + } +} + +class MCRecoTrackParticleAssociation extends Link { + constructor(from, to, weight) { + super(from, to); + this.color = colors["mctracks"]; + this.weight = weight; + } + + draw(ctx) { + drawStraightLink(ctx, this); + } +} + +class MCRecoClusterParticleAssociation extends Link { + constructor(from, to, weight) { + super(from, to); + this.color = colors["mcclusters"]; + this.weight = weight; + } + + draw(ctx) { + drawStraightLink(ctx, this); + } +} + export const linkTypes = { "parents": ParentLink, "daughters": DaughterLink, - "trackerHits": Link, - "startVertex": Link, - "particles": Link, + "edm4hep::MCRecoParticleAssociation": MCRecoParticleAssociation, + "edm4hep::MCRecoClusterParticleAssociation": MCRecoClusterParticleAssociation, + "edm4hep::MCRecoTrackParticleAssociation": MCRecoTrackParticleAssociation, + "clusters": Clusters, + "tracks": Tracks, + "particles": Particles, + "particle": Particles, + "startVertex": Vertex, + "associatedParticle": Vertex, }; diff --git a/js/types/load.js b/js/types/load.js index 1623a67d..201602e4 100644 --- a/js/types/load.js +++ b/js/types/load.js @@ -1,87 +1,225 @@ import { objectTypes } from "./objects.js"; import { datatypes } from "../../output/datatypes.js"; -import { - loadMembers, - loadOneToOneRelations, - loadOneToManyRelations, -} from "./dynamic.js"; -import { generateRandomColor, colors } from "./links.js"; - -export function loadObjectType(collection, datatype, type) { - const objects = []; - let oneToOne = {}; - if (datatype.oneToOneRelations) - datatype.oneToOneRelations.forEach((relation) => { - oneToOne[relation.name] = []; - if (colors[relation.name] === undefined) { - colors[relation.name] = generateRandomColor(); - } - }); - - let oneToMany = {}; - if (datatype.oneToManyRelations) - datatype.oneToManyRelations.forEach((relation) => { - oneToMany[relation.name] = []; - if (colors[relation.name] === undefined) { - colors[relation.name] = generateRandomColor(); - } - }); +import { linkTypes } from "./links.js"; - for (const [index, particle] of collection.entries()) { - const newObject = new type(index); +function loadMembers(object, data, membersToLoad) { + for (const member of membersToLoad) { + const name = member.name; + if (data[name] === undefined) continue; // load up to date data + object[name] = data[name]; + } +} - loadMembers(newObject, particle, datatype.members); +function loadEmptyRelations(object, relations) { + const oneToOneRelations = relations.oneToOneRelations ?? []; + if (oneToOneRelations) object.oneToOneRelations = {}; - objects.push(newObject); + const oneToManyRelations = relations.oneToManyRelations ?? []; + if (oneToManyRelations) object.oneToManyRelations = {}; + for (const { name } of oneToManyRelations) { + object.oneToManyRelations[name] = []; } +} + +export function loadPlainObject( + collection, + datatype, + collectionId, + collectionName +) { + const objects = []; for (const [index, particle] of collection.entries()) { - const newObject = objects[index]; - - loadOneToOneRelations( - newObject, - particle, - datatype.oneToOneRelations, - oneToOne, - objects - ); - - loadOneToManyRelations( - newObject, - particle, - datatype.oneToManyRelations, - oneToMany, - objects - ); + const newObject = new objectTypes[datatype](); + newObject.index = index; + newObject.collectionId = collectionId; + newObject.collectionName = collectionName; + + loadMembers(newObject, particle, datatypes[datatype].members); + loadEmptyRelations(newObject, datatypes[datatype]); + + objects.push(newObject); } - return [objects, oneToOne, oneToMany]; + return objects; } export function loadObjects(jsonData, event, objectsToLoad) { const eventData = jsonData["Event " + event]; - const objects = {}; + const datatypesToLoad = objectsToLoad.filter( + (object) => !object.includes("Association") + ); + const associations = objectsToLoad.filter((object) => + object.includes("Association") + ); + + const objects = { + "datatypes": {}, + "associations": {}, + }; + + datatypesToLoad.forEach((datatype) => { + objects.datatypes[datatype] = { + collection: [], + oneToMany: {}, + oneToOne: {}, + }; + }); + + associations.forEach((association) => { + objects.associations[association] = []; + }); + + for (const datatype of datatypesToLoad) { + Object.entries(eventData).forEach(([key, element]) => { + const collectionName = `${datatype}Collection`; + if (element.collType === collectionName) { + const collection = element.collection; + const collectionId = element.collID; + const objectCollection = loadPlainObject( + collection, + datatype, + collectionId, + key + ); + objects.datatypes[datatype].collection.push(...objectCollection); + } + }); + } - for (const type of objectsToLoad) { - let collectionType = Object.values(eventData).filter( - (element) => element.collType === `${type}Collection` - ); + for (const datatype of datatypesToLoad) { + const oneToOneRelations = datatypes?.[datatype]?.oneToOneRelations ?? []; + oneToOneRelations.forEach((relation) => { + objects.datatypes[datatype].oneToOne[relation.name] = []; + }); - collectionType = collectionType.map((coll) => coll.collection); - collectionType = collectionType.flat(); + const oneToManyRelations = datatypes?.[datatype]?.oneToManyRelations ?? []; + oneToManyRelations.forEach((relation) => { + objects.datatypes[datatype].oneToMany[relation.name] = []; + }); - const [loadedCollection, oneToOne, oneToMany] = loadObjectType( - collectionType, - datatypes[type], - objectTypes[type] - ); + Object.values(eventData).forEach((element) => { + const collectionName = `${datatype}Collection`; + if (element.collType === collectionName) { + const fromCollection = objects.datatypes[datatype].collection.filter( + (object) => object.collectionId === element.collID + ); + + // load One To One Relations + for (const { type, name } of oneToOneRelations) { + if (objects.datatypes?.[type] === undefined) continue; + const oneToOneRelationData = element.collection + .map((object) => object[name]) + .filter((object) => object !== undefined); + + if (oneToOneRelationData.length === 0) continue; + + const toCollectionID = + oneToOneRelationData.find( + (relation) => relation.collectionID !== undefined + ).collectionID ?? NaN; + + const toCollection = objects.datatypes[type].collection.filter( + (object) => object.collectionId === toCollectionID + ); + + if (toCollection) { + for (const [index, relation] of oneToOneRelationData.entries()) { + if (relation.index < 0) continue; + const fromObject = fromCollection[index]; + const toObject = toCollection[relation.index]; + + const linkType = linkTypes[name]; + const link = new linkType(fromObject, toObject); + fromObject.oneToOneRelations[name] = link; + objects.datatypes[datatype].oneToOne[name].push(link); + } + } + } + + // load One To Many Relations + for (const { type, name } of oneToManyRelations) { + if (objects.datatypes?.[type] === undefined) continue; + const oneToManyRelationData = element.collection + .map((object) => object[name]) + .filter((object) => object !== undefined); + + if (oneToManyRelationData.length === 0) continue; + + const toCollectionID = + oneToManyRelationData.find( + (relation) => relation?.[0]?.collectionID !== undefined + )?.[0]?.collectionID ?? NaN; + const toCollection = objects.datatypes[type].collection.filter( + (object) => object.collectionId === toCollectionID + ); + + if (toCollection) { + for (const [index, relation] of oneToManyRelationData.entries()) { + if (relation.length === 0) continue; + const fromObject = fromCollection[index]; + for (const relationElement of relation) { + if (relationElement.index < 0) continue; + const toObject = toCollection[relationElement.index]; + + const linkType = linkTypes[name]; + const link = new linkType(fromObject, toObject); + fromObject.oneToManyRelations[name].push(link); + objects.datatypes[datatype].oneToMany[name].push(link); + } + } + } + } + } + }); + } - objects[type] = { - collection: loadedCollection, - oneToMany: oneToMany, - oneToOne: oneToOne, - }; + // Currently, all associations are one-to-one + for (const association of associations) { + Object.values(eventData).forEach((element) => { + const collectionName = `${association}Collection`; + if (element.collType === collectionName) { + const collection = element.collection; + if (collection.length === 0) return; + + const { type: fromType, name: fromName } = + datatypes[association].oneToOneRelations[0]; + const { type: toType, name: toName } = + datatypes[association].oneToOneRelations[1]; + + const fromCollectionID = collection.find( + (relation) => relation[fromName].collectionID !== undefined + )[fromName].collectionID; + const toCollectionID = collection.find( + (relation) => relation[toName].collectionID !== undefined + )[toName].collectionID; + + const fromCollection = objects.datatypes[fromType].collection.filter( + (object) => object.collectionId === fromCollectionID + ); + const toCollection = objects.datatypes[toType].collection.filter( + (object) => object.collectionId === toCollectionID + ); + + for (const associationElement of collection) { + const fromObject = fromCollection[associationElement[fromName].index]; + const toObject = toCollection[associationElement[toName].index]; + + const linkType = linkTypes[association]; + const link = new linkType( + fromObject, + toObject, + associationElement.weight + ); + objects.associations[association].push(link); + fromObject.associations = {}; + fromObject.associations[association] = link; + toObject.associations = {}; + toObject.associations[association] = link; + } + } + }); } return objects; diff --git a/js/types/objects.js b/js/types/objects.js index d0fe160f..5e47ed73 100644 --- a/js/types/objects.js +++ b/js/types/objects.js @@ -1,53 +1,77 @@ -import { EDMObject } from "./edmobject.js"; -import { drawTex, drawRoundedRect } from "../graphic-primitives.js"; +import { + drawTex, + drawRoundedRect, + drawTextLines, + drawObjectHeader, + drawObjectInfoTip, +} from "../lib/graphic-primitives.js"; import { getName } from "../lib/getName.js"; import { linkTypes } from "./links.js"; +import { parseCharge } from "../lib/parseCharge.js"; +import { getSimStatusDisplayValuesFromBit } from "../../mappings/sim-status.js"; -export class Cluster extends EDMObject { - constructor(id) { - super(id); +const TOP_MARGIN = 45; + +class EDMObject { + constructor() { + this.x = NaN; + this.y = NaN; + this.index = NaN; + this.collectionId = NaN; + this.width = 120; + this.height = 260; + this.lineColor = "black"; + this.lineWidth = 2; + this.color = "white"; } -} -export class ParticleID extends EDMObject { - constructor(id) { - super(id); + draw(ctx) { + drawRoundedRect( + ctx, + this.x, + this.y, + this.width, + this.height, + this.color, + this.radius + ); + drawObjectHeader(ctx, this); } -} -export class ReconstructedParticle extends EDMObject { - constructor(id) { - super(id); + isHere(mouseX, mouseY) { + return ( + mouseX > this.x && + mouseX < this.x + this.width && + mouseY > this.y && + mouseY < this.y + this.height + ); } -} -export class Vertex extends EDMObject { - constructor(id) { - super(id); + isVisible(x, y, width, height) { + return ( + x + width > this.x && + x < this.x + this.width && + y + height > this.y && + y < this.y + this.height + ); } -} -export class Track extends EDMObject { - constructor(id) { - super(id); + showObjectTip(ctx) { + const x = this.x; + const y = this.y - 10; + const collectionName = "Collection: " + this.collectionName; + drawObjectInfoTip(ctx, x, y, collectionName); } } export class MCParticle extends EDMObject { - constructor(id) { - super(id); - - // Appearance - this.x = 0; - this.y = 0; - this.width = 120; - this.height = 240; - this.lineColor = "black"; - this.lineWidth = 2; - this.color = "white"; + constructor() { + super(); this.row = -1; - this.texImg = null; + this.color = "#dff6ff"; + this.radius = 15; + this.height = 270; } updateTexImg(text) { @@ -63,17 +87,15 @@ export class MCParticle extends EDMObject { } draw(ctx) { - // drawCross(ctx, this.x, this.y); - const boxCenterX = this.x + this.width / 2; - drawRoundedRect(ctx, this.x, this.y, this.width, this.height, "#f5f5f5"); + super.draw(ctx); if (this.texImg.complete) { drawTex( ctx, boxCenterX, - this.y + this.height * 0.4, + this.y + TOP_MARGIN + 80, this.texImg, this.width ); @@ -82,81 +104,54 @@ export class MCParticle extends EDMObject { drawTex( ctx, boxCenterX, - this.y + this.height * 0.4, + this.y + TOP_MARGIN + 80, this.texImg, this.width ); }; } - const topY = this.y + 20; + const topY = this.y + TOP_MARGIN; const topLines = []; - topLines.push("ID: " + this.id); + topLines.push("ID: " + this.index); topLines.push("Gen. stat.: " + this.generatorStatus); - topLines.push("Sim. stat.: " + this.simulatorStatus); - - const bottomY = this.y + this.height * 0.6; + const simulatorStatus = getSimStatusDisplayValuesFromBit( + this.simulatorStatus + ); + const simulatorStatusFirstLetter = simulatorStatus + .map((s) => s[0]) + .join(""); + const simulatorStatusString = + simulatorStatusFirstLetter !== "" + ? simulatorStatusFirstLetter + : this.simulatorStatus; + topLines.push("Sim. stat.: " + simulatorStatusString); + + const bottomY = this.y + this.height * 0.65; const bottomLines = []; bottomLines.push("p = " + this.momentum + " GeV"); bottomLines.push("d = " + this.vertex + " mm"); bottomLines.push("t = " + this.time + " ns"); bottomLines.push("m = " + this.mass + " GeV"); - if (Math.abs(this.charge) < 1.0 && this.charge != 0) { - if (Math.round(this.charge * 1000) === 667) { - bottomLines.push("q = 2/3 e"); - } - if (Math.round(this.charge * 1000) === -667) { - bottomLines.push("q = -2/3 e"); - } - if (Math.round(this.charge * 1000) === 333) { - bottomLines.push("q = 1/3 e"); - } - if (Math.round(this.charge * 1000) === -333) { - bottomLines.push("q = -1/3 e"); - } - } else { - bottomLines.push("q = " + this.charge + " e"); - } + bottomLines.push(parseCharge(this.charge)); - ctx.save(); - ctx.font = "16px sans-serif"; - for (const [i, lineText] of topLines.entries()) { - ctx.fillText( - lineText, - boxCenterX - ctx.measureText(lineText).width / 2, - topY + i * 23 - ); - } + drawTextLines(ctx, topLines, boxCenterX, topY, 23); - for (const [i, lineText] of bottomLines.entries()) { - ctx.fillText( - lineText, - boxCenterX - ctx.measureText(lineText).width / 2, - bottomY + i * 22 - ); - } - ctx.restore(); + drawTextLines(ctx, bottomLines, boxCenterX, bottomY, 22); } - isHere(mouseX, mouseY) { - return ( - mouseX > this.x && - mouseX < this.x + this.width && - mouseY > this.y && - mouseY < this.y + this.height + showObjectTip(ctx) { + const x = this.x; + const y = this.y - 10; + const collectionName = "Collection: " + this.collectionName; + const simulatorStatus = getSimStatusDisplayValuesFromBit( + this.simulatorStatus ); - } - isVisible(x, y, width, height) { - return ( - x + width > this.x && - x < this.x + this.width && - y + height > this.y && - y < this.y + this.height - ); + drawObjectInfoTip(ctx, x, y, collectionName, ...simulatorStatus); } - static setup(mcCollection, canvas) { + static setup(mcCollection) { for (const mcParticle of mcCollection) { const parentLength = mcParticle.oneToManyRelations["parents"].length; const daughterLength = mcParticle.oneToManyRelations["daughters"].length; @@ -189,95 +184,6 @@ export class MCParticle extends EDMObject { mcParticle.time = Math.round(mcParticle.time * 100) / 100; mcParticle.mass = Math.round(mcParticle.mass * 100) / 100; } - - const getMaxRow = (parentLinks) => { - let maxRow = -1; - for (const parentLink of parentLinks) { - const parent = parentLink.from; - if (parent.row === -1) { - return -1; - } - - if (parent.row > maxRow) { - maxRow = parent.row; - } - } - - return maxRow; - }; - - let repeat = true; - while (repeat) { - repeat = false; - for (const mcParticle of mcCollection) { - if (mcParticle.row >= 0) { - continue; - } - const parentRow = getMaxRow(mcParticle.oneToManyRelations["parents"]); - if (parentRow >= 0) { - mcParticle.row = parentRow + 1; - } else { - repeat = true; - } - } - } - - const rows = mcCollection.map((obj) => { - return obj.row; - }); - const maxRow = Math.max(...rows); - - // Order infoBoxes into rows - const mcRows = []; - for (let i = 0; i <= maxRow; i++) { - mcRows.push([]); - } - for (const box of mcCollection) { - mcRows[box.row].push(box); - } - const rowWidths = mcRows.map((obj) => { - return obj.length; - }); - const maxRowWidth = Math.max(...rowWidths); - - const boxWidth = mcCollection[0].width; - const boxHeight = mcCollection[0].height; - const horizontalGap = boxWidth * 0.4; - const verticalGap = boxHeight * 0.3; - - canvas.width = - boxWidth * (maxRowWidth + 1) + horizontalGap * (maxRowWidth + 1); - canvas.height = boxHeight * (maxRow + 1) + verticalGap * (maxRow + 2); - - for (const [i, row] of mcRows.entries()) { - for (const [j, box] of row.entries()) { - if (row.length % 2 === 0) { - const distanceFromCenter = j - row.length / 2; - if (distanceFromCenter < 0) { - box.x = - canvas.width / 2 - - boxWidth - - horizontalGap / 2 + - (distanceFromCenter + 1) * boxWidth + - (distanceFromCenter + 1) * horizontalGap; - } else { - box.x = - canvas.width / 2 + - horizontalGap / 2 + - distanceFromCenter * boxWidth + - distanceFromCenter * horizontalGap; - } - } else { - const distanceFromCenter = j - row.length / 2; - box.x = - canvas.width / 2 - - boxWidth / 2 + - distanceFromCenter * boxWidth + - distanceFromCenter * horizontalGap; - } - box.y = i * verticalGap + verticalGap + i * boxHeight; - } - } } static filter({ collection }, filteredObjects, criteriaFunction) { @@ -327,11 +233,181 @@ export class MCParticle extends EDMObject { } } +class ReconstructedParticle extends EDMObject { + constructor() { + super(); + this.width = 140; + this.height = 190; + this.color = "#fbffdf"; + this.radius = 30; + } + + draw(ctx) { + const boxCenterX = this.x + this.width / 2; + + super.draw(ctx); + + const topY = this.y + 1.5 * TOP_MARGIN; + const lines = []; + + lines.push("ID: " + this.index); + + const x = parseInt(this.momentum.x * 100) / 100; + const y = parseInt(this.momentum.y * 100) / 100; + const z = parseInt(this.momentum.z * 100) / 100; + lines.push(`p = (x=${x},`); + lines.push(`y=${y},`); + lines.push(`z=${z}) GeV`); + + const energy = parseInt(this.energy * 100) / 100; + lines.push("e = " + energy + " GeV"); + + lines.push(parseCharge(this.charge)); + + drawTextLines(ctx, lines, boxCenterX, topY, 23); + } + + static setup(recoCollection) {} + + static filter() {} +} + +class Cluster extends EDMObject { + constructor() { + super(); + this.width = 140; + this.height = 170; + this.color = "#ffe8df"; + this.radius = 20; + } + + draw(ctx) { + const boxCenterX = this.x + this.width / 2; + + super.draw(ctx); + + const topY = this.y + TOP_MARGIN; + const lines = []; + lines.push("ID: " + this.index); + lines.push("type: " + this.type); + const energy = parseInt(this.energy * 100) / 100; + lines.push("e = " + energy + " GeV"); + const x = parseInt(this.position.x * 100) / 100; + const y = parseInt(this.position.y * 100) / 100; + const z = parseInt(this.position.z * 100) / 100; + lines.push(`pos = (x=${x},`); + lines.push(`y=${y},`); + lines.push(`z=${z}) mm`); + + drawTextLines(ctx, lines, boxCenterX, topY, 23); + } + + static setup(clusterCollection) {} +} + +class Track extends EDMObject { + constructor() { + super(); + this.width = 140; + this.height = 150; + this.color = "#fff6df"; + this.radius = 25; + } + + draw(ctx) { + const boxCenterX = this.x + this.width / 2; + + super.draw(ctx); + + const topY = this.y + TOP_MARGIN; + + const lines = []; + lines.push("ID: " + this.index); + lines.push("type: " + this.type); + const chi2 = parseInt(this.chi2 * 100) / 100; + const ndf = parseInt(this.ndf * 100) / 100; + const chiNdf = `${chi2}/${ndf}`; + lines.push("chi2/ndf = " + chiNdf); + lines.push("dEdx = " + this.dEdx); + + const trackerHitsCount = this.oneToManyRelations["trackerHits"].length; + lines.push("tracker hits: " + trackerHitsCount); + + drawTextLines(ctx, lines, boxCenterX, topY, 23); + } + + static setup(trackCollection) {} +} + +class ParticleID extends EDMObject { + constructor() { + super(); + this.width = 140; + this.height = 140; + this.color = "#c9edf7"; + this.radius = 25; + } + + draw(ctx) { + const boxCenterX = this.x + this.width / 2; + + super.draw(ctx); + + const topY = this.y + TOP_MARGIN; + + const lines = []; + lines.push("ID: " + this.index); + lines.push("type: " + this.type); + lines.push("PDG: " + this.PDG); + lines.push("algorithm: " + this.algorithmType); + lines.push("likelihood: " + this.likelihood); + + drawTextLines(ctx, lines, boxCenterX, topY, 23); + } + + static setup(particleIDCollection) {} +} + +class Vertex extends EDMObject { + constructor() { + super(); + this.width = 140; + this.height = 150; + this.color = "#f5d3ef"; + this.radius = 25; + } + + draw(ctx) { + const boxCenterX = this.x + this.width / 2; + + super.draw(ctx); + + const topY = this.y + TOP_MARGIN; + + const lines = []; + lines.push("ID: " + this.index); + const x = parseInt(this.position.x * 100) / 100; + const y = parseInt(this.position.y * 100) / 100; + const z = parseInt(this.position.z * 100) / 100; + lines.push(`pos = (x=${x},`); + lines.push(`y=${y},`); + lines.push(`z=${z}) mm`); + const chi2 = parseInt(this.chi2 * 100) / 100; + const ndf = parseInt(this.ndf * 100) / 100; + const chiNdf = `${chi2}/${ndf}`; + lines.push("chi2/ndf = " + chiNdf); + + drawTextLines(ctx, lines, boxCenterX, topY, 23); + } + + static setup(vertexCollection) {} +} + export const objectTypes = { + "edm4hep::MCParticle": MCParticle, + "edm4hep::ReconstructedParticle": ReconstructedParticle, "edm4hep::Cluster": Cluster, + "edm4hep::Track": Track, "edm4hep::ParticleID": ParticleID, - "edm4hep::ReconstructedParticle": ReconstructedParticle, "edm4hep::Vertex": Vertex, - "edm4hep::Track": Track, - "edm4hep::MCParticle": MCParticle, }; diff --git a/js/views/association-view.js b/js/views/association-view.js new file mode 100644 index 00000000..4f04bf03 --- /dev/null +++ b/js/views/association-view.js @@ -0,0 +1,45 @@ +import { canvas } from "../main.js"; + +// List 1:1 association in a vertical list +export function buildAssociationView(viewObjects, associationName) { + const associations = viewObjects.associations[associationName]; + const length = associations.length; + + const fromWidth = associations[0].from.width; + const toWidth = associations[0].to.width; + const fromHorizontalGap = 0.3 * fromWidth; + const toHorizontalGap = 0.3 * toWidth; + const gap = 2 * (fromWidth + toWidth); + const totalWidth = gap + fromWidth + toWidth; + + const width = totalWidth > window.innerWidth ? totalWidth : window.innerWidth; + canvas.width = width; + + const fromHeight = associations[0].from.height; + const toHeight = associations[0].to.height; + + const height = Math.max(fromHeight, toHeight); + const verticalGap = 0.3 * height; + + const totalHeight = length * (height + verticalGap) + verticalGap; + + canvas.height = totalHeight; + + let accHeight = 0; + + const fromX = width / 2 - fromWidth - fromHorizontalGap; + + const toX = width / 2 + toHorizontalGap; + + associations.forEach((association) => { + association.from.x = fromX; + association.to.x = toX; + + const space = height + verticalGap; + const fromY = accHeight + space / 2 - fromHeight / 2; + const toY = accHeight + space / 2 - toHeight / 2; + association.from.y = fromY; + association.to.y = toY; + accHeight += height + verticalGap; + }); +} diff --git a/js/views/clustertree.js b/js/views/clustertree.js new file mode 100644 index 00000000..e38432c3 --- /dev/null +++ b/js/views/clustertree.js @@ -0,0 +1,13 @@ +import { buildTree } from "./tree.js"; +import { preFilterTree } from "./pre-filter.js"; + +export function clusterTree(viewCurrentObjects) { + const clusterCollection = + viewCurrentObjects.datatypes["edm4hep::Cluster"].collection ?? []; + + buildTree(clusterCollection, "clusters"); +} + +export function preFilterClusterTree(currentObjects, viewObjects) { + preFilterTree(currentObjects, viewObjects, "edm4hep::Cluster", ["clusters"]); +} diff --git a/js/views/list.js b/js/views/list.js new file mode 100644 index 00000000..a6806249 --- /dev/null +++ b/js/views/list.js @@ -0,0 +1,28 @@ +import { canvas } from "../main.js"; + +export function listView(collection) { + const width = window.innerWidth; + canvas.width = width; + + const gap = 1; + const objWidth = collection[0].width; + const objHorizontalGap = gap * objWidth; + const objHeight = collection[0].height; + const objVerticalGap = gap * objHeight; + + const cols = Math.ceil(width / (objWidth + objHorizontalGap)); + const rows = Math.ceil(collection.length / cols); + + const height = rows * (objHeight + objVerticalGap / 2) + objVerticalGap / 2; + canvas.height = height > window.innerHeight ? height : window.innerHeight; + + for (let i = 0; i < collection.length; i++) { + const x = (i % cols) * objWidth + (((i % cols) + 1) * objHorizontalGap) / 2; + const y = + Math.floor(i / cols) * objHeight + + ((Math.floor(i / cols) + 1) * objVerticalGap) / 2; + + collection[i].x = x; + collection[i].y = y; + } +} diff --git a/js/views/mcclusterassociation.js b/js/views/mcclusterassociation.js new file mode 100644 index 00000000..1558072c --- /dev/null +++ b/js/views/mcclusterassociation.js @@ -0,0 +1,19 @@ +import { preFilterAssociation } from "./pre-filter.js"; +import { buildAssociationView } from "./association-view.js"; + +export function mcClusterAssociation(viewObjects) { + buildAssociationView( + viewObjects, + "edm4hep::MCRecoClusterParticleAssociation" + ); +} + +export function preFilterMCCluster(currentObjects, viewObjects) { + preFilterAssociation( + currentObjects, + viewObjects, + "edm4hep::MCRecoClusterParticleAssociation", + "edm4hep::Cluster", + "edm4hep::MCParticle" + ); +} diff --git a/js/views/mcparticletree.js b/js/views/mcparticletree.js new file mode 100644 index 00000000..7f4ec1cf --- /dev/null +++ b/js/views/mcparticletree.js @@ -0,0 +1,103 @@ +import { canvas } from "../main.js"; +import { preFilterTree } from "./pre-filter.js"; + +export function mcParticleTree(viewCurrentObjects) { + const mcCollection = + viewCurrentObjects.datatypes["edm4hep::MCParticle"].collection ?? []; + + const getMaxRow = (parentLinks) => { + let maxRow = -1; + for (const parentLink of parentLinks) { + const parent = parentLink.from; + if (parent.row === -1) { + return -1; + } + + if (parent.row > maxRow) { + maxRow = parent.row; + } + } + + return maxRow; + }; + + let repeat = true; + while (repeat) { + repeat = false; + for (const mcParticle of mcCollection) { + if (mcParticle.row >= 0) { + continue; + } + const parentRow = getMaxRow(mcParticle.oneToManyRelations["parents"]); + if (parentRow >= 0) { + mcParticle.row = parentRow + 1; + } else { + repeat = true; + } + } + } + + const rows = mcCollection.map((obj) => { + return obj.row; + }); + const maxRow = Math.max(...rows); + + // Order infoBoxes into rows + const mcRows = []; + for (let i = 0; i <= maxRow; i++) { + mcRows.push([]); + } + for (const box of mcCollection) { + mcRows[box.row].push(box); + } + const rowWidths = mcRows.map((obj) => { + return obj.length; + }); + const maxRowWidth = Math.max(...rowWidths); + + const boxWidth = mcCollection[0].width; + const boxHeight = mcCollection[0].height; + const horizontalGap = boxWidth * 0.4; + const verticalGap = boxHeight * 0.3; + + canvas.width = + boxWidth * (maxRowWidth + 1) + horizontalGap * (maxRowWidth + 1); + canvas.height = boxHeight * (maxRow + 1) + verticalGap * (maxRow + 2); + + for (const [i, row] of mcRows.entries()) { + for (const [j, box] of row.entries()) { + if (row.length % 2 === 0) { + const distanceFromCenter = j - row.length / 2; + if (distanceFromCenter < 0) { + box.x = + canvas.width / 2 - + boxWidth - + horizontalGap / 2 + + (distanceFromCenter + 1) * boxWidth + + (distanceFromCenter + 1) * horizontalGap; + } else { + box.x = + canvas.width / 2 + + horizontalGap / 2 + + distanceFromCenter * boxWidth + + distanceFromCenter * horizontalGap; + } + } else { + const distanceFromCenter = j - row.length / 2; + box.x = + canvas.width / 2 - + boxWidth / 2 + + distanceFromCenter * boxWidth + + distanceFromCenter * horizontalGap; + } + box.y = i * verticalGap + verticalGap + i * boxHeight; + } + } +} + +export function preFilterMCTree(currentObjects, viewObjects) { + preFilterTree(currentObjects, viewObjects, "edm4hep::MCParticle", [ + "parents", + "daughters", + ]); +} diff --git a/js/views/mcrecoassociation.js b/js/views/mcrecoassociation.js new file mode 100644 index 00000000..9c3b5306 --- /dev/null +++ b/js/views/mcrecoassociation.js @@ -0,0 +1,16 @@ +import { preFilterAssociation } from "./pre-filter.js"; +import { buildAssociationView } from "./association-view.js"; + +export function mcRecoAssociation(viewObjects) { + buildAssociationView(viewObjects, "edm4hep::MCRecoParticleAssociation"); +} + +export function preFilterMCReco(currentObjects, viewObjects) { + preFilterAssociation( + currentObjects, + viewObjects, + "edm4hep::MCRecoParticleAssociation", + "edm4hep::ReconstructedParticle", + "edm4hep::MCParticle" + ); +} diff --git a/js/views/mctrackassociation.js b/js/views/mctrackassociation.js new file mode 100644 index 00000000..f9d6bde0 --- /dev/null +++ b/js/views/mctrackassociation.js @@ -0,0 +1,16 @@ +import { preFilterAssociation } from "./pre-filter.js"; +import { buildAssociationView } from "./association-view.js"; + +export function mcTrackAssociation(viewObjects) { + buildAssociationView(viewObjects, "edm4hep::MCRecoTrackParticleAssociation"); +} + +export function preFilterMCTrack(currentObjects, viewObjects) { + preFilterAssociation( + currentObjects, + viewObjects, + "edm4hep::MCRecoTrackParticleAssociation", + "edm4hep::Track", + "edm4hep::MCParticle" + ); +} diff --git a/js/views/onewayview.js b/js/views/onewayview.js new file mode 100644 index 00000000..c69bddaa --- /dev/null +++ b/js/views/onewayview.js @@ -0,0 +1,46 @@ +import { canvas } from "../main.js"; + +export function oneWayView(viewObjects, fromCollectionName, relationName) { + const relations = + viewObjects.datatypes[fromCollectionName].oneToOne[relationName]; + + const fromCollection = relations.map((relation) => relation.from); + const toCollection = relations.map((relation) => relation.to); + + const fromWidth = fromCollection[0].width; + const toWidth = toCollection[0].width; + const fromHorizontalGap = 0.3 * fromWidth; + const toHorizontalGap = 0.3 * toWidth; + const gap = 2 * (fromWidth + toWidth); + const totalWidth = gap + fromWidth + toWidth; + + const width = totalWidth > window.innerWidth ? totalWidth : window.innerWidth; + canvas.width = width; + + const fromHeight = fromCollection[0].height; + const toHeight = toCollection[0].height; + + const height = Math.max(fromHeight, toHeight); + const verticalGap = 0.3 * height; + + const totalHeight = + fromCollection.length * (height + verticalGap) + verticalGap; + + canvas.height = totalHeight; + + let accHeight = 0; + + const fromX = width / 2 - fromWidth - fromHorizontalGap; + + const toX = width / 2 + toHorizontalGap; + + for (let i = 0; i < fromCollection.length; i++) { + fromCollection[i].x = fromX; + toCollection[i].x = toX; + + const space = height + verticalGap; + fromCollection[i].y = accHeight + space / 2 - fromHeight / 2; + toCollection[i].y = accHeight + space / 2 - toHeight / 2; + accHeight += height + verticalGap; + } +} diff --git a/js/views/particleidlist.js b/js/views/particleidlist.js new file mode 100644 index 00000000..c3c13cb6 --- /dev/null +++ b/js/views/particleidlist.js @@ -0,0 +1,13 @@ +import { listView } from "./list.js"; +import { preFilterList } from "./pre-filter.js"; + +export function particleIDList(viewCurrentObjects) { + const vertexCollection = + viewCurrentObjects.datatypes["edm4hep::ParticleID"].collection ?? []; + + listView(vertexCollection); +} + +export function preFilterParticleIDList(currentObjects, viewObjects) { + preFilterList(currentObjects, viewObjects, "edm4hep::ParticleID"); +} diff --git a/js/views/pre-filter.js b/js/views/pre-filter.js new file mode 100644 index 00000000..bad016ed --- /dev/null +++ b/js/views/pre-filter.js @@ -0,0 +1,66 @@ +import { emptyCopyObject } from "../lib/copy.js"; + +export function preFilterAssociation( + currentObjects, + viewObjects, + associationName, + fromCollectionName, + toCollectionName +) { + emptyCopyObject(currentObjects, viewObjects); + + const association = currentObjects.associations[associationName]; + + const fromCollection = association.map((association) => association.from); + + const toCollection = association.map((association) => association.to); + + viewObjects.datatypes[fromCollectionName].collection = fromCollection; + + viewObjects.datatypes[toCollectionName].collection = toCollection; + + viewObjects.associations[associationName] = association; +} + +export function preFilterTree( + currentObjects, + viewObjects, + collectionName, + relationsNames +) { + emptyCopyObject(currentObjects, viewObjects); + viewObjects.datatypes[collectionName].collection = + currentObjects.datatypes[collectionName].collection; + + relationsNames.forEach((relationName) => { + viewObjects.datatypes[collectionName].oneToMany[relationName] = + currentObjects.datatypes[collectionName].oneToMany[relationName]; + }); +} + +export function preFilterList(currentObjects, viewObjects, collectionName) { + emptyCopyObject(currentObjects, viewObjects); + + viewObjects.datatypes[collectionName].collection = + currentObjects.datatypes[collectionName].collection; +} + +export function preFilterOneWay( + currentObjects, + viewObjects, + relationName, + fromCollectionName, + toCollectionName +) { + emptyCopyObject(currentObjects, viewObjects); + + const relations = + currentObjects.datatypes[fromCollectionName].oneToOne[relationName]; + + const fromCollection = relations.map((relation) => relation.from); + const toCollection = relations.map((relation) => relation.to); + + viewObjects.datatypes[fromCollectionName].oneToOne[relationName] = relations; + viewObjects.datatypes[fromCollectionName].collection = fromCollection; + viewObjects.datatypes[toCollectionName].collection = toCollection; +} diff --git a/js/views/recoclustertrack.js b/js/views/recoclustertrack.js new file mode 100644 index 00000000..d9fd30ea --- /dev/null +++ b/js/views/recoclustertrack.js @@ -0,0 +1,189 @@ +import { canvas } from "../main.js"; +import { emptyCopyObject } from "../lib/copy.js"; + +export function recoClusterTrackVertex(viewObjects) { + const recoParticles = + viewObjects.datatypes["edm4hep::ReconstructedParticle"].collection; + + const findFirstObject = (relationName) => { + const object = recoParticles.find((particle) => { + const relation = particle.oneToManyRelations[relationName]; + if (relation.length > 0) { + return relation[0].to; + } + }); + return object; + }; + + const firstRecoParticle = recoParticles[0]; + const recoHeight = firstRecoParticle.height; + const recoVerticalGap = parseInt(recoHeight * 0.3); + const recoVerticalSpace = recoHeight + recoVerticalGap; + const recoHalfHeight = parseInt(recoHeight / 2); + const recoWidth = firstRecoParticle.width; + const recoHorizontalGap = recoWidth * 0.3; + + const firstCluster = findFirstObject("clusters"); + const clusterHeight = firstCluster.height; + const clusterVerticalGap = clusterHeight * 0.3; + const clusterWidth = firstCluster.width; + const firstTrack = findFirstObject("tracks"); + const trackHeight = firstTrack.height; + const trackVerticalGap = trackHeight * 0.3; + const trackWidth = firstTrack.width; + + const firstVertex = recoParticles.find((particle) => { + const vertexRelation = particle.oneToOneRelations["startVertex"]; + if (vertexRelation !== undefined) { + return vertexRelation.to; + } + }); + + let vertexHeight = 0; + let vertexVerticalGap = 0; + let vertexWidth = 0; + if (firstVertex !== undefined) { + vertexHeight = firstVertex.height; + vertexVerticalGap = vertexHeight * 0.3; + vertexWidth = firstVertex.width; + } + + const widestObject = Math.max(clusterWidth, trackWidth, vertexWidth); + const widestGap = widestObject * 0.3; + + const totalHorizontalGap = + 2 * recoHorizontalGap + recoWidth + widestObject + 2 * widestGap; + + const width = + totalHorizontalGap > window.innerWidth + ? totalHorizontalGap + : window.innerWidth; + + canvas.width = width; + + const recoX = width / 2 - recoWidth; + + const otherX = width / 2 + widestGap; + + let totalHeight = 0; + + recoParticles.forEach((particle) => { + const clusterRelations = particle.oneToManyRelations["clusters"]; + const trackRelations = particle.oneToManyRelations["tracks"]; + const vertexRelation = particle.oneToOneRelations["startVertex"]; + + const relationsHeight = parseInt( + clusterRelations.length * (clusterHeight + clusterVerticalGap) + + trackRelations.length * (trackHeight + trackVerticalGap) + + (vertexRelation !== undefined ? vertexHeight + vertexVerticalGap : 0) + ); + + const height = + recoVerticalSpace > relationsHeight ? recoVerticalSpace : relationsHeight; + + const recoY = totalHeight + height / 2 - recoHalfHeight; + particle.y = recoY; + particle.x = recoX; + + const initialGap = (height - relationsHeight) / 2; + + let accumulatedRelationsHeight = initialGap + totalHeight; + + clusterRelations.forEach((clusterRelation) => { + const cluster = clusterRelation.to; + cluster.x = otherX; + + const y = clusterVerticalGap / 2 + accumulatedRelationsHeight; + cluster.y = y; + accumulatedRelationsHeight += clusterHeight + clusterVerticalGap / 2; + }); + + trackRelations.forEach((trackRelation) => { + const track = trackRelation.to; + track.x = otherX; + + const y = trackVerticalGap / 2 + accumulatedRelationsHeight; + track.y = y; + accumulatedRelationsHeight += trackHeight + trackVerticalGap / 2; + }); + + if (vertexRelation !== undefined) { + const vertex = vertexRelation.to; + vertex.x = otherX; + + const y = vertexVerticalGap / 2 + accumulatedRelationsHeight; + vertex.y = y; + accumulatedRelationsHeight += vertexHeight + vertexVerticalGap / 2; + } + + totalHeight += height; + }); + + canvas.height = totalHeight; +} + +export function preFilterRecoClusterTrackVertex(currentObjects, viewObjects) { + emptyCopyObject(currentObjects, viewObjects); + + const fromDatatype = + currentObjects.datatypes["edm4hep::ReconstructedParticle"]; + + const fromCollection = fromDatatype.collection; + + const recoParticles = []; + const clusters = []; + const tracks = []; + const vertexCollection = []; + + fromCollection.forEach((particle) => { + const clusterRelations = particle.oneToManyRelations["clusters"]; + const trackRelations = particle.oneToManyRelations["tracks"]; + const vertexRelation = particle.oneToOneRelations["startVertex"]; + + const total = clusterRelations.length + trackRelations.length; + + if (total === 0) { + return; + } + + clusterRelations.forEach((clusterRelation) => { + const cluster = clusterRelation.to; + clusters.push(cluster); + }); + + trackRelations.forEach((trackRelation) => { + const track = trackRelation.to; + tracks.push(track); + }); + + if (vertexRelation !== undefined) { + const vertex = vertexRelation.to; + vertexCollection.push(vertex); + } + + recoParticles.push(particle); + }); + + viewObjects.datatypes["edm4hep::ReconstructedParticle"].collection = + recoParticles; + viewObjects.datatypes["edm4hep::ReconstructedParticle"].oneToMany[ + "clusters" + ] = + currentObjects.datatypes["edm4hep::ReconstructedParticle"].oneToMany[ + "clusters" + ]; + viewObjects.datatypes["edm4hep::ReconstructedParticle"].oneToMany["tracks"] = + currentObjects.datatypes["edm4hep::ReconstructedParticle"].oneToMany[ + "tracks" + ]; + viewObjects.datatypes["edm4hep::ReconstructedParticle"].oneToOne[ + "startVertex" + ] = + currentObjects.datatypes["edm4hep::ReconstructedParticle"].oneToOne[ + "startVertex" + ]; + + viewObjects.datatypes["edm4hep::Cluster"].collection = clusters; + viewObjects.datatypes["edm4hep::Track"].collection = tracks; + viewObjects.datatypes["edm4hep::Vertex"].collection = vertexCollection; +} diff --git a/js/views/recoparticleid.js b/js/views/recoparticleid.js new file mode 100644 index 00000000..e145b9de --- /dev/null +++ b/js/views/recoparticleid.js @@ -0,0 +1,16 @@ +import { preFilterOneWay } from "./pre-filter.js"; +import { oneWayView } from "./onewayview.js"; + +export function recoParticleID(viewObjects) { + oneWayView(viewObjects, "edm4hep::ParticleID", "particle"); +} + +export function preFilterRecoParticleID(currentObjects, viewObjects) { + preFilterOneWay( + currentObjects, + viewObjects, + "particle", + "edm4hep::ParticleID", + "edm4hep::ReconstructedParticle" + ); +} diff --git a/js/views/recoparticletree.js b/js/views/recoparticletree.js new file mode 100644 index 00000000..a578a90a --- /dev/null +++ b/js/views/recoparticletree.js @@ -0,0 +1,16 @@ +import { buildTree } from "./tree.js"; +import { preFilterTree } from "./pre-filter.js"; + +export function recoParticleTree(viewCurrentObjects) { + const recoCollection = + viewCurrentObjects.datatypes["edm4hep::ReconstructedParticle"].collection ?? + []; + + buildTree(recoCollection, "particles"); +} + +export function preFilterRecoTree(currentObjects, viewObjects) { + preFilterTree(currentObjects, viewObjects, "edm4hep::ReconstructedParticle", [ + "particles", + ]); +} diff --git a/js/views/scrolls.js b/js/views/scrolls.js new file mode 100644 index 00000000..3624add2 --- /dev/null +++ b/js/views/scrolls.js @@ -0,0 +1,9 @@ +import { canvas } from "../main.js"; + +export function scrollTopCenter() { + return { x: (canvas.width - window.innerWidth) / 2, y: 0 }; +} + +export function scrollTopLeft() { + return { x: 0, y: 0 }; +} diff --git a/js/views/tracktree.js b/js/views/tracktree.js new file mode 100644 index 00000000..6ff0ff36 --- /dev/null +++ b/js/views/tracktree.js @@ -0,0 +1,13 @@ +import { buildTree } from "./tree.js"; +import { preFilterTree } from "./pre-filter.js"; + +export function trackTree(viewCurrentObjects) { + const trackCollection = + viewCurrentObjects.datatypes["edm4hep::Track"].collection ?? []; + + buildTree(trackCollection, "tracks"); +} + +export function preFilterTrackTree(currentObjects, viewObjects) { + preFilterTree(currentObjects, viewObjects, "edm4hep::Track", ["tracks"]); +} diff --git a/js/views/tree.js b/js/views/tree.js new file mode 100644 index 00000000..bfe89342 --- /dev/null +++ b/js/views/tree.js @@ -0,0 +1,79 @@ +import { canvas } from "../main.js"; + +// All particles that are related to itself have an one to many relation +export function buildTree(collection, relationOfReference) { + const nodes = new Set(); + const children = new Set(); + + for (const object of collection) { + const objects = object.oneToManyRelations[relationOfReference].map( + (link) => link.to + ); + nodes.add(`${object.index}-${object.collectionId}`); + for (const childObject of objects) { + children.add(`${childObject.index}-${childObject.collectionId}`); + } + } + + const rootNodesIds = nodes.difference(children); + const rootNodes = []; + + collection.forEach((object) => { + if (rootNodesIds.has(`${object.index}-${object.collectionId}`)) { + rootNodes.push(object); + } + }); + + rootNodes.forEach((rootNode) => { + const stack = [[rootNode, 0]]; + + while (stack.length > 0) { + const [node, row] = stack.pop(); + const id = `${node.index}-${node.collectionId}`; + if (nodes.has(id)) { + nodes.delete(id); + node.row = row; + + const childObjectLinks = node.oneToManyRelations[relationOfReference]; + + childObjectLinks.forEach((link) => { + stack.push([link.to, row + 1]); + }); + } + } + }); + + const horizontalGap = collection[0].width * 0.4; + const verticalGap = collection[0].height * 0.3; + const boxWidth = collection[0].width; + const boxHeight = collection[0].height; + + const matrix = []; + + collection.forEach((object) => { + const row = object.row; + if (matrix[row] === undefined) { + matrix[row] = []; + } + matrix[row].push(object); + }); + + matrix.forEach((row, i) => { + row.forEach((object, j) => { + object.x = j * horizontalGap + j * boxWidth + horizontalGap; + object.y = i * verticalGap + i * boxHeight + verticalGap; + }); + }); + + const totalWidth = + boxWidth * matrix[0].length + horizontalGap * (matrix[0].length + 1); + + canvas.width = + totalWidth > window.innerWidth ? totalWidth : window.innerWidth; + + const totalHeight = + boxHeight * (matrix.length + 1) + verticalGap * (matrix.length + 2); + + canvas.height = + totalHeight > window.innerHeight ? totalHeight : window.innerHeight; +} diff --git a/js/views/vertexlist.js b/js/views/vertexlist.js new file mode 100644 index 00000000..59962c5f --- /dev/null +++ b/js/views/vertexlist.js @@ -0,0 +1,13 @@ +import { listView } from "./list.js"; +import { preFilterList } from "./pre-filter.js"; + +export function vertexList(viewCurrentObjects) { + const vertexCollection = + viewCurrentObjects.datatypes["edm4hep::Vertex"].collection ?? []; + + listView(vertexCollection); +} + +export function preFilterVertexList(currentObjects, viewObjects) { + preFilterList(currentObjects, viewObjects, "edm4hep::Vertex"); +} diff --git a/js/views/views-dictionary.js b/js/views/views-dictionary.js new file mode 100644 index 00000000..894b8e14 --- /dev/null +++ b/js/views/views-dictionary.js @@ -0,0 +1,116 @@ +import { mcParticleTree, preFilterMCTree } from "./mcparticletree.js"; +import { mcRecoAssociation, preFilterMCReco } from "./mcrecoassociation.js"; +import { recoParticleTree, preFilterRecoTree } from "./recoparticletree.js"; +import { setupMCParticleFilter } from "../filter/mcparticle.js"; +import { trackTree, preFilterTrackTree } from "./tracktree.js"; +import { clusterTree, preFilterClusterTree } from "./clustertree.js"; +import { scrollTopCenter, scrollTopLeft } from "./scrolls.js"; +import { preFilterMCTrack, mcTrackAssociation } from "./mctrackassociation.js"; +import { + preFilterMCCluster, + mcClusterAssociation, +} from "./mcclusterassociation.js"; +import { + recoClusterTrackVertex, + preFilterRecoClusterTrackVertex, +} from "./recoclustertrack.js"; +import { setupNoFilter } from "../filter/nofilter.js"; +import { vertexList, preFilterVertexList } from "./vertexlist.js"; +import { particleIDList, preFilterParticleIDList } from "./particleidlist.js"; +import { recoParticleID, preFilterRecoParticleID } from "./recoparticleid.js"; +import { spanWithColor } from "../lib/html-string.js"; + +export const views = { + "Monte Carlo Particle Tree": { + filters: setupMCParticleFilter, + viewFunction: mcParticleTree, + scrollFunction: scrollTopCenter, + preFilterFunction: preFilterMCTree, + description: `

${spanWithColor( + "Red", + "#AA0000" + )} relations mean parent relation (from bottom to top), ${spanWithColor( + "green", + "#00AA00" + )} relations mean daughter relation (from top to bottom).

`, + }, + "Reconstructed Particle Tree": { + filters: setupNoFilter, + viewFunction: recoParticleTree, + scrollFunction: scrollTopLeft, + preFilterFunction: preFilterRecoTree, + description: `

A tree of the Reconstructed Particles. ${spanWithColor( + "Purple", + "#AA00AA" + )} relations mean relation between particles.

`, + }, + "Track Tree": { + filters: setupNoFilter, + viewFunction: trackTree, + scrollFunction: scrollTopLeft, + preFilterFunction: preFilterTrackTree, + description: `

A tree of the Tracks.

`, + }, + "Cluster Tree": { + filters: setupNoFilter, + viewFunction: clusterTree, + scrollFunction: scrollTopLeft, + preFilterFunction: preFilterClusterTree, + description: `

A tree of the Clusters.

`, + }, + "RecoParticle-Cluster-Track-Vertex": { + filters: setupNoFilter, + viewFunction: recoClusterTrackVertex, + scrollFunction: scrollTopCenter, + preFilterFunction: preFilterRecoClusterTrackVertex, + description: `

Relations that a Reconstruced Particle has with other objects. ${spanWithColor( + "Green", + "#AAAA00" + )} connections are towards Tracks, and ${spanWithColor( + "sky blue", + "#00AAAA" + )} connections are towards Clusters.

`, + }, + "Monte Carlo-Reconstructed Particle": { + filters: setupNoFilter, + viewFunction: mcRecoAssociation, + scrollFunction: scrollTopCenter, + preFilterFunction: preFilterMCReco, + description: `

Association between Monte Carlo Particles and Reconstructed Particles. 1:1 relation.

`, + }, + "Monte Carlo Particle-Track": { + filters: setupNoFilter, + viewFunction: mcTrackAssociation, + scrollFunction: scrollTopCenter, + preFilterFunction: preFilterMCTrack, + description: `

Association between Monte Carlo Particles and Tracks. 1:1 relation.

`, + }, + "Monte Carlo Particle-Cluster": { + filters: setupNoFilter, + viewFunction: mcClusterAssociation, + scrollFunction: scrollTopCenter, + preFilterFunction: preFilterMCCluster, + description: `

Association between Monte Carlo Particles and Clusters. 1:1 relation.

`, + }, + "ParticleID List": { + filters: setupNoFilter, + viewFunction: particleIDList, + scrollFunction: scrollTopLeft, + preFilterFunction: preFilterParticleIDList, + description: `

A list of ParticleIDs found in the event.

`, + }, + "Vertex List": { + filters: setupNoFilter, + viewFunction: vertexList, + scrollFunction: scrollTopLeft, + preFilterFunction: preFilterVertexList, + description: `

A list of Vertices found in the event.

`, + }, + "ParticleID-Reconstructed Particle": { + filters: setupNoFilter, + viewFunction: recoParticleID, + scrollFunction: scrollTopCenter, + preFilterFunction: preFilterRecoParticleID, + description: `

1:1 relation from ParticleID to Reconstructed Particle.

`, + }, +}; diff --git a/js/views/views.js b/js/views/views.js new file mode 100644 index 00000000..982d1011 --- /dev/null +++ b/js/views/views.js @@ -0,0 +1,149 @@ +import { currentObjects, currentEvent } from "../event-number.js"; +import { copyObject } from "../lib/copy.js"; +import { checkEmptyObject } from "../lib/empty-object.js"; +import { getVisible } from "../events.js"; +import { drawAll } from "../draw.js"; +import { canvas } from "../main.js"; +import { views } from "./views-dictionary.js"; +import { + mouseDown, + mouseUp, + mouseOut, + mouseMove, + onScroll, +} from "../events.js"; +import { emptyViewMessage, hideEmptyViewMessage } from "../lib/messages.js"; +import { showViewInformation, hideViewInformation } from "../information.js"; +import { emptyCanvas } from "../draw.js"; + +const currentView = {}; + +const viewOptions = document.getElementById("view-selector"); + +const scrollLocations = {}; + +function paintButton(view) { + for (const button of buttons) { + if (button.innerText === view) { + button.style.backgroundColor = "#c5c5c5"; + } else { + button.style.backgroundColor = "#f1f1f1"; + } + } +} + +function getViewScrollIndex() { + return `${currentEvent.event}-${getView()}`; +} + +function scroll() { + const index = getViewScrollIndex(); + window.scrollTo(scrollLocations[index].x, scrollLocations[index].y); +} + +function setInfoButtonName(view) { + const button = document.getElementById("view-information-button"); + button.innerText = view; +} + +const drawView = (view) => { + paintButton(view); + + const dragTools = { + draggedObject: null, + isDragging: false, + prevMouseX: 0, + prevMouseY: 0, + }; + + const { + preFilterFunction, + viewFunction, + scrollFunction, + filters, + description, + } = views[view]; + + const viewObjects = {}; + const viewCurrentObjects = {}; + const viewVisibleObjects = {}; + + preFilterFunction(currentObjects, viewObjects); + const isEmpty = checkEmptyObject(viewObjects); + + if (isEmpty) { + emptyCanvas(); + emptyViewMessage(); + hideViewInformation(); + return; + } + showViewInformation(view, description); + hideEmptyViewMessage(); + viewFunction(viewObjects); + copyObject(viewObjects, viewCurrentObjects); + + const scrollIndex = getViewScrollIndex(); + + if (scrollLocations[scrollIndex] === undefined) { + const viewScrollLocation = scrollFunction(); + scrollLocations[scrollIndex] = viewScrollLocation; + } + + scroll(); + drawAll(viewCurrentObjects); + getVisible(viewCurrentObjects, viewVisibleObjects); + filters(viewObjects, viewCurrentObjects, viewVisibleObjects); + setInfoButtonName(getView()); + + canvas.onmousedown = (event) => { + mouseDown(event, viewVisibleObjects, dragTools); + }; + canvas.onmouseup = (event) => { + mouseUp(event, viewCurrentObjects, dragTools); + }; + canvas.onmouseout = (event) => { + mouseOut(event, dragTools); + }; + canvas.onmousemove = (event) => { + mouseMove(event, viewVisibleObjects, dragTools); + }; + window.onscroll = () => { + onScroll(viewCurrentObjects, viewVisibleObjects); + }; +}; + +export function saveScrollLocation() { + const index = getViewScrollIndex(); + if (scrollLocations[index] === undefined) return; + scrollLocations[index] = { + x: window.scrollX, + y: window.scrollY, + }; +} + +export const setView = (view) => { + currentView.view = view; +}; + +export const getView = () => { + return currentView.view; +}; + +export const drawCurrentView = () => { + drawView(currentView.view); +}; + +const buttons = []; + +for (const key in views) { + const button = document.createElement("button"); + button.appendChild(document.createTextNode(key)); + button.onclick = () => { + saveScrollLocation(); + setView(key); + drawView(key); + }; + button.className = "view-button"; + buttons.push(button); + viewOptions.appendChild(button); +} diff --git a/data/particles.js b/mappings/particles.js similarity index 100% rename from data/particles.js rename to mappings/particles.js diff --git a/mappings/sim-status.js b/mappings/sim-status.js new file mode 100644 index 00000000..db438605 --- /dev/null +++ b/mappings/sim-status.js @@ -0,0 +1,34 @@ +export const SimStatusBitFieldDisplayValues = { + 23: "Overlay", + 24: "Stopped", + 25: "LeftDetector", + 26: "DecayedInCalorimeter", + 27: "DecayedInTracker", + 28: "VertexIsNotEndpointOfParent", + 29: "Backscatter", + 30: "CreatedInSimulation", +}; + +export function parseBits(bit) { + const bits = []; + + for (let i = 0; i < 32; i++) { + if (bit & (1 << i)) { + bits.push(i); + } + } + + return bits; +} + +export function getSimStatusDisplayValues(bits) { + return bits.map((bit) => + SimStatusBitFieldDisplayValues[bit] !== undefined + ? SimStatusBitFieldDisplayValues[bit] + : `Bit ${bit}` + ); +} + +export function getSimStatusDisplayValuesFromBit(bit) { + return getSimStatusDisplayValues(parseBits(bit)); +} diff --git a/model/index.js b/model/index.js index 1670a059..5db3966f 100644 --- a/model/index.js +++ b/model/index.js @@ -17,17 +17,17 @@ const configTypes = new Set([ "edm4hep::Vertex", "edm4hep::ReconstructedParticle", "edm4hep::Track", + "edm4hep::MCRecoParticleAssociation", + "edm4hep::MCRecoTrackParticleAssociation", + "edm4hep::MCRecoClusterParticleAssociation", ]); const selectedTypes = Object.entries(datatypes).filter(([key, _]) => configTypes.has(key) ); -const componentsDefinition = {}; const datatypesDefinition = {}; -class Component {} - class DataTypeMember { constructor(name, unit = null) { this.name = name; @@ -36,7 +36,8 @@ class DataTypeMember { } class Relation { - constructor(name) { + constructor(type, name) { + this.type = type; this.name = name; } } @@ -63,9 +64,9 @@ const parseDatatypesMembers = (members) => { const parseRelation = (relations) => { return relations.map((relation) => { - const [_, name] = parseString(relation); + const [type, name] = parseString(relation); - return new Relation(name); + return new Relation(type, name); }); }; diff --git a/output/datatypes.js b/output/datatypes.js index 942da49d..92cb2b88 100644 --- a/output/datatypes.js +++ b/output/datatypes.js @@ -46,9 +46,11 @@ export const datatypes = { ], "oneToManyRelations": [ { + "type": "edm4hep::MCParticle", "name": "parents" }, { + "type": "edm4hep::MCParticle", "name": "daughters" } ] @@ -70,6 +72,7 @@ export const datatypes = { ], "oneToOneRelations": [ { + "type": "edm4hep::ReconstructedParticle", "name": "particle" } ] @@ -107,9 +110,11 @@ export const datatypes = { ], "oneToManyRelations": [ { + "type": "edm4hep::Cluster", "name": "clusters" }, { + "type": "edm4hep::CalorimeterHit", "name": "hits" } ] @@ -130,16 +135,15 @@ export const datatypes = { }, { "name": "dEdxError" - }, - { - "name": "radiusOfInnermostHit" } ], "oneToManyRelations": [ { + "type": "edm4hep::TrackerHit", "name": "trackerHits" }, { + "type": "edm4hep::Track", "name": "tracks" } ] @@ -147,13 +151,13 @@ export const datatypes = { "edm4hep::Vertex": { "members": [ { - "name": "primary" + "name": "type" }, { "name": "chi2" }, { - "name": "probability" + "name": "ndf" }, { "name": "position" @@ -167,6 +171,7 @@ export const datatypes = { ], "oneToOneRelations": [ { + "type": "edm4hep::ReconstructedParticle", "name": "associatedParticle" } ] @@ -204,19 +209,74 @@ export const datatypes = { ], "oneToManyRelations": [ { + "type": "edm4hep::Cluster", "name": "clusters" }, { + "type": "edm4hep::Track", "name": "tracks" }, { + "type": "edm4hep::ReconstructedParticle", "name": "particles" } ], "oneToOneRelations": [ { + "type": "edm4hep::Vertex", "name": "startVertex" } ] + }, + "edm4hep::MCRecoParticleAssociation": { + "members": [ + { + "name": "weight" + } + ], + "oneToOneRelations": [ + { + "type": "edm4hep::ReconstructedParticle", + "name": "rec" + }, + { + "type": "edm4hep::MCParticle", + "name": "sim" + } + ] + }, + "edm4hep::MCRecoClusterParticleAssociation": { + "members": [ + { + "name": "weight" + } + ], + "oneToOneRelations": [ + { + "type": "edm4hep::Cluster", + "name": "rec" + }, + { + "type": "edm4hep::MCParticle", + "name": "sim" + } + ] + }, + "edm4hep::MCRecoTrackParticleAssociation": { + "members": [ + { + "name": "weight" + } + ], + "oneToOneRelations": [ + { + "type": "edm4hep::Track", + "name": "rec" + }, + { + "type": "edm4hep::MCParticle", + "name": "sim" + } + ] } } \ No newline at end of file diff --git a/test/dynamic.test.js b/test/dynamic.test.js deleted file mode 100644 index 10c2d7dd..00000000 --- a/test/dynamic.test.js +++ /dev/null @@ -1,247 +0,0 @@ -import { - loadMembers, - loadOneToOneRelations, - loadOneToManyRelations, -} from "../js/types/dynamic.js"; - -let object; -let data; - -beforeEach(() => { - object = { - "id": 1, - }; - data = {}; -}); - -test("load members given some defined members and data", () => { - const members = [ - { - "name": "type", - }, - { - "name": "chi2", - }, - { - "name": "ndf", - }, - { - "name": "dEdx", - }, - { - "name": "dEdxError", - }, - { - "name": "radiusOfInnermostHit", - }, - ]; - data = { - "chi2": 0.0, - "dEdx": 0.0, - "dEdxError": 0.0, - "dxQuantities": [ - { - "error": 0.0, - "type": 0, - "value": 0.0, - }, - ], - "ndf": 0, - "radiusOfInnermostHit": 17.0, - "subDetectorHitNumbers": [], - "trackStates": [ - { - "D0": 0.13514114916324615, - "Z0": -0.10983038693666458, - "covMatrix": [ - 0.01773529127240181, -0.0011217461433261633, 7.128114521037787e-5, - 2.307129989276291e-7, -1.38431239804504e-8, 1.2183726250114546e-10, - 9.953266999218613e-5, -6.313419817161048e-6, 2.394112907921908e-9, - 0.019905241206288338, -3.138819374726154e-5, 1.986780489460216e-6, - -4.870325809314124e-10, -0.001264480990357697, 8.076488302322105e-5, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - ], - "location": 0, - "omega": 0.004053603857755661, - "phi": -0.8681905269622803, - "referencePoint": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - }, - "tanLambda": -1.99562406539917, - "time": 0.0, - }, - ], - "trackerHits": [ - { - "collectionID": 5, - "index": 0, - }, - { - "collectionID": 5, - "index": 1, - }, - ], - "tracks": [], - "type": 0, - }; - - loadMembers(object, data, members); - expect(object).toEqual({ - "id": 1, - "type": 0, - "chi2": 0.0, - "ndf": 0, - "dEdx": 0.0, - "dEdxError": 0.0, - "radiusOfInnermostHit": 17.0, - }); -}); - -test("load one to one relations with some definition and data", () => { - data = { - "charge": 1.0, - "clusters": [], - "covMatrix": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - "energy": 12.062528610229492, - "goodnessOfPID": 0.0, - "mass": 2.315242290496826, - "momentum": { - "x": 11.738886833190918, - "y": 1.2114704847335815, - "z": 0.9354811906814575, - }, - "particleIDUsed": { - "collectionID": -2, - "index": -2, - }, - "particleIDs": [ - { - "collectionID": 4, - "index": 45, - }, - ], - "particles": [ - { - "collectionID": 14, - "index": 24, - }, - { - "collectionID": 14, - "index": 22, - }, - { - "collectionID": 14, - "index": 74, - }, - { - "collectionID": 14, - "index": 23, - }, - { - "collectionID": 14, - "index": 25, - }, - { - "collectionID": 14, - "index": 26, - }, - ], - "referencePoint": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - }, - "startVertex": { - "collectionID": 2, - "index": 2, - }, - "tracks": [], - "type": 0, - }; - const oneToOneRelations = [ - { - "type": "edm4hep::Vertex", - "name": "startVertex", - }, - ]; - const oneToOne = { - "startVertex": [], - }; - loadOneToOneRelations(object, data, oneToOneRelations, oneToOne, []); - expect(object.oneToOneRelations).not.toBeNull(); -}); - -test("load one to many relations with some definition and data", () => { - data = { - "PDG": -11, - "charge": 1.0, - "colorFlow": { - "a": 0, - "b": 0, - }, - "daughters": [ - { - "collectionID": 11, - "index": 4, - }, - { - "collectionID": 11, - "index": 5, - }, - ], - "endpoint": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - }, - "generatorStatus": 21, - "mass": 0.0, - "momentum": { - "x": 0.0, - "y": 0.0, - "z": -119.99999237060547, - }, - "momentumAtEndpoint": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - }, - "parents": [ - { - "collectionID": 11, - "index": 1, - }, - ], - "simulatorStatus": 0, - "spin": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - }, - "time": 0.0, - "vertex": { - "x": -0.01184066478163004, - "y": -2.074451003863942e-6, - "z": -0.08278788626194, - }, - }; - const oneToManyRelations = [ - { - "type": "edm4hep::MCParticle", - "name": "parents", - }, - { - "type": "edm4hep::MCParticle", - "name": "daughters", - }, - ]; - const oneToMany = { - "parents": [], - "daughters": [], - }; - loadOneToManyRelations(object, data, oneToManyRelations, oneToMany, []); - expect(object.oneToManyRelations.daughters.length).toEqual(2); - expect(object.oneToManyRelations.parents.length).toEqual(1); -}); diff --git a/test/filterMCParticle.test.js b/test/filterMCParticle.test.js index c3c54ca3..0b7989ea 100644 --- a/test/filterMCParticle.test.js +++ b/test/filterMCParticle.test.js @@ -5,20 +5,20 @@ import { Checkbox, buildCriteriaFunction, } from "../js/menu/filter/parameters.js"; -import { CheckboxBuilder } from "../js/menu/filter/builders.js"; let objects = {}; const data = { "Event 0": { "Collection": { + "collID": 0, "collType": "edm4hep::MCParticleCollection", "collection": [ { "momentum": 0, "charge": 0, "mass": 0, - "simStatus": 70, + "simulatorStatus": 70, "parents": [], "daughters": [ { @@ -31,7 +31,7 @@ const data = { "momentum": 100, "charge": 1, "mass": 10, - "simStatus": 24, + "simulatorStatus": 24, "daughters": [ { "collectionID": 0, @@ -49,7 +49,7 @@ const data = { "momentum": 200, "charge": 2, "mass": 20, - "simStatus": 25, + "simulatorStatus": 25, "daughters": [ { "collectionID": 0, @@ -67,7 +67,7 @@ const data = { "momentum": 300, "charge": 3, "mass": 30, - "simStatus": 26, + "simulatorStatus": 26, "daughters": [ { "collectionID": 0, @@ -85,7 +85,7 @@ const data = { "momentum": 400, "charge": 4, "mass": 40, - "simStatus": 27, + "simulatorStatus": 27, "parents": [ { "collectionID": 0, @@ -121,8 +121,8 @@ describe("filter by ranges", () => { const filteredObjects = reconnect(criteriaFunction, objects); expect( - filteredObjects["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.id + filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( + (mcParticle) => mcParticle.index ) ).toEqual([3, 4]); }); @@ -145,8 +145,8 @@ describe("filter by ranges", () => { const filteredObjects = reconnect(criteriaFunction, objects); expect( - filteredObjects["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.id + filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( + (mcParticle) => mcParticle.index ) ).toEqual([3, 4]); }); @@ -154,7 +154,7 @@ describe("filter by ranges", () => { describe("filter by checkboxes", () => { it("filter by a single checkbox", () => { - const simulatorStatus = new Checkbox("simStatus", 23); + const simulatorStatus = new Checkbox("simulatorStatus", 23); simulatorStatus.checked = true; const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); const criteriaFunction = buildCriteriaFunction(checkboxFilters); @@ -162,18 +162,18 @@ describe("filter by checkboxes", () => { const filteredObjects = reconnect(criteriaFunction, objects); expect( - filteredObjects["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.id + filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( + (mcParticle) => mcParticle.index ) ).toEqual([]); }); it("filter by a combination of checkboxes", () => { - const simulatorStatus1 = new Checkbox("simStatus", 23); + const simulatorStatus1 = new Checkbox("simulatorStatus", 23); simulatorStatus1.checked = true; - const simulatorStatus2 = new Checkbox("simStatus", 26); + const simulatorStatus2 = new Checkbox("simulatorStatus", 26); simulatorStatus2.checked = true; - const simulatorStatus3 = new Checkbox("simStatus", 27); + const simulatorStatus3 = new Checkbox("simulatorStatus", 27); simulatorStatus3.checked = true; const checkboxFilters = Checkbox.buildFilter([ simulatorStatus1, @@ -185,10 +185,10 @@ describe("filter by checkboxes", () => { const filteredObjects = reconnect(criteriaFunction, objects); expect( - filteredObjects["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.id + filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( + (mcParticle) => mcParticle.index ) - ).toEqual([]); + ).toEqual([3, 4]); }); }); @@ -198,7 +198,7 @@ describe("filter by ranges and checkboxes", () => { property: "charge", unit: "e", }); - const simulatorStatus = new Checkbox("simStatus", 26); + const simulatorStatus = new Checkbox("simulatorStatus", 26); const rangeFilters = Range.buildFilter([charge]); const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); const criteriaFunction = buildCriteriaFunction( @@ -209,8 +209,8 @@ describe("filter by ranges and checkboxes", () => { const filteredObjects = reconnect(criteriaFunction, objects); expect( - filteredObjects["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.id + filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( + (mcParticle) => mcParticle.index ) ).toEqual([0, 1, 2, 3, 4]); }); @@ -221,7 +221,7 @@ describe("filter by ranges and checkboxes", () => { unit: "e", }); charge.max = 3; - const simulatorStatus = new Checkbox("simStatus", 23); + const simulatorStatus = new Checkbox("simulatorStatus", 23); simulatorStatus.checked = true; const rangeFilters = Range.buildFilter([charge]); const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); @@ -233,8 +233,8 @@ describe("filter by ranges and checkboxes", () => { const filteredObjects = reconnect(criteriaFunction, objects); expect( - filteredObjects["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.id + filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( + (mcParticle) => mcParticle.index ) ).toEqual([]); }); diff --git a/test/load.json b/test/load.json index daa5c4c7..9f99294d 100644 --- a/test/load.json +++ b/test/load.json @@ -178,7 +178,7 @@ ], "particles": [ { - "collectionID": 14, + "collectionID": 13, "index": 1 } ], diff --git a/test/load.test.js b/test/load.test.js index c9e813c4..2f81e54c 100644 --- a/test/load.test.js +++ b/test/load.test.js @@ -1,223 +1,51 @@ -import { loadObjectType, loadObjects } from "../js/types/load.js"; -import { datatypes } from "../output/datatypes.js"; -import { objectTypes } from "../js/types/objects.js"; +import { loadObjects } from "../js/types/load.js"; import json from "./load.json" assert { type: "json" }; -test("load a collection of particles", () => { - const type = "edm4hep::Track"; - const collection = [ - { - "chi2": 0.0, - "dEdx": 0.0, - "dEdxError": 0.0, - "dxQuantities": [ - { - "error": 0.0, - "type": 0, - "value": 0.0, - }, - ], - "ndf": 0, - "radiusOfInnermostHit": 17.0, - "subDetectorHitNumbers": [], - "trackStates": [ - { - "D0": -0.05963143706321716, - "Z0": -0.9309114217758179, - "covMatrix": [ - 0.00838407501578331, -0.0005293499561958015, 3.361300696269609e-5, - 1.1750994133308268e-7, -7.233749155233227e-9, 3.568003878462456e-11, - 0.00010174162162002176, -6.439207481889753e-6, 3.929798264579176e-9, - 0.004548509605228901, -1.0186887266172562e-5, 6.442872972911573e-7, - -3.0552818608420296e-10, -0.00028709869366139174, - 1.831963163567707e-5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - ], - "location": 0, - "omega": 0.0032351072877645493, - "phi": -2.237527847290039, - "referencePoint": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - }, - "tanLambda": -4.997164249420166, - "time": 0.0, - }, - ], - "trackerHits": [ - { - "collectionID": 5, - "index": 0, - }, - { - "collectionID": 5, - "index": 1, - }, - ], - "tracks": [], - "type": 0, - }, - { - "chi2": 0.0, - "dEdx": 0.0, - "dEdxError": 0.0, - "dxQuantities": [ - { - "error": 0.0, - "type": 0, - "value": 0.0, - }, - ], - "ndf": 0, - "radiusOfInnermostHit": 17.0, - "subDetectorHitNumbers": [], - "trackStates": [ - { - "D0": -0.0391334593296051, - "Z0": -0.9311737418174744, - "covMatrix": [ - 0.0035914022009819746, -0.00022589370200876147, - 1.4322121387522202e-5, 4.24616963812241e-8, -2.698678391865883e-9, - 8.189490210107342e-13, 8.363036613445729e-5, -5.291213710734155e-6, - 3.081625399303789e-9, 0.00030561615130864084, -6.513672360597411e-6, - 4.117676155601657e-7, -2.10651204812784e-10, -1.7557771570864134e-5, - 1.1170427569595631e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - ], - "location": 0, - "omega": -0.0019104206003248692, - "phi": -2.798056125640869, - "referencePoint": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - }, - "tanLambda": -4.306049823760986, - "time": 0.0, - }, - ], - "trackerHits": [ - { - "collectionID": 5, - "index": 2, - }, - { - "collectionID": 5, - "index": 3, - }, - ], - "tracks": [], - "type": 0, - }, - { - "chi2": 0.0, - "dEdx": 0.0, - "dEdxError": 0.0, - "dxQuantities": [ - { - "error": 0.0, - "type": 0, - "value": 0.0, - }, - ], - "ndf": 0, - "radiusOfInnermostHit": 17.0, - "subDetectorHitNumbers": [], - "trackStates": [ - { - "D0": -0.050047606229782104, - "Z0": -0.9527733325958252, - "covMatrix": [ - 0.007592398207634687, -0.0004792569379787892, 3.0433289794018492e-5, - 1.0142189665884871e-7, -6.283491504888161e-9, - 2.5339810111324468e-11, 0.0001311719825025648, - -8.306328709295485e-6, 4.581083068444514e-9, 0.0035560736432671547, - -1.2910893929074518e-5, 8.168165095412405e-7, - -3.557094618855672e-10, -0.00022385688498616219, - 1.4287375051935669e-5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - ], - "location": 0, - "omega": -0.003163003595545888, - "phi": -2.4250643253326416, - "referencePoint": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - }, - "tanLambda": -3.454428195953369, - "time": 0.0, - }, - ], - "trackerHits": [ - { - "collectionID": 5, - "index": 4, - }, - { - "collectionID": 5, - "index": 5, - }, - ], - "tracks": [], - "type": 0, - }, - ]; - const [objects, oneToOne, { trackerHits, tracks }] = loadObjectType( - collection, - datatypes[type], - objectTypes[type] - ); - - expect(objects.length).toEqual(3); - expect(oneToOne).toEqual({}); - expect(trackerHits.length).toEqual(6); - expect(tracks.length).toEqual(0); -}); - test("load a json file with a collection of objects", () => { const objects = loadObjects(json, 1, [ "edm4hep::MCParticle", "edm4hep::ReconstructedParticle", ]); - expect(objects["edm4hep::MCParticle"]).toBeDefined(); - expect(objects["edm4hep::ReconstructedParticle"]).toBeDefined(); + const datatypes = objects.datatypes; + + expect(datatypes["edm4hep::MCParticle"]).toBeDefined(); + expect(datatypes["edm4hep::ReconstructedParticle"]).toBeDefined(); - expect(objects["edm4hep::MCParticle"].collection.length).toEqual(3); + expect(datatypes["edm4hep::MCParticle"].collection.length).toEqual(3); expect( - objects["edm4hep::MCParticle"].collection.map((val) => val.id) + datatypes["edm4hep::MCParticle"].collection.map((val) => val.index) ).toEqual([0, 1, 2]); - expect(objects["edm4hep::MCParticle"].oneToMany["daughters"]).toBeDefined(); - expect(objects["edm4hep::MCParticle"].oneToMany["parents"]).toBeDefined(); + expect(datatypes["edm4hep::MCParticle"].oneToMany["daughters"]).toBeDefined(); + expect(datatypes["edm4hep::MCParticle"].oneToMany["parents"]).toBeDefined(); + expect( - objects["edm4hep::MCParticle"].oneToMany["daughters"][0].from.id + datatypes["edm4hep::MCParticle"].oneToMany["daughters"][0].from.index ).toEqual(0); expect( - objects["edm4hep::MCParticle"].oneToMany["daughters"][0].to.id + datatypes["edm4hep::MCParticle"].oneToMany["daughters"][0].to.index ).toEqual(2); - expect(objects["edm4hep::ReconstructedParticle"].collection.length).toEqual( + expect(datatypes["edm4hep::ReconstructedParticle"].collection.length).toEqual( 2 ); expect( - objects["edm4hep::ReconstructedParticle"].collection.map((val) => val.id) + datatypes["edm4hep::ReconstructedParticle"].collection.map( + (val) => val.index + ) ).toEqual([0, 1]); expect( - objects["edm4hep::ReconstructedParticle"].oneToMany["particles"] + datatypes["edm4hep::ReconstructedParticle"].oneToMany["particles"] ).toBeDefined(); expect( - objects["edm4hep::ReconstructedParticle"].oneToOne["startVertex"] + datatypes["edm4hep::ReconstructedParticle"].oneToOne["startVertex"] ).toBeDefined(); expect( - objects["edm4hep::ReconstructedParticle"].oneToMany["particles"][0].from.id + datatypes["edm4hep::ReconstructedParticle"].oneToMany["particles"][0].from + .index ).toEqual(0); expect( - objects["edm4hep::ReconstructedParticle"].oneToMany["particles"][0].to.id + datatypes["edm4hep::ReconstructedParticle"].oneToMany["particles"][0].to + .index ).toEqual(1); - - expect( - objects["edm4hep::ReconstructedParticle"].oneToOne["startVertex"][0].to - ).toBeUndefined(); - expect( - objects["edm4hep::ReconstructedParticle"].oneToOne["startVertex"][1].to - ).toBeUndefined(); }); diff --git a/test/objects.test.js b/test/objects.test.js index b52328dd..31cdee92 100644 --- a/test/objects.test.js +++ b/test/objects.test.js @@ -6,6 +6,8 @@ describe("MCParticle", () => { beforeEach(() => { mcParticle = new MCParticle(1); + mcParticle.x = 0; + mcParticle.y = 0; }); afterEach(() => { @@ -89,7 +91,11 @@ describe("Link", () => { beforeEach(() => { firstObject = new MCParticle(0); + firstObject.x = 0; + firstObject.y = 0; secondObject = new MCParticle(1); + secondObject.x = 0; + secondObject.y = 0; link = new Link(firstObject, secondObject); }); @@ -102,7 +108,7 @@ describe("Link", () => { secondObject.x = 140; secondObject.y = 250; - expect(link.isVisible(0, 0, 250, 250)).toBe(true); + expect(link.isVisible(0, 0, 300, 300)).toBe(true); }); it("should return false if the link is not visible", () => { diff --git a/test/primitives.test.js b/test/primitives.test.js index f58cb271..550f9999 100644 --- a/test/primitives.test.js +++ b/test/primitives.test.js @@ -1,5 +1,5 @@ import { jest } from "@jest/globals"; -import { drawRoundedRect, drawTex } from "../js/graphic-primitives.js"; +import { drawRoundedRect, drawTex } from "../js/lib/graphic-primitives.js"; let ctx; @@ -24,7 +24,7 @@ afterEach(() => { describe("drawRoundedRect", () => { it("should draw a rounded rectangle with the correct properties", () => { - drawRoundedRect(ctx, 10, 20, 100, 200, "red"); + drawRoundedRect(ctx, 10, 20, 100, 200, "red", 15); expect(ctx.save).toHaveBeenCalled(); expect(ctx.fillStyle).toBe("red"); diff --git a/test/tools.test.js b/test/tools.test.js index 84cdaa98..7a0ad3f1 100644 --- a/test/tools.test.js +++ b/test/tools.test.js @@ -1,4 +1,4 @@ -import { infoMsg, errorMsg } from "../js/tools"; +import { infoMsg, errorMsg } from "../js/lib/messages.js"; let msgDiv;