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

fix: Render SZ segment labels properly in Safari #774

Merged
merged 1 commit into from
Jul 16, 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
4 changes: 2 additions & 2 deletions common/components/maps/LineMap.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const redLineSegments: SegmentRenderOptions[] = [
{
mapSide: '0',
boundingSize: 40,
content: (
content: () => (
<div style={{ fontSize: 4 }}>
Greetings amigos thank you for inviting me into your SVG
</div>
Expand All @@ -35,7 +35,7 @@ const redLineSegments: SegmentRenderOptions[] = [
{
mapSide: '1',
boundingSize: 40,
content: (
content: () => (
<div style={{ fontSize: 4 }}>And on this side too! I also like being on this side!</div>
),
},
Expand Down
70 changes: 25 additions & 45 deletions common/components/maps/LineMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type SegmentLabel = {
mapSide: MapSide;
boundingSize?: number;
offset?: { x: number; y: number };
content: React.ReactNode;
content: (size: { width: number; height: number }) => React.ReactNode;
};

export type SegmentRenderOptions = {
Expand All @@ -33,12 +33,12 @@ export type SegmentRenderOptions = {
labels?: SegmentLabel[];
};

export type TooltipRenderer = (props: {
type TooltipRenderer = (props: {
segmentLocation: SegmentLocation<true>;
isHorizontal: boolean;
}) => React.ReactNode;

export type TooltipOptions = {
type TooltipOptions = {
render: TooltipRenderer;
snapToSegment?: boolean;
maxDistance?: number;
Expand All @@ -64,7 +64,7 @@ const getPropsForStrokeOptions = (options: Partial<StrokeOptions>) => {
};
};

const getLabelPositionProps = (
const getSegmentLabelBounds = (
segmentBounds: Rect,
segmentLabel: SegmentLabel,
isHorizontal: boolean
Expand All @@ -75,32 +75,19 @@ const getLabelPositionProps = (
if (isHorizontal) {
const moveAcross = mapSide === '0';
return {
foreignObjectProps: {
x: top + offset.x,
y: 0 - left - (moveAcross ? boundingSize + width : 0) + offset.y,
width: height,
height: boundingSize,
style: { transform: 'rotate(90deg)' },
},
wrapperDivStyles: {
flexDirection: 'row',
alignItems: moveAcross ? 'flex-end' : 'flex-start',
},
x: top + offset.x,
y: 0 - left - (moveAcross ? boundingSize + width : 0) + offset.y,
width: height,
height: boundingSize,
} as const;
}
const moveAcross = mapSide === '1';
return {
foreignObjectProps: {
x: right - (moveAcross ? boundingSize + width : 0) + offset.x,
y: top + offset.y,
height,
width: boundingSize,
},
wrapperDivStyles: {
flexDirection: 'column',
alignItems: moveAcross ? 'flex-end' : 'flex-start',
},
} as const;
x: right - (moveAcross ? boundingSize + width : 0) + offset.x,
y: top + offset.y,
height,
width: boundingSize,
};
};

export const LineMap: React.FC<LineMapProps> = ({
Expand Down Expand Up @@ -173,27 +160,15 @@ export const LineMap: React.FC<LineMapProps> = ({
});

const computedLabels = labels.map((label, index) => {
const { foreignObjectProps, wrapperDivStyles } = getLabelPositionProps(
bounds,
label,
isHorizontal
);
const { x, y, width, height } = getSegmentLabelBounds(bounds, label, isHorizontal);
return (
<foreignObject
<g
key={`label-${fromStationId}-${toStationId}-${index}`}
{...foreignObjectProps}
transform={`translate(${x}, ${y})`}
>
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
...wrapperDivStyles,
}}
>
{label.content}
</div>
</foreignObject>
<rect x={0} y={0} width={width} height={height} fill="transparent" />
{label.content({ width, height })}
</g>
);
});

Expand Down Expand Up @@ -263,7 +238,12 @@ export const LineMap: React.FC<LineMapProps> = ({
};

const renderComputedLabels = () => {
return computedSegmentExtras.map((segment) => segment.computedLabels).flat();
const transform = isHorizontal ? 'rotate(90)' : undefined;
return (
<g transform={transform}>
{computedSegmentExtras.map((segment) => segment.computedLabels).flat()}
</g>
);
};

const renderTooltip = () => {
Expand Down
4 changes: 2 additions & 2 deletions common/components/maps/useDiagramCoordinates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ export const useDiagramCoordinates = (options: Options) => {
const scaleBasis = getScaleBasis({ width: viewportWidth, height: viewportHeight });
setSvgProps({
viewBox: `${x} ${y} ${width} ${height}`,
width: width * scaleBasis,
height: height * scaleBasis,
width: Math.round(width * scaleBasis),
height: Math.round(height * scaleBasis),
});
}
}
Expand Down
46 changes: 46 additions & 0 deletions common/utils/time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,49 @@ export const getFormattedTimeString = (value: number, unit: 'minutes' | 'seconds
return `${duration.format('H')}h ${duration.format('m').padStart(2, '0')}m`;
}
};

interface GetClockFormattedTimeStringOptions {
truncateLeadingZeros?: boolean;
showSeconds?: boolean;
showHours?: boolean;
use12Hour?: boolean;
}

export const getClockFormattedTimeString = (
time: number,
options: GetClockFormattedTimeStringOptions = {}
): string => {
time = Math.round(time);
const {
truncateLeadingZeros = true,
showSeconds = false,
showHours = true,
use12Hour = false,
} = options;
let seconds = time,
minutes = 0,
hours = 0;
const minutesToAdd = Math.floor(seconds / 60);
seconds = seconds % 60;
minutes = minutes += minutesToAdd;
const hoursToAdd = Math.floor(minutes / 60);
minutes = minutes % 60;
hours += hoursToAdd;
const isPM = hours >= 12 && hours < 24;
hours = (use12Hour && hours > 12 ? hours - 12 : hours) % 24;
// eslint-disable-next-line prefer-const
let [hoursString, minutesString, secondsString] = [hours, minutes, seconds].map((num) =>
num.toString().padStart(2, '0')
);
let timeString = [hoursString, minutesString, secondsString]
.slice(showHours ? 0 : 1)
.slice(0, showSeconds ? 3 : 2)
.join(':');
if (truncateLeadingZeros && timeString.startsWith('0')) {
timeString = timeString.slice(1);
}
if (use12Hour) {
return `${timeString} ${isPM ? 'PM' : 'AM'}`;
}
return timeString;
};
2 changes: 1 addition & 1 deletion modules/slowzones/map/DirectionIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const DirectionIndicator: React.FC<DirectionIndicatorProps> = ({
}) => {
const rotation = isHorizontal ? (direction === '1' ? 90 : 270) : direction === '1' ? 180 : 0;
return (
<div
<span
className={styles.directionIndicator}
style={{
borderTopColor: color,
Expand Down
102 changes: 78 additions & 24 deletions modules/slowzones/map/SlowSegmentLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,64 +3,116 @@ import React, { useMemo } from 'react';
import type { SlowZoneResponse } from '../../../common/types/dataPoints';
import type { LineMetadata } from '../../../common/types/lines';

import { getFormattedTimeString } from '../../../common/utils/time';
import type { SlowZoneDirection, SlowZonesSegment } from './segment';
import { getClockFormattedTimeString } from '../../../common/utils/time';
import type { ByDirection, SlowZoneDirection, SlowZonesSegment } from './segment';
import { DIRECTIONS } from './segment';

import styles from './SlowSegmentLabel.module.css';
import { DirectionIndicator } from './DirectionIndicator';

interface SlowZoneLabelProps {
direction: SlowZoneDirection;
slowZone: SlowZoneResponse;
color: string;
offset: number;
isHorizontal: boolean;
containingWidth: number;
}

const LABEL_INNER_PADDING = 2;
const LABEL_HEIGHT = 4;
const FONT_SIZE = 3;

const SlowZoneLabel: React.FC<SlowZoneLabelProps> = ({
direction,
isHorizontal,
color,
containingWidth,
offset,
isHorizontal,
slowZone: { delay, baseline },
}) => {
const delayString = useMemo(() => getFormattedTimeString(delay), [delay]);

const delayString = useMemo(
() =>
getClockFormattedTimeString(delay, {
showHours: false,
showSeconds: true,
truncateLeadingZeros: true,
}),
[delay]
);
const indicatorBeforeText = direction === '1';
const indicatorSolidArrow = indicatorBeforeText
? isHorizontal
? '❮'
: '▲'
: isHorizontal
? '❯'
: '▼';
const fractionOverBaseline = -1 + (delay + baseline) / baseline;
const isBold = fractionOverBaseline >= 0.5;

const indicator = (
<tspan fontSize={FONT_SIZE * 1.25} fill={color}>
{indicatorSolidArrow}
</tspan>
);

const delayText = <tspan fontWeight={isBold ? 'bold' : undefined}>{delayString}</tspan>;

return (
<div
style={{
flexDirection: isHorizontal && direction === '0' ? 'row-reverse' : 'row',
fontWeight: fractionOverBaseline >= 0.5 ? 'bold' : 'normal',
whiteSpace: 'nowrap',
}}
className={styles.slowZoneLabel}
>
<DirectionIndicator
direction={direction}
color={color}
isHorizontal={isHorizontal}
size={2}
/>
+{delayString}
</div>
<text y={offset} x={containingWidth / 2} textAnchor="middle" fontSize={LABEL_HEIGHT}>
{indicatorBeforeText ? <>{indicator} </> : null}
{delayText}
{!indicatorBeforeText ? <> {indicator}</> : null}
</text>
);
};

interface SlowSegmentLabelProps {
segment: SlowZonesSegment;
line: LineMetadata;
isHorizontal: boolean;
width: number;
height: number;
}

const getDirectionLabelOffsets = (slowZones: ByDirection<SlowZoneResponse[]>, height: number) => {
const hasZero = slowZones['0'].length > 0;
const hasOne = slowZones['1'].length > 0;
const isBidi = hasZero && hasOne;
const midline = height / 2;
if (isBidi) {
const bidiOffset = LABEL_INNER_PADDING + LABEL_HEIGHT;
return {
'0': midline + bidiOffset / 2,
'1': midline - bidiOffset / 2,
};
}
if (hasZero) {
return {
'0': midline,
};
}
if (hasOne) {
return {
'1': midline,
};
}
return {};
};

export const SlowSegmentLabel: React.FC<SlowSegmentLabelProps> = (props) => {
const {
isHorizontal,
segment: { slowZones },
line,
width,
height,
} = props;

const offsets = getDirectionLabelOffsets(slowZones, height);

return (
<div className={styles.slowSegmentLabel}>
<g className={styles.slowSegmentLabel}>
{DIRECTIONS.map((direction) => {
const [zone] = slowZones[direction];
if (!zone) {
Expand All @@ -73,9 +125,11 @@ export const SlowSegmentLabel: React.FC<SlowSegmentLabelProps> = (props) => {
slowZone={zone}
color={line.color}
isHorizontal={isHorizontal}
offset={offsets[direction]!}
containingWidth={width}
/>
);
})}
</div>
</g>
);
};
9 changes: 8 additions & 1 deletion modules/slowzones/map/SlowZonesMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,14 @@ export const SlowZonesMap: React.FC<SlowZonesMapProps> = ({
mapSide: '0' as const,
boundingSize: isHorizontal ? 15 : 20,
...getSegmentLabelOverrides(segment.segmentLocation, isHorizontal),
content: <SlowSegmentLabel isHorizontal={isHorizontal} segment={segment} line={line} />,
content: (size) => (
<SlowSegmentLabel
isHorizontal={isHorizontal}
segment={segment}
line={line}
{...size}
/>
),
},
],
strokes: Object.entries(segment.slowZones).map(([direction, zones]) => {
Expand Down
Loading