Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DistanceUnitFormatter and DurationUnitFormatter, two react compon… #777

Merged
merged 4 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,19 @@
"DataSource": "Data source",
"DataSources": "Data sources",
"Highlights": "Highlights",
"minuteAbbr": "min.",
"hourAbbr": "hr",
"minuteAbbr": "min",
"secondAbbr": "sec",
"meterAbbr": "m",
"kilometerAbbr": "km",
"mileAbbr": "mi",
"feetAbbr": "ft",
"mpsAbbr": "m/sec",
"kphAbbr": "kph",
"mphAbbr": "mph",
"ftpsAbbr": "ft/s",
"copy": "copy",
"passengerAbbr": "pass.",
"secondAbbr": "sec",
"Max": "Max",
"Min": "Min",
"Left": "Left",
Expand Down
11 changes: 10 additions & 1 deletion locales/fr/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,19 @@
"DataSource": "Source de données",
"DataSources": "Sources de données",
"Highlights": "Faits saillants",
"hourAbbr": "h",
"secondAbbr": "s",
"minuteAbbr": "min",
"meterAbbr": "m",
"kilometerAbbr": "km",
"mileAbbr": "mi",
"feetAbbr": "pi",
"mpsAbbr": "m/s",
"kphAbbr": "km/h",
"mphAbbr": "mi/h",
"ftpsAbbr": "pi/s",
"copy": "copie",
"passengerAbbr": "pass.",
"secondAbbr": "sec",
"Max": "Max",
"Min": "Min",
"Left": "Gauche",
Expand Down
33 changes: 33 additions & 0 deletions packages/chaire-lib-common/src/utils/DateTimeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,38 @@ const hoursToSeconds = function (hours) {
return _isFinite(hours) ? hours * 3600 : null;
};

/**
* Convert a duration in seconds to a formatted time string.
* The function takes a duration in seconds and returns a string
* in the format "XX hr YY min ZZ sec", where each non-zero
* component is included with its corresponding unit.
*
* @param {number} seconds - The duration in seconds to be converted.
* @param {string} hourUnit - The unit for hours (e.g., "hr").
* @param {string} minuteUnit - The unit for minutes (e.g., "min").
* @param {string} secondsUnit - The unit for seconds (e.g., "sec").
* @returns {string} - The formatted time string or an empty string if the input is invalid.
*/
const toXXhrYYminZZsec = function (seconds: number, hourUnit: string, minuteUnit: string, secondsUnit: string) {
if (_isBlank(seconds)) {
return null;
}
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);

const timeComponents = [
{ value: hours, unit: hourUnit },
{ value: minutes, unit: minuteUnit },
{ value: secs, unit: secondsUnit }
];

return timeComponents
.map((component) => (component.value > 0 ? component.value + ' ' + component.unit : ''))
.filter(Boolean) // Discard empty components (ex: no seconds)
.join(' ');
};

/**
* Convert a time string (HH:MM[:ss]) to the number of seconds since midnight.
* Time strings can be > 24:00, for example for schedules for a day that end
Expand Down Expand Up @@ -168,6 +200,7 @@ export {
minutesToHoursDecimal,
minutesToSeconds,
hoursToSeconds,
toXXhrYYminZZsec,
timeStrToSecondsSinceMidnight,
intTimeToSecondsSinceMidnight,
roundSecondsToNearestMinute
Expand Down
21 changes: 21 additions & 0 deletions packages/chaire-lib-common/src/utils/PhysicsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ const mpsToKph = function (mps: number): number {
return mps * 3.6;
};

// M/s to ft/s
const mpsToFtps = function (mps: number): number {
return mps * 3.281;
};

const ftpsToMps = function (ftps: number): number {
return ftps / 3.281;
};

const kphToMph = function (kph: number): number {
return kph / 1.60934;
};
Expand All @@ -36,6 +45,14 @@ const milesToMeters = function (miles: number): number {
return miles * 1609.34;
};

const metersToFeet = function (meters: number): number {
return meters * 3.281;
};

const feetToMeters = function (feet: number): number {
return feet / 3.281;
};

const kmToMiles = function (km: number): number {
return km / 1.60934;
};
Expand Down Expand Up @@ -89,12 +106,16 @@ const durationFromAccelerationDecelerationDistanceAndRunningSpeed = function (
export {
kphToMps,
mpsToKph,
mpsToFtps,
ftpsToMps,
kphToMph,
mphToKph,
mpsToMph,
mphToMps,
metersToMiles,
milesToMeters,
metersToFeet,
feetToMeters,
kmToMiles,
milesToKm,
sqFeetToSqMeters,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2023, Polytechnique Montreal and contributors
*
* This file is licensed under the MIT License.
* License text available at https://opensource.org/licenses/MIT
*/
import React, { useState, useEffect } from 'react';
import { withTranslation, WithTranslation } from 'react-i18next';
import { roundToDecimals } from 'chaire-lib-common/lib/utils/MathUtils';
import { metersToMiles, metersToFeet } from 'chaire-lib-common/lib/utils/PhysicsUtils';

