├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── public ├── assets │ └── image │ │ ├── Openscope1.png │ │ ├── Openscope2.png │ │ ├── Openscope3.png │ │ ├── Openscope4.png │ │ ├── Openscope5.png │ │ ├── close.dark.svg │ │ ├── close.white.svg │ │ ├── connect.white.svg │ │ ├── darkmode.dark.svg │ │ ├── display.dark.svg │ │ ├── display.grey.svg │ │ ├── headset.dark.svg │ │ ├── headset.grey.svg │ │ ├── icon.ico │ │ ├── lightmode.grey.svg │ │ ├── logo.svg │ │ ├── minimize.dark.svg │ │ ├── minimize.white.svg │ │ ├── open.dark.svg │ │ ├── open.grey.svg │ │ ├── radar.dark.svg │ │ ├── radar.grey.svg │ │ ├── restore.dark.svg │ │ ├── restore.white.svg │ │ ├── runway.dark.large.svg │ │ ├── runway.dark.svg │ │ ├── runway.white.large.svg │ │ ├── runway.white.svg │ │ ├── setting.dark.svg │ │ ├── setting.grey.svg │ │ ├── voice.dark.svg │ │ └── voice.grey.svg ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── image │ │ ├── Openscope1.png │ │ ├── Openscope2.png │ │ ├── Openscope3.png │ │ ├── Openscope4.png │ │ ├── Openscope5.png │ │ ├── close.dark.svg │ │ ├── close.white.svg │ │ ├── connect.white.svg │ │ ├── darkmode.dark.svg │ │ ├── display.dark.svg │ │ ├── display.grey.svg │ │ ├── headset.dark.svg │ │ ├── headset.grey.svg │ │ ├── icon.ico │ │ ├── lightmode.grey.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── minimize.dark.svg │ │ ├── minimize.white.svg │ │ ├── open.dark.svg │ │ ├── open.grey.svg │ │ ├── radar.dark.svg │ │ ├── radar.grey.svg │ │ ├── restore.dark.svg │ │ ├── restore.white.svg │ │ ├── runway.dark.large.svg │ │ ├── runway.dark.svg │ │ ├── runway.white.large.svg │ │ ├── runway.white.svg │ │ ├── setting.dark.svg │ │ ├── setting.grey.svg │ │ ├── voice.dark.svg │ │ └── voice.grey.svg ├── background.ts ├── components │ ├── AircraftList.vue │ ├── AppBar.vue │ ├── DraggableBase.vue │ ├── MessageContainer.vue │ ├── MetarContainer.vue │ └── RadarScreen.vue ├── lib │ ├── coordparser.ts │ ├── datatype.d.ts │ ├── elementId.ts │ ├── global.ts │ ├── ipcChannel.ts │ ├── popupMsger.ts │ ├── sectortype.ts │ ├── settingtype.ts │ └── spaceformatter.ts ├── main.ts ├── preloads │ └── preload.ts ├── shims-vue.d.ts └── utils │ ├── drawer.ts │ ├── prfsectorloader.ts │ ├── settingloader.ts │ └── wxfetcher.worker.ts └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | #Electron-builder output 26 | /dist_electron -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npmmirror.com/mirrors/electron/ 2 | ecltron_custom_dir={{ version }} -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for 14 | software and other kinds of works. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | the GNU General Public License is intended to guarantee your freedom 19 | to share and change all versions of a program--to make sure it remains 20 | free software for all its users. We, the Free Software Foundation, use 21 | the GNU General Public License for most of our software; it applies 22 | also to any other work released this way by its authors. You can apply 23 | it to your programs, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | them if you wish), that you receive source code or can get it if you 29 | want it, that you can change the software or use pieces of it in new 30 | free programs, and that you know you can do these things. 31 | 32 | To protect your rights, we need to prevent others from denying you 33 | these rights or asking you to surrender the rights. Therefore, you 34 | have certain responsibilities if you distribute copies of the 35 | software, or if you modify it: responsibilities to respect the freedom 36 | of others. 37 | 38 | For example, if you distribute copies of such a program, whether 39 | gratis or for a fee, you must pass on to the recipients the same 40 | freedoms that you received. You must make sure that they, too, receive 41 | or can get the source code. And you must show them these terms so they 42 | know their rights. 43 | 44 | Developers that use the GNU GPL protect your rights with two steps: 45 | (1) assert copyright on the software, and (2) offer you this License 46 | giving you legal permission to copy, distribute and/or modify it. 47 | 48 | For the developers' and authors' protection, the GPL clearly explains 49 | that there is no warranty for this free software. For both users' and 50 | authors' sake, the GPL requires that modified versions be marked as 51 | changed, so that their problems will not be attributed erroneously to 52 | authors of previous versions. 53 | 54 | Some devices are designed to deny users access to install or run 55 | modified versions of the software inside them, although the 56 | manufacturer can do so. This is fundamentally incompatible with the 57 | aim of protecting users' freedom to change the software. The 58 | systematic pattern of such abuse occurs in the area of products for 59 | individuals to use, which is precisely where it is most unacceptable. 60 | Therefore, we have designed this version of the GPL to prohibit the 61 | practice for those products. If such problems arise substantially in 62 | other domains, we stand ready to extend this provision to those 63 | domains in future versions of the GPL, as needed to protect the 64 | freedom of users. 65 | 66 | Finally, every program is threatened constantly by software patents. 67 | States should not allow patents to restrict development and use of 68 | software on general-purpose computers, but in those that do, we wish 69 | to avoid the special danger that patents applied to a free program 70 | could make it effectively proprietary. To prevent this, the GPL 71 | assures that patents cannot be used to render the program non-free. 72 | 73 | The precise terms and conditions for copying, distribution and 74 | modification follow. 75 | 76 | ### TERMS AND CONDITIONS 77 | 78 | #### 0. Definitions. 79 | 80 | "This License" refers to version 3 of the GNU General Public License. 81 | 82 | "Copyright" also means copyright-like laws that apply to other kinds 83 | of works, such as semiconductor masks. 84 | 85 | "The Program" refers to any copyrightable work licensed under this 86 | License. Each licensee is addressed as "you". "Licensees" and 87 | "recipients" may be individuals or organizations. 88 | 89 | To "modify" a work means to copy from or adapt all or part of the work 90 | in a fashion requiring copyright permission, other than the making of 91 | an exact copy. The resulting work is called a "modified version" of 92 | the earlier work or a work "based on" the earlier work. 93 | 94 | A "covered work" means either the unmodified Program or a work based 95 | on the Program. 96 | 97 | To "propagate" a work means to do anything with it that, without 98 | permission, would make you directly or secondarily liable for 99 | infringement under applicable copyright law, except executing it on a 100 | computer or modifying a private copy. Propagation includes copying, 101 | distribution (with or without modification), making available to the 102 | public, and in some countries other activities as well. 103 | 104 | To "convey" a work means any kind of propagation that enables other 105 | parties to make or receive copies. Mere interaction with a user 106 | through a computer network, with no transfer of a copy, is not 107 | conveying. 108 | 109 | An interactive user interface displays "Appropriate Legal Notices" to 110 | the extent that it includes a convenient and prominently visible 111 | feature that (1) displays an appropriate copyright notice, and (2) 112 | tells the user that there is no warranty for the work (except to the 113 | extent that warranties are provided), that licensees may convey the 114 | work under this License, and how to view a copy of this License. If 115 | the interface presents a list of user commands or options, such as a 116 | menu, a prominent item in the list meets this criterion. 117 | 118 | #### 1. Source Code. 119 | 120 | The "source code" for a work means the preferred form of the work for 121 | making modifications to it. "Object code" means any non-source form of 122 | a work. 123 | 124 | A "Standard Interface" means an interface that either is an official 125 | standard defined by a recognized standards body, or, in the case of 126 | interfaces specified for a particular programming language, one that 127 | is widely used among developers working in that language. 128 | 129 | The "System Libraries" of an executable work include anything, other 130 | than the work as a whole, that (a) is included in the normal form of 131 | packaging a Major Component, but which is not part of that Major 132 | Component, and (b) serves only to enable use of the work with that 133 | Major Component, or to implement a Standard Interface for which an 134 | implementation is available to the public in source code form. A 135 | "Major Component", in this context, means a major essential component 136 | (kernel, window system, and so on) of the specific operating system 137 | (if any) on which the executable work runs, or a compiler used to 138 | produce the work, or an object code interpreter used to run it. 139 | 140 | The "Corresponding Source" for a work in object code form means all 141 | the source code needed to generate, install, and (for an executable 142 | work) run the object code and to modify the work, including scripts to 143 | control those activities. However, it does not include the work's 144 | System Libraries, or general-purpose tools or generally available free 145 | programs which are used unmodified in performing those activities but 146 | which are not part of the work. For example, Corresponding Source 147 | includes interface definition files associated with source files for 148 | the work, and the source code for shared libraries and dynamically 149 | linked subprograms that the work is specifically designed to require, 150 | such as by intimate data communication or control flow between those 151 | subprograms and other parts of the work. 152 | 153 | The Corresponding Source need not include anything that users can 154 | regenerate automatically from other parts of the Corresponding Source. 155 | 156 | The Corresponding Source for a work in source code form is that same 157 | work. 158 | 159 | #### 2. Basic Permissions. 160 | 161 | All rights granted under this License are granted for the term of 162 | copyright on the Program, and are irrevocable provided the stated 163 | conditions are met. This License explicitly affirms your unlimited 164 | permission to run the unmodified Program. The output from running a 165 | covered work is covered by this License only if the output, given its 166 | content, constitutes a covered work. This License acknowledges your 167 | rights of fair use or other equivalent, as provided by copyright law. 168 | 169 | You may make, run and propagate covered works that you do not convey, 170 | without conditions so long as your license otherwise remains in force. 171 | You may convey covered works to others for the sole purpose of having 172 | them make modifications exclusively for you, or provide you with 173 | facilities for running those works, provided that you comply with the 174 | terms of this License in conveying all material for which you do not 175 | control copyright. Those thus making or running the covered works for 176 | you must do so exclusively on your behalf, under your direction and 177 | control, on terms that prohibit them from making any copies of your 178 | copyrighted material outside their relationship with you. 179 | 180 | Conveying under any other circumstances is permitted solely under the 181 | conditions stated below. Sublicensing is not allowed; section 10 makes 182 | it unnecessary. 183 | 184 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 185 | 186 | No covered work shall be deemed part of an effective technological 187 | measure under any applicable law fulfilling obligations under article 188 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 189 | similar laws prohibiting or restricting circumvention of such 190 | measures. 191 | 192 | When you convey a covered work, you waive any legal power to forbid 193 | circumvention of technological measures to the extent such 194 | circumvention is effected by exercising rights under this License with 195 | respect to the covered work, and you disclaim any intention to limit 196 | operation or modification of the work as a means of enforcing, against 197 | the work's users, your or third parties' legal rights to forbid 198 | circumvention of technological measures. 199 | 200 | #### 4. Conveying Verbatim Copies. 201 | 202 | You may convey verbatim copies of the Program's source code as you 203 | receive it, in any medium, provided that you conspicuously and 204 | appropriately publish on each copy an appropriate copyright notice; 205 | keep intact all notices stating that this License and any 206 | non-permissive terms added in accord with section 7 apply to the code; 207 | keep intact all notices of the absence of any warranty; and give all 208 | recipients a copy of this License along with the Program. 209 | 210 | You may charge any price or no price for each copy that you convey, 211 | and you may offer support or warranty protection for a fee. 212 | 213 | #### 5. Conveying Modified Source Versions. 214 | 215 | You may convey a work based on the Program, or the modifications to 216 | produce it from the Program, in the form of source code under the 217 | terms of section 4, provided that you also meet all of these 218 | conditions: 219 | 220 | - a) The work must carry prominent notices stating that you modified 221 | it, and giving a relevant date. 222 | - b) The work must carry prominent notices stating that it is 223 | released under this License and any conditions added under 224 | section 7. This requirement modifies the requirement in section 4 225 | to "keep intact all notices". 226 | - c) You must license the entire work, as a whole, under this 227 | License to anyone who comes into possession of a copy. This 228 | License will therefore apply, along with any applicable section 7 229 | additional terms, to the whole of the work, and all its parts, 230 | regardless of how they are packaged. This License gives no 231 | permission to license the work in any other way, but it does not 232 | invalidate such permission if you have separately received it. 233 | - d) If the work has interactive user interfaces, each must display 234 | Appropriate Legal Notices; however, if the Program has interactive 235 | interfaces that do not display Appropriate Legal Notices, your 236 | work need not make them do so. 237 | 238 | A compilation of a covered work with other separate and independent 239 | works, which are not by their nature extensions of the covered work, 240 | and which are not combined with it such as to form a larger program, 241 | in or on a volume of a storage or distribution medium, is called an 242 | "aggregate" if the compilation and its resulting copyright are not 243 | used to limit the access or legal rights of the compilation's users 244 | beyond what the individual works permit. Inclusion of a covered work 245 | in an aggregate does not cause this License to apply to the other 246 | parts of the aggregate. 247 | 248 | #### 6. Conveying Non-Source Forms. 249 | 250 | You may convey a covered work in object code form under the terms of 251 | sections 4 and 5, provided that you also convey the machine-readable 252 | Corresponding Source under the terms of this License, in one of these 253 | ways: 254 | 255 | - a) Convey the object code in, or embodied in, a physical product 256 | (including a physical distribution medium), accompanied by the 257 | Corresponding Source fixed on a durable physical medium 258 | customarily used for software interchange. 259 | - b) Convey the object code in, or embodied in, a physical product 260 | (including a physical distribution medium), accompanied by a 261 | written offer, valid for at least three years and valid for as 262 | long as you offer spare parts or customer support for that product 263 | model, to give anyone who possesses the object code either (1) a 264 | copy of the Corresponding Source for all the software in the 265 | product that is covered by this License, on a durable physical 266 | medium customarily used for software interchange, for a price no 267 | more than your reasonable cost of physically performing this 268 | conveying of source, or (2) access to copy the Corresponding 269 | Source from a network server at no charge. 270 | - c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | - d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | - e) Convey the object code using peer-to-peer transmission, 288 | provided you inform other peers where the object code and 289 | Corresponding Source of the work are being offered to the general 290 | public at no charge under subsection 6d. 291 | 292 | A separable portion of the object code, whose source code is excluded 293 | from the Corresponding Source as a System Library, need not be 294 | included in conveying the object code work. 295 | 296 | A "User Product" is either (1) a "consumer product", which means any 297 | tangible personal property which is normally used for personal, 298 | family, or household purposes, or (2) anything designed or sold for 299 | incorporation into a dwelling. In determining whether a product is a 300 | consumer product, doubtful cases shall be resolved in favor of 301 | coverage. For a particular product received by a particular user, 302 | "normally used" refers to a typical or common use of that class of 303 | product, regardless of the status of the particular user or of the way 304 | in which the particular user actually uses, or expects or is expected 305 | to use, the product. A product is a consumer product regardless of 306 | whether the product has substantial commercial, industrial or 307 | non-consumer uses, unless such uses represent the only significant 308 | mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to 312 | install and execute modified versions of a covered work in that User 313 | Product from a modified version of its Corresponding Source. The 314 | information must suffice to ensure that the continued functioning of 315 | the modified object code is in no case prevented or interfered with 316 | solely because modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or 331 | updates for a work that has been modified or installed by the 332 | recipient, or for the User Product in which it has been modified or 333 | installed. Access to a network may be denied when the modification 334 | itself materially and adversely affects the operation of the network 335 | or violates the rules and protocols for communication across the 336 | network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | #### 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders 364 | of that material) supplement the terms of this License with terms: 365 | 366 | - a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | - b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | - c) Prohibiting misrepresentation of the origin of that material, 372 | or requiring that modified versions of such material be marked in 373 | reasonable ways as different from the original version; or 374 | - d) Limiting the use for publicity purposes of names of licensors 375 | or authors of the material; or 376 | - e) Declining to grant rights under trademark law for use of some 377 | trade names, trademarks, or service marks; or 378 | - f) Requiring indemnification of licensors and authors of that 379 | material by anyone who conveys the material (or modified versions 380 | of it) with contractual assumptions of liability to the recipient, 381 | for any liability that these contractual assumptions directly 382 | impose on those licensors and authors. 383 | 384 | All other non-permissive additional terms are considered "further 385 | restrictions" within the meaning of section 10. If the Program as you 386 | received it, or any part of it, contains a notice stating that it is 387 | governed by this License along with a term that is a further 388 | restriction, you may remove that term. If a license document contains 389 | a further restriction but permits relicensing or conveying under this 390 | License, you may add to a covered work material governed by the terms 391 | of that license document, provided that the further restriction does 392 | not survive such relicensing or conveying. 393 | 394 | If you add terms to a covered work in accord with this section, you 395 | must place, in the relevant source files, a statement of the 396 | additional terms that apply to those files, or a notice indicating 397 | where to find the applicable terms. 398 | 399 | Additional terms, permissive or non-permissive, may be stated in the 400 | form of a separately written license, or stated as exceptions; the 401 | above requirements apply either way. 402 | 403 | #### 8. Termination. 404 | 405 | You may not propagate or modify a covered work except as expressly 406 | provided under this License. Any attempt otherwise to propagate or 407 | modify it is void, and will automatically terminate your rights under 408 | this License (including any patent licenses granted under the third 409 | paragraph of section 11). 410 | 411 | However, if you cease all violation of this License, then your license 412 | from a particular copyright holder is reinstated (a) provisionally, 413 | unless and until the copyright holder explicitly and finally 414 | terminates your license, and (b) permanently, if the copyright holder 415 | fails to notify you of the violation by some reasonable means prior to 416 | 60 days after the cessation. 417 | 418 | Moreover, your license from a particular copyright holder is 419 | reinstated permanently if the copyright holder notifies you of the 420 | violation by some reasonable means, this is the first time you have 421 | received notice of violation of this License (for any work) from that 422 | copyright holder, and you cure the violation prior to 30 days after 423 | your receipt of the notice. 424 | 425 | Termination of your rights under this section does not terminate the 426 | licenses of parties who have received copies or rights from you under 427 | this License. If your rights have been terminated and not permanently 428 | reinstated, you do not qualify to receive new licenses for the same 429 | material under section 10. 430 | 431 | #### 9. Acceptance Not Required for Having Copies. 432 | 433 | You are not required to accept this License in order to receive or run 434 | a copy of the Program. Ancillary propagation of a covered work 435 | occurring solely as a consequence of using peer-to-peer transmission 436 | to receive a copy likewise does not require acceptance. However, 437 | nothing other than this License grants you permission to propagate or 438 | modify any covered work. These actions infringe copyright if you do 439 | not accept this License. Therefore, by modifying or propagating a 440 | covered work, you indicate your acceptance of this License to do so. 441 | 442 | #### 10. Automatic Licensing of Downstream Recipients. 443 | 444 | Each time you convey a covered work, the recipient automatically 445 | receives a license from the original licensors, to run, modify and 446 | propagate that work, subject to this License. You are not responsible 447 | for enforcing compliance by third parties with this License. 448 | 449 | An "entity transaction" is a transaction transferring control of an 450 | organization, or substantially all assets of one, or subdividing an 451 | organization, or merging organizations. If propagation of a covered 452 | work results from an entity transaction, each party to that 453 | transaction who receives a copy of the work also receives whatever 454 | licenses to the work the party's predecessor in interest had or could 455 | give under the previous paragraph, plus a right to possession of the 456 | Corresponding Source of the work from the predecessor in interest, if 457 | the predecessor has it or can get it with reasonable efforts. 458 | 459 | You may not impose any further restrictions on the exercise of the 460 | rights granted or affirmed under this License. For example, you may 461 | not impose a license fee, royalty, or other charge for exercise of 462 | rights granted under this License, and you may not initiate litigation 463 | (including a cross-claim or counterclaim in a lawsuit) alleging that 464 | any patent claim is infringed by making, using, selling, offering for 465 | sale, or importing the Program or any portion of it. 466 | 467 | #### 11. Patents. 468 | 469 | A "contributor" is a copyright holder who authorizes use under this 470 | License of the Program or a work on which the Program is based. The 471 | work thus licensed is called the contributor's "contributor version". 472 | 473 | A contributor's "essential patent claims" are all patent claims owned 474 | or controlled by the contributor, whether already acquired or 475 | hereafter acquired, that would be infringed by some manner, permitted 476 | by this License, of making, using, or selling its contributor version, 477 | but do not include claims that would be infringed only as a 478 | consequence of further modification of the contributor version. For 479 | purposes of this definition, "control" includes the right to grant 480 | patent sublicenses in a manner consistent with the requirements of 481 | this License. 482 | 483 | Each contributor grants you a non-exclusive, worldwide, royalty-free 484 | patent license under the contributor's essential patent claims, to 485 | make, use, sell, offer for sale, import and otherwise run, modify and 486 | propagate the contents of its contributor version. 487 | 488 | In the following three paragraphs, a "patent license" is any express 489 | agreement or commitment, however denominated, not to enforce a patent 490 | (such as an express permission to practice a patent or covenant not to 491 | sue for patent infringement). To "grant" such a patent license to a 492 | party means to make such an agreement or commitment not to enforce a 493 | patent against the party. 494 | 495 | If you convey a covered work, knowingly relying on a patent license, 496 | and the Corresponding Source of the work is not available for anyone 497 | to copy, free of charge and under the terms of this License, through a 498 | publicly available network server or other readily accessible means, 499 | then you must either (1) cause the Corresponding Source to be so 500 | available, or (2) arrange to deprive yourself of the benefit of the 501 | patent license for this particular work, or (3) arrange, in a manner 502 | consistent with the requirements of this License, to extend the patent 503 | license to downstream recipients. "Knowingly relying" means you have 504 | actual knowledge that, but for the patent license, your conveying the 505 | covered work in a country, or your recipient's use of the covered work 506 | in a country, would infringe one or more identifiable patents in that 507 | country that you have reason to believe are valid. 508 | 509 | If, pursuant to or in connection with a single transaction or 510 | arrangement, you convey, or propagate by procuring conveyance of, a 511 | covered work, and grant a patent license to some of the parties 512 | receiving the covered work authorizing them to use, propagate, modify 513 | or convey a specific copy of the covered work, then the patent license 514 | you grant is automatically extended to all recipients of the covered 515 | work and works based on it. 516 | 517 | A patent license is "discriminatory" if it does not include within the 518 | scope of its coverage, prohibits the exercise of, or is conditioned on 519 | the non-exercise of one or more of the rights that are specifically 520 | granted under this License. You may not convey a covered work if you 521 | are a party to an arrangement with a third party that is in the 522 | business of distributing software, under which you make payment to the 523 | third party based on the extent of your activity of conveying the 524 | work, and under which the third party grants, to any of the parties 525 | who would receive the covered work from you, a discriminatory patent 526 | license (a) in connection with copies of the covered work conveyed by 527 | you (or copies made from those copies), or (b) primarily for and in 528 | connection with specific products or compilations that contain the 529 | covered work, unless you entered into that arrangement, or that patent 530 | license was granted, prior to 28 March 2007. 531 | 532 | Nothing in this License shall be construed as excluding or limiting 533 | any implied license or other defenses to infringement that may 534 | otherwise be available to you under applicable patent law. 535 | 536 | #### 12. No Surrender of Others' Freedom. 537 | 538 | If conditions are imposed on you (whether by court order, agreement or 539 | otherwise) that contradict the conditions of this License, they do not 540 | excuse you from the conditions of this License. If you cannot convey a 541 | covered work so as to satisfy simultaneously your obligations under 542 | this License and any other pertinent obligations, then as a 543 | consequence you may not convey it at all. For example, if you agree to 544 | terms that obligate you to collect a royalty for further conveying 545 | from those to whom you convey the Program, the only way you could 546 | satisfy both those terms and this License would be to refrain entirely 547 | from conveying the Program. 548 | 549 | #### 13. Use with the GNU Affero General Public License. 550 | 551 | Notwithstanding any other provision of this License, you have 552 | permission to link or combine any covered work with a work licensed 553 | under version 3 of the GNU Affero General Public License into a single 554 | combined work, and to convey the resulting work. The terms of this 555 | License will continue to apply to the part which is the covered work, 556 | but the special requirements of the GNU Affero General Public License, 557 | section 13, concerning interaction through a network will apply to the 558 | combination as such. 559 | 560 | #### 14. Revised Versions of this License. 561 | 562 | The Free Software Foundation may publish revised and/or new versions 563 | of the GNU General Public License from time to time. Such new versions 564 | will be similar in spirit to the present version, but may differ in 565 | detail to address new problems or concerns. 566 | 567 | Each version is given a distinguishing version number. If the Program 568 | specifies that a certain numbered version of the GNU General Public 569 | License "or any later version" applies to it, you have the option of 570 | following the terms and conditions either of that numbered version or 571 | of any later version published by the Free Software Foundation. If the 572 | Program does not specify a version number of the GNU General Public 573 | License, you may choose any version ever published by the Free 574 | Software Foundation. 575 | 576 | If the Program specifies that a proxy can decide which future versions 577 | of the GNU General Public License can be used, that proxy's public 578 | statement of acceptance of a version permanently authorizes you to 579 | choose that version for the Program. 580 | 581 | Later license versions may give you additional or different 582 | permissions. However, no additional obligations are imposed on any 583 | author or copyright holder as a result of your choosing to follow a 584 | later version. 585 | 586 | #### 15. Disclaimer of Warranty. 587 | 588 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 589 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 590 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 591 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 592 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 593 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 594 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 595 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 596 | CORRECTION. 597 | 598 | #### 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 602 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 603 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 604 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 605 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 606 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 607 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 608 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 609 | 610 | #### 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | ### How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these 626 | terms. 627 | 628 | To do so, attach the following notices to the program. It is safest to 629 | attach them to the start of each source file to most effectively state 630 | the exclusion of warranty; and each file should have at least the 631 | "copyright" line and a pointer to where the full notice is found. 632 | 633 | 634 | Copyright (C) 635 | 636 | This program is free software: you can redistribute it and/or modify 637 | it under the terms of the GNU General Public License as published by 638 | the Free Software Foundation, either version 3 of the License, or 639 | (at your option) any later version. 640 | 641 | This program is distributed in the hope that it will be useful, 642 | but WITHOUT ANY WARRANTY; without even the implied warranty of 643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 644 | GNU General Public License for more details. 645 | 646 | You should have received a copy of the GNU General Public License 647 | along with this program. If not, see . 648 | 649 | Also add information on how to contact you by electronic and paper 650 | mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands \`show w' and \`show c' should show the 661 | appropriate parts of the General Public License. Of course, your 662 | program's commands might be different; for a GUI interface, you would 663 | use an "about box". 664 | 665 | You should also get your employer (if you work as a programmer) or 666 | school, if any, to sign a "copyright disclaimer" for the program, if 667 | necessary. For more information on this, and how to apply and follow 668 | the GNU GPL, see . 669 | 670 | The GNU General Public License does not permit incorporating your 671 | program into proprietary programs. If your program is a subroutine 672 | library, you may consider it more useful to permit linking proprietary 673 | applications with the library. If this is what you want to do, use the 674 | GNU Lesser General Public License instead of this License. But first, 675 | please read . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | openscope 4 | 5 |

