├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── bracket-heat ├── Cargo.toml ├── LICENSE ├── README.md ├── resources │ ├── advisor.html │ ├── index.html │ ├── isp-template.ron │ ├── locinfo.html │ ├── pngegg.png │ ├── three.js │ └── tower_Marker.png └── src │ ├── calculators │ └── mod.rs │ ├── data_defs │ ├── linkbudget.rs │ ├── mod.rs │ ├── tower.rs │ └── wisp.rs │ ├── los │ ├── los_plot.rs │ ├── map_click.rs │ └── mod.rs │ ├── main.rs │ └── tiler │ ├── heightmap.rs │ ├── losmap.rs │ ├── mod.rs │ ├── signalmap.rs │ └── three_d.rs ├── rf-signal-algorithms ├── Cargo.toml ├── README.md ├── examples │ ├── cost.rs │ ├── ecc33.rs │ ├── egli.rs │ ├── fspl.rs │ ├── hata.rs │ ├── itm3.rs │ ├── pel.rs │ ├── soil.rs │ └── sui.rs ├── resources │ ├── 1 │ │ └── N38W093T97.hgt │ ├── 3 │ │ └── N38W093.hgt │ └── third │ │ └── N38W093.hgt └── src │ ├── geometry │ └── mod.rs │ ├── lib.rs │ ├── mapping │ ├── bheat │ │ ├── heat_cache.rs │ │ ├── mod.rs │ │ └── tile_reader.rs │ ├── latlon.rs │ ├── mod.rs │ └── srtm │ │ ├── mod.rs │ │ ├── tile.rs │ │ └── tile_cache.rs │ ├── rfcalc │ ├── cost_hata.rs │ ├── ecc33.rs │ ├── egli.rs │ ├── fresnel.rs │ ├── fspl.rs │ ├── hata.rs │ ├── itwom3.rs │ ├── itwom3_port │ │ ├── helpers.rs │ │ ├── mod.rs │ │ ├── prop.rs │ │ └── pure.rs │ ├── mod.rs │ ├── pel.rs │ ├── soil.rs │ └── sui.rs │ └── units │ ├── distance.rs │ ├── frequency.rs │ └── mod.rs ├── screenshots ├── basemap.jpg ├── heightmap1.jpg ├── heightmap2.jpg ├── locexplorer.jpg ├── quickestimate.jpg ├── search.jpg └── signaloptimize.jpg ├── src └── main.rs └── terrain-cooker ├── Cargo.toml └── src ├── main.rs └── tile_writer.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode 4 | bracket-heat/resources/gmap_key.txt 5 | bracket-heat/resources/isp.ron 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rf-signals" 3 | version = "0.1.0" 4 | authors = ["Herbert Wolverson "] 5 | edition = "2018" 6 | description = "Rust wrapper for https://github.com/Cloud-RF/Signal-Server signal algorithms." 7 | readme = "README.md" 8 | license = "GPL2" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | rf-signal-algorithms = { path = "rf-signal-algorithms/" } 14 | 15 | [workspace] 16 | members = [ 17 | "rf-signal-algorithms", 18 | "bracket-heat", 19 | "terrain-cooker", 20 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | Appendix: How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 19yy 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) 19yy name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Library General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RF_Signals 2 | 3 | This is a long-term project for my employer, [iZones](https://izones.net/). They kindly gave me permission to open source a big chunk of our wireless planning software. We're a WISP (Wireless Internet Service Provider), and hope this helps other WISPs to succeed. 4 | 5 | This project is divided into three parts: 6 | 7 | * *rf_signal_algorithms* is a pure Rust port of [Cloud-RF/Signal Server](https://github.com/Cloud-RF/Signal-Server)'s algorithms. In turn, this is based upon SPLAT! by Alex Farrant and John A. Magliacane. I've retained the GPL2 license, because the parent requires it. 8 | * *terrain-cooker* reads `las` files (LiDAR point clouds) and converts them into `bheat` files for efficient processing inside `bracket-heat`. 9 | * *bracket-heat* is a web-based planning tool to help you decide if Wireless Internet installations are likely to work. 10 | 11 | ## Bracket-Heat 12 | 13 | One of my early projects was WispTools.net, which used Longley-Rice/ITM (a lot of people thought it used Radio Mobile, but there's no connection other than similar algorithms). Bracket-Heat is a modernized version of the planner, designed for self-hosting. 14 | 15 | When you first open `bracket-heat`, you are presented with a map showing your towers: 16 | 17 | ![](screenshots/basemap.jpg) 18 | 19 | You can enter any address into the search box, and it runs a Google reverse geolocation to find the location and zoom in: 20 | 21 | ![](screenshots/search.jpg) 22 | 23 | You can switch to satellite view, terrain view, etc. to help you find the location. You can click the "height" button at any time to see an overlay of your LiDAR data - great for checking that your data works, and getting an overview of the general area: 24 | 25 | ![](screenshots/heightmap1.jpg) 26 | ![](screenshots/heightmap2.jpg) 27 | 28 | Once you've found the building, you can click on it to see a quick path/service estimate for the location: 29 | 30 | ![](screenshots/quickestimate.jpg) 31 | 32 | From the quick path analysis window, you can click "location explorer" to examine a detailed (1m resolution) slice of the path from your chosen point to your towers: 33 | 34 | ![](screenshots/locexplorer.jpg) 35 | 36 | You can also click "signal optimizer" to use a wizard to identify your target area, block off any parts you don't want to evaluate, and display a signal estimate grid for the target location: 37 | 38 | ![](screenshots/signaloptimize.jpg) 39 | 40 | We've had pretty good success cutting down on failed site surveys, and finding signal where we might not have expected it, with this tool. Hopefully, it can help your WISP, also. 41 | 42 | ## Ported Algorithms 43 | 44 | This crate provides Rust implementations of a number of algorithms that are useful in wireless calculations: 45 | 46 | * ITM3/Longley-Rice - the power behind Splat! and other functions. 47 | * HATA with the COST123 extension. 48 | * ECC33. 49 | * EGLI. 50 | * HATA. 51 | * Plane Earth. 52 | * SOIL. 53 | * SUI. 54 | 55 | Additionally, helper functions provide: 56 | 57 | * Basic Free-Space Path Loss (FSPL) calculation. 58 | * Fresnel size calculation. 59 | 60 | ## SRTM .hgt Reader 61 | 62 | There's also an SRTM .hgt reader. You can get these from various places for pretty much the whole planet. See Radio Mobile for details. This will eventually be in its own feature. For now, it maintains an LRU cache of height tiles and tries to find the best resolution available to answer an elevation query. 63 | 64 | An example query: 65 | 66 | ```rust 67 | let loc = LatLon::new(38.947775, -92.323385); 68 | let altitude = get_altitude(&loc, "resources"); 69 | ``` 70 | 71 | This requires the `hgt` files from the `resources` directory to function. 72 | 73 | ## Porting Status 74 | 75 | All algorithms started out in Cloud_RF's Signal Server (in C or C++) and were ported to Rust. 76 | 77 | |Algorithm |Status | 78 | |------------|---------| 79 | |COST/HATA |Ported to Pure Rust| 80 | |ECC33 |Ported to Pure Rust| 81 | |EGLI |Ported to Pure Rust| 82 | |Fresnel Zone|Pure Rust (not in original)| 83 | |FSPL |Pure Rust| 84 | |HATA |Ported to Pure Rust| 85 | |ITWOM3 |Ported to Pure Rust| 86 | |Plane Earth |Ported to Pure Rust| 87 | |SOIL |Ported to Pure Rust| 88 | |SUI |Ported to Pure Rust| 89 | 90 | # So how do I use this? 91 | 92 | If you want to use this yourself, there's a few steps to get going. 93 | 94 | ## Requirements 95 | 96 | * You need a decently powerful PC to host it. We use a 12-core Xeon server with lots of storage, but it runs fine on my development laptop (i7, 12 gb RAM, 1 tb SSD). The program is mostly disk-bound (the actual calculations are pretty fast), so faster storage is good. 97 | * You'll need to obtain `.hgt` format data for your coverage area. These are the same files used by Splat! and Radio Mobile. You can also take various DEM files and convert them to HGT tile format. It supports all three popular resolutions of hgt file. 98 | * You need LiDAR `.las` files for your coverage area. Your county assessor, or state land-grant university probably has them. For Missouri, I obtain them from [MSDIS](https://msdis.maps.arcgis.com/apps/View/index.html?appid=276d7a04beef4bb2820a13b12a144598). You should be able to use any LiDAR file, so long as it is in LAS format, and in a cartographic projection supported by Proj. 99 | * You'll probably want to have access to Linux (or Windows Services for Linux) for converting LiDAR files. PROJ is a bear to get running in Rust on Windows. 100 | * You need a Google Maps API key. 101 | * Rocket requires that you run rust in `nightly` mode. 102 | 103 | ## Initial Setup 104 | 105 | 1. Make sure you have a fully working Rust installation, along with a C++ and Clang build chain working. 106 | 2. Clone this project (with `git clone`) to a directory on your computer. 107 | 3. Setup a directory that you want to use to store `bheat` files. 108 | 4. Make sure you know where you put your `hgt` and `las` files. 109 | 5. Modify `terrain_cooker/src/main.rs`. Change `LIDAR_PATH` to the directory in which you are storing your `.las` files. 110 | 6. In `terrain_cooker/src/tile_writer.rs` around line 48 change the base path to your `hgt` files. 111 | 7. In `terrain_cooker/src/tile_writer.rs` around line 110 change the output directory to where you want to save `bheat` files. 112 | 113 | (**Note**: this will be made configurable without recompiling soon, promise). 114 | 115 | ## Cook some terrain 116 | 117 | In your project directory, type `cd terrain-cooker`. Then run the conversion with `cargo run --release`. It will evaluate all `.las` files in the LIDAR folder, reading `.hgt` files from your heightmap directory and spit out `bheat` tiles. BHeat tiles start with a heightmap derived from your `hgt` files - and then adjust ground height and add terrain clutter heights from LiDAR files. This is an additive process: you can re-run it with new LiDAR data whenever you want, and the new data will be merged in. 118 | 119 | ## Add in your Google maps key 120 | 121 | In `bracket-heat`, create a file named `gmap_key.txt` and paste your Google Maps API key into it. I didn't want to give you mine! 122 | 123 | ## Setup your WISP Information 124 | 125 | 1. Change to the `bracket-heat` directory. 126 | 2. Copy `resources/isp-template.ron` to `resources/isp.ron`. 127 | 128 | The default file looks like this: 129 | 130 | ~~~ron 131 | Wisp( 132 | listen_port: 8000, 133 | name: "iZones LLC", 134 | center: (38.947775, -92.323385), 135 | map_zoom: 10, 136 | heat_path: "z:/bheat", 137 | 138 | link_budgets: [ 139 | LinkBudget( 140 | name: "Medusa + 450b HG", 141 | xmit_eirp: 49.0, 142 | receive_gain: 20.0 143 | ), 144 | LinkBudget( 145 | name: "UBNT 120° Sector + PowerBeam 400", 146 | xmit_eirp: 44.0, 147 | receive_gain: 25.0 148 | ), 149 | ], 150 | 151 | towers: [ 152 | Tower( 153 | name: "Paquin", 154 | lat: 38.947927398900426, 155 | lon: -92.3233740822584, 156 | height_meters: 55, 157 | max_range_km: 16.0934, 158 | access_points: [ 159 | AP( 160 | name: "Medusa", 161 | frequency_ghz: 3.6, 162 | max_range_km: 16.0934, 163 | link_budget: 71, 164 | ), 165 | AP( 166 | name: "58 UBNT", 167 | frequency_ghz: 5.8, 168 | max_range_km: 16.0934, 169 | link_budget: 69, 170 | ), 171 | AP( 172 | name: "60 Ghz", 173 | frequency_ghz: 60.0, 174 | max_range_km: 3.21869, 175 | link_budget: 97, 176 | ) 177 | ], 178 | ), 179 | Tower( 180 | name: "Biggs", 181 | lat: 38.798403, 182 | lon: -92.289524, 183 | height_meters: 60, 184 | max_range_km: 16.0934, 185 | access_points: [ 186 | AP( 187 | name: "Medusa", 188 | frequency_ghz: 3.6, 189 | max_range_km: 16.0934, 190 | link_budget: 71, 191 | ), 192 | AP( 193 | name: "58 UBNT", 194 | frequency_ghz: 5.8, 195 | max_range_km: 16.0934, 196 | link_budget: 69, 197 | ), 198 | AP( 199 | name: "60 Ghz", 200 | frequency_ghz: 60.0, 201 | max_range_km: 3.21869, 202 | link_budget: 97, 203 | ) 204 | ] 205 | ) 206 | ] 207 | ) 208 | ~~~ 209 | 210 | Make changes as you see fit: 211 | 212 | * `listen_port` is the TCP port on which the webserver (Rocket) will listen. 213 | * `name` just changes the title bar of the app, use it to brand with your WISP. 214 | * `center` is the lat/lng position you want to see when you start the app. 215 | * `map_zoom` is the zoom level at which you want to start when the app opens. 216 | * `heat_path` must point at the `bheat` directory you created and populated with cooked data. 217 | * `link_budgets` lets you define the types of radios you use, and wish to utilize when running RF calculations. 218 | * `towers` describes all of your towers. It's important to add `AP` entries to them, describing what services are available at which location. 219 | 220 | When you calculate the `link_budget` for an AP, take the transmitter's power and antenna gain and add them together. Then add the antenna gain of the CPE (NOT the power) you typically use. 221 | 222 | ## Run the program 223 | 224 | Launch the program with `cargo run --release` in your `bracket-heat` directory. Open a browser to `http://localhost::` (`` being whatever you set as the `listen_port` in your config file). If everything works, you now have an RF analysis suite. 225 | -------------------------------------------------------------------------------- /bracket-heat/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bracket-heat" 3 | version = "0.1.0" 4 | authors = ["Herbert Wolverson "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | rocket = "0.4.7" 11 | rocket_contrib = { version = "0.4.5", features = ["json"] } 12 | rf-signal-algorithms = { path = "../rf-signal-algorithms/" } 13 | serde = "1.0.117" 14 | ron = "0.6.2" 15 | lazy_static = "1.4.0" 16 | parking_lot = "0.11.0" 17 | png = "0.16.7" 18 | -------------------------------------------------------------------------------- /bracket-heat/LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | Appendix: How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 19yy 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) 19yy name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Library General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /bracket-heat/README.md: -------------------------------------------------------------------------------- 1 | # Bracket-Heat 2 | 3 | An in-development RF heat-mapping system. 4 | 5 | ## Requirements 6 | 7 | * This requires nightly Rust, because of Rocket. ```rustup override set nightly``` should do the trick. 8 | * You need to put your Google Maps API key in `bracket-heat/resources/gmap_key.txt`. One line without a `\n` at the end. I didn't want to share my Google account details with you. Sorry. 9 | 10 | ## Setup 11 | 12 | * Edit `isp.ron` in `resources` to match your WISP. 13 | 14 | -------------------------------------------------------------------------------- /bracket-heat/resources/advisor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | _BANNER_ 4 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | 68 |
Processing - Please Wait
69 | 70 |
71 |
72 |

73 | Hi! I'm SignalBot. Please pan/zoom until the target house is towards the center of the view; I've tried to pick a good starting point. 74 | Zoom in as far as you can while still being able to see the building edges. 75 |

