UNPKG

webglimpse

Version:

Webglimpse is a data visualization library for the web.

900 lines 230 kB
/* * Copyright (c) 2014, Metron, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import { Pane, xFrac, isLeftMouseDown } from '../core'; import { TimelineUi } from './timeline_ui'; import { newVerticalScrollLayout, newVerticalScrollbar, attachTimelineVerticalScrollMouseListeners } from '../scroll'; import { white, rgb, gray, rgba } from '../color'; import { newTimeAxisPainter } from './time_axis_painter'; import { newInsets, newInsetPane, newInsetLayout } from '../layout/inset_layout'; import { rowPaneFactoryChooser_DEFAULT } from './timeline_styles'; import { axisZoomStep } from '../plot/axis'; import { newColumnLayout } from '../layout/column_layout'; import { newCardLayout } from '../layout/card_layout'; import { newRowLayout } from '../layout/row_layout'; import { Side, newBackgroundPainter, xyFrac_VERTSHADER, solid_FRAGSHADER, putQuadXys, newSolidPane, Background, Highlight } from '../misc'; import { newOverlayLayout } from '../layout/overlay_layout'; import { alwaysTrue, GL, hasval, isNumber } from '../util/util'; import { Program, Attribute, UniformColor } from '../shader'; import { newDynamicBuffer } from '../buffer'; import { TextureRenderer } from '../texture'; import { Label, fitToLabel, newLabelPainter } from '../label'; import { newTimeGridPainter } from './time_grid_painter'; import { newBorderPainter } from '../painter/border_painter'; import { createTextTextureFactory } from '../text'; export class TimelinePane extends Pane { constructor(layout, model, ui) { super(layout, true); this._model = model; this._ui = ui; } get model() { return this._model; } get ui() { return this._ui; } } export function newTimelinePane(drawable, timeAxis, model, options, ui) { // Misc const font = (hasval(options) && hasval(options.font) ? options.font : '11px verdana,sans-serif'); const rowPaneFactoryChooser = (hasval(options) && hasval(options.rowPaneFactoryChooser) ? options.rowPaneFactoryChooser : rowPaneFactoryChooser_DEFAULT); const cursor = (hasval(options) && hasval(options.cursor) ? options.cursor : 'auto'); // Scroll const showScrollbar = (hasval(options) && hasval(options.showScrollbar) ? options.showScrollbar : true); const scrollbarOptions = (hasval(options) ? options.scrollbarOptions : null); // Colors const fgColor = (hasval(options) && hasval(options.fgColor) ? options.fgColor : white); const bgColor = (hasval(options) && hasval(options.bgColor) ? options.bgColor : rgb(0.098, 0.165, 0.243)); const rowLabelColor = (hasval(options) && hasval(options.rowLabelColor) ? options.rowLabelColor : fgColor); const rowLabelBgColor = (hasval(options) && hasval(options.rowLabelBgColor) ? options.rowLabelBgColor : bgColor); const rowHighlightColor = (hasval(options) && hasval(options.rowHighlightColor) ? options.rowHighlightColor : rgb(1, 0.623, 0)); const groupLabelColor = (hasval(options) && hasval(options.groupLabelColor) ? options.groupLabelColor : fgColor); const groupHighlightColor = (hasval(options) && hasval(options.groupHighlightColor) ? options.groupHighlightColor : rgb(0, 1, 1)); const axisLabelColor = (hasval(options) && hasval(options.axisLabelColor) ? options.axisLabelColor : fgColor); const rowBgColor = (hasval(options) && hasval(options.rowBgColor) ? options.rowBgColor : rgb(0.020, 0.086, 0.165)); const rowAltBgColor = (hasval(options) && hasval(options.rowAltBgColor) ? options.rowAltBgColor : rgb(0.020, 0.086, 0.165)); const gridColor = (hasval(options) && hasval(options.gridColor) ? options.gridColor : gray(0.5)); const selectedIntervalFillColor = (hasval(options) && hasval(options.selectedIntervalFillColor) ? options.selectedIntervalFillColor : rgba(0, 0.6, 0.8, 0.157)); const selectedIntervalBorderColor = (hasval(options) && hasval(options.selectedIntervalBorderColor) ? options.selectedIntervalBorderColor : rgb(0, 0.2, 1.0)); // Axes const showTopAxis = (hasval(options) && hasval(options.showTopAxis) ? options.showTopAxis : true); const showBottomAxis = (hasval(options) && hasval(options.showBottomAxis) ? options.showBottomAxis : true); const topTimeZone = (hasval(options) && hasval(options.topTimeZone) ? options.topTimeZone : '+0000'); const bottomTimeZone = (hasval(options) && hasval(options.bottomTimeZone) ? options.bottomTimeZone : '+0000'); const tickSpacing = (hasval(options) && hasval(options.tickSpacing) ? options.tickSpacing : 60); const axisLabelAlign = (hasval(options) && hasval(options.axisLabelAlign) ? options.axisLabelAlign : 0.5); // Sizing const groupLabelInsets = (hasval(options) && hasval(options.groupLabelInsets) ? options.groupLabelInsets : newInsets(6, 10)); const groupHighlightWidth = (hasval(options) && hasval(options.groupHighlightWidth) ? options.groupHighlightWidth : 2); const groupHighlightInsets = (hasval(options) && hasval(options.groupHighlightInsets) ? options.groupHighlightInsets : newInsets(6, 0, 2, 4)); const rowLabelInsets = (hasval(options) && hasval(options.rowLabelInsets) ? options.rowLabelInsets : newInsets(0, 35)); const rowLabelPaneWidth = (hasval(options) && hasval(options.rowLabelPaneWidth) ? options.rowLabelPaneWidth : 140); const rowHighlightWidth = (hasval(options) && hasval(options.rowHighlightWidth) ? options.rowHighlightWidth : 2); const rowHighlightInsets = (hasval(options) && hasval(options.rowHighlightInsets) ? options.rowHighlightInsets : newInsets(1, 0, 2, 10)); const rowSeparatorHeight = (hasval(options) && hasval(options.rowSeparatorHeight) ? options.rowSeparatorHeight : 2); let scrollbarWidth = (hasval(options) && hasval(options.scrollbarWidth) ? options.scrollbarWidth : 16); scrollbarWidth = showScrollbar ? scrollbarWidth : 0; // if the scrollbar is not showing, set its width to 0 const axisPaneHeight = (hasval(options) && hasval(options.axisPaneHeight) ? options.axisPaneHeight : 40); const draggableEdgeWidth = (hasval(options) && hasval(options.draggableEdgeWidth) ? options.draggableEdgeWidth : 6); const snapToDistance = (hasval(options) && hasval(options.snapToDistance) ? options.snapToDistance : 10); // Event / Selection const allowEventMultiSelection = (hasval(options) && hasval(options.allowEventMultiSelection) ? options.allowEventMultiSelection : true); const selectedIntervalMode = (hasval(options) && hasval(options.selectedIntervalMode) ? options.selectedIntervalMode : 'range'); const centerSelectedIntervalOnDoubleClick = (hasval(options) && hasval(options.centerSelectedIntervalOnDoubleClick) ? options.centerSelectedIntervalOnDoubleClick : true); const defaultMouseWheelListener = function (ev) { const zoomFactor = Math.pow(axisZoomStep, ev.wheelSteps); timeAxis.zoom(zoomFactor, timeAxis.vAtFrac(xFrac(ev))); }; const mouseWheelListener = (hasval(options) && hasval(options.mouseWheelListener) ? options.mouseWheelListener : defaultMouseWheelListener); let outsideManagedUi = false; if (!ui) { outsideManagedUi = false; ui = new TimelineUi(model, { allowEventMultiSelection: allowEventMultiSelection }); } else { // remove old panes (if the ui is being reused) outsideManagedUi = true; ui.panes.removeAll(); } const selection = ui.selection; const redraw = function () { drawable.redraw(); }; selection.selectedInterval.changed.on(redraw); selection.hoveredEvent.changed.on(redraw); selection.selectedEvents.valueAdded.on(redraw); selection.selectedEvents.valueRemoved.on(redraw); // even if the model defines cursors, we may need to redraw when the mouse position changes // (we might not actually need to if: none of the rows actually use the cursor, or if the // cursor doesn't show a vertical or horizontal line) // this check just avoids redrawing unncessarily in the easy-to-verify common case where // no cursors are defined const redrawCursor = function () { if (!model.cursors.isEmpty) { drawable.redraw(); } }; selection.hoveredY.changed.on(redrawCursor); selection.hoveredTime_PMILLIS.changed.on(redrawCursor); // Scroll Pane and Maximized Row Pane // // setup Pane which either shows timeline content, or only maximized rows // able to switch between the two depending on model.root.maximizedRowGuids.isEmpty // Scroll Pane const tickTimeZone = (showTopAxis ? topTimeZone : bottomTimeZone); const contentPaneOpts = { selectedIntervalMode: selectedIntervalMode, rowPaneFactoryChooser: rowPaneFactoryChooser, font: font, fgColor: fgColor, rowLabelColor: rowLabelColor, rowLabelBgColor: rowLabelBgColor, rowHighlightColor: rowHighlightColor, groupLabelColor: groupLabelColor, groupHighlightColor: groupHighlightColor, axisLabelColor: axisLabelColor, bgColor: bgColor, rowBgColor: rowBgColor, rowAltBgColor: rowAltBgColor, gridColor: gridColor, gridTickSpacing: tickSpacing, gridTimeZone: tickTimeZone, referenceDate: options.referenceDate, groupLabelInsets: groupLabelInsets, groupHighlightWidth: groupHighlightWidth, groupHighlightInsets: groupHighlightInsets, rowLabelInsets: rowLabelInsets, rowLabelPaneWidth: rowLabelPaneWidth, rowHighlightWidth: rowHighlightWidth, rowHighlightInsets: rowHighlightInsets, rowSeparatorHeight: rowSeparatorHeight, draggableEdgeWidth: draggableEdgeWidth, snapToDistance: snapToDistance, mouseWheelListener: mouseWheelListener }; let contentPaneArgs; let contentPane = null; if (showScrollbar) { const scrollLayout = newVerticalScrollLayout(); const scrollable = new Pane(scrollLayout, false); ui.addPane('scroll-content-pane', scrollable); contentPaneArgs = { drawable: drawable, scrollLayout: scrollLayout, timeAxis: timeAxis, model: model, ui: ui, options: contentPaneOpts }; const scrollContentPane = newTimelineContentPane(contentPaneArgs); ui.addPane('content-pane', scrollContentPane); scrollable.addPane(scrollContentPane, 0); const scrollbar = newVerticalScrollbar(scrollLayout, drawable, scrollbarOptions); ui.addPane('scrollbar', scrollbar); contentPane = new Pane(newColumnLayout(false), false); ui.addPane('scroll-outer-pane', contentPane); contentPane.addPane(scrollbar, 0, { width: scrollbarWidth, ignoreHeight: true }); contentPane.addPane(scrollable, 1); } else { contentPaneArgs = { drawable: drawable, scrollLayout: null, timeAxis: timeAxis, model: model, ui: ui, options: contentPaneOpts }; contentPane = newTimelineContentPane(contentPaneArgs); ui.addPane('content-pane', contentPane); } // Card Pane Switching Logic const timelineCardPane = new Pane(newCardLayout()); ui.addPane('switch-content-pane', timelineCardPane); const maximizedContentPane = new Pane(newRowLayout()); ui.addPane('maximize-content-pane', maximizedContentPane); const insetMaximizedContentPane = newInsetPane(maximizedContentPane, newInsets(0, scrollbarWidth, 0, 0)); ui.addPane('inset-maximize-content-pane', insetMaximizedContentPane); const contentActive = model.root.maximizedRowGuids.isEmpty; timelineCardPane.addPane(insetMaximizedContentPane, !contentActive); timelineCardPane.addPane(contentPane, contentActive); setupRowContainerPane(contentPaneArgs, maximizedContentPane, model.root.maximizedRowGuids, true, 'maximized'); const updateMaximizedRows = function (rowGuid, rowIndex) { const contentActiveInner = model.root.maximizedRowGuids.isEmpty; timelineCardPane.setLayoutArg(insetMaximizedContentPane, !contentActiveInner); timelineCardPane.setLayoutArg(contentPane, contentActiveInner); drawable.redraw(); }; model.root.maximizedRowGuids.valueAdded.on(updateMaximizedRows); model.root.maximizedRowGuids.valueRemoved.on(updateMaximizedRows); // Overlay and Underlay Panes // const underlayPane = new Pane(newRowLayout()); ui.addPane('underlay-pane', underlayPane); const axisInsets = newInsets(0, scrollbarWidth, 0, rowLabelPaneWidth); // top time axis pane const axisOpts = { tickSpacing: tickSpacing, font: font, textColor: axisLabelColor, tickColor: axisLabelColor, labelAlign: axisLabelAlign, referenceDate: options.referenceDate, isFuturePositive: options.isFuturePositive, timeAxisFormat: options.timeAxisFormat }; if (showTopAxis) { const topAxisPane = newTimeAxisPane(contentPaneArgs, null, cursor); ui.addPane('top-axis-pane', topAxisPane); topAxisPane.addPainter(newTimeAxisPainter(timeAxis, Side.TOP, topTimeZone, tickTimeZone, axisOpts)); underlayPane.addPane(newInsetPane(topAxisPane, axisInsets), 0, { height: axisPaneHeight, width: null }); } // pane containing pinned rows specified in TimelineRoot.topPinnedRowGuids const topPinnedPane = new Pane(newRowLayout()); ui.addPane('top-pinned-pane', topPinnedPane); const insetTopPinnedPane = newInsetPane(topPinnedPane, newInsets(0, scrollbarWidth, 0, 0)); ui.addPane('inset-top-pinned-pane', insetTopPinnedPane); setupRowContainerPane(contentPaneArgs, topPinnedPane, model.root.topPinnedRowGuids, false, 'toppinned'); underlayPane.addPane(insetTopPinnedPane, 1); // main pane containing timeline groups and rows underlayPane.addPane(timelineCardPane, 2, { height: 'pref-max', width: null }); // pane containing pinned rows specified in TimelineRoot.bottomPinnedRowGuids const bottomPinnedPane = new Pane(newRowLayout()); ui.addPane('bottom-pinned-pane', bottomPinnedPane); const insetBottomPinnedPane = newInsetPane(bottomPinnedPane, newInsets(0, scrollbarWidth, 0, 0)); ui.addPane('inset-bottom-pinned-pane', insetBottomPinnedPane); setupRowContainerPane(contentPaneArgs, bottomPinnedPane, model.root.bottomPinnedRowGuids, false, 'bottompinned'); underlayPane.addPane(insetBottomPinnedPane, 3); // bottom time axis pane if (showBottomAxis) { const bottomAxisPane = newTimeAxisPane(contentPaneArgs, null, cursor); ui.addPane('bottom-axis-pane', bottomAxisPane); bottomAxisPane.addPainter(newTimeAxisPainter(timeAxis, Side.BOTTOM, bottomTimeZone, tickTimeZone, axisOpts)); underlayPane.addPane(newInsetPane(bottomAxisPane, axisInsets), 4, { height: axisPaneHeight, width: null }); } const updateMillisPerPx = function () { const w = underlayPane.viewport.w - axisInsets.left - axisInsets.right; ui.millisPerPx.value = timeAxis.tSize_MILLIS / w; }; underlayPane.viewportChanged.on(updateMillisPerPx); timeAxis.limitsChanged.on(updateMillisPerPx); const timelinePane = new TimelinePane(newOverlayLayout(), model, ui); ui.addPane('timeline-pane', timelinePane); timelinePane.addPainter(newBackgroundPainter(bgColor)); timelinePane.addPane(underlayPane, true); if (selectedIntervalMode === 'single' || selectedIntervalMode === 'single-unmodifiable') { const overlayPane = new Pane(null, false, alwaysTrue); ui.addPane('overlay-pane', overlayPane); overlayPane.addPainter(newTimelineSingleSelectionPainter(timeAxis, selection.selectedInterval, selectedIntervalBorderColor, selectedIntervalFillColor)); timelinePane.addPane(newInsetPane(overlayPane, axisInsets, null, false)); } else if (selectedIntervalMode === 'range' || selectedIntervalMode === 'range-unmodifiable') { const overlayPane = new Pane(null, false, alwaysTrue); ui.addPane('overlay-pane', overlayPane); overlayPane.addPainter(newTimelineRangeSelectionPainter(timeAxis, selection.selectedInterval, selectedIntervalBorderColor, selectedIntervalFillColor)); timelinePane.addPane(newInsetPane(overlayPane, axisInsets, null, false)); } // Enable double click to center selection on mouse if (centerSelectedIntervalOnDoubleClick) { const doubleClick = function (ev) { if (selectedIntervalMode === 'single') { if (ev.clickCount > 1) { const time_PMILLIS = timeAtPointer_PMILLIS(timeAxis, ev); selection.selectedInterval.setInterval(time_PMILLIS, time_PMILLIS); } } else if (selectedIntervalMode === 'range') { if (ev.clickCount > 1) { const time_PMILLIS = timeAtPointer_PMILLIS(timeAxis, ev); const offset_PMILLIS = selection.selectedInterval.start_PMILLIS + 0.5 * selection.selectedInterval.duration_MILLIS; selection.selectedInterval.pan(time_PMILLIS - offset_PMILLIS); } } }; ui.input.mouseDown.on(doubleClick); } timelinePane.dispose.on(function () { // only dispose the ui if we created it (and this manage its lifecycle) if (!outsideManagedUi) { ui.dispose.fire(); } selection.selectedInterval.changed.off(redraw); selection.hoveredEvent.changed.off(redraw); selection.hoveredY.changed.off(redrawCursor); selection.hoveredTime_PMILLIS.changed.off(redrawCursor); selection.selectedEvents.valueAdded.off(redraw); selection.selectedEvents.valueRemoved.off(redraw); underlayPane.viewportChanged.off(updateMillisPerPx); timeAxis.limitsChanged.off(updateMillisPerPx); model.root.maximizedRowGuids.valueAdded.off(updateMaximizedRows); model.root.maximizedRowGuids.valueRemoved.off(updateMaximizedRows); }); return timelinePane; } function newTimeIntervalMask(timeAxis, interval, selectedIntervalMode) { if (selectedIntervalMode === 'range') { return function (viewport, i, j) { const time_PMILLIS = timeAxis.tAtFrac_PMILLIS(viewport.xFrac(i)); // allow a 10 pixel selection buffer to make it easier to grab ends of the selection const buffer_MILLIS = timeAxis.tSize_MILLIS / viewport.w * 10; return interval.overlaps(time_PMILLIS - buffer_MILLIS, time_PMILLIS + buffer_MILLIS); }; } else if (selectedIntervalMode === 'single') { return function (viewport, i, j) { const time_PMILLIS = timeAxis.tAtFrac_PMILLIS(viewport.xFrac(i)); // allow a 10 pixel selection buffer to make it easier to grab the selection const buffer_MILLIS = timeAxis.tSize_MILLIS / viewport.w * 10; return time_PMILLIS < interval.cursor_PMILLIS + buffer_MILLIS && time_PMILLIS > interval.cursor_PMILLIS - buffer_MILLIS; }; } } function attachTimeAxisMouseListeners(pane, axis, args) { let vGrab = null; pane.mouseDown.on(function (ev) { if (isLeftMouseDown(ev.mouseEvent) && !hasval(vGrab)) { vGrab = axis.vAtFrac(xFrac(ev)); } }); pane.mouseMove.on(function (ev) { if (isLeftMouseDown(ev.mouseEvent) && hasval(vGrab)) { axis.pan(vGrab - axis.vAtFrac(xFrac(ev))); } }); pane.mouseUp.on(function (ev) { vGrab = null; }); pane.mouseWheel.on(args.options.mouseWheelListener); } function newTimeAxisPane(args, row, cursor) { const timeAxis = args.timeAxis; const ui = args.ui; const draggableEdgeWidth = args.options.draggableEdgeWidth; const scrollLayout = args.scrollLayout; const drawable = args.drawable; const selectedIntervalMode = args.options.selectedIntervalMode; const input = ui.input; const axisPane = new Pane(newOverlayLayout()); if (scrollLayout) { attachTimelineVerticalScrollMouseListeners(axisPane, scrollLayout, drawable); } attachTimeAxisMouseListeners(axisPane, timeAxis, args); const onMouseMove = function (ev) { const time_PMILLIS = timeAxis.tAtFrac_PMILLIS(xFrac(ev)); input.mouseMove.fire(ev); input.timeHover.fire(time_PMILLIS, ev); if (row) { input.rowHover.fire(row, ev); } }; axisPane.mouseMove.on(onMouseMove); const onMouseExit = function (ev) { input.mouseExit.fire(ev); input.timeHover.fire(null, ev); if (row) { input.rowHover.fire(null, ev); } }; axisPane.mouseExit.on(onMouseExit); const onMouseDown = function (ev) { input.mouseDown.fire(ev); }; axisPane.mouseDown.on(onMouseDown); const onMouseUp = function (ev) { input.mouseUp.fire(ev); }; axisPane.mouseUp.on(onMouseUp); const onContextMenu = function (ev) { input.contextMenu.fire(ev); }; axisPane.contextMenu.on(onContextMenu); if (cursor) { axisPane.mouseCursor = cursor; } if (selectedIntervalMode === 'single' || selectedIntervalMode === 'range') { const selection = ui.selection; const selectedIntervalPane = new Pane(null, true, newTimeIntervalMask(timeAxis, selection.selectedInterval, selectedIntervalMode)); attachTimeSelectionMouseListeners(selectedIntervalPane, timeAxis, selection.selectedInterval, input, draggableEdgeWidth, selectedIntervalMode, args.options.mouseWheelListener); axisPane.addPane(selectedIntervalPane, false); selectedIntervalPane.mouseMove.on(onMouseMove); selectedIntervalPane.mouseExit.on(onMouseExit); selectedIntervalPane.mouseDown.on(onMouseDown); selectedIntervalPane.mouseUp.on(onMouseUp); selectedIntervalPane.contextMenu.on(onContextMenu); } // Dispose // // mouse listeners are disposed of automatically by Pane return axisPane; } function timeAtPointer_PMILLIS(timeAxis, ev) { return timeAxis.tAtFrac_PMILLIS(ev.paneViewport.xFrac(ev.i)); } function attachTimeSelectionMouseListeners(pane, timeAxis, interval, input, draggableEdgeWidth, selectedIntervalMode, mouseWheelListener) { if (selectedIntervalMode === 'single') { const chooseDragMode = function (ev) { return 'center'; }; attachTimeIntervalSelectionMouseListeners(pane, timeAxis, interval, input, draggableEdgeWidth, selectedIntervalMode, chooseDragMode, mouseWheelListener); } else if (selectedIntervalMode === 'range') { // Edges are draggable when interval is at least this wide const minIntervalWidthForEdgeDraggability = 3 * draggableEdgeWidth; // When dragging an edge, the interval cannot be made narrower than this // // Needs to be greater than minIntervalWidthForEdgeDraggability -- by enough to // cover floating-point precision loss -- so a user can't accidentally make // the interval so narrow that it can't easily be widened again. // const minIntervalWidthWhenDraggingEdge = minIntervalWidthForEdgeDraggability + 1; const chooseDragMode = function (ev) { const intervalWidth = (interval.duration_MILLIS) * ev.paneViewport.w / timeAxis.vSize; if (intervalWidth < minIntervalWidthForEdgeDraggability) { // If interval isn't very wide, don't try to allow edge dragging return 'center'; } else { const time_PMILLIS = timeAtPointer_PMILLIS(timeAxis, ev); const mouseOffset = (time_PMILLIS - interval.start_PMILLIS) * ev.paneViewport.w / timeAxis.vSize; if (mouseOffset < draggableEdgeWidth) { // If mouse is near the left edge, drag the interval's start-time return 'start'; } else if (mouseOffset < intervalWidth - draggableEdgeWidth) { // If mouse is in the center, drag the whole interval return 'center'; } else { // If mouse is near the right edge, drag the interval's end-time return 'end'; } } }; attachTimeIntervalSelectionMouseListeners(pane, timeAxis, interval, input, draggableEdgeWidth, selectedIntervalMode, chooseDragMode, mouseWheelListener); } } function attachTimeIntervalSelectionMouseListeners(pane, timeAxis, interval, input, draggableEdgeWidth, selectedIntervalMode, chooseDragMode, mouseWheelListener) { // see comments in attachTimeSelectionMouseListeners( ... ) const minIntervalWidthForEdgeDraggability = 3 * draggableEdgeWidth; const minIntervalWidthWhenDraggingEdge = minIntervalWidthForEdgeDraggability + 1; // Hook up input notifications // pane.mouseWheel.on(mouseWheelListener); pane.contextMenu.on(function (ev) { input.contextMenu.fire(ev); }); // Begin interval-drag // let dragMode = null; let dragOffset_MILLIS = null; pane.mouseMove.on(function (ev) { if (!dragMode) { const mouseCursors = { 'center': 'move', 'start': 'w-resize', 'end': 'e-resize' }; pane.mouseCursor = mouseCursors[chooseDragMode(ev)]; } }); pane.mouseDown.on(function (ev) { dragMode = isLeftMouseDown(ev.mouseEvent) ? chooseDragMode(ev) : null; if (!hasval(dragMode)) { dragOffset_MILLIS = null; } }); // Compute (and remember) the pointer time, for use by the drag listeners below // let dragPointer_PMILLIS = null; const updateDragPointer = function (ev) { if (hasval(dragMode)) { dragPointer_PMILLIS = timeAtPointer_PMILLIS(timeAxis, ev); } }; pane.mouseDown.on(updateDragPointer); pane.mouseMove.on(updateDragPointer); // Dragging interval-center // const grabCenter = function () { if (dragMode === 'center') { dragOffset_MILLIS = dragPointer_PMILLIS - interval.start_PMILLIS; } }; pane.mouseDown.on(grabCenter); const dragCenter = function () { if (dragMode === 'center') { const newStart_PMILLIS = (dragPointer_PMILLIS - dragOffset_MILLIS); const newEnd_PMILLIS = interval.end_PMILLIS + (newStart_PMILLIS - interval.start_PMILLIS); interval.setInterval(newStart_PMILLIS, newEnd_PMILLIS); } }; pane.mouseMove.on(dragCenter); // Dragging interval-start // const grabStart = function () { if (dragMode === 'start') { dragOffset_MILLIS = dragPointer_PMILLIS - interval.start_PMILLIS; } }; pane.mouseDown.on(grabStart); const dragStart = function () { if (dragMode === 'start') { const wMin_MILLIS = minIntervalWidthWhenDraggingEdge * timeAxis.vSize / pane.viewport.w; const newStart_PMILLIS = dragPointer_PMILLIS - dragOffset_MILLIS; interval.start_PMILLIS = Math.min(interval.end_PMILLIS - wMin_MILLIS, newStart_PMILLIS); } }; pane.mouseMove.on(dragStart); // Dragging interval-end // const grabEnd = function () { if (dragMode === 'end') { dragOffset_MILLIS = dragPointer_PMILLIS - interval.end_PMILLIS; } }; pane.mouseDown.on(grabEnd); const dragEnd = function () { if (dragMode === 'end') { const wMin_MILLIS = minIntervalWidthWhenDraggingEdge * timeAxis.vSize / pane.viewport.w; const newEnd_PMILLIS = dragPointer_PMILLIS - dragOffset_MILLIS; interval.end_PMILLIS = Math.max(interval.start_PMILLIS + wMin_MILLIS, newEnd_PMILLIS); interval.cursor_PMILLIS = interval.end_PMILLIS; } }; pane.mouseMove.on(dragEnd); // Finish interval-drag // pane.mouseUp.on(function (ev) { dragOffset_MILLIS = null; dragPointer_PMILLIS = null; dragMode = null; }); } export function newTimelineSingleSelectionPainter(timeAxis, interval, borderColor, fillColor) { const program = new Program(xyFrac_VERTSHADER, solid_FRAGSHADER); const a_XyFrac = new Attribute(program, 'a_XyFrac'); const u_Color = new UniformColor(program, 'u_Color'); // holds vertices for fill and border const coords = new Float32Array(12 + 8); const coordsBuffer = newDynamicBuffer(); return function (gl, viewport) { if (hasval(interval.cursor_PMILLIS)) { const fracSelection = timeAxis.tFrac(interval.cursor_PMILLIS); const fracWidth = 1 / viewport.w; const fracHeight = 1 / viewport.h; const thickWidth = 3 / viewport.w; const highlightWidth = 7 / viewport.w; let index = 0; // fill vertices coords[index++] = fracSelection - highlightWidth; coords[index++] = 1; coords[index++] = fracSelection + highlightWidth; coords[index++] = 1; coords[index++] = fracSelection - highlightWidth; coords[index++] = 0; coords[index++] = fracSelection + highlightWidth; coords[index++] = 0; // selection vertices index = putQuadXys(coords, index, fracSelection - thickWidth / 2, fracSelection + thickWidth / 2, 1, 0 + fracHeight); // selection gl.blendFuncSeparate(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA); gl.enable(GL.BLEND); program.use(gl); coordsBuffer.setData(coords); a_XyFrac.setDataAndEnable(gl, coordsBuffer, 2, GL.FLOAT); u_Color.setData(gl, fillColor); gl.drawArrays(GL.TRIANGLE_STRIP, 0, 4); u_Color.setData(gl, borderColor); gl.drawArrays(GL.TRIANGLES, 4, 6); a_XyFrac.disable(gl); program.endUse(gl); } }; } function newTimelineRangeSelectionPainter(timeAxis, interval, borderColor, fillColor) { const program = new Program(xyFrac_VERTSHADER, solid_FRAGSHADER); const a_XyFrac = new Attribute(program, 'a_XyFrac'); const u_Color = new UniformColor(program, 'u_Color'); // holds vertices for fill and border const coords = new Float32Array(12 + 8 + 48); const coordsBuffer = newDynamicBuffer(); return function (gl, viewport) { if (hasval(interval.start_PMILLIS) && hasval(interval.end_PMILLIS)) { const fracStart = timeAxis.tFrac(interval.start_PMILLIS); const fracEnd = timeAxis.tFrac(interval.end_PMILLIS); const fracSelection = timeAxis.tFrac(interval.cursor_PMILLIS); const fracWidth = 1 / viewport.w; const fracHeight = 1 / viewport.h; const thickWidth = 3 / viewport.w; let index = 0; // fill vertices coords[index++] = fracStart; coords[index++] = 1; coords[index++] = fracEnd; coords[index++] = 1; coords[index++] = fracStart; coords[index++] = 0; coords[index++] = fracEnd; coords[index++] = 0; // border vertices index = putQuadXys(coords, index, fracStart, fracEnd - fracWidth, +1, +1 - fracHeight); // top index = putQuadXys(coords, index, fracStart + fracWidth, fracEnd, 0 + fracHeight, 0); // bottom index = putQuadXys(coords, index, fracStart, fracStart + fracWidth, 1 - fracHeight, 0); // left index = putQuadXys(coords, index, fracEnd - fracWidth, fracEnd, 1, 0 + fracHeight); // right // selection vertices index = putQuadXys(coords, index, fracSelection - thickWidth, fracSelection, 1, 0 + fracHeight); // selection gl.blendFuncSeparate(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA); gl.enable(GL.BLEND); program.use(gl); coordsBuffer.setData(coords); a_XyFrac.setDataAndEnable(gl, coordsBuffer, 2, GL.FLOAT); u_Color.setData(gl, fillColor); gl.drawArrays(GL.TRIANGLE_STRIP, 0, 4); u_Color.setData(gl, borderColor); gl.drawArrays(GL.TRIANGLES, 4, 30); a_XyFrac.disable(gl); program.endUse(gl); } }; } function newGroupCollapseExpandArrowPainter(group) { const program = new Program(xyFrac_VERTSHADER, solid_FRAGSHADER); const a_XyFrac = new Attribute(program, 'a_XyFrac'); const u_Color = new UniformColor(program, 'u_Color'); // holds vertices for triangle const coords = new Float32Array(6); const coordsBuffer = newDynamicBuffer(); return function (gl, viewport) { let sizeFracX = 0.5; const sizeX = sizeFracX * viewport.w; const sizeY = sizeX * Math.sqrt(3) / 2; let sizeFracY = sizeY / viewport.h; const bufferFracX = 0.05; const bufferSize = bufferFracX * viewport.w; const bufferFracY = bufferSize / viewport.h; const centerFracX = 0.5; const centerFracY = bufferFracY + sizeFracY / 2; if (group.collapsed) { sizeFracX = sizeY / viewport.w; sizeFracY = sizeX / viewport.h; const fracStartX = centerFracX - sizeFracX / 2; const fracEndX = centerFracX + sizeFracX / 2; const fracStartY = 1 - (centerFracY - sizeFracY / 2); const fracEndY = 1 - (centerFracY + sizeFracY / 2); let index = 0; coords[index++] = fracStartX; coords[index++] = fracStartY; coords[index++] = fracEndX; coords[index++] = (fracStartY + fracEndY) / 2; coords[index++] = fracStartX; coords[index++] = fracEndY; } else { const fracStartX = centerFracX - sizeFracX / 2; const fracEndX = centerFracX + sizeFracX / 2; const fracStartY = 1 - (centerFracY - sizeFracY / 2); const fracEndY = 1 - (centerFracY + sizeFracY / 2); let index = 0; coords[index++] = fracStartX; coords[index++] = fracStartY; coords[index++] = fracEndX; coords[index++] = fracStartY; coords[index++] = (fracStartX + fracEndX) / 2; coords[index++] = fracEndY; } program.use(gl); coordsBuffer.setData(coords); a_XyFrac.setDataAndEnable(gl, coordsBuffer, 2, GL.FLOAT); u_Color.setData(gl, white); gl.drawArrays(GL.TRIANGLES, 0, 3); a_XyFrac.disable(gl); program.endUse(gl); }; } function newTimelineContentPane(args) { const drawable = args.drawable; const scrollLayout = args.scrollLayout; const timeAxis = args.timeAxis; const model = args.model; const ui = args.ui; const options = args.options; const root = model.root; const selectedIntervalMode = options.selectedIntervalMode; const rowPaneFactoryChooser = options.rowPaneFactoryChooser; const font = options.font; const fgColor = options.fgColor; const rowLabelColor = options.rowLabelColor; const groupLabelColor = options.groupLabelColor; const groupHighlightColor = options.groupHighlightColor; const groupHighlightWidth = options.groupHighlightWidth; const groupHighlightInsets = options.groupHighlightInsets; const axisLabelColor = options.axisLabelColor; const bgColor = options.bgColor; const rowBgColor = options.rowBgColor; const rowAltBgColor = options.rowAltBgColor; const gridColor = options.gridColor; const gridTimeZone = options.gridTimeZone; const gridTickSpacing = options.gridTickSpacing; const groupLabelInsets = options.groupLabelInsets; const rowLabelInsets = options.rowLabelInsets; const rowLabelPaneWidth = options.rowLabelPaneWidth; const rowSeparatorHeight = options.rowSeparatorHeight; const draggableEdgeWidth = options.draggableEdgeWidth; const snapToDistance = options.snapToDistance; const textureRenderer = new TextureRenderer(); const createGroupLabelTexture = createTextTextureFactory(font); const createRowLabelTexture = createTextTextureFactory(font); // Group panes // const timelineContentPane = new Pane(newRowLayout()); const groupHeaderPanes = {}; const groupContentPanes = {}; const groupContainerPanes = {}; const addGroup = function (groupGuid, groupIndex) { const group = model.group(groupGuid); const groupLabelFont = hasval(group.labelFont) ? group.labelFont : font; const groupLabel = new Label(group.label, groupLabelFont, groupLabelColor); const groupLabelPane = new Pane({ updatePrefSize: fitToLabel(groupLabel) }, false); groupLabelPane.addPainter(newLabelPainter(groupLabel, 0, 1, 0, 1)); const groupArrowPane = new Pane({ updatePrefSize: function (parentPrefSize) { parentPrefSize.w = 16; parentPrefSize.h = 0; } }, false); groupArrowPane.addPainter(newGroupCollapseExpandArrowPainter(group)); const groupPane = new Pane(newColumnLayout(), false); groupPane.addPane(groupArrowPane, 0); groupPane.addPane(groupLabelPane, 1); const groupButton = newInsetPane(groupPane, groupLabelInsets, bgColor); const redrawLabel = function () { groupLabel.text = group.label; drawable.redraw(); }; group.attrsChanged.on(redrawLabel); /// handle rollup group row /// const groupHeaderStripe = new Pane(newRowLayout()); groupHeaderStripe.addPane(new Pane(null), 0, { height: null }); groupHeaderStripe.addPane(newSolidPane(groupLabelColor), 1, { height: 1 }); groupHeaderStripe.addPane(new Pane(null), 2, { height: null }); const rollupRow = model.row(group.rollupGuid); let groupHeaderPane = null; let groupHeaderUnderlay = null; if (rollupRow) { const rowBackgroundPanes = newRowBackgroundPanes(args, group.rowGuids, rollupRow); const rowBackgroundPane = rowBackgroundPanes.rowBackgroundPane; const rowInsetPane = rowBackgroundPanes.rowInsetPane; const rollupUi = ui.rowUi(rollupRow.rowGuid); // expose panes in api via TimelineRowUi rollupUi.addPane('background', rowBackgroundPane); rollupUi.addPane('inset', rowInsetPane); const rollupDataAxis = rollupRow.dataAxis; let rollupContentPane = null; let rollupPaneFactory = null; const rollupContentOptions = { timelineFont: font, timelineFgColor: fgColor, draggableEdgeWidth: draggableEdgeWidth, snapToDistance: snapToDistance, isMaximized: false, mouseWheelListener: args.options.mouseWheelListener }; const refreshRollupContentPane = function () { const newRollupPaneFactory = (rollupUi.paneFactory || rowPaneFactoryChooser(rollupRow)); if (newRollupPaneFactory !== rollupPaneFactory) { if (rollupContentPane) { rollupContentPane.dispose.fire(); rowInsetPane.removePane(rollupContentPane); } rollupPaneFactory = newRollupPaneFactory; rollupContentPane = (rollupPaneFactory && rollupPaneFactory(drawable, timeAxis, rollupDataAxis, model, rollupRow, ui, rollupContentOptions)); if (rollupContentPane) { rowInsetPane.addPane(rollupContentPane); } drawable.redraw(); } }; rollupUi.paneFactoryChanged.on(refreshRollupContentPane); rollupRow.attrsChanged.on(refreshRollupContentPane); rollupRow.eventGuids.valueAdded.on(refreshRollupContentPane); rollupRow.eventGuids.valueRemoved.on(refreshRollupContentPane); rollupRow.timeseriesGuids.valueAdded.on(refreshRollupContentPane); rollupRow.timeseriesGuids.valueRemoved.on(refreshRollupContentPane); refreshRollupContentPane(); const groupButtonHeaderUnderlay = new Pane(newColumnLayout()); groupButtonHeaderUnderlay.addPane(groupButton, 0); groupButtonHeaderUnderlay.addPane(groupHeaderStripe, 1, { ignoreHeight: true }); groupHeaderUnderlay = new Pane(newColumnLayout()); groupHeaderUnderlay.addPainter(newBackgroundPainter(bgColor)); groupHeaderUnderlay.addPane(groupButtonHeaderUnderlay, 0, { width: rowLabelPaneWidth }); groupHeaderUnderlay.addPane(rowBackgroundPane, 1, { width: null }); groupHeaderPane = groupHeaderUnderlay; } else { groupHeaderUnderlay = new Pane(newColumnLayout()); groupHeaderUnderlay.addPainter(newBackgroundPainter(bgColor)); groupHeaderUnderlay.addPane(groupButton, 0); groupHeaderUnderlay.addPane(groupHeaderStripe, 1, { ignoreHeight: true }); const groupHeaderOverlay = newTimeAxisPane(args, null, group.cursor); const groupHeaderOverlayInsets = newInsets(0, 0, 0, rowLabelPaneWidth); groupHeaderPane = new Pane(newOverlayLayout()); groupHeaderPane.addPane(groupHeaderUnderlay, true); groupHeaderPane.addPane(newInsetPane(groupHeaderOverlay, groupHeaderOverlayInsets, null, false), false); } const groupHeaderHighlight = new Pane(newColumnLayout(), false); const groupHighlight = new Highlight(group.highlightColor || groupHighlightColor); const highlightInnerPane = new Pane(null); highlightInnerPane.addPainter(groupHighlight.newPainter()); const insets = group.highlightInsets || groupHighlightInsets; const width = group.highlightWidth || groupHighlightWidth; const containerWidth = insets ? insets.left + insets.right + width : width; const highlightPane = newInsetPane(highlightInnerPane, insets); groupHeaderHighlight.addPane(highlightPane, 0, { width: containerWidth, height: null }); const groupContainerOverlayPane = new Pane(newOverlayLayout()); const groupContainerRowPane = new Pane(newRowLayout()); const groupContentPane = new Pane(newRowLayout()); groupContainerOverlayPane.addPane(groupContainerRowPane, true, { width: null, height: null }); groupContainerOverlayPane.addPane(groupHeaderHighlight, false, { width: null, height: null }); groupContainerRowPane.updateLayoutArgs(function (layoutArg) { const shift = (isNumber(layoutArg) && layoutArg >= 2 * groupIndex); return (shift ? layoutArg + 2 : layoutArg); }); groupContainerRowPane.addPane(groupHeaderPane, 2 * groupIndex); groupContainerRowPane.addPane(groupContentPane, 2 * groupIndex + 1, { hide: group.collapsed }); timelineContentPane.addPane(groupContainerOverlayPane, groupIndex); groupHeaderPanes[groupGuid] = groupHeaderPane; groupContentPanes[groupGuid] = groupContentPane; groupContainerPanes[groupGuid] = groupContainerOverlayPane; const groupAttrsChanged = function (timelineGroup) { const groupContentLayoutOpts = groupContainerRowPane.layoutOptions(groupContentPane); const groupHighlightLayoutOpts = groupHeaderHighlight.layoutOptions(highlightPane); let redraw = false; if (timelineGroup.highlighted !== (!groupHighlightLayoutOpts.hide)) { groupHighlightLayoutOpts.hide = !timelineGroup.highlighted; redraw = true; } if (timelineGroup.highlightColor !== groupHighlight.color) { groupHighlight.color = timelineGroup.highlightColor; redraw = true; } if (timelineGroup.dashPattern !== groupHighlight.dashPattern) { groupHighlight.dashPattern = timelineGroup.dashPattern; redraw = true; } if (timelineGroup.dashLength !== groupHighlight.dashLength) { groupHighlight.dashLength = timelineGroup.dashLength; redraw = true; } if (timelineGroup.collapsed !== groupContentLayoutOpts.hide) { groupContentLayoutOpts.hide = timelineGroup.collapsed; redraw = true; } if (timelineGroup.labelFont !== groupContentLayoutOpts.labelFont) { groupLabel.font = timelineGroup.labelFont; redraw = true; } if (redraw) { drawable.redraw(); } }.bind(this, group); group.attrsChanged.on(groupAttrsChanged); groupAttrsChanged(); groupButton.mouseDown.on(function (ev) { if (isLeftMouseDown(ev.mouseEvent)) { group.collapsed = !group.collapsed; } }); // Handle hidden property // groupContainerRowPane.layoutOptions(groupContentPane).hide = group.hidden; groupContainerRowPane.layoutOptions(groupHeaderPane).hide = group.hidden; setupRowContainerPane(args, groupContentPane, group.rowGuids, false, group.groupGuid); groupContentPane.dispose.on(function () { group.attrsChanged.off(redrawLabel); group.attrsChanged.off(groupAttrsChanged); }); }; root.groupGuids.forEach(addGroup); root.groupGuids.valueAdded.on(addGroup); const moveGroup = function (groupGuid, groupOldIndex, groupNewIndex) { const nMin = Math.min(groupOldIndex, groupNewIndex); const nMax = Math.max(groupOldIndex, groupNewIndex); for (let n = nMin; n <= nMax; n++) { const groupGuidTemp = root.groupGuids.valueAt(n); timelineContentPane.setLayoutArg(groupContainerPanes[groupGuidTemp], n); // timelineContentPane.setLayoutArg(groupHeaderPanes[groupGuidTemp], 2 * n); // timelineContentPane.setLayoutArg(groupContentPanes[groupGuidTemp], 2 * n + 1); } drawable.redraw(); }; root.groupGuids.valueMoved.on(moveGroup); const removeGroup = function (groupGuid, groupIndex) { // const contentPane: Pane = groupContentPanes[groupGuid]; // const headerPane: Pane = groupHeaderPanes[groupGuid]; const groupPane = groupContainerPanes[groupGuid]; // contentPane.dispose.fire(); // headerPane.dispose.fire(); groupPane.dispose.fire(); // timelineContentPane.removePane(contentPane); // timelineContentPane.removePane(headerPane); timelineContentPane.removePane(groupPane); timelineContentPane.updateLayoutArgs(function (layoutArg) { // const shift = (isNumber(layoutArg) && layoutArg > 2 * groupIndex + 1); // return (shift ? layoutArg - 2 : layoutArg); const shift = (isNumber(layoutArg) && layoutArg > groupIndex); return (shift ? layoutArg - 1 : layoutArg); }); // delete groupHeaderPanes[groupGuid]; // delete groupContentPanes[groupGuid]; delete groupContainerPanes[groupGuid]; drawable.redraw(); }; root.groupGuids.valueRemoved.on(removeGroup); // Handle listing for hidden property // const groupAttrsChangedListeners = {}; const attachGroupAttrsChangedListener = function (groupGuid, groupIndex) { const group = model.group(groupGuid); const groupAttrsChangedListener = function () { if (hasval(group.hidden) && hasval(groupContentPanes[groupGuid])) { timelineContentPane.layoutOptions(groupContentPanes[groupGuid]).hide = group.hidden; timelineContentPane.layoutOptions(group