diff --git a/css/clean-switch.css b/css/clean-switch.css new file mode 100644 index 00000000..4f2dd2ad --- /dev/null +++ b/css/clean-switch.css @@ -0,0 +1,82 @@ +#toggles { + margin-bottom: 10px; +} +.toggle { + display: flex; + justify-content: space-between; + max-width: 100%; + padding-right: 12px; +} + +/* +https://miladd3.github.io/clean-switch/ +MIT License +*/ +.cl-switch input[type="checkbox"] { + display: none; + visibility: hidden; +} +.switcher { + display: inline-block; + border-radius: 100px; + width: 35px; + height: 15px; + background-color: #ccc; + position: relative; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + vertical-align: middle; +} +.switcher { + cursor: pointer; +} +.switcher:before { + content: ""; + display: block; + width: 20px; + height: 20px; + background-color: #fff; + border-radius: 50%; + margin-top: -2.5px; + position: absolute; + top: 0; + left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin-right: 0; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; +} +.switcher:active:before { + transition: all, 0.1s; +} +.toggle-label { + font-family: sans-serif; + font-size: 16px; + vertical-align: middle; +} +.cl-switch input[type="checkbox"]:checked + .switcher { + background-color: #8591d5; +} +.cl-switch input[type="checkbox"]:checked + .switcher:before { + left: 100%; + margin-left: -20px; + background-color: #3f51b5; +} +.cl-switch [disabled]:not([disabled="false"]) + .switcher { + background: #ccc !important; +} +.cl-switch [disabled]:not([disabled="false"]) + .switcher:before { + background-color: #e2e2e2 !important; +} +.cl-switch.cl-switch-black input[type="checkbox"]:checked + .switcher { + background-color: #676767; +} +.cl-switch.cl-switch-black input[type="checkbox"]:checked + .switcher:before { + background-color: #343434; +} diff --git a/css/event.css b/css/event.css index 19f506bc..db36c030 100644 --- a/css/event.css +++ b/css/event.css @@ -10,6 +10,7 @@ transform: translateX(-50%); background-color: #e1e1e1; padding: 5px 10px; + border: 1px solid #000; border-radius: 5px; } diff --git a/css/filter.css b/css/filter.css index 1bd6f5a1..91907f32 100644 --- a/css/filter.css +++ b/css/filter.css @@ -1,49 +1,160 @@ -#filter { - min-width: 100px; +#filters { + display: none; position: fixed; - flex-direction: column; - background-color: #e1e1e1; - border-radius: 5px; - border: 1px solid #000; - padding: 10px; top: 10px; right: 10px; z-index: 1; + width: 300px; + max-height: 65vh; + padding: 10px; + background-color: #e1e1e1; + border-radius: 5px; + border: 1px solid #000; } -#filter-header { +#filters-header { display: flex; + flex-direction: row; justify-content: space-between; align-items: center; + max-height: 5vh; +} + +#filters-title { + font-weight: bold; } -#filter-button { +#filters-content { + width: 100%; +} + +#filter-menu-handler { cursor: pointer; + width: 20px; + height: 20px; } #close-filter { display: none; } -#filter-content { +#filters-body { display: none; flex-direction: column; + align-items: center; margin-top: 10px; + overflow-y: auto; + max-height: 60vh; + padding-right: 5px; } -#filters { +#filters-body::-webkit-scrollbar { + width: 7px; +} + +#filters-body::-webkit-scrollbar-track { + background: #e1e1e1; + border-radius: 5px; +} + +#filters-body::-webkit-scrollbar-thumb { + background: #afafaf; + border-radius: 5px; +} + +#filters-body::-webkit-scrollbar-thumb:hover { + background: #858585; +} + +#invert-filters-section { + font-style: italic; display: flex; - flex-direction: column; - padding: 10px 0; + flex-direction: row; +} + +#filters-buttons { + margin-top: 10px; + width: fit-content; } .filter-action { + font-weight: 500; padding: 5px; + margin: 0 5px; border-radius: 5px; - font-weight: 500; border: 1px solid #000; } .filter-action:hover { background-color: #c5c5c5; } + +.filter-collection-title { + font-weight: bold; + text-align: center; + margin: 5px 0; +} + +.filter-collection-subtitle { + font-weight: 500; +} + +.range-input { + width: 45px; + margin: 0 5px; + padding: 4px; + border-radius: 3px; + border: 1px solid #000; + text-align: center; +} + +.range-input:focus-visible { + outline: none; +} + +.filter-collection-container { + width: 100%; + display: flex; + flex-direction: column; + margin-bottom: 10px; +} + +.range-filter { + margin: 4px 0; + display: grid; + align-items: center; + grid-template-columns: 1fr 140px 40px; +} + +.range-inputs { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.range-unit { + text-align: right; +} + +.filter-sub-container { + padding: 5px 0; +} + +.filter-checkbox-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.checkbox-title-container { + display: flex; + flex-direction: row; + padding: 2px; + background-color: #c5c5c5; + border-radius: 5px; + margin: 2px; +} + +.filter-checkbox { + margin: 2px; +} diff --git a/css/main.css b/css/main.css index 74d505a4..5d2b30d4 100644 --- a/css/main.css +++ b/css/main.css @@ -6,10 +6,6 @@ body { font-size: 16px; } -.manipulation-tool { - display: none; -} - input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; @@ -30,3 +26,7 @@ button { background: none; padding: 0; } + +#input-file { + cursor: pointer; +} diff --git a/css/toggle.css b/css/toggle.css deleted file mode 100644 index d877a96c..00000000 --- a/css/toggle.css +++ /dev/null @@ -1,73 +0,0 @@ -#toggle { - position: fixed; - flex-direction: row; - justify-content: center; - align-items: center; - top: 10px; - left: 10px; - z-index: 1; -} - -.toggle-label { - margin-right: 10px; - margin-left: 10px; -} - -.switch { - position: relative; - display: inline-block; - width: 60px; - height: 34px; -} - -.switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #e1e1e1; - -webkit-transition: 0.4s; - transition: 0.4s; -} - -.slider:before { - position: absolute; - content: ""; - height: 26px; - width: 26px; - left: 4px; - bottom: 4px; - background-color: white; - -webkit-transition: 0.4s; - transition: 0.4s; -} - -input:checked + .slider { - background-color: #2196f3; -} - -input:focus + .slider { - box-shadow: 0 0 1px #2196f3; -} - -input:checked + .slider:before { - -webkit-transform: translateX(26px); - -ms-transform: translateX(26px); - transform: translateX(26px); -} - -.slider.round { - border-radius: 34px; -} - -.slider.round:before { - border-radius: 50%; -} diff --git a/css/views.css b/css/views.css index 8c9cc7f7..42fe5bb8 100644 --- a/css/views.css +++ b/css/views.css @@ -19,17 +19,17 @@ height: fit-content; } -#views { +#left-menu { display: none; flex-direction: column; position: fixed; - top: 25%; + top: 10px; left: 10px; - width: fit-content; + width: 260px; height: fit-content; - max-height: 50%; + max-height: 63vh; background-color: #e1e1e1; - padding: 15px; + padding: 15px 0px 15px 10px; border: 1px solid #000; border-radius: 5px; } @@ -41,10 +41,14 @@ align-items: center; } -#open-views { +#close-views { display: none; } +.views-handler { + margin-right: 12px; +} + #views-menu-handler { cursor: pointer; margin-left: 10px; @@ -52,7 +56,7 @@ #view-selector { margin-top: 10px; - display: flex; + display: none; flex-direction: column; justify-content: flex-start; overflow-y: auto; diff --git a/img/close-left.svg b/img/close-left.svg deleted file mode 100644 index 8fd678d1..00000000 --- a/img/close-left.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/img/open.svg b/img/open.svg deleted file mode 100644 index 12924cad..00000000 --- a/img/open.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/index.html b/index.html index a2d762c0..b05efb9b 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,6 @@ - @@ -20,6 +19,7 @@ + @@ -68,26 +68,22 @@ -
- Show PDG IDs - -
- -
-
- Filters -
+
+
+ Filters +
Open filter Close filter
-
-
+
+
+
+
+ + Invert filters?
-
+
@@ -176,12 +172,21 @@

-
+
+
+
+ Show PDG IDs + +
+
- Select a view: + Select a view
- Close - Open + Close + Open
@@ -213,7 +218,6 @@