76 | 77 |
78 | 82 | 86 | 90 | 93 |
94 | 95 | 96 | 453 | -------------------------------------------------------------------------------- /bracket-heat/resources/isp-template.ron: -------------------------------------------------------------------------------- 1 | Wisp( 2 | listen_port: 8000, 3 | name: "iZones LLC", 4 | center: (38.947775, -92.323385), 5 | map_zoom: 10, 6 | heat_path: "z:/bheat", 7 | 8 | link_budgets: [ 9 | LinkBudget( 10 | name: "Medusa + 450b HG", 11 | xmit_eirp: 49.0, 12 | receive_gain: 20.0 13 | ), 14 | LinkBudget( 15 | name: "UBNT 120° Sector + PowerBeam 400", 16 | xmit_eirp: 44.0, 17 | receive_gain: 25.0 18 | ), 19 | ], 20 | 21 | towers: [ 22 | Tower( 23 | name: "Paquin", 24 | lat: 38.947927398900426, 25 | lon: -92.3233740822584, 26 | height_meters: 55, 27 | max_range_km: 16.0934, 28 | access_points: [ 29 | AP( 30 | name: "Medusa", 31 | frequency_ghz: 3.6, 32 | max_range_km: 16.0934, 33 | link_budget: 71, 34 | ), 35 | AP( 36 | name: "58 UBNT", 37 | frequency_ghz: 5.8, 38 | max_range_km: 16.0934, 39 | link_budget: 69, 40 | ), 41 | AP( 42 | name: "60 Ghz", 43 | frequency_ghz: 60.0, 44 | max_range_km: 3.21869, 45 | link_budget: 97, 46 | ) 47 | ], 48 | ), 49 | Tower( 50 | name: "Biggs", 51 | lat: 38.798403, 52 | lon: -92.289524, 53 | height_meters: 60, 54 | max_range_km: 16.0934, 55 | access_points: [ 56 | AP( 57 | name: "Medusa", 58 | frequency_ghz: 3.6, 59 | max_range_km: 16.0934, 60 | link_budget: 71, 61 | ), 62 | AP( 63 | name: "58 UBNT", 64 | frequency_ghz: 5.8, 65 | max_range_km: 16.0934, 66 | link_budget: 69, 67 | ), 68 | AP( 69 | name: "60 Ghz", 70 | frequency_ghz: 60.0, 71 | max_range_km: 3.21869, 72 | link_budget: 97, 73 | ) 74 | ] 75 | ) 76 | ] 77 | ) 78 | -------------------------------------------------------------------------------- /bracket-heat/resources/locinfo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Location Detail 6 | 9 | 10 | 11 | 12 | 13 | 14 | 81 | 82 | -------------------------------------------------------------------------------- /bracket-heat/resources/pngegg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/bracket-heat/resources/pngegg.png -------------------------------------------------------------------------------- /bracket-heat/resources/tower_Marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/bracket-heat/resources/tower_Marker.png -------------------------------------------------------------------------------- /bracket-heat/src/calculators/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::WISP; 2 | use rf_signal_algorithms::{ 3 | bheat::heat_altitude, free_space_path_loss_db, geometry::haversine_distance, 4 | itwom_point_to_point, lat_lon_path_1m, lat_lon_vec_to_heights, Distance, Frequency, LatLon, 5 | PTPClimate, PTPPath, 6 | }; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct WirelessService { 10 | tower: String, 11 | tower_pos: LatLon, 12 | name: String, 13 | pos: LatLon, 14 | height: Distance, 15 | frequency: Frequency, 16 | link_budget_db: f64, 17 | range: Distance, 18 | } 19 | 20 | #[derive(Clone, Debug)] 21 | pub struct PossibleLink { 22 | pub tower: String, 23 | pub tower_pos: LatLon, 24 | pub name: String, 25 | pub cpe_height: f64, 26 | pub mode: String, 27 | pub signal: f64, 28 | pub range_km: f64, 29 | } 30 | 31 | /// Finds all APs within range of a given point 32 | pub fn services_in_range(pos: &LatLon) -> Vec { 33 | let mut result = Vec::new(); 34 | 35 | let wisp_reader = WISP.read(); 36 | wisp_reader.towers.iter().for_each(|t| { 37 | let range = haversine_distance(pos, &LatLon::new(t.lat, t.lon)); 38 | 39 | t.access_points 40 | .iter() 41 | .filter(|ap| range.as_km() < ap.max_range_km) 42 | .for_each(|ap| { 43 | result.push(WirelessService { 44 | tower: t.name.clone(), 45 | tower_pos: LatLon::new(t.lat, t.lon), 46 | name: ap.name.clone(), 47 | pos: LatLon::new(t.lat, t.lon), 48 | height: Distance::with_meters(t.height_meters), 49 | frequency: Frequency::with_ghz(ap.frequency_ghz), 50 | link_budget_db: ap.link_budget, 51 | range: range.clone(), 52 | }); 53 | }); 54 | }); 55 | 56 | result 57 | } 58 | 59 | pub fn evaluate_wireless_services( 60 | pos: &LatLon, 61 | services: &Vec, 62 | heat_path: &str, 63 | ) -> Vec { 64 | let mut result = Vec::new(); 65 | services.iter().for_each(|svc| { 66 | evaluate_wireless_service(pos, svc, heat_path, &mut result); 67 | }); 68 | result 69 | } 70 | 71 | fn evaluate_wireless_service( 72 | pos: &LatLon, 73 | service: &WirelessService, 74 | heat_path: &str, 75 | results: &mut Vec, 76 | ) { 77 | if service.range.as_meters() < 50.0 { 78 | results.push(PossibleLink { 79 | tower: service.tower.clone(), 80 | tower_pos: service.tower_pos, 81 | name: service.name.clone(), 82 | cpe_height: 0.0, 83 | mode: "<50m Range".to_string(), 84 | signal: service.link_budget_db 85 | - free_space_path_loss_db(service.frequency, service.range), 86 | range_km: service.range.as_km(), 87 | }); 88 | } else { 89 | // ITM requires that the tower not include clutter 90 | let base_tower_height = heat_altitude(service.pos.lat(), service.pos.lon(), heat_path) 91 | .unwrap_or((Distance::with_meters(0.0), Distance::with_meters(0.0))) 92 | .0 93 | .as_meters(); 94 | 95 | // Calculate the line between tower and SM 96 | let path = lat_lon_path_1m(&service.pos, pos); 97 | 98 | // Create a list of altitudes to use 99 | let mut los_path = lat_lon_vec_to_heights(&path, heat_path); 100 | 101 | // Force the tower height in spot 0. 102 | los_path[0] = base_tower_height; 103 | 104 | let mut found_los = false; 105 | let mut cpe_height = 0.25; 106 | let mut last_mode = String::new(); 107 | while cpe_height < 3.6 && !found_los { 108 | let (loss, mode) = itm_eval(cpe_height, &los_path, service); 109 | let signal = service.link_budget_db - loss; 110 | let mut ok = true; 111 | if signal < -80.0 { 112 | ok = false; 113 | } 114 | // Reject 5.8 or higher with 2 obstacles 115 | if ok && service.frequency.as_ghz() > 5.0 && mode == "2_Hrzn_Diff" { 116 | ok = false; 117 | } 118 | if ok && service.frequency.as_ghz() > 9.0 && mode != "L-o-S" { 119 | ok = false; 120 | } 121 | if mode == last_mode { 122 | ok = false; 123 | } 124 | if ok { 125 | results.push(PossibleLink { 126 | tower: service.tower.clone(), 127 | tower_pos: service.tower_pos, 128 | name: service.name.clone(), 129 | cpe_height, 130 | mode: mode.clone(), 131 | signal, 132 | range_km: service.range.as_km(), 133 | }); 134 | } 135 | if mode == "L-o-S" { 136 | found_los = true; 137 | } else { 138 | cpe_height += 0.25; 139 | last_mode = mode.clone(); 140 | } 141 | } 142 | } 143 | } 144 | 145 | fn itm_eval( 146 | cpe_height: f64, 147 | path_as_distances: &Vec, 148 | service: &WirelessService, 149 | ) -> (f64, String) { 150 | // Setup an ITM terrain path and retrieve the data 151 | let mut terrain_path = PTPPath::new( 152 | path_as_distances.clone(), 153 | service.height, 154 | Distance::with_meters(cpe_height), 155 | Distance::with_meters(1.0), 156 | ) 157 | .unwrap(); 158 | 159 | let lr = itwom_point_to_point( 160 | &mut terrain_path, 161 | PTPClimate::default(), 162 | service.frequency, 163 | 0.5, 164 | 0.5, 165 | 1, 166 | ); 167 | 168 | (lr.dbloss, lr.mode) 169 | } 170 | -------------------------------------------------------------------------------- /bracket-heat/src/data_defs/linkbudget.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Serialize, Deserialize, Default)] 4 | pub struct LinkBudget { 5 | pub name: String, 6 | pub xmit_eirp: f64, 7 | pub receive_gain: f64, 8 | } 9 | -------------------------------------------------------------------------------- /bracket-heat/src/data_defs/mod.rs: -------------------------------------------------------------------------------- 1 | mod wisp; 2 | pub use wisp::*; 3 | mod tower; 4 | pub use tower::*; 5 | mod linkbudget; 6 | pub use linkbudget::*; 7 | -------------------------------------------------------------------------------- /bracket-heat/src/data_defs/tower.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Serialize, Deserialize, Default)] 4 | pub struct Tower { 5 | pub name: String, 6 | pub lat: f64, 7 | pub lon: f64, 8 | pub height_meters: f64, 9 | pub max_range_km: f64, 10 | pub access_points: Vec, 11 | } 12 | 13 | #[derive(Clone, Serialize, Deserialize, Default)] 14 | pub struct AP { 15 | pub name: String, 16 | pub frequency_ghz: f64, 17 | pub max_range_km: f64, 18 | pub link_budget: f64, 19 | } 20 | -------------------------------------------------------------------------------- /bracket-heat/src/data_defs/wisp.rs: -------------------------------------------------------------------------------- 1 | use super::{LinkBudget, Tower}; 2 | use ron::de::from_reader; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fs::File; 5 | 6 | #[derive(Clone, Serialize, Deserialize, Default)] 7 | pub struct Wisp { 8 | pub listen_port: u16, 9 | pub name: String, 10 | pub center: (f64, f64), 11 | pub map_zoom: u32, 12 | pub towers: Vec, 13 | pub heat_path: String, 14 | pub link_budgets: Vec, 15 | } 16 | 17 | pub fn load_wisp() -> Wisp { 18 | let f = File::open("resources/isp.ron").unwrap(); 19 | let wisp: Wisp = match from_reader(f) { 20 | Ok(x) => x, 21 | Err(e) => { 22 | println!("{:?}", e); 23 | panic!("Unable to load WISP definition file. Is it in resources?"); 24 | } 25 | }; 26 | wisp 27 | } 28 | -------------------------------------------------------------------------------- /bracket-heat/src/los/los_plot.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{ 2 | bheat::heat_altitude, fresnel_radius, geometry::haversine_distance, itwom_point_to_point, 3 | lat_lon_path_1m, lat_lon_vec_to_heights, Distance, Frequency, LatLon, PTPClimate, PTPPath, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Clone, Serialize, Deserialize, Default)] 8 | pub struct LineOfSightPlot { 9 | pub tower_base_height: f64, 10 | pub srtm: Vec, 11 | pub lidar: Vec, 12 | pub fresnel: Vec, 13 | pub dbloss: f64, 14 | pub mode: String, 15 | pub distance_m: f64, 16 | } 17 | 18 | pub fn los_plot( 19 | pos: &LatLon, 20 | tower_index: usize, 21 | cpe_height: f64, 22 | frequency: Frequency, 23 | heat_path: &str, 24 | ) -> LineOfSightPlot { 25 | let reader = crate::WISP.read(); 26 | let t = &reader.towers[tower_index]; 27 | let d = haversine_distance(pos, &LatLon::new(t.lat, t.lon)); 28 | let base_tower_height = heat_altitude(t.lat, t.lon, heat_path) 29 | .unwrap_or((Distance::with_meters(0.0), Distance::with_meters(0.0))) 30 | .0 31 | .as_meters(); 32 | let path = lat_lon_path_1m(&LatLon::new(t.lat, t.lon), pos); // Tower is 1st 33 | 34 | // Calculate the LoS and loss - should be cached data 35 | let los_path = lat_lon_vec_to_heights(&path, heat_path); 36 | let (dbloss, mode) = { 37 | let mut path_as_distances: Vec = los_path.iter().map(|d| *d as f64).collect(); 38 | if path_as_distances.iter().filter(|h| **h == 0.0).count() > 0 { 39 | (0.0, "Missing Data".to_string()) 40 | } else { 41 | path_as_distances[0] = base_tower_height; 42 | let mut terrain_path = PTPPath::new( 43 | path_as_distances, 44 | Distance::with_meters(t.height_meters), 45 | Distance::with_meters(cpe_height), 46 | Distance::with_meters(1.0), 47 | ) 48 | .unwrap(); 49 | 50 | let lr = itwom_point_to_point( 51 | &mut terrain_path, 52 | PTPClimate::default(), 53 | frequency, 54 | 0.5, 55 | 0.5, 56 | 1, 57 | ); 58 | 59 | (lr.dbloss, format!("{} ({})", lr.mode, lr.error_num)) 60 | } 61 | }; 62 | 63 | // Expand out the srtm, lidar and fresnel fields 64 | let mut srtm = Vec::new(); 65 | let mut lidar = Vec::new(); 66 | let mut fresnel = Vec::new(); 67 | 68 | let mut walker = 0.0; 69 | path.iter().for_each(|loc| { 70 | let h = heat_altitude(loc.lat(), loc.lon(), heat_path) 71 | .unwrap_or((Distance::with_meters(0), Distance::with_meters(0))); 72 | srtm.push(h.0.as_meters()); 73 | lidar.push(h.1.as_meters()); 74 | fresnel.push(fresnel_radius(walker, d.as_meters() - walker, frequency.as_mhz()) * 0.6); 75 | walker += 1.0; 76 | }); 77 | 78 | LineOfSightPlot { 79 | tower_base_height: base_tower_height, 80 | srtm, 81 | lidar, 82 | fresnel, 83 | dbloss, 84 | mode, 85 | distance_m: d.as_meters(), 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /bracket-heat/src/los/map_click.rs: -------------------------------------------------------------------------------- 1 | use crate::WISP; 2 | use rf_signal_algorithms::{ 3 | bheat::heat_altitude, geometry::haversine_distance, itwom_point_to_point, lat_lon_path_1m, 4 | lat_lon_vec_to_heights, Distance, Frequency, LatLon, PTPClimate, PTPPath, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Clone, Serialize, Deserialize, Default)] 9 | pub struct ClickSite { 10 | pub base_height_m: f64, 11 | pub lidar_height_m: f64, 12 | pub towers: Vec, 13 | } 14 | 15 | #[derive(Clone, Serialize, Deserialize, Default)] 16 | pub struct TowerEvaluation { 17 | pub tower: String, 18 | pub name: String, 19 | pub lat: f64, 20 | pub lon: f64, 21 | pub rssi: f64, 22 | pub distance_km: f64, 23 | pub mode: String, 24 | } 25 | 26 | pub fn evaluate_tower_click( 27 | pos: &LatLon, 28 | frequency: Frequency, 29 | cpe_height: f64, 30 | heat_path: &str, 31 | link_budget: f64, 32 | ) -> ClickSite { 33 | let services = crate::calculators::services_in_range(pos); 34 | let evaluation = crate::calculators::evaluate_wireless_services(pos, &services, heat_path); 35 | 36 | let towers = evaluation 37 | .iter() 38 | .map(|e| TowerEvaluation { 39 | tower: e.tower.clone(), 40 | name: format!("{}:{} @{}m", e.tower, e.name, e.cpe_height), 41 | lat: e.tower_pos.lat(), 42 | lon: e.tower_pos.lon(), 43 | rssi: e.signal, 44 | distance_km: e.range_km, 45 | mode: e.mode.clone(), 46 | }) 47 | .collect(); 48 | 49 | let h = heat_altitude(pos.lat(), pos.lon(), heat_path) 50 | .unwrap_or((Distance::with_meters(0), Distance::with_meters(0))); 51 | ClickSite { 52 | base_height_m: h.0.as_meters(), 53 | lidar_height_m: h.1.as_meters(), 54 | towers, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /bracket-heat/src/los/mod.rs: -------------------------------------------------------------------------------- 1 | mod map_click; 2 | pub use map_click::*; 3 | mod los_plot; 4 | pub use los_plot::*; 5 | -------------------------------------------------------------------------------- /bracket-heat/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, decl_macro)] 2 | #[macro_use] 3 | extern crate rocket; 4 | use lazy_static::*; 5 | use parking_lot::RwLock; 6 | mod data_defs; 7 | use data_defs::*; 8 | mod calculators; 9 | mod los; 10 | mod tiler; 11 | 12 | // Please save your API key in gmap_key.txt in the `resources` directory. 13 | const GOOGLE_MAPS_API_KEY: &str = include_str!("../resources/gmap_key.txt"); 14 | const INDEX_HTML: &str = include_str!("../resources/index.html"); 15 | const ADVISOR_HTML: &str = include_str!("../resources/advisor.html"); 16 | use rf_signal_algorithms::{Frequency, LatLon}; 17 | use rocket::{config::Environment, Config, Response}; 18 | use rocket::{http::ContentType, http::Status, response::content}; 19 | use rocket_contrib::json::Json; 20 | 21 | // Data Storage Holders 22 | lazy_static! { 23 | static ref INDEX_FINAL: RwLock = RwLock::new(String::new()); 24 | } 25 | 26 | lazy_static! { 27 | static ref ADVISOR_FINAL: RwLock = RwLock::new(String::new()); 28 | } 29 | 30 | lazy_static! { 31 | static ref WISP: RwLock = RwLock::new(Wisp::default()); 32 | } 33 | 34 | #[get("/")] 35 | fn index() -> content::Html { 36 | content::Html(INDEX_FINAL.read().clone()) 37 | } 38 | 39 | #[get("/advisor.html")] 40 | fn advisor() -> content::Html { 41 | content::Html(ADVISOR_FINAL.read().clone()) 42 | } 43 | 44 | #[get("/three.js")] 45 | fn three_js<'a>() -> rocket::response::Stream { 46 | use std::fs::File; 47 | rocket::response::Stream::from(File::open("resources/three.js").unwrap()) 48 | } 49 | 50 | #[get("/locinfo.html")] 51 | fn loc_info<'a>() -> rocket::response::Stream { 52 | use std::fs::File; 53 | rocket::response::Stream::from(File::open("resources/locinfo.html").unwrap()) 54 | } 55 | 56 | #[get("/tower_Marker.png")] 57 | fn tower_marker<'a>() -> rocket::response::Stream { 58 | use std::fs::File; 59 | rocket::response::Stream::from(File::open("resources/tower_Marker.png").unwrap()) 60 | } 61 | 62 | #[get("/pngegg.png")] 63 | fn pngegg<'a>() -> rocket::response::Stream { 64 | use std::fs::File; 65 | rocket::response::Stream::from(File::open("resources/pngegg.png").unwrap()) 66 | } 67 | 68 | #[get("/towers", format = "json")] 69 | fn towers() -> Json> { 70 | Json(WISP.read().towers.clone()) 71 | } 72 | 73 | #[get("/budgets", format = "json")] 74 | fn budgets() -> Json> { 75 | Json(WISP.read().link_budgets.clone()) 76 | } 77 | 78 | #[get("/heightmap////")] 79 | fn heightmap<'a>(swlat: f64, swlon: f64, nelat: f64, nelon: f64) -> Response<'a> { 80 | let heat_path = WISP.read().heat_path.clone(); 81 | let image_buffer = tiler::heightmap_tile(swlat, swlon, nelat, nelon, &heat_path); 82 | let mut response_build = Response::build(); 83 | response_build.header(ContentType::PNG); 84 | response_build.status(Status::Ok); 85 | response_build.streamed_body(std::io::Cursor::new(image_buffer)); 86 | response_build.finalize() 87 | } 88 | 89 | #[get("/heightmap_detail////")] 90 | fn heightmap_detail<'a>(swlat: f64, swlon: f64, nelat: f64, nelon: f64) -> Response<'a> { 91 | let heat_path = WISP.read().heat_path.clone(); 92 | let image_buffer = tiler::heightmap_detail(swlat, swlon, nelat, nelon, &heat_path); 93 | let mut response_build = Response::build(); 94 | response_build.header(ContentType::PNG); 95 | response_build.status(Status::Ok); 96 | response_build.streamed_body(std::io::Cursor::new(image_buffer)); 97 | response_build.finalize() 98 | } 99 | 100 | #[get("/losmap/////")] 101 | fn losmap<'a>(swlat: f64, swlon: f64, nelat: f64, nelon: f64, cpe_height: f64) -> Response<'a> { 102 | let heat_path = WISP.read().heat_path.clone(); 103 | let image_buffer = tiler::losmap_tile(swlat, swlon, nelat, nelon, cpe_height, &heat_path); 104 | let mut response_build = Response::build(); 105 | response_build.header(ContentType::PNG); 106 | response_build.status(Status::Ok); 107 | response_build.streamed_body(std::io::Cursor::new(image_buffer)); 108 | response_build.finalize() 109 | } 110 | 111 | #[get("/signalmap///////")] 112 | fn signalmap<'a>( 113 | swlat: f64, 114 | swlon: f64, 115 | nelat: f64, 116 | nelon: f64, 117 | cpe_height: f64, 118 | frequency: f64, 119 | link_budget: f64, 120 | ) -> Response<'a> { 121 | let heat_path = WISP.read().heat_path.clone(); 122 | let image_buffer = tiler::signalmap_tile( 123 | swlat, 124 | swlon, 125 | nelat, 126 | nelon, 127 | cpe_height, 128 | frequency, 129 | &heat_path, 130 | link_budget, 131 | ); 132 | let mut response_build = Response::build(); 133 | response_build.header(ContentType::PNG); 134 | response_build.status(Status::Ok); 135 | response_build.streamed_body(std::io::Cursor::new(image_buffer)); 136 | response_build.finalize() 137 | } 138 | 139 | #[get("/signalmap_detail////")] 140 | fn signalmap_detail<'a>(swlat: f64, swlon: f64, nelat: f64, nelon: f64) -> Response<'a> { 141 | let heat_path = WISP.read().heat_path.clone(); 142 | let image_buffer = tiler::signalmap_detail(swlat, swlon, nelat, nelon, &heat_path); 143 | let mut response_build = Response::build(); 144 | response_build.header(ContentType::PNG); 145 | response_build.status(Status::Ok); 146 | response_build.streamed_body(std::io::Cursor::new(image_buffer)); 147 | response_build.finalize() 148 | } 149 | 150 | #[get( 151 | "/mapclick/////", 152 | format = "json" 153 | )] 154 | fn map_click<'a>( 155 | lat: f64, 156 | lon: f64, 157 | frequency: f64, 158 | cpe_height: f64, 159 | link_budget: f64, 160 | ) -> Json { 161 | let heat_path = WISP.read().heat_path.clone(); 162 | let pos = LatLon::new(lat, lon); 163 | Json(los::evaluate_tower_click( 164 | &pos, 165 | Frequency::with_ghz(frequency), 166 | cpe_height, 167 | &heat_path, 168 | link_budget, 169 | )) 170 | } 171 | 172 | #[get("/3d//", format = "json")] 173 | fn tile3d(lat: f64, lon: f64) -> Json { 174 | let heat_path = WISP.read().heat_path.clone(); 175 | Json(tiler::build_3d_heightmap(lat, lon, &heat_path)) 176 | } 177 | 178 | #[get("/losplot/////")] 179 | fn los_plot<'a>( 180 | lat: f64, 181 | lon: f64, 182 | tower_name: String, 183 | cpe_height: f64, 184 | frequency: f64, 185 | ) -> Json { 186 | let tower_index = WISP 187 | .read() 188 | .towers 189 | .iter() 190 | .enumerate() 191 | .find(|(_i, t)| t.name == tower_name) 192 | .map(|(i, _)| i) 193 | .unwrap(); 194 | let heat_path = WISP.read().heat_path.clone(); 195 | let pos = LatLon::new(lat, lon); 196 | Json(los::los_plot( 197 | &pos, 198 | tower_index, 199 | cpe_height, 200 | Frequency::with_ghz(frequency), 201 | &heat_path, 202 | )) 203 | } 204 | 205 | fn main() { 206 | let wisp_def = load_wisp(); 207 | 208 | // Do some replace magic to place the correct key and version in the HTML 209 | *INDEX_FINAL.write() = INDEX_HTML 210 | .replace("_BANNER_", "Bracket-Heat 0.1") 211 | .replace("_GMAPKEY_", &GOOGLE_MAPS_API_KEY.replace("\n", "")) 212 | .replace("_CENTER_LAT_", &wisp_def.center.0.to_string()) 213 | .replace("_CENTER_LON_", &wisp_def.center.1.to_string()) 214 | .replace("_MAP_ZOOM_", &wisp_def.map_zoom.to_string()) 215 | .replace("_ISP_NAME_", &format!("\"{}\"", &wisp_def.name)); 216 | 217 | *ADVISOR_FINAL.write() = ADVISOR_HTML 218 | .replace("_BANNER_", "Bracket-Heat 0.1") 219 | .replace("_GMAPKEY_", &GOOGLE_MAPS_API_KEY.replace("\n", "")) 220 | .replace("_CENTER_LAT_", &wisp_def.center.0.to_string()) 221 | .replace("_CENTER_LON_", &wisp_def.center.1.to_string()) 222 | .replace("_MAP_ZOOM_", &wisp_def.map_zoom.to_string()) 223 | .replace("_ISP_NAME_", &format!("\"{}\"", &wisp_def.name)); 224 | 225 | let config = Config::build(Environment::Production) 226 | .port(wisp_def.listen_port) 227 | .finalize() 228 | .unwrap(); 229 | 230 | *WISP.write() = wisp_def; 231 | 232 | rocket::custom(config) 233 | .mount( 234 | "/", 235 | routes![ 236 | index, 237 | advisor, 238 | tower_marker, 239 | towers, 240 | heightmap, 241 | heightmap_detail, 242 | losmap, 243 | signalmap, 244 | signalmap_detail, 245 | map_click, 246 | pngegg, 247 | los_plot, 248 | budgets, 249 | three_js, 250 | loc_info, 251 | tile3d, 252 | ], 253 | ) 254 | .launch(); 255 | } 256 | -------------------------------------------------------------------------------- /bracket-heat/src/tiler/heightmap.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const TILE_SIZE: u32 = 256; 2 | pub(crate) const DETAIL_SIZE: u32 = 256; 3 | 4 | use std::io::Read; 5 | use std::io::{Cursor, Seek, SeekFrom}; 6 | 7 | use rf_signal_algorithms::{height_tile_elevations, lat_lon_tile}; 8 | 9 | pub fn heightmap_tile(swlat: f64, swlon: f64, nelat: f64, nelon: f64, heat_path: &str) -> Vec { 10 | let mut image_data = vec![0u8; TILE_SIZE as usize * TILE_SIZE as usize * 4]; 11 | 12 | let points = lat_lon_tile(swlat, swlon, nelat, nelon, TILE_SIZE as usize); 13 | let heights = height_tile_elevations(&points, heat_path); 14 | 15 | let min_height = heights 16 | .iter() 17 | .filter(|a| **a > 0.0) 18 | .min_by(|a, b| a.partial_cmp(b).unwrap()) 19 | .unwrap_or(&0.0); 20 | let max_height = heights 21 | .iter() 22 | .max_by(|a, b| a.partial_cmp(b).unwrap()) 23 | .unwrap_or(&1.0); 24 | let h_scale = 255.0 / (*max_height as f64 - *min_height as f64); 25 | 26 | heights.iter().enumerate().for_each(|(i, h)| { 27 | if *h > 0.0 { 28 | let x = points[i].0; 29 | let y = points[i].1; 30 | let base = ((((TILE_SIZE - 1) - y) as usize * 4 * TILE_SIZE as usize) 31 | + (x as usize * 4)) as usize; 32 | let n = ((*h as f64 - *min_height as f64) * h_scale) as u8; 33 | image_data[base] = n; 34 | image_data[base + 1] = n; 35 | image_data[base + 2] = n; 36 | image_data[base + 3] = 128; 37 | } 38 | }); 39 | 40 | let mut w = Cursor::new(Vec::new()); 41 | { 42 | let mut encoder = png::Encoder::new(&mut w, TILE_SIZE as _, TILE_SIZE as _); 43 | encoder.set_color(png::ColorType::RGBA); 44 | encoder.set_depth(png::BitDepth::Eight); 45 | let mut writer = encoder.write_header().unwrap(); 46 | writer.write_image_data(&image_data).unwrap(); 47 | } 48 | let mut out = Vec::new(); 49 | w.seek(SeekFrom::Start(0)).unwrap(); 50 | w.read_to_end(&mut out).unwrap(); 51 | out 52 | } 53 | 54 | pub fn heightmap_detail( 55 | swlat: f64, 56 | swlon: f64, 57 | nelat: f64, 58 | nelon: f64, 59 | heat_path: &str, 60 | ) -> Vec { 61 | let mut image_data = vec![0u8; DETAIL_SIZE as usize * DETAIL_SIZE as usize * 4]; 62 | 63 | let points = lat_lon_tile(swlat, swlon, nelat, nelon, DETAIL_SIZE as usize); 64 | let heights = height_tile_elevations(&points, heat_path); 65 | 66 | let min_height = heights 67 | .iter() 68 | .filter(|a| **a > 0.0) 69 | .min_by(|a, b| a.partial_cmp(b).unwrap()) 70 | .unwrap_or(&0.0); 71 | let max_height = heights 72 | .iter() 73 | .max_by(|a, b| a.partial_cmp(b).unwrap()) 74 | .unwrap_or(&1.0); 75 | let h_scale = 255.0 / (*max_height as f64 - *min_height as f64); 76 | 77 | heights.iter().enumerate().for_each(|(i, h)| { 78 | if *h > 0.0 { 79 | let x = points[i].0; 80 | let y = points[i].1; 81 | let base = ((((DETAIL_SIZE - 1) - y) as usize * 4 * DETAIL_SIZE as usize) 82 | + (x as usize * 4)) as usize; 83 | let n = ((*h as f64 - *min_height as f64) * h_scale) as u8; 84 | if base + 3 < image_data.len() { 85 | image_data[base] = n; 86 | image_data[base + 1] = n; 87 | image_data[base + 2] = n; 88 | image_data[base + 3] = 255; 89 | } 90 | } 91 | }); 92 | 93 | let mut w = Cursor::new(Vec::new()); 94 | { 95 | let mut encoder = png::Encoder::new(&mut w, DETAIL_SIZE as _, DETAIL_SIZE as _); 96 | encoder.set_color(png::ColorType::RGBA); 97 | encoder.set_depth(png::BitDepth::Eight); 98 | let mut writer = encoder.write_header().unwrap(); 99 | writer.write_image_data(&image_data).unwrap(); 100 | } 101 | let mut out = Vec::new(); 102 | w.seek(SeekFrom::Start(0)).unwrap(); 103 | w.read_to_end(&mut out).unwrap(); 104 | out 105 | } 106 | -------------------------------------------------------------------------------- /bracket-heat/src/tiler/losmap.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const TILE_SIZE: u32 = 64; 2 | 3 | use std::io::Read; 4 | use std::io::{Cursor, Seek, SeekFrom}; 5 | 6 | use crate::WISP; 7 | use rf_signal_algorithms::{ 8 | bheat::heat_altitude, geometry::haversine_distance, has_line_of_sight, lat_lon_path_10m, 9 | lat_lon_tile, lat_lon_vec_to_heights, Distance, LatLon, 10 | }; 11 | 12 | pub fn losmap_tile( 13 | swlat: f64, 14 | swlon: f64, 15 | nelat: f64, 16 | nelon: f64, 17 | cpe_height: f64, 18 | heat_path: &str, 19 | ) -> Vec { 20 | let mut image_data = vec![0u8; TILE_SIZE as usize * TILE_SIZE as usize * 4]; 21 | let wisp_reader = WISP.read(); 22 | 23 | let points = lat_lon_tile(swlat, swlon, nelat, nelon, TILE_SIZE as usize); 24 | points.iter().for_each(|(x, y, p)| { 25 | let is_visible = wisp_reader 26 | .towers 27 | .iter() 28 | .filter(|t| haversine_distance(p, &LatLon::new(t.lat, t.lon)).as_km() < t.max_range_km) 29 | .map(|t| { 30 | let base_tower_height = heat_altitude(t.lat, t.lon, heat_path) 31 | .unwrap_or((Distance::with_meters(0.0), Distance::with_meters(0.0))) 32 | .0 33 | .as_meters(); 34 | let path = lat_lon_path_10m(p, &LatLon::new(t.lat, t.lon)); 35 | let los_path = lat_lon_vec_to_heights(&path, heat_path); 36 | has_line_of_sight( 37 | &los_path, 38 | Distance::with_meters(cpe_height), 39 | Distance::with_meters(t.height_meters + base_tower_height), 40 | ) 41 | }) 42 | .filter(|has_los| *has_los) 43 | .count() 44 | > 0; 45 | 46 | if is_visible { 47 | let base = ((((TILE_SIZE - 1) - *y) as usize * 4 * TILE_SIZE as usize) 48 | + ((*x) as usize * 4)) as usize; 49 | image_data[base] = 0; 50 | image_data[base + 1] = 255; 51 | image_data[base + 2] = 0; 52 | image_data[base + 3] = 128; 53 | } 54 | }); 55 | 56 | let mut w = Cursor::new(Vec::new()); 57 | { 58 | let mut encoder = png::Encoder::new(&mut w, TILE_SIZE as _, TILE_SIZE as _); 59 | encoder.set_color(png::ColorType::RGBA); 60 | encoder.set_depth(png::BitDepth::Eight); 61 | let mut writer = encoder.write_header().unwrap(); 62 | writer.write_image_data(&image_data).unwrap(); 63 | } 64 | let mut out = Vec::new(); 65 | w.seek(SeekFrom::Start(0)).unwrap(); 66 | w.read_to_end(&mut out).unwrap(); 67 | out 68 | } 69 | -------------------------------------------------------------------------------- /bracket-heat/src/tiler/mod.rs: -------------------------------------------------------------------------------- 1 | mod heightmap; 2 | pub(crate) use heightmap::*; 3 | mod losmap; 4 | pub(crate) use losmap::losmap_tile; 5 | mod signalmap; 6 | pub(crate) use signalmap::*; 7 | mod three_d; 8 | pub(crate) use three_d::*; 9 | -------------------------------------------------------------------------------- /bracket-heat/src/tiler/signalmap.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const TILE_SIZE: u32 = 64; 2 | pub(crate) const DETAIL_SIZE: u32 = 16; 3 | 4 | use std::io::Read; 5 | use std::io::{Cursor, Seek, SeekFrom}; 6 | 7 | use crate::WISP; 8 | use rf_signal_algorithms::{ 9 | bheat::heat_altitude, free_space_path_loss_db, geometry::haversine_distance, 10 | itwom_point_to_point, lat_lon_path_10m, lat_lon_path_1m, lat_lon_tile, lat_lon_vec_to_heights, 11 | Distance, Frequency, LatLon, PTPClimate, PTPPath, 12 | }; 13 | 14 | pub fn signalmap_tile( 15 | swlat: f64, 16 | swlon: f64, 17 | nelat: f64, 18 | nelon: f64, 19 | cpe_height: f64, 20 | frequency: f64, 21 | heat_path: &str, 22 | link_budget: f64, 23 | ) -> Vec { 24 | let mut image_data = vec![0u8; TILE_SIZE as usize * TILE_SIZE as usize * 4]; 25 | let wisp_reader = WISP.read(); 26 | 27 | let points = lat_lon_tile(swlat, swlon, nelat, nelon, TILE_SIZE as usize); 28 | points.iter().for_each(|(x, y, p)| { 29 | let dbloss = wisp_reader 30 | .towers 31 | .iter() 32 | .filter(|t| haversine_distance(p, &LatLon::new(t.lat, t.lon)).as_km() < t.max_range_km) 33 | .map(|t| { 34 | let base_tower_height = heat_altitude(t.lat, t.lon, heat_path) 35 | .unwrap_or((Distance::with_meters(0.0), Distance::with_meters(0.0))) 36 | .0 37 | .as_meters(); 38 | let path = lat_lon_path_10m(&LatLon::new(t.lat, t.lon), p); 39 | let los_path = lat_lon_vec_to_heights(&path, heat_path); 40 | let d = haversine_distance(p, &LatLon::new(t.lat, t.lon)); 41 | if d.as_meters() < 50.0 { 42 | free_space_path_loss_db(Frequency::with_ghz(frequency), d) 43 | } else { 44 | let mut path_as_distances: Vec = 45 | los_path.iter().map(|d| *d as f64).collect(); 46 | path_as_distances[0] = base_tower_height; 47 | let mut terrain_path = PTPPath::new( 48 | path_as_distances, 49 | Distance::with_meters(t.height_meters), 50 | Distance::with_meters(cpe_height), 51 | Distance::with_meters(10.0), 52 | ) 53 | .unwrap(); 54 | 55 | let lr = itwom_point_to_point( 56 | &mut terrain_path, 57 | PTPClimate::default(), 58 | Frequency::with_ghz(frequency), 59 | 0.5, 60 | 0.5, 61 | 1, 62 | ); 63 | 64 | lr.dbloss 65 | } 66 | }) 67 | .min_by(|a, b| a.partial_cmp(b).unwrap()) 68 | .unwrap_or(400.0); 69 | 70 | let temporary_link_budget = link_budget - dbloss; 71 | if temporary_link_budget > -90.0 { 72 | //println!("Link budget: {}", temporary_link_budget); 73 | let color = ramp(&temporary_link_budget); 74 | let base = ((((TILE_SIZE - 1) - *y) as usize * 4 * TILE_SIZE as usize) 75 | + ((*x) as usize * 4)) as usize; 76 | image_data[base] = color.0; 77 | image_data[base + 1] = color.1; 78 | image_data[base + 2] = color.2; 79 | image_data[base + 3] = color.3; 80 | } 81 | }); 82 | 83 | let mut w = Cursor::new(Vec::new()); 84 | { 85 | let mut encoder = png::Encoder::new(&mut w, TILE_SIZE as _, TILE_SIZE as _); 86 | encoder.set_color(png::ColorType::RGBA); 87 | encoder.set_depth(png::BitDepth::Eight); 88 | let mut writer = encoder.write_header().unwrap(); 89 | writer.write_image_data(&image_data).unwrap(); 90 | } 91 | let mut out = Vec::new(); 92 | w.seek(SeekFrom::Start(0)).unwrap(); 93 | w.read_to_end(&mut out).unwrap(); 94 | out 95 | } 96 | 97 | fn ramp(rssi: &f64) -> (u8, u8, u8, u8) { 98 | let rssi = f64::min(0.0, *rssi); 99 | let rrsi_abs = rssi.abs() as u8; 100 | //println!("{} .. {}", rssi, rrsi_abs); 101 | 102 | if rrsi_abs < 50 { 103 | (0, 255, 0, 128) 104 | } else if rrsi_abs < 55 { 105 | (64, 255, 0, 192) 106 | } else if rrsi_abs < 59 { 107 | (255, 255, 0, 192) 108 | } else if rrsi_abs < 63 { 109 | (0, 255, 0, 192) 110 | } else if rrsi_abs < 68 { 111 | (255, 255, 0, 192) 112 | } else if rrsi_abs < 75 { 113 | (255, 0, 0, 192) 114 | } else { 115 | (255, 0, 255, 192) 116 | } 117 | 118 | //COLOR_RAMP[rrsi_abs as usize - 55] 119 | //(255 - (rrsi_abs * 2), 0, 0) 120 | } 121 | 122 | pub fn signalmap_detail( 123 | swlat: f64, 124 | swlon: f64, 125 | nelat: f64, 126 | nelon: f64, 127 | heat_path: &str, 128 | ) -> Vec { 129 | let mut image_data = vec![0u8; DETAIL_SIZE as usize * DETAIL_SIZE as usize * 4]; 130 | let wisp_reader = WISP.read(); 131 | let cpe_height = 3.0; 132 | let link_budget = 52.0 + 19.0; 133 | let frequency = 3.6; 134 | 135 | let points = lat_lon_tile(swlat, swlon, nelat, nelon, DETAIL_SIZE as usize); 136 | points.iter().for_each(|(x, y, p)| { 137 | //println!("Point"); 138 | let dbloss = wisp_reader 139 | .towers 140 | .iter() 141 | .filter(|t| haversine_distance(p, &LatLon::new(t.lat, t.lon)).as_km() < t.max_range_km) 142 | .map(|t| { 143 | let base_tower_height = heat_altitude(t.lat, t.lon, heat_path) 144 | .unwrap_or((Distance::with_meters(0.0), Distance::with_meters(0.0))) 145 | .0 146 | .as_meters(); 147 | let path = lat_lon_path_1m(&LatLon::new(t.lat, t.lon), p); 148 | let los_path = lat_lon_vec_to_heights(&path, heat_path); 149 | let d = haversine_distance(p, &LatLon::new(t.lat, t.lon)); 150 | if d.as_meters() < 50.0 { 151 | free_space_path_loss_db(Frequency::with_ghz(frequency), d) 152 | } else { 153 | let mut path_as_distances: Vec = 154 | los_path.iter().map(|d| *d as f64).collect(); 155 | path_as_distances[0] = base_tower_height; 156 | let mut terrain_path = PTPPath::new( 157 | path_as_distances, 158 | Distance::with_meters(t.height_meters), 159 | Distance::with_meters(cpe_height), 160 | Distance::with_meters(10.0), 161 | ) 162 | .unwrap(); 163 | 164 | let lr = itwom_point_to_point( 165 | &mut terrain_path, 166 | PTPClimate::default(), 167 | Frequency::with_ghz(frequency), 168 | 0.5, 169 | 0.5, 170 | 1, 171 | ); 172 | 173 | //println!("->{}", lr.dbloss); 174 | lr.dbloss 175 | } 176 | }) 177 | .min_by(|a, b| a.partial_cmp(b).unwrap()) 178 | .unwrap_or(400.0); 179 | 180 | //println!("picked {}", dbloss); 181 | let temporary_link_budget = link_budget - dbloss; 182 | //println!("Link budget: {}", temporary_link_budget); 183 | //let color = ramp(&temporary_link_budget); 184 | let color = if temporary_link_budget > -70.0 { 185 | (128, 255, 128, 128) 186 | } else if temporary_link_budget > -70.0 { 187 | (0, 255, 0, 128) 188 | } else if temporary_link_budget > -80.0 { 189 | (255, 255, 0, 128) 190 | } else { 191 | (255, 0, 0, 64) 192 | }; 193 | let base = ((((DETAIL_SIZE - 1) - *y) as usize * 4 * DETAIL_SIZE as usize) 194 | + ((*x) as usize * 4)) as usize; 195 | if base + 3 < image_data.len() { 196 | image_data[base] = color.0; 197 | image_data[base + 1] = color.1; 198 | image_data[base + 2] = color.2; 199 | image_data[base + 3] = 192; 200 | } 201 | }); 202 | 203 | //println!("Baking image"); 204 | let mut w = Cursor::new(Vec::new()); 205 | { 206 | let mut encoder = png::Encoder::new(&mut w, DETAIL_SIZE as _, DETAIL_SIZE as _); 207 | encoder.set_color(png::ColorType::RGBA); 208 | encoder.set_depth(png::BitDepth::Eight); 209 | let mut writer = encoder.write_header().unwrap(); 210 | writer.write_image_data(&image_data).unwrap(); 211 | } 212 | let mut out = Vec::new(); 213 | w.seek(SeekFrom::Start(0)).unwrap(); 214 | w.read_to_end(&mut out).unwrap(); 215 | out 216 | } 217 | -------------------------------------------------------------------------------- /bracket-heat/src/tiler/three_d.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{bheat::heat_altitude, Distance}; 2 | use serde::{Deserialize, Serialize}; 3 | pub(crate) const TILE_SIZE: u32 = 256; 4 | const HEIGHT_DIVISOR: f32 = 10.0; 5 | 6 | #[derive(Clone, Serialize)] 7 | pub struct TerrainBlob { 8 | pub width: u32, 9 | pub height: u32, 10 | pub terrain: Vec, 11 | pub clutter: Vec, 12 | } 13 | 14 | pub fn build_3d_heightmap(lat: f64, lon: f64, heat_path: &str) -> TerrainBlob { 15 | let mut terrain = Vec::new(); 16 | let mut clutter = Vec::new(); 17 | const STEP: f64 = 0.001; 18 | 19 | let mut height = 0; 20 | let mut lt = lat - 0.2; 21 | while lt < lat + 0.2 { 22 | let mut ln = lon - 0.2; 23 | while ln < lon + 0.2 { 24 | let h1 = heat_altitude(lt, ln, heat_path) 25 | .unwrap_or((Distance::with_meters(0), Distance::with_meters(0))); 26 | terrain.push(h1.0.as_meters() as f32 / HEIGHT_DIVISOR); 27 | clutter.push(h1.1.as_meters() as f32 / HEIGHT_DIVISOR); 28 | 29 | ln += STEP; 30 | } 31 | lt += STEP; 32 | height += 1; 33 | } 34 | 35 | TerrainBlob { 36 | width: (terrain.len() / height) as u32, 37 | height: height as u32, 38 | terrain, 39 | clutter, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rf-signal-algorithms/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rf-signal-algorithms" 3 | version = "0.1.0" 4 | authors = ["Herbert Wolverson "] 5 | edition = "2018" 6 | description = "Rust wrapper for https://github.com/Cloud-RF/Signal-Server signal algorithms." 7 | readme = "README.md" 8 | license = "GPL2" 9 | repository = "https://github.com/thebracket/rf-signals" 10 | keywords = ["rf", "radio", "longley-rice"] 11 | categories = [ "algorithms" ] 12 | publish = false 13 | 14 | #default-features = [ "srtm" ] 15 | 16 | #[features] 17 | #srtm = [ "geo", "lru", "lazy_static", "parking_lot", "memmap" ] 18 | 19 | [dependencies] 20 | num-complex = "0.4.0" 21 | geo = { version = "0.17.0", optional = false } 22 | lazy_static = { version = "1.4.0", optional = false } 23 | parking_lot = { version = "0.11.0", optional = false } 24 | memmap = { version = "0.7.0", optional = false } 25 | bytemuck = "1.4.1" 26 | quadtree-f32 = "0.3.0" 27 | rayon = "1.5.0" 28 | lru = "0.6.5" 29 | 30 | [dev-dependencies] 31 | float-cmp = "0.8" 32 | 33 | [build-dependencies] 34 | bindgen = "0.58.1" 35 | cc = "1.0.66" 36 | -------------------------------------------------------------------------------- /rf-signal-algorithms/README.md: -------------------------------------------------------------------------------- 1 | # RF_Signals 2 | 3 | This is a Rust wrapper for [Cloud-RF/Signal Server](https://github.com/Cloud-RF/Signal-Server) algorithms. In turn, this is based upon SPLAT! by Alex Farrant and John A. Magliacane. 4 | 5 | I've retained the GPL2 license, because the parent requires it. I needed this for work, I'm hoping someone finds it useful. 6 | 7 | ## Support Algorithms 8 | 9 | This crate provides Rust implementations of a number of algorithms that are useful in wireless calculations: 10 | 11 | * ITM3/Longley-Rice - the power behind Splat! and other functions. 12 | * HATA with the COST123 extension. 13 | * ECC33. 14 | * EGLI. 15 | * HATA. 16 | * Plane Earth. 17 | * SOIL. 18 | * SUI. 19 | 20 | Additionally, helper functions provide: 21 | 22 | * Basic Free-Space Path Loss (FSPL) calculation. 23 | * Fresnel size calculation. 24 | 25 | ## SRTM .hgt Reader 26 | 27 | There's also an SRTM .hgt reader. You can get these from various places for pretty much the whole planet. See Radio Mobile for details. This will eventually be in its own feature. For now, it maintains an LRU cache of height tiles and tries to find the best resolution available to answer an elevation query. 28 | 29 | An example query: 30 | 31 | ```rust 32 | let loc = LatLon::new(38.947775, -92.323385); 33 | let altitude = get_altitude(&loc, "resources"); 34 | ``` 35 | 36 | This requires the `hgt` files from the `resources` directory to function. 37 | 38 | ## Porting Status 39 | 40 | All algorithms started out in Cloud_RF's Signal Server (in C or C++) and were ported to Rust. 41 | 42 | |Algorithm |Status | 43 | |------------|---------| 44 | |COST/HATA |Ported to Pure Rust| 45 | |ECC33 |Ported to Pure Rust| 46 | |EGLI |Ported to Pure Rust| 47 | |Fresnel Zone|Pure Rust (not in original)| 48 | |FSPL |Pure Rust| 49 | |HATA |Ported to Pure Rust| 50 | |ITWOM3 |Ported to Pure Rust| 51 | |Plane Earth |Ported to Pure Rust| 52 | |SOIL |Ported to Pure Rust| 53 | |SUI |Ported to Pure Rust| 54 | 55 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/cost.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{cost_path_loss, Distance, EstimateMode, Frequency}; 2 | 3 | const DISTANCE_METERS: f32 = 1000.0; 4 | const XMIT_HEIGHT: f32 = 30.0; 5 | const RECV_HEIGHT: f32 = 5.0; 6 | 7 | fn main() { 8 | println!( 9 | "Cost Urban : {}", 10 | cost_path_loss( 11 | Frequency::with_mhz(1700.0), 12 | Distance::with_meters(XMIT_HEIGHT), 13 | Distance::with_meters(RECV_HEIGHT), 14 | Distance::with_meters(DISTANCE_METERS), 15 | EstimateMode::Urban 16 | ) 17 | .unwrap() 18 | ); 19 | println!( 20 | "Cost Suburban : {}", 21 | cost_path_loss( 22 | Frequency::with_mhz(1700.0), 23 | Distance::with_meters(XMIT_HEIGHT), 24 | Distance::with_meters(RECV_HEIGHT), 25 | Distance::with_meters(DISTANCE_METERS), 26 | EstimateMode::Suburban 27 | ) 28 | .unwrap() 29 | ); 30 | println!( 31 | "Cost Open : {}", 32 | cost_path_loss( 33 | Frequency::with_mhz(1700.0), 34 | Distance::with_meters(XMIT_HEIGHT), 35 | Distance::with_meters(RECV_HEIGHT), 36 | Distance::with_meters(DISTANCE_METERS), 37 | EstimateMode::Rural 38 | ) 39 | .unwrap() 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/ecc33.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{ecc33_path_loss, Distance, EstimateMode, Frequency}; 2 | 3 | const DISTANCE_METERS: f32 = 1000.0; 4 | const XMIT_HEIGHT: f32 = 30.0; 5 | const RECV_HEIGHT: f32 = 5.0; 6 | 7 | fn main() { 8 | println!( 9 | "ECC33 Mode 1 : {}", 10 | ecc33_path_loss( 11 | Frequency::with_mhz(500.0), 12 | Distance::with_meters(XMIT_HEIGHT), 13 | Distance::with_meters(RECV_HEIGHT), 14 | Distance::with_meters(DISTANCE_METERS), 15 | EstimateMode::Urban 16 | ) 17 | .unwrap() 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/egli.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{egli_path_loss, Distance, Frequency}; 2 | 3 | const DISTANCE_METERS: f32 = 1000.0; 4 | const XMIT_HEIGHT: f32 = 30.0; 5 | const RECV_HEIGHT: f32 = 5.0; 6 | const FREQ_MHZ: f32 = 1500.0; 7 | 8 | fn main() { 9 | println!( 10 | "EGLI : {}", 11 | egli_path_loss( 12 | Frequency::with_mhz(FREQ_MHZ), 13 | Distance::with_meters(XMIT_HEIGHT), 14 | Distance::with_meters(RECV_HEIGHT), 15 | Distance::with_meters(DISTANCE_METERS), 16 | ) 17 | .unwrap() 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/fspl.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{free_space_path_loss_db, Distance, Frequency}; 2 | 3 | const DISTANCE_METERS: f32 = 1000.0; 4 | const FREQ_MHZ: f32 = 1500.0; 5 | 6 | fn main() { 7 | println!( 8 | "Free Space Loss : {}", 9 | free_space_path_loss_db( 10 | Frequency::with_mhz(FREQ_MHZ), 11 | Distance::with_meters(DISTANCE_METERS) 12 | ) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/hata.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{hata_path_loss, Distance, EstimateMode, Frequency}; 2 | 3 | const DISTANCE_METERS: f32 = 1000.0; 4 | const XMIT_HEIGHT: f32 = 30.0; 5 | const RECV_HEIGHT: f32 = 5.0; 6 | const FREQ_MHZ: f32 = 1500.0; 7 | 8 | fn main() { 9 | println!( 10 | "HATA Mode 1 : {}", 11 | hata_path_loss( 12 | Frequency::with_mhz(FREQ_MHZ), 13 | Distance::with_meters(XMIT_HEIGHT), 14 | Distance::with_meters(RECV_HEIGHT), 15 | Distance::with_meters(DISTANCE_METERS), 16 | EstimateMode::Urban 17 | ) 18 | .unwrap() 19 | ); 20 | println!( 21 | "HATA Mode 2 : {}", 22 | hata_path_loss( 23 | Frequency::with_mhz(FREQ_MHZ), 24 | Distance::with_meters(XMIT_HEIGHT), 25 | Distance::with_meters(RECV_HEIGHT), 26 | Distance::with_meters(DISTANCE_METERS), 27 | EstimateMode::Suburban 28 | ) 29 | .unwrap() 30 | ); 31 | println!( 32 | "HATA Mode 3 : {}", 33 | hata_path_loss( 34 | Frequency::with_mhz(FREQ_MHZ), 35 | Distance::with_meters(XMIT_HEIGHT), 36 | Distance::with_meters(RECV_HEIGHT), 37 | Distance::with_meters(DISTANCE_METERS), 38 | EstimateMode::Rural 39 | ) 40 | .unwrap() 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/itm3.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{itwom_point_to_point, Distance, Frequency, PTPClimate, PTPPath}; 2 | 3 | const FREQ_MHZ: f32 = 5840.0; 4 | const XMIT_HEIGHT: f32 = 3.0; 5 | const RECV_HEIGHT: f32 = 30.0; 6 | const TERRAIN_STEP: f64 = 10.0; 7 | 8 | fn main() { 9 | let mut terrain_path = PTPPath::new( 10 | vec![1.0; 200], 11 | Distance::with_meters(XMIT_HEIGHT), 12 | Distance::with_meters(RECV_HEIGHT), 13 | Distance::with_meters(TERRAIN_STEP), 14 | ) 15 | .unwrap(); 16 | 17 | let itwom_test = itwom_point_to_point( 18 | &mut terrain_path, 19 | PTPClimate::default(), 20 | Frequency::with_mhz(FREQ_MHZ), 21 | 0.5, 22 | 0.5, 23 | 1, 24 | ); 25 | 26 | println!("ITWOM3 Loss : {}", itwom_test.dbloss); 27 | println!("ITWOM3 Mode : {}", itwom_test.mode); 28 | println!("ITWOM3 Error # : {}", itwom_test.error_num); 29 | // Ideally 0. 30 | } 31 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/pel.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{plane_earth_path_loss, Distance}; 2 | 3 | const DISTANCE_METERS: f32 = 1000.0; 4 | const XMIT_HEIGHT: f32 = 30.0; 5 | const RECV_HEIGHT: f32 = 5.0; 6 | 7 | fn main() { 8 | println!( 9 | "Plane Earth : {}", 10 | plane_earth_path_loss( 11 | Distance::with_meters(XMIT_HEIGHT), 12 | Distance::with_meters(RECV_HEIGHT), 13 | Distance::with_meters(DISTANCE_METERS) 14 | ) 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/soil.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{soil_path_loss, Distance, Frequency}; 2 | 3 | const DISTANCE_METERS: f32 = 1000.0; 4 | const FREQ_MHZ: f32 = 1500.0; 5 | 6 | fn main() { 7 | for t in 1..=15 { 8 | println!( 9 | "Soil Mode {} : {}", 10 | t, 11 | soil_path_loss( 12 | Frequency::with_mhz(FREQ_MHZ), 13 | Distance::with_meters(DISTANCE_METERS), 14 | t as f64 15 | ) 16 | .unwrap() 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rf-signal-algorithms/examples/sui.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::{sui_path_loss, Distance, EstimateMode, Frequency}; 2 | 3 | const DISTANCE_METERS: f32 = 1000.0; 4 | const FREQ_MHZ: f32 = 1500.0; 5 | const XMIT_HEIGHT: f32 = 30.0; 6 | const RECV_HEIGHT: f32 = 5.0; 7 | 8 | fn main() { 9 | println!( 10 | "SUI Mode 1 : {}", 11 | sui_path_loss( 12 | Frequency::with_mhz(FREQ_MHZ + 2000.0), 13 | Distance::with_meters(XMIT_HEIGHT), 14 | Distance::with_meters(RECV_HEIGHT), 15 | Distance::with_meters(DISTANCE_METERS), 16 | EstimateMode::Urban 17 | ) 18 | .unwrap() 19 | ); 20 | println!( 21 | "SUI Mode 2 : {}", 22 | sui_path_loss( 23 | Frequency::with_mhz(FREQ_MHZ + 2000.0), 24 | Distance::with_meters(XMIT_HEIGHT), 25 | Distance::with_meters(RECV_HEIGHT), 26 | Distance::with_meters(DISTANCE_METERS), 27 | EstimateMode::Suburban 28 | ) 29 | .unwrap() 30 | ); 31 | println!( 32 | "SUI Mode 3 : {}", 33 | sui_path_loss( 34 | Frequency::with_mhz(FREQ_MHZ + 2000.0), 35 | Distance::with_meters(XMIT_HEIGHT), 36 | Distance::with_meters(RECV_HEIGHT), 37 | Distance::with_meters(DISTANCE_METERS), 38 | EstimateMode::Rural 39 | ) 40 | .unwrap() 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /rf-signal-algorithms/resources/1/N38W093T97.hgt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/rf-signal-algorithms/resources/1/N38W093T97.hgt -------------------------------------------------------------------------------- /rf-signal-algorithms/resources/3/N38W093.hgt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/rf-signal-algorithms/resources/3/N38W093.hgt -------------------------------------------------------------------------------- /rf-signal-algorithms/resources/third/N38W093.hgt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/rf-signal-algorithms/resources/third/N38W093.hgt -------------------------------------------------------------------------------- /rf-signal-algorithms/src/geometry/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{Distance, LatLon}; 2 | 3 | pub fn haversine_distance(src: &LatLon, dst: &LatLon) -> Distance { 4 | use geo::algorithm::haversine_distance::HaversineDistance; 5 | Distance::with_meters(src.to_point().haversine_distance(&dst.to_point())) 6 | } 7 | 8 | pub fn haversine_intermediate(src: &LatLon, dst: &LatLon, extent: f64) -> LatLon { 9 | use geo::algorithm::haversine_intermediate::HaversineIntermediate; 10 | let start = src.to_radians_point(); 11 | let end = dst.to_radians_point(); 12 | let hi = start.haversine_intermediate(&end, extent); 13 | LatLon::from_radians_point(&hi) 14 | } 15 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod units; 2 | pub use units::{Distance, Frequency}; 3 | mod rfcalc; 4 | pub use rfcalc::*; 5 | 6 | //#[cfg(feature = "srtm")] 7 | mod mapping; 8 | 9 | //#[cfg(feature = "srtm")] 10 | pub mod srtm { 11 | pub use crate::mapping::srtm::*; 12 | } 13 | 14 | pub mod bheat { 15 | pub use crate::mapping::bheat::*; 16 | } 17 | 18 | //#[cfg(feature = "srtm")] 19 | pub use mapping::latlon::LatLon; 20 | pub use mapping::{ 21 | has_line_of_sight, height_tile_elevations, lat_lon_path_10m, lat_lon_path_1m, lat_lon_tile, 22 | lat_lon_vec_to_heights, 23 | }; 24 | 25 | // Re-export geo 26 | //#[cfg(feature = "srtm")] 27 | pub mod geo { 28 | pub use geo::*; 29 | } 30 | 31 | pub mod geometry; 32 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/mapping/bheat/heat_cache.rs: -------------------------------------------------------------------------------- 1 | use super::MapTile; 2 | use crate::Distance; 3 | use lazy_static::*; 4 | use memmap::{Mmap, MmapOptions}; 5 | use parking_lot::RwLock; 6 | use std::collections::HashMap; 7 | use std::fs::File; 8 | 9 | lazy_static! { 10 | static ref HEAT_CACHE: RwLock>> = RwLock::new(HashMap::new()); 11 | } 12 | 13 | pub fn heat_altitude(lat: f64, lon: f64, heat_path: &str) -> Option<(Distance, Distance)> { 14 | let filename = MapTile::get_tile_name(lat, lon, heat_path); 15 | 16 | let read_lock = HEAT_CACHE.read(); 17 | if let Some(tile_file) = read_lock.get(&filename) { 18 | if let Some(mm) = tile_file { 19 | Some(get_elevation(lat, lon, mm)) 20 | } else { 21 | None 22 | } 23 | } else { 24 | // We don't have it - try and load it 25 | std::mem::drop(read_lock); 26 | let mut write_lock = HEAT_CACHE.write(); 27 | if let Ok(cache_file) = File::open(&filename) { 28 | let mapped_file = unsafe { MmapOptions::new().map(&cache_file).unwrap() }; 29 | let elevation = get_elevation(lat, lon, &mapped_file); 30 | write_lock.insert(filename, Some(mapped_file)); 31 | Some(elevation) 32 | } else { 33 | write_lock.insert(filename, None); 34 | None 35 | } 36 | } 37 | } 38 | 39 | fn get_elevation(lat: f64, lon: f64, memory: &Mmap) -> (Distance, Distance) { 40 | let index = MapTile::index(lat, lon); 41 | let offset = index * 2; 42 | let heights = bytemuck::cast_slice::(&memory); 43 | let ground = heights[offset] / 10; 44 | let clutter = heights[offset + 1] / 10; 45 | ( 46 | Distance::with_meters(ground), 47 | Distance::with_meters(clutter), 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/mapping/bheat/mod.rs: -------------------------------------------------------------------------------- 1 | mod heat_cache; 2 | mod tile_reader; 3 | 4 | pub use heat_cache::heat_altitude; 5 | use tile_reader::MapTile; 6 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/mapping/bheat/tile_reader.rs: -------------------------------------------------------------------------------- 1 | const ROW_SIZE: usize = 768; 2 | const COL_SIZE: usize = 768; 3 | 4 | pub struct MapTile {} 5 | 6 | impl MapTile { 7 | pub fn index(lat: f64, lon: f64) -> usize { 8 | let lat_abs = lat.abs(); 9 | let lon_abs = lon.abs(); 10 | let lat_floor = lat_abs.floor(); 11 | let lon_floor = lon_abs.floor(); 12 | 13 | let sub_lat = (lat_abs.fract() * 100.0).floor(); 14 | let sub_lon = (lon_abs.fract() * 100.0).floor(); 15 | 16 | let base_lat = lat_floor + (sub_lat / 100.0); 17 | let lat_min = (lat_abs - base_lat) * 100.0; 18 | let row_index = (lat_min * ROW_SIZE as f64) as usize; 19 | 20 | let base_lon = lon_floor + (sub_lon / 100.0); 21 | let lon_min = (lon_abs - base_lon) * 100.0; 22 | let col_index = (lon_min * COL_SIZE as f64) as usize; 23 | 24 | (row_index * COL_SIZE) + col_index 25 | } 26 | 27 | pub fn get_tile_name(lat: f64, lon: f64, heat_path: &str) -> String { 28 | let lat_c = if lat < 0.0 { 'S' } else { 'N' }; 29 | let lon_c = if lon < 0.0 { 'W' } else { 'E' }; 30 | 31 | let lat_abs = lat.abs(); 32 | let lon_abs = lon.abs(); 33 | let lat_floor = lat_abs.floor(); 34 | let lon_floor = lon_abs.floor(); 35 | 36 | let sub_lat = (lat_abs.fract() * 100.0).floor(); 37 | let sub_lon = (lon_abs.fract() * 100.0).floor(); 38 | 39 | format!( 40 | "{}/{}{:03}t{:02}_{}{:03 }t{:02}.bheat", 41 | heat_path, lat_c, lat_floor as i32, sub_lat, lon_c, lon_floor as i32, sub_lon, 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/mapping/latlon.rs: -------------------------------------------------------------------------------- 1 | use super::srtm::SrtmTile; 2 | use geo::Point; 3 | 4 | // Represents a point on the globe 5 | #[derive(Debug, Copy, Clone, PartialEq)] 6 | pub struct LatLon(Point); 7 | 8 | impl From> for LatLon { 9 | fn from(item: Point) -> Self { 10 | Self(item) 11 | } 12 | } 13 | 14 | impl LatLon { 15 | /// Create a new lat/lon location 16 | pub fn new(lat: f64, lon: f64) -> Self { 17 | Self(Point::new(lon, lat)) 18 | } 19 | 20 | /// Retrieve a Geo::Point for the location 21 | pub fn to_point(&self) -> Point { 22 | self.0 23 | } 24 | 25 | /// Retrieve Latitude 26 | pub fn lat(&self) -> f64 { 27 | self.0.lat() 28 | } 29 | 30 | /// Retrieve Longitude 31 | pub fn lon(&self) -> f64 { 32 | self.0.lng() 33 | } 34 | 35 | /// Retrieve a geo point in Radians 36 | pub fn to_radians_point(&self) -> Point { 37 | const DEG_RAD: f64 = 1.74532925199e-02; 38 | Point::new(self.0.lat() * DEG_RAD, self.0.lng() * DEG_RAD) 39 | } 40 | 41 | /// Create from a geo point in Radians 42 | pub fn from_radians_point(pt: &Point) -> Self { 43 | const DEG_RAD: f64 = 1.74532925199e-02; 44 | LatLon(Point::new(pt.lat() / DEG_RAD, pt.lng() / DEG_RAD)) 45 | } 46 | 47 | pub fn floor(&self) -> LatLon { 48 | LatLon::new(self.0.lat().floor(), self.0.lng().floor()) 49 | } 50 | 51 | /// Calculate the SRTM tile containing this point 52 | pub fn to_srtm1(&self) -> SrtmTile { 53 | let floor = self.floor(); 54 | SrtmTile::Srtm1 { 55 | lat: floor.0.lat() as i16, 56 | lon: floor.0.lng() as i16, 57 | } 58 | } 59 | 60 | /// Calculate the SRTM3 tile containing this point 61 | pub fn to_srtm3(&self) -> SrtmTile { 62 | let floor = self.floor(); 63 | SrtmTile::Srtm3 { 64 | lat: floor.0.lat() as i16, 65 | lon: floor.0.lng() as i16, 66 | } 67 | } 68 | 69 | /// Calculate the SRTM-Third tile containing this point 70 | pub fn to_srtm_third(&self) -> SrtmTile { 71 | let floor = self.floor(); 72 | let lat_extent_base_10 = self.lat() * 10.0 - floor.lat() * 10.0; 73 | let lat_extent_base_9 = ((lat_extent_base_10 / 10.0) * 9.0) + 1.0; 74 | let lat_sub_tile = lat_extent_base_9.floor(); 75 | 76 | let lon_extent_base_10 = self.lon() * 10.0 - floor.lon() * 10.0; 77 | let lon_extent_base_9 = ((lon_extent_base_10 / 10.0) * 9.0) + 1.0; 78 | let lon_sub_tile = lon_extent_base_9.floor(); 79 | 80 | SrtmTile::SrtmThird { 81 | lat: floor.lat() as i16, 82 | lon: floor.lon() as i16, 83 | lat_tile: lat_sub_tile as u8, 84 | lon_tile: lon_sub_tile as u8, 85 | } 86 | } 87 | 88 | pub fn to_cache_tuple(&self) -> (i32, i32) { 89 | ( 90 | (self.lat() * 100_000.0) as i32, 91 | (self.lon() * 100_000.0) as i32, 92 | ) 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn test_srtm1_filename() { 102 | let loc = LatLon::new(38.947775, -92.323385); 103 | let srtm1 = loc.to_srtm1(); 104 | assert_eq!(srtm1, SrtmTile::Srtm1 { lat: 38, lon: -93 }); 105 | } 106 | 107 | #[test] 108 | fn test_srtm3_filename() { 109 | let loc = LatLon::new(38.947775, -92.323385); 110 | let srtm3 = loc.to_srtm3(); 111 | assert_eq!(srtm3, SrtmTile::Srtm3 { lat: 38, lon: -93 }); 112 | } 113 | 114 | #[test] 115 | fn test_srtm_third_filename() { 116 | let loc = LatLon::new(38.947775, -92.323385); 117 | let srtm3 = loc.to_srtm_third(); 118 | assert_eq!( 119 | srtm3, 120 | SrtmTile::SrtmThird { 121 | lat: 38, 122 | lon: -93, 123 | lat_tile: 9, 124 | lon_tile: 7, 125 | } 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/mapping/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod latlon; 2 | pub mod srtm; 3 | pub use latlon::LatLon; 4 | pub mod bheat; 5 | use bheat::heat_altitude; 6 | use rayon::prelude::*; 7 | 8 | use crate::{ 9 | geometry::{haversine_distance, haversine_intermediate}, 10 | Distance, 11 | }; 12 | 13 | /// Create a grid of LatLon entries for a bounded tile, returning (x, y, LatLon). 14 | /// Used as a starting point for creating map tiles. 15 | pub fn lat_lon_tile( 16 | swlat: f64, 17 | swlon: f64, 18 | nelat: f64, 19 | nelon: f64, 20 | tile_size: usize, 21 | ) -> Vec<(u32, u32, LatLon)> { 22 | let mut points = Vec::with_capacity(tile_size * tile_size); 23 | let mut lat = swlat; 24 | let lat_step = (nelat - swlat) / tile_size as f64; 25 | let lon_step = (nelon - swlon) / tile_size as f64; 26 | let mut y = 0; 27 | while lat < nelat - lat_step { 28 | let mut lon = swlon; 29 | let mut x = 0; 30 | while lon < nelon { 31 | points.push((x, y, LatLon::new(lat, lon))); 32 | lon += lon_step; 33 | x += 1; 34 | } 35 | lat += lat_step; 36 | y += 1; 37 | } 38 | points 39 | } 40 | 41 | fn highest_altitude(point: &LatLon, heat_path: &str) -> f64 { 42 | let altitudes = heat_altitude(point.lat(), point.lon(), heat_path) 43 | .unwrap_or((Distance::with_meters(0.0), Distance::with_meters(0.0))); 44 | f64::max( 45 | altitudes.0.as_meters(), 46 | altitudes.1.as_meters(), 47 | ) 48 | } 49 | 50 | pub fn height_tile_elevations(points: &[(u32, u32, LatLon)], heat_path: &str) -> Vec { 51 | points 52 | .par_iter() 53 | .map(|(_, _, point)| highest_altitude(point, heat_path)) 54 | .collect() 55 | } 56 | 57 | pub fn lat_lon_path_10m(src: &LatLon, dst: &LatLon) -> Vec { 58 | let d = haversine_distance(src, dst); 59 | let extent_step = 1.0 / (d.as_meters() / 10.0); 60 | let mut extent = 0.0; 61 | let mut path = Vec::with_capacity((d.as_meters() / 10.0) as usize); 62 | while extent <= 1.0 { 63 | let step_point = haversine_intermediate(src, dst, extent); 64 | path.push(step_point); 65 | extent += extent_step; 66 | } 67 | path 68 | } 69 | 70 | pub fn lat_lon_path_1m(src: &LatLon, dst: &LatLon) -> Vec { 71 | let d = haversine_distance(src, dst); 72 | let extent_step = 1.0 / d.as_meters(); 73 | let mut extent = 0.0; 74 | let mut path = Vec::with_capacity((d.as_meters() / 10.0) as usize); 75 | while extent <= 1.0 { 76 | let step_point = haversine_intermediate(src, dst, extent); 77 | path.push(step_point); 78 | extent += extent_step; 79 | } 80 | path 81 | } 82 | 83 | pub fn lat_lon_vec_to_heights(points: &[LatLon], heat_path: &str) -> Vec { 84 | points 85 | .par_iter() 86 | .map(|point| highest_altitude(point, heat_path)) 87 | .collect() 88 | } 89 | 90 | pub fn has_line_of_sight( 91 | los_path: &[f64], 92 | start_elevation: Distance, 93 | end_elevation: Distance, 94 | ) -> bool { 95 | let start_height = los_path[0] + start_elevation.as_meters(); 96 | let end_height = end_elevation.as_meters() as u16; // Not using terrain because of confusion with clutter on lidar 97 | let height_step = (end_height as f64 - start_height as f64) / los_path.len() as f64; 98 | let mut current_height = start_height as f64; 99 | let mut visible = true; 100 | for p in los_path.iter() { 101 | if current_height < *p { 102 | visible = false; 103 | break; 104 | } 105 | current_height += height_step; 106 | } 107 | visible 108 | } 109 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/mapping/srtm/mod.rs: -------------------------------------------------------------------------------- 1 | mod tile; 2 | pub use tile::*; 3 | mod tile_cache; 4 | pub use tile_cache::*; 5 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/mapping/srtm/tile.rs: -------------------------------------------------------------------------------- 1 | use crate::LatLon; 2 | use std::path::Path; 3 | 4 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 5 | /// Denotes the types of SRTM (.hgt) tile formats that are available. 6 | pub enum SrtmTile { 7 | Srtm1 { 8 | lat: i16, 9 | lon: i16, 10 | }, 11 | Srtm3 { 12 | lat: i16, 13 | lon: i16, 14 | }, 15 | SrtmThird { 16 | lat: i16, 17 | lon: i16, 18 | lat_tile: u8, 19 | lon_tile: u8, 20 | }, 21 | } 22 | 23 | impl SrtmTile { 24 | /// Determine if a tile is available, and return available strategy 25 | /// for retrieving it at the best possible resolution. 26 | pub fn check_availability(loc: &LatLon, terrain_path: &str) -> Option { 27 | let third = loc.to_srtm_third(); 28 | if Path::new(&third.filename(terrain_path)).exists() { 29 | return Some(third); 30 | } 31 | 32 | let three = loc.to_srtm3(); 33 | if Path::new(&three.filename(terrain_path)).exists() { 34 | return Some(three); 35 | } 36 | 37 | let one = loc.to_srtm1(); 38 | if Path::new(&one.filename(terrain_path)).exists() { 39 | return Some(one); 40 | } 41 | 42 | None 43 | } 44 | 45 | /// Calculates an SRTM filename based on an SrtmTile entry. 46 | /// terrain_path denotes a directory prefix to attach. 47 | pub fn filename(&self, terrain_path: &str) -> String { 48 | match self { 49 | SrtmTile::Srtm1 { lat, lon } => format!( 50 | "{}/1/{}{:02}{}{:03}.hgt", 51 | terrain_path, 52 | if *lat >= 0 { 'N' } else { 'S' }, 53 | lat.abs(), 54 | if *lon >= 0 { 'E' } else { 'W' }, 55 | lon.abs() 56 | ), 57 | SrtmTile::Srtm3 { lat, lon } => format!( 58 | "{}/3/{}{:02}{}{:03}.hgt", 59 | terrain_path, 60 | if *lat >= 0 { 'N' } else { 'S' }, 61 | lat.abs(), 62 | if *lon >= 0 { 'E' } else { 'W' }, 63 | lon.abs() 64 | ), 65 | SrtmTile::SrtmThird { 66 | lat, 67 | lon, 68 | lat_tile, 69 | lon_tile, 70 | } => format!( 71 | "{}/third/{}{:02}{}{:03}T{:01}{:01}.hgt", 72 | terrain_path, 73 | if *lat >= 0 { 'N' } else { 'S' }, 74 | lat.abs(), 75 | if *lon >= 0 { 'E' } else { 'W' }, 76 | lon.abs(), 77 | lat_tile, 78 | lon_tile 79 | ), 80 | } 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | 88 | #[test] 89 | fn test_srtm1_filename() { 90 | let tile = SrtmTile::Srtm1 { lat: 38, lon: -93 }; 91 | let desired = "/1/N38W093.hgt"; 92 | assert_eq!(desired, tile.filename("")); 93 | } 94 | 95 | #[test] 96 | fn test_srtm3_filename() { 97 | let tile = SrtmTile::Srtm3 { lat: 38, lon: -93 }; 98 | let desired = "/3/N38W093.hgt"; 99 | assert_eq!(desired, tile.filename("")); 100 | } 101 | 102 | #[test] 103 | fn test_srtm_third_filename() { 104 | let tile = SrtmTile::SrtmThird { 105 | lat: 38, 106 | lon: -93, 107 | lat_tile: 9, 108 | lon_tile: 7, 109 | }; 110 | let desired = "/third/N38W093T97.hgt"; 111 | assert_eq!(desired, tile.filename("")); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/mapping/srtm/tile_cache.rs: -------------------------------------------------------------------------------- 1 | use super::SrtmTile; 2 | use crate::Distance; 3 | use crate::LatLon; 4 | use lazy_static::*; 5 | use memmap::{Mmap, MmapOptions}; 6 | use parking_lot::RwLock; 7 | use std::collections::HashMap; 8 | use std::fs::File; 9 | 10 | lazy_static! { 11 | static ref TILE_CACHE: RwLock> = RwLock::new(HashMap::new()); 12 | } 13 | 14 | pub fn get_altitude(loc: &LatLon, terrain_path: &str) -> Option { 15 | // Check for already cached tiles 16 | let in_cache = check_existing(loc); 17 | if in_cache.is_some() { 18 | return in_cache; 19 | } 20 | 21 | // If we've got this far, it isn't cached. Try and load it. 22 | if let Some(tile) = SrtmTile::check_availability(loc, terrain_path) { 23 | let mut cache_writer = TILE_CACHE.write(); 24 | if let Ok(cache_file) = File::open(&tile.filename(terrain_path)) { 25 | let mapped_file = unsafe { MmapOptions::new().map(&cache_file).unwrap() }; 26 | let elevation = get_elevation(loc, &tile, &mapped_file); 27 | cache_writer.insert(tile, mapped_file); 28 | return Some(elevation); 29 | } else { 30 | return None; 31 | } 32 | } 33 | 34 | // Failure 35 | None 36 | } 37 | 38 | fn check_existing(loc: &LatLon) -> Option { 39 | let cache_reader = TILE_CACHE.read(); 40 | 41 | let third = loc.to_srtm_third(); 42 | if let Some(mm) = cache_reader.get(&third) { 43 | return Some(get_elevation(loc, &third, &mm)); 44 | } 45 | 46 | let three = loc.to_srtm3(); 47 | if let Some(mm) = cache_reader.get(&three) { 48 | return Some(get_elevation(loc, &three, &mm)); 49 | } 50 | 51 | let one = loc.to_srtm1(); 52 | if let Some(mm) = cache_reader.get(&one) { 53 | return Some(get_elevation(loc, &one, &mm)); 54 | } 55 | 56 | None 57 | } 58 | 59 | fn get_elevation(loc: &LatLon, tile: &SrtmTile, memory: &Mmap) -> Distance { 60 | let floor = loc.floor(); 61 | let offset = match tile { 62 | SrtmTile::Srtm1 { .. } => { 63 | const BYTES_PER_SAMPLE: usize = 2; 64 | const N_SAMPLES: usize = 1201; 65 | const SAMPLES_PER_DEGREE: usize = N_SAMPLES - 1; 66 | let row = 67 | ((floor.lat() + 1.0 - loc.lat()) * SAMPLES_PER_DEGREE as f64).round() as usize; 68 | let col = ((loc.lon() - floor.lon()) * SAMPLES_PER_DEGREE as f64).round() as usize; 69 | BYTES_PER_SAMPLE * ((row * N_SAMPLES) + col) 70 | } 71 | SrtmTile::Srtm3 { .. } => { 72 | const BYTES_PER_SAMPLE: usize = 2; 73 | const N_SAMPLES: usize = 3601; 74 | const SAMPLES_PER_DEGREE: usize = N_SAMPLES - 1; 75 | let row = 76 | ((floor.lat() + 1.0 - loc.lat()) * SAMPLES_PER_DEGREE as f64).round() as usize; 77 | let col = ((loc.lon() - floor.lon()) * SAMPLES_PER_DEGREE as f64).round() as usize; 78 | BYTES_PER_SAMPLE * ((row * N_SAMPLES) + col) 79 | } 80 | SrtmTile::SrtmThird { 81 | lat_tile, lon_tile, .. 82 | } => { 83 | const SAMPLES_PER_DEGREE: usize = 1200; 84 | const SAMPLES_PER_EXTENT: usize = 1201; 85 | const BYTES_PER_SAMPLE: usize = 2; 86 | let lat_extent_base_10 = loc.lat() * 10.0 - floor.lat() * 10.0; 87 | let lat_extent_base_9 = ((lat_extent_base_10 / 10.0) * 9.0) + 1.0; 88 | let lon_extent_base_10 = loc.lon() * 10.0 - floor.lon() * 10.0; 89 | let lon_extent_base_9 = ((lon_extent_base_10 / 10.0) * 9.0) + 1.0; 90 | let lat_percent = lat_extent_base_9 - *lat_tile as f64; 91 | let lon_percent = lon_extent_base_9 - *lon_tile as f64; 92 | let row = ((1.0 - lat_percent) * SAMPLES_PER_DEGREE as f64).round() as usize; 93 | let col = (lon_percent * SAMPLES_PER_DEGREE as f64).round() as usize; 94 | BYTES_PER_SAMPLE * ((row * SAMPLES_PER_EXTENT) + col) 95 | } 96 | }; 97 | 98 | let h = { 99 | let high_byte = *memory.get(offset + 1).unwrap(); 100 | let low_byte = *memory.get(offset).unwrap(); 101 | ((low_byte as u16) << 8) | high_byte as u16 102 | }; 103 | Distance::with_meters(h) 104 | } 105 | 106 | #[cfg(test)] 107 | mod test { 108 | use super::get_altitude; 109 | use super::LatLon; 110 | 111 | #[test] 112 | fn test_srtm_third_elevation() { 113 | let loc = LatLon::new(38.947775, -92.323385); 114 | let altitude = get_altitude(&loc, "resources"); 115 | assert!(altitude.is_some()); 116 | if let Some(alt) = altitude { 117 | assert_eq!(alt.as_meters(), 232.0); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/cost_hata.rs: -------------------------------------------------------------------------------- 1 | use crate::{Distance, EstimateMode, Frequency}; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum CostError { 5 | FrequencyOutOfRange, 6 | HeightOutOfRange, 7 | DistanceOutOfRange, 8 | } 9 | 10 | /// COST231 extension to HATA path loss model. 11 | /// Original: https://github.com/Cloud-RF/Signal-Server/blob/master/models/cost.cc 12 | /// See: http://morse.colorado.edu/~tlen5510/text/classwebch3.html 13 | /// Frequency must be 1,500 .. 2,000 Mhz 14 | /// tx_height must be 30..200 meters 15 | /// rx_height must be 1..20 meters 16 | /// Distance must be 1..20 km 17 | pub fn cost_path_loss( 18 | frequency: Frequency, 19 | tx_height: Distance, 20 | rx_height: Distance, 21 | distance: Distance, 22 | mode: EstimateMode, 23 | ) -> Result { 24 | let f = frequency.as_mhz(); 25 | if f < 1_500.0 || f > 2_000.0 { 26 | return Err(CostError::FrequencyOutOfRange); 27 | } 28 | let txh = tx_height.as_meters(); 29 | let rxh = rx_height.as_meters(); 30 | if txh < 30.0 || txh > 200.0 { 31 | return Err(CostError::HeightOutOfRange); 32 | } 33 | if rxh < 1.0 || rxh > 10.0 { 34 | return Err(CostError::HeightOutOfRange); 35 | } 36 | let d = distance.as_km(); 37 | if d < 1.0 || d > 20.0 { 38 | return Err(CostError::DistanceOutOfRange); 39 | } 40 | let mode = mode.to_mode(); 41 | 42 | let mut c = 3.0; // 3dB for Urban 43 | let mut lrxh = (11.75 * rxh).log10(); 44 | let mut c_h = 3.2 * (lrxh * lrxh) - 4.97; // Large city (conservative) 45 | let mut c0 = (69.55f64).floor(); // Note: used .floor() here because for some reason the original assigns a double to an int. 46 | let mut cf = (26.16f64).floor(); 47 | if f > 1500.0 { 48 | c0 = 46.3; 49 | cf = 33.9; 50 | } 51 | if mode == 2 { 52 | c = 0.0; // Medium city (average) 53 | lrxh = (1.54 * rxh).log10(); 54 | c_h = 8.29 * (lrxh * lrxh) - 1.1; 55 | } 56 | if mode == 3 { 57 | c = -3.0; // Small city (Optimistic) 58 | c_h = (1.1 * f.log10() - 0.7) * rxh - (1.56 * f.log10()) + 0.8; 59 | } 60 | let logf = f.log10(); 61 | let dbloss = c0 + (cf * logf) - (13.82 * txh.log10()) - c_h 62 | + (44.9 - 6.55 * txh.log10()) * d.log10() 63 | + c; 64 | return Ok(dbloss); 65 | } 66 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/ecc33.rs: -------------------------------------------------------------------------------- 1 | use crate::{Distance, EstimateMode, Frequency}; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum ECC33Error { 5 | FrequencyOutOfRange, 6 | HeightOfOutRange, 7 | DistanceOutOfRange, 8 | } 9 | 10 | /// ECC33 Path Loss Estimation 11 | /// Original: https://github.com/Cloud-RF/Signal-Server/blob/master/models/ecc33.cc 12 | /// Frequency must be 30..1,000 Mhz 13 | /// Heights must be >1m 14 | /// Distance must be 1..50km 15 | pub fn ecc33_path_loss( 16 | frequency: Frequency, 17 | tx_height: Distance, 18 | rx_height: Distance, 19 | distance: Distance, 20 | mode: EstimateMode, 21 | ) -> Result { 22 | let tx_h = tx_height.as_meters(); 23 | let mut rx_h = rx_height.as_meters(); 24 | if tx_h < 1.0 || rx_h < 1.0 { 25 | return Err(ECC33Error::HeightOfOutRange); 26 | } 27 | let d = distance.as_km(); 28 | if d < 1.0 || d > 50.0 { 29 | return Err(ECC33Error::DistanceOutOfRange); 30 | } 31 | let f = frequency.as_ghz(); 32 | if f < 0.03 || f > 1.0 { 33 | return Err(ECC33Error::FrequencyOutOfRange); 34 | } 35 | let mode = mode.to_mode(); 36 | 37 | // Sanity check as this model operates within limited Txh/Rxh bounds 38 | if tx_h - rx_h < 0.0 { 39 | rx_h = rx_h / (d * 2.0); 40 | } 41 | 42 | let mut gr = 0.759 * rx_h - 1.862; // Big city with tall buildings (1) 43 | let afs = 92.4 + 20.0 * d.log10() + 20.0 * f.log10(); 44 | let abm = 20.41 + 9.83 * d.log10() + 7.894 * f.log10() + 9.56 * (f.log10() * f.log10()); 45 | let gb = (tx_h / 200.0).log10() * (13.958 + 5.8 * (d.log10() * d.log10())); 46 | if mode > 1 { 47 | // Medium city (Europe) 48 | gr = (42.57 + 13.7 * f.log10()) * (rx_h.log10() - 0.585); 49 | } 50 | 51 | Ok(afs + abm - gb - gr) 52 | } 53 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/egli.rs: -------------------------------------------------------------------------------- 1 | use crate::{Distance, Frequency}; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum EgliError { 5 | FrequencyOutOfRange, 6 | InvalidHeight, 7 | DistanceOutOfRange, 8 | } 9 | 10 | /// EGLI Path Loss Calculation 11 | /// Original: https://github.com/Cloud-RF/Signal-Server/blob/master/models/egli.cc 12 | /// Frequency must be 30 to 10000Mhz 13 | /// Heights must be > 1m 14 | /// Distance must be 1..50 km 15 | /// See http://people.seas.harvard.edu/~jones/es151/prop_models/propagation.html#pel 16 | pub fn egli_path_loss( 17 | frequency: Frequency, 18 | tx_height: Distance, 19 | rx_height: Distance, 20 | distance: Distance, 21 | ) -> Result { 22 | let f = frequency.as_mhz(); 23 | if f < 30.0 || f > 10_000.0 { 24 | return Err(EgliError::FrequencyOutOfRange); 25 | } 26 | let h1 = tx_height.as_meters(); 27 | let h2 = rx_height.as_meters(); 28 | if h1 < 1.0 || h2 < 1.0 { 29 | return Err(EgliError::InvalidHeight); 30 | } 31 | let d = distance.as_km(); 32 | if d < 1.0 || d > 50.0 { 33 | return Err(EgliError::DistanceOutOfRange); 34 | } 35 | 36 | let mut lp50; 37 | let c1; 38 | let c2; 39 | 40 | if h1 > 10.0 && h2 > 10.0 { 41 | lp50 = 85.9; 42 | c1 = 2.0; 43 | c2 = 2.0; 44 | } else if h1 > 10.0 { 45 | lp50 = 76.3; 46 | c1 = 2.0; 47 | c2 = 1.0; 48 | } else if h2 > 10.0 { 49 | lp50 = 76.3; 50 | c1 = 1.0; 51 | c2 = 2.0; 52 | } else 53 | // both antenna heights below 10 metres 54 | { 55 | lp50 = 66.7; 56 | c1 = 1.0; 57 | c2 = 1.0; 58 | } 59 | 60 | lp50 += 4.0 * _10log10f(d) + 2.0 * _10log10f(f) - c1 * _10log10f(h1) - c2 * _10log10f(h2); 61 | 62 | Ok(lp50) 63 | } 64 | 65 | #[inline(always)] 66 | fn _10log10f(x: f64) -> f64 { 67 | 4.342944 * x.ln() 68 | } 69 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/fresnel.rs: -------------------------------------------------------------------------------- 1 | /// Calculates the Fresnel radius of a connection. 2 | /// Most of the time, you are interested in 60% of this number (0.6) in wireless links. 3 | /// d1_meters is the distance from the start, d2_meters is the distance from the end. 4 | pub fn fresnel_radius(d1_meters: f64, d2_meters: f64, freq_mhz: f64) -> f64 { 5 | 17.31 * ((d1_meters * d2_meters) / (freq_mhz * (d1_meters + d2_meters))).sqrt() 6 | } 7 | 8 | #[cfg(test)] 9 | mod test { 10 | use super::fresnel_radius; 11 | 12 | #[test] 13 | fn quick_fresnel_test() { 14 | assert!(float_cmp::approx_eq!( 15 | f64, 16 | fresnel_radius(1000.0, 1000.0, 2437.0), 17 | 7.8406903990353305, 18 | ulps = 2 19 | )); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/fspl.rs: -------------------------------------------------------------------------------- 1 | use super::{Distance, Frequency}; 2 | 3 | /// Calculate the pure free-space path loss in dB of a signal and distance. 4 | pub fn free_space_path_loss_db(frequency: Frequency, distance: Distance) -> f64 { 5 | let d = distance.as_km(); 6 | let f = frequency.as_mhz(); 7 | 32.44 + (20.0 * f.log10()) + (20.0 * d.log10()) 8 | } 9 | 10 | #[cfg(test)] 11 | mod test { 12 | use super::{free_space_path_loss_db, Distance, Frequency}; 13 | 14 | #[test] 15 | fn test_fpl() { 16 | assert!(float_cmp::approx_eq!( 17 | f64, 18 | free_space_path_loss_db(Frequency::with_ghz(5.8), Distance::with_kilometers(10)), 19 | 127.70855987125874, 20 | ulps = 2 21 | )); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/hata.rs: -------------------------------------------------------------------------------- 1 | use crate::{Distance, EstimateMode, Frequency}; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum HataError { 5 | FrequencyOutOfRange, 6 | TxHeightOutOfRange, 7 | RxHeightOutOfRange, 8 | DistanceOutOfRange, 9 | } 10 | 11 | /// HATA path loss estimation 12 | /// Original: https://github.com/Cloud-RF/Signal-Server/blob/master/models/hata.cc 13 | /// Frequency must be from 150 to 1500Mhz 14 | /// TX Height must be from 30-200m 15 | /// RX Height must be from 1-10m 16 | /// Distance must be from 1-20km 17 | pub fn hata_path_loss( 18 | frequency: Frequency, 19 | tx_height: Distance, 20 | rx_height: Distance, 21 | distance: Distance, 22 | mode: EstimateMode, 23 | ) -> Result { 24 | let mode = mode.to_mode(); 25 | let f = frequency.as_mhz(); 26 | if f < 150.0 || f > 1500.0 { 27 | return Err(HataError::FrequencyOutOfRange); 28 | } 29 | let h_b = tx_height.as_meters(); 30 | if h_b < 30.0 || h_b > 200.0 { 31 | return Err(HataError::TxHeightOutOfRange); 32 | } 33 | let h_m = rx_height.as_meters(); 34 | if h_m < 1.0 || h_m > 10.0 { 35 | return Err(HataError::RxHeightOutOfRange); 36 | } 37 | let d = distance.as_km(); 38 | if d < 1.0 || d > 20.0 { 39 | return Err(HataError::DistanceOutOfRange); 40 | } 41 | 42 | let lh_m; 43 | let c_h; 44 | let logf = f.log10(); 45 | 46 | if f < 200.0 { 47 | lh_m = (1.54 * h_m).log10(); 48 | c_h = 8.29 * (lh_m * lh_m) - 1.1; 49 | } else { 50 | lh_m = (11.75 * h_m).log10(); 51 | c_h = 3.2 * (lh_m * lh_m) - 4.97; 52 | } 53 | 54 | let l_u = 55 | 69.55 + 26.16 * logf - 13.82 * h_b.log10() - c_h + (44.9 - 6.55 * h_b.log10()) * d.log10(); 56 | 57 | if mode == 0 || mode == 1 { 58 | return Ok(l_u); //URBAN 59 | } 60 | 61 | if mode == 2 { 62 | //SUBURBAN 63 | let logf_28 = (f / 28.0).log10(); 64 | return Ok(l_u - 2.0 * logf_28 * logf_28 - 5.4); 65 | } 66 | 67 | if mode == 3 { 68 | //OPEN 69 | return Ok(l_u - 4.78 * logf * logf + 18.33 * logf - 40.94); 70 | } 71 | 72 | Ok(0.0) 73 | } 74 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/itwom3.rs: -------------------------------------------------------------------------------- 1 | use super::{Distance, Frequency}; 2 | 3 | fn point_to_point( 4 | elev: &mut [f64], 5 | tht_m: f64, 6 | rht_m: f64, 7 | eps_dielect: f64, 8 | sgm_conductivity: f64, 9 | eno_ns_surfref: f64, 10 | frq_mhz: f64, 11 | radio_climate: ::std::os::raw::c_int, 12 | pol: ::std::os::raw::c_int, 13 | conf: f64, 14 | rel: f64, 15 | ) -> PTPResult { 16 | let mut dbloss = 0.0f64; 17 | let mut mode = String::new(); 18 | let mut errnum: std::os::raw::c_int = 0; 19 | 20 | use super::itwom3_port::ItWomState; 21 | 22 | let mut itm = ItWomState::default(); 23 | 24 | itm.point_to_point( 25 | elev, 26 | tht_m, 27 | rht_m, 28 | eps_dielect, 29 | sgm_conductivity, 30 | eno_ns_surfref, 31 | frq_mhz, 32 | radio_climate, 33 | pol, 34 | conf, 35 | rel, 36 | &mut dbloss, 37 | &mut mode, 38 | &mut errnum, 39 | ); 40 | 41 | PTPResult { 42 | dbloss: dbloss, 43 | mode: mode, 44 | error_num: errnum, 45 | } 46 | } 47 | 48 | #[derive(Debug)] 49 | pub struct PTPResult { 50 | pub dbloss: f64, 51 | pub mode: String, 52 | pub error_num: i32, 53 | } 54 | 55 | #[derive(Debug, PartialEq)] 56 | pub enum PTPError { 57 | //DistanceTooShort, 58 | DistanceTooLong, 59 | AltitudeTooHigh, 60 | AltitudeTooLow, 61 | } 62 | 63 | /// Describes terrain for an IWOM Point-To-Point path. 64 | /// Elevations should be an array of altitudes along the path, starting at the transmitter and ending at the receiver. 65 | /// The height of transmitters and receivers is height above the elevations specified. 66 | #[derive(Debug)] 67 | pub struct PTPPath { 68 | pub elevations: Vec, 69 | pub transmit_height: Distance, 70 | pub receive_height: Distance, 71 | } 72 | 73 | impl PTPPath { 74 | /// Construct a new PTP path for ITWOM evaluation. The constructor takes care of pre-pending the fields 75 | /// used by the C algorithm. 76 | pub fn new( 77 | elevations: Vec, 78 | transmit_height: Distance, 79 | receive_height: Distance, 80 | step_size: Distance, 81 | ) -> Result { 82 | let total_distance: f64 = elevations.len() as f64 * step_size.as_meters(); 83 | if total_distance > 2000000.0 { 84 | return Err(PTPError::DistanceTooLong); 85 | } 86 | 87 | for a in &elevations { 88 | if *a < 0.5 { 89 | return Err(PTPError::AltitudeTooLow); 90 | } else if *a > 3000.0 { 91 | return Err(PTPError::AltitudeTooHigh); 92 | } 93 | } 94 | 95 | let mut path = Self { 96 | elevations, 97 | transmit_height, 98 | receive_height, 99 | }; 100 | 101 | // Index 0 is the number of elements, next up is the distance per step 102 | path.elevations.insert(0, step_size.as_meters()); 103 | path.elevations 104 | .insert(0, path.elevations.len() as f64 - 3.0); 105 | 106 | Ok(path) 107 | } 108 | } 109 | 110 | #[derive(Debug)] 111 | pub struct PTPClimate { 112 | pub eps_dialect: f64, 113 | pub sgm_conductivity: f64, 114 | pub eno_ns_surfref: f64, 115 | pub radio_climate: i32, 116 | } 117 | 118 | pub enum GroundConductivity { 119 | SaltWater, 120 | GoodGround, 121 | FreshWater, 122 | MarshyLand, 123 | Farmland, 124 | Forest, 125 | AverageGround, 126 | Mountain, 127 | Sand, 128 | City, 129 | PoorGround, 130 | } 131 | 132 | pub enum RadioClimate { 133 | Equatorial, 134 | ContinentalSubtropical, 135 | MaritimeSubtropical, 136 | Desert, 137 | ContinentalTemperate, 138 | MaritimeTemperateLand, 139 | MaritimeTemperateSea, 140 | } 141 | 142 | impl PTPClimate { 143 | pub fn default() -> Self { 144 | Self { 145 | eps_dialect: 15.0, 146 | sgm_conductivity: 0.005, 147 | eno_ns_surfref: 301.0, 148 | radio_climate: 5, 149 | } 150 | } 151 | 152 | pub fn new(ground: GroundConductivity, climate: RadioClimate) -> Self { 153 | Self { 154 | eps_dialect: match ground { 155 | GroundConductivity::SaltWater => 80.0, 156 | GroundConductivity::GoodGround => 25.0, 157 | GroundConductivity::FreshWater => 80.0, 158 | GroundConductivity::MarshyLand => 12.0, 159 | GroundConductivity::Farmland => 15.0, 160 | GroundConductivity::Forest => 15.0, 161 | GroundConductivity::AverageGround => 15.0, 162 | GroundConductivity::Mountain => 13.0, 163 | GroundConductivity::Sand => 13.0, 164 | GroundConductivity::City => 5.0, 165 | GroundConductivity::PoorGround => 4.0, 166 | }, 167 | sgm_conductivity: match ground { 168 | GroundConductivity::SaltWater => 5.0, 169 | GroundConductivity::GoodGround => 0.020, 170 | GroundConductivity::FreshWater => 0.010, 171 | GroundConductivity::MarshyLand => 0.007, 172 | GroundConductivity::Farmland => 0.005, 173 | GroundConductivity::Forest => 0.005, 174 | GroundConductivity::AverageGround => 0.005, 175 | GroundConductivity::Mountain => 0.002, 176 | GroundConductivity::Sand => 0.002, 177 | GroundConductivity::City => 0.001, 178 | GroundConductivity::PoorGround => 0.001, 179 | }, 180 | eno_ns_surfref: 301.0, 181 | radio_climate: match climate { 182 | RadioClimate::Equatorial => 1, 183 | RadioClimate::ContinentalSubtropical => 2, 184 | RadioClimate::MaritimeSubtropical => 3, 185 | RadioClimate::Desert => 4, 186 | RadioClimate::ContinentalTemperate => 5, 187 | RadioClimate::MaritimeTemperateLand => 6, 188 | RadioClimate::MaritimeTemperateSea => 7, 189 | }, 190 | } 191 | } 192 | } 193 | 194 | pub fn itwom_point_to_point( 195 | path: &mut PTPPath, 196 | climate: PTPClimate, 197 | frequency: Frequency, 198 | confidence: f64, 199 | rel: f64, 200 | polarity: i32, 201 | ) -> PTPResult { 202 | point_to_point( 203 | &mut path.elevations, 204 | path.transmit_height.as_meters(), 205 | path.receive_height.as_meters(), 206 | climate.eps_dialect, 207 | climate.sgm_conductivity, 208 | climate.eno_ns_surfref, 209 | frequency.as_mhz(), 210 | climate.radio_climate, 211 | polarity, 212 | confidence, 213 | rel, 214 | ) 215 | } 216 | 217 | #[cfg(test)] 218 | mod test { 219 | use super::*; 220 | 221 | /*#[test] 222 | fn test_too_short() { 223 | assert_eq!( 224 | PTPPath::new( 225 | vec![1.0; 2], 226 | Distance::with_meters(100.0), 227 | Distance::with_meters(100.0), 228 | Distance::with_meters(10.0) 229 | ) 230 | .err(), 231 | Some(PTPError::DistanceTooShort) 232 | ); 233 | }*/ 234 | 235 | #[test] 236 | fn test_too_long() { 237 | assert_eq!( 238 | PTPPath::new( 239 | vec![1.0; 2000000], 240 | Distance::with_meters(100.0), 241 | Distance::with_meters(100.0), 242 | Distance::with_meters(10.0) 243 | ) 244 | .err(), 245 | Some(PTPError::DistanceTooLong) 246 | ); 247 | } 248 | 249 | #[test] 250 | fn altitudes_too_low() { 251 | assert_eq!( 252 | PTPPath::new( 253 | vec![0.4; 200], 254 | Distance::with_meters(100.0), 255 | Distance::with_meters(100.0), 256 | Distance::with_meters(10.0) 257 | ) 258 | .err(), 259 | Some(PTPError::AltitudeTooLow) 260 | ); 261 | } 262 | 263 | #[test] 264 | fn altitudes_too_high() { 265 | assert_eq!( 266 | PTPPath::new( 267 | vec![3500.0; 200], 268 | Distance::with_meters(100.0), 269 | Distance::with_meters(100.0), 270 | Distance::with_meters(10.0) 271 | ) 272 | .err(), 273 | Some(PTPError::AltitudeTooHigh) 274 | ); 275 | } 276 | 277 | #[test] 278 | fn basic_fspl_test() { 279 | let mut terrain_path = PTPPath::new( 280 | vec![1.0; 200], 281 | Distance::with_meters(100.0), 282 | Distance::with_meters(100.0), 283 | Distance::with_meters(10.0), 284 | ) 285 | .unwrap(); 286 | 287 | let itwom_test = itwom_point_to_point( 288 | &mut terrain_path, 289 | PTPClimate::default(), 290 | Frequency::with_mhz(5800.0), 291 | 0.5, 292 | 0.5, 293 | 1, 294 | ); 295 | 296 | assert_eq!(itwom_test.mode, "L-o-S"); 297 | assert_eq!(itwom_test.error_num, 0); 298 | assert_eq!(itwom_test.dbloss.floor(), 113.0); 299 | } 300 | 301 | #[test] 302 | fn basic_one_obstruction() { 303 | let mut elevations = vec![1.0; 200]; 304 | elevations[100] = 110.0; 305 | let mut terrain_path = PTPPath::new( 306 | elevations, 307 | Distance::with_meters(100.0), 308 | Distance::with_meters(100.0), 309 | Distance::with_meters(10.0), 310 | ) 311 | .unwrap(); 312 | 313 | let itwom_test = itwom_point_to_point( 314 | &mut terrain_path, 315 | PTPClimate::default(), 316 | Frequency::with_mhz(5800.0), 317 | 0.5, 318 | 0.5, 319 | 1, 320 | ); 321 | 322 | assert_eq!(itwom_test.mode, "1_Hrzn_Diff"); 323 | assert_eq!(itwom_test.error_num, 0); 324 | } 325 | 326 | #[test] 327 | fn basic_two_obstructions() { 328 | let mut elevations = vec![1.0; 200]; 329 | elevations[100] = 110.0; 330 | elevations[150] = 110.0; 331 | let mut terrain_path = PTPPath::new( 332 | elevations, 333 | Distance::with_meters(100.0), 334 | Distance::with_meters(100.0), 335 | Distance::with_meters(10.0), 336 | ) 337 | .unwrap(); 338 | 339 | let itwom_test = itwom_point_to_point( 340 | &mut terrain_path, 341 | PTPClimate::default(), 342 | Frequency::with_mhz(5800.0), 343 | 0.5, 344 | 0.5, 345 | 1, 346 | ); 347 | 348 | assert_eq!(itwom_test.mode, "2_Hrzn_Diff"); 349 | assert_eq!(itwom_test.error_num, 0); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/itwom3_port/helpers.rs: -------------------------------------------------------------------------------- 1 | // The idea here is to deprecate these and port them to proper Rust 2 | // syntax. 3 | 4 | pub(crate) fn pow(n: f64, e: f64) -> f64 { 5 | n.powf(e) 6 | } 7 | 8 | pub(crate) fn abs(n: f64) -> f64 { 9 | n.abs() 10 | } 11 | 12 | pub(crate) fn fabs(n: f64) -> f64 { 13 | n.abs() 14 | } 15 | 16 | pub(crate) fn log(n: f64) -> f64 { 17 | n.ln() 18 | } 19 | 20 | pub(crate) fn log10(n: f64) -> f64 { 21 | n.log10() 22 | } 23 | 24 | pub(crate) fn cos(n: f64) -> f64 { 25 | n.cos() 26 | } 27 | 28 | pub(crate) fn sin(n: f64) -> f64 { 29 | n.sin() 30 | } 31 | 32 | pub(crate) fn acos(n: f64) -> f64 { 33 | n.acos() 34 | } 35 | 36 | pub(crate) fn asin(n: f64) -> f64 { 37 | n.asin() 38 | } 39 | 40 | pub(crate) fn exp(n: f64) -> f64 { 41 | n.exp() 42 | } 43 | 44 | pub(crate) fn sqrt(n: f64) -> f64 { 45 | n.sqrt() 46 | } 47 | 48 | pub(crate) fn mymax(a: f64, b: f64) -> f64 { 49 | if a > b { 50 | return a; 51 | } else { 52 | return b; 53 | } 54 | } 55 | 56 | pub(crate) fn fortran_dim(x: f64, y: f64) -> f64 { 57 | /* This performs the FORTRAN DIM function. Result is x-y 58 | if x is greater than y; otherwise result is 0.0 */ 59 | 60 | if x > y { 61 | return x - y; 62 | } else { 63 | return 0.0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/itwom3_port/prop.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default)] 2 | pub(crate) struct PropType { 3 | pub(crate) aref: f64, 4 | pub(crate) dist: f64, 5 | pub(crate) hg: [f64; 2], 6 | pub(crate) rch: [f64; 2], 7 | pub(crate) wn: f64, 8 | pub(crate) dh: f64, 9 | pub(crate) dhd: f64, 10 | pub(crate) ens: f64, 11 | pub(crate) encc: f64, 12 | pub(crate) cch: f64, 13 | pub(crate) cd: f64, 14 | pub(crate) gme: f64, 15 | pub(crate) zgndreal: f64, 16 | pub(crate) zgndimag: f64, 17 | pub(crate) he: [f64; 2], 18 | pub(crate) dl: [f64; 2], 19 | pub(crate) the: [f64; 2], 20 | pub(crate) tiw: f64, 21 | pub(crate) ght: f64, 22 | pub(crate) ghr: f64, 23 | pub(crate) rph: f64, 24 | pub(crate) hht: f64, 25 | pub(crate) hhr: f64, 26 | pub(crate) tgh: f64, 27 | pub(crate) tsgh: f64, 28 | pub(crate) thera: f64, 29 | pub(crate) thenr: f64, 30 | pub(crate) rpl: i32, 31 | pub(crate) kwx: i32, 32 | pub(crate) mdp: i32, 33 | pub(crate) ptx: i32, 34 | pub(crate) los: i32, 35 | } 36 | 37 | #[derive(Default)] 38 | pub(crate) struct PropVType { 39 | pub(crate) sgc: f64, 40 | pub(crate) lvar: i32, 41 | pub(crate) mdvar: i32, 42 | pub(crate) klim: i32, 43 | } 44 | 45 | #[derive(Default)] 46 | pub(crate) struct PropAType { 47 | pub(crate) dlsa: f64, 48 | pub(crate) dx: f64, 49 | pub(crate) ael: f64, 50 | pub(crate) ak1: f64, 51 | pub(crate) ak2: f64, 52 | pub(crate) aed: f64, 53 | pub(crate) emd: f64, 54 | pub(crate) aes: f64, 55 | pub(crate) ems: f64, 56 | pub(crate) dls: [f64; 2], 57 | pub(crate) dla: f64, 58 | pub(crate) tha: f64, 59 | } 60 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/itwom3_port/pure.rs: -------------------------------------------------------------------------------- 1 | // Functions in this module are "pure" - they don't mutate state. 2 | // Separating these out to help find the thread_local/static C that 3 | // needs to retain state. 4 | 5 | use super::helpers::*; 6 | use super::PropType; 7 | use num_complex::Complex; 8 | 9 | pub(crate) fn curve(c1: f64, c2: f64, x1: f64, x2: f64, x3: f64, de: f64) -> f64 { 10 | let mut temp1 = (de - x2) / x3; 11 | let mut temp2 = de / x1; 12 | 13 | temp1 *= temp1; 14 | temp2 *= temp2; 15 | 16 | (c1 + c2 / (1.0 + temp1)) * temp2 / (1.0 + temp2) 17 | } 18 | 19 | pub(crate) fn ahd(td: f64) -> f64 { 20 | let i; 21 | let a = [133.4, 104.6, 71.8]; 22 | let b = [0.332e-3, 0.212e-3, 0.157e-3]; 23 | let c = [-4.343, -1.086, 2.171]; 24 | 25 | if td <= 10e3 { 26 | i = 0; 27 | } else if td <= 70e3 { 28 | i = 1; 29 | } else { 30 | i = 2; 31 | } 32 | 33 | a[i] + b[i] * td + c[i] * td.ln() 34 | } 35 | 36 | pub(crate) fn h0f(r: f64, et: f64) -> f64 { 37 | let a = [25.0, 80.0, 177.0, 395.0, 705.0]; 38 | let b = [24.0, 45.0, 68.0, 80.0, 105.0]; 39 | let q; 40 | let x; 41 | let mut h0fv; 42 | let temp; 43 | let mut it; 44 | 45 | it = et as i32; 46 | 47 | if it <= 0 { 48 | it = 1; 49 | q = 0.0; 50 | } else if it >= 5 { 51 | it = 5; 52 | q = 0.0; 53 | } else { 54 | q = et - it as f64; 55 | } 56 | 57 | /* x=pow(1.0/r,2.0); */ 58 | 59 | temp = 1.0 / r; 60 | x = temp * temp; 61 | 62 | h0fv = 4.343 * log((a[it as usize - 1] * x + b[it as usize - 1]) * x + 1.0); 63 | 64 | if q != 0.0 { 65 | h0fv = (1.0 - q) * h0fv + q * 4.343 * log((a[it as usize] * x + b[it as usize]) * x + 1.0); 66 | } 67 | 68 | h0fv 69 | } 70 | 71 | pub(crate) fn saalos(d: f64, prop: &PropType) -> f64 { 72 | let ensa; 73 | let encca; 74 | let mut q; 75 | let mut dp; 76 | let mut dx; 77 | let mut tde; 78 | let mut ucrpc; 79 | let mut ctip; 80 | let mut tip; 81 | let mut tic = 0.0; 82 | let mut stic; 83 | let ctic; 84 | let mut sta; 85 | let mut ttc; 86 | let mut crpc = 0.0; 87 | let mut ssnps = 0.0; 88 | let mut d1a; 89 | let mut rsp; 90 | let mut tsp; 91 | let mut arte; 92 | let zi; 93 | let pd; 94 | let pdk; 95 | let mut hone; 96 | let tvsr; 97 | let saalosv; 98 | let mut hc; 99 | let mut cttc = 0.0; 100 | 101 | //q = 0.0; // Never read after assignment 102 | 103 | if d == 0.0 { 104 | //tsp = 1.0; 105 | //rsp = 0.0; 106 | //d1a = 50.0; 107 | saalosv = 0.0; 108 | } else if prop.hg[1] > prop.cch { 109 | saalosv = 0.0; 110 | } else { 111 | pd = d; 112 | pdk = pd / 1000.0; 113 | //tsp = 1.0; 114 | //rsp = 0.0; 115 | d1a = pd; 116 | /* at first, hone is transmitter antenna height 117 | relative to receive site ground level. */ 118 | hone = prop.tgh + prop.tsgh - (prop.rch[1] - prop.hg[1]); 119 | 120 | if prop.tgh > prop.cch { 121 | /* for TX ant above all clutter height */ 122 | ensa = 1.0 + prop.ens * 0.000001; 123 | encca = 1.0 + prop.encc * 0.000001; 124 | dp = pd; 125 | 126 | for _j in 0..5 { 127 | tde = dp / 6378137.0; 128 | hc = (prop.cch + 6378137.0) * (1.0 - cos(tde)); 129 | dx = (prop.cch + 6378137.0) * sin(tde); 130 | ucrpc = sqrt((hone - prop.cch + hc) * (hone - prop.cch + hc) + (dx * dx)); 131 | ctip = (hone - prop.cch + hc) / ucrpc; 132 | tip = acos(ctip); 133 | tic = tip + tde; 134 | tic = f64::max(0.0, tic); 135 | stic = sin(tic); 136 | sta = (ensa / encca) * stic; 137 | ttc = asin(sta); 138 | cttc = sqrt(1.0 - (sin(ttc)) * (sin(ttc))); 139 | crpc = (prop.cch - prop.hg[1]) / cttc; 140 | if crpc >= dp { 141 | crpc = dp - 1.0 / dp; 142 | } 143 | 144 | ssnps = (3.1415926535897 / 2.0) - tic; 145 | d1a = (crpc * sin(ttc)) / (1.0 - 1.0 / 6378137.0); 146 | dp = pd - d1a; 147 | } 148 | 149 | ctic = cos(tic); 150 | 151 | /* if the ucrpc path touches the canopy before reaching the 152 | end of the ucrpc, the entry point moves toward the 153 | transmitter, extending the crpc and d1a. Estimating the d1a: */ 154 | 155 | if ssnps <= 0.0 { 156 | d1a = f64::min(0.1 * pd, 600.0); 157 | crpc = d1a; 158 | /* hone must be redefined as being barely above 159 | the canopy height with respect to the receiver 160 | canopy height, which despite the earth curvature 161 | is at or above the transmitter antenna height. */ 162 | hone = prop.cch + 1.0; 163 | rsp = 0.997; 164 | tsp = 1.0 - rsp; 165 | } else { 166 | if prop.ptx >= 1 { 167 | /* polarity ptx is vertical or circular */ 168 | q = (ensa * cttc - encca * ctic) / (ensa * cttc + encca * ctic); 169 | rsp = q * q; 170 | tsp = 1.0 - rsp; 171 | 172 | if prop.ptx == 2 { 173 | /* polarity is circular - new */ 174 | q = (ensa * ctic - encca * cttc) / (ensa * ctic + encca * cttc); 175 | rsp = (ensa * cttc - encca * ctic) / (ensa * cttc + encca * ctic); 176 | rsp = (q * q + rsp * rsp) / 2.0; 177 | tsp = 1.0 - rsp; 178 | } 179 | } else { 180 | /* ptx is 0, horizontal, or undefined */ 181 | 182 | q = (ensa * ctic - encca * cttc) / (ensa * ctic + encca * cttc); 183 | rsp = q * q; 184 | tsp = 1.0 - rsp; 185 | } 186 | } 187 | /* tvsr is defined as tx ant height above receiver ant height */ 188 | tvsr = f64::max(0.0, prop.tgh + prop.tsgh - prop.rch[1]); 189 | 190 | if d1a < 50.0 { 191 | arte = 0.0195 * crpc - 20.0 * log10(tsp); 192 | } else { 193 | if d1a < 225.0 { 194 | if tvsr > 1000.0 { 195 | q = d1a * (0.03 * exp(-0.14 * pdk)); 196 | } else { 197 | q = d1a * (0.07 * exp(-0.17 * pdk)); 198 | } 199 | 200 | arte = q 201 | + (0.7 * pdk - f64::max(0.01, log10(prop.wn * 47.7) - 2.0)) 202 | * (prop.hg[1] / hone); 203 | } else { 204 | q = 0.00055 * (pdk) + log10(pdk) * (0.041 - 0.0017 * sqrt(hone) + 0.019); 205 | 206 | arte = d1a * q - (18.0 * log10(rsp)) / (exp(hone / 37.5)); 207 | 208 | zi = 1.5 * sqrt(hone - prop.cch); 209 | 210 | if pdk > zi { 211 | q = (pdk - zi) 212 | * 10.2 213 | * ((sqrt(f64::max(0.01, log10(prop.wn * 47.7) - 2.0))) / (100.0 - zi)); 214 | } else { 215 | q = ((zi - pdk) / zi) 216 | * (-20.0 * f64::max(0.01, log10(prop.wn * 47.7) - 2.0)) 217 | / sqrt(hone); 218 | } 219 | arte = arte + q; 220 | } 221 | } 222 | } else { 223 | /* for TX at or below clutter height */ 224 | 225 | q = (prop.cch - prop.tgh) * (2.06943 - 1.56184 * exp(1.0 / prop.cch - prop.tgh)); 226 | q = q + (17.98 - 0.84224 * (prop.cch - prop.tgh)) * exp(-0.00000061 * pd); 227 | arte = q + 1.34795 * 20.0 * log10(pd + 1.0); 228 | arte = arte - (f64::max(0.01, log10(prop.wn * 47.7) - 2.0)) * (prop.hg[1] / prop.tgh); 229 | } 230 | saalosv = arte; 231 | } 232 | saalosv 233 | } 234 | 235 | pub(crate) fn qerfi(q: f64) -> f64 { 236 | let x; 237 | let mut t; 238 | let mut v; 239 | let c0 = 2.515516698; 240 | let c1 = 0.802853; 241 | let c2 = 0.010328; 242 | let d1 = 1.432788; 243 | let d2 = 0.189269; 244 | let d3 = 0.001308; 245 | 246 | x = 0.5 - q; 247 | t = mymax(0.5 - x.abs(), 0.000001); 248 | t = (-2.0 * t.ln()).sqrt(); 249 | v = t - ((c2 * t + c1) * t + c0) / (((d3 * t + d2) * t + d1) * t + 1.0); 250 | 251 | if x < 0.0 { 252 | v = -v; 253 | } 254 | 255 | v 256 | } 257 | 258 | pub(crate) fn qlrps( 259 | fmhz: f64, 260 | zsys: f64, 261 | en0: f64, 262 | ipol: i32, 263 | eps: f64, 264 | sgm: f64, 265 | prop: &mut PropType, 266 | ) { 267 | let gma = 157e-9; 268 | 269 | prop.wn = fmhz / 47.7; 270 | prop.ens = en0; 271 | 272 | if zsys != 0.0 { 273 | prop.ens *= (-zsys / 9460.0).exp(); 274 | } 275 | 276 | prop.gme = gma * (1.0 - 0.04665 * (prop.ens / 179.3).exp()); 277 | let mut prop_zgnd; 278 | let zq = Complex::new(eps, 376.62 * sgm / prop.wn); 279 | 280 | prop_zgnd = (zq - 1.0).sqrt(); 281 | 282 | if ipol != 0 { 283 | prop_zgnd = prop_zgnd / zq; 284 | } 285 | 286 | prop.zgndreal = prop_zgnd.re; 287 | prop.zgndimag = prop_zgnd.im; 288 | } 289 | 290 | pub(crate) fn hzns2(pfl: &[f64], prop: &mut PropType) { 291 | let mut wq; 292 | let np; 293 | let rp; 294 | //let mut i; 295 | //let mut j; 296 | let xi; 297 | let za; 298 | let zb; 299 | let qc; 300 | let mut q; 301 | let mut sb; 302 | let mut sa; 303 | let dr; 304 | let dshh; 305 | 306 | np = pfl[0] as usize; // Is this really a floor? 307 | xi = pfl[1]; 308 | za = pfl[2] + prop.hg[0]; 309 | zb = pfl[np + 2] + prop.hg[1]; 310 | prop.tiw = xi; 311 | prop.ght = za; 312 | prop.ghr = zb; 313 | qc = 0.5 * prop.gme; 314 | q = qc * prop.dist; 315 | prop.the[1] = ((zb - za) / prop.dist).atan(); 316 | prop.the[0] = (prop.the[1]) - q; 317 | prop.the[1] = -prop.the[1] - q; 318 | prop.dl[0] = prop.dist; 319 | prop.dl[1] = prop.dist; 320 | prop.hht = 0.0; 321 | prop.hhr = 0.0; 322 | prop.los = 1; 323 | 324 | if np >= 2 { 325 | sa = 0.0; 326 | sb = prop.dist; 327 | wq = true; 328 | 329 | for j in 1..np { 330 | sa += xi; 331 | q = pfl[j + 2] - (qc * sa + prop.the[0]) * sa - za; 332 | 333 | if q > 0.0 { 334 | prop.los = 0; 335 | prop.the[0] += q / sa; 336 | prop.dl[0] = sa; 337 | prop.the[0] = f64::min(prop.the[0], 1.569); 338 | prop.hht = pfl[j + 2]; 339 | wq = false; 340 | } 341 | } 342 | 343 | if !wq { 344 | for i in 1..np { 345 | sb -= xi; 346 | q = pfl[np + 2 - i] - (qc * (prop.dist - sb) + prop.the[1]) * (prop.dist - sb) - zb; 347 | if q > 0.0 { 348 | prop.the[1] += q / (prop.dist - sb); 349 | prop.the[1] = f64::min(prop.the[1], 1.57); 350 | prop.the[1] = mymax(prop.the[1], -1.568); 351 | prop.hhr = pfl[np + 2 - i]; 352 | prop.dl[1] = mymax(0.0, prop.dist - sb); 353 | } 354 | } 355 | prop.the[0] = ((prop.hht - za) / prop.dl[0]).atan() - 0.5 * prop.gme * prop.dl[0]; 356 | prop.the[1] = ((prop.hhr - zb) / prop.dl[1]).atan() - 0.5 * prop.gme * prop.dl[1]; 357 | } 358 | } 359 | 360 | if (prop.dl[1]) < (prop.dist) { 361 | dshh = prop.dist - prop.dl[0] - prop.dl[1]; 362 | 363 | if dshh as i32 == 0 { 364 | /* one obstacle */ 365 | dr = prop.dl[1] / (1.0 + zb / prop.hht); 366 | } else { 367 | /* two obstacles */ 368 | 369 | dr = prop.dl[1] / (1.0 + zb / prop.hhr); 370 | } 371 | } else { 372 | /* line of sight */ 373 | 374 | dr = (prop.dist) / (1.0 + zb / za); 375 | } 376 | rp = 2 + ((0.5 + dr / xi).floor()) as i32; 377 | prop.rpl = rp; 378 | prop.rph = pfl[rp as usize]; 379 | } 380 | 381 | pub(crate) fn z1sq2(z: &[f64], x1: f64, x2: f64, z0: &mut f64, zn: &mut f64) { 382 | /* corrected for use with ITWOM */ 383 | let xn; 384 | let mut xa; 385 | let mut xb; 386 | let mut x; 387 | let mut a; 388 | let mut b; 389 | let mut bn; 390 | let n; 391 | let mut ja; 392 | let jb; 393 | 394 | xn = z[0]; 395 | xa = (fortran_dim(x1 / z[1], 0.0)).floor(); 396 | xb = xn - (fortran_dim(xn, x2 / z[1])).floor(); 397 | 398 | if xb <= xa { 399 | xa = fortran_dim(xa, 1.0); 400 | xb = xn - fortran_dim(xn, xb + 1.0); 401 | } 402 | 403 | //ja = xa as i32; // Never read? 404 | jb = xb as i32; 405 | xa = (2.0 * ((xb - xa) / 2.0)) - 1.0; // Note that there were some whacky type conversions here 406 | x = -0.5 * (xa + 1.0); 407 | xb += x; 408 | ja = jb - 1 - xa as i32; 409 | n = jb - ja; 410 | a = z[ja as usize + 2] + z[jb as usize + 2]; 411 | b = (z[ja as usize + 2] - z[jb as usize + 2]) * x; 412 | bn = 2.0 * (x * x); 413 | 414 | for _i in 2..n { 415 | ja += 1; 416 | x += 1.0; 417 | bn += x * x; 418 | a += z[ja as usize + 2]; 419 | b += z[ja as usize + 2] * x; 420 | } 421 | 422 | a /= xa + 2.0; 423 | b = b / bn; 424 | *z0 = a - (b * xb); 425 | *zn = a + (b * (xn - xb)); 426 | } 427 | 428 | pub(crate) fn d1thx2(pfl: &[f64], x1: f64, x2: f64) -> f64 { 429 | let np; 430 | let mut ka; 431 | let kb; 432 | let n; 433 | let mut k; 434 | let kmx; 435 | let mut d1thx2v; 436 | let sn; 437 | let mut xa; 438 | let mut xb; 439 | let mut xc; 440 | 441 | //double *s; 442 | 443 | np = pfl[0] as i32; // Is this really just a floor? 444 | xa = x1 / pfl[1]; 445 | xb = x2 / pfl[1]; 446 | d1thx2v = 0.0; 447 | 448 | if xb - xa < 2.0 { 449 | // exit out 450 | return d1thx2v; 451 | } 452 | 453 | ka = (0.1 * (xb - xa + 8.0)) as i32; 454 | kmx = i32::max(25, (83350 / (pfl[1]) as i32) as i32); 455 | ka = i32::min(i32::max(4, ka), kmx); 456 | n = 10 * ka - 5; 457 | kb = n - ka + 1; 458 | sn = n - 1; 459 | let mut s = Vec::::with_capacity(n as usize + 2); 460 | for _i in 0..(n + 2) { 461 | s.push(0.0); 462 | } 463 | s[0] = sn as f64; 464 | s[1] = 1.0; 465 | xb = (xb - xa) / sn as f64; 466 | k = (xa + 1.0) as i32; 467 | xc = xa - (k as f64); 468 | 469 | for j in 0..n { 470 | while xc > 0.0 && k < np { 471 | xc -= 1.0; 472 | k += 1; 473 | } 474 | 475 | s[j as usize + 2] = pfl[k as usize + 2] + (pfl[k as usize + 2] - pfl[k as usize + 1]) * xc; 476 | xc = xc + xb; 477 | } 478 | 479 | z1sq2(&s, 0.0, sn as f64, &mut xa, &mut xb); 480 | xb = (xb - xa) / sn as f64; 481 | 482 | for j in 0..n { 483 | s[j as usize + 2] -= xa; 484 | xa = xa + xb; 485 | } 486 | 487 | d1thx2v = qtile(n - 1, &mut s, ka - 1) - qtile(n - 1, &mut s, kb - 1); // Warning: double-check that i interpreted s+2 right 488 | d1thx2v /= 1.0 - 0.8 * (-(x2 - x1) / 50.0e3).exp(); 489 | return d1thx2v; 490 | } 491 | 492 | pub(crate) fn qtile(nn: i32, a: &mut [f64], ir: i32) -> f64 { 493 | let mut q = 0.0; 494 | let mut r; 495 | let mut m; 496 | let mut n; 497 | let mut i; 498 | let mut j; 499 | let mut j1 = 0; 500 | let mut i0 = 0; 501 | let k; 502 | let mut done = false; 503 | let mut goto10 = true; 504 | 505 | m = 0; 506 | n = nn; 507 | k = f64::min(f64::max(0.0, ir as f64), n as f64); 508 | 509 | while !done { 510 | if goto10 { 511 | q = a[k as usize + 2]; 512 | i0 = m; 513 | j1 = n; 514 | } 515 | 516 | i = i0; 517 | 518 | while i <= n && a[i as usize + 2] >= q { 519 | i += 1; 520 | } 521 | 522 | if i > n { 523 | i = n; 524 | } 525 | 526 | j = j1; 527 | 528 | while j >= m && a[j as usize + 2] <= q { 529 | j -= 1; 530 | } 531 | 532 | if j < m { 533 | j = m; 534 | } 535 | 536 | if i < j { 537 | r = a[i as usize]; 538 | a[i as usize + 2] = a[j as usize + 2]; 539 | a[j as usize + 2] = r; 540 | i0 = i + 1; 541 | j1 = j - 1; 542 | goto10 = false; 543 | } else if i < k as i32 { 544 | a[k as usize + 2] = a[i as usize + 2]; 545 | a[i as usize + 2] = q; 546 | m = i + 1; 547 | goto10 = true; 548 | } else if j > k as i32 { 549 | a[k as usize + 2] = a[j as usize + 2]; 550 | a[j as usize + 2] = q; 551 | n = j - 1; 552 | goto10 = true; 553 | } else { 554 | done = true; 555 | } 556 | } 557 | 558 | return q; 559 | } 560 | 561 | pub(crate) fn fht(x: f64, pk: f64) -> f64 { 562 | let w; 563 | let mut fhtv; 564 | 565 | if x < 200.0 { 566 | w = -pk.ln(); 567 | 568 | if pk < 1.0e-5 || x * w * w * w > 5495.0 { 569 | fhtv = -117.0; 570 | 571 | if x > 1.0 { 572 | fhtv = 40.0 * x.log10() + fhtv; 573 | } 574 | } else { 575 | fhtv = 2.5e-5 * x * x / pk - 8.686 * w - 15.0; 576 | } 577 | } else { 578 | fhtv = 0.05751 * x - 10.0 * x.log10(); 579 | 580 | if x < 2000.0 { 581 | w = 0.0134 * x * (-0.005 * x).exp(); 582 | fhtv = (1.0 - w) * fhtv + w * (40.0 * x.log10() - 117.0); 583 | } 584 | } 585 | return fhtv; 586 | } 587 | 588 | pub(crate) fn alos2(d: f64, prop: &mut PropType) -> f64 { 589 | let prop_zgnd = Complex::new(prop.zgndreal, prop.zgndimag); 590 | let mut r; 591 | let cd; 592 | let cr; 593 | let dr; 594 | let hr; 595 | let hrg; 596 | let ht; 597 | let htg; 598 | let hrp; 599 | let re; 600 | let s; 601 | let sps; 602 | let mut q; 603 | let pd; 604 | let drh; 605 | /* int rp; */ 606 | let mut alosv; 607 | 608 | //cd = 0.0; // Never read after assignment 609 | //cr = 0.0; 610 | htg = prop.hg[0]; 611 | hrg = prop.hg[1]; 612 | ht = prop.ght; 613 | hr = prop.ghr; 614 | /* rp=prop.rpl; */ 615 | hrp = prop.rph; 616 | pd = prop.dist; 617 | 618 | if d == 0.0 { 619 | alosv = 0.0; 620 | } else { 621 | q = prop.he[0] + prop.he[1]; 622 | sps = q / (pd * pd + q * q).sqrt(); 623 | q = (1.0 - 0.8 * (-pd / 50e3).exp()) * prop.dh; 624 | 625 | if prop.mdp < 0 { 626 | dr = pd / (1.0 + hrg / htg); 627 | 628 | if dr < (0.5 * pd) { 629 | drh = 6378137.0 630 | - (-(0.5 * pd) * (0.5 * pd) 631 | + 6378137.0 * 6378137.0 632 | + (0.5 * pd - dr) * (0.5 * pd - dr)) 633 | .sqrt(); 634 | } else { 635 | drh = 6378137.0 636 | - (-(0.5 * pd) * (0.5 * pd) 637 | + 6378137.0 * 6378137.0 638 | + (dr - 0.5 * pd) * (dr - 0.5 * pd)) 639 | .sqrt(); 640 | } 641 | 642 | if (sps < 0.05) && (prop.cch > hrg) && (prop.dist < prop.dl[0]) { 643 | /* if far from transmitter and receiver below canopy */ 644 | cd = mymax(0.01, pd * (prop.cch - hrg) / (htg - hrg)); 645 | cr = mymax(0.01, pd - dr + dr * (prop.cch - drh) / htg); 646 | q = (1.0 - 0.8 * (-pd / 50e3).exp()) 647 | * prop.dh 648 | * (f64::min(-20.0 * (cd / cr).log10(), 1.0)); 649 | } 650 | } 651 | 652 | s = 0.78 * q * (-pow(q / 16.0, 0.25)).exp(); 653 | q = (-f64::min(10.0, prop.wn * s * sps)).exp(); 654 | r = q * (sps - prop_zgnd) / (sps + prop_zgnd); 655 | q = abq_alos(r); 656 | q = f64::min(q, 1.0); 657 | 658 | if q < 0.25 || q < sps { 659 | r = r * (sps / q).sqrt(); 660 | } 661 | q = prop.wn * prop.he[0] * prop.he[1] / (pd * 3.1415926535897); 662 | 663 | if prop.mdp < 0 { 664 | q = prop.wn * ((ht - hrp) * (hr - hrp)) / (pd * 3.1415926535897); 665 | } 666 | q -= q.floor(); 667 | 668 | if q < 0.5 { 669 | q *= 3.1415926535897; 670 | } else { 671 | q = (1.0 - q) * 3.1415926535897; 672 | } 673 | /* no longer valid complex conjugate removed 674 | by removing minus sign from in front of sin function */ 675 | re = abq_alos(Complex::new(q.cos(), q.sin()) + r); 676 | alosv = -10.0 * re.log10(); 677 | prop.tgh = prop.hg[0]; /*tx above gnd hgt set to antenna height AGL */ 678 | prop.tsgh = prop.rch[0] - prop.hg[0]; /* tsgh set to tx site gl AMSL */ 679 | 680 | if (prop.hg[1] < prop.cch) && (prop.thera < 0.785) && (prop.thenr < 0.785) { 681 | if sps < 0.05 { 682 | alosv = alosv + saalos(pd, prop); 683 | } else { 684 | alosv = saalos(pd, prop); 685 | } 686 | } 687 | } 688 | alosv = f64::min(22.0, alosv); 689 | return alosv; 690 | } 691 | 692 | pub(crate) fn abq_alos(r: Complex) -> f64 { 693 | return r.re * r.re + r.im * r.im; 694 | } 695 | 696 | pub(crate) fn aknfe(v2: f64) -> f64 { 697 | let a; 698 | 699 | if v2 < 5.76 { 700 | a = 6.02 + 9.11 * sqrt(v2) - 1.27 * v2; 701 | } else { 702 | a = 12.953 + 10.0 * log10(v2); 703 | } 704 | a 705 | } 706 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/mod.rs: -------------------------------------------------------------------------------- 1 | mod itwom3; 2 | pub use itwom3::{ 3 | itwom_point_to_point, GroundConductivity, PTPClimate, PTPPath, PTPResult, RadioClimate, 4 | }; 5 | mod fspl; 6 | mod itwom3_port; 7 | pub use fspl::free_space_path_loss_db; 8 | mod fresnel; 9 | pub use fresnel::fresnel_radius; 10 | mod cost_hata; 11 | pub use cost_hata::cost_path_loss; 12 | mod ecc33; 13 | pub use ecc33::ecc33_path_loss; 14 | mod egli; 15 | pub use egli::egli_path_loss; 16 | mod hata; 17 | pub use hata::hata_path_loss; 18 | mod pel; 19 | pub use pel::plane_earth_path_loss; 20 | mod soil; 21 | pub use soil::soil_path_loss; 22 | mod sui; 23 | use super::{Distance, Frequency}; 24 | pub use sui::sui_path_loss; 25 | 26 | /// Defines the calculation more for SUI, HATA, etc. path loss 27 | #[derive(Debug, PartialEq)] 28 | pub enum EstimateMode { 29 | Urban, 30 | Obstructed, 31 | Suburban, 32 | PartiallyObstructed, 33 | Rural, 34 | Open, 35 | } 36 | 37 | impl EstimateMode { 38 | fn to_mode(&self) -> i32 { 39 | match self { 40 | EstimateMode::Urban => 1, 41 | EstimateMode::Obstructed => 1, 42 | EstimateMode::Suburban => 2, 43 | EstimateMode::PartiallyObstructed => 2, 44 | EstimateMode::Rural => 3, 45 | EstimateMode::Open => 3, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/pel.rs: -------------------------------------------------------------------------------- 1 | use crate::Distance; 2 | 3 | /// Plane Earth Model 4 | /// Original C implementation: https://github.com/Cloud-RF/Signal-Server/blob/master/models/pel.cc 5 | /// Taken from "Antennas and Propagation for wireless communication systems" * 6 | /// ISBN 978-0-470-84879-1 (by Alex Farrant) 7 | /// 8 | /// Distance (meters) is unbounded. 9 | /// Frequency is not used in this calculation. 10 | /// Transmitter height and receiver height are height AMSL. 11 | pub fn plane_earth_path_loss(tx_height: Distance, rx_height: Distance, distance: Distance) -> f64 { 12 | let d = distance.as_km(); 13 | let txh = tx_height.as_meters(); 14 | let rxh = rx_height.as_meters(); 15 | (40.0 * d.log10()) + (20.0 * txh.log10()) + (20.0 * rxh.log10()) 16 | } 17 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/soil.rs: -------------------------------------------------------------------------------- 1 | use crate::{Distance, Frequency}; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum SoilError { 5 | PermittivityOutOfRange, 6 | } 7 | 8 | /// Soil Path Loss formula. 9 | /// Original: https://github.com/Cloud-RF/Signal-Server/blob/master/models/soil.cc 10 | /// Permitivity must be from 1 to 15, 1 = Worst, 15 = Best. 11 | /// Distance and Frequency are unbounded. 12 | pub fn soil_path_loss( 13 | frequency: Frequency, 14 | distance: Distance, 15 | terrain_permittivity: f64, 16 | ) -> Result { 17 | if terrain_permittivity < 1.0 || terrain_permittivity > 15.0 { 18 | return Err(SoilError::PermittivityOutOfRange); 19 | } 20 | let d = distance.as_km(); 21 | let f = frequency.as_mhz(); 22 | let soil = 120.0 / terrain_permittivity; 23 | Ok(6.4 + (20.0 * d.log10()) + (20.0 * f.log10()) + (8.69 * soil)) 24 | } 25 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/rfcalc/sui.rs: -------------------------------------------------------------------------------- 1 | use crate::{Distance, EstimateMode, Frequency}; 2 | use std::f64::consts::PI; 3 | 4 | #[derive(Debug, PartialEq)] 5 | pub enum SuiError { 6 | FrequencyOutOfRange, 7 | } 8 | 9 | /// Calculates SUI path loss. 10 | /// Original: https://github.com/Cloud-RF/Signal-Server/blob/master/models/sui.cc 11 | /// Frequency must be in 1900 to 11,000 Mhz range. 12 | /// Transmitter height, receiver_height and distance are unbounded. 13 | /// See http://www.cl.cam.ac.uk/research/dtg/lce-pub/public/vsa23/VTC05_Empirical.pdf 14 | /// And https://mentor.ieee.org/802.19/file/08/19-08-0010-00-0000-sui-path-loss-model.doc 15 | pub fn sui_path_loss( 16 | frequency: Frequency, 17 | tx_height: Distance, 18 | rx_height: Distance, 19 | distance: Distance, 20 | mode: EstimateMode, 21 | ) -> Result { 22 | let mode = mode.to_mode(); 23 | let d = distance.as_meters(); 24 | let f = frequency.as_mhz(); 25 | if f < 1900.0 || f > 11000.0 { 26 | return Err(SuiError::FrequencyOutOfRange); 27 | } 28 | let txh = tx_height.as_meters(); 29 | let rxh = rx_height.as_meters(); 30 | let mut a = 4.6; 31 | let mut b = 0.0075; 32 | let mut c = 12.6; 33 | let s = 8.2; 34 | let mut xhcf = -10.8; 35 | 36 | if mode == 2 { 37 | // Suburban 38 | a = 4.0; 39 | b = 0.0065; 40 | c = 17.1; 41 | xhcf = -10.8; 42 | } 43 | if mode == 3 { 44 | // Rural 45 | a = 3.6; 46 | b = 0.005; 47 | c = 20.0; 48 | xhcf = -20.0; 49 | } 50 | 51 | let d0 = 100.0; 52 | let big_a = 20.0 * ((4.0 * PI * d0) / (300.0 / f)).log10(); 53 | let y = a - (b * txh) + (c / txh); 54 | let mut xf = 0.0; 55 | let mut xh = 0.0; 56 | 57 | if f > 2000.0 { 58 | xf = 6.0 * (f / 2.0).log10(); 59 | xh = xhcf * (rxh / 2.0).log10(); 60 | } 61 | 62 | Ok(big_a + (10.0 * y) * (d / d0).log10() + xf + xh + s) 63 | } 64 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/units/distance.rs: -------------------------------------------------------------------------------- 1 | /// Provides distance unit conversions for this crate. 2 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] 3 | pub struct Distance(pub f64); 4 | 5 | impl Distance { 6 | /// Specify a distance in meters 7 | pub fn with_meters>(meters: T) -> Self { 8 | Self(meters.into()) 9 | } 10 | 11 | /// Specify a distance in km 12 | pub fn with_kilometers>(km: T) -> Self { 13 | Self(km.into() * 1000.0) 14 | } 15 | 16 | /// Specify a distance in feet 17 | pub fn with_feet>(feet: T) -> Self { 18 | Self(feet.into() * 0.3048) 19 | } 20 | 21 | /// Specify a distance in miles 22 | pub fn with_miles>(miles: T) -> Self { 23 | Self(miles.into() * 1609.34) 24 | } 25 | 26 | /// Retrieve the distance as meters 27 | pub fn as_meters(&self) -> f64 { 28 | self.0 29 | } 30 | 31 | /// Retrieve the distance as kilometers 32 | pub fn as_km(&self) -> f64 { 33 | self.0 / 1000.0 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod test { 39 | use super::Distance; 40 | 41 | #[test] 42 | pub fn meters_from_f32() { 43 | let m = Distance::with_meters(10.0f32); 44 | assert_eq!(m.0, 10.0f64); 45 | } 46 | 47 | #[test] 48 | pub fn meters_from_f64() { 49 | let m = Distance::with_meters(10.0f64); 50 | assert_eq!(m.0, 10.0f64); 51 | } 52 | 53 | #[test] 54 | pub fn meters_from_km() { 55 | let m = Distance::with_kilometers(1.0); 56 | assert_eq!(1000.0, m.0); 57 | } 58 | 59 | #[test] 60 | pub fn meters_from_feet() { 61 | let m = Distance::with_feet(1.0); 62 | assert_eq!(0.3048, m.0); 63 | } 64 | 65 | #[test] 66 | pub fn meters_from_miles() { 67 | let m = Distance::with_miles(1.0); 68 | assert_eq!(1609.34, m.0); 69 | } 70 | 71 | #[test] 72 | pub fn mile_to_km() { 73 | let m = Distance::with_miles(1.0); 74 | let km = m.as_km(); 75 | assert_eq!(1.60934, km); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/units/frequency.rs: -------------------------------------------------------------------------------- 1 | /// Type to represent a radio frequency in Hz 2 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] 3 | pub struct Frequency(pub f64); 4 | 5 | impl Frequency { 6 | /// Specify frequency as Hz 7 | pub fn with_hz>(hz: T) -> Self { 8 | Self(hz.into()) 9 | } 10 | 11 | /// Specify frequency as Mhz 12 | pub fn with_mhz>(mhz: T) -> Self { 13 | Self(mhz.into() * 1_000.0) 14 | } 15 | 16 | /// Specify frequency as Ghz 17 | pub fn with_ghz>(ghz: T) -> Self { 18 | Self(ghz.into() * 1_000_000.0) 19 | } 20 | 21 | /// Retrieve frequency as Hz 22 | pub fn as_hz(&self) -> f64 { 23 | self.0 24 | } 25 | 26 | /// Retrieve frequency as Mhz 27 | pub fn as_mhz(&self) -> f64 { 28 | self.0 / 1_000.0 29 | } 30 | 31 | /// Retrieve frequency as Ghz 32 | pub fn as_ghz(&self) -> f64 { 33 | self.0 / 1_000_000.0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rf-signal-algorithms/src/units/mod.rs: -------------------------------------------------------------------------------- 1 | mod distance; 2 | pub use distance::Distance; 3 | mod frequency; 4 | pub use frequency::Frequency; 5 | -------------------------------------------------------------------------------- /screenshots/basemap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/screenshots/basemap.jpg -------------------------------------------------------------------------------- /screenshots/heightmap1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/screenshots/heightmap1.jpg -------------------------------------------------------------------------------- /screenshots/heightmap2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/screenshots/heightmap2.jpg -------------------------------------------------------------------------------- /screenshots/locexplorer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/screenshots/locexplorer.jpg -------------------------------------------------------------------------------- /screenshots/quickestimate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/screenshots/quickestimate.jpg -------------------------------------------------------------------------------- /screenshots/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/screenshots/search.jpg -------------------------------------------------------------------------------- /screenshots/signaloptimize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebracket/rf-signals/3bd43337acabb72dd0571ea555170452a755cbab/screenshots/signaloptimize.jpg -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Workspace root. You probably didn't mean to run this."); 3 | } 4 | -------------------------------------------------------------------------------- /terrain-cooker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "terrain_cooker" 3 | version = "0.1.0" 4 | authors = ["herbert"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | las = "0.7" 11 | proj = "0.22" 12 | geo-types = "0.7" 13 | bytemuck = "1.4" 14 | image = "0.23" 15 | rf-signal-algorithms = { path = "../rf-signal-algorithms" } 16 | -------------------------------------------------------------------------------- /terrain-cooker/src/main.rs: -------------------------------------------------------------------------------- 1 | use geo_types::Point; 2 | use las::{point::Classification, Header, Read, Reader}; 3 | use proj::*; 4 | mod tile_writer; 5 | use rf_signal_algorithms::srtm::get_altitude; 6 | use rf_signal_algorithms::*; 7 | use std::{collections::HashMap, fs::read_dir, path::Path}; 8 | use tile_writer::*; 9 | 10 | const LIDAR_PATH: &str = "/home/herbert/lidar/"; 11 | 12 | fn get_projection_string(header: &Header) -> Option { 13 | let mut result = None; 14 | for v in header.all_vlrs() { 15 | if v.description == "GeoTiff ASCII parameters" { 16 | let mut s = String::new(); 17 | v.data.iter().for_each(|c| { 18 | s.push(*c as char); 19 | }); 20 | 21 | let mut split = s.split("|"); 22 | 23 | result = Some(split.nth(0).unwrap().to_string()); 24 | } 25 | } 26 | result 27 | } 28 | 29 | fn store_altitude( 30 | t: &mut MapTile, 31 | lat: f64, 32 | lon: f64, 33 | classification: &Classification, 34 | altitude_m: u16, 35 | ) { 36 | let index = t.index(lat, lon); 37 | match classification { 38 | Classification::Ground => t.store_ground(index, altitude_m), 39 | _ => t.store_clutter(index, altitude_m), 40 | } 41 | } 42 | 43 | fn main() { 44 | let dir = Path::new(LIDAR_PATH); 45 | if !dir.is_dir() { 46 | panic!("Must be a directory"); 47 | } 48 | 49 | for entry in read_dir(&dir).unwrap() { 50 | if let Ok(entry) = entry { 51 | if entry.path().is_file() && entry.path().extension().unwrap() == "las" { 52 | println!("Working on {}...", entry.path().to_str().as_ref().unwrap()); 53 | let mut reader = Reader::from_path(entry.path().to_str().as_ref().unwrap()) 54 | .expect("Unable to open LAS file"); 55 | let projection = get_projection_string(reader.header()).unwrap(); 56 | let to = "WGS84"; 57 | let converter = Proj::new_known_crs(&projection, &to, None).unwrap(); 58 | 59 | let mut tile_cache = HashMap::::new(); 60 | 61 | /*println!("{}..{}", reader.header().bounds().min.z, reader.header().bounds().max.z); 62 | let altitude_conversion = if reader.header().bounds().max.z > 500.0 { 63 | println!("Looks like feet"); 64 | 0.3048 65 | } else { 66 | println!("Looks like meters"); 67 | 1.0 68 | };*/ 69 | 70 | let mut altitude_conversion = 0.3048; 71 | reader 72 | .points() 73 | .filter(|p| p.is_ok()) 74 | .map(|p| p.unwrap()) 75 | .filter(|p| p.classification == Classification::Ground) 76 | .take(3) 77 | .for_each(|p| { 78 | let tmp = converter.convert(Point::new(p.x, p.y)).unwrap(); 79 | let ll = LatLon::new(tmp.lat(), tmp.lng()); 80 | let h = get_altitude( 81 | &ll, 82 | "/home/herbert/lidarserver/terrain" 83 | ).unwrap_or(Distance::with_meters(0)).as_meters(); 84 | let margin = h / 10.0; 85 | if p.z >= h-margin && p.z <= h+margin { 86 | altitude_conversion = 1.0; 87 | println!("It appears to be in feet. Known height: {}m, found: {}m. Margin: {}", h, p.z * altitude_conversion as f64, margin); 88 | } else { 89 | altitude_conversion = 0.3048; 90 | println!("It appears to be in feet. Known height: {}m, found: {}m", h, p.z * altitude_conversion as f64); 91 | } 92 | }); 93 | 94 | reader 95 | .points() 96 | .filter(|p| p.is_ok()) 97 | .map(|p| p.unwrap()) 98 | .for_each(|p| { 99 | let tmp = converter.convert(Point::new(p.x, p.y)).unwrap(); 100 | let filename = MapTile::get_tile_name(tmp.lat(), tmp.lng()); 101 | if let Some(t) = tile_cache.get_mut(&filename) { 102 | store_altitude( 103 | t, 104 | tmp.lat(), 105 | tmp.lng(), 106 | &p.classification, 107 | (p.z as f32 * 10.0 * altitude_conversion) as u16, 108 | ); 109 | } else { 110 | let mut tile = MapTile::get_tile(tmp.lat(), tmp.lng()); 111 | store_altitude( 112 | &mut tile, 113 | tmp.lat(), 114 | tmp.lng(), 115 | &p.classification, 116 | (p.z as f32 * 10.0 * altitude_conversion) as u16, 117 | ); 118 | tile_cache.insert(filename, tile); 119 | } 120 | }); 121 | tile_cache.iter().for_each(|(_, v)| { 122 | v.save(); 123 | v.save_png(); 124 | }); 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /terrain-cooker/src/tile_writer.rs: -------------------------------------------------------------------------------- 1 | use rf_signal_algorithms::srtm::get_altitude; 2 | use rf_signal_algorithms::*; 3 | use std::fs::File; 4 | use std::io::prelude::*; 5 | use std::path::Path; 6 | 7 | const ROW_SIZE: usize = 768; 8 | const COL_SIZE: usize = 768; 9 | const NUM_CELLS: usize = COL_SIZE * ROW_SIZE; 10 | const TOTAL_ENTRIES: usize = NUM_CELLS * 2; 11 | 12 | pub struct MapTile { 13 | filename: String, 14 | heights: Vec, 15 | } 16 | 17 | impl MapTile { 18 | pub fn get_tile(lat: f64, lon: f64) -> Self { 19 | let filename = MapTile::get_tile_name(lat, lon); 20 | if Path::new(&filename).exists() { 21 | println!("Loading existing tile area: {}", filename); 22 | let mut tile = MapTile { 23 | filename, 24 | heights: vec![0; TOTAL_ENTRIES], 25 | }; 26 | // Load the tile 27 | let mut file = File::open(&tile.filename).expect("Unable to open file"); 28 | let size_of_data = TOTAL_ENTRIES; 29 | let mut h = [0u8; 2]; 30 | for i in 0..size_of_data { 31 | let bytes_read = file.read(&mut h).expect("Read fail"); 32 | if bytes_read != 2 { 33 | panic!("Overread"); 34 | } 35 | let h = bytemuck::from_bytes::(&h); 36 | tile.heights[i] = *h; 37 | } 38 | tile 39 | } else { 40 | // Make a new one 41 | let mut tile = MapTile { 42 | filename, 43 | heights: vec![0; TOTAL_ENTRIES], 44 | }; 45 | for idx in 0..(NUM_CELLS) { 46 | let (plat, plon) = tile.coords(idx, lat, lon); 47 | let ll = LatLon::new(plat, plon); 48 | let h = get_altitude(&ll, "/home/herbert/lidarserver/terrain") 49 | .unwrap_or(Distance::with_meters(0)) 50 | .as_meters(); 51 | tile.heights[idx * 2] = (h * 10.0) as u16; 52 | } 53 | tile.save(); 54 | tile 55 | } 56 | } 57 | 58 | pub fn index(&self, lat: f64, lon: f64) -> usize { 59 | let lat_abs = lat.abs(); 60 | let lon_abs = lon.abs(); 61 | let lat_floor = lat_abs.floor(); 62 | let lon_floor = lon_abs.floor(); 63 | 64 | let sub_lat = (lat_abs.fract() * 100.0).floor(); 65 | let sub_lon = (lon_abs.fract() * 100.0).floor(); 66 | 67 | let base_lat = lat_floor + (sub_lat / 100.0); 68 | let lat_min = (lat_abs - base_lat) * 100.0; 69 | let row_index = (lat_min * ROW_SIZE as f64) as usize; 70 | 71 | let base_lon = lon_floor + (sub_lon / 100.0); 72 | let lon_min = (lon_abs - base_lon) * 100.0; 73 | let col_index = (lon_min * COL_SIZE as f64) as usize; 74 | 75 | (row_index * COL_SIZE) + col_index 76 | } 77 | 78 | pub fn store_ground(&mut self, index: usize, altitude: u16) { 79 | if self.heights[index * 2] < altitude { 80 | self.heights[index * 2] = altitude; 81 | } 82 | } 83 | 84 | pub fn store_clutter(&mut self, index: usize, altitude: u16) { 85 | if self.heights[(index * 2) + 1] < altitude { 86 | self.heights[(index * 2) + 1] = altitude; 87 | } 88 | } 89 | 90 | pub fn save(&self) { 91 | let filename = format!("{}", self.filename); 92 | let mut file = File::create(&filename).expect("Creating file failed"); 93 | file.write_all(bytemuck::cast_slice(&self.heights)) 94 | .expect("Write failed"); 95 | } 96 | 97 | pub fn get_tile_name(lat: f64, lon: f64) -> String { 98 | let lat_c = if lat < 0.0 { 'S' } else { 'N' }; 99 | let lon_c = if lon < 0.0 { 'W' } else { 'E' }; 100 | 101 | let lat_abs = lat.abs(); 102 | let lon_abs = lon.abs(); 103 | let lat_floor = lat_abs.floor(); 104 | let lon_floor = lon_abs.floor(); 105 | 106 | let sub_lat = (lat_abs.fract() * 100.0).floor(); 107 | let sub_lon = (lon_abs.fract() * 100.0).floor(); 108 | 109 | format!( 110 | "/home/herbert/bheat/{}{:03}t{:02}_{}{:03 }t{:02}.bheat", 111 | lat_c, lat_floor as i32, sub_lat, lon_c, lon_floor as i32, sub_lon, 112 | ) 113 | } 114 | 115 | pub fn save_png(&self) { 116 | let t_max_height_m = self.heights.iter().step_by(2).max().unwrap_or(&0) / 10; 117 | let t_min_height_m = self.heights.iter().step_by(2).min().unwrap_or(&0) / 10; 118 | let c_max_height_m = self.heights.iter().skip(1).step_by(2).max().unwrap_or(&0) / 10; 119 | let c_min_height_m = self.heights.iter().skip(1).step_by(2).min().unwrap_or(&0) / 10; 120 | let t_span = t_max_height_m - t_min_height_m; 121 | let c_span = c_max_height_m - c_min_height_m; 122 | 123 | let filename = format!("{}.png", self.filename); 124 | let mut imgbuf = image::ImageBuffer::new(COL_SIZE as u32, ROW_SIZE as u32); 125 | for (x, y, pixel) in imgbuf.enumerate_pixels_mut() { 126 | let index = (y as usize * ROW_SIZE) + x as usize; 127 | let h = self.heights[index * 2] / 10; 128 | let h2 = self.heights[(index * 2) + 1] / 10; 129 | let shade_f = (h - t_min_height_m) as f32 / t_span as f32; 130 | let mut shade = if h > 0 { (255.0 * shade_f) as u8 } else { 0 }; 131 | let shade2_f = (h2 - c_min_height_m) as f32 / c_span as f32; 132 | let shade2 = if h2 > 0 { (255.0 * shade2_f) as u8 } else { 0 }; 133 | 134 | shade = u8::max(shade, shade2); 135 | 136 | *pixel = image::Rgb([shade, shade, shade]); 137 | } 138 | imgbuf.save(&filename).expect("Save PNG failed"); 139 | } 140 | 141 | pub fn coords(&self, index: usize, lat: f64, lon: f64) -> (f64, f64) { 142 | let col = index % COL_SIZE; 143 | let row = index / COL_SIZE; 144 | 145 | let lat_abs = lat.abs(); 146 | let lon_abs = lon.abs(); 147 | let lat_floor = lat_abs.floor(); 148 | let lon_floor = lon_abs.floor(); 149 | 150 | let sub_lat = (lat_abs.fract() * 100.0).floor(); 151 | let sub_lon = (lon_abs.fract() * 100.0).floor(); 152 | 153 | let base_lat = lat_floor + (sub_lat / 100.0); 154 | let lat_step = (1.0 / COL_SIZE as f64) / 100.0; 155 | let lat_pos = base_lat + (lat_step * row as f64); 156 | 157 | let base_lon = lon_floor + (sub_lon / 100.0); 158 | let lon_step = (1.0 / COL_SIZE as f64) / 100.0; 159 | let lon_pos = base_lon + (lon_step * col as f64); 160 | 161 | let lat_final = if lat < 0.0 { lat_pos * -1.0 } else { lat_pos }; 162 | 163 | let lon_final = if lon < 0.0 { lon_pos * -1.0 } else { lon_pos }; 164 | 165 | (lat_final, lon_final) 166 | } 167 | } 168 | --------------------------------------------------------------------------------