const destinationUnitOptions = ['km', 'm', 'mi', 'ft'] as const;
type destinationUnitOptionsType = typeof destinationUnitOptions[number];

export interface DistanceUnitFormatterProps extends WithTranslation {
value: number;
sourceUnit: 'km' | 'm';
destinationUnit?: destinationUnitOptionsType;
}

const DistanceUnitFormatter: React.FunctionComponent<DistanceUnitFormatterProps> = (
props: DistanceUnitFormatterProps
) => {

const [destinationUnit, setDestinationUnit] = useState<destinationUnitOptionsType | undefined>(props.destinationUnit);
const valueInMeters = props.sourceUnit === 'm' ? props.value : props.value / 1000;

useEffect(() => {
// If the destination unit was not specified, we choose the best one based on the magnitude of the value.
if (destinationUnit === undefined) {
if (valueInMeters < 1000) {
setDestinationUnit('m');
} else {
setDestinationUnit('km');
}
}
}, [valueInMeters]);

const unitFormatters: Record<destinationUnitOptionsType, (value: number) => string> = {
m: (value) => `${roundToDecimals(value, 0)} ${props.t('main:meterAbbr')}`,
km: (value) => `${roundToDecimals(value / 1000, 2)} ${props.t('main:kilometerAbbr')}`,
mi: (value) => `${roundToDecimals(metersToMiles(value), 2)} ${props.t('main:mileAbbr')}`,
ft: (value) => `${roundToDecimals(metersToFeet(value), 0)} ${props.t('main:feetAbbr')}`
};

const formattedValue = destinationUnit ? unitFormatters[destinationUnit](valueInMeters) : '';

const cycleThroughDestinationUnits = () => {
// Infer the next unit based on the currently displayed unit.
setDestinationUnit((prevUnit) => {
return destinationUnitOptions[
(destinationUnitOptions.indexOf(prevUnit as destinationUnitOptionsType) + 1) %
destinationUnitOptions.length
];
});
};

return <span onClick={cycleThroughDestinationUnits}>{formattedValue}</span>;
};

export default withTranslation([])(DistanceUnitFormatter);
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2023, Polytechnique Montreal and contributors
*
* This file is licensed under the MIT License.
* License text available at https://opensource.org/licenses/MIT
*/
import React, { useState, useEffect } from 'react';
import { withTranslation, WithTranslation } from 'react-i18next';
import { toXXhrYYminZZsec } from 'chaire-lib-common/lib/utils/DateTimeUtils';
import { roundToDecimals } from 'chaire-lib-common/lib/utils/MathUtils';

const destinationUnitOptions = ['hrMinSec', 's', 'm', 'h'] as const;
type destinationUnitOptionsType = typeof destinationUnitOptions[number];

export interface DurationUnitFormatterProps extends WithTranslation {
value: number;
sourceUnit: 's' | 'm' | 'h';
destinationUnit?: destinationUnitOptionsType;
}

const DurationUnitFormatter: React.FunctionComponent<DurationUnitFormatterProps> = (
props: DurationUnitFormatterProps
) => {
const [destinationUnit, setDestinationUnit] = useState<destinationUnitOptionsType | undefined>(props.destinationUnit);

const valueInSeconds =
props.sourceUnit === 's'
? props.value
: props.sourceUnit === 'm'
? props.value * 60
: props.sourceUnit === 'h'
? props.value * 60 * 60
: props.value;

useEffect(() => {
// If the destination unit was not specified, we choose a default.
if (destinationUnit === undefined) {
setDestinationUnit('hrMinSec');
}
}, [valueInSeconds]);

const unitFormatters: Record<destinationUnitOptionsType, (value: number) => string> = {
hrMinSec: (value) =>
toXXhrYYminZZsec(value, props.t('main:hourAbbr'), props.t('main:minuteAbbr'), props.t('main:secondAbbr')) ||
`${value.toString()} ${props.t('main:secondAbbr')}`,
s: (value) => `${roundToDecimals(value, 2)} ${props.t('main:secondAbbr')}`,
m: (value) => `${roundToDecimals(value / 60, 2)} ${props.t('main:minuteAbbr')}`,
h: (value) => `${roundToDecimals(value / 3600, 2)} ${props.t('main:hourAbbr')}`
};

const formattedValue = destinationUnit ? unitFormatters[destinationUnit](valueInSeconds) : '';

const cycleThroughDestinationUnits = () => {
// Infer the next unit based on the currently displayed unit.
setDestinationUnit((prevUnit) => {
return destinationUnitOptions[
(destinationUnitOptions.indexOf(prevUnit as destinationUnitOptionsType) + 1) %
destinationUnitOptions.length
];
});
};

return <span onClick={cycleThroughDestinationUnits}>{formattedValue}</span>;
};