- \ No newline at end of file diff --git a/js/draw/app.js b/js/draw/app.js index bbc9d6e6..76658687 100644 --- a/js/draw/app.js +++ b/js/draw/app.js @@ -1,10 +1,4 @@ -import { - Application, - Container, - Culler, - CullerPlugin, - extensions, -} from "../pixi.min.mjs"; +import { Application, Container, Culler } from "../pixi.min.mjs"; import { dragEnd } from "./drag.js"; import { addScroll } from "./scroll.js"; diff --git a/js/draw/box.js b/js/draw/box.js index 9df12faa..ae46d8d6 100644 --- a/js/draw/box.js +++ b/js/draw/box.js @@ -147,17 +147,14 @@ export function addLinesToBox(lines, box, y) { return text.position.y + text.height; } -export async function svgElementToPixiSprite(id, src, size) { - let asset; - +export async function svgElementToPixiSprite(id, src) { if (!Cache.has(id)) { - Cache.set(id, await Assets.load(src)); + const texture = await Assets.load(src); + Cache.set(id, texture); } + const texture = Cache.get(id); + const sprite = Sprite.from(texture); - asset = Cache.get(id); - const sprite = Sprite.from(asset); - sprite.width = size; - sprite.height = size; return sprite; } diff --git a/js/draw/drag.js b/js/draw/drag.js index 734fa2df..5521da2f 100644 --- a/js/draw/drag.js +++ b/js/draw/drag.js @@ -46,7 +46,9 @@ export function dragMove(event) { } export function dragEnd() { - currentObject.zIndex = 1; + if (currentObject) { + currentObject.zIndex = 1; + } const app = getApp(); app.stage.off("pointermove", dragMove); } diff --git a/js/draw/link.js b/js/draw/link.js index 0eb7ae23..c96e1981 100644 --- a/js/draw/link.js +++ b/js/draw/link.js @@ -49,41 +49,18 @@ function bezierCurve({ return curve; } -export function drawBezierLink(link) { +export function drawBezierLink(link, reverse = false) { const app = getApp(); const container = getContainer(); - const [fromX, fromY] = fromPoints(link.from); - const [cpFromX, cpFromY, cpToX, cpToY, toX, toY] = toPoints( - link.from, - link.to - ); - - let curve = bezierCurve({ - fromX: fromX + link.xShift, - fromY: fromY, - cpFromX: cpFromX + link.xShift, - cpFromY: cpFromY, - cpToX: cpToX + link.xShift, - cpToY: cpToY, - toX: toX + link.xShift, - toY: toY, - color: link.color, - }); - - link.renderedLink = curve; - - const boxFrom = link.from.renderedBox; - const boxTo = link.to.renderedBox; - - const boxFromOnMove = () => { - container.removeChild(curve); + if (!reverse) { const [fromX, fromY] = fromPoints(link.from); const [cpFromX, cpFromY, cpToX, cpToY, toX, toY] = toPoints( link.from, link.to ); - curve = bezierCurve({ + + let curve = bezierCurve({ fromX: fromX + link.xShift, fromY: fromY, cpFromX: cpFromX + link.xShift, @@ -94,29 +71,87 @@ export function drawBezierLink(link) { toY: toY, color: link.color, }); + link.renderedLink = curve; - link.renderedLink.renderable = link.isVisible(); + + const boxFrom = link.from.renderedBox; + const boxTo = link.to.renderedBox; + + const boxFromOnMove = () => { + container.removeChild(curve); + const [fromX, fromY] = fromPoints(link.from); + const [cpFromX, cpFromY, cpToX, cpToY, toX, toY] = toPoints( + link.from, + link.to + ); + curve = bezierCurve({ + fromX: fromX + link.xShift, + fromY: fromY, + cpFromX: cpFromX + link.xShift, + cpFromY: cpFromY, + cpToX: cpToX + link.xShift, + cpToY: cpToY, + toX: toX + link.xShift, + toY: toY, + color: link.color, + }); + link.renderedLink = curve; + link.renderedLink.renderable = link.isVisible(); + container.addChild(curve); + }; + + boxFrom.on("pointerdown", () => { + app.stage.on("pointermove", boxFromOnMove); + }); + app.stage.on("pointerup", () => { + app.stage.off("pointermove", boxFromOnMove); + }); + app.stage.on("pointerupoutside", () => { + app.stage.off("pointermove", boxFromOnMove); + }); + + const boxToOnMove = () => { + container.removeChild(curve); + const [fromX, fromY] = fromPoints(link.from); + const [cpFromX, cpFromY, cpToX, cpToY, toX, toY] = toPoints( + link.from, + link.to + ); + curve = bezierCurve({ + fromX: fromX + link.xShift, + fromY: fromY, + cpFromX: cpFromX + link.xShift, + cpFromY: cpFromY, + cpToX: cpToX + link.xShift, + cpToY: cpToY, + toX: toX + link.xShift, + toY: toY, + color: link.color, + }); + link.renderedLink = curve; + link.renderedLink.renderable = link.isVisible(); + container.addChild(curve); + }; + + boxTo.on("pointerdown", () => { + app.stage.on("pointermove", boxToOnMove); + }); + app.stage.on("pointerup", () => { + app.stage.off("pointermove", boxToOnMove); + }); + app.stage.on("pointerupoutside", () => { + app.stage.off("pointermove", boxToOnMove); + }); + container.addChild(curve); - }; - - boxFrom.on("pointerdown", () => { - app.stage.on("pointermove", boxFromOnMove); - }); - app.stage.on("pointerup", () => { - app.stage.off("pointermove", boxFromOnMove); - }); - app.stage.on("pointerupoutside", () => { - app.stage.off("pointermove", boxFromOnMove); - }); - - const boxToOnMove = () => { - container.removeChild(curve); - const [fromX, fromY] = fromPoints(link.from); + } else { + const [fromX, fromY] = fromPoints(link.to); const [cpFromX, cpFromY, cpToX, cpToY, toX, toY] = toPoints( - link.from, - link.to + link.to, + link.from ); - curve = bezierCurve({ + + let curve = bezierCurve({ fromX: fromX + link.xShift, fromY: fromY, cpFromX: cpFromX + link.xShift, @@ -127,20 +162,79 @@ export function drawBezierLink(link) { toY: toY, color: link.color, }); + link.renderedLink = curve; - link.renderedLink.renderable = link.isVisible(); + + const boxFrom = link.to.renderedBox; + const boxTo = link.from.renderedBox; + + const boxFromOnMove = () => { + container.removeChild(curve); + const [fromX, fromY] = fromPoints(link.to); + const [cpFromX, cpFromY, cpToX, cpToY, toX, toY] = toPoints( + link.to, + link.from + ); + curve = bezierCurve({ + fromX: fromX + link.xShift, + fromY: fromY, + cpFromX: cpFromX + link.xShift, + cpFromY: cpFromY, + cpToX: cpToX + link.xShift, + cpToY: cpToY, + toX: toX + link.xShift, + toY: toY, + color: link.color, + }); + link.renderedLink = curve; + link.renderedLink.renderable = link.isVisible(); + container.addChild(curve); + }; + + boxFrom.on("pointerdown", () => { + app.stage.on("pointermove", boxFromOnMove); + }); + app.stage.on("pointerup", () => { + app.stage.off("pointermove", boxFromOnMove); + }); + app.stage.on("pointerupoutside", () => { + app.stage.off("pointermove", boxFromOnMove); + }); + + const boxToOnMove = () => { + container.removeChild(curve); + const [fromX, fromY] = fromPoints(link.to); + const [cpFromX, cpFromY, cpToX, cpToY, toX, toY] = toPoints( + link.to, + link.from + ); + curve = bezierCurve({ + fromX: fromX + link.xShift, + fromY: fromY, + cpFromX: cpFromX + link.xShift, + cpFromY: cpFromY, + cpToX: cpToX + link.xShift, + cpToY: cpToY, + toX: toX + link.xShift, + toY: toY, + color: link.color, + }); + link.renderedLink = curve; + link.renderedLink.renderable = link.isVisible(); + container.addChild(curve); + }; + + boxTo.on("pointerdown", () => { + app.stage.on("pointermove", boxToOnMove); + }); + + app.stage.on("pointerup", () => { + app.stage.off("pointermove", boxToOnMove); + }); + app.stage.on("pointerupoutside", () => { + app.stage.off("pointermove", boxToOnMove); + }); + container.addChild(curve); - }; - - boxTo.on("pointerdown", () => { - app.stage.on("pointermove", boxToOnMove); - }); - app.stage.on("pointerup", () => { - app.stage.off("pointermove", boxToOnMove); - }); - app.stage.on("pointerupoutside", () => { - app.stage.off("pointermove", boxToOnMove); - }); - - container.addChild(curve); + } } diff --git a/js/draw/scroll.js b/js/draw/scroll.js index 7e028dbb..7dd598f4 100644 --- a/js/draw/scroll.js +++ b/js/draw/scroll.js @@ -1,8 +1,18 @@ +import { Graphics } from "../pixi.min.mjs"; import { getApp, getContainerSize, getContainer } from "./app.js"; import { setRenderable } from "./renderable.js"; const SPEED = 0.5; +const scrollBars = { + horizontalThumb: null, + prevHorizontalX: NaN, + verticalThumb: null, + prevVerticalY: NaN, + currentFunction: null, + objects: null, +}; + export const scrollTopLeft = () => { const x = 0; const y = 0; @@ -23,6 +33,208 @@ export const scrollTopCenter = () => { return { x, y }; }; +export const setScroll = (x, y) => { + const container = getContainer(); + container.position.set(x, y); +}; + +function startDragHorizontalThumb(event) { + const app = getApp(); + + scrollBars.prevHorizontalX = + event.data.global.x - scrollBars.horizontalThumb.width / 2; + + app.stage.on("pointermove", moveHorizontalThumb); +} + +function moveHorizontalThumb(event) { + const horizontalScrollBar = scrollBars.horizontalThumb; + const app = getApp(); + const container = getContainer(); + + const { width: containerWidth } = getContainerSize(); + const renderer = app.renderer; + const rendererWidth = renderer.width; + + const x = event.data.global.x - scrollBars.horizontalThumb.width / 2; + const deltaX = (x - scrollBars.prevHorizontalX) * 0.25; + const newThumbX = horizontalScrollBar.x + deltaX; + + if (newThumbX > 0 && newThumbX < rendererWidth - horizontalScrollBar.width) { + horizontalScrollBar.position.set(newThumbX, horizontalScrollBar.y); + const newContainerX = + (horizontalScrollBar.x / rendererWidth) * containerWidth; + container.x = -newContainerX; + scrollBars.prevHorizontalX = newThumbX; + } + + const { objects } = scrollBars; + setRenderable(objects); +} + +function stopHorizontalThumbDrag() { + const app = getApp(); + app.stage.off("pointermove", moveHorizontalThumb); +} + +function startDragVerticalThumb(event) { + const app = getApp(); + + scrollBars.prevVerticalY = + event.data.global.y - scrollBars.verticalThumb.height / 2; + + app.stage.on("pointermove", moveVerticalThumb); +} + +function moveVerticalThumb(event) { + const verticalScrollBar = scrollBars.verticalThumb; + const app = getApp(); + const container = getContainer(); + + const { height: containerHeight } = getContainerSize(); + const renderer = app.renderer; + const rendererHeight = renderer.height; + + const y = event.data.global.y - scrollBars.verticalThumb.height / 2; + const deltaY = (y - scrollBars.prevVerticalY) * 0.25; + const newThumbY = verticalScrollBar.y + deltaY; + + if (newThumbY > 0 && newThumbY < rendererHeight - verticalScrollBar.height) { + verticalScrollBar.position.set(verticalScrollBar.x, newThumbY); + const newContainerY = + (verticalScrollBar.y / rendererHeight) * containerHeight; + container.y = -newContainerY; + scrollBars.prevVerticalY = newThumbY; + } + + const { objects } = scrollBars; + setRenderable(objects); +} + +function stopVerticalThumbDrag() { + const app = getApp(); + app.stage.off("pointermove", moveVerticalThumb); +} + +const addScrollBars = (app, container) => { + const scrollBarColor = "#e1e1e1"; + const renderer = app.renderer; + const rendererWidth = renderer.width; + const rendererHeight = renderer.height; + + const horizontalScrollBarHeight = 7; + const horizontalScrollBarX = 0; + const horizontalScrollBarY = rendererHeight - horizontalScrollBarHeight; + const horizontalScrollBar = new Graphics(); + horizontalScrollBar.rect( + horizontalScrollBarX, + horizontalScrollBarY, + rendererWidth, + horizontalScrollBarHeight + ); + horizontalScrollBar.fill(scrollBarColor); + horizontalScrollBar.zIndex = 4; + + const verticalScrollBarWidth = 7; + const verticalScrollBarX = rendererWidth - verticalScrollBarWidth; + const verticalScrollBarY = 0; + const verticalScrollBar = new Graphics(); + verticalScrollBar.rect( + verticalScrollBarX, + verticalScrollBarY, + verticalScrollBarWidth, + rendererHeight + ); + verticalScrollBar.fill(scrollBarColor); + verticalScrollBar.zIndex = 4; + + app.stage.addChild(horizontalScrollBar); + app.stage.addChild(verticalScrollBar); + + const thumbColor = "#afafaf"; + + const { width: containerWidth, height: containerHeight } = getContainerSize(); + const horizontalThumbWidth = (rendererWidth / containerWidth) * rendererWidth; + const verticalThumbHeight = + (rendererHeight / containerHeight) * rendererHeight; + + const containerX = container.x; + const containerY = container.y; + + const horizontalThumbX = + (Math.abs(containerX) / containerWidth) * rendererWidth; + const verticalThumbY = + (Math.abs(containerY) / containerHeight) * rendererHeight; + + const horizontalThumb = new Graphics(); + horizontalThumb.roundRect( + 0, + 0, + horizontalThumbWidth, + horizontalScrollBarHeight, + 5 + ); + horizontalThumb.fill(thumbColor); + horizontalThumb.zIndex = 5; + horizontalThumb.position.set(horizontalThumbX, horizontalScrollBarY); + horizontalScrollBar.addChild(horizontalThumb); + + const verticalThumb = new Graphics(); + verticalThumb.roundRect(0, 0, verticalScrollBarWidth, verticalThumbHeight, 5); + verticalThumb.fill(thumbColor); + verticalThumb.zIndex = 5; + verticalThumb.position.set(verticalScrollBarX, verticalThumbY); + verticalScrollBar.addChild(verticalThumb); + + scrollBars.horizontalThumb = horizontalThumb; + scrollBars.verticalThumb = verticalThumb; + + horizontalThumb.cursor = "pointer"; + horizontalThumb.eventMode = "static"; + horizontalThumb.interactiveChildren = false; + + horizontalThumb.on("pointerdown", startDragHorizontalThumb); + + verticalThumb.cursor = "pointer"; + verticalThumb.eventMode = "static"; + verticalThumb.interactiveChildren = false; + + verticalThumb.on("pointerdown", startDragVerticalThumb); + + setScrollBarsPosition(); + + return [horizontalScrollBar, verticalScrollBar]; +}; + +export const setScrollBarsPosition = () => { + const app = getApp(); + const renderer = app.renderer; + const rendererWidth = renderer.width; + const rendererHeight = renderer.height; + + const container = getContainer(); + const { width: containerWidth, height: containerHeight } = getContainerSize(); + + const containerX = container.x; + const containerY = container.y; + + const horizontalThumbX = + (Math.abs(containerX) / containerWidth) * rendererWidth; + const verticalThumbY = + (Math.abs(containerY) / containerHeight) * rendererHeight; + + scrollBars.horizontalThumb.position.set( + horizontalThumbX, + scrollBars.horizontalThumb.y + ); + scrollBars.verticalThumb.position.set( + scrollBars.verticalThumb.x, + verticalThumbY + ); + scrollBars.prevHorizontalX = horizontalThumbX; + scrollBars.prevVerticalY = verticalThumbY; +}; + export const addScroll = (app, objects) => { const container = getContainer(); const renderer = app.renderer; @@ -33,6 +245,23 @@ export const addScroll = (app, objects) => { const screenWidth = renderer.width; const screenHeight = renderer.height; + scrollBars.objects = objects; + + let [horizontalScrollBar, verticalScrollBar] = addScrollBars(app, container); + window.addEventListener("resize", () => { + setTimeout(() => { + app.stage.removeChild(horizontalScrollBar); + app.stage.removeChild(verticalScrollBar); + + [horizontalScrollBar, verticalScrollBar] = addScrollBars(app, container); + }); + }); + + app.stage.on("pointerup", stopHorizontalThumbDrag); + app.stage.on("pointerupoutside", stopHorizontalThumbDrag); + app.stage.on("pointerup", stopVerticalThumbDrag); + app.stage.on("pointerupoutside", stopVerticalThumbDrag); + app.canvas.addEventListener("wheel", (e) => { if (e.shiftKey) { const deltaX = parseInt(e.deltaY * SPEED); @@ -42,6 +271,10 @@ export const addScroll = (app, objects) => { newXPosition > screenWidth - getContainerSize().width; if (isXInBounds) { container.x = newXPosition; + const newHorizontalThumbX = + (Math.abs(container.x) / getContainerSize().width) * screenWidth; + scrollBars.horizontalThumb.x = newHorizontalThumbX; + scrollBars.prevHorizontalX = newHorizontalThumbX; } } else { const deltaX = parseInt(e.deltaX * SPEED); @@ -58,10 +291,18 @@ export const addScroll = (app, objects) => { if (isXInBounds) { container.x = newXPosition; + const newHorizontalThumbX = + (Math.abs(container.x) / getContainerSize().width) * screenWidth; + scrollBars.horizontalThumb.x = newHorizontalThumbX; + scrollBars.prevHorizontalX = newHorizontalThumbX; } if (isYInBounds) { container.y = newYPosition; + const newVerticalThumbY = + (Math.abs(container.y) / getContainerSize().height) * screenHeight; + scrollBars.verticalThumb.y = newVerticalThumbY; + scrollBars.prevVerticalY = newVerticalThumbY; } } setRenderable(objects); diff --git a/js/event-number.js b/js/event-number.js index eef474b0..483adee1 100644 --- a/js/event-number.js +++ b/js/event-number.js @@ -2,7 +2,7 @@ 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"; +import { drawView, getView, saveScrollLocation } from "./views/views.js"; const eventNumber = document.getElementById("selected-event"); const previousEvent = document.getElementById("previous-event"); @@ -49,7 +49,7 @@ export function renderEvent(eventNumber) { currentEvent.event = eventNumber; loadSelectedEvent(); updateEventNumber(); - drawCurrentView(); + drawView(getView()); } previousEvent.addEventListener("click", () => { diff --git a/js/filters/collections/cluster.js b/js/filters/collections/cluster.js new file mode 100644 index 00000000..eea1671c --- /dev/null +++ b/js/filters/collections/cluster.js @@ -0,0 +1,50 @@ +import { + addCollectionTitle, + collectionFilterContainer, +} from "../components/lib.js"; +import { magnitudeRangeLogic, RangeComponent } from "../components/range.js"; +import { rangeLogic } from "../components/range.js"; + +function renderClusterFilters() { + const container = collectionFilterContainer(); + const title = addCollectionTitle("Cluster"); + container.appendChild(title); + + const position = new RangeComponent("position", "position", "mm"); + const energy = new RangeComponent("energy", "energy", "GeV"); + + container.appendChild(position.render()); + container.appendChild(energy.render()); + + return { + container, + filters: { + position, + energy, + }, + }; +} + +export function initClusterFilters(parentContainer) { + const { container, filters } = renderClusterFilters(); + const { position, energy } = filters; + + parentContainer.appendChild(container); + + const criteriaFunction = (object) => { + const { min: minPosition, max: maxPosition } = position.getValues(); + const { min: minEnergy, max: maxEnergy } = energy.getValues(); + + if (!magnitudeRangeLogic(minPosition, maxPosition, object, "position")) { + return false; + } + + if (!rangeLogic(minEnergy, maxEnergy, object, "energy")) { + return false; + } + + return true; + }; + + return criteriaFunction; +} diff --git a/js/filters/collections/mcparticle.js b/js/filters/collections/mcparticle.js new file mode 100644 index 00000000..dd5d9653 --- /dev/null +++ b/js/filters/collections/mcparticle.js @@ -0,0 +1,123 @@ +import { + CheckboxComponent, + checkboxLogic, + bitfieldCheckboxLogic, + objectSatisfiesCheckbox, +} from "../components/checkbox.js"; +import { RangeComponent, rangeLogic } from "../components/range.js"; +import { SimStatusBitFieldDisplayValues } from "../../../mappings/sim-status.js"; +import { + addCollectionTitle, + collectionFilterContainer, + createCheckboxContainer, + createCollectionSubtitle, + createSubContainer, +} from "../components/lib.js"; + +function renderMCParticleFilters(viewObjects) { + const container = collectionFilterContainer(); + const title = addCollectionTitle("MC Particle"); + container.appendChild(title); + + const charge = new RangeComponent("charge", "charge", "e"); + const mass = new RangeComponent("mass", "mass", "GeV"); + const momentum = new RangeComponent("momentum", "momentum", "GeV"); + const position = new RangeComponent("position", "position", "mm"); + const time = new RangeComponent("time", "time", "ns"); + const vertex = new RangeComponent("vertex", "vertex", "mm"); + + const range = [charge, mass, momentum, position, time, vertex]; + + range.forEach((rangeFilter) => { + container.appendChild(rangeFilter.render()); + }); + + const checkboxes = { + simStatus: [], + generatorStatus: [], + }; + + const simStatusContainer = createSubContainer(); + const simStatusTitle = createCollectionSubtitle("Simulator Status"); + simStatusContainer.appendChild(simStatusTitle); + const simStatusCheckboxesContainer = createCheckboxContainer(); + + Object.keys(SimStatusBitFieldDisplayValues).forEach((status) => { + const checkbox = new CheckboxComponent( + "simulatorStatus", + status, + SimStatusBitFieldDisplayValues[status] + ); + checkboxes.simStatus.push(checkbox); + simStatusCheckboxesContainer.appendChild(checkbox.render()); + }); + simStatusContainer.appendChild(simStatusCheckboxesContainer); + + const generatorStatusContainer = createSubContainer(); + const generatorStatusTitle = createCollectionSubtitle("Generator Status"); + generatorStatusContainer.appendChild(generatorStatusTitle); + const genStatusCheckboxesContainer = createCheckboxContainer(); + + const generatorStatus = new Set(); + viewObjects.datatypes["edm4hep::MCParticle"].collection.forEach( + (mcparticle) => generatorStatus.add(mcparticle.generatorStatus) + ); + + generatorStatus.forEach((status) => { + const checkbox = new CheckboxComponent( + "generatorStatus", + status, + status, + true + ); + checkboxes.generatorStatus.push(checkbox); + genStatusCheckboxesContainer.appendChild(checkbox.render()); + }); + generatorStatusContainer.appendChild(genStatusCheckboxesContainer); + + container.appendChild(simStatusContainer); + container.appendChild(generatorStatusContainer); + + return { + container, + filters: { + range, + checkboxes, + }, + }; +} + +export function initMCParticleFilters(parentContainer, viewObjects) { + const { container, filters } = renderMCParticleFilters(viewObjects); + const { range, checkboxes } = filters; + + parentContainer.appendChild(container); + + const criteriaFunction = (object) => { + for (const filter of range) { + const { min, max } = filter.getValues(); + if (!rangeLogic(min, max, object, filter.propertyName)) { + return false; + } + } + + const { simStatus, generatorStatus } = checkboxes; + + const someSimStatusCheckbox = objectSatisfiesCheckbox( + object, + simStatus, + "simulatorStatus", + bitfieldCheckboxLogic + ); + const someGenStatusCheckbox = objectSatisfiesCheckbox( + object, + generatorStatus, + "generatorStatus", + checkboxLogic + ); + + return someSimStatusCheckbox && someGenStatusCheckbox; + }; + + return criteriaFunction; +} diff --git a/js/filters/collections/particleid.js b/js/filters/collections/particleid.js new file mode 100644 index 00000000..5af9546d --- /dev/null +++ b/js/filters/collections/particleid.js @@ -0,0 +1,110 @@ +import { + CheckboxComponent, + checkboxLogic, + objectSatisfiesCheckbox, +} from "../components/checkbox.js"; +import { + addCollectionTitle, + collectionFilterContainer, + createCheckboxContainer, + createCollectionSubtitle, + createSubContainer, +} from "../components/lib.js"; + +function renderParticleIdFilters(viewObjects) { + const container = collectionFilterContainer(); + const title = addCollectionTitle("Particle ID"); + container.appendChild(title); + + const checkboxes = { + type: [], + pdg: [], + algorithmType: [], + }; + + const typeContainer = createSubContainer(); + const typeTitle = createCollectionSubtitle("Type"); + typeContainer.appendChild(typeTitle); + const typeCheckboxesContainer = createCheckboxContainer(); + const typeSet = new Set(); + viewObjects.datatypes["edm4hep::ParticleID"].collection.forEach( + (particleId) => typeSet.add(particleId.type) + ); + typeSet.forEach((type) => { + const checkbox = new CheckboxComponent("type", type, type, true); + checkboxes.type.push(checkbox); + typeCheckboxesContainer.appendChild(checkbox.render()); + }); + typeContainer.appendChild(typeCheckboxesContainer); + + const pdgContainer = createSubContainer(); + const pdgTitle = createCollectionSubtitle("PDG"); + pdgContainer.appendChild(pdgTitle); + const pdgCheckboxesContainer = createCheckboxContainer(); + const pdgSet = new Set(); + viewObjects.datatypes["edm4hep::ParticleID"].collection.forEach( + (particleId) => pdgSet.add(particleId.PDG) + ); + pdgSet.forEach((pdg) => { + const checkbox = new CheckboxComponent("PDG", pdg, pdg, true); + checkboxes.pdg.push(checkbox); + pdgCheckboxesContainer.appendChild(checkbox.render()); + }); + pdgContainer.appendChild(pdgCheckboxesContainer); + + const algorithmTypeContainer = createSubContainer(); + const algorithmTypeTitle = createCollectionSubtitle("Algorithm Type"); + algorithmTypeContainer.appendChild(algorithmTypeTitle); + const algorithmTypeCheckboxesContainer = createCheckboxContainer(); + const algorithmTypeSet = new Set(); + viewObjects.datatypes["edm4hep::ParticleID"].collection.forEach( + (particleId) => algorithmTypeSet.add(particleId.algorithmType) + ); + algorithmTypeSet.forEach((algorithmType) => { + const checkbox = new CheckboxComponent( + "algorithmType", + algorithmType, + algorithmType, + true + ); + checkboxes.algorithmType.push(checkbox); + algorithmTypeCheckboxesContainer.appendChild(checkbox.render()); + }); + algorithmTypeContainer.appendChild(algorithmTypeCheckboxesContainer); + + container.appendChild(typeContainer); + container.appendChild(pdgContainer); + container.appendChild(algorithmTypeContainer); + + return { + container, + filters: { + checkboxes, + }, + }; +} + +export function initParticleIdFilters(parentContainer, viewObjects) { + const { container, filters } = renderParticleIdFilters(viewObjects); + const { checkboxes } = filters; + + parentContainer.appendChild(container); + + const criteriaFunction = (particleId) => { + let satisfies = true; + + Object.values(checkboxes).forEach((checkboxes) => { + const res = objectSatisfiesCheckbox( + particleId, + checkboxes, + checkboxes[0].propertyName, + checkboxLogic + ); + satisfies = satisfies && res; + }); + + return satisfies; + }; + + return criteriaFunction; +} diff --git a/js/filters/collections/recoparticle.js b/js/filters/collections/recoparticle.js new file mode 100644 index 00000000..2ce94722 --- /dev/null +++ b/js/filters/collections/recoparticle.js @@ -0,0 +1,50 @@ +import { + addCollectionTitle, + collectionFilterContainer, +} from "../components/lib.js"; +import { RangeComponent } from "../components/range.js"; +import { rangeLogic } from "../components/range.js"; + +function renderRecoParticleFilters() { + const container = collectionFilterContainer(); + const title = addCollectionTitle("Reconstructed Particle"); + container.appendChild(title); + + const energy = new RangeComponent("energy", "energy", "GeV"); + const charge = new RangeComponent("charge", "charge", "e"); + const momentum = new RangeComponent("momentum", "momentum", "GeV"); + + const range = [energy, charge, momentum]; + + range.forEach((rangeFilter) => { + container.appendChild(rangeFilter.render()); + }); + + return { + container, + filters: { + range, + }, + }; +} + +export function initRecoParticleFilters(parentContainer) { + const { container, filters } = renderRecoParticleFilters(); + const { range } = filters; + + parentContainer.appendChild(container); + + const criteriaFunction = (object) => { + for (const filter of range) { + const { min, max } = filter.getValues(); + + if (!rangeLogic(min, max, object, filter.propertyName)) { + return false; + } + } + + return true; + }; + + return criteriaFunction; +} diff --git a/js/filters/collections/track.js b/js/filters/collections/track.js new file mode 100644 index 00000000..12b2219d --- /dev/null +++ b/js/filters/collections/track.js @@ -0,0 +1,41 @@ +import { + addCollectionTitle, + collectionFilterContainer, +} from "../components/lib.js"; +import { RangeComponent, rangeLogic } from "../components/range.js"; + +function renderTrackFilters() { + const container = collectionFilterContainer(); + const title = addCollectionTitle("Track"); + container.appendChild(title); + + const chiNdf = new RangeComponent("chiNdf", "chi^2/ndf", ""); + + container.appendChild(chiNdf.render()); + + return { + container, + filters: { + chiNdf, + }, + }; +} + +export function initTrackFilters(parentContainer) { + const { container, filters } = renderTrackFilters(); + const { chiNdf } = filters; + + parentContainer.appendChild(container); + + const criteriaFunction = (object) => { + const { min: minChiNdf, max: maxChiNdf } = chiNdf.getValues(); + + if (!rangeLogic(minChiNdf, maxChiNdf, object, "chiNdf")) { + return false; + } + + return true; + }; + + return criteriaFunction; +} diff --git a/js/filters/collections/vertex.js b/js/filters/collections/vertex.js new file mode 100644 index 00000000..121f8776 --- /dev/null +++ b/js/filters/collections/vertex.js @@ -0,0 +1,41 @@ +import { + addCollectionTitle, + collectionFilterContainer, +} from "../components/lib.js"; +import { magnitudeRangeLogic, RangeComponent } from "../components/range.js"; + +function renderVertexFilters() { + const container = collectionFilterContainer(); + const title = addCollectionTitle("Vertex"); + container.appendChild(title); + + const position = new RangeComponent("position", "position", "mm"); + + container.appendChild(position.render()); + + return { + container, + filters: { + position, + }, + }; +} + +export function initVertexFilters(parentContainer) { + const { container, filters } = renderVertexFilters(); + const { position } = filters; + + parentContainer.appendChild(container); + + const criteriaFunction = (object) => { + const { min: minPosition, max: maxPosition } = position.getValues(); + + if (!magnitudeRangeLogic(minPosition, maxPosition, object, "position")) { + return false; + } + + return true; + }; + + return criteriaFunction; +} diff --git a/js/filters/components/checkbox.js b/js/filters/components/checkbox.js new file mode 100644 index 00000000..7caaf09c --- /dev/null +++ b/js/filters/components/checkbox.js @@ -0,0 +1,83 @@ +const createCheckboxContainer = () => { + const container = document.createElement("div"); + container.classList.add("checkbox-title-container"); + return container; +}; + +const createCheckbox = () => { + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.classList.add("filter-checkbox"); + return checkbox; +}; + +export class CheckboxComponent { + constructor(propertyName, displayedName, value, firstCheckbox = true) { + this.propertyName = propertyName; + this.displayedName = displayedName; + this.value = value; + this.firstCheckbox = firstCheckbox; + } + + render() { + const div = createCheckboxContainer(); + const checkbox = createCheckbox(); + this.checkbox = checkbox; + const displayedName = document.createElement("label"); + displayedName.textContent = this.displayedName; + + if (this.firstCheckbox) { + div.appendChild(checkbox); + div.appendChild(displayedName); + } else { + div.appendChild(displayedName); + div.appendChild(checkbox); + } + + return div; + } + + getValues() { + return { + checked: this.checkbox.checked, + value: this.value, + }; + } +} + +export function checkboxLogic(value, object, property) { + return object[property] === value; +} + +export function bitfieldCheckboxLogic(value, object, property) { + return (parseInt(object[property]) & (1 << parseInt(value))) !== 0; +} + +export function objectSatisfiesCheckbox( + object, + checkboxes, + property, + logicFunction +) { + const checkedBoxes = []; + + for (const checkbox of checkboxes) { + const { checked, value } = checkbox.getValues(); + + if (checked) { + checkedBoxes.push(value); + } + } + + if (checkedBoxes.length === 0) { + return true; + } + + for (const checked of checkedBoxes) { + if (logicFunction(checked, object, property)) { + return true; + } + } + + return false; +} diff --git a/js/filters/components/lib.js b/js/filters/components/lib.js new file mode 100644 index 00000000..b6451a53 --- /dev/null +++ b/js/filters/components/lib.js @@ -0,0 +1,31 @@ +export function addCollectionTitle(name) { + const title = document.createElement("span"); + title.textContent = name; + title.classList.add("filter-collection-title"); + return title; +} + +export function collectionFilterContainer() { + const container = document.createElement("div"); + container.classList.add("filter-collection-container"); + return container; +} + +export function createCollectionSubtitle(name) { + const title = document.createElement("span"); + title.textContent = name; + title.classList.add("filter-collection-subtitle"); + return title; +} + +export function createSubContainer() { + const container = document.createElement("div"); + container.classList.add("filter-sub-container"); + return container; +} + +export function createCheckboxContainer() { + const container = document.createElement("div"); + container.classList.add("filter-checkbox-container"); + return container; +} diff --git a/js/filters/components/range.js b/js/filters/components/range.js new file mode 100644 index 00000000..5fdd5e52 --- /dev/null +++ b/js/filters/components/range.js @@ -0,0 +1,89 @@ +const createInput = (placeholder) => { + const input = document.createElement("input"); + input.type = "number"; + input.placeholder = placeholder; + input.classList.add("range-input"); + + return input; +}; + +const createUnitElement = (unit) => { + const unitElement = document.createElement("span"); + unitElement.textContent = unit; + unitElement.classList.add("range-unit"); + + return unitElement; +}; + +export class RangeComponent { + constructor(propertyName, displayedName, unit) { + this.propertyName = propertyName; + this.displayedName = displayedName; + this.unit = unit; + } + + render() { + const div = document.createElement("div"); + div.classList.add("range-filter"); + const displayedName = document.createElement("label"); + displayedName.textContent = this.displayedName; + div.appendChild(displayedName); + + const range = document.createElement("div"); + range.classList.add("range-inputs"); + + const min = createInput("min"); + this.min = min; + range.appendChild(min); + range.appendChild(document.createTextNode("-")); + const max = createInput("max"); + this.max = max; + range.appendChild(max); + + div.appendChild(range); + + const unit = createUnitElement(this.unit); + div.appendChild(unit); + + return div; + } + + getValues() { + return { + min: this.min.value, + max: this.max.value, + }; + } +} + +export function rangeLogic(min, max, object, property) { + const minVal = parseFloat(min); + const maxVal = parseFloat(max); + + if (minVal && maxVal) { + return object[property] >= minVal && object[property] <= maxVal; + } else if (minVal) { + return object[property] >= minVal; + } else if (maxVal) { + return object[property] <= maxVal; + } + return true; +} + +export function magnitudeRangeLogic(min, max, object, property) { + const minVal = parseFloat(min); + const maxVal = parseFloat(max); + + const objectMagnitude = Math.sqrt( + Object.values(object[property]).reduce((acc, val) => acc + val ** 2, 0) + ); + + if (minVal && maxVal) { + return objectMagnitude >= minVal && objectMagnitude <= maxVal; + } else if (minVal) { + return objectMagnitude >= minVal; + } else if (maxVal) { + return objectMagnitude <= maxVal; + } + return true; +} diff --git a/js/filters/filter-out.js b/js/filters/filter-out.js new file mode 100644 index 00000000..6392e77d --- /dev/null +++ b/js/filters/filter-out.js @@ -0,0 +1,35 @@ +import { emptyCopyObject } from "../lib/copy.js"; + +export function filterOut( + viewObjects, + viewCurrentObjects, + criteriaFunctions, + inverted = false +) { + emptyCopyObject(viewObjects, viewCurrentObjects); + + const ids = new Set(); + for (const [collection, criteriaFunction] of Object.entries( + criteriaFunctions + )) { + const originalCollection = viewObjects.datatypes[collection].collection; + let filteredCollection; + + if (inverted) { + filteredCollection = originalCollection.filter( + (object) => !criteriaFunction(object) + ); + } else { + filteredCollection = originalCollection.filter((object) => + criteriaFunction(object) + ); + } + + filteredCollection.forEach((object) => + ids.add(`${object.index}-${object.collectionId}`) + ); + viewCurrentObjects.datatypes[collection].collection = filteredCollection; + } + + return ids; +} diff --git a/js/filters/filter.js b/js/filters/filter.js new file mode 100644 index 00000000..be963560 --- /dev/null +++ b/js/filters/filter.js @@ -0,0 +1,114 @@ +import { setScroll, setScrollBarsPosition } from "../draw/scroll.js"; +import { copyObject } from "../lib/copy.js"; +import { initClusterFilters } from "./collections/cluster.js"; +import { initMCParticleFilters } from "./collections/mcparticle.js"; +import { initParticleIdFilters } from "./collections/particleid.js"; +import { initRecoParticleFilters } from "./collections/recoparticle.js"; +import { initTrackFilters } from "./collections/track.js"; +import { initVertexFilters } from "./collections/vertex.js"; +import { filterOut } from "./filter-out.js"; +import { restoreRelations } from "./relations.js"; + +const map = { + "edm4hep::MCParticle": initMCParticleFilters, + "edm4hep::ReconstructedParticle": initRecoParticleFilters, + "edm4hep::Cluster": initClusterFilters, + "edm4hep::Track": initTrackFilters, + "edm4hep::Vertex": initVertexFilters, + "edm4hep::ParticleID": initParticleIdFilters, +}; + +const openFiltersButton = document.getElementById("open-filter"); +const closeFiltersButton = document.getElementById("close-filter"); +const filtersBody = document.getElementById("filters-body"); + +openFiltersButton.addEventListener("click", () => { + filtersBody.style.display = "flex"; + openFiltersButton.style.display = "none"; + closeFiltersButton.style.display = "block"; +}); + +closeFiltersButton.addEventListener("click", () => { + filtersBody.style.display = "none"; + openFiltersButton.style.display = "block"; + closeFiltersButton.style.display = "none"; +}); + +const filters = { + apply: null, + reset: null, +}; + +export function initFilters( + { viewObjects, viewCurrentObjects }, + collections, + reconnectFunction, + { render, filterScroll, originalScroll, setRenderable } +) { + const criteriaFunctions = {}; + + const resetFiltersContent = () => { + const content = document.getElementById("filters-content"); + content.replaceChildren(); + + for (const collection of collections) { + delete criteriaFunctions[collection]; + const init = map[collection]; + if (init) { + const criteriaFunction = init(content, viewObjects); + criteriaFunctions[collection] = criteriaFunction; + } + } + + const filters = document.getElementById("filters"); + if (Object.keys(criteriaFunctions).length === 0) { + filters.style.display = "none"; + } else { + filters.style.display = "block"; + } + + const filterOutCheckbox = document.getElementById("invert-filter"); + filterOutCheckbox.checked = false; + }; + + resetFiltersContent(); + + filters.apply = async () => { + const filterOutValue = document.getElementById("invert-filter").checked; + const ids = filterOut( + viewObjects, + viewCurrentObjects, + criteriaFunctions, + filterOutValue + ); + reconnectFunction(viewCurrentObjects, ids); + await render(viewCurrentObjects); + const { x, y } = filterScroll(); + setScroll(x, y); + setScrollBarsPosition(); + setRenderable(viewCurrentObjects); + }; + filters.reset = async () => { + restoreRelations(viewCurrentObjects); + resetFiltersContent(); + copyObject(viewObjects, viewCurrentObjects); + await render(viewCurrentObjects); + originalScroll(); + setRenderable(viewCurrentObjects); + }; +} + +const applyButton = document.getElementById("filter-apply"); +applyButton.addEventListener("click", () => { + filters.apply(); +}); +applyButton.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + filters.apply(); + } +}); + +const resetButton = document.getElementById("filter-reset"); +resetButton.addEventListener("click", () => { + filters.reset(); +}); diff --git a/js/filters/mcparticle.js b/js/filters/mcparticle.js deleted file mode 100644 index 488725d2..00000000 --- a/js/filters/mcparticle.js +++ /dev/null @@ -1,47 +0,0 @@ -import { Toggle } from "../menu/toggle.js"; -import { togglePDG, toggleId } from "../menu/show-pdg.js"; -import { - bits, - genStatus, - renderRangeParameters, - parametersRange, - renderGenSim, - start, - getWidthFilterContent, -} from "../menu/filter/filter.js"; - -const filter = document.getElementById("filter"); -const filters = document.getElementById("filters"); -const manipulationTools = document.getElementsByClassName("manipulation-tool"); - -export function setupMCParticleFilter(viewObjects, viewCurrentObjects) { - 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 Toggle("show-pdg"); - pdgToggle.init( - () => { - toggleId(viewCurrentObjects); - }, - () => { - togglePDG(viewCurrentObjects); - } - ); - - start(viewObjects, viewCurrentObjects); -} diff --git a/js/filters/nofilter.js b/js/filters/nofilter.js deleted file mode 100644 index 0e41a3fc..00000000 --- a/js/filters/nofilter.js +++ /dev/null @@ -1,7 +0,0 @@ -export function setupNoFilter() { - const manipulationTools = - document.getElementsByClassName("manipulation-tool"); - for (const tool of manipulationTools) { - tool.style.display = "none"; - } -} diff --git a/js/filters/reconnect.js b/js/filters/reconnect.js new file mode 100644 index 00000000..12eb4d90 --- /dev/null +++ b/js/filters/reconnect.js @@ -0,0 +1,44 @@ +export function reconnect( + viewCurrentObjects, + collectionsNames, + ids, + reconnectFunction +) { + for (const collectionName of collectionsNames) { + const { collection, oneToOne, oneToMany } = + viewCurrentObjects.datatypes[collectionName]; + + for (const object of collection) { + const { oneToManyRelations, oneToOneRelations } = object; + object.saveRelations(); + + for (const [relationName, relations] of Object.entries( + oneToManyRelations + )) { + object.oneToManyRelations[relationName] = []; + + for (const relation of relations) { + const toObject = relation.to; + const toObjectId = `${toObject.index}-${toObject.collectionId}`; + + if (ids.has(toObjectId)) { + } else { + } + } + } + + for (const [relationName, relation] of Object.entries( + oneToOneRelations + )) { + object.oneToOneRelations[relationName] = null; + + const toObject = relation.to; + const toObjectId = `${toObject.index}-${toObject.collectionId}`; + + if (ids.has(toObjectId)) { + } else { + } + } + } + } +} diff --git a/js/filters/reconnect/association.js b/js/filters/reconnect/association.js new file mode 100644 index 00000000..6f18380b --- /dev/null +++ b/js/filters/reconnect/association.js @@ -0,0 +1,30 @@ +export function reconnectAssociation(viewCurrentObjects, ids) { + const idsToRemove = new Set(); + + for (const { collection } of Object.values(viewCurrentObjects.datatypes)) { + for (const object of collection) { + const associations = object.associations; + + for (const [associationName, association] of Object.entries( + associations + )) { + const toObject = association.to; + const toId = `${toObject.index}-${toObject.collectionId}`; + + if (ids.has(toId)) { + viewCurrentObjects.associations[associationName].push(association); + } else { + idsToRemove.add(`${object.index}-${object.collectionId}`); + } + } + } + } + + for (const [collectionName, { collection }] of Object.entries( + viewCurrentObjects.datatypes + )) { + viewCurrentObjects.datatypes[collectionName].collection = collection.filter( + (object) => !idsToRemove.has(`${object.index}-${object.collectionId}`) + ); + } +} diff --git a/js/filters/reconnect/mcparticletree.js b/js/filters/reconnect/mcparticletree.js new file mode 100644 index 00000000..72dd0e6f --- /dev/null +++ b/js/filters/reconnect/mcparticletree.js @@ -0,0 +1,88 @@ +import { linkTypes } from "../../types/links.js"; + +const findParentRow = (object, uniqueRows, rowToIndex) => { + const thisRowIndex = rowToIndex[object.row]; + if (thisRowIndex > 0 && thisRowIndex < uniqueRows.length) { + return uniqueRows[thisRowIndex - 1]; + } + return NaN; +}; + +const findDaughterRow = (object, uniqueRows, rowToIndex) => { + const thisRowIndex = rowToIndex[object.row]; + if (thisRowIndex >= 0 && thisRowIndex < uniqueRows.length - 1) { + return uniqueRows[thisRowIndex + 1]; + } + return NaN; +}; + +export function reconnectMCParticleTree(viewCurrentObjects) { + const { collection, oneToMany } = + viewCurrentObjects.datatypes["edm4hep::MCParticle"]; + + const sortedCollection = collection.sort((a, b) => a.row - b.row); + + const beginRowsIndex = {}; + sortedCollection.forEach((object, index) => { + if (beginRowsIndex[object.row] === undefined) { + beginRowsIndex[object.row] = index; + } + }); + + const rows = sortedCollection.map((object) => object.row); + const uniqueRows = [...new Set(rows)]; + + const rowToIndex = {}; + for (const [index, row] of uniqueRows.entries()) { + rowToIndex[row] = index; + } + + const rowToObjectsCount = {}; + + sortedCollection.forEach((object) => { + if (rowToObjectsCount[object.row] === undefined) { + rowToObjectsCount[object.row] = 1; + return; + } + rowToObjectsCount[object.row] += 1; + }); + + for (const object of sortedCollection) { + object.saveRelations(); + + object.oneToManyRelations = { + "parents": [], + "daughters": [], + }; + + const parentRow = findParentRow(object, uniqueRows, rowToIndex); + if (parentRow !== NaN) { + const beginIndex = beginRowsIndex[parentRow]; + const endIndex = beginIndex + rowToObjectsCount[parentRow]; + + for (let i = beginIndex; i < endIndex; i++) { + const newParentLink = new linkTypes["parents"]( + object, + sortedCollection[i] + ); + object.oneToManyRelations["parents"].push(newParentLink); + oneToMany["parents"].push(newParentLink); + } + } + + const daughterRow = findDaughterRow(object, uniqueRows, rowToIndex); + if (daughterRow !== NaN) { + const beginIndex = beginRowsIndex[daughterRow]; + const endIndex = beginIndex + rowToObjectsCount[daughterRow]; + + for (let i = beginIndex; i < endIndex; i++) { + const newDaughterLink = new linkTypes["daughters"]( + object, + sortedCollection[i] + ); + object.oneToManyRelations["daughters"].push(newDaughterLink); + oneToMany["daughters"].push(newDaughterLink); + } + } + } +} diff --git a/js/filters/reconnect/mixed.js b/js/filters/reconnect/mixed.js new file mode 100644 index 00000000..49a54327 --- /dev/null +++ b/js/filters/reconnect/mixed.js @@ -0,0 +1,58 @@ +export function reconnectMixedViews(viewCurrentObjects, ids) { + const idsToRemove = new Set(); + + for (const { collection, oneToMany, oneToOne } of Object.values( + viewCurrentObjects.datatypes + )) { + { + for (const object of collection) { + const { oneToManyRelations, oneToOneRelations } = object; + object.saveRelations(); + + const objectId = `${object.index}-${object.collectionId}`; + + object.oneToManyRelations = {}; + object.oneToOneRelations = {}; + + for (const [relationName, relations] of Object.entries( + oneToManyRelations + )) { + object.oneToManyRelations[relationName] = []; + for (const relation of relations) { + const { to } = relation; + const toId = `${to.index}-${to.collectionId}`; + + if (ids.has(toId)) { + oneToMany[relationName].push(relation); + object.oneToManyRelations[relationName].push(relation); + } else { + idsToRemove.add(objectId); + } + } + } + + for (const [relationName, relation] of Object.entries( + oneToOneRelations + )) { + const { to } = relation; + const toId = `${to.index}-${to.collectionId}`; + + if (ids.has(toId)) { + oneToOne[relationName].push(relation); + object.oneToOneRelations[relationName] = relation; + } else { + idsToRemove.add(objectId); + } + } + } + } + } + + for (const [collectionName, { collection }] of Object.entries( + viewCurrentObjects.datatypes + )) { + viewCurrentObjects.datatypes[collectionName].collection = collection.filter( + (object) => !idsToRemove.has(`${object.index}-${object.collectionId}`) + ); + } +} diff --git a/js/filters/reconnect/tree.js b/js/filters/reconnect/tree.js new file mode 100644 index 00000000..b6df8042 --- /dev/null +++ b/js/filters/reconnect/tree.js @@ -0,0 +1,66 @@ +import { datatypes } from "../../../output/datatypes.js"; +import { linkTypes } from "../../types/links.js"; + +export function reconnectTree(viewCurrentObjects, ids) { + const tree = Object.entries(viewCurrentObjects.datatypes).filter( + ([_, { collection }]) => collection.length !== 0 + )[0]; + const collectionName = tree[0]; + const { collection: unsortedCollection } = tree[1]; + const sortedCollection = unsortedCollection.sort((a, b) => a.row - b.row); + const rows = sortedCollection.map((object) => object.row); + const uniqueRows = [...new Set(rows)]; + uniqueRows.sort((a, b) => a - b); + + const beginRowsIndex = {}; + sortedCollection.forEach((object, index) => { + if (beginRowsIndex[object.row] === undefined) { + beginRowsIndex[object.row] = index; + } + }); + + const rowsCount = {}; + rows.forEach((row) => { + if (rowsCount[row] === undefined) { + rowsCount[row] = 1; + return; + } + rowsCount[row] += 1; + }); + + // Assuming al trees are oneToManyRelations + const relationName = datatypes[collectionName].oneToManyRelations.filter( + ({ type }) => type === collectionName + )[0].name; + const relationClass = linkTypes[relationName]; + + for (const object of sortedCollection) { + object.saveRelations(); + + object.oneToManyRelations = { + [relationName]: [], + }; + + const objectRow = object.row; + const nextRow = objectRow + 1; + + if (beginRowsIndex[nextRow] !== undefined) { + const beginIndex = beginRowsIndex[nextRow]; + const count = rowsCount[nextRow]; + const endIndex = beginIndex + count; + + for (let i = beginIndex; i < endIndex; i++) { + const daughter = sortedCollection[i]; + const daughterId = `${daughter.index}-${daughter.collectionId}`; + + if (ids.has(daughterId)) { + const relation = new relationClass(object, daughter); + object.oneToManyRelations[relationName].push(relation); + viewCurrentObjects.datatypes[collectionName].oneToMany[ + relationName + ].push(relation); + } + } + } + } +} diff --git a/js/filters/relations.js b/js/filters/relations.js new file mode 100644 index 00000000..960799a0 --- /dev/null +++ b/js/filters/relations.js @@ -0,0 +1,7 @@ +export function restoreRelations(objects) { + for (const { collection } of Object.values(objects.datatypes)) { + for (const object of collection) { + object.restoreRelations(); + } + } +} diff --git a/js/lib/generate-svg.js b/js/lib/generate-svg.js index 872c34b1..af23b5cf 100644 --- a/js/lib/generate-svg.js +++ b/js/lib/generate-svg.js @@ -1,17 +1,28 @@ -const SCALE = 1.25; - const store = {}; -export async function textToSVG(id, text, size) { +export async function textToSVG(id, text, maxWidth, maxHeight) { if (store[id]) { return store[id]; } - const mathjaxContainer = await MathJax.tex2svgPromise(`${text}`); + const mathjaxContainer = await MathJax.tex2svgPromise(`${text}`, {}); const svg = mathjaxContainer.firstElementChild; - svg.setAttribute("width", `${parseInt(size * SCALE)}px`); - svg.setAttribute("height", `${parseInt(size * SCALE)}px`); + const width = parseFloat(svg.getAttribute("width").replace("ex", "")); + const height = parseFloat(svg.getAttribute("height").replace("ex", "")); + + const imageRatio = width / height; + + let finalHeight = maxHeight; + let finalWidth = parseInt(finalHeight * imageRatio); + + if (finalWidth > maxWidth) { + finalWidth = maxWidth; + finalHeight = parseInt(finalWidth / imageRatio); + } + + svg.setAttribute("width", `${finalWidth}px`); + svg.setAttribute("height", `${finalHeight}px`); const src = "data:image/svg+xml;base64," + diff --git a/js/main.js b/js/main.js index 271600ad..1eeb53c7 100644 --- a/js/main.js +++ b/js/main.js @@ -34,7 +34,7 @@ function showEventSwitcher() { } function showViewsMenu() { - const viewsMenu = document.getElementById("views"); + const viewsMenu = document.getElementById("left-menu"); const aboutButton = document.getElementById("information-button"); viewsMenu.style.display = "flex"; @@ -47,6 +47,12 @@ function hideDeploySwitch() { deploySwitch.style.display = "none"; } +function showFilters() { + const filters = document.getElementById("filters"); + + filters.style.display = "block"; +} + document.getElementById("input-file").addEventListener("change", (event) => { for (const file of event.target.files) { if (!file.name.endsWith("edm4hep.json")) { @@ -143,6 +149,7 @@ document showEventSwitcher(); showViewsMenu(); showFileNameMenu(); + showFilters(); selectViewInformation(); renderEvent(eventNum); }); diff --git a/js/menu/filter/builders.js b/js/menu/filter/builders.js deleted file mode 100644 index 6f9cf82b..00000000 --- a/js/menu/filter/builders.js +++ /dev/null @@ -1,56 +0,0 @@ -import { ValueCheckBox, BitfieldCheckbox } from "./parameters.js"; - -export class CheckboxBuilder { - constructor(name, fullName) { - this.uniqueValues = new Set(); - this.checkBoxes = []; - this.name = name; - this.fullName = fullName; - } - - add(val) { - this.uniqueValues.add(val); - } - - setCheckBoxes() { - this.checkBoxes = Array.from(this.uniqueValues).map( - (option) => new ValueCheckBox(this.name, option) - ); - this.checkBoxes.sort((a, b) => a.value - b.value); - } - - render(container) { - const section = document.createElement("div"); - section.style.maxWidth = "fit-content"; - this.checkBoxes.forEach((checkbox) => (checkbox.checked = false)); - const title = document.createElement("p"); - title.style.fontWeight = "bold"; - title.textContent = this.fullName; - section.appendChild(title); - const options = document.createElement("div"); - options.style.display = "flex"; - options.style.flexDirection = "row"; - options.style.flexWrap = "wrap"; - section.appendChild(options); - container.appendChild(section); - this.checkBoxes.forEach((checkbox) => checkbox.render(options)); - } - - reset() { - this.uniqueValues = new Set(); - } -} - -export class BitFieldBuilder extends CheckboxBuilder { - constructor(name, fullName, dictionary) { - super(name, fullName); - this.dictionary = dictionary; - } - - setCheckBoxes() { - this.checkBoxes = Object.entries(this.dictionary).map( - ([key, value]) => new BitfieldCheckbox(this.name, key, value) - ); - this.checkBoxes.sort((a, b) => a.value - b.value); - } -} diff --git a/js/menu/filter/filter.js b/js/menu/filter/filter.js deleted file mode 100644 index 8b518ad9..00000000 --- a/js/menu/filter/filter.js +++ /dev/null @@ -1,129 +0,0 @@ -import { CheckboxBuilder, BitFieldBuilder } from "./builders.js"; -import { Range, Checkbox, buildCriteriaFunction } from "./parameters.js"; -import { reconnect } from "./reconnect.js"; -import { units } from "../../types/units.js"; -import { copyObject } from "../../lib/copy.js"; -import { SimStatusBitFieldDisplayValues } from "../../../mappings/sim-status.js"; -import { renderObjects } from "../../draw/render.js"; - -const filterButton = document.getElementById("filter-button"); -const openFilter = document.getElementById("open-filter"); -const closeFilter = document.getElementById("close-filter"); -const filterContent = document.getElementById("filter-content"); -const filters = document.getElementById("filters"); -const apply = document.getElementById("filter-apply"); -const reset = document.getElementById("filter-reset"); - -let active = false; - -export function renderRangeParameters(rangeParameters) { - const rangeFilters = document.createElement("div"); - rangeFilters.id = "range-filters"; - rangeFilters.style.display = "grid"; - rangeFilters.style.width = "fit-content"; - rangeFilters.style.columnGap = "10px"; - rangeFilters.style.rowGap = "5px"; - rangeFilters.style.alignItems = "center"; - rangeFilters.style.gridTemplateColumns = - "fit-content(100%) fit-content(100%)"; - rangeParameters.forEach((parameter) => { - parameter.min = undefined; - parameter.max = undefined; - parameter.render(rangeFilters); - }); - filters.appendChild(rangeFilters); -} - -export function getWidthFilterContent() { - const filterContent = document.getElementById("filter-content"); - filterContent.style.display = "flex"; - const rangeFilters = document.getElementById("range-filters"); - const width = rangeFilters.offsetWidth; - filterContent.style.display = "none"; - return `${width}px`; -} - -export function renderGenSim(sim, gen) { - const div = document.createElement("div"); - div.style.display = "flex"; - div.style.flexDirection = "column"; - div.style.width = "fit-content"; - div.style.alignItems = "start"; - sim.render(div); - gen.render(div); - filters.appendChild(div); -} - -let parametersRange = units.sort((a, b) => - a.property.localeCompare(b.property) -); - -parametersRange = parametersRange.map((parameter) => new Range(parameter)); - -const bits = new BitFieldBuilder( - "simulatorStatus", - "Simulation status", - SimStatusBitFieldDisplayValues -); -bits.setCheckBoxes(); - -const genStatus = new CheckboxBuilder("generatorStatus", "Generator status"); - -function applyFilter(loadedObjects, currentObjects) { - const rangeFunctions = Range.buildFilter(parametersRange); - const checkboxFunctions = Checkbox.buildFilter(bits.checkBoxes); - const genStatusFunctions = Checkbox.buildFilter(genStatus.checkBoxes); - - const criteriaFunction = buildCriteriaFunction( - rangeFunctions, - checkboxFunctions, - genStatusFunctions - ); - - const filteredObjects = reconnect(criteriaFunction, loadedObjects); - - copyObject(filteredObjects, currentObjects); - renderObjects(currentObjects); -} - -function removeFilter(loadedObjects, currentObjects) { - copyObject(loadedObjects, currentObjects); - renderObjects(currentObjects); - - filters.innerHTML = ""; - - renderRangeParameters(parametersRange); - renderGenSim(bits, genStatus); -} - -export function start(loadedObjects, currentObjects) { - 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"; - } - }); - - apply.addEventListener("click", () => - applyFilter(loadedObjects, currentObjects) - ); - - document.addEventListener("keydown", (event) => { - if (event.key === "Enter" && active) { - applyFilter(loadedObjects, currentObjects); - } - }); - - reset.addEventListener("click", () => - removeFilter(loadedObjects, currentObjects) - ); -} - -export { bits, genStatus, parametersRange }; diff --git a/js/menu/filter/parameters.js b/js/menu/filter/parameters.js deleted file mode 100644 index 7555cbb0..00000000 --- a/js/menu/filter/parameters.js +++ /dev/null @@ -1,221 +0,0 @@ -class FilterParameter { - property; - - constructor(property) { - this.property = property; - } - - render(container) {} - - buildCondition() {} - - static parametersFunctions(parameters) { - const functions = parameters.map((parameter) => parameter.buildCondition()); - return functions.filter((fn) => fn); - } -} - -function createNumberInput(placeholder) { - const input = document.createElement("input"); - input.type = "number"; - input.placeholder = placeholder; - input.style.width = "35px"; - - return input; -} - -export class Range extends FilterParameter { - min; - max; - - constructor({ property, unit }) { - super(property); - this.unit = unit; - } - - render(container) { - const label = document.createElement("label"); - label.textContent = `${this.property}`; - - const inputMin = createNumberInput("min"); - inputMin.addEventListener("input", (e) => { - this.min = e.target.value; - }); - - const separator = document.createTextNode("-"); - - const inputMax = createNumberInput("max"); - inputMax.addEventListener("input", (e) => { - this.max = e.target.value; - }); - - const unitElement = document.createTextNode(`${this.unit}`); - - const content = document.createElement("div"); - content.appendChild(inputMin); - content.appendChild(separator); - content.appendChild(inputMax); - content.appendChild(unitElement); - content.style.display = "grid"; - content.style.gridAutoFlow = "column"; - content.style.columnGap = "5px"; - content.style.display = "flex"; - content.style.flexDirection = "row"; - content.style.justifyContent = "flex-start"; - - container.appendChild(label); - container.appendChild(content); - } - - buildCondition() { - if (!this.min && !this.max) return null; - - return (particle) => { - if (particle) { - if (particle[this.property] < this.min) { - return false; - } - - if (particle[this.property] > this.max) { - return false; - } - - return true; - } - }; - } - - static buildFilter(parametersRange) { - const rangeFunctions = Range.parametersFunctions(parametersRange); - - const func = rangeFunctions.reduce( - (acc, fn) => { - return (particle) => acc(particle) && fn(particle); - }, - () => true - ); - - return func; - } -} - -export class Checkbox extends FilterParameter { - value; - - constructor(property, value, displayValue = null) { - super(property); - this.value = value; - if (displayValue) { - this.displayValue = displayValue; - } else { - this.displayValue = value; - } - } - - render(container) { - const div = document.createElement("div"); - container.appendChild(div); - - const label = document.createElement("label"); - label.textContent = this.displayValue; - div.appendChild(label); - - const input = document.createElement("input"); - input.type = "checkbox"; - div.appendChild(input); - - div.style.display = "flex"; - div.style.flexDirection = "row"; - div.style.alignItems = "center"; - div.style.backgroundColor = "#dddddd"; - div.style.borderRadius = "5px"; - div.style.margin = "3px"; - - input.addEventListener("change", () => { - this.checked = input.checked; - }); - } - - buildCondition() { - if (!this.checked) return null; - - return (particle) => particle[this.property] === this.value; - } - - static buildFilter(parametersCheckbox) { - const checkboxFunctions = Checkbox.parametersFunctions(parametersCheckbox); - - if (checkboxFunctions.length === 0) return () => true; - - const func = checkboxFunctions.reduce( - (acc, fn) => { - return (particle) => acc(particle) || fn(particle); - }, - () => false - ); - - return func; - } -} - -export class ValueCheckBox extends Checkbox { - // Classic checkbox - constructor(property, value, displayValue) { - super(property, value, displayValue); - } -} - -export class BitfieldCheckbox extends Checkbox { - // Bit manipulation EDM4hep - constructor(property, value, displayValue) { - super(property, value, displayValue); - } - - buildCondition() { - if (!this.checked) return null; - - return (particle) => - (parseInt(particle[this.property]) & (1 << parseInt(this.value))) !== 0; - } - - render(container) { - const div = document.createElement("div"); - container.appendChild(div); - - const input = document.createElement("input"); - input.type = "checkbox"; - div.appendChild(input); - - const label = document.createElement("label"); - label.textContent = this.displayValue; - div.appendChild(label); - - div.style.display = "flex"; - div.style.flexDirection = "row"; - div.style.alignItems = "center"; - div.style.backgroundColor = "#dddddd"; - div.style.borderRadius = "5px"; - div.style.margin = "3px"; - - input.addEventListener("change", () => { - this.checked = input.checked; - }); - } - - static getDisplayValue(dictionary, option) { - return dictionary[option] ?? option; - } -} - -export function buildCriteriaFunction(...functions) { - const filterFunctions = functions.filter((fn) => typeof fn === "function"); - - const finalFunction = filterFunctions.reduce( - (acc, fn) => { - return (particle) => acc(particle) && fn(particle); - }, - () => true - ); - - return (particle) => finalFunction(particle); -} diff --git a/js/menu/filter/reconnect.js b/js/menu/filter/reconnect.js deleted file mode 100644 index 28ccac22..00000000 --- a/js/menu/filter/reconnect.js +++ /dev/null @@ -1,16 +0,0 @@ -import { emptyCopyObject } from "../../lib/copy.js"; -import { objectTypes } from "../../types/objects.js"; - -export function reconnect(criteriaFunction, loadedObjects) { - const filteredObjects = {}; - - emptyCopyObject(loadedObjects, filteredObjects); - - const filterFunction = objectTypes["edm4hep::MCParticle"].filter; - - const mcParticles = loadedObjects.datatypes["edm4hep::MCParticle"]; - - filterFunction(mcParticles, filteredObjects.datatypes, criteriaFunction); - - return filteredObjects; -} diff --git a/js/menu/toggle.js b/js/menu/toggle.js deleted file mode 100644 index 6d2136e4..00000000 --- a/js/menu/toggle.js +++ /dev/null @@ -1,25 +0,0 @@ -const prev = { - function: null, -}; - -export class Toggle { - constructor(id) { - this.isSliderActive = false; - this.slider = document.getElementById(id); - } - - init(activeFunction, inactiveFunction) { - const newFunction = () => { - this.isSliderActive = !this.isSliderActive; - if (this.isSliderActive) { - activeFunction(); - } else { - inactiveFunction(); - } - }; - - this.slider.removeEventListener("click", prev.function); - this.slider.addEventListener("click", newFunction); - prev.function = newFunction; - } -} diff --git a/js/menu/show-pdg.js b/js/toggle/show-pdg.js similarity index 100% rename from js/menu/show-pdg.js rename to js/toggle/show-pdg.js diff --git a/js/toggle/toggle.js b/js/toggle/toggle.js new file mode 100644 index 00000000..ebff7081 --- /dev/null +++ b/js/toggle/toggle.js @@ -0,0 +1,74 @@ +import { togglePDG, toggleId } from "./show-pdg.js"; + +export class Toggle { + constructor(elementId, swicthId) { + this.elementId = elementId; + this.swicthId = swicthId; + this.isSliderActive = false; + } + + setActions(activeFunction, inactiveFunction) { + const toggle = document.getElementById(this.swicthId); + + toggle.addEventListener("click", () => { + this.isSliderActive = !this.isSliderActive; + const viewCurrentObjects = this.getViewCurrentObjects(); + + if (this.isSliderActive) { + activeFunction(viewCurrentObjects); + } else { + inactiveFunction(viewCurrentObjects); + } + }); + } + + display() { + const toggle = document.getElementById(this.elementId); + toggle.style.display = "flex"; + } + + setViewCurrentObjects(viewCurrentObjects) { + this.viewCurrentObjects = viewCurrentObjects; + } + + getViewCurrentObjects() { + return this.viewCurrentObjects; + } +} + +const pdgToggle = new Toggle("pdg-toggle", "pdg-toggle-switch"); +pdgToggle.setActions(toggleId, togglePDG); + +const togglesPerCollection = { + "edm4hep::MCParticle": [pdgToggle], +}; + +export function setupToggles(collections, viewCurrentObjects) { + const allToggles = document.getElementsByClassName("toggle"); + + for (const toggle of allToggles) { + toggle.style.display = "none"; + } + + let displayedToggles = 0; + + for (const collection of collections) { + const togglesFromCollection = togglesPerCollection[collection]; + + if (!togglesFromCollection) { + continue; + } + + for (const toggle of togglesFromCollection) { + toggle.display(); + toggle.setViewCurrentObjects(viewCurrentObjects); + displayedToggles++; + } + } + + if (displayedToggles === 0) { + document.getElementById("toggles").style.display = "none"; + } else { + document.getElementById("toggles").style.display = "block"; + } +} diff --git a/js/types/links.js b/js/types/links.js index 5427cfc0..c3d08231 100644 --- a/js/types/links.js +++ b/js/types/links.js @@ -58,11 +58,13 @@ export class Link { class ParentLink extends Link { constructor(from, to) { - super(to, from); + super(from, to); this.color = colors["parents"]; this.xShift = 3; - // parent is this.from - // current object is this.to + } + + draw() { + drawBezierLink(this, true); } } @@ -71,8 +73,6 @@ class DaughterLink extends Link { super(from, to); this.color = colors["daughters"]; this.xShift = -3; - // current object is this.from - // daughter is this.to } } diff --git a/js/types/objects.js b/js/types/objects.js index cad6c032..a7cbf95a 100644 --- a/js/types/objects.js +++ b/js/types/objects.js @@ -1,5 +1,4 @@ import { getName } from "../lib/getName.js"; -import { linkTypes } from "./links.js"; import { parseCharge } from "../lib/parseCharge.js"; import { getSimStatusDisplayValuesFromBit } from "../../mappings/sim-status.js"; import { @@ -73,6 +72,27 @@ class EDMObject { y < this.y + this.height ); } + + saveRelations() { + const relations = {}; + + if (!this.relations) { + relations.oneToManyRelations = this.oneToManyRelations; + relations.oneToOneRelations = this.oneToOneRelations; + this.relations = relations; + + this.oneToManyRelations = {}; + this.oneToOneRelations = {}; + } + } + + restoreRelations() { + if (this.relations) { + this.oneToManyRelations = this.relations.oneToManyRelations; + this.oneToOneRelations = this.relations.oneToOneRelations; + } + this.relations = null; + } } export class MCParticle extends EDMObject { @@ -82,7 +102,8 @@ export class MCParticle extends EDMObject { this.texImg = null; this.color = "#dff6ff"; this.radius = 15; - this.height = 270; + this.width = 135; + this.height = 280; this.titleName = "MCParticle"; } @@ -132,8 +153,8 @@ export class MCParticle extends EDMObject { async drawImage(text, imageY) { const id = `${text}-${IMAGE_SIZE}`; - const src = await textToSVG(id, text, IMAGE_SIZE); - const sprite = await svgElementToPixiSprite(id, src, IMAGE_SIZE); + const src = await textToSVG(id, text, this.width * 0.9, IMAGE_SIZE); + const sprite = await svgElementToPixiSprite(id, src); this.image = sprite; addImageToBox(sprite, this.renderedBox, imageY); } @@ -158,8 +179,10 @@ export class MCParticle extends EDMObject { return isVisible; } - static setup(mcCollection) { - for (const mcParticle of mcCollection) { + static setRows(mcCollection) { + mcCollection.forEach((mcParticle) => { + mcParticle.row = -1; + const parentLength = mcParticle.oneToManyRelations["parents"].length; const daughterLength = mcParticle.oneToManyRelations["daughters"].length; @@ -171,7 +194,11 @@ export class MCParticle extends EDMObject { if (parentLength === 0) { mcParticle.row = 0; } + }); + } + static setup(mcCollection) { + for (const mcParticle of mcCollection) { const name = getName(mcParticle.PDG); mcParticle.name = name; mcParticle.textToRender = name; @@ -192,52 +219,6 @@ export class MCParticle extends EDMObject { mcParticle.mass = Math.round(mcParticle.mass * 100) / 100; } } - - static filter({ collection }, filteredObjects, criteriaFunction) { - for (const mcParticle of collection) { - if (!criteriaFunction(mcParticle)) { - const parentParticles = mcParticle.oneToManyRelations["parents"] - .map((link) => link.from) - .filter((parent) => criteriaFunction(parent)); - const childrenParticles = mcParticle.oneToManyRelations["daughters"] - .map((link) => link.to) - .filter((child) => criteriaFunction(child)); - - for (const parent of parentParticles) { - for (const child of childrenParticles) { - const linkToParent = new linkTypes["parents"](child, parent); - - const linkToChild = new linkTypes["daughters"](parent, child); - - filteredObjects["edm4hep::MCParticle"].oneToMany["parents"].push( - linkToParent - ); - filteredObjects["edm4hep::MCParticle"].oneToMany["daughters"].push( - linkToChild - ); - } - } - } else { - filteredObjects["edm4hep::MCParticle"].collection.push(mcParticle); - - for (const link of mcParticle.oneToManyRelations["parents"]) { - if (criteriaFunction(link.from)) { - filteredObjects["edm4hep::MCParticle"].oneToMany["parents"].push( - link - ); - } - } - - for (const link of mcParticle.oneToManyRelations["daughters"]) { - if (criteriaFunction(link.to)) { - filteredObjects["edm4hep::MCParticle"].oneToMany["daughters"].push( - link - ); - } - } - } - } - } } class ReconstructedParticle extends EDMObject { @@ -270,8 +251,6 @@ class ReconstructedParticle extends EDMObject { } static setup(recoCollection) {} - - static filter() {} } class Cluster extends EDMObject { @@ -324,6 +303,7 @@ class Track extends EDMObject { const chi2 = parseInt(this.chi2 * 100) / 100; const ndf = parseInt(this.ndf * 100) / 100; const chiNdf = `${chi2}/${ndf}`; + this.chiNdf = chiNdf; lines.push("chi2/ndf = " + chiNdf); lines.push("dEdx = " + this.dEdx); const trackerHitsCount = this.oneToManyRelations["trackerHits"].length; @@ -339,7 +319,7 @@ class ParticleID extends EDMObject { constructor() { super(); this.width = 140; - this.height = 140; + this.height = 160; this.color = "#c9edf7"; this.radius = 25; this.titleName = "Particle ID"; diff --git a/js/views/mcparticletree.js b/js/views/templates/mcparticletree.js similarity index 65% rename from js/views/mcparticletree.js rename to js/views/templates/mcparticletree.js index 4e12ed03..445fae1d 100644 --- a/js/views/mcparticletree.js +++ b/js/views/templates/mcparticletree.js @@ -1,13 +1,16 @@ -import { preFilterTree } from "../filters/pre-filter.js"; +import { preFilterTree } from "../../filters/pre-filter.js"; +import { MCParticle } from "../../types/objects.js"; export function mcParticleTree(viewCurrentObjects) { const mcCollection = viewCurrentObjects.datatypes["edm4hep::MCParticle"].collection ?? []; + MCParticle.setRows(mcCollection); + const getMaxRow = (parentLinks) => { let maxRow = -1; for (const parentLink of parentLinks) { - const parent = parentLink.from; + const parent = parentLink.to; if (parent.row === -1) { return -1; } @@ -59,36 +62,16 @@ export function mcParticleTree(viewCurrentObjects) { const horizontalGap = boxWidth * 0.4; const verticalGap = boxHeight * 0.3; - const width = - boxWidth * (maxRowWidth + 1) + horizontalGap * (maxRowWidth + 1); + let width = boxWidth * (maxRowWidth + 1) + horizontalGap * (maxRowWidth + 2); + if (width < window.innerWidth) { + width = window.innerWidth; + } const 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 = - width / 2 - - boxWidth - - horizontalGap / 2 + - (distanceFromCenter + 1) * boxWidth + - (distanceFromCenter + 1) * horizontalGap; - } else { - box.x = - width / 2 + - horizontalGap / 2 + - distanceFromCenter * boxWidth + - distanceFromCenter * horizontalGap; - } - } else { - const distanceFromCenter = j - row.length / 2; - box.x = - width / 2 - - boxWidth / 2 + - distanceFromCenter * boxWidth + - distanceFromCenter * horizontalGap; - } + const half = Math.floor(row.length / 2); + box.x = width / 2 - (half - j) * (boxWidth + horizontalGap); box.y = i * verticalGap + verticalGap + i * boxHeight; } } diff --git a/js/views/templates/onewayview.js b/js/views/templates/onewayview.js index 3bcd1b4c..3f749a60 100644 --- a/js/views/templates/onewayview.js +++ b/js/views/templates/onewayview.js @@ -5,6 +5,10 @@ export function oneWayView(viewObjects, fromCollectionName, relationName) { const fromCollection = relations.map((relation) => relation.from); const toCollection = relations.map((relation) => relation.to); + if (fromCollection.length === 0 || toCollection.length === 0) { + return [0, 0]; + } + const fromWidth = fromCollection[0].width; const toWidth = toCollection[0].width; const fromHorizontalGap = 0.3 * fromWidth; diff --git a/js/views/recoclustertrack.js b/js/views/templates/recoclustertrack.js similarity index 97% rename from js/views/recoclustertrack.js rename to js/views/templates/recoclustertrack.js index 40d95cbe..f5533808 100644 --- a/js/views/recoclustertrack.js +++ b/js/views/templates/recoclustertrack.js @@ -1,9 +1,13 @@ -import { emptyCopyObject } from "../lib/copy.js"; +import { emptyCopyObject } from "../../lib/copy.js"; export function recoClusterTrackVertex(viewObjects) { const recoParticles = viewObjects.datatypes["edm4hep::ReconstructedParticle"].collection; + if (recoParticles.length === 0) { + return [0, 0]; + } + const findFirstObject = (relationName) => { const object = recoParticles.find((particle) => { const relation = particle.oneToManyRelations[relationName]; @@ -11,6 +15,10 @@ export function recoClusterTrackVertex(viewObjects) { return relation[0].to; } }); + + if (!object) { + return { width: 0, height: 0 }; + } return object; }; diff --git a/js/views/views-dictionary.js b/js/views/views-dictionary.js index d67a2216..1b9b5642 100644 --- a/js/views/views-dictionary.js +++ b/js/views/views-dictionary.js @@ -1,7 +1,6 @@ -import { mcParticleTree, preFilterMCTree } from "./mcparticletree.js"; +import { mcParticleTree, preFilterMCTree } from "./templates/mcparticletree.js"; import { mcRecoAssociation, preFilterMCReco } from "./mcrecoassociation.js"; import { recoParticleTree, preFilterRecoTree } from "./recoparticletree.js"; -import { setupMCParticleFilter } from "../filters/mcparticle.js"; import { trackTree, preFilterTrackTree } from "./tracktree.js"; import { clusterTree, preFilterClusterTree } from "./clustertree.js"; import { preFilterMCTrack, mcTrackAssociation } from "./mctrackassociation.js"; @@ -12,20 +11,24 @@ import { import { recoClusterTrackVertex, preFilterRecoClusterTrackVertex, -} from "./recoclustertrack.js"; -import { setupNoFilter } from "../filters/nofilter.js"; +} from "./templates/recoclustertrack.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"; import { scrollTopCenter, scrollTopLeft } from "../draw/scroll.js"; +import { reconnectMCParticleTree } from "../filters/reconnect/mcparticletree.js"; +import { reconnectAssociation } from "../filters/reconnect/association.js"; +import { reconnectTree } from "../filters/reconnect/tree.js"; +import { reconnectMixedViews } from "../filters/reconnect/mixed.js"; export const views = { "Monte Carlo Particle Tree": { - filters: setupMCParticleFilter, viewFunction: mcParticleTree, scrollFunction: scrollTopCenter, preFilterFunction: preFilterMCTree, + reconnectFunction: reconnectMCParticleTree, + collections: ["edm4hep::MCParticle"], description: `

${spanWithColor( "Red", "#AA0000" @@ -35,34 +38,43 @@ export const views = { )} relations mean daughter relation (from top to bottom).

`, }, "Reconstructed Particle Tree": { - filters: setupNoFilter, viewFunction: recoParticleTree, scrollFunction: scrollTopLeft, preFilterFunction: preFilterRecoTree, + reconnectFunction: reconnectTree, + collections: ["edm4hep::ReconstructedParticle"], 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, + reconnectFunction: reconnectTree, + collections: ["edm4hep::Track"], description: `

A tree of the Tracks.

`, }, "Cluster Tree": { - filters: setupNoFilter, viewFunction: clusterTree, scrollFunction: scrollTopLeft, preFilterFunction: preFilterClusterTree, + reconnectFunction: reconnectTree, + collections: ["edm4hep::Cluster"], description: `

A tree of the Clusters.

`, }, "RecoParticle-Cluster-Track-Vertex": { - filters: setupNoFilter, viewFunction: recoClusterTrackVertex, scrollFunction: scrollTopCenter, preFilterFunction: preFilterRecoClusterTrackVertex, + reconnectFunction: reconnectMixedViews, + collections: [ + "edm4hep::ReconstructedParticle", + "edm4hep::Cluster", + "edm4hep::Track", + "edm4hep::Vertex", + ], description: `

Relations that a Reconstruced Particle has with other objects. ${spanWithColor( "Green", "#AAAA00" @@ -72,45 +84,51 @@ export const views = { )} connections are towards Clusters.

`, }, "Monte Carlo-Reconstructed Particle": { - filters: setupNoFilter, viewFunction: mcRecoAssociation, scrollFunction: scrollTopCenter, preFilterFunction: preFilterMCReco, + reconnectFunction: reconnectAssociation, + collections: ["edm4hep::MCParticle", "edm4hep::ReconstructedParticle"], description: `

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

`, }, "Monte Carlo Particle-Track": { - filters: setupNoFilter, viewFunction: mcTrackAssociation, scrollFunction: scrollTopCenter, preFilterFunction: preFilterMCTrack, + reconnectFunction: reconnectAssociation, + collections: ["edm4hep::MCParticle", "edm4hep::Track"], description: `

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

`, }, "Monte Carlo Particle-Cluster": { - filters: setupNoFilter, viewFunction: mcClusterAssociation, scrollFunction: scrollTopCenter, preFilterFunction: preFilterMCCluster, + reconnectFunction: reconnectAssociation, + collections: ["edm4hep::MCParticle", "edm4hep::Cluster"], description: `

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

`, }, "ParticleID List": { - filters: setupNoFilter, viewFunction: particleIDList, scrollFunction: scrollTopLeft, preFilterFunction: preFilterParticleIDList, + reconnectFunction: () => {}, + collections: ["edm4hep::ParticleID"], description: `

A list of ParticleIDs found in the event.

`, }, "Vertex List": { - filters: setupNoFilter, viewFunction: vertexList, scrollFunction: scrollTopLeft, preFilterFunction: preFilterVertexList, + reconnectFunction: () => {}, + collections: ["edm4hep::Vertex"], description: `

A list of Vertices found in the event.

`, }, "ParticleID-Reconstructed Particle": { - filters: setupNoFilter, viewFunction: recoParticleID, scrollFunction: scrollTopCenter, preFilterFunction: preFilterRecoParticleID, + reconnectFunction: reconnectMixedViews, + collections: ["edm4hep::ParticleID", "edm4hep::ReconstructedParticle"], description: `

1:1 relation from ParticleID to Reconstructed Particle.

`, }, }; diff --git a/js/views/views.js b/js/views/views.js index 59b48d9c..d46a24bf 100644 --- a/js/views/views.js +++ b/js/views/views.js @@ -2,11 +2,18 @@ import { currentObjects, currentEvent } from "../event-number.js"; import { copyObject } from "../lib/copy.js"; import { checkEmptyObject } from "../lib/empty-object.js"; import { views } from "./views-dictionary.js"; -import { emptyViewMessage, hideEmptyViewMessage } from "../lib/messages.js"; +import { + emptyViewMessage, + hideEmptyViewMessage, + showMessage, +} from "../lib/messages.js"; import { showViewInformation, hideViewInformation } from "../information.js"; import { renderObjects } from "../draw/render.js"; import { getContainer, saveSize } from "../draw/app.js"; import { setRenderable } from "../draw/renderable.js"; +import { initFilters } from "../filters/filter.js"; +import { setupToggles } from "../toggle/toggle.js"; +import { setScrollBarsPosition } from "../draw/scroll.js"; const currentView = {}; @@ -48,6 +55,7 @@ export function scroll() { const { x, y } = scrollLocations[index]; container.position.set(x, y); + setScrollBarsPosition(); } function setInfoButtonName(view) { @@ -55,27 +63,14 @@ function setInfoButtonName(view) { button.innerText = view; } -const addTask = (() => { - let pending = Promise.resolve(); - - const run = async (view) => { - try { - await pending; - } finally { - return drawView(view); - } - }; - - return (view) => (pending = run(view)); -})(); - -const drawView = async (view) => { +export const drawView = async (view) => { const { preFilterFunction, viewFunction, scrollFunction, - filters, + collections, description, + reconnectFunction, } = views[view]; const viewObjects = {}; @@ -96,26 +91,53 @@ const drawView = async (view) => { const viewCurrentObjects = {}; copyObject(viewObjects, viewCurrentObjects); - let [width, height] = viewFunction(viewObjects); - if (width < window.innerWidth) { - width = window.innerWidth; - } - if (height < window.innerHeight) { - height = window.innerHeight; - } - saveSize(width, height); + const render = async (objects) => { + const empty = checkEmptyObject(objects); + + if (empty) { + showMessage("No objects satisfy the filter options"); + return; + } + + let [width, height] = viewFunction(objects); + if (width === 0 && height === 0) { + showMessage("No objects satisfy the filter options"); + return; + } + + if (width < window.innerWidth) { + width = window.innerWidth; + } + if (height < window.innerHeight) { + height = window.innerHeight; + } + saveSize(width, height); + await renderObjects(objects); + }; + + await render(viewCurrentObjects); const scrollIndex = getViewScrollIndex(); if (scrollLocations[scrollIndex] === undefined) { const viewScrollLocation = scrollFunction(); scrollLocations[scrollIndex] = viewScrollLocation; } - - await renderObjects(viewObjects); scroll(); setRenderable(viewCurrentObjects); - filters(viewObjects, viewCurrentObjects); + initFilters( + { viewObjects, viewCurrentObjects }, + collections, + reconnectFunction, + { + render, + filterScroll: scrollFunction, + originalScroll: scroll, + setRenderable, + } + ); + + setupToggles(collections, viewCurrentObjects); }; export function saveScrollLocation() { @@ -136,10 +158,6 @@ export const getView = () => { return currentView.view; }; -export const drawCurrentView = () => { - addTask(currentView.view); -}; - const buttons = []; for (const key in views) { @@ -148,7 +166,7 @@ for (const key in views) { button.onclick = () => { saveScrollLocation(); setView(key); - addTask(key); + drawView(getView()); }; button.className = "view-button"; buttons.push(button); diff --git a/mappings/sim-status.js b/mappings/sim-status.js index db438605..d4512531 100644 --- a/mappings/sim-status.js +++ b/mappings/sim-status.js @@ -1,12 +1,12 @@ export const SimStatusBitFieldDisplayValues = { - 23: "Overlay", - 24: "Stopped", - 25: "LeftDetector", - 26: "DecayedInCalorimeter", - 27: "DecayedInTracker", - 28: "VertexIsNotEndpointOfParent", - 29: "Backscatter", - 30: "CreatedInSimulation", + "Overlay": 23, + "Stopped": 24, + "LeftDetector": 25, + "DecayedInCalorimeter": 26, + "DecayedInTracker": 27, + "VertexIsNotEndpointOfParent": 28, + "Backscatter": 29, + "CreatedInSimulation": 30, }; export function parseBits(bit) { @@ -22,11 +22,13 @@ export function parseBits(bit) { } export function getSimStatusDisplayValues(bits) { - return bits.map((bit) => - SimStatusBitFieldDisplayValues[bit] !== undefined - ? SimStatusBitFieldDisplayValues[bit] - : `Bit ${bit}` - ); + const values = Object.entries(SimStatusBitFieldDisplayValues); + + return bits.map((bit) => { + const [value, _] = values.find(([_, v]) => v === bit); + + return value; + }); } export function getSimStatusDisplayValuesFromBit(bit) { diff --git a/test/filter.json b/test/filter.json new file mode 100644 index 00000000..19340e02 --- /dev/null +++ b/test/filter.json @@ -0,0 +1,94 @@ +{ + "Event 0": { + "Collection": { + "collID": 0, + "collType": "edm4hep::MCParticleCollection", + "collection": [ + { + "momentum": 0, + "charge": 0, + "mass": 0, + "simulatorStatus": 70, + "parents": [], + "daughters": [ + { + "collectionID": 0, + "index": 1 + } + ] + }, + { + "momentum": 100, + "charge": 1, + "mass": 10, + "simulatorStatus": 24, + "daughters": [ + { + "collectionID": 0, + "index": 3 + } + ], + "parents": [ + { + "collectionID": 0, + "index": 0 + } + ] + }, + { + "momentum": 200, + "charge": 2, + "mass": 20, + "simulatorStatus": 25, + "daughters": [ + { + "collectionID": 0, + "index": 4 + } + ], + "parents": [ + { + "collectionID": 0, + "index": 0 + } + ] + }, + { + "momentum": 300, + "charge": 3, + "mass": 30, + "simulatorStatus": 26, + "daughters": [ + { + "collectionID": 0, + "index": 4 + } + ], + "parents": [ + { + "collectionID": 0, + "index": 1 + } + ] + }, + { + "momentum": 400, + "charge": 4, + "mass": 40, + "simulatorStatus": 27, + "parents": [ + { + "collectionID": 0, + "index": 2 + }, + { + "collectionID": 0, + "index": 3 + } + ], + "daughters": [] + } + ] + } + } +} \ No newline at end of file diff --git a/test/filterMCParticle.test.js b/test/filterMCParticle.test.js index 0b7989ea..e9eac78b 100644 --- a/test/filterMCParticle.test.js +++ b/test/filterMCParticle.test.js @@ -1,241 +1,44 @@ -import { reconnect } from "../js/menu/filter/reconnect.js"; import { loadObjects } from "../js/types/load.js"; -import { - Range, - Checkbox, - buildCriteriaFunction, -} from "../js/menu/filter/parameters.js"; +import { filterOut } from "../js/filters/filter-out.js"; +import data from "./filter.json" assert { type: "json" }; let objects = {}; -const data = { - "Event 0": { - "Collection": { - "collID": 0, - "collType": "edm4hep::MCParticleCollection", - "collection": [ - { - "momentum": 0, - "charge": 0, - "mass": 0, - "simulatorStatus": 70, - "parents": [], - "daughters": [ - { - "collectionID": 0, - "index": 1, - }, - ], - }, - { - "momentum": 100, - "charge": 1, - "mass": 10, - "simulatorStatus": 24, - "daughters": [ - { - "collectionID": 0, - "index": 3, - }, - ], - "parents": [ - { - "collectionID": 0, - "index": 0, - }, - ], - }, - { - "momentum": 200, - "charge": 2, - "mass": 20, - "simulatorStatus": 25, - "daughters": [ - { - "collectionID": 0, - "index": 4, - }, - ], - "parents": [ - { - "collectionID": 0, - "index": 0, - }, - ], - }, - { - "momentum": 300, - "charge": 3, - "mass": 30, - "simulatorStatus": 26, - "daughters": [ - { - "collectionID": 0, - "index": 4, - }, - ], - "parents": [ - { - "collectionID": 0, - "index": 1, - }, - ], - }, - { - "momentum": 400, - "charge": 4, - "mass": 40, - "simulatorStatus": 27, - "parents": [ - { - "collectionID": 0, - "index": 2, - }, - { - "collectionID": 0, - "index": 3, - }, - ], - "daughters": [], - }, - ], - }, - }, +const range = { + "edm4hep::MCParticle": (object) => + object.momentum >= 300 && + object.momentum <= 1000 && + object.mass >= 20 && + object.mass <= 30, +}; + +const checkboxes = { + "edm4hep::MCParticle": (object) => + object.simulatorStatus === 24 || object.simulatorStatus === 26, +}; + +const all = { + "edm4hep::MCParticle": () => true, }; beforeAll(() => { objects = loadObjects(data, 0, ["edm4hep::MCParticle"]); }); -describe("filter by ranges", () => { - it("filter by a single range parameter", () => { - const momentum = new Range({ - property: "momentum", - unit: "GeV", - }); - momentum.min = 300; - momentum.max = 1000; - const rangeFilters = Range.buildFilter([momentum]); - const criteriaFunction = buildCriteriaFunction(rangeFilters); - - const filteredObjects = reconnect(criteriaFunction, objects); - - expect( - filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.index - ) - ).toEqual([3, 4]); - }); - - it("filter by a combination of ranges", () => { - const charge = new Range({ - property: "charge", - unit: "e", - }); - charge.min = 3; - const mass = new Range({ - property: "mass", - unit: "GeV", - }); - mass.min = 20; - mass.max = 40; - const rangeFilters = Range.buildFilter([mass, charge]); - const criteriaFunction = buildCriteriaFunction(rangeFilters); - - const filteredObjects = reconnect(criteriaFunction, objects); +test("filter by ranges", () => { + const ids = filterOut(objects, {}, range); - expect( - filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.index - ) - ).toEqual([3, 4]); - }); + expect(ids).toEqual(new Set(["3-0"])); }); -describe("filter by checkboxes", () => { - it("filter by a single checkbox", () => { - const simulatorStatus = new Checkbox("simulatorStatus", 23); - simulatorStatus.checked = true; - const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); - const criteriaFunction = buildCriteriaFunction(checkboxFilters); +test("filter by property equality", () => { + const ids = filterOut(objects, {}, checkboxes); - const filteredObjects = reconnect(criteriaFunction, objects); - - expect( - filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.index - ) - ).toEqual([]); - }); - - it("filter by a combination of checkboxes", () => { - const simulatorStatus1 = new Checkbox("simulatorStatus", 23); - simulatorStatus1.checked = true; - const simulatorStatus2 = new Checkbox("simulatorStatus", 26); - simulatorStatus2.checked = true; - const simulatorStatus3 = new Checkbox("simulatorStatus", 27); - simulatorStatus3.checked = true; - const checkboxFilters = Checkbox.buildFilter([ - simulatorStatus1, - simulatorStatus2, - simulatorStatus3, - ]); - const criteriaFunction = buildCriteriaFunction(checkboxFilters); - - const filteredObjects = reconnect(criteriaFunction, objects); - - expect( - filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.index - ) - ).toEqual([3, 4]); - }); + expect(ids).toEqual(new Set(["1-0", "3-0"])); }); -describe("filter by ranges and checkboxes", () => { - it("show all particles when no kind of filter is applied", () => { - const charge = new Range({ - property: "charge", - unit: "e", - }); - const simulatorStatus = new Checkbox("simulatorStatus", 26); - const rangeFilters = Range.buildFilter([charge]); - const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); - const criteriaFunction = buildCriteriaFunction( - rangeFilters, - checkboxFilters - ); - - const filteredObjects = reconnect(criteriaFunction, objects); - - expect( - filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.index - ) - ).toEqual([0, 1, 2, 3, 4]); - }); - - it("filter by a combination of ranges and checkboxes", () => { - const charge = new Range({ - property: "charge", - unit: "e", - }); - charge.max = 3; - const simulatorStatus = new Checkbox("simulatorStatus", 23); - simulatorStatus.checked = true; - const rangeFilters = Range.buildFilter([charge]); - const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); - const criteriaFunction = buildCriteriaFunction( - rangeFilters, - checkboxFilters - ); - - const filteredObjects = reconnect(criteriaFunction, objects); +test("filter by function that allows all objects", () => { + const ids = filterOut(objects, {}, all); - expect( - filteredObjects.datatypes["edm4hep::MCParticle"].collection.map( - (mcParticle) => mcParticle.index - ) - ).toEqual([]); - }); + expect(ids).toEqual(new Set(["0-0", "1-0", "2-0", "3-0", "4-0"])); });