Skip to content
Open
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
87 changes: 85 additions & 2 deletions src/components/svgs/stations/bjsubway-basic.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RmgFields, RmgFieldsField } from '@railmapgen/rmg-components';
import { RmgButtonGroup, RmgFields, RmgFieldsField } from '@railmapgen/rmg-components';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AttrsProps, CanvasType, CategoriesType, CityCode } from '../../../constants/constants';
Expand Down Expand Up @@ -31,6 +31,8 @@ const BjsubwayBasicStation = (props: StationComponentProps) => {
open = defaultBjsubwayBasicStationAttributes.open,
construction = defaultBjsubwayBasicStationAttributes.construction,
scale = defaultBjsubwayBasicStationAttributes.scale,
minorOffsetX = defaultBjsubwayBasicStationAttributes.minorOffsetX,
minorOffsetY = defaultBjsubwayBasicStationAttributes.minorOffsetY,
} = attrs[StationType.BjsubwayBasic] ?? defaultBjsubwayBasicStationAttributes;

const onPointerDown = React.useCallback(
Expand Down Expand Up @@ -71,6 +73,17 @@ const BjsubwayBasicStation = (props: StationComponentProps) => {
const [textX, textY] = getTextOffset(nameOffsetX, nameOffsetY);
const textAnchor = nameOffsetX === 'left' ? 'end' : nameOffsetX === 'right' ? 'start' : 'middle';

const getMinorTextOffset = (offset: '-2' | '-1' | '0' | '1' | '2') => {
if (offset === '-2') return -8;
else if (offset === '-1') return -2.5;
else if (offset === '0') return 0;
else if (offset === '1') return 2.5;
else if (offset === '2') return 8;
else return 0;
};
const minorTextX = getMinorTextOffset(minorOffsetX);
const minorTextY = getMinorTextOffset(minorOffsetY);

return (
<g id={id} transform={`translate(${x}, ${y})`}>
<circle
Expand All @@ -85,7 +98,7 @@ const BjsubwayBasicStation = (props: StationComponentProps) => {
onPointerUp={onPointerUp}
style={{ cursor: 'move' }}
/>
<g transform={`translate(${textX}, ${textY})`} textAnchor={textAnchor}>
<g transform={`translate(${textX + minorTextX}, ${textY + minorTextY})`} textAnchor={textAnchor}>
<MultilineText
text={names[0].split('\n')}
fontSize={LINE_HEIGHT.zh}
Expand Down Expand Up @@ -131,6 +144,8 @@ export interface BjsubwayBasicStationAttributes extends StationAttributes {
open: boolean;
construction: boolean;
scale: number;
minorOffsetX: '-2' | '-1' | '0' | '1' | '2';
minorOffsetY: '-2' | '-1' | '0' | '1' | '2';
}

const defaultBjsubwayBasicStationAttributes: BjsubwayBasicStationAttributes = {
Expand All @@ -140,6 +155,8 @@ const defaultBjsubwayBasicStationAttributes: BjsubwayBasicStationAttributes = {
open: true,
construction: false,
scale: 1,
minorOffsetX: '0',
minorOffsetY: '0',
};

const BJSubwayBasicAttrsComponent = (props: AttrsProps<BjsubwayBasicStationAttributes>) => {
Expand Down Expand Up @@ -236,6 +253,72 @@ const BJSubwayBasicAttrsComponent = (props: AttrsProps<BjsubwayBasicStationAttri
},
minW: 'full',
},
{
type: 'custom',
label: t('panel.details.stations.bjsubwayBasic.minorOffset.labelX'),
component: (
<RmgButtonGroup
selections={[
{
label: t('panel.details.stations.bjsubwayBasic.minorOffset.-2'),
value: '-2',
},
{
label: t('panel.details.stations.bjsubwayBasic.minorOffset.-1'),
value: '-1',
},
{
label: t('panel.details.stations.bjsubwayBasic.minorOffset.0'),
value: '0',
},
{
label: t('panel.details.stations.bjsubwayBasic.minorOffset.1'),
value: '1',
},
{
label: t('panel.details.stations.bjsubwayBasic.minorOffset.2'),
value: '2',
},
]}
defaultValue={attrs.minorOffsetX ?? defaultBjsubwayBasicStationAttributes.minorOffsetX}
onChange={val => {
attrs.minorOffsetX = val as '-2' | '-1' | '0' | '1' | '2';
handleAttrsUpdate(id, attrs);
}}
multiSelect={false}
/>
),
minW: 'full',
},
{
type: 'custom',
label: t('panel.details.stations.bjsubwayBasic.minorOffset.labelY'),
component: (
<RmgButtonGroup
selections={[
{
label: t('panel.details.stations.bjsubwayBasic.minorOffset.-1'),
value: '-1',
},
{
label: t('panel.details.stations.bjsubwayBasic.minorOffset.0'),
value: '0',
},
{
label: t('panel.details.stations.bjsubwayBasic.minorOffset.1'),
value: '1',
},
]}
Comment on lines +298 to +311
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistency between X and Y offset options. The minorOffsetX button group offers 5 options (-2, -1, 0, 1, 2) while minorOffsetY only offers 3 options (-1, 0, 1). While this might be intentional based on design requirements, the TypeScript interface at lines 147-148 defines both as supporting the full range '-2' | '-1' | '0' | '1' | '2', which could lead to confusion or runtime issues if users expect parity between X and Y options. Consider either documenting why Y has fewer options or updating the type definition to reflect the actual available options.

Copilot uses AI. Check for mistakes.
defaultValue={attrs.minorOffsetY ?? defaultBjsubwayBasicStationAttributes.minorOffsetY}
onChange={val => {
attrs.minorOffsetY = val as '-2' | '-1' | '0' | '1' | '2';
handleAttrsUpdate(id, attrs);
}}
multiSelect={false}
/>
),
minW: 'full',
},
];

return <RmgFields fields={fields} />;
Expand Down
105 changes: 90 additions & 15 deletions src/components/svgs/stations/bjsubway-int.tsx

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,30 @@
"displayName": "Beijing Subway basic station",
"open": "Is opened",
"construction": "Is under construction",
"scale": "Names horizontal scale"
"scale": "Names horizontal scale",
"minorOffset": {
"labelX": "Minor names offset X",
"labelY": "Minor names offset Y",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"bjsubwayInt": {
"displayName": "Beijing Subway interchange station",
"outOfStation": "Out of station interchange",
"scale": "Names horizontal scale"
"scale": "Names horizontal scale",
"minorOffset": {
"labelX": "Minor names offset X",
"labelY": "Minor names offset Y",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"mtr": {
"displayName": "Hongkong MTR station",
Expand Down
22 changes: 20 additions & 2 deletions src/i18n/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,30 @@
"displayName": "北京地下鉄基本駅",
"open": "開業済み",
"construction": "工事中",
"scale": "駅名の横方向スケール"
"scale": "駅名の横方向スケール",
"minorOffset": {
"labelX": "駅名の水平方向のずれ",
"labelY": "駅名の垂直方向のずれ",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"bjsubwayInt": {
"displayName": "北京地下鉄乗り換え駅",
"outOfStation": "改札外乗り換え",
"scale": "駅名の横方向スケール"
"scale": "駅名の横方向スケール",
"minorOffset": {
"labelX": "駅名の水平方向のずれ",
"labelY": "駅名の垂直方向のずれ",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"mtr": {
"displayName": "香港MTR駅",
Expand Down
22 changes: 20 additions & 2 deletions src/i18n/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,30 @@
"displayName": "베이징 지하철 기본역",
"open": "개통여부",
"construction": "공사중",
"scale": "역명 가로 크기 조절"
"scale": "역명 가로 크기 조절",
"minorOffset": {
"labelX": "관측소 이름의 수평 변위",
"labelY": "관측소 이름의 수직 변위",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"bjsubwayInt": {
"displayName": "베이징 지하철 환승역",
"outOfStation": "역을 나가 환승",
"scale": "역명 가로 크기 조절"
"scale": "역명 가로 크기 조절",
"minorOffset": {
"labelX": "관측소 이름의 수평 변위",
"labelY": "관측소 이름의 수직 변위",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"mtr": {
"displayName": "홍콩 MTR 역"
Expand Down
22 changes: 20 additions & 2 deletions src/i18n/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,30 @@
"displayName": "北京地铁基本车站",
"open": "开通",
"construction": "施工",
"scale": "站名横向缩放"
"scale": "站名横向缩放",
"minorOffset": {
"labelX": "站名横向微小位移",
"labelY": "站名纵向微小位移",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"bjsubwayInt": {
"displayName": "北京地铁换乘车站",
"outOfStation": "出站换乘",
"scale": "站名横向缩放"
"scale": "站名横向缩放",
"minorOffset": {
"labelX": "站名横向微小位移",
"labelY": "站名纵向微小位移",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"mtr": {
"displayName": "香港MTR车站"
Expand Down
22 changes: 20 additions & 2 deletions src/i18n/translations/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,30 @@
"displayName": "北京地鐵基本車站",
"open": "開通",
"construction": "施工",
"scale": "站名橫向縮放"
"scale": "站名橫向縮放",
"minorOffset": {
"labelX": "站名橫向微小位移",
"labelY": "站名縱向微小位移",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"bjsubwayInt": {
"displayName": "北京地鐵換乘車站",
"outOfStation": "出站轉車",
"scale": "站名橫向縮放"
"scale": "站名橫向縮放",
"minorOffset": {
"labelX": "站名橫向微小位移",
"labelY": "站名縱向微小位移",
"-2": "-2x",
"-1": "-1x",
"0": "0x",
"1": "1x",
"2": "2x"
}
},
"mtr": {
"displayName": "香港MTR車站"
Expand Down
12 changes: 12 additions & 0 deletions src/util/save.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,4 +885,16 @@ describe('Unit tests for param upgrade function', () => {
'{"graph":{"options":{"type":"directed","multi":true,"allowSelfLoops":true},"attributes":{},"nodes":[{"key":"stn_basic","attributes":{"visible":true,"zIndex":0,"x":100,"y":100,"type":"hzmetro-basic","hzmetro-basic":{"names":["车站"],"nameOffsetX":"right","nameOffsetY":"top","color":["hangzhou","1","#dd4231","#fff"],"scale":1}}},{"key":"stn_int","attributes":{"visible":true,"zIndex":0,"x":200,"y":200,"type":"hzmetro-int","hzmetro-int":{"names":["换乘站"],"nameOffsetX":"left","nameOffsetY":"bottom","transfer":[[]],"scale":1,"mirror":false}}}],"edges":[]},"svgViewBoxZoom":100,"svgViewBoxMin":{"x":0,"y":0},"version":68}';
expect(newParam).toEqual(expectParam);
});

it('68 -> 69', () => {
// Bump save version to add minor offsets to bjsubway basic and interchange stations.
const oldParam =
'{"graph":{"options":{"type":"directed","multi":true,"allowSelfLoops":true},"attributes":{},"nodes":[{"key":"stn_basic","attributes":{"visible":true,"zIndex":0,"x":100,"y":100,"type":"bjsubway-basic","bjsubway-basic":{"names":["车站"],"nameOffsetX":"right","nameOffsetY":"top","open":true,"construction":false,"scale":1}}},{"key":"stn_int","attributes":{"visible":true,"zIndex":0,"x":200,"y":200,"type":"bjsubway-int","bjsubway-int":{"names":["换乘站"],"nameOffsetX":"left","nameOffsetY":"bottom","outOfStation":false,"scale":1}}}],"edges":[]},"svgViewBoxZoom":100,"svgViewBoxMin":{"x":0,"y":0},"version":68}';
const newParam = UPGRADE_COLLECTION[68](oldParam);
const graph = new MultiDirectedGraph() as MultiDirectedGraph<NodeAttributes, EdgeAttributes, GraphAttributes>;
expect(() => graph.import(JSON.parse(newParam))).not.toThrow();
const expectParam =
'{"graph":{"options":{"type":"directed","multi":true,"allowSelfLoops":true},"attributes":{},"nodes":[{"key":"stn_basic","attributes":{"visible":true,"zIndex":0,"x":100,"y":100,"type":"bjsubway-basic","bjsubway-basic":{"names":["车站"],"nameOffsetX":"right","nameOffsetY":"top","open":true,"construction":false,"scale":1,"minorOffsetX":"0","minorOffsetY":"0"}}},{"key":"stn_int","attributes":{"visible":true,"zIndex":0,"x":200,"y":200,"type":"bjsubway-int","bjsubway-int":{"names":["换乘站"],"nameOffsetX":"left","nameOffsetY":"bottom","outOfStation":false,"scale":1,"minorOffsetX":"0","minorOffsetY":"0"}}}],"edges":[]},"svgViewBoxZoom":100,"svgViewBoxMin":{"x":0,"y":0},"version":69}';
expect(newParam).toEqual(expectParam);
});
});
27 changes: 26 additions & 1 deletion src/util/save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface RMPSave {
images?: { id: string; base64: string }[];
}

export const CURRENT_VERSION = 68;
export const CURRENT_VERSION = 69;

/**
* Parse the version from a save string without fully validating the save.
Expand Down Expand Up @@ -906,4 +906,29 @@ export const UPGRADE_COLLECTION: { [version: number]: (param: string) => string
});
return JSON.stringify({ ...p, version: 68, graph: graph.export() });
},
68: param => {
// Bump save version to add minorOffset to beijing subway basic and interchange stations.
const p = JSON.parse(param);
const graph = new MultiDirectedGraph() as MultiDirectedGraph<NodeAttributes, EdgeAttributes, GraphAttributes>;
graph.import(p?.graph);
graph
.filterNodes(
(node, attr) =>
node.startsWith('stn') &&
(attr.type === StationType.BjsubwayBasic || attr.type === StationType.BjsubwayInt)
)
.forEach(node => {
const type = graph.getNodeAttribute(node, 'type');
const attr = graph.getNodeAttribute(node, type) as any;
if (typeof attr.minorOffsetX !== 'number') {
attr.minorOffsetX = '0';
graph.mergeNodeAttributes(node, { [type]: attr });
}
if (typeof attr.minorOffsetY !== 'number') {
Comment on lines +923 to +927
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type mismatch in the migration logic. The code sets minorOffsetX to the string '0' when it should be the number 0, but then checks if the type is NOT 'number'. This means the value will always be set to '0' (string) even when it should remain undefined or be converted to the correct type.

According to the BjsubwayBasicStationAttributes and BjsubwayIntStationAttributes interfaces, minorOffsetX should be of type '-2' | '-1' | '0' | '1' | '2' (a string union type), not a number. The condition should check typeof attr.minorOffsetX !== 'string' or check if the attribute is undefined.

Suggested change
if (typeof attr.minorOffsetX !== 'number') {
attr.minorOffsetX = '0';
graph.mergeNodeAttributes(node, { [type]: attr });
}
if (typeof attr.minorOffsetY !== 'number') {
if (typeof attr.minorOffsetX !== 'string') {
attr.minorOffsetX = '0';
graph.mergeNodeAttributes(node, { [type]: attr });
}
if (typeof attr.minorOffsetY !== 'string') {

Copilot uses AI. Check for mistakes.
Comment on lines +923 to +927
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same type mismatch issue as with minorOffsetX. The code sets minorOffsetY to the string '0' but checks if the type is NOT 'number'. According to the TypeScript interface, minorOffsetY should be of type '-2' | '-1' | '0' | '1' | '2' (a string union type), not a number. The condition should check typeof attr.minorOffsetY !== 'string' or check if the attribute is undefined.

Suggested change
if (typeof attr.minorOffsetX !== 'number') {
attr.minorOffsetX = '0';
graph.mergeNodeAttributes(node, { [type]: attr });
}
if (typeof attr.minorOffsetY !== 'number') {
if (typeof attr.minorOffsetX !== 'string') {
attr.minorOffsetX = '0';
graph.mergeNodeAttributes(node, { [type]: attr });
}
if (typeof attr.minorOffsetY !== 'string') {

Copilot uses AI. Check for mistakes.
attr.minorOffsetY = '0';
graph.mergeNodeAttributes(node, { [type]: attr });
}
});
return JSON.stringify({ ...p, version: 69, graph: graph.export() });
},
};