export default withTranslation([])(DurationUnitFormatter);
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2023, Polytechnique Montreal and contributors
*
* This file is licensed under the MIT License.
* License text available at https://opensource.org/licenses/MIT
*/
import React, { useState, useEffect } from 'react';
import { withTranslation, WithTranslation } from 'react-i18next';
import { roundToDecimals } from 'chaire-lib-common/lib/utils/MathUtils';
import { mpsToKph, mpsToMph, mpsToFtps } from 'chaire-lib-common/lib/utils/PhysicsUtils';

const destinationUnitOptions = ['km/h', 'm/s', 'mph', 'ft/s'] as const;
type destinationUnitOptionsType = typeof destinationUnitOptions[number];

export interface SpeedUnitFormatterProps extends WithTranslation {
value: number;
sourceUnit: 'm/s' | 'km/h';
destinationUnit?: destinationUnitOptionsType;
}

const SpeedUnitFormatter: React.FunctionComponent<SpeedUnitFormatterProps> = (
props: SpeedUnitFormatterProps
) => {
const [destinationUnit, setDestinationUnit] = useState<destinationUnitOptionsType | undefined>(props.destinationUnit);

const valueInMetersPerSecond = props.sourceUnit === 'm/s' ? props.value : mpsToKph(props.value);

useEffect(() => {
// If the destination unit was not specified, we choose a default one.
if (destinationUnit === undefined) {
setDestinationUnit('km/h');
}
}, [valueInMetersPerSecond]);

const unitFormatters: Record<destinationUnitOptionsType, (value: number) => string> = {
'm/s': (value) => `${roundToDecimals(value, 0)} ${props.t('main:mpsAbbr')}`,
'km/h': (value) => `${roundToDecimals(mpsToKph(value), 1)} ${props.t('main:kphAbbr')}`,
'mph': (value) => `${roundToDecimals(mpsToMph(value), 1)} ${props.t('main:mphAbbr')}`,
'ft/s': (value) => `${roundToDecimals(mpsToFtps(value), 0)} ${props.t('main:ftpsAbbr')}`
};

const formattedValue = destinationUnit ? unitFormatters[destinationUnit](valueInMetersPerSecond) : '';

const cycleThroughDestinationUnits = () => {
// Infer the next unit based on the currently displayed unit.
setDestinationUnit((prevUnit) => {
return destinationUnitOptions[
(destinationUnitOptions.indexOf(prevUnit as destinationUnitOptionsType) + 1) %
destinationUnitOptions.length
];
});
};

return <span onClick={cycleThroughDestinationUnits}>{formattedValue}</span>;
};

export default withTranslation([])(SpeedUnitFormatter);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import React from 'react';
import { withTranslation, WithTranslation } from 'react-i18next';

import DistanceUnitFormatter from 'chaire-lib-frontend/lib/components/pageParts/DistanceUnitFormatter';
import DurationUnitFormatter from 'chaire-lib-frontend/lib/components/pageParts/DurationUnitFormatter';
import serviceLocator from 'chaire-lib-common/lib/utils/ServiceLocator';
import Path from 'transition-common/lib/services/path/Path';
import Node from 'transition-common/lib/services/nodes/Node';
Expand Down Expand Up @@ -120,10 +122,10 @@ const TransitPathButton: React.FunctionComponent<PathButtonProps> = (props: Path
<li className={'_path-segment-list'} key={`segment${nodeIndex}`}>
<span className="_path-segment-label-container">
<span className="_path-segment-label">
{segments[nodeIndex] ? segments[nodeIndex].travelTimeSeconds : '?'}s
{segments[nodeIndex].travelTimeSeconds ? <DurationUnitFormatter value={segments[nodeIndex].travelTimeSeconds!} sourceUnit='s' destinationUnit='s' /> : '? s'}
</span>
<span className="_path-segment-label">
{segments[nodeIndex] ? segments[nodeIndex].distanceMeters : '?'}m
{segments[nodeIndex].distanceMeters ? <DistanceUnitFormatter value={segments[nodeIndex].distanceMeters!} sourceUnit='m' /> : '?'}
</span>
{cumulativeTimeSecondsAfter && cumulativeDistanceMeters && <br />}
{cumulativeTimeSecondsAfter && cumulativeDistanceMeters && (
Expand All @@ -138,12 +140,14 @@ const TransitPathButton: React.FunctionComponent<PathButtonProps> = (props: Path
)}
{cumulativeTimeSecondsAfter && (
<span className="_path-segment-label" title={props.t('main:Cumulative')}>
{Math.ceil(cumulativeTimeSecondsAfter / 60)}&nbsp;min
<DurationUnitFormatter value={cumulativeTimeSecondsAfter} sourceUnit='s' />
</span>
)}
&#8213;
&nbsp;
{cumulativeDistanceMeters && (
<span className="_path-segment-label" title={props.t('main:Cumulative')}>
{Math.ceil(cumulativeDistanceMeters)}m
<DistanceUnitFormatter value={cumulativeDistanceMeters} sourceUnit='m' />
</span>
)}
</span>
Expand Down
Loading