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

2 | Title Image 3 |

4 | 5 | ## Model View 6 | Display EPANET models directly in the browser. No data leaves your computer; all data rendered and processed locally. 7 | 8 | This app is being powered by [epanet-js](https://github.com/modelcreate/epanet-js). 9 | 10 | 11 | 12 | ## Future Enhancements 13 | Model View is under active development; this version is an early preview. 14 | 15 | * Add more support for object types, e.g. Reservoirs, Fixed Heads, Nodes 16 | * Mobile-friendly UI 17 | * Flow direction on pipes 18 | * Clean up of the codebase 19 | 20 | Please feel free to submit feature requests or bugs to GitHub. 21 | 22 | ## Running App Locally 23 | 24 | This project was created with create-react-app and uses Typescript. To run a local copy fork the repo and in the project directory, you can run: 25 | 26 | ### `npm start` 27 | 28 | Runs the app in the development mode.
29 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 30 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "target": "modelview", 4 | "public": "build", 5 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 6 | "rewrites": [ 7 | { 8 | "source": "**", 9 | "destination": "/index.html" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "model-view", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://modelview.matrado.ca", 6 | "prettier": {}, 7 | "dependencies": { 8 | "@material-ui/core": "^4.10.0", 9 | "@material-ui/lab": "^4.0.0-alpha.55", 10 | "@turf/turf": "^5.1.6", 11 | "@types/jest": "24.0.11", 12 | "@types/node": "^14.0.5", 13 | "@types/proj4": "^2.5.0", 14 | "@types/react": "^16.9.35", 15 | "@types/react-dom": "^16.9.8", 16 | "@types/react-map-gl": "^5.2.4", 17 | "@types/react-window": "^1.8.2", 18 | "@types/victory": "^33.1.4", 19 | "date-fns": "^2.14.0", 20 | "epanet-js": "^0.3.0", 21 | "immutable": "^4.0.0-rc.12", 22 | "proj4": "^2.6.2", 23 | "react": "^16.13.1", 24 | "react-dom": "^16.13.1", 25 | "react-dropzone": "^11.0.1", 26 | "react-map-gl": "5.2.5", 27 | "react-scripts": "3.4.1", 28 | "react-window": "^1.8.5", 29 | "typescript": "^3.9.3", 30 | "victory": "34.3.7" 31 | }, 32 | "scripts": { 33 | "predeploy": "npm run build", 34 | "deploy": "gh-pages -d build", 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": "react-app" 42 | }, 43 | "browserslist": [ 44 | ">0.2%", 45 | "not dead", 46 | "not ie <= 11", 47 | "not op_mini all" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | modelview.matrado.ca -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-view/a5fe11daf820c5331dcabc96b4da9de5bacdf564/public/favicon.ico -------------------------------------------------------------------------------- /public/imgs/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-view/a5fe11daf820c5331dcabc96b4da9de5bacdf564/public/imgs/background.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 33 | Model View - Matrado 34 | 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Model View", 3 | "name": "Model View", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /src/components/App/index.css: -------------------------------------------------------------------------------- 1 | 2 | .App-header { 3 | min-height: 100vh; 4 | 5 | font-size: calc(10px + 2vmin); 6 | color: white; 7 | } 8 | 9 | .tooltip { 10 | position: absolute; 11 | margin: 8px; 12 | padding: 4px; 13 | background: rgba(0, 0, 0, 0.8); 14 | color: #fff; 15 | max-width: 300px; 16 | font-size: 10px; 17 | z-index: 9; 18 | pointer-events: none; 19 | } 20 | 21 | .mapboxgl-ctrl-attrib { 22 | padding: 0 5px; 23 | background-color: rgba(0, 0, 0, 0.15)!important; 24 | margin: 0; 25 | } -------------------------------------------------------------------------------- /src/components/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ModelDropZone from "../ModelDropZone"; 3 | import VectorMap from "../VectorMap"; 4 | import Landing from "../Landing"; 5 | import ModelInfo, { ModelInfoSetting } from "../ModelInfo"; 6 | import { EpanetResults } from "../../utils/EpanetBinary"; 7 | 8 | import { runEpanet, ReportingInfo } from "../../utils/epanet"; 9 | import EpanetGeoJSON from "../../interfaces/EpanetGeoJSON"; 10 | import "./index.css"; 11 | import "mapbox-gl/dist/mapbox-gl.css"; 12 | import { 13 | FeatureCollection, 14 | Geometries, 15 | Properties, 16 | Feature, 17 | } from "@turf/helpers"; 18 | 19 | // TODO: Clean up and remove the requirement for settings and check for null 20 | const setting: ModelInfoSetting = { 21 | modeName: "Test Model", 22 | currentTimestep: 0, 23 | reportingInfo: { 24 | Periods: 0, 25 | Duration: 0, 26 | ReportStep: 0, 27 | StartTime: 0, 28 | }, 29 | selectedFeature: null, 30 | }; 31 | 32 | type Props = {}; 33 | 34 | interface AppState { 35 | modelGeoJson?: FeatureCollection; 36 | epanetResults?: EpanetResults; 37 | isLoading: boolean; 38 | isFileLoaded: boolean; 39 | projectionString: string; 40 | setting: ModelInfoSetting; 41 | } 42 | 43 | class App extends Component { 44 | state: Readonly = { 45 | isLoading: false, 46 | isFileLoaded: false, 47 | projectionString: "", 48 | setting, 49 | }; 50 | 51 | loadDemo = () => { 52 | const projectionString = 53 | "+proj=utm +zone=17 +datum=NAD83 +units=m +no_defs"; 54 | this.setState((prevState) => ({ projectionString, isLoading: true })); 55 | fetch("static/models/example1.inp") 56 | .then((res) => res.text()) 57 | .then((body) => { 58 | runEpanet(body, this.droppedJson); 59 | }); 60 | }; 61 | 62 | droppedJson = (file: [EpanetGeoJSON, EpanetResults, ReportingInfo]) => { 63 | this.setState((prevState) => ({ 64 | isFileLoaded: true, 65 | modelGeoJson: file[0], 66 | epanetResults: file[1], 67 | setting: { 68 | ...prevState.setting, 69 | reportingInfo: file[2], 70 | //timesteps: file[0].model.timesteps.map( 71 | // (t) => new Date(t.substr(0, 16)) 72 | //), 73 | }, 74 | })); 75 | }; 76 | _updateSettings = (value: string) => { 77 | this.setState((prevState) => ({ 78 | setting: { 79 | ...prevState.setting, 80 | currentTimestep: parseInt(value), 81 | }, 82 | })); 83 | }; 84 | 85 | _updateSelectedFeature = (value: Feature) => { 86 | if (value.properties !== null && this.state.epanetResults) { 87 | const selectedFeature: { [name: string]: any } = value.properties; 88 | 89 | const type = value.geometry && value.geometry.type; 90 | 91 | let tsv = {}; 92 | const index = value.id; 93 | if (typeof index === "number" && type === "Point") { 94 | tsv = this.state.epanetResults.results.nodes[index]; 95 | } else if (typeof index === "number" && type === "LineString") { 96 | tsv = this.state.epanetResults.results.links[index]; 97 | } 98 | 99 | const props = { 100 | ...selectedFeature, 101 | ...tsv, 102 | }; 103 | 104 | this.setState((prevState) => ({ 105 | setting: { 106 | ...prevState.setting, 107 | selectedFeature: props, 108 | }, 109 | })); 110 | } 111 | }; 112 | 113 | _clearSelectedFeature = () => { 114 | this.setState((prevState) => ({ 115 | setting: { 116 | ...prevState.setting, 117 | selectedFeature: null, 118 | }, 119 | })); 120 | }; 121 | 122 | _updateProjectionString = (projectionString: string) => { 123 | this.setState((prevState) => ({ projectionString, isLoading: true })); 124 | }; 125 | 126 | render() { 127 | const { 128 | isLoading, 129 | isFileLoaded, 130 | modelGeoJson, 131 | setting, 132 | projectionString, 133 | } = this.state; 134 | 135 | return ( 136 | 137 |
138 |
139 | {modelGeoJson && projectionString !== "" ? ( 140 | <> 141 | 146 | 151 | 152 | ) : ( 153 | 159 | )} 160 |
161 |
162 |
163 | ); 164 | } 165 | } 166 | 167 | export default App; 168 | -------------------------------------------------------------------------------- /src/components/App/test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from '.'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/FeatureProperties/index.css: -------------------------------------------------------------------------------- 1 | .tvd-form { 2 | display: table; 3 | width: 100%; 4 | text-align: center; 5 | } 6 | 7 | .tvd-form label { 8 | width: 1px; 9 | white-space: nowrap; 10 | padding-right: 5px; 11 | } 12 | 13 | .tvd-form select { 14 | display: table-cell; 15 | padding: 0 0 0 5px; 16 | } 17 | 18 | .feature-data-table { 19 | text-align: left; 20 | } 21 | 22 | .close-button { 23 | position: absolute; 24 | top: 95px; 25 | right: 15px; 26 | z-index: 101; 27 | } -------------------------------------------------------------------------------- /src/components/FeatureProperties/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from "react"; 2 | import TimeSeriesChart from "../TimeSeriesChart"; 3 | import { ModelInfoSetting } from "../ModelInfo"; 4 | import "./index.css"; 5 | 6 | type FeatureProperties = { 7 | feature: ModelInfoSetting; 8 | tsv: string[]; 9 | selectedFeature: { [name: string]: any }; 10 | onClearSelected: () => void; 11 | }; 12 | 13 | const FeatureProperties: FunctionComponent = ({ 14 | feature, 15 | tsv, 16 | selectedFeature, 17 | onClearSelected, 18 | }) => { 19 | //const selectId = Object.keys(selectedFeature).find(key => selectedFeature[key].constructor === Array) 20 | const [timeSeriesId, setTimeSeriesId] = useState(tsv[0]); 21 | 22 | if (selectedFeature[timeSeriesId] === undefined) { 23 | setTimeSeriesId(tsv[0]); 24 | } 25 | 26 | const precise = (x: number): string => { 27 | return x < 0 ? x.toPrecision(2) : x.toFixed(2).toString(); 28 | }; 29 | 30 | //const title: string = selectedFeature.us_node_id ? `${selectedFeature.us_node_id}.${selectedFeature.ds_node_id}.${selectedFeature.link_suffix}` : selectedFeature.node_id 31 | return ( 32 |
33 |
34 | 37 | {selectedFeature[timeSeriesId] && ( 38 | 45 | )} 46 |
47 | 48 | 59 |
60 |
61 | 62 | 63 | {Object.keys(selectedFeature).map((keyName, i) => ( 64 | 65 | {selectedFeature[keyName].constructor !== Array ? ( 66 | <> 67 | 68 | 69 | 70 | ) : ( 71 | <> 72 | 73 | 78 | 79 | )} 80 | 81 | ))} 82 | 83 |
{keyName}{selectedFeature[keyName]}{keyName} 74 | {precise( 75 | selectedFeature[keyName][feature.currentTimestep] 76 | )} 77 |
84 |
85 |
86 | ); 87 | }; 88 | 89 | export default FeatureProperties; 90 | -------------------------------------------------------------------------------- /src/components/Landing/index.css: -------------------------------------------------------------------------------- 1 | .react-select-container { 2 | font-size: 12px; 3 | margin-bottom: 10px; 4 | margin-right: 15px; 5 | } 6 | 7 | .model-proj-subtitle{ 8 | margin: 25px 5px 5px; 9 | font-size: 14px; 10 | } 11 | 12 | 13 | 14 | .flex-grid { 15 | display: flex; 16 | min-height: 100vh; 17 | font-family: 'Roboto', sans-serif; 18 | background: linear-gradient(rgba(0, 0, 0, 0.1),rgba(0, 0, 0, 0.1)), url(/imgs/background.png) no-repeat center right; 19 | background-size: cover; 20 | color:rgba(0,0,0,.77); 21 | } 22 | 23 | .flex-grid .col1 { 24 | padding: 20px; 25 | background-color: #fafafa; 26 | box-shadow: 1px; 27 | box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12); 28 | } 29 | 30 | .flex-grid h3 { 31 | margin: 0; 32 | padding-top: 40px; 33 | font-size: 1em; 34 | margin-left: 3px; 35 | margin-bottom: -5px; 36 | } 37 | 38 | .flex-grid h1 { 39 | margin: 0; 40 | font-size: 2.8em; 41 | } 42 | 43 | .flex-grid .subtitle { 44 | margin-top: -4px; 45 | margin-left: 4px; 46 | font-size: calc(8px + 1.4vmin); 47 | } 48 | .flex-grid .droparea { 49 | text-align: center; 50 | padding: 60px; 51 | border: 2px dashed rgb(145, 145, 145); 52 | border-radius: 15px; 53 | margin: 30px 50px; 54 | color: rgb(145, 145, 145) 55 | } 56 | 57 | .droparea p { 58 | font-size: 0.6em; 59 | color: rgb(145, 145, 145); 60 | } 61 | 62 | .blurb { 63 | font-size: 0.5em; 64 | color: rgb(145, 145, 145); 65 | margin-left: 5px; 66 | } 67 | 68 | 69 | .col1 { 70 | flex: 3; 71 | } 72 | .col2 { 73 | flex: 4; 74 | } 75 | 76 | .btns-float-left { 77 | float:right; 78 | margin-right: 15px; 79 | } 80 | 81 | 82 | @media (max-width: 400px) { 83 | .flex-grid { 84 | display: block; 85 | } 86 | } -------------------------------------------------------------------------------- /src/components/Landing/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import CircularProgress from "@material-ui/core/CircularProgress"; 3 | import SelectProjection from "../SelectProjection"; 4 | import Button from "@material-ui/core/Button"; 5 | import { 6 | WithStyles, 7 | withStyles, 8 | createStyles, 9 | Theme 10 | } from "@material-ui/core/styles"; 11 | import "./index.css"; 12 | 13 | const styles = (theme: Theme) => 14 | createStyles({ 15 | button: { 16 | //margin: theme.spacing.unit 17 | }, 18 | input: { 19 | display: "none" 20 | } 21 | }); 22 | 23 | interface LandingProperties extends WithStyles { 24 | isLoading: boolean; 25 | isFileLoaded: boolean; 26 | onSelectProj: (proj: string) => void; 27 | onLoadDemo: () => void; 28 | } 29 | 30 | const Landing: FunctionComponent = ({ 31 | isLoading, 32 | isFileLoaded, 33 | onSelectProj, 34 | onLoadDemo, 35 | classes 36 | }) => { 37 | return ( 38 |
39 |
40 |

Matrado

41 |

Model View

42 | 43 | {isFileLoaded ? ( 44 | 45 | ) : isLoading ? ( 46 | 47 | ) : ( 48 | <> 49 |

Share and view models in the browser

50 |

51 |

View EPANET watermodels online.

52 |

53 | Model View is under active development, this version is an early 54 | preview. This application and its source code is free and open 55 | source under AGPLv3. Feature requests and issues can be logged on 56 | GitHub, contact me on{" "} 57 | LinkedIn or 58 | email - luke@matrado.ca 59 |

60 | 61 |
62 |

Drop EPANET .inp file here

63 |

64 | All data is proccessed client side, no model data sent to the 65 | server. 66 |

67 | 74 |
75 |
76 | 83 |
84 | 85 | )} 86 |
87 |
88 |
89 | ); 90 | }; 91 | 92 | export default withStyles(styles)(Landing); 93 | -------------------------------------------------------------------------------- /src/components/ModelDropZone/index.tsx: -------------------------------------------------------------------------------- 1 | import { useDropzone } from "react-dropzone"; 2 | import React, { useMemo, useCallback, FunctionComponent } from "react"; 3 | import { runEpanet, ReportingInfo } from "../../utils/epanet"; 4 | import { EpanetResults } from "../../utils/EpanetBinary"; 5 | 6 | import EpanetGeoJSON from "../../interfaces/EpanetGeoJSON"; 7 | 8 | const overlayStyle = { 9 | position: "absolute", 10 | top: 0, 11 | right: 0, 12 | bottom: 0, 13 | left: 0, 14 | padding: "2.5em 0", 15 | background: "rgba(0,0,0,0.5)", 16 | textAlign: "center", 17 | color: "#fff", 18 | } as React.CSSProperties; 19 | 20 | const baseStyle = { 21 | position: "relative", 22 | } as React.CSSProperties; 23 | 24 | const activeStyle = { 25 | borderStyle: "solid", 26 | borderColor: "#6c6", 27 | backgroundColor: "#eee", 28 | }; 29 | 30 | const acceptStyle = { 31 | borderStyle: "solid", 32 | borderColor: "#00e676", 33 | }; 34 | 35 | const rejectStyle = { 36 | borderStyle: "solid", 37 | borderColor: "#ff1744", 38 | }; 39 | 40 | type ModelDropZone = { 41 | onDroppedJson: (file: [EpanetGeoJSON, EpanetResults, ReportingInfo]) => void; 42 | }; 43 | 44 | const ModelDropZone: FunctionComponent = ({ 45 | onDroppedJson, 46 | children, 47 | }) => { 48 | const onDrop = useCallback( 49 | (acceptedFiles: File[]) => { 50 | if (acceptedFiles[0] !== undefined) { 51 | const reader = new FileReader(); 52 | reader.onload = () => { 53 | if (typeof reader.result === "string") { 54 | runEpanet(reader.result, onDroppedJson); 55 | } 56 | }; 57 | 58 | reader.readAsText(acceptedFiles[0]); 59 | } 60 | }, 61 | [onDroppedJson] 62 | ); 63 | 64 | const { 65 | getRootProps, 66 | isDragActive, 67 | isDragAccept, 68 | isDragReject, 69 | } = useDropzone({ 70 | accept: ["application/json", ""], 71 | multiple: false, 72 | onDrop, 73 | }); 74 | 75 | const style = useMemo( 76 | () => ({ 77 | ...baseStyle, 78 | ...(isDragActive ? activeStyle : {}), 79 | ...(isDragAccept ? acceptStyle : {}), 80 | ...(isDragReject ? rejectStyle : {}), 81 | }), 82 | [isDragActive, isDragAccept, isDragReject] 83 | ); 84 | 85 | return ( 86 |
87 | {isDragActive &&
Drop files here
} 88 | {children} 89 |
90 | ); 91 | }; 92 | 93 | export default ModelDropZone; 94 | -------------------------------------------------------------------------------- /src/components/ModelInfo/index.css: -------------------------------------------------------------------------------- 1 | .control-panel h1, .control-panel h2 { 2 | margin: 0; 3 | text-align: center 4 | } 5 | 6 | .input { 7 | margin-left: 25px; 8 | margin-right: 35px; 9 | } 10 | .input input{ 11 | width:100%; 12 | } 13 | 14 | .control-panel { 15 | position: absolute; 16 | top: 0; 17 | right: 0; 18 | background: #fff; 19 | box-shadow: 0 2px 4px rgba(0,0,0,0.3); 20 | padding: 12px 12px; 21 | font-size: 0.5em; 22 | color: #6b6b76; 23 | outline: none; 24 | } 25 | 26 | 27 | .control-panel-data { 28 | overflow-x: auto; 29 | } 30 | 31 | 32 | 33 | .control-panel p { 34 | margin-bottom: 16px; 35 | } 36 | 37 | hr { 38 | border: none; 39 | background: #ededed; 40 | height: 1px; 41 | } 42 | 43 | @media (min-width:320px) { 44 | 45 | .control-panel { 46 | margin: 0; 47 | min-width: calc(100% - 24px); 48 | z-index:100; 49 | } 50 | .time-controls { 51 | text-align: center; 52 | } 53 | 54 | .control-panel h1, .control-panel h2 { 55 | display: inline; 56 | font-size: 1.5em; 57 | } 58 | 59 | .control-panel h1 { 60 | margin-left: 10px; 61 | } 62 | 63 | .control-panel-data { 64 | height: calc(100vh - 270px) 65 | } 66 | .input { 67 | margin-top: 10px; 68 | } 69 | } 70 | 71 | @media only screen and (min-width:568px) { 72 | 73 | .control-panel { 74 | margin: 0; 75 | max-width: 200px; 76 | min-width: 100px; 77 | z-index:0; 78 | } 79 | .control-panel-data { 80 | height: calc(100vh - 210px) 81 | } 82 | 83 | .control-panel h1, .control-panel h2 { 84 | display: block; 85 | } 86 | .control-panel h2 { 87 | font-size: 1em; 88 | } 89 | .control-panel h1 { 90 | font-size: 1.5em; 91 | margin-left: 0px; 92 | } 93 | .input { 94 | margin-top: 0px; 95 | } 96 | } 97 | 98 | 99 | @media only screen and (min-width:961px) { 100 | 101 | .control-panel { 102 | max-width: 300px; 103 | margin: 20px; 104 | 105 | } 106 | .control-panel-data { 107 | max-height: calc(100vh - 280px); 108 | height: 100% 109 | } 110 | .control-panel h2 { 111 | font-size: 1.5em; 112 | } 113 | .control-panel h1 { 114 | font-size: 2em; 115 | } 116 | } 117 | 118 | /* 119 | CSS Media Debug 120 | @media only screen and (min-width:320px) { 121 | 122 | body::before { 123 | content: "smartphones"; 124 | font-weight: bold; 125 | display: block; 126 | text-align: center; 127 | background: rgba(255, 255, 0, 0.9); 128 | position: absolute; 129 | top: 0; 130 | left: 0; 131 | right: 0; 132 | z-index: 99; 133 | } 134 | } 135 | @media only screen and (min-width:568px) { 136 | 137 | body::before { 138 | content: "portrait tablets, portrait iPad,"; 139 | font-weight: bold; 140 | display: block; 141 | text-align: center; 142 | background: rgba(255, 255, 0, 0.9); 143 | position: absolute; 144 | top: 0; 145 | left: 0; 146 | right: 0; 147 | z-index: 99; 148 | } 149 | } 150 | @media only screen and (min-width:961px) { 151 | 152 | body::before { 153 | content: "Full Screen"; 154 | font-weight: bold; 155 | display: block; 156 | text-align: center; 157 | background: rgba(255, 255, 0, 0.9); 158 | position: absolute; 159 | top: 0; 160 | left: 0; 161 | right: 0; 162 | z-index: 99; 163 | } 164 | } 165 | 166 | 167 | */ -------------------------------------------------------------------------------- /src/components/ModelInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { Properties } from "@turf/helpers"; 3 | import FeatureProperties from "../FeatureProperties"; 4 | import { ReportingInfo } from "../../utils/epanet"; 5 | import format from "date-fns/format"; 6 | import addSeconds from "date-fns/addSeconds"; 7 | import "./index.css"; 8 | 9 | type DefaultContainer = {}; 10 | 11 | const DefaultContainer: FunctionComponent = ({ 12 | children, 13 | }) =>
{children}
; 14 | 15 | export interface ModelInfoSetting { 16 | modeName: string; 17 | currentTimestep: number; 18 | reportingInfo: ReportingInfo; 19 | selectedFeature: Properties; 20 | } 21 | 22 | type ModelInfoProps = { 23 | settings: ModelInfoSetting; 24 | onChange: (value: string) => void; 25 | onClearSelected: () => void; 26 | }; 27 | 28 | const ModelInfo: FunctionComponent = ({ 29 | settings, 30 | onChange, 31 | onClearSelected, 32 | }) => { 33 | const currentSecond = 34 | settings.reportingInfo.StartTime + 35 | settings.currentTimestep * settings.reportingInfo.ReportStep; 36 | 37 | const day = Math.floor(currentSecond / 86400) + 1; 38 | const hour = Math.floor((currentSecond % 86400) / 3600); 39 | const min = Math.floor((currentSecond % 3600) / 60); 40 | 41 | return ( 42 | 43 |
44 |

Day {day}

45 |

46 | {hour.toString().padStart(2, "0")}:{min.toString().padStart(2, "0")} 47 |

48 |
49 | onChange(evt.target.value)} 56 | /> 57 |
58 |
59 | 60 | {settings.selectedFeature && ( 61 | 68 | settings.selectedFeature && 69 | settings.selectedFeature[key].constructor === Array 70 | )} 71 | /> 72 | )} 73 |
74 | ); 75 | }; 76 | 77 | export default ModelInfo; 78 | -------------------------------------------------------------------------------- /src/components/SelectProjection/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Button from "@material-ui/core/Button"; 3 | import { proj4List } from "./epsg"; 4 | 5 | import TextField from "@material-ui/core/TextField"; 6 | import Autocomplete, { 7 | AutocompleteRenderGroupParams, 8 | } from "@material-ui/lab/Autocomplete"; 9 | import useMediaQuery from "@material-ui/core/useMediaQuery"; 10 | import ListSubheader from "@material-ui/core/ListSubheader"; 11 | import { useTheme, makeStyles } from "@material-ui/core/styles"; 12 | import { VariableSizeList, ListChildComponentProps } from "react-window"; 13 | import { Typography } from "@material-ui/core"; 14 | 15 | type SelectProjectionProps = { 16 | onSelectProj: (proj: string) => void; 17 | }; 18 | 19 | const LISTBOX_PADDING = 8; // px 20 | 21 | function renderRow(props: ListChildComponentProps) { 22 | const { data, index, style } = props; 23 | return React.cloneElement(data[index], { 24 | style: { 25 | ...style, 26 | top: (style.top as number) + LISTBOX_PADDING, 27 | }, 28 | }); 29 | } 30 | 31 | const OuterElementContext = React.createContext({}); 32 | 33 | const OuterElementType = React.forwardRef((props, ref) => { 34 | const outerProps = React.useContext(OuterElementContext); 35 | return
; 36 | }); 37 | 38 | function useResetCache(data: any) { 39 | const ref = React.useRef(null); 40 | React.useEffect(() => { 41 | if (ref.current != null) { 42 | ref.current.resetAfterIndex(0, true); 43 | } 44 | }, [data]); 45 | return ref; 46 | } 47 | 48 | // Adapter for react-window 49 | const ListboxComponent = React.forwardRef( 50 | function ListboxComponent(props, ref) { 51 | const { children, ...other } = props; 52 | const itemData = React.Children.toArray(children); 53 | const theme = useTheme(); 54 | const smUp = useMediaQuery(theme.breakpoints.up("sm"), { noSsr: true }); 55 | const itemCount = itemData.length; 56 | const itemSize = smUp ? 36 : 48; 57 | 58 | const getChildSize = (child: React.ReactNode) => { 59 | if (React.isValidElement(child) && child.type === ListSubheader) { 60 | return 48; 61 | } 62 | 63 | return itemSize; 64 | }; 65 | 66 | const getHeight = () => { 67 | if (itemCount > 8) { 68 | return 8 * itemSize; 69 | } 70 | return itemData.map(getChildSize).reduce((a, b) => a + b, 0); 71 | }; 72 | 73 | const gridRef = useResetCache(itemCount); 74 | 75 | return ( 76 |
77 | 78 | getChildSize(itemData[index])} 86 | overscanCount={5} 87 | itemCount={itemCount} 88 | > 89 | {renderRow} 90 | 91 | 92 |
93 | ); 94 | } 95 | ); 96 | 97 | const useStyles = makeStyles({ 98 | listbox: { 99 | boxSizing: "border-box", 100 | "& ul": { 101 | padding: 0, 102 | margin: 0, 103 | }, 104 | }, 105 | }); 106 | 107 | const renderGroup = (params: AutocompleteRenderGroupParams) => [ 108 | 109 | {params.group} 110 | , 111 | params.children, 112 | ]; 113 | 114 | function SelectProjection({ onSelectProj }: SelectProjectionProps) { 115 | const [value, setValue] = useState(undefined); 116 | const classes = useStyles(); 117 | 118 | return ( 119 | <> 120 |

121 | Type out your map projection below if known, e.g. British National Grid 122 | - EPSG:27700, this will include background maps in your model. 123 |

124 |

125 | Otherwise select 'No Basemap' to load without the background maps. 126 |

127 | 135 | > 136 | } 137 | renderGroup={renderGroup} 138 | onChange={(event, newInputValue) => { 139 | console.log(newInputValue?.value); 140 | setValue(newInputValue?.value); 141 | }} 142 | options={proj4List} 143 | getOptionLabel={(option) => option.label} 144 | renderInput={(params) => ( 145 | 146 | )} 147 | renderOption={(option) => ( 148 | {option.label} 149 | )} 150 | /> 151 | 152 | {value && ( 153 |
154 | 163 |
164 | )} 165 | 166 | 175 | 176 | ); 177 | } 178 | 179 | export default SelectProjection; 180 | -------------------------------------------------------------------------------- /src/components/TimeSeriesChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { VictoryChart, VictoryLine, VictoryAxis, VictoryTheme } from "victory"; 3 | 4 | type TimeSeriesChartProps = { 5 | timeseriesData: number[]; 6 | startTime: number; 7 | reportStep: number; 8 | periods: number; 9 | currentTimestep: number; 10 | }; 11 | 12 | const TimeSeriesChart: FunctionComponent = ({ 13 | timeseriesData, 14 | startTime, 15 | reportStep, 16 | periods, 17 | currentTimestep, 18 | }) => { 19 | const currentSecond = startTime + currentTimestep * reportStep; 20 | 21 | const avgData = 22 | timeseriesData.reduce((p, c) => p + c, 0) / timeseriesData.length; 23 | const multipler = avgData >= 0 ? 1 : -1; 24 | 25 | const data = timeseriesData.map((data, i) => ({ 26 | x: startTime + reportStep * i, 27 | y: data * multipler, 28 | })); 29 | const max = Math.max(...timeseriesData); 30 | const min = Math.min(...timeseriesData); 31 | const domainMax = Math.max(Math.abs(max), Math.abs(min)); 32 | const domainMin = Math.min(Math.abs(max), Math.abs(min)); 33 | const diff = domainMax - domainMin === 0 ? 1 : domainMax - domainMin; 34 | 35 | const precise = (x: number): string => { 36 | return x < 0 ? x.toPrecision(2) : x.toFixed(2).toString(); 37 | }; 38 | 39 | const getTickFormat = (t: number): string => { 40 | if (t % 86400 === 0) { 41 | return `Day ${t / 86400 + 1}`; 42 | } 43 | const hour = Math.floor((t % 86400) / 3600); 44 | const minute = Math.floor((t % 3600) / 60); 45 | 46 | return `${hour.toString().padStart(2, "0")}:${minute 47 | .toString() 48 | .padStart(2, "0")}`; 49 | }; 50 | 51 | const ticks = Array.from(Array(periods).keys()).reduce((acc, i) => { 52 | const tickInt = periods * reportStep > 86400 * 2 ? 86400 : 21600; 53 | 54 | const time = startTime + reportStep * i; 55 | if (time === 0 || time % tickInt === 0) { 56 | return acc.concat(time); 57 | } 58 | return acc; 59 | }, [] as number[]); 60 | 61 | return ( 62 |
63 | 69 | getTickFormat(t)} 73 | tickValues={ticks} 74 | /> 75 | 76 | currentSecond} //timesteps[currentTimestep].getTime()} 81 | /> 82 | 85 | currentSecond === datum.x ? precise(datum.y) : null 86 | } 87 | //labels={(d: { x: number; y: number }) => { 88 | // console.log(d); 89 | // return currentSecond === d.x ? precise(d.y) : null; 90 | //}} 91 | style={{ 92 | data: { stroke: "#1528f7" }, 93 | labels: { fill: "#00000", fontSize: 20, textAnchor: "start" }, 94 | }} 95 | /> 96 | 97 |
98 | ); 99 | }; 100 | 101 | export default TimeSeriesChart; 102 | -------------------------------------------------------------------------------- /src/components/VectorMap/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from "react"; 2 | import ReactMapGL, { 3 | PointerEvent, 4 | Source, 5 | Layer, 6 | WebMercatorViewport, 7 | ViewportProps, 8 | ExtraState, 9 | } from "react-map-gl"; 10 | import { fromJS } from "immutable"; 11 | import { 12 | OsZoomStackLight, 13 | BlankMap, 14 | MapboxStyle, 15 | HydrantStyle, 16 | MainStyle, 17 | FixedHeadStyle, 18 | ValveStyle, 19 | } from "../../mapstyles"; 20 | import { reprojectFeatureCollection } from "../../utils/reproject"; 21 | import { 22 | FeatureCollection, 23 | Feature, 24 | Geometries, 25 | Properties, 26 | } from "@turf/helpers"; 27 | import bbox from "@turf/bbox"; 28 | import bboxPolygon from "@turf/bbox-polygon"; 29 | import length from "@turf/length"; 30 | import buffer from "@turf/buffer"; 31 | import { AttributionControl, Map } from "mapbox-gl"; 32 | import * as GeoJSON from "geojson"; 33 | 34 | type VectorMapProps = { 35 | projectionString: string; 36 | modelGeoJson: FeatureCollection; 37 | onSelectFeature: (value: Feature) => void; 38 | }; 39 | interface VectorMapState { 40 | modelGeoJson?: FeatureCollection; 41 | mapStyle?: any; 42 | viewport: any; 43 | x?: number; 44 | y?: number; 45 | hoveredFeature?: any; 46 | interactiveLayerIds: string[]; 47 | usingOsBaseMap: boolean; 48 | } 49 | 50 | const extractAssetType = ( 51 | geoJson: FeatureCollection, 52 | types: string[] 53 | ): GeoJSON.FeatureCollection => { 54 | // Difference between turf and other GeoJSON is causing issues!! 55 | //@ts-ignore 56 | const features: GeoJSON.Feature< 57 | GeoJSON.Geometry, 58 | GeoJSON.GeoJsonProperties 59 | >[] = geoJson.features.filter( 60 | (feature) => 61 | feature.geometry !== null && 62 | feature.properties !== null && 63 | types.includes(feature.properties.category) 64 | ); 65 | 66 | return { 67 | type: "FeatureCollection", 68 | features, 69 | }; 70 | }; 71 | 72 | function VectorMap({ 73 | projectionString, 74 | modelGeoJson, 75 | onSelectFeature, 76 | }: VectorMapProps) { 77 | const [map, setMap] = useState(); 78 | const [viewport, setViewport] = useState({ 79 | width: 400, 80 | height: 100, 81 | latitude: 0, 82 | longitude: 0, 83 | zoom: 1, 84 | }); 85 | 86 | const geoJson = useMemo(() => { 87 | const model = reprojectFeatureCollection(modelGeoJson, projectionString); 88 | const boundingBox = bbox(model); 89 | const bboxPoly = bboxPolygon(boundingBox); 90 | const bboxPolyBuffer = buffer(bboxPoly, length(bboxPoly) / 4); 91 | const viewportBoundingBox = bbox(bboxPolyBuffer); 92 | 93 | return { 94 | model, 95 | boundingBox, 96 | viewportBoundingBox, 97 | bboxPoly: { 98 | type: "FeatureCollection", 99 | features: [bboxPolygon(boundingBox), bboxPolyBuffer], 100 | }, 101 | }; 102 | }, [modelGeoJson, projectionString]); 103 | 104 | const onViewPortChange = ( 105 | viewState: ViewportProps, 106 | interactionState: ExtraState, 107 | oldViewState: ViewportProps 108 | ) => { 109 | if (projectionString !== "METERS") { 110 | return setViewport(viewState); 111 | } 112 | const [minLng, minLat, maxLng, maxLat] = geoJson.viewportBoundingBox; 113 | const { latitude, longitude } = viewState; 114 | 115 | const newLongitude = 116 | longitude > maxLng ? maxLng : longitude < minLng ? minLng : longitude; 117 | const newLatitude = 118 | latitude > maxLat ? maxLat : latitude < minLat ? minLat : latitude; 119 | 120 | setViewport({ 121 | ...viewState, 122 | longitude: newLongitude, 123 | latitude: newLatitude, 124 | }); 125 | }; 126 | 127 | const geoJsonSources = useMemo(() => { 128 | const { model } = geoJson; 129 | 130 | return { 131 | pipes: extractAssetType(model, ["Pipe", "Valve", "Pump"]), 132 | valves: extractAssetType(model, ["Valve"]), 133 | pumps: extractAssetType(model, ["Pump"]), 134 | junctions: extractAssetType(model, ["Junction"]), 135 | tanks: extractAssetType(model, ["Tank"]), 136 | reserviors: extractAssetType(model, ["Reservior"]), 137 | }; 138 | }, [geoJson]); 139 | 140 | const _addImage = () => { 141 | if (map) { 142 | map.addImage("valve", ValveStyle.toJS().images[0][1]); 143 | map.addImage("triangleSolid", FixedHeadStyle.toJS().images[0][1]); 144 | 145 | // TODO: This is not DRY or where I should be doing this but I 146 | // need the attribution and I have the map here so ill add now 147 | // and split later 148 | const britishNationalGrid = 149 | "+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +datum=OSGB36 +units=m +no_defs"; 150 | const usingOsBaseMap = projectionString === britishNationalGrid; 151 | if (usingOsBaseMap) { 152 | map.addControl( 153 | new AttributionControl({ 154 | customAttribution: 155 | "Contains OS data © Crown copyright and database right 2019", 156 | }) 157 | ); 158 | } 159 | 160 | const geoJson = reprojectFeatureCollection( 161 | modelGeoJson, 162 | projectionString 163 | ); 164 | const jsonbbox = bbox(geoJson); 165 | 166 | const { longitude, latitude, zoom } = new WebMercatorViewport({ 167 | width: 400, 168 | height: 400, 169 | }).fitBounds([ 170 | [jsonbbox[0], jsonbbox[1]], 171 | [jsonbbox[2], jsonbbox[3]], 172 | ]); 173 | setViewport({ 174 | ...viewport, 175 | longitude, 176 | latitude, 177 | zoom, 178 | }); 179 | } 180 | }; 181 | 182 | const style = useMemo(() => { 183 | const britishNationalGrid = 184 | "+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +datum=OSGB36 +units=m +no_defs"; 185 | 186 | // TODO: Clean up!! 187 | const usingOsBaseMap = projectionString === britishNationalGrid; 188 | const baseStyle = usingOsBaseMap 189 | ? OsZoomStackLight 190 | : projectionString === "METERS" 191 | ? BlankMap 192 | : MapboxStyle; 193 | const immutBase = fromJS(baseStyle); //) 194 | const mapStyle = immutBase; 195 | 196 | return mapStyle; 197 | }, [projectionString]); 198 | 199 | const _onClick = (event: PointerEvent) => { 200 | if ( 201 | event && 202 | event.features && 203 | event.features.length > 0 && 204 | event.features[0].properties 205 | ) { 206 | console.log(event.features[0].toJSON()); 207 | onSelectFeature(event.features[0].toJSON()); 208 | } 209 | }; 210 | 211 | return ( 212 | ref && setMap(ref.getMap())} 216 | onViewportChange={onViewPortChange} 217 | onLoad={() => { 218 | _addImage(); 219 | }} 220 | attributionControl={true} 221 | onClick={_onClick} 222 | width="100%" 223 | height="100vh" 224 | maxZoom={24} 225 | interactiveLayerIds={["pipes", "junctions", "valves", "reserviors"]} 226 | clickRadius={2} 227 | > 228 | 229 | 235 | 236 | 237 | 242 | 248 | 249 | 250 | 251 | 257 | 258 | 259 | 264 | 270 | 271 | { 272 | // 278 | // 284 | // 285 | // 286 | // 298 | // 303 | // 304 | } 305 | 306 | ); 307 | } 308 | 309 | export default VectorMap; 310 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './components/App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/interfaces/EpanetGeoJSON.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FeatureCollection, 3 | Feature, 4 | Geometry, 5 | Point, 6 | LineString, 7 | } from "geojson"; 8 | 9 | export default interface EpanetGeoJSON extends FeatureCollection { 10 | features: EpanetFeature[]; 11 | } 12 | 13 | export type EpanetFeature = NodeFeature | LinkFeature; 14 | export type NodeFeature = Junction | Tank | Reservior; 15 | export type LinkFeature = Pipe | Valve | Pump; 16 | 17 | // Two parent types - Nodes and Links 18 | 19 | interface Node extends Feature { 20 | id: number; 21 | properties: NodeProperties; 22 | } 23 | 24 | interface Link extends Feature { 25 | id: number; 26 | properties: LinkProperties; 27 | } 28 | 29 | // Six main feature types 30 | 31 | export interface Junction extends Node { 32 | properties: JunctionProperties; 33 | } 34 | 35 | export interface Tank extends Node { 36 | properties: TankProperties; 37 | } 38 | 39 | export interface Reservior extends Node { 40 | properties: ReservoirProperties; 41 | } 42 | 43 | export interface Pipe extends Link { 44 | properties: PipeProperties; 45 | } 46 | 47 | export interface Valve extends Link { 48 | properties: ValveProperties; 49 | } 50 | 51 | export interface Pump extends Link { 52 | properties: PowerPumpProperties | HeadPumpProperties; 53 | } 54 | 55 | // Nodes type properties 56 | 57 | type NodeCategories = "Junction" | "Tank" | "Reservior"; 58 | 59 | interface NodeProperties { 60 | type: "Node"; 61 | category: NodeCategories; 62 | id: string; 63 | } 64 | 65 | interface JunctionProperties extends NodeProperties { 66 | category: "Junction"; 67 | elevation: number; 68 | demand?: number; 69 | pattern?: string; 70 | } 71 | 72 | interface TankProperties extends NodeProperties { 73 | category: "Tank"; 74 | elevation: number; 75 | initLevel: number; 76 | minLevel: number; 77 | maxLevel: number; 78 | diameter: number; 79 | minVolume: number; 80 | volCurve: string; 81 | overflow?: boolean; 82 | } 83 | 84 | interface ReservoirProperties extends NodeProperties { 85 | category: "Reservior"; 86 | head: number; 87 | pattern?: string; 88 | } 89 | 90 | // Links type properties 91 | 92 | type LinkCategories = "Pipe" | "Valve" | "Pump"; 93 | 94 | interface LinkProperties { 95 | type: "Link"; 96 | category: LinkCategories; 97 | id: string; 98 | usNodeId: string; 99 | dsNodeId: string; 100 | } 101 | 102 | type PipeStatus = "Open" | "Closed" | "CV"; 103 | 104 | interface PipeProperties extends LinkProperties { 105 | category: "Pipe"; 106 | length: number; 107 | diameter: number; 108 | roughness: number; 109 | minorLoss: number; 110 | status?: PipeStatus; 111 | } 112 | 113 | type ValveType = "PRV" | "PSV" | "PBV" | "FCV" | "TCV" | "GPV"; 114 | 115 | interface ValveProperties extends LinkProperties { 116 | category: "Valve"; 117 | diameter: number; 118 | valveType: ValveType; 119 | setting: number; 120 | minorLoss: number; 121 | } 122 | 123 | type PumpMode = "Power" | "Head"; 124 | 125 | interface PumpProperties extends LinkProperties { 126 | category: "Pump"; 127 | mode: PumpMode; 128 | speed?: number; 129 | pattern?: string; 130 | } 131 | 132 | interface PowerPumpProperties extends PumpProperties { 133 | mode: "Power"; 134 | power: number; 135 | } 136 | 137 | interface HeadPumpProperties extends PumpProperties { 138 | mode: "Head"; 139 | head: string; 140 | } 141 | -------------------------------------------------------------------------------- /src/interfaces/ModelFeatureCollection.ts: -------------------------------------------------------------------------------- 1 | 2 | import { FeatureCollection, Geometries, Properties } from '@turf/helpers'; 3 | 4 | export default interface ModelFeatureCollection extends FeatureCollection { 5 | model: { 6 | "timesteps": string[], 7 | [name: string]: any; 8 | } 9 | } -------------------------------------------------------------------------------- /src/mapstyles/base/blank.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Blank", 4 | "metadata": { 5 | "mapbox:autocomposite": true, 6 | "mapbox:type": "template", 7 | "mapbox:sdk-support": { 8 | "js": "0.50.0", 9 | "android": "6.7.0", 10 | "ios": "4.6.0" 11 | } 12 | }, 13 | "center": [-1.464858786792547, 50.939150779110975], 14 | "zoom": 13.12365211904204, 15 | "bearing": -0.44200633613297663, 16 | "pitch": 0, 17 | "light": { 18 | "intensity": 0.25, 19 | "color": "hsl(0, 0%, 100%)" 20 | }, 21 | "sprite": "mapbox://sprites/mapbox/basic-v8", 22 | "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", 23 | "sources": {}, 24 | "layers": [ 25 | { 26 | "id": "background", 27 | "type": "background", 28 | "paint": { 29 | "background-color": "#e6e5e1" 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/mapstyles/base/map-style-basic-v8.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Basic", 4 | "metadata": { 5 | "mapbox:autocomposite": true 6 | }, 7 | "sources": { 8 | "mapbox": { 9 | "url": "mapbox://mapbox.mapbox-streets-v7", 10 | "type": "vector" 11 | } 12 | }, 13 | "sprite": "mapbox://sprites/mapbox/basic-v8", 14 | "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", 15 | "layers": [ 16 | { 17 | "id": "background", 18 | "type": "background", 19 | "paint": { 20 | "background-color": "#dedede" 21 | }, 22 | "interactive": true 23 | }, 24 | { 25 | "id": "landuse_overlay_national_park", 26 | "type": "fill", 27 | "source": "mapbox", 28 | "source-layer": "landuse_overlay", 29 | "filter": [ 30 | "==", 31 | "class", 32 | "national_park" 33 | ], 34 | "paint": { 35 | "fill-color": "#d2edae", 36 | "fill-opacity": 0.75 37 | }, 38 | "interactive": true 39 | }, 40 | { 41 | "id": "landuse_park", 42 | "type": "fill", 43 | "source": "mapbox", 44 | "source-layer": "landuse", 45 | "filter": [ 46 | "==", 47 | "class", 48 | "park" 49 | ], 50 | "paint": { 51 | "fill-color": "#d2edae" 52 | }, 53 | "interactive": true 54 | }, 55 | { 56 | "id": "waterway", 57 | "type": "line", 58 | "source": "mapbox", 59 | "source-layer": "waterway", 60 | "filter": [ 61 | "all", 62 | [ 63 | "==", 64 | "$type", 65 | "LineString" 66 | ], 67 | [ 68 | "in", 69 | "class", 70 | "river", 71 | "canal" 72 | ] 73 | ], 74 | "paint": { 75 | "line-color": "#a0cfdf", 76 | "line-width": { 77 | "base": 1.4, 78 | "stops": [ 79 | [ 80 | 8, 81 | 0.5 82 | ], 83 | [ 84 | 20, 85 | 15 86 | ] 87 | ] 88 | } 89 | }, 90 | "interactive": true 91 | }, 92 | { 93 | "id": "water", 94 | "type": "fill", 95 | "source": "mapbox", 96 | "source-layer": "water", 97 | "paint": { 98 | "fill-color": "#a0cfdf" 99 | }, 100 | "interactive": true 101 | }, 102 | { 103 | "id": "building", 104 | "type": "fill", 105 | "source": "mapbox", 106 | "source-layer": "building", 107 | "paint": { 108 | "fill-color": "#d6d6d6" 109 | }, 110 | "interactive": true 111 | }, 112 | { 113 | "interactive": true, 114 | "layout": { 115 | "line-cap": "butt", 116 | "line-join": "miter" 117 | }, 118 | "filter": [ 119 | "all", 120 | [ 121 | "==", 122 | "$type", 123 | "LineString" 124 | ], 125 | [ 126 | "all", 127 | [ 128 | "in", 129 | "class", 130 | "motorway_link", 131 | "street", 132 | "street_limited", 133 | "service", 134 | "track", 135 | "pedestrian", 136 | "path", 137 | "link" 138 | ], 139 | [ 140 | "==", 141 | "structure", 142 | "tunnel" 143 | ] 144 | ] 145 | ], 146 | "type": "line", 147 | "source": "mapbox", 148 | "id": "tunnel_minor", 149 | "paint": { 150 | "line-color": "#efefef", 151 | "line-width": { 152 | "base": 1.55, 153 | "stops": [ 154 | [ 155 | 4, 156 | 0.25 157 | ], 158 | [ 159 | 20, 160 | 30 161 | ] 162 | ] 163 | }, 164 | "line-dasharray": [ 165 | 0.36, 166 | 0.18 167 | ] 168 | }, 169 | "source-layer": "road" 170 | }, 171 | { 172 | "interactive": true, 173 | "layout": { 174 | "line-cap": "butt", 175 | "line-join": "miter" 176 | }, 177 | "filter": [ 178 | "all", 179 | [ 180 | "==", 181 | "$type", 182 | "LineString" 183 | ], 184 | [ 185 | "all", 186 | [ 187 | "in", 188 | "class", 189 | "motorway", 190 | "primary", 191 | "secondary", 192 | "tertiary", 193 | "trunk" 194 | ], 195 | [ 196 | "==", 197 | "structure", 198 | "tunnel" 199 | ] 200 | ] 201 | ], 202 | "type": "line", 203 | "source": "mapbox", 204 | "id": "tunnel_major", 205 | "paint": { 206 | "line-color": "#fff", 207 | "line-width": { 208 | "base": 1.4, 209 | "stops": [ 210 | [ 211 | 6, 212 | 0.5 213 | ], 214 | [ 215 | 20, 216 | 30 217 | ] 218 | ] 219 | }, 220 | "line-dasharray": [ 221 | 0.28, 222 | 0.14 223 | ] 224 | }, 225 | "source-layer": "road" 226 | }, 227 | { 228 | "interactive": true, 229 | "layout": { 230 | "line-cap": "round", 231 | "line-join": "round" 232 | }, 233 | "filter": [ 234 | "all", 235 | [ 236 | "==", 237 | "$type", 238 | "LineString" 239 | ], 240 | [ 241 | "all", 242 | [ 243 | "in", 244 | "class", 245 | "motorway_link", 246 | "street", 247 | "street_limited", 248 | "service", 249 | "track", 250 | "pedestrian", 251 | "path", 252 | "link" 253 | ], 254 | [ 255 | "in", 256 | "structure", 257 | "none", 258 | "ford" 259 | ] 260 | ] 261 | ], 262 | "type": "line", 263 | "source": "mapbox", 264 | "id": "road_minor", 265 | "paint": { 266 | "line-color": "#efefef", 267 | "line-width": { 268 | "base": 1.55, 269 | "stops": [ 270 | [ 271 | 4, 272 | 0.25 273 | ], 274 | [ 275 | 20, 276 | 30 277 | ] 278 | ] 279 | } 280 | }, 281 | "source-layer": "road" 282 | }, 283 | { 284 | "interactive": true, 285 | "layout": { 286 | "line-cap": "round", 287 | "line-join": "round" 288 | }, 289 | "filter": [ 290 | "all", 291 | [ 292 | "==", 293 | "$type", 294 | "LineString" 295 | ], 296 | [ 297 | "all", 298 | [ 299 | "in", 300 | "class", 301 | "motorway", 302 | "primary", 303 | "secondary", 304 | "tertiary", 305 | "trunk" 306 | ], 307 | [ 308 | "in", 309 | "structure", 310 | "none", 311 | "ford" 312 | ] 313 | ] 314 | ], 315 | "type": "line", 316 | "source": "mapbox", 317 | "id": "road_major", 318 | "paint": { 319 | "line-color": "#fff", 320 | "line-width": { 321 | "base": 1.4, 322 | "stops": [ 323 | [ 324 | 6, 325 | 0.5 326 | ], 327 | [ 328 | 20, 329 | 30 330 | ] 331 | ] 332 | } 333 | }, 334 | "source-layer": "road" 335 | }, 336 | { 337 | "interactive": true, 338 | "layout": { 339 | "line-cap": "butt", 340 | "line-join": "miter" 341 | }, 342 | "filter": [ 343 | "all", 344 | [ 345 | "==", 346 | "$type", 347 | "LineString" 348 | ], 349 | [ 350 | "all", 351 | [ 352 | "in", 353 | "class", 354 | "motorway_link", 355 | "street", 356 | "street_limited", 357 | "service", 358 | "track", 359 | "pedestrian", 360 | "path", 361 | "link" 362 | ], 363 | [ 364 | "==", 365 | "structure", 366 | "bridge" 367 | ] 368 | ] 369 | ], 370 | "type": "line", 371 | "source": "mapbox", 372 | "id": "bridge_minor case", 373 | "paint": { 374 | "line-color": "#dedede", 375 | "line-width": { 376 | "base": 1.6, 377 | "stops": [ 378 | [ 379 | 12, 380 | 0.5 381 | ], 382 | [ 383 | 20, 384 | 10 385 | ] 386 | ] 387 | }, 388 | "line-gap-width": { 389 | "base": 1.55, 390 | "stops": [ 391 | [ 392 | 4, 393 | 0.25 394 | ], 395 | [ 396 | 20, 397 | 30 398 | ] 399 | ] 400 | } 401 | }, 402 | "source-layer": "road" 403 | }, 404 | { 405 | "interactive": true, 406 | "layout": { 407 | "line-cap": "butt", 408 | "line-join": "miter" 409 | }, 410 | "filter": [ 411 | "all", 412 | [ 413 | "==", 414 | "$type", 415 | "LineString" 416 | ], 417 | [ 418 | "all", 419 | [ 420 | "in", 421 | "class", 422 | "motorway", 423 | "primary", 424 | "secondary", 425 | "tertiary", 426 | "trunk" 427 | ], 428 | [ 429 | "==", 430 | "structure", 431 | "bridge" 432 | ] 433 | ] 434 | ], 435 | "type": "line", 436 | "source": "mapbox", 437 | "id": "bridge_major case", 438 | "paint": { 439 | "line-color": "#dedede", 440 | "line-width": { 441 | "base": 1.6, 442 | "stops": [ 443 | [ 444 | 12, 445 | 0.5 446 | ], 447 | [ 448 | 20, 449 | 10 450 | ] 451 | ] 452 | }, 453 | "line-gap-width": { 454 | "base": 1.55, 455 | "stops": [ 456 | [ 457 | 4, 458 | 0.25 459 | ], 460 | [ 461 | 20, 462 | 30 463 | ] 464 | ] 465 | } 466 | }, 467 | "source-layer": "road" 468 | }, 469 | { 470 | "interactive": true, 471 | "layout": { 472 | "line-cap": "round", 473 | "line-join": "round" 474 | }, 475 | "filter": [ 476 | "all", 477 | [ 478 | "==", 479 | "$type", 480 | "LineString" 481 | ], 482 | [ 483 | "all", 484 | [ 485 | "in", 486 | "class", 487 | "motorway_link", 488 | "street", 489 | "street_limited", 490 | "service", 491 | "track", 492 | "pedestrian", 493 | "path", 494 | "link" 495 | ], 496 | [ 497 | "==", 498 | "structure", 499 | "bridge" 500 | ] 501 | ] 502 | ], 503 | "type": "line", 504 | "source": "mapbox", 505 | "id": "bridge_minor", 506 | "paint": { 507 | "line-color": "#efefef", 508 | "line-width": { 509 | "base": 1.55, 510 | "stops": [ 511 | [ 512 | 4, 513 | 0.25 514 | ], 515 | [ 516 | 20, 517 | 30 518 | ] 519 | ] 520 | } 521 | }, 522 | "source-layer": "road" 523 | }, 524 | { 525 | "interactive": true, 526 | "layout": { 527 | "line-cap": "round", 528 | "line-join": "round" 529 | }, 530 | "filter": [ 531 | "all", 532 | [ 533 | "==", 534 | "$type", 535 | "LineString" 536 | ], 537 | [ 538 | "all", 539 | [ 540 | "in", 541 | "class", 542 | "motorway", 543 | "primary", 544 | "secondary", 545 | "tertiary", 546 | "trunk" 547 | ], 548 | [ 549 | "==", 550 | "structure", 551 | "bridge" 552 | ] 553 | ] 554 | ], 555 | "type": "line", 556 | "source": "mapbox", 557 | "id": "bridge_major", 558 | "paint": { 559 | "line-color": "#fff", 560 | "line-width": { 561 | "base": 1.4, 562 | "stops": [ 563 | [ 564 | 6, 565 | 0.5 566 | ], 567 | [ 568 | 20, 569 | 30 570 | ] 571 | ] 572 | } 573 | }, 574 | "source-layer": "road" 575 | }, 576 | { 577 | "interactive": true, 578 | "layout": { 579 | "line-cap": "round", 580 | "line-join": "round" 581 | }, 582 | "filter": [ 583 | "all", 584 | [ 585 | "==", 586 | "$type", 587 | "LineString" 588 | ], 589 | [ 590 | "all", 591 | [ 592 | "<=", 593 | "admin_level", 594 | 2 595 | ], 596 | [ 597 | "==", 598 | "maritime", 599 | 0 600 | ] 601 | ] 602 | ], 603 | "type": "line", 604 | "source": "mapbox", 605 | "id": "admin_country", 606 | "paint": { 607 | "line-color": "#8b8a8a", 608 | "line-width": { 609 | "base": 1.3, 610 | "stops": [ 611 | [ 612 | 3, 613 | 0.5 614 | ], 615 | [ 616 | 22, 617 | 15 618 | ] 619 | ] 620 | } 621 | }, 622 | "source-layer": "admin" 623 | }, 624 | { 625 | "interactive": true, 626 | "minzoom": 5, 627 | "layout": { 628 | "icon-image": "{maki}-11", 629 | "text-offset": [ 630 | 0, 631 | 0.5 632 | ], 633 | "text-field": "{name_en}", 634 | "text-font": [ 635 | "Open Sans Semibold", 636 | "Arial Unicode MS Bold" 637 | ], 638 | "text-max-width": 8, 639 | "text-anchor": "top", 640 | "text-size": 11, 641 | "icon-size": 1 642 | }, 643 | "filter": [ 644 | "all", 645 | [ 646 | "==", 647 | "$type", 648 | "Point" 649 | ], 650 | [ 651 | "all", 652 | [ 653 | "==", 654 | "scalerank", 655 | 1 656 | ], 657 | [ 658 | "==", 659 | "localrank", 660 | 1 661 | ] 662 | ] 663 | ], 664 | "type": "symbol", 665 | "source": "mapbox", 666 | "id": "poi_label", 667 | "paint": { 668 | "text-color": "#666", 669 | "text-halo-width": 1, 670 | "text-halo-color": "rgba(255,255,255,0.75)", 671 | "text-halo-blur": 1 672 | }, 673 | "source-layer": "poi_label" 674 | }, 675 | { 676 | "interactive": true, 677 | "layout": { 678 | "symbol-placement": "line", 679 | "text-field": "{name_en}", 680 | "text-font": [ 681 | "Open Sans Semibold", 682 | "Arial Unicode MS Bold" 683 | ], 684 | "text-transform": "uppercase", 685 | "text-letter-spacing": 0.1, 686 | "text-size": { 687 | "base": 1.4, 688 | "stops": [ 689 | [ 690 | 10, 691 | 8 692 | ], 693 | [ 694 | 20, 695 | 14 696 | ] 697 | ] 698 | } 699 | }, 700 | "filter": [ 701 | "all", 702 | [ 703 | "==", 704 | "$type", 705 | "LineString" 706 | ], 707 | [ 708 | "in", 709 | "class", 710 | "motorway", 711 | "primary", 712 | "secondary", 713 | "tertiary", 714 | "trunk" 715 | ] 716 | ], 717 | "type": "symbol", 718 | "source": "mapbox", 719 | "id": "road_major_label", 720 | "paint": { 721 | "text-color": "#666", 722 | "text-halo-color": "rgba(255,255,255,0.75)", 723 | "text-halo-width": 2 724 | }, 725 | "source-layer": "road_label" 726 | }, 727 | { 728 | "interactive": true, 729 | "minzoom": 8, 730 | "layout": { 731 | "text-field": "{name_en}", 732 | "text-font": [ 733 | "Open Sans Semibold", 734 | "Arial Unicode MS Bold" 735 | ], 736 | "text-max-width": 6, 737 | "text-size": { 738 | "stops": [ 739 | [ 740 | 6, 741 | 12 742 | ], 743 | [ 744 | 12, 745 | 16 746 | ] 747 | ] 748 | } 749 | }, 750 | "filter": [ 751 | "all", 752 | [ 753 | "==", 754 | "$type", 755 | "Point" 756 | ], 757 | [ 758 | "in", 759 | "type", 760 | "town", 761 | "village", 762 | "hamlet", 763 | "suburb", 764 | "neighbourhood", 765 | "island" 766 | ] 767 | ], 768 | "type": "symbol", 769 | "source": "mapbox", 770 | "id": "place_label_other", 771 | "paint": { 772 | "text-color": "#666", 773 | "text-halo-color": "rgba(255,255,255,0.75)", 774 | "text-halo-width": 1, 775 | "text-halo-blur": 1 776 | }, 777 | "source-layer": "place_label" 778 | }, 779 | { 780 | "interactive": true, 781 | "layout": { 782 | "text-field": "{name_en}", 783 | "text-font": [ 784 | "Open Sans Bold", 785 | "Arial Unicode MS Bold" 786 | ], 787 | "text-max-width": 10, 788 | "text-size": { 789 | "stops": [ 790 | [ 791 | 3, 792 | 12 793 | ], 794 | [ 795 | 8, 796 | 16 797 | ] 798 | ] 799 | } 800 | }, 801 | "maxzoom": 16, 802 | "filter": [ 803 | "all", 804 | [ 805 | "==", 806 | "$type", 807 | "Point" 808 | ], 809 | [ 810 | "==", 811 | "type", 812 | "city" 813 | ] 814 | ], 815 | "type": "symbol", 816 | "source": "mapbox", 817 | "id": "place_label_city", 818 | "paint": { 819 | "text-color": "#666", 820 | "text-halo-color": "rgba(255,255,255,0.75)", 821 | "text-halo-width": 1, 822 | "text-halo-blur": 1 823 | }, 824 | "source-layer": "place_label" 825 | }, 826 | { 827 | "interactive": true, 828 | "layout": { 829 | "text-field": "{name_en}", 830 | "text-font": [ 831 | "Open Sans Regular", 832 | "Arial Unicode MS Regular" 833 | ], 834 | "text-max-width": 10, 835 | "text-size": { 836 | "stops": [ 837 | [ 838 | 3, 839 | 14 840 | ], 841 | [ 842 | 8, 843 | 22 844 | ] 845 | ] 846 | } 847 | }, 848 | "maxzoom": 12, 849 | "filter": [ 850 | "==", 851 | "$type", 852 | "Point" 853 | ], 854 | "type": "symbol", 855 | "source": "mapbox", 856 | "id": "country_label", 857 | "paint": { 858 | "text-color": "#666", 859 | "text-halo-color": "rgba(255,255,255,0.75)", 860 | "text-halo-width": 1, 861 | "text-halo-blur": 1 862 | }, 863 | "source-layer": "country_label" 864 | } 865 | ] 866 | } -------------------------------------------------------------------------------- /src/mapstyles/index.ts: -------------------------------------------------------------------------------- 1 | import OsZoomStackLight from "./base/open-zoom-stack-light.json"; 2 | import MapboxStyle from "./base/style.json"; 3 | import BlankMap from "./base/blank.json"; 4 | import HydrantStyle from "./water/hydrant"; 5 | import MainStyle from "./water/main"; 6 | import MeterStyle from "./water/meter"; 7 | import ValveStyle from "./water/valve"; 8 | import FixedHeadStyle from "./water/fixedhead"; 9 | import TankStyle from "./water/tank"; 10 | 11 | export { 12 | BlankMap, 13 | OsZoomStackLight, 14 | HydrantStyle, 15 | MainStyle, 16 | MeterStyle, 17 | ValveStyle, 18 | FixedHeadStyle, 19 | TankStyle, 20 | MapboxStyle, 21 | }; 22 | -------------------------------------------------------------------------------- /src/mapstyles/water/fixedhead.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from "immutable"; 2 | import WaterIcons from "./waterIcons"; 3 | 4 | const layout = { 5 | visibility: "visible", 6 | "icon-image": "triangleSolid", 7 | "icon-size": ["interpolate", ["exponential", 2], ["zoom"], 14, 0.5, 22, 2], 8 | "text-field": "{id}", 9 | //'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'], 10 | "text-offset": [0.4, 0], 11 | "text-anchor": "left", 12 | //"text-max-width": 3, 13 | "text-size": { 14 | base: 2, 15 | stops: [ 16 | [8, 8], 17 | [12, 8], 18 | [12, 8], 19 | [13, 12], 20 | ], 21 | }, 22 | "text-rotate": 0, 23 | "icon-allow-overlap": true, 24 | "text-allow-overlap": true, 25 | "text-ignore-placement": false, 26 | "icon-ignore-placement": true, 27 | }; 28 | 29 | const paint = { 30 | "text-color": "black", 31 | "text-halo-color": "white", 32 | "text-halo-width": 2, 33 | }; 34 | 35 | const icons = { 36 | triangleSolid: WaterIcons.triangleSolid, 37 | }; 38 | 39 | const images = []; 40 | for (const key in icons) { 41 | const iconImage = new Image(); 42 | iconImage.src = 43 | "data:image/svg+xml;charset=utf-8;base64," + btoa(WaterIcons.triangleSolid); 44 | images.push([key, iconImage]); 45 | } 46 | 47 | const FixedHeadStyle = fromJS({ 48 | id: "fixedhead-geojson", 49 | source: "fixedhead", 50 | type: "symbol", 51 | images, 52 | paint, 53 | layout, 54 | minZoom: 1, 55 | }); 56 | 57 | export default FixedHeadStyle; 58 | -------------------------------------------------------------------------------- /src/mapstyles/water/hydrant.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from "immutable"; 2 | 3 | const layout = { visibility: "visible" }; 4 | const paint = { 5 | "circle-opacity": { 6 | stops: [ 7 | [15, 0], 8 | [16, 1], 9 | ], 10 | }, 11 | "circle-stroke-opacity": { 12 | stops: [ 13 | [15, 0], 14 | [16, 1], 15 | ], 16 | }, 17 | 18 | "circle-color": [ 19 | "case", 20 | ["==", ["get", "operational"], "Abandoned"], 21 | "#33d935", 22 | ["==", ["get", "type"], "Fire"], 23 | "#b300ff", 24 | ["==", ["get", "type"], "Washout"], 25 | "#fff", 26 | /* other */ "#7986cb", 27 | ], 28 | "circle-radius": { 29 | base: 1, 30 | stops: [ 31 | [17, 1], 32 | [22, 5], 33 | ], 34 | }, 35 | "circle-stroke-color": [ 36 | "case", 37 | ["==", ["get", "operational"], "Abandoned"], 38 | "#33d935", 39 | 40 | /* other */ "#5c6bc0", 41 | ], 42 | "circle-stroke-width": { 43 | base: 0.5, 44 | stops: [ 45 | [15, 1.25], 46 | [22, 4], 47 | ], 48 | }, 49 | }; 50 | 51 | const HydrantStyle = fromJS({ 52 | id: "hydrants-geojson", 53 | source: "hydrants", 54 | type: "circle", 55 | paint, 56 | layout, 57 | }); 58 | 59 | export default HydrantStyle; 60 | -------------------------------------------------------------------------------- /src/mapstyles/water/main.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | const layout = { visibility: 'visible' }; 4 | 5 | const paint = { 6 | 'line-color': [ 7 | 'case', 8 | ["==", ['get', 'operationa'], 'Abandoned'], '#7af500', 9 | ["==", ['get', 'operationa'], 'Removed'], '#7af500', 10 | ["==", ['get', 'operationa'], 'Isolated'], '#5e9294', 11 | ["==", ['get', 'operationa'], 'Proposed'], '#ff7f00', 12 | ["==", ['get', 'type'], 'Fire'], '#00ffff', 13 | ["==", ['get', 'type'], 'Distributi'], '#1528f7', 14 | ["==", ['get', 'type'], 'Trunk'], '#e31a1c', 15 | /* other */ '#1528f7' 16 | ], 17 | 'line-width': 2 18 | }; 19 | 20 | 21 | const MainStyle = fromJS({ 22 | id: 'main-geojson', 23 | source: 'mains', 24 | type: 'line', 25 | paint, 26 | layout 27 | }); 28 | 29 | export default MainStyle 30 | -------------------------------------------------------------------------------- /src/mapstyles/water/meter.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import WaterIcons from './waterIcons' 3 | 4 | 5 | const layout = { 6 | 'visibility': 'visible', 7 | 'symbol-placement': 'line-center', 8 | 'icon-image': 'meter', 9 | 'icon-size': { 10 | 'base': 1.75, 11 | 'stops': [[10, 0.4], [22, 1]] 12 | }, 13 | 'icon-allow-overlap': true, 14 | 'icon-ignore-placement': true 15 | }; 16 | 17 | 18 | const icons = { 19 | 'meter': WaterIcons.meter, 20 | }; 21 | 22 | const images = []; 23 | for (const key in icons) { 24 | const iconImage = new Image(); 25 | iconImage.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(WaterIcons.meter); 26 | images.push([key, iconImage]) 27 | } 28 | 29 | 30 | const MeterStyle = fromJS({ 31 | id: 'meter-geojson', 32 | source: 'meters', 33 | type: 'symbol', 34 | images, 35 | layout, 36 | minZoom: 1 37 | }); 38 | 39 | export default MeterStyle 40 | -------------------------------------------------------------------------------- /src/mapstyles/water/tank.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from "immutable"; 2 | import WaterIcons from "./waterIcons"; 3 | 4 | const layout = { 5 | visibility: "visible", 6 | "icon-image": "squareSolid", 7 | "icon-size": ["interpolate", ["exponential", 2], ["zoom"], 14, 0.5, 22, 2], 8 | "text-field": "{id}", 9 | //'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'], 10 | "text-offset": [0.4, 0], 11 | "text-anchor": "left", 12 | //"text-max-width": 3, 13 | "text-size": { 14 | base: 2, 15 | stops: [ 16 | [8, 8], 17 | [12, 8], 18 | [12, 8], 19 | [13, 12], 20 | ], 21 | }, 22 | "text-rotate": 0, 23 | "icon-allow-overlap": true, 24 | "text-allow-overlap": true, 25 | "text-ignore-placement": false, 26 | "icon-ignore-placement": true, 27 | }; 28 | 29 | const paint = { 30 | "text-color": "black", 31 | "text-halo-color": "white", 32 | "text-halo-width": 2, 33 | }; 34 | 35 | const icons = { 36 | squareSolid: WaterIcons.squareSolid, 37 | }; 38 | 39 | const images = []; 40 | for (const key in icons) { 41 | const iconImage = new Image(); 42 | iconImage.src = 43 | "data:image/svg+xml;charset=utf-8;base64," + btoa(WaterIcons.squareSolid); 44 | images.push([key, iconImage]); 45 | } 46 | 47 | const TankStyle = fromJS({ 48 | id: "tank-geojson", 49 | source: "tank", 50 | type: "symbol", 51 | images, 52 | paint, 53 | layout, 54 | minZoom: 1, 55 | }); 56 | 57 | export default TankStyle; 58 | -------------------------------------------------------------------------------- /src/mapstyles/water/valve.ts: -------------------------------------------------------------------------------- 1 | import WaterIcons from "./waterIcons"; 2 | import { fromJS } from "immutable"; 3 | 4 | const layout = { 5 | visibility: "visible", 6 | "symbol-placement": "line-center", 7 | "icon-image": "valve", 8 | "icon-size": { 9 | base: 1.75, 10 | stops: [[10, 0.4], [22, 1]] 11 | }, 12 | "icon-rotate": ["*", ["get", "geom_orien"], -1], 13 | "text-field": "{description}", 14 | "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"], 15 | "text-offset": [0, 0.6], 16 | "text-anchor": "top", 17 | "text-size": 8, 18 | "icon-allow-overlap": true, 19 | "icon-ignore-placement": true 20 | }; 21 | 22 | //const paint = { 23 | // "text-color": "black", 24 | // "text-halo-color": "white", 25 | // "text-halo-width": 2 26 | //}; 27 | 28 | const icons = { 29 | defaultValve: WaterIcons.defaultValve("#b300ff"), 30 | sensitiveValve: WaterIcons.defaultValve("#ff7f00"), 31 | washoutValve: WaterIcons.washoutValve, 32 | closedValve: WaterIcons.defaultClosedValve, 33 | closedValvePCCPRAPSA: WaterIcons.closedValve("#FFF"), 34 | closedValveDMA: WaterIcons.closedValve("#33a02c"), 35 | closedValveWSZ: WaterIcons.closedValve("#ff7f00"), 36 | closedValveWOA: WaterIcons.closedValve("#e31a1c"), 37 | pressureReducing: WaterIcons.pressureReducing, 38 | pressureRelief: WaterIcons.pressureRelief, 39 | pressureSustaining: WaterIcons.pressureSustaining, 40 | refluxValve: WaterIcons.refluxValve 41 | }; 42 | 43 | let images = []; 44 | for (const key in icons) { 45 | const iconImage = new Image(); 46 | iconImage.src = 47 | "data:image/svg+xml;charset=utf-8;base64," + 48 | btoa(WaterIcons.defaultValve("#b300ff")); 49 | images.push([key, iconImage]); 50 | } 51 | 52 | const ValveStyle = fromJS({ 53 | id: "valve-geojson", 54 | source: "valves", 55 | type: "symbol", 56 | images, 57 | layout, 58 | minZoom: 1 59 | }); 60 | 61 | export default ValveStyle; 62 | -------------------------------------------------------------------------------- /src/mapstyles/water/waterIcons.ts: -------------------------------------------------------------------------------- 1 | const squareSolid = 2 | ''; 3 | const squareCross = 4 | ''; 5 | const triangleSolid = 6 | ''; 7 | const closedValve = (color: String) => { 8 | return ( 9 | '' 12 | ); 13 | }; 14 | 15 | const defaultClosedValve = 16 | ''; 17 | 18 | const defaultValve = (color: String) => { 19 | return ( 20 | '' 23 | ); 24 | }; 25 | const washoutValve = 26 | ''; 27 | 28 | const pressureReducing = 29 | ''; 30 | const pressureRelief = 31 | ''; 32 | const pressureSustaining = 33 | ''; 34 | const refluxValve = 35 | ''; 36 | 37 | const meter = 38 | ''; 39 | 40 | const mapMarker = 41 | ''; 42 | 43 | const WaterIcons = { 44 | squareSolid, 45 | squareCross, 46 | triangleSolid, 47 | closedValve, 48 | defaultClosedValve, 49 | pressureReducing, 50 | pressureRelief, 51 | pressureSustaining, 52 | refluxValve, 53 | defaultValve, 54 | washoutValve, 55 | meter, 56 | mapMarker, 57 | }; 58 | 59 | export default WaterIcons; 60 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/utils/EpanetBinary/index.ts: -------------------------------------------------------------------------------- 1 | enum NodeResultTypes { 2 | Demand, 3 | Head, 4 | Pressure, 5 | WaterQuality 6 | } 7 | 8 | enum LinkResultTypes { 9 | Flow, 10 | Velcoity, 11 | Headloss, 12 | AvgWaterQuality, 13 | Status, 14 | Setting, 15 | ReactionRate, 16 | Friction 17 | } 18 | 19 | export interface LinkResults { 20 | flow: number[]; 21 | velcoity: number[]; 22 | headloss: number[]; 23 | avgWaterQuality: number[]; 24 | status: number[]; 25 | setting: number[]; 26 | reactionRate: number[]; 27 | friction: number[]; 28 | } 29 | 30 | export interface NodeResults { 31 | demand: number[]; 32 | head: number[]; 33 | pressure: number[]; 34 | waterQuality: number[]; 35 | } 36 | 37 | export interface EpanetProlog { 38 | nodeCount: number; 39 | resAndTankCount: number; 40 | linkCount: number; 41 | pumpCount: number; 42 | valveCount: number; 43 | reportingPeriods: number; 44 | } 45 | 46 | export interface EpanetResults { 47 | prolog: EpanetProlog; 48 | results: { 49 | nodes: NodeResults[]; 50 | links: LinkResults[]; 51 | }; 52 | } 53 | 54 | export function readBinary(results: Uint8Array): EpanetResults { 55 | const view1 = new DataView(results.buffer); 56 | const prolog: EpanetProlog = { 57 | nodeCount: view1.getInt32(8, true), 58 | resAndTankCount: view1.getInt32(12, true), 59 | linkCount: view1.getInt32(16, true), 60 | pumpCount: view1.getInt32(20, true), 61 | valveCount: view1.getInt32(24, true), 62 | reportingPeriods: view1.getInt32(results.byteLength - 12, true) 63 | }; 64 | 65 | const offsetResults = 66 | 884 + 67 | 36 * prolog.nodeCount + 68 | 52 * prolog.linkCount + 69 | 8 * prolog.resAndTankCount + 70 | 28 * prolog.pumpCount + 71 | 4; 72 | 73 | const nodes: NodeResults[] = [...Array(prolog.nodeCount)].map((_, i) => { 74 | return getNodeResults(prolog, offsetResults, i, view1); 75 | }); 76 | const links: LinkResults[] = [...Array(prolog.linkCount)].map((_, i) => { 77 | return getLinkResults(prolog, offsetResults, i, view1); 78 | }); 79 | 80 | const data: EpanetResults = { 81 | prolog, 82 | results: { 83 | nodes, 84 | links 85 | } 86 | }; 87 | return data; 88 | } 89 | 90 | const getNodeResults = ( 91 | prolog: EpanetProlog, 92 | offsetResults: number, 93 | nodeIndex: number, 94 | dataView: DataView 95 | ): NodeResults => { 96 | const nodeResults = { 97 | demand: [], 98 | head: [], 99 | pressure: [], 100 | waterQuality: [] 101 | }; 102 | 103 | const result: NodeResults = [ 104 | "demand", 105 | "head", 106 | "pressure", 107 | "waterQuality" 108 | ].reduce((map, obj, i) => { 109 | return { 110 | ...map, 111 | [obj]: getResultByteOffSet(prolog, offsetResults, true, nodeIndex, i).map( 112 | x => dataView.getFloat32(x, true) 113 | ) 114 | }; 115 | }, nodeResults); 116 | 117 | return result; 118 | }; 119 | 120 | const getLinkResults = ( 121 | prolog: EpanetProlog, 122 | offsetResults: number, 123 | linkIndex: number, 124 | dataView: DataView 125 | ): LinkResults => { 126 | const linkResults = { 127 | flow: [], 128 | velcoity: [], 129 | headloss: [], 130 | avgWaterQuality: [], 131 | status: [], 132 | setting: [], 133 | reactionRate: [], 134 | friction: [] 135 | }; 136 | 137 | const result: LinkResults = [ 138 | "flow", 139 | "velcoity", 140 | "headloss", 141 | "avgWaterQuality", 142 | "status", 143 | "setting", 144 | "reactionRate", 145 | "friction" 146 | ].reduce((map, obj, i) => { 147 | return { 148 | ...map, 149 | [obj]: getResultByteOffSet( 150 | prolog, 151 | offsetResults, 152 | false, 153 | linkIndex, 154 | i 155 | ).map(x => dataView.getFloat32(x, true)) 156 | }; 157 | }, linkResults); 158 | 159 | return result; 160 | }; 161 | 162 | const getResultByteOffSet = ( 163 | prolog: EpanetProlog, 164 | offsetResults: number, 165 | isNode: boolean, 166 | objIndex: number, 167 | resultType: NodeResultTypes | LinkResultTypes 168 | ): number[] => { 169 | const linkResultOffset = isNode ? 0 : 16 * prolog.nodeCount; 170 | const typeCount = isNode ? prolog.nodeCount : prolog.linkCount; 171 | const resultSize = 16 * prolog.nodeCount + 32 * prolog.linkCount; 172 | const answer = [...Array(prolog.reportingPeriods)].map( 173 | (_, i) => 174 | offsetResults + 175 | resultSize * i + 176 | linkResultOffset + 177 | 4 * objIndex + 178 | 4 * resultType * typeCount 179 | ); 180 | return answer; 181 | }; 182 | -------------------------------------------------------------------------------- /src/utils/EpanetToGeojson/index.ts: -------------------------------------------------------------------------------- 1 | import { featureCollection, feature, Feature } from "@turf/helpers"; 2 | import { EpanetResults } from "../EpanetBinary"; 3 | 4 | import EpanetGeoJSON, { 5 | NodeFeature, 6 | LinkFeature, 7 | Junction, 8 | Tank, 9 | Reservior, 10 | Pipe, 11 | Valve, 12 | Pump, 13 | EpanetFeature, 14 | } from "../../interfaces/EpanetGeoJSON"; 15 | import { link } from "fs"; 16 | 17 | interface NodeLookup { 18 | [id: string]: NodeFeature; 19 | } 20 | 21 | interface LinkLookup { 22 | [id: string]: LinkFeature; 23 | } 24 | 25 | interface EpanetData { 26 | nodeIndex: number; 27 | linkIndex: number; 28 | currentFunction: string; 29 | errors: string[]; 30 | nodes: NodeLookup; 31 | links: LinkLookup; 32 | } 33 | 34 | export function toGeoJson(inpFile: string): EpanetGeoJSON { 35 | const epanetData: EpanetData = { 36 | currentFunction: "", 37 | nodeIndex: 0, 38 | linkIndex: 0, 39 | errors: [], 40 | nodes: {}, 41 | links: {}, 42 | }; 43 | 44 | const lines = inpFile.split("\n"); 45 | const data = lines.reduce((previousValue, currentValue, currentIndex) => { 46 | return readLine(previousValue, currentValue, currentIndex); 47 | }, epanetData); 48 | 49 | // const nodeFeatures = Object.values(data.nodes).reduce( 50 | // (previousValue, currentValue, currentIndex) => { 51 | // return previousValue.concat(pointFeature(currentValue)); 52 | // }, 53 | // [] as NodeLookup[] 54 | // ); 55 | // 56 | // const linkFeatures = Object.values(data.links).reduce( 57 | // (previousValue, currentValue, currentIndex) => { 58 | // return previousValue.concat(lineFeature(currentValue, data)); 59 | // }, 60 | // [] as LinkLookup[] 61 | // ); 62 | 63 | // const fc = featureCollection(nodeFeatures.concat(linkFeatures)); 64 | // console.log(fc); 65 | 66 | const links = (Object.keys(data.links) as Array).reduce( 67 | (acc, l) => { 68 | const link = data.links[l]; 69 | const { usNodeId, dsNodeId } = link.properties; 70 | const usGeometry = data.nodes[usNodeId].geometry.coordinates; 71 | const dsGeometry = data.nodes[dsNodeId].geometry.coordinates; 72 | 73 | return acc.concat({ 74 | ...link, 75 | geometry: { 76 | ...link.geometry, 77 | coordinates: [usGeometry, ...link.geometry.coordinates, dsGeometry], 78 | }, 79 | }); 80 | }, 81 | [] as LinkFeature[] 82 | ); 83 | console.log(links); 84 | 85 | if (data.errors.length > 0) { 86 | console.log(data.errors); 87 | } 88 | 89 | const model: EpanetGeoJSON = { 90 | type: "FeatureCollection", 91 | features: [...links, ...Object.values(data.nodes)], 92 | }; 93 | 94 | return model; 95 | } 96 | 97 | function readLine( 98 | epanetData: EpanetData, 99 | unTrimmedCurrentLine: string, 100 | lineNumber: number 101 | ): EpanetData { 102 | // Removing comments from string and any extra spacing/tabs 103 | // From: "J-1952A 311.450000 ; Comment" 104 | // To: "J-1952A 311.450000" 105 | const commentStart = unTrimmedCurrentLine.indexOf(";"); 106 | const trimTo = 107 | commentStart === -1 ? unTrimmedCurrentLine.length : commentStart; 108 | const currLine = unTrimmedCurrentLine 109 | .substring(0, trimTo) 110 | .replace(/\s+/g, " ") 111 | .trim(); 112 | 113 | // if line starts with ; or is blank skip 114 | if (currLine[0] === ";" || currLine[0] === "" || currLine[0] === undefined) { 115 | return epanetData; 116 | } 117 | 118 | // if line starts with [ then new section 119 | if (currLine[0] === "[" || currLine[currLine.length - 1] === "]") { 120 | epanetData.currentFunction = currLine; 121 | return epanetData; 122 | } 123 | 124 | switch (epanetData.currentFunction) { 125 | case "[JUNCTIONS]": 126 | return junctions(epanetData, currLine, lineNumber); 127 | case "[RESERVOIRS]": 128 | return reservoirs(epanetData, currLine, lineNumber); 129 | case "[PIPES]": 130 | return pipes(epanetData, currLine, lineNumber); 131 | case "[VALVES]": 132 | return valves(epanetData, currLine, lineNumber); 133 | case "[COORDINATES]": 134 | return coordinates(epanetData, currLine, lineNumber); 135 | case "[VERTICES]": 136 | return vertices(epanetData, currLine, lineNumber); 137 | case "[PUMPS]": 138 | return pumps(epanetData, currLine, lineNumber); 139 | case "[TANKS]": 140 | return tanks(epanetData, currLine, lineNumber); 141 | default: 142 | return epanetData; 143 | } 144 | } 145 | 146 | //function pointFeature(node: Node): NodeFeature { 147 | // const geometry = { 148 | // type: "Point", 149 | // coordinates: [node.x, node.y], 150 | // }; 151 | // 152 | // const props = { 153 | // ...node, 154 | // }; 155 | // 156 | // const test: NodeFeature = { 157 | // type: "Feature", 158 | // id: node.index, 159 | // properties: {}, 160 | // }; 161 | // 162 | // return feature(geometry, props); 163 | //} 164 | // 165 | //function lineFeature(link: Link, epanetData: EpanetData): LinkFeature { 166 | // const us = [ 167 | // epanetData.nodes[link.us_node_id].x, 168 | // epanetData.nodes[link.us_node_id].y, 169 | // ]; 170 | // const ds = [ 171 | // epanetData.nodes[link.ds_node_id].x, 172 | // epanetData.nodes[link.ds_node_id].y, 173 | // ]; 174 | // 175 | // const bends = link.bends 176 | // ? [us].concat(link.bends).concat([ds]) 177 | // : [us].concat([ds]); 178 | // 179 | // const geometry = { 180 | // type: "LineString", 181 | // coordinates: bends, 182 | // }; 183 | // 184 | // const props = { 185 | // ...link, 186 | // }; 187 | // 188 | // delete props.bends; 189 | // 190 | // return feature(geometry, props); 191 | //} 192 | 193 | function junctions( 194 | epanetData: EpanetData, 195 | currLine: string, 196 | lineNumber: number 197 | ): EpanetData { 198 | const data = currLine.split(" "); 199 | if (data.length < 2 || data.length > 4) { 200 | return { 201 | ...epanetData, 202 | errors: epanetData.errors.concat(`Error Reading Line ${lineNumber}`), 203 | }; 204 | } 205 | const [id] = data; 206 | 207 | const junction: Junction = { 208 | type: "Feature", 209 | id: epanetData.nodeIndex, 210 | geometry: { 211 | type: "Point", 212 | coordinates: [0, 0], 213 | }, 214 | properties: { 215 | type: "Node", 216 | category: "Junction", 217 | id, 218 | elevation: parseFloat(data[1]), 219 | demand: parseFloat(data[2]), 220 | pattern: data[3], 221 | }, 222 | }; 223 | 224 | return { 225 | ...epanetData, 226 | nodes: { 227 | ...epanetData.nodes, 228 | [id]: junction, 229 | }, 230 | nodeIndex: epanetData.nodeIndex + 1, 231 | }; 232 | } 233 | 234 | function reservoirs( 235 | epanetData: EpanetData, 236 | currLine: string, 237 | lineNumber: number 238 | ): EpanetData { 239 | const data = currLine.split(" "); 240 | if (data.length < 2 || data.length > 3) { 241 | return { 242 | ...epanetData, 243 | errors: epanetData.errors.concat(`Error Reading Line ${lineNumber}`), 244 | }; 245 | } 246 | const [id] = data; 247 | 248 | const reservior: Reservior = { 249 | type: "Feature", 250 | id: epanetData.nodeIndex, 251 | geometry: { 252 | type: "Point", 253 | coordinates: [0, 0], 254 | }, 255 | properties: { 256 | type: "Node", 257 | category: "Reservior", 258 | id, 259 | head: parseFloat(data[1]), 260 | pattern: data[2], 261 | }, 262 | }; 263 | 264 | return { 265 | ...epanetData, 266 | nodes: { 267 | ...epanetData.nodes, 268 | [id]: reservior, 269 | }, 270 | nodeIndex: epanetData.nodeIndex + 1, 271 | }; 272 | } 273 | 274 | function tanks( 275 | epanetData: EpanetData, 276 | currLine: string, 277 | lineNumber: number 278 | ): EpanetData { 279 | const data = currLine.split(" "); 280 | if (data.length < 7 || data.length > 8) { 281 | return { 282 | ...epanetData, 283 | errors: epanetData.errors.concat(`Error Reading Line ${lineNumber}`), 284 | }; 285 | } 286 | 287 | const [id] = data; 288 | 289 | const tank: Tank = { 290 | type: "Feature", 291 | id: epanetData.nodeIndex, 292 | geometry: { 293 | type: "Point", 294 | coordinates: [0, 0], 295 | }, 296 | properties: { 297 | type: "Node", 298 | category: "Tank", 299 | id, 300 | elevation: parseFloat(data[1]), 301 | initLevel: parseFloat(data[2]), 302 | minLevel: parseFloat(data[3]), 303 | maxLevel: parseFloat(data[4]), 304 | diameter: parseFloat(data[5]), 305 | minVolume: parseFloat(data[6]), 306 | volCurve: data[7], 307 | overflow: data[8] ? data[8].toLowerCase() === "true" : undefined, 308 | }, 309 | }; 310 | 311 | return { 312 | ...epanetData, 313 | nodes: { 314 | ...epanetData.nodes, 315 | [id]: tank, 316 | }, 317 | nodeIndex: epanetData.nodeIndex + 1, 318 | }; 319 | } 320 | 321 | function pipes( 322 | epanetData: EpanetData, 323 | currLine: string, 324 | lineNumber: number 325 | ): EpanetData { 326 | const data = currLine.split(" "); 327 | if (data.length < 6 || data.length > 8) { 328 | return { 329 | ...epanetData, 330 | errors: epanetData.errors.concat(`Error Reading Line ${lineNumber}`), 331 | }; 332 | } 333 | 334 | const [ 335 | id, 336 | usNodeId, 337 | dsNodeId, 338 | length, 339 | diameter, 340 | roughness, 341 | minorLoss, 342 | statusAsString, 343 | ] = data; 344 | 345 | let status: "Open" | "Closed" | "CV" | undefined = undefined; 346 | 347 | switch (statusAsString && statusAsString.toLowerCase()) { 348 | case "open": 349 | status = "Open"; 350 | break; 351 | 352 | case "closed": 353 | status = "Closed"; 354 | break; 355 | 356 | case "cv": 357 | status = "CV"; 358 | break; 359 | 360 | default: 361 | break; 362 | } 363 | 364 | const pipe: Pipe = { 365 | type: "Feature", 366 | id: epanetData.linkIndex, 367 | geometry: { 368 | type: "LineString", 369 | coordinates: [], 370 | }, 371 | properties: { 372 | type: "Link", 373 | category: "Pipe", 374 | id, 375 | usNodeId, 376 | dsNodeId, 377 | length: parseFloat(length), 378 | diameter: parseFloat(diameter), 379 | roughness: parseFloat(roughness), 380 | minorLoss: parseFloat(minorLoss), 381 | status, 382 | }, 383 | }; 384 | 385 | return { 386 | ...epanetData, 387 | links: { 388 | ...epanetData.links, 389 | [id]: pipe, 390 | }, 391 | linkIndex: epanetData.linkIndex + 1, 392 | }; 393 | } 394 | 395 | function pumps( 396 | epanetData: EpanetData, 397 | currLine: string, 398 | lineNumber: number 399 | ): EpanetData { 400 | const data = currLine.split(" "); 401 | if ( 402 | data.length < 5 || 403 | data.length === 6 || 404 | data.length === 8 || 405 | data.length > 9 406 | ) { 407 | return { 408 | ...epanetData, 409 | errors: epanetData.errors.concat(`Error Reading Line ${lineNumber}`), 410 | }; 411 | } 412 | 413 | const [id, usNodeId, dsNodeId] = data; 414 | 415 | const pump: Pump = { 416 | type: "Feature", 417 | id: epanetData.linkIndex, 418 | geometry: { 419 | type: "LineString", 420 | coordinates: [], 421 | }, 422 | properties: { 423 | type: "Link", 424 | category: "Pump", 425 | id, 426 | usNodeId, 427 | dsNodeId, 428 | mode: "Power", 429 | power: 2, 430 | speed: 1, 431 | pattern: "dummy", 432 | }, 433 | }; 434 | 435 | return { 436 | ...epanetData, 437 | links: { 438 | ...epanetData.links, 439 | [id]: pump, 440 | }, 441 | linkIndex: epanetData.linkIndex + 1, 442 | }; 443 | } 444 | 445 | function valves( 446 | epanetData: EpanetData, 447 | currLine: string, 448 | lineNumber: number 449 | ): EpanetData { 450 | const data = currLine.split(" "); 451 | 452 | const [id, usNodeId, dsNodeId] = data; 453 | 454 | const valve: Valve = { 455 | type: "Feature", 456 | id: epanetData.linkIndex, 457 | geometry: { 458 | type: "LineString", 459 | coordinates: [], 460 | }, 461 | properties: { 462 | type: "Link", 463 | category: "Valve", 464 | id, 465 | usNodeId, 466 | dsNodeId, 467 | diameter: 100, 468 | valveType: "TCV", 469 | setting: 100, 470 | minorLoss: 0, 471 | }, 472 | }; 473 | 474 | return { 475 | ...epanetData, 476 | links: { 477 | ...epanetData.links, 478 | [id]: valve, 479 | }, 480 | linkIndex: epanetData.linkIndex + 1, 481 | }; 482 | } 483 | 484 | function coordinates( 485 | epanetData: EpanetData, 486 | currLine: string, 487 | lineNumber: number 488 | ): EpanetData { 489 | const data = currLine.split(" "); 490 | if (epanetData.nodes[data[0]] === undefined) { 491 | debugger; 492 | } 493 | 494 | const node = epanetData.nodes[data[0]]; 495 | const x = parseFloat(data[1]); 496 | const y = parseFloat(data[2]); 497 | 498 | epanetData.nodes[data[0]] = { 499 | ...node, 500 | geometry: { 501 | ...node.geometry, 502 | coordinates: [x, y], 503 | }, 504 | }; 505 | 506 | return epanetData; 507 | } 508 | 509 | function vertices( 510 | epanetData: EpanetData, 511 | currLine: string, 512 | lineNumber: number 513 | ): EpanetData { 514 | const data = currLine.split(" "); 515 | 516 | const link = epanetData.links[data[0]]; 517 | 518 | const existingBends = link.geometry.coordinates; 519 | const newBend = [parseFloat(data[1]), parseFloat(data[2])]; 520 | 521 | const bends = existingBends ? existingBends.concat([newBend]) : [newBend]; 522 | 523 | epanetData.links[data[0]] = { 524 | ...link, 525 | geometry: { 526 | ...link.geometry, 527 | coordinates: bends, 528 | }, 529 | }; 530 | 531 | return epanetData; 532 | } 533 | -------------------------------------------------------------------------------- /src/utils/epanet/index.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace, TimeParameter } from "epanet-js"; 2 | import { readBinary, EpanetResults } from "../EpanetBinary"; 3 | import { toGeoJson } from "../EpanetToGeojson"; 4 | 5 | import EpanetGeoJSON from "../../interfaces/EpanetGeoJSON"; 6 | 7 | export interface ReportingInfo { 8 | Periods: number; 9 | ReportStep: number; 10 | StartTime: number; 11 | Duration: number; 12 | } 13 | 14 | // convert inp to geojson 15 | // run epanet 16 | // merge results into geojson 17 | // give back geojson 18 | 19 | export function runEpanet( 20 | fromProject: string, 21 | openModel: (file: [EpanetGeoJSON, EpanetResults, ReportingInfo]) => void 22 | ): void { 23 | // Initialise a new Workspace and Project object 24 | const ws = new Workspace(); 25 | const model = new Project(ws); 26 | 27 | // Write a copy of the inp file to the workspace 28 | ws.writeFile("net1.inp", fromProject); 29 | 30 | // Runs toolkit methods: EN_open, EN_solveH & EN_close 31 | model.open("net1.inp", "report.rpt", "out.bin"); 32 | model.solveH(); 33 | model.saveH(); 34 | 35 | const keys = ["Periods", "ReportStep", "StartTime", "Duration"] as Array< 36 | keyof ReportingInfo 37 | >; 38 | 39 | const timeInfo = keys.reduce((acc, k) => { 40 | acc[k] = model.getTimeParameter(TimeParameter[k]); 41 | return acc; 42 | }, {} as ReportingInfo); 43 | 44 | model.closeH(); 45 | model.close(); 46 | 47 | const resultView = ws.readFile("out.bin", "binary"); 48 | 49 | // const epaNetEngine = Module(); 50 | //@ts-ignore 51 | // epaNetEngine.onRuntimeInitialized = _ => { 52 | // const FS = epaNetEngine.fs; 53 | // FS.writeFile("/net1.inp", fromProject); 54 | // const t0 = performance.now(); 55 | // console.log(epaNetEngine._epanet_run()); 56 | // const t1 = performance.now(); 57 | // console.log("Call to _epanet_run took " + (t1 - t0) + " milliseconds."); 58 | 59 | //const resultView = FS.readFile("/net1.bin"); 60 | 61 | const t2 = performance.now(); 62 | const results = readBinary(resultView); 63 | const t3 = performance.now(); 64 | 65 | const geoJson = toGeoJson(fromProject); 66 | console.log(geoJson); 67 | console.log(results); 68 | console.log("Call to readBinary took " + (t3 - t2) + " milliseconds."); 69 | 70 | openModel([geoJson, results, timeInfo]); 71 | // }; 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/reproject/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FeatureCollection, 3 | Geometries, 4 | Properties, 5 | Feature, 6 | } from "@turf/helpers"; 7 | import { featureReduce, coordEach, coordReduce } from "@turf/meta"; 8 | import clone from "@turf/clone"; 9 | import { featureCollection } from "@turf/helpers"; 10 | import proj4 from "proj4"; 11 | 12 | export function reprojectFeatureCollection( 13 | geoJson: FeatureCollection, 14 | fromProject: string 15 | ): FeatureCollection { 16 | const proj = fromProject === "METERS" ? generateProj(geoJson) : fromProject; 17 | 18 | const initialValue: Array = []; 19 | 20 | const features = featureReduce( 21 | geoJson, 22 | function (previousValue, currentFeature, featureIndex) { 23 | const featureReproject = Object.assign( 24 | {}, 25 | currentFeature, 26 | reprojectFeature(currentFeature, proj) 27 | ); 28 | return previousValue.concat(featureReproject); 29 | }, 30 | initialValue 31 | ); 32 | 33 | return featureCollection(features); 34 | } 35 | 36 | export function reprojectFeature( 37 | feature: Feature, 38 | fromProject: string 39 | ): Feature { 40 | const newFeature = clone(feature); 41 | 42 | coordEach(newFeature, function (currentCoord) { 43 | const newCoord = reprojectCoord(currentCoord, fromProject); 44 | currentCoord[0] = newCoord[0]; 45 | currentCoord[1] = newCoord[1]; 46 | }); 47 | 48 | // TODO: Check again later, there is a bug in Mapbox GL JS where if the last two coords 49 | // are duplicates then it won't draw the line at high zooms. We will check here and remove 50 | // them if they exist 51 | // https://github.com/mapbox/mapbox-gl-js/issues/5171 52 | 53 | if ( 54 | newFeature.geometry && 55 | newFeature.geometry.type === "LineString" && 56 | newFeature.geometry.coordinates.length > 2 57 | ) { 58 | const totalCoords = newFeature.geometry.coordinates.length; 59 | const x1 = newFeature.geometry.coordinates[totalCoords - 1][0]; 60 | const x2 = newFeature.geometry.coordinates[totalCoords - 2][0]; 61 | const y1 = newFeature.geometry.coordinates[totalCoords - 1][1]; 62 | const y2 = newFeature.geometry.coordinates[totalCoords - 2][1]; 63 | if (x1 === x2 && y1 === y2) { 64 | newFeature.geometry.coordinates.pop(); 65 | } 66 | } 67 | 68 | return newFeature; 69 | } 70 | 71 | export function reprojectCoord(coord: number[], fromProject: string): number[] { 72 | //@ts-ignore 73 | return proj4(fromProject, proj4("EPSG:4326"), coord); 74 | } 75 | 76 | function generateProj( 77 | geoJson: FeatureCollection 78 | ): string { 79 | const minXY = coordReduce( 80 | geoJson, 81 | function (previousValue, currentCoord) { 82 | const minX = 83 | currentCoord[0] < previousValue[0] ? currentCoord[0] : previousValue[0]; 84 | const minY = 85 | currentCoord[1] < previousValue[1] ? currentCoord[1] : previousValue[1]; 86 | 87 | return [minX, minY]; 88 | }, 89 | [Infinity, Infinity] 90 | ); 91 | 92 | return `+proj=tmerc +k=0.9996012717 +x_0=${minXY[0]} +y_0=${minXY[1]} +units=m +no_defs`; 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/reproject/test.ts: -------------------------------------------------------------------------------- 1 | import { reprojectCoord, reprojectFeature, reprojectFeatureCollection } from '.'; 2 | import { FeatureCollection, Geometries, Properties, LineString, Feature, Geometry } from '@turf/helpers'; 3 | 4 | 5 | const fromProjection = '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=375,-111,431,0,0,0,0 +units=m +no_defs' 6 | 7 | const geoIn2: FeatureCollection = { 8 | "type": "FeatureCollection", 9 | "features": [ 10 | { 11 | "type": "Feature", "geometry": { "type": "Point", "coordinates": [429157, 623009] }, 12 | "properties": { "table": "wn_node", "node_id": "1234", "totdemnd": 0.0, "totleak": 0.0 } 13 | }, 14 | { 15 | "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[429157, 623009], [429157, 623009]] }, 16 | "properties": { "table": "wn_meter", "us_node_id": "1234", "ds_node_id": "12345", "link_suffix": "1" } 17 | }] 18 | } 19 | 20 | const geoIn: FeatureCollection = { 21 | "type": "FeatureCollection", 22 | "features": [ 23 | { 24 | "type": "Feature", "geometry": { "type": "Point", "coordinates": [429157, 623009] }, 25 | "properties": { "table": "wn_node", "node_id": "1234", "totdemnd": 0.0, "totleak": 0.0 } 26 | }, 27 | { 28 | "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[429157, 623009], [429157, 623009]] }, 29 | "properties": { "table": "wn_meter", "us_node_id": "1234", "ds_node_id": "12345", "link_suffix": "1" } 30 | }] 31 | } 32 | 33 | const geoOut: FeatureCollection = { 34 | "type": "FeatureCollection", 35 | "features": [ 36 | { 37 | "type": "Feature", "geometry": { "type": "Point", "coordinates": [-1.5399906584608514, 55.50003197082179] }, 38 | "properties": { "table": "wn_node", "node_id": "1234", "totdemnd": 0.0, "totleak": 0.0 } 39 | }, 40 | { 41 | "type": "Feature", "geometry": { 42 | "type": "LineString", "coordinates": [[-1.5399906584608514, 55.50003197082179], [-1.5399906584608514, 55.50003197082179]] 43 | }, 44 | "properties": { "table": "wn_meter", "us_node_id": "1234", "ds_node_id": "12345", "link_suffix": "1" } 45 | }] 46 | } 47 | 48 | 49 | 50 | it('reprojects a single coordinate', () => { 51 | const coordIn = [429157, 623009] 52 | const coordOut = [-1.5399906584608514, 55.50003197082179] 53 | const fromProjection = '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=375,-111,431,0,0,0,0 +units=m +no_defs' 54 | const geoReprojected = reprojectCoord(coordIn, fromProjection) 55 | expect(geoReprojected).toEqual(coordOut); 56 | 57 | }); 58 | 59 | 60 | 61 | it('reprojects a feature', () => { 62 | const lineIn: Feature = { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[429157, 623009], [429157, 623009]] }, "properties": {} } 63 | const lineOut: Feature = { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[-1.5399906584608514, 55.50003197082179], [-1.5399906584608514, 55.50003197082179]] }, "properties": {} } 64 | const fromProjection = '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=375,-111,431,0,0,0,0 +units=m +no_defs' 65 | 66 | const lineReproject = reprojectFeature(lineIn, fromProjection) 67 | expect(lineReproject).toEqual(lineOut); 68 | 69 | }) 70 | 71 | 72 | it('reprojects a feature and removes duplicate end point', () => { 73 | const lineIn: Feature = { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[429157, 623009], [429157, 623009], [429157, 623009]] }, "properties": {} } 74 | const lineOut: Feature = { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[-1.5399906584608514, 55.50003197082179], [-1.5399906584608514, 55.50003197082179]] }, "properties": {} } 75 | const fromProjection = '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=375,-111,431,0,0,0,0 +units=m +no_defs' 76 | 77 | const lineReproject = reprojectFeature(lineIn, fromProjection) 78 | expect(lineReproject).toEqual(lineOut); 79 | 80 | }) 81 | 82 | it('reprojects a FeatureCollection', () => { 83 | const fromProjection = '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=375,-111,431,0,0,0,0 +units=m +no_defs' 84 | const geoReprojected = reprojectFeatureCollection(geoIn, fromProjection) 85 | expect(geoIn2).toEqual(geoIn); 86 | expect(geoReprojected).toEqual(geoOut); 87 | 88 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve" 17 | }, 18 | "exclude": ["src/utils/epanet/output.js"], 19 | "include": ["src"] 20 | } 21 | --------------------------------------------------------------------------------