Skip to content

Commit

Permalink
#674 Shanghai Suburban Railway style
Browse files Browse the repository at this point in the history
  • Loading branch information
thekingofcity committed Dec 26, 2024
1 parent f98b2b6 commit 49c0e27
Show file tree
Hide file tree
Showing 14 changed files with 381 additions and 19 deletions.
31 changes: 27 additions & 4 deletions src/components/side-panel/station-side-panel/more-section.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { Box, Heading } from '@chakra-ui/react';
import { useRootDispatch, useRootSelector } from '../../../redux';
import { RmgButtonGroup, RmgFields, RmgFieldsField } from '@railmapgen/rmg-components';
import { useTranslation } from 'react-i18next';
import { FACILITIES, Facilities, RmgStyle, Services } from '../../../constants/constants';
import { useRootDispatch, useRootSelector } from '../../../redux';
import {
updateStationCharacterSpacing,
updateStationCharacterSpacingToAll,
updateStationFacility,
updateStationIntPadding,
updateStationIntPaddingToAll,
updateStationLoopPivot,
updateStationOneLine,
updateStationServices,
} from '../../../redux/param/action';
import { RmgButtonGroup, RmgFields, RmgFieldsField } from '@railmapgen/rmg-components';
import { useTranslation } from 'react-i18next';

export default function MoreSection() {
const { t } = useTranslation();
const dispatch = useRootDispatch();

const selectedStation = useRootSelector(state => state.app.selectedStation);
const { style, loop } = useRootSelector(state => state.param);
const { services, facility, loop_pivot, one_line, int_padding } = useRootSelector(
const { services, facility, loop_pivot, one_line, int_padding, character_spacing } = useRootSelector(
state => state.param.stn_list[selectedStation]
);

Expand Down Expand Up @@ -102,6 +104,27 @@ export default function MoreSection() {
oneLine: true,
hidden: ![RmgStyle.SHMetro].includes(style),
},
{
type: 'input',
label: t('StationSidePanel.more.characterSpacing'),
value: character_spacing.toString(),
validator: val => Number.isInteger(val),
onChange: val => dispatch(updateStationCharacterSpacing(selectedStation, Number(val))),
hidden: ![RmgStyle.SHSuburbanRailway].includes(style),
},
{
type: 'custom',
label: t('StationSidePanel.more.intPaddingApplyGlobal'),
component: (
<RmgButtonGroup
selections={[{ label: t('StationSidePanel.more.apply'), value: '', disabled: false }]}
defaultValue=""
onChange={() => dispatch(updateStationCharacterSpacingToAll(selectedStation))}
/>
),
oneLine: true,
hidden: ![RmgStyle.SHSuburbanRailway].includes(style),
},
];

return (
Expand Down
11 changes: 10 additions & 1 deletion src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ export enum RmgStyle {
MTR = 'mtr',
GZMTR = 'gzmtr',
SHMetro = 'shmetro',
SHSuburbanRailway = 'shsubrwy',
}

export enum CanvasType {
Destination = 'destination',
RunIn = 'runin',
RailMap = 'railmap',
Indoor = 'indoor',
Platform = 'platform',
}

export const canvasConfig: { [s in RmgStyle]: CanvasType[] } = {
[RmgStyle.MTR]: [CanvasType.Destination, CanvasType.RailMap],
[RmgStyle.GZMTR]: [CanvasType.RunIn, CanvasType.RailMap],
[RmgStyle.SHMetro]: [CanvasType.Destination, CanvasType.RunIn, CanvasType.RailMap, CanvasType.Indoor],
[RmgStyle.SHSuburbanRailway]: [CanvasType.Destination, CanvasType.RunIn, CanvasType.Platform],
};

export enum SidePanelMode {
Expand Down Expand Up @@ -154,11 +157,17 @@ export interface StationInfo {
*/
one_line: boolean;
/**
* Padding between int box and station name. Default to 355 in updateParam.
* Padding between int box and station name in shmetro/station.
* Default to 355 in updateParam.
* This is calculated from (svg_height - 200) * 1.414 where typical svg_height
* is 450 and station element is tilted at a 45-degree angle.
*/
int_padding: number;
/**
* Controls spacing between text characters in station names.
* Default to 20 in updateParam.
*/
character_spacing: number;
}

export type StationDict = Record<string, StationInfo>;
Expand Down
9 changes: 6 additions & 3 deletions src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@
"oneLine": "Display Chinese and English in one line",
"intPadding": "Padding between station name and interchange box",
"intPaddingApplyGlobal": "Apply current padding to all stations",
"apply": "Apply"
"apply": "Apply",
"characterSpacing": "Station Name Letter Spacing"
},
"footer": {
"current": "Set as current",
Expand Down Expand Up @@ -225,12 +226,14 @@
"destination": "Destination",
"runin": "Running-in",
"railmap": "Rail map",
"indoor": "Indoor"
"indoor": "Indoor",
"platform": "Platform Num"
},

"RmgStyle": {
"mtr": "MTR",
"gzmtr": "Guangzhou Metro",
"shmetro": "Shanghai Metro"
"shmetro": "Shanghai Metro",
"shsubrwy": "Shanghai Suburban Railway"
}
}
9 changes: 6 additions & 3 deletions src/i18n/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@
"oneLine": "在一行内展示中英文站名",
"intPadding": "车站名与换乘线路间的间距",
"intPaddingApplyGlobal": "将当前的间距应用到所有车站上",
"apply": "应用"
"apply": "应用",
"characterSpacing": "车站名文字间距"
},
"footer": {
"current": "设置为当前车站",
Expand Down Expand Up @@ -295,12 +296,14 @@
"destination": "终点站站牌",
"runin": "当前站名牌",
"railmap": "站台门路线图",
"indoor": "车内路线图"
"indoor": "车内路线图",
"platform": "站台编号"
},

