Skip to content

Commit

Permalink
fix(slider): Focus isue and standardize time function for time slider (
Browse files Browse the repository at this point in the history
…Canadian-Geospatial-Platform#2563)

* fix(slider): Tooltip with iso format function
Closes Canadian-Geospatial-Platform#1974, Canadian-Geospatial-Platform#2505

* fix overlap issue with label, style issue and start improve date mgt

* update time slider and refine overlap

* fix reviewable

---------

Co-authored-by: jolevesq <[email protected]>
  • Loading branch information
jolevesq and jolevesq authored Oct 25, 2024
1 parent 317055e commit 4bfd792
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 270 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { UIEventProcessor } from './ui-event-processor';
import { GVWMS } from '@/geo/layer/gv-layers/raster/gv-wms';
import { GVEsriImage } from '@/geo/layer/gv-layers/raster/gv-esri-image';
import { AbstractGVLayer } from '@/geo/layer/gv-layers/abstract-gv-layer';
import { DateMgt } from '@/core/utils/date-mgt';

// GV Important: See notes in header of MapEventProcessor file for information on the paradigm to apply when working with UIEventProcessor vs UIState

Expand Down Expand Up @@ -140,8 +141,8 @@ export class TimeSliderEventProcessor extends AbstractEventProcessor {
const { range } = temporalDimensionInfo.range;
const defaultValueIsArray = Array.isArray(temporalDimensionInfo.default);
const defaultValue = defaultValueIsArray ? temporalDimensionInfo.default[0] : temporalDimensionInfo.default;
const minAndMax: number[] = [new Date(range[0]).getTime(), new Date(range[range.length - 1]).getTime()];
const { field, singleHandle, nearestValues } = temporalDimensionInfo;
const minAndMax: number[] = [DateMgt.convertToMilliseconds(range[0]), DateMgt.convertToMilliseconds(range[range.length - 1])];
const { field, singleHandle, nearestValues, displayPattern } = temporalDimensionInfo;

// If the field type has an alias, use that as a label
let fieldAlias = field;
Expand All @@ -154,9 +155,9 @@ export class TimeSliderEventProcessor extends AbstractEventProcessor {

// eslint-disable-next-line no-nested-ternary
const values = singleHandle
? [new Date(temporalDimensionInfo.default).getTime()]
? [DateMgt.convertToMilliseconds(temporalDimensionInfo.default)]
: defaultValueIsArray
? [new Date(temporalDimensionInfo.default[0]).getTime(), new Date(temporalDimensionInfo.default[1]).getTime()]
? [DateMgt.convertToMilliseconds(temporalDimensionInfo.default[0]), DateMgt.convertToMilliseconds(temporalDimensionInfo.default[1])]
: [...minAndMax];

// If using discrete axis
Expand All @@ -180,6 +181,7 @@ export class TimeSliderEventProcessor extends AbstractEventProcessor {
delay: 1000,
locked: undefined,
reversed: undefined,
displayPattern,
};
}

Expand Down Expand Up @@ -262,7 +264,7 @@ export class TimeSliderEventProcessor extends AbstractEventProcessor {
let filter: string;
if (geoviewLayer instanceof WMS || geoviewLayer instanceof GVWMS) {
if (filtering) {
const newValue = `${new Date(values[0]).toISOString().slice(0, new Date(values[0]).toISOString().length - 5)}Z`;
const newValue = DateMgt.formatDateToISO(values[0]);
filter = `${field}=date '${newValue}'`;
} else {
filter = `${field}=date '${defaultValue}'`;
Expand All @@ -274,14 +276,14 @@ export class TimeSliderEventProcessor extends AbstractEventProcessor {
filter = `time=${minAndMax[0]},${defaultValue}`;
}
} else if (filtering) {
filter = `${field} >= date '${new Date(values[0]).toISOString()}'`;
filter = `${field} >= date '${DateMgt.formatDateToISO(values[0])}'`;
if (values.length > 1) {
filter += ` and ${field} <= date '${new Date(values[1]).toISOString()}'`;
filter += ` and ${field} <= date '${DateMgt.formatDateToISO(values[1])}'`;
}
} else {
filter = `${field} >= date '${new Date(minAndMax[0]).toISOString()}'`;
filter = `${field} >= date '${DateMgt.formatDateToISO(minAndMax[0])}'`;
if (values.length > 1) {
filter += `and ${field} <= date '${new Date(minAndMax[1]).toISOString()}'`;
filter += `and ${field} <= date '${DateMgt.formatDateToISO(minAndMax[1])}'`;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { GitHubIcon } from '@/ui/icons';
import { handleEscapeKey } from '@/core/utils/utilities';
import { FocusTrapContainer } from '../../common';
import { useUIActiveTrapGeoView } from '@/core/stores/store-interface-and-intial-values/ui-state';
import { DateMgt } from '@/core/utils/date-mgt';

// eslint-disable-next-line no-underscore-dangle
declare const __VERSION__: TypeAppVersion;
Expand Down Expand Up @@ -136,7 +137,7 @@ export default function Version(): JSX.Element {
</Link>
</Box>
<Typography component="div">{`v.${__VERSION__.major}.${__VERSION__.minor}.${__VERSION__.patch}`}</Typography>
<Typography component="div">{new Date(__VERSION__.timestamp).toLocaleDateString()}</Typography>
<Typography component="div">{DateMgt.formatDate(__VERSION__.timestamp, 'YYYY-MM-DD')}</Typography>
</Box>
</Paper>
</FocusTrapContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const getSxClasses = (theme: Theme): any => ({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
paddingRight: '10px',
},
'>div': {
display: 'flex',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,12 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps):
filterVariant: 'date',
muiFilterDatePickerProps: {
timezone: 'UTC',
format: 'YYYY/MM/DD',
format: 'YYYY-MM-DD',
// NOTE: reason for type cast as undefined as x-mui-datepicker prop type saying Date cant be assigned to undefined.
minDate: DateMgt.getDayjsDate('1600/01/01') as unknown as undefined,
minDate: DateMgt.getDayjsDate('1600-01-01') as unknown as undefined,
slotProps: {
textField: {
placeholder: language === VALID_DISPLAY_LANGUAGE[1] ? 'AAAA/MM/JJ' : 'YYYY/MM/DD',
placeholder: language === VALID_DISPLAY_LANGUAGE[1] ? 'AAAA-MM-JJ' : 'YYYY-MM-DD',
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function LayerOpacityControl(props: LayerOpacityControlProps): JSX.Elemen
value={(layerDetails.opacity ? layerDetails.opacity : 1) * 100}
onChange={handleSetOpacity}
marks={marks}
valueLabelDisplay="auto"
/>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useStore } from 'zustand';
import { useGeoViewStore } from '@/core/stores/stores-managers';
import { TypeGetStore, TypeSetStore } from '@/core/stores/geoview-store';
import { TimeSliderEventProcessor } from '@/api/event-processors/event-processor-children/time-slider-event-processor';
import { DatePrecision, TimePrecision } from '@/core/utils/date-mgt';

// GV Important: See notes in header of MapEventProcessor file for information on the paradigm to apply when working with TimeSliderEventProcessor vs TimeSliderState

Expand All @@ -25,6 +26,7 @@ export interface ITimeSliderState {
setSelectedLayerPath: (layerPath: string) => void;
setDefaultValue: (layerPath: string, defaultValue: string) => void;
setValues: (layerPath: string, values: number[]) => void;
setDisplayPattern: (layerPath: string, value: [DatePrecision, TimePrecision]) => void;
};

setterActions: {
Expand All @@ -40,6 +42,7 @@ export interface ITimeSliderState {
setSliderFilters: (newSliderFilters: Record<string, string>) => void;
setDefaultValue: (layerPath: string, defaultValue: string) => void;
setValues: (layerPath: string, values: number[]) => void;
setDisplayPattern: (layerPath: string, value: [DatePrecision, TimePrecision]) => void;
};
}

Expand All @@ -58,7 +61,6 @@ export function initializeTimeSliderState(set: TypeSetStore, get: TypeGetStore):
sliderFilters: {},

// #region ACTIONS

actions: {
addOrUpdateSliderFilter(layerPath: string, filter: string): void {
// Redirect to event processor
Expand Down Expand Up @@ -102,6 +104,10 @@ export function initializeTimeSliderState(set: TypeSetStore, get: TypeGetStore):
const { defaultValue, field, minAndMax, filtering } = get().timeSliderState.timeSliderLayers[layerPath];
TimeSliderEventProcessor.updateFilters(get().mapId, layerPath, defaultValue, field, filtering, minAndMax, values);
},
setDisplayPattern(layerPath: string, value: [DatePrecision, TimePrecision]): void {
// Redirect to setter
get().timeSliderState.setterActions.setDisplayPattern(layerPath, value);
},
},

setterActions: {
Expand Down Expand Up @@ -219,6 +225,16 @@ export function initializeTimeSliderState(set: TypeSetStore, get: TypeGetStore):
},
});
},
setDisplayPattern(layerPath: string, value: [DatePrecision, TimePrecision]): void {
const sliderLayers = get().timeSliderState.timeSliderLayers;
sliderLayers[layerPath].displayPattern = value;
set({
timeSliderState: {
...get().timeSliderState,
timeSliderLayers: { ...sliderLayers },
},
});
},
},

// #endregion ACTIONS
Expand Down Expand Up @@ -247,6 +263,7 @@ export interface TypeTimeSliderValues {
singleHandle: boolean;
title?: string;
values: number[];
displayPattern: [DatePrecision, TimePrecision];
}

// **********************************************************
Expand Down
92 changes: 76 additions & 16 deletions packages/geoview-core/src/core/utils/date-mgt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const DEFAULT_DATE_PRECISION = {
/** ******************************************************************************************************************************
* Type used to define the date precision pattern to use.
*/
type DatePrecision = 'year' | 'month' | 'day';
export type DatePrecision = 'year' | 'month' | 'day' | undefined;

/** ******************************************************************************************************************************
* constant/interface used to define the precision for time object (hh, mm, ss).
Expand All @@ -67,7 +67,7 @@ const timeUnitsESRI = {
/** ******************************************************************************************************************************
* Type used to define the time precision pattern to use.
*/
type TimePrecision = 'hour' | 'minute' | 'second';
export type TimePrecision = 'hour' | 'minute' | 'second' | undefined;

/** ******************************************************************************************************************************
* Type used to define the range values for an OGC time dimension.
Expand All @@ -87,6 +87,7 @@ export type TimeDimension = {
range: RangeItems;
nearestValues: 'discrete' | 'absolute';
singleHandle: boolean;
displayPattern: [DatePrecision | undefined, TimePrecision | undefined];
};

/** ******************************************************************************************************************************
Expand Down Expand Up @@ -191,6 +192,16 @@ export abstract class DateMgt {
return dayjs(date).local().format();
}

/**
* Convert a date local to a UTC date
* @param {Date | string} date date to use
* @returns {string} UTC date or empty string if invalid date (when field value is null)
*/
static convertToUTC(date: Date | string): string {
// check if it is a valid date and if so, return ISO string
return typeof date === 'string' && !isValidDate(date) ? '' : dayjs(date).utc(false).format();
}

/**
* Format a date to specific format like 'YYYY-MM-DD'
* @param {Date | string} date date to use
Expand All @@ -204,32 +215,77 @@ export abstract class DateMgt {
return dayjs(date).format(format);
}

/**
* Convert a date local to a UTC date
* @param {Date | string} date date to use
* @returns {string} UTC date or empty string if invalid date (when field value is null)
*/
static convertToUTC(date: Date | string): string {
// check if it is a valid date and if so, return ISO string
return typeof date === 'string' && !isValidDate(date) ? '' : dayjs(date).utc(false).format();
}

/**
* Format a date to a pattern
* @param {Date | string} date date to use
* @param {DatePrecision} datePattern the date precision pattern to use
* @param {TimePrecision}timePattern the time precision pattern to use
* @returns {string} formatted date
*/
static format(date: Date | string, datePattern: DatePrecision, timePattern?: TimePrecision): string {
static formatDatePattern(date: Date | number | string, datePattern: DatePrecision, timePattern?: TimePrecision): string {
// check if it is a valid date
if (typeof date === 'string' && !isValidDate(date)) throw new Error(`${INVALID_DATE} (format)`);
const validDate = typeof date !== 'number' ? DateMgt.convertToMilliseconds(date) : date;

// create or reformat date in ISO format
const pattern = `${DEFAULT_DATE_PRECISION[datePattern]}${timePattern !== undefined ? DEFAULT_TIME_PRECISION[timePattern] : ''}`;
const pattern = `${datePattern !== undefined ? DEFAULT_DATE_PRECISION[datePattern] : ''}${
timePattern !== undefined ? DEFAULT_TIME_PRECISION[timePattern] : ''
}`;

// output as local by default
return dayjs(date).utc(false).format(pattern);
return dayjs(new Date(validDate)).utc(true).format(pattern).replace('T', ' ').split('+')[0];
}

/**
* Converts a Date object to an ISO 8601 formatted string in the local time zone.
* The resulting string will be in the format: YYYY-MM-DDTHH:mm:ss.sss
*
* @param {Date | number | string} date - The Date object to be formatted.
* @returns {string} The formatted date string in ISO 8601 format.
*
* @throws {TypeError} If the input is not a valid Date object.
*/
static formatDateToISO(date: Date | number | string): string {
// check if it is a valid date
if (typeof date === 'string' && !isValidDate(date)) throw new Error(`${INVALID_DATE} (format)`);
const validDate = typeof date === 'number' ? DateMgt.convertMilisecondsToDate(date) : date;

return `${dayjs(validDate).utc(true).format('YYYY-MM-DDTHH:mm:ss')}Z`;
}

/**
* Attempts to guess the display pattern for a given date based on the provided format string.
*
* @param {(Date | number | string)[]} dates - An array of dates to analyze. Can be Date objects, timestamps (numbers), or date strings.
* @param {boolean} [onlyMinMax=true] - If true, only considers the minimum and maximum dates in the array.
* @returns {[DatePrecision | undefined, TimePrecision | undefined]} A tuple containing the guessed date and time precision.
*/
static guessDisplayPattern(
dates: Date[] | number[] | string[],
onlyMinMax = true
): [DatePrecision | undefined, TimePrecision | undefined] {
// check if it is a valid dates array
const validDates = dates.map((date) => {
if (typeof date === 'string' && !isValidDate(date)) throw new Error(`${INVALID_DATE} (format)`);
return typeof date !== 'number' ? DateMgt.convertToMilliseconds(date) : date;
});

// Check if range occurs in a single day or year
// TODO: we should check date pattern before and see if it should be only YYYY for example... use extractDateFormat
const delta: [DatePrecision | undefined, TimePrecision | undefined][] = [];
if (validDates.length === 1) {
delta.push(['day', 'minute']);
} else if (onlyMinMax) {
const timeDelta = validDates[validDates.length - 1] - validDates[0];
delta.push(timeDelta > 86400000 ? ['day', undefined] : [undefined, 'minute']);
} else {
for (let i = 0; i < validDates.length - 1; i++) {
const timeDelta = validDates[i + 1] - validDates[i];
delta.push(timeDelta > 86400000 ? ['day', undefined] : [undefined, 'minute']);
}
}

return delta[0];
}

/**
Expand Down Expand Up @@ -310,13 +366,15 @@ export abstract class DateMgt {
timeExtent[1]
)}Z${calcDuration()}`;
const rangeItem = this.createRangeOGC(dimensionValues);

const timeDimension: TimeDimension = {
field: startTimeField,
default: rangeItem.range[rangeItem.range.length - 1],
unitSymbol: '',
range: rangeItem,
nearestValues: startTimeField === '' ? 'absolute' : 'discrete',
singleHandle,
displayPattern: DateMgt.guessDisplayPattern(rangeItem.range),
};

return timeDimension;
Expand All @@ -329,13 +387,15 @@ export abstract class DateMgt {
*/
static createDimensionFromOGC(ogcTimeDimension: TypeJsonObject | string): TimeDimension {
const dimensionObject = typeof ogcTimeDimension === 'object' ? ogcTimeDimension : JSON.parse(<string>ogcTimeDimension);
const rangeItem = this.createRangeOGC(dimensionObject.values);
const timeDimension: TimeDimension = {
field: dimensionObject.name,
default: dimensionObject.default,
unitSymbol: dimensionObject.unitSymbol || '',
range: this.createRangeOGC(dimensionObject.values),
range: rangeItem,
nearestValues: dimensionObject.nearestValues !== false ? 'absolute' : 'discrete',
singleHandle: true,
displayPattern: DateMgt.guessDisplayPattern(rangeItem.range),
};

return timeDimension;
Expand Down
Loading

0 comments on commit 4bfd792

Please sign in to comment.