├── .gitignore
├── .vscode
└── settings.json
├── README-cn.md
├── README.md
├── package-lock.json
├── package.json
├── public
├── app-icon.png
├── beijing.json
├── changsha.json
├── favicon.ico
├── guangzhou.json
├── hongkong.json
├── index.html
├── manifest.json
├── robots.txt
├── shanghai.json
├── shenzhen.json
└── tianjing.json
├── src
├── Common
│ ├── AutoGrowthInput.scss
│ ├── AutoGrowthInput.tsx
│ ├── api.ts
│ ├── color.ts
│ ├── const.ts
│ ├── teyvat.ts
│ └── util.ts
├── Data
│ ├── Shape.ts
│ └── UserData.ts
├── DataStructure
│ ├── Bend.ts
│ ├── ConnectType.ts
│ ├── Direction.ts
│ ├── Display.ts
│ ├── Line.ts
│ ├── LineRecord.ts
│ ├── Mode.ts
│ ├── Point.ts
│ ├── Rail.ts
│ ├── RailPair.ts
│ ├── Station.ts
│ ├── Straight.ts
│ ├── Track.ts
│ └── Vector.ts
├── Entrance
│ ├── App.scss
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── reportWebVitals.js
│ └── setupTests.js
├── Grid
│ └── Scale.ts
├── Line
│ ├── Handle.ts
│ └── LinePoints.ts
├── Render
│ ├── Card
│ │ ├── Cards.scss
│ │ ├── Cards.tsx
│ │ ├── LineCard.scss
│ │ ├── LineCard.tsx
│ │ ├── StationCard.scss
│ │ └── StationCard.tsx
│ ├── Component
│ │ └── LineRender.tsx
│ ├── Delete
│ │ ├── DeleteConfirmation.scss
│ │ └── DeleteConfirmation.tsx
│ ├── ErrorFallback
│ │ ├── ErrorFallback.scss
│ │ └── ErrorFallback.tsx
│ ├── Header
│ │ ├── Component
│ │ │ ├── OpacityControl.scss
│ │ │ ├── OpacityControl.tsx
│ │ │ ├── ShapeSelector.scss
│ │ │ └── ShapeSelector.tsx
│ │ ├── Menu.scss
│ │ └── Menu.tsx
│ ├── Layer
│ │ ├── DevelopLayer.scss
│ │ ├── DevelopLayer.tsx
│ │ ├── RenderLayer.scss
│ │ ├── RenderLayer.tsx
│ │ ├── ScaleLayer.scss
│ │ └── ScaleLayer.tsx
│ └── Recovery
│ │ ├── Recovery.scss
│ │ └── Recovery.tsx
├── Resource
│ ├── Icon
│ │ ├── airwave.tsx
│ │ ├── arrow.tsx
│ │ ├── auto.tsx
│ │ ├── bolt.tsx
│ │ ├── clock.arrow.circlepath.svg
│ │ ├── edit.tsx
│ │ ├── expand.tsx
│ │ ├── export.svg
│ │ ├── export.tsx
│ │ ├── finished.tsx
│ │ ├── globe.svg
│ │ ├── goto.tsx
│ │ ├── infinity.tsx
│ │ ├── no.svg
│ │ ├── ok.svg
│ │ ├── pageview.tsx
│ │ ├── plus.tsx
│ │ ├── share.svg
│ │ ├── share2.svg
│ │ ├── shrink.tsx
│ │ └── touch.tsx
│ └── Shape
│ │ └── shape.tsx
├── Style
│ └── Cursor.ts
├── WelcomeTour
│ ├── Driver.ts
│ ├── HightLights.tsx
│ ├── Steps
│ │ ├── common-operation.ts
│ │ ├── export.ts
│ │ ├── line-card.ts
│ │ ├── quick-edit.ts
│ │ ├── skip.ts
│ │ ├── station-card.ts
│ │ └── tag-setting.ts
│ ├── WelcomeTour.scss
│ └── WelcomeTour.tsx
├── i18n
│ ├── config.ts
│ └── locales
│ │ ├── en.json
│ │ └── zh.json
├── index.js
└── types
│ └── svg.d.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "i18n-ally.localesPaths": ["src/i18n/locales"],
3 | "i18n-ally.extract.autoDetect": true,
4 | "i18n-ally.keystyle": "nested",
5 | "i18n-ally.sourceLanguage": "zh",
6 | "i18n-ally.displayLanguage": "zh",
7 | "i18n-ally.extract.ignored": [
8 | "“\n ",
9 | "”\n ",
10 | "”\n ",
11 | "\n 重做\n ",
12 | ".title .click-panel",
13 | ".title .click-panel"
14 | ],
15 | "i18n-ally.extract.ignoredByFiles": {
16 | "src/WelcomeTour/WelcomeTour.tsx": [
17 | ".welcome-tour .body"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/README-cn.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | # Mini Metro Web
5 | 迷你地铁地图构建工具: `创建迷你地铁风格的地铁线路图`。
6 |
7 | 支持**无限**站点与线路,支持**多次穿过**同一站点,设置**背景图**,设置**支线**,支持**导出图片**。
8 |
9 | [https://mini-metro-web.gitlab.io/](https://mini-metro-web.gitlab.io/)
10 |
11 | [English Version](https://github.com/RyanEdo/mini-metro-web/blob/master/README.md)
12 |
13 |
14 | ## 更新日志
15 | #### 1.2.1 `修复添加站点时,连续双击站点导致的站点重复添加报错`
16 | #### 1.2.0 `支持英文`
17 | #### 1.1.1 `增加更多站点形状,新增站点时支持修改默认形状`
18 | #### 1.1.0 `支持修改背景颜色与背景图`
19 | #### 1.0.2 `加入刷新后快速恢复通知`
20 | #### 1.0.1 `加入报错指引`
21 |
22 |
23 | ## 基本用法
24 |
25 | ### 菜单
26 | 点击左上角标题进入菜单,点击任意空白处退出菜单
27 |
28 | ### 创建站点
29 | 1. 菜单 => 站点 => 添加站点
30 | 2. 点击空白处添加
31 |
32 | ### 创建线路
33 | 1. 点击任意站点
34 | 2. 选择 操作 => 以此为起点新建线路
35 | 3. 按照提示依次点击站点添加到线路
36 |
37 | ### 删除站点/线路
38 | 1. 点击站点/线路
39 | 2. 选择 操作 => 删除
40 |
41 | ### 设置背景图
42 | 1. 点击线路 => 设定背景
43 | 2. 选择 `纯白` / `浅黄` / `取色器` 设定背景颜色
44 | 3. 选择 `导入背景图` 导入图片
45 | 4. 导入图片后,进入修改图片页面,可以拖动修改图片位置,也可以修改图片透明度
46 |
47 | ### 导出图片/文件
48 | 菜单 => 作为图片/文件导出
49 |
50 | ## 进阶用法
51 | 请参考应用内教程或[视频教程](https://space.bilibili.com/8217854)
52 |
53 | ## 构建
54 |
55 | 和大部分react项目一样,先运行`npm i`,然后:
56 |
57 | ### `npm start`
58 |
59 | 本地运行
60 | 打开 [http://localhost:3000](http://localhost:3000) 浏览器中查看.
61 |
62 | ### `npm run build`
63 |
64 | 打包成静态文件
65 |
66 | ### 注意
67 | ##### 建议安装 `i18n-ally`[VS Code 插件地址](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally) 。项目有部分字符串由该插件生成,安装后能自动在字符串位置显示原文。
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | # Mini Metro Web
5 | Mini Metro Map Building Tool: `Create mini metro-style subway maps`.
6 |
7 | Supports **unlimited** stations and lines, supports **multiple crossings** of the same station, set **background images**, set **sub lines**, and supports **exporting images**.
8 |
9 | https://mini-metro-web.gitlab.io/
10 |
11 | [中文文档](https://github.com/RyanEdo/mini-metro-web/blob/master/README-cn.md)
12 |
13 | ## Changelog
14 | #### 1.2.1 `fix adding duplicate stations when double clicking at adding station mode`
15 | #### 1.2.0 `Support for English`
16 | #### 1.1.1 `Added more station shapes, support for modifying default shapes when adding new stations`
17 | #### 1.1.0 `Support for changing background color and background image`
18 | #### 1.0.2 `Added quick recovery notification after refresh`
19 | #### 1.0.1 `Added error guidance`
20 |
21 | ## Basic Usage
22 |
23 | ### Menu
24 | Click the title in the top left corner to enter the menu, click any blank area to exit the menu.
25 |
26 | ### Create Station
27 | 1. Menu => Station => Add Station
28 | 2. Click on a blank area to add
29 |
30 | ### Create Line
31 | 1. Click any station
32 | 2. Select Action => Create new line from this point
33 | 3. Follow the prompts to click stations in sequence to add them to the line
34 |
35 | ### Delete Station/Line
36 | 1. Click the station/line
37 | 2. Select Action => Delete
38 |
39 | ### Set Background Image
40 | 1. Click the line => Set background
41 | 2. Choose `Pure White` / `Light Yellow` / `Color Picker` to set the background color
42 | 3. Choose `Import Background Image` to import an image
43 | 4. After importing the image, enter the image editing page, where you can drag to adjust the image position and modify the image transparency
44 |
45 | ### Export Image/File
46 | Menu => Export as image/file
47 |
48 | ## Advanced Usage
49 | Please refer to the in-app tutorial or video tutorial
50 |
51 | ## Build
52 |
53 | Like most React projects, first run `npm i`, then:
54 |
55 | ### `npm start`
56 |
57 | Run locally
58 | Open http://localhost:3000 to view in the browser.
59 |
60 | ### `npm run build`
61 |
62 | Build into static files
63 |
64 | ### Note
65 | ##### It is recommended to install the `i18n-ally` VS Code plugin. Some strings in the project are generated by this plugin, and after installation, the original text will be automatically displayed at the string location.
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-metro-web",
3 | "version": "1.2.1",
4 | "private": true,
5 | "dependencies": {
6 | "@svgr/webpack": "^8.1.0",
7 | "@testing-library/jest-dom": "^5.16.5",
8 | "@testing-library/react": "^13.4.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "@types/downloadjs": "^1.4.6",
11 | "classnames": "^2.5.1",
12 | "downloadjs": "^1.4.7",
13 | "driver.js": "^1.3.1",
14 | "html-to-image": "^1.11.11",
15 | "i18next": "^23.16.3",
16 | "i18next-browser-languagedetector": "^8.0.0",
17 | "moment": "^2.30.1",
18 | "qrcode": "^1.5.3",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "react-error-boundary": "^4.0.13",
22 | "react-i18next": "^15.1.0",
23 | "react-scripts": "5.0.1",
24 | "sass": "^1.62.0",
25 | "ua-parser-js": "^1.0.37",
26 | "web-vitals": "^2.1.4"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | },
52 | "devDependencies": {
53 | "@types/qrcode": "^1.5.5",
54 | "@types/ua-parser-js": "^0.7.39"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/public/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RyanEdo/mini-metro-web/fdbd501c7a9f7a5ef6f1822986b450f3565e5137/public/app-icon.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RyanEdo/mini-metro-web/fdbd501c7a9f7a5ef6f1822986b450f3565e5137/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
19 |
20 |
29 | Mini Metro Web
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Mini Metro Web",
3 | "name": "Mini Metro Web",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "app-icon.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "app-icon.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#ffffff",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/Common/AutoGrowthInput.scss:
--------------------------------------------------------------------------------
1 | .auto-growth-container {
2 | position: relative;
3 | width: fit-content;
4 | .auto-growth-span {
5 | // font-size: 36px;
6 | border: none;
7 | // font-weight: 500;
8 | // width: 100px;
9 | // display: inline-block;
10 | // margin-top: 14px;
11 | opacity: 0;
12 | background-color: transparent;
13 | white-space: nowrap;
14 | }
15 | .auto-growth-input {
16 | // font-size: 36px;
17 | white-space: nowrap;
18 |
19 | border: none;
20 | // font-weight: 500;
21 | width: calc(100% + 18px);
22 | // margin-top: 14px;
23 | position: absolute;
24 | left: 0;
25 | padding: 0;
26 | background-color: transparent;
27 | &:disabled{
28 | appearance: none;
29 | color: inherit;
30 | opacity: inherit;
31 | cursor: default;
32 | border: none;
33 | &:focus,&:focus-visible{
34 | outline: none;
35 | }
36 | }
37 | }
38 | .click-panel{
39 | position: absolute;
40 | left: 0;
41 | width: calc(100% + 18px);
42 | height: 100%;
43 | top: 0;
44 | // cursor: pointer;
45 | }
46 |
47 | &.disabled{
48 | .auto-growth-span{
49 | opacity: 1;
50 | }
51 | .auto-growth-input{
52 | // opacity: 0;
53 | display: none;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Common/AutoGrowthInput.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React, {
3 | CSSProperties,
4 | Dispatch,
5 | LegacyRef,
6 | RefAttributes,
7 | RefObject,
8 | SetStateAction,
9 | StyleHTMLAttributes,
10 | forwardRef,
11 | useEffect,
12 | useState,
13 | } from "react";
14 | import "./AutoGrowthInput.scss";
15 | type InputProps = {
16 | value?: number | string;
17 | onInput?: (x: any) => void;
18 | className?: string;
19 | type?: string;
20 | disabled?: boolean;
21 | style?: CSSProperties;
22 | onClick?: (x: any) => void;
23 | }
24 | export const AutoGrowthInput = forwardRef(function ({
25 | value,
26 | onInput,
27 | className = "",
28 | type = "",
29 | disabled = false,
30 | style = {},
31 | onClick,
32 | }, ref) {
33 | return (
34 |
42 |
43 | {value}
44 |
45 |
{
56 | if (document.activeElement === e.currentTarget) e.stopPropagation();
57 | }}
58 | onBlur={(e)=>{
59 | if(type === "number")
60 | setTimeout(()=>{
61 | //@ts-ignore
62 | e.target.value = value
63 | })
64 | }}
65 | >
66 | {disabled?
:<>>}
67 |
68 | );
69 | })
70 |
--------------------------------------------------------------------------------
/src/Common/api.ts:
--------------------------------------------------------------------------------
1 | export const getExistMap = (id:string) => {
2 | const url = `/${id}.json`;
3 | return fetch(url).then(res=>res.text());
4 | }
--------------------------------------------------------------------------------
/src/Common/color.ts:
--------------------------------------------------------------------------------
1 | import { arrayToMap } from "./util";
2 |
3 | export const colorSH = [
4 | {
5 | "line": "1号线",
6 | "color": "#EA0B2A",
7 | "color_name": "正红色",
8 | "rgb": [234, 11, 42]
9 | },
10 | {
11 | "line": "2号线",
12 | "color": "#94D40B",
13 | "color_name": "绿色",
14 | "rgb": [148, 212, 11]
15 | },
16 | {
17 | "line": "3号线",
18 | "color": "#F8D000",
19 | "color_name": "黄色",
20 | "rgb": [248, 208, 0]
21 | },
22 | {
23 | "line": "4号线",
24 | "color": "#60269E",
25 | "color_name": "紫色",
26 | "rgb": [96, 38, 158]
27 | },
28 | {
29 | "line": "5号线",
30 | "color": "#934C9A",
31 | "color_name": "紫红色",
32 | "rgb": [147, 76, 154]
33 | },
34 | {
35 | "line": "6号线",
36 | "color": "#D80169",
37 | "color_name": "品红色",
38 | "rgb": [216, 1, 105]
39 | },
40 | {
41 | "line": "7号线",
42 | "color": "#FE6B01",
43 | "color_name": "橙色",
44 | "rgb": [254, 107, 1]
45 | },
46 | {
47 | "line": "8号线",
48 | "color": "#00A0E8",
49 | "color_name": "蓝色",
50 | "rgb": [0, 160, 232]
51 | },
52 | {
53 | "line": "9号线",
54 | "color": "#6FC5E8",
55 | "color_name": "淡蓝色",
56 | "rgb": [111, 197, 232]
57 | },
58 | {
59 | "line": "10号线",
60 | "color": "#C3A5E1",
61 | "color_name": "淡紫色",
62 | "rgb": [195, 165, 225]
63 | },
64 | {
65 | "line": "11号线",
66 | "color": "#792330",
67 | "color_name": "棕色",
68 | "rgb": [121, 35, 48]
69 | },
70 | {
71 | "line": "12号线",
72 | "color": "#007A61",
73 | "color_name": "深绿色",
74 | "rgb": [0, 122, 97]
75 | },
76 | {
77 | "line": "13号线",
78 | "color": "#F095CE",
79 | "color_name": "粉色",
80 | "rgb": [240, 149, 206]
81 | },
82 | {
83 | "line": "14号线",
84 | "color": "#827805",
85 | "color_name": "橄榄绿色",
86 | "rgb": [130, 120, 5]
87 | },
88 | {
89 | "line": "15号线",
90 | "color": "#BDA686",
91 | "color_name": "香槟金色",
92 | "rgb": [189, 166, 134]
93 | },
94 | {
95 | "line": "16号线",
96 | "color": "#2AD2C5",
97 | "color_name": "水绿色",
98 | "rgb": [42, 210, 197]
99 | }
100 | ];
101 |
102 | export const colorSHMap = arrayToMap("color",colorSH);
--------------------------------------------------------------------------------
/src/Common/const.ts:
--------------------------------------------------------------------------------
1 | const gauge = 10;
2 | const line_radius = 10;
3 | const handleLength = 45;
4 | const handleWidth = 15;
5 | export { gauge, line_radius, handleLength, handleWidth };
6 |
--------------------------------------------------------------------------------
/src/Data/Shape.ts:
--------------------------------------------------------------------------------
1 | export const Shape = {
2 | 'cicle': '圆形', // typo
3 | 'square': '正方形',
4 | 'triangle': '三角形',
5 | 'start': '五角星', // typo
6 | 'pentagon': '五边形',
7 | 'hexagon': '六边形',
8 | 'cross': '十字形',
9 | 'rhombus': '菱形',
10 | 'diamond': '钻石',
11 | 'leaf': '叶子',
12 | 'ginkgo': '银杏'
13 | };
14 |
--------------------------------------------------------------------------------
/src/DataStructure/Bend.ts:
--------------------------------------------------------------------------------
1 | import { Rail } from "./Rail";
2 | import { Track } from "./Track";
3 |
4 | export class Bend {
5 | // filter empty indexes
6 | static round1(track: Track) {
7 | const emptyRails = track.getEmptyRails().map((rail) => rail.index);
8 | if (emptyRails.length) return emptyRails;
9 | return [0, 1, 2];
10 | }
11 | // filter opposite rails
12 | static round2(track: Track, round1Indexes: number[], rail?: Rail) {
13 | // rail not exist or in/out rail direction not just opposite to the candidate rail direction
14 | if (!rail || !rail.track.direction.oppositeTo(track.direction))
15 | return round1Indexes;
16 | return round1Indexes.filter(
17 | (railIndex) => rail.oppositeIndex() === railIndex
18 | );
19 | }
20 | // filter center rail
21 | static round3(round2Indexes: number[]) {
22 | if (round2Indexes.find((railIndex) => railIndex === 1)) return 1;
23 | }
24 |
25 | // filter most close rail
26 | static round4(track: Track, rail?: Rail) {
27 | if (!rail) return 0;
28 | return rail.track.direction.rotationTo(track.direction) > 0 ? 0 : 2;
29 | }
30 |
31 | static getBestRailIndex(track: Track, rail?: Rail) {
32 | const round1Indexes = this.round1(track);
33 | if (round1Indexes.length === 1) return round1Indexes[0];
34 | const round2Indexes = this.round2(track, round1Indexes, rail);
35 | if (round2Indexes.length === 1) return round2Indexes[0];
36 | const round3Res = this.round3(round2Indexes);
37 | if (round3Res === 1) return 1;
38 | const round4Res = this.round4(track, rail);
39 | return round4Res;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/DataStructure/ConnectType.ts:
--------------------------------------------------------------------------------
1 | export enum ConnectType {
2 | straight,
3 | straightFirst,
4 | bendFirst
5 | }
--------------------------------------------------------------------------------
/src/DataStructure/Direction.ts:
--------------------------------------------------------------------------------
1 | //clockwise
2 |
3 | export enum Direct {
4 | up,
5 | upRight,
6 | right,
7 | rightDown,
8 | down,
9 | downLeft,
10 | left,
11 | leftUp,
12 | // Quadrant I bisected by a diagonal line
13 | // upRightA means the upper half
14 | // upRightB means the lower half
15 | // all the directions is marked in clockwise
16 | upRightA,
17 | upRightB,
18 | rightDownA,
19 | rightDownB,
20 | downLeftA,
21 | downLeftB,
22 | leftUpA,
23 | leftUpB,
24 | coincide,
25 | }
26 |
27 | export class Direction {
28 | direct: Direct;
29 | standard: boolean;
30 | lean: boolean;
31 | constructor(direct: Direct) {
32 | this.direct = direct;
33 | this.standard = direct < 8;
34 | this.lean = direct % 2 === 1;
35 | }
36 |
37 | delta(direct: Direct){
38 | const diff = Math.abs(direct - this.direct);
39 | return Math.min(diff, 8-diff);
40 | }
41 |
42 | opposite() {
43 | if (this.direct < 8) return new Direction((this.direct + 4) % 8);
44 | if (this.direct >= 8 && this.direct < 12)
45 | return new Direction(this.direct + 4);
46 | if (this.direct >= 12 && this.direct < 16)
47 | return new Direction(this.direct - 4);
48 | return new Direction(this.direct);
49 | }
50 |
51 | oppositeTo(direction: Direction | undefined) {
52 | if (direction) return direction.opposite().direct === this.direct;
53 | throw new Error("no direction!");
54 | }
55 |
56 | sameTo(direction: Direction) {
57 | return this.direct === direction.direct;
58 | }
59 |
60 | // how many rotation should this direction do to coincide to given direction
61 | // 1 2 3 means clockwise rotate 1 2 3 times
62 | // -1 -2 -3 means counterclockwise
63 | // 0 means no need rotate
64 | // 4 means opposite direction
65 | rotationTo(direction: Direction) {
66 | if (this.oppositeTo(direction)) return 4;
67 | const side = direction.direct - this.direct;
68 | if (side < -4) return side + 8;
69 | if (side > 4) return side - 8;
70 | return side;
71 | }
72 | getBendSteps(bendFirst: boolean) {
73 | if (this.direct < 8 || this.direct > 15) {
74 | throw new Error("this is not bend direction");
75 | }
76 | const firstStep = new Direction(this.direct - 7 - (this.direct % 2));
77 | const secondStep = new Direction((this.direct - 8 + (this.direct % 2)) % 8);
78 | return bendFirst ? [firstStep, secondStep] : [secondStep, firstStep];
79 | }
80 | }
81 |
82 | export const DirectionVictor = [
83 | [0, 1],
84 | [1, 1],
85 | [1, 0],
86 | [1, -1],
87 | [0, -1],
88 | [-1, -1],
89 | [-1, 0],
90 | [-1, 1],
91 | ];
92 |
93 | export const DirectionVictorReverseY = DirectionVictor.map(([x,y])=>[x,-y]);
94 |
--------------------------------------------------------------------------------
/src/DataStructure/Display.ts:
--------------------------------------------------------------------------------
1 | import { Station } from "./Station";
2 |
3 | export class DisplayStation {
4 | stationName: string;
5 | bendFirst: boolean;
6 | constructor(stationName: string, bendFirst: boolean){
7 | this.stationName = stationName;
8 | this.bendFirst = bendFirst;
9 | }
10 | }
--------------------------------------------------------------------------------
/src/DataStructure/Line.ts:
--------------------------------------------------------------------------------
1 | import { LineProps } from "../Data/UserData";
2 | import { Bend } from "./Bend";
3 | import { ConnectType } from "./ConnectType";
4 | import { LineRecord } from "./LineRecord";
5 | import { Rail } from "./Rail";
6 | import { RailPair } from "./RailPair";
7 | import { Station } from "./Station";
8 | import { Straight } from "./Straight";
9 | import { Vector } from "./Vector";
10 | export class Line {
11 | empty: boolean;
12 | departureRecord: LineRecord | undefined;
13 | _dev_tag: string | undefined;
14 | displayLine?: LineProps;
15 | constructor() {
16 | this.empty = false;
17 | }
18 |
19 | getTerminalRecord(){
20 | let p = this.departureRecord;
21 | while(p?.nextLineRecord){
22 | p = p.nextLineRecord;
23 | if(p === this.departureRecord || !p.nextLineRecord) return p;
24 | }
25 | return p;
26 | }
27 |
28 | linkAll(stations: Station[]){
29 | stations.reduce((pre,cur)=>{
30 | this.link(pre,cur);
31 | return cur;
32 | });
33 | }
34 |
35 |
36 | // connect B station and C station
37 | link(B: Station, C: Station, bendFirst: boolean = true) {
38 | const railPair = this.applyBestRailPair(B,C,bendFirst);
39 | railPair.setLine(this);
40 | let bLineRecord = B.getJoint(this);
41 | if (!bLineRecord) {
42 | //if record not exist, add one
43 | bLineRecord = new LineRecord(B, this);
44 | // register cLineRecord in C station
45 | B.addLineRecord(bLineRecord);
46 | this.departureRecord = bLineRecord;
47 | }
48 | let cLineRecord// = C.getJoint(this);
49 | if (!cLineRecord) {
50 | cLineRecord = new LineRecord(C, this);
51 | // register cLineRecord in C station
52 | C.addLineRecord(cLineRecord);
53 | }
54 | // establish doubly linked list
55 | bLineRecord?.establishConnectionTo(cLineRecord);
56 | // update rail and connect type for B and C
57 | LineRecord.updateLineRecords(bLineRecord, cLineRecord, railPair);
58 | }
59 |
60 | applyBestRailPair(B: Station, C: Station, bendFirst: boolean) {
61 | const direction = new Vector(B.position, C.position);
62 | if (direction.standard) {
63 | const bOutIndex = Straight.getBestRailIndex(B, C, this);
64 | const cInIndex = Rail.oppositeIndex(bOutIndex);
65 | // if(B.displayStation?.stationName==='风起地站'&&C.displayStation?.stationName==='达达乌帕谷') debugger
66 | const bTrack = B.getTrack(direction);
67 | const cTrack = C.getTrack(direction.opposite());
68 | const bRail = bTrack.getAvailableRail(bOutIndex);
69 | const cRail = cTrack.getAvailableRail(cInIndex);
70 | return new RailPair(bRail, cRail);
71 | } else {
72 | const [bOutDirection, cInDirectionOpposite] =
73 | direction.getBendSteps(bendFirst);
74 | const cInDirection = cInDirectionOpposite.opposite();
75 | const bTrack = B.getTrack(bOutDirection);
76 | const cTrack = C.getTrack(cInDirection);
77 | const bLastRail = B.getJoint(this)?.lastRail;
78 | const cNextRail = C.getJoint(this)?.nextRail;
79 | const bOutIndex = Bend.getBestRailIndex(bTrack, bLastRail);
80 | const cInIndex = Bend.getBestRailIndex(cTrack, cNextRail);
81 | const bRail = bTrack.getAvailableRail(bOutIndex);
82 | const cRail = cTrack.getAvailableRail(cInIndex);
83 | return new RailPair(bRail, cRail);
84 | }
85 | }
86 | }
87 |
88 | export class EmptyLine extends Line {
89 | constructor() {
90 | super();
91 | this.empty = true;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/DataStructure/LineRecord.ts:
--------------------------------------------------------------------------------
1 | import { ConnectType } from "./ConnectType";
2 | import { Line } from "./Line";
3 | import { Rail } from "./Rail";
4 | import { RailPair } from "./RailPair";
5 | import { Station } from "./Station";
6 |
7 | export class LineRecord {
8 | station: Station;
9 | line: Line | undefined;
10 | lastLineRecord: LineRecord | undefined;
11 | nextLineRecord: LineRecord | undefined;
12 | lastRail: Rail | undefined;
13 | nextRail: Rail | undefined;
14 |
15 | constructor(station: Station, line?: Line) {
16 | this.station = station;
17 | if (line) {
18 | this.line = line;
19 | }
20 | }
21 |
22 | getInDirection(){
23 | return this.lastRail?.track.direction;
24 | }
25 |
26 | getOutDirection(){
27 | return this.nextRail?.track.direction;
28 | }
29 | establishConnectionTo(BRecord: LineRecord) {
30 | LineRecord.establishConnection(this, BRecord);
31 | }
32 |
33 | static establishConnection(ARecord: LineRecord, BRecord: LineRecord) {
34 | ARecord.nextLineRecord = BRecord;
35 | BRecord.lastLineRecord = ARecord;
36 | }
37 |
38 | // update the rail and connect type information for connectting 2 linerecords
39 | static updateLineRecords(
40 | ARecord: LineRecord,
41 | BRecord: LineRecord,
42 | railPair: RailPair,
43 | ) {
44 | const { departureRail, arrivalRail } = railPair;
45 | ARecord.nextRail = departureRail;
46 | BRecord.lastRail = arrivalRail;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/DataStructure/Mode.ts:
--------------------------------------------------------------------------------
1 | export enum Mode {
2 | normal,
3 | moving,
4 | touchMoving,
5 | touchScaling
6 | }
7 |
8 |
9 | export enum FunctionMode{
10 | normal,
11 | addingStation,
12 | dragingStation,
13 | lineEditing,
14 | backgroundEditing,
15 | customBackground,
16 | editingCustomBackgroundPosition,
17 | selectingStation,
18 | choosingExistMap,
19 | }
--------------------------------------------------------------------------------
/src/DataStructure/Point.ts:
--------------------------------------------------------------------------------
1 | import { MouseEvent, Touch } from "react";
2 | import { Straight } from "./Straight";
3 |
4 | export class Point {
5 | x: number;
6 | y: number;
7 | yReversed: boolean;
8 | q_start: boolean;
9 | q: boolean;
10 | q_end: boolean;
11 | constructor(x: number = 0, y: number = 0, yReversed: boolean = false) {
12 | this.x = x;
13 | this.y = y;
14 | this.yReversed = yReversed;
15 | this.q_start = false;
16 | this.q = false;
17 | this.q_end = false;
18 | }
19 |
20 | round(){
21 | return new Point(Math.round(this.x), Math.round(this.y))
22 | }
23 |
24 | offset(A: Point) {
25 | return new Point(A.x + this.x, A.y + this.y);
26 | }
27 |
28 | displacementTo(A: Point) {
29 | return new Point(this.x - A.x, this.y - A.y);
30 | }
31 |
32 | distanceTo(A: Point) {
33 | return Math.sqrt(Math.pow(this.x - A.x, 2) + Math.pow(this.y - A.y, 2));
34 | }
35 |
36 | sameTo(A: Point) {
37 | return A.x === this.x && A.y === this.y && A.yReversed === this.yReversed;
38 | }
39 |
40 | reverseY() {
41 | return new Point(this.x, -this.y, !this.yReversed);
42 | }
43 |
44 | static getPointFromTouch(A: Touch) {
45 | return new Point(A.clientX, A.clientY);
46 | }
47 |
48 | static getPointFromMouse(A: MouseEvent) {
49 | return new Point(A.clientX, A.clientY);
50 | }
51 |
52 | static getMidPoint(A: Point, B: Point) {
53 | return new Point((A.x + B.x) / 2, (A.y + B.y) / 2);
54 | }
55 |
56 | static getDisplacement(A: Point, B: Point) {
57 | return new Point(A.x - B.x, A.y - B.y);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/DataStructure/Rail.ts:
--------------------------------------------------------------------------------
1 | import { EmptyLine, Line } from "./Line";
2 | import { RailPair } from "./RailPair";
3 | import { Track } from "./Track";
4 |
5 | export class Rail {
6 | track: Track;
7 | index: number;
8 | line: Line;
9 | extra: boolean;
10 | constructor(track: Track, index: number) {
11 | this.track = track;
12 | this.index = index;
13 | this.line = new EmptyLine();
14 | this.extra = false;
15 | }
16 |
17 | oppositeIndex(){
18 | return 2 - this.index;
19 | }
20 |
21 | setLine(line: Line){
22 | this.line = line;
23 | }
24 |
25 | static getStraightConnectRailPair(aEmptyRails: Rail[], bEmptyRails: Rail[]) {
26 | if (aEmptyRails.length === 0 || bEmptyRails.length === 0) return;
27 | const railPairs: RailPair[] = [];
28 | aEmptyRails.forEach((aRail) => {
29 | bEmptyRails.forEach((bRail) => {
30 | //found just opposite rail
31 | if (aRail.index + bRail.index === 2) {
32 | railPairs.push(new RailPair(aRail,bRail));
33 | }
34 | });
35 | });
36 | return railPairs;
37 | }
38 |
39 | static getBestRail(rails: Rail[]){
40 | return rails.find(rail=>rail.index === 1) || rails[0];
41 | }
42 |
43 | static getRailByIndex(rails: Rail[], index: number){
44 | return rails.find(rail=>rail.index === index);
45 | }
46 |
47 | static oppositeIndex(index: number){
48 | return 2 - index;
49 | }
50 |
51 | }
52 |
53 | export class ExtraRail extends Rail{
54 | constructor(track: Track, index: number){
55 | super(track, index);
56 | this.extra = true;
57 | }
58 | }
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/DataStructure/RailPair.ts:
--------------------------------------------------------------------------------
1 | import { Direction } from "./Direction";
2 | import { Line } from "./Line";
3 | import { Rail } from "./Rail";
4 | import { Station } from "./Station";
5 |
6 | export class RailPair {
7 | departureRail: Rail;
8 | arrivalRail: Rail;
9 | center: boolean;
10 | constructor(departureRail: Rail, arrivalRail: Rail) {
11 | this.departureRail = departureRail;
12 | this.arrivalRail = arrivalRail;
13 | this.center = departureRail.index === 1;
14 | }
15 |
16 | reverse(){
17 | const temp = this.departureRail;
18 | this.departureRail = this.arrivalRail;
19 | this.arrivalRail = temp;
20 | }
21 |
22 | setLine(line: Line){
23 | this.departureRail.setLine(line);
24 | this.arrivalRail.setLine(line);
25 | }
26 | }
--------------------------------------------------------------------------------
/src/DataStructure/Station.ts:
--------------------------------------------------------------------------------
1 | import { StationProps } from "../Data/UserData";
2 | import { Direct, Direction } from "./Direction";
3 | import { Line } from "./Line";
4 | import { LineRecord } from "./LineRecord";
5 | import { Point } from "./Point";
6 | import { Rail } from "./Rail";
7 | import { Track } from "./Track";
8 |
9 | export class Station {
10 | position: Point;
11 | tracks: Track[];
12 | lineRecords: Map;
13 | _dev_tag: string | undefined;
14 | handlers: (Line | undefined | null)[];
15 | displayStation?: StationProps;
16 | constructor(position: Point) {
17 | this.position = position;
18 | this.tracks = new Array(8)
19 | .fill(true)
20 | .map((x, direct: Direct) => new Track(this, new Direction(direct)));
21 | this.lineRecords = new Map();
22 | this.handlers = new Array(8);
23 | }
24 |
25 | lineCount(){
26 | const set = new Set();
27 | this.lineRecords.forEach(line=>{
28 | set.add(line);
29 | })
30 | return set.size;
31 | }
32 | isEmpty(direct: Direct){
33 | return !this.handlers[direct] && this.tracks[direct].isEmpty();
34 | }
35 |
36 | getBestDirectionForName() {
37 | let space = 0,
38 | endIndex = 0,
39 | maxSpace = 0,
40 | firstSpace;
41 | for (let i = 0; i < 8; i++) {
42 | const empty = !this.handlers[i] && this.tracks[i].isEmpty();
43 | if (empty) {
44 | space++;
45 | if (space > maxSpace) {
46 | maxSpace = space;
47 | endIndex = i;
48 | }
49 | } else {
50 | if (firstSpace === undefined) firstSpace = space;
51 | space = 0;
52 | }
53 | }
54 | // all empty
55 | if (space === 8 || maxSpace===0) {
56 | return Direct.right;
57 | }
58 | if(maxSpace <= 2) {
59 | if(this.isEmpty(2)) return 2;
60 | if(this.isEmpty(6)) return 6;
61 | if(this.isEmpty(1)) return 1;
62 | if(this.isEmpty(3)) return 3;
63 | if(this.isEmpty(5)) return 5;
64 | if(this.isEmpty(7)) return 7;
65 | }
66 | if(firstSpace === undefined) firstSpace =0;
67 | if (space + firstSpace >= maxSpace) {
68 | // max space containing 0
69 | if (firstSpace >= space) {
70 | return Math.floor((firstSpace - space) / 2);
71 | } else if (space > firstSpace) {
72 | return 7 - Math.floor((space - firstSpace) / 2);
73 | }
74 | }
75 | const best = endIndex - Math.floor(maxSpace / 2)
76 | // max space not containing 0
77 | if(maxSpace%2===0){
78 | if(best%2===0){
79 | return best+1;
80 | }
81 | }
82 | return best;
83 |
84 | }
85 |
86 |
87 | getBestDirectionForName2(){
88 | }
89 |
90 |
91 | addLineRecord(lineRecord: LineRecord) {
92 | const { line, station } = lineRecord;
93 | if (!line) {
94 | throw new Error("line is undefined while add line record to statation");
95 | }
96 | if (station !== this) {
97 | throw new Error(
98 | "you are adding a line record which not belongs to this station"
99 | );
100 | }
101 | // this lineRecords is the array saving linerecords
102 | const lineRecordsArr = this.lineRecords.get(line) || [];
103 | lineRecordsArr.push(lineRecord);
104 | // this.lineRecords is the map Line=>Line
105 | // containing all the information of the lines go through this station
106 | this.lineRecords.set(line, lineRecordsArr);
107 | }
108 |
109 | getTrack(direction: Direction) {
110 | return this.tracks[direction.direct];
111 | }
112 |
113 | getRail(direction: Direction, index: number) {
114 | return this.getTrack(direction).getRail(index);
115 | }
116 |
117 | // getBestRail(direction: Direction){
118 | // return this.getTrack(direction).getBestRail();
119 | // }
120 |
121 | // find the start or end of the Line
122 | // if no records find, this station must be the departure station
123 | // if both start and end exist, this station is the loop line joint
124 | getJoint(line: Line) {
125 | const terminal = this.lineRecords
126 | .get(line)
127 | ?.find((lineRecord) => !lineRecord.lastLineRecord);
128 | const departure = this.lineRecords
129 | .get(line)
130 | ?.find((lineRecord) => !lineRecord.nextLineRecord);
131 | return departure || terminal;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/DataStructure/Straight.ts:
--------------------------------------------------------------------------------
1 | import { Direction } from "./Direction";
2 | import { Line } from "./Line";
3 | import { Point } from "./Point";
4 | import { Rail } from "./Rail";
5 | import { Station } from "./Station";
6 | import { Track } from "./Track";
7 | import { Vector } from "./Vector";
8 |
9 | export class Straight {
10 | // round 1 : filter empty rails
11 | static round1(B: Station, C: Station) {
12 | const direction = new Vector(B.position, C.position);
13 | const bTrack = B.getTrack(direction);
14 | const cTrack = C.getTrack(direction.opposite());
15 | const initScores = [0, 1, 2];
16 | let max = 0;
17 | const round1Indexes = initScores
18 | .map((index) => {
19 | let score = 0;
20 | // if rail in bTrack is empty, this rail add one score
21 | if (bTrack.rails[index].line.empty) {
22 | score++;
23 | }
24 | // if rail in cTrack is empty, this rail add one score
25 | if (cTrack.rails[Rail.oppositeIndex(index)].line.empty) {
26 | score++;
27 | }
28 | // record the highest score
29 | if (score > max) {
30 | max = score;
31 | }
32 | return { index, score };
33 | })
34 | .filter(({ score }) => {
35 | // find the highest score indexes
36 | return score === max;
37 | });
38 | return round1Indexes;
39 | }
40 |
41 | // round 2 : filter straight pass rails
42 | static round2(
43 | B: Station,
44 | C: Station,
45 | round1Indexes: RoundResult[],
46 | line: Line
47 | ) {
48 | const direction = new Vector(B.position, C.position);
49 | const bLineRecord = B.getJoint(line);
50 | const cLineRecord = C.getJoint(line);
51 | let max = 0;
52 | const round2Indexes = round1Indexes
53 | .map(({ index }) => {
54 | let score = 0;
55 | if (
56 | bLineRecord?.lastRail?.track.direction.oppositeTo(direction) &&
57 | bLineRecord?.lastRail?.oppositeIndex() === index
58 | ) {
59 | score++;
60 | }
61 | if (
62 | cLineRecord?.nextRail?.track.direction.sameTo(direction) &&
63 | cLineRecord?.nextRail?.index === index
64 | ) {
65 | score++;
66 | }
67 | if (score > max) {
68 | max = score;
69 | }
70 | return { index, score };
71 | })
72 | .filter(({ score }) => {
73 | // find the highest score indexes
74 | return score === max;
75 | });
76 | return round2Indexes;
77 | }
78 |
79 | // round 3 : filter center rails
80 | static round3(B: Station, C: Station, round2Indexes: RoundResult[]) {
81 | const round3Res = round2Indexes.find(({ index }) => index === 1);
82 | return round3Res ? 1 : 0;
83 | }
84 |
85 | // round 4 : filter most close to the last or next rail
86 | static round4(B: Station, C: Station, line: Line) {
87 | const direction = new Vector(B.position, C.position);
88 | const bLineRecord = B.getJoint(line);
89 | const cLineRecord = C.getJoint(line);
90 | let zeroRailScore = 0;
91 | let secondRailScore = 0;
92 | if (bLineRecord?.lastRail)
93 | if (bLineRecord.lastRail.track.direction.rotationTo(direction) > 0)
94 | zeroRailScore++;
95 | else secondRailScore++;
96 |
97 | if (cLineRecord?.nextRail)
98 | if (cLineRecord.nextRail.track.direction.rotationTo(direction) > 0)
99 | zeroRailScore++;
100 | else secondRailScore++;
101 |
102 | return zeroRailScore > secondRailScore ? 0 : 2;
103 | }
104 |
105 | static getBestRailIndex(B: Station, C: Station, line: Line) {
106 | if (B._dev_tag === "B" && line._dev_tag === "line8") {
107 | // debugger;
108 | }
109 | // round 1:
110 | const round1Indexes = Straight.round1(B, C);
111 | if (round1Indexes.length === 1) {
112 | return round1Indexes[0].index;
113 | }
114 | // round 2:
115 | const round2Indexes = Straight.round2(B, C, round1Indexes, line);
116 | if (round2Indexes.length === 1) {
117 | return round2Indexes[0].index;
118 | }
119 | // round 3:
120 | const round3Index = Straight.round3(B, C, round2Indexes);
121 | if (round3Index === 1) {
122 | return round3Index;
123 | }
124 | // round 4:
125 | const round4Index = Straight.round4(B, C, line);
126 | return round4Index;
127 | }
128 | }
129 |
130 | class RoundResult {
131 | score!: number;
132 | index!: number;
133 | }
134 |
--------------------------------------------------------------------------------
/src/DataStructure/Track.ts:
--------------------------------------------------------------------------------
1 | import { Direction } from "./Direction";
2 | import { ExtraRail, Rail } from "./Rail";
3 | import { Station } from "./Station";
4 |
5 | export class Track {
6 | station: Station;
7 | direction: Direction;
8 | rails: Rail[];
9 | // extra rail won't occupy the space of rail
10 | // extraRails save extraRail by index, every index has an array
11 | extraRails: ExtraRail[][];
12 | constructor(station: Station, direction: Direction) {
13 | this.station = station;
14 | this.direction = direction;
15 | this.rails = new Array(3)
16 | .fill(true)
17 | .map((x, index) => new Rail(this, index));
18 | this.extraRails = new Array(3).fill(true).map((x, index) => new Array());
19 | }
20 |
21 | isEmpty(){
22 | return !this.rails.some(rail=>!rail.line.empty)
23 | }
24 |
25 | getEmptyRails() {
26 | return this.rails.filter((rail) => rail.line.empty);
27 | }
28 |
29 | getRail(index: number) {
30 | return this.rails[index];
31 | }
32 |
33 | applyExtraRail(index: number) {
34 | const extraRail = new ExtraRail(this, index);
35 | this.extraRails[index].push(extraRail);
36 | return extraRail;
37 | }
38 |
39 | getAvailableRail(index: number) {
40 | const rail = this.getRail(index);
41 | return rail.line.empty ? rail : this.applyExtraRail(index);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/DataStructure/Vector.ts:
--------------------------------------------------------------------------------
1 | import { Direct, Direction, DirectionVictorReverseY } from "./Direction";
2 | import { Point } from "./Point";
3 |
4 | export class Vector extends Direction {
5 | start: Point; // startPoint
6 | end: Point; // endPoint
7 | constructor(start: Point, end: Point) {
8 | super(Vector.getDirection(start, end));
9 | this.start = start;
10 | this.end = end;
11 | }
12 |
13 | verticalProlong(length: number) {
14 | const A = this.start,
15 | B = this.end;
16 | if (A.x === B.x)
17 | return [new Point(B.x + length, B.y), new Point(B.x - length, B.y)];
18 | const diffSign = (A.xB.x&&A.y>B.y);
19 | const tan = (B.y - A.y) / (B.x - A.x);
20 | const alpha = Math.abs(Math.atan(tan));
21 | const sin = Math.sin(alpha);
22 | const cos = Math.cos(alpha);
23 |
24 | if(diffSign)
25 | return [
26 | new Point(B.x - sin * length, B.y + cos * length),
27 | new Point(B.x + sin * length, B.y - cos * length),
28 | ];
29 | return [
30 | new Point(B.x - sin * length, B.y - cos * length),
31 | new Point(B.x + sin * length, B.y + cos * length),
32 | ];
33 |
34 | }
35 |
36 | prolong(length: number) {
37 | const A = this.start,
38 | B = this.end;
39 | const kX = A.x < B.x ? 1 : -1;
40 | const kY = A.y < B.y ? 1 : -1;
41 | if (A.x === B.x) return new Point(A.x, B.y + kY * length);
42 | const tan = (B.y - A.y) / (B.x - A.x);
43 | const alpha = Math.abs(Math.atan(tan));
44 | const sin = Math.sin(alpha);
45 | const cos = Math.cos(alpha);
46 | return new Point(B.x + kX * cos * length, B.y + kY * sin * length);
47 | }
48 |
49 | normalize(k: number = 1) {
50 | const deltaX = this.end.x - this.start.x;
51 | const deltaY = this.end.y - this.start.y;
52 | const module = Math.sqrt(
53 | Math.pow(deltaX, 2) + Math.pow(deltaY, 2)
54 | );
55 | return new Vector(
56 | new Point(0, 0),
57 | new Point((k * deltaX) / module, (k * deltaY) / module)
58 | );
59 | }
60 |
61 | passesThroughPoint(A: Point) {
62 | return (
63 | (A.x - this.start.x) * (A.y - this.end.y) ===
64 | (A.y - this.start.y) * (A.x - this.end.x)
65 | );
66 | }
67 |
68 | round() {
69 | return new Vector(this.start.round(), this.end.round());
70 | }
71 |
72 | passesThroughPointRound(point: Point) {
73 | const A = point.round();
74 | const vector = this.round();
75 | return this.passesThroughPoint.bind(vector)(A);
76 | }
77 |
78 | getCrossPointTo(b: Vector) {
79 | const a = this;
80 | const A = a.start,
81 | B = a.end,
82 | C = b.start,
83 | D = b.end;
84 |
85 | const m = (A.x - B.x) / (A.y - B.y);
86 | const n = (C.x - D.x) / (C.y - D.y);
87 | // if(!Number.isFinite(m)) return new Point(A.x, (A.x - C.x) / n + C.y);
88 | // if(!Number.isFinite(n)) return new Point(C.x, (C.x - A.x) / m + A.y);
89 | if (!Number.isFinite(m)) return new Point(n * (A.y - C.y) + C.x, A.y);
90 | if (!Number.isFinite(n)) return new Point(m * (C.y - A.y) + A.x, C.y);
91 | const y = (C.x - A.x + m * A.y - n * C.y) / (m - n);
92 | const x = n * (y - C.y) + C.x;
93 | return new Point(x, y);
94 | }
95 |
96 | static getVectorByPointAndDirection(A: Point, direction: Direction) {
97 | const [x, y] = DirectionVictorReverseY[direction.direct];
98 | return new Vector(A, new Point(A.x + x, A.y + y));
99 | }
100 |
101 | static getDirection(start: Point, end: Point) {
102 | const A = start.reverseY();
103 | const B = end.reverseY();
104 | if (A.x === B.x && A.y === B.y) return Direct.coincide;
105 | if (A.x === B.x) return B.y > A.y ? Direct.up : Direct.down;
106 | if (A.y === B.y) return B.x > A.x ? Direct.right : Direct.left;
107 | const deltaX = B.x - A.x;
108 | const deltaY = B.y - A.y;
109 | if (deltaX > 0 && deltaY > 0)
110 | if (deltaX === deltaY) return Direct.upRight;
111 | else if (deltaX < deltaY) return Direct.upRightA;
112 | else return Direct.upRightB;
113 | if (deltaX > 0 && deltaY < 0)
114 | if (deltaX === -deltaY) return Direct.rightDown;
115 | else if (deltaX > -deltaY) return Direct.rightDownA;
116 | else return Direct.rightDownB;
117 | if (deltaX < 0 && deltaY < 0)
118 | if (-deltaX === -deltaY) return Direct.downLeft;
119 | else if (-deltaX < -deltaY) return Direct.downLeftA;
120 | else return Direct.downLeftB;
121 | if (deltaX < 0 && deltaY > 0)
122 | if (-deltaX === deltaY) return Direct.leftUp;
123 | else if (-deltaX > deltaY) return Direct.leftUpA;
124 | else return Direct.leftUpB;
125 | debugger
126 | throw Error("error happend when getting direction");
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Entrance/App.scss:
--------------------------------------------------------------------------------
1 |
2 | body{
3 | margin: 0;
4 | touch-action: none;
5 | overscroll-behavior: none;
6 | }
7 | html{
8 | touch-action: none;
9 | overscroll-behavior: none;
10 | }
11 |
12 | .App{
13 | height: 100vh;
14 | width: 100vw;
15 | overflow: hidden;
16 | user-select: none;
17 | }
18 |
19 | *{
20 | font-family: 'PingFang SC';
21 | -webkit-tap-highlight-color: transparent;
22 | }
--------------------------------------------------------------------------------
/src/Entrance/App.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 | import React from 'react';
4 | import { Direct, Direction } from '../DataStructure/Direction';
5 | import { Track } from '../DataStructure/Track';
6 | import { Station } from '../DataStructure/Station';
7 | import { Point } from '../DataStructure/Point';
8 | import { EmptyLine, Line } from '../DataStructure/Line';
9 | import { Vector } from '../DataStructure/Vector';
10 |
11 | test('renders learn react link', () => {
12 | render();
13 | const linkElement = screen;
14 | const direction = new Direction(Direct.up);
15 | const station = new Station(new Point(1,2));
16 | const track = new Track(station,new Direction(Direct.up));
17 | const A = new Point(1,2);
18 | const B = new Point(3,4);
19 | const vector = new Vector(A,B);
20 | console.log(station.getTrack(vector).direction.direct)
21 | console.log(direction)
22 |
23 | expect(linkElement).toBeDefined();
24 | });
25 |
26 |
27 | test('vector cross pointer',()=>{
28 | const a = new Vector(new Point(0,0), new Point(1,1));
29 | const b = new Vector(new Point(0,5), new Point(5,0));
30 | const abCrossPoint = a.getCrossPointTo(b);
31 | console.log(abCrossPoint);
32 | expect(abCrossPoint.x).toEqual(2.5);
33 | expect(abCrossPoint.y).toEqual(2.5);
34 |
35 | })
--------------------------------------------------------------------------------
/src/Entrance/App.tsx:
--------------------------------------------------------------------------------
1 | import '../i18n/config';
2 | import { ErrorBoundary } from "react-error-boundary";
3 | import {
4 | browserInfo,
5 | mapToArr,
6 | mediateMap,
7 | readFileFromIndexedDB,
8 | setLocalStorage,
9 | } from "../Common/util";
10 | import {
11 | CardShowing,
12 | ChangeSteps,
13 | InsertInfo,
14 | LineChanges,
15 | LineProps,
16 | RecordType,
17 | StationProps,
18 | UserDataType,
19 | initData,
20 | setDataFromJson,
21 | } from "../Data/UserData";
22 | import { FunctionMode, Mode } from "../DataStructure/Mode";
23 | import { Cards } from "../Render/Card/Cards";
24 | import {
25 | DeleteConfirmation,
26 | showConfirmationInterface,
27 | } from "../Render/Delete/DeleteConfirmation";
28 | import { Menu } from "../Render/Header/Menu";
29 | import ScaleLayer from "../Render/Layer/ScaleLayer";
30 | import { WelcomeTour } from "../WelcomeTour/WelcomeTour";
31 | import "./App.scss";
32 | import "driver.js/dist/driver.css";
33 | import React, { useEffect, useRef, useState } from "react";
34 | import { ErrorFallback } from "../Render/ErrorFallback/ErrorFallback";
35 | import { Recovery } from "../Render/Recovery/Recovery";
36 | function App() {
37 | const [editingMode, setEditingMode] = useState(Mode.normal);
38 | const [functionMode, setFunctionMode] = useState(FunctionMode.normal);
39 | const [record, setRecord] = useState([]);
40 | const [currentRecordIndex, setCurrentRecordIndex] = useState(-1);
41 | const [insertInfo, setInsertInfo] = useState();
42 | const [data, setDataOriginal] = useState(initData);
43 | const [showName, setShowName] = useState(true);
44 | const [autoHiddenName, setAutoHiddenName] = useState(true);
45 | const [drawing, setDrawing] = useState(false);
46 | const [translateX, setTranslateX] = useState(0);
47 | const [translateY, setTranslateY] = useState(0);
48 | const [page, setPage] = useState("title");
49 | const [scale, setScale] = useState(1);
50 | const ref = useRef();
51 | const menuRef = useRef();
52 | const [saved, setSaved] = useState(true);
53 | const [defaultShape, setDefaultShape] = useState('cicle');
54 | const [showTour, setShowTour] = useState(() => {
55 | return (
56 | window.innerWidth >= 710 && !localStorage.getItem("skip-tour-viewed")
57 | );
58 | });
59 | const [recoveredFromError, setRecoveredFromError] = useState(false);
60 | const [showConfirmation, setShowConfirmation] =
61 | useState();
62 | // keep latest data if crash happend
63 | const setData = (data: React.SetStateAction) => {
64 | if (typeof data === "function") {
65 | setDataOriginal((state) => {
66 | const newState = data(state);
67 | setLocalStorage(newState, () => setSaved(false));
68 | return newState;
69 | });
70 | } else {
71 | setLocalStorage(data, () => setSaved(false));
72 | setDataOriginal(data);
73 | }
74 | };
75 | useEffect(() => {
76 | setShowConfirmation(() => ref.current?.showConfirmation);
77 | }, [ref.current?.showConfirmation]);
78 | const [cardShowing, setCardShowing] = useState(new CardShowing());
79 | const transfromTools = {
80 | scale,
81 | setScale,
82 | translateX,
83 | translateY,
84 | setTranslateX,
85 | setTranslateY,
86 | };
87 | const {backgroundColor} = data;
88 | useEffect(() => {
89 | document.getElementById('theme-color')!.setAttribute('content', backgroundColor?backgroundColor:"#ffffff");
90 | }, [backgroundColor])
91 | return (
92 | {
95 | const last = localStorage.getItem("last");
96 | if (last) {
97 | const data = setDataFromJson(setData, last);
98 | readFileFromIndexedDB("image")
99 | .then((file) => {
100 | setData((data) => ({
101 | ...data,
102 | // backgroundColor: "image",
103 | backgroundImage: file as File,
104 | }));
105 | })
106 | .catch((e) => {
107 | console.error(e);
108 | });
109 | mediateMap(data, transfromTools);
110 | }
111 | setRecoveredFromError(true);
112 | }}
113 | >
114 |
115 |
151 |
152 |
183 |
195 |
196 |
203 |
204 |
205 | );
206 | }
207 |
208 | export default App;
209 |
--------------------------------------------------------------------------------
/src/Entrance/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/Entrance/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/Line/Handle.ts:
--------------------------------------------------------------------------------
1 | import { Station } from "./../DataStructure/Station";
2 | import {
3 | Direct,
4 | Direction,
5 | DirectionVictorReverseY,
6 | } from "../DataStructure/Direction";
7 | import { Line } from "../DataStructure/Line";
8 | import { LineRecord } from "../DataStructure/LineRecord";
9 | import { Point } from "../DataStructure/Point";
10 | import { Vector } from "../DataStructure/Vector";
11 | import { handleLength, handleWidth } from "../Common/const";
12 |
13 | const getLPLPoints = (allKeyPoints: Point[]) => {
14 | const LQLPoints = allKeyPoints.slice();
15 | // LQLPoints.pop();
16 | // LQLPoints.shift();
17 | return LQLPoints;
18 | };
19 |
20 | const checkIfStraightTrackHasHanderOrLine = (
21 | direction: Direction,
22 | station: Station
23 | ) => {
24 | const { direct } = direction.opposite();
25 | return station.handlers[direct] || !station.tracks[direct].isEmpty();
26 | };
27 | const checkifHandeCanGoStraight = (
28 | outDirection: Direction,
29 | station: Station
30 | ) => {
31 | const ifStraightTrackHasHander = !checkIfStraightTrackHasHanderOrLine(
32 | outDirection,
33 | station
34 | );
35 | return ifStraightTrackHasHander;
36 | };
37 | const getDepartureGoStraightHandeCommand = (A: Point, B: Point) => {
38 | const BA = new Vector(B, A);
39 | const C = BA.prolong(handleLength);
40 | const BC = new Vector(B, C);
41 | const [D, E] = BC.verticalProlong(handleWidth);
42 | return `M ${D.x} ${D.y} L ${E.x} ${E.y} M ${C.x} ${C.y}`;
43 | };
44 | const addHandleForStation = (station: Station, line: Line, direct: Direct) => {
45 | station.handlers[direct] = line;
46 | };
47 | const getBestDirectionForHandle = (station: Station, direction: Direction) => {
48 | let min = Infinity,
49 | bestChoice = 0;
50 | for (let i = 0; i < station.handlers.length; i++) {
51 | const notEmpty = station.handlers[i] || !station.tracks[i].isEmpty();
52 | if (!notEmpty) {
53 | const delta = direction.opposite().delta(i);
54 | if (delta < min) {
55 | bestChoice = i;
56 | min = delta;
57 | }
58 | }
59 | }
60 | return bestChoice;
61 | };
62 | const getDepartureBestChoiceHandeCommand = (
63 | A: Point,
64 | B: Point,
65 | pathStartPoint: Point
66 | ) => {
67 | const F = pathStartPoint;
68 | const AB = new Vector(A, B);
69 | const C = AB.prolong(handleLength - 1);
70 | const BC = new Vector(B, C);
71 | const [D, E] = BC.verticalProlong(handleWidth);
72 | return `M ${D.x} ${D.y} L ${E.x} ${E.y} M ${C.x} ${C.y} L ${A.x} ${A.y} L ${F.x} ${F.y}`;
73 | };
74 | const getBestChoiceHandleCommand = (
75 | station: Station,
76 | direction: Direction,
77 | pathStartPoint: Point,
78 | line: Line
79 | ) => {
80 | const handleDirect = getBestDirectionForHandle(station, direction);
81 | if (handleDirect === undefined) {
82 | return ` M ${pathStartPoint.x} ${pathStartPoint.y}`;
83 | } else {
84 | const A = station.position;
85 | const [x, y] = DirectionVictorReverseY[handleDirect];
86 | const B = new Point(A.x + x, A.y + y);
87 | addHandleForStation(station, line!, handleDirect);
88 | return getDepartureBestChoiceHandeCommand(A, B, pathStartPoint);
89 | }
90 | };
91 | const getStartHandleCommand = (
92 | A: Point,
93 | B: Point,
94 | departureRecord: LineRecord
95 | ) => {
96 | let command = "";
97 | const outDirection = departureRecord?.getOutDirection();
98 | const { station, line } = departureRecord;
99 | const ifHandeCanGoStraight = checkifHandeCanGoStraight(
100 | outDirection!,
101 | station
102 | );
103 | if (ifHandeCanGoStraight) {
104 | command = getDepartureGoStraightHandeCommand(A, B);
105 | addHandleForStation(station, line!, outDirection!.opposite().direct);
106 | } else {
107 | command = getBestChoiceHandleCommand(station, outDirection!, A, line!);
108 | }
109 | return command;
110 | };
111 |
112 | const getTerminalGoStraightHandeCommand = (C: Point, D: Point) => {
113 | const CD = new Vector(C, D);
114 | const E = CD.prolong(handleLength);
115 | const CE = new Vector(C, E);
116 | const [F, G] = CE.verticalProlong(handleWidth);
117 | return ` L ${E.x} ${E.y} M ${F.x} ${F.y} L ${G.x} ${G.y}`;
118 | };
119 |
120 | const getTerminalBestChoiceHandeCommand = (
121 | A: Point,
122 | B: Point,
123 | pathEndPoint: Point
124 | ) => {
125 | const F = pathEndPoint;
126 | const AB = new Vector(A, B);
127 | const C = AB.prolong(handleLength - 1);
128 | const BC = new Vector(B, C);
129 | const [D, E] = BC.verticalProlong(handleWidth);
130 | return `L ${F.x} ${F.y} L ${A.x} ${A.y} L ${C.x} ${C.y} M ${D.x} ${D.y} L ${E.x} ${E.y} `;
131 | };
132 |
133 | const getBestChoiceTerminalHandleCommand = (
134 | station: Station,
135 | direction: Direction,
136 | pathStartPoint: Point,
137 | line: Line
138 | ) => {
139 | const handleDirect = getBestDirectionForHandle(station, direction);
140 | if (handleDirect === undefined) {
141 | return ` L ${pathStartPoint.x} ${pathStartPoint.y}`;
142 | } else {
143 | const A = station.position;
144 | const [x, y] = DirectionVictorReverseY[handleDirect];
145 | const B = new Point(A.x + x, A.y + y);
146 | addHandleForStation(station, line!, handleDirect);
147 | return getTerminalBestChoiceHandeCommand(A, B, pathStartPoint);
148 | }
149 | };
150 |
151 | const getEndHandleCommand = (
152 | C: Point,
153 | D: Point,
154 | terminalRecord: LineRecord
155 | ) => {
156 | let command = "";
157 | const inDirection = terminalRecord?.getInDirection();
158 | const { station, line } = terminalRecord;
159 | const ifHandeCanGoStraight = checkifHandeCanGoStraight(inDirection!, station);
160 | if (ifHandeCanGoStraight) {
161 | command = getTerminalGoStraightHandeCommand(C, D);
162 | addHandleForStation(station, line!, inDirection!.opposite().direct);
163 | } else {
164 | command = getBestChoiceTerminalHandleCommand(
165 | station,
166 | inDirection!,
167 | D,
168 | line!
169 | );
170 | }
171 | return command;
172 | };
173 | const getHandleCommand = (line: Line, allKeyPoints: Point[]) => {
174 | const [A, B] = allKeyPoints;
175 | const C = allKeyPoints[allKeyPoints.length - 2],
176 | D = allKeyPoints[allKeyPoints.length - 1];
177 | const { departureRecord, displayLine } = line;
178 | const { subLine } = displayLine!;
179 | const terminalRecord = line.getTerminalRecord();
180 | let startHandleCommand = getStartHandleCommand(A, B, departureRecord!);
181 | let endHandleCommand = ` L ${A.x} ${A.y}`; // loop line
182 | if (!(departureRecord?.station === terminalRecord?.station)) {
183 | endHandleCommand = getEndHandleCommand(C, D, terminalRecord!);
184 | }
185 | const LQLPoints = getLPLPoints(allKeyPoints);
186 | // subline no need handler in joint
187 | if (
188 | subLine &&
189 | departureRecord?.station?.lineCount &&
190 | departureRecord?.station?.lineCount() >= 2
191 | ) {
192 | startHandleCommand = ` M ${A.x} ${A.y} `;
193 | }
194 | if (
195 | subLine &&
196 | terminalRecord?.station?.lineCount &&
197 | terminalRecord?.station?.lineCount() >= 2
198 | ) {
199 | endHandleCommand = "";
200 | }
201 | return { startHandleCommand, LQLPoints, endHandleCommand };
202 | };
203 | const clearHandleFromRecord = (lineRecord: LineRecord | undefined) => {
204 | if (lineRecord) {
205 | const { station, line } = lineRecord;
206 | station.handlers.forEach((handle, index) => {
207 | if (handle === line) {
208 | station.handlers[index] = null;
209 | }
210 | });
211 | } else {
212 | console.error("no linerecord to clear handle");
213 | }
214 | };
215 | const clearHandle = (line: Line) => {
216 | const { departureRecord } = line;
217 | const terminalRecord = line.getTerminalRecord();
218 | clearHandleFromRecord(departureRecord);
219 | clearHandleFromRecord(terminalRecord);
220 | };
221 |
222 | export { getHandleCommand, clearHandle };
223 |
--------------------------------------------------------------------------------
/src/Line/LinePoints.ts:
--------------------------------------------------------------------------------
1 | import { gauge, line_radius } from "../Common/const";
2 | import { Direction } from "../DataStructure/Direction";
3 | import { Line } from "../DataStructure/Line";
4 | import { LineRecord } from "../DataStructure/LineRecord";
5 | import { Point } from "../DataStructure/Point";
6 | import { Rail } from "../DataStructure/Rail";
7 | import { Vector } from "../DataStructure/Vector";
8 |
9 | const getTurningPoint = (
10 | A: Point,
11 | B: Point,
12 | aDirection: Direction,
13 | bDirection: Direction
14 | ) => {
15 | const aVector = Vector.getVectorByPointAndDirection(A, aDirection);
16 | const bVector = Vector.getVectorByPointAndDirection(B, bDirection);
17 | const crossPoint = aVector.getCrossPointTo(bVector);
18 | return crossPoint;
19 | };
20 | const getOffsetPointFromDirectionAndRail = (
21 | point: Point,
22 | direction: Direction,
23 | rail: Rail
24 | ) => {
25 | // if(rail.track.station.displayStation!.stationName==='风起地站' && rail.line.displayLine?.lineName==='2号线')
26 | // debugger
27 | const s = Math.SQRT1_2;
28 |
29 | // lean
30 | const directionOffset = [
31 | [[-1,0],[0,0],[1,0]],// 0 up
32 | [[-s,-s],[0,0],[s,s]],// 1 upRight
33 | [[0,-1],[0,0],[0,1]],// 2 right
34 | [[s,-s],[0,0],[-s,s]],// 3 rightDown
35 | [[1,0],[0,0],[-1,0]],// 4 down
36 | [[s,s],[0,0],[-s,-s]],// 5 downLeft
37 | [[0,1],[0,0],[0,-1]],// 6 left
38 | [[-s,s],[0,0],[s,-s]],// 7 leftUp
39 | ];
40 |
41 | // const directionOffset = [
42 | // [0, -1],
43 | // [SQRT1_2, -SQRT1_2],
44 | // [1, 0],
45 | // [SQRT1_2, SQRT1_2],
46 | // [0, 1],
47 | // [-SQRT1_2, SQRT1_2],
48 | // [-1, 0],
49 | // [-SQRT1_2, -SQRT1_2],
50 | // ];
51 | // const offsetIndex = (direction.direct + rail.index - 1 + 8) % 8;
52 | const offset = directionOffset[direction.direct];
53 | const [offsetX, offsetY] = offset[rail.index].map((x) => x * gauge);
54 | return new Point(offsetX + point.x, offsetY + point.y);
55 | };
56 | const getStartOffsetPointOfStation = (lineRecord: LineRecord) => {
57 | const { station, nextRail } = lineRecord;
58 | return getOffsetPointFromDirectionAndRail(
59 | station.position,
60 | nextRail!.track.direction,
61 | nextRail!
62 | );
63 | };
64 | const getEndOffsetPointOfStation = (lineRecord: LineRecord) => {
65 | const { station, lastRail } = lineRecord;
66 | return getOffsetPointFromDirectionAndRail(
67 | station.position,
68 | lastRail!.track.direction,
69 | lastRail!
70 | );
71 | };
72 | const getInOffsetPointOfStation = (lineRecord: LineRecord) => {
73 | const { station, lastRail } = lineRecord;
74 | return getOffsetPointFromDirectionAndRail(
75 | station.position,
76 | lastRail!.track.direction,
77 | lastRail!
78 | );
79 | };
80 | const getOutOffsetPointOfStation = (lineRecord: LineRecord) => {
81 | const { station, nextRail } = lineRecord;
82 | return getOffsetPointFromDirectionAndRail(
83 | station.position,
84 | nextRail!.track.direction,
85 | nextRail!
86 | );
87 | };
88 | const getPointsBetweenStations = (lineRecord: LineRecord) => {
89 | const { nextLineRecord } = lineRecord;
90 | if (nextLineRecord) {
91 | const AOffsetPoint = getStartOffsetPointOfStation(lineRecord);
92 | const BOffsetPoint = getEndOffsetPointOfStation(nextLineRecord);
93 | if (
94 | lineRecord.getOutDirection()!.oppositeTo(nextLineRecord.getInDirection())
95 | ) {
96 | //direct to next station
97 | return [AOffsetPoint, BOffsetPoint];
98 | } else {
99 | //has turning
100 | const turningPoint = getTurningPoint(
101 | AOffsetPoint,
102 | BOffsetPoint,
103 | lineRecord.getOutDirection()!,
104 | nextLineRecord.getInDirection()!
105 | );
106 | return [AOffsetPoint, turningPoint, BOffsetPoint];
107 | }
108 | } else throw new Error("No NextLineRecord!");
109 | return [];
110 | };
111 | const getPointsInStation = (lineRecord: LineRecord) => {
112 | const { nextLineRecord, lastLineRecord } = lineRecord;
113 | if (nextLineRecord && lastLineRecord) {
114 | const AOffsetPoint = getInOffsetPointOfStation(lineRecord);
115 | const BOffsetPoint = getOutOffsetPointOfStation(lineRecord);
116 | const inDirection = lineRecord.getInDirection();
117 | const outDirection = lineRecord.getOutDirection();
118 | // same point in and out
119 | if (
120 | AOffsetPoint.sameTo(BOffsetPoint) ||
121 | inDirection?.oppositeTo(outDirection)
122 | )
123 | return [];
124 | else {
125 | // same in same out, need two points
126 | if (inDirection?.sameTo(outDirection!)) {
127 | return []; // no need points cause last and next will add end and start points
128 | // return [AOffsetPoint, BOffsetPoint];
129 | }
130 | const crossPointInStation = getTurningPoint(
131 | AOffsetPoint,
132 | BOffsetPoint,
133 | inDirection!,
134 | outDirection!
135 | );
136 | // if(crossPointInStation.x === Infinity) debugger
137 | // not same point, but cross in inPoint or outPoint
138 | if (
139 | crossPointInStation.sameTo(AOffsetPoint) ||
140 | crossPointInStation.sameTo(BOffsetPoint)
141 | )
142 | return [];
143 | return [crossPointInStation];
144 | }
145 | } else return [];
146 | };
147 | const isPointInStationInNextLine = ([A]: Point[], [B, C]: Point[]) => {
148 | if (A) {
149 | const BC = new Vector(B, C);
150 | return BC.passesThroughPoint(A);
151 | }
152 | return false;
153 | };
154 | // keypoints record all points to draw line
155 | const getAllKeyPoints = (line: Line) => {
156 | let lineRecord = line.departureRecord;
157 | let keyPoints: Point[] = [];
158 | if (!lineRecord) {
159 | console.error("No DepartureStation!");
160 | return [];
161 | }
162 | while (lineRecord.nextLineRecord) {
163 | const pointsInStation = getPointsInStation(lineRecord);
164 | // if(pointsInStation[0]&&(pointsInStation[0].x === 500 && pointsInStation[0].y === 600)) debugger;
165 | keyPoints = keyPoints.concat(pointsInStation);
166 | const pointsBetweenStations = getPointsBetweenStations(lineRecord);
167 |
168 | // cross point in next line, delete next line start point
169 | if (isPointInStationInNextLine(pointsInStation, pointsBetweenStations))
170 | pointsBetweenStations.shift();
171 | // next line start point is the same with this line's end,
172 | // delete next line start point
173 | if (
174 | keyPoints.length &&
175 | pointsBetweenStations[0].sameTo(keyPoints[keyPoints.length - 1])
176 | )
177 | pointsBetweenStations.shift();
178 | keyPoints = keyPoints.concat(pointsBetweenStations);
179 | lineRecord = lineRecord.nextLineRecord;
180 | if (lineRecord === line.departureRecord) break;
181 | }
182 | // to-do why duplicate
183 | return keyPoints.filter((p,index)=>index>0?!(p.x===keyPoints[index-1].x&&p.y===keyPoints[index-1].y):true);
184 | };
185 |
186 | const deleteDuplicatedPoints = (keyPoints: Point[]) => {
187 | if (keyPoints.length <= 2) return keyPoints; // no need delete duplicate point
188 | const start = keyPoints[0],
189 | end = keyPoints[keyPoints.length - 1];
190 | const QKeyPoints: Point[] = [start];
191 | for (let i = 1; i < keyPoints.length - 1; i++) {
192 | const A = keyPoints[i - 1];
193 | const B = keyPoints[i];
194 | const C = keyPoints[i + 1];
195 | const AC = new Vector(A, C);
196 | const AB = new Vector(A, B);
197 | const BC = new Vector(B, C);
198 | if (!AC.passesThroughPointRound(B) //|| AB.direct !== BC.direct
199 | )
200 | QKeyPoints.push(B);
201 | }
202 | QKeyPoints.push(end);
203 | return QKeyPoints;
204 | };
205 |
206 | const addLPointsAroundQPoints = (qPoints: Point[]) => {
207 | if (qPoints.length <= 2) return qPoints; // no need add L points
208 | const start = qPoints[0],
209 | end = qPoints[qPoints.length - 1];
210 | const LQLKeyPoints: Point[] = [start];
211 | for (let i = 1; i < qPoints.length - 1; i++) {
212 | const A = qPoints[i - 1];
213 | const B = qPoints[i];
214 | const C = qPoints[i + 1];
215 | const BA = new Vector(B, A);
216 | const BC = new Vector(B, C);
217 | const BAOffsetPoint = BA.normalize(line_radius).end.offset(B);
218 | const BCOffsetPoint = BC.normalize(line_radius).end.offset(B);
219 | BAOffsetPoint.q_start = true;
220 | B.q = true;
221 | BCOffsetPoint.q_end = true;
222 | LQLKeyPoints.push(BAOffsetPoint);
223 | LQLKeyPoints.push(B);
224 | LQLKeyPoints.push(BCOffsetPoint);
225 | }
226 | LQLKeyPoints.push(end);
227 | return LQLKeyPoints;
228 | };
229 | const getRoundedPoints = (keyPoints: Point[]) => {
230 | const QKeyPoints = deleteDuplicatedPoints(keyPoints);
231 | const LQLPoints = addLPointsAroundQPoints(QKeyPoints);
232 | return LQLPoints;
233 | };
234 | const generateLineCommand = (LQLPoints: Point[]) => {
235 | if (LQLPoints.length <= 1) {
236 | console.error(
237 | "no enough point to draw line. now we have " + LQLPoints.length
238 | );
239 | return "";
240 | }
241 | const start = LQLPoints[0],
242 | end = LQLPoints[LQLPoints.length - 1];
243 | const MCommand = ``;
244 | const EndCommand = ` L ${end.x} ${end.y} `;
245 | let path = "";
246 | for (let i = 1; i < LQLPoints.length - 1; i++) {
247 | const P = LQLPoints[i];
248 | switch (true) {
249 | case P.q_start:
250 | path += ` L ${P.x} ${P.y} `;
251 | break;
252 | case P.q:
253 | path += ` Q ${P.x} ${P.y} `;
254 | break;
255 | case P.q_end:
256 | path += ` , ${P.x} ${P.y} `;
257 | break;
258 | default:
259 | throw new Error("no Point flag for command!");
260 | }
261 | }
262 | return MCommand + path + EndCommand;
263 | };
264 |
265 | export { getAllKeyPoints, getRoundedPoints, generateLineCommand };
266 |
--------------------------------------------------------------------------------
/src/Render/Card/Cards.scss:
--------------------------------------------------------------------------------
1 | .cards {
2 | position: fixed;
3 | bottom: 0;
4 | // left: -65px;
5 | // zoom: 0.85
6 | width: fit-content;
7 | max-width: 100vw;
8 | height: 500px;
9 | overflow-x: scroll;
10 | overflow-y: visible;
11 | white-space: nowrap;
12 | padding-top: 200px;
13 | // padding-right: 100px;
14 | box-sizing: border-box;
15 | pointer-events: none;
16 | &::-webkit-scrollbar {
17 | display: none;
18 | // width: 0.01px;
19 | }
20 | .card-container {
21 | display: inline-block;
22 | pointer-events: auto;
23 |
24 | & > div {
25 | margin-left: 50px;
26 | position: relative;
27 | box-shadow: 0 4px 159px 7px rgba(0, 0, 0, 0.25);
28 | // filter: drop-shadow(0 0 2px #999);
29 | }
30 | &:last-child {
31 | margin-right: 50px;
32 | }
33 | }
34 |
35 | // @media (max-width: 555px) {
36 | // height: 285px;
37 | // &>div{
38 | // margin-left: 50px;
39 | // // box-shadow: none;
40 | // // border: 1px solid rgba(0, 0, 0, 0.266);
41 |
42 | // &:last-child{
43 | // margin-right: 50px;
44 | // }
45 | // zoom: 0.7;
46 | // }
47 | // }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Render/Card/Cards.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | CSSProperties,
3 | Dispatch,
4 | RefObject,
5 | SetStateAction,
6 | useEffect,
7 | useLayoutEffect,
8 | useRef,
9 | useState,
10 | } from "react";
11 | import { Line } from "../../DataStructure/Line";
12 | import { Station } from "../../DataStructure/Station";
13 | import { DisplayStation } from "../../DataStructure/Display";
14 | import { LineCard } from "./LineCard";
15 | import "./Cards.scss";
16 | import { StationCard } from "./StationCard";
17 | import { CardShowing, InsertInfo, UserDataType } from "../../Data/UserData";
18 | import { browserInfo, mapToArr, onWheelX, onWheelY } from "../../Common/util";
19 | import { showConfirmationInterface } from "../Delete/DeleteConfirmation";
20 | import { FunctionMode } from "../../DataStructure/Mode";
21 |
22 | export function Cards({
23 | data,
24 | setData,
25 | showConfirmation,
26 | menuRef,
27 | functionMode,
28 | setFunctionMode,
29 | insertInfo,
30 | setInsertInfo,
31 | cardShowing,
32 | setCardShowing,
33 | }: {
34 | data: UserDataType;
35 | setData: Dispatch>;
36 | showConfirmation?: showConfirmationInterface;
37 | menuRef: RefObject;
38 | functionMode: FunctionMode;
39 | setFunctionMode: React.Dispatch>;
40 | insertInfo?: InsertInfo;
41 | setInsertInfo: React.Dispatch>;
42 | cardShowing: CardShowing;
43 | setCardShowing: Dispatch>;
44 | }) {
45 | const { lines, stations } = data;
46 | const { engine } = browserInfo;
47 | const [pointerEvents, setPointerEvents] = useState<"auto" | "none">("none");
48 | const { lineIds, stationIds, stationFirst } = cardShowing;
49 | const linesComp = lineIds?.map((lineId) => {
50 | const line = lines.get(lineId);
51 | if (line)
52 | return (
53 |
54 |
68 |
69 | );
70 | });
71 | const stationComp = stationIds?.map((stationId) => {
72 | const station = stations.get(stationId);
73 | if (station)
74 | return (
75 |
76 |
90 |
91 | );
92 | });
93 | const [style, setStyle] = useState();
94 | const handleStyle = () => {
95 | const style: CSSProperties =
96 | engine.name === "WebKit"
97 | ? { pointerEvents: "auto", height: 370, paddingTop: 70 }
98 | : {};
99 | if (
100 | ((stationIds?.length || 0) + (lineIds?.length || 0)) * 555 <
101 | window.innerWidth
102 | ) {
103 | style.paddingRight = 100;
104 | style.pointerEvents = "none";
105 | }
106 | setStyle(style);
107 | };
108 |
109 | useEffect(() => {
110 | handleStyle();
111 | window.addEventListener("resize", handleStyle);
112 | return () => window.removeEventListener("resize", handleStyle);
113 | }, [cardShowing]);
114 | return (
115 | {
121 | const { target, currentTarget } = e;
122 | if (target === currentTarget) setPointerEvents("none");
123 | else setPointerEvents("auto");
124 | }}
125 | >
126 | {stationFirst ? stationComp : linesComp}
127 | {stationFirst ? linesComp : stationComp}
128 |
129 | );
130 | }
131 |
--------------------------------------------------------------------------------
/src/Render/Component/LineRender.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | CSSProperties,
3 | Dispatch,
4 | SetStateAction,
5 | memo,
6 | useEffect,
7 | useState,
8 | } from "react";
9 | import { Line } from "../../DataStructure/Line";
10 | import { Point } from "../../DataStructure/Point";
11 |
12 | import {
13 | generateLineCommand,
14 | getAllKeyPoints,
15 | getRoundedPoints,
16 | } from "../../Line/LinePoints";
17 | import { clearHandle, getHandleCommand } from "../../Line/Handle";
18 | import { gauge } from "../../Common/const";
19 | import {
20 | CardShowing,
21 | DrawProps,
22 | DrawerSize,
23 | UserDataType,
24 | } from "../../Data/UserData";
25 |
26 | function LineRender({
27 | line,
28 | cardShowing,
29 | setCardShowing,
30 | command,
31 | data,
32 | setData,
33 | drawing,
34 | drawerX,
35 | drawerY,
36 | }: {
37 | line: Line;
38 | cardShowing: CardShowing;
39 | setCardShowing: Dispatch>;
40 | command: string;
41 | data: UserDataType;
42 | setData: Dispatch>;
43 | } & DrawProps &
44 | DrawerSize) {
45 | const { stations } = data;
46 | const { displayLine, departureRecord } = line;
47 | const { color, lineId, subLine, lineName } = displayLine!;
48 | const { lineIds, stationIds } = cardShowing;
49 | const showing = lineIds?.length || stationIds?.length;
50 | const emphasis =
51 | lineIds?.includes(lineId) ||
52 | (stationIds &&
53 | stationIds.length === 1 &&
54 | stationIds[0] &&
55 | stations.get(stationIds[0]) &&
56 | stations.get(stationIds[0])?.lineIds?.includes(lineId));
57 |
58 | const [moved, setMoved] = useState(false);
59 | const onClick = () => {
60 | if (!moved) {
61 | setCardShowing({ lineIds: [lineId] });
62 | }
63 | };
64 | return (
65 | <>
66 |
67 |
94 |
95 | {/* {renderPoints(allKeyPoints)} */}
96 | >
97 | );
98 | }
99 |
100 | export default memo(LineRender);
101 |
--------------------------------------------------------------------------------
/src/Render/Delete/DeleteConfirmation.scss:
--------------------------------------------------------------------------------
1 | .delete-confirmation-container {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | z-index: -1000;
6 | height: 100vh;
7 | width: 100vw;
8 | background-color: rgba(255, 255, 255, 0.2);
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | opacity: 0;
13 | transition:
14 | 0.3s ease-in-out opacity,
15 | 0.3s ease-in-out z-index;
16 | &.before-animated,
17 | &.before-disappear {
18 | backdrop-filter: blur(1px);
19 | }
20 | &.animated {
21 | opacity: 1;
22 | backdrop-filter: blur(10px);
23 | z-index: 1000;
24 | }
25 | .delete-confirmation {
26 | text-align: center;
27 |
28 | .title {
29 | font-weight: 500;
30 | font-size: 36px;
31 | padding-left: 13px;
32 | }
33 | .sub-title {
34 | font-weight: 500;
35 | font-size: 18px;
36 | }
37 | .preview {
38 | margin-top: 74.2px;
39 | display: inline-block;
40 | // border-top: 1px solid black;
41 | width: fit-content;
42 | // padding: 0 40px;
43 | text-align: center;
44 | position: relative;
45 | // display: flex;
46 | // align-items: center;
47 | // justify-content: center;
48 | // &>div{
49 | // transform: translateY(-50%);
50 | // }
51 | .preview-content {
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | .icon {
56 | display: inline-block;
57 | margin-left: 40px;
58 |
59 | .line {
60 | // width: 35.5px;
61 | margin-right: 35.5px;
62 |
63 | .sign-input {
64 | // width: 35.5px;
65 | * {
66 | min-width: 35.5px;
67 | height: 35.5px;
68 | border: none;
69 | border-radius: 8.87px;
70 | background-color: #ea0b2a;
71 | font-size: 23.25px;
72 | color: white;
73 | text-align: center;
74 | }
75 | }
76 | }
77 | .station {
78 | display: flex;
79 | justify-content: center;
80 | align-items: center;
81 | margin-right: 25.8px;
82 | svg {
83 | &.square {
84 | zoom: 0.9;
85 | }
86 | &.triangle {
87 | zoom: 1.05;
88 | }
89 | &.start {
90 | zoom: 1.15;
91 | }
92 | &.hexagon {
93 | zoom: 0.9;
94 | }
95 | &.pentagon {
96 | zoom: 1.1;
97 | }
98 | &.diamond {
99 | zoom: 1.15;
100 | }
101 | &.leaf {
102 | zoom: 0.9;
103 | }
104 | &.ginkgo {
105 | zoom: 0.8;
106 | }
107 | }
108 | }
109 | }
110 | .text {
111 | margin-right: 40px;
112 | display: inline-block;
113 | font-weight: 500;
114 | font-size: 18px;
115 |
116 | line-height: 35.5px;
117 | // vertical-align: middle;
118 | }
119 | }
120 |
121 | .delete-line {
122 | border-top: 1px solid #ea0b2a;
123 | position: absolute;
124 | top: 50%;
125 | width: 100%;
126 | transition: 0.3s ease-in-out;
127 | // animation-delay: 5s;
128 | transition-delay: 0.3s;
129 | }
130 | }
131 |
132 | .delete {
133 | font-weight: 500;
134 | font-size: 18px;
135 | margin-top: 104.8px;
136 | cursor: pointer;
137 | color: #ea0b2a;
138 | }
139 | .back {
140 | margin-top: 16.12px;
141 | font-weight: 500;
142 | font-size: 18px;
143 | cursor: pointer;
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/Render/Delete/DeleteConfirmation.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | Dispatch,
3 | SetStateAction,
4 | forwardRef,
5 | useEffect,
6 | useImperativeHandle,
7 | useState,
8 | } from "react";
9 | import { LineProps, StationProps } from "../../Data/UserData";
10 | import "./DeleteConfirmation.scss";
11 | import { AutoGrowthInput } from "../../Common/AutoGrowthInput";
12 | import shapes from "../../Resource/Shape/shape";
13 | import classNames from "classnames";
14 | import { useTranslation } from "react-i18next";
15 | import i18n from "../../i18n/config";
16 | export interface showConfirmationInterface {
17 | (
18 | {
19 | line,
20 | station,
21 | stationIndex,
22 | }: { line?: LineProps; station?: StationProps; stationIndex?: number },
23 | callback?: any
24 | ): void;
25 | }
26 | enum ShowMode {
27 | none,
28 | beforeAnimate,
29 | animated,
30 | beforeDisappear,
31 | }
32 | export const DeleteConfirmation = forwardRef(function (
33 | {}: any,
34 | ref: React.Ref | undefined
35 | ) {
36 | const [show, setShow] = useState(ShowMode.none);
37 | // const [title, setTitle] = useState();
38 | // const [subTitle, setSubTitle] = useState();
39 | const [line, setLine] = useState();
40 | const [station, setStation] = useState();
41 | const [stationIndex, setStationIndex] = useState();
42 | const [callback, setCallback] = useState(() => () => {});
43 | const {t} = useTranslation();
44 | //@ts-ignore
45 | window.setShow = setShow
46 | const showWithAnimate = () => {
47 | setShow(ShowMode.beforeAnimate);
48 | setTimeout(() => setShow(ShowMode.animated));
49 | };
50 | const disappearWithAnimate = () => {
51 | setShow(ShowMode.beforeDisappear);
52 | setTimeout(() => setShow(ShowMode.none),300);
53 | };
54 | const showConfirmation: showConfirmationInterface = (
55 | { line, station, stationIndex },
56 | callback
57 | ) => {
58 | showWithAnimate();
59 | setLine(line);
60 | setStation(station);
61 | setCallback(() => callback);
62 | setStationIndex(stationIndex);
63 | };
64 | useImperativeHandle(
65 | ref,
66 | () => {
67 | return {
68 | showConfirmation,
69 | };
70 | },
71 | []
72 | );
73 | const remove = line && station;
74 | const deleteText = remove ? t('delete.remove') : t('delete.delete');
75 | const title = t('que-shi-yao-deletetext-ma', {deleteText});
76 | const { stationName, shape } = station || {};
77 | const { lineName, sign, color } = line || {};
78 | const index = stationIndex ? stationIndex + 1 : 1;
79 | const getOrdinalSuffix = (number: number)=> {
80 | const suffixes = ["th", "st", "nd", "rd"];
81 | const v = number % 100;
82 | return number + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]);
83 | }
84 | const subTitle = t('delete.subtile', {lineName, index:i18n.language === 'en-US'? getOrdinalSuffix(index):index});
85 |
86 | return (
87 |
97 |
98 |
{title}
99 | {remove ?
{subTitle}
: <>>}
100 |
101 |
102 |
103 | {line && !station ? (
104 |
112 | ) : (
113 |
114 | {
115 | //@ts-ignore
116 | shapes[shape]
117 | }
118 |
119 | )}
120 |
121 |
{stationName || lineName}
122 |
123 |
127 |
128 |
{
131 | disappearWithAnimate();
132 | if (typeof callback === "function") callback();
133 | }}
134 | >
135 | {deleteText}
136 |
137 |
138 | {t('delete.cancel')}
139 |
140 |
141 |
142 | );
143 | });
144 |
--------------------------------------------------------------------------------
/src/Render/ErrorFallback/ErrorFallback.scss:
--------------------------------------------------------------------------------
1 | .error-layer {
2 | text-align: center;
3 | .title {
4 | margin-top: 20vh;
5 | font-size: 36px;
6 | font-weight: 1000;
7 | }
8 | .sub-title {
9 | margin-top: 0.5vh;
10 | font-size: 18px;
11 | font-weight: 500;
12 | }
13 | .error-btn {
14 | span{
15 | margin-left: 55px;
16 | text-align: left;
17 | }
18 | user-select: none;
19 | font-size: 18px;
20 | font-weight: 500;
21 | cursor: pointer;
22 | --gap: min(20px, 2vw);
23 | position: relative;
24 | svg{
25 | position: absolute;
26 | left: calc(var(--gap) + 20px);
27 | }
28 | &.recover-from-cache {
29 | margin-top: 12vh;
30 | background-color: rgb(234, 11, 42, 0.09);
31 | color: #ea0b2a;
32 | svg {
33 | width: 20px;
34 |
35 | }
36 | }
37 | &.export-from-cache {
38 | margin-top: 0.5vh;
39 | background-color: #D9D9D9;
40 | svg {
41 | width: 16px;
42 | padding: 0 2px;
43 |
44 | }
45 | }
46 | padding: 20px 30px;
47 | width: 315px;
48 | margin: auto;
49 | border-radius: 10px;
50 | display: flex;
51 | justify-content: left;
52 | align-items: center;
53 |
54 | }
55 | .export-error{
56 | position: absolute;
57 | bottom: 10vh;
58 | margin: auto;
59 | left: 0;
60 | right: 0;
61 | font-size: 18px;
62 | font-weight: 500;
63 | .export-error-file{
64 | color: #ea0b2a;
65 | cursor: pointer;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Render/ErrorFallback/ErrorFallback.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./ErrorFallback.scss";
3 | import { ReactComponent as RecoverIcon } from "../../Resource/Icon/clock.arrow.circlepath.svg";
4 | import { ReactComponent as ExportIcon } from "../../Resource/Icon/export.svg";
5 | import { exportFile, exportJson } from "../../Common/util";
6 | import moment from "moment";
7 | import { useTranslation } from "react-i18next";
8 |
9 | export const ErrorFallback = ({
10 | error,
11 | resetErrorBoundary,
12 | }: {
13 | error: Error;
14 | resetErrorBoundary: () => void;
15 | }) => {
16 | const exportFile = (key: string) => {
17 | const current = localStorage.getItem(key);
18 | exportJson(
19 | current!,
20 | `recovery-${key}-${moment().format("YYYY-MM-DD_HH-mm-ss")}.json`
21 | );
22 | };
23 | const { t } = useTranslation();
24 | return (
25 |
26 |
{t('error.metError')}
27 |
{t('error.dontWorry')}
28 |
29 |
30 | {t('error.recoverFromCache')}
31 |
32 |
exportFile("last")}
35 | >
36 |
37 | {t('error.exportNoErrorFile')}
38 |
39 |
40 |
41 | {t('error.orYouCould')}
42 | exportFile("current")}
45 | >
46 | {t('error.exportError')}
47 |
48 |
49 |
{t('error.sendAuthor')}
50 |
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/Render/Header/Component/OpacityControl.scss:
--------------------------------------------------------------------------------
1 | .tool {
2 | position: relative;
3 | display: inline-block;
4 | cursor: pointer;
5 | perspective: 1000px;
6 | perspective-origin: 0 50%;
7 |
8 | &:has(.slider-container) {
9 | z-index: 600;
10 | }
11 |
12 | .slider-container {
13 | transition:
14 | transform 0s,
15 | transform 0.3s ease-in-out,
16 | all 0.3s ease-in-out;
17 | &.show {
18 | opacity: 1;
19 | pointer-events: unset;
20 | top: 100%;
21 | transform: scale(calc(1 + var(--value) * 0.075)) rotate3d(0, 1, 0, 0deg);
22 | }
23 | opacity: 0;
24 | pointer-events: none;
25 | position: absolute;
26 | top: 50%;
27 | left: 0;
28 | margin-top: 8px;
29 | background: rgba(255, 255, 255, 0.136);
30 | box-shadow: 0 0 100px rgba(#000000, 0.15);
31 | // border: #ea0b2a 1px solid;
32 | z-index: 600;
33 | width: 400%;
34 | max-width: 100vw;
35 | overflow: hidden;
36 | height: 40px;
37 | border-radius: 5px;
38 | backdrop-filter: blur(20px);
39 | transform: scale(calc(1 + var(--value) * 0.075)) rotate3d(0, 1, 0, var(--deg));
40 | transform-origin: 0 0;
41 | .opacity-text {
42 | position: absolute;
43 | top: 0;
44 | bottom: 0;
45 | margin: auto;
46 | pointer-events: none;
47 | display: flex;
48 | justify-content: center;
49 | align-items: center;
50 | margin-left: 20px;
51 | font-size: 14px;
52 | opacity: var(--value);
53 | span {
54 | margin-left: 2px;
55 | font-size: 10px;
56 | font-weight: 600;
57 | vertical-align: baseline;
58 | margin-top: 2.5px;
59 | }
60 | }
61 | .slider {
62 | appearance: none;
63 | width: 100%;
64 | height: 100%;
65 | appearance: none;
66 | margin: 0;
67 | background-color: transparent;
68 | background-image: repeating-linear-gradient(
69 | to right,
70 | transparent,
71 | transparent calc(18% - 1px),
72 | #05051a1c 18%
73 | );
74 | &::-webkit-slider-thumb {
75 | box-shadow: -20rem 0 0 20rem rgba(#ea0b2a, 0.2);
76 | cursor: col-resize;
77 | background-color: rgba(#ea0b2a, 0.2);
78 | }
79 |
80 | @supports not (-webkit-touch-callout: none) {
81 | &::-webkit-slider-thumb {
82 | -webkit-appearance: none;
83 | appearance: none;
84 | width: 0;
85 | }
86 | }
87 | @supports (-webkit-min-device-pixel-ratio: 0) {
88 | &::-webkit-slider-thumb {
89 | -webkit-appearance: auto;
90 | appearance: auto;
91 | width: auto;
92 | }
93 | }
94 |
95 |
96 | &::-webkit-slider-runnable-track {
97 | // pointer-events: none;
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Render/Header/Component/OpacityControl.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useState,
3 | useRef,
4 | useEffect,
5 | CSSProperties,
6 | useLayoutEffect,
7 | } from "react";
8 | import "./OpacityControl.scss";
9 | import classNames from "classnames";
10 | import { browserInfo } from "../../../Common/util";
11 | import { useTranslation } from "react-i18next";
12 | interface OpacityControlProps {
13 | opacity: number;
14 | setOpacity: (value: number) => void;
15 | }
16 |
17 |
18 | const OpacityControl: React.FC = ({
19 | opacity,
20 | setOpacity,
21 | }) => {
22 | const [showSlider, setShowSlider] = useState(false);
23 | const sliderRef = useRef(null);
24 | const inputRef = useRef(null);
25 | const toolRef = useRef(null);
26 | const { t } = useTranslation();
27 | const handleClick = () => {
28 | setShowSlider(!showSlider);
29 | };
30 |
31 | const handleSliderChange = (event: React.ChangeEvent) => {
32 | setOpacity(Number(event.target.value));
33 | };
34 |
35 | useEffect(() => {
36 | if (showSlider && sliderRef.current && toolRef.current) {
37 | const toolRect =
38 | toolRef.current.getBoundingClientRect();
39 | const viewportWidth = window.innerWidth;
40 |
41 | if (toolRect.left + 230 > viewportWidth) {
42 | sliderRef.current.style.left = "unset";
43 | sliderRef.current.style.right = "0";
44 | (sliderRef.current.style as any)['--deg'] = "-25deg";
45 | sliderRef.current.style.transformOrigin = "100% 50%";
46 | toolRef.current.style.perspectiveOrigin = "100% 50%";
47 | } else {
48 | sliderRef.current.style.left = "0";
49 | sliderRef.current.style.transformOrigin = "0 50%";
50 | toolRef.current.style.perspectiveOrigin = "0 50%";
51 | (sliderRef.current.style as any)['--deg'] = "25deg";
52 | }
53 | }
54 | }, [showSlider]);
55 | useLayoutEffect(() => {
56 | if (sliderRef.current) {
57 | sliderRef.current.style.transition = "0.3s ease-in-out";
58 | }
59 | }, [showSlider]);
60 | useEffect(() => {
61 | const handleClickCapture = (event: TouchEvent | MouseEvent) => {
62 | if (event.target !== inputRef.current && event.target !== toolRef.current) {
63 | console.log("focusout");
64 | setShowSlider(false);
65 | }
66 | };
67 | const resize = () => setShowSlider(false);
68 | document.addEventListener("touchstart", handleClickCapture, true);
69 | document.addEventListener("click", handleClickCapture, true);
70 | document.addEventListener("resize",resize);
71 | return () => {
72 | document.removeEventListener("touchstart", handleClickCapture, true);
73 | document.addEventListener("click", handleClickCapture, true);
74 | document.removeEventListener("resize",resize);
75 | };
76 | }, []);
77 | const { engine } = browserInfo;
78 | const safari = engine.name === "WebKit";
79 | return (
80 |
81 | {t('opacity')}
82 | {
83 |
e.stopPropagation()}
87 | style={{ "--value": opacity } as CSSProperties}
88 | onTransitionEnd={() => {
89 | if (showSlider && sliderRef.current) {
90 | sliderRef.current.style.transition = "none";
91 | }
92 | }}
93 | >
94 |
98 | {Math.floor(opacity * 100)}
99 | %
100 |
101 | e.stopPropagation()}
111 | style={{ "--value": opacity , "--safari": safari} as CSSProperties}
112 | onBlur={() => setShowSlider(false)}
113 | />
114 |
115 | }
116 |
117 | );
118 | };
119 |
120 | export default OpacityControl;
121 |
--------------------------------------------------------------------------------
/src/Render/Header/Component/ShapeSelector.scss:
--------------------------------------------------------------------------------
1 | .tool {
2 | position: relative;
3 | display: inline-block;
4 | cursor: pointer;
5 | perspective: 1000px;
6 | perspective-origin: 0 50%;
7 |
8 | &:has(.shape-selector-container) {
9 | z-index: 600;
10 | }
11 |
12 | .shape-selector-container {
13 | .color-detail {
14 | .color-detail-choosing {
15 | box-sizing: border-box;
16 | padding: 10px 10px;
17 | width: 222.75px;
18 | display: grid;
19 | border: white 1px solid;
20 | grid-template-columns: repeat(4, 25%);
21 | grid-template-rows: repeat(3, 33.33%);
22 | // grid-row-gap: 9px;
23 | // grid-column-gap: 6px;
24 | .shape-container {
25 | height: 46.13px;
26 | display: flex;
27 | justify-content: center;
28 | align-items: center;
29 | border-left: 1px black dotted;
30 | border-bottom: 1px black solid;
31 | transform: translate(-1px, 1px);
32 | cursor: pointer;
33 |
34 | &.left {
35 | border-left: none;
36 | }
37 | &.bottom {
38 | border-bottom: none;
39 | }
40 | .shape-preview {
41 | width: 19.5px;
42 | height: 19.5px;
43 | // border-radius: 50%;
44 | // border: 2px solid;
45 | display: flex;
46 | justify-content: center;
47 | align-items: center;
48 | &.shape-selected {
49 | svg {
50 | * {
51 | stroke: #ea0b2a;
52 | }
53 | }
54 | }
55 | &.square {
56 | zoom: 0.9;
57 | }
58 | &.triangle {
59 | zoom: 1.05;
60 | }
61 | &.start {
62 | zoom: 1.15;
63 | }
64 | &.hexagon {
65 | zoom: 0.9;
66 | }
67 | &.pentagon {
68 | zoom: 1.1;
69 | }
70 | &.diamond{
71 | zoom: 1.15;
72 | }
73 | &.leaf{
74 | zoom: 0.9;
75 | }
76 | }
77 | }
78 | }
79 | }
80 | transition:
81 | transform 0s,
82 | transform 0.3s ease-in-out,
83 | all 0.3s ease-in-out;
84 | &.show {
85 | opacity: 1;
86 | pointer-events: unset;
87 | top: 100%;
88 | transform: scale(calc(1 + var(--value) * 0.075)) rotate3d(0, 1, 0, 0deg);
89 | }
90 | opacity: 0;
91 | pointer-events: none;
92 | position: absolute;
93 | top: 50%;
94 | left: 0;
95 | margin-top: 8px;
96 | background: rgba(255, 255, 255, 0.136);
97 | box-shadow: 0 0 100px rgba(#000000, 0.15);
98 | // border: #ea0b2a 1px solid;
99 | z-index: 600;
100 | width: 222.75px;
101 | max-width: 100vw;
102 | overflow: hidden;
103 | height: 162px;
104 | border-radius: 5px;
105 | backdrop-filter: blur(20px);
106 | transform: scale(calc(1 + var(--value) * 0.075)) rotate3d(0, 1, 0, var(--deg));
107 | transform-origin: 0 0;
108 | .opacity-text {
109 | position: absolute;
110 | top: 0;
111 | bottom: 0;
112 | margin: auto;
113 | pointer-events: none;
114 | display: flex;
115 | justify-content: center;
116 | align-items: center;
117 | margin-left: 20px;
118 | font-size: 14px;
119 | opacity: var(--value);
120 | span {
121 | margin-left: 2px;
122 | font-size: 10px;
123 | font-weight: 600;
124 | vertical-align: baseline;
125 | margin-top: 2.5px;
126 | }
127 | }
128 | .slider {
129 | appearance: none;
130 | width: 100%;
131 | height: 100%;
132 | appearance: none;
133 | margin: 0;
134 | background-color: transparent;
135 | background-image: repeating-linear-gradient(
136 | to right,
137 | transparent,
138 | transparent calc(18% - 1px),
139 | #05051a1c 18%
140 | );
141 | &::-webkit-slider-thumb {
142 | box-shadow: -20rem 0 0 20rem rgba(#ea0b2a, 0.2);
143 | cursor: col-resize;
144 | background-color: rgba(#ea0b2a, 0.2);
145 | }
146 |
147 | @supports not (-webkit-touch-callout: none) {
148 | &::-webkit-slider-thumb {
149 | -webkit-appearance: none;
150 | appearance: none;
151 | width: 0;
152 | }
153 | }
154 | @supports (-webkit-min-device-pixel-ratio: 0) {
155 | &::-webkit-slider-thumb {
156 | -webkit-appearance: auto;
157 | appearance: auto;
158 | width: auto;
159 | }
160 | }
161 |
162 |
163 | &::-webkit-slider-runnable-track {
164 | // pointer-events: none;
165 | }
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/Render/Header/Component/ShapeSelector.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useState,
3 | useRef,
4 | useEffect,
5 | CSSProperties,
6 | useLayoutEffect,
7 | SetStateAction,
8 | Dispatch,
9 | } from "react";
10 | import "./ShapeSelector.scss";
11 | import classNames from "classnames";
12 | import { browserInfo } from "../../../Common/util";
13 | import shapes from "../../../Resource/Shape/shape";
14 | import { Shape } from "../../../Data/Shape";
15 | import { useTranslation } from "react-i18next";
16 | interface OpacityControlProps {
17 | defaultShape: string;
18 | setDefaultShape: Dispatch>;
19 | }
20 |
21 | const ShapeSelector: React.FC = ({
22 | defaultShape,
23 | setDefaultShape,
24 | }) => {
25 | const [showSlider, setShowSlider] = useState(false);
26 | const sliderRef = useRef(null);
27 | const toolRef = useRef(null);
28 | const { t } = useTranslation();
29 | const handleClick = () => {
30 | setShowSlider(!showSlider);
31 | };
32 |
33 | useEffect(() => {
34 | if (showSlider && sliderRef.current && toolRef.current) {
35 | const toolRect = toolRef.current.getBoundingClientRect();
36 | const viewportWidth = window.innerWidth;
37 |
38 | if (toolRect.left + 230 > viewportWidth) {
39 | sliderRef.current.style.left = "unset";
40 | sliderRef.current.style.right = "0";
41 | (sliderRef.current.style as any)["--deg"] = "-25deg";
42 | sliderRef.current.style.transformOrigin = "100% 50%";
43 | toolRef.current.style.perspectiveOrigin = "100% 50%";
44 | } else {
45 | sliderRef.current.style.left = "0";
46 | sliderRef.current.style.transformOrigin = "0 50%";
47 | toolRef.current.style.perspectiveOrigin = "0 50%";
48 | (sliderRef.current.style as any)["--deg"] = "25deg";
49 | }
50 | }
51 | }, [showSlider]);
52 | useLayoutEffect(() => {
53 | if (sliderRef.current) {
54 | sliderRef.current.style.transition = "0.3s ease-in-out";
55 | }
56 | }, [showSlider]);
57 | useEffect(() => {
58 | const handleClickCapture = (event: TouchEvent | MouseEvent) => {
59 | if (!toolRef.current?.contains(event.target as Node)) {
60 | console.log("focusout");
61 | setShowSlider(false);
62 | }
63 | };
64 | const resize = () => setShowSlider(false);
65 | document.addEventListener("touchstart", handleClickCapture, true);
66 | document.addEventListener("click", handleClickCapture, true);
67 | document.addEventListener("resize", resize);
68 | return () => {
69 | document.removeEventListener("touchstart", handleClickCapture, true);
70 | document.addEventListener("click", handleClickCapture, true);
71 | document.removeEventListener("resize", resize);
72 | };
73 | }, []);
74 | const { engine } = browserInfo;
75 | const safari = engine.name === "WebKit";
76 | const column = 4;
77 | const row = 3;
78 | const grid = new Array(column * row).fill(0);
79 | Object.keys(shapes).forEach((shape, index) => (grid[index] = shape));
80 | return (
81 |
82 | {
83 | t(`shape.${defaultShape}`)
84 | }
85 | {
86 |
e.stopPropagation()}
90 | style={{} as CSSProperties}
91 | onTransitionEnd={() => {
92 | if (showSlider && sliderRef.current) {
93 | sliderRef.current.style.transition = "none";
94 | }
95 | }}
96 | >
97 |
98 |
99 | {grid.map((shape, index) => {
100 | const left = index % column === 0;
101 | const bottom = Math.floor(index / column) === row - 1;
102 | // console.log({ shape });
103 | return (
104 |
{
111 | if (shape) setDefaultShape(shape);
112 | }}
113 | >
114 |
121 | {
122 | //@ts-ignore
123 | shapes[shape]
124 | }
125 |
126 |
127 | );
128 | })}
129 |
130 |
131 |
132 | }
133 |
134 | );
135 | };
136 |
137 | export default ShapeSelector;
138 |
--------------------------------------------------------------------------------
/src/Render/Header/Menu.scss:
--------------------------------------------------------------------------------
1 | .menu {
2 | &.page-menu {
3 | position: fixed;
4 | top: 0;
5 | left: 0;
6 | height: 100vh;
7 | width: 100vw;
8 | z-index: 500;
9 | background-color: rgba(255, 255, 255, 0.2);
10 | backdrop-filter: blur(10px);
11 | .title {
12 | .tools {
13 | margin-left: -50px;
14 | .tool {
15 | margin-left: -5px;
16 | opacity: 0;
17 | }
18 | }
19 | }
20 | .dots {
21 | width: calc(100vw - 51.6px - 51.6px);
22 | transition:
23 | 0.3s ease-in-out opacity,
24 | 0.3s ease-in-out width;
25 | opacity: 1;
26 | }
27 | .menus {
28 | transition: 0.3s ease-in-out opacity;
29 | opacity: 1;
30 | .columns {
31 | .column {
32 | @media (max-width: 710px) {
33 | margin-top: 28.5px;
34 | opacity: 1;
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
41 | &.page-title {
42 | // position: fixed;
43 | // top: 0;
44 | // left: 0;
45 | // height: 100vh;
46 | // width: 100vw;
47 | .title {
48 | .click-panel {
49 | cursor: pointer;
50 | }
51 | .tools {
52 | margin-left: -50px;
53 | .tool {
54 | margin-left: -5px;
55 | opacity: 0;
56 | }
57 | }
58 | }
59 | user-select: none;
60 | z-index: -500;
61 | .dots {
62 | margin-top: 100.66px;
63 | position: fixed;
64 | opacity: 1;
65 | width: 0;
66 | transition:
67 | 0.3s ease-in-out opacity,
68 | 0.3s ease-in-out width;
69 | }
70 |
71 | .menus {
72 | margin-top: 101.66px;
73 | position: fixed;
74 | opacity: 0;
75 | transition: 0.3s ease-in-out opacity;
76 |
77 | .columns {
78 | justify-content: space-between;
79 | transition: 0.3s ease-in-out width;
80 | width: calc(70vw - 51.6px - 51.6px);
81 | }
82 | }
83 | }
84 |
85 | &.page-tools {
86 | .title {
87 | }
88 | .dots {
89 | margin-top: 100.66px;
90 | position: fixed;
91 | opacity: 1;
92 | width: 0;
93 | transition:
94 | 0.3s ease-in-out opacity,
95 | 0.3s ease-in-out width;
96 | }
97 | .menus {
98 | margin-top: 101.66px;
99 | position: fixed;
100 | opacity: 0;
101 | transition: 0.3s ease-in-out opacity;
102 |
103 | .columns {
104 | justify-content: space-between;
105 | transition: 0.3s ease-in-out width;
106 | width: calc(70vw - 51.6px - 51.6px);
107 | }
108 | }
109 | }
110 | .title {
111 | position: fixed;
112 | margin: 35.5px 0 0 51.6px;
113 | z-index: 500;
114 | .auto-growth-container {
115 | display: inline-block;
116 | * {
117 | font-size: 36px;
118 | font-weight: 1000;
119 | }
120 | input {
121 | // background-color: rgba(255, 255, 255, 0.901);
122 | // backdrop-filter: blur(10px);
123 | // padding-left: 18px;
124 | // margin-left: -18px;
125 | }
126 | }
127 | .tools {
128 | @media (max-width: 710px) {
129 | // display: block !important;
130 | margin-left: -21.77px;
131 | .tool {
132 | margin-top: 5px;
133 |
134 | &:first-child {
135 | // display: none;
136 | }
137 | }
138 | }
139 | display: inline-block;
140 | transition: 0.3s ease-in-out;
141 | .tool {
142 | transition: 0.3s ease-in-out;
143 | opacity: 1;
144 | display: inline-block;
145 | font-size: 18px;
146 | font-weight: 500;
147 | color: #ea0b2a;
148 | &.tool-title{
149 | color: #00A0E8;
150 | cursor:auto;
151 | }
152 | margin-left: 21.77px;
153 | cursor: pointer;
154 | &.disabled {
155 | color: #ea0b2a55;
156 | cursor:auto;
157 | }
158 | svg{
159 | vertical-align: text-bottom;
160 | line-height: 26px;
161 | height: 26px;
162 | [fill="black"]{
163 | fill: rgba(234, 11, 42, 0.3333333333) !important;
164 | fill-opacity: 1 !important;
165 | }
166 | }
167 |
168 |
169 | }
170 | }
171 | }
172 |
173 | .dots {
174 | // margin-top: 20.16px;
175 | border-bottom: dotted black 1px;
176 | margin: 100.66px 51.6px 0 51.6px;
177 | }
178 | .menus {
179 | height: calc(100vh - 101px);
180 | overflow-y: overlay;
181 | overflow-x: hidden;
182 | &::-webkit-scrollbar {
183 | display: none;
184 | }
185 | .columns {
186 | margin: 0 51.6px 0 51.6px;
187 | display: flex;
188 | @media (max-width: 710px) {
189 | display: grid;
190 | }
191 | justify-content: space-between;
192 | transition: 0.3s ease-in-out width;
193 | width: calc(100vw - 51.6px - 51.6px);
194 | .column {
195 | margin-top: 25.8px;
196 | display: inline-block;
197 | vertical-align: top;
198 | width: 20vw;
199 | transition: 0.3s ease-in-out;
200 | @media (max-width: 710px) {
201 | width: auto;
202 | margin-top: -15px;
203 | opacity: 0;
204 | &:last-child{
205 | padding-bottom: 150px;
206 | }
207 | }
208 | .column-title {
209 | font-size: 36px;
210 | font-weight: 300;
211 | }
212 | .column-items {
213 | // margin-top: 17px;
214 |
215 | .column-item {
216 | width: fit-content;
217 | margin-top: 17px;
218 | font-size: 18px;
219 | font-weight: 500;
220 | color: #ea0b2a;
221 | cursor: pointer;
222 | &.sub-menu{
223 | margin-top: 10px;
224 | margin-left: 10px;
225 | .sub-item{
226 | margin-right: 10px;
227 | }
228 | }
229 | &.small{
230 | font-size: small;
231 | }
232 | &.author{
233 | margin-top: 4px;
234 | &:hover{
235 | // text-decoration: underline;
236 | }
237 | }
238 | &.friend{
239 | position: relative;
240 | svg{
241 | margin-left: 5px;
242 | position: absolute;
243 | margin-top: 3px;
244 | width: 17px;
245 | // height: 25px;
246 | // vertical-align: bottom;
247 | }
248 | }
249 | }
250 | }
251 | }
252 | }
253 | .notice{
254 | position: fixed;
255 | left: 0;
256 | bottom: 0;
257 | }
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/src/Render/Layer/DevelopLayer.scss:
--------------------------------------------------------------------------------
1 | .DevelopLayer {
2 | .grid {
3 | display: grid;
4 | grid-template-columns: repeat(100, 100px);
5 | grid-template-rows: repeat(50, 100px);
6 | .grid-item {
7 | border: 1px solid black;
8 | height: 100px;
9 | width: 100px;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Render/Layer/DevelopLayer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./DevelopLayer.scss";
3 | import { Point } from "../../DataStructure/Point";
4 | import { Station } from "../../DataStructure/Station";
5 | import { Line } from "../../DataStructure/Line";
6 | import LineRender from "../Component/LineRender";
7 | import { CardShowing } from "../../Data/UserData";
8 |
9 | function DevelopLayer() {
10 | // test line and station
11 |
12 | const pointA = new Point(200, 200);
13 | const pointB = new Point(300, 300);
14 | const pointC = new Point(600, 300);
15 | const pointD = new Point(800, 300);
16 | const pointE = new Point(200, 400);
17 | const pointF = new Point(800, 200);
18 | const pointG = new Point(500, 500);
19 |
20 | const A = new Station(pointA);
21 | const B = new Station(pointB);
22 | B._dev_tag = "B";
23 | const C = new Station(pointC);
24 | const D = new Station(pointD);
25 | const E = new Station(pointE);
26 | const F = new Station(pointF);
27 | const G = new Station(pointG);
28 |
29 | const line1 = new Line();
30 | const line3 = new Line();
31 | const line8 = new Line();
32 | const line9 = new Line();
33 |
34 | line1.linkAll([A, B, C, D]);
35 | // line3.linkAll([E, B, C, D, F, E]);
36 | line8._dev_tag='line8';
37 | line8.linkAll([E, B, C, D]);
38 | line9.linkAll([A, E, G,D,F]);
39 |
40 | // console.log(line1, line3, line8);
41 | console.log(A, B, C, D, E, F);
42 |
43 |
44 | const allStationsList = [A, B, C, D, E, F, G];
45 | const allLinesList = [line1, line3, line8, line9];
46 |
47 | const renderStations = (allStationsList: Station[]) => {
48 | return (
49 |
50 | {allStationsList.map((station, index) => (
51 |
58 | {String.fromCharCode("A".charCodeAt(0) + index)}
59 |
60 | ))}
61 |
62 | );
63 | };
64 |
65 | // const renderLines = (allLinesList: Line[], cardShowing: CardShowing, setCardShowing: Dispatch>) => {
66 | // return (
67 | //
68 | // {allLinesList.map((line) => {
69 | // return ;
71 | // })}
72 | //
73 | // );
74 | // };
75 |
76 |
77 | return (
78 |
79 | {/*
80 | {new Array(100 * 50).fill(1).map((x, index) => (
81 |
{index}
82 | ))}
83 |
*/}
84 | {renderStations(allStationsList)}
85 | {/* {renderLines(allLinesList)} */}
86 |
87 |
88 | );
89 | }
90 |
91 | export default DevelopLayer;
92 |
--------------------------------------------------------------------------------
/src/Render/Layer/RenderLayer.scss:
--------------------------------------------------------------------------------
1 | .station-render {
2 | cursor: pointer;
3 | .station-shape {
4 | height: 30px;
5 | width: 30px;
6 | // transform: translate(-50%,-50%);
7 | display: inline-flex;
8 | // align-content: center;
9 | justify-content: center;
10 | align-items: center;
11 | svg {
12 | fill: white;
13 | // &.square {
14 | // zoom: 0.9;
15 | // }
16 | // &.triangle {
17 | // zoom: 1.05;
18 | // }
19 | // &.start {
20 | // zoom: 1.15;
21 | // }
22 | // &.hexagon {
23 | // zoom: 0.9;
24 | // }
25 | // &.pentagon {
26 | // zoom: 1.1;
27 | // }
28 |
29 | &.shadow{
30 | position: absolute;
31 | transform: scale(1.5);
32 | transform-origin: center;
33 | *{
34 | fill: inherit;
35 | stroke: inherit;
36 | }
37 | }
38 | }
39 | }
40 | .station-name {
41 | display: inline-block;
42 | font-size: 18px;
43 | font-weight: 500;
44 | // font-size: 30px;
45 | // font-weight: 1000;
46 | vertical-align: top;
47 | // margin-left: 5px;
48 | white-space: nowrap;
49 |
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Render/Layer/ScaleLayer.scss:
--------------------------------------------------------------------------------
1 | .ScaleLayer{
2 | width: 100vw;
3 | height: 100vh;
4 | overflow: hidden;
5 | .layer-for-welcome-tour{
6 | position: fixed;
7 | z-index: -500;
8 | width: 70vw;
9 | height: 70vh;
10 | left: 15vw;
11 | top: 15vw;
12 | }
13 | .transform-layer{
14 | transform-origin: left top;
15 | // never use will-change here, it makes child elements blurry
16 | // will-change: transform;
17 | }
18 |
19 | .background-layer{
20 | position: absolute;
21 | pointer-events: none;
22 | transform-origin: left top;
23 | appearance: none;
24 | border: none;
25 | box-shadow: none;
26 | border-image-width: 0;
27 |
28 | &:not([src]) {
29 | opacity: 0 !important;
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Render/Recovery/Recovery.scss:
--------------------------------------------------------------------------------
1 | .App{
2 | perspective: 1000px;
3 | perspective-origin: 100vw 105px;
4 | z-index: 10000;
5 | }
6 | .recovery-notification-container {
7 |
8 | position: fixed;
9 | right: 0;
10 | top: 0;
11 | transform-origin: right center;
12 | transition: ease-in-out 0.5s;
13 |
14 | &.show {
15 | transform: rotate3d(0, 1, 0, 0deg);
16 | opacity: 1;
17 | }
18 | &:not(.show) {
19 | transform: rotate3d(0, 1, 0, -90deg);
20 | opacity: 0;
21 | }
22 | .recovery-notification {
23 | @media screen and (max-width: 520px) {
24 | display: none;
25 | }
26 | position: fixed;
27 | top: 30px;
28 | right: 30px;
29 | width: 363px;
30 | height: 75px;
31 | display: flex;
32 | // justify-content: space-between;
33 | align-items: center;
34 | background-color: rgb(255, 255, 255, 0.72);
35 | box-shadow: 0 0 80px 0 rgba(0, 0, 0, 0.25);
36 | backdrop-filter: blur(15px);
37 | border-radius: 15px;
38 | transition: ease-in-out 0.3s;
39 |
40 | // zoom: 1.2;
41 | &:hover {
42 | background-color: rgba(255, 255, 255, 0.001);
43 | }
44 | .icon {
45 | margin: 0 20px;
46 | }
47 |
48 | .icon,
49 | .ok,
50 | .no {
51 | width: 36px;
52 | height: 36px;
53 |
54 | border-radius: 50%;
55 | display: flex;
56 | justify-content: center;
57 | align-items: center;
58 | }
59 |
60 | .ok,
61 | .no {
62 | cursor: pointer;
63 | position: absolute;
64 | }
65 |
66 | .icon,
67 | .ok {
68 | background-color: rgb(235, 10, 40, 0.1);
69 | }
70 |
71 | .no {
72 | background-color: #d9d9d9;
73 | right: 20px;
74 | }
75 |
76 | .icon {
77 | svg {
78 | width: 20px;
79 | height: 20px;
80 | }
81 | }
82 |
83 | .ok {
84 | right: 60px;
85 | transition: ease-in-out 0.3s;
86 | &:hover {
87 | background-color: rgb(235, 10, 40, 0.2);
88 | }
89 | svg {
90 | width: 16px;
91 | }
92 | }
93 |
94 | .no {
95 | transition: ease-in-out 0.3s;
96 | &:hover {
97 | background-color: #c9c9c9;
98 | }
99 | svg {
100 | width: 12px;
101 | }
102 | }
103 |
104 | .text {
105 | .title {
106 | font-size: 16px;
107 | }
108 | .sub-title {
109 | font-size: 12px;
110 | }
111 | font-weight: 500;
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/Render/Recovery/Recovery.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | Dispatch,
3 | SetStateAction,
4 | useEffect,
5 | useRef,
6 | useState,
7 | } from "react";
8 | import { ReactComponent as RecoverIcon } from "../../Resource/Icon/clock.arrow.circlepath.svg";
9 | import { ReactComponent as OkIcon } from "../../Resource/Icon/ok.svg";
10 | import { ReactComponent as NoIcon } from "../../Resource/Icon/no.svg";
11 |
12 | import "./Recovery.scss";
13 | import { setDataFromJson, UserDataType } from "../../Data/UserData";
14 | import { mediateMap, readFileFromIndexedDB } from "../../Common/util";
15 | import classNames from "classnames";
16 | import { useTranslation } from "react-i18next";
17 | export function Recovery({
18 | data,
19 | setData,
20 | recoveredFromError,
21 | setRecoveredFromError,
22 | transfromTools,
23 | }: {
24 | data: UserDataType;
25 | setData: Dispatch>;
26 | recoveredFromError: boolean;
27 | setRecoveredFromError: Dispatch>;
28 | transfromTools: {
29 | scale: number;
30 | setScale: React.Dispatch>;
31 | translateX: number;
32 | translateY: number;
33 | setTranslateX: React.Dispatch>;
34 | setTranslateY: React.Dispatch>;
35 | };
36 | }) {
37 | const {t} = useTranslation();
38 | const notificationRef = useRef(null);
39 | const [showNotification, setShowNotification] = useState(false);
40 | useEffect(() => {
41 | const current = localStorage.getItem("current");
42 | const show = current && !recoveredFromError;
43 | setRecoveredFromError(false);
44 | setShowNotification(!!show);
45 | const handleClickCapture = (event: TouchEvent | MouseEvent) => {
46 | if (
47 | notificationRef.current &&
48 | !notificationRef.current.contains(event.target as Node)
49 | ) {
50 | console.log("recovery focusout");
51 | setShowNotification(false);
52 | }
53 | };
54 | document.addEventListener("touchstart", handleClickCapture, true);
55 | document.addEventListener("click", handleClickCapture, true);
56 | return () => {
57 | document.removeEventListener("touchstart", handleClickCapture, true);
58 | document.addEventListener("click", handleClickCapture, true);
59 | };
60 | }, []);
61 | return (
62 |
69 |
75 |
76 |
77 |
78 |
79 |
{t('recover.text')}
80 |
{t('recover.subTitle')}
81 |
82 |
{
85 | const current = localStorage.getItem("current");
86 | if (current) {
87 | const data = setDataFromJson(setData, current);
88 | readFileFromIndexedDB("image")
89 | .then((file) => {
90 | setData((data) => ({
91 | ...data,
92 | // backgroundColor: "image",
93 | backgroundImage: file as File,
94 | }));
95 | })
96 | .catch((e) => {
97 | console.error(e);
98 | });
99 | mediateMap(data, transfromTools);
100 | }
101 | setShowNotification(false);
102 | }}
103 | >
104 |
105 |
106 |
{
109 | setShowNotification(false);
110 | }}
111 | >
112 |
113 |
114 |
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/src/Resource/Icon/airwave.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const AirWaveIcon =({className}:{className?: string})=> (
4 |
7 | );
8 |
9 | export default AirWaveIcon;
--------------------------------------------------------------------------------
/src/Resource/Icon/arrow.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const arrowIcon = ({ className }: { className?: string }) => (
4 |
28 | );
29 |
30 | export default arrowIcon;
31 |
--------------------------------------------------------------------------------
/src/Resource/Icon/auto.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const AutoIcon = ({ className }: { className?: string }) => ();
11 |
12 | export default AutoIcon;
13 |
--------------------------------------------------------------------------------
/src/Resource/Icon/bolt.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const BoltIcon = ({ className }: { className?: string }) => ();
11 |
12 | export default BoltIcon;
13 |
14 |
--------------------------------------------------------------------------------
/src/Resource/Icon/clock.arrow.circlepath.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/Resource/Icon/edit.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const edit =({className}:{className: string})=> (
4 |
18 | );
19 |
20 | export default edit;
--------------------------------------------------------------------------------
/src/Resource/Icon/expand.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const expand =({className}:{className?: string})=> (
4 |
14 | );
15 |
16 | export default expand;
--------------------------------------------------------------------------------
/src/Resource/Icon/export.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/Resource/Icon/export.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ExportIcon = ({ className }: { className?: string }) => ();
11 |
12 | export default ExportIcon;
13 |
14 |
--------------------------------------------------------------------------------
/src/Resource/Icon/finished.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const FinishedIcon =({className}:{className?: string})=> (
4 |
12 |
13 | );
14 |
15 | export default FinishedIcon;
--------------------------------------------------------------------------------
/src/Resource/Icon/globe.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/Resource/Icon/goto.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const GoToIcon = ({ className }: { className?: string }) => (
4 |
38 | );
39 |
40 | export default GoToIcon;
41 |
--------------------------------------------------------------------------------
/src/Resource/Icon/infinity.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Infinity = ({ className }: { className?: string }) => (
4 |
30 | );
31 |
32 | export default Infinity;
33 |
--------------------------------------------------------------------------------
/src/Resource/Icon/no.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/Resource/Icon/ok.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/Resource/Icon/pageview.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const PageViewIcon =({className}:{className?: string})=> (
11 | );
12 |
13 | export default PageViewIcon;
--------------------------------------------------------------------------------
/src/Resource/Icon/plus.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const plusIcon = ({ className }: { className?: string }) => (
4 |
28 | );
29 |
30 | export default plusIcon;
31 |
--------------------------------------------------------------------------------
/src/Resource/Icon/share.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/Resource/Icon/share2.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/Resource/Icon/shrink.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const shrink =({className}:{className: string})=> (
4 |
14 | );
15 |
16 | export default shrink;
--------------------------------------------------------------------------------
/src/Resource/Icon/touch.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const TouchIcon = ({ className }: { className?: string }) => ();
11 |
12 | export default TouchIcon;
13 |
--------------------------------------------------------------------------------
/src/Style/Cursor.ts:
--------------------------------------------------------------------------------
1 | import { Mode } from "../DataStructure/Mode";
2 |
3 | export const getCursor = (mode: Mode)=>{
4 | let cursor;
5 | //chrome has bug here, the cursor won't change sometimes while DOM already updated.
6 | switch(mode){
7 | case Mode.moving:{
8 | cursor = 'grabbing';
9 | break;
10 | }
11 | default:{
12 | cursor = 'default';
13 | break;
14 | }
15 | }
16 | return cursor;
17 | }
--------------------------------------------------------------------------------
/src/WelcomeTour/Driver.ts:
--------------------------------------------------------------------------------
1 | import { driver as Driver, Config } from "driver.js";
2 | import {t} from 'i18next';
3 | export const showTour = async (id: string, callback: Function) => {
4 | const driver = Driver();
5 | const { getSteps } = await import(/* webpackMode: "eager" */ `./Steps/${id}`);
6 | const eventListeners: EventListenerOrEventListenerObject[] = [];
7 | const touch = window.ontouchend === null;
8 | const eventName =// touch ? "touchend" :
9 | "click";
10 | const config: Config = {
11 | prevBtnText: t('shang-yi-bu'),
12 | doneBtnText: t('wan-cheng'),
13 | nextBtnText: t('xia-yi-bu'),
14 | progressText: "{{current}} / {{total}}",
15 | showProgress: true,
16 | allowClose: false,
17 | showButtons: ["close"],
18 | // onPrevClick:(ele)=>{
19 | // if(ele){
20 | // const element = ele as HTMLElement;
21 | // eventListeners.forEach(listener=>{
22 | // element.removeEventListener('click',listener);
23 | // })
24 | // driver.movePrevious();
25 | // }
26 | // },
27 | onHighlighted: (ele, step, opt) => {
28 | const steps = opt.config.steps!;
29 | const last = step === steps[steps.length - 1];
30 | const next = steps[opt.state.activeIndex! + 1];
31 |
32 | if (ele) {
33 | const element = ele as HTMLElement;
34 | const moveToNext = () => {
35 | const gotoNext = () => {
36 | // element.removeEventListener(eventName, moveToNext);
37 | element.removeEventListener("click", moveToNext);
38 | element.removeEventListener("touchend", moveToNext);
39 | if(driver.getActiveStep()===step)
40 | driver.moveNext();
41 | };
42 | setTimeout(() => {
43 | if (!next || document.querySelector(next.element as string)) {
44 | gotoNext();
45 | }else{
46 | setTimeout(()=>{
47 | if (!next || document.querySelector(next.element as string))
48 | gotoNext()
49 | },300)
50 | }
51 | }, 100);
52 | };
53 | eventListeners.push(moveToNext);
54 | element.addEventListener("click", moveToNext);
55 | element.addEventListener("touchend", moveToNext);
56 | }
57 | },
58 | onNextClick: (ele) => {
59 | if (ele) {
60 | const element = ele as HTMLElement;
61 | element.dispatchEvent(
62 | new Event("click", { bubbles: true, cancelable: true })
63 | );
64 | }
65 | },
66 | steps: getSteps(driver),
67 | onDestroyed: (ele,step) => {
68 | if (ele) {
69 | const element = ele as HTMLElement;
70 | if(step.element!==".tour-btn")
71 | element.dispatchEvent(
72 | new Event("click", { bubbles: true, cancelable: true })
73 | );
74 | }
75 | callback();
76 | },
77 | };
78 | driver.setConfig(config);
79 |
80 | driver.drive();
81 | };
82 |
--------------------------------------------------------------------------------
/src/WelcomeTour/HightLights.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Infinity from "../Resource/Icon/infinity";
3 | import AirWaveIcon from "../Resource/Icon/airwave";
4 | import PageViewIcon from "../Resource/Icon/pageview";
5 | import BoltIcon from "../Resource/Icon/bolt";
6 | import TouchIcon from "../Resource/Icon/touch";
7 | import AutoIcon from "../Resource/Icon/auto";
8 | import ExportIcon from "../Resource/Icon/export";
9 | import {t} from 'i18next';
10 | export const hightLights = [
11 | {
12 | id:'common-operation',
13 | icon: ,
14 | title: t('wu-xian-zi-chan'),
15 | subTitle: t('wu-xian-da-hua-bu'),
16 | introText: [
17 | [false, t('zhi-chi'), t('wu-xian-tiao-xian-lu')],
18 | [false, t('zhi-chi'), t('wu-xian-ge-zhan-dian')],
19 | ],
20 | more: t('le-jie-ji-ben-cao-zuo')
21 | },
22 | {
23 | id:'line-card',
24 | icon: ,
25 | title: t('ling-huo-zou-xian'),
26 | subTitle: t('yi-tiao-xian-lu-neng-duo-ci-chuan-guo-tong-yi-zhan'),
27 | introText: [
28 | [false, t('zhi-chi'), t('q-zi-zou-xian-yu-zou-xian')],
29 | [false, t('zhi-chi-she-zhi'), t('zhi-xian')],
30 | ],
31 | more: t('le-jie-xian-lu-she-zhi')
32 | },
33 | {
34 | id:'station-card',
35 | icon: ,
36 | title: t('fang-bian-cha-kan'),
37 | subTitle: t('ka-pian-hua-zhan-shi-zhan-dian-yu-xian-lu'),
38 | introText: [
39 | [false, t('zhan-dian-xian-lu-hu-xiang'), t('guan-lian')],
40 | [true, t('gao-liang'), t("zhan-shi-xuan-zhong-zhan-dian-yu-xian-lu"),]
41 | ],
42 | more: t('le-jie-zhan-dian-ka-pian')
43 | },
44 | {
45 | id:'quick-edit',
46 | icon: ,
47 | title: t('gao-xiao-bian-ji'),
48 | subTitle: t('lian-xu-chuang-jian-zhan-dian-mo-shi'),
49 | introText: [
50 | [true, t('lian-xu-tian-jia-zhan-dian'), t('mo-shi')],
51 | [false, t('zhi-chi'), t('che-hui'), t('yu'), t('zhong-zuo')],
52 | ],
53 | more: t('le-jie-kuai-su-chuang-jian')
54 | },
55 | {
56 | id:'mobile',
57 | icon: ,
58 | title: t('yi-dong-duan-zhi-chi'),
59 | subTitle: t('wan-zheng-de-chu-kong-shi-jian-zhi-chi'),
60 | introText: [
61 | [true, t('dan-zhi'), t('tuo-dong'), t('shuang-zhi'), t('suo-fang-di-tu')],
62 | [false, t('shi-bie-dong-zuo-yi-tu'), t('jian-shao-wu-cao-zuo')],
63 | ],
64 | more: t('zai-ping-ban-shang-ti-yan')
65 | },
66 | {
67 | id:'tag-setting',
68 | icon: ,
69 | title: t('zi-dong-bi-rang'),
70 | subTitle: t('zi-dong-tian-jia-pian-yi-zhi'),
71 | introText: [
72 | [false, t('gong-xian-xian-lu'), t('bu-hui-zhong-die')],
73 | [false, t('zhan-dian-ming-cheng-zi-dong-xuan-ze-bai-fang-wei-zhi')],
74 | ],
75 | more: t('le-jie-biao-qian-she-zhi')
76 | },
77 | {
78 | id:'export',
79 | icon: ,
80 | title: t('dao-ru-dao-chu'),
81 | subTitle: t('dao-chu-gao-fen-bian-shuai-tu-pian'),
82 | introText: [
83 | [false, t('dao-chu'), t('pngsvg-tu-pian')],
84 | [false, t('zhi-chi-cong-mo-ban-chuang-jian')],
85 | ],
86 | more: t('le-jie-dao-ru-dao-chu')
87 | },
88 | ];
89 |
--------------------------------------------------------------------------------
/src/WelcomeTour/Steps/common-operation.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep, Driver } from "driver.js";
2 | import {t} from 'i18next';
3 |
4 | export const getSteps = (driver: Driver): DriveStep[] => [
5 | {
6 | element: ".ScaleLayer",
7 | onHighlighted:()=>{},
8 | popover: {
9 | title: t('tour.tryScale'),
10 | description: window.ontouchend === null? t('tour.touchScale'): t('tour.mouseScale'),
11 | showButtons:['next'],
12 | onNextClick:driver.moveNext
13 | },
14 | },
15 | {
16 | element: ".title .click-panel",
17 | popover: {
18 | title: t('tour.clickTile'),
19 | description: t('tour.clickOpenMenu'),
20 | },
21 | },
22 | {
23 | element: ".title",
24 | popover: {
25 | title: t('zai-ci-dian-ji-biao-ti'),
26 | description: t('ke-yi-xiu-gai-biao-ti'),
27 | },
28 | },
29 | {
30 | element: ".menu",
31 | popover: {
32 | title: t('dian-ji-ren-yi-kong-bai-qu-yu-tui-chu-cai-dan'),
33 | description: t('tui-chu-cai-dan'),
34 | },
35 | },
36 | {
37 | element: ".station-descend-31",
38 | popover: {
39 | title: t('dian-ji-zhan-dian'),
40 | description: t('yi-da-kai-zhan-dian-ka-pian'),
41 | },
42 | },
43 | {
44 | element: ".station-card",
45 | popover: {
46 | title: t('zhan-dian-ka-pian'),
47 | description: t('ke-yi-zai-zhe-li-bian-ji-zhan-dian-de-suo-you-she-zhi'),
48 | showButtons:["next"],
49 | // onNextClick:driver.moveNext
50 | },
51 | },
52 | {
53 | element: ".ScaleLayer",
54 | popover: {
55 | title: t('chang-shi-dian-ji-xian-lu'),
56 | description: t('tu-zhong-de-cai-se-xian-tiao-ji-wei-xian-lu-ru-guo-wu-fa-xuan-zhong-ke-yi-chang-shi-fang-da-di-tu-zai-dian-xuan'),
57 | },
58 | },
59 | {
60 | element: ".line-card",
61 | popover: {
62 | title: t('xian-lu-ka-pian'),
63 | description: t('ke-yi-zai-zhe-li-bian-ji-xian-lu-de-suo-you-she-zhi'),
64 | showButtons:["next"],
65 | // onNextClick:driver.moveNext
66 | },
67 | },
68 | ];
69 |
--------------------------------------------------------------------------------
/src/WelcomeTour/Steps/export.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep, Driver } from "driver.js";
2 | import { browserInfo } from "../../Common/util";
3 | import {t} from 'i18next';
4 | export const getSteps = (driver: Driver): DriveStep[] => {
5 | const { engine } = browserInfo;
6 | const webkit = engine.name === "WebKit";
7 | return[
8 | {
9 | element: ".title .click-panel",
10 | popover: {
11 | title: t('dian-ji-biao-ti'),
12 | description: t('dian-ji-biao-ti-da-kai-cai-dan'),
13 | },
14 | },
15 | {
16 | element: ".existed-map-btn",
17 | popover: {
18 | title: t('cong-mo-ban-xin-jian'),
19 | description: t('zai-yi-you-de-di-tu-shang-xiu-gai'),
20 | },
21 | },
22 | {
23 | element: ".tools",
24 | onHighlighted:()=>{},
25 | popover: {
26 | title: t('xuan-ze-yi-ge-ni-xi-huan-de-cheng-shi'),
27 | description: t('ran-hou-dian-ji-xia-yi-bu'),
28 | showButtons:["next"],
29 | onNextClick:()=>{
30 | driver.moveNext();
31 | }
32 | },
33 | },
34 | {
35 | element: ".confirm-add-from-existed-map-btn",
36 | popover: {
37 | title: t('yi-ci-wei-mo-ban-xin-jian'),
38 | description: t('que-ren-xin-jian-qian-qing-que-bao-yi-jing-bao-cun-le-dang-qian-de-di-tu'),
39 | },
40 | },
41 | {
42 | element: ".title .click-panel",
43 | popover: {
44 | title: t('da-kai-cai-dan'),
45 | description: t('xuan-ze-zuo-wei-wen-jian-dao-chu'),
46 | },
47 | },
48 | {
49 | element: ".export-as-file-btn",
50 | popover: {
51 | title: t('zuo-wei-wen-jian-dao-chu'),
52 | description: t('xuan-ze-zuo-wei-wen-jian-dao-chu-0'),
53 | },
54 | },
55 | {
56 | element: ".import-file-btn",
57 | onHighlighted:()=>{},
58 | popover: {
59 | title: t('dao-ru-gang-cai-dao-chu-wen-jian'),
60 | description: t('xia-ci-dian-ji-dao-ru-wen-jian-ke-yi-ji-xu-bian-ji'),
61 | showButtons:["next"],
62 | onNextClick:driver.moveNext
63 | },
64 | },
65 | {
66 | element: `.export-as${webkit?'-svg':''}-image-btn`,
67 | popover: {
68 | title: t('dao-chu-tu-pian'),
69 | description: t('dian-ji-dao-chu-tu-pian-dao-chu-ke-neng-hui-dao-zhi-ka-dun-ji-miao-shu-yu-zheng-chang-xian-xiang'),
70 | },
71 | },
72 | {
73 | element: `.recover-btn`,
74 | popover: {
75 | title: t('cong-huan-cun-zhong-hui-fu-shu-ju'),
76 | description: t('ru-guo-mei-bao-cun-de-shi-hou-bu-xiao-xin-shua-xin-ke-yi-dian-ji-zhe-li-hui-fu-shu-ju'),
77 | },
78 | },
79 | {
80 | element: `.export-recover-btn`,
81 | popover: {
82 | title: t('dao-chu-hui-fu-shu-ju'),
83 | description: t('ru-guo-mou-yi-bu-cao-zuo-hou-dao-zhi-bao-cuo-huo-beng-kui-dian-ji-zhe-li-dao-chu-zui-hou-yi-ci-zheng-que-de-shu-ju-ru-guo-yu-dao-zhe-zhong-qing-kuang-qing-fan-kui-gei-zuo-zhe'),
84 | },
85 | },
86 | ];
87 | }
--------------------------------------------------------------------------------
/src/WelcomeTour/Steps/line-card.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep, Driver } from "driver.js";
2 | import {t} from 'i18next';
3 | export const getSteps = (driver: Driver): DriveStep[] => [
4 | {
5 | element: ".ScaleLayer",
6 | popover: {
7 | title: t('dian-ji-ren-yi-xian-lu'),
8 | description: t('tu-zhong-cai-se-qu-xian-biao-shi-di-tie-xian-lu'),
9 | },
10 | },
11 | {
12 | element: ".line-card",
13 | popover: {
14 | title: t('xian-lu-ka-pian-0'),
15 | description: t('ke-yi-zai-zhe-li-bian-ji-xian-lu-de-suo-you-she-zhi-0'),
16 | showButtons:["next"],
17 | // onNextClick:driver.moveNext
18 | },
19 | },
20 | {
21 | element: ".bend-first",
22 | popover: {
23 | title: t('dian-ji-ci-chu-kong-zhi-xian-lu-zai-liang-zhan-zhi-jian-de-zou-xiang'),
24 | description: t('zhan-dian-qu-jian-lian-xian-zhi-you-liang-zhong-fang-shi-xie-xian-yu-zhi-xian'),
25 | },
26 | },
27 | {
28 | element: ".expand",
29 | popover: { title: t('fang-da-an-niu'), description: t('ke-yi-tuo-kuan-xian-lu-ka-pian') },
30 | },
31 | {
32 | element: ".shrink",
33 | popover: { title: t('suo-xiao-an-niu'), description: t('zai-suo-xiao-zhuang-tai-ke-yi-dian-ji-geng-duo-she-zhi') },
34 | },
35 | {
36 | element: ".edit",
37 | popover: { title: t('bian-ji-an-niu'), description: t('jin-ru-geng-duo-she-zhi-mian-ban') },
38 | },
39 | {
40 | element: ".name-detail",
41 | popover: { title: t('ci-chu-xiu-gai-xian-lu-ming-cheng-yu-biao-shi'), description: t('pai-xu-ke-yi-kong-zhi-xian-lu-jian-de-zhe-dang-guan-xi') ,showButtons:["next"],onNextClick:driver.moveNext},
42 | },
43 | {
44 | element: ".edit-tool.color",
45 | popover: { title: t('dian-ji-ci-chu-xiu-gai-xian-lu-biao-shi-se'), description: t('zuo-xia-jiao-de-yan-se-xuan-ze-qi-ke-yi-qu-se') ,side:"right"},
46 | },
47 | {
48 | element: ".edit-tool.operation",
49 | popover: { title: t('dian-ji-ci-chu-shan-chu-xian-lu-huo-zhe-she-zhi-zhi-xian'), description: t('she-zhi-zhi-xian-hou-xian-lu-hui-yi-xu-xian-xian-shi-qie-lian-jie-chu-bu-zai-xian-shi-ba-shou'),side:"right" },
50 | },
51 | {
52 | element: ".done",
53 | popover: { title: t('dian-ji-ci-chu-wan-cheng-she-zhi'), description: t('hui-dao-zhan-dian-ye') },
54 | },
55 | {
56 | element: ".stations-count",
57 | popover: { title: t('xian-shi-tu-jing-zhan-dian-ka-pian'), description: t('dian-ji-ci-chu-suo-yi-tu-jing-de-zhan-dian-ka-pian') },
58 | },
59 | ];
60 |
--------------------------------------------------------------------------------
/src/WelcomeTour/Steps/quick-edit.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep, Driver } from "driver.js";
2 | import {t} from 'i18next';
3 | export const getSteps = (driver: Driver): DriveStep[] => [
4 | {
5 | element: ".title .click-panel",
6 | popover: {
7 | title: t('da-kai-cai-dan-0'),
8 | description: t('dian-ji-biao-ti-da-kai-cai-dan-0'),
9 | },
10 | },
11 | {
12 | element: "#menu-add-station",
13 | popover: {
14 | title: t('dian-ji-tian-jia-zhan-dian'),
15 | description: t('jin-ru-tian-jia-zhan-dian-mo-shi'),
16 | },
17 | },
18 | {
19 | element: ".ScaleLayer",
20 | onHighlighted: () => {},
21 | popover: {
22 | title: t('dian-ji-ren-yi-kong-bai-chu-tian-jia-zhan-dian'),
23 | description: t('tian-jia-hao-zhi-hou-dian-ji-xia-yi-bu-jian-yi-nin-zai-ping-mu-zuo-shang-jiao-de-kong-bai-qu-yu-tian-jia-san-ge-zhan-dian'),
24 | showButtons: ["next"],
25 | onNextClick: () => {
26 | driver.moveNext();
27 | },
28 | },
29 | },
30 | {
31 | element: "#add-station-finish-btn",
32 | popover: { title: t('dian-ji-wan-cheng'), description: t('tui-chu-bian-ji-mo-shi') },
33 | },
34 | {
35 | element: ".station-descend-1",
36 | popover: {
37 | title: t('dian-ji-gang-cai-chuang-jian-de-zhan-dian'),
38 | description: t('da-kai-zhan-dian-xin-xi-ka-pian'),
39 | },
40 | },
41 | {
42 | element: ".station-card-operation",
43 | popover: { title: t('dian-ji-cao-zuo-xuan-xiang-ka'), description: t('wo-men-lai-tian-jia-xian-lu') },
44 | },
45 | {
46 | element: ".add-new-line-btn",
47 | popover: {
48 | title: t('dian-ji-yi-ci-wei-qi-dian-xin-jian-xian-lu'),
49 | description: t('jin-ru-tian-jia-xian-lu-mo-shi'),
50 | },
51 | },
52 | {
53 | element: ".station-descend-2",
54 | popover: {
55 | title: t('dian-ji-zhan-dian-0'),
56 | description: t('lian-jie-zhan-dian'),
57 | },
58 | },
59 | {
60 | element: ".station-descend-3",
61 | popover: {
62 | title: t('dian-ji-zhan-dian'),
63 | description: t('lian-jie-zhan-dian'),
64 | },
65 | },
66 | {
67 | element: "#add-line-finish-btn",
68 | popover: { title: t('dian-ji-wan-cheng'), description: t('xian-lu-jiu-chuang-jian-hao-le') },
69 | },
70 | ];
71 |
--------------------------------------------------------------------------------
/src/WelcomeTour/Steps/skip.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep, Driver } from "driver.js";
2 |
3 | export const getSteps = (driver: Driver): DriveStep[] => [
4 | {
5 | element: ".title .click-panel",
6 | // onHighlighted:()=>{},
7 | popover: {
8 | title: "下次可以点这里再次打开教程",
9 | description: "点击标题打开菜单",
10 | showButtons:["next"],
11 | // onNextClick:driver.moveNext
12 | },
13 | },
14 | {
15 | element: ".tour-btn",
16 | onHighlighted:()=>{},
17 | popover: {
18 | title: "使用教程",
19 | description: "点这里再次打开",
20 | showButtons:["next"],
21 | onNextClick: driver.moveNext,
22 | },
23 | },
24 | ];
25 |
--------------------------------------------------------------------------------
/src/WelcomeTour/Steps/station-card.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep, Driver } from "driver.js";
2 | import {t} from 'i18next';
3 | export const getSteps = (driver: Driver): DriveStep[] => [
4 | {
5 | element: ".station-descend-31",
6 | popover: {
7 | title: t('dian-ji-zhan-dian'),
8 | description: t('yi-da-kai-zhan-dian-ka-pian-0'),
9 | },
10 | },
11 | {
12 | element: ".station-card",
13 | popover: {
14 | title: t('zhan-dian-ka-pian'),
15 | description: t('ke-yi-zai-zhe-li-bian-ji-zhan-dian-de-suo-you-she-zhi'),
16 | showButtons:["next"],
17 | // onNextClick:driver.moveNext
18 | },
19 | },
20 | {
21 | element: ".name-detail",
22 | popover: {
23 | title: t('bian-ji-zhan-dian-wei-zhi'),
24 | description: t('dian-ji-zuo-biao-zhi-hou-shi-yong-shu-biao-gun-lun-ke-yi-jing-que-tiao-jie'),
25 | showButtons:["next"],
26 | // onNextClick:driver.moveNext
27 | },
28 | },
29 | {
30 | element: ".edit-tool.color",
31 | popover: { title: t('dian-ji-ci-chu-xiu-gai-zhan-dian-xing-zhuang'), description: t('xiu-gai-xing-zhuang') ,side:"right"},
32 | },
33 | {
34 | element: ".edit-tool.operation",
35 | popover: { title: t('dian-ji-ci-chu-shan-chu-zhan-dian-huo-xin-jian-xian-lu'), description: t('shan-chu-huo-xin-jian'),side:"right" },
36 | },
37 | {
38 | element: ".edit-tool.operation.tag",
39 | popover: { title: t('dian-ji-ci-chu-she-zhi-zhan-dian-ming-cheng-wei-zhi'), description: t('zhan-dian-ming-wei-zhi') },
40 | },
41 | {
42 | element: ".tag-detail",
43 | popover: { title: t('dian-ji-hui-se-de-fang-kuai-xuan-ze-fang-wei'), description: t('dian-ji-zhong-jian-de-wen-zi-hui-fu-zi-dong-wei-zhi'),showButtons:['next'],onNextClick:driver.moveNext },
44 | },
45 | ];
46 |
--------------------------------------------------------------------------------
/src/WelcomeTour/Steps/tag-setting.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep, Driver } from "driver.js";
2 | import {t} from 'i18next';
3 | export const getSteps = (driver: Driver): DriveStep[] => [
4 | {
5 | element: ".title .click-panel",
6 | popover: {
7 | title: t('dian-ji-biao-ti'),
8 | description: t('dian-ji-biao-ti-da-kai-cai-dan-1'),
9 | },
10 | },
11 | {
12 | element: ".auto-hidden-btn",
13 | popover: {
14 | title: t('guan-bi-zi-dong-yin-cang'),
15 | description: t('zhan-dian-ming-hui-zai-di-tu-suo-fang-dao-xiao-chi-du-shi-zi-dong-yin-cang-guan-bi-zi-dong-yin-cang-yi-que-bao-ke-yi-yi-zhi-xian-shi-zhan-dian-ming-cheng'),
16 | },
17 | },
18 | {
19 | element: ".menu",
20 | popover: {
21 | title: t('dian-ji-ren-yi-kong-bai-qu-yu-tui-chu-cai-dan-0'),
22 | description: t('tui-chu-cai-dan-0'),
23 | },
24 | },
25 | {
26 | element: ".station-descend-31",
27 | popover: {
28 | title: t('dian-ji-zhan-dian'),
29 | description: t('yi-da-kai-zhan-dian-ka-pian-1'),
30 | },
31 | },
32 | {
33 | element: ".edit-tools",
34 | popover: { title: t('zai-zhe-li-xiang-xia-gun-dong-yi-xia'), description: t('zai-dian-ji-biao-qian-an-niu') },
35 | },
36 | {
37 | element: ".tag-detail",
38 | popover: { title: t('chang-shi-gai-bian-zhan-dian-ming-dao-you-xia-jiao'), description: t('dian-ji-you-xia-jiao-de-hui-se-fang-kuai') },
39 | },
40 | {
41 | element: ".station-name-descend-31",
42 | popover: { title: t('ke-yi-kan-dao-zhan-dian-ming-yi-jing-wei-yu-zhan-dian-you-xia-jiao-le'), description: t('ke-yi-yong-zhe-zhong-fang-fa-xiu-gai-zhan-dian-ming-wei-zhi'), showButtons:["next"],onNextClick:driver.moveNext },
43 | },
44 | {
45 | element: ".tag-item.center",
46 | popover: { title: t('dian-ji-zhong-jian-de-wen-zi-hui-fu-dao-zi-dong-xuan-ze-wei-zhi'), description: t('hui-fu-dao-zi-dong-xuan-ze-wei-zhi') },
47 | },
48 | ];
49 |
--------------------------------------------------------------------------------
/src/WelcomeTour/WelcomeTour.scss:
--------------------------------------------------------------------------------
1 | .welcome-tour-container {
2 | position: fixed;
3 | height: 100vh;
4 | width: 100vw;
5 | left: 0;
6 | top: 0;
7 | background-color: rgba(0, 0, 0, 0.4);
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | z-index: 2000;
12 | font-weight: 500;
13 | font-size: 18px;
14 | .welcome-tour {
15 | margin-top: -10vh;
16 | // width: 800px;
17 | // min-width: 45vw;
18 | width: 80vw;
19 | max-width: 800px;
20 | height: 475px;
21 | background-color: #454545;
22 | border-radius: 12px;
23 | .header {
24 | margin: 33.3px 33.3px 20px 33.3px;
25 | white-space: nowrap;
26 | position: relative;
27 | .icon {
28 | img {
29 | width: 40px;
30 | height: 40px;
31 | border-radius: 5px;
32 | }
33 | }
34 | .title {
35 | display: inline-block;
36 | margin-left: 13px;
37 | vertical-align: top;
38 | .sub-title {
39 | font-size: 12px;
40 | color: white;
41 | width: fit-content;
42 | margin-top: -2px;
43 | }
44 | .main-title {
45 | color: white;
46 | font-weight: 800;
47 | width: fit-content;
48 | }
49 | }
50 | .control {
51 | display: inline-flex;
52 | vertical-align: top;
53 | font-size: 12px;
54 | position: absolute;
55 | right: 0;
56 | height: 40px;
57 | justify-content: center;
58 | align-items: center;
59 |
60 | .skip-tour {
61 | color: rgba(255, 255, 255, 0.6);
62 | margin: 0 20px;
63 | cursor: pointer;
64 | // text-transform: capitalize;
65 | }
66 | .start-tour {
67 | text-transform: capitalize;
68 | cursor: pointer;
69 | color: white;
70 | padding: 7.33px 20px;
71 | background-color: #2196f3;
72 | border-radius: 35px;
73 | }
74 | }
75 | }
76 | .divider {
77 | border-bottom: rgba(255, 255, 255, 0.27) solid 1px;
78 | }
79 | .body {
80 | white-space: nowrap;
81 | overflow-x: scroll;
82 | position: relative;
83 | &::-webkit-scrollbar {
84 | display: none;
85 | }
86 | .intro {
87 | display: inline-block;
88 | color: white;
89 | width: 285px;
90 | margin-left: 38px;
91 | margin-top: 36.67px;
92 | vertical-align: top;
93 | &:last-child{
94 | margin-right: 38px;
95 | }
96 | .hight-light {
97 | display: inline-flex;
98 | justify-content: center;
99 | align-items: center;
100 | background-color: #727272;
101 | padding: 4px 15px;
102 | border-radius: 35px;
103 | .icon {
104 | height: 20px;
105 | width: 20px;
106 | svg {
107 | height: 20px;
108 | width: 20px;
109 | }
110 | .air{
111 | width: 16px;
112 | margin-left: 2px;
113 | }
114 | }
115 | .title {
116 | margin-left: 5px;
117 | text-transform: capitalize;
118 | }
119 | }
120 | .detail-card {
121 | height: 240px;
122 | background-color: #727272;
123 | border-radius: 14px;
124 | margin-top: 17.3px;
125 | display: flow-root;
126 | position: relative;
127 | .qrcode-container{
128 | position: absolute;
129 | border-radius: 14px;
130 | margin: auto;
131 | left: 0;
132 | right: 0;
133 | top:0;
134 | bottom: 0;
135 | width: 100%;
136 | height: 100%;
137 | background-color: #0000002e;
138 | backdrop-filter: blur(10px);
139 | cursor: pointer;
140 | opacity: 0;
141 | pointer-events: none;
142 | transition: 0.3s ease-in-out;
143 | &.show{
144 | opacity: 1;
145 | pointer-events: auto;
146 | }
147 | canvas{
148 | position: absolute;
149 | border-radius: 14px;
150 | margin: auto;
151 | left: 0;
152 | right: 0;
153 | top:0;
154 | bottom: 0;
155 | }
156 | }
157 |
158 | .title {
159 | margin-top: 20.67px;
160 | margin-left: 24px;
161 | width: 265px;
162 | white-space: normal;
163 | text-transform: capitalize;
164 | }
165 | .left-down {
166 | position: absolute;
167 | left: 24px;
168 | bottom: 20.67px;
169 | .intro-text {
170 | .intro-text-line {
171 | &:not(.line-card-text){
172 | text-transform: capitalize;
173 | }
174 | :not(.emphasis) {
175 | color: rgba(255, 255, 255, 0.6);
176 | }
177 | }
178 | }
179 | .more {
180 | margin-top: 26px;
181 | cursor: pointer;
182 | &:hover{
183 | // text-decoration: underline;
184 | opacity: 0.9;
185 | }
186 | .more-text {
187 | text-transform: capitalize;
188 | &.finished{
189 | opacity: 0.6;
190 | }
191 | }
192 | .more-icon {
193 | height: 20px;
194 | width: 28px;
195 | vertical-align: top;
196 | display: inline-block;
197 | svg {
198 | height: 28px;
199 | width: 28px;
200 | &.finished{
201 | width: 21px;
202 | // margin-top: -1px;
203 | margin-left: 2px;
204 | opacity: 0.6;
205 | }
206 | }
207 | }
208 | }
209 | }
210 | }
211 | }
212 | }
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/WelcomeTour/WelcomeTour.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | LegacyRef,
3 | MutableRefObject,
4 | RefObject,
5 | useEffect,
6 | useRef,
7 | useState,
8 | useTransition,
9 | } from "react";
10 | import "./WelcomeTour.scss";
11 | import Infinity from "../Resource/Icon/infinity";
12 | import GoToIcon from "../Resource/Icon/goto";
13 | import { hightLights } from "./HightLights";
14 | import classNames from "classnames";
15 | import { onWheelX } from "../Common/util";
16 | import { showTour } from "./Driver";
17 | import { ShowTourProps, UserDataType } from "../Data/UserData";
18 | import FinishedIcon from "../Resource/Icon/finished";
19 | import QRCode from "qrcode";
20 | import { useTranslation } from "react-i18next";
21 | export function WelcomeTour({
22 | showTour: show,
23 | setShowTour: setShow,
24 | }: ShowTourProps) {
25 | const [qrCode, setQRCode] = useState(false);
26 | const {t} = useTranslation();
27 | const [visitedSteps, setVisitedSteps] = useState(() => {
28 | const visitedStepsJson = localStorage.getItem("visited-steps");
29 | return visitedStepsJson ? JSON.parse(visitedStepsJson) : [];
30 | });
31 | const setVisited = (step: string) => {
32 | const steps = visitedSteps.concat([step]);
33 | localStorage.setItem("visited-steps", JSON.stringify(steps));
34 | setVisitedSteps(steps);
35 | };
36 | const reset = () => {
37 | localStorage.setItem("visited-steps", JSON.stringify([]));
38 | setVisitedSteps([]);
39 | localStorage.setItem("skip-tour-viewed", "Y");
40 | };
41 | const next = hightLights.find(
42 | (highlight) => !visitedSteps.includes(highlight.id)
43 | );
44 | const canvasRef = useRef(null);
45 | const showQRCode = () => {
46 | setQRCode(true);
47 | setVisited("mobile");
48 | setTimeout(() => {
49 | QRCode.toCanvas(canvasRef.current, window.location.href);
50 | });
51 | };
52 | const scrollToNext = ()=>{
53 | setTimeout(() => {
54 | const contianer = document.querySelector(".welcome-tour .body");
55 | if(next && contianer){
56 | const nextIntro = document.querySelector(`.intro-${next.id}`)
57 | if(nextIntro){
58 | const {offsetLeft} = nextIntro as HTMLDivElement;
59 | contianer.scrollTo(offsetLeft-38,0);
60 | }
61 | }
62 | },100);
63 | }
64 | useEffect(() => {
65 | scrollToNext();
66 | }, []);
67 | return (
68 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | {t('welcome.welcome')}
79 | {t('welecome.minimetroweb')}
80 |
81 |
82 | {next ? (
83 | <>
84 | {
87 | setShow(false);
88 | // if (!localStorage.getItem("skip-tour-viewed"))
89 | // showTour("skip", () => {
90 | localStorage.setItem("skip-tour-viewed", "Y");
91 | // });
92 | }}
93 | >
94 | {t('welcome.skip')}
95 |
96 | {
99 | if (next.id === "mobile") {
100 | showQRCode();
101 | } else {
102 | setShow(false);
103 | showTour(next.id, () => {
104 | setShow(true);
105 | setVisited(next.id);
106 | scrollToNext();
107 | });
108 | }
109 | }}
110 | >
111 | {visitedSteps.length ? t('welcome.goOn') : ""}
112 | {next.more}
113 |
114 | >
115 | ) : (
116 | <>
117 |
118 | {t('welcome.restart')}
119 |
120 | {
123 | localStorage.setItem("skip-tour-viewed", "Y");
124 | setShow(false);
125 | }}
126 | >
127 | {t('welcome.done')}
128 |
129 | >
130 | )}
131 |
132 |
133 |
134 |
135 | {hightLights.map((hightLight) => {
136 | const { id, icon, title, subTitle, introText, more } = hightLight;
137 | const finished = visitedSteps.includes(id);
138 | return (
139 |
140 |
141 | {icon}
142 | {title}
143 |
144 |
145 |
{subTitle}
146 |
147 |
148 | {introText.map((line) => {
149 | const emphasisStart = line[0];
150 | const lineText = line.slice(1);
151 | return (
152 |
153 | {lineText.map((text, index) => {
154 | const emphasis =
155 | (Number(emphasisStart) + index) % 2;
156 | return (
157 |
158 | {text}
159 |
160 | );
161 | })}
162 |
163 | );
164 | })}
165 |
166 |
{
169 | if (id === "mobile") {
170 | showQRCode();
171 | } else {
172 | showTour(id, () => {
173 | setVisited(id);
174 | setShow(true);
175 | scrollToNext();
176 | });
177 | setShow(false);
178 | }
179 | }}
180 | >
181 |
184 | {more}
185 |
186 |
187 | {finished ? (
188 |
189 | ) : (
190 |
191 | )}
192 |
193 |
194 |
195 | {id === "mobile" ? (
196 |
setQRCode(false)}
202 | >
203 |
204 |
205 | ) : (
206 | <>>
207 | )}
208 |
209 |
210 | );
211 | })}
212 |
213 |
214 |
215 | );
216 | }
217 |
--------------------------------------------------------------------------------
/src/i18n/config.ts:
--------------------------------------------------------------------------------
1 |
2 | import i18n from 'i18next';
3 | import { initReactI18next } from 'react-i18next';
4 | import translation_en from './locales/en.json';
5 | import translation_zh from './locales/zh.json';
6 | import LanguageDetector from 'i18next-browser-languagedetector';
7 | const resources = {
8 | en: { translation: translation_en },
9 | zh: { translation: translation_zh },
10 | };
11 |
12 | i18n
13 | .use(LanguageDetector)
14 | .use(initReactI18next)
15 | .init({
16 | resources,
17 | fallbackLng: 'en',
18 | detection: {
19 | order: ['queryString', 'cookie', 'localStorage', 'navigator', 'htmlTag', 'path', 'subdomain'],
20 | caches: ['localStorage', 'cookie']
21 | },
22 | interpolation: { escapeValue: false },
23 | });
24 |
25 | export default i18n;
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import App from './Entrance/App';
5 | import reportWebVitals from './Entrance/reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
--------------------------------------------------------------------------------
/src/types/svg.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | import React = require('react');
3 | export const ReactComponent: React.FC>;
4 | const src: string;
5 | export default src;
6 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
5 |
6 | /* Basic Options */
7 | // "incremental": true, /* Enable incremental compilation */
8 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
9 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
10 | // "lib": [], /* Specify library files to be included in the compilation. */
11 | // "allowJs": true, /* Allow javascript files to be compiled. */
12 | // "checkJs": true, /* Report errors in .js files. */
13 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
14 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
16 | // "sourceMap": true, /* Generates corresponding '.map' file. */
17 | // "outFile": "./", /* Concatenate and emit output to single file. */
18 | // "outDir": "./", /* Redirect output structure to the directory. */
19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
20 | // "composite": true, /* Enable project compilation */
21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
22 | // "removeComments": true, /* Do not emit comments to output. */
23 | // "noEmit": true, /* Do not emit outputs. */
24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
27 |
28 | /* Strict Type-Checking Options */
29 | "strict": true, /* Enable all strict type-checking options. */
30 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
31 | // "strictNullChecks": true, /* Enable strict null checks. */
32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
37 |
38 | /* Additional Checks */
39 | // "noUnusedLocals": true, /* Report errors on unused locals. */
40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
43 |
44 | /* Module Resolution Options */
45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
49 | // "typeRoots": [], /* List of folders to include type definitions from. */
50 | // "types": [], /* Type declaration files to be included in compilation. */
51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
52 | "resolveJsonModule": true,
53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
56 |
57 | /* Source Map Options */
58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
62 |
63 | /* Experimental Options */
64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
66 |
67 | /* Advanced Options */
68 | "skipLibCheck": true, /* Skip type checking of declaration files. */
69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
70 | }
71 | }
72 |
--------------------------------------------------------------------------------