"RmgStyle": {
"mtr": "港铁",
"gzmtr": "广州地铁",
"shmetro": "上海地铁"
"shmetro": "上海地铁",
"shsubrwy": "上海市域铁路"
}
}
9 changes: 6 additions & 3 deletions src/i18n/translations/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@
"oneLine": "在一行內展示中英文站名",
"intPadding": "車站名與換乘線路間的間距",
"intPaddingApplyGlobal": "將當前的間距應用到所有車站上",
"apply": "應用"
"apply": "應用",
"characterSpacing": "車站名文字間距"
},
"footer": {
"current": "設定為當前車站",
Expand Down Expand Up @@ -286,11 +287,13 @@
"destination": "終點站牌",
"runin": "當前站名牌",
"railmap": "幕門路綫圖",
"indoor": "車內路綫圖"
"indoor": "車內路綫圖",
"platform": "站台編號"
},
"RmgStyle": {
"mtr": "港鐵",
"gzmtr": "廣州地鐵",
"shmetro": "上海地鐵"
"shmetro": "上海地鐵",
"shsubrwy": "上海市域鐵路"
}
}
24 changes: 23 additions & 1 deletion src/redux/param/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ export const updateStationIntPaddingToAll = (stationId: string) => {
const stationInfo = getState().param.stn_list[stationId];
const int_padding = stationInfo.int_padding;

const stationList = JSON.parse(JSON.stringify(getState().param.stn_list)) as StationDict;
const stationList = structuredClone(getState().param.stn_list);
Object.values(stationList).forEach(stnInfo => {
stnInfo.int_padding = int_padding;
});
Expand All @@ -490,6 +490,28 @@ export const updateStationIntPaddingToAll = (stationId: string) => {
};
};

export const updateStationCharacterSpacing = (stationId: string, character_spacing: number) => {
return (dispatch: RootDispatch, getState: () => RootState) => {
const stationInfo = getState().param.stn_list[stationId];

dispatch(setStation(stationId, { ...stationInfo, character_spacing }));
};
};

export const updateStationCharacterSpacingToAll = (stationId: string) => {
return (dispatch: RootDispatch, getState: () => RootState) => {
const stationInfo = getState().param.stn_list[stationId];
const character_spacing = stationInfo.character_spacing;

const stationList = structuredClone(getState().param.stn_list);
Object.values(stationList).forEach(stnInfo => {
stnInfo.character_spacing = character_spacing;
});

dispatch(setStationsBulk(stationList));
};
};

