diff --git a/package-lock.json b/package-lock.json index 839d9340..e55970f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rmp", - "version": "3.5.40", + "version": "3.5.41", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "rmp", - "version": "3.5.40", + "version": "3.5.41", "license": "GPL-3.0-only", "dependencies": { "@chakra-ui/react": "^2.8.2", @@ -16,7 +16,7 @@ "@railmapgen/rmg-palette-resources": "^2.2.3", "@railmapgen/rmg-runtime": "^9.0.5", "@railmapgen/rmg-translate": "^3.1.2", - "@railmapgen/svg-assets": "^3.1.1", + "@railmapgen/svg-assets": "^4.0.3", "@reduxjs/toolkit": "^2.1.0", "bezier-js": "^6.1.4", "canvas-size": "^1.2.6", @@ -47,7 +47,7 @@ "@typescript-eslint/parser": "^6.15.0", "@vitejs/plugin-legacy": "^5.2.0", "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-plugin-prettier": "^5.1.0", "eslint-plugin-react": "^7.33.2", "graphology-types": "^0.24.7", @@ -3668,22 +3668,22 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -3704,9 +3704,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "node_modules/@jest/schemas": { @@ -3887,9 +3887,9 @@ "integrity": "sha512-YEgJ5r680chz0kPyYmBUhkCq5gqlVTpAjmPaLBBlekZoLEJyjWNGh29GuARVJdD1li6MJStuLCr5pR2zSkk2AA==" }, "node_modules/@railmapgen/svg-assets": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@railmapgen/svg-assets/-/svg-assets-3.1.1.tgz", - "integrity": "sha512-yVhFofpJa8byLicsWyuFla2X9sVa9eWKg0+Ef4eXeyOEOl4IWsrd4LGQZqfMnHcJUU5qDfTIaXlGuDcrlidM9g==" + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@railmapgen/svg-assets/-/svg-assets-4.0.3.tgz", + "integrity": "sha512-gTilmzSciGI7hD29DA/svsYd1EHvZidF8k1DxupEBilmzNqiBFCs6op3HVv6gBxauFwOqHbvsR6jhyGpgmLj1g==" }, "node_modules/@reduxjs/toolkit": { "version": "2.1.0", @@ -6607,16 +6607,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -11392,9 +11392,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "optional": true, "peer": true, @@ -13982,19 +13982,19 @@ } }, "@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" } }, @@ -14005,9 +14005,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "@jest/schemas": { @@ -14150,9 +14150,9 @@ "integrity": "sha512-YEgJ5r680chz0kPyYmBUhkCq5gqlVTpAjmPaLBBlekZoLEJyjWNGh29GuARVJdD1li6MJStuLCr5pR2zSkk2AA==" }, "@railmapgen/svg-assets": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@railmapgen/svg-assets/-/svg-assets-3.1.1.tgz", - "integrity": "sha512-yVhFofpJa8byLicsWyuFla2X9sVa9eWKg0+Ef4eXeyOEOl4IWsrd4LGQZqfMnHcJUU5qDfTIaXlGuDcrlidM9g==" + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@railmapgen/svg-assets/-/svg-assets-4.0.3.tgz", + "integrity": "sha512-gTilmzSciGI7hD29DA/svsYd1EHvZidF8k1DxupEBilmzNqiBFCs6op3HVv6gBxauFwOqHbvsR6jhyGpgmLj1g==" }, "@reduxjs/toolkit": { "version": "2.1.0", @@ -16056,16 +16056,16 @@ } }, "eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -19363,9 +19363,9 @@ "dev": true }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "optional": true, "peer": true, diff --git a/package.json b/package.json index 937535e7..c4ee23b5 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@railmapgen/rmg-palette-resources": "^2.2.3", "@railmapgen/rmg-runtime": "^9.0.5", "@railmapgen/rmg-translate": "^3.1.2", - "@railmapgen/svg-assets": "^3.1.1", + "@railmapgen/svg-assets": "^4.0.3", "@reduxjs/toolkit": "^2.1.0", "bezier-js": "^6.1.4", "canvas-size": "^1.2.6", @@ -46,10 +46,10 @@ "@typescript-eslint/parser": "^6.15.0", "@vitejs/plugin-legacy": "^5.2.0", "@vitejs/plugin-react": "^4.2.1", - "graphology-types": "^0.24.7", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-plugin-prettier": "^5.1.0", "eslint-plugin-react": "^7.33.2", + "graphology-types": "^0.24.7", "prettier": "^3.1.1", "typescript": "^5.3.3", "vite": "^5.0.12", @@ -64,5 +64,5 @@ "lint:fix": "eslint ./src --fix", "preview": "vite preview" }, - "version": "3.5.40" + "version": "3.5.41" } diff --git a/src/components/svgs/stations/gzmtr-basic.tsx b/src/components/svgs/stations/gzmtr-basic.tsx index f8291644..8df15223 100644 --- a/src/components/svgs/stations/gzmtr-basic.tsx +++ b/src/components/svgs/stations/gzmtr-basic.tsx @@ -162,7 +162,7 @@ const GzmtrBasicStation = (props: StationComponentProps) => { }; /** - * GzmtrStation specific props. + * GzmtrBasicStation specific props. */ export interface GzmtrBasicStationAttributes extends StationAttributes, AttributesWithColor { nameOffsetX: NameOffsetX; diff --git a/src/components/svgs/stations/gzmtr-int-2024.tsx b/src/components/svgs/stations/gzmtr-int-2024.tsx new file mode 100644 index 00000000..52e4ad56 --- /dev/null +++ b/src/components/svgs/stations/gzmtr-int-2024.tsx @@ -0,0 +1,449 @@ +import { Button, FormLabel, VStack } from '@chakra-ui/react'; +import { RmgFields, RmgFieldsField, RmgLabel } from '@railmapgen/rmg-components'; +import { MonoColour } from '@railmapgen/rmg-palette-resources'; +import { InterchangeStation2024 } from '@railmapgen/svg-assets/gzmtr'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { MdAdd } from 'react-icons/md'; +import { AttrsProps, CanvasType, CategoriesType, CityCode } from '../../../constants/constants'; +import { + NameOffsetX, + NameOffsetY, + Station, + StationAttributes, + StationComponentProps, + StationType, + defaultStationAttributes, +} from '../../../constants/stations'; +import { InterchangeInfo, StationAttributesWithInterchange } from '../../panels/details/interchange-field'; +import { MultilineText, NAME_DY } from '../common/multiline-text'; +import { InterchangeCardGZMTR, defaultGZMTRTransferInfo } from './gzmtr-int-common'; + +const GzmtrInt2024Station = (props: StationComponentProps) => { + const { id, x, y, attrs, handlePointerDown, handlePointerMove, handlePointerUp } = props; + const { + names = defaultStationAttributes.names, + nameOffsetX = defaultGzmtrInt2024StationAttributes.nameOffsetX, + nameOffsetY = defaultGzmtrInt2024StationAttributes.nameOffsetY, + transfer = defaultGzmtrInt2024StationAttributes.transfer, + open = defaultGzmtrInt2024StationAttributes.open, + secondaryNames = defaultGzmtrInt2024StationAttributes.secondaryNames, + preferVertical = defaultGzmtrInt2024StationAttributes.preferVertical, + anchorAt = defaultGzmtrInt2024StationAttributes.anchorAt, + } = attrs[StationType.GzmtrInt2024] ?? defaultGzmtrInt2024StationAttributes; + + const onPointerDown = React.useCallback( + (e: React.PointerEvent) => handlePointerDown(id, e), + [id, handlePointerDown] + ); + const onPointerMove = React.useCallback( + (e: React.PointerEvent) => handlePointerMove(id, e), + [id, handlePointerMove] + ); + const onPointerUp = React.useCallback( + (e: React.PointerEvent) => handlePointerUp(id, e), + [id, handlePointerUp] + ); + + const transferAll = transfer.flat().slice(0, 4); // slice to make sure at most 4 transfers + + // temporary fix for the missing id on the top element of the station + const iconEl = React.useRef(null); + iconEl.current?.querySelectorAll('path')?.forEach(elem => elem.setAttribute('id', `stn_core_${id}`)); + + const [iconBBox, setIconBBox] = React.useState({ x1: 0, x2: 0, y1: 0, y2: 0 }); + React.useEffect(() => { + const { height: iconHeight, width: iconWidth, x: iconX1, y: iconY1 } = iconEl.current!.getBBox(); + const [iconX2, iconY2] = [iconX1 + iconWidth, iconY1 + iconHeight]; + setIconBBox({ x1: iconX1, x2: iconX2, y1: iconY1, y2: iconY2 }); + }, [JSON.stringify(transferAll), anchorAt, setIconBBox, iconEl]); + const textDX = preferVertical && transferAll.length === 2 ? 0 : 8; + + const stations = transferAll.map(s => ({ + style: s[6] === 'gz' ? 'gzmtr' : ('fmetro' as 'gzmtr' | 'fmetro'), + lineNum: s[4], + stnNum: s[5], + strokeColour: s[2], + })); + + const textX = nameOffsetX === 'left' ? iconBBox.x1 + textDX : nameOffsetX === 'right' ? iconBBox.x2 - textDX : 0; + const textY = + (names[NAME_DY[nameOffsetY].namesPos].split('\\').length * NAME_DY[nameOffsetY].lineHeight + + (iconBBox.y2 - iconBBox.y1) / 2) * + NAME_DY[nameOffsetY].polarity; + const textAnchor = + nameOffsetX === 'left' + ? 'end' + : nameOffsetX === 'right' + ? 'start' + : !open && nameOffsetX === 'middle' && secondaryNames.join('') === '' + ? // Special hook to align station name and (Under Construction) when there are no secondaryNames. + 'end' + : // Default to middle when nameOffsetX === 'middle'. + 'middle'; + + const secondaryTextRef = React.useRef(null); + const [secondaryTextWidth, setSecondaryTextWidth] = React.useState(0); + React.useEffect(() => setSecondaryTextWidth(secondaryTextRef.current?.getBBox().width ?? 0), [...secondaryNames]); + + const textRef = React.useRef(null); + const [textWidth, setTextWidth] = React.useState(0); + React.useEffect(() => setTextWidth(textRef.current?.getBBox().width ?? 0), [...names]); + + const secondaryDx = (textWidth + (secondaryTextWidth + 12 * 2) / 2) * (nameOffsetX === 'left' ? -1 : 1); + const underConstructionDx = + (textWidth + secondaryTextWidth + (secondaryTextWidth !== 0 ? 12 * 2 : 0)) * (nameOffsetX === 'left' ? -1 : 1); + + return ( + + + = 0 ? anchorAt : undefined} + /> + + + + + + {secondaryNames.join('') !== '' && ( + + + ( + + + ) + + + + {secondaryNames[0]} + + + {secondaryNames[1]} + + + + )} + {!open && ( + + + (未开通) + + + (Under Construction) + + + )} + + ); +}; + +/** + * GzmtrInt2024Station specific props. + */ +export interface GzmtrInt2024StationAttributes extends StationAttributes, StationAttributesWithInterchange { + nameOffsetX: NameOffsetX; + nameOffsetY: NameOffsetY; + /** + * Whether to show a Under Construction hint. + */ + open: boolean; + secondaryNames: [string, string]; + preferVertical: boolean; + anchorAt: number; +} + +const defaultGzmtrInt2024StationAttributes: GzmtrInt2024StationAttributes = { + ...defaultStationAttributes, + nameOffsetX: 'right', + nameOffsetY: 'top', + transfer: [ + [ + [CityCode.Guangzhou, 'gz1', '#F3D03E', MonoColour.white, '1', '14', 'gz'], + [CityCode.Guangzhou, 'gz3', '#ECA154', MonoColour.white, '3', '11', 'gz'], + ], + ], + open: true, + secondaryNames: ['', ''], + preferVertical: true, + anchorAt: -1, +}; + +const gzmtrInt2024StationAttrsComponents = (props: AttrsProps) => { + const { id, attrs, handleAttrsUpdate } = props; + const { t } = useTranslation(); + + const fields: RmgFieldsField[] = [ + { + type: 'textarea', + label: t('panel.details.stations.common.nameZh'), + value: attrs.names[0], + onChange: val => { + attrs.names[0] = val; + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + { + type: 'textarea', + label: t('panel.details.stations.common.nameEn'), + value: attrs.names[1], + onChange: val => { + attrs.names[1] = val; + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + { + type: 'select', + label: t('panel.details.stations.common.nameOffsetX'), + value: attrs.nameOffsetX, + options: { + left: t('panel.details.stations.common.left'), + middle: t('panel.details.stations.common.middle'), + right: t('panel.details.stations.common.right'), + }, + disabledOptions: attrs.nameOffsetY === 'middle' ? ['middle'] : [], + onChange: val => { + attrs.nameOffsetX = val as Exclude; + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + { + type: 'select', + label: t('panel.details.stations.common.nameOffsetY'), + value: attrs.nameOffsetY, + options: { + top: t('panel.details.stations.common.top'), + middle: t('panel.details.stations.common.middle'), + bottom: t('panel.details.stations.common.bottom'), + }, + disabledOptions: attrs.nameOffsetX === 'middle' ? ['middle'] : [], + onChange: val => { + attrs.nameOffsetY = val as Exclude; + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + { + type: 'select', + label: t('panel.details.stations.gzmtrInt2024.anchorAt'), + value: attrs.anchorAt ?? '-1', + options: { + '-1': t('panel.details.stations.gzmtrInt2024.anchorAtNone'), + ...Object.fromEntries( + Array.from({ length: Math.min(attrs.transfer.flat().length, 4) }, (_, i) => [i.toString(), i]) + ), + }, + onChange: val => { + attrs.anchorAt = Number(val); + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + { + type: 'switch', + label: t('panel.details.stations.gzmtrInt2024.preferVertical'), + oneLine: true, + isChecked: attrs.preferVertical, + onChange: val => { + attrs.preferVertical = val; + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + { + type: 'switch', + label: t('panel.details.stations.gzmtrInt.open'), + oneLine: true, + isChecked: attrs.open, + onChange: val => { + attrs.open = val; + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + { + type: 'input', + label: t('panel.details.stations.gzmtrInt.secondaryNameZh'), + value: attrs.secondaryNames[0], + onChange: val => { + attrs.secondaryNames[0] = val; + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + { + type: 'input', + label: t('panel.details.stations.gzmtrInt.secondaryNameEn'), + value: attrs.secondaryNames[1], + onChange: val => { + attrs.secondaryNames[1] = val.toString(); + handleAttrsUpdate(id, attrs); + }, + minW: 'full', + }, + ]; + + const maximumTransfers = [4, 4, 0]; + const transfer = attrs.transfer ?? defaultGzmtrInt2024StationAttributes.transfer; + + const handleAdd = (setIndex: number) => (interchangeInfo: InterchangeInfo) => { + const newTransferInfo = structuredClone(transfer); + if (newTransferInfo.length <= setIndex) { + for (let i = newTransferInfo.length; i <= setIndex; i++) { + newTransferInfo[i] = [defaultGZMTRTransferInfo]; + } + } + newTransferInfo[setIndex].push(interchangeInfo); + + attrs.transfer = newTransferInfo; + handleAttrsUpdate(id, attrs); + }; + + const handleDelete = (setIndex: number) => (interchangeIndex: number) => { + if (transfer.length > setIndex && transfer[setIndex].length > interchangeIndex) { + const newTransferInfo = transfer.map((set, setIdx) => + setIdx === setIndex ? set.filter((_, intIdx) => intIdx !== interchangeIndex) : set + ); + + attrs.transfer = newTransferInfo; + attrs.anchorAt = -1; + handleAttrsUpdate(id, attrs); + } + }; + + const handleUpdate = (setIndex: number) => (interchangeIndex: number, interchangeInfo: InterchangeInfo) => { + if (transfer.length > setIndex && transfer[setIndex].length > interchangeIndex) { + const newTransferInfo = transfer.map((set, setIdx) => + setIdx === setIndex + ? set.map((int, intIdx) => + intIdx === interchangeIndex + ? ([0, 1, 2, 3, 4, 5, 6].map(i => + interchangeInfo[i] === undefined ? int[i] : interchangeInfo[i] + ) as InterchangeInfo) + : int + ) + : set + ); + + attrs.transfer = newTransferInfo; + handleAttrsUpdate(id, attrs); + } + }; + + const handleAddInterchangeGroup = () => handleAdd(transfer.length)(defaultGZMTRTransferInfo); + + return ( + <> + + + + + {transfer.map((infoList, i) => ( + + + {i === 0 + ? t('panel.details.stations.interchange.within') + : i === 1 + ? t('panel.details.stations.interchange.outStation') + : t('panel.details.stations.interchange.outSystem')} + + + infoList.length ? handleAdd(i) : undefined} + onDelete={handleDelete(i)} + onUpdate={handleUpdate(i)} + /> + + ))} + + {maximumTransfers[transfer.length] > 0 && ( + + )} + + + + ); +}; + +const gzmtrInt2024StationIcon = ( + + + +); + +const gzmtrInt2024Station: Station = { + component: GzmtrInt2024Station, + icon: gzmtrInt2024StationIcon, + defaultAttrs: defaultGzmtrInt2024StationAttributes, + attrsComponent: gzmtrInt2024StationAttrsComponents, + metadata: { + displayName: 'panel.details.stations.gzmtrInt2024.displayName', + cities: [CityCode.Guangzhou], + canvas: [CanvasType.RailMap], + categories: [CategoriesType.Metro], + tags: [], + }, +}; + +export default gzmtrInt2024Station; diff --git a/src/components/svgs/stations/gzmtr-int-common.tsx b/src/components/svgs/stations/gzmtr-int-common.tsx new file mode 100644 index 00000000..8afe5c67 --- /dev/null +++ b/src/components/svgs/stations/gzmtr-int-common.tsx @@ -0,0 +1,151 @@ +import { Box, HStack, IconButton, Text, VStack } from '@chakra-ui/react'; +import { RmgCard, RmgFields, RmgFieldsField, RmgLabel } from '@railmapgen/rmg-components'; +import { MonoColour } from '@railmapgen/rmg-palette-resources'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { MdAdd, MdContentCopy, MdDelete } from 'react-icons/md'; +import { CityCode } from '../../../constants/constants'; +import { useRootDispatch, useRootSelector } from '../../../redux'; +import { openPaletteAppClip } from '../../../redux/runtime/runtime-slice'; +import { InterchangeInfo } from '../../panels/details/interchange-field'; +import ThemeButton from '../../panels/theme-button'; + +export const defaultGZMTRTransferInfo = [ + CityCode.Guangzhou, + '', + '#AAAAAA', + MonoColour.white, + '', + '', + 'gz', +] as InterchangeInfo; + +/** + * A Guangzhou Metro specified interchange card props. + * For general use, see `src\components\panels\details\interchange-card.tsx` + */ +interface InterchangeCardGZMTRProps { + interchangeList: InterchangeInfo[]; + onAdd?: (info: InterchangeInfo) => void; + onDelete?: (index: number) => void; + onUpdate?: (index: number, info: InterchangeInfo) => void; +} + +/** + * A Guangzhou Metro specified interchange card. + * For general use, see `src\components\panels\details\interchange-card.tsx` + */ +export function InterchangeCardGZMTR(props: InterchangeCardGZMTRProps) { + const { interchangeList, onAdd, onDelete, onUpdate } = props; + const dispatch = useRootDispatch(); + const { + paletteAppClip: { output }, + } = useRootSelector(state => state.runtime); + + const { t } = useTranslation(); + + const [indexRequestedTheme, setIndexRequestedTheme] = React.useState(); + + React.useEffect(() => { + if (indexRequestedTheme !== undefined && output) { + onUpdate?.(indexRequestedTheme, [ + ...output, + interchangeList[indexRequestedTheme][4], + interchangeList[indexRequestedTheme][5], + interchangeList[indexRequestedTheme][6], + ]); + setIndexRequestedTheme(undefined); + } + }, [output?.toString()]); + + const interchangeFields: RmgFieldsField[][] = interchangeList.map((it, i) => [ + { + type: 'input', + label: t('panel.details.stations.common.lineCode'), + value: it[4], + minW: '80px', + onChange: val => onUpdate?.(i, [it[0], it[1], it[2], it[3], val, it[5], it[6]]), + }, + { + type: 'input', + label: t('panel.details.stations.common.stationCode'), + value: it[5], + minW: '80px', + onChange: val => onUpdate?.(i, [it[0], it[1], it[2], it[3], it[4], val, it[6]]), + }, + ]); + + const handleFoshanChange = (it: InterchangeInfo, i: number, foshan: boolean) => + onUpdate?.(i, [it[0], it[1], it[2], it[3], it[4], it[5], foshan ? 'fs' : 'gz']); + + return ( + + {interchangeList.length === 0 && ( + + + {t('panel.details.stations.interchange.noInterchanges')} + + + onAdd?.(defaultGZMTRTransferInfo)} + icon={} + /> + + )} + + {interchangeList.map((it, i) => ( + + + { + setIndexRequestedTheme(i); + dispatch(openPaletteAppClip([it[0], it[1], it[2], it[3]])); + }} + /> + + + + + + {onAdd && i === interchangeFields.length - 1 ? ( + onAdd?.(interchangeList.slice(-1)[0])} // duplicate last leg + icon={} + /> + ) : ( + + )} + + {onDelete && ( + onDelete?.(i)} + icon={} + /> + )} + + handleFoshanChange(it, i, val), + }, + ]} + /> + + + ))} + + ); +} diff --git a/src/components/svgs/stations/gzmtr-int.tsx b/src/components/svgs/stations/gzmtr-int.tsx index 4a54db81..9b4f2f67 100644 --- a/src/components/svgs/stations/gzmtr-int.tsx +++ b/src/components/svgs/stations/gzmtr-int.tsx @@ -1,11 +1,10 @@ -import { Box, Button, FormLabel, HStack, IconButton, Text, VStack, useColorModeValue } from '@chakra-ui/react'; -import { RmgCard, RmgFields, RmgFieldsField, RmgLabel } from '@railmapgen/rmg-components'; -import { MonoColour } from '@railmapgen/rmg-palette-resources'; +import { Button, FormLabel, VStack, useColorModeValue } from '@chakra-ui/react'; +import { RmgFields, RmgFieldsField, RmgLabel } from '@railmapgen/rmg-components'; import { StationNumber as FoshanStationNumber } from '@railmapgen/svg-assets/fmetro'; import { StationNumber } from '@railmapgen/svg-assets/gzmtr'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { MdAdd, MdContentCopy, MdDelete } from 'react-icons/md'; +import { MdAdd } from 'react-icons/md'; import { AttrsProps, CanvasType, CategoriesType, CityCode } from '../../../constants/constants'; import { NameOffsetX, @@ -16,11 +15,9 @@ import { StationType, defaultStationAttributes, } from '../../../constants/stations'; -import { useRootDispatch, useRootSelector } from '../../../redux'; -import { openPaletteAppClip } from '../../../redux/runtime/runtime-slice'; import { InterchangeInfo, StationAttributesWithInterchange } from '../../panels/details/interchange-field'; -import ThemeButton from '../../panels/theme-button'; import { MultilineText, NAME_DY } from '../common/multiline-text'; +import { InterchangeCardGZMTR, defaultGZMTRTransferInfo } from './gzmtr-int-common'; const CODE_POS = [ [[0, 0]], @@ -42,8 +39,6 @@ const CODE_POS = [ ], ]; -const defaultTransferInfo = [CityCode.Guangzhou, '', '#AAAAAA', MonoColour.white, '', '', 'gz'] as InterchangeInfo; - const GzmtrIntStation = (props: StationComponentProps) => { const { id, x, y, attrs, handlePointerDown, handlePointerMove, handlePointerUp } = props; const { @@ -349,7 +344,7 @@ const GzmtrIntStation = (props: StationComponentProps) => { }; /** - * GzmtrStation specific props. + * GzmtrIntStation specific props. */ export interface GzmtrIntStationAttributes extends StationAttributes, StationAttributesWithInterchange { nameOffsetX: NameOffsetX; @@ -480,7 +475,7 @@ const gzmtrIntStationAttrsComponents = (props: AttrsProps handleAdd(transfer.length)(defaultTransferInfo); + const handleAddInterchangeGroup = () => handleAdd(transfer.length)(defaultGZMTRTransferInfo); return ( <> @@ -537,7 +532,7 @@ const gzmtrIntStationAttrsComponents = (props: AttrsProps - infoList.length ? handleAdd(i) : undefined} onDelete={handleDelete(i)} @@ -566,10 +561,10 @@ const gzmtrIntStationAttrsComponents = (props: AttrsProps - + - + @@ -594,125 +589,3 @@ const gzmtrIntStation: Station = { }; export default gzmtrIntStation; - -interface InterchangeCardProps { - interchangeList: InterchangeInfo[]; - onAdd?: (info: InterchangeInfo) => void; - onDelete?: (index: number) => void; - onUpdate?: (index: number, info: InterchangeInfo) => void; -} - -function InterchangeCard(props: InterchangeCardProps) { - const { interchangeList, onAdd, onDelete, onUpdate } = props; - const dispatch = useRootDispatch(); - const { - paletteAppClip: { output }, - } = useRootSelector(state => state.runtime); - - const { t } = useTranslation(); - - const [indexRequestedTheme, setIndexRequestedTheme] = React.useState(); - - React.useEffect(() => { - if (indexRequestedTheme !== undefined && output) { - onUpdate?.(indexRequestedTheme, [ - ...output, - interchangeList[indexRequestedTheme][4], - interchangeList[indexRequestedTheme][5], - interchangeList[indexRequestedTheme][6], - ]); - setIndexRequestedTheme(undefined); - } - }, [output?.toString()]); - - const interchangeFields: RmgFieldsField[][] = interchangeList.map((it, i) => [ - { - type: 'input', - label: t('panel.details.stations.common.lineCode'), - value: it[4], - minW: '80px', - onChange: val => onUpdate?.(i, [it[0], it[1], it[2], it[3], val, it[5], it[6]]), - }, - { - type: 'input', - label: t('panel.details.stations.common.stationCode'), - value: it[5], - minW: '80px', - onChange: val => onUpdate?.(i, [it[0], it[1], it[2], it[3], it[4], val, it[6]]), - }, - ]); - - const handleFoshanChange = (it: InterchangeInfo, i: number, foshan: boolean) => - onUpdate?.(i, [it[0], it[1], it[2], it[3], it[4], it[5], foshan ? 'fs' : 'gz']); - - return ( - - {interchangeList.length === 0 && ( - - - {t('panel.details.stations.interchange.noInterchanges')} - - - onAdd?.(defaultTransferInfo)} - icon={} - /> - - )} - - {interchangeList.map((it, i) => ( - - - { - setIndexRequestedTheme(i); - dispatch(openPaletteAppClip([it[0], it[1], it[2], it[3]])); - }} - /> - - - - - - {onAdd && i === interchangeFields.length - 1 ? ( - onAdd?.(interchangeList.slice(-1)[0])} // duplicate last leg - icon={} - /> - ) : ( - - )} - - {onDelete && ( - onDelete?.(i)} - icon={} - /> - )} - - handleFoshanChange(it, i, val), - }, - ]} - /> - - - ))} - - ); -} diff --git a/src/components/svgs/stations/stations.ts b/src/components/svgs/stations/stations.ts index e1ffbe48..2e04d64f 100644 --- a/src/components/svgs/stations/stations.ts +++ b/src/components/svgs/stations/stations.ts @@ -5,6 +5,7 @@ import shmetroIntStation from './shmetro-int'; import shmetroOsysiStation from './shmetro-osysi'; import gzmtrBasicStation from './gzmtr-basic'; import gzmtrIntStation from './gzmtr-int'; +import gzmtrInt2024Station from './gzmtr-int-2024'; import bjsubwayBasicStation from './bjsubway-basic'; import bjsubwayIntStation from './bjsubway-int'; import mtrStation from './mtr'; @@ -26,6 +27,7 @@ const stations = { [StationType.ShmetroOutOfSystemInt]: shmetroOsysiStation, [StationType.GzmtrBasic]: gzmtrBasicStation, [StationType.GzmtrInt]: gzmtrIntStation, + [StationType.GzmtrInt2024]: gzmtrInt2024Station, [StationType.BjsubwayBasic]: bjsubwayBasicStation, [StationType.BjsubwayInt]: bjsubwayIntStation, [StationType.MTR]: mtrStation, diff --git a/src/constants/stations.ts b/src/constants/stations.ts index 6770bd52..b9d2d83a 100644 --- a/src/constants/stations.ts +++ b/src/constants/stations.ts @@ -5,6 +5,7 @@ import { ShmetroIntStationAttributes } from '../components/svgs/stations/shmetro import { ShmetroOsysiStationAttributes } from '../components/svgs/stations/shmetro-osysi'; import { GzmtrBasicStationAttributes } from '../components/svgs/stations/gzmtr-basic'; import { GzmtrIntStationAttributes } from '../components/svgs/stations/gzmtr-int'; +import { GzmtrInt2024StationAttributes } from '../components/svgs/stations/gzmtr-int-2024'; import { BjsubwayBasicStationAttributes } from '../components/svgs/stations/bjsubway-basic'; import { BjsubwayIntStationAttributes } from '../components/svgs/stations/bjsubway-int'; import { MTRStationAttributes } from '../components/svgs/stations/mtr'; @@ -26,6 +27,7 @@ export enum StationType { ShmetroOutOfSystemInt = 'shmetro-osysi', GzmtrBasic = 'gzmtr-basic', GzmtrInt = 'gzmtr-int', + GzmtrInt2024 = 'gzmtr-int-2024', BjsubwayBasic = 'bjsubway-basic', BjsubwayInt = 'bjsubway-int', MTR = 'mtr', @@ -48,6 +50,7 @@ export interface ExternalStationAttributes { [StationType.ShmetroOutOfSystemInt]?: ShmetroOsysiStationAttributes; [StationType.GzmtrBasic]?: GzmtrBasicStationAttributes; [StationType.GzmtrInt]?: GzmtrIntStationAttributes; + [StationType.GzmtrInt2024]?: GzmtrInt2024StationAttributes; [StationType.BjsubwayBasic]?: BjsubwayBasicStationAttributes; [StationType.BjsubwayInt]?: BjsubwayIntStationAttributes; [StationType.MTR]?: MTRStationAttributes; diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index cb339d21..7c7cf7b5 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -214,6 +214,12 @@ "secondaryNameEn": "Secondary name in English", "foshan": "Foshan" }, + "gzmtrInt2024": { + "displayName": "Guangzhou Metro interchange station (2024)", + "anchorAt": "Anchor at", + "anchorAtNone": "Center", + "preferVertical": "Vertical layout (2 interchanges only)" + }, "bjsubwayBasic": { "displayName": "Beijing Subway basic station", "open": "Is opened" diff --git a/src/i18n/translations/ja.json b/src/i18n/translations/ja.json index 31ca24f5..6541d4ce 100644 --- a/src/i18n/translations/ja.json +++ b/src/i18n/translations/ja.json @@ -214,6 +214,12 @@ "secondaryNameEn": "英語の補助駅名", "foshan": "佛山" }, + "gzmtrInt2024": { + "displayName": "広州地下鉄乗り換え駅(令和6年)", + "anchorAt": "錨位置", + "anchorAtNone": "中心", + "preferVertical": "縦向き版面構成(乗り換え駅2つのみ)" + }, "bjsubwayBasic": { "displayName": "北京地下鉄基本駅", "open": "開業済み" diff --git a/src/i18n/translations/ko.json b/src/i18n/translations/ko.json index e3fbaa0b..78f16ce6 100644 --- a/src/i18n/translations/ko.json +++ b/src/i18n/translations/ko.json @@ -214,6 +214,12 @@ "secondaryNameEn": "영문 제2명칭", "foshan": "포산" }, + "gzmtrInt2024": { + "displayName": "광저우 지하철 환승역 (2024)", + "anchorAt": "앵커 위치", + "anchorAtNone": "중앙", + "preferVertical": "세로 레이아웃 (환승 2개만)" + }, "bjsubwayBasic": { "displayName": "베이징 지하철 기본역", "open": "개통여부" diff --git a/src/i18n/translations/zh-Hans.json b/src/i18n/translations/zh-Hans.json index 8c857481..b0fb206a 100644 --- a/src/i18n/translations/zh-Hans.json +++ b/src/i18n/translations/zh-Hans.json @@ -190,7 +190,7 @@ "displayName": "上海地铁基本车站" }, "shmetroBasic2020": { - "displayName": "上海地铁基本车站(2020)" + "displayName": "上海地铁基本车站(2020)" }, "shmetroInt": { "displayName": "上海地铁换乘车站", @@ -214,6 +214,12 @@ "secondaryNameEn": "英文第二名称", "foshan": "佛山" }, + "gzmtrInt2024": { + "displayName": "广州地铁换乘站(2024)", + "anchorAt": "定位于", + "anchorAtNone": "中心", + "preferVertical": "垂直布局(仅限2个换乘)" + }, "bjsubwayBasic": { "displayName": "北京地铁基本车站", "open": "是否开通" diff --git a/src/i18n/translations/zh-Hant.json b/src/i18n/translations/zh-Hant.json index 1a3c8ac8..fde51283 100644 --- a/src/i18n/translations/zh-Hant.json +++ b/src/i18n/translations/zh-Hant.json @@ -190,7 +190,7 @@ "displayName": "上海地鐵基本車站" }, "shmetroBasic2020": { - "displayName": "上海地鐵基本車站(2020)" + "displayName": "上海地鐵基本車站(2020)" }, "shmetroInt": { "displayName": "上海地鐵換乘車站", @@ -214,6 +214,12 @@ "secondaryNameEn": "英文第二名稱", "foshan": "佛山" }, + "gzmtrInt2024": { + "displayName": "廣州地鐵換乘站(2024)", + "anchorAt": "定位於", + "anchorAtNone": "中心", + "preferVertical": "垂直佈局(僅限2個換乘)" + }, "bjsubwayBasic": { "displayName": "北京地鐵基本車站", "open": "是否開通" diff --git a/src/util/save.test.ts b/src/util/save.test.ts index d8500134..97979d3f 100644 --- a/src/util/save.test.ts +++ b/src/util/save.test.ts @@ -401,4 +401,16 @@ describe('Unit tests for param upgrade function', () => { '{"graph":{"options":{"type":"directed","multi":true,"allowSelfLoops":true},"attributes":{},"nodes":[],"edges":[]},"svgViewBoxZoom":100,"svgViewBoxMin":{"x":0,"y":0},"version":30}'; expect(newParam).toEqual(expectParam); }); + + it('30 -> 31', () => { + // Bump save version to support Singapore MRT facilities. + const oldParam = + '{"graph":{"options":{"type":"directed","multi":true,"allowSelfLoops":true},"attributes":{},"nodes":[],"edges":[]},"svgViewBoxZoom":100,"svgViewBoxMin":{"x":0,"y":0},"version":30}'; + const newParam = UPGRADE_COLLECTION[30](oldParam); + const graph = new MultiDirectedGraph() as MultiDirectedGraph; + expect(() => graph.import(JSON.parse(newParam))).not.toThrow(); + const expectParam = + '{"graph":{"options":{"type":"directed","multi":true,"allowSelfLoops":true},"attributes":{},"nodes":[],"edges":[]},"svgViewBoxZoom":100,"svgViewBoxMin":{"x":0,"y":0},"version":31}'; + expect(newParam).toEqual(expectParam); + }); }); diff --git a/src/util/save.ts b/src/util/save.ts index 4e064db2..eca40dc6 100644 --- a/src/util/save.ts +++ b/src/util/save.ts @@ -30,7 +30,7 @@ export interface RMPSave { svgViewBoxMin: { x: number; y: number }; } -export const CURRENT_VERSION = 30; +export const CURRENT_VERSION = 31; /** * Load the tutorial. @@ -400,9 +400,12 @@ export const UPGRADE_COLLECTION: { [version: number]: (param: string) => string return JSON.stringify({ ...p, version: 28, graph: graph.export() }); }, 28: param => - // Bump save version to support Qingdao Metro Station. + // Bump save version to support Qingdao Metro tation. JSON.stringify({ ...JSON.parse(param), version: 29 }), 29: param => // Bump save version to support Singapore MRT facilities. JSON.stringify({ ...JSON.parse(param), version: 30 }), + 30: param => + // Bump save version to support Guangzhou Metro interchange station 2024. + JSON.stringify({ ...JSON.parse(param), version: 31 }), };