6 | 7 |

8 | A cross platform ATC terminal 9 |

10 | 11 |

12 | package-version 13 | license-GPL-v3 14 | issues 15 | dep-version 16 | stars 17 |

18 | 19 | > Openscope is a cross-platform controller terminal for [VATSIM](https://vatsim.net/) FSD Server (not recognized yet) & [TeleFlight Server](https://openvmsys.cn/tfs/#/), supporting Windows / Linux / macOS. I wrote this software mainly for those linux (and of course, macOS!) users who want to be a controller but restricted by windows based Euroscope controller software. I made it open source because I want to grow a community of controller, in which there are men and women full of passion for challenge, and have the courage to break free. 20 | 21 |

22 | release 23 | downloads
24 | Download 25 |

26 | 27 | # Screenshots 28 | 29 | ![](https://openvmsys.cn/openscope/img/Openscope5.png) 30 | ![](https://openvmsys.cn/openscope/img/Openscope1.png) 31 | ![](https://openvmsys.cn/openscope/img/Openscope2.png) 32 | 33 | 34 | # Contributing 35 | 36 | All contributions are welcomed. Before forking this repository, please consider the followings: 37 | 38 | - Am I familier with [TypeScript](https://www.typescriptlang.org/) and [Electron](https://electronjs.org/)? 39 | - Am I familier with [Euroscope](https://www.euroscope.hu/wp/) and its configuration files? 40 | - Do I know the basic knowledge of ATC? 41 | 42 | ## Debug 43 | 44 | For the first start, run: 45 | 46 | ```bash 47 | npm i && npm run electron:serve 48 | ``` 49 | 50 | # Thanks to 51 | 52 | - ***iconv-jschardet*** package for supporting gbk format. 53 | 54 | 55 | - ***Wenlue Zhang*** for pointing out the sucked code for UTC Time Display interval function, and his code review helps a lot. 56 | 57 | - ***Ian Cowan*** for his [AviationAPI](https://aviationapi.com/about). 58 | 59 | # FAQ 60 | 61 | - How to request metar in openscope? 62 | - Use the built in metar req. 63 | 64 | - How to switch between each sector? 65 | - Press Shift+Q/W/E/R to switch from these sector views. 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openscope-project", 3 | "version": "0.4.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "electron:build": "vue-cli-service electron:build", 10 | "electron:serve": "tsc && vue-cli-service electron:serve", 11 | "postinstall": "electron-builder install-app-deps", 12 | "postuninstall": "electron-builder install-app-deps" 13 | }, 14 | "dependencies": { 15 | "@vscode/codicons": "^0.0.32", 16 | "ant-design-vue": "^3.2.15", 17 | "iconv-jschardet": "^2.0.32", 18 | "jquery": "^3.6.3", 19 | "typings": "^2.1.1", 20 | "vue": "^3.2.13" 21 | }, 22 | "devDependencies": { 23 | "@types/electron-devtools-installer": "^2.2.0", 24 | "@types/jquery": "^3.5.16", 25 | "@typescript-eslint/eslint-plugin": "^5.4.0", 26 | "@typescript-eslint/parser": "^5.4.0", 27 | "@vue/cli-plugin-eslint": "~5.0.0", 28 | "@vue/cli-plugin-typescript": "~5.0.0", 29 | "@vue/cli-service": "~5.0.0", 30 | "@vue/eslint-config-typescript": "^9.1.0", 31 | "electron": "^19.1.9", 32 | "electron-devtools-installer": "^3.1.0", 33 | "eslint": "^7.32.0", 34 | "eslint-plugin-vue": "^8.0.3", 35 | "ts-loader": "~8.2.0", 36 | "typescript": "~4.5.5", 37 | "vue-cli-plugin-electron-builder": "~2.1.1", 38 | "worker-loader": "^3.0.8", 39 | "@types/node": "^14.10.1" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/OpenVMSys/openscope-project.git" 44 | }, 45 | "keywords": [ 46 | "controller", 47 | "euroscope", 48 | "openvmsys" 49 | ], 50 | "author": "Guo Tingjin", 51 | "license": "GPL-v3", 52 | "bugs": { 53 | "url": "https://github.com/OpenVMSys/openscope-project/issues" 54 | }, 55 | "homepage": "https://github.com/OpenVMSys/openscope-project#readme" 56 | } -------------------------------------------------------------------------------- /public/assets/image/Openscope1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/public/assets/image/Openscope1.png -------------------------------------------------------------------------------- /public/assets/image/Openscope2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/public/assets/image/Openscope2.png -------------------------------------------------------------------------------- /public/assets/image/Openscope3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/public/assets/image/Openscope3.png -------------------------------------------------------------------------------- /public/assets/image/Openscope4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/public/assets/image/Openscope4.png -------------------------------------------------------------------------------- /public/assets/image/Openscope5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/public/assets/image/Openscope5.png -------------------------------------------------------------------------------- /public/assets/image/close.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/close.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/connect.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/darkmode.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/display.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/display.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/headset.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/headset.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/public/assets/image/icon.ico -------------------------------------------------------------------------------- /public/assets/image/lightmode.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/minimize.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/minimize.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/open.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/open.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/radar.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/radar.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/restore.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/restore.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/runway.dark.large.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/runway.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/runway.white.large.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/runway.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/setting.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/setting.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/voice.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/image/voice.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 18 | 19 | 20 | 23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 53 | -------------------------------------------------------------------------------- /src/assets/image/Openscope1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/src/assets/image/Openscope1.png -------------------------------------------------------------------------------- /src/assets/image/Openscope2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/src/assets/image/Openscope2.png -------------------------------------------------------------------------------- /src/assets/image/Openscope3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/src/assets/image/Openscope3.png -------------------------------------------------------------------------------- /src/assets/image/Openscope4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/src/assets/image/Openscope4.png -------------------------------------------------------------------------------- /src/assets/image/Openscope5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/src/assets/image/Openscope5.png -------------------------------------------------------------------------------- /src/assets/image/close.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/close.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/connect.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/darkmode.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/display.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/display.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/headset.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/headset.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/src/assets/image/icon.ico -------------------------------------------------------------------------------- /src/assets/image/lightmode.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericple/openscope-project/1a6470a8b50e8220b864542459f4272d898b824f/src/assets/image/logo.png -------------------------------------------------------------------------------- /src/assets/image/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/minimize.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/minimize.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/open.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/open.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/radar.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/radar.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/restore.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/restore.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/runway.dark.large.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/runway.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/runway.white.large.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/runway.white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/setting.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/setting.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/voice.dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/voice.grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { app, protocol, BrowserWindow, Menu, ipcMain, dialog, globalShortcut, Notification, Tray, nativeImage } from 'electron' 4 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' 5 | import path from 'path' 6 | import ipcChannel from './lib/ipcChannel' 7 | import { METAR_URL, obsAcfDataApi } from './lib/global' 8 | import https from 'https' 9 | const isDevelopment = process.env.NODE_ENV !== 'production' 10 | const appIcon = nativeImage.createFromPath(path.join(__dirname, 'public', 'assets', 'image', 'logo.png')) 11 | 12 | // Scheme must be registered before the app is ready 13 | protocol.registerSchemesAsPrivileged([ 14 | { scheme: 'app', privileges: { secure: true, standard: true } } 15 | ]) 16 | Menu.setApplicationMenu(null); 17 | const preloadPath = path.join(__dirname, 'preloads', 'preload.js'); 18 | console.log(preloadPath); 19 | async function createWindow() { 20 | // Create the browser window. 21 | const win = new BrowserWindow({ 22 | minWidth: 1400, 23 | minHeight: 900, 24 | frame: false, 25 | icon: appIcon, 26 | webPreferences: { 27 | preload: preloadPath 28 | }, 29 | }) 30 | 31 | if (process.env.WEBPACK_DEV_SERVER_URL) { 32 | await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string) 33 | if (!process.env.IS_TEST) win.webContents.openDevTools() 34 | } else { 35 | createProtocol('app') 36 | win.loadURL('app://./index.html') 37 | } 38 | ipcMain.handle(ipcChannel.app.window.close, () => { 39 | app.quit(); 40 | }) 41 | ipcMain.handle(ipcChannel.app.window.maximizeOrRestore, () => { 42 | if (win.isMaximized()) { 43 | win.restore() 44 | } else { 45 | win.maximize() 46 | } 47 | }) 48 | ipcMain.handle(ipcChannel.app.window.minimize, () => { 49 | win.minimize() 50 | }) 51 | ipcMain.handle(ipcChannel.app.update.prfFile, () => { 52 | console.log('Select sector') 53 | SelectSector(); 54 | }) 55 | ipcMain.handle(ipcChannel.app.func.fetchWeather, (e, args: string) => { 56 | https.get(METAR_URL + args, (res) => { 57 | res.on('data', (chunk: Buffer) => { 58 | win.webContents.send(ipcChannel.app.func.fetchWeather, chunk.toString()) 59 | }) 60 | }); 61 | }); 62 | const switch2R1 = globalShortcut.register('Shift+Q', () => { 63 | win.webContents.send(ipcChannel.app.func.switchRadarView, 0) 64 | }) 65 | const switch2R2 = globalShortcut.register('Shift+W', () => { 66 | win.webContents.send(ipcChannel.app.func.switchRadarView, 1) 67 | }) 68 | const switch2R3 = globalShortcut.register('Shift+E', () => { 69 | win.webContents.send(ipcChannel.app.func.switchRadarView, 2) 70 | }) 71 | const switch2R4 = globalShortcut.register('Shift+R', () => { 72 | win.webContents.send(ipcChannel.app.func.switchRadarView, 3) 73 | }) 74 | if (!switch2R1 || !switch2R2 || !switch2R3 || !switch2R4) new Notification({ 75 | title: "An error occured", 76 | body: "Radar view switcher may not be fully functional", 77 | icon: appIcon 78 | }).show() 79 | function SelectSector() { 80 | dialog.showOpenDialog(win, { 81 | filters: [{ name: 'prf Config', extensions: ['prf'] }] 82 | }).then((result) => { 83 | if (result.canceled) return; 84 | win.webContents.send(ipcChannel.app.update.prfFile, { path: result.filePaths[0] }); 85 | }) 86 | } 87 | } 88 | 89 | app.on('window-all-closed', () => { 90 | if (process.platform !== 'darwin') { 91 | app.quit() 92 | } 93 | }) 94 | 95 | app.on('activate', () => { 96 | // On macOS it's common to re-create a window in the app when the 97 | // dock icon is clicked and there are no other windows open. 98 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 99 | }) 100 | 101 | app.on('ready', async () => { 102 | createWindow() 103 | }) 104 | 105 | // Exit cleanly on request from parent process in development mode. 106 | if (isDevelopment) { 107 | if (process.platform === 'win32') { 108 | process.on('message', (data) => { 109 | if (data === 'graceful-exit') { 110 | app.quit() 111 | } 112 | }) 113 | } else { 114 | process.on('SIGTERM', () => { 115 | app.quit() 116 | }) 117 | } 118 | } 119 | ipcMain.handle(ipcChannel.app.update.obsAcfData, (e, args) => { 120 | https.get(obsAcfDataApi + args, (res) => { 121 | let result = ''; 122 | res.on('data', (chunk: Buffer) => { 123 | result += chunk.toString() 124 | 125 | }); 126 | res.on('end', () => { 127 | e.sender.send(ipcChannel.app.update.obsAcfData, result) 128 | }); 129 | res.on('error', e => { 130 | console.error(e); 131 | }); 132 | }); 133 | }); -------------------------------------------------------------------------------- /src/components/AircraftList.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 101 | 102 | -------------------------------------------------------------------------------- /src/components/AppBar.vue: -------------------------------------------------------------------------------- 1 | 2 | 48 | 49 | 94 | 95 | 259 | -------------------------------------------------------------------------------- /src/components/DraggableBase.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/MessageContainer.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 96 | 97 | -------------------------------------------------------------------------------- /src/components/MetarContainer.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 81 | 82 | -------------------------------------------------------------------------------- /src/components/RadarScreen.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/coordparser.ts: -------------------------------------------------------------------------------- 1 | import { Coordinate_B, Coordinate_A, PolarCoordinate } from "./datatype"; 2 | import {EARTH_RADIUS_LONG} from "./global"; 3 | 4 | 5 | /** 6 | * 将B类坐标转换成A类坐标 7 | * @param coord B类坐标 8 | */ 9 | export function parse2CoordA(coord: Coordinate_B): Coordinate_A { 10 | console.log(coord); 11 | throw "function not implemented"; 12 | } 13 | export function DegToRad(deg:number){ 14 | return Math.PI*(deg/180); 15 | } 16 | /** 17 | * 将A类坐标转换成B类坐标 18 | * @param coord A类坐标 19 | * @returns B类直角绘制坐标 20 | */ 21 | export function parse2CoordB_(coord: Coordinate_A): Coordinate_B { 22 | let latres; 23 | let lonres; 24 | try { 25 | const pureLatNumber = coord.latitude.substring(1, coord.latitude.length).split("."); 26 | const pureLonNumber = coord.longitude.substring(1, coord.longitude.length).split("."); 27 | if (pureLatNumber.length == 4) { 28 | const deg = parseInt(pureLatNumber[0]); const min = parseInt(pureLatNumber[1]); 29 | const sec = parseInt(pureLatNumber[2]); const subsec = parseInt(pureLatNumber[3]); 30 | latres = deg + min / 60 + (sec + subsec / 1000) / 3600; 31 | if (coord.latitude.startsWith("N")) latres = -latres; 32 | } 33 | if (pureLonNumber.length == 4) { 34 | const deg = parseInt(pureLonNumber[0]); const min = parseInt(pureLonNumber[1]); 35 | const sec = parseInt(pureLonNumber[2]); const subsec = parseInt(pureLonNumber[3]); 36 | lonres = deg + min / 60 + (sec + subsec / 1000) / 3600; 37 | } 38 | if(latres == undefined || lonres == undefined) return {latitude:0,longitude:0} 39 | return { 40 | latitude: Math.log(Math.tan((Math.PI*0.25)+(0.5*DegToRad(latres))))*EARTH_RADIUS_LONG, 41 | longitude: DegToRad(lonres)*EARTH_RADIUS_LONG 42 | } 43 | } catch (error) { 44 | console.log(coord.latitude, coord.longitude); 45 | throw error; 46 | } 47 | } 48 | export function parse2CoordB(coord: Coordinate_A){ 49 | let latres = parseFloat(coord.latitude.substring(1, coord.latitude.length)); 50 | const lonres = parseFloat(coord.longitude.substring(1,coord.longitude.length)); 51 | if(coord.latitude.startsWith("N")) latres = -latres; 52 | return { 53 | latitude: latres, 54 | longitude: lonres 55 | } 56 | } 57 | /** 58 | * 将B类坐标转换为极坐标 59 | * @param coord B类坐标 60 | * @returns 极坐标 61 | */ 62 | export function parse2PolarCoord(coord: Coordinate_B): PolarCoordinate { 63 | return { 64 | radius: Math.atan(coord.latitude / coord.longitude), 65 | length: Math.sqrt(coord.latitude ^ 2 + coord.longitude ^ 2) 66 | } 67 | } 68 | 69 | export const parserMap = { 70 | 'obscure': parse2CoordB_, 71 | 'precise': parse2CoordB 72 | } 73 | 74 | /** 75 | * 实现坐标与坐标的转换 76 | * 内置坐标类型: `Coordinate_A` `Coordinate_B` `PolarCoordinate` 77 | */ 78 | export default { 79 | parse2CoordA, parse2CoordB_, parse2PolarCoord, parse2CoordB 80 | } 81 | -------------------------------------------------------------------------------- /src/lib/datatype.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 由一对字符串组成的坐标,形如 `N40.36.18.000` `E112.52.13.000` 3 | */ 4 | export type Coordinate_A = { 5 | latitude: string, 6 | longitude: string 7 | } 8 | /** 9 | * 由一对浮点数组成的坐标,形如 `40.3613692` `112.7219753` 10 | */ 11 | export type Coordinate_B = { 12 | latitude: number, 13 | longitude: number 14 | } 15 | /** 16 | * 极坐标 17 | * @param radius 极角 18 | * @param length 极径 19 | */ 20 | export type PolarCoordinate = { 21 | radius: number, 22 | length: number 23 | } 24 | 25 | /** 26 | * prf配置文件的配置项 27 | */ 28 | export type PrfItem = { 29 | type: string, 30 | flag: string, 31 | data: string 32 | } 33 | /** 34 | * asr文件中定义的配置 35 | */ 36 | export type AsrSetting = { 37 | flag: string, 38 | data: string 39 | } 40 | /** 41 | * asr文件中定义的绘制项 42 | * @param type indicate the type of item. Possible value contains 43 | * `ARTCC low boundary` `Fixes` `Free Text` 44 | * `Geo` `Regions` `Runways` `Sids` `Stars` 45 | * `VORs` 46 | * @param name indicate the name of item. If the item is freetext, the format of name should be 47 | * `group`\\`name` 48 | * @param flag indicate the item to draw. Possible value contains 49 | * `name` `symbol` `freetext`, for runways, it will be more complex. 50 | */ 51 | export type AsrDrawItem = { 52 | type: string, 53 | name: string, 54 | flag: string, 55 | draw: boolean 56 | } 57 | /** 58 | * asr文件中定义的默认显示区域,由左上角坐标及右下角坐标组成 59 | */ 60 | export type AsrWindowArea = { 61 | coord1: Coordinate_B, 62 | coord2: Coordinate_B 63 | } 64 | 65 | /** 66 | * sct文件中定义的颜色组,读取时转化为16进制颜色代码 67 | */ 68 | export type SctDefinition = { 69 | flag: string, 70 | color: string 71 | } 72 | /** 73 | * sct文件的版权和信息内容,仅读取版权 74 | */ 75 | export type SctInfo = { 76 | copyright: string 77 | } 78 | /** 79 | * sct文件中定义的VOR及NDB 80 | */ 81 | export type SctVorNdb = { 82 | name: string, 83 | frequency: string, 84 | coord: Coordinate_B, 85 | } 86 | export type SctAirport = { 87 | icao: string, 88 | frequency: string, 89 | coord: Coordinate_B, 90 | class: string, 91 | 92 | } 93 | export type SctRunway = { 94 | endPointA: string, 95 | endPointB: string, 96 | HeadingA: number, 97 | HeadingB: number, 98 | coordA: Coordinate_B, 99 | coordB: Coordinate_B, 100 | airportCode: string, 101 | airportName: string, 102 | 103 | } 104 | export type SctFix = { 105 | name: string, 106 | coord: Coordinate_B, 107 | 108 | } 109 | export type SctARTCC = { 110 | group: string, 111 | coords: { 112 | coordA: Coordinate_B, 113 | coordB: Coordinate_B, 114 | }[] 115 | } 116 | export type SctSIDSTAR = { 117 | group: string, 118 | coords: { 119 | coordA: Coordinate_B, 120 | coordB: Coordinate_B, 121 | }[] 122 | } 123 | export type SctLoHiAirway = { 124 | group: string, 125 | coords: { 126 | coordA: Coordinate_B, 127 | coordB: Coordinate_B, 128 | }[] 129 | 130 | } 131 | export type SctGEO = { 132 | group: string, 133 | items: { 134 | coordA: Coordinate_B, 135 | coordB: Coordinate_B, 136 | colorFlag: string, 137 | }[], 138 | } 139 | export type SctREGIONS = { 140 | regionName: string, 141 | items: { 142 | colorFlag: string, 143 | coords: Coordinate_B[], 144 | }[] 145 | } 146 | 147 | export type EseAirspaceDisplay = { 148 | sectorControlling: string, 149 | sectorCovered: string[] 150 | } 151 | export type EseAirspaceSubsection = { 152 | sector: { 153 | name: string | null, 154 | loAltLimit: number | null, 155 | upAltLimit: number | null 156 | } | null, 157 | owner: string[] | null, 158 | altowner: string[] | null, 159 | border: string[] | null 160 | } 161 | export type EseAirspace = { 162 | sectorline: string, 163 | displayCondition: EseAirspaceDisplay[], 164 | coords: Coordinate_B[], 165 | subsection: EseAirspaceSubsection, 166 | active: { 167 | airport: string, 168 | runway: string 169 | } | null 170 | } 171 | export type EseFreetext = { 172 | coord: Coordinate_B, 173 | group: string, 174 | text: string, 175 | 176 | } 177 | export type EsePosition = { 178 | name: string, 179 | callsign: string, 180 | frequency: string, 181 | identifier: string, 182 | middleLetter: string, 183 | prefix: string, 184 | suffix: string, 185 | notUsed1: string, 186 | notUsed2: string, 187 | startOfRange: string, 188 | endOfRange: string 189 | } 190 | export type EseSidsStars = { 191 | type: string, 192 | airport: string, 193 | runway: string, 194 | name: string, 195 | route: string[], 196 | 197 | } 198 | 199 | 200 | /** 201 | * @param type Symbology definition type, possible value contains 202 | * `Airports` `Low airways` `High airways` `Fixes` and etc. 203 | * @param flag SYmbology definition flag, possible value contains 204 | * `symbol` `name` `line`. 205 | */ 206 | export type SymbologyDefine = { 207 | type: string, 208 | flag: string, 209 | data: { 210 | color: string, 211 | fontSymbolSize: number, 212 | lineWeight: number, 213 | lineStyle: number, 214 | alignment: number 215 | } 216 | } 217 | export type SymbologyDrawScript = { 218 | ident: number, 219 | sciprts: string[] 220 | } 221 | 222 | export type VoiceChannelDefine = { 223 | type: string, 224 | name: string, 225 | frequency: string, 226 | server: string, 227 | callsign: string 228 | } 229 | 230 | export type ProfileDefine = { 231 | info: { 232 | ident: string, 233 | range: number, 234 | facility: number 235 | }, 236 | atis2: string | null, 237 | atis3: string | null, 238 | atis4: string | null 239 | } 240 | -------------------------------------------------------------------------------- /src/lib/elementId.ts: -------------------------------------------------------------------------------- 1 | export const elementId = { 2 | RadarWindow: { 3 | Appbar: { 4 | Buttons: { 5 | connect: "button-connect", 6 | voiceSetting: "button-output", 7 | atisSetting: "button-atis", 8 | displaySetting: "button-display", 9 | runwaySetting: "button-runway", 10 | sectorSelect: "button-select-sector", 11 | setting: "button-setting", 12 | close: "button-close-app", 13 | maximizeOrRestore: "button-restore-app", 14 | minimize: "button-minimize-app", 15 | theme: "button-theme-toggle" 16 | }, 17 | Tags: { 18 | currentCI: "appbar-connection-info", 19 | currentFrequency: "appbar-frequency", 20 | currentATIS: "appbar-atis-info", 21 | currentSector: "appbar-current-sector", 22 | currentTime: "appbar-utc-time", 23 | currentCoord: "appbar-coordinate" 24 | }, 25 | Container: { 26 | metar: "app-bar" 27 | } 28 | }, 29 | Canvas: { 30 | screen: "radar" 31 | }, 32 | Footer: { 33 | channelChooser: "footer-channel-chooser", 34 | messageContainer: "footer-channel-message-container", 35 | toolbar: { 36 | messagebox: "tool-bar-messagebox" 37 | } 38 | }, 39 | SubWin: { 40 | metar: "metarList" 41 | } 42 | } 43 | } 44 | 45 | export default elementId; -------------------------------------------------------------------------------- /src/lib/global.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | export const VMETAR_URL = "https://api.aviationapi.com/v1/weather/metar?apt="; 3 | export const METAR_URL = "https://metar.vatsim.net/"; 4 | export const DefaultSectorSettingFilePath = path.join(__dirname, "config", "defaultsector.txt"); 5 | export const obsAcfDataApi = "https://api.aviationapi.com/v1/vatsim/pilots?apt="; 6 | export const EARTH_RADIUS_LONG = 63.78137; 7 | export const EARTH_RADIUS_S = 63.567523142; 8 | -------------------------------------------------------------------------------- /src/lib/ipcChannel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 定义了程序所使用的所有ipcChannel 3 | */ 4 | export const ipcChannel = { 5 | app: { 6 | window: { 7 | close: "app.window.close", 8 | maximizeOrRestore: "app.window.maximize", 9 | minimize: "app.window.minimize" 10 | }, 11 | update: { 12 | prfFile: "app.update.prfFile", 13 | theme: "app.toggle.theme", 14 | themeSystem: "app.toggle.theme.system", 15 | canvasIndex: "app.update.canvas.index", 16 | canvasPos: "app.update.canvas.pos", 17 | obsAcfData: "app.update.obsAcfData", 18 | }, 19 | msg: { 20 | error: "app.errmsg", 21 | warning: "app.wrnmsg", 22 | info: "app.info" 23 | }, 24 | func: { 25 | connectToNetwork: "app.func.connecttonetwork", 26 | fetchWeather: "app.func.fetchweather", 27 | initRadar: "app.func.initradar", 28 | switchRadarView: "app.func.switchradarview" 29 | } 30 | } 31 | } 32 | 33 | export default ipcChannel; -------------------------------------------------------------------------------- /src/lib/popupMsger.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from "electron"; 2 | import ipcChannel from "./ipcChannel"; 3 | 4 | export type BoxMessage = { 5 | title: string; 6 | message: string; 7 | callback?: (response?: number, checkboxChecked?: boolean) => void; 8 | } 9 | 10 | export function ErrorBox(options: BoxMessage): void { 11 | ipcRenderer.invoke(ipcChannel.app.msg.error, {title: options.title, message: options.message}); 12 | ipcRenderer.on(ipcChannel.app.msg.error, (e,args) => { 13 | if(options.callback) options.callback(args.response, args.checkboxChecked); 14 | }); 15 | } 16 | 17 | export function WarningBox(options: BoxMessage): void { 18 | ipcRenderer.invoke(ipcChannel.app.msg.warning, {title: options.title, message: options.message}); 19 | ipcRenderer.on(ipcChannel.app.msg.warning, (e,args) => { 20 | if(options.callback) options.callback(args.response, args.checkboxChecked); 21 | }); 22 | } 23 | 24 | export function InfoBox(options: BoxMessage): void { 25 | ipcRenderer.invoke(ipcChannel.app.msg.info, {title: options.title, message: options.message}); 26 | ipcRenderer.on(ipcChannel.app.msg.info, (e,args) => { 27 | if(options.callback) options.callback(args.response, args.checkboxChecked); 28 | }); 29 | } -------------------------------------------------------------------------------- /src/lib/sectortype.ts: -------------------------------------------------------------------------------- 1 | import { parse2CoordB } from "./coordparser"; 2 | import { AsrDrawItem, AsrSetting, AsrWindowArea, EseAirspace, EseFreetext, EsePosition, EseSidsStars, PrfItem, SctAirport, SctARTCC, SctDefinition, SctFix, SctGEO, SctInfo, SctLoHiAirway, SctREGIONS, SctRunway, SctSIDSTAR, SctVorNdb } from "./datatype"; 3 | 4 | class AsrData { 5 | settings: AsrSetting[] = []; 6 | items: AsrDrawItem[] = []; 7 | windowArea: AsrWindowArea | null = { 8 | coord1: parse2CoordB({ 9 | latitude: "N0.0.0.0", 10 | longitude: "E0.0.0.0" 11 | }), 12 | coord2: parse2CoordB({ 13 | latitude: "N0.0.0.0", 14 | longitude: "E0.0.0.0" 15 | }) 16 | }; 17 | } 18 | 19 | class PrfData { 20 | settings: PrfItem[] = []; 21 | } 22 | 23 | class SctData { 24 | definitions: SctDefinition[] = []; 25 | info: SctInfo = { copyright: "" }; 26 | vors: SctVorNdb[] = []; 27 | ndbs: SctVorNdb[] = []; 28 | airports: SctAirport[] = []; 29 | runways: SctRunway[] = []; 30 | fixes: SctFix[] = []; 31 | ARTCCs: SctARTCC[] = []; 32 | sids: SctSIDSTAR[] = []; 33 | stars: SctSIDSTAR[] = []; 34 | loAirways: SctLoHiAirway[] = []; 35 | hiAirways: SctLoHiAirway[] = []; 36 | GEOs: SctGEO[] = []; 37 | REGIONs: SctREGIONS[] = []; 38 | } 39 | 40 | class EseData { 41 | airspacces: EseAirspace[] = []; 42 | freetexts: EseFreetext[] = []; 43 | positions: EsePosition[] = []; 44 | sidsstars: EseSidsStars[] = []; 45 | } 46 | 47 | 48 | 49 | export { 50 | AsrData, PrfData, EseData, SctData 51 | } -------------------------------------------------------------------------------- /src/lib/settingtype.ts: -------------------------------------------------------------------------------- 1 | import { SymbologyDefine, SymbologyDrawScript, VoiceChannelDefine, ProfileDefine } from "./datatype"; 2 | 3 | export class SymbologyData { 4 | colors: SymbologyDefine[] = []; 5 | scripts: SymbologyDrawScript[] = []; 6 | m_ClipArea = 0; 7 | } 8 | 9 | export class VoiceData { 10 | channels: VoiceChannelDefine[] = []; 11 | } 12 | 13 | export class ProfileData { 14 | profile: ProfileDefine[] = []; 15 | } 16 | 17 | export default { 18 | SymbologyData,VoiceData,ProfileData 19 | } -------------------------------------------------------------------------------- /src/lib/spaceformatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 清除字符串中多余的空格 3 | * @param source 原字符串 4 | * @returns 处理后的字符串 5 | */ 6 | export function CleanSpaces(source: string) : string 7 | { 8 | source = source.trim(); 9 | while( source.lastIndexOf(" ") !== -1 ) 10 | { 11 | source = source.replace(" "," "); 12 | } 13 | return source; 14 | } 15 | 16 | export default { 17 | CleanSpaces 18 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import Antd from 'ant-design-vue' 4 | import 'ant-design-vue/dist/antd.css' 5 | 6 | createApp(App).use(Antd).mount('#app') 7 | -------------------------------------------------------------------------------- /src/preloads/preload.ts: -------------------------------------------------------------------------------- 1 | import ipcChannel from '../lib/ipcChannel' 2 | import { ipcRenderer, contextBridge } from 'electron' 3 | import Drawer from '../utils/drawer' 4 | import elementId from '../lib/elementId' 5 | import path from 'path' 6 | import fs from 'fs' 7 | import { DefaultSectorSettingFilePath } from '../lib/global' 8 | contextBridge.exposeInMainWorld('appbar', { 9 | quitApp: () => ipcRenderer.invoke(ipcChannel.app.window.close), 10 | maximizeApp: () => ipcRenderer.invoke(ipcChannel.app.window.maximizeOrRestore), 11 | minimizeApp: () => ipcRenderer.invoke(ipcChannel.app.window.minimize), 12 | updateTime: () => { 13 | return getTime() 14 | }, 15 | openSector: () => ipcRenderer.invoke(ipcChannel.app.update.prfFile) 16 | }) 17 | const drawerGroup: Drawer[] = []; 18 | let activeDrawerIndex = 0; 19 | contextBridge.exposeInMainWorld('radar', { 20 | init: function (rootEl: string) { 21 | const drawer = new Drawer(rootEl) 22 | drawerGroup.push(drawer) 23 | const screen = document.getElementById(elementId.RadarWindow.Canvas.screen) 24 | if (!screen) return 25 | screen.addEventListener('wheel', (e) => { 26 | drawerGroup[activeDrawerIndex].UpdateCanvasIndex(e); 27 | }) 28 | screen.oncontextmenu = function (ev: MouseEvent) { 29 | const distX = ev.clientX - screen.offsetLeft; 30 | const distY = ev.clientY - screen.offsetTop; 31 | screen.onmousemove = function (e) { 32 | const tX = e.clientX - distX; 33 | const tY = e.clientY - distY; 34 | screen.style.left = `${tX}px` 35 | screen.style.top = `${tY}px` 36 | drawerGroup[activeDrawerIndex].UpdateCanvasPosE(e) 37 | } 38 | } 39 | screen.onmouseup = function () { 40 | drawerGroup[activeDrawerIndex].ClearCanvas(); 41 | screen.style.left = '0px' 42 | screen.style.top = '45px' 43 | screen.onmousemove = null 44 | } 45 | ipcRenderer.on(ipcChannel.app.update.prfFile, (e, args) => { 46 | const sectorindicator = document.getElementById(elementId.RadarWindow.Appbar.Tags.currentCoord) 47 | if (sectorindicator) sectorindicator.innerText = path.basename(args.path) 48 | if (!fs.existsSync(DefaultSectorSettingFilePath)) { 49 | fs.openSync(DefaultSectorSettingFilePath, 1) 50 | } 51 | fs.writeFileSync(DefaultSectorSettingFilePath, args.path, 'utf-8') 52 | drawerGroup[activeDrawerIndex].UpdateCache(args.path); 53 | drawerGroup[activeDrawerIndex].ClearCanvas(); 54 | }) 55 | ipcRenderer.on(ipcChannel.app.func.switchRadarView, (e, args) => { 56 | //如果所选视图大于已有 57 | while (args + 1 > drawerGroup.length) { 58 | drawerGroup.push(new Drawer('radar')) 59 | } 60 | drawerGroup.forEach((o) => { 61 | o.Hide() 62 | }) 63 | activeDrawerIndex = args 64 | drawerGroup[args].Show() 65 | }) 66 | } 67 | }) 68 | let isSearchingWx = false 69 | ipcRenderer.on(ipcChannel.app.func.fetchWeather, (e, arg) => { 70 | isSearchingWx = false 71 | const metars: string[] = arg.split('\n') 72 | metars.forEach(metar => { 73 | const icao = metar.substring(0, 4) 74 | let exist = false 75 | for (let index = 0; index < weatherData.length; index++) { 76 | const weather = weatherData[index]; 77 | if (weather.icao == icao && weather.metar !== metar) { 78 | weather.metar = metar 79 | weather.updateTime = getTime() 80 | exist = true 81 | } 82 | } 83 | if (!exist) weatherData.push({ icao: icao, updateTime: getTime(), metar: metar }) 84 | }) 85 | }) 86 | const weatherData: { icao: string, metar: string, updateTime: string }[] = [] 87 | contextBridge.exposeInMainWorld('weather', { 88 | requestWx: (icaos: string) => { 89 | isSearchingWx = true 90 | ipcRenderer.invoke(ipcChannel.app.func.fetchWeather, icaos) 91 | }, 92 | weatherData: () => { 93 | return weatherData 94 | }, 95 | searchStatus: () => { 96 | return isSearchingWx 97 | } 98 | }) 99 | const aircraftData: { 100 | title: string, 101 | cols: string[], 102 | contextMenu: any[], 103 | key: string, 104 | aircrafts: any[], 105 | }[] = [ 106 | { 107 | title: 'Departure', 108 | cols: [ 109 | "C/S", "TYPE", "C", "D", "STS", "R", "DEP", "ARR", 110 | "RWY", "SIE", "SID", "S/PAD", "ALT", "CRZ", "ASSR" 111 | ], 112 | contextMenu: [ 113 | { 114 | title: "Clearance", 115 | items: [ 116 | "Unset", 117 | "Pending", 118 | "Ready" 119 | ] 120 | }, 121 | { 122 | title: "Status", 123 | items: [ 124 | "Stand", 125 | "Push", 126 | "Taxi", 127 | "Takeoff", 128 | "Climb", 129 | "Cruise", 130 | "Descend", 131 | "Approach" 132 | ] 133 | } 134 | ], 135 | key: '1', 136 | aircrafts: [ 137 | { 138 | 'C/S': 'CALLSIGN', 139 | 'TYPE': 'TYPE', 140 | 'C': 'CLEARANCE', 141 | 'D': 'UNKNOWN', 142 | 'STS': 'STATUS', 143 | 'R': 'UNKNOWN', 144 | 'DEP': 'A', 145 | 'ARR': 'B', 146 | 'RWY': 'C', 147 | 'SIE': 'SIE', 148 | }, 149 | { 150 | 'C/S': 'CALLSIGNB', 151 | 'TYPE': 'TYPE', 152 | 'C': 'CLEARANCE', 153 | 'D': 'UNKNOWN', 154 | 'STS': 'STATUS', 155 | 'R': 'UNKNOWN', 156 | 'DEP': 'A', 157 | 'ARR': 'B', 158 | 'RWY': 'C', 159 | 'SIE': 'SIE', 160 | } 161 | ] 162 | }, { 163 | title: 'Exit', 164 | key: '2', 165 | cols: [ 166 | "RWY", "C/S", "TYPE", "DEP", "ARR", "SIE", "SID", 167 | "STE", "STAR", "NXT", "EXT", "COPN", "CRZ", "ASSR" 168 | ], 169 | contextMenu: [ 170 | "Runway", "C/S", "TYPE", "DEP", "ARR", "SIE", "SID", 171 | "STE", "STAR", "NXT", "EXT", "COPN", "CRZ", "ASSR" 172 | ], 173 | aircrafts: [ 174 | ] 175 | }, { 176 | title: 'Inbound', 177 | key: '3', 178 | cols: [ 179 | "RWY", "C/S", "TYPE", "R", "DEP", "ARR", "STE", "STAR", 180 | "NXT", "S/PAD", "ENT", "COPN", "CRZ", "ASSR" 181 | ], 182 | contextMenu: [ 183 | "RWY", "C/S", "TYPE", "R", "DEP", "ARR", "STE", "STAR", 184 | "NXT", "S/PAD", "ENT", "COPN", "CRZ", "ASSR" 185 | ], 186 | aircrafts: [ 187 | ] 188 | } 189 | ] 190 | let obsAcfData = ""; 191 | contextBridge.exposeInMainWorld('acflist', { 192 | init: () => { 193 | setInterval(() => { 194 | ipcRenderer.on(ipcChannel.app.update.obsAcfData, (e, arg) => { 195 | obsAcfData = arg 196 | }) 197 | }, 5000) 198 | }, 199 | aircraftData: () => { 200 | return aircraftData 201 | }, 202 | getObsAcfData: () => { 203 | ipcRenderer.invoke(ipcChannel.app.update.obsAcfData, "ZYTL"); 204 | return obsAcfData; 205 | } 206 | }) 207 | function getTime() { 208 | const date = new Date() 209 | const year = date.getUTCFullYear().toString() 210 | const month = (date.getUTCMonth() + 1).toString() 211 | const day = date.getUTCDate().toString() 212 | const hrs = date.getUTCHours().toString().padStart(2, "0") 213 | const min = date.getUTCMinutes().toString().padStart(2, "0") 214 | const sec = date.getUTCSeconds().toString().padStart(2, "0") 215 | return `${month}/${day}/${year} ${hrs}:${min}:${sec}` 216 | } 217 | console.log('preloaded') -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/drawer.ts: -------------------------------------------------------------------------------- 1 | import { AsrData, EseData, PrfData, SctData } from "@/lib/sectortype"; 2 | import elementId from "../lib/elementId"; 3 | import { LoadAsrFileSync, LoadPrfFileSync, LoadSctFileSync, LoadEseFileSync, ReadPrfData, ConvertEsePath, ReadSctFix, ReadSymbol, ReadEseFreeText, ReadSctGeo, ReadSctRegions, ReadSctDefine, AlignPath, ReadSctLoHiAw, ReadSctAirport, ReadSctVORNDB, ReadSctARTCC, ReadSctSidStar, ReadSctRunway } from "./prfsectorloader"; 4 | import { VoiceData, SymbologyData, ProfileData } from "@/lib/settingtype"; 5 | import { LoadSymbologySync, LoadVoiceSync, LoadProfileSync } from "./settingloader"; 6 | // import { ipcRenderer } from "electron"; 7 | // import ipcChannel from "../lib/ipcChannel"; 8 | import path from 'path'; 9 | import { SctSIDSTAR, SctVorNdb } from "@/lib/datatype"; 10 | // import fs from 'fs'; 11 | // import { DefaultSectorSettingFilePath } from "../lib/global"; 12 | 13 | const SCROLL_INDEX = 1.2; 14 | const AP_FREETEXT_LIMIT = 20000;//要显示机场FREETEXT的最小缩放倍数 15 | const FIX_NAME_LIMIT = 150; 16 | const AW_NAME_LIMIT_MIN = 100; 17 | const AW_NAME_LIMIT_MAX = 150; 18 | const ZOOM_INDEX_MAX = 250000;//最大缩放倍数 19 | export class Drawer { 20 | public rootElement: HTMLElement | null; 21 | public canvas: HTMLCanvasElement; 22 | public canvasContext: CanvasRenderingContext2D | null; 23 | public coordIndicator: HTMLElement | null; 24 | public canvasIndex: number; 25 | public canvasPosX: number; 26 | public canvasPosY: number; 27 | public prfCache: PrfData | undefined; 28 | public asrCache: AsrData | undefined; 29 | public sectorCache: SctData | undefined; 30 | public eseCache: EseData | undefined; 31 | public symbolCache: SymbologyData | undefined; 32 | public voiceCache: VoiceData | undefined; 33 | public profileCache: ProfileData | undefined; 34 | constructor(rootElement: string) { 35 | this.canvas = document.createElement('canvas'); 36 | this.canvasContext = this.canvas.getContext('2d'); 37 | this.coordIndicator = document.getElementById(elementId.RadarWindow.Appbar.Tags.currentCoord); 38 | this.rootElement = document.getElementById(rootElement); 39 | this.canvas.id = "radar-drawer"; 40 | this.rootElement?.appendChild(this.canvas); 41 | this.canvasIndex = 1; 42 | this.canvasPosX = 0; 43 | this.canvasPosY = 0; 44 | //当页面大小被改变时,重新进行绘制 45 | window.addEventListener('resize', () => { 46 | this.ClearCanvas(); 47 | }); 48 | } 49 | public Hide() { 50 | this.canvas.style.display = 'none' 51 | } 52 | public Show() { 53 | this.canvas.style.display = 'block' 54 | } 55 | public ClearCanvas(): void { 56 | this.canvas.height = window.innerHeight; 57 | this.canvas.width = window.innerWidth; 58 | this.Draw(); 59 | } 60 | public UpdateCanvasIndex(event: WheelEvent): void { 61 | const xresult = this.canvasPosX + (event.movementX) / this.canvasIndex; 62 | const yresult = this.canvasPosY + (event.movementY) / this.canvasIndex; 63 | this.canvasPosX = xresult; 64 | this.canvasPosY = yresult; 65 | if (event.deltaY < 0) { 66 | const result = this.canvasIndex * SCROLL_INDEX; 67 | if (result < ZOOM_INDEX_MAX) this.canvasIndex = result; 68 | } 69 | else { 70 | const result = this.canvasIndex / SCROLL_INDEX; 71 | if (result > 1) this.canvasIndex = result; 72 | } 73 | /** 74 | * 检测canvasIndex是否小于绘制FREETEXT的最低要求 75 | */ 76 | if (this.canvasIndex < AP_FREETEXT_LIMIT) { 77 | this.asrCache?.items.forEach((item) => { 78 | if (item.name.lastIndexOf("\\") !== -1) item.draw = false; 79 | }); 80 | } else { 81 | this.asrCache?.items.forEach((item) => { 82 | if (item.name.lastIndexOf("\\") !== -1) item.draw = true; 83 | }); 84 | } 85 | /** 86 | * 检测canvasIndex是否小于绘制FIXNAME的最低要求 87 | */ 88 | if (this.canvasIndex < FIX_NAME_LIMIT) { 89 | this.asrCache?.items.forEach((item) => { 90 | if (item.type == "Fixes") item.draw = false; 91 | }); 92 | } else { 93 | this.asrCache?.items.forEach((item) => { 94 | if (item.type == "Fixes") item.draw = true; 95 | }); 96 | } 97 | /** 98 | * 检测canvasIndex是否在限定绘制Airways name的范围 99 | */ 100 | if (this.canvasIndex > AW_NAME_LIMIT_MIN && this.canvasIndex < AW_NAME_LIMIT_MAX) { 101 | this.asrCache?.items.forEach((item) => { 102 | if (item.type.endsWith("airways") && item.flag == "name") item.draw = true; 103 | }); 104 | } else { 105 | this.asrCache?.items.forEach((item) => { 106 | if (item.type.endsWith("airways") && item.flag == "name") item.draw = false; 107 | }); 108 | } 109 | if (!this.coordIndicator) return; 110 | this.coordIndicator.innerText = `Lat: ${this.canvasPosY} Lon: ${-this.canvasPosX}`; 111 | this.ClearCanvas(); 112 | } 113 | public UpdateCanvasPosE(e: MouseEvent): void { 114 | const xresult = this.canvasPosX + (e.movementX) / this.canvasIndex; 115 | const yresult = this.canvasPosY + (e.movementY) / this.canvasIndex; 116 | this.canvasPosX = xresult; 117 | this.canvasPosY = yresult; 118 | if (!this.coordIndicator) return; 119 | this.coordIndicator.innerText = `Lat: ${this.canvasPosY} Lon: ${-this.canvasPosX}`; 120 | } 121 | public UpdateCanvasPosXY(x:number,y:number){ 122 | this.canvasPosX += x / this.canvasIndex; 123 | this.canvasPosY += y / this.canvasIndex; 124 | } 125 | /** 126 | * 更新绘制缓存 127 | * @param prfPath prf文件目录 128 | */ 129 | public UpdateCache(prfPath: string): void { 130 | this.prfCache = LoadPrfFileSync(prfPath); 131 | let asrpath = ReadPrfData(this.prfCache, "Recent1"); 132 | if (!asrpath) return; 133 | asrpath = path.join(prfPath, "..", AlignPath(asrpath)); 134 | this.asrCache = LoadAsrFileSync(asrpath); 135 | let sctpath = ReadPrfData(this.prfCache, "sector"); 136 | if (!sctpath) return; 137 | sctpath = path.join(prfPath, "..", AlignPath(sctpath)); 138 | this.sectorCache = LoadSctFileSync(sctpath); 139 | const esepath = ConvertEsePath(sctpath); 140 | this.eseCache = LoadEseFileSync(esepath); 141 | let symbologypath = ReadPrfData(this.prfCache, "SettingsfileSYMBOLOGY"); 142 | if (!symbologypath) return; 143 | symbologypath = path.join(prfPath, "..", AlignPath(symbologypath)); 144 | this.symbolCache = LoadSymbologySync(symbologypath); 145 | const ap = document.getElementById('back'); 146 | const backgroundColor = ReadSymbol(this.symbolCache.colors, "Sector", "active sector background")?.color; 147 | if (backgroundColor !== undefined) { 148 | this.canvas.style.backgroundColor = backgroundColor; 149 | if(ap) ap.style.backgroundColor = backgroundColor; 150 | } 151 | let voicepath = ReadPrfData(this.prfCache, "SettingsfileVOICE"); 152 | if (!voicepath) return; 153 | voicepath = path.join(prfPath, "..", AlignPath(voicepath)); 154 | this.voiceCache = LoadVoiceSync(voicepath); 155 | let profilepath = ReadPrfData(this.prfCache, "SettingsfilePROFILE"); 156 | if (!profilepath) return; 157 | profilepath = path.join(prfPath, "..", AlignPath(profilepath)); 158 | this.profileCache = LoadProfileSync(profilepath); 159 | } 160 | public UpdateAcfCache(): void { 161 | return 162 | } 163 | public Draw(): void { 164 | if (!this.canvasContext) return; 165 | this.canvasContext.translate(window.innerWidth / 2, window.innerHeight / 2); 166 | this.canvasContext.translate(this.canvasPosX, this.canvasPosY); 167 | this.canvasContext.translate(this.canvasPosX * (this.canvasIndex - 1), this.canvasPosY * (this.canvasIndex - 1)); 168 | if (!this.asrCache) return; 169 | this.asrCache.items.forEach((item) => { 170 | if (!this.sectorCache || !this.symbolCache) return; 171 | if (item.type == "Regions") { 172 | const regions = ReadSctRegions(this.sectorCache.REGIONs, item.name);//从缓存中提取出对应regions区域 173 | if (!regions) return;//如果缓存中不存在该区域,则返回,进行下一次操作 174 | regions.items.forEach((item) => {//对每个提取出来的区域进行绘制 175 | if (!this.sectorCache || !this.canvasContext) return; 176 | this.canvasContext.beginPath();//开始绘制 177 | const color = ReadSctDefine(this.sectorCache.definitions, item.colorFlag);//从缓存中提取出对应的颜色,region的颜色被定义在sct中 178 | if (!color) return;//如果sct中不存在对应的颜色flag定义,说明扇区有问题或读取失败 179 | this.canvasContext.lineWidth = 0.1; 180 | this.canvasContext.strokeStyle = color; 181 | this.canvasContext.fillStyle = color; 182 | const count = item.coords.length;//获取坐标总数,为for循环做好准备 183 | const line = new Path2D();//新建一个二维路径 184 | line.moveTo(item.coords[0].longitude * this.canvasIndex, item.coords[0].latitude * this.canvasIndex);//将二维路径绘制点移动到初始坐标 185 | // this.canvasContext.moveTo(item.coords[0].longtitude * this.canvasIndex, item.coords[0].latitude * this.canvasIndex); 186 | for (let index = 1; index < count; index++) {//循环绘制下一个坐标 187 | const coord = item.coords[index]; 188 | // console.log("drawing",coord.longtitude, coord.latitude); 189 | line.lineTo(coord.longitude * this.canvasIndex, coord.latitude * this.canvasIndex); 190 | // this.canvasContext.lineTo(coord.longtitude * this.canvasIndex, coord.latitude * this.canvasIndex); 191 | } 192 | // line.moveTo(coord0.longtitude * this.canvasIndex, coord0.latitude * this.canvasIndex);//将二维路径绘制点移动到初始坐标 193 | line.closePath();//闭合路径 194 | // this.canvasContext.closePath(); 195 | this.canvasContext.stroke(line); 196 | this.canvasContext.fill(line); 197 | }); 198 | } 199 | }) 200 | this.asrCache.items.forEach((item) => { 201 | if (!item.draw) return; 202 | if (!this.canvasContext) return; 203 | if (!this.sectorCache || !this.symbolCache || !this.eseCache) return; 204 | if (item.type == "Fixes")//绘制fix的symbol或name 205 | { 206 | const coord = ReadSctFix(this.sectorCache.fixes, item.name); 207 | if (!coord) return; 208 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, item.flag); 209 | if (!symbol) return; 210 | this.canvasContext.fillStyle = symbol.color; 211 | if (item.flag == "name") { 212 | this.canvasContext.font = symbol.fontSymbolSize * 2.5 + "px Arial"; 213 | this.canvasContext.fillText(item.name, coord.longitude * this.canvasIndex, coord.latitude * this.canvasIndex); 214 | } 215 | else { 216 | //这里的代码后续需要实现DrawScript 217 | const size = symbol.fontSymbolSize; 218 | this.canvasContext.fillRect(coord.longitude * this.canvasIndex, coord.latitude * this.canvasIndex, size, size); 219 | } 220 | } 221 | if (item.type == "Geo")//绘制地面线 222 | { 223 | const geogroup = ReadSctGeo(this.sectorCache.GEOs, item.name); 224 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, "line"); 225 | if (!geogroup || !symbol) return; 226 | this.canvasContext.lineWidth = symbol.lineWeight + this.canvasIndex / 100000; 227 | geogroup.forEach((geo) => { 228 | if (!this.sectorCache || !this.canvasContext) return; 229 | const colorDef = ReadSctDefine(this.sectorCache.definitions, geo.colorFlag); 230 | if (!colorDef) return; 231 | const line = new Path2D(); 232 | line.moveTo(geo.coordA.longitude * this.canvasIndex, geo.coordA.latitude * this.canvasIndex); 233 | line.lineTo(geo.coordB.longitude * this.canvasIndex, geo.coordB.latitude * this.canvasIndex); 234 | this.canvasContext.strokeStyle = colorDef; 235 | this.canvasContext.stroke(line); 236 | }); 237 | } 238 | if (item.type == "High airways")//绘制高空航路 239 | { 240 | const aw = ReadSctLoHiAw(this.sectorCache.hiAirways, item.name); 241 | // console.log(aw) 242 | if (!aw) return; 243 | aw.coords.forEach(coord => { 244 | if (!coord || !this.symbolCache || !this.canvasContext) return; 245 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, item.flag); 246 | if (!symbol) return; 247 | const line = new Path2D(); 248 | line.moveTo(coord.coordA.longitude * this.canvasIndex, coord.coordA.latitude * this.canvasIndex); 249 | line.lineTo(coord.coordB.longitude * this.canvasIndex, coord.coordB.latitude * this.canvasIndex); 250 | if (item.flag == "name") { 251 | this.canvasContext.fillStyle = symbol.color; 252 | this.canvasContext.font = symbol.fontSymbolSize * 3 + "px Arial"; 253 | this.canvasContext.fillText(aw.group, (coord.coordA.longitude * this.canvasIndex + coord.coordB.longitude * this.canvasIndex) / 2, (coord.coordA.latitude * this.canvasIndex + coord.coordB.latitude * this.canvasIndex) / 2) 254 | } 255 | else { 256 | this.canvasContext.lineWidth = symbol.lineWeight; 257 | this.canvasContext.strokeStyle = symbol.color; 258 | this.canvasContext.stroke(line); 259 | } 260 | }); 261 | } 262 | if (item.type == "Low airways")//绘制低空航路 263 | { 264 | const aw = ReadSctLoHiAw(this.sectorCache.hiAirways, item.name); 265 | // console.log(aw); 266 | if (!aw) return; 267 | aw.coords.forEach(coord => { 268 | if (!coord || !this.symbolCache || !this.canvasContext) return; 269 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, item.flag); 270 | if (!symbol) return; 271 | const line = new Path2D(); 272 | line.moveTo(coord.coordA.longitude * this.canvasIndex, coord.coordA.latitude * this.canvasIndex); 273 | line.lineTo(coord.coordB.longitude * this.canvasIndex, coord.coordB.latitude * this.canvasIndex); 274 | if (item.flag == "name") { 275 | this.canvasContext.fillStyle = symbol.color; 276 | this.canvasContext.font = symbol.fontSymbolSize * 3 + "px Arial"; 277 | this.canvasContext.fillText(aw.group, (coord.coordA.longitude * this.canvasIndex + coord.coordB.longitude * this.canvasIndex) / 2, (coord.coordA.latitude * this.canvasIndex + coord.coordB.latitude * this.canvasIndex) / 2) 278 | } 279 | else { 280 | this.canvasContext.lineWidth = symbol.lineWeight; 281 | this.canvasContext.setLineDash([15, 25]); 282 | this.canvasContext.strokeStyle = symbol.color; 283 | this.canvasContext.stroke(line); 284 | } 285 | }); 286 | } 287 | if (item.type == "Runways") { 288 | const nameItems = item.name.split(" "); 289 | const runway = ReadSctRunway(this.sectorCache.runways, nameItems[0], nameItems[2]); 290 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, item.flag); 291 | if (!runway || !symbol) return; 292 | this.canvasContext.strokeStyle = symbol.color; 293 | this.canvasContext.font = symbol.fontSymbolSize * 2.3 + "px Arial"; 294 | if (item.flag == "name") { 295 | this.canvasContext.fillText(runway.endPointA, runway.coordA.longitude * this.canvasIndex, runway.coordA.latitude * this.canvasIndex); 296 | this.canvasContext.fillText(runway.endPointB, runway.coordB.longitude * this.canvasIndex, runway.coordB.latitude * this.canvasIndex); 297 | } 298 | else { 299 | const line = new Path2D(); 300 | line.moveTo(runway.coordA.longitude * this.canvasIndex, runway.coordA.latitude * this.canvasIndex); 301 | line.lineTo(runway.coordB.longitude * this.canvasIndex, runway.coordB.latitude * this.canvasIndex); 302 | this.canvasContext.stroke(line); 303 | } 304 | } 305 | if (item.type == "NDBs" || item.type == "VORs") { 306 | let vorndb: SctVorNdb | undefined; 307 | if (item.type == "NDBs") vorndb = ReadSctVORNDB(this.sectorCache.ndbs, item.name); 308 | if (item.type == "VORs") vorndb = ReadSctVORNDB(this.sectorCache.vors, item.name); 309 | if (!vorndb) return; 310 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, item.flag); 311 | if (!symbol) return; 312 | this.canvasContext.fillStyle = symbol.color; 313 | const size = symbol.fontSymbolSize; 314 | this.canvasContext.font = size * 2.3 + "px Arial"; 315 | if (item.flag == "name") { 316 | this.canvasContext.fillText(vorndb.name, vorndb.coord.longitude * this.canvasIndex, vorndb.coord.latitude * this.canvasIndex); 317 | } 318 | else { 319 | //这里同样要实现绘制脚本 320 | this.canvasContext.fillRect(vorndb.coord.longitude * this.canvasIndex, vorndb.coord.latitude * this.canvasIndex, size, size) 321 | } 322 | } 323 | if (item.type == "Airports") { 324 | const airport = ReadSctAirport(this.sectorCache.airports, item.name); 325 | if (!airport) return; 326 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, item.flag); 327 | if (!symbol) return; 328 | this.canvasContext.fillStyle = symbol.color; 329 | const size = symbol.fontSymbolSize; 330 | this.canvasContext.font = size * 2.3 + "px Arial"; 331 | if (item.flag == "name") { 332 | this.canvasContext.fillText(airport.icao, airport.coord.longitude * this.canvasIndex, airport.coord.latitude * this.canvasIndex); 333 | } 334 | else { 335 | this.canvasContext.fillRect(airport.coord.longitude * this.canvasIndex, airport.coord.latitude * this.canvasIndex, size, size); 336 | } 337 | } 338 | if (item.type == "Free Text")//绘制freetext 339 | { 340 | const groupandtext = item.name.split("\\"); 341 | const origincoord = ReadEseFreeText(this.eseCache.freetexts, groupandtext[0], groupandtext[1]); 342 | const symbol = ReadSymbol(this.symbolCache.colors, "Other", item.flag); 343 | if (!origincoord) return; 344 | if (!symbol) return; 345 | this.canvasContext.font = symbol.fontSymbolSize * 3.3 + "px Arial"; 346 | this.canvasContext.fillText(groupandtext[1], origincoord.longitude * this.canvasIndex, origincoord.latitude * this.canvasIndex); 347 | } 348 | if (item.type == "ARTCC boundary") { 349 | const artcc = ReadSctARTCC(this.sectorCache.ARTCCs, item.name); 350 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, "line"); 351 | if (!symbol || !artcc) return; 352 | this.canvasContext.lineWidth = symbol.lineWeight; 353 | this.canvasContext.strokeStyle = symbol.color; 354 | artcc.coords.forEach((coord) => { 355 | const line = new Path2D(); 356 | line.moveTo(coord.coordA.longitude * this.canvasIndex, coord.coordA.latitude * this.canvasIndex); 357 | line.lineTo(coord.coordB.longitude * this.canvasIndex, coord.coordB.latitude * this.canvasIndex); 358 | this.canvasContext?.stroke(line); 359 | }); 360 | } 361 | if (item.type == "Sids" || item.type == "Stars") { 362 | let sidstar: SctSIDSTAR | undefined; 363 | if (item.type == "Sids") sidstar = ReadSctSidStar(this.sectorCache.sids, item.name); 364 | if (item.type == "Stars") sidstar = ReadSctSidStar(this.sectorCache.stars, item.name); 365 | const symbol = ReadSymbol(this.symbolCache.colors, item.type, "line"); 366 | if (!sidstar || !symbol) return; 367 | this.canvasContext.lineWidth = symbol.lineWeight; 368 | this.canvasContext.strokeStyle = symbol.color; 369 | this.canvasContext.setLineDash([5, 5]); 370 | sidstar.coords.forEach((coordpair) => { 371 | const line = new Path2D(); 372 | line.moveTo(coordpair.coordA.longitude * this.canvasIndex, coordpair.coordA.latitude * this.canvasIndex); 373 | line.lineTo(coordpair.coordB.longitude * this.canvasIndex, coordpair.coordB.latitude * this.canvasIndex); 374 | this.canvasContext?.stroke(line); 375 | }); 376 | } 377 | }); 378 | } 379 | } 380 | 381 | export default Drawer; 382 | -------------------------------------------------------------------------------- /src/utils/settingloader.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import * as jschardet from "iconv-jschardet"; 3 | import { ProfileData, SymbologyData, VoiceData } from "../lib/settingtype"; 4 | 5 | export function LoadSymbology(path: string, callback: (err: NodeJS.ErrnoException | null, data: SymbologyData) => void): void { 6 | if (fs.existsSync(path)) { 7 | const result: SymbologyData = new SymbologyData(); 8 | fs.readFile(path, (err, data) => { 9 | const encodetype = jschardet.detect(data); 10 | let decodedData: string[]; 11 | switch (encodetype.encoding) { 12 | case 'UTF-8': 13 | decodedData = data.toString('utf-8').split("\n"); 14 | break; 15 | default: 16 | decodedData = jschardet.decode(data, 'gbk').split("\n"); 17 | break; 18 | } 19 | decodedData.forEach((line) => { 20 | const dataline = line.split(":"); 21 | if (dataline.length == 7) { 22 | const hexcolor = dataline[2]; 23 | result.colors.push({ 24 | type: dataline[0], 25 | flag: dataline[1], 26 | data: { 27 | color: hexcolor, 28 | fontSymbolSize: parseFloat(dataline[3]), 29 | lineWeight: parseFloat(dataline[4]) == 0 ? 0.5 : parseFloat(dataline[4]) / 3, 30 | lineStyle: parseInt(dataline[5]), 31 | alignment: parseInt(dataline[6]) 32 | } 33 | }); 34 | } 35 | else { 36 | if (dataline[0] == "SYMBOL") { 37 | result.scripts.push({ 38 | ident: parseInt(dataline[1]), 39 | sciprts: [] 40 | }); 41 | } 42 | else if (dataline[0] == "SYMBOLITEM") { 43 | result.scripts[result.scripts.length - 1].sciprts.push(dataline[1]); 44 | } 45 | else if (dataline[0] == "m_ClipArea") { 46 | result.m_ClipArea = parseInt(dataline[1]); 47 | } 48 | } 49 | }); 50 | callback(err, result); 51 | }); 52 | } 53 | else { 54 | throw `path: ${path} does not exist.`; 55 | } 56 | } 57 | 58 | export function LoadSymbologySync(path: string): SymbologyData { 59 | if (fs.existsSync(path)) { 60 | const result: SymbologyData = new SymbologyData(); 61 | const data = fs.readFileSync(path); 62 | const encodetype = jschardet.detect(data); 63 | let decodedData: string[]; 64 | switch (encodetype.encoding) { 65 | case 'UTF-8': 66 | decodedData = data.toString('utf-8').split("\n"); 67 | break; 68 | default: 69 | decodedData = jschardet.decode(data, 'gbk').split("\n"); 70 | break; 71 | } 72 | decodedData.forEach((line) => { 73 | const dataline = line.split(":"); 74 | if (dataline.length == 7) { 75 | const color = "#" + parseInt(dataline[2]).toString(16).padStart(6, "0"); 76 | result.colors.push({ 77 | type: dataline[0], 78 | flag: dataline[1], 79 | data: { 80 | color: color, 81 | fontSymbolSize: parseFloat(dataline[3]), 82 | lineWeight: parseFloat(dataline[4]) == 0 ? 0.5 : parseFloat(dataline[4]) / 3, 83 | lineStyle: parseInt(dataline[5]), 84 | alignment: parseInt(dataline[6]) 85 | } 86 | }); 87 | } 88 | else { 89 | if (dataline[0] == "SYMBOL") { 90 | result.scripts.push({ 91 | ident: parseInt(dataline[1]), 92 | sciprts: [] 93 | }); 94 | } 95 | else if (dataline[0] == "SYMBOLITEM") { 96 | result.scripts[result.scripts.length - 1].sciprts.push(dataline[1]); 97 | } 98 | else if (dataline[0] == "m_ClipArea") { 99 | result.m_ClipArea = parseInt(dataline[1]); 100 | } 101 | } 102 | }); 103 | return result; 104 | } 105 | else { 106 | throw `path: ${path} does not exist.`; 107 | } 108 | } 109 | 110 | export function LoadVoice(path: string, callback: (err: NodeJS.ErrnoException | null, data: VoiceData) => void): void { 111 | if (fs.existsSync(path)) { 112 | const result: VoiceData = new VoiceData(); 113 | fs.readFile(path, (err, data) => { 114 | const encodetype = jschardet.detect(data); 115 | let decodedData: string[]; 116 | switch (encodetype.encoding) { 117 | case 'UTF-8': 118 | decodedData = data.toString('utf-8').split("\n"); 119 | break; 120 | default: 121 | decodedData = jschardet.decode(data, 'gbk').split("\n"); 122 | break; 123 | } 124 | decodedData.forEach((line) => { 125 | const dataline = line.split(":"); 126 | if (dataline.length == 5) { 127 | result.channels.push({ 128 | type: dataline[0], 129 | name: dataline[1], 130 | frequency: dataline[2], 131 | server: dataline[3], 132 | callsign: dataline[4] 133 | }); 134 | } 135 | }); 136 | callback(err, result); 137 | }); 138 | } 139 | else { 140 | throw `path: ${path} does not exist.`; 141 | } 142 | } 143 | 144 | export function LoadVoiceSync(path: string): VoiceData { 145 | if (fs.existsSync(path)) { 146 | const result: VoiceData = new VoiceData(); 147 | const data = fs.readFileSync(path); 148 | const encodetype = jschardet.detect(data); 149 | let decodedData: string[]; 150 | switch (encodetype.encoding) { 151 | case 'UTF-8': 152 | decodedData = data.toString('utf-8').split("\n"); 153 | break; 154 | default: 155 | decodedData = jschardet.decode(data, 'gbk').split("\n"); 156 | break; 157 | } 158 | decodedData.forEach((line) => { 159 | const dataline = line.split(":"); 160 | if (dataline.length == 5) { 161 | result.channels.push({ 162 | type: dataline[0], 163 | name: dataline[1], 164 | frequency: dataline[2], 165 | server: dataline[3], 166 | callsign: dataline[4] 167 | }); 168 | } 169 | }); 170 | return result; 171 | } 172 | else { 173 | throw `path: ${path} does not exist.`; 174 | } 175 | } 176 | 177 | export function LoadProfile(path: string, callback: (err: NodeJS.ErrnoException | null, data: ProfileData) => void): void { 178 | if (fs.existsSync(path)) { 179 | const result: ProfileData = new ProfileData(); 180 | fs.readFile(path, (err, data) => { 181 | const encodetype = jschardet.detect(data); 182 | let decodedData: string[]; 183 | switch (encodetype.encoding) { 184 | case 'UTF-8': 185 | decodedData = data.toString('utf-8').split("\n"); 186 | break; 187 | default: 188 | decodedData = jschardet.decode(data, 'gbk').split("\n"); 189 | break; 190 | } 191 | decodedData.forEach((line) => { 192 | //跳过空行 193 | if (line == "") return; 194 | //除去注释 195 | if (line.lastIndexOf(";") !== -1) line = line.split(";")[0]; 196 | const dataline = line.split(":"); 197 | if (dataline.length == 4) { 198 | if (dataline[0] == "PROFILE") { 199 | result.profile.push({ 200 | info: { 201 | ident: dataline[1], 202 | range: parseInt(dataline[2]), 203 | facility: parseInt(dataline[3]) 204 | }, 205 | atis2: "", 206 | atis3: "", 207 | atis4: "" 208 | }); 209 | } 210 | } 211 | if (dataline.length == 2) { 212 | if (dataline[0] == "ATIS2") { 213 | result.profile[result.profile.length - 1].atis2 = dataline[1]; 214 | } 215 | else if (dataline[0] == "ATIS3") { 216 | result.profile[result.profile.length - 1].atis3 = dataline[1]; 217 | } 218 | else if (dataline[0] == "ATIS4") { 219 | result.profile[result.profile.length - 1].atis4 = dataline[1]; 220 | } 221 | } 222 | }); 223 | callback(err, result); 224 | }); 225 | } 226 | else { 227 | throw `path: ${path} does not exist.`; 228 | } 229 | } 230 | 231 | export function LoadProfileSync(path: string): ProfileData { 232 | if (fs.existsSync(path)) { 233 | const result: ProfileData = new ProfileData(); 234 | const data = fs.readFileSync(path); 235 | const encodetype = jschardet.detect(data); 236 | let decodedData: string[]; 237 | switch (encodetype.encoding) { 238 | case 'UTF-8': 239 | decodedData = data.toString('utf-8').split("\n"); 240 | break; 241 | default: 242 | decodedData = jschardet.decode(data, 'gbk').split("\n"); 243 | break; 244 | } 245 | decodedData.forEach((line) => { 246 | //跳过空行 247 | if (line == "") return; 248 | //除去注释 249 | if (line.lastIndexOf(";") !== -1) line = line.split(";")[0]; 250 | const dataline = line.split(":"); 251 | if (dataline.length == 4) { 252 | if (dataline[0] == "PROFILE") { 253 | result.profile.push({ 254 | info: { 255 | ident: dataline[1], 256 | range: parseInt(dataline[2]), 257 | facility: parseInt(dataline[3]) 258 | }, 259 | atis2: "", 260 | atis3: "", 261 | atis4: "" 262 | }); 263 | } 264 | } 265 | if (dataline.length == 2) { 266 | if (dataline[0] == "ATIS2") { 267 | result.profile[result.profile.length - 1].atis2 = dataline[1]; 268 | } 269 | else if (dataline[0] == "ATIS3") { 270 | result.profile[result.profile.length - 1].atis3 = dataline[1]; 271 | } 272 | else if (dataline[0] == "ATIS4") { 273 | result.profile[result.profile.length - 1].atis4 = dataline[1]; 274 | } 275 | } 276 | }); 277 | return result; 278 | } 279 | else { 280 | throw `path: ${path} does not exist.`; 281 | } 282 | } 283 | 284 | export default { 285 | LoadSymbology, LoadSymbologySync, 286 | LoadVoice, LoadVoiceSync, 287 | LoadProfile, LoadProfileSync 288 | } -------------------------------------------------------------------------------- /src/utils/wxfetcher.worker.ts: -------------------------------------------------------------------------------- 1 | // import https from 'https' 2 | // https.get("https://metar.vatsim.net/ZBAA", (res) => { 3 | // res.on('data', (chunk: Buffer) => { 4 | // postMessage(chunk.toString()) 5 | // }) 6 | // }); 7 | const httpRequest = new XMLHttpRequest(); 8 | onmessage = function(e){ 9 | httpRequest.open('GET','https://metar.vatsim.net/'+e.data,true) 10 | httpRequest.setRequestHeader("Referer","https://metar.vatsim.net") 11 | httpRequest.send() 12 | httpRequest.onreadystatechange = function(){ 13 | if(httpRequest.readyState == 4 && httpRequest.status == 200){ 14 | console.log(httpRequest.responseText) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "outDir": "dist_electron", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "useDefineForClassFields": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "types": [ 18 | "webpack-env" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | "tests/**/*.ts", 37 | "tests/**/*.tsx", 38 | "src/utils/wxfetcher.worker.tsrkersrc/utils/wxfetcher.worker.tscher.worker.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } --------------------------------------------------------------------------------