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 3cf15d8ea..82aff8d4a 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,7 @@ import { Box, Heading } from '@chakra-ui/react'; import { RmgButtonGroup, RmgFields, RmgFieldsField } from '@railmapgen/rmg-components'; import { useTranslation } from 'react-i18next'; -import { FACILITIES, Facilities, RmgStyle, Services, TEMP } from '../../../constants/constants'; +import { FACILITIES, Facilities, PanelTypeShmetro, RmgStyle, Services, TEMP } from '../../../constants/constants'; import { useRootDispatch, useRootSelector } from '../../../redux'; import { updateStationCharacterSpacing, @@ -20,7 +20,7 @@ export default function MoreSection() { const dispatch = useRootDispatch(); const selectedStation = useRootSelector(state => state.app.selectedStation); - const { style, loop } = useRootSelector(state => state.param); + const { style, loop, info_panel_type } = useRootSelector(state => state.param); const { services, facility, loop_pivot, one_line, int_padding, character_spacing, underConstruction } = useRootSelector(state => state.param.stn_list[selectedStation]); @@ -79,7 +79,7 @@ export default function MoreSection() { label: t('StationSidePanel.more.oneLine'), isChecked: one_line, onChange: checked => dispatch(updateStationOneLine(selectedStation, checked)), - hidden: ![RmgStyle.SHMetro].includes(style), + hidden: !(style === RmgStyle.SHMetro && info_panel_type !== PanelTypeShmetro.sh2024), minW: 'full', oneLine: true, }, diff --git a/src/constants/constants.ts b/src/constants/constants.ts index e038d7027..6ae4273ff 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -192,6 +192,7 @@ export enum PanelTypeGZMTR { export enum PanelTypeShmetro { sh = 'sh', sh2020 = 'sh2020', + sh2024 = 'sh2024', } /** diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 9a71177a7..60ea30da3 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -86,7 +86,8 @@ "gz1421": "2017 style", "gz7w": "2022 style", "sh": "Default", - "sh2020": "2020 (Beta)", + "sh2020": "2020", + "sh2024": "2024 (Beta)", "nameDisplay": "Display station names", "alternatively": "Alternatively", "onOneSide": "Same side", diff --git a/src/i18n/translations/ja.json b/src/i18n/translations/ja.json index 663e1c9bc..12e3d894a 100644 --- a/src/i18n/translations/ja.json +++ b/src/i18n/translations/ja.json @@ -84,7 +84,8 @@ "gz1421": "2017風格", "gz7w": "2022風格", "sh": "デフォルト", - "sh2020": "2020風格(プレビュー版)", + "sh2020": "2020風格", + "sh2024": "2024風格(プレビュー版)", "nameDisplay": "駅名表示", "alternatively": "交互表示", "onOneSide": "片側表示", diff --git a/src/i18n/translations/ko.json b/src/i18n/translations/ko.json index 6c9ec4086..6dc6181a5 100644 --- a/src/i18n/translations/ko.json +++ b/src/i18n/translations/ko.json @@ -84,6 +84,7 @@ "gz7w": "2022년 모양", "sh": "묵인", "sh2020": "2020모양(사전 검토)", + "sh2024": "2024모양", "nameDisplay": "역 명칭 보이기", "alternatively": "교차 표시", "onOneSide": "같은 쪽", diff --git a/src/i18n/translations/zh-Hans.json b/src/i18n/translations/zh-Hans.json index 171e863d5..37d573ecb 100644 --- a/src/i18n/translations/zh-Hans.json +++ b/src/i18n/translations/zh-Hans.json @@ -84,7 +84,8 @@ "gz1421": "2017样式", "gz7w": "2022样式", "sh": "默认", - "sh2020": "2020样式(预览版)", + "sh2020": "2020样式", + "sh2024": "2024样式(预览版)", "nameDisplay": "显示车站名称", "alternatively": "交错显示", "onOneSide": "同一侧", diff --git a/src/i18n/translations/zh-Hant.json b/src/i18n/translations/zh-Hant.json index 6bd308d59..d42fbf7b3 100644 --- a/src/i18n/translations/zh-Hant.json +++ b/src/i18n/translations/zh-Hant.json @@ -79,7 +79,8 @@ "gz1421": "2017樣式", "gz7w": "2022樣式", "sh": "預設", - "sh2020": "2020樣式(預覽版)", + "sh2020": "2020樣式", + "sh2024": "2024樣式(預覽版)", "nameDisplay": "顯示車站名稱", "alternatively": "交錯顯示", "onOneSide": "同一側", diff --git a/src/svgs/shmetro/indoor/indoor-shmetro.tsx b/src/svgs/shmetro/indoor/indoor-shmetro.tsx index f9c453120..8a8f88fbc 100644 --- a/src/svgs/shmetro/indoor/indoor-shmetro.tsx +++ b/src/svgs/shmetro/indoor/indoor-shmetro.tsx @@ -2,7 +2,7 @@ import { memo, useMemo } from 'react'; import { adjacencyList, criticalPathMethod, getStnState, getXShareMTR } from '../../methods/share'; import StationSHMetro from './station-shmetro'; import { StationsSHMetro } from '../../methods/mtr'; -import { CanvasType, Services, StationDict } from '../../../constants/constants'; +import { CanvasType, PanelTypeShmetro, Services, StationDict } from '../../../constants/constants'; import { useRootSelector } from '../../../redux'; import LoopSHMetro from '../loop/loop-shmetro'; import SvgWrapper from '../../svg-wrapper'; @@ -11,7 +11,13 @@ const CANVAS_TYPE = CanvasType.Indoor; export default function IndoorWrapperSHMetro() { const { canvasScale } = useRootSelector(state => state.app); - const { svgWidth: svgWidths, svg_height: svgHeight, theme, loop } = useRootSelector(store => store.param); + const { + svgWidth: svgWidths, + svg_height: svgHeight, + theme, + loop, + info_panel_type, + } = useRootSelector(store => store.param); const svgWidth = svgWidths[CANVAS_TYPE]; @@ -25,12 +31,12 @@ export default function IndoorWrapperSHMetro() { > {loop ? : } - + {info_panel_type !== PanelTypeShmetro.sh2024 ? : } ); } -export const DefsSHMetro = memo(function DefsSHMetro() { +const DefsSHMetro = memo(function DefsSHMetro() { return ( @@ -61,6 +67,32 @@ export const DefsSHMetro = memo(function DefsSHMetro() { + + + + + + + + + + + + + + + + + + + + + ); }); @@ -89,7 +121,7 @@ const IndoorSHMetro = () => { const realCP = criticalPathMethod(criticalPath.nodes[1], criticalPath.nodes.slice(-2)[0], adjMat); const xShares = useMemo(() => { - console.log('computing x shares'); + console.debug('computing x shares'); return Object.keys(param.stn_list).reduce( (acc, cur) => ({ ...acc, [cur]: getXShareMTR(cur, adjMat, branches) }), {} as { [stnId: string]: number } @@ -142,10 +174,16 @@ const IndoorSHMetro = () => { 0 ); + const lineBadgeDX = Math.min( + ...Object.entries(xs) + .filter(([k]) => !['linestart', 'lineend'].includes(k)) + .map(([, v]) => v) + ); return ( + {param.info_panel_type === PanelTypeShmetro.sh2024 && } ); }; @@ -286,6 +324,51 @@ const InfoElements = memo(() => { InfoElements.displayName = 'InfoElements'; +const InfoElements2024 = () => { + const { + svg_height, + svgWidth: { indoor: svg_width }, + } = useRootSelector(store => store.param); + + return ( + + + 友情提示:请留意您需要换乘线路的首末班时间,以免耽误您的出行,末班车进站前三分钟停售该末班车车票。 + + + Please pay attention to the interchange schedule if you want to transfer to other lines. Stop selling + tickets 3 minutes before the last train services. + + + ); +}; + +const LineBadge = (props: { dx: number }) => { + const { dx } = props; + const { line_name, theme } = useRootSelector(store => store.param); + const num = line_name[0].match(/^(\d+)号线$/)?.[1] ?? ''; + const width = num.length > 1 ? 26.4 : 21.6; + const textDX = num.length > 1 ? 0 : 4; + const letterSpacing = num.length > 1 ? -1.5 : 0; + const padding = 15; + return ( + + + + {num} + + + ); +}; + /* Some unused functions to split branches from the main line. * Note the branches here has a slightly different meaning. * It refers to all the line sections that have a parallel line diff --git a/src/svgs/shmetro/indoor/station-shmetro.tsx b/src/svgs/shmetro/indoor/station-shmetro.tsx index 0d1f9eb6b..5b6bca2dc 100644 --- a/src/svgs/shmetro/indoor/station-shmetro.tsx +++ b/src/svgs/shmetro/indoor/station-shmetro.tsx @@ -1,8 +1,14 @@ import { ColourHex } from '@railmapgen/rmg-palette-resources'; +import { Translation } from '@railmapgen/rmg-translate'; import { Fragment, Ref, SVGProps, forwardRef, useEffect, useMemo, useRef, useState } from 'react'; -import { ExtendedInterchangeInfo, InterchangeGroup, Services } from '../../../constants/constants'; +import { + ExtendedInterchangeInfo, + InterchangeGroup, + PanelTypeGZMTR, + PanelTypeShmetro, + Services, +} from '../../../constants/constants'; import { useRootSelector } from '../../../redux'; -import { Translation } from '@railmapgen/rmg-translate'; /** * Which direction to display station name. Currently shmetro only. @@ -18,15 +24,53 @@ interface Props { export const StationSHMetro = (props: Props) => { const { stnId, nameDirection, services, color } = props; - const stnInfo = useRootSelector(store => store.param.stn_list[stnId]); + const { stn_list, info_panel_type } = useRootSelector(store => store.param); + const stnInfo = stn_list[stnId]; - const transfer = [...(stnInfo.transfer.groups[0]?.lines || []), ...(stnInfo.transfer.groups[1]?.lines || [])]; let stationIconStyle: string; - if (stnInfo.services.length === 3) stationIconStyle = 'direct_indoor_sh'; - else if (stnInfo.services.length === 2) stationIconStyle = 'express_indoor_sh'; - else if (stnInfo.transfer.groups[1]?.lines?.length ?? 0 > 0) stationIconStyle = 'osi_indoor_sh'; - else if (transfer.length > 0) stationIconStyle = 'int2_indoor_sh'; - else stationIconStyle = 'stn_indoor_sh'; + const stationIconColor: { [pos: string]: string } = {}; + if (info_panel_type === PanelTypeShmetro.sh2024) { + const int_length = stnInfo.transfer.groups.at(0)?.lines?.length ?? 0; + const osi_osysi_length = [ + ...(stnInfo.transfer.groups.at(1)?.lines || []), + ...(stnInfo.transfer.groups.at(2)?.lines || []), + ].length; + + stationIconColor.stroke = 'var(--rmg-theme-colour)'; + if (stnInfo.services.length === 3) { + stationIconStyle = 'stn_sh_2020_direct'; + } else if (stnInfo.services.length === 2) { + stationIconStyle = 'stn_sh_2020_express'; + } else if (osi_osysi_length > 1) { + // 不管多少条站内换乘,只要有超过1个的出站换乘就是3个圆了 + stationIconStyle = 'stn_sh_2024_osysi3'; + } else if (int_length > 0 && osi_osysi_length === 0) { + // 仅换乘车站 + stationIconStyle = 'stn_sh_2024_int'; + } else if (int_length > 0 && osi_osysi_length > 0) { + // 站内换乘+出站换乘 + stationIconStyle = 'stn_sh_2024_int_osysi'; + } else if (int_length === 0 && osi_osysi_length === 1) { + // 仅2线出站换乘 + stationIconStyle = 'stn_sh_2024_osysi2'; + } else { + stationIconStyle = 'stn_sh_2024'; + delete stationIconColor.stroke; + stationIconColor.fill = 'var(--rmg-theme-colour)'; + } + } else { + const non_osysi_transfer = [ + ...(stnInfo.transfer.groups[0]?.lines || []), + ...(stnInfo.transfer.groups[1]?.lines || []), + ]; + if (stnInfo.services.length === 3) stationIconStyle = 'direct_indoor_sh'; + else if (stnInfo.services.length === 2) stationIconStyle = 'express_indoor_sh'; + else if (stnInfo.transfer.groups[1]?.lines?.length ?? 0 > 0) stationIconStyle = 'osi_indoor_sh'; + else if (non_osysi_transfer.length > 0) stationIconStyle = 'int2_indoor_sh'; + else stationIconStyle = 'stn_indoor_sh'; + stationIconColor.stroke = + non_osysi_transfer.length > 0 ? 'var(--rmg-black)' : (color ?? 'var(--rmg-theme-colour)'); + } const dr = nameDirection === 'left' || nameDirection === 'right' ? 90 : 0; return ( @@ -39,8 +83,8 @@ export const StationSHMetro = (props: Props) => { /> 0 ? 'var(--rmg-black)' : (color ?? 'var(--rmg-theme-colour)')} transform={`rotate(${dr})`} + {...stationIconColor} // different styles use either `fill` or `stroke` /> {/* This should be in IntBoxGroupProps, put here because the station icon will cover this */} {stnInfo.services.length > 1 && ( @@ -54,6 +98,25 @@ export const StationSHMetro = (props: Props) => { export default StationSHMetro; +const stationNameDY = ( + info_panel_type: PanelTypeShmetro | PanelTypeGZMTR, + nameDirection: NameDirection, + nameENLn: number +) => { + if (info_panel_type === PanelTypeShmetro.sh2024) { + if (nameDirection === 'upward') return 5; + if (nameDirection === 'downward') return -45 - 12 * (nameENLn - 1); + if (nameDirection === 'left' || nameDirection === 'right') return -10 * (nameENLn - 1); + } else { + if (nameDirection === 'upward') return -2; + if (nameDirection === 'downward') return -30 - 12 * (nameENLn - 1); + if (nameDirection === 'left' || nameDirection === 'right') return -10 * (nameENLn - 1); + } + return 0; +}; + +const STATION_NAME_2024_ZH_FONT_SIZE = 22; + interface StationNameGElementProps { name: Translation; groups: InterchangeGroup[]; @@ -63,6 +126,9 @@ interface StationNameGElementProps { const StationNameGElement = (props: StationNameGElementProps) => { const { name, groups, nameDirection, services } = props; + const { en: enName = '' } = name; + const nameENLn = enName.split('\\').length; + const { info_panel_type } = useRootSelector(store => store.param); const dy = { upward: 60, downward: -30, left: 0, right: 0 }[nameDirection]; const osysi_dx = // only compute when there is an out-of-system transfer @@ -106,36 +172,62 @@ const StationNameGElement = (props: StationNameGElementProps) => { : 0; const nameRef = useRef(null); const MIN_NAME_LINE_LENGTH = 60; - const [nameWidth, setNameWidth] = useState(MIN_NAME_LINE_LENGTH); + const [nameSize, setNameSize] = useState({ width: MIN_NAME_LINE_LENGTH, height: 0, x: 0, y: 0 }); useEffect(() => { - if (nameRef?.current) setNameWidth(Math.max(MIN_NAME_LINE_LENGTH, nameRef.current.getBBox().width)); + if (nameRef?.current) setNameSize(nameRef.current.getBBox()); }, [name.zh, name.en]); + + const panel_type = info_panel_type as PanelTypeShmetro; + const verticalLineY = { + [PanelTypeShmetro.sh2024]: { upward: -15, downward: -30 }, + [PanelTypeShmetro.sh2020]: { upward: -23, downward: -10 }, + [PanelTypeShmetro.sh]: { upward: -23, downward: -10 }, + }; + const intBoxGroup2024DY = { + upward: stationNameDY(info_panel_type, nameDirection, nameENLn) + nameSize.height + INT_BOX_SIZE.height / 2, + downward: + stationNameDY(info_panel_type, nameDirection, nameENLn) - + STATION_NAME_2024_ZH_FONT_SIZE / 2 - + INT_BOX_SIZE.height / 2, + left: 7, + right: 7, + }; + return ( {nameDirection === 'upward' || nameDirection === 'downward' ? ( <> + {info_panel_type !== PanelTypeShmetro.sh2024 && ( + + )} - ) : ( <> - + {info_panel_type !== PanelTypeShmetro.sh2024 && ( + + )} { )} - {[...(groups[0].lines || []), ...(groups[1]?.lines || [])].length && ( - + {info_panel_type !== PanelTypeShmetro.sh2024 ? ( + [...(groups[0].lines || []), ...(groups[1]?.lines || [])].length && ( + + ) + ) : ( + )} - {groups[2]?.lines?.length && ( + {info_panel_type !== PanelTypeShmetro.sh2024 && groups[2]?.lines?.length && ( @@ -169,55 +265,56 @@ const StationName = forwardRef(function StationName( props: { stnName: Translation; nameDirection: NameDirection } & SVGProps, ref: Ref ) { + const { info_panel_type } = useRootSelector(store => store.param); const { stnName, nameDirection, ...others } = props; const { zh: zhName = '', en: enName = '' } = stnName; const name = zhName.split('\\'); const nameENLn = enName.split('\\').length; const dx = { upward: 0, downward: 0, left: -60, right: 60 }[nameDirection]; - const dy = { - upward: -2, - downward: -30 - 12 * (nameENLn - 1), - left: -10 * (nameENLn - 1), - right: -10 * (nameENLn - 1), - }[nameDirection]; + const dy = stationNameDY(info_panel_type, nameDirection, nameENLn); const anchor = { upward: 'middle', downward: 'middle', left: 'end', right: 'start' }[nameDirection]; + const fontSize = { + zh: info_panel_type === PanelTypeShmetro.sh2024 ? STATION_NAME_2024_ZH_FONT_SIZE : 16, + en: info_panel_type === PanelTypeShmetro.sh2024 ? 11 : 9.6, + }; + return ( - {useMemo( - () => ( - <> - {name.map((txt, i, array) => ( - - {txt} - - ))} - - {enName.split('\\')?.map((txt, i) => ( - 1 ? name.length * 7.5 : 0) : 0) - } - > - {txt} - - ))} - - - ), - [zhName, enName] - )} + {name.map((txt, i, array) => ( + + {txt} + + ))} + + {enName.split('\\')?.map((txt, i) => ( + 1 ? name.length * 7.5 : 0) : 0)} + > + {txt} + + ))} + ); }); +const INT_BOX_SIZE = { + width: { + singleDigit: 27, + doubleDigit: 33, + }, + height: 30, + padding: 2, +}; + interface IntBoxGroupProps { intInfos: ExtendedInterchangeInfo[][]; arrowDirection: NameDirection; @@ -349,6 +446,180 @@ const IntBoxGroup = (props: IntBoxGroupProps & SVGProps) => { ); }; +const IntBoxGroup2024 = forwardRef(function IntBoxGroup2024( + props: { groups: InterchangeGroup[]; dy: number }, + ref: Ref +) { + const { groups, dy } = props; + const directionPolarity = 1; + + const transfer = [groups.at(0)?.lines ?? [], groups.at(1)?.lines ?? [], groups.at(2)?.lines ?? []]; + + const [outOfSystemLine, setOutOfSystemLine] = useState(0); // also for start point of 出站换乘 + const [intBoxesDX, setIntBoxesDX] = useState<{ [k in string]: number }>({}); + const textLineRefs = useRef<{ [k in string]: SVGGElement }>({}); + const [intBoxGroupWidth, setIntBoxGroupWidth] = useState(0); + useEffect(() => { + // update the width of each text line + const textLineWidth = Object.fromEntries( + transfer + .flat() + .filter(info => !info.name[0].match(/^(\d+)号线$/)) + .map(info => { + const key = info.name[0]; + return [key, textLineRefs.current[key]?.getBBox().width ?? 0]; + }) + ); + + const getBBoxWidth = (info: ExtendedInterchangeInfo) => { + const key = info.name[0]; + const lineNumber = key.match(/^(\d+)号线$/); + const boxWidth = lineNumber + ? Number(lineNumber[1]) >= 10 + ? INT_BOX_SIZE.width.doubleDigit + : INT_BOX_SIZE.width.singleDigit + : textLineWidth[info.name[0]]; + intBoxDX[key] = dx * directionPolarity; + return boxWidth + INT_BOX_SIZE.padding; + }; + + let dx = 0; // update in every box + const intBoxDX: { [k in string]: number } = {}; + transfer[0].forEach(info => { + dx += getBBoxWidth(info); + }); + let outOfStationLine = 0; + if (transfer[1].length) { + // there will be a line and a text element for 出站换乘 + // each will take 22px + const elementWidth = INT_BOX_SIZE.height; + if (transfer[0].length) { + outOfStationLine = dx + INT_BOX_SIZE.padding; + dx += elementWidth + 2 * INT_BOX_SIZE.padding; + } else { + dx += INT_BOX_SIZE.padding; + // hide this line if there is no previous transfer + outOfStationLine = 0; + dx += elementWidth; + } + transfer[1].forEach(info => { + dx += getBBoxWidth(info); + }); + } + transfer[2].forEach(info => { + dx += getBBoxWidth(info); + }); + setIntBoxesDX(intBoxDX); + setOutOfSystemLine(outOfStationLine); + setIntBoxGroupWidth(dx); + }, [JSON.stringify(transfer)]); + + // only in case of one non out-of-station text line transfer, the text will be centered + const nonOutOfStationTransfer = [groups.at(0)?.lines ?? [], groups.at(2)?.lines ?? []]; + const onlyOneNonOutOfStationTextInt = + transfer.flat().length === 1 && + nonOutOfStationTransfer.flat().length === 1 && + !nonOutOfStationTransfer.flat()[0].name[0].match(/^(\d+)号线$/); + + const makeBoxElement = (info: ExtendedInterchangeInfo) => { + const key = info.name[0]; + const isLineNumber = Boolean(key.match(/^(\d+)号线$/)); + return ( + { + if (el && !isLineNumber) textLineRefs.current[key] = el; + }} + transform={`translate(${intBoxesDX[key] ?? 0},${-INT_BOX_SIZE.height / 2})`} + > + {isLineNumber ? ( + + ) : ( + + )} + + ); + }; + + const dx = onlyOneNonOutOfStationTextInt ? 0 : -intBoxGroupWidth / 2; + return ( + + {transfer[0].map(makeBoxElement)} + {transfer[1].length && ( + <> + {transfer[0].length > 0 && ( + + )} + + 出站 + 换乘 + + {transfer[1].map(makeBoxElement)} + + )} + {transfer[2].map(makeBoxElement)} + + ); +}); + +const IntBoxNumber2024 = (props: { info: ExtendedInterchangeInfo }) => { + const { + info: { name, theme }, + } = props; + const num = name[0].match(/^(\d+)号线$/)?.[1] ?? ''; + const width = num.length > 1 ? INT_BOX_SIZE.width.doubleDigit : INT_BOX_SIZE.width.singleDigit; + const letterSpacing = num.length > 1 ? -3 : 0; + return ( + + + + {num} + + + ); +}; + +const IntBoxText2024 = (props: { info: ExtendedInterchangeInfo; onlyOne: boolean }) => { + const { + info: { name }, + onlyOne, + } = props; + return ( + + + {name[0]} + + + {name[1]} + + + ); +}; + const OSysIText = (props: { osysiInfos: ExtendedInterchangeInfo[]; nameDirection: NameDirection }) => { const anchor = { upward: 'middle', downward: 'middle', left: 'start', right: 'end' }[props.nameDirection]; return useMemo( diff --git a/src/svgs/shmetro/main-shmetro.tsx b/src/svgs/shmetro/main-shmetro.tsx index d4b88b782..e72996036 100644 --- a/src/svgs/shmetro/main-shmetro.tsx +++ b/src/svgs/shmetro/main-shmetro.tsx @@ -1,7 +1,7 @@ import { adjacencyList, criticalPathMethod, drawLine, getStnState, getXShareMTR } from '../methods/share'; import StationSHMetro from './station-shmetro'; import ColineSHMetro from './coline-shmetro'; -import { AtLeastOneOfPartial, Services, StationDict } from '../../constants/constants'; +import { AtLeastOneOfPartial, PanelTypeShmetro, Services, StationDict } from '../../constants/constants'; import { useRootSelector } from '../../redux'; import { useMemo } from 'react'; @@ -425,64 +425,60 @@ const ServicesElements = (props: { servicesLevel: Services[]; lineXs: number[] } const dx_hint = props.servicesLevel.length === 2 ? 350 : 500; - return useMemo( - () => ( - - {servicesLevel.map((service, i) => ( - - - {`${service}运行线`} + return ( + + {servicesLevel.map((service, i) => ( + + + {`${service}运行线`} + + ))} + + 图例: + {servicesLevel.map((serviceLevel, i) => ( + + + + {`${serviceLevel}停靠站`} ))} - - 图例: - {servicesLevel.map((serviceLevel, i) => ( - - - - {`${serviceLevel}停靠站`} - - ))} - - ), - [svg_height, direction, svgWidth, props.servicesLevel, props.lineXs] + ); }; export const DirectionElements = () => { - const { direction, svgWidth, coline } = useRootSelector(store => store.param); + const { direction, svgWidth, coline, info_panel_type } = useRootSelector(store => store.param); // arrow will be black stroke with white fill in coline const isColine = !!Object.keys(coline).length; - return useMemo( - () => ( - - 列车前进方向 - - - ), - [direction, coline, svgWidth.railmap] + const arrowColor = + info_panel_type === PanelTypeShmetro.sh2024 + ? 'var(--rmg-black)' + : !isColine + ? 'var(--rmg-theme-colour)' + : 'var(--rmg-white)'; + + return ( + + 列车前进方向 + + ); }; diff --git a/src/svgs/shmetro/railmap-shmetro.tsx b/src/svgs/shmetro/railmap-shmetro.tsx index 9b3af68b0..4740d18a9 100644 --- a/src/svgs/shmetro/railmap-shmetro.tsx +++ b/src/svgs/shmetro/railmap-shmetro.tsx @@ -62,6 +62,30 @@ const DefsSHMetro = memo(function DefsSHMetro() { + + + + + + + + + + + + + + + + + + + @@ -146,6 +170,23 @@ const DefsSHMetro = memo(function DefsSHMetro() { {/* Outline filter of white pass color in Pujiang Line */} + + {/* 2024 Station border white outline */} + + + + + + + ); }); diff --git a/src/svgs/shmetro/runin-shmetro.tsx b/src/svgs/shmetro/runin-shmetro.tsx index 82b15e97c..6816687bb 100644 --- a/src/svgs/shmetro/runin-shmetro.tsx +++ b/src/svgs/shmetro/runin-shmetro.tsx @@ -1,12 +1,12 @@ /* eslint @typescript-eslint/no-non-null-assertion: 0 */ +import { Translation } from '@railmapgen/rmg-translate'; import { memo, SVGProps, useMemo } from 'react'; -import { CanvasType, StationDict } from '../../constants/constants'; +import { CanvasType, PanelTypeShmetro, StationDict } from '../../constants/constants'; import { useRootSelector } from '../../redux'; import { isColineBranch } from '../../redux/param/coline-action'; import { calculateColineStations } from '../methods/shmetro-coline'; import SvgWrapper from '../svg-wrapper'; import PujiangLineDefs from './pujiang-line-filter'; -import { Translation } from '@railmapgen/rmg-translate'; const LINE_WIDTH = 12; @@ -453,18 +453,15 @@ const CurrentText = () => { const param = useRootSelector(store => store.param); const { localisedName } = param.stn_list[param.current_stn_idx]; const { zh: zhName = '', en: enName = '' } = localisedName; - return useMemo( - () => ( - <> - - {zhName.replace('\\', '')} - - - {enName.replace('\\', '')} - - - ), - [zhName, enName] + return ( + <> + + {zhName.replace('\\', '')} + + + {enName.replace('\\', '')} + + ); }; @@ -505,8 +502,8 @@ const NextText = (props: { nextName: Translation } & SVGProps) => { }; const PrevStn = (props: { stnIds: string[] }) => { - const param = useRootSelector(store => store.param); - const prevNames = props.stnIds.map(stnId => param.stn_list[stnId].localisedName); + const { stn_list, direction, svgWidth, info_panel_type } = useRootSelector(store => store.param); + const prevNames = props.stnIds.map(stnId => stn_list[stnId].localisedName); const prevHintDy = (props.stnIds.length > 1 ? 15 : 125) + prevNames.map(name => name.zh?.split('\\')?.length ?? 1).reduce((acc, cur) => acc + cur, -prevNames.length) * @@ -522,28 +519,41 @@ const PrevStn = (props: { stnIds: string[] }) => { return ( {props.stnIds.length > 1 && ( )} - - 上一站 - - - Past Stop - + {info_panel_type === PanelTypeShmetro.sh2024 ? ( + <> + + 上一站 + + + Previous Station + + + ) : ( + <> + + 上一站 + + + Past Stop + + + )} ); }; const NextStn = (props: { stnIds: string[] }) => { - const param = useRootSelector(store => store.param); - const nextNames = props.stnIds.map(stnId => param.stn_list[stnId].localisedName); + const { stn_list, direction, svgWidth, info_panel_type } = useRootSelector(store => store.param); + const nextNames = props.stnIds.map(stnId => stn_list[stnId].localisedName); const nextHintDy = (props.stnIds.length > 1 ? 15 : 125) + nextNames.map(name => name.zh?.split('\\')?.length ?? 1).reduce((acc, cur) => acc + cur, -nextNames.length) * @@ -556,15 +566,17 @@ const NextStn = (props: { stnIds: string[] }) => { ? (nextZhName.split('\\').length - 1) * -50 + (nextEnName.split('\\').length - 1) * -30 : 0) + 70; + const nextTextEn = info_panel_type === PanelTypeShmetro.sh2024 ? 'Next Station' : 'Next Stop'; + return ( - + {props.stnIds.length > 1 && ( )} @@ -572,8 +584,8 @@ const NextStn = (props: { stnIds: string[] }) => { 下一站 - - Next Stop + + {nextTextEn} diff --git a/src/svgs/shmetro/station-shmetro.tsx b/src/svgs/shmetro/station-shmetro.tsx index 25b58c7c9..0eeb0322a 100644 --- a/src/svgs/shmetro/station-shmetro.tsx +++ b/src/svgs/shmetro/station-shmetro.tsx @@ -1,8 +1,17 @@ import { ColourHex } from '@railmapgen/rmg-palette-resources'; -import { ExtendedInterchangeInfo, Facilities, InterchangeGroup } from '../../constants/constants'; -import { useRootSelector } from '../../redux'; -import { forwardRef, memo, Ref, SVGProps, useEffect, useMemo, useRef, useState } from 'react'; import { Translation } from '@railmapgen/rmg-translate'; +import { forwardRef, memo, Ref, SVGProps, useEffect, useMemo, useRef, useState } from 'react'; +import { ExtendedInterchangeInfo, Facilities, InterchangeGroup, PanelTypeShmetro } from '../../constants/constants'; +import { useRootSelector } from '../../redux'; + +const INT_BOX_SIZE = { + width: { + singleDigit: 19.8, + doubleDigit: 24.2, + }, + height: 22, + padding: 2, +}; interface Props { stnId: string; @@ -28,7 +37,34 @@ const StationSHMetro = (props: Props) => { let stationIconStyle: string; const stationIconColor: { [pos: string]: string } = {}; - if (info_panel_type === 'sh2020') { + if (info_panel_type === PanelTypeShmetro.sh2024) { + const int_length = stnInfo.transfer.groups.at(0)?.lines?.length ?? 0; + const osi_osysi_length = [ + ...(stnInfo.transfer.groups.at(1)?.lines || []), + ...(stnInfo.transfer.groups.at(2)?.lines || []), + ].length; + + if (stnInfo.services.length === 3) stationIconStyle = 'stn_sh_2020_direct'; + else if (stnInfo.services.length === 2) stationIconStyle = 'stn_sh_2020_express'; + else if (osi_osysi_length > 1) { + // 不管多少条站内换乘,只要有超过1个的出站换乘就是3个圆了 + stationIconStyle = 'stn_sh_2024_osysi3'; + stationIconColor.stroke = stnState === -1 ? 'gray' : color ? color : 'var(--rmg-theme-colour)'; + } else if (int_length > 0 && osi_osysi_length === 0) { + // 仅换乘车站 + stationIconStyle = 'stn_sh_2024_int'; + stationIconColor.stroke = stnState === -1 ? 'gray' : color ? color : 'var(--rmg-theme-colour)'; + } else if (int_length > 0 && osi_osysi_length > 0) { + // 站内换乘+出站换乘 + stationIconStyle = 'stn_sh_2024_int_osysi'; + stationIconColor.stroke = stnState === -1 ? 'gray' : color ? color : 'var(--rmg-theme-colour)'; + } else if (int_length === 0 && osi_osysi_length === 1) { + // 仅2线出站换乘 + stationIconStyle = 'stn_sh_2024_osysi2'; + stationIconColor.stroke = stnState === -1 ? 'gray' : color ? color : 'var(--rmg-theme-colour)'; + } else stationIconStyle = 'stn_sh_2020'; + stationIconColor.fill = stnState === -1 ? 'gray' : color ? color : 'var(--rmg-theme-colour)'; + } else if (info_panel_type === PanelTypeShmetro.sh2020) { if (stnInfo.services.length === 3) stationIconStyle = 'stn_sh_2020_direct'; else if (stnInfo.services.length === 2) stationIconStyle = 'stn_sh_2020_express'; else stationIconStyle = 'stn_sh_2020'; @@ -44,8 +80,10 @@ const StationSHMetro = (props: Props) => { } const bank = bank_ ?? 0; - const dx = (direction === 'l' ? 6 : -6) + branchNameDX + bank * 30; - const dy = (info_panel_type === 'sh2020' ? -20 : -6) + Math.abs(bank) * (info_panel_type === 'sh2020' ? 25 : 11); + const dx2024 = info_panel_type === PanelTypeShmetro.sh2024 ? (direction === 'l' ? -5 : 5) : 0; + const dx = (direction === 'l' ? 6 : -6) + branchNameDX + bank * 30 + dx2024; + const is2020or2024 = info_panel_type === PanelTypeShmetro.sh2020 || info_panel_type === PanelTypeShmetro.sh2024; + const dy = (is2020or2024 ? -20 : -6) + Math.abs(bank) * (is2020or2024 ? 25 : 11); const dr = bank ? 0 : direction === 'l' ? -45 : 45; return ( <> @@ -53,10 +91,7 @@ const StationSHMetro = (props: Props) => { xlinkHref={`#${stationIconStyle}`} {...stationIconColor} // different styles use either `fill` or `stroke` // sh and sh2020 have different headings of int_sh, so -1 | 1 is multiplied - transform={ - `translate(${bank * (info_panel_type === 'sh2020' ? 5 : 0)},0)` + - `rotate(${bank * 90 * (info_panel_type === 'sh2020' ? 1 : -1)})` - } + transform={`translate(${bank * (is2020or2024 ? 5 : 0)},0)rotate(${bank * 90 * (is2020or2024 ? 1 : -1)})`} /> { intPadding={stnInfo.int_padding} /> - {stnState === 0 ? : undefined} + {stnState === 0 && } ); }; @@ -90,6 +125,7 @@ interface StationNameGElementProps { const StationNameGElement = (props: StationNameGElementProps) => { const { name, groups, stnState, direction, facility, bank, oneLine, intPadding } = props; + const { info_panel_type } = useRootSelector(store => store.param); // legacy ref to get the exact station name width const stnNameEl = useRef(null); @@ -103,9 +139,14 @@ const StationNameGElement = (props: StationNameGElementProps) => { // interchange will have a line under the name, and should be stretched when placed horizontal in loop const lineDx = bank ? -12 : 0; + // int group const intEl = useRef(null); const [intWidth, setIntWidth] = useState(0); - useEffect(() => setIntWidth(intEl.current?.getBBox().width ?? 0), [JSON.stringify(groups)]); + useEffect(() => { + // IntBoxGroup2024 will use double render to get the width of the text elements + // so we need to wait and get the int group width + setTimeout(() => setIntWidth(intEl.current?.getBBox().width ?? 0), 10); + }, [JSON.stringify(groups), directionPolarity]); const intDx = intPadding - intWidth; return ( @@ -115,15 +156,27 @@ const StationNameGElement = (props: StationNameGElementProps) => { - + {info_panel_type !== PanelTypeShmetro.sh2024 ? ( + <> + + + ) : ( + + )} )} @@ -136,23 +189,27 @@ const StationNameGElement = (props: StationNameGElementProps) => { - {/* this is out-of-station text displayed above the IntBoxGroup */} - {groups[1]?.lines?.length && ( - - - - )} - - {/* deal out-of-system here as it's dx is fixed and has nothing to do with IntBoxGroup */} - {groups[2]?.lines?.length && ( - - - + {info_panel_type !== PanelTypeShmetro.sh2024 && ( + <> + {/* this is out-of-station text displayed above the IntBoxGroup */} + {groups[1]?.lines?.length && ( + + + + )} + {/* deal out-of-system here as it's dx is fixed and has nothing to do with IntBoxGroup */} + {groups[2]?.lines?.length && ( + + + + )} + )} @@ -160,11 +217,17 @@ const StationNameGElement = (props: StationNameGElementProps) => { }; const StationName = forwardRef(function StationName( - props: { stnName: Translation; oneLine: boolean; directionPolarity: 1 | -1 } & SVGProps, + props: { + stnName: Translation; + oneLine: boolean; + directionPolarity: 1 | -1; + stnState: -1 | 0 | 1; + } & SVGProps, ref: Ref ) { - const { stnName, oneLine, directionPolarity, ...others } = props; + const { stnName, oneLine, directionPolarity, stnState, ...others } = props; const { zh: zhName = '', en: enName = '' } = stnName; + const { info_panel_type } = useRootSelector(store => store.param); const zhEl = useRef(null); const [enDx, setEnDx] = useState(0); @@ -175,56 +238,54 @@ const StationName = forwardRef(function StationName( const [ZH_HEIGHT, EN_HEIGHT] = [20, 8]; + const fontSize = { + zh: info_panel_type === PanelTypeShmetro.sh2024 && !stnState ? 18 : 16, + en: info_panel_type === PanelTypeShmetro.sh2024 && !stnState ? 8 : 9, + }; + return ( - {useMemo( - () => ( - <> - - {zhName.split('\\').map((txt, i, arr) => ( - - {txt} - - ))} - - - {enName.split('\\').map((txt, i, arr) => ( - - {txt} - - ))} - - - ), - [zhName, enName, oneLine, enDx, directionPolarity] - )} + + {zhName.split('\\').map((txt, i, arr) => ( + + {txt} + + ))} + + + {enName.split('\\').map((txt, i, arr) => ( + + {txt} + + ))} + ); }); const CurrentStationText = () => { - const { stn_list } = useRootSelector(store => store.param); + const { stn_list, info_panel_type } = useRootSelector(store => store.param); const servicesPresent = new Set( Object.values(stn_list) .map(stn => stn.services) .flat() ); - const dy = [-1, 35, 50, 75][servicesPresent.size]; + const dy = { + [PanelTypeShmetro.sh]: [-1, 35, 45, 70], + [PanelTypeShmetro.sh2020]: [-1, 20, 45, 70], + [PanelTypeShmetro.sh2024]: [-1, 20, 45, 70], + }[info_panel_type as PanelTypeShmetro][servicesPresent.size]; return ( - + 本站 @@ -288,6 +349,126 @@ const IntBoxGroup = forwardRef(function IntBoxGroup( ); }); +const IntBoxGroup2024 = forwardRef(function IntBoxGroup2024( + props: { groups: InterchangeGroup[]; direction: 'l' | 'r'; stnState: -1 | 0 | 1; intPadding: number }, + ref: Ref +) { + const { groups, direction, stnState, intPadding } = props; + const directionPolarity = direction === 'l' ? 1 : -1; + + const transfer = [groups.at(0)?.lines ?? [], groups.at(1)?.lines ?? [], groups.at(2)?.lines ?? []]; + + const [outOfSystemLine, setOutOfSystemLine] = useState([0, 0]); // also for start point of 出站换乘 + const [intBoxesDX, setIntBoxesDX] = useState<{ [k in string]: number }>({}); + const textLineRefs = useRef<{ [k in string]: SVGGElement }>({}); + const [intBoxGroupWidth, setIntBoxGroupWidth] = useState(0); + useEffect(() => { + // update the width of each text line + const textLineWidth = Object.fromEntries( + transfer + .flat() + .filter(info => !info.name[0].match(/^(\d+)号线$/)) + .map(info => { + const key = info.name[0]; + return [key, textLineRefs.current[key]?.getBBox().width ?? 0]; + }) + ); + + const getBBoxWidth = (info: ExtendedInterchangeInfo) => { + const key = info.name[0]; + const lineNumber = key.match(/^(\d+)号线$/); + const boxWidth = lineNumber + ? Number(lineNumber[1]) >= 10 + ? INT_BOX_SIZE.width.doubleDigit + : INT_BOX_SIZE.width.singleDigit + : textLineWidth[info.name[0]]; + intBoxDX[key] = dx * directionPolarity + (lineNumber && direction === 'r' ? -boxWidth : 0); + return boxWidth + INT_BOX_SIZE.padding; + }; + + let dx = 0; // update in every box + const intBoxDX: { [k in string]: number } = {}; + transfer[0].forEach(info => { + dx += getBBoxWidth(info); + }); + let outOfSystemLine = [0, 0]; + if (transfer[1].length) { + // there will be a line and a text element for 出站换乘 + // each will take 22px + const elementWidth = INT_BOX_SIZE.height; + if (transfer[0].length) { + outOfSystemLine = [dx, dx + elementWidth]; + dx += elementWidth * 2; + } else { + dx += INT_BOX_SIZE.padding; + // hide this line if there is no previous transfer + outOfSystemLine = [dx, dx]; + dx += elementWidth; + } + transfer[1].forEach(info => { + dx += getBBoxWidth(info); + }); + } + transfer[2].forEach(info => { + dx += getBBoxWidth(info); + }); + setIntBoxesDX(intBoxDX); + setOutOfSystemLine(outOfSystemLine); + setIntBoxGroupWidth(dx); + }, [JSON.stringify(transfer), direction]); + + const makeBoxElement = (info: ExtendedInterchangeInfo) => { + const key = info.name[0]; + const isLineNumber = Boolean(key.match(/^(\d+)号线$/)); + return ( + { + if (el && !isLineNumber) textLineRefs.current[key] = el; + }} + transform={`translate(${intBoxesDX[key] ?? 0},${-INT_BOX_SIZE.height / 2})`} + > + {isLineNumber ? ( + + ) : ( + + )} + + ); + }; + + const intBoxDX = (intPadding - intBoxGroupWidth) * directionPolarity; + return ( + + {transfer[0].map(makeBoxElement)} + {transfer[1].length && ( + <> + {transfer[0].length > 0 && ( + + )} + + 出站 + 换乘 + + {transfer[1].map(makeBoxElement)} + + )} + {transfer[2].map(makeBoxElement)} + + ); +}); + const IntBoxMaglev = memo( function IntBoxMaglev(props: { info: ExtendedInterchangeInfo }) { return ( @@ -314,6 +495,29 @@ const IntBoxNumber = memo( (prevProps, nextProps) => JSON.stringify(prevProps.info) === JSON.stringify(nextProps.info) ); +const IntBoxNumber2024 = (props: { info: ExtendedInterchangeInfo }) => { + const { + info: { name, theme }, + } = props; + const num = name[0].match(/^(\d+)号线$/)?.[1] ?? ''; + const width = num.length > 1 ? INT_BOX_SIZE.width.doubleDigit : INT_BOX_SIZE.width.singleDigit; + const letterSpacing = num.length > 1 ? -2.5 : 0; + return ( + + + + {num} + + + ); +}; + const IntBoxLetter = memo( function IntBoxLetter(props: { info: ExtendedInterchangeInfo }) { // box width: 16 * number of characters + 12 @@ -335,6 +539,29 @@ const IntBoxLetter = memo( (prevProps, nextProps) => JSON.stringify(prevProps.info) === JSON.stringify(nextProps.info) ); +const IntBoxText2024 = (props: { info: ExtendedInterchangeInfo; state: -1 | 0 | 1; direction: 'l' | 'r' }) => { + const { + info: { name }, + state, + direction, + } = props; + return ( + + + {name[0]} + + + {name[1]} + + + ); +}; + const OSIText = (props: { osiInfos: ExtendedInterchangeInfo[] }) => { // get the all names from the out of station interchanges const lineNames = props.osiInfos.map(info => info.name[0]).join(','); @@ -361,17 +588,14 @@ const OSysIText = (props: { osysiInfos: ExtendedInterchangeInfo[]; direction: 'l const lineNames = props.osysiInfos.map(info => info.name[0]).join(','); const lineNamesEn = props.osysiInfos.map(info => info.name[1]).join(', '); - return useMemo( - () => ( - - - 转乘{lineNames} - - - To {lineNamesEn} - - - ), - [JSON.stringify(props.osysiInfos), props.direction] + return ( + + + 转乘{lineNames} + + + To {lineNamesEn} + + ); };