Repository: RyanEdo/mini-metro-web Branch: master Commit: fdbd501c7a9f Files: 104 Total size: 572.7 KB Directory structure: gitextract_0n7p8h2y/ ├── .gitignore ├── .vscode/ │ └── settings.json ├── README-cn.md ├── README.md ├── package.json ├── public/ │ ├── beijing.json │ ├── changsha.json │ ├── 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 │ │ │ ├── edit.tsx │ │ │ ├── expand.tsx │ │ │ ├── export.tsx │ │ │ ├── finished.tsx │ │ │ ├── goto.tsx │ │ │ ├── infinity.tsx │ │ │ ├── pageview.tsx │ │ │ ├── plus.tsx │ │ │ ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: .vscode/settings.json ================================================ { "i18n-ally.localesPaths": ["src/i18n/locales"], "i18n-ally.extract.autoDetect": true, "i18n-ally.keystyle": "nested", "i18n-ally.sourceLanguage": "zh", "i18n-ally.displayLanguage": "zh", "i18n-ally.extract.ignored": [ "“\n ", "”\n ", "”\n ", "\n 重做\n ", ".title .click-panel", ".title .click-panel" ], "i18n-ally.extract.ignoredByFiles": { "src/WelcomeTour/WelcomeTour.tsx": [ ".welcome-tour .body" ] } } ================================================ FILE: README-cn.md ================================================ # Mini Metro Web 迷你地铁地图构建工具: `创建迷你地铁风格的地铁线路图`。 支持**无限**站点与线路,支持**多次穿过**同一站点,设置**背景图**,设置**支线**,支持**导出图片**。 [https://mini-metro-web.gitlab.io/](https://mini-metro-web.gitlab.io/) [English Version](https://github.com/RyanEdo/mini-metro-web/blob/master/README.md) ## 更新日志 #### 1.2.1 `修复添加站点时,连续双击站点导致的站点重复添加报错` #### 1.2.0 `支持英文` #### 1.1.1 `增加更多站点形状,新增站点时支持修改默认形状` #### 1.1.0 `支持修改背景颜色与背景图` #### 1.0.2 `加入刷新后快速恢复通知` #### 1.0.1 `加入报错指引` ## 基本用法 ### 菜单 点击左上角标题进入菜单,点击任意空白处退出菜单 ### 创建站点 1. 菜单 => 站点 => 添加站点 2. 点击空白处添加 ### 创建线路 1. 点击任意站点 2. 选择 操作 => 以此为起点新建线路 3. 按照提示依次点击站点添加到线路 ### 删除站点/线路 1. 点击站点/线路 2. 选择 操作 => 删除 ### 设置背景图 1. 点击线路 => 设定背景 2. 选择 `纯白` / `浅黄` / `取色器` 设定背景颜色 3. 选择 `导入背景图` 导入图片 4. 导入图片后,进入修改图片页面,可以拖动修改图片位置,也可以修改图片透明度 ### 导出图片/文件 菜单 => 作为图片/文件导出 ## 进阶用法 请参考应用内教程或[视频教程](https://space.bilibili.com/8217854) ## 构建 和大部分react项目一样,先运行`npm i`,然后: ### `npm start` 本地运行 打开 [http://localhost:3000](http://localhost:3000) 浏览器中查看. ### `npm run build` 打包成静态文件 ### 注意 ##### 建议安装 `i18n-ally`[VS Code 插件地址](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally) 。项目有部分字符串由该插件生成,安装后能自动在字符串位置显示原文。 ================================================ FILE: README.md ================================================ # Mini Metro Web Mini Metro Map Building Tool: `Create mini metro-style subway maps`. Supports **unlimited** stations and lines, supports **multiple crossings** of the same station, set **background images**, set **sub lines**, and supports **exporting images**. https://mini-metro-web.gitlab.io/ [中文文档](https://github.com/RyanEdo/mini-metro-web/blob/master/README-cn.md) ## Changelog #### 1.2.1 `fix adding duplicate stations when double clicking at adding station mode` #### 1.2.0 `Support for English` #### 1.1.1 `Added more station shapes, support for modifying default shapes when adding new stations` #### 1.1.0 `Support for changing background color and background image` #### 1.0.2 `Added quick recovery notification after refresh` #### 1.0.1 `Added error guidance` ## Basic Usage ### Menu Click the title in the top left corner to enter the menu, click any blank area to exit the menu. ### Create Station 1. Menu => Station => Add Station 2. Click on a blank area to add ### Create Line 1. Click any station 2. Select Action => Create new line from this point 3. Follow the prompts to click stations in sequence to add them to the line ### Delete Station/Line 1. Click the station/line 2. Select Action => Delete ### Set Background Image 1. Click the line => Set background 2. Choose `Pure White` / `Light Yellow` / `Color Picker` to set the background color 3. Choose `Import Background Image` to import an image 4. After importing the image, enter the image editing page, where you can drag to adjust the image position and modify the image transparency ### Export Image/File Menu => Export as image/file ## Advanced Usage Please refer to the in-app tutorial or video tutorial ## Build Like most React projects, first run `npm i`, then: ### `npm start` Run locally Open http://localhost:3000 to view in the browser. ### `npm run build` Build into static files ### Note ##### 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. ================================================ FILE: package.json ================================================ { "name": "mini-metro-web", "version": "1.2.1", "private": true, "dependencies": { "@svgr/webpack": "^8.1.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/downloadjs": "^1.4.6", "classnames": "^2.5.1", "downloadjs": "^1.4.7", "driver.js": "^1.3.1", "html-to-image": "^1.11.11", "i18next": "^23.16.3", "i18next-browser-languagedetector": "^8.0.0", "moment": "^2.30.1", "qrcode": "^1.5.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", "react-i18next": "^15.1.0", "react-scripts": "5.0.1", "sass": "^1.62.0", "ua-parser-js": "^1.0.37", "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "@types/qrcode": "^1.5.5", "@types/ua-parser-js": "^0.7.39" } } ================================================ FILE: public/beijing.json ================================================ {"title":"北京","stations":[{"stationId":1,"stationName":"苹果园","position":[2056,3189],"shape":"cicle","lineIds":[1,6]},{"stationId":2,"stationName":"金安桥","position":[1892,3213],"shape":"cicle","lineIds":[1,6,11]},{"stationId":3,"stationName":"四道桥","position":[1606,3286],"shape":"cicle","lineIds":[1]},{"stationId":4,"stationName":"桥户营","position":[1524,3322],"shape":"cicle","lineIds":[1]},{"stationId":5,"stationName":"上岸","position":[1488,3395],"shape":"cicle","lineIds":[1]},{"stationId":6,"stationName":"栗园庄","position":[1499,3488],"shape":"cicle","lineIds":[1]},{"stationId":7,"stationName":"小园","position":[1410,3542],"shape":"cicle","lineIds":[1],"tagDirection":3},{"stationId":8,"stationName":"石厂","position":[1269,3552],"shape":"cicle","lineIds":[1]},{"stationId":9,"stationName":"古城","position":[2169,3372],"shape":"cicle","lineIds":[2]},{"stationId":10,"stationName":"八角游乐园","position":[2393,3372],"shape":"cicle","lineIds":[2]},{"stationId":11,"stationName":"八宝山","position":[2626,3372],"shape":"cicle","lineIds":[2]},{"stationId":12,"stationName":"玉泉路","position":[2795,3372],"shape":"cicle","lineIds":[2]},{"stationId":13,"stationName":"五棵松","position":[3006,3372],"shape":"cicle","lineIds":[2]},{"stationId":14,"stationName":"万寿路","position":[3214,3372],"shape":"cicle","lineIds":[2]},{"stationId":15,"stationName":"公主坟","position":[3365,3372],"shape":"cicle","lineIds":[2,10]},{"stationId":16,"stationName":"军事博物馆","position":[3481,3372],"shape":"cicle","lineIds":[2,9]},{"stationId":17,"stationName":"木樨地","position":[3642,3372],"shape":"cicle","lineIds":[2,15],"tagDirection":3},{"stationId":18,"stationName":"南礼士路","position":[3792,3374],"shape":"cicle","lineIds":[2],"tagDirection":7},{"stationId":19,"stationName":"复兴门","position":[3835,3374],"shape":"cicle","lineIds":[2,3],"tagDirection":5},{"stationId":20,"stationName":"西单","position":[4007,3372],"shape":"cicle","lineIds":[2,4]},{"stationId":21,"stationName":"天安门西","position":[4179,3372],"shape":"cicle","lineIds":[2],"tagDirection":0},{"stationId":22,"stationName":"天安门东","position":[4278,3368],"shape":"cicle","lineIds":[2],"tagDirection":0},{"stationId":23,"stationName":"王府井","position":[4382,3366],"shape":"cicle","lineIds":[2,8],"tagDirection":4},{"stationId":24,"stationName":"东单","position":[4451,3363],"shape":"cicle","lineIds":[2,5],"tagDirection":1},{"stationId":25,"stationName":"建国门","position":[4624,3361],"shape":"cicle","lineIds":[2,3],"tagDirection":1},{"stationId":26,"stationName":"永安里","position":[4771,3362],"shape":"cicle","lineIds":[2]},{"stationId":27,"stationName":"国贸","position":[4884,3355],"shape":"cicle","lineIds":[2,10],"tagDirection":1},{"stationId":28,"stationName":"大望路","position":[5024,3363],"shape":"cicle","lineIds":[2,13],"tagDirection":1},{"stationId":29,"stationName":"四惠","position":[5221,3359],"shape":"cicle","lineIds":[2]},{"stationId":30,"stationName":"四惠东","position":[5423,3361],"shape":"cicle","lineIds":[2]},{"stationId":31,"stationName":"高碑店","position":[5580,3352],"shape":"cicle","lineIds":[2]},{"stationId":32,"stationName":"传媒大学","position":[5812,3354],"shape":"cicle","lineIds":[2]},{"stationId":33,"stationName":"双桥","position":[6034,3345],"shape":"cicle","lineIds":[2]},{"stationId":34,"stationName":"管庄","position":[6256,3355],"shape":"cicle","lineIds":[2]},{"stationId":35,"stationName":"八里桥","position":[6453,3385],"shape":"cicle","lineIds":[2]},{"stationId":36,"stationName":"通州北苑","position":[6639,3408],"shape":"cicle","lineIds":[2]},{"stationId":37,"stationName":"果园","position":[6732,3513],"shape":"cicle","lineIds":[2]},{"stationId":38,"stationName":"九棵树","position":[6841,3543],"shape":"cicle","lineIds":[2]},{"stationId":39,"stationName":"梨园","position":[6953,3609],"shape":"cicle","lineIds":[2]},{"stationId":40,"stationName":"临河里","position":[7054,3691],"shape":"cicle","lineIds":[2]},{"stationId":41,"stationName":"土桥","position":[7130,3727],"shape":"cicle","lineIds":[2]},{"stationId":42,"stationName":"花庄","position":[7221,3879],"shape":"cicle","lineIds":[2,7]},{"stationId":43,"stationName":"环球度假区","position":[7057,3955],"shape":"cicle","lineIds":[2,7]},{"stationId":44,"stationName":"积水潭","position":[3997,2960],"shape":"cicle","lineIds":[3,18],"tagDirection":1},{"stationId":45,"stationName":"鼓楼大街","position":[4204,2957],"shape":"cicle","lineIds":[3,8]},{"stationId":46,"stationName":"安定门","position":[4348,2954],"shape":"cicle","lineIds":[3]},{"stationId":47,"stationName":"雍和宫","position":[4437,2953],"shape":"cicle","lineIds":[3,5]},{"stationId":48,"stationName":"东直门","position":[4624,3030],"shape":"cicle","lineIds":[3,12,22],"tagDirection":7},{"stationId":49,"stationName":"东四十条","position":[4607,3108],"shape":"cicle","lineIds":[3],"tagDirection":3},{"stationId":50,"stationName":"朝阳门","position":[4612,3201],"shape":"cicle","lineIds":[3,6],"tagDirection":7},{"stationId":51,"stationName":"北京站","position":[4539,3396],"shape":"cicle","lineIds":[3]},{"stationId":52,"stationName":"崇文门","position":[4437,3436],"shape":"cicle","lineIds":[3,5],"tagDirection":5},{"stationId":53,"stationName":"前门","position":[4245,3444],"shape":"cicle","lineIds":[3,8],"tagDirection":5},{"stationId":54,"stationName":"和平门","position":[4108,3445],"shape":"cicle","lineIds":[3],"tagDirection":4},{"stationId":55,"stationName":"宣武门","position":[4009,3449],"shape":"cicle","lineIds":[3,4],"tagDirection":5},{"stationId":56,"stationName":"长椿街","position":[3900,3452],"shape":"cicle","lineIds":[3]},{"stationId":57,"stationName":"阜成门","position":[3826,3209],"shape":"cicle","lineIds":[3]},{"stationId":58,"stationName":"车公庄","position":[3810,3122],"shape":"cicle","lineIds":[3,6]},{"stationId":59,"stationName":"西直门","position":[3820,3041],"shape":"cicle","lineIds":[3,4,12]},{"stationId":60,"stationName":"天宫院","position":[3465,5743],"shape":"cicle","lineIds":[4]},{"stationId":61,"stationName":"生物医药基地","position":[3483,5581],"shape":"cicle","lineIds":[4]},{"stationId":62,"stationName":"义和庄","position":[3457,5322],"shape":"cicle","lineIds":[4]},{"stationId":63,"stationName":"黄村火车站","position":[3592,5217],"shape":"cicle","lineIds":[4]},{"stationId":64,"stationName":"黄村西大街","position":[3592,5129],"shape":"cicle","lineIds":[4]},{"stationId":65,"stationName":"清源路","position":[3591,5019],"shape":"cicle","lineIds":[4]},{"stationId":66,"stationName":"枣园","position":[3588,4912],"shape":"cicle","lineIds":[4]},{"stationId":67,"stationName":"高米店南","position":[3582,4811],"shape":"cicle","lineIds":[4]},{"stationId":68,"stationName":"高米店北","position":[3574,4711],"shape":"cicle","lineIds":[4]},{"stationId":69,"stationName":"西红门","position":[3553,4548],"shape":"cicle","lineIds":[4]},{"stationId":70,"stationName":"新宫","position":[3922,4320],"shape":"cicle","lineIds":[4,18]},{"stationId":71,"stationName":"公益西桥","position":[3974,4085],"shape":"cicle","lineIds":[4],"tagDirection":2},{"stationId":72,"stationName":"角门西","position":[3978,3988],"shape":"cicle","lineIds":[4,10]},{"stationId":73,"stationName":"马家堡","position":[3980,3915],"shape":"cicle","lineIds":[4]},{"stationId":74,"stationName":"北京南站","position":[4056,3796],"shape":"cicle","lineIds":[4,13],"tagDirection":1},{"stationId":75,"stationName":"陶然亭","position":[4010,3662],"shape":"cicle","lineIds":[4]},{"stationId":76,"stationName":"菜市口","position":[4010,3553],"shape":"cicle","lineIds":[4,7],"tagDirection":5},{"stationId":77,"stationName":"灵境胡同","position":[4004,3287],"shape":"cicle","lineIds":[4]},{"stationId":78,"stationName":"西四","position":[3999,3204],"shape":"cicle","lineIds":[4]},{"stationId":79,"stationName":"平安里","position":[3995,3107],"shape":"cicle","lineIds":[4,6,18]},{"stationId":80,"stationName":"新街口","position":[3943,3040],"shape":"cicle","lineIds":[4],"tagDirection":7},{"stationId":81,"stationName":"动物园","position":[3656,3064],"shape":"cicle","lineIds":[4],"tagDirection":1},{"stationId":82,"stationName":"国家图书馆","position":[3518,3015],"shape":"cicle","lineIds":[4,9,15]},{"stationId":83,"stationName":"魏公村","position":[3498,2867],"shape":"cicle","lineIds":[4]},{"stationId":84,"stationName":"人民大学","position":[3480,2777],"shape":"cicle","lineIds":[4]},{"stationId":85,"stationName":"海淀黄庄","position":[3442,2686],"shape":"cicle","lineIds":[4,10],"tagDirection":5},{"stationId":86,"stationName":"中关村","position":[3431,2606],"shape":"cicle","lineIds":[4]},{"stationId":87,"stationName":"北京大学东门","position":[3424,2524],"shape":"cicle","lineIds":[4]},{"stationId":88,"stationName":"圆明园","position":[3368,2450],"shape":"cicle","lineIds":[4]},{"stationId":89,"stationName":"西苑","position":[3175,2464],"shape":"cicle","lineIds":[4,15]},{"stationId":90,"stationName":"北宫门","position":[3043,2423],"shape":"cicle","lineIds":[4]},{"stationId":91,"stationName":"安河桥北","position":[2966,2324],"shape":"cicle","lineIds":[4]},{"stationId":92,"stationName":"宋家庄","position":[4550,3988],"shape":"cicle","lineIds":[5,10,25],"tagDirection":5},{"stationId":93,"stationName":"刘家窑","position":[4487,3871],"shape":"cicle","lineIds":[5]},{"stationId":94,"stationName":"蒲黄榆","position":[4497,3790],"shape":"cicle","lineIds":[5,13],"tagDirection":1},{"stationId":95,"stationName":"天坛东门","position":[4474,3621],"shape":"cicle","lineIds":[5]},{"stationId":96,"stationName":"磁器口","position":[4465,3515],"shape":"cicle","lineIds":[5,7],"tagDirection":5},{"stationId":97,"stationName":"灯市口","position":[4444,3275],"shape":"cicle","lineIds":[5]},{"stationId":98,"stationName":"东四","position":[4441,3203],"shape":"cicle","lineIds":[5,6],"tagDirection":3},{"stationId":99,"stationName":"张自忠路","position":[4438,3110],"shape":"cicle","lineIds":[5]},{"stationId":100,"stationName":"北新桥","position":[4435,3038],"shape":"cicle","lineIds":[5,22]},{"stationId":101,"stationName":"和平里北街","position":[4451,2859],"shape":"cicle","lineIds":[5]},{"stationId":102,"stationName":"和平西桥","position":[4446,2762],"shape":"cicle","lineIds":[5]},{"stationId":103,"stationName":"惠新西街南口","position":[4441,2675],"shape":"cicle","lineIds":[5,10],"tagDirection":1},{"stationId":104,"stationName":"惠新西街北口","position":[4436,2568],"shape":"cicle","lineIds":[5]},{"stationId":105,"stationName":"大屯路东","position":[4440,2408],"shape":"cicle","lineIds":[5,14],"tagDirection":7},{"stationId":106,"stationName":"北苑路北","position":[4447,2142],"shape":"cicle","lineIds":[5]},{"stationId":107,"stationName":"立水桥南","position":[4411,2027],"shape":"cicle","lineIds":[5]},{"stationId":108,"stationName":"立水桥","position":[4390,1916],"shape":"cicle","lineIds":[5,12]},{"stationId":109,"stationName":"天通苑南","position":[4393,1782],"shape":"cicle","lineIds":[5]},{"stationId":110,"stationName":"天通苑","position":[4394,1694],"shape":"cicle","lineIds":[5]},{"stationId":111,"stationName":"天通苑北","position":[4395,1610],"shape":"cicle","lineIds":[5]},{"stationId":112,"stationName":"潞城","position":[7740,3420],"shape":"cicle","lineIds":[6]},{"stationId":113,"stationName":"东夏园","position":[7605,3415],"shape":"cicle","lineIds":[6],"tagDirection":0},{"stationId":114,"stationName":"郝家府","position":[7444,3414],"shape":"cicle","lineIds":[6]},{"stationId":115,"stationName":"北运河东","position":[7337,3414],"shape":"cicle","lineIds":[6],"tagDirection":0},{"stationId":116,"stationName":"北运河西","position":[7150,3416],"shape":"cicle","lineIds":[6]},{"stationId":117,"stationName":"通州北关","position":[6877,3267],"shape":"cicle","lineIds":[6]},{"stationId":118,"stationName":"物资学院路","position":[6659,3178],"shape":"cicle","lineIds":[6]},{"stationId":119,"stationName":"草房","position":[6422,3201],"shape":"cicle","lineIds":[6]},{"stationId":120,"stationName":"常营","position":[6263,3189],"shape":"cicle","lineIds":[6],"tagDirection":0},{"stationId":121,"stationName":"黄渠","position":[6049,3204],"shape":"cicle","lineIds":[6],"tagDirection":0},{"stationId":122,"stationName":"褡裢坡","position":[5906,3206],"shape":"cicle","lineIds":[6]},{"stationId":123,"stationName":"青年路","position":[5440,3215],"shape":"cicle","lineIds":[6]},{"stationId":124,"stationName":"十里堡","position":[5286,3215],"shape":"cicle","lineIds":[6]},{"stationId":125,"stationName":"金台路","position":[5047,3211],"shape":"cicle","lineIds":[6,13],"tagDirection":7},{"stationId":126,"stationName":"呼家楼","position":[4882,3213],"shape":"cicle","lineIds":[6,10],"tagDirection":7},{"stationId":127,"stationName":"东大桥","position":[4783,3216],"shape":"cicle","lineIds":[6]},{"stationId":128,"stationName":"南锣鼓巷","position":[4308,3108],"shape":"cicle","lineIds":[6,8],"tagDirection":1},{"stationId":129,"stationName":"北海北","position":[4134,3114],"shape":"cicle","lineIds":[6]},{"stationId":130,"stationName":"车公庄西","position":[3707,3122],"shape":"cicle","lineIds":[6],"tagDirection":4},{"stationId":131,"stationName":"二里沟","position":[3603,3122],"shape":"cicle","lineIds":[6,15]},{"stationId":132,"stationName":"白石桥南","position":[3523,3116],"shape":"cicle","lineIds":[6,9],"tagDirection":7},{"stationId":133,"stationName":"花园桥","position":[3373,3123],"shape":"cicle","lineIds":[6]},{"stationId":134,"stationName":"慈寿寺","position":[3221,3114],"shape":"cicle","lineIds":[6,10],"tagDirection":5},{"stationId":135,"stationName":"海淀五路居","position":[3031,3120],"shape":"cicle","lineIds":[6]},{"stationId":136,"stationName":"田村","position":[2795,3151],"shape":"cicle","lineIds":[6]},{"stationId":137,"stationName":"廖公庄","position":[2539,3122],"shape":"cicle","lineIds":[6]},{"stationId":138,"stationName":"西黄村","position":[2335,3110],"shape":"cicle","lineIds":[6]},{"stationId":139,"stationName":"杨庄","position":[2136,3168],"shape":"cicle","lineIds":[6],"tagDirection":3},{"stationId":140,"stationName":"高楼金","position":[7109,3815],"shape":"cicle","lineIds":[7]},{"stationId":141,"stationName":"群芳","position":[6974,3811],"shape":"cicle","lineIds":[7]},{"stationId":142,"stationName":"万盛东","position":[6840,3811],"shape":"cicle","lineIds":[7],"tagDirection":0},{"stationId":143,"stationName":"万盛西","position":[6588,3813],"shape":"cicle","lineIds":[7]},{"stationId":144,"stationName":"黑庄户","position":[6257,3859],"shape":"cicle","lineIds":[7]},{"stationId":145,"stationName":"郎辛庄","position":[5964,3881],"shape":"cicle","lineIds":[7]},{"stationId":146,"stationName":"黄厂","position":[5804,3956],"shape":"cicle","lineIds":[7]},{"stationId":147,"stationName":"焦化厂","position":[5639,3891],"shape":"cicle","lineIds":[7]},{"stationId":148,"stationName":"双合","position":[5534,3849],"shape":"cicle","lineIds":[7]},{"stationId":149,"stationName":"垡头","position":[5384,3839],"shape":"cicle","lineIds":[7]},{"stationId":150,"stationName":"欢乐谷景区","position":[5267,3781],"shape":"cicle","lineIds":[7]},{"stationId":151,"stationName":"南楼梓庄","position":[5277,3700],"shape":"cicle","lineIds":[7]},{"stationId":152,"stationName":"化工","position":[5300,3573],"shape":"cicle","lineIds":[7]},{"stationId":153,"stationName":"百子湾","position":[5244,3521],"shape":"cicle","lineIds":[7]},{"stationId":154,"stationName":"大郊亭","position":[5145,3514],"shape":"cicle","lineIds":[7]},{"stationId":155,"stationName":"九龙山","position":[5055,3514],"shape":"cicle","lineIds":[7,13],"tagDirection":7},{"stationId":156,"stationName":"双井","position":[4884,3511],"shape":"cicle","lineIds":[7,10],"tagDirection":7},{"stationId":157,"stationName":"广渠门外","position":[4756,3510],"shape":"cicle","lineIds":[7]},{"stationId":158,"stationName":"广渠门内","position":[4605,3510],"shape":"cicle","lineIds":[7],"tagDirection":0},{"stationId":159,"stationName":"桥湾","position":[4351,3519],"shape":"cicle","lineIds":[7]},{"stationId":160,"stationName":"珠市口","position":[4250,3533],"shape":"cicle","lineIds":[7,8]},{"stationId":161,"stationName":"虎坊桥","position":[4112,3551],"shape":"cicle","lineIds":[7],"tagDirection":3},{"stationId":162,"stationName":"广安门内","position":[3848,3552],"shape":"cicle","lineIds":[7]},{"stationId":163,"stationName":"达官营","position":[3622,3547],"shape":"cicle","lineIds":[7,15]},{"stationId":164,"stationName":"湾子","position":[3544,3547],"shape":"cicle","lineIds":[7]},{"stationId":165,"stationName":"北京西站","position":[3478,3499],"shape":"cicle","lineIds":[7,9],"tagDirection":1},{"stationId":166,"stationName":"瀛海","position":[4756,4839],"shape":"cicle","lineIds":[8]},{"stationId":167,"stationName":"德茂","position":[4681,4720],"shape":"cicle","lineIds":[8]},{"stationId":168,"stationName":"五福堂","position":[4505,4596],"shape":"cicle","lineIds":[8]},{"stationId":169,"stationName":"火箭万源","position":[4452,4453],"shape":"cicle","lineIds":[8]},{"stationId":170,"stationName":"东高地","position":[4360,4396],"shape":"cicle","lineIds":[8]},{"stationId":171,"stationName":"和义","position":[4287,4298],"shape":"cicle","lineIds":[8]},{"stationId":172,"stationName":"大红门南","position":[4276,4079],"shape":"cicle","lineIds":[8]},{"stationId":173,"stationName":"海户屯","position":[4269,3928],"shape":"cicle","lineIds":[8]},{"stationId":174,"stationName":"木樨园","position":[4264,3854],"shape":"cicle","lineIds":[8]},{"stationId":175,"stationName":"永定门外","position":[4260,3772],"shape":"cicle","lineIds":[8,13]},{"stationId":176,"stationName":"天桥","position":[4253,3627],"shape":"cicle","lineIds":[8]},{"stationId":177,"stationName":"金鱼胡同","position":[4378,3292],"shape":"cicle","lineIds":[8],"tagDirection":6},{"stationId":178,"stationName":"中国美术馆","position":[4374,3209],"shape":"cicle","lineIds":[8],"tagDirection":6},{"stationId":179,"stationName":"什刹海","position":[4228,3070],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":180,"stationName":"安德里北街","position":[4218,2874],"shape":"cicle","lineIds":[8]},{"stationId":181,"stationName":"安华桥","position":[4213,2761],"shape":"cicle","lineIds":[8]},{"stationId":182,"stationName":"北土城","position":[4208,2677],"shape":"cicle","lineIds":[8,10],"tagDirection":1},{"stationId":183,"stationName":"奥体中心","position":[4204,2588],"shape":"cicle","lineIds":[8]},{"stationId":184,"stationName":"奥林匹克公园","position":[4184,2424],"shape":"cicle","lineIds":[8,14],"tagDirection":5},{"stationId":185,"stationName":"森林公园南门","position":[4192,2345],"shape":"cicle","lineIds":[8]},{"stationId":186,"stationName":"林萃桥","position":[3996,2227],"shape":"cicle","lineIds":[8]},{"stationId":187,"stationName":"永泰庄","position":[3812,2069],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":188,"stationName":"西小口","position":[3782,1978],"shape":"cicle","lineIds":[8]},{"stationId":189,"stationName":"育新","position":[3739,1846],"shape":"cicle","lineIds":[8]},{"stationId":190,"stationName":"霍营","position":[3869,1728],"shape":"cicle","lineIds":[8,12]},{"stationId":191,"stationName":"回龙观东大街","position":[3896,1634],"shape":"cicle","lineIds":[8]},{"stationId":192,"stationName":"平西府","position":[3770,1540],"shape":"cicle","lineIds":[8]},{"stationId":193,"stationName":"育知路","position":[3536,1568],"shape":"cicle","lineIds":[8]},{"stationId":194,"stationName":"朱辛庄","position":[3403,1403],"shape":"cicle","lineIds":[8,19]},{"stationId":195,"stationName":"郭公庄","position":[3285,4303],"shape":"cicle","lineIds":[9,21]},{"stationId":196,"stationName":"丰台科技园","position":[3238,4194],"shape":"cicle","lineIds":[9]},{"stationId":197,"stationName":"科怡路","position":[3240,4121],"shape":"cicle","lineIds":[9]},{"stationId":198,"stationName":"丰台南路","position":[3232,4035],"shape":"cicle","lineIds":[9,15]},{"stationId":199,"stationName":"丰台东大街","position":[3205,3895],"shape":"cicle","lineIds":[9]},{"stationId":200,"stationName":"七里庄","position":[3209,3776],"shape":"cicle","lineIds":[9,13]},{"stationId":201,"stationName":"六里桥","position":[3294,3644],"shape":"cicle","lineIds":[9,10]},{"stationId":202,"stationName":"六里桥东","position":[3417,3577],"shape":"cicle","lineIds":[9]},{"stationId":203,"stationName":"白堆子","position":[3524,3208],"shape":"cicle","lineIds":[9],"tagDirection":6},{"stationId":204,"stationName":"巴沟","position":[3200,2702],"shape":"cicle","lineIds":[10,23],"tagDirection":6},{"stationId":205,"stationName":"苏州街","position":[3329,2690],"shape":"cicle","lineIds":[10,15],"tagDirection":5},{"stationId":206,"stationName":"知春里","position":[3553,2683],"shape":"cicle","lineIds":[10]},{"stationId":207,"stationName":"知春路","position":[3666,2681],"shape":"cicle","lineIds":[10,12]},{"stationId":208,"stationName":"西土城","position":[3809,2683],"shape":"cicle","lineIds":[10,19],"tagDirection":1},{"stationId":209,"stationName":"牡丹园","position":[3964,2680],"shape":"cicle","lineIds":[10,18],"tagDirection":1},{"stationId":210,"stationName":"健德门","position":[4080,2679],"shape":"cicle","lineIds":[10],"tagDirection":1},{"stationId":211,"stationName":"安贞门","position":[4326,2676],"shape":"cicle","lineIds":[10],"tagDirection":1},{"stationId":212,"stationName":"芍药居","position":[4625,2670],"shape":"cicle","lineIds":[10,12]},{"stationId":213,"stationName":"太阳宫","position":[4749,2709],"shape":"cicle","lineIds":[10,17]},{"stationId":214,"stationName":"三元桥","position":[4836,2831],"shape":"cicle","lineIds":[10,22]},{"stationId":215,"stationName":"亮马桥","position":[4884,2952],"shape":"cicle","lineIds":[10]},{"stationId":216,"stationName":"农业展览馆","position":[4883,3033],"shape":"cicle","lineIds":[10],"tagDirection":2},{"stationId":217,"stationName":"团结湖","position":[4884,3109],"shape":"cicle","lineIds":[10],"tagDirection":2},{"stationId":218,"stationName":"金台夕照","position":[4883,3278],"shape":"cicle","lineIds":[10]},{"stationId":219,"stationName":"劲松","position":[4879,3602],"shape":"cicle","lineIds":[10]},{"stationId":220,"stationName":"潘家园","position":[4875,3692],"shape":"cicle","lineIds":[10]},{"stationId":221,"stationName":"十里河","position":[4847,3785],"shape":"cicle","lineIds":[10,13,16],"tagDirection":1},{"stationId":222,"stationName":"分钟寺","position":[4806,3924],"shape":"cicle","lineIds":[10]},{"stationId":223,"stationName":"成寿寺","position":[4741,3987],"shape":"cicle","lineIds":[10],"tagDirection":7},{"stationId":224,"stationName":"石榴庄","position":[4407,3987],"shape":"cicle","lineIds":[10],"tagDirection":4},{"stationId":225,"stationName":"大红门","position":[4258,3992],"shape":"cicle","lineIds":[10],"tagDirection":5},{"stationId":226,"stationName":"角门东","position":[4123,3995],"shape":"cicle","lineIds":[10],"tagDirection":4},{"stationId":227,"stationName":"草桥","position":[3780,3988],"shape":"cicle","lineIds":[10,18,20]},{"stationId":228,"stationName":"纪家庙","position":[3600,4002],"shape":"cicle","lineIds":[10]},{"stationId":229,"stationName":"首经贸","position":[3468,4002],"shape":"cicle","lineIds":[10,21]},{"stationId":230,"stationName":"丰台站","position":[3311,3944],"shape":"cicle","lineIds":[10,15]},{"stationId":231,"stationName":"泥洼","position":[3308,3860],"shape":"cicle","lineIds":[10]},{"stationId":232,"stationName":"西局","position":[3307,3779],"shape":"cicle","lineIds":[10,13],"tagDirection":1},{"stationId":233,"stationName":"莲花桥","position":[3370,3468],"shape":"cicle","lineIds":[10]},{"stationId":234,"stationName":"西钓鱼台","position":[3247,3211],"shape":"cicle","lineIds":[10]},{"stationId":235,"stationName":"车道沟","position":[3204,2967],"shape":"cicle","lineIds":[10]},{"stationId":236,"stationName":"长春桥","position":[3209,2861],"shape":"cicle","lineIds":[10]},{"stationId":237,"stationName":"火器营","position":[3157,2787],"shape":"cicle","lineIds":[10]},{"stationId":238,"stationName":"模式口","position":[1828,3113],"shape":"cicle","lineIds":[11]},{"stationId":239,"stationName":"北辛安","position":[1914,3292],"shape":"cicle","lineIds":[11]},{"stationId":240,"stationName":"新首钢","position":[1914,3350],"shape":"cicle","lineIds":[11]},{"stationId":241,"stationName":"大钟寺","position":[3717,2780],"shape":"cicle","lineIds":[12]},{"stationId":242,"stationName":"五道口","position":[3643,2517],"shape":"cicle","lineIds":[12]},{"stationId":243,"stationName":"上地","position":[3468,2116],"shape":"cicle","lineIds":[12]},{"stationId":244,"stationName":"清河站","position":[3416,2035],"shape":"cicle","lineIds":[12,19]},{"stationId":245,"stationName":"西二旗","position":[3329,1916],"shape":"cicle","lineIds":[12,19]},{"stationId":246,"stationName":"龙泽","position":[3460,1737],"shape":"cicle","lineIds":[12]},{"stationId":247,"stationName":"回龙观","position":[3627,1738],"shape":"cicle","lineIds":[12]},{"stationId":248,"stationName":"北苑","position":[4611,2016],"shape":"cicle","lineIds":[12]},{"stationId":249,"stationName":"望京西","position":[4765,2489],"shape":"cicle","lineIds":[12,14]},{"stationId":250,"stationName":"光熙门","position":[4584,2763],"shape":"cicle","lineIds":[12],"tagDirection":2},{"stationId":251,"stationName":"柳芳","position":[4593,2865],"shape":"cicle","lineIds":[12],"tagDirection":2},{"stationId":252,"stationName":"善各庄","position":[5048,2175],"shape":"cicle","lineIds":[13]},{"stationId":253,"stationName":"来广营","position":[4936,2242],"shape":"cicle","lineIds":[13]},{"stationId":254,"stationName":"东湖渠","position":[4940,2341],"shape":"cicle","lineIds":[13]},{"stationId":255,"stationName":"望京","position":[4960,2461],"shape":"cicle","lineIds":[13,14]},{"stationId":256,"stationName":"阜通","position":[4981,2528],"shape":"cicle","lineIds":[13]},{"stationId":257,"stationName":"望京南","position":[5085,2600],"shape":"cicle","lineIds":[13]},{"stationId":258,"stationName":"将台","position":[5168,2736],"shape":"cicle","lineIds":[13]},{"stationId":259,"stationName":"东风北桥","position":[5125,2862],"shape":"cicle","lineIds":[13]},{"stationId":260,"stationName":"枣营","position":[5016,3005],"shape":"cicle","lineIds":[13],"tagDirection":2},{"stationId":261,"stationName":"朝阳公园","position":[5049,3111],"shape":"cicle","lineIds":[13]},{"stationId":262,"stationName":"平乐园","position":[5039,3593],"shape":"cicle","lineIds":[13]},{"stationId":263,"stationName":"北工大西门","position":[5039,3693],"shape":"cicle","lineIds":[13]},{"stationId":264,"stationName":"方庄","position":[4668,3788],"shape":"cicle","lineIds":[13],"tagDirection":0},{"stationId":265,"stationName":"景泰","position":[4376,3794],"shape":"cicle","lineIds":[13]},{"stationId":266,"stationName":"景风门","position":[3926,3820],"shape":"cicle","lineIds":[13,18],"tagDirection":1},{"stationId":267,"stationName":"西铁营","position":[3826,3833],"shape":"cicle","lineIds":[13]},{"stationId":268,"stationName":"菜户营","position":[3690,3770],"shape":"cicle","lineIds":[13],"tagDirection":4},{"stationId":269,"stationName":"丽泽商务区","position":[3579,3769],"shape":"cicle","lineIds":[13],"tagDirection":4},{"stationId":270,"stationName":"东管头","position":[3478,3771],"shape":"cicle","lineIds":[13],"tagDirection":0},{"stationId":271,"stationName":"大井","position":[3027,3794],"shape":"cicle","lineIds":[13],"tagDirection":7},{"stationId":272,"stationName":"郭庄子","position":[2797,3798],"shape":"cicle","lineIds":[13]},{"stationId":273,"stationName":"大瓦窑","position":[2671,3852],"shape":"cicle","lineIds":[13]},{"stationId":274,"stationName":"园博园","position":[2282,3833],"shape":"cicle","lineIds":[13]},{"stationId":275,"stationName":"张郭庄","position":[2138,3865],"shape":"cicle","lineIds":[13]},{"stationId":276,"stationName":"俸伯","position":[7113,1121],"shape":"cicle","lineIds":[14]},{"stationId":277,"stationName":"顺义","position":[6836,1146],"shape":"cicle","lineIds":[14],"tagDirection":0},{"stationId":278,"stationName":"石门","position":[6677,1148],"shape":"cicle","lineIds":[14],"tagDirection":7},{"stationId":279,"stationName":"南法信","position":[6361,1161],"shape":"cicle","lineIds":[14]},{"stationId":280,"stationName":"后沙峪","position":[5908,1305],"shape":"cicle","lineIds":[14]},{"stationId":281,"stationName":"花梨坎","position":[5842,1602],"shape":"cicle","lineIds":[14]},{"stationId":282,"stationName":"国展","position":[5817,1746],"shape":"cicle","lineIds":[14]},{"stationId":283,"stationName":"孙河","position":[5613,1995],"shape":"cicle","lineIds":[14]},{"stationId":284,"stationName":"马泉营","position":[5301,2109],"shape":"cicle","lineIds":[14]},{"stationId":285,"stationName":"崔各庄","position":[5196,2224],"shape":"cicle","lineIds":[14]},{"stationId":286,"stationName":"望京东","position":[5137,2414],"shape":"cicle","lineIds":[14]},{"stationId":287,"stationName":"关庄","position":[4576,2435],"shape":"cicle","lineIds":[14]},{"stationId":288,"stationName":"安立路","position":[4344,2420],"shape":"cicle","lineIds":[14],"tagDirection":4},{"stationId":289,"stationName":"北沙滩","position":[3947,2431],"shape":"cicle","lineIds":[14],"tagDirection":7},{"stationId":290,"stationName":"六道口","position":[3794,2446],"shape":"cicle","lineIds":[14,19]},{"stationId":291,"stationName":"清华东路西口","position":[3661,2440],"shape":"cicle","lineIds":[14]},{"stationId":292,"stationName":"北安河","position":[1570,1765],"shape":"cicle","lineIds":[15]},{"stationId":293,"stationName":"温阳路","position":[1880,1761],"shape":"cicle","lineIds":[15]},{"stationId":294,"stationName":"稻香湖路","position":[2148,1757],"shape":"cicle","lineIds":[15]},{"stationId":295,"stationName":"屯佃","position":[2425,1762],"shape":"cicle","lineIds":[15]},{"stationId":296,"stationName":"永丰","position":[2651,1728],"shape":"cicle","lineIds":[15]},{"stationId":297,"stationName":"永丰南","position":[2748,1790],"shape":"cicle","lineIds":[15]},{"stationId":298,"stationName":"西北旺","position":[2845,1959],"shape":"cicle","lineIds":[15]},{"stationId":299,"stationName":"马连洼","position":[2991,2120],"shape":"cicle","lineIds":[15]},{"stationId":300,"stationName":"农大南路","position":[3089,2232],"shape":"cicle","lineIds":[15]},{"stationId":301,"stationName":"万泉河桥","position":[3268,2596],"shape":"cicle","lineIds":[15]},{"stationId":302,"stationName":"苏州桥","position":[3347,2826],"shape":"cicle","lineIds":[15]},{"stationId":303,"stationName":"万寿寺","position":[3365,2969],"shape":"cicle","lineIds":[15]},{"stationId":304,"stationName":"甘家口","position":[3611,3212],"shape":"cicle","lineIds":[15],"tagDirection":2},{"stationId":305,"stationName":"玉渊潭东门","position":[3612,3306],"shape":"cicle","lineIds":[15],"tagDirection":2},{"stationId":306,"stationName":"红莲南路","position":[3633,3671],"shape":"cicle","lineIds":[15]},{"stationId":307,"stationName":"东管头南","position":[3497,3890],"shape":"cicle","lineIds":[15,21]},{"stationId":308,"stationName":"富丰桥","position":[3136,4084],"shape":"cicle","lineIds":[15],"tagDirection":7},{"stationId":309,"stationName":"看丹","position":[2991,4097],"shape":"cicle","lineIds":[15]},{"stationId":310,"stationName":"榆树庄","position":[2856,4059],"shape":"cicle","lineIds":[15]},{"stationId":311,"stationName":"洪泰庄","position":[2759,3961],"shape":"cicle","lineIds":[15]},{"stationId":312,"stationName":"宛平城","position":[2562,3956],"shape":"cicle","lineIds":[15]},{"stationId":313,"stationName":"周家庄","position":[5030,3879],"shape":"cicle","lineIds":[16]},{"stationId":314,"stationName":"十八里店","position":[5225,4003],"shape":"cicle","lineIds":[16]},{"stationId":315,"stationName":"北神树","position":[5794,4260],"shape":"cicle","lineIds":[16]},{"stationId":316,"stationName":"次渠北","position":[6033,4352],"shape":"cicle","lineIds":[16]},{"stationId":317,"stationName":"次渠","position":[6182,4411],"shape":"cicle","lineIds":[16,25]},{"stationId":318,"stationName":"嘉会湖","position":[6369,4521],"shape":"cicle","lineIds":[16]},{"stationId":319,"stationName":"未来科学城北","position":[4957,1143],"shape":"cicle","lineIds":[17]},{"stationId":320,"stationName":"未来科学城","position":[4934,1290],"shape":"cicle","lineIds":[17]},{"stationId":321,"stationName":"天通苑东","position":[4689,1758],"shape":"cicle","lineIds":[17]},{"stationId":322,"stationName":"清河营","position":[4687,1938],"shape":"cicle","lineIds":[17]},{"stationId":323,"stationName":"红军营","position":[4684,2109],"shape":"cicle","lineIds":[17]},{"stationId":324,"stationName":"西坝河","position":[4659,2798],"shape":"cicle","lineIds":[17],"tagDirection":2},{"stationId":325,"stationName":"左家庄","position":[4693,2947],"shape":"cicle","lineIds":[17]},{"stationId":326,"stationName":"工人体育场","position":[4771,3104],"shape":"cicle","lineIds":[17],"tagDirection":6},{"stationId":327,"stationName":"新发地","position":[3729,4212],"shape":"cicle","lineIds":[18]},{"stationId":328,"stationName":"牛街","position":[3901,3550],"shape":"cicle","lineIds":[18],"tagDirection":1},{"stationId":329,"stationName":"太平桥","position":[3899,3349],"shape":"cicle","lineIds":[18],"tagDirection":7},{"stationId":330,"stationName":"北太平庄","position":[3967,2758],"shape":"cicle","lineIds":[18]},{"stationId":331,"stationName":"昌平西山口","position":[2220,0],"shape":"cicle","lineIds":[19]},{"stationId":332,"stationName":"十三陵景区","position":[2343,44],"shape":"cicle","lineIds":[19]},{"stationId":333,"stationName":"昌平","position":[2602,241],"shape":"cicle","lineIds":[19]},{"stationId":334,"stationName":"昌平东关","position":[2887,229],"shape":"cicle","lineIds":[19]},{"stationId":335,"stationName":"北邵洼","position":[3086,226],"shape":"cicle","lineIds":[19]},{"stationId":336,"stationName":"南邵","position":[3141,371],"shape":"cicle","lineIds":[19]},{"stationId":337,"stationName":"沙河高教园","position":[3071,800],"shape":"cicle","lineIds":[19]},{"stationId":338,"stationName":"沙河","position":[3155,963],"shape":"cicle","lineIds":[19]},{"stationId":339,"stationName":"巩华城","position":[3206,1137],"shape":"cicle","lineIds":[19]},{"stationId":340,"stationName":"生命科学园","position":[3208,1498],"shape":"cicle","lineIds":[19]},{"stationId":341,"stationName":"朱房北","position":[3599,2085],"shape":"cicle","lineIds":[19]},{"stationId":342,"stationName":"清河小营桥","position":[3703,2099],"shape":"cicle","lineIds":[19],"tagDirection":5},{"stationId":343,"stationName":"学知园","position":[3788,2296],"shape":"cicle","lineIds":[19]},{"stationId":344,"stationName":"学院桥","position":[3800,2564],"shape":"cicle","lineIds":[19]},{"stationId":345,"stationName":"大兴新城","position":[3921,5090],"shape":"cicle","lineIds":[20]},{"stationId":346,"stationName":"大兴机场","position":[4433,7332],"shape":"cicle","lineIds":[20]},{"stationId":347,"stationName":"阎村东","position":[1274,5156],"shape":"cicle","lineIds":[21,24]},{"stationId":348,"stationName":"苏庄","position":[1519,5214],"shape":"cicle","lineIds":[21]},{"stationId":349,"stationName":"良乡南关","position":[1674,5214],"shape":"cicle","lineIds":[21]},{"stationId":350,"stationName":"良乡大学城西","position":[1829,5215],"shape":"cicle","lineIds":[21]},{"stationId":351,"stationName":"良乡大学城","position":[2031,5215],"shape":"cicle","lineIds":[21]},{"stationId":352,"stationName":"良乡大学城北","position":[2101,5147],"shape":"cicle","lineIds":[21]},{"stationId":353,"stationName":"广阳城","position":[2116,4967],"shape":"cicle","lineIds":[21]},{"stationId":354,"stationName":"篱笆房","position":[2161,4840],"shape":"cicle","lineIds":[21]},{"stationId":355,"stationName":"长阳","position":[2393,4808],"shape":"cicle","lineIds":[21]},{"stationId":356,"stationName":"稻田","position":[2454,4497],"shape":"cicle","lineIds":[21]},{"stationId":357,"stationName":"大葆台","position":[3183,4368],"shape":"cicle","lineIds":[21]},{"stationId":358,"stationName":"白盆窑","position":[3400,4250],"shape":"cicle","lineIds":[21]},{"stationId":359,"stationName":"花乡东桥","position":[3467,4154],"shape":"cicle","lineIds":[21]},{"stationId":360,"stationName":"3号航站楼","position":[6422,1920],"shape":"cicle","lineIds":[22]},{"stationId":361,"stationName":"2号航站楼","position":[6194,1653],"shape":"cicle","lineIds":[22]},{"stationId":362,"stationName":"颐和园西门","position":[2899,2589],"shape":"cicle","lineIds":[23]},{"stationId":363,"stationName":"茶棚","position":[2747,2625],"shape":"cicle","lineIds":[23]},{"stationId":364,"stationName":"万安","position":[2586,2606],"shape":"cicle","lineIds":[23]},{"stationId":365,"stationName":"国家植物园","position":[2423,2511],"shape":"cicle","lineIds":[23]},{"stationId":366,"stationName":"香山","position":[2311,2506],"shape":"cicle","lineIds":[23]},{"stationId":367,"stationName":"紫草坞","position":[1133,5200],"shape":"cicle","lineIds":[24]},{"stationId":368,"stationName":"阎村","position":[1068,5279],"shape":"cicle","lineIds":[24]},{"stationId":369,"stationName":"星城","position":[880,5309],"shape":"cicle","lineIds":[24]},{"stationId":370,"stationName":"大石河东","position":[665,5347],"shape":"cicle","lineIds":[24]},{"stationId":371,"stationName":"马各庄","position":[432,5395],"shape":"cicle","lineIds":[24]},{"stationId":372,"stationName":"饶乐府","position":[327,5432],"shape":"cicle","lineIds":[24]},{"stationId":373,"stationName":"房山城关","position":[162,5386],"shape":"cicle","lineIds":[24]},{"stationId":374,"stationName":"燕山","position":[0,5250],"shape":"cicle","lineIds":[24]},{"stationId":375,"stationName":"肖村","position":[4750,4104],"shape":"cicle","lineIds":[25]},{"stationId":376,"stationName":"小红门","position":[4858,4167],"shape":"cicle","lineIds":[25]},{"stationId":377,"stationName":"旧宫","position":[4874,4377],"shape":"cicle","lineIds":[25]},{"stationId":378,"stationName":"亦庄桥","position":[5069,4416],"shape":"cicle","lineIds":[25]},{"stationId":379,"stationName":"亦庄文化园","position":[5172,4377],"shape":"cicle","lineIds":[25]},{"stationId":380,"stationName":"万源街","position":[5320,4417],"shape":"cicle","lineIds":[25]},{"stationId":381,"stationName":"荣京东街","position":[5399,4514],"shape":"cicle","lineIds":[25]},{"stationId":382,"stationName":"荣昌东街","position":[5483,4618],"shape":"cicle","lineIds":[25]},{"stationId":383,"stationName":"同济南路","position":[5665,4717],"shape":"cicle","lineIds":[25]},{"stationId":384,"stationName":"经海路","position":[5889,4610],"shape":"cicle","lineIds":[25]},{"stationId":385,"stationName":"次渠南","position":[6080,4495],"shape":"cicle","lineIds":[25]},{"stationId":386,"stationName":"亦庄火车站","position":[6285,4320],"shape":"cicle","lineIds":[25]}],"lines":[{"lineId":1,"lineName":"S1线","color":"#B35A1F","stationIds":[1,2,3,4,5,6,7,8],"sign":"S1线","order":1,"bendFirst":[true,true,true,true,true,true,true,true]},{"lineId":2,"lineName":"1号线八通线","color":"#CC0000","stationIds":[9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"sign":"1","order":2,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":3,"lineName":"2号线","color":"#0065B3","stationIds":[44,45,46,47,48,49,50,25,51,52,53,54,55,56,19,57,58,59,44],"sign":"2","order":3,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":4,"lineName":"4号线大兴线","color":"#008187","stationIds":[60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,55,20,77,78,79,80,59,81,82,83,84,85,86,87,88,89,90,91],"sign":"4","order":4,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":5,"lineName":"5号线","color":"#A61D7F","stationIds":[92,93,94,95,96,52,24,97,98,99,100,47,101,102,103,104,105,106,107,108,109,110,111],"sign":"5","order":5,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":6,"lineName":"6号线","color":"#D0970A","stationIds":[112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,50,98,128,129,79,58,130,131,132,133,134,135,136,137,138,139,1,2],"sign":"6","order":6,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":7,"lineName":"7号线","color":"#F9BE58","stationIds":[43,42,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,96,159,160,161,76,162,163,164,165],"sign":"7","order":7,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":8,"lineName":"8号线","color":"#018237","stationIds":[166,167,168,169,170,171,172,173,174,175,176,160,53,23,177,178,128,179,45,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194],"sign":"8","order":8,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":9,"lineName":"9号线","color":"#86B81C","stationIds":[195,196,197,198,199,200,201,202,165,16,203,132,82],"sign":"9","order":9,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":10,"lineName":"10号线","color":"#019AC3","stationIds":[204,205,85,206,207,208,209,210,182,211,103,212,213,214,215,216,217,126,218,27,156,219,220,221,222,223,92,224,225,226,72,227,228,229,230,231,232,201,233,15,234,134,235,236,237,204],"sign":"10","order":10,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":11,"lineName":"11号线","color":"#EE3F42","stationIds":[238,2,239,240],"sign":"11","order":11,"bendFirst":[true,true,true,true]},{"lineId":12,"lineName":"13号线","color":"#FCD600","stationIds":[59,241,207,242,243,244,245,246,247,190,108,248,249,212,250,251,48],"sign":"13","order":12,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":13,"lineName":"14号线","color":"#E4A8A3","stationIds":[252,253,254,255,256,257,258,259,260,261,125,28,155,262,263,221,264,94,265,175,74,266,267,268,269,270,232,200,271,272,273,274,275],"sign":"14","order":13,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":14,"lineName":"15号线","color":"#793E8C","stationIds":[276,277,278,279,280,281,282,283,284,285,286,255,249,287,105,288,184,289,290,291],"sign":"15","order":14,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":15,"lineName":"16号线","color":"#6CB46B","stationIds":[292,293,294,295,296,297,298,299,300,89,301,205,302,303,82,131,304,305,17,163,306,307,230,198,308,309,310,311,312],"sign":"16","order":15,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":16,"lineName":"17号线","color":"#00AD8E","stationIds":[221,313,314,315,316,317,318],"sign":"17","order":16,"bendFirst":[true,true,true,true,true,true,true]},{"lineId":17,"lineName":"17号线北段","color":"#00AD8E","stationIds":[319,320,321,322,323,213,324,325,326],"sign":"17","order":17,"bendFirst":[true,true,true,true,true,true,true,true,true]},{"lineId":18,"lineName":"19号线","color":"#EB81B9","stationIds":[70,327,227,266,328,329,79,44,330,209],"sign":"19","order":18,"bendFirst":[true,true,true,true,true,true,true,true,true,true]},{"lineId":19,"lineName":"昌平线","color":"#EB81B9","stationIds":[331,332,333,334,335,336,337,338,339,194,340,245,244,341,342,343,290,344,208],"sign":"昌平线","order":19,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":20,"lineName":"大兴国际机场线","color":"#2249A3","stationIds":[227,345,346],"sign":"大兴国际机场线","order":20,"bendFirst":[true,true,true]},{"lineId":21,"lineName":"房山线","color":"#EE782E","stationIds":[347,348,349,350,351,352,353,354,355,356,357,195,358,359,229,307],"sign":"房山线","order":21,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":22,"lineName":"首都机场线","color":"#A49ABD","stationIds":[360,361,214,48,100],"sign":"首都机场线","order":22,"bendFirst":[true,true,true,true,true]},{"lineId":23,"lineName":"西郊线","color":"#FC0601","stationIds":[204,362,363,364,365,366],"sign":"西郊线","order":23,"bendFirst":[true,true,true,true,true,true]},{"lineId":24,"lineName":"燕房线","color":"#EE782E","stationIds":[347,367,368,369,370,371,372,373,374],"sign":"燕房线","order":24,"bendFirst":[true,true,true,true,true,true,true,true,true]},{"lineId":25,"lineName":"亦庄线","color":"#F0087D","stationIds":[92,375,376,377,378,379,380,381,382,383,384,385,317,386],"sign":"亦庄线","order":25,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true]}]} ================================================ FILE: public/changsha.json ================================================ {"stations":[{"stationId":1,"stationName":"开福区政府","position":[1496,328],"shape":"cicle","lineIds":[1]},{"stationId":2,"stationName":"马厂","position":[1529,436],"shape":"cicle","lineIds":[1]},{"stationId":3,"stationName":"北辰三角洲","position":[1477,586],"shape":"cicle","lineIds":[1]},{"stationId":4,"stationName":"开福寺","position":[1437,724],"shape":"cicle","lineIds":[1]},{"stationId":5,"stationName":"文昌阁","position":[1423,821],"shape":"cicle","lineIds":[1,6]},{"stationId":6,"stationName":"培元桥","position":[1406,887],"shape":"cicle","lineIds":[1]},{"stationId":7,"stationName":"五一广场","position":[1406,995],"shape":"cicle","lineIds":[1,2],"tagDirection":1},{"stationId":8,"stationName":"黄兴广场","position":[1406,1054],"shape":"cicle","lineIds":[1],"tagDirection":6},{"stationId":9,"stationName":"南门口","position":[1403,1114],"shape":"cicle","lineIds":[1],"tagDirection":6},{"stationId":10,"stationName":"侯家塘","position":[1463,1211],"shape":"cicle","lineIds":[1,3]},{"stationId":11,"stationName":"南湖路","position":[1497,1288],"shape":"cicle","lineIds":[1],"tagDirection":6},{"stationId":12,"stationName":"黄土岭","position":[1496,1343],"shape":"cicle","lineIds":[1,4],"tagDirection":5},{"stationId":13,"stationName":"涂家冲","position":[1489,1417],"shape":"cicle","lineIds":[1]},{"stationId":14,"stationName":"铁道学院","position":[1515,1587],"shape":"cicle","lineIds":[1]},{"stationId":15,"stationName":"友谊路","position":[1510,1712],"shape":"cicle","lineIds":[1]},{"stationId":16,"stationName":"省政府·清风","position":[1517,1837],"shape":"cicle","lineIds":[1]},{"stationId":17,"stationName":"桂花坪","position":[1525,1956],"shape":"cicle","lineIds":[1]},{"stationId":18,"stationName":"大托","position":[1533,2123],"shape":"cicle","lineIds":[1]},{"stationId":19,"stationName":"中信广场","position":[1539,2211],"shape":"cicle","lineIds":[1]},{"stationId":20,"stationName":"尚双塘","position":[1564,2329],"shape":"cicle","lineIds":[1]},{"stationId":21,"stationName":"梅溪湖西","position":[460,1089],"shape":"cicle","lineIds":[2]},{"stationId":22,"stationName":"麓云路","position":[562,1040],"shape":"cicle","lineIds":[2]},{"stationId":23,"stationName":"文化艺术中心","position":[646,981],"shape":"cicle","lineIds":[2]},{"stationId":24,"stationName":"梅溪湖东","position":[723,932],"shape":"cicle","lineIds":[2]},{"stationId":25,"stationName":"望城坡(汽车西站)","position":[778,874],"shape":"cicle","lineIds":[2]},{"stationId":26,"stationName":"金星路","position":[927,888],"shape":"cicle","lineIds":[2]},{"stationId":27,"stationName":"西湖公园","position":[1041,922],"shape":"cicle","lineIds":[2]},{"stationId":28,"stationName":"溁湾镇","position":[1156,972],"shape":"cicle","lineIds":[2,4]},{"stationId":29,"stationName":"橘子洲","position":[1267,992],"shape":"cicle","lineIds":[2],"tagDirection":5},{"stationId":30,"stationName":"湘江中路","position":[1335,996],"shape":"cicle","lineIds":[2],"tagDirection":0},{"stationId":31,"stationName":"芙蓉广场","position":[1488,1000],"shape":"cicle","lineIds":[2],"tagDirection":4},{"stationId":32,"stationName":"迎宾路口","position":[1563,1000],"shape":"cicle","lineIds":[2,6],"tagDirection":1},{"stationId":33,"stationName":"袁家岭","position":[1648,1003],"shape":"cicle","lineIds":[2],"tagDirection":4},{"stationId":34,"stationName":"长沙火车站","position":[1751,1012],"shape":"cicle","lineIds":[2,3]},{"stationId":35,"stationName":"锦泰广场","position":[1817,1030],"shape":"cicle","lineIds":[2],"tagDirection":4},{"stationId":36,"stationName":"万家丽广场","position":[1943,1033],"shape":"cicle","lineIds":[2,5]},{"stationId":37,"stationName":"人民东路","position":[2028,1107],"shape":"cicle","lineIds":[2,6],"tagDirection":1},{"stationId":38,"stationName":"长沙大道","position":[2086,1265],"shape":"cicle","lineIds":[2],"tagDirection":2},{"stationId":39,"stationName":"沙湾公园","position":[2090,1372],"shape":"cicle","lineIds":[2,4],"tagDirection":7},{"stationId":40,"stationName":"杜花路","position":[2206,1485],"shape":"cicle","lineIds":[2],"tagDirection":5},{"stationId":41,"stationName":"长沙火车南站","position":[2294,1480],"shape":"cicle","lineIds":[2,4]},{"stationId":42,"stationName":"光达","position":[2492,1464],"shape":"cicle","lineIds":[2,4],"tagDirection":7},{"stationId":43,"stationName":"山塘","position":[900,1961],"shape":"cicle","lineIds":[3,7]},{"stationId":44,"stationName":"洋湖湿地","position":[1029,1801],"shape":"cicle","lineIds":[3]},{"stationId":45,"stationName":"洋湖新城","position":[1096,1663],"shape":"cicle","lineIds":[3]},{"stationId":46,"stationName":"阳光","position":[1012,1534],"shape":"cicle","lineIds":[3]},{"stationId":47,"stationName":"中南大学","position":[1018,1407],"shape":"cicle","lineIds":[3]},{"stationId":48,"stationName":"阜埠河","position":[1117,1285],"shape":"cicle","lineIds":[3,4]},{"stationId":49,"stationName":"灵官渡","position":[1347,1156],"shape":"cicle","lineIds":[3],"tagDirection":5},{"stationId":50,"stationName":"东塘","position":[1571,1252],"shape":"cicle","lineIds":[3],"tagDirection":0},{"stationId":51,"stationName":"桂花公园","position":[1656,1243],"shape":"cicle","lineIds":[3]},{"stationId":52,"stationName":"阿弥岭","position":[1723,1202],"shape":"cicle","lineIds":[3]},{"stationId":53,"stationName":"朝阳村","position":[1749,1083],"shape":"cicle","lineIds":[3,6],"tagDirection":5},{"stationId":54,"stationName":"烈士公园东","position":[1731,867],"shape":"cicle","lineIds":[3]},{"stationId":55,"stationName":"丝茅冲","position":[1711,711],"shape":"cicle","lineIds":[3]},{"stationId":56,"stationName":"四方坪","position":[1712,640],"shape":"cicle","lineIds":[3]},{"stationId":57,"stationName":"雅雀湖","position":[1780,575],"shape":"cicle","lineIds":[3]},{"stationId":58,"stationName":"长沙大学","position":[1953,539],"shape":"cicle","lineIds":[3]},{"stationId":59,"stationName":"月湖公园北","position":[2052,522],"shape":"cicle","lineIds":[3,5]},{"stationId":60,"stationName":"湘龙","position":[2251,492],"shape":"cicle","lineIds":[3]},{"stationId":61,"stationName":"星沙","position":[2510,492],"shape":"cicle","lineIds":[3]},{"stationId":62,"stationName":"松雅湖(南)","position":[2677,494],"shape":"cicle","lineIds":[3],"tagDirection":4},{"stationId":63,"stationName":"星沙文体中心","position":[2757,494],"shape":"cicle","lineIds":[3]},{"stationId":64,"stationName":"螺丝塘","position":[2873,494],"shape":"cicle","lineIds":[3]},{"stationId":65,"stationName":"广生","position":[2997,499],"shape":"cicle","lineIds":[3]},{"stationId":66,"stationName":"罐子岭","position":[873,0],"shape":"cicle","lineIds":[4]},{"stationId":67,"stationName":"月亮岛西","position":[937,76],"shape":"cicle","lineIds":[4]},{"stationId":68,"stationName":"湘江新城","position":[979,131],"shape":"cicle","lineIds":[4]},{"stationId":69,"stationName":"汉王陵公园","position":[1117,271],"shape":"cicle","lineIds":[4]},{"stationId":70,"stationName":"福元大桥西","position":[1229,416],"shape":"cicle","lineIds":[4]},{"stationId":71,"stationName":"茶子山","position":[1237,522],"shape":"cicle","lineIds":[4]},{"stationId":72,"stationName":"观沙岭","position":[1166,643],"shape":"cicle","lineIds":[4]},{"stationId":73,"stationName":"六沟垅","position":[1184,735],"shape":"cicle","lineIds":[4,6]},{"stationId":74,"stationName":"望月湖","position":[1201,882],"shape":"cicle","lineIds":[4]},{"stationId":75,"stationName":"湖南师大","position":[1123,1086],"shape":"cicle","lineIds":[4]},{"stationId":76,"stationName":"湖南大学","position":[1107,1151],"shape":"cicle","lineIds":[4]},{"stationId":77,"stationName":"碧沙湖","position":[1320,1304],"shape":"cicle","lineIds":[4]},{"stationId":78,"stationName":"砂子塘","position":[1570,1325],"shape":"cicle","lineIds":[4],"tagDirection":1},{"stationId":79,"stationName":"赤岗岭","position":[1694,1351],"shape":"cicle","lineIds":[4]},{"stationId":80,"stationName":"树木岭","position":[1842,1389],"shape":"cicle","lineIds":[4]},{"stationId":81,"stationName":"圭塘","position":[1952,1373],"shape":"cicle","lineIds":[4,5]},{"stationId":82,"stationName":"粟塘","position":[2165,1372],"shape":"cicle","lineIds":[4]},{"stationId":83,"stationName":"平阳","position":[2212,1427],"shape":"cicle","lineIds":[4],"tagDirection":1},{"stationId":84,"stationName":"杜家坪","position":[2579,1533],"shape":"cicle","lineIds":[4]},{"stationId":85,"stationName":"水渡河","position":[2281,150],"shape":"cicle","lineIds":[5]},{"stationId":86,"stationName":"土桥","position":[2202,310],"shape":"cicle","lineIds":[5]},{"stationId":87,"stationName":"白茅铺","position":[2129,445],"shape":"cicle","lineIds":[5]},{"stationId":88,"stationName":"马栏山","position":[2049,632],"shape":"cicle","lineIds":[5]},{"stationId":89,"stationName":"鸭子铺","position":[2002,698],"shape":"cicle","lineIds":[5]},{"stationId":90,"stationName":"火炬村","position":[1954,864],"shape":"cicle","lineIds":[5]},{"stationId":91,"stationName":"马王堆","position":[1950,948],"shape":"cicle","lineIds":[5]},{"stationId":92,"stationName":"芙蓉区政府","position":[1945,1095],"shape":"cicle","lineIds":[5,6],"tagDirection":5},{"stationId":93,"stationName":"高桥北","position":[1942,1186],"shape":"cicle","lineIds":[5]},{"stationId":94,"stationName":"高桥南","position":[1938,1278],"shape":"cicle","lineIds":[5]},{"stationId":95,"stationName":"木桥","position":[1950,1461],"shape":"cicle","lineIds":[5]},{"stationId":96,"stationName":"雨花区政府","position":[1992,1573],"shape":"cicle","lineIds":[5]},{"stationId":97,"stationName":"大塘","position":[2006,1639],"shape":"cicle","lineIds":[5]},{"stationId":98,"stationName":"板塘冲","position":[2037,1786],"shape":"cicle","lineIds":[5]},{"stationId":99,"stationName":"毛竹塘","position":[2007,1998],"shape":"cicle","lineIds":[5]},{"stationId":100,"stationName":"谢家桥","position":[0,1228],"shape":"cicle","lineIds":[6]},{"stationId":101,"stationName":"象鼻窝","position":[174,1223],"shape":"cicle","lineIds":[6]},{"stationId":102,"stationName":"中塘","position":[279,1142],"shape":"cicle","lineIds":[6]},{"stationId":103,"stationName":"一师范西校区","position":[290,1018],"shape":"cicle","lineIds":[6]},{"stationId":104,"stationName":"长庆","position":[270,938],"shape":"cicle","lineIds":[6]},{"stationId":105,"stationName":"和馨园","position":[257,803],"shape":"cicle","lineIds":[6]},{"stationId":106,"stationName":"长丰","position":[359,741],"shape":"cicle","lineIds":[6]},{"stationId":107,"stationName":"麓谷体育公园","position":[461,749],"shape":"cicle","lineIds":[6]},{"stationId":108,"stationName":"麓谷公园","position":[605,767],"shape":"cicle","lineIds":[6]},{"stationId":109,"stationName":"涧塘","position":[748,763],"shape":"cicle","lineIds":[6]},{"stationId":110,"stationName":"湖南工商大学","position":[864,755],"shape":"cicle","lineIds":[6]},{"stationId":111,"stationName":"白鸽咀","position":[982,770],"shape":"cicle","lineIds":[6]},{"stationId":112,"stationName":"湘雅三医院","position":[1073,767],"shape":"cicle","lineIds":[6]},{"stationId":113,"stationName":"湘雅医院","position":[1521,814],"shape":"cicle","lineIds":[6]},{"stationId":114,"stationName":"烈士公园南","position":[1554,902],"shape":"cicle","lineIds":[6],"tagDirection":2},{"stationId":115,"stationName":"窑岭湘雅二医院","position":[1612,1064],"shape":"cicle","lineIds":[6]},{"stationId":116,"stationName":"花桥","position":[2128,1101],"shape":"cicle","lineIds":[6]},{"stationId":117,"stationName":"隆平水稻博物馆","position":[2230,1043],"shape":"cicle","lineIds":[6]},{"stationId":118,"stationName":"农科院农大","position":[2433,1035],"shape":"cicle","lineIds":[6]},{"stationId":119,"stationName":"东湖","position":[2600,1013],"shape":"cicle","lineIds":[6]},{"stationId":120,"stationName":"韶光","position":[2713,990],"shape":"cicle","lineIds":[6]},{"stationId":121,"stationName":"龙华","position":[2930,947],"shape":"cicle","lineIds":[6]},{"stationId":122,"stationName":"檀木桥","position":[3144,930],"shape":"cicle","lineIds":[6]},{"stationId":123,"stationName":"曹家坪","position":[3356,913],"shape":"cicle","lineIds":[6]},{"stationId":124,"stationName":"龙峰","position":[3481,927],"shape":"cicle","lineIds":[6]},{"stationId":125,"stationName":"大路村","position":[3698,928],"shape":"cicle","lineIds":[6]},{"stationId":126,"stationName":"木马塅","position":[3760,992],"shape":"cicle","lineIds":[6],"tagDirection":1},{"stationId":127,"stationName":"黄花机场T1T2","position":[3821,1041],"shape":"cicle","lineIds":[6],"tagDirection":6},{"stationId":128,"stationName":"大王山","position":[810,2061],"shape":"cicle","lineIds":[7]},{"stationId":129,"stationName":"桐溪","position":[725,2193],"shape":"cicle","lineIds":[7]},{"stationId":130,"stationName":"红桥","position":[735,2358],"shape":"cicle","lineIds":[7]},{"stationId":131,"stationName":"坪塘","position":[766,2515],"shape":"cicle","lineIds":[7]},{"stationId":132,"stationName":"双湖","position":[918,2685],"shape":"cicle","lineIds":[7]},{"stationId":133,"stationName":"黄家湾","position":[1111,3046],"shape":"cicle","lineIds":[7]},{"stationId":134,"stationName":"船形山","position":[1135,3154],"shape":"cicle","lineIds":[7]},{"stationId":135,"stationName":"湘潭北站","position":[1073,3316],"shape":"cicle","lineIds":[7]},{"stationId":136,"stationName":"磁浮机场站","position":[3832,1032],"shape":"cicle","lineIds":[8],"tagDirection":2},{"stationId":137,"stationName":"磁浮榔梨","position":[2975,1341],"shape":"cicle","lineIds":[8]},{"stationId":138,"stationName":"磁浮高铁站","position":[2316,1460],"shape":"cicle","lineIds":[8]}],"lines":[{"lineId":1,"lineName":"1号线","color":"#CC0000","stationIds":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],"sign":"1","order":1,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":2,"lineName":"2号线","color":"#52A8C9","stationIds":[21,22,23,24,25,26,27,28,29,30,7,31,32,33,34,35,36,37,38,39,40,41,42],"sign":"2","order":2,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true]},{"lineId":3,"lineName":"3号线","color":"#86B81C","stationIds":[43,44,45,46,47,48,49,10,50,51,52,53,34,54,55,56,57,58,59,60,61,62,63,64,65],"sign":"3","order":3,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":4,"lineName":"4号线","color":"#D10195","stationIds":[66,67,68,69,70,71,72,73,74,28,75,76,48,77,12,78,79,80,81,39,82,83,41,42,84],"sign":"4","order":4,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,true,true]},{"lineId":5,"lineName":"5号线","color":"#FCD600","stationIds":[85,86,87,59,88,89,90,91,36,92,93,94,81,95,96,97,98,99],"sign":"5","order":5,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":6,"lineName":"6号线","color":"#0065B3","stationIds":[100,101,102,103,104,105,106,107,108,109,110,111,112,73,5,113,114,32,115,53,92,37,116,117,118,119,120,121,122,123,124,125,126,127],"sign":"6","order":6,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true]},{"lineId":7,"lineName":"长株潭西环线","color":"#EB81B9","stationIds":[43,128,129,130,131,132,133,134,135],"sign":"长株潭西环线","order":7,"bendFirst":[true,true,true,true,true,true,true,true,true]},{"lineId":8,"lineName":"磁浮快线","color":"#B97CAF","stationIds":[136,137,138],"sign":"磁浮快线","order":8,"bendFirst":[true,true,true]}],"title":"长沙"} ================================================ FILE: public/guangzhou.json ================================================ {"stations":[{"stationId":1,"stationName":"西塱","position":[2254,5008],"shape":"cicle","lineIds":[1,19],"tagDirection":4},{"stationId":2,"stationName":"坑口","position":[2259,4874],"shape":"cicle","lineIds":[1]},{"stationId":3,"stationName":"花地湾","position":[2275,4792],"shape":"cicle","lineIds":[1]},{"stationId":4,"stationName":"芳村","position":[2292,4676],"shape":"cicle","lineIds":[1]},{"stationId":5,"stationName":"黄沙","position":[2333,4557],"shape":"cicle","lineIds":[1,8]},{"stationId":6,"stationName":"长寿路","position":[2357,4478],"shape":"cicle","lineIds":[1]},{"stationId":7,"stationName":"陈家祠","position":[2399,4405],"shape":"cicle","lineIds":[1,10],"tagDirection":6},{"stationId":8,"stationName":"西门口","position":[2493,4410],"shape":"cicle","lineIds":[1],"tagDirection":4},{"stationId":9,"stationName":"公园前","position":[2578,4408],"shape":"cicle","lineIds":[1,3],"tagDirection":7},{"stationId":10,"stationName":"农讲所","position":[2691,4396],"shape":"cicle","lineIds":[1],"tagDirection":1},{"stationId":11,"stationName":"烈士陵园","position":[2791,4395],"shape":"cicle","lineIds":[1]},{"stationId":12,"stationName":"东山口","position":[2888,4423],"shape":"cicle","lineIds":[1,8]},{"stationId":13,"stationName":"杨箕","position":[3023,4381],"shape":"cicle","lineIds":[1,7]},{"stationId":14,"stationName":"体育西路","position":[3150,4351],"shape":"cicle","lineIds":[1,4,5],"tagDirection":7},{"stationId":15,"stationName":"体育中心","position":[3218,4313],"shape":"cicle","lineIds":[1],"tagDirection":0},{"stationId":16,"stationName":"广州东站","position":[3183,4153],"shape":"cicle","lineIds":[1,5]},{"stationId":17,"stationName":"广州南站","position":[2633,5775],"shape":"cicle","lineIds":[2,3,9,17]},{"stationId":18,"stationName":"林岳东(2号线)","position":[2431,5723],"shape":"cicle","lineIds":[2],"tagDirection":0},{"stationId":19,"stationName":"林岳西","position":[2306,5725],"shape":"cicle","lineIds":[2]},{"stationId":20,"stationName":"石洲","position":[2096,5756],"shape":"cicle","lineIds":[2]},{"stationId":21,"stationName":"仙涌","position":[1917,5832],"shape":"cicle","lineIds":[2]},{"stationId":22,"stationName":"花卉世界","position":[1548,5928],"shape":"cicle","lineIds":[2]},{"stationId":23,"stationName":"登洲","position":[1423,5869],"shape":"cicle","lineIds":[2]},{"stationId":24,"stationName":"湾华","position":[1252,5742],"shape":"cicle","lineIds":[2]},{"stationId":25,"stationName":"石梁","position":[1122,5735],"shape":"cicle","lineIds":[2]},{"stationId":26,"stationName":"魁奇路","position":[1021,5723],"shape":"cicle","lineIds":[2,19]},{"stationId":27,"stationName":"沙岗","position":[877,5697],"shape":"cicle","lineIds":[2]},{"stationId":28,"stationName":"石湾","position":[767,5609],"shape":"cicle","lineIds":[2]},{"stationId":29,"stationName":"张槎","position":[612,5552],"shape":"cicle","lineIds":[2]},{"stationId":30,"stationName":"智慧新城","position":[489,5551],"shape":"cicle","lineIds":[2]},{"stationId":31,"stationName":"绿岛湖","position":[277,5643],"shape":"cicle","lineIds":[2]},{"stationId":32,"stationName":"湖涌","position":[48,5633],"shape":"cicle","lineIds":[2]},{"stationId":33,"stationName":"南庄","position":[0,5833],"shape":"cicle","lineIds":[2]},{"stationId":34,"stationName":"石壁","position":[2714,5721],"shape":"cicle","lineIds":[3,9]},{"stationId":35,"stationName":"会江","position":[2798,5532],"shape":"cicle","lineIds":[3]},{"stationId":36,"stationName":"南浦","position":[2866,5333],"shape":"cicle","lineIds":[3]},{"stationId":37,"stationName":"洛溪","position":[2921,5233],"shape":"cicle","lineIds":[3]},{"stationId":38,"stationName":"南洲","position":[2908,5014],"shape":"cicle","lineIds":[3,19]},{"stationId":39,"stationName":"东晓南","position":[2876,4949],"shape":"cicle","lineIds":[3]},{"stationId":40,"stationName":"江泰路","position":[2740,4840],"shape":"cicle","lineIds":[3]},{"stationId":41,"stationName":"昌岗","position":[2703,4756],"shape":"cicle","lineIds":[3,10]},{"stationId":42,"stationName":"江南西","position":[2674,4690],"shape":"cicle","lineIds":[3]},{"stationId":43,"stationName":"市二宫","position":[2633,4602],"shape":"cicle","lineIds":[3]},{"stationId":44,"stationName":"海珠广场","position":[2588,4514],"shape":"cicle","lineIds":[3,8],"tagDirection":3},{"stationId":45,"stationName":"纪念堂","position":[2569,4341],"shape":"cicle","lineIds":[3]},{"stationId":46,"stationName":"越秀公园","position":[2548,4262],"shape":"cicle","lineIds":[3]},{"stationId":47,"stationName":"广州火车站","position":[2501,4181],"shape":"cicle","lineIds":[3,7]},{"stationId":48,"stationName":"三元里","position":[2503,4068],"shape":"cicle","lineIds":[3]},{"stationId":49,"stationName":"飞翔公园","position":[2572,3973],"shape":"cicle","lineIds":[3]},{"stationId":50,"stationName":"白云公园","position":[2644,3846],"shape":"cicle","lineIds":[3]},{"stationId":51,"stationName":"白云文化广场","position":[2691,3767],"shape":"cicle","lineIds":[3]},{"stationId":52,"stationName":"萧岗","position":[2741,3677],"shape":"cicle","lineIds":[3]},{"stationId":53,"stationName":"江夏","position":[2769,3549],"shape":"cicle","lineIds":[3]},{"stationId":54,"stationName":"黄边","position":[2808,3446],"shape":"cicle","lineIds":[3]},{"stationId":55,"stationName":"嘉禾望岗","position":[2827,3288],"shape":"cicle","lineIds":[3,5,13]},{"stationId":56,"stationName":"番禺广场","position":[3791,6308],"shape":"cicle","lineIds":[4,15,17]},{"stationId":57,"stationName":"市桥","position":[3552,6165],"shape":"cicle","lineIds":[4]},{"stationId":58,"stationName":"汉溪长隆","position":[3236,5733],"shape":"cicle","lineIds":[4,9]},{"stationId":59,"stationName":"大石","position":[3152,5485],"shape":"cicle","lineIds":[4]},{"stationId":60,"stationName":"厦滘","position":[3143,5282],"shape":"cicle","lineIds":[4]},{"stationId":61,"stationName":"沥滘","position":[3125,5113],"shape":"cicle","lineIds":[4,19]},{"stationId":62,"stationName":"大塘","position":[3151,4883],"shape":"cicle","lineIds":[4]},{"stationId":63,"stationName":"客村","position":[3138,4700],"shape":"cicle","lineIds":[4,10],"tagDirection":3},{"stationId":64,"stationName":"广州塔","position":[3171,4599],"shape":"cicle","lineIds":[4,18]},{"stationId":65,"stationName":"珠江新城","position":[3147,4469],"shape":"cicle","lineIds":[4,7],"tagDirection":5},{"stationId":66,"stationName":"石牌桥","position":[3256,4331],"shape":"cicle","lineIds":[4],"tagDirection":3},{"stationId":67,"stationName":"岗顶","position":[3331,4314],"shape":"cicle","lineIds":[4]},{"stationId":68,"stationName":"华师","position":[3388,4259],"shape":"cicle","lineIds":[4]},{"stationId":69,"stationName":"五山","position":[3453,4134],"shape":"cicle","lineIds":[4]},{"stationId":70,"stationName":"天河客运站","position":[3370,3952],"shape":"cicle","lineIds":[4,8]},{"stationId":71,"stationName":"机场北(2号航站楼)","position":[2991,1700],"shape":"cicle","lineIds":[5]},{"stationId":72,"stationName":"机场南(1号航站楼)","position":[2965,1796],"shape":"cicle","lineIds":[5]},{"stationId":73,"stationName":"高增","position":[2890,2111],"shape":"cicle","lineIds":[5,11]},{"stationId":74,"stationName":"人和","position":[2894,2310],"shape":"cicle","lineIds":[5]},{"stationId":75,"stationName":"龙归","position":[2943,2775],"shape":"cicle","lineIds":[5]},{"stationId":76,"stationName":"白云大道北","position":[2916,3439],"shape":"cicle","lineIds":[5],"tagDirection":4},{"stationId":77,"stationName":"永泰","position":[2997,3460],"shape":"cicle","lineIds":[5]},{"stationId":78,"stationName":"同和","position":[3197,3692],"shape":"cicle","lineIds":[5]},{"stationId":79,"stationName":"京溪南方医院","position":[3196,3810],"shape":"cicle","lineIds":[5]},{"stationId":80,"stationName":"梅花园","position":[3140,3910],"shape":"cicle","lineIds":[5]},{"stationId":81,"stationName":"燕塘","position":[3206,4063],"shape":"cicle","lineIds":[5,8]},{"stationId":82,"stationName":"林和西","position":[3174,4247],"shape":"cicle","lineIds":[5,18],"tagDirection":7},{"stationId":83,"stationName":"黄村","position":[4007,4344],"shape":"cicle","lineIds":[6,16]},{"stationId":84,"stationName":"车陂","position":[3890,4418],"shape":"cicle","lineIds":[6]},{"stationId":85,"stationName":"车陂南","position":[3830,4503],"shape":"cicle","lineIds":[6,7]},{"stationId":86,"stationName":"万胜围","position":[3782,4685],"shape":"cicle","lineIds":[6,10]},{"stationId":87,"stationName":"官洲","position":[3703,4994],"shape":"cicle","lineIds":[6]},{"stationId":88,"stationName":"大学城北","position":[3791,5081],"shape":"cicle","lineIds":[6]},{"stationId":89,"stationName":"大学城南","position":[3940,5229],"shape":"cicle","lineIds":[6,9]},{"stationId":90,"stationName":"新造","position":[4092,5381],"shape":"cicle","lineIds":[6]},{"stationId":91,"stationName":"石碁","position":[4586,6090],"shape":"cicle","lineIds":[6]},{"stationId":92,"stationName":"海傍","position":[4688,6267],"shape":"cicle","lineIds":[6]},{"stationId":93,"stationName":"低涌","position":[4783,6467],"shape":"cicle","lineIds":[6]},{"stationId":94,"stationName":"东涌","position":[4724,6806],"shape":"cicle","lineIds":[6]},{"stationId":95,"stationName":"庆盛","position":[4839,6992],"shape":"cicle","lineIds":[6]},{"stationId":96,"stationName":"黄阁汽车城","position":[5032,7304],"shape":"cicle","lineIds":[6]},{"stationId":97,"stationName":"黄阁","position":[5122,7415],"shape":"cicle","lineIds":[6]},{"stationId":98,"stationName":"蕉门","position":[5210,7655],"shape":"cicle","lineIds":[6]},{"stationId":99,"stationName":"金洲","position":[5323,7741],"shape":"cicle","lineIds":[6]},{"stationId":100,"stationName":"飞沙角","position":[5342,7844],"shape":"cicle","lineIds":[6]},{"stationId":101,"stationName":"广隆","position":[5342,7941],"shape":"cicle","lineIds":[6]},{"stationId":102,"stationName":"大涌","position":[5494,8072],"shape":"cicle","lineIds":[6]},{"stationId":103,"stationName":"塘坑","position":[5665,8131],"shape":"cicle","lineIds":[6]},{"stationId":104,"stationName":"南横","position":[5878,8183],"shape":"cicle","lineIds":[6]},{"stationId":105,"stationName":"南沙客运港","position":[6044,7992],"shape":"cicle","lineIds":[6]},{"stationId":106,"stationName":"滘口","position":[2019,4524],"shape":"cicle","lineIds":[7]},{"stationId":107,"stationName":"坦尾","position":[2118,4410],"shape":"cicle","lineIds":[7,8]},{"stationId":108,"stationName":"中山八","position":[2262,4402],"shape":"cicle","lineIds":[7],"tagDirection":7},{"stationId":109,"stationName":"西场","position":[2315,4289],"shape":"cicle","lineIds":[7]},{"stationId":110,"stationName":"西村","position":[2356,4240],"shape":"cicle","lineIds":[7,10]},{"stationId":111,"stationName":"小北","position":[2704,4268],"shape":"cicle","lineIds":[7]},{"stationId":112,"stationName":"淘金","position":[2800,4299],"shape":"cicle","lineIds":[7]},{"stationId":113,"stationName":"区庄","position":[2902,4318],"shape":"cicle","lineIds":[7,8]},{"stationId":114,"stationName":"动物园","position":[3007,4317],"shape":"cicle","lineIds":[7],"tagDirection":0},{"stationId":115,"stationName":"五羊邨","position":[3077,4464],"shape":"cicle","lineIds":[7],"tagDirection":6},{"stationId":116,"stationName":"猎德","position":[3256,4477],"shape":"cicle","lineIds":[7],"tagDirection":3},{"stationId":117,"stationName":"潭村","position":[3394,4489],"shape":"cicle","lineIds":[7]},{"stationId":118,"stationName":"员村","position":[3571,4505],"shape":"cicle","lineIds":[7,16]},{"stationId":119,"stationName":"科韵路","position":[3707,4470],"shape":"cicle","lineIds":[7]},{"stationId":120,"stationName":"东圃","position":[3950,4568],"shape":"cicle","lineIds":[7]},{"stationId":121,"stationName":"三溪","position":[4092,4621],"shape":"cicle","lineIds":[7]},{"stationId":122,"stationName":"鱼珠","position":[4257,4658],"shape":"cicle","lineIds":[7,12]},{"stationId":123,"stationName":"大沙地","position":[4395,4630],"shape":"cicle","lineIds":[7]},{"stationId":124,"stationName":"大沙东","position":[4520,4600],"shape":"cicle","lineIds":[7,9]},{"stationId":125,"stationName":"文冲","position":[4625,4624],"shape":"cicle","lineIds":[7]},{"stationId":126,"stationName":"双沙","position":[4777,4684],"shape":"cicle","lineIds":[7]},{"stationId":127,"stationName":"庙头","position":[4962,4815],"shape":"cicle","lineIds":[7]},{"stationId":128,"stationName":"夏园","position":[5118,4833],"shape":"cicle","lineIds":[7,12]},{"stationId":129,"stationName":"保盈大道","position":[5195,4920],"shape":"cicle","lineIds":[7]},{"stationId":130,"stationName":"夏港","position":[5187,5027],"shape":"cicle","lineIds":[7]},{"stationId":131,"stationName":"黄埔新港","position":[5183,5139],"shape":"cicle","lineIds":[7]},{"stationId":132,"stationName":"香雪","position":[4947,3939],"shape":"cicle","lineIds":[8]},{"stationId":133,"stationName":"萝岗","position":[4745,3906],"shape":"cicle","lineIds":[8,9],"tagDirection":3},{"stationId":134,"stationName":"苏元","position":[4619,3953],"shape":"cicle","lineIds":[8,16]},{"stationId":135,"stationName":"暹岗","position":[4536,3939],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":136,"stationName":"金峰","position":[4446,3848],"shape":"cicle","lineIds":[8]},{"stationId":137,"stationName":"黄陂","position":[4272,3779],"shape":"cicle","lineIds":[8]},{"stationId":138,"stationName":"高塘石","position":[4039,3752],"shape":"cicle","lineIds":[8]},{"stationId":139,"stationName":"柯木塱","position":[3894,3728],"shape":"cicle","lineIds":[8]},{"stationId":140,"stationName":"龙洞","position":[3710,3753],"shape":"cicle","lineIds":[8]},{"stationId":141,"stationName":"植物园","position":[3591,3761],"shape":"cicle","lineIds":[8]},{"stationId":142,"stationName":"长湴","position":[3429,3877],"shape":"cicle","lineIds":[8]},{"stationId":143,"stationName":"天平架","position":[3148,4067],"shape":"cicle","lineIds":[8]},{"stationId":144,"stationName":"沙河顶","position":[3004,4190],"shape":"cicle","lineIds":[8]},{"stationId":145,"stationName":"黄花岗","position":[2934,4251],"shape":"cicle","lineIds":[8]},{"stationId":146,"stationName":"东湖","position":[2820,4511],"shape":"cicle","lineIds":[8]},{"stationId":147,"stationName":"团一大广场","position":[2717,4481],"shape":"cicle","lineIds":[8],"tagDirection":4},{"stationId":148,"stationName":"北京路","position":[2637,4475],"shape":"cicle","lineIds":[8],"tagDirection":0},{"stationId":149,"stationName":"一德路","position":[2506,4532],"shape":"cicle","lineIds":[8]},{"stationId":150,"stationName":"文化公园","position":[2424,4570],"shape":"cicle","lineIds":[8,10]},{"stationId":151,"stationName":"如意坊","position":[2247,4513],"shape":"cicle","lineIds":[8]},{"stationId":152,"stationName":"河沙","position":[2109,4307],"shape":"cicle","lineIds":[8]},{"stationId":153,"stationName":"沙贝","position":[2048,4138],"shape":"cicle","lineIds":[8]},{"stationId":154,"stationName":"横沙","position":[2002,4080],"shape":"cicle","lineIds":[8]},{"stationId":155,"stationName":"浔峰岗","position":[1954,4021],"shape":"cicle","lineIds":[8]},{"stationId":156,"stationName":"燕山","position":[4685,3711],"shape":"cicle","lineIds":[9]},{"stationId":157,"stationName":"水西","position":[4722,3787],"shape":"cicle","lineIds":[9,16]},{"stationId":158,"stationName":"科丰路","position":[4596,4034],"shape":"cicle","lineIds":[9]},{"stationId":159,"stationName":"加庄","position":[4536,4184],"shape":"cicle","lineIds":[9]},{"stationId":160,"stationName":"姬堂","position":[4494,4341],"shape":"cicle","lineIds":[9]},{"stationId":161,"stationName":"裕丰围","position":[4492,4714],"shape":"cicle","lineIds":[9,12]},{"stationId":162,"stationName":"长洲","position":[4228,4900],"shape":"cicle","lineIds":[9]},{"stationId":163,"stationName":"深井","position":[4030,4975],"shape":"cicle","lineIds":[9]},{"stationId":164,"stationName":"板桥","position":[3816,5494],"shape":"cicle","lineIds":[9]},{"stationId":165,"stationName":"员岗","position":[3591,5503],"shape":"cicle","lineIds":[9]},{"stationId":166,"stationName":"南村万博","position":[3410,5614],"shape":"cicle","lineIds":[9,15]},{"stationId":167,"stationName":"钟村","position":[3104,5798],"shape":"cicle","lineIds":[9]},{"stationId":168,"stationName":"谢村","position":[2893,5757],"shape":"cicle","lineIds":[9]},{"stationId":169,"stationName":"大洲","position":[2507,5824],"shape":"cicle","lineIds":[9]},{"stationId":170,"stationName":"陈村北","position":[2387,5901],"shape":"cicle","lineIds":[9]},{"stationId":171,"stationName":"陈村","position":[2303,5982],"shape":"cicle","lineIds":[9]},{"stationId":172,"stationName":"锦龙","position":[2349,6131],"shape":"cicle","lineIds":[9]},{"stationId":173,"stationName":"南涌","position":[2316,6269],"shape":"cicle","lineIds":[9]},{"stationId":174,"stationName":"美的","position":[2203,6327],"shape":"cicle","lineIds":[9]},{"stationId":175,"stationName":"北滘公园","position":[2054,6363],"shape":"cicle","lineIds":[9]},{"stationId":176,"stationName":"美的大道","position":[2012,6215],"shape":"cicle","lineIds":[9]},{"stationId":177,"stationName":"滘心","position":[2104,3293],"shape":"cicle","lineIds":[10]},{"stationId":178,"stationName":"亭岗","position":[2134,3436],"shape":"cicle","lineIds":[10]},{"stationId":179,"stationName":"石井","position":[2252,3551],"shape":"cicle","lineIds":[10]},{"stationId":180,"stationName":"小坪","position":[2365,3657],"shape":"cicle","lineIds":[10]},{"stationId":181,"stationName":"石潭","position":[2345,3784],"shape":"cicle","lineIds":[10]},{"stationId":182,"stationName":"聚龙","position":[2322,3850],"shape":"cicle","lineIds":[10]},{"stationId":183,"stationName":"上步","position":[2292,3953],"shape":"cicle","lineIds":[10]},{"stationId":184,"stationName":"同德","position":[2282,4027],"shape":"cicle","lineIds":[10]},{"stationId":185,"stationName":"鹅掌坦","position":[2293,4099],"shape":"cicle","lineIds":[10]},{"stationId":186,"stationName":"彩虹桥","position":[2388,4323],"shape":"cicle","lineIds":[10],"tagDirection":2},{"stationId":187,"stationName":"华林寺","position":[2408,4486],"shape":"cicle","lineIds":[10],"tagDirection":2},{"stationId":188,"stationName":"同福西","position":[2458,4645],"shape":"cicle","lineIds":[10]},{"stationId":189,"stationName":"凤凰新村","position":[2494,4715],"shape":"cicle","lineIds":[10]},{"stationId":190,"stationName":"沙园","position":[2540,4776],"shape":"cicle","lineIds":[10,19]},{"stationId":191,"stationName":"宝岗大道","position":[2631,4802],"shape":"cicle","lineIds":[10]},{"stationId":192,"stationName":"晓港","position":[2753,4732],"shape":"cicle","lineIds":[10]},{"stationId":193,"stationName":"中大","position":[2863,4741],"shape":"cicle","lineIds":[10]},{"stationId":194,"stationName":"鹭江","position":[3017,4709],"shape":"cicle","lineIds":[10]},{"stationId":195,"stationName":"赤岗","position":[3283,4698],"shape":"cicle","lineIds":[10]},{"stationId":196,"stationName":"磨碟沙","position":[3347,4675],"shape":"cicle","lineIds":[10,15]},{"stationId":197,"stationName":"新港东","position":[3505,4675],"shape":"cicle","lineIds":[10]},{"stationId":198,"stationName":"琶洲","position":[3601,4677],"shape":"cicle","lineIds":[10]},{"stationId":199,"stationName":"清塘","position":[2563,1940],"shape":"cicle","lineIds":[11]},{"stationId":200,"stationName":"清布","position":[2444,1881],"shape":"cicle","lineIds":[11]},{"stationId":201,"stationName":"莲塘","position":[2386,1771],"shape":"cicle","lineIds":[11]},{"stationId":202,"stationName":"马鞍山公园","position":[2313,1665],"shape":"cicle","lineIds":[11]},{"stationId":203,"stationName":"花都广场","position":[2161,1649],"shape":"cicle","lineIds":[11]},{"stationId":204,"stationName":"花果山公园","position":[2118,1791],"shape":"cicle","lineIds":[11]},{"stationId":205,"stationName":"花城路","position":[2062,1866],"shape":"cicle","lineIds":[11]},{"stationId":206,"stationName":"广州北站","position":[1950,1898],"shape":"cicle","lineIds":[11]},{"stationId":207,"stationName":"花都汽车城","position":[1738,1866],"shape":"cicle","lineIds":[11]},{"stationId":208,"stationName":"飞鹅岭","position":[1486,1879],"shape":"cicle","lineIds":[11]},{"stationId":209,"stationName":"双岗","position":[4678,4718],"shape":"cicle","lineIds":[12]},{"stationId":210,"stationName":"南海神庙","position":[4909,4825],"shape":"cicle","lineIds":[12],"tagDirection":5},{"stationId":211,"stationName":"南岗","position":[5349,4710],"shape":"cicle","lineIds":[12]},{"stationId":212,"stationName":"沙村","position":[5638,4556],"shape":"cicle","lineIds":[12]},{"stationId":213,"stationName":"白江","position":[5891,4430],"shape":"cicle","lineIds":[12]},{"stationId":214,"stationName":"新塘","position":[5979,4345],"shape":"cicle","lineIds":[12]},{"stationId":215,"stationName":"官湖","position":[6300,4290],"shape":"cicle","lineIds":[12]},{"stationId":216,"stationName":"新沙","position":[6429,4294],"shape":"cicle","lineIds":[12]},{"stationId":217,"stationName":"白云东平","position":[3113,3151],"shape":"cicle","lineIds":[13]},{"stationId":218,"stationName":"夏良","position":[3170,2885],"shape":"cicle","lineIds":[13]},{"stationId":219,"stationName":"太和","position":[3406,2669],"shape":"cicle","lineIds":[13]},{"stationId":220,"stationName":"竹料","position":[3618,2177],"shape":"cicle","lineIds":[13]},{"stationId":221,"stationName":"钟落潭","position":[3941,1906],"shape":"cicle","lineIds":[13]},{"stationId":222,"stationName":"马沥","position":[4438,1724],"shape":"cicle","lineIds":[13]},{"stationId":223,"stationName":"新和","position":[4605,1530],"shape":"cicle","lineIds":[13,14]},{"stationId":224,"stationName":"太平","position":[4854,1141],"shape":"cicle","lineIds":[13]},{"stationId":225,"stationName":"神岗","position":[5108,911],"shape":"cicle","lineIds":[13]},{"stationId":226,"stationName":"赤草","position":[5537,650],"shape":"cicle","lineIds":[13]},{"stationId":227,"stationName":"从化客运站","position":[5882,345],"shape":"cicle","lineIds":[13]},{"stationId":228,"stationName":"东风","position":[5943,0],"shape":"cicle","lineIds":[13]},{"stationId":229,"stationName":"红卫","position":[4809,1670],"shape":"cicle","lineIds":[14]},{"stationId":230,"stationName":"新南","position":[4887,1890],"shape":"cicle","lineIds":[14]},{"stationId":231,"stationName":"枫下","position":[5018,2032],"shape":"cicle","lineIds":[14]},{"stationId":232,"stationName":"知识城","position":[5186,2169],"shape":"cicle","lineIds":[14]},{"stationId":233,"stationName":"何棠下","position":[5371,2350],"shape":"cicle","lineIds":[14]},{"stationId":234,"stationName":"旺村","position":[5452,2430],"shape":"cicle","lineIds":[14]},{"stationId":235,"stationName":"汤村","position":[5581,2589],"shape":"cicle","lineIds":[14]},{"stationId":236,"stationName":"镇龙北","position":[5704,2766],"shape":"cicle","lineIds":[14]},{"stationId":237,"stationName":"镇龙","position":[5864,2817],"shape":"cicle","lineIds":[14,16]},{"stationId":238,"stationName":"冼村","position":[3216,4405],"shape":"cicle","lineIds":[15],"tagDirection":2},{"stationId":239,"stationName":"龙潭","position":[3348,4844],"shape":"cicle","lineIds":[15]},{"stationId":240,"stationName":"沙溪","position":[3316,5308],"shape":"cicle","lineIds":[15]},{"stationId":241,"stationName":"横沥","position":[5045,8129],"shape":"cicle","lineIds":[15]},{"stationId":242,"stationName":"万顷沙","position":[5505,8678],"shape":"cicle","lineIds":[15]},{"stationId":243,"stationName":"天河公园","position":[3563,4409],"shape":"cicle","lineIds":[16]},{"stationId":244,"stationName":"棠东","position":[3828,4353],"shape":"cicle","lineIds":[16]},{"stationId":245,"stationName":"大观南路","position":[4004,4217],"shape":"cicle","lineIds":[16]},{"stationId":246,"stationName":"天河智慧城","position":[3966,4030],"shape":"cicle","lineIds":[16]},{"stationId":247,"stationName":"神舟路","position":[4248,4010],"shape":"cicle","lineIds":[16]},{"stationId":248,"stationName":"科学城","position":[4427,4006],"shape":"cicle","lineIds":[16]},{"stationId":249,"stationName":"长平","position":[4832,3559],"shape":"cicle","lineIds":[16]},{"stationId":250,"stationName":"金坑","position":[5202,3169],"shape":"cicle","lineIds":[16]},{"stationId":251,"stationName":"镇龙西","position":[5672,2881],"shape":"cicle","lineIds":[16]},{"stationId":252,"stationName":"中新","position":[6066,2809],"shape":"cicle","lineIds":[16]},{"stationId":253,"stationName":"坑贝","position":[6361,2875],"shape":"cicle","lineIds":[16]},{"stationId":254,"stationName":"凤岗","position":[6641,2903],"shape":"cicle","lineIds":[16]},{"stationId":255,"stationName":"朱村","position":[6949,2939],"shape":"cicle","lineIds":[16]},{"stationId":256,"stationName":"山田","position":[7380,2898],"shape":"cicle","lineIds":[16]},{"stationId":257,"stationName":"钟岗","position":[7855,2869],"shape":"cicle","lineIds":[16]},{"stationId":258,"stationName":"增城广场","position":[8079,2882],"shape":"cicle","lineIds":[16]},{"stationId":259,"stationName":"陈头岗","position":[2591,5530],"shape":"cicle","lineIds":[17]},{"stationId":260,"stationName":"市广路","position":[3304,5949],"shape":"cicle","lineIds":[17]},{"stationId":261,"stationName":"海心沙","position":[3179,4547],"shape":"cicle","lineIds":[18],"tagDirection":2},{"stationId":262,"stationName":"大剧院","position":[3179,4501],"shape":"cicle","lineIds":[18],"tagDirection":2},{"stationId":263,"stationName":"花城大道","position":[3182,4470],"shape":"cicle","lineIds":[18],"tagDirection":2},{"stationId":264,"stationName":"妇儿中心","position":[3180,4437],"shape":"cicle","lineIds":[18]},{"stationId":265,"stationName":"黄埔大道","position":[3179,4392],"shape":"cicle","lineIds":[18]},{"stationId":266,"stationName":"天河南","position":[3179,4348],"shape":"cicle","lineIds":[18]},{"stationId":267,"stationName":"体育中心南","position":[3178,4320],"shape":"cicle","lineIds":[18],"tagDirection":7},{"stationId":268,"stationName":"石溪","position":[2794,4983],"shape":"cicle","lineIds":[19]},{"stationId":269,"stationName":"燕岗","position":[2654,4911],"shape":"cicle","lineIds":[19]},{"stationId":270,"stationName":"沙涌","position":[2402,4831],"shape":"cicle","lineIds":[19]},{"stationId":271,"stationName":"鹤洞","position":[2336,4937],"shape":"cicle","lineIds":[19]},{"stationId":272,"stationName":"菊树","position":[2063,5004],"shape":"cicle","lineIds":[19],"tagDirection":4},{"stationId":273,"stationName":"龙溪","position":[1858,5002],"shape":"cicle","lineIds":[19],"tagDirection":4},{"stationId":274,"stationName":"金融高新区","position":[1520,5004],"shape":"cicle","lineIds":[19]},{"stationId":275,"stationName":"千灯湖","position":[1488,5114],"shape":"cicle","lineIds":[19]},{"stationId":276,"stationName":"礌岗","position":[1489,5236],"shape":"cicle","lineIds":[19]},{"stationId":277,"stationName":"南桂路","position":[1476,5321],"shape":"cicle","lineIds":[19]},{"stationId":278,"stationName":"桂城","position":[1349,5346],"shape":"cicle","lineIds":[19]},{"stationId":279,"stationName":"朝安","position":[1270,5386],"shape":"cicle","lineIds":[19]},{"stationId":280,"stationName":"普君北路","position":[1157,5386],"shape":"cicle","lineIds":[19],"tagDirection":0},{"stationId":281,"stationName":"祖庙","position":[1055,5392],"shape":"cicle","lineIds":[19]},{"stationId":282,"stationName":"同济路","position":[1020,5470],"shape":"cicle","lineIds":[19]},{"stationId":283,"stationName":"季华园","position":[1020,5591],"shape":"cicle","lineIds":[19]},{"stationId":284,"stationName":"澜石","position":[1006,5822],"shape":"cicle","lineIds":[19]},{"stationId":285,"stationName":"世纪莲","position":[1084,6011],"shape":"cicle","lineIds":[19]},{"stationId":286,"stationName":"东平","position":[1226,6010],"shape":"cicle","lineIds":[19]},{"stationId":287,"stationName":"新城东","position":[1335,6076],"shape":"cicle","lineIds":[19]}],"lines":[{"lineId":1,"lineName":"1号线","color":"#FCD600","stationIds":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"sign":"1","order":1,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":2,"lineName":"佛山2号线","color":"#FC0601","stationIds":[17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33],"sign":"佛山2","order":2,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":3,"lineName":"2号线","color":"#0065B3","stationIds":[17,34,35,36,37,38,39,40,41,42,43,44,9,45,46,47,48,49,50,51,52,53,54,55],"sign":"2","order":3,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":4,"lineName":"3号线","color":"#FFA500","stationIds":[56,57,58,59,60,61,62,63,64,65,14,66,67,68,69,70],"sign":"3","order":4,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":5,"lineName":"3号线(北延段)","color":"#FFA500","stationIds":[71,72,73,74,75,55,76,77,78,79,80,81,16,82,14],"sign":"3","order":5,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,false,true]},{"lineId":6,"lineName":"4号线","color":"#018237","stationIds":[83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105],"sign":"4","order":6,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":7,"lineName":"5号线","color":"#C5003E","stationIds":[106,107,108,109,110,47,111,112,113,114,13,115,65,116,117,118,119,85,120,121,122,123,124,125,126,127,128,129,130,131],"sign":"5","order":7,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":8,"lineName":"6号线","color":"#741D51","stationIds":[132,133,134,135,136,137,138,139,140,141,142,70,81,143,144,145,113,12,146,147,148,44,149,150,5,151,107,152,153,154,155],"sign":"6","order":8,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":9,"lineName":"7号线","color":"#86B81C","stationIds":[156,157,133,158,159,160,124,161,162,163,89,164,165,166,58,167,168,34,17,169,170,171,172,173,174,175,176],"sign":"7","order":9,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":10,"lineName":"8号线","color":"#008187","stationIds":[177,178,179,180,181,182,183,184,185,110,186,7,187,150,188,189,190,191,41,192,193,194,63,195,196,197,198,86],"sign":"8","order":10,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":11,"lineName":"9号线","color":"#64BD91","stationIds":[73,199,200,201,202,203,204,205,206,207,208],"sign":"9","order":11,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true]},{"lineId":12,"lineName":"13号线","color":"#818530","stationIds":[122,161,209,210,128,211,212,213,214,215,216],"sign":"13","order":12,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true]},{"lineId":13,"lineName":"14号线","color":"#871C2A","stationIds":[55,217,218,219,220,221,222,223,224,225,226,227,228],"sign":"14","order":13,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":14,"lineName":"14号线支线(知识城线)","color":"#871C2A","stationIds":[223,229,230,231,232,233,234,235,236,237],"sign":"14","order":14,"bendFirst":[true,true,true,true,true,true,true,true,true,true]},{"lineId":15,"lineName":"18号线","color":"#2249A3","stationIds":[238,196,239,240,166,56,241,242],"sign":"18","order":15,"bendFirst":[false,true,true,true,true,true,true,true]},{"lineId":16,"lineName":"21号线","color":"#0C1335","stationIds":[118,243,244,83,245,246,247,248,134,157,249,250,251,237,252,253,254,255,256,257,258],"sign":"21","order":16,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":17,"lineName":"22号线","color":"#B35A1F","stationIds":[259,17,260,56],"sign":"22","order":17,"bendFirst":[true,true,true,true]},{"lineId":18,"lineName":"APM线","color":"#02B6E3","stationIds":[64,261,262,263,264,265,266,267,82],"sign":"APM线","order":18,"bendFirst":[true,true,true,true,true,true,true,true,true]},{"lineId":19,"lineName":"广佛线","color":"#B8D201","stationIds":[61,38,268,269,190,270,271,1,272,273,274,275,276,277,278,279,280,281,282,283,26,284,285,286,287],"sign":"广佛线","order":19,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]}],"title":"广州"} ================================================ FILE: public/hongkong.json ================================================ {"stations":[{"stationId":1,"stationName":"迪士尼","position":[1084,2120],"shape":"cicle","lineIds":[1]},{"stationId":2,"stationName":"欣澳","position":[928,1949],"shape":"cicle","lineIds":[1,4]},{"stationId":3,"stationName":"金钟","position":[2290,2486],"shape":"cicle","lineIds":[2,5,10,12]},{"stationId":4,"stationName":"会展","position":[2368,2458],"shape":"cicle","lineIds":[2],"tagDirection":7},{"stationId":5,"stationName":"红磡","position":[2452,2256],"shape":"cicle","lineIds":[2,11]},{"stationId":6,"stationName":"旺角东","position":[2362,2058],"shape":"cicle","lineIds":[2],"tagDirection":2},{"stationId":7,"stationName":"九龙塘","position":[2392,1899],"shape":"cicle","lineIds":[2,6]},{"stationId":8,"stationName":"大围","position":[2420,1549],"shape":"cicle","lineIds":[2,11]},{"stationId":9,"stationName":"沙田","position":[2505,1457],"shape":"cicle","lineIds":[2]},{"stationId":10,"stationName":"火炭","position":[2616,1326],"shape":"cicle","lineIds":[2]},{"stationId":11,"stationName":"大学","position":[2735,1140],"shape":"cicle","lineIds":[2]},{"stationId":12,"stationName":"大埔墟","position":[2338,831],"shape":"cicle","lineIds":[2]},{"stationId":13,"stationName":"太和","position":[2249,770],"shape":"cicle","lineIds":[2]},{"stationId":14,"stationName":"粉岭","position":[2022,355],"shape":"cicle","lineIds":[2]},{"stationId":15,"stationName":"上水","position":[1913,262],"shape":"cicle","lineIds":[2,13]},{"stationId":16,"stationName":"落马洲","position":[1294,131],"shape":"cicle","lineIds":[2]},{"stationId":17,"stationName":"罗湖","position":[1771,0],"shape":"cicle","lineIds":[13]},{"stationId":18,"stationName":"东涌","position":[48,2388],"shape":"cicle","lineIds":[4]},{"stationId":19,"stationName":"青衣","position":[1711,1700],"shape":"cicle","lineIds":[4,7]},{"stationId":20,"stationName":"荔景","position":[1896,1790],"shape":"cicle","lineIds":[4,12],"tagDirection":7},{"stationId":21,"stationName":"南昌","position":[2171,2011],"shape":"cicle","lineIds":[4,11]},{"stationId":22,"stationName":"奥运","position":[2235,2097],"shape":"cicle","lineIds":[4],"tagDirection":6},{"stationId":23,"stationName":"九龙","position":[2251,2227],"shape":"cicle","lineIds":[4,7],"tagDirection":6},{"stationId":24,"stationName":"香港","position":[2215,2429],"shape":"cicle","lineIds":[4,7]},{"stationId":25,"stationName":"坚尼地城","position":[1924,2464],"shape":"cicle","lineIds":[5]},{"stationId":26,"stationName":"香港大学","position":[1993,2436],"shape":"cicle","lineIds":[5],"tagDirection":4},{"stationId":27,"stationName":"西营盘","position":[2061,2423],"shape":"cicle","lineIds":[5]},{"stationId":28,"stationName":"上环","position":[2163,2413],"shape":"cicle","lineIds":[5],"tagDirection":0},{"stationId":29,"stationName":"中环","position":[2226,2460],"shape":"cicle","lineIds":[5,12],"tagDirection":4},{"stationId":30,"stationName":"湾仔","position":[2364,2506],"shape":"cicle","lineIds":[5]},{"stationId":31,"stationName":"铜锣湾","position":[2471,2475],"shape":"cicle","lineIds":[5]},{"stationId":32,"stationName":"天后","position":[2553,2450],"shape":"cicle","lineIds":[5]},{"stationId":33,"stationName":"炮台山","position":[2575,2393],"shape":"cicle","lineIds":[5]},{"stationId":34,"stationName":"北角","position":[2640,2368],"shape":"cicle","lineIds":[5,9]},{"stationId":35,"stationName":"鲗鱼涌","position":[2732,2400],"shape":"cicle","lineIds":[5,9]},{"stationId":36,"stationName":"太古","position":[2795,2432],"shape":"cicle","lineIds":[5],"tagDirection":5},{"stationId":37,"stationName":"西湾河","position":[2857,2457],"shape":"cicle","lineIds":[5]},{"stationId":38,"stationName":"筲箕湾","position":[2924,2488],"shape":"cicle","lineIds":[5]},{"stationId":39,"stationName":"杏花邨","position":[3031,2512],"shape":"cicle","lineIds":[5]},{"stationId":40,"stationName":"柴湾","position":[3005,2632],"shape":"cicle","lineIds":[5]},{"stationId":41,"stationName":"黄埔","position":[2531,2228],"shape":"cicle","lineIds":[6]},{"stationId":42,"stationName":"何文田","position":[2463,2184],"shape":"cicle","lineIds":[6,11]},{"stationId":43,"stationName":"油麻地","position":[2342,2147],"shape":"cicle","lineIds":[6,12],"tagDirection":1},{"stationId":44,"stationName":"旺角","position":[2328,2082],"shape":"cicle","lineIds":[6,12],"tagDirection":6},{"stationId":45,"stationName":"太子","position":[2317,2031],"shape":"cicle","lineIds":[6,12]},{"stationId":46,"stationName":"石硖尾","position":[2322,1959],"shape":"cicle","lineIds":[6],"tagDirection":3},{"stationId":47,"stationName":"乐富","position":[2507,1899],"shape":"cicle","lineIds":[6]},{"stationId":48,"stationName":"黄大仙","position":[2575,1861],"shape":"cicle","lineIds":[6],"tagDirection":7},{"stationId":49,"stationName":"钻石山","position":[2647,1876],"shape":"cicle","lineIds":[6,11]},{"stationId":50,"stationName":"彩虹","position":[2724,1931],"shape":"cicle","lineIds":[6]},{"stationId":51,"stationName":"九龙湾","position":[2774,2042],"shape":"cicle","lineIds":[6]},{"stationId":52,"stationName":"牛头角","position":[2825,2124],"shape":"cicle","lineIds":[6]},{"stationId":53,"stationName":"观塘","position":[2898,2156],"shape":"cicle","lineIds":[6]},{"stationId":54,"stationName":"蓝田","position":[2963,2213],"shape":"cicle","lineIds":[6]},{"stationId":55,"stationName":"油塘","position":[3005,2299],"shape":"cicle","lineIds":[6,9]},{"stationId":56,"stationName":"调景岭","position":[3157,2237],"shape":"cicle","lineIds":[6,9]},{"stationId":57,"stationName":"机场","position":[0,2121],"shape":"cicle","lineIds":[7]},{"stationId":58,"stationName":"博览馆","position":[65,2057],"shape":"cicle","lineIds":[7]},{"stationId":59,"stationName":"康城","position":[3326,2325],"shape":"cicle","lineIds":[8]},{"stationId":60,"stationName":"将军澳","position":[3232,2204],"shape":"cicle","lineIds":[8,9]},{"stationId":61,"stationName":"坑口","position":[3277,2124],"shape":"cicle","lineIds":[9]},{"stationId":62,"stationName":"宝琳","position":[3211,2052],"shape":"cicle","lineIds":[9]},{"stationId":63,"stationName":"海洋公园","position":[2380,2790],"shape":"cicle","lineIds":[10]},{"stationId":64,"stationName":"黄竹坑","position":[2316,2797],"shape":"cicle","lineIds":[10]},{"stationId":65,"stationName":"利东","position":[2201,2850],"shape":"cicle","lineIds":[10]},{"stationId":66,"stationName":"海怡半岛","position":[2125,2850],"shape":"cicle","lineIds":[10]},{"stationId":67,"stationName":"屯门","position":[367,1327],"shape":"cicle","lineIds":[11]},{"stationId":68,"stationName":"兆康","position":[424,1159],"shape":"cicle","lineIds":[11]},{"stationId":69,"stationName":"天水围","position":[678,802],"shape":"cicle","lineIds":[11]},{"stationId":70,"stationName":"朗屏","position":[889,802],"shape":"cicle","lineIds":[11]},{"stationId":71,"stationName":"元朗","position":[989,816],"shape":"cicle","lineIds":[11]},{"stationId":72,"stationName":"锦上路","position":[1269,928],"shape":"cicle","lineIds":[11]},{"stationId":73,"stationName":"荃湾西","position":[1737,1594],"shape":"cicle","lineIds":[11]},{"stationId":74,"stationName":"美孚","position":[2013,1898],"shape":"cicle","lineIds":[11]},{"stationId":75,"stationName":"柯士甸","position":[2300,2230],"shape":"cicle","lineIds":[11],"tagDirection":4},{"stationId":76,"stationName":"尖东","position":[2384,2323],"shape":"cicle","lineIds":[11]},{"stationId":77,"stationName":"土瓜湾","position":[2514,2115],"shape":"cicle","lineIds":[11]},{"stationId":78,"stationName":"宋皇台","position":[2545,2020],"shape":"cicle","lineIds":[11]},{"stationId":79,"stationName":"启德","position":[2628,1973],"shape":"cicle","lineIds":[11]},{"stationId":80,"stationName":"显径","position":[2343,1636],"shape":"cicle","lineIds":[11]},{"stationId":81,"stationName":"车公庙","position":[2499,1530],"shape":"cicle","lineIds":[11],"tagDirection":3},{"stationId":82,"stationName":"沙田围","position":[2583,1508],"shape":"cicle","lineIds":[11]},{"stationId":83,"stationName":"第一城","position":[2669,1448],"shape":"cicle","lineIds":[11]},{"stationId":84,"stationName":"石门","position":[2718,1401],"shape":"cicle","lineIds":[11]},{"stationId":85,"stationName":"大水坑","position":[2861,1194],"shape":"cicle","lineIds":[11]},{"stationId":86,"stationName":"恒安","position":[2893,1097],"shape":"cicle","lineIds":[11]},{"stationId":87,"stationName":"马鞍山","position":[2951,1030],"shape":"cicle","lineIds":[11]},{"stationId":88,"stationName":"乌溪沙","position":[3071,987],"shape":"cicle","lineIds":[11]},{"stationId":89,"stationName":"荃湾","position":[1813,1539],"shape":"cicle","lineIds":[12]},{"stationId":90,"stationName":"大窝口","position":[1885,1567],"shape":"cicle","lineIds":[12]},{"stationId":91,"stationName":"葵兴","position":[1948,1644],"shape":"cicle","lineIds":[12]},{"stationId":92,"stationName":"葵芳","position":[1916,1707],"shape":"cicle","lineIds":[12]},{"stationId":93,"stationName":"美孚","position":[1999,1889],"shape":"cicle","lineIds":[12]},{"stationId":94,"stationName":"荔枝角","position":[2118,1903],"shape":"cicle","lineIds":[12]},{"stationId":95,"stationName":"长沙湾","position":[2198,1923],"shape":"cicle","lineIds":[12]},{"stationId":96,"stationName":"深水埗","position":[2259,1969],"shape":"cicle","lineIds":[12],"tagDirection":0},{"stationId":97,"stationName":"佐敦","position":[2351,2229],"shape":"cicle","lineIds":[12],"tagDirection":3},{"stationId":98,"stationName":"尖沙咀","position":[2357,2303],"shape":"cicle","lineIds":[12],"tagDirection":6}],"lines":[{"lineId":1,"lineName":"迪士尼","color":"#EB81B9","stationIds":[1,2],"sign":"迪士尼","order":1,"bendFirst":[true,true]},{"lineId":2,"lineName":"东铁线","color":"#69C7F4","stationIds":[3,4,5,6,7,8,9,10,11,12,13,14,15,16],"sign":"东铁线","order":2,"bendFirst":[true,true,false,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":4,"lineName":"东涌线","color":"#FFA500","stationIds":[18,2,19,20,21,22,23,24],"sign":"东涌线","order":8,"bendFirst":[true,true,true,true,true,true,false,true]},{"lineId":5,"lineName":"港岛线","color":"#3080B7","stationIds":[25,26,27,28,29,3,30,31,32,33,34,35,36,37,38,39,40],"sign":"港岛线","order":5,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":6,"lineName":"观塘线","color":"#00AB4F","stationIds":[41,42,43,44,45,46,7,47,48,49,50,51,52,53,54,55,56],"sign":"观塘线","order":10,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":7,"lineName":"机场快线","color":"#008187","stationIds":[24,23,19,57,58],"sign":"机场快线","order":7,"bendFirst":[true,true,true,true,true]},{"lineId":8,"lineName":"将军澳线","color":"#793E8C","stationIds":[59,60],"sign":"将军澳线","order":8,"bendFirst":[true,true],"subLine":true},{"lineId":9,"lineName":"将军澳线","color":"#793E8C","stationIds":[34,35,55,56,60,61,62],"sign":"将军澳线","order":4,"bendFirst":[true,true,true,true,true,true,true]},{"lineId":10,"lineName":"南港岛线","color":"#B8D201","stationIds":[3,63,64,65,66],"sign":"南港岛线","order":10,"bendFirst":[true,true,true,true,true]},{"lineId":11,"lineName":"屯马线","color":"#871C2A","stationIds":[67,68,69,70,71,72,73,74,21,75,76,5,42,77,78,79,49,80,8,81,82,83,84,85,86,87,88],"sign":"屯马线","order":11,"bendFirst":[true,true,true,true,true,true,true,true,false,true,true,true,true,true,true,true,true,true,false,true,true,true,true,true,true,true,true]},{"lineId":12,"lineName":"荃湾线","color":"#FC0601","stationIds":[89,90,91,92,20,93,94,95,96,45,44,43,97,98,3,29],"sign":"荃湾线","order":12,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":13,"lineName":"东铁线","color":"#69C7F4","stationIds":[17,15],"sign":"东铁线","order":13,"bendFirst":[true,true],"subLine":true}],"title":"香港"} ================================================ FILE: public/index.html ================================================ Mini Metro Web
================================================ FILE: public/manifest.json ================================================ { "short_name": "Mini Metro Web", "name": "Mini Metro Web", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "app-icon.png", "type": "image/png", "sizes": "192x192" }, { "src": "app-icon.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#ffffff", "background_color": "#ffffff" } ================================================ FILE: public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: public/shanghai.json ================================================ {"stations":[{"stationId":1,"stationName":"莘庄","position":[3658,2970],"shape":"cicle","lineIds":[1,5]},{"stationId":2,"stationName":"外环路","position":[3735,2872],"shape":"cicle","lineIds":[1]},{"stationId":3,"stationName":"莲花路","position":[3834,2772],"shape":"cicle","lineIds":[1]},{"stationId":4,"stationName":"锦江乐园","position":[3946,2659],"shape":"cicle","lineIds":[1]},{"stationId":5,"stationName":"上海南站","position":[4105,2535],"shape":"cicle","lineIds":[1,3,18]},{"stationId":6,"stationName":"漕宝路","position":[4136,2398],"shape":"cicle","lineIds":[1,15],"tagDirection":3},{"stationId":7,"stationName":"上海体育馆","position":[4179,2253],"shape":"cicle","lineIds":[1,4],"tagDirection":5},{"stationId":8,"stationName":"徐家汇","position":[4171,2126],"shape":"cicle","lineIds":[1,10,14]},{"stationId":9,"stationName":"衡山路","position":[4269,2036],"shape":"cicle","lineIds":[1],"tagDirection":4},{"stationId":10,"stationName":"常熟路","position":[4296,1946],"shape":"cicle","lineIds":[1,8]},{"stationId":11,"stationName":"陕西南路","position":[4392,1930],"shape":"cicle","lineIds":[1,11,15]},{"stationId":12,"stationName":"黄陂南路","position":[4534,1842],"shape":"cicle","lineIds":[1,17],"tagDirection":7},{"stationId":13,"stationName":"人民广场","position":[4556,1754],"shape":"cicle","lineIds":[1,2,9]},{"stationId":14,"stationName":"新闸路","position":[4486,1697],"shape":"cicle","lineIds":[1],"tagDirection":4},{"stationId":15,"stationName":"汉中路","position":[4392,1662],"shape":"cicle","lineIds":[1,15,16],"tagDirection":7},{"stationId":16,"stationName":"上海火车站","position":[4384,1585],"shape":"cicle","lineIds":[1,3,4],"tagDirection":3},{"stationId":17,"stationName":"中山北路","position":[4397,1492],"shape":"cicle","lineIds":[1]},{"stationId":18,"stationName":"延长路","position":[4358,1364],"shape":"cicle","lineIds":[1]},{"stationId":19,"stationName":"上海马戏城","position":[4325,1282],"shape":"cicle","lineIds":[1]},{"stationId":20,"stationName":"汶水路","position":[4307,1156],"shape":"cicle","lineIds":[1]},{"stationId":21,"stationName":"彭浦新村","position":[4291,1015],"shape":"cicle","lineIds":[1]},{"stationId":22,"stationName":"共康路","position":[4275,892],"shape":"cicle","lineIds":[1]},{"stationId":23,"stationName":"通河新村","position":[4220,770],"shape":"cicle","lineIds":[1]},{"stationId":24,"stationName":"呼兰路","position":[4182,684],"shape":"cicle","lineIds":[1]},{"stationId":25,"stationName":"共富新村","position":[4145,530],"shape":"cicle","lineIds":[1]},{"stationId":26,"stationName":"宝安公路","position":[4114,386],"shape":"cicle","lineIds":[1]},{"stationId":27,"stationName":"友谊西路","position":[4084,268],"shape":"cicle","lineIds":[1]},{"stationId":28,"stationName":"富锦路","position":[4051,159],"shape":"cicle","lineIds":[1]},{"stationId":29,"stationName":"浦东国际机场","position":[7861,2572],"shape":"cicle","lineIds":[2,22]},{"stationId":30,"stationName":"海天三路","position":[7773,2397],"shape":"cicle","lineIds":[2]},{"stationId":31,"stationName":"远东大道","position":[7358,2087],"shape":"cicle","lineIds":[2]},{"stationId":32,"stationName":"凌空路","position":[7046,2153],"shape":"cicle","lineIds":[2]},{"stationId":33,"stationName":"川沙","position":[6787,2214],"shape":"cicle","lineIds":[2]},{"stationId":34,"stationName":"华夏东路","position":[6614,2115],"shape":"cicle","lineIds":[2]},{"stationId":35,"stationName":"创新中路","position":[6542,1942],"shape":"cicle","lineIds":[2]},{"stationId":36,"stationName":"唐镇","position":[6367,1940],"shape":"cicle","lineIds":[2]},{"stationId":37,"stationName":"广兰路","position":[6015,1971],"shape":"cicle","lineIds":[2]},{"stationId":38,"stationName":"金科路","position":[5825,2039],"shape":"cicle","lineIds":[2],"tagDirection":0},{"stationId":39,"stationName":"张江高科","position":[5680,2062],"shape":"cicle","lineIds":[2]},{"stationId":40,"stationName":"龙阳路","position":[5381,2045],"shape":"cicle","lineIds":[2,8,19,21,22]},{"stationId":41,"stationName":"世纪公园","position":[5314,1987],"shape":"cicle","lineIds":[2]},{"stationId":42,"stationName":"上海科技馆","position":[5249,1893],"shape":"cicle","lineIds":[2],"tagDirection":5},{"stationId":43,"stationName":"世纪大道","position":[5077,1794],"shape":"cicle","lineIds":[2,4,7,10]},{"stationId":44,"stationName":"东昌路","position":[4960,1748],"shape":"cicle","lineIds":[2],"tagDirection":6},{"stationId":45,"stationName":"陆家嘴","position":[4826,1699],"shape":"cicle","lineIds":[2,17]},{"stationId":46,"stationName":"南京东路","position":[4651,1700],"shape":"cicle","lineIds":[2,11]},{"stationId":47,"stationName":"南京西路","position":[4404,1783],"shape":"cicle","lineIds":[2,15,16]},{"stationId":48,"stationName":"静安寺","position":[4270,1849],"shape":"cicle","lineIds":[2,8,17],"tagDirection":1},{"stationId":49,"stationName":"江苏路","position":[4111,1877],"shape":"cicle","lineIds":[2,14]},{"stationId":50,"stationName":"中山公园","position":[3962,1902],"shape":"cicle","lineIds":[2,3,4]},{"stationId":51,"stationName":"娄山关路","position":[3845,1970],"shape":"cicle","lineIds":[2,18]},{"stationId":52,"stationName":"威宁路","position":[3678,1932],"shape":"cicle","lineIds":[2]},{"stationId":53,"stationName":"北新泾","position":[3545,1917],"shape":"cicle","lineIds":[2]},{"stationId":54,"stationName":"淞虹路","position":[3400,1899],"shape":"cicle","lineIds":[2]},{"stationId":55,"stationName":"虹桥2号航站楼","position":[3068,2139],"shape":"cicle","lineIds":[2,11],"tagDirection":4},{"stationId":56,"stationName":"虹桥火车站","position":[2994,2141],"shape":"cicle","lineIds":[2,20,11],"tagDirection":7},{"stationId":57,"stationName":"徐泾东","position":[2797,2198],"shape":"cicle","lineIds":[2],"tagDirection":4},{"stationId":58,"stationName":"江杨北路","position":[4203,3],"shape":"cicle","lineIds":[3]},{"stationId":59,"stationName":"铁力路","position":[4416,0],"shape":"cicle","lineIds":[3]},{"stationId":60,"stationName":"友谊路","position":[4564,41],"shape":"cicle","lineIds":[3]},{"stationId":61,"stationName":"宝杨路","position":[4600,128],"shape":"cicle","lineIds":[3]},{"stationId":62,"stationName":"水产路","position":[4687,268],"shape":"cicle","lineIds":[3]},{"stationId":63,"stationName":"淞滨路","position":[4733,372],"shape":"cicle","lineIds":[3]},{"stationId":64,"stationName":"张华浜","position":[4791,501],"shape":"cicle","lineIds":[3]},{"stationId":65,"stationName":"淞发路","position":[4809,630],"shape":"cicle","lineIds":[3]},{"stationId":66,"stationName":"长江南路","position":[4719,761],"shape":"cicle","lineIds":[3,21]},{"stationId":67,"stationName":"殷高西路","position":[4653,881],"shape":"cicle","lineIds":[3]},{"stationId":68,"stationName":"江湾镇","position":[4655,1026],"shape":"cicle","lineIds":[3],"tagDirection":6},{"stationId":69,"stationName":"大柏树","position":[4637,1187],"shape":"cicle","lineIds":[3]},{"stationId":70,"stationName":"赤峰路","position":[4629,1269],"shape":"cicle","lineIds":[3]},{"stationId":71,"stationName":"虹口足球场","position":[4596,1367],"shape":"cicle","lineIds":[3,9]},{"stationId":72,"stationName":"东宝兴路","position":[4607,1482],"shape":"cicle","lineIds":[3],"tagDirection":6},{"stationId":73,"stationName":"宝山路","position":[4568,1566],"shape":"cicle","lineIds":[3,4],"tagDirection":4},{"stationId":74,"stationName":"中潭路","position":[4215,1536],"shape":"cicle","lineIds":[3,4]},{"stationId":75,"stationName":"镇坪路","position":[4102,1618],"shape":"cicle","lineIds":[3,4,8]},{"stationId":76,"stationName":"曹杨路","position":[3982,1688],"shape":"cicle","lineIds":[3,4,14,17]},{"stationId":77,"stationName":"金沙江路","position":[3937,1760],"shape":"cicle","lineIds":[3,4,16],"tagDirection":5},{"stationId":78,"stationName":"延安西路","position":[3975,1985],"shape":"cicle","lineIds":[3,4]},{"stationId":79,"stationName":"虹桥路","position":[4013,2106],"shape":"cicle","lineIds":[3,4,11],"tagDirection":1},{"stationId":80,"stationName":"宜山路","position":[4077,2214],"shape":"cicle","lineIds":[3,4,10],"tagDirection":2},{"stationId":81,"stationName":"漕溪路","position":[4189,2314],"shape":"cicle","lineIds":[3],"tagDirection":5},{"stationId":82,"stationName":"龙漕路","position":[4248,2387],"shape":"cicle","lineIds":[3,15]},{"stationId":83,"stationName":"石龙路","position":[4237,2502],"shape":"cicle","lineIds":[3]},{"stationId":84,"stationName":"海伦路","position":[4692,1488],"shape":"cicle","lineIds":[4,11]},{"stationId":85,"stationName":"临平路","position":[4812,1472],"shape":"cicle","lineIds":[4]},{"stationId":86,"stationName":"大连路","position":[4936,1502],"shape":"cicle","lineIds":[4,15]},{"stationId":87,"stationName":"杨树浦路","position":[4977,1562],"shape":"cicle","lineIds":[4],"tagDirection":2},{"stationId":88,"stationName":"浦东大道","position":[4998,1678],"shape":"cicle","lineIds":[4,17],"tagDirection":1},{"stationId":89,"stationName":"浦电路(4号线)","position":[5125,1858],"shape":"cicle","lineIds":[4],"tagDirection":2},{"stationId":90,"stationName":"蓝村路","position":[5081,1964],"shape":"cicle","lineIds":[4,7]},{"stationId":91,"stationName":"塘桥","position":[4992,1984],"shape":"cicle","lineIds":[4],"tagDirection":7},{"stationId":92,"stationName":"南浦大桥","position":[4802,1996],"shape":"cicle","lineIds":[4]},{"stationId":93,"stationName":"西藏南路","position":[4700,2062],"shape":"cicle","lineIds":[4,9],"tagDirection":3},{"stationId":94,"stationName":"鲁班路","position":[4556,2089],"shape":"cicle","lineIds":[4]},{"stationId":95,"stationName":"大木桥路","position":[4437,2141],"shape":"cicle","lineIds":[4,15],"tagDirection":3},{"stationId":96,"stationName":"东安路","position":[4354,2173],"shape":"cicle","lineIds":[4,8]},{"stationId":97,"stationName":"上海体育场","position":[4240,2225],"shape":"cicle","lineIds":[4],"tagDirection":0},{"stationId":98,"stationName":"闵行开发区","position":[3502,4075],"shape":"cicle","lineIds":[24]},{"stationId":99,"stationName":"文井路","position":[3612,4045],"shape":"cicle","lineIds":[24]},{"stationId":100,"stationName":"华宁路","position":[3757,4007],"shape":"cicle","lineIds":[24]},{"stationId":101,"stationName":"金平路","position":[3907,3968],"shape":"cicle","lineIds":[24]},{"stationId":102,"stationName":"东川路","position":[4004,3899],"shape":"cicle","lineIds":[5,24]},{"stationId":103,"stationName":"剑川路","position":[3970,3816],"shape":"cicle","lineIds":[5]},{"stationId":104,"stationName":"北桥","position":[3905,3630],"shape":"cicle","lineIds":[5]},{"stationId":105,"stationName":"颛桥","position":[3823,3411],"shape":"cicle","lineIds":[5]},{"stationId":106,"stationName":"银都路","position":[3707,3188],"shape":"cicle","lineIds":[5]},{"stationId":107,"stationName":"春申路","position":[3663,3099],"shape":"cicle","lineIds":[5]},{"stationId":108,"stationName":"江川路","position":[4039,4027],"shape":"cicle","lineIds":[5]},{"stationId":109,"stationName":"西渡","position":[4129,4188],"shape":"cicle","lineIds":[5]},{"stationId":110,"stationName":"萧塘","position":[4223,4422],"shape":"cicle","lineIds":[5]},{"stationId":111,"stationName":"奉浦大道","position":[4295,4661],"shape":"cicle","lineIds":[5]},{"stationId":112,"stationName":"环城东路","position":[4437,4770],"shape":"cicle","lineIds":[5]},{"stationId":113,"stationName":"望园路","position":[4640,4762],"shape":"cicle","lineIds":[5],"tagDirection":0},{"stationId":114,"stationName":"金海湖","position":[4730,4793],"shape":"cicle","lineIds":[5]},{"stationId":115,"stationName":"奉贤新城","position":[4768,4942],"shape":"cicle","lineIds":[5]},{"stationId":116,"stationName":"东方体育中心","position":[4608,2548],"shape":"cicle","lineIds":[7,9,14]},{"stationId":117,"stationName":"灵岩南路","position":[4758,2595],"shape":"cicle","lineIds":[7]},{"stationId":118,"stationName":"上南路","position":[4869,2593],"shape":"cicle","lineIds":[7],"tagDirection":0},{"stationId":119,"stationName":"华夏西路","position":[4950,2582],"shape":"cicle","lineIds":[7],"tagDirection":2},{"stationId":120,"stationName":"高青路","position":[4963,2482],"shape":"cicle","lineIds":[7]},{"stationId":121,"stationName":"东明路","position":[4913,2355],"shape":"cicle","lineIds":[7,16]},{"stationId":122,"stationName":"高科西路","position":[4903,2224],"shape":"cicle","lineIds":[7,8],"tagDirection":7},{"stationId":123,"stationName":"临沂新村","position":[4972,2149],"shape":"cicle","lineIds":[7]},{"stationId":124,"stationName":"上海儿童医学中心","position":[5039,2048],"shape":"cicle","lineIds":[7]},{"stationId":125,"stationName":"浦电路(6号线)","position":[5098,1880],"shape":"cicle","lineIds":[7],"tagDirection":6},{"stationId":126,"stationName":"源深体育中心","position":[5151,1751],"shape":"cicle","lineIds":[7],"tagDirection":2},{"stationId":127,"stationName":"民生路","position":[5240,1723],"shape":"cicle","lineIds":[7,21]},{"stationId":128,"stationName":"北洋泾路","position":[5328,1690],"shape":"cicle","lineIds":[7]},{"stationId":129,"stationName":"德平路","position":[5447,1627],"shape":"cicle","lineIds":[7],"tagDirection":4},{"stationId":130,"stationName":"云山路","position":[5516,1572],"shape":"cicle","lineIds":[7,17]},{"stationId":131,"stationName":"金桥路","position":[5623,1510],"shape":"cicle","lineIds":[7]},{"stationId":132,"stationName":"博兴路","position":[5672,1446],"shape":"cicle","lineIds":[7]},{"stationId":133,"stationName":"五莲路","position":[5683,1361],"shape":"cicle","lineIds":[7]},{"stationId":134,"stationName":"巨峰路","position":[5688,1274],"shape":"cicle","lineIds":[7,15],"tagDirection":1},{"stationId":135,"stationName":"东靖路","position":[5692,1174],"shape":"cicle","lineIds":[7]},{"stationId":136,"stationName":"五洲大道","position":[5697,1056],"shape":"cicle","lineIds":[7]},{"stationId":137,"stationName":"洲海路","position":[5699,959],"shape":"cicle","lineIds":[7]},{"stationId":138,"stationName":"外高桥保税区南","position":[5824,865],"shape":"cicle","lineIds":[7]},{"stationId":139,"stationName":"航津路","position":[5744,727],"shape":"cicle","lineIds":[7]},{"stationId":140,"stationName":"外高桥保税区北","position":[5674,603],"shape":"cicle","lineIds":[7],"tagDirection":5},{"stationId":141,"stationName":"港城路","position":[5555,549],"shape":"cicle","lineIds":[7,11],"tagDirection":1},{"stationId":142,"stationName":"花木路","position":[5432,1969],"shape":"cicle","lineIds":[8],"tagDirection":2},{"stationId":143,"stationName":"芳华路","position":[5306,2150],"shape":"cicle","lineIds":[8],"tagDirection":7},{"stationId":144,"stationName":"锦绣路","position":[5205,2205],"shape":"cicle","lineIds":[8]},{"stationId":145,"stationName":"杨高南路","position":[5050,2205],"shape":"cicle","lineIds":[8]},{"stationId":146,"stationName":"云台路","position":[4809,2259],"shape":"cicle","lineIds":[8],"tagDirection":3},{"stationId":147,"stationName":"耀华路","position":[4751,2296],"shape":"cicle","lineIds":[8,9],"tagDirection":3},{"stationId":148,"stationName":"长清路","position":[4682,2337],"shape":"cicle","lineIds":[8,16]},{"stationId":149,"stationName":"后滩","position":[4542,2362],"shape":"cicle","lineIds":[8]},{"stationId":150,"stationName":"龙华中路","position":[4375,2237],"shape":"cicle","lineIds":[8,15]},{"stationId":151,"stationName":"肇嘉浜路","position":[4307,2087],"shape":"cicle","lineIds":[8,10],"tagDirection":3},{"stationId":152,"stationName":"昌平路","position":[4231,1744],"shape":"cicle","lineIds":[8]},{"stationId":153,"stationName":"长寿路","position":[4187,1673],"shape":"cicle","lineIds":[8,16]},{"stationId":154,"stationName":"岚皋路","position":[4024,1519],"shape":"cicle","lineIds":[8]},{"stationId":155,"stationName":"新村路","position":[4031,1443],"shape":"cicle","lineIds":[8]},{"stationId":156,"stationName":"大华三路","position":[4034,1341],"shape":"cicle","lineIds":[8]},{"stationId":157,"stationName":"行知路","position":[4022,1236],"shape":"cicle","lineIds":[8]},{"stationId":158,"stationName":"大场镇","position":[3966,1149],"shape":"cicle","lineIds":[8]},{"stationId":159,"stationName":"场中路","position":[3939,1044],"shape":"cicle","lineIds":[8]},{"stationId":160,"stationName":"上大路","position":[3892,932],"shape":"cicle","lineIds":[8]},{"stationId":161,"stationName":"南陈路","position":[3789,864],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":162,"stationName":"上海大学","position":[3694,875],"shape":"cicle","lineIds":[8],"tagDirection":0},{"stationId":163,"stationName":"祁华路","position":[3540,857],"shape":"cicle","lineIds":[8]},{"stationId":164,"stationName":"顾村公园","position":[3535,635],"shape":"cicle","lineIds":[8,18]},{"stationId":165,"stationName":"刘行","position":[3428,505],"shape":"cicle","lineIds":[8]},{"stationId":166,"stationName":"潘广路","position":[3363,439],"shape":"cicle","lineIds":[8]},{"stationId":167,"stationName":"罗南新村","position":[3379,193],"shape":"cicle","lineIds":[8]},{"stationId":168,"stationName":"美兰湖","position":[3304,62],"shape":"cicle","lineIds":[8]},{"stationId":169,"stationName":"沈杜公路","position":[4927,3467],"shape":"cicle","lineIds":[9,23]},{"stationId":170,"stationName":"联航路","position":[4911,3346],"shape":"cicle","lineIds":[9]},{"stationId":171,"stationName":"江月路","position":[4891,3239],"shape":"cicle","lineIds":[9]},{"stationId":172,"stationName":"浦江镇","position":[4867,3115],"shape":"cicle","lineIds":[9]},{"stationId":173,"stationName":"芦恒路","position":[4787,2890],"shape":"cicle","lineIds":[9]},{"stationId":174,"stationName":"凌兆新村","position":[4701,2668],"shape":"cicle","lineIds":[9]},{"stationId":175,"stationName":"杨思","position":[4739,2471],"shape":"cicle","lineIds":[9]},{"stationId":176,"stationName":"成山路","position":[4767,2374],"shape":"cicle","lineIds":[9,16]},{"stationId":177,"stationName":"中华艺术宫","position":[4741,2229],"shape":"cicle","lineIds":[9]},{"stationId":178,"stationName":"陆家浜路","position":[4666,1963],"shape":"cicle","lineIds":[9,10]},{"stationId":179,"stationName":"老西门","position":[4643,1891],"shape":"cicle","lineIds":[9,11]},{"stationId":180,"stationName":"大世界","position":[4596,1807],"shape":"cicle","lineIds":[9,17],"tagDirection":1},{"stationId":181,"stationName":"曲阜路","position":[4520,1658],"shape":"cicle","lineIds":[9,15]},{"stationId":182,"stationName":"中兴路","position":[4495,1549],"shape":"cicle","lineIds":[9],"tagDirection":1},{"stationId":183,"stationName":"西藏北路","position":[4493,1447],"shape":"cicle","lineIds":[9]},{"stationId":184,"stationName":"曲阳路","position":[4716,1316],"shape":"cicle","lineIds":[9]},{"stationId":185,"stationName":"四平路","position":[4820,1332],"shape":"cicle","lineIds":[9,11]},{"stationId":186,"stationName":"鞍山新村","position":[4902,1349],"shape":"cicle","lineIds":[9]},{"stationId":187,"stationName":"江浦路","position":[4988,1332],"shape":"cicle","lineIds":[9,21]},{"stationId":188,"stationName":"黄兴路","position":[5089,1293],"shape":"cicle","lineIds":[9]},{"stationId":189,"stationName":"延吉中路","position":[5154,1196],"shape":"cicle","lineIds":[9]},{"stationId":190,"stationName":"黄兴公园","position":[5138,1127],"shape":"cicle","lineIds":[9]},{"stationId":191,"stationName":"翔殷路","position":[5124,1031],"shape":"cicle","lineIds":[9],"tagDirection":2},{"stationId":192,"stationName":"嫩江路","position":[5124,933],"shape":"cicle","lineIds":[9],"tagDirection":2},{"stationId":193,"stationName":"市光路","position":[5124,854],"shape":"cicle","lineIds":[9],"tagDirection":2},{"stationId":194,"stationName":"曹路","position":[6636,1368],"shape":"cicle","lineIds":[10]},{"stationId":195,"stationName":"民雷路","position":[6486,1398],"shape":"cicle","lineIds":[10]},{"stationId":196,"stationName":"顾唐路","position":[6369,1420],"shape":"cicle","lineIds":[10]},{"stationId":197,"stationName":"金海路","position":[6191,1450],"shape":"cicle","lineIds":[10,15],"tagDirection":1},{"stationId":198,"stationName":"金吉路","position":[6092,1438],"shape":"cicle","lineIds":[10]},{"stationId":199,"stationName":"金桥","position":[5918,1473],"shape":"cicle","lineIds":[10]},{"stationId":200,"stationName":"台儿庄路","position":[5779,1553],"shape":"cicle","lineIds":[10],"tagDirection":3},{"stationId":201,"stationName":"蓝天路","position":[5585,1671],"shape":"cicle","lineIds":[10,17]},{"stationId":202,"stationName":"芳甸路","position":[5389,1763],"shape":"cicle","lineIds":[10]},{"stationId":203,"stationName":"杨高中路","position":[5291,1806],"shape":"cicle","lineIds":[10,21],"tagDirection":6},{"stationId":204,"stationName":"商城路","position":[4967,1779],"shape":"cicle","lineIds":[10],"tagDirection":6},{"stationId":205,"stationName":"小南门","position":[4789,1913],"shape":"cicle","lineIds":[10]},{"stationId":206,"stationName":"马当路","position":[4577,1986],"shape":"cicle","lineIds":[10,16]},{"stationId":207,"stationName":"打浦桥","position":[4491,2018],"shape":"cicle","lineIds":[10]},{"stationId":208,"stationName":"嘉善路","position":[4412,2053],"shape":"cicle","lineIds":[10,15]},{"stationId":209,"stationName":"桂林路","position":[3985,2333],"shape":"cicle","lineIds":[10,18]},{"stationId":210,"stationName":"漕河泾开发区","position":[3782,2375],"shape":"cicle","lineIds":[10]},{"stationId":211,"stationName":"合川路","position":[3652,2416],"shape":"cicle","lineIds":[10]},{"stationId":212,"stationName":"星中路","position":[3494,2500],"shape":"cicle","lineIds":[10]},{"stationId":213,"stationName":"七宝","position":[3297,2528],"shape":"cicle","lineIds":[10]},{"stationId":214,"stationName":"中春路","position":[3182,2587],"shape":"cicle","lineIds":[10]},{"stationId":215,"stationName":"九亭","position":[2999,2709],"shape":"cicle","lineIds":[10]},{"stationId":216,"stationName":"泗泾","position":[2407,2898],"shape":"cicle","lineIds":[10]},{"stationId":217,"stationName":"佘山","position":[2102,3040],"shape":"cicle","lineIds":[10]},{"stationId":218,"stationName":"洞泾","position":[2109,3236],"shape":"cicle","lineIds":[10]},{"stationId":219,"stationName":"松江大学城","position":[2130,3541],"shape":"cicle","lineIds":[10]},{"stationId":220,"stationName":"松江新城","position":[2112,3778],"shape":"cicle","lineIds":[10]},{"stationId":221,"stationName":"松江体育中心","position":[2110,3921],"shape":"cicle","lineIds":[10]},{"stationId":222,"stationName":"醉白池","position":[2098,4070],"shape":"cicle","lineIds":[10]},{"stationId":223,"stationName":"松江南站","position":[2114,4233],"shape":"cicle","lineIds":[10]},{"stationId":224,"stationName":"基隆路","position":[5709,568],"shape":"cicle","lineIds":[11]},{"stationId":225,"stationName":"高桥","position":[5434,555],"shape":"cicle","lineIds":[11],"tagDirection":0},{"stationId":226,"stationName":"高桥西","position":[5297,566],"shape":"cicle","lineIds":[11]},{"stationId":227,"stationName":"双江路","position":[5199,545],"shape":"cicle","lineIds":[11]},{"stationId":228,"stationName":"国帆路","position":[4936,686],"shape":"cicle","lineIds":[11],"tagDirection":3},{"stationId":229,"stationName":"新江湾城","position":[4874,796],"shape":"cicle","lineIds":[11]},{"stationId":230,"stationName":"殷高东路","position":[4873,864],"shape":"cicle","lineIds":[11]},{"stationId":231,"stationName":"三门路","position":[4888,949],"shape":"cicle","lineIds":[11],"tagDirection":1},{"stationId":232,"stationName":"江湾体育场","position":[4938,1038],"shape":"cicle","lineIds":[11],"tagDirection":2},{"stationId":233,"stationName":"五角场","position":[4951,1101],"shape":"cicle","lineIds":[11]},{"stationId":234,"stationName":"国权路","position":[4905,1188],"shape":"cicle","lineIds":[11,21]},{"stationId":235,"stationName":"同济大学","position":[4868,1260],"shape":"cicle","lineIds":[11]},{"stationId":236,"stationName":"邮电新村","position":[4747,1397],"shape":"cicle","lineIds":[11]},{"stationId":237,"stationName":"四川北路","position":[4647,1561],"shape":"cicle","lineIds":[11],"tagDirection":2},{"stationId":238,"stationName":"天潼路","position":[4628,1643],"shape":"cicle","lineIds":[11,15]},{"stationId":239,"stationName":"豫园","position":[4679,1801],"shape":"cicle","lineIds":[11,17]},{"stationId":240,"stationName":"新天地","position":[4556,1918],"shape":"cicle","lineIds":[11,16],"tagDirection":0},{"stationId":241,"stationName":"上海图书馆","position":[4248,2002],"shape":"cicle","lineIds":[11]},{"stationId":242,"stationName":"交通大学","position":[4157,2059],"shape":"cicle","lineIds":[11,14]},{"stationId":243,"stationName":"宋园路","position":[3925,2116],"shape":"cicle","lineIds":[11],"tagDirection":3},{"stationId":244,"stationName":"伊犁路","position":[3844,2092],"shape":"cicle","lineIds":[11],"tagDirection":0},{"stationId":245,"stationName":"水城路","position":[3727,2086],"shape":"cicle","lineIds":[11]},{"stationId":246,"stationName":"龙溪路","position":[3605,2137],"shape":"cicle","lineIds":[11,25]},{"stationId":247,"stationName":"龙柏新村","position":[3509,2312],"shape":"cicle","lineIds":[25]},{"stationId":248,"stationName":"紫藤路","position":[3453,2384],"shape":"cicle","lineIds":[25]},{"stationId":249,"stationName":"航中路","position":[3358,2427],"shape":"cicle","lineIds":[25]},{"stationId":250,"stationName":"上海动物园","position":[3486,2179],"shape":"cicle","lineIds":[11]},{"stationId":251,"stationName":"虹桥1号航站楼","position":[3278,2168],"shape":"cicle","lineIds":[11]},{"stationId":252,"stationName":"嘉定北","position":[2178,166],"shape":"cicle","lineIds":[26]},{"stationId":253,"stationName":"嘉定西","position":[2083,310],"shape":"cicle","lineIds":[26]},{"stationId":254,"stationName":"白银路","position":[2258,628],"shape":"cicle","lineIds":[26]},{"stationId":255,"stationName":"嘉定新城","position":[2348,781],"shape":"cicle","lineIds":[14,26]},{"stationId":256,"stationName":"马陆","position":[2574,884],"shape":"cicle","lineIds":[14]},{"stationId":257,"stationName":"陈翔公路","position":[2873,1016],"shape":"cicle","lineIds":[14]},{"stationId":258,"stationName":"南翔","position":[3036,1112],"shape":"cicle","lineIds":[14]},{"stationId":259,"stationName":"桃浦新村","position":[3301,1265],"shape":"cicle","lineIds":[14]},{"stationId":260,"stationName":"武威路","position":[3451,1315],"shape":"cicle","lineIds":[14]},{"stationId":261,"stationName":"祁连山路","position":[3565,1365],"shape":"cicle","lineIds":[14]},{"stationId":262,"stationName":"李子园","position":[3704,1391],"shape":"cicle","lineIds":[14]},{"stationId":263,"stationName":"上海西站","position":[3814,1454],"shape":"cicle","lineIds":[14,18]},{"stationId":264,"stationName":"真如","position":[3877,1574],"shape":"cicle","lineIds":[14,17]},{"stationId":265,"stationName":"枫桥路","position":[3919,1663],"shape":"cicle","lineIds":[14],"tagDirection":6},{"stationId":266,"stationName":"隆德路","position":[4040,1776],"shape":"cicle","lineIds":[14,16]},{"stationId":267,"stationName":"上海游泳馆","position":[4219,2290],"shape":"cicle","lineIds":[14],"tagDirection":1},{"stationId":268,"stationName":"龙华","position":[4334,2354],"shape":"cicle","lineIds":[14,15]},{"stationId":269,"stationName":"云锦路","position":[4390,2416],"shape":"cicle","lineIds":[14]},{"stationId":270,"stationName":"龙耀路","position":[4402,2484],"shape":"cicle","lineIds":[14]},{"stationId":271,"stationName":"三林","position":[4923,2649],"shape":"cicle","lineIds":[14]},{"stationId":272,"stationName":"三林东","position":[5035,2617],"shape":"cicle","lineIds":[14],"tagDirection":3},{"stationId":273,"stationName":"浦三路","position":[5195,2573],"shape":"cicle","lineIds":[14]},{"stationId":274,"stationName":"御桥","position":[5514,2500],"shape":"cicle","lineIds":[14,21],"tagDirection":7},{"stationId":275,"stationName":"罗山路","position":[5736,2549],"shape":"cicle","lineIds":[14,19]},{"stationId":276,"stationName":"秀沿路","position":[5789,2700],"shape":"cicle","lineIds":[14]},{"stationId":277,"stationName":"康新公路","position":[5977,2777],"shape":"cicle","lineIds":[14]},{"stationId":278,"stationName":"迪士尼","position":[6484,2669],"shape":"cicle","lineIds":[14]},{"stationId":279,"stationName":"上海赛车场","position":[2066,762],"shape":"cicle","lineIds":[14]},{"stationId":280,"stationName":"昌吉东路","position":[1809,1145],"shape":"cicle","lineIds":[14]},{"stationId":281,"stationName":"上海汽车城","position":[1612,1227],"shape":"cicle","lineIds":[14]},{"stationId":282,"stationName":"安亭","position":[1425,1196],"shape":"cicle","lineIds":[14]},{"stationId":283,"stationName":"兆丰路","position":[1308,1191],"shape":"cicle","lineIds":[14]},{"stationId":284,"stationName":"光明路","position":[977,1119],"shape":"cicle","lineIds":[14]},{"stationId":285,"stationName":"花桥","position":[849,1093],"shape":"cicle","lineIds":[14]},{"stationId":286,"stationName":"七莘路","position":[3429,2765],"shape":"cicle","lineIds":[15]},{"stationId":287,"stationName":"虹莘路","position":[3608,2707],"shape":"cicle","lineIds":[15]},{"stationId":288,"stationName":"顾戴路","position":[3723,2672],"shape":"cicle","lineIds":[15]},{"stationId":289,"stationName":"东兰路","position":[3724,2523],"shape":"cicle","lineIds":[15]},{"stationId":290,"stationName":"虹梅路","position":[3777,2478],"shape":"cicle","lineIds":[15]},{"stationId":291,"stationName":"虹漕路","position":[3910,2441],"shape":"cicle","lineIds":[15]},{"stationId":292,"stationName":"桂林公园","position":[4001,2411],"shape":"cicle","lineIds":[15,18],"tagDirection":1},{"stationId":293,"stationName":"国际客运中心","position":[4786,1580],"shape":"cicle","lineIds":[15]},{"stationId":294,"stationName":"提篮桥","position":[4873,1546],"shape":"cicle","lineIds":[15],"tagDirection":7},{"stationId":295,"stationName":"江浦公园","position":[5042,1436],"shape":"cicle","lineIds":[15,21]},{"stationId":296,"stationName":"宁国路","position":[5128,1395],"shape":"cicle","lineIds":[15]},{"stationId":297,"stationName":"隆昌路","position":[5252,1329],"shape":"cicle","lineIds":[15]},{"stationId":298,"stationName":"爱国路","position":[5331,1283],"shape":"cicle","lineIds":[15]},{"stationId":299,"stationName":"复兴岛","position":[5418,1273],"shape":"cicle","lineIds":[15]},{"stationId":300,"stationName":"东陆路","position":[5596,1256],"shape":"cicle","lineIds":[15]},{"stationId":301,"stationName":"杨高北路","position":[5834,1280],"shape":"cicle","lineIds":[15]},{"stationId":302,"stationName":"金京路","position":[5959,1283],"shape":"cicle","lineIds":[15]},{"stationId":303,"stationName":"申江路","position":[6073,1279],"shape":"cicle","lineIds":[15]},{"stationId":304,"stationName":"金运路","position":[2998,1671],"shape":"cicle","lineIds":[16]},{"stationId":305,"stationName":"金沙江西路","position":[3156,1670],"shape":"cicle","lineIds":[16]},{"stationId":306,"stationName":"丰庄","position":[3356,1656],"shape":"cicle","lineIds":[16]},{"stationId":307,"stationName":"祁连山南路","position":[3477,1705],"shape":"cicle","lineIds":[16]},{"stationId":308,"stationName":"真北路","position":[3624,1759],"shape":"cicle","lineIds":[16]},{"stationId":309,"stationName":"大渡河路","position":[3749,1764],"shape":"cicle","lineIds":[16,18],"tagDirection":5},{"stationId":310,"stationName":"武宁路","position":[4110,1738],"shape":"cicle","lineIds":[16,17]},{"stationId":311,"stationName":"江宁路","position":[4253,1638],"shape":"cicle","lineIds":[16]},{"stationId":312,"stationName":"自然博物馆","position":[4428,1717],"shape":"cicle","lineIds":[16],"tagDirection":6},{"stationId":313,"stationName":"淮海中路","position":[4448,1881],"shape":"cicle","lineIds":[16],"tagDirection":6},{"stationId":314,"stationName":"世博会博物馆","position":[4621,2105],"shape":"cicle","lineIds":[16],"tagDirection":4},{"stationId":315,"stationName":"世博大道","position":[4648,2253],"shape":"cicle","lineIds":[16]},{"stationId":316,"stationName":"华鹏路","position":[5070,2318],"shape":"cicle","lineIds":[16]},{"stationId":317,"stationName":"下南路","position":[5207,2288],"shape":"cicle","lineIds":[16]},{"stationId":318,"stationName":"北蔡","position":[5325,2280],"shape":"cicle","lineIds":[16],"tagDirection":0},{"stationId":319,"stationName":"陈春路","position":[5387,2330],"shape":"cicle","lineIds":[16],"tagDirection":5},{"stationId":320,"stationName":"莲溪路","position":[5469,2390],"shape":"cicle","lineIds":[16,21]},{"stationId":321,"stationName":"华夏中路","position":[5636,2324],"shape":"cicle","lineIds":[16,19]},{"stationId":322,"stationName":"中科路","position":[5829,2293],"shape":"cicle","lineIds":[16]},{"stationId":323,"stationName":"学林路","position":[5948,2246],"shape":"cicle","lineIds":[16]},{"stationId":324,"stationName":"张江路","position":[6096,2190],"shape":"cicle","lineIds":[16]},{"stationId":325,"stationName":"封浜","position":[2775,1406],"shape":"cicle","lineIds":[17]},{"stationId":326,"stationName":"乐秀路","position":[2946,1428],"shape":"cicle","lineIds":[17]},{"stationId":327,"stationName":"临洮路","position":[3113,1451],"shape":"cicle","lineIds":[17]},{"stationId":328,"stationName":"嘉怡路","position":[3262,1489],"shape":"cicle","lineIds":[17]},{"stationId":329,"stationName":"定边路","position":[3429,1517],"shape":"cicle","lineIds":[17]},{"stationId":330,"stationName":"真新新村","position":[3524,1526],"shape":"cicle","lineIds":[17]},{"stationId":331,"stationName":"真光路","position":[3646,1548],"shape":"cicle","lineIds":[17],"tagDirection":5},{"stationId":332,"stationName":"铜川路","position":[3775,1574],"shape":"cicle","lineIds":[17,18],"tagDirection":7},{"stationId":333,"stationName":"中宁路","position":[3950,1631],"shape":"cicle","lineIds":[17]},{"stationId":334,"stationName":"武定路","position":[4166,1808],"shape":"cicle","lineIds":[17]},{"stationId":335,"stationName":"浦东南路","position":[4929,1696],"shape":"cicle","lineIds":[17],"tagDirection":7},{"stationId":336,"stationName":"源深路","position":[5115,1668],"shape":"cicle","lineIds":[17]},{"stationId":337,"stationName":"昌邑路","position":[5206,1642],"shape":"cicle","lineIds":[17,21]},{"stationId":338,"stationName":"歇浦路","position":[5320,1574],"shape":"cicle","lineIds":[17]},{"stationId":339,"stationName":"黄杨路","position":[5715,1741],"shape":"cicle","lineIds":[17]},{"stationId":340,"stationName":"云顺路","position":[5808,1711],"shape":"cicle","lineIds":[17]},{"stationId":341,"stationName":"浦东足球场","position":[5959,1665],"shape":"cicle","lineIds":[17]},{"stationId":342,"stationName":"金粤路","position":[6095,1660],"shape":"cicle","lineIds":[17]},{"stationId":343,"stationName":"桂桥路","position":[6156,1587],"shape":"cicle","lineIds":[17]},{"stationId":344,"stationName":"锦秋路","position":[3622,879],"shape":"cicle","lineIds":[18]},{"stationId":345,"stationName":"丰翔路","position":[3613,999],"shape":"cicle","lineIds":[18]},{"stationId":346,"stationName":"南大路","position":[3608,1089],"shape":"cicle","lineIds":[18]},{"stationId":347,"stationName":"祁安路","position":[3653,1166],"shape":"cicle","lineIds":[18]},{"stationId":348,"stationName":"古浪路","position":[3727,1236],"shape":"cicle","lineIds":[18]},{"stationId":349,"stationName":"武威东路","position":[3726,1325],"shape":"cicle","lineIds":[18],"tagDirection":2},{"stationId":350,"stationName":"梅岭北路","position":[3775,1648],"shape":"cicle","lineIds":[18],"tagDirection":6},{"stationId":351,"stationName":"长风公园","position":[3768,1829],"shape":"cicle","lineIds":[18]},{"stationId":352,"stationName":"红宝石路","position":[3788,2093],"shape":"cicle","lineIds":[18]},{"stationId":353,"stationName":"姚虹路","position":[3869,2166],"shape":"cicle","lineIds":[18],"tagDirection":5},{"stationId":354,"stationName":"吴中路","position":[3944,2238],"shape":"cicle","lineIds":[18],"tagDirection":5},{"stationId":355,"stationName":"华东理工大学","position":[4095,2639],"shape":"cicle","lineIds":[18],"tagDirection":2},{"stationId":356,"stationName":"罗秀路","position":[4163,2766],"shape":"cicle","lineIds":[18]},{"stationId":357,"stationName":"朱梅路","position":[4183,2838],"shape":"cicle","lineIds":[18]},{"stationId":358,"stationName":"华泾西","position":[4206,2947],"shape":"cicle","lineIds":[18]},{"stationId":359,"stationName":"虹梅南路","position":[4135,3043],"shape":"cicle","lineIds":[18]},{"stationId":360,"stationName":"景西路","position":[3992,3084],"shape":"cicle","lineIds":[18]},{"stationId":361,"stationName":"曙建路","position":[3984,3175],"shape":"cicle","lineIds":[18]},{"stationId":362,"stationName":"双柏路","position":[4022,3249],"shape":"cicle","lineIds":[18]},{"stationId":363,"stationName":"元江路","position":[4124,3453],"shape":"cicle","lineIds":[18]},{"stationId":364,"stationName":"永德路","position":[4237,3690],"shape":"cicle","lineIds":[18]},{"stationId":365,"stationName":"紫竹高新区","position":[4317,3849],"shape":"cicle","lineIds":[18]},{"stationId":366,"stationName":"周浦东","position":[5874,2981],"shape":"cicle","lineIds":[19]},{"stationId":367,"stationName":"鹤沙航城","position":[5917,3303],"shape":"cicle","lineIds":[19]},{"stationId":368,"stationName":"航头东","position":[5980,3532],"shape":"cicle","lineIds":[19]},{"stationId":369,"stationName":"新场","position":[6294,3626],"shape":"cicle","lineIds":[19]},{"stationId":370,"stationName":"野生动物园","position":[6797,3578],"shape":"cicle","lineIds":[19]},{"stationId":371,"stationName":"惠南","position":[7421,3543],"shape":"cicle","lineIds":[19]},{"stationId":372,"stationName":"惠南东","position":[7743,3817],"shape":"cicle","lineIds":[19]},{"stationId":373,"stationName":"书院","position":[8310,4489],"shape":"cicle","lineIds":[19]},{"stationId":374,"stationName":"临港大道","position":[8913,4846],"shape":"cicle","lineIds":[19]},{"stationId":375,"stationName":"滴水湖","position":[9100,5009],"shape":"cicle","lineIds":[19]},{"stationId":376,"stationName":"诸光路","position":[2735,2164],"shape":"cicle","lineIds":[20]},{"stationId":377,"stationName":"蟠龙路","position":[2591,2218],"shape":"cicle","lineIds":[20]},{"stationId":378,"stationName":"徐盈路","position":[2344,2300],"shape":"cicle","lineIds":[20]},{"stationId":379,"stationName":"徐泾北城","position":[2221,2326],"shape":"cicle","lineIds":[20]},{"stationId":380,"stationName":"嘉松中路","position":[2044,2440],"shape":"cicle","lineIds":[20]},{"stationId":381,"stationName":"赵巷","position":[1727,2469],"shape":"cicle","lineIds":[20]},{"stationId":382,"stationName":"汇金路","position":[1321,2469],"shape":"cicle","lineIds":[20]},{"stationId":383,"stationName":"青浦新城","position":[1061,2492],"shape":"cicle","lineIds":[20]},{"stationId":384,"stationName":"漕盈路","position":[774,2477],"shape":"cicle","lineIds":[20]},{"stationId":385,"stationName":"淀山湖大道","position":[626,2737],"shape":"cicle","lineIds":[20]},{"stationId":386,"stationName":"朱家角","position":[295,3075],"shape":"cicle","lineIds":[20]},{"stationId":387,"stationName":"东方绿舟","position":[0,3096],"shape":"cicle","lineIds":[20]},{"stationId":388,"stationName":"殷高路","position":[4760,864],"shape":"cicle","lineIds":[21]},{"stationId":389,"stationName":"上海财经大学","position":[4768,1004],"shape":"cicle","lineIds":[21],"tagDirection":2},{"stationId":390,"stationName":"复旦大学","position":[4800,1118],"shape":"cicle","lineIds":[21]},{"stationId":391,"stationName":"抚顺路","position":[4963,1245],"shape":"cicle","lineIds":[21]},{"stationId":392,"stationName":"平凉路","position":[5072,1491],"shape":"cicle","lineIds":[21],"tagDirection":2},{"stationId":393,"stationName":"丹阳路","position":[5116,1543],"shape":"cicle","lineIds":[21],"tagDirection":2},{"stationId":394,"stationName":"迎春路","position":[5315,1871],"shape":"cicle","lineIds":[21],"tagDirection":1},{"stationId":395,"stationName":"芳芯路","position":[5397,2170],"shape":"cicle","lineIds":[21],"tagDirection":2},{"stationId":396,"stationName":"北中路","position":[5418,2254],"shape":"cicle","lineIds":[21],"tagDirection":2},{"stationId":397,"stationName":"康桥","position":[5478,2739],"shape":"cicle","lineIds":[21]},{"stationId":398,"stationName":"周浦","position":[5481,2940],"shape":"cicle","lineIds":[21]},{"stationId":399,"stationName":"繁荣路","position":[5510,3056],"shape":"cicle","lineIds":[21]},{"stationId":400,"stationName":"沈梅路","position":[5611,3170],"shape":"cicle","lineIds":[21]},{"stationId":401,"stationName":"鹤涛路","position":[5664,3363],"shape":"cicle","lineIds":[21]},{"stationId":402,"stationName":"下沙","position":[5695,3537],"shape":"cicle","lineIds":[21]},{"stationId":403,"stationName":"航头","position":[5765,3708],"shape":"cicle","lineIds":[21]},{"stationId":404,"stationName":"三鲁公路","position":[5079,3520],"shape":"cicle","lineIds":[23]},{"stationId":405,"stationName":"闵瑞路","position":[5108,3602],"shape":"cicle","lineIds":[23]},{"stationId":406,"stationName":"浦航路","position":[5111,3671],"shape":"cicle","lineIds":[23]},{"stationId":407,"stationName":"东城一路","position":[5126,3777],"shape":"cicle","lineIds":[23]},{"stationId":408,"stationName":"汇臻路","position":[5050,3829],"shape":"cicle","lineIds":[23]}],"lines":[{"lineId":1,"lineName":"1号线","color":"#E3002A","stationIds":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"sign":1,"order":1,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":2,"lineName":"2号线","color":"#86B81C","stationIds":[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,13,47,48,49,50,51,52,53,54,55,56,57],"sign":2,"order":2,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":3,"lineName":"3号线","color":"#FCD600","stationIds":[58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,16,74,75,76,77,50,78,79,80,81,82,83,5],"sign":3,"order":4,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,true,false,false,false,false,true,true,true,true,true]},{"lineId":4,"lineName":"4号线","color":"#5A2B8D","stationIds":[80,79,78,50,77,76,75,74,16,73,84,85,86,87,88,43,89,90,91,92,93,94,95,96,97,7,80],"sign":4,"order":3,"bendFirst":[true,true,true,true,false,true,true,false,false,true,true,true,true,true,true,true,false,true,true,true,true,true,true,true,true,false,true]},{"lineId":5,"lineName":"5号线","color":"#96499A","stationIds":[115,114,113,112,111,110,109,108,102,103,104,105,106,107,1],"sign":5,"order":5,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":7,"lineName":"6号线","color":"#F0087D","stationIds":[116,117,118,119,120,121,122,123,124,90,125,43,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141],"sign":"6","order":7,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true]},{"lineId":8,"lineName":"7号线","color":"#EE782E","stationIds":[142,40,143,144,145,122,146,147,148,149,150,96,151,10,48,152,153,75,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168],"sign":"7","order":8,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":9,"lineName":"8号线","color":"#01A2E2","stationIds":[169,170,171,172,173,174,116,175,176,147,177,93,178,179,180,13,181,182,183,71,184,185,186,187,188,189,190,191,192,193],"sign":"8","order":9,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":10,"lineName":"9号线","color":"#69C7F4","stationIds":[194,195,196,197,198,199,200,201,202,203,43,204,205,178,206,207,208,151,8,80,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223],"sign":"9","order":10,"bendFirst":[true,true,true,true,true,true,true,true,true,true,false,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":11,"lineName":"10号线","color":"#C6AFD4","stationIds":[224,141,225,226,227,228,229,230,231,232,233,234,235,185,236,84,237,238,46,239,179,240,11,241,242,79,243,244,245,246,250,251,55,56],"sign":"10","order":11,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true]},{"lineId":14,"lineName":"11号线","color":"#871C2A","stationIds":[278,277,276,275,274,273,272,271,116,270,269,268,267,8,242,49,266,76,265,264,263,262,261,260,259,258,257,256,255,279,280,281,282,283,284,285],"sign":"11","order":14,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":15,"lineName":"12号线","color":"#007A61","stationIds":[286,287,288,289,290,291,292,6,82,268,150,95,208,11,47,15,181,238,293,294,86,295,296,297,298,299,300,134,301,302,303,197],"sign":"12","order":15,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":16,"lineName":"13号线","color":"#EB81B9","stationIds":[304,305,306,307,308,309,77,266,310,153,311,15,312,47,313,240,206,314,315,148,176,121,316,317,318,319,320,321,322,323,324],"sign":"13","order":16,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":17,"lineName":"14号线","color":"#818530","stationIds":[325,326,327,328,329,330,331,332,264,333,76,310,334,48,12,180,239,45,335,88,336,337,338,130,201,339,340,341,342,343],"sign":"14","order":17,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":18,"lineName":"15号线","color":"#DAC17D","stationIds":[164,344,345,346,347,348,349,263,332,350,309,351,51,352,353,354,209,292,5,355,356,357,358,359,360,361,362,363,364,365],"sign":"15","order":18,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,true,true,true,true,true,true,true,true,true,true]},{"lineId":19,"lineName":"16号线","color":"#98D1C0","stationIds":[40,321,275,366,367,368,369,370,371,372,373,374,375],"sign":"16","order":19,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":20,"lineName":"17号线","color":"#B85A4E","stationIds":[56,376,377,378,379,380,381,382,383,384,385,386,387],"sign":"17","order":20,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":21,"lineName":"18号线","color":"#D0970A","stationIds":[66,388,389,390,234,391,187,295,392,393,337,127,203,394,40,395,396,320,274,397,398,399,400,401,402,403],"sign":"18","order":21,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":22,"lineName":"磁悬浮","color":"#008187","stationIds":[29,40],"sign":"磁浮线","order":22,"bendFirst":[true,true]},{"lineId":23,"lineName":"浦江线","color":"#B5B5B6","stationIds":[169,404,405,406,407,408],"sign":"浦江线","order":23,"bendFirst":[true,true,true,true,true,true]},{"lineId":24,"lineName":"5号线支线","color":"#96499A","stationIds":[98,99,100,101,102],"sign":"5","order":24,"bendFirst":[true,true,true,true,true],"subLine":true},{"lineId":25,"lineName":"10号线支线","color":"#C6AFD4","stationIds":[249,248,247,246],"sign":"10","order":25,"bendFirst":[true,true,true,true],"subLine":true},{"lineId":26,"lineName":"11号线支线","color":"#871C2A","stationIds":[255,254,253,252],"sign":"11","order":26,"bendFirst":[false,true,true,true],"subLine":true}],"title":"上海"} ================================================ FILE: public/shenzhen.json ================================================ {"stations":[{"stationId":1,"stationName":"罗湖","position":[3489,2743],"shape":"cicle","lineIds":[1],"tagDirection":6},{"stationId":2,"stationName":"国贸","position":[3491,2667],"shape":"cicle","lineIds":[1],"tagDirection":1},{"stationId":3,"stationName":"老街","position":[3472,2621],"shape":"cicle","lineIds":[1,4],"tagDirection":7},{"stationId":4,"stationName":"大剧院","position":[3380,2646],"shape":"cicle","lineIds":[1,3]},{"stationId":5,"stationName":"科学馆","position":[3254,2658],"shape":"cicle","lineIds":[1,7],"tagDirection":5},{"stationId":6,"stationName":"华强路","position":[3154,2659],"shape":"cicle","lineIds":[1]},{"stationId":7,"stationName":"岗厦","position":[2984,2715],"shape":"cicle","lineIds":[1,11],"tagDirection":3},{"stationId":8,"stationName":"会展中心","position":[2913,2716],"shape":"cicle","lineIds":[1,5],"tagDirection":4},{"stationId":9,"stationName":"购物公园","position":[2848,2717],"shape":"cicle","lineIds":[1,4]},{"stationId":10,"stationName":"香蜜湖","position":[2689,2675],"shape":"cicle","lineIds":[1],"tagDirection":4},{"stationId":11,"stationName":"车公庙","position":[2561,2701],"shape":"cicle","lineIds":[1,9,10,12],"tagDirection":3},{"stationId":12,"stationName":"竹子林","position":[2441,2730],"shape":"cicle","lineIds":[1]},{"stationId":13,"stationName":"侨城东","position":[2269,2740],"shape":"cicle","lineIds":[1]},{"stationId":14,"stationName":"华侨城","position":[2156,2729],"shape":"cicle","lineIds":[1],"tagDirection":4},{"stationId":15,"stationName":"世界之窗","position":[2044,2695],"shape":"cicle","lineIds":[1,3],"tagDirection":5},{"stationId":16,"stationName":"白石洲","position":[1973,2668],"shape":"cicle","lineIds":[1],"tagDirection":0},{"stationId":17,"stationName":"高新园","position":[1841,2661],"shape":"cicle","lineIds":[1]},{"stationId":18,"stationName":"深大","position":[1744,2677],"shape":"cicle","lineIds":[1]},{"stationId":19,"stationName":"桃园","position":[1550,2749],"shape":"cicle","lineIds":[1,13]},{"stationId":20,"stationName":"大新","position":[1453,2741],"shape":"cicle","lineIds":[1]},{"stationId":21,"stationName":"鲤鱼门","position":[1333,2744],"shape":"cicle","lineIds":[1],"tagDirection":1},{"stationId":22,"stationName":"前海湾","position":[1282,2693],"shape":"cicle","lineIds":[1,6,12]},{"stationId":23,"stationName":"新安","position":[1248,2589],"shape":"cicle","lineIds":[1]},{"stationId":24,"stationName":"宝安中心","position":[1175,2518],"shape":"cicle","lineIds":[1,6],"tagDirection":2},{"stationId":25,"stationName":"宝体","position":[1109,2456],"shape":"cicle","lineIds":[1],"tagDirection":1},{"stationId":26,"stationName":"坪洲","position":[1009,2375],"shape":"cicle","lineIds":[1],"tagDirection":1},{"stationId":27,"stationName":"西乡","position":[933,2312],"shape":"cicle","lineIds":[1],"tagDirection":1},{"stationId":28,"stationName":"固戍","position":[770,2050],"shape":"cicle","lineIds":[1]},{"stationId":29,"stationName":"后瑞","position":[657,1775],"shape":"cicle","lineIds":[1]},{"stationId":30,"stationName":"机场东","position":[529,1594],"shape":"cicle","lineIds":[1,13]},{"stationId":31,"stationName":"比亚迪北","position":[5913,1242],"shape":"cicle","lineIds":[2],"tagDirection":2},{"stationId":32,"stationName":"龙背","position":[5897,1191],"shape":"cicle","lineIds":[2],"tagDirection":1},{"stationId":33,"stationName":"自然博物馆西","position":[5884,1145],"shape":"cicle","lineIds":[2],"tagDirection":1},{"stationId":34,"stationName":"未来城","position":[5828,1080],"shape":"cicle","lineIds":[2],"tagDirection":2},{"stationId":35,"stationName":"燕子岭","position":[5918,1035],"shape":"cicle","lineIds":[2]},{"stationId":36,"stationName":"综合保税区","position":[5914,971],"shape":"cicle","lineIds":[2]},{"stationId":37,"stationName":"中芯国际","position":[5860,948],"shape":"cicle","lineIds":[2],"tagDirection":0},{"stationId":38,"stationName":"坪山中心","position":[5818,954],"shape":"cicle","lineIds":[2],"tagDirection":3},{"stationId":39,"stationName":"文化聚落","position":[5758,962],"shape":"cicle","lineIds":[2],"tagDirection":0},{"stationId":40,"stationName":"站前路东","position":[5707,979],"shape":"cicle","lineIds":[2],"tagDirection":0},{"stationId":41,"stationName":"坪山高铁站","position":[5577,1009],"shape":"cicle","lineIds":[2]},{"stationId":42,"stationName":"赤湾","position":[1277,3272],"shape":"cicle","lineIds":[3,6]},{"stationId":43,"stationName":"蛇口港","position":[1426,3298],"shape":"cicle","lineIds":[3]},{"stationId":44,"stationName":"海上世界","position":[1453,3213],"shape":"cicle","lineIds":[3,13],"tagDirection":3},{"stationId":45,"stationName":"水湾","position":[1499,3180],"shape":"cicle","lineIds":[3]},{"stationId":46,"stationName":"东角头","position":[1614,3201],"shape":"cicle","lineIds":[3]},{"stationId":47,"stationName":"湾厦","position":[1688,3130],"shape":"cicle","lineIds":[3]},{"stationId":48,"stationName":"海月","position":[1679,3060],"shape":"cicle","lineIds":[3]},{"stationId":49,"stationName":"登良","position":[1678,2972],"shape":"cicle","lineIds":[3]},{"stationId":50,"stationName":"后海","position":[1727,2880],"shape":"cicle","lineIds":[3,12]},{"stationId":51,"stationName":"科苑","position":[1764,2794],"shape":"cicle","lineIds":[3],"tagDirection":6},{"stationId":52,"stationName":"红树湾","position":[1987,2812],"shape":"cicle","lineIds":[3],"tagDirection":7},{"stationId":53,"stationName":"侨城北","position":[2194,2624],"shape":"cicle","lineIds":[3]},{"stationId":54,"stationName":"深康","position":[2291,2599],"shape":"cicle","lineIds":[3]},{"stationId":55,"stationName":"安托山","position":[2365,2576],"shape":"cicle","lineIds":[3,9],"tagDirection":0},{"stationId":56,"stationName":"侨香","position":[2446,2566],"shape":"cicle","lineIds":[3]},{"stationId":57,"stationName":"香蜜","position":[2541,2536],"shape":"cicle","lineIds":[3]},{"stationId":58,"stationName":"香梅北","position":[2629,2525],"shape":"cicle","lineIds":[3],"tagDirection":0},{"stationId":59,"stationName":"景田","position":[2736,2530],"shape":"cicle","lineIds":[3,10]},{"stationId":60,"stationName":"莲花西","position":[2803,2597],"shape":"cicle","lineIds":[3]},{"stationId":61,"stationName":"福田","position":[2831,2663],"shape":"cicle","lineIds":[3,4,12]},{"stationId":62,"stationName":"市民中心","position":[2914,2646],"shape":"cicle","lineIds":[3,5]},{"stationId":63,"stationName":"岗厦北","position":[2982,2681],"shape":"cicle","lineIds":[3,11,12,14],"tagDirection":3},{"stationId":64,"stationName":"华强北","position":[3156,2619],"shape":"cicle","lineIds":[3,9],"tagDirection":7},{"stationId":65,"stationName":"燕南","position":[3228,2618],"shape":"cicle","lineIds":[3],"tagDirection":0},{"stationId":66,"stationName":"湖贝","position":[3557,2621],"shape":"cicle","lineIds":[3],"tagDirection":2},{"stationId":67,"stationName":"黄贝岭","position":[3666,2603],"shape":"cicle","lineIds":[3,6]},{"stationId":68,"stationName":"新秀","position":[3790,2587],"shape":"cicle","lineIds":[3]},{"stationId":69,"stationName":"莲塘口岸","position":[3858,2529],"shape":"cicle","lineIds":[3]},{"stationId":70,"stationName":"仙湖路","position":[3966,2423],"shape":"cicle","lineIds":[3]},{"stationId":71,"stationName":"莲塘","position":[4045,2423],"shape":"cicle","lineIds":[3]},{"stationId":72,"stationName":"梧桐山南","position":[4134,2485],"shape":"cicle","lineIds":[3]},{"stationId":73,"stationName":"沙头角","position":[4534,2521],"shape":"cicle","lineIds":[3]},{"stationId":74,"stationName":"海山","position":[4679,2508],"shape":"cicle","lineIds":[3]},{"stationId":75,"stationName":"盐田港西","position":[4771,2438],"shape":"cicle","lineIds":[3]},{"stationId":76,"stationName":"深外高中","position":[4748,2223],"shape":"cicle","lineIds":[3]},{"stationId":77,"stationName":"盐田路","position":[4865,2167],"shape":"cicle","lineIds":[3]},{"stationId":78,"stationName":"鸿安围","position":[4944,2158],"shape":"cicle","lineIds":[3]},{"stationId":79,"stationName":"盐田墟","position":[5066,2191],"shape":"cicle","lineIds":[3]},{"stationId":80,"stationName":"大梅沙","position":[5380,2123],"shape":"cicle","lineIds":[3]},{"stationId":81,"stationName":"小梅沙","position":[5567,2042],"shape":"cicle","lineIds":[3]},{"stationId":82,"stationName":"福保","position":[2858,2985],"shape":"cicle","lineIds":[4]},{"stationId":83,"stationName":"益田","position":[2812,2901],"shape":"cicle","lineIds":[4],"tagDirection":3},{"stationId":84,"stationName":"石厦","position":[2838,2827],"shape":"cicle","lineIds":[4,9],"tagDirection":7},{"stationId":85,"stationName":"少年宫","position":[2904,2578],"shape":"cicle","lineIds":[4,5],"tagDirection":1},{"stationId":86,"stationName":"莲花村","position":[2981,2577],"shape":"cicle","lineIds":[4,11],"tagDirection":1},{"stationId":87,"stationName":"华新","position":[3169,2574],"shape":"cicle","lineIds":[4,9],"tagDirection":1},{"stationId":88,"stationName":"通新岭","position":[3265,2573],"shape":"cicle","lineIds":[4,7],"tagDirection":3},{"stationId":89,"stationName":"红岭","position":[3348,2580],"shape":"cicle","lineIds":[4,10]},{"stationId":90,"stationName":"晒布","position":[3529,2571],"shape":"cicle","lineIds":[4],"tagDirection":7},{"stationId":91,"stationName":"翠竹","position":[3601,2502],"shape":"cicle","lineIds":[4]},{"stationId":92,"stationName":"田贝","position":[3603,2391],"shape":"cicle","lineIds":[4,9]},{"stationId":93,"stationName":"水贝","position":[3552,2324],"shape":"cicle","lineIds":[4]},{"stationId":94,"stationName":"草埔","position":[3467,2219],"shape":"cicle","lineIds":[4]},{"stationId":95,"stationName":"布吉","position":[3514,2040],"shape":"cicle","lineIds":[4,6,14]},{"stationId":96,"stationName":"木棉湾","position":[3605,1994],"shape":"cicle","lineIds":[4]},{"stationId":97,"stationName":"大芬","position":[3686,1922],"shape":"cicle","lineIds":[4]},{"stationId":98,"stationName":"丹竹头","position":[3783,1866],"shape":"cicle","lineIds":[4]},{"stationId":99,"stationName":"六约","position":[4109,1721],"shape":"cicle","lineIds":[4]},{"stationId":100,"stationName":"塘坑","position":[4229,1672],"shape":"cicle","lineIds":[4]},{"stationId":101,"stationName":"横岗","position":[4392,1579],"shape":"cicle","lineIds":[4]},{"stationId":102,"stationName":"永湖","position":[4483,1474],"shape":"cicle","lineIds":[4]},{"stationId":103,"stationName":"荷坳","position":[4545,1333],"shape":"cicle","lineIds":[4]},{"stationId":104,"stationName":"大运","position":[4584,1207],"shape":"cicle","lineIds":[4,14,15]},{"stationId":105,"stationName":"爱联","position":[4646,1089],"shape":"cicle","lineIds":[4]},{"stationId":106,"stationName":"吉祥","position":[4745,969],"shape":"cicle","lineIds":[4]},{"stationId":107,"stationName":"龙城广场","position":[4848,895],"shape":"cicle","lineIds":[4]},{"stationId":108,"stationName":"南联","position":[4958,844],"shape":"cicle","lineIds":[4]},{"stationId":109,"stationName":"双龙","position":[5076,776],"shape":"cicle","lineIds":[4,15]},{"stationId":110,"stationName":"福田口岸","position":[2975,2895],"shape":"cicle","lineIds":[5,11]},{"stationId":111,"stationName":"福民","position":[2961,2832],"shape":"cicle","lineIds":[5,9,11]},{"stationId":112,"stationName":"莲花北","position":[2896,2458],"shape":"cicle","lineIds":[5]},{"stationId":113,"stationName":"上梅林","position":[2897,2359],"shape":"cicle","lineIds":[5,10]},{"stationId":114,"stationName":"民乐","position":[2790,2122],"shape":"cicle","lineIds":[5]},{"stationId":115,"stationName":"白石龙","position":[2727,2049],"shape":"cicle","lineIds":[5]},{"stationId":116,"stationName":"深圳北站","position":[2605,1959],"shape":"cicle","lineIds":[5,6,7]},{"stationId":117,"stationName":"红山","position":[2537,1845],"shape":"cicle","lineIds":[5,7]},{"stationId":118,"stationName":"上塘","position":[2430,1692],"shape":"cicle","lineIds":[5]},{"stationId":119,"stationName":"龙胜","position":[2422,1616],"shape":"cicle","lineIds":[5]},{"stationId":120,"stationName":"龙华","position":[2526,1547],"shape":"cicle","lineIds":[5]},{"stationId":121,"stationName":"清湖","position":[2668,1423],"shape":"cicle","lineIds":[5]},{"stationId":122,"stationName":"清湖北","position":[2706,1343],"shape":"cicle","lineIds":[5]},{"stationId":123,"stationName":"竹村","position":[2705,1197],"shape":"cicle","lineIds":[5]},{"stationId":124,"stationName":"茜坑","position":[2713,1091],"shape":"cicle","lineIds":[5]},{"stationId":125,"stationName":"长湖","position":[2750,956],"shape":"cicle","lineIds":[5]},{"stationId":126,"stationName":"观澜","position":[2850,912],"shape":"cicle","lineIds":[5]},{"stationId":127,"stationName":"松元厦","position":[2951,858],"shape":"cicle","lineIds":[5]},{"stationId":128,"stationName":"观澜湖","position":[3065,848],"shape":"cicle","lineIds":[5]},{"stationId":129,"stationName":"牛湖","position":[3195,818],"shape":"cicle","lineIds":[5]},{"stationId":130,"stationName":"荔湾","position":[1237,3086],"shape":"cicle","lineIds":[6]},{"stationId":131,"stationName":"铁路公园","position":[1202,3035],"shape":"cicle","lineIds":[6]},{"stationId":132,"stationName":"妈湾","position":[1162,2985],"shape":"cicle","lineIds":[6]},{"stationId":133,"stationName":"前湾公园","position":[1198,2903],"shape":"cicle","lineIds":[6]},{"stationId":134,"stationName":"前湾","position":[1260,2840],"shape":"cicle","lineIds":[6]},{"stationId":135,"stationName":"桂湾","position":[1282,2740],"shape":"cicle","lineIds":[6]},{"stationId":136,"stationName":"临海","position":[1195,2622],"shape":"cicle","lineIds":[6]},{"stationId":137,"stationName":"宝华","position":[1153,2567],"shape":"cicle","lineIds":[6]},{"stationId":138,"stationName":"翻身","position":[1235,2467],"shape":"cicle","lineIds":[6]},{"stationId":139,"stationName":"灵芝","position":[1344,2374],"shape":"cicle","lineIds":[6,13]},{"stationId":140,"stationName":"洪浪北","position":[1408,2318],"shape":"cicle","lineIds":[6]},{"stationId":141,"stationName":"兴东","position":[1492,2246],"shape":"cicle","lineIds":[6]},{"stationId":142,"stationName":"留仙洞","position":[1738,2258],"shape":"cicle","lineIds":[6]},{"stationId":143,"stationName":"西丽","position":[1846,2257],"shape":"cicle","lineIds":[6,9]},{"stationId":144,"stationName":"大学城","position":[1955,2244],"shape":"cicle","lineIds":[6]},{"stationId":145,"stationName":"塘朗","position":[2299,2163],"shape":"cicle","lineIds":[6]},{"stationId":146,"stationName":"长岭陂","position":[2405,2070],"shape":"cicle","lineIds":[6]},{"stationId":147,"stationName":"民治","position":[2711,1888],"shape":"cicle","lineIds":[6]},{"stationId":148,"stationName":"五和","position":[2909,1795],"shape":"cicle","lineIds":[6,11]},{"stationId":149,"stationName":"坂田","position":[3011,1789],"shape":"cicle","lineIds":[6]},{"stationId":150,"stationName":"杨美","position":[3102,1796],"shape":"cicle","lineIds":[6]},{"stationId":151,"stationName":"上水径","position":[3356,1843],"shape":"cicle","lineIds":[6]},{"stationId":152,"stationName":"下水径","position":[3409,1915],"shape":"cicle","lineIds":[6]},{"stationId":153,"stationName":"长龙","position":[3440,1983],"shape":"cicle","lineIds":[6]},{"stationId":154,"stationName":"百鸽笼","position":[3606,2109],"shape":"cicle","lineIds":[6]},{"stationId":155,"stationName":"布心","position":[3683,2254],"shape":"cicle","lineIds":[6]},{"stationId":156,"stationName":"太安","position":[3673,2329],"shape":"cicle","lineIds":[6,9]},{"stationId":157,"stationName":"怡景","position":[3703,2505],"shape":"cicle","lineIds":[6]},{"stationId":158,"stationName":"松岗","position":[599,341],"shape":"cicle","lineIds":[7,12]},{"stationId":159,"stationName":"溪头","position":[679,317],"shape":"cicle","lineIds":[7]},{"stationId":160,"stationName":"松岗公园","position":[785,285],"shape":"cicle","lineIds":[7]},{"stationId":161,"stationName":"薯田埔","position":[979,254],"shape":"cicle","lineIds":[7]},{"stationId":162,"stationName":"合水口","position":[1112,248],"shape":"cicle","lineIds":[7]},{"stationId":163,"stationName":"公明广场","position":[1216,289],"shape":"cicle","lineIds":[7]},{"stationId":164,"stationName":"红花山","position":[1375,287],"shape":"cicle","lineIds":[7]},{"stationId":165,"stationName":"楼村","position":[1536,305],"shape":"cicle","lineIds":[7]},{"stationId":166,"stationName":"科学公园","position":[1643,289],"shape":"cicle","lineIds":[7]},{"stationId":167,"stationName":"光明","position":[1755,347],"shape":"cicle","lineIds":[7,8]},{"stationId":168,"stationName":"光明大街","position":[1729,444],"shape":"cicle","lineIds":[7]},{"stationId":169,"stationName":"凤凰城","position":[1631,589],"shape":"cicle","lineIds":[7]},{"stationId":170,"stationName":"长圳","position":[1611,800],"shape":"cicle","lineIds":[7]},{"stationId":171,"stationName":"上屋","position":[1688,1173],"shape":"cicle","lineIds":[7]},{"stationId":172,"stationName":"官田","position":[1799,1231],"shape":"cicle","lineIds":[7]},{"stationId":173,"stationName":"阳台山东","position":[2220,1383],"shape":"cicle","lineIds":[7]},{"stationId":174,"stationName":"元芬","position":[2354,1501],"shape":"cicle","lineIds":[7]},{"stationId":175,"stationName":"上芬","position":[2469,1660],"shape":"cicle","lineIds":[7]},{"stationId":176,"stationName":"梅林关","position":[2834,2119],"shape":"cicle","lineIds":[7]},{"stationId":177,"stationName":"翰岭","position":[2991,2322],"shape":"cicle","lineIds":[7],"tagDirection":1},{"stationId":178,"stationName":"银湖","position":[3201,2381],"shape":"cicle","lineIds":[7,10]},{"stationId":179,"stationName":"八卦岭","position":[3249,2444],"shape":"cicle","lineIds":[7,9],"tagDirection":6},{"stationId":180,"stationName":"体育中心","position":[3263,2517],"shape":"cicle","lineIds":[7],"tagDirection":3},{"stationId":181,"stationName":"圳美","position":[1795,186],"shape":"cicle","lineIds":[8]},{"stationId":182,"stationName":"中大","position":[1838,96],"shape":"cicle","lineIds":[8]},{"stationId":183,"stationName":"深理工","position":[1988,0],"shape":"cicle","lineIds":[8]},{"stationId":184,"stationName":"西丽湖","position":[1959,2128],"shape":"cicle","lineIds":[9]},{"stationId":185,"stationName":"茶光","position":[1845,2322],"shape":"cicle","lineIds":[9]},{"stationId":186,"stationName":"珠光","position":[1908,2381],"shape":"cicle","lineIds":[9]},{"stationId":187,"stationName":"龙井","position":[2049,2424],"shape":"cicle","lineIds":[9]},{"stationId":188,"stationName":"桃源村","position":[2122,2465],"shape":"cicle","lineIds":[9]},{"stationId":189,"stationName":"深云","position":[2240,2498],"shape":"cicle","lineIds":[9]},{"stationId":190,"stationName":"农林","position":[2484,2660],"shape":"cicle","lineIds":[9]},{"stationId":191,"stationName":"上沙","position":[2657,2818],"shape":"cicle","lineIds":[9]},{"stationId":192,"stationName":"沙尾","position":[2728,2856],"shape":"cicle","lineIds":[9]},{"stationId":193,"stationName":"皇岗村","position":[2900,2832],"shape":"cicle","lineIds":[9],"tagDirection":4},{"stationId":194,"stationName":"皇岗口岸","position":[3042,2846],"shape":"cicle","lineIds":[9]},{"stationId":195,"stationName":"赤尾","position":[3138,2750],"shape":"cicle","lineIds":[9],"tagDirection":3},{"stationId":196,"stationName":"华强南","position":[3170,2692],"shape":"cicle","lineIds":[9],"tagDirection":3},{"stationId":197,"stationName":"黄木岗","position":[3175,2507],"shape":"cicle","lineIds":[9,14]},{"stationId":198,"stationName":"红岭北","position":[3345,2443],"shape":"cicle","lineIds":[9,10],"tagDirection":0},{"stationId":199,"stationName":"笋岗","position":[3415,2430],"shape":"cicle","lineIds":[9]},{"stationId":200,"stationName":"洪湖","position":[3527,2403],"shape":"cicle","lineIds":[9]},{"stationId":201,"stationName":"前湾","position":[1260,2840],"shape":"cicle","lineIds":[10],"tagDirection":2},{"stationId":202,"stationName":"梦海","position":[1290,2875],"shape":"cicle","lineIds":[10]},{"stationId":203,"stationName":"怡海","position":[1340,2914],"shape":"cicle","lineIds":[10]},{"stationId":204,"stationName":"荔林","position":[1434,2945],"shape":"cicle","lineIds":[10]},{"stationId":205,"stationName":"南油西","position":[1486,2973],"shape":"cicle","lineIds":[10]},{"stationId":206,"stationName":"南油","position":[1560,2961],"shape":"cicle","lineIds":[10,13]},{"stationId":207,"stationName":"南山书城","position":[1589,2870],"shape":"cicle","lineIds":[10],"tagDirection":3},{"stationId":208,"stationName":"深大南","position":[1659,2804],"shape":"cicle","lineIds":[10]},{"stationId":209,"stationName":"粤海门","position":[1758,2737],"shape":"cicle","lineIds":[10],"tagDirection":4},{"stationId":210,"stationName":"高新南","position":[1803,2736],"shape":"cicle","lineIds":[10]},{"stationId":211,"stationName":"红树湾南","position":[2017,2834],"shape":"cicle","lineIds":[10,12],"tagDirection":4},{"stationId":212,"stationName":"深湾","position":[2088,2833],"shape":"cicle","lineIds":[10]},{"stationId":213,"stationName":"深圳湾公园","position":[2233,2848],"shape":"cicle","lineIds":[10]},{"stationId":214,"stationName":"下沙","position":[2546,2775],"shape":"cicle","lineIds":[10]},{"stationId":215,"stationName":"香梅","position":[2699,2610],"shape":"cicle","lineIds":[10]},{"stationId":216,"stationName":"梅景","position":[2682,2453],"shape":"cicle","lineIds":[10]},{"stationId":217,"stationName":"下梅林","position":[2720,2407],"shape":"cicle","lineIds":[10]},{"stationId":218,"stationName":"梅村","position":[2826,2379],"shape":"cicle","lineIds":[10]},{"stationId":219,"stationName":"孖岭","position":[2980,2379],"shape":"cicle","lineIds":[10,11]},{"stationId":220,"stationName":"泥岗","position":[3279,2386],"shape":"cicle","lineIds":[10]},{"stationId":221,"stationName":"园岭","position":[3347,2501],"shape":"cicle","lineIds":[10],"tagDirection":2},{"stationId":222,"stationName":"红岭南","position":[3350,2664],"shape":"cicle","lineIds":[10]},{"stationId":223,"stationName":"鹿丹村","position":[3388,2707],"shape":"cicle","lineIds":[10]},{"stationId":224,"stationName":"人民南","position":[3484,2709],"shape":"cicle","lineIds":[10],"tagDirection":2},{"stationId":225,"stationName":"向西村","position":[3560,2666],"shape":"cicle","lineIds":[10],"tagDirection":3},{"stationId":226,"stationName":"文锦","position":[3612,2641],"shape":"cicle","lineIds":[10],"tagDirection":3},{"stationId":227,"stationName":"双拥街","position":[3632,1077],"shape":"cicle","lineIds":[11]},{"stationId":228,"stationName":"平湖","position":[3560,1127],"shape":"cicle","lineIds":[11]},{"stationId":229,"stationName":"禾花","position":[3513,1203],"shape":"cicle","lineIds":[11]},{"stationId":230,"stationName":"华南城","position":[3525,1304],"shape":"cicle","lineIds":[11]},{"stationId":231,"stationName":"木古","position":[3527,1370],"shape":"cicle","lineIds":[11]},{"stationId":232,"stationName":"上李朗","position":[3573,1523],"shape":"cicle","lineIds":[11]},{"stationId":233,"stationName":"凉帽山","position":[3492,1601],"shape":"cicle","lineIds":[11]},{"stationId":234,"stationName":"甘坑","position":[3360,1585],"shape":"cicle","lineIds":[11]},{"stationId":235,"stationName":"雪象","position":[3092,1495],"shape":"cicle","lineIds":[11]},{"stationId":236,"stationName":"岗头","position":[3021,1493],"shape":"cicle","lineIds":[11]},{"stationId":237,"stationName":"华为","position":[2974,1559],"shape":"cicle","lineIds":[11]},{"stationId":238,"stationName":"贝尔路","position":[2966,1647],"shape":"cicle","lineIds":[11]},{"stationId":239,"stationName":"坂田北","position":[2936,1715],"shape":"cicle","lineIds":[11]},{"stationId":240,"stationName":"光雅园","position":[2911,1900],"shape":"cicle","lineIds":[11]},{"stationId":241,"stationName":"南坑","position":[2908,1955],"shape":"cicle","lineIds":[11]},{"stationId":242,"stationName":"雅宝","position":[2905,2018],"shape":"cicle","lineIds":[11]},{"stationId":243,"stationName":"冬瓜岭","position":[2978,2459],"shape":"cicle","lineIds":[11]},{"stationId":244,"stationName":"碧头","position":[502,219],"shape":"cicle","lineIds":[12]},{"stationId":245,"stationName":"后亭","position":[571,538],"shape":"cicle","lineIds":[12]},{"stationId":246,"stationName":"沙井","position":[547,755],"shape":"cicle","lineIds":[12]},{"stationId":247,"stationName":"马安山","position":[472,894],"shape":"cicle","lineIds":[12]},{"stationId":248,"stationName":"塘尾","position":[481,1043],"shape":"cicle","lineIds":[12]},{"stationId":249,"stationName":"桥头","position":[412,1182],"shape":"cicle","lineIds":[12]},{"stationId":250,"stationName":"福永","position":[363,1328],"shape":"cicle","lineIds":[12,13]},{"stationId":251,"stationName":"机场北","position":[284,1552],"shape":"cicle","lineIds":[12,16]},{"stationId":252,"stationName":"机场","position":[439,1824],"shape":"cicle","lineIds":[12]},{"stationId":253,"stationName":"碧海湾","position":[863,2313],"shape":"cicle","lineIds":[12]},{"stationId":254,"stationName":"宝安","position":[1106,2518],"shape":"cicle","lineIds":[12],"tagDirection":5},{"stationId":255,"stationName":"南山","position":[1537,2823],"shape":"cicle","lineIds":[12,13]},{"stationId":256,"stationName":"海上田园东","position":[115,752],"shape":"cicle","lineIds":[13]},{"stationId":257,"stationName":"海上田园南","position":[46,881],"shape":"cicle","lineIds":[13],"tagDirection":2},{"stationId":258,"stationName":"国展北","position":[71,1032],"shape":"cicle","lineIds":[13,16]},{"stationId":259,"stationName":"国展","position":[82,1104],"shape":"cicle","lineIds":[13,16]},{"stationId":260,"stationName":"福海西","position":[197,1180],"shape":"cicle","lineIds":[13]},{"stationId":261,"stationName":"桥头西","position":[277,1233],"shape":"cicle","lineIds":[13]},{"stationId":262,"stationName":"怀德","position":[491,1397],"shape":"cicle","lineIds":[13]},{"stationId":263,"stationName":"福围","position":[514,1495],"shape":"cicle","lineIds":[13]},{"stationId":264,"stationName":"兴围","position":[647,1635],"shape":"cicle","lineIds":[13]},{"stationId":265,"stationName":"黄田","position":[759,1762],"shape":"cicle","lineIds":[13],"tagDirection":1},{"stationId":266,"stationName":"钟屋南","position":[894,1878],"shape":"cicle","lineIds":[13]},{"stationId":267,"stationName":"西乡桃源","position":[926,1954],"shape":"cicle","lineIds":[13]},{"stationId":268,"stationName":"平峦山","position":[1000,2032],"shape":"cicle","lineIds":[13]},{"stationId":269,"stationName":"宝田一路","position":[1087,2119],"shape":"cicle","lineIds":[13]},{"stationId":270,"stationName":"宝安客运站","position":[1139,2170],"shape":"cicle","lineIds":[13]},{"stationId":271,"stationName":"流塘","position":[1195,2226],"shape":"cicle","lineIds":[13]},{"stationId":272,"stationName":"上川","position":[1254,2285],"shape":"cicle","lineIds":[13]},{"stationId":273,"stationName":"新安公园","position":[1411,2441],"shape":"cicle","lineIds":[13]},{"stationId":274,"stationName":"同乐南","position":[1490,2500],"shape":"cicle","lineIds":[13]},{"stationId":275,"stationName":"中山公园","position":[1534,2594],"shape":"cicle","lineIds":[13]},{"stationId":276,"stationName":"南头古城","position":[1544,2679],"shape":"cicle","lineIds":[13]},{"stationId":277,"stationName":"南光","position":[1545,2897],"shape":"cicle","lineIds":[13]},{"stationId":278,"stationName":"四海","position":[1522,3060],"shape":"cicle","lineIds":[13]},{"stationId":279,"stationName":"花果山","position":[1493,3120],"shape":"cicle","lineIds":[13],"tagDirection":6},{"stationId":280,"stationName":"太子湾","position":[1413,3336],"shape":"cicle","lineIds":[13]},{"stationId":281,"stationName":"左炮台东","position":[1281,3369],"shape":"cicle","lineIds":[13]},{"stationId":282,"stationName":"罗湖北","position":[3432,2256],"shape":"cicle","lineIds":[14]},{"stationId":283,"stationName":"石芽岭","position":[3682,1779],"shape":"cicle","lineIds":[14]},{"stationId":284,"stationName":"六约北","position":[4060,1638],"shape":"cicle","lineIds":[14]},{"stationId":285,"stationName":"四联","position":[4231,1580],"shape":"cicle","lineIds":[14]},{"stationId":286,"stationName":"坳背","position":[4437,1377],"shape":"cicle","lineIds":[14]},{"stationId":287,"stationName":"嶂背","position":[4798,1141],"shape":"cicle","lineIds":[14]},{"stationId":288,"stationName":"南约","position":[5083,1183],"shape":"cicle","lineIds":[14]},{"stationId":289,"stationName":"宝龙","position":[5280,1181],"shape":"cicle","lineIds":[14]},{"stationId":290,"stationName":"锦龙","position":[5641,1262],"shape":"cicle","lineIds":[14]},{"stationId":291,"stationName":"坪山围","position":[5739,1150],"shape":"cicle","lineIds":[14,15]},{"stationId":292,"stationName":"坪山广场","position":[5790,1058],"shape":"cicle","lineIds":[14]},{"stationId":293,"stationName":"坪山中心","position":[5818,995],"shape":"cicle","lineIds":[14],"tagDirection":5},{"stationId":294,"stationName":"坑梓","position":[6012,610],"shape":"cicle","lineIds":[14]},{"stationId":295,"stationName":"沙田","position":[6308,502],"shape":"cicle","lineIds":[14]},{"stationId":296,"stationName":"大运中心","position":[4510,1111],"shape":"cicle","lineIds":[15]},{"stationId":297,"stationName":"龙城公园","position":[4486,1019],"shape":"cicle","lineIds":[15]},{"stationId":298,"stationName":"黄阁坑","position":[4484,873],"shape":"cicle","lineIds":[15]},{"stationId":299,"stationName":"愉园","position":[4536,781],"shape":"cicle","lineIds":[15]},{"stationId":300,"stationName":"回龙埔","position":[4661,772],"shape":"cicle","lineIds":[15]},{"stationId":301,"stationName":"尚景","position":[4738,770],"shape":"cicle","lineIds":[15]},{"stationId":302,"stationName":"盛平","position":[4829,748],"shape":"cicle","lineIds":[15]},{"stationId":303,"stationName":"龙园","position":[4966,754],"shape":"cicle","lineIds":[15]},{"stationId":304,"stationName":"新塘围","position":[5167,809],"shape":"cicle","lineIds":[15]},{"stationId":305,"stationName":"龙东","position":[5266,849],"shape":"cicle","lineIds":[15]},{"stationId":306,"stationName":"宝龙同乐","position":[5425,972],"shape":"cicle","lineIds":[15]},{"stationId":307,"stationName":"坪山","position":[5577,1007],"shape":"cicle","lineIds":[15],"tagDirection":4},{"stationId":308,"stationName":"新和","position":[5657,1032],"shape":"cicle","lineIds":[15]},{"stationId":309,"stationName":"六和","position":[5680,1104],"shape":"cicle","lineIds":[15]},{"stationId":310,"stationName":"坪环","position":[5769,1210],"shape":"cicle","lineIds":[15],"tagDirection":5},{"stationId":311,"stationName":"东纵纪念馆","position":[5854,1204],"shape":"cicle","lineIds":[15],"tagDirection":4},{"stationId":312,"stationName":"沙壆","position":[5952,1199],"shape":"cicle","lineIds":[15],"tagDirection":2},{"stationId":313,"stationName":"燕子湖","position":[6056,1179],"shape":"cicle","lineIds":[15]},{"stationId":314,"stationName":"石井","position":[6133,1145],"shape":"cicle","lineIds":[15]},{"stationId":315,"stationName":"技术大学","position":[6314,1080],"shape":"cicle","lineIds":[15]},{"stationId":316,"stationName":"田心","position":[6444,1126],"shape":"cicle","lineIds":[15]},{"stationId":317,"stationName":"国展南","position":[86,1226],"shape":"cicle","lineIds":[16]},{"stationId":318,"stationName":"会展城","position":[0,942],"shape":"cicle","lineIds":[16]}],"lines":[{"lineId":1,"lineName":"1号线/罗宝线","color":"#00AB4F","stationIds":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"sign":"1","order":1,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":2,"lineName":"坪山云巴1号线","color":"#2249A3","stationIds":[31,32,33,34,35,36,37,38,39,40,41],"sign":"坪山云巴1","order":2,"bendFirst":[true,true,true,true,true,true,true,true,true,false,true]},{"lineId":3,"lineName":"2号线/8号线","color":"#B35A1F","stationIds":[42,43,44,45,46,47,48,49,50,51,52,15,53,54,55,56,57,58,59,60,61,62,63,64,65,4,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81],"sign":"2","order":3,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":4,"lineName":"3号线/龙岗线","color":"#01A2E2","stationIds":[82,83,84,9,61,85,86,87,88,89,3,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109],"sign":"3","order":4,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":5,"lineName":"4号线/龙华线","color":"#CC0000","stationIds":[110,111,8,62,85,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129],"sign":"4","order":5,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":6,"lineName":"5号线/环中线","color":"#96499A","stationIds":[42,130,131,132,133,134,135,22,136,137,24,138,139,140,141,142,143,144,145,146,116,147,148,149,150,151,152,153,95,154,155,156,157,67],"sign":"5","order":6,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":7,"lineName":"6号线/光明线","color":"#008187","stationIds":[158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,117,116,176,177,178,179,180,88,5],"sign":"6","order":7,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":8,"lineName":"6号线支线","color":"#00AD8E","stationIds":[167,181,182,183],"sign":"6","order":8,"bendFirst":[true,true,true,true]},{"lineId":9,"lineName":"7号线/西丽线","color":"#2249A3","stationIds":[184,143,185,186,187,188,189,55,190,11,191,192,84,193,111,194,195,196,64,87,197,179,198,199,200,92,156],"sign":"7","order":9,"bendFirst":[true,true,true,true,true,true,true,true,true,false,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":10,"lineName":"9号线/梅林线","color":"#86656D","stationIds":[201,202,203,204,205,206,207,208,209,210,211,212,213,214,11,215,59,216,217,218,113,219,178,220,198,221,89,222,223,224,225,226],"sign":"9","order":10,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":11,"lineName":"10号线/坂田线","color":"#EA6183","stationIds":[227,228,229,230,231,232,233,234,235,236,237,238,239,148,240,241,242,219,243,86,63,7,111,110],"sign":"10","order":11,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":12,"lineName":"11号线/机场线","color":"#741D51","stationIds":[244,158,245,246,247,248,249,250,251,252,253,254,22,255,50,211,11,61,63],"sign":"11","order":12,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true]},{"lineId":13,"lineName":"12号线/南宝线","color":"#A49ABD","stationIds":[256,257,258,259,260,261,250,262,263,30,264,265,266,267,268,269,270,271,272,139,273,274,275,276,19,255,277,206,278,279,44,280,281],"sign":"12","order":13,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":14,"lineName":"14号线/东部快线","color":"#DAC17D","stationIds":[63,197,282,95,283,284,285,286,104,287,288,289,290,291,292,293,294,295],"sign":"14","order":14,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":15,"lineName":"16号线/龙坪线","color":"#1C24A4","stationIds":[104,296,297,298,299,300,301,302,303,109,304,305,306,307,308,309,291,310,311,312,313,314,315,316],"sign":"16","order":15,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":16,"lineName":"20号线","color":"#019AC3","stationIds":[251,317,259,258,318],"sign":"20","order":16,"bendFirst":[true,true,true,true,true]}],"title":"深圳"} ================================================ FILE: public/tianjing.json ================================================ {"stations":[{"stationId":1,"stationName":"刘园","position":[538,599],"shape":"cicle","lineIds":[1]},{"stationId":2,"stationName":"瑞景新苑","position":[598,677],"shape":"cicle","lineIds":[1]},{"stationId":3,"stationName":"佳园里","position":[661,759],"shape":"cicle","lineIds":[1]},{"stationId":4,"stationName":"本溪路","position":[774,824],"shape":"cicle","lineIds":[1]},{"stationId":5,"stationName":"勤俭道","position":[916,895],"shape":"cicle","lineIds":[1]},{"stationId":6,"stationName":"洪湖里","position":[998,970],"shape":"cicle","lineIds":[1]},{"stationId":7,"stationName":"西站","position":[1047,1109],"shape":"cicle","lineIds":[1,6],"tagDirection":4},{"stationId":8,"stationName":"西北角","position":[1096,1213],"shape":"cicle","lineIds":[1]},{"stationId":9,"stationName":"西南角","position":[1099,1305],"shape":"cicle","lineIds":[1,2]},{"stationId":10,"stationName":"二纬路","position":[1126,1376],"shape":"cicle","lineIds":[1]},{"stationId":11,"stationName":"海光寺","position":[1154,1455],"shape":"cicle","lineIds":[1]},{"stationId":12,"stationName":"鞍山道","position":[1252,1475],"shape":"cicle","lineIds":[1]},{"stationId":13,"stationName":"营口道","position":[1356,1492],"shape":"cicle","lineIds":[1,3]},{"stationId":14,"stationName":"小白楼","position":[1533,1530],"shape":"cicle","lineIds":[1],"tagDirection":5},{"stationId":15,"stationName":"下瓦房","position":[1608,1627],"shape":"cicle","lineIds":[1,5]},{"stationId":16,"stationName":"南楼","position":[1618,1719],"shape":"cicle","lineIds":[1],"tagDirection":2},{"stationId":17,"stationName":"土城","position":[1702,1787],"shape":"cicle","lineIds":[1]},{"stationId":18,"stationName":"陈塘庄","position":[1864,1844],"shape":"cicle","lineIds":[1]},{"stationId":19,"stationName":"复兴门","position":[1981,1907],"shape":"cicle","lineIds":[1]},{"stationId":20,"stationName":"华山里","position":[1998,2017],"shape":"cicle","lineIds":[1],"tagDirection":2},{"stationId":21,"stationName":"财经大学","position":[2148,2039],"shape":"cicle","lineIds":[1,9]},{"stationId":22,"stationName":"双林","position":[2300,2105],"shape":"cicle","lineIds":[1]},{"stationId":23,"stationName":"李楼","position":[2551,2134],"shape":"cicle","lineIds":[1]},{"stationId":24,"stationName":"洪泥河东","position":[2828,2203],"shape":"cicle","lineIds":[1]},{"stationId":25,"stationName":"高庄子","position":[2987,2248],"shape":"cicle","lineIds":[1]},{"stationId":26,"stationName":"国家会展中心","position":[3219,2389],"shape":"cicle","lineIds":[1]},{"stationId":27,"stationName":"国瑞路","position":[3286,2459],"shape":"cicle","lineIds":[1]},{"stationId":28,"stationName":"东沽路","position":[3463,2555],"shape":"cicle","lineIds":[1]},{"stationId":29,"stationName":"滨海国际机场","position":[3005,1358],"shape":"cicle","lineIds":[2]},{"stationId":30,"stationName":"空港经济区","position":[2729,1102],"shape":"cicle","lineIds":[2]},{"stationId":31,"stationName":"国山路","position":[2479,1138],"shape":"cicle","lineIds":[2]},{"stationId":32,"stationName":"登州路","position":[2339,1174],"shape":"cicle","lineIds":[2]},{"stationId":33,"stationName":"屿东城","position":[2177,1207],"shape":"cicle","lineIds":[2,9]},{"stationId":34,"stationName":"翠阜新村","position":[1959,1239],"shape":"cicle","lineIds":[2],"tagDirection":3},{"stationId":35,"stationName":"靖江路","position":[1821,1254],"shape":"cicle","lineIds":[2,5],"tagDirection":3},{"stationId":36,"stationName":"顺驰桥","position":[1719,1265],"shape":"cicle","lineIds":[2]},{"stationId":37,"stationName":"远洋国际中心","position":[1589,1314],"shape":"cicle","lineIds":[2],"tagDirection":3},{"stationId":38,"stationName":"天津站","position":[1496,1301],"shape":"cicle","lineIds":[2,3,8],"tagDirection":3},{"stationId":39,"stationName":"建国道","position":[1389,1289],"shape":"cicle","lineIds":[2]},{"stationId":40,"stationName":"东南角","position":[1282,1310],"shape":"cicle","lineIds":[2,4]},{"stationId":41,"stationName":"鼓楼","position":[1191,1307],"shape":"cicle","lineIds":[2]},{"stationId":42,"stationName":"广开四马路","position":[999,1302],"shape":"cicle","lineIds":[2]},{"stationId":43,"stationName":"长虹公园","position":[873,1301],"shape":"cicle","lineIds":[2,6]},{"stationId":44,"stationName":"咸阳路","position":[729,1301],"shape":"cicle","lineIds":[2]},{"stationId":45,"stationName":"芥园西道","position":[579,1252],"shape":"cicle","lineIds":[2]},{"stationId":46,"stationName":"卞兴","position":[469,1217],"shape":"cicle","lineIds":[2],"tagDirection":7},{"stationId":47,"stationName":"曹庄","position":[289,1222],"shape":"cicle","lineIds":[2]},{"stationId":48,"stationName":"小淀","position":[1955,287],"shape":"cicle","lineIds":[3]},{"stationId":49,"stationName":"丰产河","position":[1739,297],"shape":"cicle","lineIds":[3]},{"stationId":50,"stationName":"华北集团","position":[1659,403],"shape":"cicle","lineIds":[3]},{"stationId":51,"stationName":"天士力","position":[1529,620],"shape":"cicle","lineIds":[3],"tagDirection":2},{"stationId":52,"stationName":"宜兴埠","position":[1569,703],"shape":"cicle","lineIds":[3],"tagDirection":2},{"stationId":53,"stationName":"张兴庄","position":[1527,759],"shape":"cicle","lineIds":[3,5]},{"stationId":54,"stationName":"铁东路","position":[1449,880],"shape":"cicle","lineIds":[3]},{"stationId":55,"stationName":"北站","position":[1453,1012],"shape":"cicle","lineIds":[3,6],"tagDirection":1},{"stationId":56,"stationName":"中山路","position":[1439,1061],"shape":"cicle","lineIds":[3]},{"stationId":57,"stationName":"金狮桥","position":[1459,1168],"shape":"cicle","lineIds":[3]},{"stationId":58,"stationName":"津湾广场","position":[1459,1359],"shape":"cicle","lineIds":[3],"tagDirection":6},{"stationId":59,"stationName":"和平路","position":[1426,1410],"shape":"cicle","lineIds":[3,4]},{"stationId":60,"stationName":"西康路","position":[1259,1557],"shape":"cicle","lineIds":[3]},{"stationId":61,"stationName":"吴家窑","position":[1239,1681],"shape":"cicle","lineIds":[3]},{"stationId":62,"stationName":"天塔","position":[1159,1721],"shape":"cicle","lineIds":[3]},{"stationId":63,"stationName":"周邓纪念馆","position":[1019,1752],"shape":"cicle","lineIds":[3]},{"stationId":64,"stationName":"红旗南路","position":[860,1751],"shape":"cicle","lineIds":[3,6],"tagDirection":1},{"stationId":65,"stationName":"王顶堤","position":[769,1758],"shape":"cicle","lineIds":[3]},{"stationId":66,"stationName":"华苑","position":[669,1807],"shape":"cicle","lineIds":[3]},{"stationId":67,"stationName":"大学城","position":[499,1955],"shape":"cicle","lineIds":[3],"tagDirection":3},{"stationId":68,"stationName":"高新区","position":[369,1960],"shape":"cicle","lineIds":[3]},{"stationId":69,"stationName":"学府工业区","position":[169,1960],"shape":"cicle","lineIds":[3]},{"stationId":70,"stationName":"杨伍庄","position":[79,1996],"shape":"cicle","lineIds":[3]},{"stationId":71,"stationName":"南站","position":[0,2108],"shape":"cicle","lineIds":[3]},{"stationId":72,"stationName":"金街","position":[1301,1361],"shape":"cicle","lineIds":[4]},{"stationId":73,"stationName":"徐州道","position":[1564,1524],"shape":"cicle","lineIds":[4],"tagDirection":0},{"stationId":74,"stationName":"六纬路","position":[1654,1522],"shape":"cicle","lineIds":[4],"tagDirection":4},{"stationId":75,"stationName":"成林道","position":[1835,1376],"shape":"cicle","lineIds":[4,5]},{"stationId":76,"stationName":"泰昌路","position":[1944,1456],"shape":"cicle","lineIds":[4]},{"stationId":77,"stationName":"万东路","position":[2053,1484],"shape":"cicle","lineIds":[4]},{"stationId":78,"stationName":"沙柳南路","position":[2268,1505],"shape":"cicle","lineIds":[4,9],"tagDirection":1},{"stationId":79,"stationName":"登州南路","position":[2433,1513],"shape":"cicle","lineIds":[4]},{"stationId":80,"stationName":"跃进北路","position":[2567,1539],"shape":"cicle","lineIds":[4]},{"stationId":81,"stationName":"航双路","position":[2718,1568],"shape":"cicle","lineIds":[4]},{"stationId":82,"stationName":"民航大学","position":[2870,1590],"shape":"cicle","lineIds":[4]},{"stationId":83,"stationName":"新兴村","position":[3084,1663],"shape":"cicle","lineIds":[4]},{"stationId":84,"stationName":"李七庄南","position":[1086,2314],"shape":"cicle","lineIds":[5]},{"stationId":85,"stationName":"中医一附院","position":[1226,2210],"shape":"cicle","lineIds":[5]},{"stationId":86,"stationName":"昌凌路","position":[1226,2112],"shape":"cicle","lineIds":[5,9]},{"stationId":87,"stationName":"凌宾路","position":[1099,1998],"shape":"cicle","lineIds":[5]},{"stationId":88,"stationName":"体育中心","position":[1104,1931],"shape":"cicle","lineIds":[5]},{"stationId":89,"stationName":"肿瘤医院","position":[1224,1889],"shape":"cicle","lineIds":[5,6]},{"stationId":90,"stationName":"天津宾馆","position":[1364,1825],"shape":"cicle","lineIds":[5,6]},{"stationId":91,"stationName":"文化中心","position":[1448,1780],"shape":"cicle","lineIds":[5,6],"tagDirection":7},{"stationId":92,"stationName":"西南楼","position":[1540,1724],"shape":"cicle","lineIds":[5],"tagDirection":7},{"stationId":93,"stationName":"直沽","position":[1718,1569],"shape":"cicle","lineIds":[5,8]},{"stationId":94,"stationName":"津塘路","position":[1793,1513],"shape":"cicle","lineIds":[5]},{"stationId":95,"stationName":"幸福公园","position":[1821,1138],"shape":"cicle","lineIds":[5]},{"stationId":96,"stationName":"月牙河","position":[1837,1031],"shape":"cicle","lineIds":[5]},{"stationId":97,"stationName":"金钟河大街","position":[1798,957],"shape":"cicle","lineIds":[5,6]},{"stationId":98,"stationName":"建昌道","position":[1725,882],"shape":"cicle","lineIds":[5],"tagDirection":5},{"stationId":99,"stationName":"思源路","position":[1654,845],"shape":"cicle","lineIds":[5]},{"stationId":100,"stationName":"志成路","position":[1570,808],"shape":"cicle","lineIds":[5]},{"stationId":101,"stationName":"宜兴埠北","position":[1433,648],"shape":"cicle","lineIds":[5],"tagDirection":5},{"stationId":102,"stationName":"辽河北道","position":[1379,581],"shape":"cicle","lineIds":[5]},{"stationId":103,"stationName":"淮河道","position":[1304,493],"shape":"cicle","lineIds":[5]},{"stationId":104,"stationName":"职业大学","position":[1199,368],"shape":"cicle","lineIds":[5]},{"stationId":105,"stationName":"北辰道","position":[1116,272],"shape":"cicle","lineIds":[5]},{"stationId":106,"stationName":"丹河北道","position":[1016,154],"shape":"cicle","lineIds":[5]},{"stationId":107,"stationName":"北辰科技园北","position":[878,0],"shape":"cicle","lineIds":[5]},{"stationId":108,"stationName":"南孙庄","position":[2563,613],"shape":"cicle","lineIds":[6]},{"stationId":109,"stationName":"南何庄","position":[2289,615],"shape":"cicle","lineIds":[6]},{"stationId":110,"stationName":"大毕庄","position":[2170,649],"shape":"cicle","lineIds":[6]},{"stationId":111,"stationName":"金钟街","position":[2040,680],"shape":"cicle","lineIds":[6]},{"stationId":112,"stationName":"徐庄子","position":[1921,770],"shape":"cicle","lineIds":[6]},{"stationId":113,"stationName":"民权门","position":[1713,992],"shape":"cicle","lineIds":[6]},{"stationId":114,"stationName":"北宁公园","position":[1611,1016],"shape":"cicle","lineIds":[6]},{"stationId":115,"stationName":"新开河","position":[1407,990],"shape":"cicle","lineIds":[6]},{"stationId":116,"stationName":"外院附中","position":[1297,966],"shape":"cicle","lineIds":[6]},{"stationId":117,"stationName":"天泰路","position":[1212,997],"shape":"cicle","lineIds":[6]},{"stationId":118,"stationName":"北竹林","position":[1106,1077],"shape":"cicle","lineIds":[6],"tagDirection":3},{"stationId":119,"stationName":"复兴路","position":[958,1117],"shape":"cicle","lineIds":[6]},{"stationId":120,"stationName":"人民医院","position":[876,1205],"shape":"cicle","lineIds":[6]},{"stationId":121,"stationName":"宜宾道","position":[872,1415],"shape":"cicle","lineIds":[6]},{"stationId":122,"stationName":"鞍山西道","position":[873,1513],"shape":"cicle","lineIds":[6]},{"stationId":123,"stationName":"天拖","position":[873,1587],"shape":"cicle","lineIds":[6]},{"stationId":124,"stationName":"一中心医院","position":[874,1647],"shape":"cicle","lineIds":[6]},{"stationId":125,"stationName":"迎风道","position":[874,1817],"shape":"cicle","lineIds":[6]},{"stationId":126,"stationName":"南翠屏","position":[965,1879],"shape":"cicle","lineIds":[6]},{"stationId":127,"stationName":"水上公园东路","position":[1110,1884],"shape":"cicle","lineIds":[6],"tagDirection":0},{"stationId":128,"stationName":"乐园道","position":[1554,1766],"shape":"cicle","lineIds":[6],"tagDirection":5},{"stationId":129,"stationName":"尖山路","position":[1619,1806],"shape":"cicle","lineIds":[6],"tagDirection":5},{"stationId":130,"stationName":"医大二院","position":[1619,1867],"shape":"cicle","lineIds":[6]},{"stationId":131,"stationName":"梅江道","position":[1635,1991],"shape":"cicle","lineIds":[6]},{"stationId":132,"stationName":"左江道","position":[1556,2079],"shape":"cicle","lineIds":[6]},{"stationId":133,"stationName":"梅江公园","position":[1555,2155],"shape":"cicle","lineIds":[6]},{"stationId":134,"stationName":"梅江会展中心","position":[1556,2236],"shape":"cicle","lineIds":[6]},{"stationId":135,"stationName":"解放南路","position":[1719,2271],"shape":"cicle","lineIds":[6]},{"stationId":136,"stationName":"洞庭路","position":[1838,2243],"shape":"cicle","lineIds":[6]},{"stationId":137,"stationName":"梅林路","position":[1927,2231],"shape":"cicle","lineIds":[6]},{"stationId":138,"stationName":"渌水道","position":[2079,2223],"shape":"cicle","lineIds":[6,7]},{"stationId":139,"stationName":"双港","position":[2155,2376],"shape":"cicle","lineIds":[7]},{"stationId":140,"stationName":"景荷道","position":[2220,2479],"shape":"cicle","lineIds":[7]},{"stationId":141,"stationName":"景荔道","position":[2275,2565],"shape":"cicle","lineIds":[7]},{"stationId":142,"stationName":"天津大学北洋园校区","position":[2465,2752],"shape":"cicle","lineIds":[7]},{"stationId":143,"stationName":"海河教育园区","position":[2601,2789],"shape":"cicle","lineIds":[7]},{"stationId":144,"stationName":"南开大学津南校区","position":[2795,2851],"shape":"cicle","lineIds":[7]},{"stationId":145,"stationName":"和慧南路","position":[2982,2866],"shape":"cicle","lineIds":[7]},{"stationId":146,"stationName":"咸水沽西","position":[3117,2873],"shape":"cicle","lineIds":[7]},{"stationId":147,"stationName":"东海路","position":[6752,2273],"shape":"cicle","lineIds":[8]},{"stationId":148,"stationName":"会展中心","position":[6696,2379],"shape":"cicle","lineIds":[8]},{"stationId":149,"stationName":"太湖路","position":[6613,2480],"shape":"cicle","lineIds":[8]},{"stationId":150,"stationName":"市民广场","position":[6410,2497],"shape":"cicle","lineIds":[8]},{"stationId":151,"stationName":"泰达","position":[6173,2424],"shape":"cicle","lineIds":[8]},{"stationId":152,"stationName":"塘沽","position":[5989,2383],"shape":"cicle","lineIds":[8]},{"stationId":153,"stationName":"胡家园","position":[5453,2274],"shape":"cicle","lineIds":[8],"tagDirection":7},{"stationId":154,"stationName":"钢管公司","position":[4418,2281],"shape":"cicle","lineIds":[8]},{"stationId":155,"stationName":"军粮城","position":[3940,2198],"shape":"cicle","lineIds":[8]},{"stationId":156,"stationName":"小东庄","position":[3412,2035],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":157,"stationName":"东丽开发区","position":[2953,1885],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":158,"stationName":"新立","position":[2739,1820],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":159,"stationName":"张贵庄","position":[2457,1758],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":160,"stationName":"二号桥","position":[2310,1727],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":161,"stationName":"一号桥","position":[2208,1718],"shape":"cicle","lineIds":[8]},{"stationId":162,"stationName":"中山门","position":[2022,1670],"shape":"cicle","lineIds":[8]},{"stationId":163,"stationName":"东兴路","position":[1805,1645],"shape":"cicle","lineIds":[8]},{"stationId":164,"stationName":"十一经路","position":[1619,1465],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":165,"stationName":"大王庄","position":[1564,1407],"shape":"cicle","lineIds":[8],"tagDirection":1},{"stationId":166,"stationName":"崂山道","position":[2215,1292],"shape":"cicle","lineIds":[9]},{"stationId":167,"stationName":"香山道","position":[2244,1362],"shape":"cicle","lineIds":[9]},{"stationId":168,"stationName":"万山道","position":[2255,1431],"shape":"cicle","lineIds":[9]},{"stationId":169,"stationName":"方山道","position":[2273,1577],"shape":"cicle","lineIds":[9],"tagDirection":2},{"stationId":170,"stationName":"金贸产业园","position":[2254,1651],"shape":"cicle","lineIds":[9],"tagDirection":2},{"stationId":171,"stationName":"龙涵道","position":[2250,1734],"shape":"cicle","lineIds":[9]},{"stationId":172,"stationName":"环宇道","position":[2237,1836],"shape":"cicle","lineIds":[9,10]},{"stationId":173,"stationName":"柳林路","position":[2227,1957],"shape":"cicle","lineIds":[9]},{"stationId":174,"stationName":"微山路","position":[1976,2050],"shape":"cicle","lineIds":[9],"tagDirection":4},{"stationId":175,"stationName":"玛钢厂","position":[1906,2053],"shape":"cicle","lineIds":[9],"tagDirection":5},{"stationId":176,"stationName":"春海路","position":[1810,2049],"shape":"cicle","lineIds":[9]},{"stationId":177,"stationName":"南珠桥","position":[1653,2042],"shape":"cicle","lineIds":[9]},{"stationId":178,"stationName":"友谊南路","position":[1554,2041],"shape":"cicle","lineIds":[9]},{"stationId":179,"stationName":"江湾二支路","position":[1438,2041],"shape":"cicle","lineIds":[9],"tagDirection":0},{"stationId":180,"stationName":"丽江道","position":[1321,2051],"shape":"cicle","lineIds":[9]},{"stationId":181,"stationName":"瑶环路","position":[1129,2132],"shape":"cicle","lineIds":[9]},{"stationId":182,"stationName":"于台","position":[1005,2214],"shape":"cicle","lineIds":[9]},{"stationId":183,"stationName":"东江道","position":[1978,1996],"shape":"cicle","lineIds":[10],"tagDirection":7},{"stationId":184,"stationName":"学苑北路","position":[2101,1937],"shape":"cicle","lineIds":[10],"tagDirection":3},{"stationId":185,"stationName":"海河东路","position":[2144,1851],"shape":"cicle","lineIds":[10]},{"stationId":186,"stationName":"雪莲南路","position":[2378,1849],"shape":"cicle","lineIds":[10]},{"stationId":187,"stationName":"招远路","position":[2486,1869],"shape":"cicle","lineIds":[10]},{"stationId":188,"stationName":"东丽文体中心","position":[2666,1889],"shape":"cicle","lineIds":[10]},{"stationId":189,"stationName":"驯海路","position":[2767,1916],"shape":"cicle","lineIds":[10]},{"stationId":190,"stationName":"东丽一经路","position":[2878,1947],"shape":"cicle","lineIds":[10]},{"stationId":191,"stationName":"东丽三经路","position":[2963,1976],"shape":"cicle","lineIds":[10]},{"stationId":192,"stationName":"东丽六经路","position":[3124,2027],"shape":"cicle","lineIds":[10]}],"lines":[{"lineId":1,"lineName":"1号线","color":"#CC0000","stationIds":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"sign":"1","order":1,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":2,"lineName":"2号线","color":"#FCD600","stationIds":[29,30,31,32,33,34,35,36,37,38,39,40,41,9,42,43,44,45,46,47],"sign":"2","order":2,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":3,"lineName":"3号线","color":"#019AC3","stationIds":[48,49,50,51,52,53,54,55,56,57,38,58,59,13,60,61,62,63,64,65,66,67,68,69,70,71],"sign":"3","order":3,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":4,"lineName":"4号线","color":"#018237","stationIds":[40,72,59,73,74,75,76,77,78,79,80,81,82,83],"sign":"4","order":4,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":5,"lineName":"5号线","color":"#EE782E","stationIds":[84,85,86,87,88,89,90,91,92,15,93,94,75,35,95,96,97,98,99,100,53,101,102,103,104,105,106,107],"sign":"5","order":5,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":6,"lineName":"6号线","color":"#A61D7F","stationIds":[108,109,110,111,112,97,113,114,55,115,116,117,118,7,119,120,43,121,122,123,124,64,125,126,127,89,90,91,128,129,130,131,132,133,134,135,136,137,138],"sign":"6","order":6,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":7,"lineName":"6号线","color":"#A61D7F","stationIds":[138,139,140,141,142,143,144,145,146],"sign":"6","order":7,"bendFirst":[true,true,true,true,true,true,true,true,true]},{"lineId":8,"lineName":"9号线","color":"#2249A3","stationIds":[147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,93,164,165,38],"sign":"9","order":8,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":9,"lineName":"10号线","color":"#B8D201","stationIds":[33,166,167,168,78,169,170,171,172,173,21,174,175,176,177,178,179,180,86,181,182],"sign":"10","order":9,"bendFirst":[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]},{"lineId":10,"lineName":"11号线","color":"#2249A3","stationIds":[183,184,185,172,186,187,188,189,190,191,192],"sign":"11","order":10,"bendFirst":[false,true,true,false,false,false,false,false,false,false,true]}],"title":"天津"} ================================================ FILE: src/Common/AutoGrowthInput.scss ================================================ .auto-growth-container { position: relative; width: fit-content; .auto-growth-span { // font-size: 36px; border: none; // font-weight: 500; // width: 100px; // display: inline-block; // margin-top: 14px; opacity: 0; background-color: transparent; white-space: nowrap; } .auto-growth-input { // font-size: 36px; white-space: nowrap; border: none; // font-weight: 500; width: calc(100% + 18px); // margin-top: 14px; position: absolute; left: 0; padding: 0; background-color: transparent; &:disabled{ appearance: none; color: inherit; opacity: inherit; cursor: default; border: none; &:focus,&:focus-visible{ outline: none; } } } .click-panel{ position: absolute; left: 0; width: calc(100% + 18px); height: 100%; top: 0; // cursor: pointer; } &.disabled{ .auto-growth-span{ opacity: 1; } .auto-growth-input{ // opacity: 0; display: none; } } } ================================================ FILE: src/Common/AutoGrowthInput.tsx ================================================ import classNames from "classnames"; import React, { CSSProperties, Dispatch, LegacyRef, RefAttributes, RefObject, SetStateAction, StyleHTMLAttributes, forwardRef, useEffect, useState, } from "react"; import "./AutoGrowthInput.scss"; type InputProps = { value?: number | string; onInput?: (x: any) => void; className?: string; type?: string; disabled?: boolean; style?: CSSProperties; onClick?: (x: any) => void; } export const AutoGrowthInput = forwardRef(function ({ value, onInput, className = "", type = "", disabled = false, style = {}, onClick, }, ref) { return (
{value} { if (document.activeElement === e.currentTarget) e.stopPropagation(); }} onBlur={(e)=>{ if(type === "number") setTimeout(()=>{ //@ts-ignore e.target.value = value }) }} > {disabled?
:<>}
); }) ================================================ FILE: src/Common/api.ts ================================================ export const getExistMap = (id:string) => { const url = `/${id}.json`; return fetch(url).then(res=>res.text()); } ================================================ FILE: src/Common/color.ts ================================================ import { arrayToMap } from "./util"; export const colorSH = [ { "line": "1号线", "color": "#EA0B2A", "color_name": "正红色", "rgb": [234, 11, 42] }, { "line": "2号线", "color": "#94D40B", "color_name": "绿色", "rgb": [148, 212, 11] }, { "line": "3号线", "color": "#F8D000", "color_name": "黄色", "rgb": [248, 208, 0] }, { "line": "4号线", "color": "#60269E", "color_name": "紫色", "rgb": [96, 38, 158] }, { "line": "5号线", "color": "#934C9A", "color_name": "紫红色", "rgb": [147, 76, 154] }, { "line": "6号线", "color": "#D80169", "color_name": "品红色", "rgb": [216, 1, 105] }, { "line": "7号线", "color": "#FE6B01", "color_name": "橙色", "rgb": [254, 107, 1] }, { "line": "8号线", "color": "#00A0E8", "color_name": "蓝色", "rgb": [0, 160, 232] }, { "line": "9号线", "color": "#6FC5E8", "color_name": "淡蓝色", "rgb": [111, 197, 232] }, { "line": "10号线", "color": "#C3A5E1", "color_name": "淡紫色", "rgb": [195, 165, 225] }, { "line": "11号线", "color": "#792330", "color_name": "棕色", "rgb": [121, 35, 48] }, { "line": "12号线", "color": "#007A61", "color_name": "深绿色", "rgb": [0, 122, 97] }, { "line": "13号线", "color": "#F095CE", "color_name": "粉色", "rgb": [240, 149, 206] }, { "line": "14号线", "color": "#827805", "color_name": "橄榄绿色", "rgb": [130, 120, 5] }, { "line": "15号线", "color": "#BDA686", "color_name": "香槟金色", "rgb": [189, 166, 134] }, { "line": "16号线", "color": "#2AD2C5", "color_name": "水绿色", "rgb": [42, 210, 197] } ]; export const colorSHMap = arrayToMap("color",colorSH); ================================================ FILE: src/Common/const.ts ================================================ const gauge = 10; const line_radius = 10; const handleLength = 45; const handleWidth = 15; export { gauge, line_radius, handleLength, handleWidth }; ================================================ FILE: src/Common/teyvat.ts ================================================ import { UserDataType } from "../Data/UserData"; import { t } from "i18next"; export const teyvat: UserDataType = { stations: [ { stationId: 1, stationName: t("wang-feng-jiao-zhan"), position: [678, -52], shape: "cicle", lineIds: [3], tagDirection: 2, }, { stationId: 2, stationName: t("zhai-xing-ya-zhan"), position: [645, 71], shape: "cicle", lineIds: [3], }, { stationId: 3, stationName: t("qian-feng-shen-dian-zhan"), position: [640, 191], shape: "cicle", lineIds: [3], }, { stationId: 4, stationName: t("Common-teyvat-192be7b183f6eee79-1"), position: [463, 418], shape: "cicle", lineIds: [2, 3, 8], }, { stationId: 5, stationName: t("Common-teyvat-192be7b183f6eee79-2"), position: [573, 641], shape: "cicle", lineIds: [3], }, { stationId: 6, stationName: t("Common-teyvat-192be7b183f6eee79-3"), position: [740, 641], shape: "cicle", lineIds: [3], }, { stationId: 7, stationName: t("Common-teyvat-192be7b183f6eee79-4"), position: [813, 533], shape: "cicle", lineIds: [3], }, { stationId: 8, stationName: t("Common-teyvat-192be7b183f6eee79-5"), position: [388, 318], shape: "cicle", lineIds: [1, 2], tagDirection: 6 }, { stationId: 9, stationName: t("Common-teyvat-192be7b183f6eee79-6"), position: [458, 193], shape: "cicle", lineIds: [2], }, { stationId: 10, stationName: t("Common-teyvat-192be7b183f6eee79-7"), position: [460, 86], shape: "cicle", lineIds: [2], }, { stationId: 11, stationName: t("Common-teyvat-192be7b183f6eee79-8"), position: [458, -64], shape: "cicle", lineIds: [2], }, { stationId: 12, stationName: t("Common-teyvat-192be7b183f6eee79-9"), position: [325, -102], shape: "cicle", lineIds: [2], }, { stationId: 13, stationName: t("Common-teyvat-192be7b183f6eee79-10"), position: [245, 168], shape: "cicle", lineIds: [1], }, { stationId: 14, stationName: t("Common-teyvat-192be7b183f6eee79-11"), position: [320, 226], shape: "cicle", lineIds: [1], }, { stationId: 15, stationName: t("Common-teyvat-192be7b183f6eee79-12"), position: [270, 358], shape: "cicle", lineIds: [1], }, { stationId: 16, stationName: t("Common-teyvat-192be7b183f6eee79-13"), position: [70, 428], shape: "cicle", lineIds: [1, 4, 8], }, { stationId: 17, stationName: t("Common-teyvat-192be7b183f6eee79-14"), position: [-2, 336], shape: "cicle", lineIds: [4], }, { stationId: 18, stationName: t("Common-teyvat-192be7b183f6eee79-15"), position: [3, 248], shape: "cicle", lineIds: [4], }, { stationId: 19, stationName: t("Common-teyvat-192be7b183f6eee79-16"), position: [-70, 151], shape: "cicle", lineIds: [4], }, { stationId: 20, stationName: t("Common-teyvat-192be7b183f6eee79-17"), position: [15, 18], shape: "cicle", lineIds: [4], }, { stationId: 21, stationName: t("Common-teyvat-192be7b183f6eee79-18"), position: [-87, -77], shape: "cicle", lineIds: [4], }, { stationId: 22, stationName: t("Common-teyvat-192be7b183f6eee79-19"), position: [83, 628], shape: "cicle", lineIds: [4], tagDirection: 2, }, { stationId: 23, stationName: t("Common-teyvat-192be7b183f6eee79-20"), position: [92, 748], shape: "cicle", lineIds: [4], tagDirection: 2, }, { stationId: 24, stationName: t("Common-teyvat-192be7b183f6eee79-21"), position: [248, 748], shape: "cicle", lineIds: [4], }, { stationId: 25, stationName: t("Common-teyvat-192be7b183f6eee79-22"), position: [338, 778], shape: "cicle", lineIds: [4], }, { stationId: 26, stationName: t("Common-teyvat-192be7b183f6eee79-23"), position: [180, 851], shape: "cicle", lineIds: [4], }, { stationId: 27, stationName: t("Common-teyvat-192be7b183f6eee79-24"), position: [-35, 703], shape: "cicle", lineIds: [8], }, { stationId: 28, stationName: t("Common-teyvat-192be7b183f6eee79-25"), position: [-230, 813], shape: "cicle", lineIds: [5, 8], }, { stationId: 29, stationName: t("Common-teyvat-192be7b183f6eee79-26"), position: [-310, 568], shape: "cicle", lineIds: [5], }, { stationId: 30, stationName: t("Common-teyvat-192be7b183f6eee79-27"), position: [-452, 473], shape: "cicle", lineIds: [5], }, { stationId: 31, stationName: t("Common-teyvat-192be7b183f6eee79-28"), position: [-240, 971], shape: "cicle", lineIds: [5], }, { stationId: 32, stationName: t("Common-teyvat-192be7b183f6eee79-29"), position: [-197, 1071], shape: "cicle", lineIds: [5], }, { stationId: 33, stationName: t("Common-teyvat-192be7b183f6eee79-30"), position: [-280, 1226], shape: "cicle", lineIds: [5], }, { stationId: 34, stationName: t("Common-teyvat-192be7b183f6eee79-31"), position: [-285, 1451], shape: "cicle", lineIds: [5, 6], }, { stationId: 35, stationName: t("Common-teyvat-192be7b183f6eee79-32"), position: [-210, 1608], shape: "cicle", lineIds: [5, 7], }, { stationId: 36, stationName: t("Common-teyvat-192be7b183f6eee79-33"), position: [-605, 1601], shape: "cicle", lineIds: [7], }, { stationId: 37, stationName: t("Common-teyvat-192be7b183f6eee79-34"), position: [-671, 1511], shape: "cicle", lineIds: [7, 11], tagDirection: 5, }, { stationId: 38, stationName: t("Common-teyvat-192be7b183f6eee79-35"), position: [-362, 1351], shape: "cicle", lineIds: [6], }, { stationId: 39, stationName: t("Common-teyvat-192be7b183f6eee79-36"), position: [-642, 1326], shape: "cicle", lineIds: [7], }, { stationId: 40, stationName: t("Common-teyvat-192be7b183f6eee79-37"), position: [-437, 1158], shape: "cicle", lineIds: [6], }, { stationId: 41, stationName: t("Common-teyvat-192be7b183f6eee79-38"), position: [-552, 1008], shape: "cicle", lineIds: [6], }, { stationId: 42, stationName: t("Common-teyvat-192be7b183f6eee79-39"), position: [-602, 946], shape: "cicle", lineIds: [6], }, { stationId: 43, stationName: t("Common-teyvat-192be7b183f6eee79-40"), position: [-525, 829], shape: "cicle", lineIds: [8], tagDirection: 4 }, { stationId: 44, stationName: t("Common-teyvat-192be7b183f6eee79-41"), position: [-742, 828], shape: "cicle", lineIds: [6, 7, 8], }, { stationId: 45, stationName: t("Common-teyvat-192be7b183f6eee79-42"), position: [-805, 728], shape: "cicle", lineIds: [7], }, { stationId: 46, stationName: t("Common-teyvat-192be7b183f6eee79-43"), position: [-955, 903], shape: "cicle", lineIds: [7], }, { stationId: 47, stationName: t("Common-teyvat-192be7b183f6eee79-44"), position: [-857, 943], shape: "cicle", lineIds: [8], tagDirection: 2 }, { stationId: 48, stationName: t("Common-teyvat-192be7b183f6eee79-45"), position: [-855, 1078], shape: "cicle", lineIds: [7], }, { stationId: 49, stationName: t("Common-teyvat-192be7b183f6eee79-46"), position: [-725, 1208], shape: "cicle", lineIds: [7], }, { stationId: 50, stationName: t("Common-teyvat-192be7b183f6eee79-47"), position: [-897, 1411], shape: "cicle", lineIds: [11, 12], }, { stationId: 51, stationName: t("Common-teyvat-192be7b183f6eee79-48"), position: [-1090, 1388], shape: "cicle", lineIds: [9, 11], }, { stationId: 52, stationName: t("Common-teyvat-192be7b183f6eee79-49"), position: [-1090, 1076], shape: "cicle", lineIds: [9, 10], }, { stationId: 53, stationName: t("Common-teyvat-192be7b183f6eee79-50"), position: [-1272, 1173], shape: "cicle", lineIds: [10], }, { stationId: 54, stationName: t("Common-teyvat-192be7b183f6eee79-51"), position: [-1405, 1271], shape: "cicle", lineIds: [10], }, { stationId: 55, stationName: t("Common-teyvat-192be7b183f6eee79-52"), position: [-1360, 1388], shape: "cicle", lineIds: [10, 11], }, { stationId: 56, stationName: t("Common-teyvat-192be7b183f6eee79-53"), position: [-1195, 1576], shape: "cicle", lineIds: [10, 12], }, { stationId: 57, stationName: t("Common-teyvat-192be7b183f6eee79-54"), position: [-1037, 1491], shape: "cicle", lineIds: [9, 12], }, { stationId: 58, stationName: t("Common-teyvat-192be7b183f6eee79-55"), position: [-1067, 1791], shape: "cicle", lineIds: [9], tagDirection: 2, }, { stationId: 59, stationName: t("Common-teyvat-192be7b183f6eee79-56"), position: [-1177, 1933], shape: "cicle", lineIds: [9], tagDirection: 3, }, { stationId: 60, stationName: t("Common-teyvat-192be7b183f6eee79-57"), position: [-1310, 1931], shape: "cicle", lineIds: [9, 10], tagDirection: 7, }, { stationId: 61, stationName: t("Common-teyvat-192be7b183f6eee79-58"), position: [-1247, 2116], shape: "cicle", lineIds: [10], }, { stationId: 62, stationName: t("Common-teyvat-192be7b183f6eee79-59"), position: [-1255, 1701], shape: "cicle", lineIds: [10], tagDirection: 3, }, { stationId: 63, stationName: t("Common-teyvat-192be7b183f6eee79-60"), position: [-1522, 1681], shape: "cicle", lineIds: [11, 12], }, { stationId: 64, stationName: t("Common-teyvat-192be7b183f6eee79-61"), position: [-1695, 1681], shape: "cicle", lineIds: [12], }, { stationId: 65, stationName: t("Common-teyvat-192be7b183f6eee79-62"), position: [-1865, 1493], shape: "cicle", lineIds: [12], }, { stationId: 66, stationName: t("Common-teyvat-192be7b183f6eee79-63"), position: [-2215, 1326], shape: "cicle", lineIds: [12], }, { stationId: 67, stationName: t("Common-teyvat-192be7b183f6eee79-64"), position: [-1812, 1908], shape: "cicle", lineIds: [11], }, { stationId: 68, stationName: t("Common-teyvat-192be7b183f6eee79-65"), position: [-1482, 1931], shape: "cicle", lineIds: [9], }, { stationId: 69, stationName: t("Common-teyvat-192be7b183f6eee79-66"), position: [-1737, 1998], shape: "cicle", lineIds: [9], tagDirection: 3 }, { stationId: 70, stationName: t("Common-teyvat-192be7b183f6eee79-67"), position: [-1917, 2093], shape: "cicle", lineIds: [9, 11], }, { stationId: 71, stationName: t("Common-teyvat-192be7b183f6eee79-68"), position: [-2077, 1883], shape: "cicle", lineIds: [9], }, { stationId: 72, stationName: t("Common-teyvat-192be7b183f6eee79-69"), position: [-1997, 2211], shape: "cicle", lineIds: [11], }, { stationId: 73, stationName: t("Common-teyvat-192be7b183f6eee79-70"), position: [-2212, 2316], shape: "cicle", lineIds: [11], }, { stationId: 74, stationName: t("Common-teyvat-192be7b183f6eee79-71"), position: [-2470, 2316], shape: "cicle", lineIds: [11], }, ].reduce((map, cur) => { map.set(cur.stationId, cur); return map; }, new Map()), lines: [ { lineId: 1, lineName: t("Common-teyvat-192be7b183f6eee79-72"), color: "#EA0B2A", stationIds: [13, 14, 8, 15, 16], sign: "1", order: 1, bendFirst: [true, true, true, true, true], }, { lineId: 2, lineName: t("Common-teyvat-192be7b183f6eee79-73"), color: "#94D40B", stationIds: [12, 11, 10, 9, 8, 4], sign: "2", order: 2, bendFirst: [false, true, true, true, true, true], }, { lineId: 3, lineName: t("Common-teyvat-192be7b183f6eee79-74"), color: "#F8D000", stationIds: [1, 2, 3, 4, 5, 6, 7], sign: "3", order: 3, bendFirst: [true, true, true, false, true, true, true], }, { lineId: 4, lineName: t("Common-teyvat-192be7b183f6eee79-75"), color: "#60269E", stationIds: [21, 20, 19, 18, 17, 16, 22, 23, 26, 25, 24], sign: "4", order: 4, bendFirst: [ true, false, true, true, true, true, true, true, false, true, true, ], }, { lineId: 5, lineName: t("Common-teyvat-192be7b183f6eee79-76"), color: "#934C9A", stationIds: [30, 29, 28, 31, 32, 33, 34, 35], sign: "5", order: 5, bendFirst: [true, true, true, true, true, true, true, true], }, { lineId: 6, lineName: t("Common-teyvat-192be7b183f6eee79-77"), color: "#D80169", stationIds: [44, 42, 41, 40, 38, 34], sign: "6", order: 6, bendFirst: [false, true, true, true, false, true], }, { lineId: 7, lineName: t("Common-teyvat-192be7b183f6eee79-78"), color: "#FE6B01", stationIds: [35, 36, 37, 39, 49, 48, 46, 45, 44], sign: "7", order: 7, bendFirst: [true, true, true, false, true, true, false, true, true], }, { lineId: 8, lineName: t("Common-teyvat-192be7b183f6eee79-79"), color: "#00A0E8", stationIds: [47, 44, 43, 28, 27, 16, 4], sign: "8", order: 8, bendFirst: [true, true, false, false, false, false, true], }, { lineId: 9, lineName: t("Common-teyvat-192be7b183f6eee79-80"), color: "#385f88", stationIds: [52, 51, 57, 58, 59, 60, 68, 69, 70, 71], sign: "2", order: 9, bendFirst: [true, true, true, false, true, true, true, true, true, true], }, { lineId: 10, lineName: t("Common-teyvat-192be7b183f6eee79-81"), color: "#e8c148", stationIds: [52, 53, 54, 55, 56, 62, 60, 61], sign: "1", order: 10, bendFirst: [false, false, false, true, false, true, true, true], }, { lineId: 11, lineName: t("Common-teyvat-192be7b183f6eee79-82"), color: "#dd8d4e", stationIds: [37, 50, 51, 55, 63, 67, 70, 72, 73, 74], sign: "3", order: 11, bendFirst: [false, true, true, true, true, true, false, true, true, true], }, { lineId: 12, lineName: t("Common-teyvat-192be7b183f6eee79-83"), color: "#35743c", stationIds: [50, 57, 56, 63, 64, 65, 66], sign: "4", order: 12, bendFirst: [true, true, true, true, true, true, true], }, ].reduce((map, cur) => { map.set(cur.lineId, cur); return map; }, new Map()), title: t("teyvat"), backgroundColor: "#fffee0", }; ================================================ FILE: src/Common/util.ts ================================================ import { MouseEvent, WheelEventHandler } from "react"; import UAParser from "ua-parser-js"; import { TransformProps, UserDataType } from "../Data/UserData"; export const mapToArr = (map: Map) => { const arr: V[] = []; map.forEach((x) => arr.push(x)); return arr; }; export const scrollOptimize = (e: MouseEvent) => { const { currentTarget: editTool } = e; const editTools = editTool.parentElement; const { scrollHeight, childNodes, offsetHeight } = editTools!; let index = 0; childNodes.forEach((x, i) => { if (x === editTool) index = i; }); const scrollY = ((scrollHeight - offsetHeight) / (childNodes.length - 1)) * index; editTools!.scrollTo({ top: scrollY, behavior: "smooth" }); }; export const arrayToMap = ( keyName: keyof T, arr: T[] ): Map => { const resultMap = new Map(); for (const item of arr) { const keyValue = item[keyName]; if (keyValue !== undefined) { resultMap.set(keyValue, item); } } return resultMap; }; export const onWheelX: WheelEventHandler = (event) => { event.stopPropagation(); event.preventDefault(); const { currentTarget, deltaY } = event; currentTarget.scrollBy({ top: 0, left: deltaY, behavior: "auto", }); }; export const onWheelY: WheelEventHandler = (event) => { const { currentTarget, deltaY } = event; const { scrollHeight, clientHeight, scrollTop } = currentTarget; const deltaHeight = scrollHeight - clientHeight; const scrollMax = scrollTop > deltaHeight - 1 && scrollTop < deltaHeight + 1 && deltaY > 0; const scrollMin = scrollTop === 0 && deltaY < 0; // console.log(deltaY, scrollTop, scrollHeight - clientHeight); if (!(scrollMax || scrollMin)) { currentTarget.scrollBy({ top: deltaY, left: 0, behavior: "auto", }); event.stopPropagation(); event.preventDefault(); } }; const parser = new UAParser(navigator.userAgent); export const browserInfo = parser.getResult(); //{engine:{name:''}} // export function generateRandomColor(): string { // Generate a random hexadecimal color const hex = Math.floor(Math.random() * 16777216).toString(16); // Ensure the color has sufficient contrast relative to white const luminance = (parseInt(hex, 16) >> 16) * 0.299 + ((parseInt(hex, 16) >> 8) & 0xff) * 0.587 + (parseInt(hex, 16) & 0xff) * 0.114; const isLightColor = luminance > 128; // Determine if the color is light // Adjust the color to be darker if it's light, or lighter if it's dark const adjustedHex = isLightColor ? (parseInt(hex, 16) - 0x333333).toString(16) : (parseInt(hex, 16) + 0x333333).toString(16); // Ensure the color is 6 digits long const finalHex = adjustedHex.padStart(6, "0"); return `#${finalHex}`; } export async function base64ToFile(base64: string): Promise { const res = await fetch(base64); const blob = await res.blob(); return new File([blob], "image", { type: blob.type }); } export function fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result as string); reader.onerror = (error) => reject(error); }); } export function exportJson(content: string, filename: string) { const exportContent = (content: string) => { const link = document.createElement("a"); link.href = URL.createObjectURL( new Blob([content], { type: "application/json" }) ); link.download = filename; link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; readFileFromIndexedDB("image") .then((file) => { fileToBase64(file!).then( (image) => { try { const data = JSON.parse(content); const dataWithImage = { ...data, image }; const json = JSON.stringify(dataWithImage); exportContent(json); } catch (e) { exportContent(content); } }, () => { exportContent(content); } ); }) .catch((e) => { console.error(e); exportContent(content); }); } export function exportPNG(content: Blob, filename: string) { const link = document.createElement("a"); link.href = URL.createObjectURL(content); link.download = filename; link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } export function exportFile(content: string, filename: string, type: string) { const link = document.createElement("a"); link.href = URL.createObjectURL(new Blob([content], { type })); link.download = filename; link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } export function importFromFile() { const input = document.createElement("input"); const promise = parseSelectedFile(input); input.type = "file"; input.accept = ".json"; // document.body.appendChild(input); input.click(); return promise.then((data) => { // document.body.removeChild(input); console.log(data); return data; }); } function parseSelectedFile(input: HTMLInputElement): Promise { return new Promise((resolve, reject) => { input.addEventListener("change", () => { const file = input.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { try { const jsonContent = event.target?.result as string; resolve(jsonContent); } catch (error) { reject(error); } }; reader.readAsText(file); } else { reject(new Error("No file selected.")); } }); }); } export const stringifyData = (data: UserDataType) => { const { stations: stationsMap, lines: linesMap, title, backgroundColor, translateX, translateY, scale, opacity, } = data; const stations = mapToArr(stationsMap); const lines = mapToArr(linesMap); return JSON.stringify({ stations, lines, title, backgroundColor, translateX, translateY, scale, opacity, }); }; export const setLocalStorage = (data: UserDataType, callback: Function) => { const last = localStorage.getItem("current"); const current = localStorage.getItem("current"); const latest = stringifyData(data); if (latest !== current) { if (current) { localStorage.setItem("last", current); } localStorage.setItem("current", latest); callback(); } }; export const getBoundary = (data: UserDataType) => { const { stations } = data; const allStationsList = mapToArr(stations); let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; allStationsList.forEach((station) => { const { position } = station; const [x, y] = position; minX = Math.min(x, minX); minY = Math.min(y, minY); maxX = Math.max(x, maxX); maxY = Math.max(y, maxY); }); return { minX, minY, maxX, maxY }; }; export const mediateMap = ( data: UserDataType, { setScale, setTranslateX, setTranslateY }: TransformProps ) => { const { minX, minY, maxX, maxY } = getBoundary(data); const width = maxX - minX; const height = maxY - minY; const { innerHeight, innerWidth } = window; let scale = 1, transformX = -minX, transformY = -minY; if (innerWidth > innerHeight) { const margin = innerWidth * 0.05; scale = (innerWidth - margin) / width; transformX = margin / 2 - minX * scale; transformY = (innerHeight - height * scale) / 2 - minY * scale; } else { const margin = innerHeight * 0.05; scale = (innerHeight - margin) / height; transformY = margin / 2 - minY * scale; transformX = (innerWidth - width * scale) / 2 - minX * scale; } // small size map if (scale > 1) { scale = 1; transformX = (innerWidth - width) / 2 - minX; transformY = (innerHeight - height) / 2 - minY; } setScale(scale); setTranslateX(transformX); setTranslateY(transformY); }; export function importImage(): Promise { return new Promise((resolve, reject) => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.onchange = (event: Event) => { const target = event.target as HTMLInputElement; if (target.files && target.files.length > 0) { const file = target.files[0]; resolve(file); } else { resolve(null); } }; input.click(); }); } interface TransformValues { translateX: number; translateY: number; scale: number; } export function calculateTransform(file: File): Promise { return new Promise((resolve, reject) => { const img = new Image(); const reader = new FileReader(); reader.onload = (e) => { img.src = e.target?.result as string; }; reader.onerror = (e) => { reject(e); }; img.onload = () => { const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; const imgWidth = img.width; const imgHeight = img.height; const imgAspectRatio = imgWidth / imgHeight; const screenAspectRatio = screenWidth / screenHeight; let scale: number; if (imgAspectRatio > screenAspectRatio) { // Image is wider than the screen scale = (screenWidth * 0.8) / imgWidth; } else { // Image is taller than the screen scale = (screenHeight * 0.8) / imgHeight; } const scaledWidth = imgWidth * scale; const scaledHeight = imgHeight * scale; const translateX = (screenWidth - scaledWidth) / 2; const translateY = (screenHeight - scaledHeight) / 2; resolve({ translateX, translateY, scale }); }; reader.readAsDataURL(file); }); } export function storeFileInIndexedDB( file: File, customFileName: string ): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open("FileDatabase", 1); request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains("files")) { db.createObjectStore("files", { keyPath: "name" }); } }; request.onsuccess = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains("files")) { db.close(); indexedDB.deleteDatabase("FileDatabase").onsuccess = () => { storeFileInIndexedDB(file, customFileName).then(resolve).catch(reject); }; } else { const transaction = db.transaction("files", "readwrite"); const objectStore = transaction.objectStore("files"); const fileRecord = { name: customFileName, content: file, }; const addRequest = objectStore.put(fileRecord); addRequest.onsuccess = () => { resolve(); }; addRequest.onerror = (event) => { reject((event.target as IDBRequest).error); }; } }; request.onerror = (event) => { reject((event.target as IDBRequest).error); }; }); } export function readFileFromIndexedDB(fileName: string): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open("FileDatabase", 1); request.onsuccess = (event) => { try { const db = (event.target as IDBOpenDBRequest).result; const transaction = db.transaction("files", "readonly"); const objectStore = transaction.objectStore("files"); const getRequest = objectStore.get(fileName); getRequest.onsuccess = (event) => { const result = (event.target as IDBRequest).result; if (result) { resolve(result.content); } else { resolve(null); } }; getRequest.onerror = (event) => { reject((event.target as IDBRequest).error); }; } catch (e) { console.error(e); reject(e); } }; request.onerror = (event) => { reject((event.target as IDBRequest).error); }; }); } export function deleteFileFromIndexedDB(fileName: string): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open("FileDatabase", 1); request.onsuccess = (event) => { const db = (event.target as IDBOpenDBRequest).result; const transaction = db.transaction("files", "readwrite"); const objectStore = transaction.objectStore("files"); const deleteRequest = objectStore.delete(fileName); deleteRequest.onsuccess = () => { resolve(); }; deleteRequest.onerror = (event) => { reject((event.target as IDBRequest).error); }; }; request.onerror = (event) => { reject((event.target as IDBRequest).error); }; }); } ================================================ FILE: src/Data/Shape.ts ================================================ export const Shape = { 'cicle': '圆形', // typo 'square': '正方形', 'triangle': '三角形', 'start': '五角星', // typo 'pentagon': '五边形', 'hexagon': '六边形', 'cross': '十字形', 'rhombus': '菱形', 'diamond': '钻石', 'leaf': '叶子', 'ginkgo': '银杏' }; ================================================ FILE: src/Data/UserData.ts ================================================ import { Dispatch, SetStateAction } from "react"; import { base64ToFile, generateRandomColor, mapToArr } from "../Common/util"; import { colorSH } from "../Common/color"; import { Direct } from "../DataStructure/Direction"; import { teyvat } from "../Common/teyvat"; import i18n from "../i18n/config"; export class StationProps { stationId!: number; stationName!: string; position!: number[]; shape!: string; lineIds!: number[]; tagDirection?: Direct; } export class LineProps { lineId!: number; lineName!: string; color!: string; stationIds!: number[]; sign!: string; order!: number; bendFirst!: boolean[]; subLine?: boolean; } export type ChangeSteps = { fromX: number; fromY: number; toX: number; toY: number; stationId: number; }; export type LineChanges = { stationId: number; lineId: number; stationIndex: number; }; export type RecordType = StationProps[] | ChangeSteps[] | LineChanges[]; export class CardShowing { constructor() { this.stationIds = []; this.lineIds = []; } stationIds?: number[]; lineIds?: number[]; stationFirst?: boolean; } export type InsertInfo = { insertIndex: number; line: LineProps }; export class UserDataType { stations!: Map; lines!: Map; title?: string; backgroundColor?: string; backgroundImage?: File; opacity?: number; translateX?: number; translateY?: number; scale?: number; } export type ShowNameProps = { showName: boolean; setShowName: React.Dispatch>; autoHiddenName: boolean; setAutoHiddenName: React.Dispatch>; }; export type DrawProps = { drawing: boolean; setDrawing: React.Dispatch>; }; export type DrawerSize = { drawerX: number; drawerY: number; }; export type TransformProps = { translateX: number; setTranslateX: React.Dispatch>; translateY: number; setTranslateY: React.Dispatch>; scale: number; setScale: React.Dispatch>; }; export type ShowTourProps = { showTour: boolean; setShowTour: React.Dispatch>; }; export type PageProps = { page: string; setPage: React.Dispatch>; }; export const initData = { ...teyvat, }; export const setDataFromJson = ( setData: Dispatch>, jsonString: string ) => { const res = JSON.parse(jsonString); const { stations: stationsArr, lines: linesArr, title, backgroundColor, translateX, translateY, scale, opacity, image, }: { stations: StationProps[]; lines: LineProps[]; title: string; backgroundColor: string; translateX: number; translateY: number; scale: number; opacity: number; image: string; } = res; const stations = stationsArr.reduce((map, cur) => { map.set(cur.stationId, cur); return map; }, new Map()); const lines = linesArr.reduce((map, cur) => { map.set(cur.lineId, cur); return map; }, new Map()); const data = { stations, lines, title, backgroundColor, translateX, translateY, scale, opacity, }; setData(data); if (image) base64ToFile(image) .then((backgroundImage) => { setData({ ...data, backgroundImage }); }) .catch((e) => { console.error(e); }); return data; }; export const deleteStation = ( data: UserDataType, setData: Dispatch>, stationId: number ) => { const { stations, lines } = data; const station = stations.get(stationId); const { lineIds } = station!; //detele stations in line lineIds.forEach((lineId) => { const newStationIds: number[] = []; const newBendFirst: boolean[] = []; const line = lines.get(lineId); const { stationIds, bendFirst } = line!; stationIds.forEach((id, index) => { if (stationId !== id) { // break the cicle if (id !== newStationIds[newStationIds.length - 1]) { newStationIds.push(id); newBendFirst.push(bendFirst[index]); } } }); lines.set(lineId, { ...line!, stationIds: newStationIds, bendFirst: newBendFirst, }); }); // delete station stations.delete(stationId); setData({ ...data }); }; export const deleteLine = ( data: UserDataType, setData: Dispatch>, lineId: number ) => { const { stations, lines } = data; const line = lines.get(lineId); const { stationIds } = line!; //detele stations in line stationIds.forEach((stationId) => { const newLineIds: number[] = []; const station = stations.get(stationId); const { lineIds } = station!; lineIds.forEach((id) => { if (lineId !== id) { newLineIds.push(id); } }); stations.set(stationId, { ...station!, lineIds: newLineIds, }); }); // delete line lines.delete(lineId); setData({ ...data }); }; export const dataProcessor = ( id: number, setData: Dispatch>, state: UserDataType ) => { const { stations, lines } = state; return { setStationName: (name: string) => { setData((state) => { const station = stations.get(id); station!.stationName = name; return { ...state }; }); }, setStationPosition: (x: number, y: number) => { setData((state) => { const station = stations.get(id); station!.position = [x, y].map((x) => Number.isNaN(x) ? 0 : Math.round(x) ); return { ...state }; }); }, setStationShape: (shape: string) => { setData((state) => { const station = stations.get(id); station!.shape = shape; return { ...state }; }); }, setStationTagDirection: (direct: Direct) => { setData((state) => { const station = stations.get(id); station!.tagDirection = direct; if (direct === 8) delete station!.tagDirection; return { ...state }; }); }, getStationById: (stationId: string | number) => { return stations.get(stationId); }, getLineById: (lineId: string | number) => { return lines.get(lineId); }, getStationsInThisLine: () => { return lines.get(id)!.stationIds.map((x) => stations.get(x)); }, setLineName: (name: string) => { setData((state) => { const line = lines.get(id); line!.lineName = name; return { ...state }; }); }, setSign: (sign: string) => { setData((state) => { const line = lines.get(id); line!.sign = sign; return { ...state }; }); }, setOrder: (order: number) => { setData((state) => { const line = lines.get(id); line!.order = order; return { ...state }; }); }, setColor: (color: string) => { setData((state) => { const line = lines.get(id); line!.color = color; return { ...state }; }); }, setSubLine: (subLine: boolean) => { setData((state) => { const line = lines.get(id); line!.subLine = subLine; if (!subLine) delete line!.subLine; return { ...state }; }); }, getBendFirst: (stationIndex: number) => { const line = lines.get(id); return line?.bendFirst[stationIndex]; }, setBendFirst: (stationIndex: number, bendFirst: boolean) => { setData((state) => { const line = lines.get(id); line!.bendFirst[stationIndex] = bendFirst; return { ...state }; }); }, deleteStation: () => deleteStation(state, setData, id), deleteLine: () => deleteLine(state, setData, id), removeStationFromLine: (lineId: number, stationIndex: number) => { setData((state) => { const station = stations.get(id); const line = lines.get(lineId); const { stationIds, bendFirst } = line!; if (stationIds[stationIndex] === id) { stationIds.splice(stationIndex, 1); bendFirst.splice(stationIndex, 1); if (!stationIds.some((stationId) => stationId === id)) { const { lineIds } = station!; station!.lineIds = lineIds.filter((id) => lineId !== id); } if (stationIds[stationIndex - 1] === stationIds[stationIndex]) { stationIds.splice(stationIndex, 1); bendFirst.splice(stationIndex, 1); } } return { ...state }; }); }, addNewLine: () => { const newLine = new LineProps(); setData((state) => { const maxId = mapToArr(lines).reduce( (pre, cur) => Math.max(pre, cur.lineId), 0 ); const lineId = maxId + 1; Object.assign(newLine, { lineId: lineId, lineName:i18n.t("lineNo",{lineId}), color: colorSH[lineId - 1] ? colorSH[lineId - 1].color : generateRandomColor(), stationIds: [id], sign: lineId.toString(), order: lineId, bendFirst: [true], }); const station = stations.get(id); const { lineIds } = station!; station!.lineIds = [...new Set(lineIds.concat([lineId]))]; lines.set(lineId, newLine); return { ...state }; }); return newLine; }, addStationToLine: (stationId: number, stationIndex: number) => { return new Promise(res=>{ setData((state) => { const line = lines.get(id); if (!line) debugger; const { stationIds, bendFirst } = line!; console.log(structuredClone(stationIds)); if ( stationIds[stationIndex] !== stationId && stationIds[stationIndex - 1] !== stationId ) { stationIds.splice(stationIndex, 0, stationId); bendFirst.splice(stationIndex, 0, true); if(stationIds.some((x,index)=>x===stationIds[index-1]||x===stationIds[index+1])) debugger const station = stations.get(stationId); const { lineIds } = station!; station!.lineIds = [...new Set(lineIds.concat([id]))]; res(true); } else { res(false); } return { ...state }; }); }); }, }; }; export const addNewStation = ( data: UserDataType, setData: Dispatch>, x: number, y: number, record: StationProps[], setRecord: React.Dispatch>, currentRecordIndex: number, setCurrentRecordIndex: React.Dispatch>, cardShowing: CardShowing, setCardShowing: Dispatch>, defaultShape: string, ) => { const { stations } = data; let max = 0; stations.forEach((station) => { max = Math.max(station.stationId, max); }); const stationId = max + 1; const newStation = { stationId, stationName: `${i18n.t('newStation')} ${max + 1}`, position: [x, y].map(Math.round), shape: defaultShape, lineIds: [], }; setCardShowing({ stationIds: [stationId] }); const newRecord = record.slice(0, currentRecordIndex + 1); setRecord(newRecord.concat([newStation])); setCurrentRecordIndex(currentRecordIndex + 1); stations.set(max + 1, newStation); setData({ ...data }); }; export const addStationFromRecord = ( data: UserDataType, setData: Dispatch>, station: StationProps ) => { const { stations } = data; let max = 0; stations.forEach((station) => { max = Math.max(station.stationId, max); }); const newStation = { ...station, stationId: max + 1 }; stations.set(max + 1, newStation); setData({ ...data }); }; ================================================ FILE: src/DataStructure/Bend.ts ================================================ import { Rail } from "./Rail"; import { Track } from "./Track"; export class Bend { // filter empty indexes static round1(track: Track) { const emptyRails = track.getEmptyRails().map((rail) => rail.index); if (emptyRails.length) return emptyRails; return [0, 1, 2]; } // filter opposite rails static round2(track: Track, round1Indexes: number[], rail?: Rail) { // rail not exist or in/out rail direction not just opposite to the candidate rail direction if (!rail || !rail.track.direction.oppositeTo(track.direction)) return round1Indexes; return round1Indexes.filter( (railIndex) => rail.oppositeIndex() === railIndex ); } // filter center rail static round3(round2Indexes: number[]) { if (round2Indexes.find((railIndex) => railIndex === 1)) return 1; } // filter most close rail static round4(track: Track, rail?: Rail) { if (!rail) return 0; return rail.track.direction.rotationTo(track.direction) > 0 ? 0 : 2; } static getBestRailIndex(track: Track, rail?: Rail) { const round1Indexes = this.round1(track); if (round1Indexes.length === 1) return round1Indexes[0]; const round2Indexes = this.round2(track, round1Indexes, rail); if (round2Indexes.length === 1) return round2Indexes[0]; const round3Res = this.round3(round2Indexes); if (round3Res === 1) return 1; const round4Res = this.round4(track, rail); return round4Res; } } ================================================ FILE: src/DataStructure/ConnectType.ts ================================================ export enum ConnectType { straight, straightFirst, bendFirst } ================================================ FILE: src/DataStructure/Direction.ts ================================================ //clockwise export enum Direct { up, upRight, right, rightDown, down, downLeft, left, leftUp, // Quadrant I bisected by a diagonal line // upRightA means the upper half // upRightB means the lower half // all the directions is marked in clockwise upRightA, upRightB, rightDownA, rightDownB, downLeftA, downLeftB, leftUpA, leftUpB, coincide, } export class Direction { direct: Direct; standard: boolean; lean: boolean; constructor(direct: Direct) { this.direct = direct; this.standard = direct < 8; this.lean = direct % 2 === 1; } delta(direct: Direct){ const diff = Math.abs(direct - this.direct); return Math.min(diff, 8-diff); } opposite() { if (this.direct < 8) return new Direction((this.direct + 4) % 8); if (this.direct >= 8 && this.direct < 12) return new Direction(this.direct + 4); if (this.direct >= 12 && this.direct < 16) return new Direction(this.direct - 4); return new Direction(this.direct); } oppositeTo(direction: Direction | undefined) { if (direction) return direction.opposite().direct === this.direct; throw new Error("no direction!"); } sameTo(direction: Direction) { return this.direct === direction.direct; } // how many rotation should this direction do to coincide to given direction // 1 2 3 means clockwise rotate 1 2 3 times // -1 -2 -3 means counterclockwise // 0 means no need rotate // 4 means opposite direction rotationTo(direction: Direction) { if (this.oppositeTo(direction)) return 4; const side = direction.direct - this.direct; if (side < -4) return side + 8; if (side > 4) return side - 8; return side; } getBendSteps(bendFirst: boolean) { if (this.direct < 8 || this.direct > 15) { throw new Error("this is not bend direction"); } const firstStep = new Direction(this.direct - 7 - (this.direct % 2)); const secondStep = new Direction((this.direct - 8 + (this.direct % 2)) % 8); return bendFirst ? [firstStep, secondStep] : [secondStep, firstStep]; } } export const DirectionVictor = [ [0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1], ]; export const DirectionVictorReverseY = DirectionVictor.map(([x,y])=>[x,-y]); ================================================ FILE: src/DataStructure/Display.ts ================================================ import { Station } from "./Station"; export class DisplayStation { stationName: string; bendFirst: boolean; constructor(stationName: string, bendFirst: boolean){ this.stationName = stationName; this.bendFirst = bendFirst; } } ================================================ FILE: src/DataStructure/Line.ts ================================================ import { LineProps } from "../Data/UserData"; import { Bend } from "./Bend"; import { ConnectType } from "./ConnectType"; import { LineRecord } from "./LineRecord"; import { Rail } from "./Rail"; import { RailPair } from "./RailPair"; import { Station } from "./Station"; import { Straight } from "./Straight"; import { Vector } from "./Vector"; export class Line { empty: boolean; departureRecord: LineRecord | undefined; _dev_tag: string | undefined; displayLine?: LineProps; constructor() { this.empty = false; } getTerminalRecord(){ let p = this.departureRecord; while(p?.nextLineRecord){ p = p.nextLineRecord; if(p === this.departureRecord || !p.nextLineRecord) return p; } return p; } linkAll(stations: Station[]){ stations.reduce((pre,cur)=>{ this.link(pre,cur); return cur; }); } // connect B station and C station link(B: Station, C: Station, bendFirst: boolean = true) { const railPair = this.applyBestRailPair(B,C,bendFirst); railPair.setLine(this); let bLineRecord = B.getJoint(this); if (!bLineRecord) { //if record not exist, add one bLineRecord = new LineRecord(B, this); // register cLineRecord in C station B.addLineRecord(bLineRecord); this.departureRecord = bLineRecord; } let cLineRecord// = C.getJoint(this); if (!cLineRecord) { cLineRecord = new LineRecord(C, this); // register cLineRecord in C station C.addLineRecord(cLineRecord); } // establish doubly linked list bLineRecord?.establishConnectionTo(cLineRecord); // update rail and connect type for B and C LineRecord.updateLineRecords(bLineRecord, cLineRecord, railPair); } applyBestRailPair(B: Station, C: Station, bendFirst: boolean) { const direction = new Vector(B.position, C.position); if (direction.standard) { const bOutIndex = Straight.getBestRailIndex(B, C, this); const cInIndex = Rail.oppositeIndex(bOutIndex); // if(B.displayStation?.stationName==='风起地站'&&C.displayStation?.stationName==='达达乌帕谷') debugger const bTrack = B.getTrack(direction); const cTrack = C.getTrack(direction.opposite()); const bRail = bTrack.getAvailableRail(bOutIndex); const cRail = cTrack.getAvailableRail(cInIndex); return new RailPair(bRail, cRail); } else { const [bOutDirection, cInDirectionOpposite] = direction.getBendSteps(bendFirst); const cInDirection = cInDirectionOpposite.opposite(); const bTrack = B.getTrack(bOutDirection); const cTrack = C.getTrack(cInDirection); const bLastRail = B.getJoint(this)?.lastRail; const cNextRail = C.getJoint(this)?.nextRail; const bOutIndex = Bend.getBestRailIndex(bTrack, bLastRail); const cInIndex = Bend.getBestRailIndex(cTrack, cNextRail); const bRail = bTrack.getAvailableRail(bOutIndex); const cRail = cTrack.getAvailableRail(cInIndex); return new RailPair(bRail, cRail); } } } export class EmptyLine extends Line { constructor() { super(); this.empty = true; } } ================================================ FILE: src/DataStructure/LineRecord.ts ================================================ import { ConnectType } from "./ConnectType"; import { Line } from "./Line"; import { Rail } from "./Rail"; import { RailPair } from "./RailPair"; import { Station } from "./Station"; export class LineRecord { station: Station; line: Line | undefined; lastLineRecord: LineRecord | undefined; nextLineRecord: LineRecord | undefined; lastRail: Rail | undefined; nextRail: Rail | undefined; constructor(station: Station, line?: Line) { this.station = station; if (line) { this.line = line; } } getInDirection(){ return this.lastRail?.track.direction; } getOutDirection(){ return this.nextRail?.track.direction; } establishConnectionTo(BRecord: LineRecord) { LineRecord.establishConnection(this, BRecord); } static establishConnection(ARecord: LineRecord, BRecord: LineRecord) { ARecord.nextLineRecord = BRecord; BRecord.lastLineRecord = ARecord; } // update the rail and connect type information for connectting 2 linerecords static updateLineRecords( ARecord: LineRecord, BRecord: LineRecord, railPair: RailPair, ) { const { departureRail, arrivalRail } = railPair; ARecord.nextRail = departureRail; BRecord.lastRail = arrivalRail; } } ================================================ FILE: src/DataStructure/Mode.ts ================================================ export enum Mode { normal, moving, touchMoving, touchScaling } export enum FunctionMode{ normal, addingStation, dragingStation, lineEditing, backgroundEditing, customBackground, editingCustomBackgroundPosition, selectingStation, choosingExistMap, } ================================================ FILE: src/DataStructure/Point.ts ================================================ import { MouseEvent, Touch } from "react"; import { Straight } from "./Straight"; export class Point { x: number; y: number; yReversed: boolean; q_start: boolean; q: boolean; q_end: boolean; constructor(x: number = 0, y: number = 0, yReversed: boolean = false) { this.x = x; this.y = y; this.yReversed = yReversed; this.q_start = false; this.q = false; this.q_end = false; } round(){ return new Point(Math.round(this.x), Math.round(this.y)) } offset(A: Point) { return new Point(A.x + this.x, A.y + this.y); } displacementTo(A: Point) { return new Point(this.x - A.x, this.y - A.y); } distanceTo(A: Point) { return Math.sqrt(Math.pow(this.x - A.x, 2) + Math.pow(this.y - A.y, 2)); } sameTo(A: Point) { return A.x === this.x && A.y === this.y && A.yReversed === this.yReversed; } reverseY() { return new Point(this.x, -this.y, !this.yReversed); } static getPointFromTouch(A: Touch) { return new Point(A.clientX, A.clientY); } static getPointFromMouse(A: MouseEvent) { return new Point(A.clientX, A.clientY); } static getMidPoint(A: Point, B: Point) { return new Point((A.x + B.x) / 2, (A.y + B.y) / 2); } static getDisplacement(A: Point, B: Point) { return new Point(A.x - B.x, A.y - B.y); } } ================================================ FILE: src/DataStructure/Rail.ts ================================================ import { EmptyLine, Line } from "./Line"; import { RailPair } from "./RailPair"; import { Track } from "./Track"; export class Rail { track: Track; index: number; line: Line; extra: boolean; constructor(track: Track, index: number) { this.track = track; this.index = index; this.line = new EmptyLine(); this.extra = false; } oppositeIndex(){ return 2 - this.index; } setLine(line: Line){ this.line = line; } static getStraightConnectRailPair(aEmptyRails: Rail[], bEmptyRails: Rail[]) { if (aEmptyRails.length === 0 || bEmptyRails.length === 0) return; const railPairs: RailPair[] = []; aEmptyRails.forEach((aRail) => { bEmptyRails.forEach((bRail) => { //found just opposite rail if (aRail.index + bRail.index === 2) { railPairs.push(new RailPair(aRail,bRail)); } }); }); return railPairs; } static getBestRail(rails: Rail[]){ return rails.find(rail=>rail.index === 1) || rails[0]; } static getRailByIndex(rails: Rail[], index: number){ return rails.find(rail=>rail.index === index); } static oppositeIndex(index: number){ return 2 - index; } } export class ExtraRail extends Rail{ constructor(track: Track, index: number){ super(track, index); this.extra = true; } } ================================================ FILE: src/DataStructure/RailPair.ts ================================================ import { Direction } from "./Direction"; import { Line } from "./Line"; import { Rail } from "./Rail"; import { Station } from "./Station"; export class RailPair { departureRail: Rail; arrivalRail: Rail; center: boolean; constructor(departureRail: Rail, arrivalRail: Rail) { this.departureRail = departureRail; this.arrivalRail = arrivalRail; this.center = departureRail.index === 1; } reverse(){ const temp = this.departureRail; this.departureRail = this.arrivalRail; this.arrivalRail = temp; } setLine(line: Line){ this.departureRail.setLine(line); this.arrivalRail.setLine(line); } } ================================================ FILE: src/DataStructure/Station.ts ================================================ import { StationProps } from "../Data/UserData"; import { Direct, Direction } from "./Direction"; import { Line } from "./Line"; import { LineRecord } from "./LineRecord"; import { Point } from "./Point"; import { Rail } from "./Rail"; import { Track } from "./Track"; export class Station { position: Point; tracks: Track[]; lineRecords: Map; _dev_tag: string | undefined; handlers: (Line | undefined | null)[]; displayStation?: StationProps; constructor(position: Point) { this.position = position; this.tracks = new Array(8) .fill(true) .map((x, direct: Direct) => new Track(this, new Direction(direct))); this.lineRecords = new Map(); this.handlers = new Array(8); } lineCount(){ const set = new Set(); this.lineRecords.forEach(line=>{ set.add(line); }) return set.size; } isEmpty(direct: Direct){ return !this.handlers[direct] && this.tracks[direct].isEmpty(); } getBestDirectionForName() { let space = 0, endIndex = 0, maxSpace = 0, firstSpace; for (let i = 0; i < 8; i++) { const empty = !this.handlers[i] && this.tracks[i].isEmpty(); if (empty) { space++; if (space > maxSpace) { maxSpace = space; endIndex = i; } } else { if (firstSpace === undefined) firstSpace = space; space = 0; } } // all empty if (space === 8 || maxSpace===0) { return Direct.right; } if(maxSpace <= 2) { if(this.isEmpty(2)) return 2; if(this.isEmpty(6)) return 6; if(this.isEmpty(1)) return 1; if(this.isEmpty(3)) return 3; if(this.isEmpty(5)) return 5; if(this.isEmpty(7)) return 7; } if(firstSpace === undefined) firstSpace =0; if (space + firstSpace >= maxSpace) { // max space containing 0 if (firstSpace >= space) { return Math.floor((firstSpace - space) / 2); } else if (space > firstSpace) { return 7 - Math.floor((space - firstSpace) / 2); } } const best = endIndex - Math.floor(maxSpace / 2) // max space not containing 0 if(maxSpace%2===0){ if(best%2===0){ return best+1; } } return best; } getBestDirectionForName2(){ } addLineRecord(lineRecord: LineRecord) { const { line, station } = lineRecord; if (!line) { throw new Error("line is undefined while add line record to statation"); } if (station !== this) { throw new Error( "you are adding a line record which not belongs to this station" ); } // this lineRecords is the array saving linerecords const lineRecordsArr = this.lineRecords.get(line) || []; lineRecordsArr.push(lineRecord); // this.lineRecords is the map Line=>Line // containing all the information of the lines go through this station this.lineRecords.set(line, lineRecordsArr); } getTrack(direction: Direction) { return this.tracks[direction.direct]; } getRail(direction: Direction, index: number) { return this.getTrack(direction).getRail(index); } // getBestRail(direction: Direction){ // return this.getTrack(direction).getBestRail(); // } // find the start or end of the Line // if no records find, this station must be the departure station // if both start and end exist, this station is the loop line joint getJoint(line: Line) { const terminal = this.lineRecords .get(line) ?.find((lineRecord) => !lineRecord.lastLineRecord); const departure = this.lineRecords .get(line) ?.find((lineRecord) => !lineRecord.nextLineRecord); return departure || terminal; } } ================================================ FILE: src/DataStructure/Straight.ts ================================================ import { Direction } from "./Direction"; import { Line } from "./Line"; import { Point } from "./Point"; import { Rail } from "./Rail"; import { Station } from "./Station"; import { Track } from "./Track"; import { Vector } from "./Vector"; export class Straight { // round 1 : filter empty rails static round1(B: Station, C: Station) { const direction = new Vector(B.position, C.position); const bTrack = B.getTrack(direction); const cTrack = C.getTrack(direction.opposite()); const initScores = [0, 1, 2]; let max = 0; const round1Indexes = initScores .map((index) => { let score = 0; // if rail in bTrack is empty, this rail add one score if (bTrack.rails[index].line.empty) { score++; } // if rail in cTrack is empty, this rail add one score if (cTrack.rails[Rail.oppositeIndex(index)].line.empty) { score++; } // record the highest score if (score > max) { max = score; } return { index, score }; }) .filter(({ score }) => { // find the highest score indexes return score === max; }); return round1Indexes; } // round 2 : filter straight pass rails static round2( B: Station, C: Station, round1Indexes: RoundResult[], line: Line ) { const direction = new Vector(B.position, C.position); const bLineRecord = B.getJoint(line); const cLineRecord = C.getJoint(line); let max = 0; const round2Indexes = round1Indexes .map(({ index }) => { let score = 0; if ( bLineRecord?.lastRail?.track.direction.oppositeTo(direction) && bLineRecord?.lastRail?.oppositeIndex() === index ) { score++; } if ( cLineRecord?.nextRail?.track.direction.sameTo(direction) && cLineRecord?.nextRail?.index === index ) { score++; } if (score > max) { max = score; } return { index, score }; }) .filter(({ score }) => { // find the highest score indexes return score === max; }); return round2Indexes; } // round 3 : filter center rails static round3(B: Station, C: Station, round2Indexes: RoundResult[]) { const round3Res = round2Indexes.find(({ index }) => index === 1); return round3Res ? 1 : 0; } // round 4 : filter most close to the last or next rail static round4(B: Station, C: Station, line: Line) { const direction = new Vector(B.position, C.position); const bLineRecord = B.getJoint(line); const cLineRecord = C.getJoint(line); let zeroRailScore = 0; let secondRailScore = 0; if (bLineRecord?.lastRail) if (bLineRecord.lastRail.track.direction.rotationTo(direction) > 0) zeroRailScore++; else secondRailScore++; if (cLineRecord?.nextRail) if (cLineRecord.nextRail.track.direction.rotationTo(direction) > 0) zeroRailScore++; else secondRailScore++; return zeroRailScore > secondRailScore ? 0 : 2; } static getBestRailIndex(B: Station, C: Station, line: Line) { if (B._dev_tag === "B" && line._dev_tag === "line8") { // debugger; } // round 1: const round1Indexes = Straight.round1(B, C); if (round1Indexes.length === 1) { return round1Indexes[0].index; } // round 2: const round2Indexes = Straight.round2(B, C, round1Indexes, line); if (round2Indexes.length === 1) { return round2Indexes[0].index; } // round 3: const round3Index = Straight.round3(B, C, round2Indexes); if (round3Index === 1) { return round3Index; } // round 4: const round4Index = Straight.round4(B, C, line); return round4Index; } } class RoundResult { score!: number; index!: number; } ================================================ FILE: src/DataStructure/Track.ts ================================================ import { Direction } from "./Direction"; import { ExtraRail, Rail } from "./Rail"; import { Station } from "./Station"; export class Track { station: Station; direction: Direction; rails: Rail[]; // extra rail won't occupy the space of rail // extraRails save extraRail by index, every index has an array extraRails: ExtraRail[][]; constructor(station: Station, direction: Direction) { this.station = station; this.direction = direction; this.rails = new Array(3) .fill(true) .map((x, index) => new Rail(this, index)); this.extraRails = new Array(3).fill(true).map((x, index) => new Array()); } isEmpty(){ return !this.rails.some(rail=>!rail.line.empty) } getEmptyRails() { return this.rails.filter((rail) => rail.line.empty); } getRail(index: number) { return this.rails[index]; } applyExtraRail(index: number) { const extraRail = new ExtraRail(this, index); this.extraRails[index].push(extraRail); return extraRail; } getAvailableRail(index: number) { const rail = this.getRail(index); return rail.line.empty ? rail : this.applyExtraRail(index); } } ================================================ FILE: src/DataStructure/Vector.ts ================================================ import { Direct, Direction, DirectionVictorReverseY } from "./Direction"; import { Point } from "./Point"; export class Vector extends Direction { start: Point; // startPoint end: Point; // endPoint constructor(start: Point, end: Point) { super(Vector.getDirection(start, end)); this.start = start; this.end = end; } verticalProlong(length: number) { const A = this.start, B = this.end; if (A.x === B.x) return [new Point(B.x + length, B.y), new Point(B.x - length, B.y)]; const diffSign = (A.xB.x&&A.y>B.y); const tan = (B.y - A.y) / (B.x - A.x); const alpha = Math.abs(Math.atan(tan)); const sin = Math.sin(alpha); const cos = Math.cos(alpha); if(diffSign) return [ new Point(B.x - sin * length, B.y + cos * length), new Point(B.x + sin * length, B.y - cos * length), ]; return [ new Point(B.x - sin * length, B.y - cos * length), new Point(B.x + sin * length, B.y + cos * length), ]; } prolong(length: number) { const A = this.start, B = this.end; const kX = A.x < B.x ? 1 : -1; const kY = A.y < B.y ? 1 : -1; if (A.x === B.x) return new Point(A.x, B.y + kY * length); const tan = (B.y - A.y) / (B.x - A.x); const alpha = Math.abs(Math.atan(tan)); const sin = Math.sin(alpha); const cos = Math.cos(alpha); return new Point(B.x + kX * cos * length, B.y + kY * sin * length); } normalize(k: number = 1) { const deltaX = this.end.x - this.start.x; const deltaY = this.end.y - this.start.y; const module = Math.sqrt( Math.pow(deltaX, 2) + Math.pow(deltaY, 2) ); return new Vector( new Point(0, 0), new Point((k * deltaX) / module, (k * deltaY) / module) ); } passesThroughPoint(A: Point) { return ( (A.x - this.start.x) * (A.y - this.end.y) === (A.y - this.start.y) * (A.x - this.end.x) ); } round() { return new Vector(this.start.round(), this.end.round()); } passesThroughPointRound(point: Point) { const A = point.round(); const vector = this.round(); return this.passesThroughPoint.bind(vector)(A); } getCrossPointTo(b: Vector) { const a = this; const A = a.start, B = a.end, C = b.start, D = b.end; const m = (A.x - B.x) / (A.y - B.y); const n = (C.x - D.x) / (C.y - D.y); // if(!Number.isFinite(m)) return new Point(A.x, (A.x - C.x) / n + C.y); // if(!Number.isFinite(n)) return new Point(C.x, (C.x - A.x) / m + A.y); if (!Number.isFinite(m)) return new Point(n * (A.y - C.y) + C.x, A.y); if (!Number.isFinite(n)) return new Point(m * (C.y - A.y) + A.x, C.y); const y = (C.x - A.x + m * A.y - n * C.y) / (m - n); const x = n * (y - C.y) + C.x; return new Point(x, y); } static getVectorByPointAndDirection(A: Point, direction: Direction) { const [x, y] = DirectionVictorReverseY[direction.direct]; return new Vector(A, new Point(A.x + x, A.y + y)); } static getDirection(start: Point, end: Point) { const A = start.reverseY(); const B = end.reverseY(); if (A.x === B.x && A.y === B.y) return Direct.coincide; if (A.x === B.x) return B.y > A.y ? Direct.up : Direct.down; if (A.y === B.y) return B.x > A.x ? Direct.right : Direct.left; const deltaX = B.x - A.x; const deltaY = B.y - A.y; if (deltaX > 0 && deltaY > 0) if (deltaX === deltaY) return Direct.upRight; else if (deltaX < deltaY) return Direct.upRightA; else return Direct.upRightB; if (deltaX > 0 && deltaY < 0) if (deltaX === -deltaY) return Direct.rightDown; else if (deltaX > -deltaY) return Direct.rightDownA; else return Direct.rightDownB; if (deltaX < 0 && deltaY < 0) if (-deltaX === -deltaY) return Direct.downLeft; else if (-deltaX < -deltaY) return Direct.downLeftA; else return Direct.downLeftB; if (deltaX < 0 && deltaY > 0) if (-deltaX === deltaY) return Direct.leftUp; else if (-deltaX > deltaY) return Direct.leftUpA; else return Direct.leftUpB; debugger throw Error("error happend when getting direction"); } } ================================================ FILE: src/Entrance/App.scss ================================================ body{ margin: 0; touch-action: none; overscroll-behavior: none; } html{ touch-action: none; overscroll-behavior: none; } .App{ height: 100vh; width: 100vw; overflow: hidden; user-select: none; } *{ font-family: 'PingFang SC'; -webkit-tap-highlight-color: transparent; } ================================================ FILE: src/Entrance/App.test.tsx ================================================ import { render, screen } from '@testing-library/react'; import App from './App'; import React from 'react'; import { Direct, Direction } from '../DataStructure/Direction'; import { Track } from '../DataStructure/Track'; import { Station } from '../DataStructure/Station'; import { Point } from '../DataStructure/Point'; import { EmptyLine, Line } from '../DataStructure/Line'; import { Vector } from '../DataStructure/Vector'; test('renders learn react link', () => { render(); const linkElement = screen; const direction = new Direction(Direct.up); const station = new Station(new Point(1,2)); const track = new Track(station,new Direction(Direct.up)); const A = new Point(1,2); const B = new Point(3,4); const vector = new Vector(A,B); console.log(station.getTrack(vector).direction.direct) console.log(direction) expect(linkElement).toBeDefined(); }); test('vector cross pointer',()=>{ const a = new Vector(new Point(0,0), new Point(1,1)); const b = new Vector(new Point(0,5), new Point(5,0)); const abCrossPoint = a.getCrossPointTo(b); console.log(abCrossPoint); expect(abCrossPoint.x).toEqual(2.5); expect(abCrossPoint.y).toEqual(2.5); }) ================================================ FILE: src/Entrance/App.tsx ================================================ import '../i18n/config'; import { ErrorBoundary } from "react-error-boundary"; import { browserInfo, mapToArr, mediateMap, readFileFromIndexedDB, setLocalStorage, } from "../Common/util"; import { CardShowing, ChangeSteps, InsertInfo, LineChanges, LineProps, RecordType, StationProps, UserDataType, initData, setDataFromJson, } from "../Data/UserData"; import { FunctionMode, Mode } from "../DataStructure/Mode"; import { Cards } from "../Render/Card/Cards"; import { DeleteConfirmation, showConfirmationInterface, } from "../Render/Delete/DeleteConfirmation"; import { Menu } from "../Render/Header/Menu"; import ScaleLayer from "../Render/Layer/ScaleLayer"; import { WelcomeTour } from "../WelcomeTour/WelcomeTour"; import "./App.scss"; import "driver.js/dist/driver.css"; import React, { useEffect, useRef, useState } from "react"; import { ErrorFallback } from "../Render/ErrorFallback/ErrorFallback"; import { Recovery } from "../Render/Recovery/Recovery"; function App() { const [editingMode, setEditingMode] = useState(Mode.normal); const [functionMode, setFunctionMode] = useState(FunctionMode.normal); const [record, setRecord] = useState([]); const [currentRecordIndex, setCurrentRecordIndex] = useState(-1); const [insertInfo, setInsertInfo] = useState(); const [data, setDataOriginal] = useState(initData); const [showName, setShowName] = useState(true); const [autoHiddenName, setAutoHiddenName] = useState(true); const [drawing, setDrawing] = useState(false); const [translateX, setTranslateX] = useState(0); const [translateY, setTranslateY] = useState(0); const [page, setPage] = useState("title"); const [scale, setScale] = useState(1); const ref = useRef(); const menuRef = useRef(); const [saved, setSaved] = useState(true); const [defaultShape, setDefaultShape] = useState('cicle'); const [showTour, setShowTour] = useState(() => { return ( window.innerWidth >= 710 && !localStorage.getItem("skip-tour-viewed") ); }); const [recoveredFromError, setRecoveredFromError] = useState(false); const [showConfirmation, setShowConfirmation] = useState(); // keep latest data if crash happend const setData = (data: React.SetStateAction) => { if (typeof data === "function") { setDataOriginal((state) => { const newState = data(state); setLocalStorage(newState, () => setSaved(false)); return newState; }); } else { setLocalStorage(data, () => setSaved(false)); setDataOriginal(data); } }; useEffect(() => { setShowConfirmation(() => ref.current?.showConfirmation); }, [ref.current?.showConfirmation]); const [cardShowing, setCardShowing] = useState(new CardShowing()); const transfromTools = { scale, setScale, translateX, translateY, setTranslateX, setTranslateY, }; const {backgroundColor} = data; useEffect(() => { document.getElementById('theme-color')!.setAttribute('content', backgroundColor?backgroundColor:"#ffffff"); }, [backgroundColor]) return ( { const last = localStorage.getItem("last"); if (last) { const data = setDataFromJson(setData, last); readFileFromIndexedDB("image") .then((file) => { setData((data) => ({ ...data, // backgroundColor: "image", backgroundImage: file as File, })); }) .catch((e) => { console.error(e); }); mediateMap(data, transfromTools); } setRecoveredFromError(true); }} >
); } export default App; ================================================ FILE: src/Entrance/reportWebVitals.js ================================================ const reportWebVitals = onPerfEntry => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals; ================================================ FILE: src/Entrance/setupTests.js ================================================ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; ================================================ FILE: src/Grid/Scale.ts ================================================ import { CardShowing, ChangeSteps, RecordType, StationProps, UserDataType, addNewStation } from "./../Data/UserData"; import { WheelEvent, Dispatch, SetStateAction, MouseEvent, TouchEvent, } from "react"; import { FunctionMode, Mode } from "../DataStructure/Mode"; import { Point } from "../DataStructure/Point"; import React from "react"; const sensitivity = -0.0006; const scaleByPointer = ( scaleRatio: number, translateX: number, translateY: number, refPoint: Point ) => new Point( refPoint.x - scaleRatio * (refPoint.x - translateX), refPoint.y - scaleRatio * (refPoint.y - translateY) ); export const onWheel = ( event: WheelEvent, preScale: number, setScale: Dispatch>, translateX: number, translateY: number, setTranslateX: Dispatch>, setTranslateY: Dispatch>, functionMode: FunctionMode, ) => { const { deltaY, clientX, clientY } = event; const nextScale = deltaY * sensitivity + preScale; const scaleRatio = nextScale / preScale; const refPoint = new Point(clientX, clientY); if (nextScale > 0.1 || (functionMode ===FunctionMode.editingCustomBackgroundPosition && nextScale > 0)) { const transform = scaleByPointer( scaleRatio, translateX, translateY, refPoint ); setTranslateX(transform.x); setTranslateY(transform.y); setScale(nextScale); } }; export const onMouseDown = ( event: MouseEvent, translateX: number, translateY: number, setEditingMode: Dispatch>, setMouseRefPoint: Dispatch>, setMouseStartTranslate: Dispatch>, setMoved: Dispatch> ) => { const point = Point.getPointFromMouse(event); setMouseRefPoint(point); setEditingMode(Mode.moving); setMouseStartTranslate(new Point(translateX, translateY)); setMoved(false); }; export const onMouseMove = ( event: MouseEvent, translateX: number, translateY: number, setTranslateX: Dispatch>, setTranslateY: Dispatch>, editingMode: Mode, mouseRefPoint: Point, mouseStartTranslate: Point, setMoved: Dispatch> ) => { // setMoved(true); switch (editingMode) { case Mode.moving: { // console.log(event); const point = Point.getPointFromMouse(event); const displacement = point.displacementTo(mouseRefPoint); const distance = point.distanceTo(mouseRefPoint); if(distance>5) setMoved(true); setTranslateX(mouseStartTranslate.x + displacement.x); setTranslateY(mouseStartTranslate.y + displacement.y); break; } } }; export const onMouseUp = ( event: MouseEvent, setEditingMode: Dispatch>, editingMode: Mode, functionMode: FunctionMode, data: UserDataType, setData: Dispatch>, moved: boolean, translateX: number, translateY: number, scale: number, record: RecordType, setRecord: React.Dispatch>, currentRecordIndex: number, setCurrentRecordIndex: React.Dispatch>, cardShowing: CardShowing, setCardShowing: Dispatch>, defaultShape: string, ) => { // console.log(event); setEditingMode(Mode.normal); if (functionMode === FunctionMode.addingStation && !moved) { console.log({ translateX, translateY, scale }); const { clientX, clientY } = event; const x = (clientX - translateX) / scale; const y = (clientY - translateY) / scale; addNewStation( data, setData, x, y, record as StationProps[], setRecord, currentRecordIndex, setCurrentRecordIndex, cardShowing, setCardShowing, defaultShape, ); } const {currentTarget, target} = event; if(currentTarget === target && !moved){ setCardShowing({}); } }; export const onMouseLeave = ( event: MouseEvent, setEditingMode: Dispatch> ) => { // console.log(event); setEditingMode(Mode.normal); }; export const onTouchStart = ( event: TouchEvent, setEditingMode: Dispatch>, setTouchRefPoint: Dispatch>, setTouchStartDistance: Dispatch>, setTouchStartScale: Dispatch>, scale: number, setTouchStartTranslate: Dispatch>, translateX: number, translateY: number, setMoved: Dispatch> ) => { event.preventDefault(); const { touches } = event; // record touch start translate position setTouchStartTranslate(new Point(translateX, translateY)); setMoved(false); switch (touches.length) { //one finger case 1: { const touch = touches[0]; setTouchRefPoint(Point.getPointFromTouch(touch)); setEditingMode(Mode.touchMoving); break; } //two finger case 2: { const touchA = touches[0]; const touchB = touches[1]; const pointA = Point.getPointFromTouch(touchA); const pointB = Point.getPointFromTouch(touchB); setTouchRefPoint(Point.getMidPoint(pointA, pointB)); setEditingMode(Mode.touchScaling); //start touch distance const distance = pointA.distanceTo(pointB); setTouchStartDistance(distance); setTouchStartScale(scale); break; } } }; export const onTouchMove = ( event: TouchEvent, editingMode: Mode, touchRefPoint: Point, translateX: number, translateY: number, setTranslateX: Dispatch>, setTranslateY: Dispatch>, touchStartDistance: number, touchStartScale: number, setScale: Dispatch>, touchStartTranslate: Point, setMoved: Dispatch> ) => { event.preventDefault(); const { touches } = event; setMoved(true); // console.log(event, touches.length, editingMode); switch (touches.length) { //one finger case 1: { if (editingMode === Mode.touchMoving) { const touch = touches[0]; const point = Point.getPointFromTouch(touch); // console.log(touchRefPoint); const displacement = point.displacementTo(touchRefPoint); setTranslateX(displacement.x + touchStartTranslate.x); setTranslateY(displacement.y + touchStartTranslate.y); } break; } //two finger case 2: { if (editingMode === Mode.touchScaling) { const touchA = touches[0]; const touchB = touches[1]; const pointA = Point.getPointFromTouch(touchA); const pointB = Point.getPointFromTouch(touchB); // for scale const distance = pointA.distanceTo(pointB); const nextScale = (touchStartScale * distance) / touchStartDistance; setScale(nextScale); // for translate const midPoint = Point.getMidPoint(pointA, pointB); const scaleRatio = nextScale / touchStartScale; const transform = scaleByPointer( scaleRatio, touchStartTranslate.x, touchStartTranslate.y, touchRefPoint ); //displacement between preMidPoint and midPoint const displacement = midPoint.displacementTo(touchRefPoint); setTranslateX(displacement.x + transform.x); setTranslateY(displacement.y + transform.y); } break; } } }; export const onTouchEnd = ( event: TouchEvent, setEditingMode: Dispatch>, editingMode: Mode, functionMode: FunctionMode, data: UserDataType, setData: Dispatch>, moved: boolean, translateX: number, translateY: number, scale: number, record: RecordType, setRecord: React.Dispatch>, currentRecordIndex: number, setCurrentRecordIndex: React.Dispatch>, cardShowing: CardShowing, setCardShowing: Dispatch>, defaultShape: string, ) => { event.preventDefault(); const { changedTouches } = event; // console.log(event,editingMode); if ( functionMode === FunctionMode.addingStation && !moved && changedTouches.length === 1 ) { const touch = changedTouches[0]; console.log({ touch, translateX, translateY, scale }); const { clientX, clientY } = touch; const x = (clientX - translateX) / scale; const y = (clientY - translateY) / scale; addNewStation( data, setData, x, y, record as StationProps[], setRecord, currentRecordIndex, setCurrentRecordIndex, cardShowing, setCardShowing, defaultShape, ); } const {currentTarget, target} = event; if(currentTarget === target && !moved){ setCardShowing({}); } setEditingMode(Mode.normal); }; ================================================ FILE: src/Line/Handle.ts ================================================ import { Station } from "./../DataStructure/Station"; import { Direct, Direction, DirectionVictorReverseY, } from "../DataStructure/Direction"; import { Line } from "../DataStructure/Line"; import { LineRecord } from "../DataStructure/LineRecord"; import { Point } from "../DataStructure/Point"; import { Vector } from "../DataStructure/Vector"; import { handleLength, handleWidth } from "../Common/const"; const getLPLPoints = (allKeyPoints: Point[]) => { const LQLPoints = allKeyPoints.slice(); // LQLPoints.pop(); // LQLPoints.shift(); return LQLPoints; }; const checkIfStraightTrackHasHanderOrLine = ( direction: Direction, station: Station ) => { const { direct } = direction.opposite(); return station.handlers[direct] || !station.tracks[direct].isEmpty(); }; const checkifHandeCanGoStraight = ( outDirection: Direction, station: Station ) => { const ifStraightTrackHasHander = !checkIfStraightTrackHasHanderOrLine( outDirection, station ); return ifStraightTrackHasHander; }; const getDepartureGoStraightHandeCommand = (A: Point, B: Point) => { const BA = new Vector(B, A); const C = BA.prolong(handleLength); const BC = new Vector(B, C); const [D, E] = BC.verticalProlong(handleWidth); return `M ${D.x} ${D.y} L ${E.x} ${E.y} M ${C.x} ${C.y}`; }; const addHandleForStation = (station: Station, line: Line, direct: Direct) => { station.handlers[direct] = line; }; const getBestDirectionForHandle = (station: Station, direction: Direction) => { let min = Infinity, bestChoice = 0; for (let i = 0; i < station.handlers.length; i++) { const notEmpty = station.handlers[i] || !station.tracks[i].isEmpty(); if (!notEmpty) { const delta = direction.opposite().delta(i); if (delta < min) { bestChoice = i; min = delta; } } } return bestChoice; }; const getDepartureBestChoiceHandeCommand = ( A: Point, B: Point, pathStartPoint: Point ) => { const F = pathStartPoint; const AB = new Vector(A, B); const C = AB.prolong(handleLength - 1); const BC = new Vector(B, C); const [D, E] = BC.verticalProlong(handleWidth); 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}`; }; const getBestChoiceHandleCommand = ( station: Station, direction: Direction, pathStartPoint: Point, line: Line ) => { const handleDirect = getBestDirectionForHandle(station, direction); if (handleDirect === undefined) { return ` M ${pathStartPoint.x} ${pathStartPoint.y}`; } else { const A = station.position; const [x, y] = DirectionVictorReverseY[handleDirect]; const B = new Point(A.x + x, A.y + y); addHandleForStation(station, line!, handleDirect); return getDepartureBestChoiceHandeCommand(A, B, pathStartPoint); } }; const getStartHandleCommand = ( A: Point, B: Point, departureRecord: LineRecord ) => { let command = ""; const outDirection = departureRecord?.getOutDirection(); const { station, line } = departureRecord; const ifHandeCanGoStraight = checkifHandeCanGoStraight( outDirection!, station ); if (ifHandeCanGoStraight) { command = getDepartureGoStraightHandeCommand(A, B); addHandleForStation(station, line!, outDirection!.opposite().direct); } else { command = getBestChoiceHandleCommand(station, outDirection!, A, line!); } return command; }; const getTerminalGoStraightHandeCommand = (C: Point, D: Point) => { const CD = new Vector(C, D); const E = CD.prolong(handleLength); const CE = new Vector(C, E); const [F, G] = CE.verticalProlong(handleWidth); return ` L ${E.x} ${E.y} M ${F.x} ${F.y} L ${G.x} ${G.y}`; }; const getTerminalBestChoiceHandeCommand = ( A: Point, B: Point, pathEndPoint: Point ) => { const F = pathEndPoint; const AB = new Vector(A, B); const C = AB.prolong(handleLength - 1); const BC = new Vector(B, C); const [D, E] = BC.verticalProlong(handleWidth); 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} `; }; const getBestChoiceTerminalHandleCommand = ( station: Station, direction: Direction, pathStartPoint: Point, line: Line ) => { const handleDirect = getBestDirectionForHandle(station, direction); if (handleDirect === undefined) { return ` L ${pathStartPoint.x} ${pathStartPoint.y}`; } else { const A = station.position; const [x, y] = DirectionVictorReverseY[handleDirect]; const B = new Point(A.x + x, A.y + y); addHandleForStation(station, line!, handleDirect); return getTerminalBestChoiceHandeCommand(A, B, pathStartPoint); } }; const getEndHandleCommand = ( C: Point, D: Point, terminalRecord: LineRecord ) => { let command = ""; const inDirection = terminalRecord?.getInDirection(); const { station, line } = terminalRecord; const ifHandeCanGoStraight = checkifHandeCanGoStraight(inDirection!, station); if (ifHandeCanGoStraight) { command = getTerminalGoStraightHandeCommand(C, D); addHandleForStation(station, line!, inDirection!.opposite().direct); } else { command = getBestChoiceTerminalHandleCommand( station, inDirection!, D, line! ); } return command; }; const getHandleCommand = (line: Line, allKeyPoints: Point[]) => { const [A, B] = allKeyPoints; const C = allKeyPoints[allKeyPoints.length - 2], D = allKeyPoints[allKeyPoints.length - 1]; const { departureRecord, displayLine } = line; const { subLine } = displayLine!; const terminalRecord = line.getTerminalRecord(); let startHandleCommand = getStartHandleCommand(A, B, departureRecord!); let endHandleCommand = ` L ${A.x} ${A.y}`; // loop line if (!(departureRecord?.station === terminalRecord?.station)) { endHandleCommand = getEndHandleCommand(C, D, terminalRecord!); } const LQLPoints = getLPLPoints(allKeyPoints); // subline no need handler in joint if ( subLine && departureRecord?.station?.lineCount && departureRecord?.station?.lineCount() >= 2 ) { startHandleCommand = ` M ${A.x} ${A.y} `; } if ( subLine && terminalRecord?.station?.lineCount && terminalRecord?.station?.lineCount() >= 2 ) { endHandleCommand = ""; } return { startHandleCommand, LQLPoints, endHandleCommand }; }; const clearHandleFromRecord = (lineRecord: LineRecord | undefined) => { if (lineRecord) { const { station, line } = lineRecord; station.handlers.forEach((handle, index) => { if (handle === line) { station.handlers[index] = null; } }); } else { console.error("no linerecord to clear handle"); } }; const clearHandle = (line: Line) => { const { departureRecord } = line; const terminalRecord = line.getTerminalRecord(); clearHandleFromRecord(departureRecord); clearHandleFromRecord(terminalRecord); }; export { getHandleCommand, clearHandle }; ================================================ FILE: src/Line/LinePoints.ts ================================================ import { gauge, line_radius } from "../Common/const"; import { Direction } from "../DataStructure/Direction"; import { Line } from "../DataStructure/Line"; import { LineRecord } from "../DataStructure/LineRecord"; import { Point } from "../DataStructure/Point"; import { Rail } from "../DataStructure/Rail"; import { Vector } from "../DataStructure/Vector"; const getTurningPoint = ( A: Point, B: Point, aDirection: Direction, bDirection: Direction ) => { const aVector = Vector.getVectorByPointAndDirection(A, aDirection); const bVector = Vector.getVectorByPointAndDirection(B, bDirection); const crossPoint = aVector.getCrossPointTo(bVector); return crossPoint; }; const getOffsetPointFromDirectionAndRail = ( point: Point, direction: Direction, rail: Rail ) => { // if(rail.track.station.displayStation!.stationName==='风起地站' && rail.line.displayLine?.lineName==='2号线') // debugger const s = Math.SQRT1_2; // lean const directionOffset = [ [[-1,0],[0,0],[1,0]],// 0 up [[-s,-s],[0,0],[s,s]],// 1 upRight [[0,-1],[0,0],[0,1]],// 2 right [[s,-s],[0,0],[-s,s]],// 3 rightDown [[1,0],[0,0],[-1,0]],// 4 down [[s,s],[0,0],[-s,-s]],// 5 downLeft [[0,1],[0,0],[0,-1]],// 6 left [[-s,s],[0,0],[s,-s]],// 7 leftUp ]; // const directionOffset = [ // [0, -1], // [SQRT1_2, -SQRT1_2], // [1, 0], // [SQRT1_2, SQRT1_2], // [0, 1], // [-SQRT1_2, SQRT1_2], // [-1, 0], // [-SQRT1_2, -SQRT1_2], // ]; // const offsetIndex = (direction.direct + rail.index - 1 + 8) % 8; const offset = directionOffset[direction.direct]; const [offsetX, offsetY] = offset[rail.index].map((x) => x * gauge); return new Point(offsetX + point.x, offsetY + point.y); }; const getStartOffsetPointOfStation = (lineRecord: LineRecord) => { const { station, nextRail } = lineRecord; return getOffsetPointFromDirectionAndRail( station.position, nextRail!.track.direction, nextRail! ); }; const getEndOffsetPointOfStation = (lineRecord: LineRecord) => { const { station, lastRail } = lineRecord; return getOffsetPointFromDirectionAndRail( station.position, lastRail!.track.direction, lastRail! ); }; const getInOffsetPointOfStation = (lineRecord: LineRecord) => { const { station, lastRail } = lineRecord; return getOffsetPointFromDirectionAndRail( station.position, lastRail!.track.direction, lastRail! ); }; const getOutOffsetPointOfStation = (lineRecord: LineRecord) => { const { station, nextRail } = lineRecord; return getOffsetPointFromDirectionAndRail( station.position, nextRail!.track.direction, nextRail! ); }; const getPointsBetweenStations = (lineRecord: LineRecord) => { const { nextLineRecord } = lineRecord; if (nextLineRecord) { const AOffsetPoint = getStartOffsetPointOfStation(lineRecord); const BOffsetPoint = getEndOffsetPointOfStation(nextLineRecord); if ( lineRecord.getOutDirection()!.oppositeTo(nextLineRecord.getInDirection()) ) { //direct to next station return [AOffsetPoint, BOffsetPoint]; } else { //has turning const turningPoint = getTurningPoint( AOffsetPoint, BOffsetPoint, lineRecord.getOutDirection()!, nextLineRecord.getInDirection()! ); return [AOffsetPoint, turningPoint, BOffsetPoint]; } } else throw new Error("No NextLineRecord!"); return []; }; const getPointsInStation = (lineRecord: LineRecord) => { const { nextLineRecord, lastLineRecord } = lineRecord; if (nextLineRecord && lastLineRecord) { const AOffsetPoint = getInOffsetPointOfStation(lineRecord); const BOffsetPoint = getOutOffsetPointOfStation(lineRecord); const inDirection = lineRecord.getInDirection(); const outDirection = lineRecord.getOutDirection(); // same point in and out if ( AOffsetPoint.sameTo(BOffsetPoint) || inDirection?.oppositeTo(outDirection) ) return []; else { // same in same out, need two points if (inDirection?.sameTo(outDirection!)) { return []; // no need points cause last and next will add end and start points // return [AOffsetPoint, BOffsetPoint]; } const crossPointInStation = getTurningPoint( AOffsetPoint, BOffsetPoint, inDirection!, outDirection! ); // if(crossPointInStation.x === Infinity) debugger // not same point, but cross in inPoint or outPoint if ( crossPointInStation.sameTo(AOffsetPoint) || crossPointInStation.sameTo(BOffsetPoint) ) return []; return [crossPointInStation]; } } else return []; }; const isPointInStationInNextLine = ([A]: Point[], [B, C]: Point[]) => { if (A) { const BC = new Vector(B, C); return BC.passesThroughPoint(A); } return false; }; // keypoints record all points to draw line const getAllKeyPoints = (line: Line) => { let lineRecord = line.departureRecord; let keyPoints: Point[] = []; if (!lineRecord) { console.error("No DepartureStation!"); return []; } while (lineRecord.nextLineRecord) { const pointsInStation = getPointsInStation(lineRecord); // if(pointsInStation[0]&&(pointsInStation[0].x === 500 && pointsInStation[0].y === 600)) debugger; keyPoints = keyPoints.concat(pointsInStation); const pointsBetweenStations = getPointsBetweenStations(lineRecord); // cross point in next line, delete next line start point if (isPointInStationInNextLine(pointsInStation, pointsBetweenStations)) pointsBetweenStations.shift(); // next line start point is the same with this line's end, // delete next line start point if ( keyPoints.length && pointsBetweenStations[0].sameTo(keyPoints[keyPoints.length - 1]) ) pointsBetweenStations.shift(); keyPoints = keyPoints.concat(pointsBetweenStations); lineRecord = lineRecord.nextLineRecord; if (lineRecord === line.departureRecord) break; } // to-do why duplicate return keyPoints.filter((p,index)=>index>0?!(p.x===keyPoints[index-1].x&&p.y===keyPoints[index-1].y):true); }; const deleteDuplicatedPoints = (keyPoints: Point[]) => { if (keyPoints.length <= 2) return keyPoints; // no need delete duplicate point const start = keyPoints[0], end = keyPoints[keyPoints.length - 1]; const QKeyPoints: Point[] = [start]; for (let i = 1; i < keyPoints.length - 1; i++) { const A = keyPoints[i - 1]; const B = keyPoints[i]; const C = keyPoints[i + 1]; const AC = new Vector(A, C); const AB = new Vector(A, B); const BC = new Vector(B, C); if (!AC.passesThroughPointRound(B) //|| AB.direct !== BC.direct ) QKeyPoints.push(B); } QKeyPoints.push(end); return QKeyPoints; }; const addLPointsAroundQPoints = (qPoints: Point[]) => { if (qPoints.length <= 2) return qPoints; // no need add L points const start = qPoints[0], end = qPoints[qPoints.length - 1]; const LQLKeyPoints: Point[] = [start]; for (let i = 1; i < qPoints.length - 1; i++) { const A = qPoints[i - 1]; const B = qPoints[i]; const C = qPoints[i + 1]; const BA = new Vector(B, A); const BC = new Vector(B, C); const BAOffsetPoint = BA.normalize(line_radius).end.offset(B); const BCOffsetPoint = BC.normalize(line_radius).end.offset(B); BAOffsetPoint.q_start = true; B.q = true; BCOffsetPoint.q_end = true; LQLKeyPoints.push(BAOffsetPoint); LQLKeyPoints.push(B); LQLKeyPoints.push(BCOffsetPoint); } LQLKeyPoints.push(end); return LQLKeyPoints; }; const getRoundedPoints = (keyPoints: Point[]) => { const QKeyPoints = deleteDuplicatedPoints(keyPoints); const LQLPoints = addLPointsAroundQPoints(QKeyPoints); return LQLPoints; }; const generateLineCommand = (LQLPoints: Point[]) => { if (LQLPoints.length <= 1) { console.error( "no enough point to draw line. now we have " + LQLPoints.length ); return ""; } const start = LQLPoints[0], end = LQLPoints[LQLPoints.length - 1]; const MCommand = ``; const EndCommand = ` L ${end.x} ${end.y} `; let path = ""; for (let i = 1; i < LQLPoints.length - 1; i++) { const P = LQLPoints[i]; switch (true) { case P.q_start: path += ` L ${P.x} ${P.y} `; break; case P.q: path += ` Q ${P.x} ${P.y} `; break; case P.q_end: path += ` , ${P.x} ${P.y} `; break; default: throw new Error("no Point flag for command!"); } } return MCommand + path + EndCommand; }; export { getAllKeyPoints, getRoundedPoints, generateLineCommand }; ================================================ FILE: src/Render/Card/Cards.scss ================================================ .cards { position: fixed; bottom: 0; // left: -65px; // zoom: 0.85 width: fit-content; max-width: 100vw; height: 500px; overflow-x: scroll; overflow-y: visible; white-space: nowrap; padding-top: 200px; // padding-right: 100px; box-sizing: border-box; pointer-events: none; &::-webkit-scrollbar { display: none; // width: 0.01px; } .card-container { display: inline-block; pointer-events: auto; & > div { margin-left: 50px; position: relative; box-shadow: 0 4px 159px 7px rgba(0, 0, 0, 0.25); // filter: drop-shadow(0 0 2px #999); } &:last-child { margin-right: 50px; } } // @media (max-width: 555px) { // height: 285px; // &>div{ // margin-left: 50px; // // box-shadow: none; // // border: 1px solid rgba(0, 0, 0, 0.266); // &:last-child{ // margin-right: 50px; // } // zoom: 0.7; // } // } } ================================================ FILE: src/Render/Card/Cards.tsx ================================================ import React, { CSSProperties, Dispatch, RefObject, SetStateAction, useEffect, useLayoutEffect, useRef, useState, } from "react"; import { Line } from "../../DataStructure/Line"; import { Station } from "../../DataStructure/Station"; import { DisplayStation } from "../../DataStructure/Display"; import { LineCard } from "./LineCard"; import "./Cards.scss"; import { StationCard } from "./StationCard"; import { CardShowing, InsertInfo, UserDataType } from "../../Data/UserData"; import { browserInfo, mapToArr, onWheelX, onWheelY } from "../../Common/util"; import { showConfirmationInterface } from "../Delete/DeleteConfirmation"; import { FunctionMode } from "../../DataStructure/Mode"; export function Cards({ data, setData, showConfirmation, menuRef, functionMode, setFunctionMode, insertInfo, setInsertInfo, cardShowing, setCardShowing, }: { data: UserDataType; setData: Dispatch>; showConfirmation?: showConfirmationInterface; menuRef: RefObject; functionMode: FunctionMode; setFunctionMode: React.Dispatch>; insertInfo?: InsertInfo; setInsertInfo: React.Dispatch>; cardShowing: CardShowing; setCardShowing: Dispatch>; }) { const { lines, stations } = data; const { engine } = browserInfo; const [pointerEvents, setPointerEvents] = useState<"auto" | "none">("none"); const { lineIds, stationIds, stationFirst } = cardShowing; const linesComp = lineIds?.map((lineId) => { const line = lines.get(lineId); if (line) return (
); }); const stationComp = stationIds?.map((stationId) => { const station = stations.get(stationId); if (station) return (
); }); const [style, setStyle] = useState(); const handleStyle = () => { const style: CSSProperties = engine.name === "WebKit" ? { pointerEvents: "auto", height: 370, paddingTop: 70 } : {}; if ( ((stationIds?.length || 0) + (lineIds?.length || 0)) * 555 < window.innerWidth ) { style.paddingRight = 100; style.pointerEvents = "none"; } setStyle(style); }; useEffect(() => { handleStyle(); window.addEventListener("resize", handleStyle); return () => window.removeEventListener("resize", handleStyle); }, [cardShowing]); return (
{ const { target, currentTarget } = e; if (target === currentTarget) setPointerEvents("none"); else setPointerEvents("auto"); }} > {stationFirst ? stationComp : linesComp} {stationFirst ? linesComp : stationComp}
); } ================================================ FILE: src/Render/Card/LineCard.scss ================================================ .line-card { width: 455px; height: 250px; border-radius: 23px; display: inline-block; // margin-left: 50px; // margin-bottom: 50px; // zoom: 0.8; --margin-left: 33px; background-color: rgba(255, 255, 255, 0.863); backdrop-filter: blur(15px); position: relative; transition: 0.3s ease-in-out width; overflow-x: hidden; .tools { position: absolute; right: 0; .tool-item { vertical-align: top; height: 34px; width: 34px; display: inline-flex; justify-content: center; align-items: center; margin: 14.5px 11.3px 0 0; background-color: #d9d9d9; border-radius: 7.25px; .expand-icon { height: 15px; width: 15px; } .edit-icon { height: 17.74px; width: 17.74px; } } } .stations-count { font-size: 18px; font-weight: 500; cursor: pointer; margin: 23px 0 0 var(--margin-left); width: fit-content; } .line-name { & > * { font-size: 24px; font-weight: 500; } margin: 2px 0 0 var(--margin-left); } .from-to { font-size: 18px; font-weight: 500; margin: 5px 0 0 var(--margin-left); color: #ababab; } .edit-detail { .name-detail { margin-top: 22.58px; white-space: nowrap; width: 254.2px; overflow-y: visible; overflow-x: scroll; height: 100px; &::-webkit-scrollbar { display: none; // width: 0.01px; } .name-item { display: inline-block; margin-right: 24.2px; vertical-align: top; &:first-child { margin-left: 32.25px; } .title { font-size: 11.25px; font-weight: 500; color: #ea0b2a; } input { margin-top: 21.77px; padding: 0; } .sign-input { // width: 35.5px; * { min-width: 35.5px; height: 35.5px; border: none; border-radius: 8.87px; background-color: #ea0b2a; font-size: 23.25px; color: white; text-align: center; } } .line-name-input, .order-input { * { font-size: 36px; border: none; font-weight: 500; // width: 100px; margin-top: 14px; } } .order-input { // width: 30px; // white-space: nowrap; vertical-align: top; } } } .color-detail { .color-detail-choosing { box-sizing: border-box; padding: 21px 32.25px; width: 255px; display: grid; grid-template-columns: repeat(5, 20%); grid-template-rows: repeat(2, 50%); grid-row-gap: 9px; grid-column-gap: 6px; .color-preview { cursor: pointer; width: 19.5px; height: 19.5px; border-radius: 50%; border: 2px solid; display: flex; justify-content: center; align-items: center; .color-preview-inner { width: 16.9px; height: 16.9px; border-radius: 50%; } } } .custom-color { margin: 0 32.25px; border-top: 1px dashed black; width: 197px; box-sizing: border-box; padding-top: 15px; // padding-left: 10px; .selected-color-preview { .color-input { // width: 19.5px; // height: 19.5px; // border-radius: 50%; display: inline-block; } .color-value { display: inline-block; margin-left: 15px; font-size: 15.25px; font-weight: 500; width: 100px; text-decoration: underline; vertical-align: top; margin-top: 1px; border: none; } } } } .operation-detail { .operation-item { &:first-child { margin-top: 15px; } &:last-child { margin-bottom: 15px; } margin-left: 32.25px; margin-top: 8px; margin-bottom: 8px; color: #2196f3; font-size: 18px; font-weight: 500; cursor: pointer; &.delete { color: #ea0b2a; } } } } .station-bar { margin: 30px 0 0 0; white-space: nowrap; width: 455px; overflow-x: scroll; overflow-y: hidden; transition: 0.3s ease-in-out width; &::-webkit-scrollbar { display: none; // width: 0.01px; } .add-first { width: var(--margin-left); display: inline-block; } .station-block { display: inline-block; width: 161px; height: 78.22px; border-left: 1px solid #cccccc; position: relative; vertical-align: top; .track { display: flex; background-color: #f0f0f0; width: 161px; height: 48.3px; .sleeper { border-left: 1px solid #cccccc; width: 53.6px; &:first-child { border: none; } } } .bend-first { position: absolute; top: 7.215px; left: 9.5px; height: 33.87px; width: 133.8px; background-color: #d9d9d9; border-radius: 4px; display: flex; align-items: center; cursor: pointer; transition: ease-in-out 0.3s background-color; &.current-inserting { background-color: #c3c3c3; } .bend-icon { svg { width: 16px; height: 16px; transform-origin: center; } width: 16px; height: 16px; display: inline-block; margin: -1px 13px 0 18.5px; &.bend { svg { transform: rotate(-45deg); } } } .bend-des { display: inline-block; font-size: 14.25px; font-weight: 500; color: rgb(0, 0, 0, 0.5); } } .station-name { font-size: 18px; font-weight: 500; margin: 8px; color: #ea0b2a; cursor: pointer; } } } .edit-panel { &.edit { right: 0; } width: 200.8px; background-color: #f2f2f2; position: absolute; right: -200.8px; top: 0; height: 250px; transition: 0.3s ease-in-out right; .edit-tools { width: 200.8px; height: 250px; overflow-y: scroll; overflow-x: hidden; &::-webkit-scrollbar { display: none; // width: 0.01px; } .edit-tool { &:first-child { margin: 21.7px 21.7px 2px 21.7px; } &:last-child { margin: 2px 21.7px 21.7px 21.7px; } cursor: pointer; margin: 2px 21.7px 2px 21.7px; width: 158px; height: 75px; border-radius: 11.3px; font-weight: 500; display: flow-root; &.selected { background-color: #d9d9d9; } .title { font-size: 18px; margin: 12.1px 0 0 20.2px; text-transform: capitalize; } .value { font-size: 11.25px; margin: 4.83px 0 0 20.2px; } &.color > .value { color: #ea0b2a; } } } .done { background-color: #2196f3; border-radius: 25px; width: 74px; height: 28.2px; position: absolute; bottom: 23.4px; right: 23.4px; display: flex; align-items: center; .done-icon { height: 17.74px; width: 17.74px; margin: 0 6.5px 0 11.3px; path { fill: #ffffff; fill-opacity: 1; } } .done-des { font-size: 12px; font-weight: 500; color: white; } } } } ================================================ FILE: src/Render/Card/LineCard.tsx ================================================ import React, { Dispatch, RefObject, SetStateAction, useEffect, useState, useTransition, } from "react"; import { Line } from "../../DataStructure/Line"; import { Station } from "../../DataStructure/Station"; import { DisplayStation } from "../../DataStructure/Display"; import "./LineCard.scss"; import ArrowIcon from "../../Resource/Icon/arrow"; import PlusIcon from "../../Resource/Icon/plus"; import ExpandIcon from "../../Resource/Icon/expand"; import ShrinkIcon from "../../Resource/Icon/shrink"; import EditIcon from "../../Resource/Icon/edit"; import classNames from "classnames"; import { CardShowing, InsertInfo, LineProps, StationProps, UserDataType, dataProcessor, } from "../../Data/UserData"; import { browserInfo, mapToArr, onWheelX, onWheelY, scrollOptimize, } from "../../Common/util"; import { AutoGrowthInput } from "../../Common/AutoGrowthInput"; import { colorSH, colorSHMap } from "../../Common/color"; import { showConfirmationInterface } from "../Delete/DeleteConfirmation"; import { FunctionMode } from "../../DataStructure/Mode"; import { Point } from "../../DataStructure/Point"; import { useTranslation } from "react-i18next"; import i18n from "../../i18n/config"; export function LineCard({ line, setData, data, showConfirmation, functionMode, setFunctionMode, insertInfo, setInsertInfo, menuRef, cardShowing, setCardShowing, }: { line: LineProps; setData: Dispatch>; data: UserDataType; showConfirmation?: showConfirmationInterface; functionMode: FunctionMode; setFunctionMode: React.Dispatch>; insertInfo?: InsertInfo; setInsertInfo: React.Dispatch>; menuRef: RefObject; cardShowing: CardShowing; setCardShowing: Dispatch>; }) { const { lineId, lineName, stationIds, sign, order, color: colorSelected, subLine = false, } = line; const { getStationById, getStationsInThisLine, setLineName, setSign, setOrder, setColor, getBendFirst, setBendFirst, deleteLine, setSubLine, } = dataProcessor(lineId, setData, data); const colorName = i18n.language === "zh" && colorSHMap.get(colorSelected)?.color_name || colorSelected; const firstStation = getStationById(stationIds[0]); const lastStation = getStationById(stationIds[stationIds.length - 1]); // if (!firstStation) { // firstStation = new StationProps(); // lastStation = firstStation; // } const [expand, setExpand] = useState(false); const [edit, setEdit] = useState(false); const [expandWidth, setExpandWidth] = useState(455); const [tab, setTab] = useState("name"); const addingStation = functionMode === FunctionMode.selectingStation || functionMode === FunctionMode.lineEditing || !firstStation; const getExpandWidth = () => { const { stationIds: stations } = cardShowing; const expected = 33 + 161 * (addingStation ? stationIds.length + 1 : stationIds.length); const hasStation = stations && stations.length; const width = expected > window.innerWidth ? window.innerWidth - (hasStation ? 505 : 100) : expected; // console.log(width); setExpandWidth(width); }; useEffect(() => { getExpandWidth(); window.addEventListener("resize", getExpandWidth); return () => window.removeEventListener("resize", getExpandWidth); }, [stationIds, addingStation, cardShowing]); useEffect(() => { getExpandWidth(); }, [expand]); const { t } = useTranslation(); const editTools = (tab: string) => { switch (tab) { case "name": { return (
{t('menu.symbol')}
setSign(e.currentTarget.value)} style={{ backgroundColor: colorSelected }} />
{t('line.name')}
setLineName(e.currentTarget.value)} className="line-name-input" value={lineName} />
{t('line.order')}
setOrder(parseInt(e.currentTarget.value))} />
); } case "color": { return (
{new Array(10).fill(1).map((x, index) => { const { color } = colorSH[index]; const choosed = color === colorSelected; return (
setColor(color)} >
); })}
setColor(e.currentTarget.value)} /> setColor(e.currentTarget.value)} >
); } case "operation": { return (
{ setSubLine(!subLine); }} > {subLine?t('line.notSubLineAnymore'):t('line.asSubline')}...
{ showConfirmation!({ line }, deleteLine); }} > {t('line.deleteLine')}
); } } }; const { engine } = browserInfo; const stationsInThisLine = getStationsInThisLine(); if (addingStation) { stationsInThisLine.unshift(new StationProps()); } return (
{expand || edit ? ( <> ) : (
{ setEdit(true); // setTab("name"); }} >
)} {edit ? ( <> ) : (
setExpand(!expand)}> {expand ? : }
)}
{ setCardShowing({ lineIds: [lineId], stationIds }); }} > {stationIds.length}{t('line.stations')}
setLineName(e.currentTarget.value)} className="line-name" value={lineName} disabled /> {edit ? (
{editTools(tab)}
) : ( <>
{firstStation ? t('line.fromTo', {from:firstStation!.stationName,to: lastStation!.stationName}) : t('line.notInUse')}
{stationsInThisLine.map((station, index) => { const { stationName, stationId } = station!; const bendFirst = getBendFirst(index); const last = index === stationsInThisLine.length - 1; let currentInserting = false; if (insertInfo) { const { insertIndex, line: insertLine } = insertInfo; if (insertIndex === index && insertLine === line) { currentInserting = true; } } return (
{ if (addingStation) { if (!firstStation) { if (menuRef?.current?.showTools) { menuRef.current.showTools( e, FunctionMode.selectingStation ); } } setInsertInfo({ insertIndex: index, line }); setFunctionMode(FunctionMode.selectingStation); } else if (last) { setInsertInfo({ insertIndex: index + 1, line }); if (menuRef?.current?.showTools) { menuRef.current.showTools( e, FunctionMode.selectingStation ); } } else { setBendFirst(index, !bendFirst); } }} >
{addingStation || last ? : }
{addingStation || last ? currentInserting ? t('line.insertHere') : t('line.insertStation') : bendFirst ? t('line.bendFirst') : t('line.straight')}
{ setCardShowing({ lineIds: [lineId], stationIds: [stationId], }); }} > {stationName}
); })}
)} {
{ scrollOptimize(e); setTab("name"); }} >
{t('line.name')}
{lineName}
{ scrollOptimize(e); setTab("color"); }} >
{t('line.color')}
{colorName}
{ scrollOptimize(e); setTab("operation"); }} >
{t('line.operation')}
{t('line.delete')}
setEdit(false)}> {t('line.done')}
}
); } ================================================ FILE: src/Render/Card/StationCard.scss ================================================ .station-card { width: 455px; height: 250px; border-radius: 23px; display: inline-block; // margin-left: 50px; // margin-bottom: 50px; // zoom: 0.8; --margin-left: 33px; background-color: rgba(255, 255, 255, 0.863); backdrop-filter: blur(15px); position: relative; transition: 0.3s ease-in-out width; overflow: hidden; .tools { position: absolute; right: 0; .tool-item { vertical-align: top; height: 34px; width: 34px; display: inline-flex; justify-content: center; align-items: center; margin: 14.5px 11.3px 0 0; background-color: #d9d9d9; border-radius: 7.25px; .expand-icon { height: 15px; width: 15px; } .edit-icon { height: 17.74px; width: 17.74px; } } } .line-count { font-size: 18px; font-weight: 500; margin: 23px 0 0 var(--margin-left); cursor: pointer; } .station-name { & > * { font-size: 25px; font-weight: 500; } margin: 2px 0 0 var(--margin-left); } .from-to { font-size: 18px; font-weight: 500; margin: 5px 0 0 var(--margin-left); color: #ababab; } .edit-detail { height: 164px; overflow-y: scroll; &::-webkit-scrollbar { display: none; // width: 0.01px; } .name-detail { margin-top: 22.58px; white-space: nowrap; width: fit-content; .name-item { display: inline-block; margin-right: 24.2px; vertical-align: top; position: relative; &:first-child { margin-left: 32.25px; } .title { font-size: 11.25px; font-weight: 500; color: #ea0b2a; } input { margin-top: 21.77px; padding: 0; } .auto-growth-span { font-size: 36px; border: none; font-weight: 500; width: 100px; margin-top: 14px; opacity: 0; } .auto-growth-input { font-size: 36px; border: none; font-weight: 500; width: calc(100% + 18px); margin-top: 14px; position: absolute; left: 0; } } } .color-detail { .color-detail-choosing { box-sizing: border-box; padding: 10px 32.25px; width: 255px; display: grid; border: white 1px solid; grid-template-columns: repeat(4, 25%); grid-template-rows: repeat(3, 33.33%); // grid-row-gap: 9px; // grid-column-gap: 6px; .shape-container { height: 46.13px; display: flex; justify-content: center; align-items: center; border-left: 1px black dotted; border-bottom: 1px black solid; transform: translate(-1px, 1px); cursor: pointer; &.left { border-left: none; } &.bottom { border-bottom: none; } .shape-preview { width: 19.5px; height: 19.5px; // border-radius: 50%; // border: 2px solid; display: flex; justify-content: center; align-items: center; &.shape-selected { svg { * { stroke: #ea0b2a; } } } &.square { zoom: 0.9; } &.triangle { zoom: 1.05; } &.start { zoom: 1.15; } &.hexagon { zoom: 0.9; } &.pentagon { zoom: 1.1; } &.diamond{ zoom: 1.15; } &.leaf{ zoom: 0.9; } } } } } .operation-detail { .operation-item { width: fit-content; &:first-child { margin-top: 15px; } &:last-child { margin-bottom: 15px; } margin-left: 32.25px; margin-top: 8px; margin-bottom: 8px; color: #2196f3; font-size: 18px; font-weight: 500; cursor: pointer; &.delete { color: #ea0b2a; } } } .tag-detail { display: grid; grid-template-columns: repeat(3, 33.33%); grid-template-rows: repeat(3, 33.33%); width: 200px; height: 126px; margin: 18px 30.25px; align-content: center; justify-content: center; align-items: center; justify-items: center; .tag-item { text-align: center; font-size: 11.25px; font-weight: 500; cursor: pointer; background-color: rgb(0, 0, 0, 13%); height: 30px; width: 55.65px; border-radius: 3px; border: 1px dotted; display: flex; justify-content: center; align-items: center; &.selected{ background-color: rgb(0, 0, 0, 23%); border: 1px solid; } &.center{ background-color: transparent; border: none; } } } } .station-bar { margin: 30px 0 0 0; white-space: nowrap; width: 455px; overflow-x: scroll; overflow-y: hidden; transition: 0.3s ease-in-out width; &::-webkit-scrollbar { display: none; // width: 0.01px; } .add-first { width: var(--margin-left); display: inline-block; } .station-block { display: inline-block; width: 161px; height: 78.22px; border-left: 1px solid #cccccc; position: relative; .track { display: flex; background-color: #f0f0f0; width: 161px; height: 48.3px; .sleeper { border-left: 1px solid #cccccc; width: 53.6px; &:first-child { border: none; } } } .bend-first { position: absolute; top: 7.215px; left: 9.5px; height: 33.87px; width: 133.8px; background-color: #d9d9d9; border-radius: 4px; display: flex; align-items: center; .bend-icon { width: 16px; height: 16px; display: inline-block; margin: 0 13px 0 18.5px; &.bend { transform: rotate(-45deg); } } .bend-des { display: inline-block; font-size: 14.25px; font-weight: 500; color: rgb(0, 0, 0, 0.5); } } .station-name { font-size: 18px; font-weight: 500; margin: 8px; color: #ea0b2a; } } } .edit-panel { &.edit { right: 0; } width: 200.8px; background-color: #f2f2f2; position: absolute; right: -200.8px; top: 0; height: 250px; transition: 0.3s ease-in-out right; .edit-tools { width: 200.8px; height: 250px; overflow-y: scroll; overflow-x: hidden; &::-webkit-scrollbar { display: none; // width: 0.01px; } .edit-tool { &:first-child { margin: 21.7px 21.7px 2px 21.7px; } &:last-child { margin: 2px 21.7px 21.7px 21.7px; } cursor: pointer; margin: 2px 21.7px 2px 21.7px; width: 158px; height: 75px; border-radius: 11.3px; font-weight: 500; display: flow-root; &.selected { background-color: #d9d9d9; } .title { font-size: 18px; margin: 12.1px 0 0 20.2px; text-transform: capitalize; } .value { font-size: 11.25px; margin: 4.83px 0 0 20.2px; .position { color: #5e5e5e; margin: 0 5px; text-decoration: underline; } } } } .done { background-color: #2196f3; border-radius: 25px; width: 74px; height: 28.2px; position: absolute; bottom: 23.4px; right: 23.4px; display: flex; align-items: center; .done-icon { height: 17.74px; width: 17.74px; margin: 0 6.5px 0 11.3px; path { fill: #ffffff; fill-opacity: 1; } } .done-des { font-size: 12px; font-weight: 500; color: white; } } } } ================================================ FILE: src/Render/Card/StationCard.tsx ================================================ import React, { Dispatch, RefObject, SetStateAction, useEffect, useState, } from "react"; import "./StationCard.scss"; import classNames from "classnames"; import { Point } from "../../DataStructure/Point"; import { CardShowing, InsertInfo, StationProps, UserDataType, dataProcessor, } from "../../Data/UserData"; import shapes from "../../Resource/Shape/shape"; import { Shape } from "../../Data/Shape"; import { AutoGrowthInput } from "../../Common/AutoGrowthInput"; import { browserInfo, onWheelX, onWheelY, scrollOptimize, } from "../../Common/util"; import { showConfirmationInterface } from "../Delete/DeleteConfirmation"; import { FunctionMode } from "../../DataStructure/Mode"; import { useTranslation } from "react-i18next"; export function StationCard({ station, setData, data, showConfirmation, menuRef, functionMode, setFunctionMode, insertInfo, setInsertInfo, cardShowing, setCardShowing, }: { station: StationProps; setData: Dispatch>; data: UserDataType; showConfirmation?: showConfirmationInterface; menuRef: RefObject; functionMode: FunctionMode; setFunctionMode: React.Dispatch>; insertInfo?: InsertInfo; setInsertInfo: React.Dispatch>; cardShowing: CardShowing; setCardShowing: Dispatch>; }) { const { stationName, lineIds, position, shape: shapeSelected, stationId, tagDirection=8, } = station; const { setStationName, setStationPosition, setStationShape, getLineById, deleteStation, removeStationFromLine, addNewLine, setStationTagDirection, } = dataProcessor(stationId, setData, data); const [x, y] = position; const setX = (x: number) => setStationPosition(x, y); const setY = (y: number) => setStationPosition(x, y); const lineCount = lineIds.length; const [tab, setTab] = useState("name"); const { engine } = browserInfo; const { t } = useTranslation(); const tags = [ t('station.up'), t('station.upRight'), t('station.right'), t('station.rightDown'), t('station.down'), t('station.downLeft'), t('station.left'), t('station.leftUp'), t('station.auto') ]; const editTools = (tab: string) => { switch (tab) { case "name": { return (
{t('station.x')}
setX(Number(x.currentTarget.value))} type="number" />
{t('station.y')}
setY(Number(y.currentTarget.value))} type="number" />
); } case "color": { const column = 4; const row = 3; const grid = new Array(column * row).fill(0); Object.keys(shapes).forEach((shape, index) => (grid[index] = shape)); return (
{grid.map((shape, index) => { const left = index % column === 0; const bottom = Math.floor(index / column) === row - 1; // console.log({ shape }); return (
{ if (shape) setStationShape(shape); }} >
{ //@ts-ignore shapes[shape] }
); })}
); } case "operation": { return (
{ const newLine = addNewLine(); setInsertInfo({ insertIndex: 1, line: newLine! }); // setFunctionMode(FunctionMode.selectingStation); if (menuRef?.current?.showTools) { menuRef.current.showTools(e, FunctionMode.selectingStation); } }} > {t('station.startHere')}
{lineIds.map((lineId) => { const line = getLineById(lineId); const stationIndexes: number[] = []; line?.stationIds.forEach((x, index) => { if (x === stationId) stationIndexes.push(index); }); const { lineName } = line!; const removeStations = stationIndexes.map((stationIndex) => (
{ showConfirmation!({ line, station, stationIndex }, () => { removeStationFromLine(lineId, stationIndex); }); }} > {t('station.remove',{lineName,stationIndex: stationIndex! + 1})}...
)); return removeStations; })}
{ showConfirmation!({ station }, () => { deleteStation(); if (menuRef?.current?.backToTitle) { menuRef.current.backToTitle(); } }); }} > {t('station.delete')}
); } case "tag": { const directMapIndex = [7, 0, 1, 6, 8, 2, 5, 4, 3]; return (
{directMapIndex.map((direct, index) => { if (index === 4) return (
{ setStationTagDirection(8); }} className="tag-item center" > {tags[tagDirection]}
); else return (
{ setStationTagDirection(direct); }} className={classNames({ "tag-item": 1, selected: tagDirection === direct, })} >
); })}
); } } }; return (
{ setCardShowing({ stationIds: [stationId], lineIds, stationFirst: true, }); }} > {lineCount}{t('station.lines')}
setStationName(e.currentTarget.value)} className="station-name" value={stationName} />
{editTools(tab)}
{
{ scrollOptimize(e); setTab("name"); }} >
{t('station.poisition')}
X{x}Y {y}
{ scrollOptimize(e); setTab("color"); }} >
{t('station.shape')}
{ t(`shape.${shapeSelected}`) }
{ scrollOptimize(e); setTab("operation"); }} >
{t('station.operation')}
{t('station.operateDes')}
{ scrollOptimize(e); setTab("tag"); }} >
{t('station.stationName')}
{tags[tagDirection]}
}
); } ================================================ FILE: src/Render/Component/LineRender.tsx ================================================ import React, { CSSProperties, Dispatch, SetStateAction, memo, useEffect, useState, } from "react"; import { Line } from "../../DataStructure/Line"; import { Point } from "../../DataStructure/Point"; import { generateLineCommand, getAllKeyPoints, getRoundedPoints, } from "../../Line/LinePoints"; import { clearHandle, getHandleCommand } from "../../Line/Handle"; import { gauge } from "../../Common/const"; import { CardShowing, DrawProps, DrawerSize, UserDataType, } from "../../Data/UserData"; function LineRender({ line, cardShowing, setCardShowing, command, data, setData, drawing, drawerX, drawerY, }: { line: Line; cardShowing: CardShowing; setCardShowing: Dispatch>; command: string; data: UserDataType; setData: Dispatch>; } & DrawProps & DrawerSize) { const { stations } = data; const { displayLine, departureRecord } = line; const { color, lineId, subLine, lineName } = displayLine!; const { lineIds, stationIds } = cardShowing; const showing = lineIds?.length || stationIds?.length; const emphasis = lineIds?.includes(lineId) || (stationIds && stationIds.length === 1 && stationIds[0] && stations.get(stationIds[0]) && stations.get(stationIds[0])?.lineIds?.includes(lineId)); const [moved, setMoved] = useState(false); const onClick = () => { if (!moved) { setCardShowing({ lineIds: [lineId] }); } }; return ( <>
{/* */} setMoved(false)} onTouchStart={() => setMoved(false)} onTouchMove={() => (!moved) && setMoved(true)} onMouseMove={() => (!moved) && setMoved(true)} onMouseUp={onClick} onTouchEnd={onClick} /> {/* */}
{/* {renderPoints(allKeyPoints)} */} ); } export default memo(LineRender); ================================================ FILE: src/Render/Delete/DeleteConfirmation.scss ================================================ .delete-confirmation-container { position: fixed; top: 0; left: 0; z-index: -1000; height: 100vh; width: 100vw; background-color: rgba(255, 255, 255, 0.2); display: flex; justify-content: center; align-items: center; opacity: 0; transition: 0.3s ease-in-out opacity, 0.3s ease-in-out z-index; &.before-animated, &.before-disappear { backdrop-filter: blur(1px); } &.animated { opacity: 1; backdrop-filter: blur(10px); z-index: 1000; } .delete-confirmation { text-align: center; .title { font-weight: 500; font-size: 36px; padding-left: 13px; } .sub-title { font-weight: 500; font-size: 18px; } .preview { margin-top: 74.2px; display: inline-block; // border-top: 1px solid black; width: fit-content; // padding: 0 40px; text-align: center; position: relative; // display: flex; // align-items: center; // justify-content: center; // &>div{ // transform: translateY(-50%); // } .preview-content { display: flex; justify-content: center; align-items: center; .icon { display: inline-block; margin-left: 40px; .line { // width: 35.5px; margin-right: 35.5px; .sign-input { // width: 35.5px; * { min-width: 35.5px; height: 35.5px; border: none; border-radius: 8.87px; background-color: #ea0b2a; font-size: 23.25px; color: white; text-align: center; } } } .station { display: flex; justify-content: center; align-items: center; margin-right: 25.8px; svg { &.square { zoom: 0.9; } &.triangle { zoom: 1.05; } &.start { zoom: 1.15; } &.hexagon { zoom: 0.9; } &.pentagon { zoom: 1.1; } &.diamond { zoom: 1.15; } &.leaf { zoom: 0.9; } &.ginkgo { zoom: 0.8; } } } } .text { margin-right: 40px; display: inline-block; font-weight: 500; font-size: 18px; line-height: 35.5px; // vertical-align: middle; } } .delete-line { border-top: 1px solid #ea0b2a; position: absolute; top: 50%; width: 100%; transition: 0.3s ease-in-out; // animation-delay: 5s; transition-delay: 0.3s; } } .delete { font-weight: 500; font-size: 18px; margin-top: 104.8px; cursor: pointer; color: #ea0b2a; } .back { margin-top: 16.12px; font-weight: 500; font-size: 18px; cursor: pointer; } } } ================================================ FILE: src/Render/Delete/DeleteConfirmation.tsx ================================================ import React, { Dispatch, SetStateAction, forwardRef, useEffect, useImperativeHandle, useState, } from "react"; import { LineProps, StationProps } from "../../Data/UserData"; import "./DeleteConfirmation.scss"; import { AutoGrowthInput } from "../../Common/AutoGrowthInput"; import shapes from "../../Resource/Shape/shape"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; import i18n from "../../i18n/config"; export interface showConfirmationInterface { ( { line, station, stationIndex, }: { line?: LineProps; station?: StationProps; stationIndex?: number }, callback?: any ): void; } enum ShowMode { none, beforeAnimate, animated, beforeDisappear, } export const DeleteConfirmation = forwardRef(function ( {}: any, ref: React.Ref | undefined ) { const [show, setShow] = useState(ShowMode.none); // const [title, setTitle] = useState(); // const [subTitle, setSubTitle] = useState(); const [line, setLine] = useState(); const [station, setStation] = useState(); const [stationIndex, setStationIndex] = useState(); const [callback, setCallback] = useState(() => () => {}); const {t} = useTranslation(); //@ts-ignore window.setShow = setShow const showWithAnimate = () => { setShow(ShowMode.beforeAnimate); setTimeout(() => setShow(ShowMode.animated)); }; const disappearWithAnimate = () => { setShow(ShowMode.beforeDisappear); setTimeout(() => setShow(ShowMode.none),300); }; const showConfirmation: showConfirmationInterface = ( { line, station, stationIndex }, callback ) => { showWithAnimate(); setLine(line); setStation(station); setCallback(() => callback); setStationIndex(stationIndex); }; useImperativeHandle( ref, () => { return { showConfirmation, }; }, [] ); const remove = line && station; const deleteText = remove ? t('delete.remove') : t('delete.delete'); const title = t('que-shi-yao-deletetext-ma', {deleteText}); const { stationName, shape } = station || {}; const { lineName, sign, color } = line || {}; const index = stationIndex ? stationIndex + 1 : 1; const getOrdinalSuffix = (number: number)=> { const suffixes = ["th", "st", "nd", "rd"]; const v = number % 100; return number + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]); } const subTitle = t('delete.subtile', {lineName, index:i18n.language === 'en-US'? getOrdinalSuffix(index):index}); return (
{title}
{remove ?
{subTitle}
: <>}
{line && !station ? (
) : (
{ //@ts-ignore shapes[shape] }
)}
{stationName || lineName}
{ disappearWithAnimate(); if (typeof callback === "function") callback(); }} > {deleteText}
{t('delete.cancel')}
); }); ================================================ FILE: src/Render/ErrorFallback/ErrorFallback.scss ================================================ .error-layer { text-align: center; .title { margin-top: 20vh; font-size: 36px; font-weight: 1000; } .sub-title { margin-top: 0.5vh; font-size: 18px; font-weight: 500; } .error-btn { span{ margin-left: 55px; text-align: left; } user-select: none; font-size: 18px; font-weight: 500; cursor: pointer; --gap: min(20px, 2vw); position: relative; svg{ position: absolute; left: calc(var(--gap) + 20px); } &.recover-from-cache { margin-top: 12vh; background-color: rgb(234, 11, 42, 0.09); color: #ea0b2a; svg { width: 20px; } } &.export-from-cache { margin-top: 0.5vh; background-color: #D9D9D9; svg { width: 16px; padding: 0 2px; } } padding: 20px 30px; width: 315px; margin: auto; border-radius: 10px; display: flex; justify-content: left; align-items: center; } .export-error{ position: absolute; bottom: 10vh; margin: auto; left: 0; right: 0; font-size: 18px; font-weight: 500; .export-error-file{ color: #ea0b2a; cursor: pointer; } } } ================================================ FILE: src/Render/ErrorFallback/ErrorFallback.tsx ================================================ import React from "react"; import "./ErrorFallback.scss"; import { ReactComponent as RecoverIcon } from "../../Resource/Icon/clock.arrow.circlepath.svg"; import { ReactComponent as ExportIcon } from "../../Resource/Icon/export.svg"; import { exportFile, exportJson } from "../../Common/util"; import moment from "moment"; import { useTranslation } from "react-i18next"; export const ErrorFallback = ({ error, resetErrorBoundary, }: { error: Error; resetErrorBoundary: () => void; }) => { const exportFile = (key: string) => { const current = localStorage.getItem(key); exportJson( current!, `recovery-${key}-${moment().format("YYYY-MM-DD_HH-mm-ss")}.json` ); }; const { t } = useTranslation(); return (
{t('error.metError')}
{t('error.dontWorry')}
{t('error.recoverFromCache')}
exportFile("last")} > {t('error.exportNoErrorFile')}
{t('error.orYouCould')} exportFile("current")} > {t('error.exportError')}
{t('error.sendAuthor')}
); }; ================================================ FILE: src/Render/Header/Component/OpacityControl.scss ================================================ .tool { position: relative; display: inline-block; cursor: pointer; perspective: 1000px; perspective-origin: 0 50%; &:has(.slider-container) { z-index: 600; } .slider-container { transition: transform 0s, transform 0.3s ease-in-out, all 0.3s ease-in-out; &.show { opacity: 1; pointer-events: unset; top: 100%; transform: scale(calc(1 + var(--value) * 0.075)) rotate3d(0, 1, 0, 0deg); } opacity: 0; pointer-events: none; position: absolute; top: 50%; left: 0; margin-top: 8px; background: rgba(255, 255, 255, 0.136); box-shadow: 0 0 100px rgba(#000000, 0.15); // border: #ea0b2a 1px solid; z-index: 600; width: 400%; max-width: 100vw; overflow: hidden; height: 40px; border-radius: 5px; backdrop-filter: blur(20px); transform: scale(calc(1 + var(--value) * 0.075)) rotate3d(0, 1, 0, var(--deg)); transform-origin: 0 0; .opacity-text { position: absolute; top: 0; bottom: 0; margin: auto; pointer-events: none; display: flex; justify-content: center; align-items: center; margin-left: 20px; font-size: 14px; opacity: var(--value); span { margin-left: 2px; font-size: 10px; font-weight: 600; vertical-align: baseline; margin-top: 2.5px; } } .slider { appearance: none; width: 100%; height: 100%; appearance: none; margin: 0; background-color: transparent; background-image: repeating-linear-gradient( to right, transparent, transparent calc(18% - 1px), #05051a1c 18% ); &::-webkit-slider-thumb { box-shadow: -20rem 0 0 20rem rgba(#ea0b2a, 0.2); cursor: col-resize; background-color: rgba(#ea0b2a, 0.2); } @supports not (-webkit-touch-callout: none) { &::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 0; } } @supports (-webkit-min-device-pixel-ratio: 0) { &::-webkit-slider-thumb { -webkit-appearance: auto; appearance: auto; width: auto; } } &::-webkit-slider-runnable-track { // pointer-events: none; } } } } ================================================ FILE: src/Render/Header/Component/OpacityControl.tsx ================================================ import React, { useState, useRef, useEffect, CSSProperties, useLayoutEffect, } from "react"; import "./OpacityControl.scss"; import classNames from "classnames"; import { browserInfo } from "../../../Common/util"; import { useTranslation } from "react-i18next"; interface OpacityControlProps { opacity: number; setOpacity: (value: number) => void; } const OpacityControl: React.FC = ({ opacity, setOpacity, }) => { const [showSlider, setShowSlider] = useState(false); const sliderRef = useRef(null); const inputRef = useRef(null); const toolRef = useRef(null); const { t } = useTranslation(); const handleClick = () => { setShowSlider(!showSlider); }; const handleSliderChange = (event: React.ChangeEvent) => { setOpacity(Number(event.target.value)); }; useEffect(() => { if (showSlider && sliderRef.current && toolRef.current) { const toolRect = toolRef.current.getBoundingClientRect(); const viewportWidth = window.innerWidth; if (toolRect.left + 230 > viewportWidth) { sliderRef.current.style.left = "unset"; sliderRef.current.style.right = "0"; (sliderRef.current.style as any)['--deg'] = "-25deg"; sliderRef.current.style.transformOrigin = "100% 50%"; toolRef.current.style.perspectiveOrigin = "100% 50%"; } else { sliderRef.current.style.left = "0"; sliderRef.current.style.transformOrigin = "0 50%"; toolRef.current.style.perspectiveOrigin = "0 50%"; (sliderRef.current.style as any)['--deg'] = "25deg"; } } }, [showSlider]); useLayoutEffect(() => { if (sliderRef.current) { sliderRef.current.style.transition = "0.3s ease-in-out"; } }, [showSlider]); useEffect(() => { const handleClickCapture = (event: TouchEvent | MouseEvent) => { if (event.target !== inputRef.current && event.target !== toolRef.current) { console.log("focusout"); setShowSlider(false); } }; const resize = () => setShowSlider(false); document.addEventListener("touchstart", handleClickCapture, true); document.addEventListener("click", handleClickCapture, true); document.addEventListener("resize",resize); return () => { document.removeEventListener("touchstart", handleClickCapture, true); document.addEventListener("click", handleClickCapture, true); document.removeEventListener("resize",resize); }; }, []); const { engine } = browserInfo; const safari = engine.name === "WebKit"; return (
{t('opacity')} {
e.stopPropagation()} style={{ "--value": opacity } as CSSProperties} onTransitionEnd={() => { if (showSlider && sliderRef.current) { sliderRef.current.style.transition = "none"; } }} > {Math.floor(opacity * 100)} % e.stopPropagation()} style={{ "--value": opacity , "--safari": safari} as CSSProperties} onBlur={() => setShowSlider(false)} />
}
); }; export default OpacityControl; ================================================ FILE: src/Render/Header/Component/ShapeSelector.scss ================================================ .tool { position: relative; display: inline-block; cursor: pointer; perspective: 1000px; perspective-origin: 0 50%; &:has(.shape-selector-container) { z-index: 600; } .shape-selector-container { .color-detail { .color-detail-choosing { box-sizing: border-box; padding: 10px 10px; width: 222.75px; display: grid; border: white 1px solid; grid-template-columns: repeat(4, 25%); grid-template-rows: repeat(3, 33.33%); // grid-row-gap: 9px; // grid-column-gap: 6px; .shape-container { height: 46.13px; display: flex; justify-content: center; align-items: center; border-left: 1px black dotted; border-bottom: 1px black solid; transform: translate(-1px, 1px); cursor: pointer; &.left { border-left: none; } &.bottom { border-bottom: none; } .shape-preview { width: 19.5px; height: 19.5px; // border-radius: 50%; // border: 2px solid; display: flex; justify-content: center; align-items: center; &.shape-selected { svg { * { stroke: #ea0b2a; } } } &.square { zoom: 0.9; } &.triangle { zoom: 1.05; } &.start { zoom: 1.15; } &.hexagon { zoom: 0.9; } &.pentagon { zoom: 1.1; } &.diamond{ zoom: 1.15; } &.leaf{ zoom: 0.9; } } } } } transition: transform 0s, transform 0.3s ease-in-out, all 0.3s ease-in-out; &.show { opacity: 1; pointer-events: unset; top: 100%; transform: scale(calc(1 + var(--value) * 0.075)) rotate3d(0, 1, 0, 0deg); } opacity: 0; pointer-events: none; position: absolute; top: 50%; left: 0; margin-top: 8px; background: rgba(255, 255, 255, 0.136); box-shadow: 0 0 100px rgba(#000000, 0.15); // border: #ea0b2a 1px solid; z-index: 600; width: 222.75px; max-width: 100vw; overflow: hidden; height: 162px; border-radius: 5px; backdrop-filter: blur(20px); transform: scale(calc(1 + var(--value) * 0.075)) rotate3d(0, 1, 0, var(--deg)); transform-origin: 0 0; .opacity-text { position: absolute; top: 0; bottom: 0; margin: auto; pointer-events: none; display: flex; justify-content: center; align-items: center; margin-left: 20px; font-size: 14px; opacity: var(--value); span { margin-left: 2px; font-size: 10px; font-weight: 600; vertical-align: baseline; margin-top: 2.5px; } } .slider { appearance: none; width: 100%; height: 100%; appearance: none; margin: 0; background-color: transparent; background-image: repeating-linear-gradient( to right, transparent, transparent calc(18% - 1px), #05051a1c 18% ); &::-webkit-slider-thumb { box-shadow: -20rem 0 0 20rem rgba(#ea0b2a, 0.2); cursor: col-resize; background-color: rgba(#ea0b2a, 0.2); } @supports not (-webkit-touch-callout: none) { &::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 0; } } @supports (-webkit-min-device-pixel-ratio: 0) { &::-webkit-slider-thumb { -webkit-appearance: auto; appearance: auto; width: auto; } } &::-webkit-slider-runnable-track { // pointer-events: none; } } } } ================================================ FILE: src/Render/Header/Component/ShapeSelector.tsx ================================================ import React, { useState, useRef, useEffect, CSSProperties, useLayoutEffect, SetStateAction, Dispatch, } from "react"; import "./ShapeSelector.scss"; import classNames from "classnames"; import { browserInfo } from "../../../Common/util"; import shapes from "../../../Resource/Shape/shape"; import { Shape } from "../../../Data/Shape"; import { useTranslation } from "react-i18next"; interface OpacityControlProps { defaultShape: string; setDefaultShape: Dispatch>; } const ShapeSelector: React.FC = ({ defaultShape, setDefaultShape, }) => { const [showSlider, setShowSlider] = useState(false); const sliderRef = useRef(null); const toolRef = useRef(null); const { t } = useTranslation(); const handleClick = () => { setShowSlider(!showSlider); }; useEffect(() => { if (showSlider && sliderRef.current && toolRef.current) { const toolRect = toolRef.current.getBoundingClientRect(); const viewportWidth = window.innerWidth; if (toolRect.left + 230 > viewportWidth) { sliderRef.current.style.left = "unset"; sliderRef.current.style.right = "0"; (sliderRef.current.style as any)["--deg"] = "-25deg"; sliderRef.current.style.transformOrigin = "100% 50%"; toolRef.current.style.perspectiveOrigin = "100% 50%"; } else { sliderRef.current.style.left = "0"; sliderRef.current.style.transformOrigin = "0 50%"; toolRef.current.style.perspectiveOrigin = "0 50%"; (sliderRef.current.style as any)["--deg"] = "25deg"; } } }, [showSlider]); useLayoutEffect(() => { if (sliderRef.current) { sliderRef.current.style.transition = "0.3s ease-in-out"; } }, [showSlider]); useEffect(() => { const handleClickCapture = (event: TouchEvent | MouseEvent) => { if (!toolRef.current?.contains(event.target as Node)) { console.log("focusout"); setShowSlider(false); } }; const resize = () => setShowSlider(false); document.addEventListener("touchstart", handleClickCapture, true); document.addEventListener("click", handleClickCapture, true); document.addEventListener("resize", resize); return () => { document.removeEventListener("touchstart", handleClickCapture, true); document.addEventListener("click", handleClickCapture, true); document.removeEventListener("resize", resize); }; }, []); const { engine } = browserInfo; const safari = engine.name === "WebKit"; const column = 4; const row = 3; const grid = new Array(column * row).fill(0); Object.keys(shapes).forEach((shape, index) => (grid[index] = shape)); return (
{ t(`shape.${defaultShape}`) } {
e.stopPropagation()} style={{} as CSSProperties} onTransitionEnd={() => { if (showSlider && sliderRef.current) { sliderRef.current.style.transition = "none"; } }} >
{grid.map((shape, index) => { const left = index % column === 0; const bottom = Math.floor(index / column) === row - 1; // console.log({ shape }); return (
{ if (shape) setDefaultShape(shape); }} >
{ //@ts-ignore shapes[shape] }
); })}
}
); }; export default ShapeSelector; ================================================ FILE: src/Render/Header/Menu.scss ================================================ .menu { &.page-menu { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; z-index: 500; background-color: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px); .title { .tools { margin-left: -50px; .tool { margin-left: -5px; opacity: 0; } } } .dots { width: calc(100vw - 51.6px - 51.6px); transition: 0.3s ease-in-out opacity, 0.3s ease-in-out width; opacity: 1; } .menus { transition: 0.3s ease-in-out opacity; opacity: 1; .columns { .column { @media (max-width: 710px) { margin-top: 28.5px; opacity: 1; } } } } } &.page-title { // position: fixed; // top: 0; // left: 0; // height: 100vh; // width: 100vw; .title { .click-panel { cursor: pointer; } .tools { margin-left: -50px; .tool { margin-left: -5px; opacity: 0; } } } user-select: none; z-index: -500; .dots { margin-top: 100.66px; position: fixed; opacity: 1; width: 0; transition: 0.3s ease-in-out opacity, 0.3s ease-in-out width; } .menus { margin-top: 101.66px; position: fixed; opacity: 0; transition: 0.3s ease-in-out opacity; .columns { justify-content: space-between; transition: 0.3s ease-in-out width; width: calc(70vw - 51.6px - 51.6px); } } } &.page-tools { .title { } .dots { margin-top: 100.66px; position: fixed; opacity: 1; width: 0; transition: 0.3s ease-in-out opacity, 0.3s ease-in-out width; } .menus { margin-top: 101.66px; position: fixed; opacity: 0; transition: 0.3s ease-in-out opacity; .columns { justify-content: space-between; transition: 0.3s ease-in-out width; width: calc(70vw - 51.6px - 51.6px); } } } .title { position: fixed; margin: 35.5px 0 0 51.6px; z-index: 500; .auto-growth-container { display: inline-block; * { font-size: 36px; font-weight: 1000; } input { // background-color: rgba(255, 255, 255, 0.901); // backdrop-filter: blur(10px); // padding-left: 18px; // margin-left: -18px; } } .tools { @media (max-width: 710px) { // display: block !important; margin-left: -21.77px; .tool { margin-top: 5px; &:first-child { // display: none; } } } display: inline-block; transition: 0.3s ease-in-out; .tool { transition: 0.3s ease-in-out; opacity: 1; display: inline-block; font-size: 18px; font-weight: 500; color: #ea0b2a; &.tool-title{ color: #00A0E8; cursor:auto; } margin-left: 21.77px; cursor: pointer; &.disabled { color: #ea0b2a55; cursor:auto; } svg{ vertical-align: text-bottom; line-height: 26px; height: 26px; [fill="black"]{ fill: rgba(234, 11, 42, 0.3333333333) !important; fill-opacity: 1 !important; } } } } } .dots { // margin-top: 20.16px; border-bottom: dotted black 1px; margin: 100.66px 51.6px 0 51.6px; } .menus { height: calc(100vh - 101px); overflow-y: overlay; overflow-x: hidden; &::-webkit-scrollbar { display: none; } .columns { margin: 0 51.6px 0 51.6px; display: flex; @media (max-width: 710px) { display: grid; } justify-content: space-between; transition: 0.3s ease-in-out width; width: calc(100vw - 51.6px - 51.6px); .column { margin-top: 25.8px; display: inline-block; vertical-align: top; width: 20vw; transition: 0.3s ease-in-out; @media (max-width: 710px) { width: auto; margin-top: -15px; opacity: 0; &:last-child{ padding-bottom: 150px; } } .column-title { font-size: 36px; font-weight: 300; } .column-items { // margin-top: 17px; .column-item { width: fit-content; margin-top: 17px; font-size: 18px; font-weight: 500; color: #ea0b2a; cursor: pointer; &.sub-menu{ margin-top: 10px; margin-left: 10px; .sub-item{ margin-right: 10px; } } &.small{ font-size: small; } &.author{ margin-top: 4px; &:hover{ // text-decoration: underline; } } &.friend{ position: relative; svg{ margin-left: 5px; position: absolute; margin-top: 3px; width: 17px; // height: 25px; // vertical-align: bottom; } } } } } } .notice{ position: fixed; left: 0; bottom: 0; } } } ================================================ FILE: src/Render/Header/Menu.tsx ================================================ import React, { Dispatch, SetStateAction, forwardRef, useEffect, useImperativeHandle, useRef, useState, } from "react"; import { ReactComponent as ShareIcon } from "../../Resource/Icon/share.svg"; import { AutoGrowthInput } from "../../Common/AutoGrowthInput"; import "./Menu.scss"; import classNames from "classnames"; import { FunctionMode, Mode } from "../../DataStructure/Mode"; import { ChangeSteps, StationProps, UserDataType, addNewStation, addStationFromRecord, deleteStation, dataProcessor, InsertInfo, RecordType, LineChanges, LineProps, ShowNameProps, DrawProps, CardShowing, setDataFromJson, TransformProps, ShowTourProps, PageProps, } from "../../Data/UserData"; import PlusIcon from "../../Resource/Icon/plus"; import { ReactComponent as GlobeIcon } from "../../Resource/Icon/globe.svg"; import { browserInfo, calculateTransform, deleteFileFromIndexedDB, exportFile, exportJson, importFromFile, importImage, mediateMap, readFileFromIndexedDB, storeFileInIndexedDB, stringifyData, } from "../../Common/util"; import moment from "moment"; import { toPng, toJpeg, toBlob, toPixelData, toSvg } from "html-to-image"; import download from "downloadjs"; import { getExistMap } from "../../Common/api"; import OpacityControl from "./Component/OpacityControl"; import ShapeSelector from "./Component/ShapeSelector"; import { useTranslation } from "react-i18next"; import i18n from "../../i18n/config"; type MenuType = { setEditingMode: React.Dispatch>; functionMode: FunctionMode; setFunctionMode: React.Dispatch>; record: RecordType; setRecord: React.Dispatch>; currentRecordIndex: number; setCurrentRecordIndex: React.Dispatch>; data: UserDataType; setData: Dispatch>; insertInfo?: InsertInfo; setInsertInfo: React.Dispatch>; cardShowing: CardShowing; setCardShowing: Dispatch>; saved: boolean; setSaved: Dispatch>; defaultShape: string; setDefaultShape: Dispatch>; } & ShowNameProps & DrawProps & TransformProps & ShowTourProps & PageProps; export const Menu = forwardRef(function ( { setEditingMode, functionMode, setFunctionMode, record, setRecord, currentRecordIndex, setCurrentRecordIndex, data, setData, insertInfo, setInsertInfo, showName, setShowName, autoHiddenName, setAutoHiddenName, setDrawing, drawing, cardShowing, setCardShowing, scale, setScale, translateX, translateY, setTranslateX, setTranslateY, saved, setSaved, setShowTour, page, setPage, defaultShape, setDefaultShape, }: MenuType, ref ) { const [titleEditable, setTitleEditable] = useState(false); const [display, setDisplay] = useState("none"); const inputRef = useRef(null); const [toolsDisPlay, setToolsDisPlay] = useState("none"); const [selectedMap, setSelectedMap] = useState(); const [originalMap, setOriginalMap] = useState(); const [mapData, setMapData] = useState>(new Map()); const undoCondition = currentRecordIndex >= 0; const redoCondition = currentRecordIndex < record.length - 1; const { title } = data; const { t } = useTranslation(); const setTitle = (title: string) => setData({ ...data, title }); const backToTitle = () => { setPage("title"); setTitleEditable(false); setFunctionMode(FunctionMode.normal); setDrawing(false); }; const { engine } = browserInfo; const showTools = (e: React.MouseEvent, functionMode: FunctionMode) => { e.stopPropagation(); setRecord([]); setCurrentRecordIndex(-1); setFunctionMode(functionMode); setTitleEditable(false); setToolsDisPlay(window.innerWidth >= 710 ? "inline-block" : "block"); setTimeout(() => setPage("tools")); }; const importImageClick = (e: React.MouseEvent) => { importImage() .then((file) => { if (file) { calculateTransform(file).then( ({ translateX: x, translateY: y, scale: s }) => { showTools(e, FunctionMode.editingCustomBackgroundPosition); setData((data) => ({ ...data, // backgroundColor: "image", backgroundImage: file, translateX: (x - translateX) / scale, translateY: (y - translateY) / scale, scale: s / scale, })); storeFileInIndexedDB(file, "image"); } ); console.log("图片 Blob 对象:", file); } else { console.log("未选择图片"); } }) .catch((error) => { console.error("导入图片时出错:", error); }); }; const transfromTools = { scale, setScale, translateX, translateY, setTranslateX, setTranslateY, }; useEffect(() => { mediateMap(data, transfromTools); }, []); useImperativeHandle( ref, () => { return { backToTitle, showTools, }; }, [] ); const tools = () => { switch (functionMode) { case FunctionMode.addingStation: { const stationRecords = record as StationProps[]; return ( <>
{currentRecordIndex >= 0 ? t("menu.alreadyAddStations", { count: currentRecordIndex + 1, }) : t("menu.clickBlankToAddStation")}
{ e.stopPropagation(); if (undoCondition) { const station = stationRecords[currentRecordIndex]; const { stationId } = station; deleteStation(data, setData, stationId); setCurrentRecordIndex(currentRecordIndex - 1); } }} > {t("menu.withdraw")}
{ e.stopPropagation(); if (redoCondition) { const station = stationRecords[currentRecordIndex + 1]; addStationFromRecord(data, setData, station); setCurrentRecordIndex(currentRecordIndex + 1); } }} > {t("menu.redo")}
{ setPage("title"); setTitleEditable(false); }} > {t("menu.done")}
); } case FunctionMode.dragingStation: { const changeRecords = record as ChangeSteps[]; return ( <>
{currentRecordIndex >= 0 ? t("menu.alreadyModified", { count: currentRecordIndex + 1 }) : t("menu.dragToChangePoistion")}
{ e.stopPropagation(); if (undoCondition) { const change = changeRecords[currentRecordIndex]; const { stationId, fromX, fromY } = change; const { setStationPosition } = dataProcessor( stationId, setData, data ); setStationPosition(fromX, fromY); setCurrentRecordIndex(currentRecordIndex - 1); } }} > {t("menu.withdraw")}
{ e.stopPropagation(); if (redoCondition) { const change = changeRecords[currentRecordIndex + 1]; const { stationId, toX, toY } = change; const { setStationPosition } = dataProcessor( stationId, setData, data ); setStationPosition(toX, toY); setCurrentRecordIndex(currentRecordIndex + 1); } }} > {t("menu.redo")}
{ setPage("title"); setTitleEditable(false); }} > {t("menu.done")}
); } case FunctionMode.lineEditing: { return ( <>
{t("menu.chooseALineFirst")}“
{ setPage("title"); setTitleEditable(false); }} > {t("menu.done")}
); } case FunctionMode.backgroundEditing: { const { backgroundColor = "#ffffff", backgroundImage } = data; const setColor = (color: string) => {setData((data) => ({ ...data, backgroundColor: color })); }; return ( <>
{t("menu.backgroudSetting")}
setColor("#ffffff")} > {t("menu.white")}
setColor("#fffee0")} > {t("menu.lightYellow")}
setColor("transparent")} > {t("menu.transparent")}
setColor(e.currentTarget.value)} />
{backgroundImage ? (
{ showTools(e, FunctionMode.editingCustomBackgroundPosition); }} > {t("menu.changeBackgroudImage")}...
) : (
{t("menu.importBackgroudImage")}...
)}
{ setPage("title"); setTitleEditable(false); }} > {t("menu.done")}
); } case FunctionMode.customBackground: case FunctionMode.editingCustomBackgroundPosition: { const { backgroundColor, backgroundImage, opacity = 1 } = data; const setColor = (color: string) => setData((data) => ({ ...data, backgroundColor: color })); const setOpacity = (opacity: number) => setData((data) => ({ ...data, opacity })); return ( <>
{t("menu.backgroudSetting")} / {t("menu.changeBackgroudImage")}
{t("menu.importImage")}
{/* {functionMode === FunctionMode.editingCustomBackgroundPosition ? (
showTools(e, FunctionMode.customBackground)}>调整地图
) : (
showTools(e, FunctionMode.editingCustomBackgroundPosition)}>调整位置与缩放
)} */}
{ showTools(e, FunctionMode.customBackground); setData((data) => ({ ...data, backgroundImage: undefined, })); deleteFileFromIndexedDB("image"); }} > {t("menu.deleteImage")}
showTools(e, FunctionMode.backgroundEditing)} > {t("menu.back")}
); } case FunctionMode.selectingStation: { const { insertIndex, line } = insertInfo!; const { lineName } = line; const changeRecords = record as LineChanges[]; return ( <>
{t("menu.clickAddStationToLine", { lineName, insertIndex: insertIndex + 1, })}
{ e.stopPropagation(); if (undoCondition) { const change = changeRecords[currentRecordIndex]; const { stationId, lineId, stationIndex } = change; const { removeStationFromLine } = dataProcessor( stationId, setData, data ); removeStationFromLine(lineId, stationIndex); setInsertInfo({ insertIndex: insertIndex === 0 ? 0 : insertIndex - 1, line, }); setCurrentRecordIndex(currentRecordIndex - 1); } }} > {t("menu.withdraw")}
{ e.stopPropagation(); if (redoCondition) { const change = changeRecords[currentRecordIndex + 1]; const { stationId, lineId, stationIndex } = change; const { addStationToLine } = dataProcessor( lineId, setData, data ); addStationToLine(stationId, stationIndex); setCurrentRecordIndex(currentRecordIndex + 1); setInsertInfo({ insertIndex: insertIndex === 0 ? 0 : insertIndex + 1, line, }); } }} > {t("menu.redo")}
{ setPage("title"); setTitleEditable(false); setInsertInfo({ insertIndex: -1, line }); }} > {t("menu.done")}
); } case FunctionMode.choosingExistMap: { const existingMap = [ { name: t("shang-hai"), id: "shanghai", webkit: true }, { name: t("bei-jing"), id: "beijing" }, { name: t("guang-zhou"), id: "guangzhou", webkit: true }, { name: t("shen-zhen"), id: "shenzhen", webkit: true }, { name: t("xiang-gang"), id: "hongkong", webkit: true }, { name: t("chang-sha"), id: "changsha", webkit: true }, { name: t("tian-jin"), id: "tianjing", webkit: true }, ]; const webkit = engine.name === "WebKit"; return ( <>
{t("menu.chooseAMap")}
{existingMap .filter((x) => (webkit ? x.webkit : true)) .map(({ name, id }) => { return (
{ let data = mapData.get(id); if (!data) { const res = await getExistMap(id); data = setDataFromJson(setData, res); mapData.set(id, data); } setData({ ...data, title }); mediateMap(data, transfromTools); setSelectedMap(id); }} className={classNames({ tool: 1, disabled: selectedMap !== id, [id]: 1, })} > {name}
); })} {selectedMap ? (
{ setPage("title"); setTitleEditable(false); const data = mapData.get(selectedMap!); if (data) setData(data); }} > {t("menu.useTemplate", { name: existingMap.find((x) => x.id === selectedMap)?.name, })}
) : ( <> )}
{ setData(originalMap!); mediateMap(originalMap!, transfromTools); setPage("title"); setTitleEditable(false); }} > {t("menu.cancel")}
); } } }; useEffect(() => { if (drawing) { toPng(document.querySelector(".transform-layer")! as HTMLElement).then( (dataUrl) => { setDrawing(false); download( dataUrl, `${title}-${moment().format("YYYY-MM-DD_HH-mm-ss")}.png` ); } ); } }, [drawing]); return (
{ if (page === "title" || page === "tools") setDisplay("none"); }} >
e.stopPropagation()}> { e.stopPropagation(); if (page === "title" || page === "tools") { setDisplay("block"); setTimeout(() => setPage("menu")); setTitleEditable(true); } }} onInput={(e) => setTitle(e.currentTarget.value)} ref={inputRef} style={{ cursor: page === "title" ? "pointer" : page === "tools" ? "default" : titleEditable ? "auto" : "default", }} disabled={!titleEditable} />
{ if (page !== "tools") { setToolsDisPlay("none"); setFunctionMode(FunctionMode.normal); } }} > {tools()}
{t("menu.station")}
showTools(e, FunctionMode.dragingStation)} > {t("menu.changeStationPosition")}...
{ e.stopPropagation(); setShowName(!showName); }} > {showName ? t("menu.hidden") : t("menu.show")} {t("menu.stationName")}...
{showName ? (
{ e.stopPropagation(); setAutoHiddenName(!autoHiddenName); }} > {autoHiddenName ? t("menu.turnOff") : t("menu.turnOn")} {t("menu.AutoHidden")}...
) : ( <> )}
{t("menu.line")}
showTools(e, FunctionMode.lineEditing)} > {t("menu.insertStation")}
{ mediateMap(data, transfromTools); }} > {t("menu.mediateMap")}
showTools(e, FunctionMode.backgroundEditing)} > {t("menu.addBackgroundImage")}
{t("menu.data")}
{ const current = localStorage.getItem("current"); if (current && !saved) exportJson( current!, `${title}-autosave-${moment().format( "YYYY-MM-DD_HH-mm-ss" )}.json` ); setData({ stations: new Map(), lines: new Map(), title: t("menu.newMap"), }); showTools(e, FunctionMode.addingStation); }} > {t("menu.addNewMap")}
{ setSelectedMap(""); setOriginalMap(data); showTools(e, FunctionMode.choosingExistMap); }} > {t("menu.addFromTemplate")}
{ e.stopPropagation(); importFromFile().then((res) => { const data = setDataFromJson(setData, res); mediateMap(data, transfromTools); }); }} > {t("menu.importFile")}
{engine.name === "WebKit" ? ( <> ) : (
{ e.stopPropagation(); setDrawing(true); }} > {t("menu.exportAsImage")}
)}
{ e.stopPropagation(); setDrawing(true); setTimeout(() => { toSvg( document.querySelector(".transform-layer")! as HTMLElement ).then((dataUrl) => { const svg = decodeURIComponent( dataUrl.replace( /data:image\/svg\+xml;charset=utf-8,/, "" ) ); console.log(svg); exportFile( svg!, `${title}-${moment().format( "YYYY-MM-DD_HH-mm-ss" )}.svg`, "image/svg+xml" ); }); }, 300); }} > {t("menu.exportAsSVG")}
{ e.stopPropagation(); const current = stringifyData(data); setSaved(true); exportJson( current!, `${title}-${moment().format("YYYY-MM-DD_HH-mm-ss")}.json` ); }} > {t("menu.exportAsFile")}
{ e.stopPropagation(); const current = localStorage.getItem("current"); if (current) { const data = setDataFromJson(setData, current); readFileFromIndexedDB("image") .then((file) => { setData((data) => ({ ...data, // backgroundColor: "image", backgroundImage: file as File, })); }) .catch((e) => { console.error(e); }); mediateMap(data, transfromTools); } }} > {t("menu.recoverData")}
{ e.stopPropagation(); const current = localStorage.getItem("last"); exportJson( current!, `${title}-recovery-${moment().format( "YYYY-MM-DD_HH-mm-ss" )}.json` ); }} > {t("menu.exportRecoveryData")}
{t("menu.about")}
{ e.stopPropagation(); window.open( "https://github.com/RyanEdo/mini-metro-web", "_blank" ); }} > {t("menu.projectAddress")}
{ if (window.innerWidth >= 710) setShowTour(true); else { e.stopPropagation(); window.open( "https://www.bilibili.com/video/BV1E4421D7Yn/", "_blank" ); } }} > {t("menu.guide")}
{window.innerWidth >= 710 ? (
{ e.stopPropagation(); window.open( "https://www.bilibili.com/video/BV1E4421D7Yn/", "_blank" ); }} > {t("menu.videoGuild")}
) : ( <> )}
{ e.stopPropagation(); console.log(i18n) i18n.changeLanguage(i18n.language === "zh-CN"? "en-US": "zh-CN"); }} > {t("menu.switchLanguage")}... {/* */}
{ e.stopPropagation(); window.open( "https://railmapgen.org/?utm_source=mini-metro-web", "_blank" ); }} > {t("menu.RMP")}
{t("menu.version")}1.2.1
{ e.stopPropagation(); window.open("https://space.bilibili.com/8217854", "_blank"); }} > {t("menu.author")}
); }); ================================================ FILE: src/Render/Layer/DevelopLayer.scss ================================================ .DevelopLayer { .grid { display: grid; grid-template-columns: repeat(100, 100px); grid-template-rows: repeat(50, 100px); .grid-item { border: 1px solid black; height: 100px; width: 100px; display: flex; justify-content: center; align-items: center; } } } ================================================ FILE: src/Render/Layer/DevelopLayer.tsx ================================================ import React from "react"; import "./DevelopLayer.scss"; import { Point } from "../../DataStructure/Point"; import { Station } from "../../DataStructure/Station"; import { Line } from "../../DataStructure/Line"; import LineRender from "../Component/LineRender"; import { CardShowing } from "../../Data/UserData"; function DevelopLayer() { // test line and station const pointA = new Point(200, 200); const pointB = new Point(300, 300); const pointC = new Point(600, 300); const pointD = new Point(800, 300); const pointE = new Point(200, 400); const pointF = new Point(800, 200); const pointG = new Point(500, 500); const A = new Station(pointA); const B = new Station(pointB); B._dev_tag = "B"; const C = new Station(pointC); const D = new Station(pointD); const E = new Station(pointE); const F = new Station(pointF); const G = new Station(pointG); const line1 = new Line(); const line3 = new Line(); const line8 = new Line(); const line9 = new Line(); line1.linkAll([A, B, C, D]); // line3.linkAll([E, B, C, D, F, E]); line8._dev_tag='line8'; line8.linkAll([E, B, C, D]); line9.linkAll([A, E, G,D,F]); // console.log(line1, line3, line8); console.log(A, B, C, D, E, F); const allStationsList = [A, B, C, D, E, F, G]; const allLinesList = [line1, line3, line8, line9]; const renderStations = (allStationsList: Station[]) => { return (
{allStationsList.map((station, index) => (
{String.fromCharCode("A".charCodeAt(0) + index)}
))}
); }; // const renderLines = (allLinesList: Line[], cardShowing: CardShowing, setCardShowing: Dispatch>) => { // return ( //
// {allLinesList.map((line) => { // return ; // })} //
// ); // }; return (
{/*
{new Array(100 * 50).fill(1).map((x, index) => (
{index}
))}
*/} {renderStations(allStationsList)} {/* {renderLines(allLinesList)} */}
); } export default DevelopLayer; ================================================ FILE: src/Render/Layer/RenderLayer.scss ================================================ .station-render { cursor: pointer; .station-shape { height: 30px; width: 30px; // transform: translate(-50%,-50%); display: inline-flex; // align-content: center; justify-content: center; align-items: center; svg { fill: white; // &.square { // zoom: 0.9; // } // &.triangle { // zoom: 1.05; // } // &.start { // zoom: 1.15; // } // &.hexagon { // zoom: 0.9; // } // &.pentagon { // zoom: 1.1; // } &.shadow{ position: absolute; transform: scale(1.5); transform-origin: center; *{ fill: inherit; stroke: inherit; } } } } .station-name { display: inline-block; font-size: 18px; font-weight: 500; // font-size: 30px; // font-weight: 1000; vertical-align: top; // margin-left: 5px; white-space: nowrap; } } ================================================ FILE: src/Render/Layer/RenderLayer.tsx ================================================ import React, { CSSProperties, Dispatch, MouseEventHandler, SetStateAction, useEffect, useLayoutEffect, useState, } from "react"; import DevelopLayer from "./DevelopLayer"; import { LineCard } from "../Card/LineCard"; import { Cards } from "../Card/Cards"; import { CardShowing, ChangeSteps, DrawProps, DrawerSize, InsertInfo, LineChanges, LineProps, RecordType, ShowNameProps, StationProps, UserDataType, dataProcessor, } from "../../Data/UserData"; import { Station } from "../../DataStructure/Station"; import { Point } from "../../DataStructure/Point"; import { mapToArr } from "../../Common/util"; import { Line } from "../../DataStructure/Line"; import LineRender from "../Component/LineRender"; import shapes, { shapesWithStyle } from "../../Resource/Shape/shape"; import "./RenderLayer.scss"; import { FunctionMode, Mode } from "../../DataStructure/Mode"; import { Direct } from "../../DataStructure/Direction"; import { clearHandle, getHandleCommand } from "../../Line/Handle"; import { getAllKeyPoints, getRoundedPoints, generateLineCommand, } from "../../Line/LinePoints"; import classNames from "classnames"; type RenderProps = { data: UserDataType; setData: Dispatch>; functionMode: FunctionMode; record: RecordType; setRecord: React.Dispatch>; currentRecordIndex: number; setCurrentRecordIndex: React.Dispatch>; translateX: number; translateY: number; scale: number; insertInfo?: InsertInfo; setInsertInfo: React.Dispatch>; setFunctionMode: React.Dispatch>; cardShowing: CardShowing; setCardShowing: Dispatch>; editingMode: Mode; } & ShowNameProps & DrawProps & DrawerSize; const buildStations = ( stations: Map ): Map => { const stationMap = new Map(); mapToArr(stations).forEach((station) => { const { position, stationId } = station; const [x, y] = position; const dStation = new Station(new Point(x, y)); dStation.displayStation = station; stationMap.set(stationId, dStation); }); return stationMap; }; const buildLines = ( lines: Map, stationMap: Map ) => { const lineMap = new Map(); mapToArr(lines) .sort((a, b) => a.order - b.order) .forEach((line) => { const { stationIds, lineId, bendFirst } = line; const dLine = new Line(); dLine.displayLine = line; for (let i = 1; i < stationIds.length; i++) { const B = stationMap.get(stationIds[i - 1])!; const C = stationMap.get(stationIds[i])!; dLine.link(B, C, !!bendFirst[i - 1]); } lineMap.set(lineId, dLine); }); return lineMap; }; const renderLines = ( allLinesList: Line[], cardShowing: CardShowing, setCardShowing: Dispatch>, commandMap: Map, data: UserDataType, setData: Dispatch>, { drawing, setDrawing }: DrawProps, { drawerX, drawerY }: DrawerSize ) => { return (
{allLinesList.map((line) => { const command = commandMap.get(line); return ( ); })}
); }; function RenderLayer({ data, setData, functionMode, setFunctionMode, record, setRecord, currentRecordIndex, setCurrentRecordIndex, translateX, translateY, scale, insertInfo, setInsertInfo, cardShowing, setCardShowing, editingMode, showName, setShowName, autoHiddenName, setAutoHiddenName, drawing, setDrawing, drawerX, drawerY, }: RenderProps) { const { lines, stations } = data; const stationMap = buildStations(stations); const lineMap = buildLines(lines, stationMap); const allStationsList = mapToArr(stationMap); const allLinesList = mapToArr(lineMap); const [mouseDown, setMouseDown] = useState(false); const [stationBeingDrag, setStationBeingDrag] = useState(); const mouseUp = (e: Event) => { setMouseDown(false); if (stationBeingDrag) { const { displayStation, position } = stationBeingDrag; const { stationId } = displayStation!; const { x: fromX, y: fromY } = position; const { setStationPosition } = dataProcessor(stationId, setData, data); if (functionMode === FunctionMode.dragingStation && mouseDown) { const { clientX, clientY } = e as MouseEvent; const toX = (clientX - translateX) / scale; const toY = (clientY - translateY) / scale; setStationPosition(toX, toY); const newRecord = record.slice( 0, currentRecordIndex + 1 ) as ChangeSteps[]; setRecord(newRecord.concat([{ stationId, fromX, fromY, toX, toY }])); setCurrentRecordIndex(currentRecordIndex + 1); } setStationBeingDrag(undefined); } }; const mouseMove = (e: Event) => { if (stationBeingDrag) { const { displayStation } = stationBeingDrag; const { stationId } = displayStation!; const { setStationPosition } = dataProcessor(stationId, setData, data); if (functionMode === FunctionMode.dragingStation && mouseDown) { setCardShowing({ stationIds: [stationId] }); const { clientX, clientY } = e as MouseEvent; const x = (clientX - translateX) / scale; const y = (clientY - translateY) / scale; setStationPosition(x, y); } } }; const touchMove = (e: Event) => { if (stationBeingDrag) { const { displayStation } = stationBeingDrag; const { stationId } = displayStation!; const { setStationPosition } = dataProcessor(stationId, setData, data); if (functionMode === FunctionMode.dragingStation && mouseDown) { const { touches } = e as TouchEvent; if (touches.length === 1) { e.stopPropagation(); const touch = touches[0]; const { clientX, clientY } = touch; console.log(clientX, clientY); const x = (clientX - translateX) / scale; const y = (clientY - translateY) / scale; setStationPosition(x, y); } } } }; const touchEnd = (e: Event) => { setMouseDown(false); if (stationBeingDrag) { const { displayStation, position } = stationBeingDrag; const { stationId } = displayStation!; const { x: fromX, y: fromY } = position; const { setStationPosition } = dataProcessor(stationId, setData, data); if (functionMode === FunctionMode.dragingStation && mouseDown) { const { changedTouches } = e as TouchEvent; if (changedTouches.length === 1) { e.stopPropagation(); const touch = changedTouches[0]; const { clientX, clientY } = touch; const toX = (clientX - translateX) / scale; const toY = (clientY - translateY) / scale; console.log("touch end:", clientX, clientY); setStationPosition(toX, toY); const newRecord = record.slice( 0, currentRecordIndex + 1 ) as ChangeSteps[]; setRecord(newRecord.concat([{ stationId, fromX, fromY, toX, toY }])); setCurrentRecordIndex(currentRecordIndex + 1); } } setStationBeingDrag(undefined); } }; useEffect(() => { const scaleLayer = document.querySelector(".ScaleLayer"); scaleLayer?.addEventListener("mouseup", mouseUp); scaleLayer?.addEventListener("mouseleave", mouseUp); scaleLayer?.addEventListener("mousemove", mouseMove); scaleLayer?.addEventListener("touchmove", touchMove); scaleLayer?.addEventListener("touchend", touchEnd); return () => { scaleLayer?.removeEventListener("mouseup", mouseUp); scaleLayer?.removeEventListener("mouseleave", mouseUp); scaleLayer?.removeEventListener("mousemove", mouseMove); scaleLayer?.removeEventListener("touchmove", touchMove); scaleLayer?.removeEventListener("touchend", touchEnd); }; }, [stationBeingDrag]); const [moved, setMoved] = useState(false); const operationStart = ( e: React.MouseEvent | React.TouchEvent, station: Station ) => { setMoved(false); if (functionMode === FunctionMode.dragingStation) { e.stopPropagation(); setMouseDown(true); setStationBeingDrag(station); } }; const renderStations = (allStationsList: Station[]) => { return (
{allStationsList.map((station, index) => { const { displayStation, position } = station; const { x, y } = position; const { stationName, shape, stationId, lineIds: stationLineIds, tagDirection, } = displayStation!; const descend = stations.size - index; const add = async () => { if (!moved) setCardShowing({ stationIds: [stationId] }); if (functionMode === FunctionMode.selectingStation) { const { insertIndex, line } = insertInfo!; const { lineId } = line; const { addStationToLine } = dataProcessor(lineId, setData, data); const added = await addStationToLine(stationId, insertIndex); console.log(insertIndex); if(added) setInsertInfo({ insertIndex: insertIndex ? insertIndex + 1 : 0, line, }); const newRecord = record.slice( 0, currentRecordIndex + 1 ) as LineChanges[]; setRecord( newRecord.concat([ { stationId, lineId, stationIndex: insertIndex }, ]) ); setCurrentRecordIndex(currentRecordIndex + 1); setCardShowing({ stationIds: [stationId], lineIds: [lineId] }); // setFunctionMode(FunctionMode.lineEditing); } }; let nameDirection: Direct = station.getBestDirectionForName(); if (tagDirection || tagDirection === 0) nameDirection = tagDirection; const SQRT1_2 = Math.SQRT1_2; const directionOffset = [ [0, -1], [SQRT1_2, -SQRT1_2], [1, 0], [SQRT1_2, SQRT1_2], [0, 1], [-SQRT1_2, SQRT1_2], [-1, 0], [-SQRT1_2, -SQRT1_2], ]; const k = 20; // distance from station to name const namePosition = directionOffset[nameDirection]; const [dX, dY] = namePosition; const nameX = dX * k + x; const nameY = dY * k + y; const translate = [ [-1, -2], [0, -2], [0, -1], [0, 0], [-1, 0], [-2, 0], [-2, -1], [-2, -2], ]; const [tX, tY] = translate[nameDirection]; const { lineIds, stationIds } = cardShowing; const emphasis = stationIds?.includes(stationId); const getEmphasisColor = (lineIds?: number[]) => Array.isArray(lineIds) && lineIds.length && lines.get(lineIds[0])?.color; const emphasisColor = getEmphasisColor(lineIds) || getEmphasisColor(stationLineIds) || "#00000055"; const nameStyle: CSSProperties = { position: "absolute", left: nameX, top: nameY, transform: `translate(${50 * tX}%,${50 * tY}%)`, }; return (
operationStart(e, station)} onTouchStart={(e) => operationStart(e, station)} className={classNames({ "station-render": 1 , [`station-trigger-descend-${descend}`]: 1})} onTouchMove={() => setMoved(true)} onMouseMove={() => setMoved(true)} onTouchEnd={add} onClick={add} >
{emphasis && //@ts-ignore shapesWithStyle( { fill: emphasisColor, stroke: emphasisColor, // width: 31, // height: 31 }, "shadow" )[shape]} { //@ts-ignore shapesWithStyle({ zIndex: 100 })[shape] }
{stationName}
{/* {String.fromCharCode("A".charCodeAt(0) + index)} */}
); })}
); }; const commandMap = new Map(); allLinesList.map((line: Line) => { const { displayLine } = line; const { color, lineId, subLine } = displayLine!; let command = "", allKeyPoints: Point[] = []; if (line.departureRecord?.nextLineRecord) { allKeyPoints = getAllKeyPoints(line); clearHandle(line); const { startHandleCommand, LQLPoints, endHandleCommand } = getHandleCommand(line, allKeyPoints); const roundedPoints = getRoundedPoints(LQLPoints); const pathCommand = generateLineCommand(roundedPoints); command = startHandleCommand + pathCommand + endHandleCommand; commandMap.set(line, command); } }); const stationComp = renderStations(allStationsList); const lineComp = renderLines( allLinesList, cardShowing, setCardShowing, commandMap, data, setData, { drawing, setDrawing }, { drawerX, drawerY } ); const [debugError, setDebugError] = useState(false); //@ts-ignore window.throwError = () => { setDebugError(true); } if(debugError){ throw new Error(); } return (
{lineComp} {stationComp} {/* */}
); } export default RenderLayer; ================================================ FILE: src/Render/Layer/ScaleLayer.scss ================================================ .ScaleLayer{ width: 100vw; height: 100vh; overflow: hidden; .layer-for-welcome-tour{ position: fixed; z-index: -500; width: 70vw; height: 70vh; left: 15vw; top: 15vw; } .transform-layer{ transform-origin: left top; // never use will-change here, it makes child elements blurry // will-change: transform; } .background-layer{ position: absolute; pointer-events: none; transform-origin: left top; appearance: none; border: none; box-shadow: none; border-image-width: 0; &:not([src]) { opacity: 0 !important; } } } ================================================ FILE: src/Render/Layer/ScaleLayer.tsx ================================================ import React, { CSSProperties, Dispatch, SetStateAction, useMemo, useState, } from "react"; import { FunctionMode, Mode } from "../../DataStructure/Mode"; import RenderLayer from "./RenderLayer"; import { onMouseDown, onMouseLeave, onMouseMove, onMouseUp, onTouchEnd, onTouchMove, onTouchStart, onWheel, } from "../../Grid/Scale"; import "./ScaleLayer.scss"; import { getCursor } from "../../Style/Cursor"; import { Point } from "../../DataStructure/Point"; import { CardShowing, ChangeSteps, DrawProps, InsertInfo, PageProps, RecordType, ShowNameProps, StationProps, TransformProps, UserDataType, } from "../../Data/UserData"; import { browserInfo, getBoundary, mapToArr } from "../../Common/util"; type ScaleLayerProp = { editingMode: Mode; setEditingMode: React.Dispatch>; data: UserDataType; setData: Dispatch>; functionMode: FunctionMode; setFunctionMode: React.Dispatch>; record: RecordType; setRecord: React.Dispatch>; currentRecordIndex: number; setCurrentRecordIndex: React.Dispatch>; insertInfo?: InsertInfo; setInsertInfo: React.Dispatch>; cardShowing: CardShowing; setCardShowing: Dispatch>; defaultShape: string; } & ShowNameProps & DrawProps & TransformProps & PageProps; function ScaleLayer({ editingMode, setEditingMode, data, setData, functionMode, setFunctionMode, record, setRecord, currentRecordIndex, setCurrentRecordIndex, insertInfo, setInsertInfo, cardShowing, setCardShowing, showName, setShowName, autoHiddenName, setAutoHiddenName, drawing, setDrawing, scale: scaleForMap, setScale: setScaleForMap, translateX: translateXForMap, translateY: translateYForMap, setTranslateX: setTranslateXForMap, setTranslateY: setTranslateYForMap, page, defaultShape }: ScaleLayerProp) { // mouseRefPoint // in mouse drag mode: this point record mouse start point const [mouseRefPoint, setMouseRefPoint] = useState(new Point()); // mouseStartTranslate // record translate position when mouse start // translate should add this value when mouse moving const [mouseStartTranslate, setMouseStartTranslate] = useState(new Point()); // touchRefPoint // 1. in touch move mode: this point record touch start point // 2. in touch scale mode: this point record the center position of start touch const [touchRefPoint, setTouchRefPoint] = useState(new Point()); // touchStartDistance // record distance between two finger touch points for scaling operation const [touchStartDistance, setTouchStartDistance] = useState(1); // touchStartScale // record the scale when touch start // the finnal scale will be calculated by this value const [touchStartScale, setTouchStartScale] = useState(1); // touchStartTranslate // record translate position when touch start // translate should add this value when touch moving const [touchStartTranslate, setTouchStartTranslate] = useState(new Point()); // touches or mouse moved, that means user trying to scale or move, not adding station const [moved, setMoved] = useState(false); const { scale: scaleForImage = 1, translateX: translateXForImage = 0, translateY: translateYForImage = 0, } = data; let scale = scaleForMap, translateX = translateXForMap, translateY = translateYForMap; if (functionMode === FunctionMode.editingCustomBackgroundPosition) { scale = scaleForImage * scaleForMap; translateX = translateXForImage * scaleForMap + translateXForMap; translateY = translateYForImage * scaleForMap + translateYForMap; } const getRelativeValue = ( value: number, type: "scale" | "translateX" | "translateY" ) => { switch (type) { case "scale": { return value / scaleForMap; } case "translateX": { return (value - translateXForMap) / scaleForMap; } case "translateY": { return (value - translateYForMap) / scaleForMap; } } }; // translate and scale used for map or background image const createSetter = ( key: "scale" | "translateX" | "translateY", originalSetter: (value: number | ((prev: number) => number)) => void ) => { return functionMode === FunctionMode.editingCustomBackgroundPosition ? (value: number | ((prev: number) => number)) => { if (typeof value === "number") { setData((data: UserDataType) => ({ ...data, [key]: getRelativeValue(value, key), })); } else { setData((data: UserDataType) => ({ ...data, [key]: getRelativeValue(value(data[key]!), key), })); } } : originalSetter; }; const setScale = createSetter("scale", setScaleForMap); const setTranslateX = createSetter("translateX", setTranslateXForMap); const setTranslateY = createSetter("translateY", setTranslateYForMap); // const scaleRelative = scaleForImage/scale; // const translateXRelative = translateX - translateXForImage; // const translateYRelative = translateY - translateYForImage; const { stations, backgroundColor, backgroundImage, opacity } = data; // const backgroundColor = // _backgroundColor === "image" ? "transparent" : _backgroundColor; const allStationsList = mapToArr(stations); const { minX, minY, maxX, maxY } = getBoundary(data); const drawerX = maxX - minX + 400; const drawerY = maxY - minY + 400; const style: CSSProperties = { backgroundColor, transform: drawing ? `scale(2)` : `translate(${translateXForMap}px,${translateYForMap}px) scale(${scaleForMap})`, width: drawing ? drawerX * 2 : undefined, height: drawing ? drawerY * 2 : undefined, }; const drawingStationMap = new Map(); allStationsList.forEach((station) => { const { stationId } = station; const [x, y] = station.position; const position = [x - minX + 200, y - minY + 200]; drawingStationMap.set(stationId, { ...station, position }); }); const drawingData = { ...data, stations: drawingStationMap }; const { engine } = browserInfo; const webkit = engine.name === "WebKit"; const display = stations.size > 100 && webkit && page === "menu" ? "none" : undefined; const imageSrc = useMemo( () => (backgroundImage ? URL.createObjectURL(backgroundImage) : undefined), [backgroundImage] ); return (
onWheel( event, scale, setScale, translateX, translateY, setTranslateX, setTranslateY, functionMode ) } onMouseDown={(event) => onMouseDown( event, translateX, translateY, setEditingMode, setMouseRefPoint, setMouseStartTranslate, setMoved ) } onMouseUp={(event) => onMouseUp( event, setEditingMode, editingMode, functionMode, data, setData, moved, translateX, translateY, scale, record, setRecord, currentRecordIndex, setCurrentRecordIndex, cardShowing, setCardShowing, defaultShape ) } onMouseLeave={(event) => onMouseLeave(event, setEditingMode)} onMouseMove={(event) => onMouseMove( event, translateX, translateY, setTranslateX, setTranslateY, editingMode, mouseRefPoint, mouseStartTranslate, setMoved ) } onTouchStart={(event) => onTouchStart( event, setEditingMode, setTouchRefPoint, setTouchStartDistance, setTouchStartScale, scale, setTouchStartTranslate, translateX, translateY, setMoved ) } onTouchMove={(event) => onTouchMove( event, editingMode, touchRefPoint, translateX, translateY, setTranslateX, setTranslateY, touchStartDistance, touchStartScale, setScale, touchStartTranslate, setMoved ) } onTouchEnd={(event) => onTouchEnd( event, setEditingMode, editingMode, functionMode, data, setData, moved, translateX, translateY, scale, record, setRecord, currentRecordIndex, setCurrentRecordIndex, cardShowing, setCardShowing, defaultShape ) } style={{ cursor: getCursor(editingMode), backgroundColor }} >
{/* */}
{imageSrc?:<>}
); } export default ScaleLayer; ================================================ FILE: src/Render/Recovery/Recovery.scss ================================================ .App{ perspective: 1000px; perspective-origin: 100vw 105px; z-index: 10000; } .recovery-notification-container { position: fixed; right: 0; top: 0; transform-origin: right center; transition: ease-in-out 0.5s; &.show { transform: rotate3d(0, 1, 0, 0deg); opacity: 1; } &:not(.show) { transform: rotate3d(0, 1, 0, -90deg); opacity: 0; } .recovery-notification { @media screen and (max-width: 520px) { display: none; } position: fixed; top: 30px; right: 30px; width: 363px; height: 75px; display: flex; // justify-content: space-between; align-items: center; background-color: rgb(255, 255, 255, 0.72); box-shadow: 0 0 80px 0 rgba(0, 0, 0, 0.25); backdrop-filter: blur(15px); border-radius: 15px; transition: ease-in-out 0.3s; // zoom: 1.2; &:hover { background-color: rgba(255, 255, 255, 0.001); } .icon { margin: 0 20px; } .icon, .ok, .no { width: 36px; height: 36px; border-radius: 50%; display: flex; justify-content: center; align-items: center; } .ok, .no { cursor: pointer; position: absolute; } .icon, .ok { background-color: rgb(235, 10, 40, 0.1); } .no { background-color: #d9d9d9; right: 20px; } .icon { svg { width: 20px; height: 20px; } } .ok { right: 60px; transition: ease-in-out 0.3s; &:hover { background-color: rgb(235, 10, 40, 0.2); } svg { width: 16px; } } .no { transition: ease-in-out 0.3s; &:hover { background-color: #c9c9c9; } svg { width: 12px; } } .text { .title { font-size: 16px; } .sub-title { font-size: 12px; } font-weight: 500; } } } ================================================ FILE: src/Render/Recovery/Recovery.tsx ================================================ import React, { Dispatch, SetStateAction, useEffect, useRef, useState, } from "react"; import { ReactComponent as RecoverIcon } from "../../Resource/Icon/clock.arrow.circlepath.svg"; import { ReactComponent as OkIcon } from "../../Resource/Icon/ok.svg"; import { ReactComponent as NoIcon } from "../../Resource/Icon/no.svg"; import "./Recovery.scss"; import { setDataFromJson, UserDataType } from "../../Data/UserData"; import { mediateMap, readFileFromIndexedDB } from "../../Common/util"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; export function Recovery({ data, setData, recoveredFromError, setRecoveredFromError, transfromTools, }: { data: UserDataType; setData: Dispatch>; recoveredFromError: boolean; setRecoveredFromError: Dispatch>; transfromTools: { scale: number; setScale: React.Dispatch>; translateX: number; translateY: number; setTranslateX: React.Dispatch>; setTranslateY: React.Dispatch>; }; }) { const {t} = useTranslation(); const notificationRef = useRef(null); const [showNotification, setShowNotification] = useState(false); useEffect(() => { const current = localStorage.getItem("current"); const show = current && !recoveredFromError; setRecoveredFromError(false); setShowNotification(!!show); const handleClickCapture = (event: TouchEvent | MouseEvent) => { if ( notificationRef.current && !notificationRef.current.contains(event.target as Node) ) { console.log("recovery focusout"); setShowNotification(false); } }; document.addEventListener("touchstart", handleClickCapture, true); document.addEventListener("click", handleClickCapture, true); return () => { document.removeEventListener("touchstart", handleClickCapture, true); document.addEventListener("click", handleClickCapture, true); }; }, []); return (
{t('recover.text')}
{t('recover.subTitle')}
{ const current = localStorage.getItem("current"); if (current) { const data = setDataFromJson(setData, current); readFileFromIndexedDB("image") .then((file) => { setData((data) => ({ ...data, // backgroundColor: "image", backgroundImage: file as File, })); }) .catch((e) => { console.error(e); }); mediateMap(data, transfromTools); } setShowNotification(false); }} >
{ setShowNotification(false); }} >
); } ================================================ FILE: src/Resource/Icon/airwave.tsx ================================================ import React from "react"; const AirWaveIcon =({className}:{className?: string})=> ( ); export default AirWaveIcon; ================================================ FILE: src/Resource/Icon/arrow.tsx ================================================ import React from "react"; const arrowIcon = ({ className }: { className?: string }) => ( ); export default arrowIcon; ================================================ FILE: src/Resource/Icon/auto.tsx ================================================ import React from "react"; const AutoIcon = ({ className }: { className?: string }) => ( ); export default AutoIcon; ================================================ FILE: src/Resource/Icon/bolt.tsx ================================================ import React from "react"; const BoltIcon = ({ className }: { className?: string }) => ( ); export default BoltIcon; ================================================ FILE: src/Resource/Icon/edit.tsx ================================================ import React from "react"; const edit =({className}:{className: string})=> ( ); export default edit; ================================================ FILE: src/Resource/Icon/expand.tsx ================================================ import React from "react"; const expand =({className}:{className?: string})=> ( ); export default expand; ================================================ FILE: src/Resource/Icon/export.tsx ================================================ import React from "react"; const ExportIcon = ({ className }: { className?: string }) => ( ); export default ExportIcon; ================================================ FILE: src/Resource/Icon/finished.tsx ================================================ import React from "react"; const FinishedIcon =({className}:{className?: string})=> ( ); export default FinishedIcon; ================================================ FILE: src/Resource/Icon/goto.tsx ================================================ import React from "react"; const GoToIcon = ({ className }: { className?: string }) => ( ); export default GoToIcon; ================================================ FILE: src/Resource/Icon/infinity.tsx ================================================ import React from "react"; const Infinity = ({ className }: { className?: string }) => ( ); export default Infinity; ================================================ FILE: src/Resource/Icon/pageview.tsx ================================================ import React from "react"; const PageViewIcon =({className}:{className?: string})=> ( ); export default PageViewIcon; ================================================ FILE: src/Resource/Icon/plus.tsx ================================================ import React from "react"; const plusIcon = ({ className }: { className?: string }) => ( ); export default plusIcon; ================================================ FILE: src/Resource/Icon/shrink.tsx ================================================ import React from "react"; const shrink =({className}:{className: string})=> ( ); export default shrink; ================================================ FILE: src/Resource/Icon/touch.tsx ================================================ import React from "react"; const TouchIcon = ({ className }: { className?: string }) => ( ); export default TouchIcon; ================================================ FILE: src/Resource/Shape/shape.tsx ================================================ import React, { CSSProperties } from "react"; const shapes = { cicle: ( ), square: ( ), triangle: ( ), start: ( ), pentagon: ( ), hexagon: ( ), cross: ( ), rhombus: ( ), diamond: ( ), leaf: ( ), ginkgo: ( ), }; export const shapesWithStyle = (style?: CSSProperties, className?: string) => ({ cicle: ( ), square: ( ), triangle: ( ), start: ( ), pentagon: ( ), hexagon: ( ), cross: ( ), rhombus: ( ), diamond: ( ), leaf: ( ), ginkgo: ( ), }); export default shapes; ================================================ FILE: src/Style/Cursor.ts ================================================ import { Mode } from "../DataStructure/Mode"; export const getCursor = (mode: Mode)=>{ let cursor; //chrome has bug here, the cursor won't change sometimes while DOM already updated. switch(mode){ case Mode.moving:{ cursor = 'grabbing'; break; } default:{ cursor = 'default'; break; } } return cursor; } ================================================ FILE: src/WelcomeTour/Driver.ts ================================================ import { driver as Driver, Config } from "driver.js"; import {t} from 'i18next'; export const showTour = async (id: string, callback: Function) => { const driver = Driver(); const { getSteps } = await import(/* webpackMode: "eager" */ `./Steps/${id}`); const eventListeners: EventListenerOrEventListenerObject[] = []; const touch = window.ontouchend === null; const eventName =// touch ? "touchend" : "click"; const config: Config = { prevBtnText: t('shang-yi-bu'), doneBtnText: t('wan-cheng'), nextBtnText: t('xia-yi-bu'), progressText: "{{current}} / {{total}}", showProgress: true, allowClose: false, showButtons: ["close"], // onPrevClick:(ele)=>{ // if(ele){ // const element = ele as HTMLElement; // eventListeners.forEach(listener=>{ // element.removeEventListener('click',listener); // }) // driver.movePrevious(); // } // }, onHighlighted: (ele, step, opt) => { const steps = opt.config.steps!; const last = step === steps[steps.length - 1]; const next = steps[opt.state.activeIndex! + 1]; if (ele) { const element = ele as HTMLElement; const moveToNext = () => { const gotoNext = () => { // element.removeEventListener(eventName, moveToNext); element.removeEventListener("click", moveToNext); element.removeEventListener("touchend", moveToNext); if(driver.getActiveStep()===step) driver.moveNext(); }; setTimeout(() => { if (!next || document.querySelector(next.element as string)) { gotoNext(); }else{ setTimeout(()=>{ if (!next || document.querySelector(next.element as string)) gotoNext() },300) } }, 100); }; eventListeners.push(moveToNext); element.addEventListener("click", moveToNext); element.addEventListener("touchend", moveToNext); } }, onNextClick: (ele) => { if (ele) { const element = ele as HTMLElement; element.dispatchEvent( new Event("click", { bubbles: true, cancelable: true }) ); } }, steps: getSteps(driver), onDestroyed: (ele,step) => { if (ele) { const element = ele as HTMLElement; if(step.element!==".tour-btn") element.dispatchEvent( new Event("click", { bubbles: true, cancelable: true }) ); } callback(); }, }; driver.setConfig(config); driver.drive(); }; ================================================ FILE: src/WelcomeTour/HightLights.tsx ================================================ import React from "react"; import Infinity from "../Resource/Icon/infinity"; import AirWaveIcon from "../Resource/Icon/airwave"; import PageViewIcon from "../Resource/Icon/pageview"; import BoltIcon from "../Resource/Icon/bolt"; import TouchIcon from "../Resource/Icon/touch"; import AutoIcon from "../Resource/Icon/auto"; import ExportIcon from "../Resource/Icon/export"; import {t} from 'i18next'; export const hightLights = [ { id:'common-operation', icon: , title: t('wu-xian-zi-chan'), subTitle: t('wu-xian-da-hua-bu'), introText: [ [false, t('zhi-chi'), t('wu-xian-tiao-xian-lu')], [false, t('zhi-chi'), t('wu-xian-ge-zhan-dian')], ], more: t('le-jie-ji-ben-cao-zuo') }, { id:'line-card', icon: , title: t('ling-huo-zou-xian'), subTitle: t('yi-tiao-xian-lu-neng-duo-ci-chuan-guo-tong-yi-zhan'), introText: [ [false, t('zhi-chi'), t('q-zi-zou-xian-yu-zou-xian')], [false, t('zhi-chi-she-zhi'), t('zhi-xian')], ], more: t('le-jie-xian-lu-she-zhi') }, { id:'station-card', icon: , title: t('fang-bian-cha-kan'), subTitle: t('ka-pian-hua-zhan-shi-zhan-dian-yu-xian-lu'), introText: [ [false, t('zhan-dian-xian-lu-hu-xiang'), t('guan-lian')], [true, t('gao-liang'), t("zhan-shi-xuan-zhong-zhan-dian-yu-xian-lu"),] ], more: t('le-jie-zhan-dian-ka-pian') }, { id:'quick-edit', icon: , title: t('gao-xiao-bian-ji'), subTitle: t('lian-xu-chuang-jian-zhan-dian-mo-shi'), introText: [ [true, t('lian-xu-tian-jia-zhan-dian'), t('mo-shi')], [false, t('zhi-chi'), t('che-hui'), t('yu'), t('zhong-zuo')], ], more: t('le-jie-kuai-su-chuang-jian') }, { id:'mobile', icon: , title: t('yi-dong-duan-zhi-chi'), subTitle: t('wan-zheng-de-chu-kong-shi-jian-zhi-chi'), introText: [ [true, t('dan-zhi'), t('tuo-dong'), t('shuang-zhi'), t('suo-fang-di-tu')], [false, t('shi-bie-dong-zuo-yi-tu'), t('jian-shao-wu-cao-zuo')], ], more: t('zai-ping-ban-shang-ti-yan') }, { id:'tag-setting', icon: , title: t('zi-dong-bi-rang'), subTitle: t('zi-dong-tian-jia-pian-yi-zhi'), introText: [ [false, t('gong-xian-xian-lu'), t('bu-hui-zhong-die')], [false, t('zhan-dian-ming-cheng-zi-dong-xuan-ze-bai-fang-wei-zhi')], ], more: t('le-jie-biao-qian-she-zhi') }, { id:'export', icon: , title: t('dao-ru-dao-chu'), subTitle: t('dao-chu-gao-fen-bian-shuai-tu-pian'), introText: [ [false, t('dao-chu'), t('pngsvg-tu-pian')], [false, t('zhi-chi-cong-mo-ban-chuang-jian')], ], more: t('le-jie-dao-ru-dao-chu') }, ]; ================================================ FILE: src/WelcomeTour/Steps/common-operation.ts ================================================ import { DriveStep, Driver } from "driver.js"; import {t} from 'i18next'; export const getSteps = (driver: Driver): DriveStep[] => [ { element: ".ScaleLayer", onHighlighted:()=>{}, popover: { title: t('tour.tryScale'), description: window.ontouchend === null? t('tour.touchScale'): t('tour.mouseScale'), showButtons:['next'], onNextClick:driver.moveNext }, }, { element: ".title .click-panel", popover: { title: t('tour.clickTile'), description: t('tour.clickOpenMenu'), }, }, { element: ".title", popover: { title: t('zai-ci-dian-ji-biao-ti'), description: t('ke-yi-xiu-gai-biao-ti'), }, }, { element: ".menu", popover: { title: t('dian-ji-ren-yi-kong-bai-qu-yu-tui-chu-cai-dan'), description: t('tui-chu-cai-dan'), }, }, { element: ".station-descend-31", popover: { title: t('dian-ji-zhan-dian'), description: t('yi-da-kai-zhan-dian-ka-pian'), }, }, { element: ".station-card", popover: { title: t('zhan-dian-ka-pian'), description: t('ke-yi-zai-zhe-li-bian-ji-zhan-dian-de-suo-you-she-zhi'), showButtons:["next"], // onNextClick:driver.moveNext }, }, { element: ".ScaleLayer", popover: { title: t('chang-shi-dian-ji-xian-lu'), 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'), }, }, { element: ".line-card", popover: { title: t('xian-lu-ka-pian'), description: t('ke-yi-zai-zhe-li-bian-ji-xian-lu-de-suo-you-she-zhi'), showButtons:["next"], // onNextClick:driver.moveNext }, }, ]; ================================================ FILE: src/WelcomeTour/Steps/export.ts ================================================ import { DriveStep, Driver } from "driver.js"; import { browserInfo } from "../../Common/util"; import {t} from 'i18next'; export const getSteps = (driver: Driver): DriveStep[] => { const { engine } = browserInfo; const webkit = engine.name === "WebKit"; return[ { element: ".title .click-panel", popover: { title: t('dian-ji-biao-ti'), description: t('dian-ji-biao-ti-da-kai-cai-dan'), }, }, { element: ".existed-map-btn", popover: { title: t('cong-mo-ban-xin-jian'), description: t('zai-yi-you-de-di-tu-shang-xiu-gai'), }, }, { element: ".tools", onHighlighted:()=>{}, popover: { title: t('xuan-ze-yi-ge-ni-xi-huan-de-cheng-shi'), description: t('ran-hou-dian-ji-xia-yi-bu'), showButtons:["next"], onNextClick:()=>{ driver.moveNext(); } }, }, { element: ".confirm-add-from-existed-map-btn", popover: { title: t('yi-ci-wei-mo-ban-xin-jian'), description: t('que-ren-xin-jian-qian-qing-que-bao-yi-jing-bao-cun-le-dang-qian-de-di-tu'), }, }, { element: ".title .click-panel", popover: { title: t('da-kai-cai-dan'), description: t('xuan-ze-zuo-wei-wen-jian-dao-chu'), }, }, { element: ".export-as-file-btn", popover: { title: t('zuo-wei-wen-jian-dao-chu'), description: t('xuan-ze-zuo-wei-wen-jian-dao-chu-0'), }, }, { element: ".import-file-btn", onHighlighted:()=>{}, popover: { title: t('dao-ru-gang-cai-dao-chu-wen-jian'), description: t('xia-ci-dian-ji-dao-ru-wen-jian-ke-yi-ji-xu-bian-ji'), showButtons:["next"], onNextClick:driver.moveNext }, }, { element: `.export-as${webkit?'-svg':''}-image-btn`, popover: { title: t('dao-chu-tu-pian'), 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'), }, }, { element: `.recover-btn`, popover: { title: t('cong-huan-cun-zhong-hui-fu-shu-ju'), 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'), }, }, { element: `.export-recover-btn`, popover: { title: t('dao-chu-hui-fu-shu-ju'), 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'), }, }, ]; } ================================================ FILE: src/WelcomeTour/Steps/line-card.ts ================================================ import { DriveStep, Driver } from "driver.js"; import {t} from 'i18next'; export const getSteps = (driver: Driver): DriveStep[] => [ { element: ".ScaleLayer", popover: { title: t('dian-ji-ren-yi-xian-lu'), description: t('tu-zhong-cai-se-qu-xian-biao-shi-di-tie-xian-lu'), }, }, { element: ".line-card", popover: { title: t('xian-lu-ka-pian-0'), description: t('ke-yi-zai-zhe-li-bian-ji-xian-lu-de-suo-you-she-zhi-0'), showButtons:["next"], // onNextClick:driver.moveNext }, }, { element: ".bend-first", popover: { title: t('dian-ji-ci-chu-kong-zhi-xian-lu-zai-liang-zhan-zhi-jian-de-zou-xiang'), description: t('zhan-dian-qu-jian-lian-xian-zhi-you-liang-zhong-fang-shi-xie-xian-yu-zhi-xian'), }, }, { element: ".expand", popover: { title: t('fang-da-an-niu'), description: t('ke-yi-tuo-kuan-xian-lu-ka-pian') }, }, { element: ".shrink", popover: { title: t('suo-xiao-an-niu'), description: t('zai-suo-xiao-zhuang-tai-ke-yi-dian-ji-geng-duo-she-zhi') }, }, { element: ".edit", popover: { title: t('bian-ji-an-niu'), description: t('jin-ru-geng-duo-she-zhi-mian-ban') }, }, { element: ".name-detail", 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}, }, { element: ".edit-tool.color", 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"}, }, { element: ".edit-tool.operation", 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" }, }, { element: ".done", popover: { title: t('dian-ji-ci-chu-wan-cheng-she-zhi'), description: t('hui-dao-zhan-dian-ye') }, }, { element: ".stations-count", 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') }, }, ]; ================================================ FILE: src/WelcomeTour/Steps/quick-edit.ts ================================================ import { DriveStep, Driver } from "driver.js"; import {t} from 'i18next'; export const getSteps = (driver: Driver): DriveStep[] => [ { element: ".title .click-panel", popover: { title: t('da-kai-cai-dan-0'), description: t('dian-ji-biao-ti-da-kai-cai-dan-0'), }, }, { element: "#menu-add-station", popover: { title: t('dian-ji-tian-jia-zhan-dian'), description: t('jin-ru-tian-jia-zhan-dian-mo-shi'), }, }, { element: ".ScaleLayer", onHighlighted: () => {}, popover: { title: t('dian-ji-ren-yi-kong-bai-chu-tian-jia-zhan-dian'), 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'), showButtons: ["next"], onNextClick: () => { driver.moveNext(); }, }, }, { element: "#add-station-finish-btn", popover: { title: t('dian-ji-wan-cheng'), description: t('tui-chu-bian-ji-mo-shi') }, }, { element: ".station-descend-1", popover: { title: t('dian-ji-gang-cai-chuang-jian-de-zhan-dian'), description: t('da-kai-zhan-dian-xin-xi-ka-pian'), }, }, { element: ".station-card-operation", popover: { title: t('dian-ji-cao-zuo-xuan-xiang-ka'), description: t('wo-men-lai-tian-jia-xian-lu') }, }, { element: ".add-new-line-btn", popover: { title: t('dian-ji-yi-ci-wei-qi-dian-xin-jian-xian-lu'), description: t('jin-ru-tian-jia-xian-lu-mo-shi'), }, }, { element: ".station-descend-2", popover: { title: t('dian-ji-zhan-dian-0'), description: t('lian-jie-zhan-dian'), }, }, { element: ".station-descend-3", popover: { title: t('dian-ji-zhan-dian'), description: t('lian-jie-zhan-dian'), }, }, { element: "#add-line-finish-btn", popover: { title: t('dian-ji-wan-cheng'), description: t('xian-lu-jiu-chuang-jian-hao-le') }, }, ]; ================================================ FILE: src/WelcomeTour/Steps/skip.ts ================================================ import { DriveStep, Driver } from "driver.js"; export const getSteps = (driver: Driver): DriveStep[] => [ { element: ".title .click-panel", // onHighlighted:()=>{}, popover: { title: "下次可以点这里再次打开教程", description: "点击标题打开菜单", showButtons:["next"], // onNextClick:driver.moveNext }, }, { element: ".tour-btn", onHighlighted:()=>{}, popover: { title: "使用教程", description: "点这里再次打开", showButtons:["next"], onNextClick: driver.moveNext, }, }, ]; ================================================ FILE: src/WelcomeTour/Steps/station-card.ts ================================================ import { DriveStep, Driver } from "driver.js"; import {t} from 'i18next'; export const getSteps = (driver: Driver): DriveStep[] => [ { element: ".station-descend-31", popover: { title: t('dian-ji-zhan-dian'), description: t('yi-da-kai-zhan-dian-ka-pian-0'), }, }, { element: ".station-card", popover: { title: t('zhan-dian-ka-pian'), description: t('ke-yi-zai-zhe-li-bian-ji-zhan-dian-de-suo-you-she-zhi'), showButtons:["next"], // onNextClick:driver.moveNext }, }, { element: ".name-detail", popover: { title: t('bian-ji-zhan-dian-wei-zhi'), description: t('dian-ji-zuo-biao-zhi-hou-shi-yong-shu-biao-gun-lun-ke-yi-jing-que-tiao-jie'), showButtons:["next"], // onNextClick:driver.moveNext }, }, { element: ".edit-tool.color", popover: { title: t('dian-ji-ci-chu-xiu-gai-zhan-dian-xing-zhuang'), description: t('xiu-gai-xing-zhuang') ,side:"right"}, }, { element: ".edit-tool.operation", 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" }, }, { element: ".edit-tool.operation.tag", popover: { title: t('dian-ji-ci-chu-she-zhi-zhan-dian-ming-cheng-wei-zhi'), description: t('zhan-dian-ming-wei-zhi') }, }, { element: ".tag-detail", 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 }, }, ]; ================================================ FILE: src/WelcomeTour/Steps/tag-setting.ts ================================================ import { DriveStep, Driver } from "driver.js"; import {t} from 'i18next'; export const getSteps = (driver: Driver): DriveStep[] => [ { element: ".title .click-panel", popover: { title: t('dian-ji-biao-ti'), description: t('dian-ji-biao-ti-da-kai-cai-dan-1'), }, }, { element: ".auto-hidden-btn", popover: { title: t('guan-bi-zi-dong-yin-cang'), 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'), }, }, { element: ".menu", popover: { title: t('dian-ji-ren-yi-kong-bai-qu-yu-tui-chu-cai-dan-0'), description: t('tui-chu-cai-dan-0'), }, }, { element: ".station-descend-31", popover: { title: t('dian-ji-zhan-dian'), description: t('yi-da-kai-zhan-dian-ka-pian-1'), }, }, { element: ".edit-tools", popover: { title: t('zai-zhe-li-xiang-xia-gun-dong-yi-xia'), description: t('zai-dian-ji-biao-qian-an-niu') }, }, { element: ".tag-detail", 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') }, }, { element: ".station-name-descend-31", 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 }, }, { element: ".tag-item.center", 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') }, }, ]; ================================================ FILE: src/WelcomeTour/WelcomeTour.scss ================================================ .welcome-tour-container { position: fixed; height: 100vh; width: 100vw; left: 0; top: 0; background-color: rgba(0, 0, 0, 0.4); display: flex; justify-content: center; align-items: center; z-index: 2000; font-weight: 500; font-size: 18px; .welcome-tour { margin-top: -10vh; // width: 800px; // min-width: 45vw; width: 80vw; max-width: 800px; height: 475px; background-color: #454545; border-radius: 12px; .header { margin: 33.3px 33.3px 20px 33.3px; white-space: nowrap; position: relative; .icon { img { width: 40px; height: 40px; border-radius: 5px; } } .title { display: inline-block; margin-left: 13px; vertical-align: top; .sub-title { font-size: 12px; color: white; width: fit-content; margin-top: -2px; } .main-title { color: white; font-weight: 800; width: fit-content; } } .control { display: inline-flex; vertical-align: top; font-size: 12px; position: absolute; right: 0; height: 40px; justify-content: center; align-items: center; .skip-tour { color: rgba(255, 255, 255, 0.6); margin: 0 20px; cursor: pointer; // text-transform: capitalize; } .start-tour { text-transform: capitalize; cursor: pointer; color: white; padding: 7.33px 20px; background-color: #2196f3; border-radius: 35px; } } } .divider { border-bottom: rgba(255, 255, 255, 0.27) solid 1px; } .body { white-space: nowrap; overflow-x: scroll; position: relative; &::-webkit-scrollbar { display: none; } .intro { display: inline-block; color: white; width: 285px; margin-left: 38px; margin-top: 36.67px; vertical-align: top; &:last-child{ margin-right: 38px; } .hight-light { display: inline-flex; justify-content: center; align-items: center; background-color: #727272; padding: 4px 15px; border-radius: 35px; .icon { height: 20px; width: 20px; svg { height: 20px; width: 20px; } .air{ width: 16px; margin-left: 2px; } } .title { margin-left: 5px; text-transform: capitalize; } } .detail-card { height: 240px; background-color: #727272; border-radius: 14px; margin-top: 17.3px; display: flow-root; position: relative; .qrcode-container{ position: absolute; border-radius: 14px; margin: auto; left: 0; right: 0; top:0; bottom: 0; width: 100%; height: 100%; background-color: #0000002e; backdrop-filter: blur(10px); cursor: pointer; opacity: 0; pointer-events: none; transition: 0.3s ease-in-out; &.show{ opacity: 1; pointer-events: auto; } canvas{ position: absolute; border-radius: 14px; margin: auto; left: 0; right: 0; top:0; bottom: 0; } } .title { margin-top: 20.67px; margin-left: 24px; width: 265px; white-space: normal; text-transform: capitalize; } .left-down { position: absolute; left: 24px; bottom: 20.67px; .intro-text { .intro-text-line { &:not(.line-card-text){ text-transform: capitalize; } :not(.emphasis) { color: rgba(255, 255, 255, 0.6); } } } .more { margin-top: 26px; cursor: pointer; &:hover{ // text-decoration: underline; opacity: 0.9; } .more-text { text-transform: capitalize; &.finished{ opacity: 0.6; } } .more-icon { height: 20px; width: 28px; vertical-align: top; display: inline-block; svg { height: 28px; width: 28px; &.finished{ width: 21px; // margin-top: -1px; margin-left: 2px; opacity: 0.6; } } } } } } } } } } ================================================ FILE: src/WelcomeTour/WelcomeTour.tsx ================================================ import React, { LegacyRef, MutableRefObject, RefObject, useEffect, useRef, useState, useTransition, } from "react"; import "./WelcomeTour.scss"; import Infinity from "../Resource/Icon/infinity"; import GoToIcon from "../Resource/Icon/goto"; import { hightLights } from "./HightLights"; import classNames from "classnames"; import { onWheelX } from "../Common/util"; import { showTour } from "./Driver"; import { ShowTourProps, UserDataType } from "../Data/UserData"; import FinishedIcon from "../Resource/Icon/finished"; import QRCode from "qrcode"; import { useTranslation } from "react-i18next"; export function WelcomeTour({ showTour: show, setShowTour: setShow, }: ShowTourProps) { const [qrCode, setQRCode] = useState(false); const {t} = useTranslation(); const [visitedSteps, setVisitedSteps] = useState(() => { const visitedStepsJson = localStorage.getItem("visited-steps"); return visitedStepsJson ? JSON.parse(visitedStepsJson) : []; }); const setVisited = (step: string) => { const steps = visitedSteps.concat([step]); localStorage.setItem("visited-steps", JSON.stringify(steps)); setVisitedSteps(steps); }; const reset = () => { localStorage.setItem("visited-steps", JSON.stringify([])); setVisitedSteps([]); localStorage.setItem("skip-tour-viewed", "Y"); }; const next = hightLights.find( (highlight) => !visitedSteps.includes(highlight.id) ); const canvasRef = useRef(null); const showQRCode = () => { setQRCode(true); setVisited("mobile"); setTimeout(() => { QRCode.toCanvas(canvasRef.current, window.location.href); }); }; const scrollToNext = ()=>{ setTimeout(() => { const contianer = document.querySelector(".welcome-tour .body"); if(next && contianer){ const nextIntro = document.querySelector(`.intro-${next.id}`) if(nextIntro){ const {offsetLeft} = nextIntro as HTMLDivElement; contianer.scrollTo(offsetLeft-38,0); } } },100); } useEffect(() => { scrollToNext(); }, []); return (
{t('welcome.welcome')}
{t('welecome.minimetroweb')}
{next ? ( <> { setShow(false); // if (!localStorage.getItem("skip-tour-viewed")) // showTour("skip", () => { localStorage.setItem("skip-tour-viewed", "Y"); // }); }} > {t('welcome.skip')} { if (next.id === "mobile") { showQRCode(); } else { setShow(false); showTour(next.id, () => { setShow(true); setVisited(next.id); scrollToNext(); }); } }} > {visitedSteps.length ? t('welcome.goOn') : ""} {next.more} ) : ( <> {t('welcome.restart')} { localStorage.setItem("skip-tour-viewed", "Y"); setShow(false); }} > {t('welcome.done')} )}
{hightLights.map((hightLight) => { const { id, icon, title, subTitle, introText, more } = hightLight; const finished = visitedSteps.includes(id); return (
{icon} {title}
{subTitle}
{introText.map((line) => { const emphasisStart = line[0]; const lineText = line.slice(1); return (
{lineText.map((text, index) => { const emphasis = (Number(emphasisStart) + index) % 2; return ( {text} ); })}
); })}
{ if (id === "mobile") { showQRCode(); } else { showTour(id, () => { setVisited(id); setShow(true); scrollToNext(); }); setShow(false); } }} > {more} {finished ? ( ) : ( )}
{id === "mobile" ? (
setQRCode(false)} >
) : ( <> )}
); })}
); } ================================================ FILE: src/i18n/config.ts ================================================ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import translation_en from './locales/en.json'; import translation_zh from './locales/zh.json'; import LanguageDetector from 'i18next-browser-languagedetector'; const resources = { en: { translation: translation_en }, zh: { translation: translation_zh }, }; i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources, fallbackLng: 'en', detection: { order: ['queryString', 'cookie', 'localStorage', 'navigator', 'htmlTag', 'path', 'subdomain'], caches: ['localStorage', 'cookie'] }, interpolation: { escapeValue: false }, }); export default i18n; ================================================ FILE: src/i18n/locales/en.json ================================================ { "language": "english", "menu": { "alreadyAddStations": "{{count}} stations added", "clickBlankToAddStation": "Click on the blank space to add a new station", "redo": "Redo", "withdraw": "Withdraw", "AutoHidden": " Auto Hide", "addStation": "Add Station", "alreadyModified": "Modified {{count}} times", "back": "Return", "backgroudSetting": "Background Settings", "cancel": "Cancel", "changeBackgroudImage": "Modify Background Image", "changeStationPosition": "Adjust Station Location", "chooseALineFirst": "First select a line, then click", "chooseAMap": "Choose a Map", "clickAddStationToLine": "Click the station to insert it into the {{insertIndex}} station of {{lineName}}", "deleteImage": "Delete Picture", "done": "Finish", "dragToChangePoistion": "Drag station to change location", "hidden": "Hide", "importBackgroudImage": "Import Background Image", "importImage": "Import Picture", "lightYellow": "Yellow", "line": "Line", "show": "Show", "station": "Station", "stationName": " Station Name", "transparent": "Transparent", "turnOff": "Disable", "turnOn": "Enable", "useTemplate": "Confirm", "white": "White", "RMP": "Rail Map Toolkit", "about": "About", "addBackgroundImage": "Set Background...", "addFromTemplate": "Create Map From Template...", "addNewMap": "New Blank Map...", "author": "Author: Ryan", "data": "Data", "exportAsFile": "Export as File...", "exportAsImage": "Export as Image...", "exportAsSVG": "Export as Vector Image...", "exportRecoveryData": "Export Recovered Data...", "guide": "Usage Tutorial...", "importFile": "Import File...", "insertStation": "Insert Station...", "mediateMap": "Centered Roadmap...", "projectAddress": "Project Address...", "recoverData": "Recover Data...", "version": "Version: ", "videoGuild": "Video Tutorial...", "symbol": "logo", "newMap": "New Map", "switchLanguage": "使用中文" }, "bei-jing": "Beijing", "chang-sha": "Changsha", "guang-zhou": "Guangzhou", "shang-hai": "Shanghai", "shen-zhen": "Shenzhen", "tian-jin": "Tianjin", "xiang-gang": "Hongkong", "station": { "auto": "automatic", "delete": "Delete Station...", "down": "bottom", "downLeft": "lower left", "left": "left side", "leftUp": "upper left", "lines": " Lines", "operateDes": "new line / delete", "operation": "Operate", "poisition": "Position", "remove": "Remove From {{lineName}}({{stationIndex}})", "right": "right side", "rightDown": "lower right", "shape": "Shape", "startHere": "New Line Starts Here...", "stationName": "Label", "up": "top", "upRight": "upper right", "x": "x-axios", "y": "y-axios" }, "shape": { "cicle": "circle", "cross": "cross", "diamond": "diamond", "ginkgo": "ginkgo", "hexagon": "hexagon", "leaf": "leaf", "pentagon": "pentagon", "rhombus": "diamond", "square": "square", "start": "star", "triangle": "triangle" }, "line": { "asSubline": "Set as Subline line", "bendFirst": "Bend First", "color": "Color", "delete": "delete", "deleteLine": "Delete Line...", "done": "Done", "fromTo": "From {{from}} to {{to}}", "insertHere": "insert here", "insertStation": "insert here", "name": "name", "notInUse": "Not Yet Open", "notSubLineAnymore": "Not as Subline", "operation": "Operate", "order": "order", "stations": " Stations", "straight": "Linar First" }, "opacity": "Opacity", "recover": { "subTitle": "Recently edited maps detected.", "text": "Need to recover data?" }, "error": { "dontWorry": "But don't worry, your map won't lost", "exportError": "export the map file contains error...", "exportNoErrorFile": "Export the map file before the error occurred", "metError": "Sorry! \nProgram Error", "orYouCould": "Or you can ", "recoverFromCache": "Restore the pre-error map from cache", "sendAuthor": "In order to send it to the author to analyze the cause of the error." }, "huan-ying-shi-yong": "Welcome", "welcome": { "done": "Finish", "goOn": "continue ", "restart": "restart ", "skip": "Skip for Now", "welcome": "Welcome to" }, "welecome": { "minimetroweb": "Mini Metro Map Building Tool" }, "chang-shi-dian-ji-xian-lu": "Try clicking the line", "dian-ji-ren-yi-kong-bai-qu-yu-tui-chu-cai-dan": "Click on any empty area to exit the menu", "dian-ji-zhan-dian": "click station", "ke-yi-xiu-gai-biao-ti": "Title can be modified", "ke-yi-zai-zhe-li-bian-ji-xian-lu-de-suo-you-she-zhi": "All settings for the route can be edited here", "ke-yi-zai-zhe-li-bian-ji-zhan-dian-de-suo-you-she-zhi": "All settings for the station can be edited here", "tour": { "clickOpenMenu": "Click on the title to open the menu", "clickTile": "click title", "mouseScale": "Hold down the left mouse button and drag to move the map, scroll the mouse wheel to zoom the map", "touchScale": "Drag the map with one finger and zoom the map with two fingers", "tryScale": "Try zooming or moving the map" }, "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": "The colored lines in the picture are the lines. If you cannot select it, you can try to enlarge the map and click on it.", "tui-chu-cai-dan": "Exit menu", "xian-lu-ka-pian": "line card", "yi-da-kai-zhan-dian-ka-pian": "to open the station card", "zai-ci-dian-ji-biao-ti": "Click on the title again", "zhan-dian-ka-pian": "station card", "bu-hui-zhong-die": " won't overlap", "che-hui": " withdraw", "dan-zhi": "using finger", "dao-chu": "Export", "dao-chu-gao-fen-bian-shuai-tu-pian": "Export high-resolution images", "dao-ru-dao-chu": "Import and export", "fang-bian-cha-kan": "Easy to view", "gao-liang": "Highlight", "gao-xiao-bian-ji": "Efficient editing", "gong-xian-xian-lu": "collinear lines", "guan-lian": " linked", "jian-shao-wu-cao-zuo": "Reduce misoperation", "ka-pian-hua-zhan-shi-zhan-dian-yu-xian-lu": "Card display of stations and routes", "le-jie-biao-qian-she-zhi": "Learn about label settings", "le-jie-dao-ru-dao-chu": "Learn import and export", "le-jie-ji-ben-cao-zuo": "Learn basic operations", "le-jie-kuai-su-chuang-jian": "Learn about quick creat", "le-jie-xian-lu-she-zhi": "Learn about line settings", "le-jie-zhan-dian-ka-pian": "Learn about station cards", "lian-xu-chuang-jian-zhan-dian-mo-shi": "Continuous station creation mode", "lian-xu-tian-jia-zhan-dian": "Add stations continuously", "ling-huo-zou-xian": "Flexible wiring", "mo-shi": "", "pngsvg-tu-pian": " PNG/SVG images", "q-zi-zou-xian-yu-zou-xian": " Q shaped routing", "shi-bie-dong-zuo-yi-tu": "", "shuang-zhi": " or ", "suo-fang-di-tu": "Zoom", "tuo-dong": " drag", "wan-zheng-de-chu-kong-shi-jian-zhi-chi": "Complete touch event support", "wu-xian-da-hua-bu": "infinite canvas", "wu-xian-ge-zhan-dian": " Unlimited stations", "wu-xian-tiao-xian-lu": " Unlimited lines", "wu-xian-zi-chan": "unlimited assets", "yi-dong-duan-zhi-chi": "Mobile support", "yi-tiao-xian-lu-neng-duo-ci-chuan-guo-tong-yi-zhan": "A line can pass through the same station multiple times", "yu": " and", "zai-ping-ban-shang-ti-yan": "Experience it on tablet", "zhan-dian-ming-cheng-zi-dong-xuan-ze-bai-fang-wei-zhi": "label automatically place", "zhan-dian-xian-lu-hu-xiang": "stations and lines", "zhan-shi-xuan-zhong-zhan-dian-yu-xian-lu": " selected", "zhi-chi": "support", "zhi-chi-cong-mo-ban-chuang-jian": "creat from templates", "zhi-chi-she-zhi": "Support settings", "zhi-xian": " sub line", "zhong-zuo": " redo", "zi-dong-bi-rang": "automatic avoidance", "zi-dong-tian-jia-pian-yi-zhi": "Automatically add offset values", "cong-huan-cun-zhong-hui-fu-shu-ju": "Recover data from cache", "cong-mo-ban-xin-jian": "New from template", "da-kai-cai-dan": "Open menu", "dao-chu-hui-fu-shu-ju": "Export recovery data", "dao-chu-tu-pian": "Export pictures", "dao-ru-gang-cai-dao-chu-wen-jian": "Import the file you just exported", "dian-ji-biao-ti": "click title", "dian-ji-biao-ti-da-kai-cai-dan": "Click on the title to open the menu", "dian-ji-dao-chu-tu-pian-dao-chu-ke-neng-hui-dao-zhi-ka-dun-ji-miao-shu-yu-zheng-chang-xian-xiang": "Click to export the image. \nExporting may cause lag for a few seconds, which is normal.", "que-ren-xin-jian-qian-qing-que-bao-yi-jing-bao-cun-le-dang-qian-de-di-tu": "Please make sure you have saved the current map before confirming the new creation.", "ran-hou-dian-ji-xia-yi-bu": "Then click next", "ru-guo-mei-bao-cun-de-shi-hou-bu-xiao-xin-shua-xin-ke-yi-dian-ji-zhe-li-hui-fu-shu-ju": "If you accidentally refresh without saving, you can click here to restore the data.", "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": "If a certain step results in an error or crash, click here to export the last correct data. \n(If you encounter this situation, please give feedback to the author)", "xia-ci-dian-ji-dao-ru-wen-jian-ke-yi-ji-xu-bian-ji": "Click to import the file next time to continue editing.", "xuan-ze-yi-ge-ni-xi-huan-de-cheng-shi": "Choose a city you like", "xuan-ze-zuo-wei-wen-jian-dao-chu": "Select Export as file", "xuan-ze-zuo-wei-wen-jian-dao-chu-0": "Select Export as file", "yi-ci-wei-mo-ban-xin-jian": "Use this as a template to create a new", "zai-yi-you-de-di-tu-shang-xiu-gai": "Modify on existing map", "zuo-wei-wen-jian-dao-chu": "Export as file", "bian-ji-an-niu": "edit button", "ci-chu-xiu-gai-xian-lu-ming-cheng-yu-biao-shi": "Modify the line name and identification here", "dian-ji-ci-chu-kong-zhi-xian-lu-zai-liang-zhan-zhi-jian-de-zou-xiang": "Click here to control the direction of the line between two stations", "dian-ji-ci-chu-shan-chu-xian-lu-huo-zhe-she-zhi-zhi-xian": "Click here to delete a line or set up a sub line", "dian-ji-ci-chu-suo-yi-tu-jing-de-zhan-dian-ka-pian": "Click here for site cards for all routes", "dian-ji-ci-chu-wan-cheng-she-zhi": "Click here to complete setup", "dian-ji-ci-chu-xiu-gai-xian-lu-biao-shi-se": "Click here to modify the line identification color", "dian-ji-ren-yi-xian-lu": "Click on any line", "fang-da-an-niu": "magnify button", "hui-dao-zhan-dian-ye": "Return to site page", "jin-ru-geng-duo-she-zhi-mian-ban": "Enter more settings panel", "ke-yi-tuo-kuan-xian-lu-ka-pian": "Line cards can be expanded", "ke-yi-zai-zhe-li-bian-ji-xian-lu-de-suo-you-she-zhi-0": "All settings for the route can be edited here", "pai-xu-ke-yi-kong-zhi-xian-lu-jian-de-zhe-dang-guan-xi": "Sorting can control the occlusion relationship between lines", "she-zhi-zhi-xian-hou-xian-lu-hui-yi-xu-xian-xian-shi-qie-lian-jie-chu-bu-zai-xian-shi-ba-shou": "After setting up the sub line, the line will be displayed as a dotted line, and the handle will no longer be displayed at the connection.", "suo-xiao-an-niu": "zoom out button", "tu-zhong-cai-se-qu-xian-biao-shi-di-tie-xian-lu": "The colored curves in the figure represent subway lines", "xian-lu-ka-pian-0": "line card", "xian-shi-tu-jing-zhan-dian-ka-pian": "Show route site cards", "zai-suo-xiao-zhuang-tai-ke-yi-dian-ji-geng-duo-she-zhi": "In the zoomed-out state, you can click on more settings", "zhan-dian-qu-jian-lian-xian-zhi-you-liang-zhong-fang-shi-xie-xian-yu-zhi-xian": "There are only two ways to connect sites, diagonal lines and straight lines.", "zuo-xia-jiao-de-yan-se-xuan-ze-qi-ke-yi-qu-se": "The color picker in the lower left corner can select colors", "da-kai-cai-dan-0": "Open menu", "da-kai-zhan-dian-xin-xi-ka-pian": "Open the site information card", "dian-ji-biao-ti-da-kai-cai-dan-0": "Click on the title to open the menu", "dian-ji-cao-zuo-xuan-xiang-ka": "Click on the Actions tab", "dian-ji-gang-cai-chuang-jian-de-zhan-dian": "Click on the site you just created", "dian-ji-ren-yi-kong-bai-chu-tian-jia-zhan-dian": "Click any blank space to add a site", "dian-ji-tian-jia-zhan-dian": "Click to add site", "dian-ji-wan-cheng": "Click Finish", "dian-ji-yi-ci-wei-qi-dian-xin-jian-xian-lu": "Click to create a new route starting from this point", "dian-ji-zhan-dian-0": "click site", "jin-ru-tian-jia-xian-lu-mo-shi": "Enter add line mode", "jin-ru-tian-jia-zhan-dian-mo-shi": "Enter add site mode", "lian-jie-zhan-dian": "connect site", "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": "After adding, click Next (it is recommended that you add three sites in the blank area in the upper left corner of the screen)", "tui-chu-bian-ji-mo-shi": "Exit edit mode", "wo-men-lai-tian-jia-xian-lu": "Let's add the line", "xian-lu-jiu-chuang-jian-hao-le": "The line is created", "bian-ji-zhan-dian-wei-zhi": "Edit site location", "dian-ji-ci-chu-shan-chu-zhan-dian-huo-xin-jian-xian-lu": "Click here to delete a site or create a new route", "dian-ji-ci-chu-she-zhi-zhan-dian-ming-cheng-wei-zhi": "Click here to set site name location", "dian-ji-ci-chu-xiu-gai-zhan-dian-xing-zhuang": "Click here to modify site shape", "dian-ji-hui-se-de-fang-kuai-xuan-ze-fang-wei": "Click on the gray square to select the direction", "dian-ji-zhong-jian-de-wen-zi-hui-fu-zi-dong-wei-zhi": "Click on the text in the middle to restore the automatic position", "dian-ji-zuo-biao-zhi-hou-shi-yong-shu-biao-gun-lun-ke-yi-jing-que-tiao-jie": "Click the coordinates and use the mouse wheel to adjust accurately", "shan-chu-huo-xin-jian": "Delete or create new", "xiu-gai-xing-zhuang": "Modify shape", "yi-da-kai-zhan-dian-ka-pian-0": "to open the site card", "zhan-dian-ming-wei-zhi": "Site name location", "chang-shi-gai-bian-zhan-dian-ming-dao-you-xia-jiao": "Try changing the site name to the lower right corner", "dian-ji-biao-ti-da-kai-cai-dan-1": "Click on the title to open the menu", "dian-ji-ren-yi-kong-bai-qu-yu-tui-chu-cai-dan-0": "Click on any empty area to exit the menu", "dian-ji-you-xia-jiao-de-hui-se-fang-kuai": "Click on the gray square in the lower right corner", "dian-ji-zhong-jian-de-wen-zi-hui-fu-dao-zi-dong-xuan-ze-wei-zhi": "Click on the text in the middle to return to the automatically selected position", "guan-bi-zi-dong-yin-cang": "Turn off auto-hide", "hui-fu-dao-zi-dong-xuan-ze-wei-zhi": "Revert to automatically selected location", "ke-yi-kan-dao-zhan-dian-ming-yi-jing-wei-yu-zhan-dian-you-xia-jiao-le": "You can see that the site name is already in the lower right corner of the site", "ke-yi-yong-zhe-zhong-fang-fa-xiu-gai-zhan-dian-ming-wei-zhi": "You can use this method to modify the location of the site name", "tui-chu-cai-dan-0": "Exit menu", "yi-da-kai-zhan-dian-ka-pian-1": "to open the site card", "zai-dian-ji-biao-qian-an-niu": "Click the label button again", "zai-zhe-li-xiang-xia-gun-dong-yi-xia": "Scroll down a bit here", "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": "Site names are automatically hidden when the map is zoomed to a smaller scale. \nTurn off auto-hide to ensure the site name is always visible", "di-current-bu-gong-total-bu": "Step {0} of {1} steps", "shang-yi-bu": "Previous Step", "wan-cheng": "Finish", "xia-yi-bu": "Next Step", "delete": { "cancel": "Cancel", "delete": "delete", "remove": "Remove", "subtile": "No longer as the {{index}} station of {{lineName}}" }, "que-shi-yao-deletetext-ma": "Do you really want to {{deleteText}}?", "teyvat": "Teyvat", "newStation": "New Station", "lineNo": "Line {{lineId}}", "Common-teyvat-192be7b183f6eee79-1": "Eagle Beach", "Common-teyvat-192be7b183f6eee79-10": "Fengshen Statue Station", "Common-teyvat-192be7b183f6eee79-11": "Mondstadt", "Common-teyvat-192be7b183f6eee79-12": "Qingquan Town", "Common-teyvat-192be7b183f6eee79-13": "Dawn Winery", "Common-teyvat-192be7b183f6eee79-14": "Benlangling", "Common-teyvat-192be7b183f6eee79-15": "Benlangling North", "Common-teyvat-192be7b183f6eee79-16": "Fenglong Scenic Area", "Common-teyvat-192be7b183f6eee79-17": "Mingguan Gorge", "Common-teyvat-192be7b183f6eee79-18": "Fenglong International Airport", "Common-teyvat-192be7b183f6eee79-19": "Snow Mountain Foot Station", "Common-teyvat-192be7b183f6eee79-2": "dadaupa valley", "Common-teyvat-192be7b183f6eee79-20": "formless ice", "Common-teyvat-192be7b183f6eee79-21": "Xueshanshan Station", "Common-teyvat-192be7b183f6eee79-22": "Seven-day God Statue Station", "Common-teyvat-192be7b183f6eee79-23": "snow mountain lake", "Common-teyvat-192be7b183f6eee79-24": "salt of the earth", "Common-teyvat-192be7b183f6eee79-25": "Wangshu Inn", "Common-teyvat-192be7b183f6eee79-26": "Shimen", "Common-teyvat-192be7b183f6eee79-27": "Qingce Village", "Common-teyvat-192be7b183f6eee79-28": "Wangshunan", "Common-teyvat-192be7b183f6eee79-29": "Return to the original", "Common-teyvat-192be7b183f6eee79-3": "Formless Thunder", "Common-teyvat-192be7b183f6eee79-30": "Luhuachi South", "Common-teyvat-192be7b183f6eee79-31": "Liyue Port", "Common-teyvat-192be7b183f6eee79-32": "gold house", "Common-teyvat-192be7b183f6eee79-33": "Qingxupu", "Common-teyvat-192be7b183f6eee79-34": "Lingju International Airport", "Common-teyvat-192be7b183f6eee79-35": "Tianheng Mountain", "Common-teyvat-192be7b183f6eee79-36": "Escape Yuling", "Common-teyvat-192be7b183f6eee79-37": "Luhuachi", "Common-teyvat-192be7b183f6eee79-38": "cuijuepo", "Common-teyvat-192be7b183f6eee79-39": "Taishan Mansion", "Common-teyvat-192be7b183f6eee79-4": "Oath Point Airport", "Common-teyvat-192be7b183f6eee79-40": "Airport East", "Common-teyvat-192be7b183f6eee79-41": "Qingyun International Airport", "Common-teyvat-192be7b183f6eee79-42": "Mount Ozura", "Common-teyvat-192be7b183f6eee79-43": "Hulao Mountain", "Common-teyvat-192be7b183f6eee79-44": "Fulong tree", "Common-teyvat-192be7b183f6eee79-45": "Nantianmen", "Common-teyvat-192be7b183f6eee79-46": "Tianqiu Valley", "Common-teyvat-192be7b183f6eee79-47": "Fuao Valley", "Common-teyvat-192be7b183f6eee79-48": "Xiangzuipo", "Common-teyvat-192be7b183f6eee79-49": "No thick forest", "Common-teyvat-192be7b183f6eee79-5": "Fengqidi Station", "Common-teyvat-192be7b183f6eee79-50": "Casazzale", "Common-teyvat-192be7b183f6eee79-51": "Fungus Cave", "Common-teyvat-192be7b183f6eee79-52": "Xumi City", "Common-teyvat-192be7b183f6eee79-53": "Huachengguo", "Common-teyvat-192be7b183f6eee79-54": "dansha cliff", "Common-teyvat-192be7b183f6eee79-55": "Subdue all the demons in Shandong", "Common-teyvat-192be7b183f6eee79-56": "Subdue all the demonic mountains", "Common-teyvat-192be7b183f6eee79-57": "Porto Ormos North", "Common-teyvat-192be7b183f6eee79-58": "puerto olmos", "Common-teyvat-192be7b183f6eee79-59": "vimo village", "Common-teyvat-192be7b183f6eee79-6": "Star falls in Hunan", "Common-teyvat-192be7b183f6eee79-60": "jhana garden", "Common-teyvat-192be7b183f6eee79-61": "Praise to the secluded place", "Common-teyvat-192be7b183f6eee79-62": "Huana Lanna", "Common-teyvat-192be7b183f6eee79-63": "Five Oasis", "Common-teyvat-192be7b183f6eee79-64": "Kawanyi", "Common-teyvat-192be7b183f6eee79-65": "Water and Sky Jungle", "Common-teyvat-192be7b183f6eee79-66": "The seat of Tuha", "Common-teyvat-192be7b183f6eee79-67": "A Ru Village", "Common-teyvat-192be7b183f6eee79-68": "Transfiguration Hall", "Common-teyvat-192be7b183f6eee79-69": "Vibrant House", "Common-teyvat-192be7b183f6eee79-7": "Xingluo Lake", "Common-teyvat-192be7b183f6eee79-70": "Oasis of plenty", "Common-teyvat-192be7b183f6eee79-71": "Tomb of the Red King", "Common-teyvat-192be7b183f6eee79-72": "Line 1", "Common-teyvat-192be7b183f6eee79-73": "Line 2", "Common-teyvat-192be7b183f6eee79-74": "Line 3", "Common-teyvat-192be7b183f6eee79-75": "Line 4", "Common-teyvat-192be7b183f6eee79-76": "Line 5", "Common-teyvat-192be7b183f6eee79-77": "Line 6", "Common-teyvat-192be7b183f6eee79-78": "Line 7", "Common-teyvat-192be7b183f6eee79-79": "Line 8", "Common-teyvat-192be7b183f6eee79-8": "Phaseless Wind Station", "Common-teyvat-192be7b183f6eee79-80": "Xumi Line 2", "Common-teyvat-192be7b183f6eee79-81": "Xumi Line 1", "Common-teyvat-192be7b183f6eee79-82": "Xumi Line 3", "Common-teyvat-192be7b183f6eee79-83": "Xumi Line 4", "Common-teyvat-192be7b183f6eee79-9": "Wangfeng Development Zone", "qian-feng-shen-dian-zhan": "Thousand Winds Temple Station", "wang-feng-jiao-zhan": "Lookout Point Station", "zhai-xing-ya-zhan": "Zhaixingya Station" } ================================================ FILE: src/i18n/locales/zh.json ================================================ { "language": "中文", "menu": { "alreadyAddStations": "已添加{{count}}站", "clickBlankToAddStation": "点击空白处新增站点", "redo": "重做", "done": "完成", "alreadyModified": "已修改{{count}}次", "dragToChangePoistion": "拖动站点更改位置", "withdraw": "撤销", "chooseALineFirst": "先选择一条线路,再点击线路卡片上的", "backgroudSetting": "背景设定", "white": "纯白", "lightYellow": "浅黄", "transparent": "透明", "changeBackgroudImage": "修改背景图", "importBackgroudImage": "导入背景图", "importImage": "导入图片", "deleteImage": "删除图片", "back": "返回", "clickAddStationToLine": "点击站点将它插入到{{lineName}}的第{{insertIndex}}站", "chooseAMap": "选择一张地图", "useTemplate": "以{{name}}为模板新建地图", "cancel": "取消", "station": "站点", "addStation": "添加站点", "changeStationPosition": "调整站点位置", "hidden": "隐藏", "show": "显示", "stationName": "站点名称", "turnOn": "启用", "AutoHidden": "自动隐藏", "line": "线路", "turnOff": "关闭", "insertStation": "插入站点...", "mediateMap": "居中路线图...", "addBackgroundImage": "设定背景...", "data": "数据", "addNewMap": "新建空白地图...", "addFromTemplate": "从已有地图新建...", "importFile": "导入文件...", "exportAsImage": "作为图片导出...", "exportAsSVG": "作为矢量图片导出...", "exportAsFile": "作为文件导出...", "recoverData": "恢复数据...", "exportRecoveryData": "导出恢复数据...", "about": "关于", "projectAddress": "项目地址...", "guide": "使用教程...", "videoGuild": "视频教程...", "RMP": "线路图工具包", "version": "版本 : ", "author": "作者 : 江户川瑞安", "symbol": "标识", "newMap": "新地图", "switchLanguage": "Switch to English" }, "shang-hai": "上海", "bei-jing": "北京", "guang-zhou": "广州", "shen-zhen": "深圳", "xiang-gang": "香港", "chang-sha": "长沙", "tian-jin": "天津", "station": { "up": "顶端", "upRight": "右上角", "right": "右侧", "rightDown": "右下角", "down": "底部", "downLeft": "左下角", "left": "左侧", "leftUp": "左上角", "auto": "自动", "x": "横坐标", "y": "纵坐标", "startHere": "以此为起点新建线路...", "remove": "从{{lineName}}的第{{stationIndex}}站移除", "delete": "删除站点...", "lines": "条线路", "poisition": "位置", "shape": "形状", "operation": "操作", "operateDes": "删除", "stationName": "标签" }, "shape": { "cicle": "圆形", "square": "正方形", "triangle": "三角形", "start": "五角星", "pentagon": "五边形", "hexagon": "六边形", "cross": "十字形", "rhombus": "菱形", "diamond": "钻石", "leaf": "叶子", "ginkgo": "银杏" }, "line": { "name": "名称", "order": "排序", "notSubLineAnymore": "不再作为支线", "asSubline": "设为支线", "deleteLine": "删除线路...", "stations": "个站点", "notInUse": "尚未开通", "fromTo": "从{{from}}开往{{to}}", "insertHere": "插入到这", "insertStation": "插入站点", "bendFirst": "斜向优先", "straight": "直线优先", "color": "颜色", "operation": "操作", "delete": "删除", "done": "完成" }, "opacity": "透明度", "recover": { "text": "需要恢复数据吗?", "subTitle": "缓存中有不久前编辑过的地图" }, "error": { "metError": "抱歉!程序出现错误", "dontWorry": "不过不用担心,您的地图没有丢失", "recoverFromCache": "从缓存中恢复错误发生前的地图", "exportNoErrorFile": "导出错误发生前的地图文件", "orYouCould": "或者您可以", "exportError": "导出当前包含错误的地图文件...", "sendAuthor": "以便发送给作者分析错误原因。" }, "huan-ying-shi-yong": "欢迎使用", "welcome": { "welcome": "欢迎使用", "skip": "暂时跳过", "goOn": "继续", "restart": "重新开始", "done": "完成" }, "welecome": { "minimetroweb": "迷你地铁地图构建工具" }, "tour": { "tryScale": "尝试缩放或者移动地图", "touchScale": "单指拖动地图,双指缩放地图", "mouseScale": "按住鼠标左键拖动移动地图,滚动鼠标滚轮缩放地图", "clickTile": "点击标题", "clickOpenMenu": "点击标题打开菜单" }, "zai-ci-dian-ji-biao-ti": "再次点击标题", "ke-yi-xiu-gai-biao-ti": "可以修改标题", "dian-ji-ren-yi-kong-bai-qu-yu-tui-chu-cai-dan": "点击任意空白区域退出菜单", "tui-chu-cai-dan": "退出菜单", "dian-ji-zhan-dian": "点击站点", "yi-da-kai-zhan-dian-ka-pian": "以打开站点卡片", "zhan-dian-ka-pian": "站点卡片", "ke-yi-zai-zhe-li-bian-ji-zhan-dian-de-suo-you-she-zhi": "可以在这里编辑站点的所有设置", "chang-shi-dian-ji-xian-lu": "尝试点击线路", "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": "图中的彩色线条即为线路,如果无法选中可以尝试放大地图再点选", "xian-lu-ka-pian": "线路卡片", "ke-yi-zai-zhe-li-bian-ji-xian-lu-de-suo-you-she-zhi": "可以在这里编辑线路的所有设置", "wu-xian-zi-chan": "无限资产", "wu-xian-da-hua-bu": "无限大画布", "zhi-chi": "支持", "wu-xian-tiao-xian-lu": "无限条线路", "wu-xian-ge-zhan-dian": "无限个站点", "le-jie-ji-ben-cao-zuo": "了解基本操作", "ling-huo-zou-xian": "灵活走线", "yi-tiao-xian-lu-neng-duo-ci-chuan-guo-tong-yi-zhan": "一条线路能多次穿过同一站", "q-zi-zou-xian-yu-zou-xian": "Q字走线与α走线", "zhi-chi-she-zhi": "支持设置", "zhi-xian": "支线", "le-jie-xian-lu-she-zhi": "了解线路设置", "fang-bian-cha-kan": "方便查看", "ka-pian-hua-zhan-shi-zhan-dian-yu-xian-lu": "卡片化展示站点与线路", "zhan-dian-xian-lu-hu-xiang": "站点线路互相", "guan-lian": "关联", "gao-liang": "高亮", "zhan-shi-xuan-zhong-zhan-dian-yu-xian-lu": "展示选中站点与线路", "le-jie-zhan-dian-ka-pian": "了解站点卡片", "gao-xiao-bian-ji": "高效编辑", "lian-xu-chuang-jian-zhan-dian-mo-shi": "连续创建站点模式", "lian-xu-tian-jia-zhan-dian": "连续添加站点", "mo-shi": "模式", "che-hui": "撤回", "yu": "与", "zhong-zuo": "重做", "le-jie-kuai-su-chuang-jian": "了解快速创建", "yi-dong-duan-zhi-chi": "移动端支持", "wan-zheng-de-chu-kong-shi-jian-zhi-chi": "完整的触控事件支持", "dan-zhi": "单指", "tuo-dong": "拖动", "shuang-zhi": "双指", "suo-fang-di-tu": "缩放地图", "shi-bie-dong-zuo-yi-tu": "识别动作意图", "jian-shao-wu-cao-zuo": "减少误操作", "zai-ping-ban-shang-ti-yan": "在平板上体验", "zi-dong-bi-rang": "自动避让", "zi-dong-tian-jia-pian-yi-zhi": "自动添加偏移值", "gong-xian-xian-lu": "共线线路", "bu-hui-zhong-die": "不会重叠", "zhan-dian-ming-cheng-zi-dong-xuan-ze-bai-fang-wei-zhi": "站点名称自动选择摆放位置", "le-jie-biao-qian-she-zhi": "了解标签设置", "dao-ru-dao-chu": "导入导出", "dao-chu-gao-fen-bian-shuai-tu-pian": "导出高分辨率图片", "dao-chu": "导出", "pngsvg-tu-pian": "PNG/SVG图片", "zhi-chi-cong-mo-ban-chuang-jian": "支持从模版创建", "le-jie-dao-ru-dao-chu": "了解导入导出", "dian-ji-biao-ti": "点击标题", "dian-ji-biao-ti-da-kai-cai-dan": "点击标题打开菜单", "cong-mo-ban-xin-jian": "从模板新建", "zai-yi-you-de-di-tu-shang-xiu-gai": "在已有的地图上修改", "xuan-ze-yi-ge-ni-xi-huan-de-cheng-shi": "选择一个你喜欢的城市", "ran-hou-dian-ji-xia-yi-bu": "然后点击下一步", "yi-ci-wei-mo-ban-xin-jian": "以此为模板新建", "que-ren-xin-jian-qian-qing-que-bao-yi-jing-bao-cun-le-dang-qian-de-di-tu": "确认新建前请确保已经保存了当前的地图", "da-kai-cai-dan": "打开菜单", "xuan-ze-zuo-wei-wen-jian-dao-chu": "选择作为文件导出", "zuo-wei-wen-jian-dao-chu": "作为文件导出", "xuan-ze-zuo-wei-wen-jian-dao-chu-0": "选择作为文件导出", "dao-ru-gang-cai-dao-chu-wen-jian": "导入刚才导出文件", "xia-ci-dian-ji-dao-ru-wen-jian-ke-yi-ji-xu-bian-ji": "下次点击导入文件可以继续编辑", "dao-chu-tu-pian": "导出图片", "dian-ji-dao-chu-tu-pian-dao-chu-ke-neng-hui-dao-zhi-ka-dun-ji-miao-shu-yu-zheng-chang-xian-xiang": "点击导出图片。导出可能会导致卡顿几秒属于正常现象", "cong-huan-cun-zhong-hui-fu-shu-ju": "从缓存中恢复数据", "ru-guo-mei-bao-cun-de-shi-hou-bu-xiao-xin-shua-xin-ke-yi-dian-ji-zhe-li-hui-fu-shu-ju": "如果没保存的时候不小心刷新,可以点击这里恢复数据", "dao-chu-hui-fu-shu-ju": "导出恢复数据", "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": "如果某一步操作后导致报错或崩溃,点击这里导出最后一次正确的数据。(如果遇到这种情况,请反馈给作者)", "dian-ji-ren-yi-xian-lu": "点击任意线路", "tu-zhong-cai-se-qu-xian-biao-shi-di-tie-xian-lu": "图中彩色曲线表示地铁线路", "xian-lu-ka-pian-0": "线路卡片", "ke-yi-zai-zhe-li-bian-ji-xian-lu-de-suo-you-she-zhi-0": "可以在这里编辑线路的所有设置", "dian-ji-ci-chu-kong-zhi-xian-lu-zai-liang-zhan-zhi-jian-de-zou-xiang": "点击此处控制线路在两站之间的走向", "zhan-dian-qu-jian-lian-xian-zhi-you-liang-zhong-fang-shi-xie-xian-yu-zhi-xian": "站点区间连线只有两种方式,斜线与直线", "fang-da-an-niu": "放大按钮", "ke-yi-tuo-kuan-xian-lu-ka-pian": "可以拓宽线路卡片", "suo-xiao-an-niu": "缩小按钮", "zai-suo-xiao-zhuang-tai-ke-yi-dian-ji-geng-duo-she-zhi": "在缩小状态可以点击更多设置", "bian-ji-an-niu": "编辑按钮", "jin-ru-geng-duo-she-zhi-mian-ban": "进入更多设置面板", "ci-chu-xiu-gai-xian-lu-ming-cheng-yu-biao-shi": "此处修改线路名称与标识", "pai-xu-ke-yi-kong-zhi-xian-lu-jian-de-zhe-dang-guan-xi": "排序可以控制线路间的遮挡关系", "dian-ji-ci-chu-xiu-gai-xian-lu-biao-shi-se": "点击此处修改线路标识色", "zuo-xia-jiao-de-yan-se-xuan-ze-qi-ke-yi-qu-se": "左下角的颜色选择器可以取色", "dian-ji-ci-chu-shan-chu-xian-lu-huo-zhe-she-zhi-zhi-xian": "点击此处删除线路或者设置支线", "she-zhi-zhi-xian-hou-xian-lu-hui-yi-xu-xian-xian-shi-qie-lian-jie-chu-bu-zai-xian-shi-ba-shou": "设置支线后线路会以虚线显示,且连接处不再显示把手", "dian-ji-ci-chu-wan-cheng-she-zhi": "点击此处完成设置", "hui-dao-zhan-dian-ye": "回到站点页", "xian-shi-tu-jing-zhan-dian-ka-pian": "显示途径站点卡片", "dian-ji-ci-chu-suo-yi-tu-jing-de-zhan-dian-ka-pian": "点击此处所以途径的站点卡片", "da-kai-cai-dan-0": "打开菜单", "dian-ji-biao-ti-da-kai-cai-dan-0": "点击标题打开菜单", "dian-ji-tian-jia-zhan-dian": "点击添加站点", "jin-ru-tian-jia-zhan-dian-mo-shi": "进入添加站点模式", "dian-ji-ren-yi-kong-bai-chu-tian-jia-zhan-dian": "点击任意空白处添加站点", "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": "添加好之后点击下一步(建议您在屏幕左上角的空白区域添加三个站点)", "dian-ji-wan-cheng": "点击完成", "tui-chu-bian-ji-mo-shi": "退出编辑模式", "dian-ji-gang-cai-chuang-jian-de-zhan-dian": "点击刚才创建的站点", "da-kai-zhan-dian-xin-xi-ka-pian": "打开站点信息卡片", "dian-ji-cao-zuo-xuan-xiang-ka": "点击操作选项卡", "wo-men-lai-tian-jia-xian-lu": "我们来添加线路", "dian-ji-yi-ci-wei-qi-dian-xin-jian-xian-lu": "点击以此为起点新建线路", "jin-ru-tian-jia-xian-lu-mo-shi": "进入添加线路模式", "dian-ji-zhan-dian-0": "点击站点", "lian-jie-zhan-dian": "连接站点", "xian-lu-jiu-chuang-jian-hao-le": "线路就创建好了", "yi-da-kai-zhan-dian-ka-pian-0": "以打开站点卡片", "bian-ji-zhan-dian-wei-zhi": "编辑站点位置", "dian-ji-zuo-biao-zhi-hou-shi-yong-shu-biao-gun-lun-ke-yi-jing-que-tiao-jie": "点击坐标之后使用鼠标滚轮可以精确调节", "dian-ji-ci-chu-xiu-gai-zhan-dian-xing-zhuang": "点击此处修改站点形状", "xiu-gai-xing-zhuang": "修改形状", "dian-ji-ci-chu-shan-chu-zhan-dian-huo-xin-jian-xian-lu": "点击此处删除站点或新建线路", "shan-chu-huo-xin-jian": "删除或新建", "dian-ji-ci-chu-she-zhi-zhan-dian-ming-cheng-wei-zhi": "点击此处设置站点名称位置", "zhan-dian-ming-wei-zhi": "站点名位置", "dian-ji-hui-se-de-fang-kuai-xuan-ze-fang-wei": "点击灰色的方块选择方位", "dian-ji-zhong-jian-de-wen-zi-hui-fu-zi-dong-wei-zhi": "点击中间的文字恢复自动位置", "dian-ji-biao-ti-da-kai-cai-dan-1": "点击标题打开菜单", "guan-bi-zi-dong-yin-cang": "关闭自动隐藏", "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": "站点名会在地图缩放到小尺度时自动隐藏。关闭自动隐藏以确保可以一直显示站点名称", "dian-ji-ren-yi-kong-bai-qu-yu-tui-chu-cai-dan-0": "点击任意空白区域退出菜单", "tui-chu-cai-dan-0": "退出菜单", "yi-da-kai-zhan-dian-ka-pian-1": "以打开站点卡片", "zai-zhe-li-xiang-xia-gun-dong-yi-xia": "在这里向下滚动一下", "zai-dian-ji-biao-qian-an-niu": "再点击标签按钮", "chang-shi-gai-bian-zhan-dian-ming-dao-you-xia-jiao": "尝试改变站点名到右下角", "dian-ji-you-xia-jiao-de-hui-se-fang-kuai": "点击右下角的灰色方块", "ke-yi-kan-dao-zhan-dian-ming-yi-jing-wei-yu-zhan-dian-you-xia-jiao-le": "可以看到站点名已经位于站点右下角了", "ke-yi-yong-zhe-zhong-fang-fa-xiu-gai-zhan-dian-ming-wei-zhi": "可以用这种方法修改站点名位置", "dian-ji-zhong-jian-de-wen-zi-hui-fu-dao-zi-dong-xuan-ze-wei-zhi": "点击中间的文字恢复到自动选择位置", "hui-fu-dao-zi-dong-xuan-ze-wei-zhi": "恢复到自动选择位置", "shang-yi-bu": "上一步", "wan-cheng": "完成", "xia-yi-bu": "下一步", "di-current-bu-gong-total-bu": "第{0}步,共{1}步", "delete": { "remove": "移除", "delete": "删除", "subtile": "不再作为{{lineName}}的第{{index}}站", "cancel": "取消" }, "que-shi-yao-deletetext-ma": "确实要{{deleteText}}吗?", "teyvat": "提瓦特", "wang-feng-jiao-zhan": "望风角站", "zhai-xing-ya-zhan": "摘星崖站", "qian-feng-shen-dian-zhan": "千风神殿站", "newStation": "新增站点", "lineNo": "{{lineId}} 号线", "Common-teyvat-192be7b183f6eee79-1": "鹰翔海滩", "Common-teyvat-192be7b183f6eee79-2": "达达乌帕谷", "Common-teyvat-192be7b183f6eee79-3": "无相之雷", "Common-teyvat-192be7b183f6eee79-4": "誓言岬机场", "Common-teyvat-192be7b183f6eee79-5": "风起地站", "Common-teyvat-192be7b183f6eee79-6": "星落湖南", "Common-teyvat-192be7b183f6eee79-7": "星落湖", "Common-teyvat-192be7b183f6eee79-8": "无相之风站", "Common-teyvat-192be7b183f6eee79-9": "望风开发区", "Common-teyvat-192be7b183f6eee79-10": "风神像站", "Common-teyvat-192be7b183f6eee79-11": "蒙德站", "Common-teyvat-192be7b183f6eee79-12": "清泉镇", "Common-teyvat-192be7b183f6eee79-13": "晨曦酒庄", "Common-teyvat-192be7b183f6eee79-14": "奔狼岭", "Common-teyvat-192be7b183f6eee79-15": "奔狼岭北", "Common-teyvat-192be7b183f6eee79-16": "风龙风景区", "Common-teyvat-192be7b183f6eee79-17": "明冠峡", "Common-teyvat-192be7b183f6eee79-18": "风龙国际机场", "Common-teyvat-192be7b183f6eee79-19": "雪山脚站", "Common-teyvat-192be7b183f6eee79-20": "无相之冰", "Common-teyvat-192be7b183f6eee79-21": "雪山顶站", "Common-teyvat-192be7b183f6eee79-22": "七天神像站", "Common-teyvat-192be7b183f6eee79-23": "雪山湖", "Common-teyvat-192be7b183f6eee79-24": "地中之盐", "Common-teyvat-192be7b183f6eee79-25": "望舒客栈", "Common-teyvat-192be7b183f6eee79-26": "石门", "Common-teyvat-192be7b183f6eee79-27": "轻策庄", "Common-teyvat-192be7b183f6eee79-28": "望舒南", "Common-teyvat-192be7b183f6eee79-29": "归离原", "Common-teyvat-192be7b183f6eee79-30": "渌华池南", "Common-teyvat-192be7b183f6eee79-31": "璃月港", "Common-teyvat-192be7b183f6eee79-32": "黄金屋", "Common-teyvat-192be7b183f6eee79-33": "青墟浦", "Common-teyvat-192be7b183f6eee79-34": "灵矩国际机场", "Common-teyvat-192be7b183f6eee79-35": "天衡山", "Common-teyvat-192be7b183f6eee79-36": "遁玉陵", "Common-teyvat-192be7b183f6eee79-37": "渌华池", "Common-teyvat-192be7b183f6eee79-38": "翠玦坡", "Common-teyvat-192be7b183f6eee79-39": "太山府", "Common-teyvat-192be7b183f6eee79-40": "机场东", "Common-teyvat-192be7b183f6eee79-41": "庆云国际机场", "Common-teyvat-192be7b183f6eee79-42": "奥藏山", "Common-teyvat-192be7b183f6eee79-43": "琥牢山", "Common-teyvat-192be7b183f6eee79-44": "伏龙树", "Common-teyvat-192be7b183f6eee79-45": "南天门", "Common-teyvat-192be7b183f6eee79-46": "天遒谷", "Common-teyvat-192be7b183f6eee79-47": "伏鳌谷", "Common-teyvat-192be7b183f6eee79-48": "香醉坡", "Common-teyvat-192be7b183f6eee79-49": "无郁稠林", "Common-teyvat-192be7b183f6eee79-50": "卡萨扎莱宫", "Common-teyvat-192be7b183f6eee79-51": "茸蕈窟", "Common-teyvat-192be7b183f6eee79-52": "须弥城", "Common-teyvat-192be7b183f6eee79-53": "化城郭", "Common-teyvat-192be7b183f6eee79-54": "丹砂崖", "Common-teyvat-192be7b183f6eee79-55": "降诸魔山东", "Common-teyvat-192be7b183f6eee79-56": "降诸魔山", "Common-teyvat-192be7b183f6eee79-57": "奥摩斯港北", "Common-teyvat-192be7b183f6eee79-58": "奥摩斯港", "Common-teyvat-192be7b183f6eee79-59": "维摩庄", "Common-teyvat-192be7b183f6eee79-60": "禅那园", "Common-teyvat-192be7b183f6eee79-61": "谒颂幽境", "Common-teyvat-192be7b183f6eee79-62": "桓那兰那", "Common-teyvat-192be7b183f6eee79-63": "五绿洲", "Common-teyvat-192be7b183f6eee79-64": "喀万驿", "Common-teyvat-192be7b183f6eee79-65": "水天丛林", "Common-teyvat-192be7b183f6eee79-66": "荼诃之座", "Common-teyvat-192be7b183f6eee79-67": "阿如村", "Common-teyvat-192be7b183f6eee79-68": "圣显厅", "Common-teyvat-192be7b183f6eee79-69": "活力之家", "Common-teyvat-192be7b183f6eee79-70": "丰饶绿洲", "Common-teyvat-192be7b183f6eee79-71": "赤王陵", "Common-teyvat-192be7b183f6eee79-72": "1号线", "Common-teyvat-192be7b183f6eee79-73": "2号线", "Common-teyvat-192be7b183f6eee79-74": "3号线", "Common-teyvat-192be7b183f6eee79-75": "4号线", "Common-teyvat-192be7b183f6eee79-76": "5号线", "Common-teyvat-192be7b183f6eee79-77": "6号线", "Common-teyvat-192be7b183f6eee79-78": "7号线", "Common-teyvat-192be7b183f6eee79-79": "8号线", "Common-teyvat-192be7b183f6eee79-80": "须弥2号线", "Common-teyvat-192be7b183f6eee79-81": "须弥1号线", "Common-teyvat-192be7b183f6eee79-82": "须弥3号线", "Common-teyvat-192be7b183f6eee79-83": "须弥4号线" } ================================================ FILE: src/index.js ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './Entrance/App'; import reportWebVitals from './Entrance/reportWebVitals'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ================================================ FILE: src/types/svg.d.ts ================================================ declare module '*.svg' { import React = require('react'); export const ReactComponent: React.FC>; const src: string; export default src; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "jsx": "react", /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ // "outDir": "./", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "resolveJsonModule": true, "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } }