From 49c0e276ddf8b540b998fe11346c1aadbc51715d Mon Sep 17 00:00:00 2001 From: thekingofcity <3353040+thekingofcity@users.noreply.github.com> Date: Thu, 26 Dec 2024 23:17:36 +0800 Subject: [PATCH] #674 Shanghai Suburban Railway style --- .../station-side-panel/more-section.tsx | 31 +++- src/constants/constants.ts | 11 +- src/i18n/translations/en.json | 9 +- src/i18n/translations/zh-Hans.json | 9 +- src/i18n/translations/zh-Hant.json | 9 +- src/redux/param/action.ts | 24 ++- src/svgs/config.ts | 3 + .../destination-shsubrwy.tsx | 147 ++++++++++++++++++ src/svgs/shanghaisuburbanrailway/index.tsx | 13 ++ .../platform-shsubrwy.tsx | 46 ++++++ .../runin-shsubrwy.tsx | 67 ++++++++ src/svgs/shmetro/destination-shmetro.tsx | 1 - src/util/param-updater-utils.test.ts | 11 +- src/util/param-updater-utils.ts | 19 ++- 14 files changed, 381 insertions(+), 19 deletions(-) create mode 100644 src/svgs/shanghaisuburbanrailway/destination-shsubrwy.tsx create mode 100644 src/svgs/shanghaisuburbanrailway/index.tsx create mode 100644 src/svgs/shanghaisuburbanrailway/platform-shsubrwy.tsx create mode 100644 src/svgs/shanghaisuburbanrailway/runin-shsubrwy.tsx diff --git a/src/components/side-panel/station-side-panel/more-section.tsx b/src/components/side-panel/station-side-panel/more-section.tsx index 09ebb06d..7112b3e4 100644 --- a/src/components/side-panel/station-side-panel/more-section.tsx +++ b/src/components/side-panel/station-side-panel/more-section.tsx @@ -1,7 +1,11 @@ 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, @@ -9,8 +13,6 @@ import { 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(); @@ -18,7 +20,7 @@ export default function MoreSection() { 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] ); @@ -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: ( + dispatch(updateStationCharacterSpacingToAll(selectedStation))} + /> + ), + oneLine: true, + hidden: ![RmgStyle.SHSuburbanRailway].includes(style), + }, ]; return ( diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 5296c1d0..87fd5a1f 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -11,6 +11,7 @@ export enum RmgStyle { MTR = 'mtr', GZMTR = 'gzmtr', SHMetro = 'shmetro', + SHSuburbanRailway = 'shsubrwy', } export enum CanvasType { @@ -18,12 +19,14 @@ export enum CanvasType { 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 { @@ -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; diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 7b91cce8..4c11da3c 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -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", @@ -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" } } diff --git a/src/i18n/translations/zh-Hans.json b/src/i18n/translations/zh-Hans.json index 4a47e162..a923b51c 100644 --- a/src/i18n/translations/zh-Hans.json +++ b/src/i18n/translations/zh-Hans.json @@ -166,7 +166,8 @@ "oneLine": "在一行内展示中英文站名", "intPadding": "车站名与换乘线路间的间距", "intPaddingApplyGlobal": "将当前的间距应用到所有车站上", - "apply": "应用" + "apply": "应用", + "characterSpacing": "车站名文字间距" }, "footer": { "current": "设置为当前车站", @@ -295,12 +296,14 @@ "destination": "终点站站牌", "runin": "当前站名牌", "railmap": "站台门路线图", - "indoor": "车内路线图" + "indoor": "车内路线图", + "platform": "站台编号" }, "RmgStyle": { "mtr": "港铁", "gzmtr": "广州地铁", - "shmetro": "上海地铁" + "shmetro": "上海地铁", + "shsubrwy": "上海市域铁路" } } diff --git a/src/i18n/translations/zh-Hant.json b/src/i18n/translations/zh-Hant.json index 2438d3b5..39f28849 100644 --- a/src/i18n/translations/zh-Hant.json +++ b/src/i18n/translations/zh-Hant.json @@ -160,7 +160,8 @@ "oneLine": "在一行內展示中英文站名", "intPadding": "車站名與換乘線路間的間距", "intPaddingApplyGlobal": "將當前的間距應用到所有車站上", - "apply": "應用" + "apply": "應用", + "characterSpacing": "車站名文字間距" }, "footer": { "current": "設定為當前車站", @@ -286,11 +287,13 @@ "destination": "終點站牌", "runin": "當前站名牌", "railmap": "幕門路綫圖", - "indoor": "車內路綫圖" + "indoor": "車內路綫圖", + "platform": "站台編號" }, "RmgStyle": { "mtr": "港鐵", "gzmtr": "廣州地鐵", - "shmetro": "上海地鐵" + "shmetro": "上海地鐵", + "shsubrwy": "上海市域鐵路" } } diff --git a/src/redux/param/action.ts b/src/redux/param/action.ts index 91503426..8d204d67 100644 --- a/src/redux/param/action.ts +++ b/src/redux/param/action.ts @@ -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; }); @@ -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; diff --git a/src/svgs/config.ts b/src/svgs/config.ts index 26843c3c..f523bed2 100644 --- a/src/svgs/config.ts +++ b/src/svgs/config.ts @@ -16,4 +16,7 @@ export const STYLE_CONFIG: Record = { shmetro: { components: () => import('./shmetro'), }, + shsubrwy: { + components: () => import('./shanghaisuburbanrailway'), + }, }; diff --git a/src/svgs/shanghaisuburbanrailway/destination-shsubrwy.tsx b/src/svgs/shanghaisuburbanrailway/destination-shsubrwy.tsx new file mode 100644 index 00000000..463e7b83 --- /dev/null +++ b/src/svgs/shanghaisuburbanrailway/destination-shsubrwy.tsx @@ -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 ( + + + + + ); +} + +const DefsSHSuburbanRailway = memo(function DefsSHSuburbanRailway() { + return ( + + {/* An extension of the line/path. Remember to minus the stroke-width. */} + + + + + ); +}); + +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 ; +}; + +const Dest = (props: { dest_name: Name }) => { + const { dest_name } = props; + const { direction, svgWidth, svg_height, theme } = useRootSelector(store => store.param); + + return ( + + + + + + ); +}; + +const Terminal = (props: { dest_name: Name }) => { + const { dest_name } = props; + const { direction, svgWidth } = useRootSelector(store => store.param); + + const stnNameZhEl = useRef(null); + const stnNameEnEl = useRef(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 ( + + + + + + + {'往' + dest_name[0]} + + + + + {'To ' + dest_name[1]} + + + + ); +}; diff --git a/src/svgs/shanghaisuburbanrailway/index.tsx b/src/svgs/shanghaisuburbanrailway/index.tsx new file mode 100644 index 00000000..b02d3393 --- /dev/null +++ b/src/svgs/shanghaisuburbanrailway/index.tsx @@ -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> = { + destination: , + runin: , + platform: , +}; + +export default shsubrwySvgs; diff --git a/src/svgs/shanghaisuburbanrailway/platform-shsubrwy.tsx b/src/svgs/shanghaisuburbanrailway/platform-shsubrwy.tsx new file mode 100644 index 00000000..f7c23e62 --- /dev/null +++ b/src/svgs/shanghaisuburbanrailway/platform-shsubrwy.tsx @@ -0,0 +1,46 @@ +import { CanvasType } from '../../constants/constants'; +import { useRootSelector } from '../../redux'; +import SvgWrapper from '../svg-wrapper'; + +const CANVAS_TYPE = CanvasType.Platform; + +const PlatformSHSuburbanRailway = () => { + const { canvasScale } = useRootSelector(state => state.app); + const { svgWidth: svgWidths, svg_height, theme } = useRootSelector(store => store.param); + + const svgWidth = svgWidths[CANVAS_TYPE]; + + return ( + + + + ); +}; + +export default PlatformSHSuburbanRailway; + +const Platform = () => { + const { svgWidth, svg_height, platform_num } = useRootSelector(store => store.param); + + const middle = svgWidth.platform / 2; + + return ( + + + {platform_num} + + + 站台 + + + Platform + + + ); +}; diff --git a/src/svgs/shanghaisuburbanrailway/runin-shsubrwy.tsx b/src/svgs/shanghaisuburbanrailway/runin-shsubrwy.tsx new file mode 100644 index 00000000..03c2c92b --- /dev/null +++ b/src/svgs/shanghaisuburbanrailway/runin-shsubrwy.tsx @@ -0,0 +1,67 @@ +import { memo } from 'react'; +import { CanvasType } from '../../constants/constants'; +import { useRootSelector } from '../../redux'; +import SvgWrapper from '../svg-wrapper'; + +const CANVAS_TYPE = CanvasType.RunIn; + +const RunInSHSuburbanRailway = () => { + const { canvasScale } = useRootSelector(state => state.app); + const { svgWidth: svgWidths, svg_height, theme } = useRootSelector(store => store.param); + + const svgWidth = svgWidths[CANVAS_TYPE]; + // get the height + const dh = svg_height - 300; + + return ( + + + + + + + ); +}; + +export default RunInSHSuburbanRailway; + +const DefsSHSuburbanRailway = memo(function DefsSHSuburbanRailway() { + return ( + + {/* An extension of the line/path. Remember to minus the stroke-width. */} + + + + + ); +}); + +const Station = () => { + const { svgWidth, stn_list, current_stn_idx } = useRootSelector(store => store.param); + const { localisedName, character_spacing } = stn_list[current_stn_idx]; + const { zh: zhName = '', en: enName = '' } = localisedName; + + const middle = svgWidth.runin / 2; + + return ( + + + {zhName.replace('\\', '')} + + + {enName.replace('\\', '')} + + + ); +}; diff --git a/src/svgs/shmetro/destination-shmetro.tsx b/src/svgs/shmetro/destination-shmetro.tsx index 381caa13..ea161476 100644 --- a/src/svgs/shmetro/destination-shmetro.tsx +++ b/src/svgs/shmetro/destination-shmetro.tsx @@ -97,7 +97,6 @@ const DestSHMetro = () => { // destination names of loop line, `sh2020` type will always be two lines const dest_names = get_dest_names(regular_dest_ids, !loop && !(info_panel_type === 'sh2020')); - console.log(dest_names); const coline_dest_names = get_dest_names(coline_dest_ids, true); // this will give the space for at most two lines of dest_names diff --git a/src/util/param-updater-utils.test.ts b/src/util/param-updater-utils.test.ts index aa56ebcd..5ba23ed3 100644 --- a/src/util/param-updater-utils.test.ts +++ b/src/util/param-updater-utils.test.ts @@ -1,12 +1,13 @@ import { MonoColour } from '@railmapgen/rmg-palette-resources'; +import { vi } from 'vitest'; import { dottieGet, getMatchedThemesWithPaths, updateThemes, v5_10_updateInterchangeGroup, v5_17_updateLocalisedName, + v5_18_addStationNameSpacingAndSvgWidthPlatform, } from './param-updater-utils'; -import { vi } from 'vitest'; import { waitForMs } from './utils'; const originalFetch = global.fetch; @@ -133,6 +134,14 @@ describe('ParamUpdaterUtils', () => { expect(param.stn_list.stn0).not.toHaveProperty('secondaryName'); }); + it('v5_18_addStationNameSpacingAndSvgWidthPlatform', () => { + const param: Record = { svgWidth: {}, stn_list: { stn0: {} } }; + v5_18_addStationNameSpacingAndSvgWidthPlatform(param); + + expect(param.stn_list.stn0.character_spacing).toEqual(75); + expect(param.svgWidth.platform).toEqual(1000); + }); + it('Can find all matched themes with paths as expected', () => { const obj = { theme: ['hongkong', 'twl', '#E2231A', '#fff'], diff --git a/src/util/param-updater-utils.ts b/src/util/param-updater-utils.ts index a8ba4b46..814dd361 100644 --- a/src/util/param-updater-utils.ts +++ b/src/util/param-updater-utils.ts @@ -1,7 +1,7 @@ -import { InterchangeGroup, Name, Note, RMGParam, RmgStyle, StationInfo } from '../constants/constants'; -import { nanoid } from 'nanoid'; import { MonoColour, Theme, updateTheme } from '@railmapgen/rmg-palette-resources'; import rmgRuntime, { logger } from '@railmapgen/rmg-runtime'; +import { nanoid } from 'nanoid'; +import { CanvasType, InterchangeGroup, Name, Note, RMGParam, RmgStyle, StationInfo } from '../constants/constants'; export const updateParam = (param: { [x: string]: any }) => { // Version 0.10 @@ -253,6 +253,7 @@ export const updateParam = (param: { [x: string]: any }) => { // Version pre 5.10 v5_10_updateInterchangeGroup(param); v5_17_updateLocalisedName(param); + v5_18_addStationNameSpacingAndSvgWidthPlatform(param); sanitiseParam(param); return param; @@ -299,6 +300,20 @@ export const v5_17_updateLocalisedName = (param: Record) => { } }; +export const v5_18_addStationNameSpacingAndSvgWidthPlatform = (param: Record) => { + const { svgWidth } = param; + if (!(CanvasType.Platform in svgWidth)) { + param.svgWidth.platform = 1000; + } + + for (const [stnId, stnInfo] of Object.entries(param.stn_list as Record)) { + const { character_spacing } = stnInfo; + if (!character_spacing) { + param.stn_list[stnId].character_spacing = 75; + } + } +}; + interface MatchedThemeWithPaths { path: string; value: Theme;