export const autoNumbering = (branchIndex: number, from: number, maxLength = 2, sort: 'asc' | 'desc' = 'asc') => {
return (dispatch: RootDispatch, getState: () => RootState) => {
const stationList = getState().param.stn_list;
Expand Down
3 changes: 3 additions & 0 deletions src/svgs/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ export const STYLE_CONFIG: Record<RmgStyle, StyleConfig> = {
shmetro: {
components: () => import('./shmetro'),
},
shsubrwy: {
components: () => import('./shanghaisuburbanrailway'),
},
};
147 changes: 147 additions & 0 deletions src/svgs/shanghaisuburbanrailway/destination-shsubrwy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { memo, useRef, useState, useEffect } from 'react';
import { CanvasType, Name, ShortDirection } from '../../constants/constants';
import { useRootSelector } from '../../redux';
import SvgWrapper from '../svg-wrapper';

const CANVAS_TYPE = CanvasType.Destination;

export default function DestinationSHSuburbanRailway() {
const { canvasScale } = useRootSelector(state => state.app);
const { svgWidth: svgWidths, svg_height: svgHeight, theme } = useRootSelector(store => store.param);

const svgWidth = svgWidths[CANVAS_TYPE];

return (
<SvgWrapper
type={CANVAS_TYPE}
svgWidth={svgWidth}
svgHeight={svgHeight}
canvasScale={canvasScale}
theme={theme}
>
<DefsSHSuburbanRailway />
<DestSHSuburbanRailway />
</SvgWrapper>
);
}

const DefsSHSuburbanRailway = memo(function DefsSHSuburbanRailway() {
return (
<defs>
{/* An extension of the line/path. Remember to minus the stroke-width. */}
<marker id="slope" viewBox="-1.5 0 3 1.5" refY={0.5}>
<path d="M0,0L1,1H-1z" fill="var(--rmg-theme-colour)" />
</marker>
</defs>
);
});

const DestSHSuburbanRailway = () => {
const { routes } = useRootSelector(store => store.helper);
const { current_stn_idx: current_stn_id, direction, stn_list } = useRootSelector(store => store.param);

// get valid destination of each branch
const get_valid_destinations = (routes: string[][], direction: ShortDirection, current_stn_id: string) => [
...new Set(
routes
.filter(route => route.includes(current_stn_id))
.map(route => {
const res = route.filter(stn_id => !['linestart', 'lineend'].includes(stn_id));
return direction === 'l' ? res[0] : res.reverse()[0];
})
),
];

// get destination id(s)
const dest_ids = get_valid_destinations(routes, direction, current_stn_id);

// turn destination id into name
const dest_name = dest_ids
.map(
id =>
[stn_list[id].localisedName.zh, stn_list[id].localisedName.en]
.filter(s => s !== undefined)
.map(s => s.replace('\\', '')) as Name
)
.at(0) ?? ['', ''];

return <Dest dest_name={dest_name} />;
};

const Dest = (props: { dest_name: Name }) => {
const { dest_name } = props;
const { direction, svgWidth, svg_height, theme } = useRootSelector(store => store.param);

return (
<g transform={`translate(0,${svg_height - 300})`}>
<path
stroke={theme[2]}
strokeWidth={12}
d={
direction === 'l'
? `M${svgWidth.destination - 24},16 H 36`
: `M24,16 H ${svgWidth.destination - 36}`
}
transform="translate(0,220)"
markerEnd="url(#slope)"
/>

<Terminal dest_name={dest_name} />
</g>
);
};

const Terminal = (props: { dest_name: Name }) => {
const { dest_name } = props;
const { direction, svgWidth } = useRootSelector(store => store.param);

const stnNameZhEl = useRef<SVGGElement | null>(null);
const stnNameEnEl = useRef<SVGGElement | null>(null);
// the original name position
const [nameWidth, setNameWidth] = useState(0);
useEffect(() => {
if (stnNameZhEl.current && stnNameEnEl.current) {
const w = Math.max(stnNameZhEl.current.getBBox().width, stnNameEnEl.current.getBBox().width);
setNameWidth(w);
}
}, [...dest_name]);

const LINE_PADDING = 24;
const ARROW_PADDING = 20;
const ARROW_WIDTH = 128;
const arrowDX = nameWidth + LINE_PADDING + ARROW_PADDING + ARROW_WIDTH;

return (
<g transform={`translate(0,145)`}>
<g transform={`translate(${direction === 'l' ? svgWidth.destination - arrowDX : arrowDX},20)`}>
<path
d="M60,60L0,0L60-60H100L55-15H160V15H55L100,60z"
fill="black"
transform={`rotate(${direction === 'l' ? 0 : 180})scale(0.8)`}
/>
</g>
<g
ref={stnNameZhEl}
transform={`translate(${direction === 'l' ? svgWidth.destination - LINE_PADDING : LINE_PADDING},25)`}
textAnchor={direction === 'l' ? 'end' : 'start'}
>
<text className="rmg-name__zh rmg-outline" fontSize={70} dy={7}>
{'往' + dest_name[0]}
</text>
</g>
<g
ref={stnNameEnEl}
transform={`translate(${direction === 'l' ? svgWidth.destination - LINE_PADDING : LINE_PADDING},25)`}
>
<text
className="rmg-name__en rmg-outline"
fontSize={25}
dx={direction === 'l' ? -nameWidth : 0}
dy={40}
>
{'To ' + dest_name[1]}
</text>
</g>
</g>
);
};
13 changes: 13 additions & 0 deletions src/svgs/shanghaisuburbanrailway/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ReactNode } from 'react';
import { CanvasType } from '../../constants/constants';
import DestinationSHSuburbanRailway from './destination-shsubrwy';
import PlatformSHSuburbanRailway from './platform-shsubrwy';
import RunInSHSuburbanRailway from './runin-shsubrwy';

const shsubrwySvgs: Partial<Record<CanvasType, ReactNode>> = {
destination: <DestinationSHSuburbanRailway />,
runin: <RunInSHSuburbanRailway />,
platform: <PlatformSHSuburbanRailway />,
};

export default shsubrwySvgs;
Loading

0 comments on commit 49c0e27

Please sign in to comment.