From 65a15df02278f85dc32afd9d2ca663a6f2eb22d1 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 1 Jul 2024 08:29:21 -0700 Subject: [PATCH] Populate plugin store with UTC defaults, refactoring, fixes. --- .../ActivityDirectiveChangelog.svelte | 11 +- .../activity/ActivityDirectiveForm.svelte | 88 ++++------ .../ActivityDirectivesTablePanel.svelte | 6 +- .../activity/ActivitySpanForm.svelte | 24 +-- .../activity/ActivitySpansTablePanel.svelte | 10 +- .../ConstraintViolationButton.svelte | 51 +++--- src/components/timeline/Timeline.svelte | 93 ++--------- .../timeline/TimelineCursors.svelte | 12 +- src/components/timeline/Tooltip.svelte | 14 +- src/components/timeline/XAxis.svelte | 22 ++- src/routes/plans/+layout.svelte | 20 ++- src/routes/plans/+page.svelte | 121 +++++--------- src/stores/plugins.ts | 4 +- src/types/plugin.ts | 28 ++-- src/types/timeline.ts | 5 +- src/utilities/plugins.ts | 36 ++++- src/utilities/time.ts | 2 +- src/utilities/timeline.ts | 151 +++--------------- 18 files changed, 252 insertions(+), 446 deletions(-) diff --git a/src/components/activity/ActivityDirectiveChangelog.svelte b/src/components/activity/ActivityDirectiveChangelog.svelte index b167451216..40b27c7ecd 100644 --- a/src/components/activity/ActivityDirectiveChangelog.svelte +++ b/src/components/activity/ActivityDirectiveChangelog.svelte @@ -4,6 +4,7 @@ import HistoryIcon from '@nasa-jpl/stellar/icons/history.svg?component'; import { createEventDispatcher, onMount } from 'svelte'; import { plan } from '../../stores/plan'; + import { plugins } from '../../stores/plugins'; import type { ActivityDirective, ActivityDirectiveRevision, @@ -112,12 +113,12 @@ // Manually check remaining fields that could have changed and require extra formatting if (current.start_offset !== previous.start_offset) { - const currentStartTimeDoy = getDoyTimeFromInterval(planStartTimeYmd, current.start_offset); - const previousStartTimeDoy = getDoyTimeFromInterval(planStartTimeYmd, previous.start_offset); + const currentStartTime = getDoyTimeFromInterval(planStartTimeYmd, current.start_offset); + const previousStartTime = getDoyTimeFromInterval(planStartTimeYmd, previous.start_offset); - differences['Start Time (UTC)'] = { - currentValue: currentStartTimeDoy, - previousValue: previousStartTimeDoy, + differences[`Start Time (${$plugins.time.primary.label})`] = { + currentValue: currentStartTime, + previousValue: previousStartTime, }; } diff --git a/src/components/activity/ActivityDirectiveForm.svelte b/src/components/activity/ActivityDirectiveForm.svelte index 7780b5ed06..8201a061a2 100644 --- a/src/components/activity/ActivityDirectiveForm.svelte +++ b/src/components/activity/ActivityDirectiveForm.svelte @@ -38,14 +38,9 @@ import { permissionHandler } from '../../utilities/permissionHandler'; import { featurePermissions } from '../../utilities/permissions'; import { pluralize } from '../../utilities/text'; - import { - getDoyTime, - getDoyTimeFromInterval, - getIntervalFromDoyRange, - getUnixEpochTimeFromInterval, - } from '../../utilities/time'; + import { getDoyTime, getIntervalFromDoyRange, getUnixEpochTimeFromInterval } from '../../utilities/time'; import { tooltip } from '../../utilities/tooltip'; - import { required, timestamp } from '../../utilities/validators'; + import { required } from '../../utilities/validators'; import Collapse from '../Collapse.svelte'; import ActivityMetadataField from '../activityMetadata/ActivityMetadataField.svelte'; import DatePickerField from '../form/DatePickerField.svelte'; @@ -101,22 +96,14 @@ $: activityType = (activityTypes ?? []).find(({ name: activityTypeName }) => activityDirective?.type === activityTypeName) ?? null; $: { - if ($plugins.time?.primary?.format && $plugins.time?.primary?.parse) { - const startTimeMs = getUnixEpochTimeFromInterval( - planStartTimeYmd, - revision ? revision.start_offset : activityDirective.start_offset, - ); - startTime = $plugins.time?.primary?.format(new Date(startTimeMs)); - } else { - startTime = getDoyTimeFromInterval( - planStartTimeYmd, - revision ? revision.start_offset : activityDirective.start_offset, - ); - } + const startTimeMs = getUnixEpochTimeFromInterval( + planStartTimeYmd, + revision ? revision.start_offset : activityDirective.start_offset, + ); + startTime = $plugins.time.primary.format(new Date(startTimeMs)); } - $: startTimeFieldValidators = $plugins.time?.primary?.validate || timestamp; - $: startTimeField = field(startTime, [required, startTimeFieldValidators]); + $: startTimeField = field(startTime, [required, $plugins.time.primary.validate]); $: activityNameField = field(activityDirective.name); $: if (activityType && activityDirective.arguments) { @@ -376,13 +363,10 @@ } function onUpdateStartTime() { - if ($startTimeField.valid /* && startTime !== $startTimeField.value */) { - console.log('startTimeField.value :>> ', $startTimeField.value); + if ($startTimeField.valid && startTime !== $startTimeField.value) { const { id } = activityDirective; const planStartTimeDoy = getDoyTime(new Date(planStartTimeYmd)); - const startTimeDoy = $plugins.time?.primary?.parse - ? getDoyTime($plugins.time?.primary?.parse($startTimeField.value)) - : $startTimeField.value; + const startTimeDoy = getDoyTime($plugins.time.primary.parse($startTimeField.value)); const start_offset = getIntervalFromDoyRange(planStartTimeDoy, startTimeDoy); if ($plan) { effects.updateActivityDirective($plan, id, { start_offset }, activityType, user); @@ -572,28 +556,11 @@ - {#if $plugins.time?.primary?.parse} -
- - - - - - -
- {:else} + {#if $plugins.time.enableDatePicker} + {:else} +
+ + + + + + +
{/if}
- - ) => { if ($plan && params && params.data && typeof params.data.start_time_ms === 'number') { - return ($plugins.time?.primary?.format ?? getDoyTime)(new Date(params.data.start_time_ms), false); + return $plugins.time.primary.format(new Date(params.data.start_time_ms)); } return ''; }, diff --git a/src/components/activity/ActivitySpanForm.svelte b/src/components/activity/ActivitySpanForm.svelte index 43ae1f99a4..893ee8430f 100644 --- a/src/components/activity/ActivitySpanForm.svelte +++ b/src/components/activity/ActivitySpanForm.svelte @@ -11,7 +11,7 @@ import { getSpanRootParent } from '../../utilities/activities'; import effects from '../../utilities/effects'; import { getFormParameters } from '../../utilities/parameters'; - import { getDoyTimeFromInterval, getUnixEpochTime, getUnixEpochTimeFromInterval } from '../../utilities/time'; + import { getUnixEpochTimeFromInterval } from '../../utilities/time'; import { tooltip } from '../../utilities/tooltip'; import Collapse from '../Collapse.svelte'; import Input from '../form/Input.svelte'; @@ -43,23 +43,15 @@ $: activityType = (activityTypes ?? []).find(({ name: activityTypeName }) => span.type === activityTypeName) ?? null; $: rootSpan = getSpanRootParent(spansMap, span.id); $: rootSpanHasChildren = (rootSpan && spanUtilityMaps.spanIdToChildIdsMap[rootSpan.id]?.length > 0) ?? false; + $: { - if ($plugins.time?.primary?.format && $plugins.time?.primary?.parse) { - const startTimeMs = getUnixEpochTimeFromInterval(planStartTimeYmd, span.start_offset); - startTime = $plugins.time?.primary?.format(new Date(startTimeMs)); - } else { - startTime = getDoyTimeFromInterval(planStartTimeYmd, span.start_offset); - } + const startTimeMs = getUnixEpochTimeFromInterval(planStartTimeYmd, span.start_offset); + startTime = $plugins.time.primary.format(new Date(startTimeMs)); } $: if (span.duration) { - if ($plugins.time?.primary?.format && $plugins.time?.primary?.parse) { - const endTimeMs = getUnixEpochTimeFromInterval(planStartTimeYmd, span.duration); - endTime = $plugins.time?.primary?.format(new Date(endTimeMs)); - } else { - const startTimeISO = new Date(getUnixEpochTime(startTime)).toISOString(); - endTime = getDoyTimeFromInterval(startTimeISO, span.duration); - } + const endTimeMs = getUnixEpochTimeFromInterval(planStartTimeYmd, span.duration); + endTime = $plugins.time.primary.format(new Date(endTimeMs)); } else { endTime = null; } @@ -182,14 +174,14 @@ diff --git a/src/components/activity/ActivitySpansTablePanel.svelte b/src/components/activity/ActivitySpansTablePanel.svelte index ef8e1b3f8f..bdcdbe3715 100644 --- a/src/components/activity/ActivitySpansTablePanel.svelte +++ b/src/components/activity/ActivitySpansTablePanel.svelte @@ -12,7 +12,6 @@ import type { Span } from '../../types/simulation'; import type { AutoSizeColumns, ViewGridSection, ViewTable } from '../../types/view'; import { filterEmpty } from '../../utilities/generic'; - import { getDoyTime } from '../../utilities/time'; import { tooltip } from '../../utilities/tooltip'; import GridMenu from '../menus/GridMenu.svelte'; import type DataGrid from '../ui/DataGrid/DataGrid.svelte'; @@ -84,13 +83,14 @@ derived_start_time: { filter: 'text', field: 'startMs', - headerName: `Absolute End Time (${$plugins.time?.primary?.label ?? 'UTC'})`, + headerName: `Absolute Start Time (${$plugins.time.primary.label})`, hide: true, resizable: true, sortable: true, valueGetter: params => { if (params && params.data && typeof params.data.startMs === 'number') { - return ($plugins.time?.primary?.format ?? getDoyTime)(new Date(params.data.startMs), false); + /* TODO could use short format here to skip ms but do we need short(er) format somewhere else? */ + return $plugins.time.primary.format(new Date(params.data.startMs)); } return ''; }, @@ -99,13 +99,13 @@ derived_end_time: { filter: 'text', field: 'endMs', - headerName: 'Absolute End Time (UTC)', + headerName: `Absolute End Time (${$plugins.time.primary.label})`, hide: true, resizable: true, sortable: true, valueGetter: params => { if (params && params.data && typeof params.data.endMs === 'number') { - return ($plugins.time?.primary?.format ?? getDoyTime)(new Date(params.data.endMs), false); + return $plugins.time.primary.format(new Date(params.data.endMs)); } return ''; }, diff --git a/src/components/constraints/ConstraintViolationButton.svelte b/src/components/constraints/ConstraintViolationButton.svelte index 75c69ab94e..ca404ef557 100644 --- a/src/components/constraints/ConstraintViolationButton.svelte +++ b/src/components/constraints/ConstraintViolationButton.svelte @@ -8,23 +8,9 @@ export let window: TimeRange; - $: ({ - doy: startDoy, - hours: startHours, - mins: startMins, - msecs: startMsecs, - secs: startSecs, - year: startYear, - } = getDoyTimeComponents(new Date(window.start))); + let isDoyPattern = false; - $: ({ - doy: endDoy, - hours: endHours, - mins: endMins, - msecs: endMsecs, - secs: endSecs, - year: endYear, - } = getDoyTimeComponents(new Date(window.end))); + $: isDoyPattern = new RegExp(/^(\d{4})-(\d{3})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?$/); function zoomToViolation(window: TimeRange): void { $viewTimeRange = window; @@ -33,21 +19,36 @@ diff --git a/src/components/timeline/Timeline.svelte b/src/components/timeline/Timeline.svelte index 8a46391e68..cdf5d6cd26 100644 --- a/src/components/timeline/Timeline.svelte +++ b/src/components/timeline/Timeline.svelte @@ -31,17 +31,7 @@ XAxisTick, } from '../../types/timeline'; import { clamp } from '../../utilities/generic'; - import { getDoyTime } from '../../utilities/time'; - import { - MAX_CANVAS_SIZE, - TimelineInteractionMode, - TimelineLockStatus, - customD3Ticks, - durationMonth, - durationWeek, - durationYear, - getXScale, - } from '../../utilities/timeline'; + import { MAX_CANVAS_SIZE, TimelineInteractionMode, TimelineLockStatus, getXScale } from '../../utilities/timeline'; import TimelineRow from './Row.svelte'; import RowHeaderDragHandleWidth from './RowHeaderDragHandleWidth.svelte'; import TimelineContextMenu from './TimelineContextMenu.svelte'; @@ -100,7 +90,6 @@ let tooltip: Tooltip; let cursorEnabled: boolean = true; let cursorHeaderHeight: number = 0; - let estimatedLabelWidthPx: number = 130; // Width of MS time which is the largest display format let histogramCursorTime: Date | null = null; let mouseOver: MouseOver | null; let removeDPRChangeListener: (() => void) | null = null; @@ -130,13 +119,12 @@ $: rows = timeline?.rows || []; $: drawWidth = clientWidth > 0 ? clientWidth - (timeline?.marginLeft ?? 0) - (timeline?.marginRight ?? 0) : 0; - $: estimatedLabelWidthPx = $plugins.time?.ticks?.tickLabelWidth ?? 130; - $: xAxisDrawHeight = 48 + 16 * ($plugins.time?.additional ? Math.max($plugins.time.additional.length, 1) : 1); + $: xAxisDrawHeight = 48 + 16 * ($plugins.time.additional.length ? Math.max($plugins.time.additional.length, 1) : 1); // Compute number of ticks based off draw width $: if (drawWidth) { const padding = 1.5; - let ticks = Math.round(drawWidth / (estimatedLabelWidthPx * padding)); + let ticks = Math.round(drawWidth / ($plugins.time.ticks.maxLabelWidth * padding)); tickCount = clamp(ticks, 2, 16); // Recompute zoom transform based off new drawWidth @@ -151,72 +139,19 @@ $: xScaleMax = getXScale(xDomainMax, drawWidth); $: xScaleView = getXScale(xDomainView, drawWidth); $: xScaleViewDuration = viewTimeRange.end - viewTimeRange.start; - $: formattedPlanStartTime = $plugins.time?.primary?.format - ? $plugins.time?.primary?.format(xDomainMax[0]) - : plan?.start_time_doy; - $: formattedPlanEndTime = $plugins.time?.primary?.format - ? $plugins.time?.primary?.format(xDomainMax[1]) - : plan?.end_time_doy; + $: formattedPlanStartTime = $plugins.time.primary.format(xDomainMax[0]); + $: formattedPlanEndTime = $plugins.time.primary.format(xDomainMax[1]); $: if (viewTimeRangeStartDate && viewTimeRangeEndDate && tickCount) { - let labelWidth = estimatedLabelWidthPx; // Adjust label width depending on zoom level - const ticksFn = $plugins.time?.ticks?.getTicks ?? customD3Ticks; - xTicksView = ticksFn(viewTimeRangeStartDate, viewTimeRangeEndDate, tickCount).map((date: Date) => { - const doyTimestamp = getDoyTime(date, true); - let formattedPrimaryDate = ''; - if ($plugins.time?.primary?.format) { - formattedPrimaryDate = $plugins.time?.primary?.format(date); - } else { - formattedPrimaryDate = doyTimestamp; - if (xScaleViewDuration > durationYear * tickCount) { - formattedPrimaryDate = doyTimestamp.slice(0, 4); - labelWidth = $plugins.time?.ticks?.tickLabelWidth ?? 28; - } else if (xScaleViewDuration > durationMonth * tickCount) { - formattedPrimaryDate = doyTimestamp.slice(0, 8); - labelWidth = $plugins.time?.ticks?.tickLabelWidth ?? 50; - } else if (xScaleViewDuration > durationWeek) { - formattedPrimaryDate = doyTimestamp.slice(0, 8); - labelWidth = $plugins.time?.ticks?.tickLabelWidth ?? 58; - } - } - - const additionalFormats: string[] = []; - let tick: XAxisTick = { additionalFormats: [], date, formattedPrimaryDate, hideLabel: false }; - - if ($plugins.time?.additional?.length) { - $plugins.time.additional.forEach(timeSystem => { - if (timeSystem.format) { - additionalFormats.push(timeSystem.format(date)); - } - }); - } else { - let formattedSecondaryDate = date.toLocaleString(); - if (xScaleViewDuration > durationYear * tickCount) { - formattedSecondaryDate = date.getFullYear().toString(); - labelWidth = $plugins.time?.ticks?.tickLabelWidth ?? 28; - } else if (xScaleViewDuration > durationMonth * tickCount) { - formattedSecondaryDate = date.toLocaleDateString(); - labelWidth = $plugins.time?.ticks?.tickLabelWidth ?? 50; - } else if (xScaleViewDuration > durationWeek) { - formattedSecondaryDate = date.toLocaleDateString(); - labelWidth = $plugins.time?.ticks?.tickLabelWidth ?? 58; - } - additionalFormats.push(formattedSecondaryDate); - } - - tick.additionalFormats = additionalFormats; - - return { additionalFormats, date, formattedPrimaryDate, hideLabel: false }; + xTicksView = $plugins.time.ticks.getTicks(viewTimeRangeStartDate, viewTimeRangeEndDate, tickCount).map(date => { + const label = $plugins.time.primary.formatTick(date, xScaleViewDuration, tickCount); + const additionalLabels = $plugins.time.additional.map(timeSystem => { + return timeSystem.formatTick + ? timeSystem.formatTick(date, xScaleViewDuration, tickCount) + : timeSystem.format(date); + }); + return { additionalLabels, date, label }; }); - - // Determine whether or not to hide the last tick label - // which has the potential to draw past the drawWidth - if (xTicksView.length) { - const lastTick = xTicksView[xTicksView.length - 1]; - if (xScaleView(lastTick.date) + labelWidth > drawWidth) { - lastTick.hideLabel = true; - } - } } afterUpdate(() => { @@ -441,7 +376,7 @@ {/if} diff --git a/src/components/timeline/TimelineCursors.svelte b/src/components/timeline/TimelineCursors.svelte index e2316d67d2..fcce2f2492 100644 --- a/src/components/timeline/TimelineCursors.svelte +++ b/src/components/timeline/TimelineCursors.svelte @@ -155,10 +155,8 @@ cursorX = offsetX; } date = new Date(unixEpochTime); - // cursorTimeDOY = getDoyTime(date); - /* BROKENNN */ - cursorTimeLabel = $plugins.time?.primary?.format ? $plugins.time?.primary?.format(date) : cursorTimeDOY; - cursorTimeLabel += ' ' + ($plugins.time?.primary?.label || 'UTC'); + cursorTimeLabel = $plugins.time.primary.format(date); + cursorTimeLabel += ' ' + $plugins.time.primary.label; } cursorMaxWidth = drawWidth - cursorX; cursorX = cursorX + marginLeft; @@ -186,7 +184,11 @@ x={cursorX} label={cursorTimeLabel} maxWidth={cursorMaxWidth} - on:click={() => addVerticalGuide(getDoyTime(date))} + on:click={() => { + if (xScaleView) { + addVerticalGuide(getDoyTime(xScaleView.invert(offsetX))); + } + }} activeCursor /> {/if} diff --git a/src/components/timeline/Tooltip.svelte b/src/components/timeline/Tooltip.svelte index d1f07463bd..a9192ed3cd 100644 --- a/src/components/timeline/Tooltip.svelte +++ b/src/components/timeline/Tooltip.svelte @@ -10,7 +10,6 @@ import type { ConstraintResultWithName } from '../../types/constraint'; import type { ResourceType, Span } from '../../types/simulation'; import type { LineLayer, LinePoint, MouseOver, Point, Row, XRangePoint } from '../../types/timeline'; - import { getDoyTime } from '../../utilities/time'; import { filterResourcesByLayer } from '../../utilities/timeline'; export let interpolateHoverValue: boolean = false; @@ -30,8 +29,7 @@ onMouseOver(mouseOver); } - $: primaryTimeLabel = $plugins.time?.primary?.label || 'UTC'; - $: primaryTimeFormatter = $plugins.time?.primary?.format || getDoyTime; + $: primaryTimeLabel = $plugins.time.primary.label; function onMouseOver(event: MouseOver | undefined) { if (event && !hidden) { @@ -238,7 +236,7 @@ function textForActivityDirective(activityDirective: ActivityDirective): string { const { anchor_id, id, name, start_time_ms, type } = activityDirective; const directiveStartTime = - typeof start_time_ms === 'number' ? primaryTimeFormatter(new Date(start_time_ms)) : 'Unknown'; + typeof start_time_ms === 'number' ? $plugins.time.primary.format(new Date(start_time_ms)) : 'Unknown'; return `
${DirectiveIcon} Activity Directive
@@ -299,7 +297,7 @@ color = (layer as LineLayer).lineColor; } - const pointTime = primaryTimeFormatter(new Date(x)); + const pointTime = $plugins.time.primary.format(new Date(x)); return `
@@ -328,8 +326,8 @@ function textForSpan(span: Span): string { const { id, duration, startMs, endMs, type } = span; - const spanStartTime = primaryTimeFormatter(new Date(startMs)); - const spanEndTime = primaryTimeFormatter(new Date(endMs)); + const spanStartTime = $plugins.time.primary.format(new Date(startMs)); + const spanEndTime = $plugins.time.primary.format(new Date(endMs)); return `
${SpanIcon} Simulated Activity (Span)
@@ -367,7 +365,7 @@ name = layer.name ? layer.name : point.name; color = (layer as LineLayer).lineColor; } - const pointTime = primaryTimeFormatter(new Date(x)); + const pointTime = $plugins.time.primary.format(new Date(x)); return `
diff --git a/src/components/timeline/XAxis.svelte b/src/components/timeline/XAxis.svelte index b1843a3ce2..206d00c756 100644 --- a/src/components/timeline/XAxis.svelte +++ b/src/components/timeline/XAxis.svelte @@ -32,7 +32,6 @@ let svg: SVGElement; let zoom: ZoomBehavior; - $: primaryTimeLabel = $plugins.time?.primary?.label ?? 'UTC'; $: svgSelection = select(svg) as Selection; /* TODO could this be a custom svelte use action? */ @@ -68,6 +67,13 @@ } dispatch('zoom', e); } + + function measureTick(tick: XAxisTick): number { + // TODO is this heuristic good enough for now? Actually measuring text is expensive + // and the previous method of estimating tick size based on the dynamic format was ugly especially for plugins + const labels = [tick.label].concat(tick.additionalLabels); + return Math.max(...labels.map(label => label.length * 6)); + }
-
{primaryTimeLabel}
- {#if $plugins.time?.additional && $plugins.time?.additional?.length > 0} - {#each $plugins.time?.additional as timeSystem} +
{$plugins.time.primary.label}
+ {#if $plugins.time.additional.length > 0} + {#each $plugins.time.additional as timeSystem}
{timeSystem.label}
@@ -97,11 +103,13 @@ {#if drawWidth > 0} {#each xTicksView as tick} - {#if !tick.hideLabel} + {@const x = xScaleView?.(tick.date) ?? 0} + {#if x + measureTick(tick) < drawWidth} + - {tick.formattedPrimaryDate} + {tick.label} - {#each tick.additionalFormats as label, i} + {#each tick.additionalLabels as label, i} import { env } from '$env/dynamic/public'; + import { mergeWith } from 'lodash-es'; import { onMount } from 'svelte'; import Nav from '../../components/app/Nav.svelte'; - import { plugins, pluginsLoaded } from '../../stores/plugins'; + import { plugins, pluginsError, pluginsLoaded } from '../../stores/plugins'; import { loadPluginCode } from '../../utilities/plugins'; - import { showFailureToast } from '../../utilities/toast'; let pluginsEnabled = env.PUBLIC_TIME_PLUGIN_ENABLED === 'true'; $pluginsLoaded = pluginsEnabled ? false : true; @@ -19,17 +19,19 @@ async function loadPlugins() { try { - $plugins = await loadPluginCode('/resources/time-plugin.js'); + // Load plugins and merge with default plugin + const userPlugins = await loadPluginCode('/resources/time-plugin.js'); + $plugins = mergeWith($plugins, userPlugins, (_, src) => (Array.isArray(src) ? src : undefined)); $pluginsLoaded = true; } catch (err) { console.log('Unable to load plugin:', err); - showFailureToast('Unable to load plugin'); - // TODO should we allow users to continue if the plugin cannot be loaded? + $pluginsLoaded = false; + $pluginsError = `Unable to load plugin: ${err}`; } } -{#if !pluginsEnabled || $pluginsLoaded} +{#if !pluginsEnabled || ($pluginsLoaded && !$pluginsError)} {:else}
@@ -38,7 +40,11 @@ class="st-typography-header" style="align-items: center;display: flex; flex: 1; justify-content: center; width: 100%" > - Loading plugins... + {#if $pluginsError} + {$pluginsError} + {:else} + Loading plugins... + {/if}
{/if} diff --git a/src/routes/plans/+page.svelte b/src/routes/plans/+page.svelte index dec2a8b6da..8f2bcddd94 100644 --- a/src/routes/plans/+page.svelte +++ b/src/routes/plans/+page.svelte @@ -36,14 +36,8 @@ import { removeQueryParam } from '../../utilities/generic'; import { permissionHandler } from '../../utilities/permissionHandler'; import { featurePermissions } from '../../utilities/permissions'; - import { - convertDoyToYmd, - convertUsToDurationString, - getDoyTime, - getShortISOForDate, - getUnixEpochTime, - } from '../../utilities/time'; - import { min, required, timestamp, unique } from '../../utilities/validators'; + import { convertDoyToYmd, convertUsToDurationString, getDoyTime, getShortISOForDate } from '../../utilities/time'; + import { min, required, unique } from '../../utilities/validators'; import type { PageData } from './$types'; export let data: PageData; @@ -104,16 +98,12 @@ { field: 'start_time', filter: 'text', - headerName: `Start ${$plugins.time?.primary?.label || 'Time'}`, + headerName: 'Start Time', resizable: true, sortable: true, valueGetter: (params: ValueGetterParams) => { if (params.data) { - if ($plugins.time?.primary?.format) { - return $plugins.time?.primary?.format(new Date(params.data.start_time)); - } else { - return params.data?.start_time_doy.split('T')[0]; - } + return $plugins.time.primary.formatShort(new Date(params.data.start_time)); } }, width: 150, @@ -121,18 +111,14 @@ { field: 'end_time', filter: 'text', - headerName: `End ${$plugins.time?.primary?.label || 'Time'}`, + headerName: 'End Time', resizable: true, sortable: true, valueGetter: (params: ValueGetterParams) => { if (params.data) { - if ($plugins.time?.primary?.format) { - const endTime = convertDoyToYmd(params.data.end_time_doy); - if (endTime) { - return $plugins.time?.primary?.format(new Date(endTime)); - } - } else { - return params.data?.end_time_doy.split('T')[0]; + const endTime = convertDoyToYmd(params.data.end_time_doy); + if (endTime) { + return $plugins.time.primary.formatShort(new Date(endTime)); } } }, @@ -198,9 +184,8 @@ ]); let simTemplateField = field(null); - $: timeFieldValidators = $plugins.time?.primary?.validate || timestamp; - $: startTimeField = field('', [required, timeFieldValidators]); - $: endTimeField = field('', [required, timeFieldValidators]); + $: startTimeField = field('', [required, $plugins.time.primary.validate]); + $: endTimeField = field('', [required, $plugins.time.primary.validate]); $: models = data.models; $: { @@ -275,12 +260,8 @@ }); async function createPlan() { - let startTime = $startTimeField.value; - let endTime = $endTimeField.value; - if ($plugins.time?.primary?.parse) { - startTime = getDoyTime($plugins.time?.primary?.parse(startTime)); - endTime = getDoyTime($plugins.time?.primary?.parse(endTime)); - } + let startTime = getDoyTime($plugins.time.primary.parse($startTimeField.value)); + let endTime = getDoyTime($plugins.time.primary.parse($endTimeField.value)); const newPlan = await effects.createPlan( endTime, $modelIdField.value, @@ -340,17 +321,8 @@ async function onStartTimeChanged() { if ($startTimeField.value && $startTimeField.valid && $endTimeField.value === '') { // Set end time as start time plus a day by default - let newEndTime = ''; - if ($plugins.time?.primary?.parse && $plugins.time?.primary?.format) { - const startTimeDate = $plugins.time?.primary?.parse($startTimeField.value); - // TODO this isn't actually incrementing the plugin's date by 1 - startTimeDate.setDate(startTimeDate.getDate() + 1); - newEndTime = $plugins.time?.primary?.format(startTimeDate); - } else { - const startTimeDate = new Date(getUnixEpochTime($startTimeField.value)); - startTimeDate.setDate(startTimeDate.getDate() + 1); - newEndTime = getDoyTime(startTimeDate, false); - } + const startTimeDate = $plugins.time.primary.parse($startTimeField.value); + let newEndTime = $plugins.time.primary.format($plugins.time.getDefaultPlanEndDate(startTimeDate)); await endTimeField.validateAndSet(newEndTime); } @@ -359,17 +331,10 @@ function updateDurationString() { if ($startTimeField.valid && $endTimeField.valid) { - let startTimeMS = 0; - let endTimeMS = 0; - if ($plugins.time?.primary?.parse) { - startTimeMS = $plugins.time?.primary?.parse($startTimeField.value).getTime(); - endTimeMS = $plugins.time?.primary?.parse($endTimeField.value).getTime(); - } else { - startTimeMS = getUnixEpochTime($startTimeField.value); - endTimeMS = getUnixEpochTime($endTimeField.value); - } + let startTimeMs = $plugins.time.primary.parse($startTimeField.value).getTime(); + let endTimeMs = $plugins.time.primary.parse($endTimeField.value).getTime(); - durationString = convertUsToDurationString((endTimeMS - startTimeMS) * 1000); + durationString = convertUsToDurationString((endTimeMs - startTimeMs) * 1000); if (!durationString) { durationString = 'None'; @@ -432,23 +397,12 @@ /> - {#if $plugins.time?.primary?.parse} -
- - - - - - -
- {:else} + {#if $plugins.time.enableDatePicker}
- {/if} - - {#if $plugins.time?.primary?.parse} -
- - - - - - -
- {:else}
+ {:else} +
+ + + + + + +
+
+ + + + + + +
{/if}
diff --git a/src/stores/plugins.ts b/src/stores/plugins.ts index 0a187ad8bc..dd4a7e21ba 100644 --- a/src/stores/plugins.ts +++ b/src/stores/plugins.ts @@ -1,7 +1,9 @@ import { writable, type Writable } from 'svelte/store'; import type { Plugins } from '../types/plugin'; +import { defaultPlugins } from '../utilities/plugins'; /* Writeable. */ -export const plugins: Writable = writable({}); +export const plugins: Writable = writable(defaultPlugins); export const pluginsLoaded: Writable = writable(false); +export const pluginsError: Writable = writable(''); diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 61f47f4dd5..bfee5701ae 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -5,20 +5,26 @@ export type PluginCode = { }; export type PluginTime = { - format?: (date: Date) => string; - formatString?: string; - label?: string; - parse?: (string: string) => Date; - validate?: (string: string) => Promise; + format: (date: Date) => string; + formatShort: (date: Date) => string; + formatString: string; + formatTick: (date: Date, durationMs: number, tickCount: number) => string; + label: string; + parse: (string: string) => Date; + validate: (string: string) => Promise; }; +type Optional = Pick, K> & Omit; + export type Plugins = { - time?: { - additional?: PluginTime[]; // TODO bikeshed - primary?: PluginTime; - ticks?: { - getTicks?: (start: Date, stop: Date, count: number) => Date[]; - tickLabelWidth?: number; + time: { + additional: Optional[]; + enableDatePicker: boolean; + getDefaultPlanEndDate: (start: Date) => Date; + primary: PluginTime; + ticks: { + getTicks: (start: Date, stop: Date, count: number) => Date[]; + maxLabelWidth: number; }; }; }; diff --git a/src/types/timeline.ts b/src/types/timeline.ts index d7d18a1cf9..62e993ffe0 100644 --- a/src/types/timeline.ts +++ b/src/types/timeline.ts @@ -220,10 +220,9 @@ export type VerticalGuideSelection = { }; export type XAxisTick = { - additionalFormats: string[]; + additionalLabels: string[]; date: Date; - formattedPrimaryDate: string; - hideLabel: boolean; + label: string; }; /** diff --git a/src/utilities/plugins.ts b/src/utilities/plugins.ts index 28f8302b4c..cf7c287e85 100644 --- a/src/utilities/plugins.ts +++ b/src/utilities/plugins.ts @@ -1,6 +1,40 @@ -import type { PluginCode } from '../types/plugin'; +import type { PluginCode, Plugins } from '../types/plugin'; +import { getDoyTime, getTimeZoneName, getUnixEpochTime } from './time'; +import { formatTickLocalTZ, formatTickUtc, utcTicks } from './timeline'; +import { timestamp } from './validators'; export async function loadPluginCode(path: string) { const code = (await import(/* @vite-ignore */ path)) as PluginCode; return await code.getPlugin(); } + +export const defaultPlugins: Plugins = { + time: { + additional: [ + { + format: date => date.toLocaleString(), + formatTick: formatTickLocalTZ, + label: getTimeZoneName(), + }, + ], + enableDatePicker: true, + getDefaultPlanEndDate: start => { + const end = new Date(start); + end.setDate(end.getDate() + 1); + return end; + }, + primary: { + format: (date: Date) => getDoyTime(date), + formatShort: (date: Date) => getDoyTime(date).split('T')[0], + formatString: 'YYYY-DDDThh:mm:ss', + formatTick: formatTickUtc, + label: 'UTC', + parse: (string: string) => new Date(getUnixEpochTime(string)), + validate: timestamp, + }, + ticks: { + getTicks: utcTicks, + maxLabelWidth: 130, + }, + }, +}; diff --git a/src/utilities/time.ts b/src/utilities/time.ts index c3071064ff..095836c4e4 100644 --- a/src/utilities/time.ts +++ b/src/utilities/time.ts @@ -353,7 +353,7 @@ export function getDoyTime(date: Date, includeMsecs = true): string { const { doy, hours, mins, msecs, secs, year } = getDoyTimeComponents(date); let doyTimestamp = `${year}-${doy}T${hours}:${mins}:${secs}`; - if (includeMsecs) { + if (includeMsecs && date.getMilliseconds() > 0) { doyTimestamp += `.${msecs}`; } diff --git a/src/utilities/timeline.ts b/src/utilities/timeline.ts index afa2bcadcd..447ba1e50c 100644 --- a/src/utilities/timeline.ts +++ b/src/utilities/timeline.ts @@ -36,135 +36,7 @@ import type { XRangeLayer, } from '../types/timeline'; import { filterEmpty } from './generic'; - -// let spiceInstance: any = undefined; - -// const LMST_SC_ID = -168900; -// const SPICE_LMST_RE = /^\d\/(\d+):(\d{2}):(\d{2}):(\d{2}):(\d+)?$/; -// const DISPLAY_LMST_RE = /^(Sol-)?(\d+)M(\d{2}):(\d{2}):(\d{2})(.(\d*))?$/; - -// function trimLeadingZeroes(s: string): string { -// return parseInt(s, 10).toString(10); -// } - -// function lmstToEphemeris(lmst: string): number { -// const matcher = lmst.match(DISPLAY_LMST_RE); -// if (!matcher) { -// return NaN; -// } - -// const sol = trimLeadingZeroes(matcher[2] || ''); -// const hour = matcher[3] || ''; -// const mins = matcher[4] || ''; -// const secs = matcher[5] || ''; -// const subsecs = parseFloat(matcher[6] || '0') -// .toFixed(5) -// .substring(2); -// const sclkch = `${sol}:${hour}:${mins}:${secs}:${subsecs}`; -// // const sclkch = sol + ':' + hour + ':' + mins + ':' + secs + ':' + subsecs; -// return spiceInstance.scs2e(LMST_SC_ID, sclkch); -// } - -// function ephemerisToLMST(et: number): string { -// const lmst: string = spiceInstance.sce2s(LMST_SC_ID, et); -// // something like "1/01641:07:16:13:65583" -// const m = lmst.match(SPICE_LMST_RE); -// if (m) { -// const sol = trimLeadingZeroes(m[1]); -// const hour = m[2]; -// const mins = m[3]; -// const secs = m[4]; -// const subsecs = m[5] || '0'; -// return sol + 'M' + hour + ':' + mins + ':' + secs + '.' + subsecs; -// } -// return ''; -// } - -// export function ephemerisToUTC(et: number): string { -// return spiceInstance.et2utc(et, 'ISOC', 100); -// } - -// export function lmstToUTC(lmst: string) { -// if (spiceInstance) { -// try { -// const et = lmstToEphemeris(lmst); -// const utc = ephemerisToUTC(et); -// return utc; -// } catch (error) { -// console.log('error :>> ', error); -// } -// } -// return ''; -// } - -// export function utcToLmst(utc: string) { -// if (spiceInstance) { -// try { -// const et = spiceInstance.str2et(utc); -// return ephemerisToLMST(et); -// } catch (error) { -// console.error(error); -// return ''; -// } -// } -// return 'no spice'; -// } - -// export async function initializeSpice() { -// const initializingSpice = await new Spice().init(); - -// // Load the kernels -// const kernelBuffers = await Promise.all( -// [ -// 'http://localhost:3000/kernels/m2020_lmst_ops210303_v1.tsc', -// 'http://localhost:3000/kernels/m2020.tls', -// 'http://localhost:3000/kernels/m2020.tsc', -// ].map(url => fetch(url).then(res => res.arrayBuffer())), -// ); - -// // Load the kernels into Spice -// for (let i = 0; i < kernelBuffers.length; i++) { -// initializingSpice.loadKernel(kernelBuffers[i]); -// } - -// spiceInstance = initializingSpice; - -// // console.log('spiceInstance :>> ', spiceInstance); - -// console.log('Spice initialized'); - -// const utc = new Date().toISOString().slice(0, -1); -// console.log(`utc: ${utc}`); -// console.log(`et: ${initializingSpice.utc2et(utc)}`); -// console.log(`MIN_SCLK: ${initializingSpice.scs2e(LMST_SC_ID, '00000:00:00:00:00')}`); -// console.log(`lmst: ${utcToLmst(utc)}`); -// } - -// (async () => { -// const initializingSpice = await new Spice().init(); - -// // Load the kernels -// const kernelBuffers = await Promise.all( -// [ -// 'http://localhost:3000/kernels/m2020_lmst_ops210303_v1.tsc', -// 'http://localhost:3000/kernels/m2020.tls', -// 'http://localhost:3000/kernels/m2020.tsc', -// ].map(url => fetch(url).then(res => res.arrayBuffer())), -// ); - -// // Load the kernels into Spice -// for (let i = 0; i < kernelBuffers.length; i++) { -// initializingSpice.loadKernel(kernelBuffers[i]); -// } - -// spiceInstance = initializingSpice; - -// // const utc = new Date().toISOString().slice(0, -1); -// // console.log(`utc: ${utc}`); -// // console.log(`et: ${initializingSpice.utc2et(utc)}`); -// // console.log(`MIN_SCLK: ${initializingSpice.scs2e(LMST_SC_ID, '00000:00:00:00:00')}`); -// // console.log(`lmst: ${utcToLmst(utc)}`); -// })(); +import { getDoyTime } from './time'; export enum TimelineLockStatus { Locked = 'Locked', @@ -248,7 +120,7 @@ export function customD3TickInterval(start: Date, stop: Date, count: number): Ti } // Based on https://github.com/d3/d3-time/blob/main/src/ticks.js -export function customD3Ticks(start: Date, stop: Date, count: number) { +export function utcTicks(start: Date, stop: Date, count: number) { const reverse = stop < start; if (reverse) { [start, stop] = [stop, start]; @@ -259,6 +131,25 @@ export function customD3Ticks(start: Date, stop: Date, count: number) { return reverse ? ticks.reverse() : ticks; } +export function formatTickUtc(date: Date, viewDurationMs: number, tickCount: number): string { + let label = getDoyTime(date); + if (viewDurationMs > durationYear * tickCount) { + label = label.slice(0, 4); + } else if (viewDurationMs > durationMonth * tickCount) { + label = label.slice(0, 8); + } else if (viewDurationMs > durationWeek) { + label = label.slice(0, 8); + } + return label; +} + +export function formatTickLocalTZ(date: Date, viewDurationMs: number, tickCount: number): string { + if (viewDurationMs > durationYear * tickCount) { + return date.getFullYear().toString(); + } + return date.toLocaleString(); +} + export const CANVAS_PADDING_X = 0; export const CANVAS_PADDING_Y = 8;