diff --git a/src/svgs/shmetro/indoor/station-shmetro.tsx b/src/svgs/shmetro/indoor/station-shmetro.tsx index 5b6bca2dc..66ddd2b70 100644 --- a/src/svgs/shmetro/indoor/station-shmetro.tsx +++ b/src/svgs/shmetro/indoor/station-shmetro.tsx @@ -15,15 +15,23 @@ import { useRootSelector } from '../../../redux'; */ export type NameDirection = 'upward' | 'downward' | 'left' | 'right'; +const oppositeNameDirection: Record = { + upward: 'downward', + downward: 'upward', + left: 'right', + right: 'left', +}; + interface Props { stnId: string; nameDirection: NameDirection; services: Services[]; color?: ColourHex; // Control the station color if coline is in effect. + isSh2024IndoorLoop?: boolean; } export const StationSHMetro = (props: Props) => { - const { stnId, nameDirection, services, color } = props; + const { stnId, nameDirection, services, color, isSh2024IndoorLoop = false } = props; const { stn_list, info_panel_type } = useRootSelector(store => store.param); const stnInfo = stn_list[stnId]; @@ -36,11 +44,11 @@ export const StationSHMetro = (props: Props) => { ...(stnInfo.transfer.groups.at(2)?.lines || []), ].length; - stationIconColor.stroke = 'var(--rmg-theme-colour)'; + stationIconColor.stroke = color ?? 'var(--rmg-theme-colour)'; if (stnInfo.services.length === 3) { - stationIconStyle = 'stn_sh_2020_direct'; + stationIconStyle = 'direct_indoor_sh'; } else if (stnInfo.services.length === 2) { - stationIconStyle = 'stn_sh_2020_express'; + stationIconStyle = 'express_indoor_sh'; } else if (osi_osysi_length > 1) { // 不管多少条站内换乘,只要有超过1个的出站换乘就是3个圆了 stationIconStyle = 'stn_sh_2024_osysi3'; @@ -56,7 +64,7 @@ export const StationSHMetro = (props: Props) => { } else { stationIconStyle = 'stn_sh_2024'; delete stationIconColor.stroke; - stationIconColor.fill = 'var(--rmg-theme-colour)'; + stationIconColor.fill = color ?? 'var(--rmg-theme-colour)'; } } else { const non_osysi_transfer = [ @@ -72,7 +80,14 @@ export const StationSHMetro = (props: Props) => { non_osysi_transfer.length > 0 ? 'var(--rmg-black)' : (color ?? 'var(--rmg-theme-colour)'); } - const dr = nameDirection === 'left' || nameDirection === 'right' ? 90 : 0; + const dr = + nameDirection === 'left' || nameDirection === 'right' + ? info_panel_type === PanelTypeShmetro.sh2024 + ? nameDirection === 'left' + ? -90 + : 90 + : 90 + : 0; return ( <> { groups={stnInfo.transfer.groups} nameDirection={nameDirection} services={services} + isSh2024IndoorLoop={isSh2024IndoorLoop} /> { - const { name, groups, nameDirection, services } = props; + const { name, groups, nameDirection, services, isSh2024IndoorLoop = false } = props; const { en: enName = '' } = name; const nameENLn = enName.split('\\').length; const { info_panel_type } = useRootSelector(store => store.param); @@ -178,6 +195,7 @@ const StationNameGElement = (props: StationNameGElementProps) => { }, [name.zh, name.en]); const panel_type = info_panel_type as PanelTypeShmetro; + const isSh2024 = info_panel_type === PanelTypeShmetro.sh2024; const verticalLineY = { [PanelTypeShmetro.sh2024]: { upward: -15, downward: -30 }, [PanelTypeShmetro.sh2020]: { upward: -23, downward: -10 }, @@ -189,45 +207,84 @@ const StationNameGElement = (props: StationNameGElementProps) => { stationNameDY(info_panel_type, nameDirection, nameENLn) - STATION_NAME_2024_ZH_FONT_SIZE / 2 - INT_BOX_SIZE.height / 2, - left: 7, - right: 7, + left: 12, + right: 12, }; + const dyByDir: Record = { upward: 60, downward: -30, left: 0, right: 0 }; + const effectiveIntBoxDir = isSh2024IndoorLoop ? oppositeNameDirection[nameDirection] : nameDirection; + const intBoxDYCompensation = isSh2024IndoorLoop ? dyByDir[effectiveIntBoxDir] - dy : 0; + // stn_sh_2024 icon edges: y=-18 (top) .. 6 (bottom). IntBoxGroup2024 dy = box bottom edge. + const intBoxGap = 5; + const effectiveIntBoxDY = (() => { + if (!isSh2024IndoorLoop) return intBoxGroup2024DY[nameDirection]; + if (effectiveIntBoxDir === 'upward' || effectiveIntBoxDir === 'downward') { + const undoParent = -dy; + const iconEdge = effectiveIntBoxDir === 'upward' ? 6 : -18; + return effectiveIntBoxDir === 'upward' + ? undoParent + iconEdge + intBoxGap + INT_BOX_SIZE.height + : undoParent + iconEdge - intBoxGap; + } + return intBoxGroup2024DY[effectiveIntBoxDir] + intBoxDYCompensation; + })(); + + if (isSh2024) { + return ( + + {nameDirection === 'upward' || nameDirection === 'downward' ? ( + + ) : ( + + )} + + {effectiveIntBoxDir === 'left' || effectiveIntBoxDir === 'right' ? ( + + + + ) : ( + + )} + + + + ); + } return ( {nameDirection === 'upward' || nameDirection === 'downward' ? ( <> - {info_panel_type !== PanelTypeShmetro.sh2024 && ( - - )} + ) : ( <> - {info_panel_type !== PanelTypeShmetro.sh2024 && ( - - )} + { )} - {info_panel_type !== PanelTypeShmetro.sh2024 ? ( - [...(groups[0].lines || []), ...(groups[1]?.lines || [])].length && ( - - ) - ) : ( - + {[...(groups[0].lines || []), ...(groups[1]?.lines || [])].length && ( + )} - {info_panel_type !== PanelTypeShmetro.sh2024 && groups[2]?.lines?.length && ( + {groups[2]?.lines?.length && ( @@ -447,13 +500,14 @@ const IntBoxGroup = (props: IntBoxGroupProps & SVGProps) => { }; const IntBoxGroup2024 = forwardRef(function IntBoxGroup2024( - props: { groups: InterchangeGroup[]; dy: number }, + props: { groups: InterchangeGroup[]; dy: number; align?: 'start' | 'center' | 'end' }, ref: Ref ) { - const { groups, dy } = props; + const { groups, dy, align = 'center' } = props; const directionPolarity = 1; const transfer = [groups.at(0)?.lines ?? [], groups.at(1)?.lines ?? [], groups.at(2)?.lines ?? []]; + const outOfStationTransfer = [...transfer[1], ...transfer[2]]; const [outOfSystemLine, setOutOfSystemLine] = useState(0); // also for start point of 出站换乘 const [intBoxesDX, setIntBoxesDX] = useState<{ [k in string]: number }>({}); @@ -489,7 +543,7 @@ const IntBoxGroup2024 = forwardRef(function IntBoxGroup2024( dx += getBBoxWidth(info); }); let outOfStationLine = 0; - if (transfer[1].length) { + if (outOfStationTransfer.length) { // there will be a line and a text element for 出站换乘 // each will take 22px const elementWidth = INT_BOX_SIZE.height; @@ -502,20 +556,17 @@ const IntBoxGroup2024 = forwardRef(function IntBoxGroup2024( outOfStationLine = 0; dx += elementWidth; } - transfer[1].forEach(info => { + outOfStationTransfer.forEach(info => { dx += getBBoxWidth(info); }); } - transfer[2].forEach(info => { - dx += getBBoxWidth(info); - }); setIntBoxesDX(intBoxDX); setOutOfSystemLine(outOfStationLine); setIntBoxGroupWidth(dx); - }, [JSON.stringify(transfer)]); + }, [JSON.stringify(transfer), JSON.stringify(outOfStationTransfer)]); // 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 nonOutOfStationTransfer = [groups.at(0)?.lines ?? []]; const onlyOneNonOutOfStationTextInt = transfer.flat().length === 1 && nonOutOfStationTransfer.flat().length === 1 && @@ -541,11 +592,16 @@ const IntBoxGroup2024 = forwardRef(function IntBoxGroup2024( ); }; - const dx = onlyOneNonOutOfStationTextInt ? 0 : -intBoxGroupWidth / 2; + const dx = (() => { + if (align === 'end') return -intBoxGroupWidth; + if (align === 'start') return 0; + if (onlyOneNonOutOfStationTextInt) return 0; + return -intBoxGroupWidth / 2; + })(); return ( {transfer[0].map(makeBoxElement)} - {transfer[1].length && ( + {outOfStationTransfer.length > 0 && ( <> {transfer[0].length > 0 && ( 出站 换乘 - {transfer[1].map(makeBoxElement)} + {outOfStationTransfer.map(makeBoxElement)} )} - {transfer[2].map(makeBoxElement)} ); }); diff --git a/src/svgs/shmetro/loop/loop-shmetro.tsx b/src/svgs/shmetro/loop/loop-shmetro.tsx index d567cd7ba..dffb92b62 100644 --- a/src/svgs/shmetro/loop/loop-shmetro.tsx +++ b/src/svgs/shmetro/loop/loop-shmetro.tsx @@ -142,11 +142,13 @@ const LoopSHMetro = (props: { bank_angle: boolean; canvas: CanvasType.RailMap | // FIXME: branches with only one station could not display properly const dy = loop_branches.length ? 0 : ((line_ys[1] - line_ys[0]) * bank) / 2; + // sh2024 with coline needs stations rendered AFTER the coline track for correct z-order + const sh2024WithColine = info_panel_type === 'sh2024' && Object.keys(coline).length > 0; return ( - {/* Order matters. The LoopColine should cover the station in RailMap. */} - {canvas === CanvasType.RailMap && ( + {/* For non-sh2024-coline: The LoopColine should cover the station in RailMap. */} + {canvas === CanvasType.RailMap && !sh2024WithColine && ( )} 0 ? -LINE_WIDTH - COLINE_GAP : 0})`}> @@ -167,6 +169,10 @@ const LoopSHMetro = (props: { bank_angle: boolean; canvas: CanvasType.RailMap | /> )} + {/* For sh2024-coline: render stations AFTER coline so icons/names appear on top of coline line */} + {canvas === CanvasType.RailMap && sh2024WithColine && ( + + )} {/* Order matters. The station should cover LoopColine's main path in Indoor. */} {canvas === CanvasType.Indoor && } @@ -245,7 +251,23 @@ const LoopStationGroup = (props: { }; }) => { const { canvas, loop_stns, xs, ys } = props; - const { current_stn_idx: current_stn_id, stn_list } = useRootSelector(store => store.param); + const { + current_stn_idx: current_stn_id, + stn_list, + info_panel_type, + coline, + } = useRootSelector(store => store.param); + + // In sh2024 indoor loop without coline, shift station icons inward by 6px + // so they appear to "penetrate" through the loop line. + const isSh2024IndoorLoop = + canvas === CanvasType.Indoor && info_panel_type === 'sh2024' && Object.keys(coline).length === 0; + const inwardOffset: Record = { + top: { dx: 0, dy: 6 }, + bottom: { dx: 0, dy: 6 }, + left: { dx: 6, dy: 0 }, + right: { dx: -6, dy: 0 }, + }; const railmap_bank: Record = { top: 0, @@ -259,13 +281,18 @@ const LoopStationGroup = (props: { top: undefined, bottom: undefined, }; - const indoor_name_direction = (side: keyof LoopStns, i: number) => - ({ - top: i % 2 === 0 ? 'upward' : 'downward', - bottom: i % 2 === 0 ? 'upward' : 'downward', - left: 'left', - right: 'right', - })[side] as NameDirection; + const indoor_name_direction = (side: keyof LoopStns, i: number): NameDirection => { + switch (side) { + case 'top': + return isSh2024IndoorLoop ? 'downward' : i % 2 === 0 ? 'upward' : 'downward'; + case 'bottom': + return isSh2024IndoorLoop ? 'upward' : i % 2 === 0 ? 'upward' : 'downward'; + case 'left': + return 'left'; + case 'right': + return 'right'; + } + }; return ( {canvas === CanvasType.RailMap && @@ -287,15 +314,22 @@ const LoopStationGroup = (props: { Object.entries(loop_stns).map(([side, stn_ids]) => stn_ids .filter(stn_id => stn_list[stn_id].services.length) - .map((stn_id, i) => ( - - - - )) + .map((stn_id, i) => { + const offset = isSh2024IndoorLoop ? inwardOffset[side as keyof LoopStns] : { dx: 0, dy: 0 }; + return ( + + + + ); + }) )} );