├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── debian ├── changelog ├── compat ├── control ├── copyright ├── files ├── init.d.ex ├── install ├── manpage.1.ex ├── manpage.sgml.ex ├── manpage.xml.ex ├── menu.ex ├── postinst.ex ├── postrm.ex ├── preinst.ex ├── prerm.ex ├── python3-repeater-start.substvars ├── repeater-start-docs.docs ├── repeater-start.cron.d.ex ├── repeater-start.debhelper.log ├── repeater-start.default.ex ├── repeater-start.doc-base.EX ├── repeater-start.substvars ├── rules ├── source │ └── format └── watch.ex ├── nohtmlstatus.txt ├── repeaters.json ├── resources ├── repeaterSTART ├── repeaterSTART.svg └── repeaterstart.desktop └── src ├── .gitignore ├── CsvRepeaterListing.py ├── HearHamRepeater.py ├── HelpDialog.py ├── IRLPNode.py ├── MaidenheadLocator.py ├── NetworkStatus.py ├── Repeater.py ├── RepeaterStartCommon.py ├── SettingsDialog.glade ├── SettingsDialog.py ├── lib └── openlocationcode.py ├── locateme.svg ├── mapbox.svg ├── repeaterstart.py ├── signaltower.svg └── signaltowerdown.svg /.gitignore: -------------------------------------------------------------------------------- 1 | debian/repeater-start 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Repeater-START 2 | 3 | ## How Can I Contribute? 4 | 5 | ### Reporting Bugs 6 | 7 | If a certain feature is not working or not working as you expected, please add a github issue. 8 | 9 | Any bugs or feature requests should include details on operating system, version used, GTK version or other libraries that may be needed. 10 | 11 | ### Your First Code Contribution 12 | 13 | You can find an issue to start on in the Github issues list. Fork the repository and make changes as described here: 14 | 15 | https://docs.github.com/en/enterprise/2.20/user/github/getting-started-with-github/fork-a-repo 16 | 17 | ### Other Questions, Feedback, or Security Notices 18 | 19 | Please contact luke (at) hearham.live 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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 | 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) 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 along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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) year 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 Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repeater-START 2 | Repeater START - Showing The Amateur Repeaters Tool 3 | 4 | This tool displays all the repeaters available through the repeater listing at https://hearham.com/repeaters 5 | 6 | Support for searching for a Maidenhead-grid-square coordinate, WhatThreeWords Position, a mountain or peak on Openstreetmap, repeater frequency/IRLP node, or finding your current location, is included. 7 | 8 | You can download this program for Windows 8, Windows 10, recent Ubuntu and Debian based systems at https://sourceforge.net/projects/repeater-start/ 9 | Simply download and open the file, select install. OR, if you prefer command line: 10 | 11 | ``` 12 | sudo dpkg -i ./repeater-start_1.0_all.deb 13 | ``` 14 | 15 | 16 | # Development 17 | 18 | Linux computer/phone: 19 | 20 | This is written in Python for GTK+. If you run it you should have: 21 | * Geoclue introspection - for getting your location easily. 22 | * libosmgpsmap introspection - for the maps widget display. 23 | 24 | Then just run "python3 repeaterstart.py" 25 | 26 | Windows: 27 | 28 | Much of this will run on Windows. Check out the "windows" branch which has Geoclue locate-me button removed, and runs in msys2. 29 | 30 | ``` 31 | pacman -S mingw-w64-x86_64-osm-gps-map 32 | pacman -S python3 33 | pacman -S mingw-w64-x86_64-python-gobject 34 | ``` 35 | 36 | are requirements, for the map library and Python. Run with mingw python in the msys2 console: 37 | 38 | ``` 39 | cd src 40 | /mingw64/bin/python3.exe ./repeaterstart.py 41 | ``` 42 | 43 | 44 | Android: 45 | 46 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.hearham.repeaterstartpremium) 49 | 50 | 51 | iOS: 52 | 53 | [Get it on Apple store](https://apps.apple.com/us/app/repeaterstart-premium/id6738314675) 56 | 57 | 58 | [![Liberapay](https://img.shields.io/liberapay/patrons/deepdaikon.svg?logo=liberapay)](https://liberapay.com/Programmin/) 59 | [Ko-fi](https://ko-fi.com/hearham) 60 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Always use the latest version for best results - alert should show if you are on an older version on the Linux app. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | For security concerns or other queries please contact luke at hearham.live. 10 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | repeater-start (1.0.1) stable; urgency=medium 2 | * Faster initial start with thousands of repeaters. 3 | * Unavailable custom repeater icon bug where it showed the same icon, should be red now. 4 | * Minor fixes. 5 | 6 | -- Luke Sat, Jan 18 2025 23:30:00 -0800 7 | 8 | repeater-start (1.0) stable; urgency=medium 9 | * Premium license key enables step by step instructions. 10 | 11 | -- Luke Mon, Sep 16 2024 21:20:00 -0800 12 | 13 | 14 | repeater-start (0.99-beta) unstable; urgency=medium 15 | * Add data/mobile check. 16 | * Add ability to add different repositories of repeaters. 17 | 18 | -- Luke Sat, Dec 23 2023 23:30:00 -0800 19 | 20 | repeater-start (0.9.1) unstable; urgency=medium 21 | * Fix Openstreetmap Nominatim search 22 | 23 | -- Luke Fri, Aug 2023 00:06:00 -0000 24 | 25 | repeater-start (0.8) unstable; urgency=medium 26 | * Optimize loading of repeaters, introduce a right click menu. 27 | 28 | -- Luke Fri, May 15 2022 20:51:00 -0800 29 | 30 | 31 | repeater-start (0.9) unstable; urgency=medium 32 | * Introduces resizable map/listing area. 33 | * Adds ability to search for Global Plus Code and center map, even offline. 34 | 35 | -- Luke Fri, Nov 18 2022 00:16:00 -0800 36 | 37 | repeater-start (0.8) unstable; urgency=medium 38 | * Optimize loading of repeaters, introduce a right click menu. 39 | 40 | -- Luke Fri, May 15 2022 20:51:00 -0800 41 | 42 | 43 | repeater-start (0.7.2) unstable; urgency=medium 44 | * Fix a null grey maps issue on some systems and icon placement when installed on Librem. 45 | 46 | -- Luke Fri, Mar 4 2022 22:26:00 -0800 47 | 48 | repeater-start (0.7.1) unstable; urgency=medium 49 | * Fix very slow startup and reinit of all the icons. 50 | 51 | -- Luke Sun, Feb 21 2021 21:22:00 -0800 52 | 53 | repeater-start (0.7) unstable; urgency=medium 54 | * Search for frequency/internet node number. 55 | * Fix display of red repeater icon for OFFLINE IRLP. 56 | 57 | -- Luke Sun, Jan 31 2021 12:00:00 -0800 58 | 59 | repeater-start (0.6) unstable; urgency=medium 60 | * Search for or view Maidenhead-grid-square. 61 | * New Nominatim.openstreetmap.org api is used. 62 | 63 | -- Luke Sun, Dec 27 2020 22:30:00 -0800 64 | 65 | repeater-start (0.5) unstable; urgency=medium 66 | * Inoperable repeaters are now shown in red. 67 | * Settings dialog for frequency filter and length units. 68 | 69 | -- Luke Mon, Nov 23 2020 23:00:00 -0800 70 | 71 | repeater-start (0.4) unstable; urgency=medium 72 | * Auto save and load the map position and zoom when reopening the app. 73 | 74 | -- Luke Thu, 17 Sep 2020 12:00:00 -0800 75 | 76 | 77 | repeater-start (0.3.3) unstable; urgency=medium 78 | * Geoclue fixes for more recent Linux - locate me button should work now when permission is added. 79 | 80 | -- Luke Sat, 25 Jul 2020 05:25:00 -0800 81 | 82 | 83 | 84 | repeater-start (0.3.2) unstable; urgency=medium 85 | * Improved search, What3words capability. 86 | 87 | -- Luke Sat, 18 Jul 2020 04:45:00 -0800 88 | 89 | repeater-start (0.3.1) unstable; urgency=medium 90 | * Improved IRLP description, link button 91 | 92 | -- Luke Wed, 4 Jun 2020 21:05:00 -0800 93 | 94 | repeater-start (0.3) unstable; urgency=medium 95 | * Improved Mapbox topo, updated API 96 | * Search ability 97 | * Faster in well used areas 98 | 99 | -- Luke Wed, 27 May 2020 21:15:00 -0800 100 | 101 | repeater-start (0.2) unstable; urgency=medium 102 | 103 | * Mapbox topo map 104 | * Better icons and ui. 105 | * Add repeater button. 106 | 107 | -- Luke Wed, 18 Mar 2020 7:15:15 -0800 108 | 109 | repeater-start (0.1-1) unstable; urgency=medium 110 | 111 | * Initial release 112 | 113 | -- Luke Fri, 28 Feb 2020 22:45:15 -0800 114 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: repeater-start 2 | Section: Education 3 | Priority: optional 4 | Maintainer: Luke 5 | Build-Depends: debhelper (>=9),python3-setuptools,python3-all,python3-setuptools 6 | Standards-Version: 3.9.6 7 | Homepage: Hearham.live/repeaters 8 | X-Python-Version: >= 2.6 9 | X-Python3-Version: >= 3.2 10 | #Vcs-Git: git://anonscm.debian.org/collab-maint/repeater-start.git 11 | #Vcs-Browser: https://anonscm.debian.org/cgit/collab-maint/repeater-start.git 12 | 13 | Package: repeater-start 14 | Architecture: all 15 | Depends: ${python3:Depends}, gir1.2-osmgpsmap-1.0, gir1.2-geoclue-2.0, python3-gi, python3-gi-cairo, xdg-utils 16 | Suggests: 17 | Description: Repeater-START (Showing The Amateur Repeaters Tool) 18 | 19 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: repeater-start 3 | Source: 4 | 5 | Files: * 6 | Copyright: 2020 Luke Bryan 7 | License: GPL2 8 | 9 | Files: debian/* 10 | Copyright: 2020 Luke 11 | License: GPL-2+ 12 | This package is free software; you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation; either version 2 of the License, or 15 | (at your option) any later version. 16 | . 17 | This package is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | . 22 | You should have received a copy of the GNU General Public License 23 | along with this program. If not, see 24 | . 25 | On Debian systems, the complete text of the GNU General 26 | Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". 27 | -------------------------------------------------------------------------------- /debian/files: -------------------------------------------------------------------------------- 1 | repeater-start_1.0.1_all.deb Education optional 2 | repeater-start_1.0.1_amd64.buildinfo Education optional 3 | -------------------------------------------------------------------------------- /debian/init.d.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # kFreeBSD do not accept scripts as interpreters, using #!/bin/sh and sourcing. 3 | if [ true != "$INIT_D_SCRIPT_SOURCED" ] ; then 4 | set "$0" "$@"; INIT_D_SCRIPT_SOURCED=true . /lib/init/init-d-script 5 | fi 6 | ### BEGIN INIT INFO 7 | # Provides: repeater-start 8 | # Required-Start: $remote_fs $syslog 9 | # Required-Stop: $remote_fs $syslog 10 | # Default-Start: 2 3 4 5 11 | # Default-Stop: 0 1 6 12 | # Short-Description: 13 | # Description: 14 | # <...> 15 | # <...> 16 | ### END INIT INFO 17 | 18 | # Author: Luke 19 | 20 | DESC="repeater-start" 21 | DAEMON=/usr/bin/repeater-start 22 | 23 | # This is an example to start a single forking daemon capable of writing 24 | # a pid file. To get other behaviors, implement do_start(), do_stop() or 25 | # other functions to override the defaults in /lib/init/init-d-script. 26 | # See also init-d-script(5) 27 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | src/* usr/share/repeater-START 2 | resources/repeaterstart.desktop usr/share/applications 3 | resources/repeaterSTART /usr/bin 4 | resources/repeaterSTART.svg usr/share/icons/hicolor/scalable/apps/ 5 | -------------------------------------------------------------------------------- /debian/manpage.1.ex: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" (C) Copyright 2020 Luke , 3 | .\" 4 | .\" First parameter, NAME, should be all caps 5 | .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection 6 | .\" other parameters are allowed: see man(7), man(1) 7 | .TH Repeater-start SECTION "February 28 2020" 8 | .\" Please adjust this date whenever revising the manpage. 9 | .\" 10 | .\" Some roff macros, for reference: 11 | .\" .nh disable hyphenation 12 | .\" .hy enable hyphenation 13 | .\" .ad l left justify 14 | .\" .ad b justify to both left and right margins 15 | .\" .nf disable filling 16 | .\" .fi enable filling 17 | .\" .br insert line break 18 | .\" .sp insert n+1 empty lines 19 | .\" for manpage-specific macros, see man(7) 20 | .SH NAME 21 | repeater-start \- program to do something 22 | .SH SYNOPSIS 23 | .B repeater-start 24 | .RI [ options ] " files" ... 25 | .br 26 | .B bar 27 | .RI [ options ] " files" ... 28 | .SH DESCRIPTION 29 | This manual page documents briefly the 30 | .B repeater-start 31 | and 32 | .B bar 33 | commands. 34 | .PP 35 | .\" TeX users may be more comfortable with the \fB\fP and 36 | .\" \fI\fP escape sequences to invode bold face and italics, 37 | .\" respectively. 38 | \fBrepeater-start\fP is a program that... 39 | .SH OPTIONS 40 | These programs follow the usual GNU command line syntax, with long 41 | options starting with two dashes (`-'). 42 | A summary of options is included below. 43 | For a complete description, see the Info files. 44 | .TP 45 | .B \-h, \-\-help 46 | Show summary of options. 47 | .TP 48 | .B \-v, \-\-version 49 | Show version of program. 50 | .SH SEE ALSO 51 | .BR bar (1), 52 | .BR baz (1). 53 | .br 54 | The programs are documented fully by 55 | .IR "The Rise and Fall of a Fooish Bar" , 56 | available via the Info system. 57 | -------------------------------------------------------------------------------- /debian/manpage.sgml.ex: -------------------------------------------------------------------------------- 1 | manpage.1'. You may view 5 | the manual page with: `docbook-to-man manpage.sgml | nroff -man | 6 | less'. A typical entry in a Makefile or Makefile.am is: 7 | 8 | manpage.1: manpage.sgml 9 | docbook-to-man $< > $@ 10 | 11 | 12 | The docbook-to-man binary is found in the docbook-to-man package. 13 | Please remember that if you create the nroff version in one of the 14 | debian/rules file targets (such as build), you will need to include 15 | docbook-to-man in your Build-Depends control field. 16 | 17 | --> 18 | 19 | 20 | FIRSTNAME"> 21 | SURNAME"> 22 | 23 | February 28 2020"> 24 | 26 | SECTION"> 27 | luke@unknown"> 28 | 29 | Repeater-start"> 30 | 31 | 32 | Debian"> 33 | GNU"> 34 | GPL"> 35 | ]> 36 | 37 | 38 | 39 |
40 | &dhemail; 41 |
42 | 43 | &dhfirstname; 44 | &dhsurname; 45 | 46 | 47 | 2003 48 | &dhusername; 49 | 50 | &dhdate; 51 |
52 | 53 | &dhucpackage; 54 | 55 | &dhsection; 56 | 57 | 58 | &dhpackage; 59 | 60 | program to do something 61 | 62 | 63 | 64 | &dhpackage; 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | DESCRIPTION 73 | 74 | This manual page documents briefly the 75 | &dhpackage; and bar 76 | commands. 77 | 78 | This manual page was written for the &debian; distribution 79 | because the original program does not have a manual page. 80 | Instead, it has documentation in the &gnu; 81 | Info format; see below. 82 | 83 | &dhpackage; is a program that... 84 | 85 | 86 | 87 | OPTIONS 88 | 89 | These programs follow the usual &gnu; command line syntax, 90 | with long options starting with two dashes (`-'). A summary of 91 | options is included below. For a complete description, see the 92 | Info files. 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | Show summary of options. 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | Show version of program. 109 | 110 | 111 | 112 | 113 | 114 | SEE ALSO 115 | 116 | bar (1), baz (1). 117 | 118 | The programs are documented fully by The Rise and 119 | Fall of a Fooish Bar available via the 120 | Info system. 121 | 122 | 123 | AUTHOR 124 | 125 | This manual page was written by &dhusername; &dhemail; for 126 | the &debian; system (and may be used by others). Permission is 127 | granted to copy, distribute and/or modify this document under 128 | the terms of the &gnu; General Public License, Version 2 any 129 | later version published by the Free Software Foundation. 130 | 131 | 132 | On Debian systems, the complete text of the GNU General Public 133 | License can be found in /usr/share/common-licenses/GPL. 134 | 135 | 136 | 137 |
138 | 139 | 155 | -------------------------------------------------------------------------------- /debian/manpage.xml.ex: -------------------------------------------------------------------------------- 1 | 2 | .
will be generated. You may view the 15 | manual page with: nroff -man .
| less'. A typical entry 16 | in a Makefile or Makefile.am is: 17 | 18 | DB2MAN = /usr/share/sgml/docbook/stylesheet/xsl/docbook-xsl/manpages/docbook.xsl 19 | XP = xsltproc -''-nonet -''-param man.charmap.use.subset "0" 20 | 21 | manpage.1: manpage.xml 22 | $(XP) $(DB2MAN) $< 23 | 24 | The xsltproc binary is found in the xsltproc package. The XSL files are in 25 | docbook-xsl. A description of the parameters you can use can be found in the 26 | docbook-xsl-doc-* packages. Please remember that if you create the nroff 27 | version in one of the debian/rules file targets (such as build), you will need 28 | to include xsltproc and docbook-xsl in your Build-Depends control field. 29 | Alternatively use the xmlto command/package. That will also automatically 30 | pull in xsltproc and docbook-xsl. 31 | 32 | Notes for using docbook2x: docbook2x-man does not automatically create the 33 | AUTHOR(S) and COPYRIGHT sections. In this case, please add them manually as 34 | ... . 35 | 36 | To disable the automatic creation of the AUTHOR(S) and COPYRIGHT sections 37 | read /usr/share/doc/docbook-xsl/doc/manpages/authors.html. This file can be 38 | found in the docbook-xsl-doc-html package. 39 | 40 | Validation can be done using: `xmllint -''-noout -''-valid manpage.xml` 41 | 42 | General documentation about man-pages and man-page-formatting: 43 | man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/ 44 | 45 | --> 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 59 | 60 | 61 | 62 | ]> 63 | 64 | 65 | 66 | &dhtitle; 67 | &dhpackage; 68 | 69 | 70 | &dhfirstname; 71 | &dhsurname; 72 | Wrote this manpage for the Debian system. 73 |
74 | &dhemail; 75 |
76 |
77 |
78 | 79 | 2007 80 | &dhusername; 81 | 82 | 83 | This manual page was written for the Debian system 84 | (and may be used by others). 85 | Permission is granted to copy, distribute and/or modify this 86 | document under the terms of the GNU General Public License, 87 | Version 2 or (at your option) any later version published by 88 | the Free Software Foundation. 89 | On Debian systems, the complete text of the GNU General Public 90 | License can be found in 91 | /usr/share/common-licenses/GPL. 92 | 93 |
94 | 95 | &dhucpackage; 96 | &dhsection; 97 | 98 | 99 | &dhpackage; 100 | program to do something 101 | 102 | 103 | 104 | &dhpackage; 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | this 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | this 122 | that 123 | 124 | 125 | 126 | 127 | &dhpackage; 128 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | DESCRIPTION 148 | This manual page documents briefly the 149 | &dhpackage; and bar 150 | commands. 151 | This manual page was written for the Debian distribution 152 | because the original program does not have a manual page. 153 | Instead, it has documentation in the GNU 154 | info 155 | 1 156 | format; see below. 157 | &dhpackage; is a program that... 158 | 159 | 160 | OPTIONS 161 | The program follows the usual GNU command line syntax, 162 | with long options starting with two dashes (`-'). A summary of 163 | options is included below. For a complete description, see the 164 | 165 | info 166 | 1 167 | files. 168 | 169 | 172 | 173 | 174 | 175 | 176 | Does this and that. 177 | 178 | 179 | 180 | 181 | 182 | 183 | Show summary of options. 184 | 185 | 186 | 187 | 188 | 189 | 190 | Show version of program. 191 | 192 | 193 | 194 | 195 | 196 | FILES 197 | 198 | 199 | /etc/foo.conf 200 | 201 | The system-wide configuration file to control the 202 | behaviour of &dhpackage;. See 203 | 204 | foo.conf 205 | 5 206 | for further details. 207 | 208 | 209 | 210 | ${HOME}/.foo.conf 211 | 212 | The per-user configuration file to control the 213 | behaviour of &dhpackage;. See 214 | 215 | foo.conf 216 | 5 217 | for further details. 218 | 219 | 220 | 221 | 222 | 223 | ENVIRONMENT 224 | 225 | 226 | FOO_CONF 227 | 228 | If used, the defined file is used as configuration 229 | file (see also ). 230 | 231 | 232 | 233 | 234 | 235 | DIAGNOSTICS 236 | The following diagnostics may be issued 237 | on stderr: 238 | 239 | 240 | Bad configuration file. Exiting. 241 | 242 | The configuration file seems to contain a broken configuration 243 | line. Use the option, to get more info. 244 | 245 | 246 | 247 | 248 | &dhpackage; provides some return codes, that can 249 | be used in scripts: 250 | 251 | Code 252 | Diagnostic 253 | 254 | 0 255 | Program exited successfully. 256 | 257 | 258 | 1 259 | The configuration file seems to be broken. 260 | 261 | 262 | 263 | 264 | 265 | BUGS 266 | The program is currently limited to only work 267 | with the foobar library. 268 | The upstreams BTS can be found 269 | at . 270 | 271 | 272 | SEE ALSO 273 | 274 | 275 | bar 276 | 1 277 | , 278 | baz 279 | 1 280 | , 281 | foo.conf 282 | 5 283 | 284 | The programs are documented fully by The Rise and 285 | Fall of a Fooish Bar available via the 286 | info 287 | 1 288 | system. 289 | 290 |
291 | 292 | -------------------------------------------------------------------------------- /debian/menu.ex: -------------------------------------------------------------------------------- 1 | ?package(repeater-start):needs="X11|text|vc|wm" section="Applications/see-menu-manual"\ 2 | title="repeater-start" command="/usr/bin/repeater-start" 3 | -------------------------------------------------------------------------------- /debian/postinst.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for repeater-start 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `configure' 10 | # * `abort-upgrade' 11 | # * `abort-remove' `in-favour' 12 | # 13 | # * `abort-remove' 14 | # * `abort-deconfigure' `in-favour' 15 | # `removing' 16 | # 17 | # for details, see https://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | configure) 23 | ;; 24 | 25 | abort-upgrade|abort-remove|abort-deconfigure) 26 | ;; 27 | 28 | *) 29 | echo "postinst called with unknown argument \`$1'" >&2 30 | exit 1 31 | ;; 32 | esac 33 | 34 | # dh_installdeb will replace this with shell code automatically 35 | # generated by other debhelper scripts. 36 | 37 | #DEBHELPER# 38 | 39 | exit 0 40 | -------------------------------------------------------------------------------- /debian/postrm.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postrm script for repeater-start 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `remove' 10 | # * `purge' 11 | # * `upgrade' 12 | # * `failed-upgrade' 13 | # * `abort-install' 14 | # * `abort-install' 15 | # * `abort-upgrade' 16 | # * `disappear' 17 | # 18 | # for details, see https://www.debian.org/doc/debian-policy/ or 19 | # the debian-policy package 20 | 21 | 22 | case "$1" in 23 | purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) 24 | ;; 25 | 26 | *) 27 | echo "postrm called with unknown argument \`$1'" >&2 28 | exit 1 29 | ;; 30 | esac 31 | 32 | # dh_installdeb will replace this with shell code automatically 33 | # generated by other debhelper scripts. 34 | 35 | #DEBHELPER# 36 | 37 | exit 0 38 | -------------------------------------------------------------------------------- /debian/preinst.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # preinst script for repeater-start 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `install' 10 | # * `install' 11 | # * `upgrade' 12 | # * `abort-upgrade' 13 | # for details, see https://www.debian.org/doc/debian-policy/ or 14 | # the debian-policy package 15 | 16 | 17 | case "$1" in 18 | install|upgrade) 19 | ;; 20 | 21 | abort-upgrade) 22 | ;; 23 | 24 | *) 25 | echo "preinst called with unknown argument \`$1'" >&2 26 | exit 1 27 | ;; 28 | esac 29 | 30 | # dh_installdeb will replace this with shell code automatically 31 | # generated by other debhelper scripts. 32 | 33 | #DEBHELPER# 34 | 35 | exit 0 36 | -------------------------------------------------------------------------------- /debian/prerm.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # prerm script for repeater-start 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `remove' 10 | # * `upgrade' 11 | # * `failed-upgrade' 12 | # * `remove' `in-favour' 13 | # * `deconfigure' `in-favour' 14 | # `removing' 15 | # 16 | # for details, see https://www.debian.org/doc/debian-policy/ or 17 | # the debian-policy package 18 | 19 | 20 | case "$1" in 21 | remove|upgrade|deconfigure) 22 | ;; 23 | 24 | failed-upgrade) 25 | ;; 26 | 27 | *) 28 | echo "prerm called with unknown argument \`$1'" >&2 29 | exit 1 30 | ;; 31 | esac 32 | 33 | # dh_installdeb will replace this with shell code automatically 34 | # generated by other debhelper scripts. 35 | 36 | #DEBHELPER# 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /debian/python3-repeater-start.substvars: -------------------------------------------------------------------------------- 1 | misc:Depends= 2 | misc:Pre-Depends= 3 | -------------------------------------------------------------------------------- /debian/repeater-start-docs.docs: -------------------------------------------------------------------------------- 1 | README.source 2 | README.Debian 3 | -------------------------------------------------------------------------------- /debian/repeater-start.cron.d.ex: -------------------------------------------------------------------------------- 1 | # 2 | # Regular cron jobs for the repeater-start package 3 | # 4 | 0 4 * * * root [ -x /usr/bin/repeater-start_maintenance ] && /usr/bin/repeater-start_maintenance 5 | -------------------------------------------------------------------------------- /debian/repeater-start.debhelper.log: -------------------------------------------------------------------------------- 1 | dh_update_autotools_config 2 | dh_prep 3 | dh_auto_install 4 | dh_install 5 | dh_installdocs 6 | dh_installchangelogs 7 | dh_icons 8 | dh_perl 9 | dh_link 10 | dh_strip_nondeterminism 11 | dh_compress 12 | dh_fixperms 13 | dh_missing 14 | dh_installdeb 15 | dh_gencontrol 16 | dh_md5sums 17 | dh_builddeb 18 | dh_builddeb 19 | -------------------------------------------------------------------------------- /debian/repeater-start.default.ex: -------------------------------------------------------------------------------- 1 | # Defaults for repeater-start initscript 2 | # sourced by /etc/init.d/repeater-start 3 | # installed at /etc/default/repeater-start by the maintainer scripts 4 | 5 | # 6 | # This is a POSIX shell fragment 7 | # 8 | 9 | # Additional options that are passed to the Daemon. 10 | DAEMON_OPTS="" 11 | -------------------------------------------------------------------------------- /debian/repeater-start.doc-base.EX: -------------------------------------------------------------------------------- 1 | Document: repeater-start 2 | Title: Debian repeater-start Manual 3 | Author: 4 | Abstract: This manual describes what repeater-start is 5 | and how it can be used to 6 | manage online manuals on Debian systems. 7 | Section: unknown 8 | 9 | Format: debiandoc-sgml 10 | Files: /usr/share/doc/repeater-start/repeater-start.sgml.gz 11 | 12 | Format: postscript 13 | Files: /usr/share/doc/repeater-start/repeater-start.ps.gz 14 | 15 | Format: text 16 | Files: /usr/share/doc/repeater-start/repeater-start.text.gz 17 | 18 | Format: HTML 19 | Index: /usr/share/doc/repeater-start/html/index.html 20 | Files: /usr/share/doc/repeater-start/html/*.html 21 | -------------------------------------------------------------------------------- /debian/repeater-start.substvars: -------------------------------------------------------------------------------- 1 | misc:Depends= 2 | misc:Pre-Depends= 3 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | #export DH_VERBOSE = 1 5 | 6 | #export PYBUILD_NAME=repeater-start 7 | 8 | %: 9 | dh $@ 10 | # --with python3 11 | 12 | 13 | # If you need to rebuild the Sphinx documentation 14 | # Add spinxdoc to the dh --with line 15 | #override_dh_auto_build: 16 | # dh_auto_build 17 | # PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bhtml docs/ build/html # HTML generator 18 | # PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bman docs/ build/man # Manpage generator 19 | 20 | override_dh_builddeb: 21 | dh_builddeb -- -Zgzip 22 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/watch.ex: -------------------------------------------------------------------------------- 1 | # Example watch control file for uscan 2 | # Rename this file to "watch" and then you can run the "uscan" command 3 | # to check for upstream updates and more. 4 | # See uscan(1) for format 5 | 6 | # Compulsory line, this is a version 4 file 7 | version=4 8 | 9 | # PGP signature mangle, so foo.tar.gz has foo.tar.gz.sig 10 | #opts="pgpsigurlmangle=s%$%.sig%" 11 | 12 | # HTTP site (basic) 13 | #http://example.com/downloads.html \ 14 | # files/repeater-start-([\d\.]+)\.tar\.gz debian uupdate 15 | 16 | # Uncommment to examine a FTP server 17 | #ftp://ftp.example.com/pub/repeater-start-(.*)\.tar\.gz debian uupdate 18 | 19 | # SourceForge hosted projects 20 | # http://sf.net/repeater-start/ repeater-start-(.*)\.tar\.gz debian uupdate 21 | 22 | # GitHub hosted projects 23 | #opts="filenamemangle="s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%-$1.tar.gz%" \ 24 | # https://github.com//repeater-start/tags \ 25 | # (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate 26 | 27 | # PyPI 28 | # https://pypi.python.org/packages/source//repeater-start/ \ 29 | # repeater-start-(.+)\.tar\.gz debian uupdate 30 | 31 | # Direct Git 32 | # opts="mode=git" http://git.example.com/repeater-start.git \ 33 | # refs/tags/v([\d\.]+) debian uupdate 34 | 35 | 36 | 37 | 38 | # Uncomment to find new files on GooglePages 39 | # http://example.googlepages.com/foo.html repeater-start-(.*)\.tar\.gz 40 | -------------------------------------------------------------------------------- /resources/repeaterSTART: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #Starts up repeater-start 3 | cd /usr/share/repeater-START/ 4 | ./repeaterstart.py $@ 5 | -------------------------------------------------------------------------------- /resources/repeaterstart.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Icon=repeaterSTART 4 | Encoding=UTF-8 5 | Name=Repeater-START 6 | Comment=Repeater-START, Showing The Amateur Repeaters Tool. Ham radio tool. 7 | Exec=repeaterSTART 8 | Terminal=false 9 | Categories=Education;Science;GTK;GNOME;Utility;HamRadio; 10 | Keywords=Maps; 11 | DBusActivatable=true 12 | X-Geoclue-Reason=Allows you to look up nearby amateur radio repeaters. 13 | X-Purism-FormFactor=Workstation;Mobile; 14 | StartupNotify=true 15 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /src/CsvRepeaterListing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import csv 3 | import os 4 | from gi.repository import GdkPixbuf 5 | 6 | from Repeater import Repeater 7 | from RepeaterStartCommon import userFile 8 | import urllib 9 | 10 | class CsvRepeaterListing(): 11 | """ Hold repeater listing and list of repeaters """ 12 | def __init__(self, filename): 13 | start = True 14 | data = False 15 | with open(filename) as csvfile: 16 | for row in csv.reader(csvfile, delimiter=',', quotechar='"'): 17 | if start: 18 | if row[0].lower() == 'name': 19 | self.name = row[1] 20 | if row[0].lower().strip() == 'icon': 21 | self.icon = row[1] 22 | #Which mode - top name, columns or data parsing? 23 | nonempty = [x for x in row if x] 24 | if len(nonempty) == 0: 25 | start = False #separator 26 | elif not start and not data: 27 | #Reading data, mode now: 28 | self.cols = row 29 | data = True 30 | self.repeaters = [] 31 | elif data: 32 | r = Repeater() 33 | setattr(r, 'node', '') 34 | setattr(r, 'status', True) # up by default 35 | setattr(r, 'irlp', 0) #not 36 | for i in range(len(self.cols)): 37 | #Set each attribute 38 | setattr(r, self.cols[i], row[i]) 39 | if self.cols[i].find('freq') == 0: 40 | setattr(r, 'freq', row[i]) 41 | if self.cols[i].find('lat') == 0: 42 | setattr(r, 'lat', float(row[i])) 43 | if self.cols[i].find('lon') == 0: 44 | setattr(r, 'lon', float(row[i])) 45 | if self.cols[i] == 'encode': 46 | setattr(r, 'pl', row[i]) 47 | if self.cols[i].find('active') == 0: 48 | active = False 49 | if row[i].lower().find('active')==0: 50 | active = True 51 | if row[i].lower().find('true')==0: 52 | active = True 53 | if row[i].lower().find('up')==0: 54 | active = True 55 | setattr(r, 'status', active) 56 | 57 | self.repeaters.append(r) 58 | if not os.path.exists(self.iconFile()): 59 | #try: 60 | urllib.request.urlretrieve(self.icon, self.iconFile()) 61 | #except: 62 | # print('FAILED Download '+self.icon) 63 | def iconFile(self): 64 | """ Full filepath of expected icon file """ 65 | return userFile('rpt-'+self.name) 66 | 67 | def getIcon(self): 68 | """ The downloaded icon to render for this particular tower type """ 69 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.iconFile(),width=21,height=21,preserve_aspect_ratio=True) 70 | return pixbuf 71 | 72 | def __str__(self): 73 | return ("Name: %s\nIcon: %s\nRepeaters: %s" % (self.name, self.icon, len(self.repeaters)) ) 74 | 75 | -------------------------------------------------------------------------------- /src/HearHamRepeater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from Repeater import Repeater 3 | 4 | class HearHamRepeater(Repeater): 5 | def __init__(self, jsonObject): 6 | """ Unpack the line to the properties of this class: """ 7 | self.id = jsonObject['id'] 8 | self.callsign = jsonObject['callsign'] 9 | self.description = jsonObject['description'] 10 | self.owner = jsonObject['callsign'] 11 | self.status = jsonObject['operational'] 12 | self.url = 'https://hearham.com/repeaters/'+str(jsonObject['id']) 13 | self.mode = jsonObject['mode'] 14 | self.node = jsonObject['internet_node'] 15 | self.group = jsonObject['group'] 16 | self.lat = float(jsonObject['latitude']) 17 | self.lon = float(jsonObject['longitude']) 18 | self.city = jsonObject['city'] 19 | self.pl = jsonObject['encode'] 20 | self.freq = jsonObject['frequency']/1000000 21 | self.offset = jsonObject['offset']/1000000 22 | -------------------------------------------------------------------------------- /src/HelpDialog.py: -------------------------------------------------------------------------------- 1 | import gi 2 | gi.require_version("Gtk", "3.0") 3 | from gi.repository import Gtk 4 | from gi.repository import WebKit2 5 | from RepeaterStartCommon import userFile 6 | import os 7 | gi.require_version('WebKit2', '4.0') 8 | 9 | class HelpDialog(Gtk.Dialog): 10 | def __init__(self, parent, repeater): 11 | super().__init__(title="Repeater Setup Help", transient_for=parent, flags=0) 12 | self.set_default_size(600, 900) 13 | box = self.get_content_area() 14 | self.helpview = WebKit2.WebView() 15 | 16 | sw = Gtk.ScrolledWindow() 17 | sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 18 | 19 | sw.add(self.helpview) 20 | box.pack_start(sw, True,True, 0) 21 | 22 | pro = userFile('.hidden') 23 | loadfile = 'file://'+pro+"/index.html" 24 | if not os.path.exists(pro): 25 | os.mkdir(pro) 26 | settings = self.helpview.get_settings() 27 | settings.set_enable_javascript(True) 28 | settings.set_allow_file_access_from_file_urls(True) 29 | #TODO unzip. 30 | with open(os.path.join(pro,'base.data.js'), 'w') as outfile: 31 | outfile.write("CALL=\""+repeater.callsign+"\"; "+ 32 | "FREQ=\""+str(repeater.freq)+"\";"+ 33 | "OFFSET=\""+str(repeater.offset)+"\";"+ 34 | "MODE=\""+str(repeater.mode)+"\";"+ 35 | "PL=\""+str(repeater.pl)+"\";"+ 36 | "URL=\""+str(repeater.url)+"\";"); 37 | self.helpview.load_uri(loadfile) 38 | #box.show() 39 | self.show_all() 40 | -------------------------------------------------------------------------------- /src/IRLPNode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from Repeater import Repeater 3 | # Deprecated. See HearHamRepeater.py for one in use. 4 | class IRLPNode(Repeater): 5 | def __init__(self, line): 6 | """ Unpack the line to the properties of this class: """ 7 | [self.node, self.callsign, self.city, self.state, self.country, self.status, self.record, self.install, self.lat, self.lon, self.lastupdate, self.freq, self.offset, self.pl, self.owner, self.url, self.lastchange, self.avrsstatus] = line.split('\t') 8 | self.lat = float(self.lat) 9 | self.lon = float(self.lon) 10 | self.mode = 'FM' 11 | #try: 12 | self.offset = float(self.offset)/1000 13 | #except ValueError as e: 14 | # print("EEK"+e) 15 | if float(self.freq) == 0: 16 | self.description = "Owned by %s" % (self.owner,) 17 | else: 18 | self.description = "Owned by %s in %s" % (self.owner, self.city) 19 | 20 | def __str__(self): 21 | return 'IRLP node %s in %s, status=%s, freq=%s' % (self.node,self.city,self.status,self.freq) 22 | -------------------------------------------------------------------------------- /src/MaidenheadLocator.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import re 3 | from math import floor 4 | #Roughly based on https://unclassified.software/files/source/MaidenheadLocator.cs 5 | 6 | def locatorToLatLng(locator): 7 | locator = locator.strip().upper() 8 | values = False 9 | if re.match(r"^[A-R]{2}[0-9]{2}[A-X]{2}$", locator): 10 | values = { 11 | 'lng': (ord(locator[0]) - ord('A')) * 20 + (ord(locator[2]) - ord('0'))*2 + (ord(locator[4]) - ord('A') +.5) / 12 - 180, 12 | 'lat': (ord(locator[1]) - ord('A')) * 10 + (ord(locator[3]) - ord('0')) + (ord(locator[5]) - ord('A')+.5) / 24 - 90 13 | } 14 | elif re.match(r"^[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}$", locator): 15 | values = { 16 | 'lng': (ord(locator[0]) - ord('A')) * 20 + (ord(locator[2]) - ord('0'))*2 + (ord(locator[4]) - ord('A') ) / 12 + (ord(locator[6]) - ord('0') + .5) / 120 - 180, 17 | 'lat': (ord(locator[1]) - ord('A')) * 10 + (ord(locator[3]) - ord('0')) + (ord(locator[5]) - ord('A') )/24 + (ord(locator[7]) - ord('0') +.5 ) / 240 - 90 18 | } 19 | elif re.match(r"^[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}[A-X]{2}$", locator): 20 | values = { 21 | 'lng': (ord(locator[0]) - ord('A')) * 20 + (ord(locator[2]) - ord('0'))*2 + (ord(locator[4]) - ord('A') ) / 12 + (ord(locator[6]) - ord('0') ) / 120 + (ord(locator[8]) - ord('A') +.5) / 120 / 24 - 180, 22 | 'lat': (ord(locator[1]) - ord('A')) * 10 + (ord(locator[3]) - ord('0')) + (ord(locator[5]) - ord('A') )/24 + (ord(locator[7]) - ord('0') ) / 240 + (ord(locator[9]) - ord('A') +.5) / 240 / 224 - 90 23 | } 24 | return values 25 | 26 | def latLongToLocator(lat,lon): 27 | locator = '' 28 | lat += 90 29 | lon += 180 30 | locator += chr(ord('A') + floor(lon/20)) 31 | locator += chr(ord('A') + floor(lat/10)) 32 | lon = lon % 20 33 | if lon < 0: 34 | lon += 20 35 | lat = lat % 10 36 | if lat < 0: 37 | lat += 10 38 | 39 | locator += chr(ord('0') + floor( lon/2)) 40 | locator += chr(ord('0') + floor( lat)) 41 | lon = lon % 2 42 | if lon < 0: 43 | lon += 2 44 | lat = lat % 1 45 | if lat < 0: 46 | lat += 1 47 | locator += chr(ord('A')+floor(lon*12)) 48 | locator += chr(ord('A')+floor(lat * 24)) 49 | lon = lon % (1/12) 50 | if lon < 0: 51 | lon += 1/12 52 | lat = lat % (1/24) 53 | if lat < 0: 54 | lat += 1/24 55 | return locator 56 | -------------------------------------------------------------------------------- /src/NetworkStatus.py: -------------------------------------------------------------------------------- 1 | 2 | import gi 3 | 4 | gi.require_version("NM", "1.0") 5 | from gi.repository import NM 6 | 7 | def isMobileData(): 8 | """ Return True if not on Wifi and potentially on metered 4g/phone connection """ 9 | isMobile = True 10 | client = NM.Client.new(None) 11 | devices = client.get_all_devices() 12 | for device in devices: 13 | if device.is_real() and device.get_state() == NM.DeviceState.ACTIVATED: 14 | if type(device) == NM.DeviceEthernet: 15 | isMobile = False 16 | if type(device) == NM.DeviceWifi: 17 | isMobile = False 18 | return isMobile 19 | 20 | if __name__ == '__main__': 21 | print('Mobile data status: %s' % (isMobileData(), )) 22 | -------------------------------------------------------------------------------- /src/Repeater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from math import pi, sin, cos, sqrt, atan2, radians 3 | 4 | class Repeater: 5 | def distance(self, lat,lon): 6 | """ Distance in km """ 7 | earthR = 6373 8 | dlat = radians(lat-self.lat) 9 | dlon = radians(lon-self.lon) 10 | a = sin(dlat/2)**2 + cos(radians(self.lat)) * cos(radians(lat)) * sin(dlon/2)**2 11 | c = 2*atan2(sqrt(a), sqrt(1-a)) 12 | return earthR*c 13 | 14 | def isDown(self): 15 | return not self.status or 'DOWN' == self.status or 'OFFLINE' == self.status 16 | 17 | def __str__(self): 18 | return "Repeater object for %s at %s frequency." % (self.callsign, self.freq) 19 | -------------------------------------------------------------------------------- /src/RepeaterStartCommon.py: -------------------------------------------------------------------------------- 1 | #Common functions 2 | import os 3 | from gi.repository import GLib 4 | 5 | def userFile(name): 6 | """ Returns available filename in user data dir for this app. """ 7 | mydir = os.path.join(GLib.get_user_data_dir(),'repeater-START') 8 | if not os.path.exists(mydir): 9 | os.mkdir(mydir) 10 | return os.path.join(mydir,name) 11 | -------------------------------------------------------------------------------- /src/SettingsDialog.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | dialog 8 | 9 | 10 | 11 | 12 | 13 | False 14 | 5 15 | 5 16 | 5 17 | 5 18 | vertical 19 | 2 20 | 21 | 22 | False 23 | end 24 | 25 | 26 | Close 27 | True 28 | True 29 | True 30 | 31 | 32 | 33 | 34 | True 35 | True 36 | 0 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | False 45 | False 46 | 0 47 | 48 | 49 | 50 | 51 | True 52 | False 53 | 0 54 | none 55 | 56 | 57 | True 58 | False 59 | 5 60 | 12 61 | 62 | 63 | True 64 | False 65 | vertical 66 | 67 | 68 | Show All 69 | True 70 | True 71 | False 72 | True 73 | True 74 | 75 | 76 | 77 | False 78 | True 79 | 0 80 | 81 | 82 | 83 | 84 | VHF 85 | True 86 | True 87 | False 88 | True 89 | True 90 | freqFilterRadio 91 | 92 | 93 | 94 | False 95 | True 96 | 1 97 | 98 | 99 | 100 | 101 | UHF 102 | True 103 | True 104 | False 105 | True 106 | True 107 | freqFilterRadio 108 | 109 | 110 | 111 | False 112 | True 113 | 2 114 | 115 | 116 | 117 | 118 | Custom: 119 | True 120 | True 121 | False 122 | True 123 | True 124 | freqFilterRadio 125 | 126 | 127 | False 128 | True 129 | 3 130 | 131 | 132 | 133 | 134 | True 135 | False 136 | 137 | 138 | True 139 | False 140 | Show only 141 | 142 | 143 | False 144 | False 145 | 0 146 | 147 | 148 | 149 | 150 | True 151 | True 152 | 5 153 | number 154 | 155 | 156 | False 157 | False 158 | 1 159 | 160 | 161 | 162 | 163 | True 164 | False 165 | Mhz - 166 | 167 | 168 | False 169 | False 170 | 2 171 | 172 | 173 | 174 | 175 | True 176 | True 177 | 5 178 | number 179 | 180 | 181 | False 182 | False 183 | 3 184 | 185 | 186 | 187 | 188 | True 189 | False 190 | Mhz 191 | 192 | 193 | False 194 | True 195 | 4 196 | 197 | 198 | 199 | 200 | False 201 | True 202 | 4 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | True 212 | False 213 | Frequency Filter 214 | 215 | 216 | 217 | 218 | False 219 | True 220 | 6 221 | 0 222 | 223 | 224 | 225 | 226 | True 227 | False 228 | 0 229 | none 230 | 231 | 232 | True 233 | False 234 | 5 235 | 12 236 | 237 | 238 | True 239 | False 240 | vertical 241 | 242 | 243 | Miles 244 | True 245 | True 246 | False 247 | True 248 | True 249 | 250 | 251 | False 252 | True 253 | 0 254 | 255 | 256 | 257 | 258 | Kilometers 259 | True 260 | True 261 | False 262 | True 263 | True 264 | distUnitsRadio 265 | 266 | 267 | False 268 | True 269 | 1 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | True 279 | False 280 | Units 281 | 282 | 283 | 284 | 285 | False 286 | True 287 | 6 288 | 1 289 | 290 | 291 | 292 | 293 | True 294 | False 295 | 0 296 | none 297 | 298 | 299 | True 300 | False 301 | 12 302 | 303 | 304 | True 305 | False 306 | vertical 307 | 308 | 309 | Allow download away from wired/wifi 310 | True 311 | True 312 | False 313 | True 314 | 315 | 316 | False 317 | True 318 | 6 319 | 0 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | True 329 | False 330 | Background Downloads 331 | 332 | 333 | 334 | 335 | False 336 | True 337 | 6 338 | 3 339 | 340 | 341 | 342 | 343 | True 344 | False 345 | 0 346 | none 347 | 348 | 349 | True 350 | False 351 | 12 352 | 353 | 354 | True 355 | False 356 | vertical 357 | 358 | 359 | 70 360 | True 361 | True 362 | 5 363 | 5 364 | 5 365 | in 366 | 367 | 368 | True 369 | False 370 | 371 | 372 | repeaterRepos 373 | True 374 | False 375 | 376 | 377 | 378 | 379 | 380 | 381 | True 382 | True 383 | 0 384 | 385 | 386 | 387 | 388 | True 389 | False 390 | 391 | 392 | gtk-add 393 | True 394 | True 395 | True 396 | True 397 | True 398 | 399 | 400 | 401 | False 402 | True 403 | 0 404 | 405 | 406 | 407 | 408 | gtk-remove 409 | True 410 | True 411 | True 412 | True 413 | True 414 | 415 | 416 | 417 | False 418 | True 419 | 1 420 | 421 | 422 | 423 | 424 | gtk-properties 425 | True 426 | True 427 | True 428 | True 429 | True 430 | 431 | 432 | 433 | False 434 | True 435 | 2 436 | 437 | 438 | 439 | 440 | False 441 | True 442 | 1 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | True 452 | False 453 | Displayed repeaters 454 | 455 | 456 | 457 | 458 | False 459 | True 460 | 3 461 | 462 | 463 | 464 | 465 | 466 | 467 | -------------------------------------------------------------------------------- /src/SettingsDialog.py: -------------------------------------------------------------------------------- 1 | import gi 2 | import os 3 | import urllib 4 | 5 | gi.require_version("Gtk", "3.0") 6 | from gi.repository import Gtk 7 | from gi.repository import Gdk 8 | import configparser 9 | from CsvRepeaterListing import CsvRepeaterListing 10 | from RepeaterStartCommon import userFile 11 | from urllib.error import HTTPError 12 | 13 | class SettingsDialog: 14 | def __init__(self, parentWin): 15 | self.parentWin = parentWin 16 | self.config = configparser.ConfigParser() 17 | if os.path.exists(userFile('settings.ini')): 18 | self.config.read(userFile('settings.ini')) 19 | else: #defaults: 20 | self.config['ViewOptions'] = { 21 | 'unitsLength': 'mi', 22 | 'filterMin' : '', 23 | 'filterMax' : '' 24 | } 25 | if not 'DownloadOptions' in self.config: 26 | self.config['DownloadOptions'] = { 27 | 'mobile' : False 28 | } 29 | #Just ham radio constants: 30 | self.UHFMIN = '420' 31 | self.UHFMAX = '450' 32 | self.VHFMIN = '144' 33 | self.VHFMAX = '148' 34 | 35 | def writeSettings(self): 36 | # Save the config to the file: 37 | if hasattr(self, 'builder'): 38 | units = 'mi' 39 | if self.builder.get_object('distUnitsRadioKM').get_active(): 40 | units = 'km' 41 | self.config['ViewOptions'] = { 42 | 'unitsLength': units, 43 | 'filterMin' : self.builder.get_object('lblMinFreq').get_text(), 44 | 'filterMax' : self.builder.get_object('lblMaxFreq').get_text() 45 | } 46 | self.config['DownloadOptions'] = { 47 | 'mobile' : self.builder.get_object('allowMobile').get_active() 48 | } 49 | with open(userFile('settings.ini'),'w') as outfile: 50 | self.config.write(outfile) 51 | 52 | def getAllowMobile(self): 53 | #note the STRING value of config values: 54 | return self.config['DownloadOptions']['mobile'].lower() != 'false' 55 | 56 | def getMinFilter(self): 57 | value = self.config['ViewOptions']['filterMin'] 58 | try: 59 | return float(value) 60 | except ValueError: 61 | return -1 62 | 63 | def getMaxFilter(self): 64 | value = self.config['ViewOptions']['filterMax'] 65 | try: 66 | return float(value) 67 | except ValueError: 68 | return 9e999 69 | 70 | def getUnit(self): 71 | return self.config['ViewOptions']['unitsLength'] 72 | 73 | def getShown(self): 74 | if 'Repeaters' in self.config: 75 | print(self.config['Repeaters']) 76 | try: 77 | for name in self.config['Repeaters']: 78 | row = Gtk.ListBoxRow() 79 | row.add(Gtk.Label(name)) 80 | row.url = self.config['Repeaters'][name] 81 | self.repolist.add(row) 82 | self.repolist.show_all() 83 | except AttributeError: 84 | print('Repeater row not found?') 85 | else: 86 | #Default case 87 | self.config['Repeaters'] = { 88 | 'Hearham Amateur Radio Repeaters': "https://hearham.com/api/repeaters/v1" 89 | } 90 | return self.config['Repeaters'] 91 | 92 | def show(self): 93 | #Create GtkDialog 94 | self.builder = Gtk.Builder() 95 | self.builder.add_from_file('SettingsDialog.glade') 96 | self.builder.connect_signals(self) 97 | self.dialog = self.builder.get_object('SettingsDialog') 98 | #self.dialog.set_parent(parentWin) 99 | self.dialog.set_transient_for(self.parentWin) #Over the main window. 100 | self.dialog.set_modal(True) 101 | #self.dialog.set_redraw_on_allocate(True) 102 | self.dialog.set_title('Settings') 103 | 104 | self.repolist = self.builder.get_object('repeaterRepos') 105 | self.getShown() 106 | 107 | self.dialog.show_all() 108 | #Load the config to the form 109 | options = self.config['ViewOptions'] 110 | if options['unitsLength'] == 'km': 111 | self.builder.get_object('distUnitsRadioKM').set_active(True) 112 | if self.getMinFilter() > 0: 113 | self.builder.get_object('filterFreqCustom').set_active(True) 114 | # ^ if nofilter is selected, the default, then they are cleared with NoFilterSet() 115 | self.builder.get_object('lblMinFreq').set_text(str(options['filterMin'])) 116 | if self.getMaxFilter() < 9e999: 117 | self.builder.get_object('filterFreqCustom').set_active(True) 118 | self.builder.get_object('lblMaxFreq').set_text(str(options['filterMax'])) 119 | if self.getMinFilter() == float(self.VHFMIN) and self.getMaxFilter() == float(self.VHFMAX): 120 | self.builder.get_object('freqFilterVHF').set_active(True) 121 | if self.getMinFilter() == float(self.UHFMIN) and self.getMaxFilter() == float(self.UHFMAX): 122 | self.builder.get_object('freqFilterUHF').set_active(True) 123 | self.builder.get_object('allowMobile').set_active(self.getAllowMobile()) 124 | 125 | # User actions on the form: 126 | def NoFilterSet(self, *args): 127 | self.builder.get_object('lblMinFreq').set_text('') 128 | self.builder.get_object('lblMaxFreq').set_text('') 129 | 130 | def VHFSet(self, *args): 131 | self.builder.get_object('lblMinFreq').set_text(self.VHFMIN) 132 | self.builder.get_object('lblMaxFreq').set_text(self.VHFMAX) 133 | 134 | def UHFSet(self, *args): 135 | self.builder.get_object('lblMinFreq').set_text(self.UHFMIN) 136 | self.builder.get_object('lblMaxFreq').set_text(self.UHFMAX) 137 | 138 | def doDialog(self, title, inputarea=''): 139 | dialogWindow = Gtk.MessageDialog(self.dialog, 140 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 141 | Gtk.MessageType.QUESTION, 142 | Gtk.ButtonsType.OK_CANCEL, 143 | title) 144 | 145 | dialogWindow.set_title(title) 146 | dialogBox = dialogWindow.get_content_area() 147 | userEntry = Gtk.Entry() 148 | userEntry.set_size_request(140,12); 149 | userEntry.set_text(inputarea) 150 | dialogBox.pack_end(userEntry, False, False, 0) 151 | dialogWindow.show_all() 152 | response = dialogWindow.run() 153 | text = userEntry.get_text() 154 | dialogWindow.destroy() 155 | return text 156 | 157 | def doMsg(self, title, msg): 158 | dialogWindow = Gtk.MessageDialog(self.dialog, 159 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 160 | Gtk.MessageType.WARNING, 161 | Gtk.ButtonsType.OK, 162 | title) 163 | dialogWindow.set_title(title) 164 | dialogBox = dialogWindow.get_content_area() 165 | userEntry = Gtk.Label() 166 | userEntry.set_size_request(140,12); 167 | userEntry.set_text(msg) 168 | dialogBox.pack_end(userEntry, False, False, 0) 169 | dialogWindow.show_all() 170 | print('shown') 171 | response = dialogWindow.run() 172 | dialogWindow.destroy() 173 | return 174 | 175 | def addRpt(self, *args): 176 | url = self.doDialog('Add repeaters by URL of listing:') 177 | tmpfile = userFile('repeater-temp-file.csv') 178 | if url and url.find('.csv') > -1 : 179 | try: 180 | #verify and get the name: 181 | #TODO should be in a background thread somehow 182 | urllib.request.urlretrieve(url, tmpfile) 183 | csv = CsvRepeaterListing(tmpfile) 184 | if csv.name: 185 | row = Gtk.ListBoxRow() 186 | row.add(Gtk.Label(csv.name)) 187 | row.url = url 188 | self.repolist.add(row) 189 | self.repolist.show_all() 190 | self.config['Repeaters'][csv.name] = url 191 | else: 192 | self.doMsg('Error:','Invalid entry? Must be a .csv file available on a server.') 193 | except HTTPError: 194 | self.doMsg('Error:','HTTP error- Must be a .csv file available on a server.') 195 | os.remove(tmpfile) 196 | elif url and url.find("hearham.com/api/repeaters/v1") > -1: 197 | #Re-add default: 198 | row = Gtk.ListBoxRow() 199 | row.add(Gtk.Label('Hearham Amateur Radio Repeaters')) 200 | row.url = "https://hearham.com/api/repeaters/v1" 201 | self.repolist.add(row) 202 | self.repolist.show_all() 203 | self.config['Repeaters']['Hearham Amateur Radio Repeaters'] = "https://hearham.com/api/repeaters/v1" 204 | elif len(url): 205 | self.doMsg('Error:','You must enter a full url of a .csv file.') 206 | return 207 | 208 | def rmRpt(self, *args): 209 | row = self.repolist.get_selected_row() 210 | for item in self.config['Repeaters']: 211 | if self.config['Repeaters'][item] == row.url: 212 | del self.config['Repeaters'][item] 213 | #TODO clean up files 214 | self.repolist.remove( row ) 215 | 216 | def propertyRpt(self, *args): 217 | url = self.doDialog('Re/set current selected url:',self.repolist.get_selected_row().url) 218 | tmpfile = userFile('repeater-temp-file.csv') 219 | if url and url.find('.csv') > -1 : 220 | #verify and get the name: 221 | try: 222 | #TODO should be in a background thread somehow 223 | urllib.request.urlretrieve(url, tmpfile) 224 | csv = CsvRepeaterListing(tmpfile) 225 | if csv.name: 226 | self.config['Repeaters'][csv.name] = url 227 | self.repolist.get_selected_row().url = url 228 | self.repolist.show_all() 229 | else: 230 | self.doMsg('Error:','Invalid entry? Must be a .csv file available on a server.') 231 | os.remove(tmpfile) 232 | except HTTPError: 233 | self.doMsg('Error:','HTTP error- Must be a .csv file available on a server.') 234 | 235 | 236 | def onDestroy(self, *args): 237 | pass 238 | 239 | # Note this is also set as the close button's clicked signal. 240 | def onClosed(self, *args): 241 | self.writeSettings() 242 | self.parentWin.displayNodes() 243 | self.parentWin.refreshListing() 244 | self.dialog.destroy() 245 | -------------------------------------------------------------------------------- /src/lib/openlocationcode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # ============================================================================== 15 | # 16 | # 17 | # Convert locations to and from short codes. 18 | # 19 | # Open Location Codes are short, 10-11 character codes that can be used instead 20 | # of street addresses. The codes can be generated and decoded offline, and use 21 | # a reduced character set that minimises the chance of codes including words. 22 | # 23 | # Codes are able to be shortened relative to a nearby location. This means that 24 | # in many cases, only four to seven characters of the code are needed. 25 | # To recover the original code, the same location is not required, as long as 26 | # a nearby location is provided. 27 | # 28 | # Codes represent rectangular areas rather than points, and the longer the 29 | # code, the smaller the area. A 10 character code represents a 13.5x13.5 30 | # meter area (at the equator. An 11 character code represents approximately 31 | # a 2.8x3.5 meter area. 32 | # 33 | # Two encoding algorithms are used. The first 10 characters are pairs of 34 | # characters, one for latitude and one for longitude, using base 20. Each pair 35 | # reduces the area of the code by a factor of 400. Only even code lengths are 36 | # sensible, since an odd-numbered length would have sides in a ratio of 20:1. 37 | # 38 | # At position 11, the algorithm changes so that each character selects one 39 | # position from a 4x5 grid. This allows single-character refinements. 40 | # 41 | # Examples: 42 | # 43 | # Encode a location, default accuracy: 44 | # encode(47.365590, 8.524997) 45 | # 46 | # Encode a location using one stage of additional refinement: 47 | # encode(47.365590, 8.524997, 11) 48 | # 49 | # Decode a full code: 50 | # coord = decode(code) 51 | # msg = "Center is {lat}, {lon}".format(lat=coord.latitudeCenter, lon=coord.longitudeCenter) 52 | # 53 | # Attempt to trim the first characters from a code: 54 | # shorten('8FVC9G8F+6X', 47.5, 8.5) 55 | # 56 | # Recover the full code from a short code: 57 | # recoverNearest('9G8F+6X', 47.4, 8.6) 58 | # recoverNearest('8F+6X', 47.4, 8.6) 59 | 60 | import re 61 | import math 62 | 63 | # A separator used to break the code into two parts to aid memorability. 64 | SEPARATOR_ = '+' 65 | 66 | # The number of characters to place before the separator. 67 | SEPARATOR_POSITION_ = 8 68 | 69 | # The character used to pad codes. 70 | PADDING_CHARACTER_ = '0' 71 | 72 | # The character set used to encode the values. 73 | CODE_ALPHABET_ = '23456789CFGHJMPQRVWX' 74 | 75 | # The base to use to convert numbers to/from. 76 | ENCODING_BASE_ = len(CODE_ALPHABET_) 77 | 78 | # The maximum value for latitude in degrees. 79 | LATITUDE_MAX_ = 90 80 | 81 | # The maximum value for longitude in degrees. 82 | LONGITUDE_MAX_ = 180 83 | 84 | # The max number of digits to process in a plus code. 85 | MAX_DIGIT_COUNT_ = 15 86 | 87 | # Maximum code length using lat/lng pair encoding. The area of such a 88 | # code is approximately 13x13 meters (at the equator), and should be suitable 89 | # for identifying buildings. This excludes prefix and separator characters. 90 | PAIR_CODE_LENGTH_ = 10 91 | 92 | # First place value of the pairs (if the last pair value is 1). 93 | PAIR_FIRST_PLACE_VALUE_ = ENCODING_BASE_**(PAIR_CODE_LENGTH_ / 2 - 1) 94 | 95 | # Inverse of the precision of the pair section of the code. 96 | PAIR_PRECISION_ = ENCODING_BASE_**3 97 | 98 | # The resolution values in degrees for each position in the lat/lng pair 99 | # encoding. These give the place value of each position, and therefore the 100 | # dimensions of the resulting area. 101 | PAIR_RESOLUTIONS_ = [20.0, 1.0, .05, .0025, .000125] 102 | 103 | # Number of digits in the grid precision part of the code. 104 | GRID_CODE_LENGTH_ = MAX_DIGIT_COUNT_ - PAIR_CODE_LENGTH_ 105 | 106 | # Number of columns in the grid refinement method. 107 | GRID_COLUMNS_ = 4 108 | 109 | # Number of rows in the grid refinement method. 110 | GRID_ROWS_ = 5 111 | 112 | # First place value of the latitude grid (if the last place is 1). 113 | GRID_LAT_FIRST_PLACE_VALUE_ = GRID_ROWS_**(GRID_CODE_LENGTH_ - 1) 114 | 115 | # First place value of the longitude grid (if the last place is 1). 116 | GRID_LNG_FIRST_PLACE_VALUE_ = GRID_COLUMNS_**(GRID_CODE_LENGTH_ - 1) 117 | 118 | # Multiply latitude by this much to make it a multiple of the finest 119 | # precision. 120 | FINAL_LAT_PRECISION_ = PAIR_PRECISION_ * GRID_ROWS_**(MAX_DIGIT_COUNT_ - 121 | PAIR_CODE_LENGTH_) 122 | 123 | # Multiply longitude by this much to make it a multiple of the finest 124 | # precision. 125 | FINAL_LNG_PRECISION_ = PAIR_PRECISION_ * GRID_COLUMNS_**(MAX_DIGIT_COUNT_ - 126 | PAIR_CODE_LENGTH_) 127 | 128 | # Minimum length of a code that can be shortened. 129 | MIN_TRIMMABLE_CODE_LEN_ = 6 130 | 131 | GRID_SIZE_DEGREES_ = 0.000125 132 | 133 | 134 | def isValid(code): 135 | """ 136 | Determines if a code is valid. 137 | To be valid, all characters must be from the Open Location Code character 138 | set with at most one separator. The separator can be in any even-numbered 139 | position up to the eighth digit. 140 | """ 141 | # The separator is required. 142 | sep = code.find(SEPARATOR_) 143 | if code.count(SEPARATOR_) > 1: 144 | return False 145 | # Is it the only character? 146 | if len(code) == 1: 147 | return False 148 | # Is it in an illegal position? 149 | if sep == -1 or sep > SEPARATOR_POSITION_ or sep % 2 == 1: 150 | return False 151 | # We can have an even number of padding characters before the separator, 152 | # but then it must be the final character. 153 | pad = code.find(PADDING_CHARACTER_) 154 | if pad != -1: 155 | # Short codes cannot have padding 156 | if sep < SEPARATOR_POSITION_: 157 | return False 158 | # Not allowed to start with them! 159 | if pad == 0: 160 | return False 161 | 162 | # There can only be one group and it must have even length. 163 | rpad = code.rfind(PADDING_CHARACTER_) + 1 164 | pads = code[pad:rpad] 165 | if len(pads) % 2 == 1 or pads.count(PADDING_CHARACTER_) != len(pads): 166 | return False 167 | # If the code is long enough to end with a separator, make sure it does. 168 | if not code.endswith(SEPARATOR_): 169 | return False 170 | # If there are characters after the separator, make sure there isn't just 171 | # one of them (not legal). 172 | if len(code) - sep - 1 == 1: 173 | return False 174 | # Check the code contains only valid characters. 175 | sepPad = SEPARATOR_ + PADDING_CHARACTER_ 176 | for ch in code: 177 | if ch.upper() not in CODE_ALPHABET_ and ch not in sepPad: 178 | return False 179 | return True 180 | 181 | 182 | def isShort(code): 183 | """ 184 | Determines if a code is a valid short code. 185 | A short Open Location Code is a sequence created by removing four or more 186 | digits from an Open Location Code. It must include a separator 187 | character. 188 | """ 189 | # Check it's valid. 190 | if not isValid(code): 191 | return False 192 | # If there are less characters than expected before the SEPARATOR. 193 | sep = code.find(SEPARATOR_) 194 | if sep >= 0 and sep < SEPARATOR_POSITION_: 195 | return True 196 | return False 197 | 198 | 199 | def isFull(code): 200 | """ 201 | Determines if a code is a valid full Open Location Code. 202 | Not all possible combinations of Open Location Code characters decode to 203 | valid latitude and longitude values. This checks that a code is valid 204 | and also that the latitude and longitude values are legal. If the prefix 205 | character is present, it must be the first character. If the separator 206 | character is present, it must be after four characters. 207 | """ 208 | if not isValid(code): 209 | return False 210 | # If it's short, it's not full 211 | if isShort(code): 212 | return False 213 | # Work out what the first latitude character indicates for latitude. 214 | firstLatValue = CODE_ALPHABET_.find(code[0].upper()) * ENCODING_BASE_ 215 | if firstLatValue >= LATITUDE_MAX_ * 2: 216 | # The code would decode to a latitude of >= 90 degrees. 217 | return False 218 | if len(code) > 1: 219 | # Work out what the first longitude character indicates for longitude. 220 | firstLngValue = CODE_ALPHABET_.find(code[1].upper()) * ENCODING_BASE_ 221 | if firstLngValue >= LONGITUDE_MAX_ * 2: 222 | # The code would decode to a longitude of >= 180 degrees. 223 | return False 224 | return True 225 | 226 | 227 | def encode(latitude, longitude, codeLength=PAIR_CODE_LENGTH_): 228 | """ 229 | Encode a location into an Open Location Code. 230 | Produces a code of the specified length, or the default length if no length 231 | is provided. 232 | The length determines the accuracy of the code. The default length is 233 | 10 characters, returning a code of approximately 13.5x13.5 meters. Longer 234 | codes represent smaller areas, but lengths > 14 are sub-centimetre and so 235 | 11 or 12 are probably the limit of useful codes. 236 | Args: 237 | latitude: A latitude in signed decimal degrees. Will be clipped to the 238 | range -90 to 90. 239 | longitude: A longitude in signed decimal degrees. Will be normalised to 240 | the range -180 to 180. 241 | codeLength: The number of significant digits in the output code, not 242 | including any separator characters. 243 | """ 244 | if codeLength < 2 or (codeLength < PAIR_CODE_LENGTH_ and 245 | codeLength % 2 == 1): 246 | raise ValueError('Invalid Open Location Code length - ' + 247 | str(codeLength)) 248 | codeLength = min(codeLength, MAX_DIGIT_COUNT_) 249 | # Ensure that latitude and longitude are valid. 250 | latitude = clipLatitude(latitude) 251 | longitude = normalizeLongitude(longitude) 252 | # Latitude 90 needs to be adjusted to be just less, so the returned code 253 | # can also be decoded. 254 | if latitude == 90: 255 | latitude = latitude - computeLatitudePrecision(codeLength) 256 | code = '' 257 | 258 | # Compute the code. 259 | # This approach converts each value to an integer after multiplying it by 260 | # the final precision. This allows us to use only integer operations, so 261 | # avoiding any accumulation of floating point representation errors. 262 | 263 | # Multiply values by their precision and convert to positive. 264 | # Force to integers so the division operations will have integer results. 265 | # Note: Python requires rounding before truncating to ensure precision! 266 | latVal = int(round((latitude + LATITUDE_MAX_) * FINAL_LAT_PRECISION_, 6)) 267 | lngVal = int(round((longitude + LONGITUDE_MAX_) * FINAL_LNG_PRECISION_, 6)) 268 | 269 | # Compute the grid part of the code if necessary. 270 | if codeLength > PAIR_CODE_LENGTH_: 271 | for i in range(0, MAX_DIGIT_COUNT_ - PAIR_CODE_LENGTH_): 272 | latDigit = latVal % GRID_ROWS_ 273 | lngDigit = lngVal % GRID_COLUMNS_ 274 | ndx = latDigit * GRID_COLUMNS_ + lngDigit 275 | code = CODE_ALPHABET_[ndx] + code 276 | latVal //= GRID_ROWS_ 277 | lngVal //= GRID_COLUMNS_ 278 | else: 279 | latVal //= pow(GRID_ROWS_, GRID_CODE_LENGTH_) 280 | lngVal //= pow(GRID_COLUMNS_, GRID_CODE_LENGTH_) 281 | # Compute the pair section of the code. 282 | for i in range(0, PAIR_CODE_LENGTH_ // 2): 283 | code = CODE_ALPHABET_[lngVal % ENCODING_BASE_] + code 284 | code = CODE_ALPHABET_[latVal % ENCODING_BASE_] + code 285 | latVal //= ENCODING_BASE_ 286 | lngVal //= ENCODING_BASE_ 287 | 288 | # Add the separator character. 289 | code = code[:SEPARATOR_POSITION_] + SEPARATOR_ + code[SEPARATOR_POSITION_:] 290 | 291 | # If we don't need to pad the code, return the requested section. 292 | if codeLength >= SEPARATOR_POSITION_: 293 | return code[0:codeLength + 1] 294 | 295 | # Pad and return the code. 296 | return code[0:codeLength] + ''.zfill(SEPARATOR_POSITION_ - 297 | codeLength) + SEPARATOR_ 298 | 299 | 300 | def decode(code): 301 | """ 302 | Decodes an Open Location Code into the location coordinates. 303 | Returns a CodeArea object that includes the coordinates of the bounding 304 | box - the lower left, center and upper right. 305 | Args: 306 | code: The Open Location Code to decode. 307 | Returns: 308 | A CodeArea object that provides the latitude and longitude of two of the 309 | corners of the area, the center, and the length of the original code. 310 | """ 311 | if not isFull(code): 312 | raise ValueError( 313 | 'Passed Open Location Code is not a valid full code - ' + str(code)) 314 | # Strip out separator character (we've already established the code is 315 | # valid so the maximum is one), and padding characters. Convert to upper 316 | # case and constrain to the maximum number of digits. 317 | code = re.sub('[+0]', '', code) 318 | code = code.upper() 319 | code = code[:MAX_DIGIT_COUNT_] 320 | # Initialise the values for each section. We work them out as integers and 321 | # convert them to floats at the end. 322 | normalLat = -LATITUDE_MAX_ * PAIR_PRECISION_ 323 | normalLng = -LONGITUDE_MAX_ * PAIR_PRECISION_ 324 | gridLat = 0 325 | gridLng = 0 326 | # How many digits do we have to process? 327 | digits = min(len(code), PAIR_CODE_LENGTH_) 328 | # Define the place value for the most significant pair. 329 | pv = PAIR_FIRST_PLACE_VALUE_ 330 | # Decode the paired digits. 331 | for i in range(0, digits, 2): 332 | normalLat += CODE_ALPHABET_.find(code[i]) * pv 333 | normalLng += CODE_ALPHABET_.find(code[i + 1]) * pv 334 | if i < digits - 2: 335 | pv //= ENCODING_BASE_ 336 | 337 | # Convert the place value to a float in degrees. 338 | latPrecision = float(pv) / PAIR_PRECISION_ 339 | lngPrecision = float(pv) / PAIR_PRECISION_ 340 | # Process any extra precision digits. 341 | if len(code) > PAIR_CODE_LENGTH_: 342 | # Initialise the place values for the grid. 343 | rowpv = GRID_LAT_FIRST_PLACE_VALUE_ 344 | colpv = GRID_LNG_FIRST_PLACE_VALUE_ 345 | # How many digits do we have to process? 346 | digits = min(len(code), MAX_DIGIT_COUNT_) 347 | for i in range(PAIR_CODE_LENGTH_, digits): 348 | digitVal = CODE_ALPHABET_.find(code[i]) 349 | row = digitVal // GRID_COLUMNS_ 350 | col = digitVal % GRID_COLUMNS_ 351 | gridLat += row * rowpv 352 | gridLng += col * colpv 353 | if i < digits - 1: 354 | rowpv //= GRID_ROWS_ 355 | colpv //= GRID_COLUMNS_ 356 | 357 | # Adjust the precisions from the integer values to degrees. 358 | latPrecision = float(rowpv) / FINAL_LAT_PRECISION_ 359 | lngPrecision = float(colpv) / FINAL_LNG_PRECISION_ 360 | 361 | # Merge the values from the normal and extra precision parts of the code. 362 | lat = float(normalLat) / PAIR_PRECISION_ + float( 363 | gridLat) / FINAL_LAT_PRECISION_ 364 | lng = float(normalLng) / PAIR_PRECISION_ + float( 365 | gridLng) / FINAL_LNG_PRECISION_ 366 | # Multiple values by 1e14, round and then divide. This reduces errors due 367 | # to floating point precision. 368 | return CodeArea(round(lat, 14), round(lng, 369 | 14), round(lat + latPrecision, 14), 370 | round(lng + lngPrecision, 14), 371 | min(len(code), MAX_DIGIT_COUNT_)) 372 | 373 | 374 | 375 | def recoverNearest(code, referenceLatitude, referenceLongitude): 376 | """ 377 | Recover the nearest matching code to a specified location. 378 | Given a short code of between four and seven characters, this recovers 379 | the nearest matching full code to the specified location. 380 | Args: 381 | code: A valid OLC character sequence. 382 | referenceLatitude: The latitude (in signed decimal degrees) to use to 383 | find the nearest matching full code. 384 | referenceLongitude: The longitude (in signed decimal degrees) to use 385 | to find the nearest matching full code. 386 | Returns: 387 | The nearest full Open Location Code to the reference location that matches 388 | the short code. If the passed code was not a valid short code, but was a 389 | valid full code, it is returned with proper capitalization but otherwise 390 | unchanged. 391 | """ 392 | # if code is a valid full code, return it properly capitalized 393 | if isFull(code): 394 | return code.upper() 395 | if not isShort(code): 396 | raise ValueError('Passed short code is not valid - ' + str(code)) 397 | # Ensure that latitude and longitude are valid. 398 | referenceLatitude = clipLatitude(referenceLatitude) 399 | referenceLongitude = normalizeLongitude(referenceLongitude) 400 | # Clean up the passed code. 401 | code = code.upper() 402 | # Compute the number of digits we need to recover. 403 | paddingLength = SEPARATOR_POSITION_ - code.find(SEPARATOR_) 404 | # The resolution (height and width) of the padded area in degrees. 405 | resolution = pow(20, 2 - (paddingLength / 2)) 406 | # Distance from the center to an edge (in degrees). 407 | halfResolution = resolution / 2.0 408 | # Use the reference location to pad the supplied short code and decode it. 409 | codeArea = decode( 410 | encode(referenceLatitude, referenceLongitude)[0:paddingLength] + code) 411 | # How many degrees latitude is the code from the reference? If it is more 412 | # than half the resolution, we need to move it north or south but keep it 413 | # within -90 to 90 degrees. 414 | if (referenceLatitude + halfResolution < codeArea.latitudeCenter and 415 | codeArea.latitudeCenter - resolution >= -LATITUDE_MAX_): 416 | # If the proposed code is more than half a cell north of the reference location, 417 | # it's too far, and the best match will be one cell south. 418 | codeArea.latitudeCenter -= resolution 419 | elif (referenceLatitude - halfResolution > codeArea.latitudeCenter and 420 | codeArea.latitudeCenter + resolution <= LATITUDE_MAX_): 421 | # If the proposed code is more than half a cell south of the reference location, 422 | # it's too far, and the best match will be one cell north. 423 | codeArea.latitudeCenter += resolution 424 | # Adjust longitude if necessary. 425 | if referenceLongitude + halfResolution < codeArea.longitudeCenter: 426 | codeArea.longitudeCenter -= resolution 427 | elif referenceLongitude - halfResolution > codeArea.longitudeCenter: 428 | codeArea.longitudeCenter += resolution 429 | return encode(codeArea.latitudeCenter, codeArea.longitudeCenter, 430 | codeArea.codeLength) 431 | 432 | 433 | def shorten(code, latitude, longitude): 434 | """ 435 | Remove characters from the start of an OLC code. 436 | This uses a reference location to determine how many initial characters 437 | can be removed from the OLC code. The number of characters that can be 438 | removed depends on the distance between the code center and the reference 439 | location. 440 | The minimum number of characters that will be removed is four. If more than 441 | four characters can be removed, the additional characters will be replaced 442 | with the padding character. At most eight characters will be removed. 443 | The reference location must be within 50% of the maximum range. This ensures 444 | that the shortened code will be able to be recovered using slightly different 445 | locations. 446 | Args: 447 | code: A full, valid code to shorten. 448 | latitude: A latitude, in signed decimal degrees, to use as the reference 449 | point. 450 | longitude: A longitude, in signed decimal degrees, to use as the reference 451 | point. 452 | Returns: 453 | Either the original code, if the reference location was not close enough, 454 | or the . 455 | """ 456 | if not isFull(code): 457 | raise ValueError('Passed code is not valid and full: ' + str(code)) 458 | if code.find(PADDING_CHARACTER_) != -1: 459 | raise ValueError('Cannot shorten padded codes: ' + str(code)) 460 | code = code.upper() 461 | codeArea = decode(code) 462 | if codeArea.codeLength < MIN_TRIMMABLE_CODE_LEN_: 463 | raise ValueError('Code length must be at least ' + 464 | MIN_TRIMMABLE_CODE_LEN_) 465 | # Ensure that latitude and longitude are valid. 466 | latitude = clipLatitude(latitude) 467 | longitude = normalizeLongitude(longitude) 468 | # How close are the latitude and longitude to the code center. 469 | coderange = max(abs(codeArea.latitudeCenter - latitude), 470 | abs(codeArea.longitudeCenter - longitude)) 471 | for i in range(len(PAIR_RESOLUTIONS_) - 2, 0, -1): 472 | # Check if we're close enough to shorten. The range must be less than 1/2 473 | # the resolution to shorten at all, and we want to allow some safety, so 474 | # use 0.3 instead of 0.5 as a multiplier. 475 | if coderange < (PAIR_RESOLUTIONS_[i] * 0.3): 476 | # Trim it. 477 | return code[(i + 1) * 2:] 478 | return code 479 | 480 | 481 | def clipLatitude(latitude): 482 | """ 483 | Clip a latitude into the range -90 to 90. 484 | Args: 485 | latitude: A latitude in signed decimal degrees. 486 | """ 487 | return min(90, max(-90, latitude)) 488 | 489 | 490 | def computeLatitudePrecision(codeLength): 491 | """ 492 | Compute the latitude precision value for a given code length. Lengths <= 493 | 10 have the same precision for latitude and longitude, but lengths > 10 494 | have different precisions due to the grid method having fewer columns than 495 | rows. 496 | """ 497 | if codeLength <= 10: 498 | return pow(20, math.floor((codeLength / -2) + 2)) 499 | return pow(20, -3) / pow(GRID_ROWS_, codeLength - 10) 500 | 501 | 502 | def normalizeLongitude(longitude): 503 | """ 504 | Normalize a longitude into the range -180 to 180, not including 180. 505 | Args: 506 | longitude: A longitude in signed decimal degrees. 507 | """ 508 | while longitude < -180: 509 | longitude = longitude + 360 510 | while longitude >= 180: 511 | longitude = longitude - 360 512 | return longitude 513 | 514 | 515 | class CodeArea(object): 516 | """ 517 | Coordinates of a decoded Open Location Code. 518 | The coordinates include the latitude and longitude of the lower left and 519 | upper right corners and the center of the bounding box for the area the 520 | code represents. 521 | Attributes: 522 | latitude_lo: The latitude of the SW corner in degrees. 523 | longitude_lo: The longitude of the SW corner in degrees. 524 | latitude_hi: The latitude of the NE corner in degrees. 525 | longitude_hi: The longitude of the NE corner in degrees. 526 | latitude_center: The latitude of the center in degrees. 527 | longitude_center: The longitude of the center in degrees. 528 | code_length: The number of significant characters that were in the code. 529 | This excludes the separator. 530 | """ 531 | def __init__(self, latitudeLo, longitudeLo, latitudeHi, longitudeHi, 532 | codeLength): 533 | self.latitudeLo = latitudeLo 534 | self.longitudeLo = longitudeLo 535 | self.latitudeHi = latitudeHi 536 | self.longitudeHi = longitudeHi 537 | self.codeLength = codeLength 538 | self.latitudeCenter = min(latitudeLo + (latitudeHi - latitudeLo) / 2, 539 | LATITUDE_MAX_) 540 | self.longitudeCenter = min( 541 | longitudeLo + (longitudeHi - longitudeLo) / 2, LONGITUDE_MAX_) 542 | 543 | def __repr__(self): 544 | return str([ 545 | self.latitudeLo, self.longitudeLo, self.latitudeHi, 546 | self.longitudeHi, self.latitudeCenter, self.longitudeCenter, 547 | self.codeLength 548 | ]) 549 | 550 | def latlng(self): 551 | return [self.latitudeCenter, self.longitudeCenter] 552 | -------------------------------------------------------------------------------- /src/locateme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 64 | 71 | 78 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/mapbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo-transparent 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/repeaterstart.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Repeater START - Showing The Amateur Repeaters Tool 4 | (C) 2019-2025 Luke Bryan. 5 | OSMGPSMap examples are (C) Hadley Rich 2008 6 | 7 | This is free software: you can redistribute it and/or modify it 8 | under the terms of the GNU General Public License 9 | as published by the Free Software Foundation; version 2. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, see . 18 | """ 19 | 20 | import sys 21 | import os.path 22 | import random 23 | import subprocess 24 | import json 25 | 26 | import gi 27 | gi.require_version("Gtk", "3.0") 28 | gi.require_version('Geoclue', '2.0') 29 | gi.require_version('OsmGpsMap', '1.0') 30 | from gi.repository import Gtk 31 | from gi.repository import Gdk 32 | from gi.repository import GLib 33 | from gi.repository import GdkPixbuf 34 | from gi.repository import GObject 35 | from gi.repository import Pango 36 | import time 37 | import random 38 | import re 39 | import datetime 40 | from gi.repository import cairo 41 | 42 | from gi.repository import Geoclue 43 | import math 44 | import shutil 45 | import urllib.request 46 | import urllib.parse 47 | from math import pi, sin, cos, sqrt, atan2, radians 48 | 49 | from RepeaterStartCommon import userFile 50 | from IRLPNode import IRLPNode 51 | from HearHamRepeater import HearHamRepeater 52 | from SettingsDialog import SettingsDialog 53 | from HelpDialog import HelpDialog 54 | from CsvRepeaterListing import CsvRepeaterListing 55 | from MaidenheadLocator import locatorToLatLng, latLongToLocator 56 | from lib import openlocationcode #Plus code. https://github.com/google/open-location-code 57 | from NetworkStatus import isMobileData 58 | 59 | from threading import Thread 60 | from gi.repository import OsmGpsMap as osmgpsmap 61 | from zipfile import ZipFile 62 | 63 | assert osmgpsmap._version == "1.0" 64 | 65 | class RTLSDRRun(Thread): 66 | def __init__(self, cmd): 67 | Thread.__init__(self) 68 | self.cmd = cmd 69 | 70 | def run(self): 71 | #TODO test commands more 72 | #cmd = 'rtl_fm -M fm -f '+self.freq+'M -l 202 | play -r 24k -t raw -e s -b 16 -c 1 -V1 -' 73 | cmds = self.cmd.split('|') 74 | self.proc = subprocess.Popen(cmds[0].split(), 75 | stdout=subprocess.PIPE, 76 | stderr=subprocess.STDOUT) 77 | subprocess.check_output(cmds[1].split(),stdin=self.proc.stdout) 78 | for line in iter(self.proc.stdout.readline, b''): 79 | line = line.decode('utf-8') 80 | 81 | class BackgroundDownload(Thread): 82 | def __init__(self, url, filename): 83 | #Thread init, as this is a thread: 84 | Thread.__init__(self) 85 | self.url = url 86 | self.filename = filename 87 | self.finished = False 88 | self.success = False 89 | def run(self): 90 | try: 91 | tmpfile = '/tmp/output'+str(int(time.time()))+str(random.random()) 92 | urllib.request.urlretrieve(self.url, tmpfile) 93 | shutil.move( tmpfile, self.filename ) 94 | self.finished = True 95 | self.success = True 96 | except urllib.error.URLError: 97 | print("offline?") 98 | self.finished = True 99 | except urllib.error.HTTPError: 100 | print("Failed to fetch.") 101 | self.finished = True 102 | 103 | class BackgroundDownloadZip(BackgroundDownload): 104 | def run(self): 105 | super().run() 106 | if os.path.exists(self.filename): 107 | with ZipFile(self.filename, 'r') as prozip: 108 | prozip.extractall(path=userFile('.hidden')) 109 | os.remove(self.filename) 110 | else: 111 | print('Unable to update premium RepeaterSTART data.') 112 | 113 | class UI(Gtk.Window): 114 | def __init__(self): 115 | Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL) 116 | self.version = '1.0.1' 117 | self.mode = '' 118 | self.set_default_size(600, 600) 119 | self.connect('destroy', self.cleanup) 120 | self.set_title('RepeaterSTART GPS Mapper') 121 | self.vbox = Gtk.VBox(False, 0) 122 | self.add(self.vbox) 123 | self.paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) 124 | self.vbox.pack_start(self.paned, True, True, 0); 125 | 126 | self.unit = 'mi' #or km 127 | self.renderedLat = None 128 | self.renderedLon = None 129 | 130 | self.rtllistener = None 131 | self.playingfreq = None 132 | self.PLAYSIZE = Gtk.IconSize.BUTTON 133 | self.settingsDialog = SettingsDialog(self) 134 | 135 | self.mainScreen = Gdk.Screen.get_default() 136 | privatetilesapi='https://api.mapbox.com/styles/v1/programmin/ck7jtie300p7e1iqi1ow2yvi3/tiles/256/#Z/#X/#Y?access_token=pk.eyJ1IjoicHJvZ3JhbW1pbiIsImEiOiJjazdpaXVpMTEwbHJ1M2VwYXRoZmU3bmw4In0.3UpUBsTCOL5zvvJ1xVdJdg' 137 | 138 | self.osm = osmgpsmap.Map( 139 | repo_uri=privatetilesapi, 140 | image_format='jpg', 141 | ) 142 | if os.path.exists(userFile('lastPosition.json')): 143 | with open(userFile('lastPosition.json')) as lastone: 144 | lastposition = json.loads(lastone.read()) 145 | self.osm.set_center_and_zoom(lastposition['lat'], 146 | lastposition['lon'], 147 | lastposition['zoom'] 148 | ) 149 | #Now map-source required or it gets some mysterious null pointers and render issue: 150 | self.osm.set_property("map-source", osmgpsmap.MapSource_t.LAST) 151 | self.osm.set_property("repo-uri", privatetilesapi) 152 | 153 | osd = osmgpsmap.MapOsd( 154 | show_dpad=True, 155 | show_zoom=True, 156 | show_crosshair=True) 157 | 158 | icon_app_path = '/usr/share/icons/hicolor/scalable/apps/repeaterSTART.svg' 159 | if os.path.exists(icon_app_path): 160 | pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_app_path) 161 | surface=Gdk.cairo_surface_create_from_pixbuf(pixbuf, 0, None) 162 | self.towerDownPic = GdkPixbuf.Pixbuf.new_from_file_at_scale('signaltowerdown.svg',width=20,height=20,preserve_aspect_ratio=True) 163 | self.towerPic = GdkPixbuf.Pixbuf.new_from_file_at_scale('signaltower.svg',width=20,height=20,preserve_aspect_ratio=True) 164 | 165 | self.osm.layer_add(osd) 166 | 167 | self.settingsDialog.getShown() #in case not initialized for: 168 | self.displayNodes() 169 | 170 | self.last_image = None 171 | 172 | self.osm.connect('button_press_event', self.on_button_press) 173 | self.osm.connect('button_release_event', self.on_button_release) 174 | self.osm.connect('changed', self.on_map_change) 175 | 176 | #connect keyboard shortcuts 177 | self.osm.set_keyboard_shortcut(osmgpsmap.MapKey_t.FULLSCREEN, Gdk.keyval_from_name("F11")) 178 | self.osm.set_keyboard_shortcut(osmgpsmap.MapKey_t.UP, Gdk.keyval_from_name("Up")) 179 | self.osm.set_keyboard_shortcut(osmgpsmap.MapKey_t.DOWN, Gdk.keyval_from_name("Down")) 180 | self.osm.set_keyboard_shortcut(osmgpsmap.MapKey_t.LEFT, Gdk.keyval_from_name("Left")) 181 | self.osm.set_keyboard_shortcut(osmgpsmap.MapKey_t.RIGHT, Gdk.keyval_from_name("Right")) 182 | 183 | #connect to tooltip 184 | self.osm.props.has_tooltip = True 185 | self.osm.connect("query-tooltip", self.on_query_tooltip) 186 | 187 | self.latlon_entry = Gtk.Entry() 188 | 189 | home_button = Gtk.Button() 190 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale('locateme.svg',width=21,height=21,preserve_aspect_ratio=True) 191 | GoImg = Gtk.Image.new_from_pixbuf(pixbuf) 192 | home_button.set_image(GoImg) 193 | home_button.set_tooltip_text('Find my location') 194 | search_button = Gtk.Button() 195 | search_button.set_image(Gtk.Image(icon_name='edit-find', 196 | icon_size=Gtk.IconSize.LARGE_TOOLBAR)) 197 | search_button.connect('clicked', self.searchToggle_clicked) 198 | self.search_text = Gtk.Entry() 199 | self.search_text.connect('activate',self.search_call) 200 | 201 | self.help_button = Gtk.Button() 202 | self.help_button.set_image(Gtk.Image(icon_name='help-browser', 203 | icon_size=Gtk.IconSize.LARGE_TOOLBAR)) 204 | self.help_button.connect('clicked', self.help_clicked) 205 | self.help_about_button = Gtk.Button() 206 | self.help_about_button.set_image(Gtk.Image(icon_name='help-about', 207 | icon_size=Gtk.IconSize.LARGE_TOOLBAR)) 208 | self.help_about_button.connect('clicked', self.helpAbout_clicked) 209 | 210 | self.pref_button = Gtk.Button() 211 | self.pref_button.set_image(Gtk.Image(icon_name="preferences-system", 212 | icon_size=Gtk.IconSize.LARGE_TOOLBAR)) 213 | self.pref_button.connect('clicked', self.pref_clicked) 214 | 215 | 216 | home_button.connect('clicked', self.home_clicked) 217 | self.back_button = Gtk.Button(stock=Gtk.STOCK_GO_BACK) 218 | self.back_button.connect('clicked', self.back_clicked) 219 | 220 | self.cache_button = Gtk.Button('Cache') 221 | self.cache_button.connect('clicked', self.cache_clicked) 222 | if self.mainScreen.get_width() < 800: 223 | #Just room for icon on Librem/phone. 224 | self.add_button = Gtk.Button() 225 | self.add_button.set_image(Gtk.Image(icon_name="list-add", 226 | icon_size=Gtk.IconSize.LARGE_TOOLBAR)) 227 | else: 228 | self.add_button = Gtk.Button('Add Repeater') 229 | self.add_button.connect('clicked', self.add_repeater_clicked) 230 | 231 | overlay = Gtk.Overlay() 232 | overlay.set_size_request(15,15) 233 | overlay.add(self.osm) 234 | top_container = Gtk.VBox() 235 | leftright_container = Gtk.HBox() 236 | mapboxlogo = Gtk.Image.new_from_pixbuf(GdkPixbuf.Pixbuf.new_from_file_at_scale('mapbox.svg',width=80,height=25,preserve_aspect_ratio=True)) 237 | leftright_container.pack_start(mapboxlogo, False, False, 0) 238 | leftright_container.pack_end(self.linkLabel(' Improve this map', self.improvement_link), False, False, 0) 239 | leftright_container.pack_end(self.linkLabel(' (c) openstreetmap ', self.credit_osm), False, False, 0) 240 | leftright_container.pack_end(self.linkLabel(' (c) mapbox ', self.credit_mapbox), False, False, 0) 241 | if not 'licenseKEY' in self.settingsDialog.config['DownloadOptions']: 242 | pro_container = Gtk.HBox() 243 | pro_inner = Gtk.VBox() 244 | pro_inner.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(1.0, 1.0, 0.8, 1.0)) 245 | pro = Gtk.Label("Go premium!", xalign=1) 246 | pro.set_has_window(True) 247 | pro.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) 248 | pro.override_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0.9, 0.0, 0.0, 1.0)) 249 | pro.connect("button-press-event", self.gopremium) 250 | pro_inner.pack_end(pro, False, False, 0) 251 | pro_container.pack_end(pro_inner, False, False, 0) 252 | top_container.pack_start(pro_container, False, False, 0) 253 | top_container.pack_end(leftright_container, False, False, 0) 254 | #self.vbox.pack_start(overlay, True, True, 0) 255 | self.paned.pack1(overlay, resize=True) 256 | overlay.add_overlay(top_container) 257 | overlay.set_overlay_pass_through(top_container,True) 258 | #overlay.set_overlay_pass_through(mapboxlink,False) 259 | hbox = Gtk.HBox(False, 0) 260 | hbox.pack_start(home_button, False, True, 0) 261 | hbox.pack_start(search_button, False, True, 0) 262 | hbox.pack_start(self.search_text, False, True, 0) 263 | hbox.pack_start(self.back_button, False, True, 0) 264 | hbox.pack_start(self.cache_button, False, True, 0) 265 | hbox.pack_start(self.add_button, False, True, 0) 266 | hbox.pack_start(self.pref_button, False, True, 0) 267 | hbox.pack_start(self.help_button, False, True, 0) 268 | hbox.pack_start(self.help_about_button, False, True, 0) 269 | 270 | #Adding image in the render code causes infinite loop. 271 | icon_app_path = '/usr/share/icons/hicolor/scalable/apps/repeaterSTART.svg' 272 | if os.path.exists(icon_app_path): 273 | pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_app_path) 274 | self.set_icon(pixbuf) 275 | 276 | #add ability to test custom map URIs 277 | #ex = Gtk.Expander(label="Display Options") 278 | #ex.props.use_markup = True 279 | vb = Gtk.VBox() 280 | self.repouri_entry = Gtk.Entry() 281 | self.image_format_entry = Gtk.Entry() 282 | self.image_format_entry.set_text(self.osm.props.image_format) 283 | 284 | 285 | lbl = Gtk.Label( 286 | """ 287 | Enter an repository URL to fetch map tiles from in the box below. Special metacharacters may be included in this url 288 | 289 | Metacharacters: 290 | \t#X\tMax X location 291 | \t#Y\tMax Y location 292 | \t#Z\tMap zoom (0 = min zoom, fully zoomed out) 293 | \t#S\tInverse zoom (max-zoom - #Z) 294 | \t#Q\tQuadtree encoded tile (qrts) 295 | \t#W\tQuadtree encoded tile (1234) 296 | \t#U\tEncoding not implemeted 297 | \t#R\tRandom integer, 0-4""") 298 | lbl.props.xalign = 0 299 | lbl.props.use_markup = True 300 | lbl.props.wrap = True 301 | 302 | #ex.add(vb) 303 | self.show_tooltips = False 304 | #self.vbox.pack_end(ex, False, True, 0) 305 | self.vbox.pack_end(self.latlon_entry, False, True, 0) 306 | self.vbox.pack_end(hbox, False, True, 0) 307 | 308 | GLib.timeout_add(500, self.print_tiles) 309 | GLib.timeout_add(1000, self.downloadBackground) 310 | self.bgdl = None 311 | 312 | self.listbox = Gtk.ListBox() 313 | self.listbox.set_activate_on_single_click(False) 314 | self.listbox.connect('row-activated', self.selrow) 315 | self.listbox.connect("button_press_event", self.buttonPress) 316 | 317 | self.searchlistbox = Gtk.ListBox() 318 | self.searchlistbox.set_activate_on_single_click(False) 319 | self.searchlistbox.connect('row-activated', self.selsearchrow) 320 | self.searchRows = [] 321 | 322 | scrolled = Gtk.ScrolledWindow() 323 | scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 324 | scrolled.add(self.listbox) 325 | #self.vbox.pack_start(scrolled, True, True, 0) 326 | self.paned.pack2(scrolled, resize=True) 327 | self.GTKListRows = [] 328 | self.playBtns = [] 329 | GObject.idle_add(self.updateMessage) 330 | 331 | def buttonPress(self,listbox, event): 332 | """ 333 | The right click feature on repeater list items 334 | """ 335 | if event.button == 3: 336 | x = int(event.x) 337 | y = int(event.y) 338 | moment = event.time 339 | listBoxRow = listbox.get_row_at_y(y) 340 | if listBoxRow is not None: 341 | listBoxRow.grab_focus() 342 | # Right click menu 343 | rcmenu = Gtk.Menu() 344 | rcgoto = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_JUMP_TO,None) 345 | if listBoxRow.repeaterID>0: 346 | rcgoto.connect("activate", self.followlink) 347 | rcgoto.repeaterID = listBoxRow.repeaterID 348 | rcgoto.set_label("_Goto Repeater Page") 349 | rcgoto.show() 350 | rccomment = Gtk.ImageMenuItem.new() 351 | rccomment.connect("activate", self.followcommentlink) 352 | rccomment.repeaterID = listBoxRow.repeaterID 353 | rccomment.set_label("Comment") 354 | rccomment.show() 355 | rcmenu.append(rcgoto) 356 | rcmenu.append(rccomment) 357 | #And link any other links in the description: 358 | desc = listBoxRow.get_children()[0].get_children()[0].get_children()[-1].get_text() 359 | for url in re.finditer(r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?', desc): 360 | link = Gtk.ImageMenuItem.new() 361 | link.url = url.group(0) 362 | link.connect("activate", self.followextralink) 363 | link.set_label(link.url) 364 | link.show() 365 | rcmenu.append(link) 366 | elif int(listBoxRow.irlp)>0: 367 | rcgoto.connect("activate", self.followIRLPlink) 368 | rcgoto.irlp = listBoxRow.irlp 369 | rcgoto.set_label("_Goto IRLP Status page") 370 | rcgoto.show() 371 | rcmenu.append(rcgoto) 372 | else: 373 | print('Unknown data') 374 | rcmenu.popup(None, None, None,None, 375 | event.button, moment) 376 | 377 | def gopremium(self, obj, obj2): 378 | os.system('xdg-open "https://hearham.com/gopremium"') 379 | 380 | def followIRLPlink(self,menuItem): 381 | os.system('xdg-open "https://www.irlp.net/status/index.php?nodeid=%s"' % (menuItem.irlp,) ) 382 | 383 | def followlink(self,menuItem): 384 | os.system('xdg-open "https://hearham.com/repeaters/%s?src=%s"' % (menuItem.repeaterID,os.name) ) 385 | 386 | def followcommentlink(self,menuItem): 387 | os.system('xdg-open "https://hearham.com/repeaters/%s/comment?src=%s"' % (menuItem.repeaterID,os.name) ) 388 | 389 | def followextralink(self,menuItem): 390 | os.system('xdg-open "%s"' % (menuItem.url.replace('"','%22'),) ) 391 | 392 | def setViews(self): 393 | if self.mode == 'search': 394 | self.search_text.show() 395 | self.back_button.hide() 396 | self.cache_button.hide() 397 | self.add_button.hide() 398 | self.pref_button.hide() 399 | self.help_button.hide() 400 | self.help_about_button.hide() 401 | else: 402 | self.search_text.hide() 403 | self.back_button.show() 404 | self.cache_button.show() 405 | self.add_button.show() 406 | self.pref_button.show() 407 | self.help_button.show() 408 | self.help_about_button.show() 409 | 410 | def search_call(self, widget): 411 | srctext = widget.get_text() 412 | try: 413 | pluscode = openlocationcode.decode(srctext) 414 | self.osm.set_center(pluscode.latitudeCenter, pluscode.longitudeCenter) 415 | except: 416 | try: 417 | gridsquare = locatorToLatLng(srctext) 418 | if gridsquare: 419 | self.osm.set_center(gridsquare['lat'], gridsquare['lng']) 420 | #Search for number - internet node no. or frequency 421 | elif re.match(r"(\d)*\.?(\d)*$", srctext): 422 | number = float(srctext) 423 | self.clearRows() 424 | lat, lon = self.osm.props.latitude, self.osm.props.longitude 425 | noneFound = True; 426 | 427 | for repeater in self.allrepeaters: 428 | km = repeater.distance(lat,lon) 429 | if self.settingsDialog.getUnit() == 'mi': 430 | km = km*.62137119 431 | freq = float(repeater.freq) 432 | off = freq + float(repeater.offset) 433 | if repeater.freq == number or number == off: 434 | row = Gtk.ListBoxRow() 435 | row.longitude = float(repeater.lon) 436 | row.latitude = float(repeater.lat) 437 | row.repeaterID = repeater.id 438 | # ^ for double click activate 439 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 440 | #print(repeater.callsign, repeater.freq, off, km) 441 | mainlbl = Gtk.Label("%s (%.3f/%.3f) (%.2f%s)" % ( 442 | repeater.callsign, freq, off, km, self.settingsDialog.getUnit() ), xalign=0) 443 | hbox.pack_start(mainlbl,True,True,0) 444 | row.add(hbox) 445 | self.listbox.add(row) 446 | self.searchRows.append(row) 447 | noneFound = False 448 | elif repeater.node == srctext: 449 | row = Gtk.ListBoxRow() 450 | row.longitude = float(repeater.lon) 451 | row.latitude = float(repeater.lat) 452 | row.repeaterID = repeater.id 453 | # ^ for double click activate 454 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 455 | mainlbl = Gtk.Label("%s node %s (%.2f%s)" % ( 456 | repeater.callsign, repeater.node, km, self.settingsDialog.getUnit() 457 | ),xalign=0) 458 | hbox.pack_start(mainlbl,True,True,0) 459 | row.add(hbox) 460 | self.listbox.add(row) 461 | self.searchRows.append(row) 462 | noneFound = False 463 | if noneFound: 464 | row = Gtk.ListBoxRow() 465 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 466 | mainlbl = Gtk.Label("Sorry, nothing found for that frequency or IRLP node number",xalign=0) 467 | hbox.pack_start(mainlbl,True,True,0) 468 | row.add(hbox) 469 | self.listbox.add(row) 470 | self.searchRows.append(row) 471 | self.listbox.show_all() 472 | 473 | #What3Words address has 2 . in it: 474 | elif re.match( r".*\..*\..*", srctext): 475 | req = urllib.request.Request( 476 | 'https://hearham.com/api/whatthreewords/v1?words=%s' % (urllib.parse.quote(srctext),), 477 | data=None, 478 | headers={ 479 | 'User-Agent': 'Repeater-START/'+self.version 480 | } 481 | ) 482 | f = urllib.request.urlopen(req) 483 | objs = json.loads(f.read().decode('utf-8')) 484 | if not objs: 485 | self.latlon_entry.set_text('Invalid what3words.com address.') 486 | else: 487 | self.osm.set_center(objs['coordinates']['lat'], objs['coordinates']['lng']) 488 | self.latlon_entry.set_text('Map Center: %s %s : %s' % ( latLongToLocator(objs['coordinates']['lat'], objs['coordinates']['lng']), objs['map'], objs['nearestPlace'] ) ) 489 | else: 490 | # Use new query format https://github.com/osm-search/Nominatim/issues/2121 491 | req = urllib.request.Request( 492 | 'https://nominatim.openstreetmap.org/search?q=%s&format=json&limit=50' % (urllib.parse.quote(srctext),), 493 | data=None, 494 | headers={ 495 | 'User-Agent': 'Repeater-START/'+self.version 496 | } 497 | ) 498 | f = urllib.request.urlopen(req) 499 | objs = json.loads(f.read().decode('utf-8')) 500 | self.clearRows() 501 | if len(objs) == 0: 502 | row = Gtk.ListBoxRow() 503 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 504 | mainlbl = Gtk.Label("Sorry, nothing found. Please enter a different peak, city or landmark.",xalign=0) 505 | hbox.pack_start(mainlbl,True,True,0) 506 | row.add(hbox) 507 | self.listbox.add(row) 508 | self.searchRows.append(row) 509 | for item in objs: 510 | row = Gtk.ListBoxRow() 511 | row.longitude = float(item['lon']) 512 | row.latitude = float(item['lat']) 513 | # ^ for double click activate 514 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 515 | mainlbl = Gtk.Label(item['display_name'],xalign=0) 516 | hbox.pack_start(mainlbl,True,True,0) 517 | row.add(hbox) 518 | self.listbox.add(row) 519 | self.searchRows.append(row) 520 | self.listbox.show_all() 521 | except urllib.error.URLError: 522 | self.latlon_entry.set_text('Network error') 523 | 524 | def clearRows(self): 525 | for r in self.GTKListRows: 526 | r.destroy() 527 | self.GTKListRows = [] 528 | for r in self.searchRows: 529 | r.destroy() 530 | self.searchRows = [] 531 | 532 | 533 | def selrow(self,widget,listboxrow): 534 | self.osm.set_center(listboxrow.latitude, listboxrow.longitude) 535 | 536 | def selsearchrow(self, widget, listboxrow): 537 | print(widget) 538 | 539 | def updateMessage(self): 540 | toupdatefile = userFile('update.response') 541 | if os.path.exists(toupdatefile): 542 | Gdk.threads_enter() 543 | try: 544 | updateinfo = json.load(open(toupdatefile)) 545 | if str(updateinfo['version']) != self.version: 546 | dlg = Gtk.MessageDialog(self, 547 | 0,Gtk.MessageType.QUESTION, 548 | Gtk.ButtonsType.YES_NO, 549 | 'There is an update available. Do you wish to install it?\n'+ 550 | updateinfo['message']) 551 | response = dlg.run() 552 | if response == Gtk.ResponseType.YES: 553 | os.system('xdg-open '+updateinfo['link']) 554 | dlg.destroy() 555 | 556 | except: 557 | print('Error update check') 558 | Gdk.threads_leave() 559 | 560 | def linkLabel(self, lbltext, connectfunction): 561 | """ Like a label, clickable. https://stackoverflow.com/questions/5822191/ """ 562 | lbl = Gtk.Label(lbltext, xalign=1); 563 | lbl.set_has_window(True) 564 | lbl.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) 565 | lbl.override_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0.0, 0.0, 0.8, 1.0)) 566 | lbl.connect("button-press-event", connectfunction) 567 | return lbl 568 | 569 | 570 | 571 | def displayNodes(self): 572 | start = time.time(); 573 | self.osm.image_remove_all() 574 | minimum = self.settingsDialog.getMinFilter() 575 | maximum = self.settingsDialog.getMaxFilter() 576 | self.allrepeaters = [] 577 | 578 | for rpt in self.settingsDialog.config['Repeaters']: 579 | url = self.settingsDialog.config['Repeaters'][rpt] 580 | if url.find('hearham.com/api/repeaters/v1') >-1: 581 | #The standard repeaters shown: 582 | irlpfile = userFile('irlp.txt') 583 | repeatersfile = userFile('repeaters.json') 584 | if os.path.exists(irlpfile): 585 | with open(irlpfile) as repfile: 586 | for line in repfile: 587 | try: 588 | self.addRepeaterIcon(IRLPNode(line), minimum, maximum) 589 | except ValueError as e: 590 | print(e) 591 | else: 592 | print('WARNING IRLP FILE NOT LOADED') 593 | if os.path.exists(repeatersfile): 594 | for repeater in json.load(open(repeatersfile)): 595 | #IRLP has been done in direct pull above. 596 | if repeater['group'] != 'IRLP': 597 | #Slow LibOSM issue https://github.com/nzjrs/osm-gps-map/issues/104 598 | GLib.idle_add(self.addRepeaterIcon, HearHamRepeater(repeater), minimum, maximum) 599 | #self.addRepeaterIcon(HearHamRepeater(repeater), minimum, maximum) 600 | else: 601 | print('WARNING: REPEATERS FILE NOT LOADED') 602 | else: 603 | customfile = userFile('rpt-'+rpt+'.csv') 604 | if os.path.exists(customfile): 605 | csv = CsvRepeaterListing(customfile) 606 | icon = csv.getIcon() 607 | iconDown = icon; 608 | 609 | # Get pixel data 610 | pixels = bytearray(iconDown.get_pixels()) 611 | for y in range(iconDown.get_height()): 612 | for x in range(iconDown.get_width()): 613 | offset = y * iconDown.get_rowstride() + x * iconDown.get_n_channels() 614 | # Set red color (R, G, B) 615 | pixels[offset] = 255 616 | pixels[offset + 1] = 0 617 | pixels[offset + 2] = 0 618 | pixels = bytes(pixels) 619 | iconDown = GdkPixbuf.Pixbuf.new_from_data(pixels, icon.get_colorspace(), icon.get_has_alpha(), 620 | icon.get_bits_per_sample(), icon.get_width(), 621 | icon.get_height(), icon.get_rowstride()) 622 | 623 | for r in csv.repeaters: 624 | if r.isDown(): 625 | self.addRepeaterWithIcon(r, minimum, maximum, iconDown) 626 | else: 627 | self.addRepeaterWithIcon(r, minimum, maximum, icon) 628 | 629 | #print('DISPLAYNODES took '+str(time.time()-start)) 630 | 631 | def credit_mapbox(self, obj, obj2): 632 | os.system('xdg-open https://www.mapbox.com/about/maps/') 633 | def credit_osm(self, obj, obj2): 634 | os.system('xdg-open http://www.openstreetmap.org/about/') 635 | def improvement_link(self, obj, obj2): 636 | os.system('xdg-open https://www.mapbox.com/map-feedback/') 637 | 638 | def addRepeaterIcon(self, repeater, minimum, maximum): 639 | if(float(repeater.freq) >= minimum and 640 | float(repeater.freq) <= maximum ): 641 | if repeater.isDown(): 642 | pixbuf = self.towerDownPic 643 | else: 644 | pixbuf = self.towerPic 645 | self.osm.image_add(repeater.lat, repeater.lon, pixbuf) 646 | self.allrepeaters.append(repeater) 647 | 648 | def addRepeaterWithIcon(self, repeater, minimum, maximum, pixbuf): 649 | if(float(repeater.freq) >= minimum and 650 | float(repeater.freq) <= maximum ): 651 | self.osm.image_add(repeater.lat, repeater.lon, pixbuf) 652 | self.allrepeaters.append(repeater) 653 | 654 | 655 | def disable_cache_toggled(self, btn): 656 | if btn.props.active: 657 | self.osm.props.tile_cache = osmgpsmap.MAP_CACHE_DISABLED 658 | else: 659 | self.osm.props.tile_cache = osmgpsmap.MAP_CACHE_AUTO 660 | 661 | def on_show_tooltips_toggled(self, btn): 662 | self.show_tooltips = btn.props.active 663 | 664 | def load_map_clicked(self, button): 665 | uri = self.repouri_entry.get_text() 666 | format = self.image_format_entry.get_text() 667 | if uri and format: 668 | if self.osm: 669 | #remove old map 670 | self.vbox.remove(self.osm) 671 | try: 672 | self.osm = osmgpsmap.Map( 673 | repo_uri=uri, 674 | image_format=format 675 | ) 676 | except Exception: 677 | print( "ERROR:" ) 678 | self.osm = osm.Map() 679 | self.vbox.pack_start(self.osm, True, True, 0) 680 | #self.osm.connect('button_release_event', self.map_clicked) 681 | self.osm.show() 682 | 683 | def print_tiles(self): 684 | if self.osm.props.tiles_queued != 0: 685 | print( '%s tiles queued' % (self.osm.props.tiles_queued,) ) 686 | return True 687 | 688 | def downloadBackground(self): 689 | if self.bgdl == None: 690 | if not(isMobileData()) or self.settingsDialog.getAllowMobile(): 691 | self.checkUpdate = BackgroundDownload('https://hearham.com/api/updatecheck/linux', userFile('update.response')) 692 | self.checkUpdate.start() 693 | if 'licenseKEY' in self.settingsDialog.config['DownloadOptions']: 694 | self.premiumUpdate = BackgroundDownloadZip('https://hearham.com/api/repeaters/v1/radios?key='+self.settingsDialog.config['DownloadOptions']['licenseKEY'], userFile('premium.zip')) 695 | self.premiumUpdate.start() 696 | 697 | for rpt in self.settingsDialog.config['Repeaters']: 698 | url = self.settingsDialog.config['Repeaters'][rpt] 699 | 700 | if url.find('hearham.com/api/repeaters/v1') >-1: 701 | self.bgdl = BackgroundDownload('https://hearham.com/nohtmlstatus.txt', userFile('irlp.txt')) 702 | self.bgdl.start() 703 | 704 | self.hearhamdl = BackgroundDownload('https://hearham.com/api/repeaters/v1', userFile('repeaters.json')) 705 | self.hearhamdl.start() 706 | 707 | elif url.find('.csv') >-1: 708 | csv = BackgroundDownload(url, userFile('rpt-'+rpt+'.csv')) 709 | csv.start() 710 | else: 711 | print('Unknown repeater list not added : '+url) 712 | 713 | #Call again 10m later 714 | else: 715 | print('(not downloading, mobile)') 716 | 717 | GLib.timeout_add(600000, self.downloadBackground) 718 | if 0 == len(self.allrepeaters): 719 | GLib.timeout_add(10000, self.displayNodes) 720 | else: 721 | if self.bgdl.finished: 722 | self.displayNodes() 723 | if self.bgdl.success: 724 | #Since we're online, do cleanup of any ancient files, clean up and also due to Mapbox TOS: 725 | old = time.time() - 60*60*24*31 726 | print( 'Going back to %s' % (old,)) 727 | for root, dirs, files in os.walk(self.osm.get_default_cache_directory()): 728 | for jpg in files: 729 | path = os.path.join(root, jpg) 730 | if jpg.endswith('.jpg') and os.path.getmtime(path) < old: 731 | print('Old, removing '+path) 732 | os.remove(path) 733 | self.bgdl = None #and do create thread again: 734 | self.downloadBackground() 735 | 736 | 737 | def zoom_in_clicked(self, button): 738 | self.osm.set_zoom(self.osm.props.zoom + 1) 739 | 740 | def zoom_out_clicked(self, button): 741 | self.osm.set_zoom(self.osm.props.zoom - 1) 742 | 743 | def home_clicked(self, button): 744 | #self.getlocation() #Freezes up, odd. 745 | GLib.timeout_add(1, self.getlocation) 746 | 747 | def back_clicked(self, button): 748 | self.osm.set_center_and_zoom(self.lastLat, self.lastLon, 12) 749 | 750 | def searchToggle_clicked(self,button): 751 | if self.mode == 'search': 752 | self.mode = '' 753 | else: 754 | self.mode = 'search' 755 | self.search_text.grab_focus() 756 | self.setViews() 757 | 758 | def help_clicked(self, button): 759 | dlg = Gtk.MessageDialog(self, 760 | 0,Gtk.MessageType.INFO, 761 | Gtk.ButtonsType.OK, 762 | 'Repeater-START is an application for amateur radio.\nWhen started, this app will try to update the latest repeaters.\n'+ 763 | 'If offline, the list will be loaded from user storage.\n'+ 764 | 'While online, you may hit "cache" and store map tiles for later use, and choose "add repeater" to contribute to the repeater database!\n' 765 | 'All repeaters in the lower half of the screen are listed by distance, closest to the center map marker first.\n'+ 766 | 'Double click a repeater to center its position selected on the map.\n'+ 767 | 'Or click play to try to listen through a RTLSDR device.') 768 | response = dlg.run() 769 | dlg.destroy() 770 | 771 | def helpAbout_clicked(self,button): 772 | changed = datetime.datetime.fromtimestamp(os.path.getmtime(userFile('repeaters.json'))).strftime('%c') 773 | dlg = Gtk.MessageDialog(self, 774 | 0,Gtk.MessageType.INFO, 775 | Gtk.ButtonsType.OK, 776 | 'Repeater-START v'+self.version+'\n'+ 777 | 'Showing The Amateur Repeaters Tool - The only open-source Linux desktop repeater app utilizing the open-data repeater database.\n\n'+ 778 | 'Your downloaded repeater database was updated:\n'+changed+ 779 | '\nFor support/questions please use the Github issues or contact@hearham.live.') 780 | response = dlg.run() 781 | dlg.destroy() 782 | 783 | def pref_clicked(self,button): 784 | self.settingsDialog.show() 785 | 786 | def getlocation(self): 787 | self.lastLat = self.osm.props.latitude 788 | self.lastLon = self.osm.props.longitude 789 | try: 790 | clue = Geoclue.Simple.new_sync('repeaterstart',Geoclue.AccuracyLevel.EXACT,None) 791 | location = clue.get_location() 792 | self.osm.set_center_and_zoom(location.get_property('latitude'), location.get_property('longitude'), 12) 793 | except GLib.Error as err: 794 | print(err) 795 | GLib.idle_add(self.privacySettingsOpen) 796 | 797 | 798 | def privacySettingsOpen(self): 799 | Gdk.threads_enter() 800 | dlg = Gtk.MessageDialog(self, 801 | 0,Gtk.MessageType.WARNING, 802 | Gtk.ButtonsType.OK, 803 | 'Please allow geolocation to use this feature.') 804 | response = dlg.run() 805 | dlg.destroy() 806 | subprocess.Popen(['gnome-control-center','privacy']) 807 | Gdk.threads_leave() 808 | 809 | 810 | def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip, data=None): 811 | if keyboard_tip: 812 | return False 813 | 814 | if self.show_tooltips: 815 | #print(dir(osmgpsmap)) 816 | #while True: 817 | # print(eval(input('>'))) 818 | #p = osmgpsmap.point_new_degrees(0.0, 0.0) 819 | p=self.osm.convert_screen_to_geographic(x, y)#, p) 820 | lat,lon = p.get_degrees() 821 | tooltip.set_markup("%+.4f, %+.4f" % (lat, lon )) 822 | return True 823 | 824 | return False 825 | 826 | def cache_clicked(self, button): 827 | bbox = self.osm.get_bbox() 828 | maxz = self.osm.props.max_zoom 829 | if maxz - self.osm.props.zoom > 2: 830 | maxz = self.osm.props.zoom + 2 831 | print( '%s max %s' % (self.osm.props.zoom, maxz) ) 832 | self.osm.download_maps( 833 | *bbox, 834 | zoom_start=self.osm.props.zoom, 835 | zoom_end=maxz 836 | ) 837 | 838 | def add_repeater_clicked(self, button): 839 | os.system('xdg-open "https://hearham.com/repeaters/add?lat=%s&lon=%s"' % (self.osm.props.latitude, self.osm.props.longitude) ) 840 | 841 | def on_map_change(self, event): 842 | if self.renderedLat != self.osm.props.latitude or self.renderedLon != self.osm.props.longitude: 843 | #Center changed. 844 | self.renderedLat = self.osm.props.latitude 845 | self.renderedLon = self.osm.props.longitude 846 | 847 | t = time.time() 848 | text = 'Map Center: %s, latitude %s longitude %s' if self.mainScreen.get_width() > 800 else '%s, lat: %s lon: %s' 849 | if self.settingsDialog.getMinFilter()>-1 or self.settingsDialog.getMaxFilter()<1E99: 850 | text += ' (Repeaters filtered in settings)' 851 | self.latlon_entry.set_text( 852 | text % ( 853 | latLongToLocator(self.renderedLat, self.renderedLon), 854 | round(self.osm.props.latitude, 4), 855 | round(self.osm.props.longitude, 4) 856 | ) 857 | ) 858 | self.refreshListing() 859 | print('on_map_change time: %s' % (time.time() - t)) 860 | 861 | def refreshListing(self): 862 | # cursor lat,lon = self.osm.get_event_location(event).get_degrees() 863 | lat, lon = self.osm.props.latitude, self.osm.props.longitude 864 | maxkm = 500 865 | self.allrepeaters = sorted(self.allrepeaters, key = lambda repeater : repeater.distance(lat,lon)) 866 | self.clearRows() 867 | self.playBtns = [] 868 | added = 0 869 | #The settinds parser take a bit of time. Get number once. 870 | minimum = self.settingsDialog.getMinFilter() 871 | maximum = self.settingsDialog.getMaxFilter() 872 | for item in self.allrepeaters: 873 | distance = item.distance(lat,lon) 874 | if( distance < maxkm and 875 | float(item.freq) >= minimum and 876 | float(item.freq) <= maximum ): 877 | self.addToList(item, lat,lon) 878 | added += 1 879 | if added > 100: #Listing excessively many makes it laggy, eg New England repeaters. 880 | break 881 | self.listbox.show_all() 882 | 883 | def addToList(self, repeater, lat, lon): 884 | row = Gtk.ListBoxRow() 885 | row.longitude = repeater.lon 886 | row.latitude = repeater.lat 887 | if isinstance(repeater, HearHamRepeater): 888 | row.repeaterID = repeater.id 889 | row.irlp = 0 890 | else:#Irlpnode: 891 | row.repeaterID = 0 892 | row.irlp = repeater.node 893 | # ^ for double click activate 894 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 895 | row.add(hbox) 896 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 897 | hbox.pack_start(vbox, True, True, 0) 898 | innerhbox = Gtk.HBox() 899 | if float(repeater.freq) == 0: 900 | lbltext = "%s Reflector %s" if self.mainScreen.get_width() < 800 else "Node %s Reflector %s" 901 | label1 = Gtk.Label(lbltext % (repeater.node, repeater.callsign), xalign=0) 902 | else: 903 | if repeater.node: 904 | lbltext = "%s, %s at %smhz" if self.mainScreen.get_width() < 800 else "Node %s, %s at %smhz" 905 | lbltext = lbltext % (repeater.node, repeater.callsign, repeater.freq) 906 | label1 = Gtk.Label(lbltext, xalign=0) 907 | if repeater.isDown(): 908 | label1.set_markup(''+lbltext+'') 909 | else: 910 | lbltext = "%s, %smhz" % (repeater.callsign, repeater.freq) 911 | label1 = Gtk.Label(lbltext, xalign=0) 912 | if not repeater.status: 913 | label1.set_markup(''+lbltext+'') 914 | try: 915 | int(repeater.status) 916 | linkknown = False 917 | for item in self.allrepeaters: 918 | if item.node == repeater.status: 919 | gothere = Gtk.Button("Linked to %s " % (str(repeater.status),)) 920 | innerhbox.pack_start(gothere, False, False, 0) 921 | gothere.connect('clicked', self.goLinkIRLP) 922 | linkknown = True 923 | if not linkknown and int(repeater.status) >1: 924 | innerhbox.pack_start(Gtk.Label("Linked to %s. " % (repeater.status,), xalign=0),False, False, 0) 925 | label2 = Gtk.Label("PL %s, Offset %s, %s" % (repeater.pl, repeater.offset, repeater.url), xalign=0) 926 | except ValueError: 927 | #Not connected to number node 928 | if float(repeater.freq) == 0: 929 | label2 = Gtk.Label(repeater.city, xalign=0) 930 | else: 931 | label2 = Gtk.Label("%s. PL %s, Offset %s, %s" % (repeater.status, repeater.pl, repeater.offset, repeater.url), xalign=0) 932 | 933 | label3 = Gtk.Label(repeater.description, xalign=0) 934 | innerhbox.pack_start(label2, True, True, 0) 935 | 936 | label1.set_ellipsize(Pango.EllipsizeMode.END) 937 | label2.set_ellipsize(Pango.EllipsizeMode.END) 938 | label3.set_ellipsize(Pango.EllipsizeMode.END) 939 | if self.mainScreen.get_width() < 800: 940 | label2.modify_font(Pango.font_description_from_string("Ubuntu 10")) 941 | label3.modify_font(Pango.font_description_from_string("Ubuntu 10")) 942 | 943 | if self.mainScreen.get_width() < 800: 944 | #mobile 945 | label1.modify_font(Pango.font_description_from_string("Ubuntu Bold 12")) 946 | else: 947 | label1.modify_font(Pango.font_description_from_string("Ubuntu Bold 22")) 948 | vbox.pack_start(label1, True, True, 5) 949 | vbox.pack_start(innerhbox, True, True, 0) 950 | vbox.pack_start(label3, True, True, 5) 951 | km = repeater.distance(lat,lon) 952 | if self.settingsDialog.getUnit() == 'mi': 953 | km = km*.62137119 954 | 955 | distlbl = Gtk.Label( '%s %s ' % ( int(km*10)/10, self.settingsDialog.getUnit() )) 956 | playbtn = Gtk.Button() 957 | playbtn.set_image(Gtk.Image(icon_name='media-playback-start', 958 | icon_size=self.PLAYSIZE)) 959 | #playbtn.set_label('') Makes icon disappear, on many systems with button icon turned off. 960 | playbtn.selFrequency = repeater.freq 961 | if repeater.freq == self.playingfreq: 962 | playbtn.set_image(Gtk.Image(icon_name='media-playback-stop', 963 | icon_size=self.PLAYSIZE)) 964 | else: 965 | playbtn.set_image(Gtk.Image(icon_name='media-playback-start', 966 | icon_size=self.PLAYSIZE)) 967 | helpbtn = Gtk.Button() 968 | helpbtn.repeater = repeater; 969 | helpbtn.set_image(Gtk.Image(icon_name='help-browser', 970 | icon_size=self.PLAYSIZE)) 971 | helpbtn.set_tooltip_text('Radio Setup Help') 972 | helpbtn.connect('clicked', self.helppro) 973 | playbtn.connect('clicked', self.playpause) 974 | rightbox = Gtk.VBox() 975 | rightbox.pack_start(distlbl, False, True, 10) 976 | rightbox.pack_start(playbtn, True, True, 0) 977 | rightbox.pack_start(helpbtn, True, True, 0) 978 | hbox.pack_start(rightbox, False, True, 0) 979 | 980 | #These two arrays should correspond! 981 | self.GTKListRows.append(row) 982 | self.playBtns.append(playbtn) 983 | self.listbox.add(row) 984 | 985 | def playpause(self, btn): 986 | if btn.selFrequency != self.playingfreq: 987 | self.playRTLSDR(btn.selFrequency) 988 | self.playingfreq = btn.selFrequency 989 | for b in self.playBtns: 990 | b.set_image(Gtk.Image(icon_name='media-playback-start', 991 | icon_size=self.PLAYSIZE)) 992 | #All others are stopped. 993 | btn.set_image(Gtk.Image(icon_name='media-playback-stop', 994 | icon_size=self.PLAYSIZE)) 995 | else: 996 | if self.rtllistener: 997 | self.rtllistener.proc.kill() 998 | self.playingfreq = None 999 | btn.set_image(Gtk.Image(icon_name='media-playback-start', 1000 | icon_size=self.PLAYSIZE)) 1001 | 1002 | def goLinkIRLP(self, btn): 1003 | label = btn.get_label() 1004 | print(label) 1005 | label = label.replace('Linked to','').strip() 1006 | for item in self.allrepeaters: 1007 | if item.node == label: 1008 | self.osm.set_center(item.lat, item.lon) 1009 | 1010 | def on_button_release(self, osm, event): 1011 | pass # Not the right lat/lon props here. 1012 | 1013 | def helppro(self, el): 1014 | if os.path.exists(userFile('.hidden')):#self.settingsDialog.config['licenseKEY']: 1015 | help = HelpDialog(self, el.repeater) 1016 | help.run() 1017 | help.destroy() 1018 | else: 1019 | dialogWindow = Gtk.MessageDialog(self, 1020 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 1021 | Gtk.MessageType.QUESTION, 1022 | Gtk.ButtonsType.OK_CANCEL, 1023 | "Enter your License key for quick, step by step repeater programming instructions.") 1024 | dialogBox = dialogWindow.get_content_area() 1025 | userEntry = Gtk.Entry() 1026 | userEntry.set_size_request(60,12); 1027 | dialogBox.pack_end(userEntry, False, False, 0) 1028 | dialogWindow.show_all() 1029 | response = dialogWindow.run() 1030 | licenseKey = userEntry.get_text() 1031 | dialogWindow.destroy() 1032 | if len(licenseKey): 1033 | self.settingsDialog.config['DownloadOptions']['licenseKEY'] = licenseKey 1034 | self.settingsDialog.writeSettings() 1035 | self.downloadBackground() 1036 | 1037 | def on_button_press(self, osm, event): 1038 | state = event.get_state() 1039 | lat,lon = self.osm.get_event_location(event).get_degrees() 1040 | print('clicked %s,%s' % (lat,lon)) 1041 | near = 999999 1042 | nearest = None 1043 | self.allrepeaters = sorted(self.allrepeaters, key = lambda repeater : repeater.distance(lat,lon)) 1044 | cnt = 0 1045 | return 1046 | for item in self.allrepeaters: 1047 | distance = item.distance(lat,lon) 1048 | self.addToList(item, lat,lon) 1049 | print(distance) 1050 | cnt += 1 1051 | if cnt > 5: 1052 | break 1053 | self.listbox.show_all() 1054 | 1055 | left = event.button == 1 and state == 0 1056 | middle = event.button == 2 or (event.button == 1 and state & Gdk.ModifierType.SHIFT_MASK) 1057 | right = event.button == 3 or (event.button == 1 and state & Gdk.ModifierType.CONTROL_MASK) 1058 | 1059 | #work around binding bug with invalid variable name 1060 | GDK_2BUTTON_PRESS = getattr(Gdk.EventType, "2BUTTON_PRESS") 1061 | GDK_3BUTTON_PRESS = getattr(Gdk.EventType, "3BUTTON_PRESS") 1062 | 1063 | if event.type == GDK_3BUTTON_PRESS: 1064 | if middle: 1065 | if self.last_image is not None: 1066 | self.osm.image_remove(self.last_image) 1067 | self.last_image = None 1068 | elif event.type == GDK_2BUTTON_PRESS: 1069 | if left: 1070 | self.osm.gps_add(lat, lon, heading=random.random()*360) 1071 | if middle: 1072 | pb = GdkPixbuf.Pixbuf.new_from_file_at_size ("poi.png", 24,24) 1073 | self.last_image = self.osm.image_add(lat,lon,pb) 1074 | if right: 1075 | pass 1076 | 1077 | 1078 | def playRTLSDR(self, mhz): 1079 | if self.rtllistener: 1080 | self.rtllistener.proc.kill() 1081 | # -l 450 is higher squelch. 1082 | cmd = 'rtl_fm -M fm -f '+mhz+'M -l 450 | play -r 24k -t raw -e s -b 16 -c 1 -V1 -' 1083 | print(cmd) 1084 | self.rtllistener = RTLSDRRun( cmd ) 1085 | self.rtllistener.start() 1086 | 1087 | def cleanup(self, obj): 1088 | stateObj = { 1089 | 'lat': self.renderedLat, 1090 | 'lon': self.renderedLon, 1091 | 'zoom': self.osm.props.zoom 1092 | } 1093 | with open(userFile('lastPosition.json'), 'w') as outfile: 1094 | outfile.write(json.dumps(stateObj)) 1095 | if self.rtllistener: 1096 | self.rtllistener.proc.kill() 1097 | Gtk.main_quit() 1098 | 1099 | 1100 | if __name__ == "__main__": 1101 | u = UI() 1102 | u.show_all() 1103 | u.setViews() 1104 | if os.name == "nt": Gdk.threads_enter() 1105 | Gtk.main() 1106 | if os.name == "nt": Gdk.threads_leave() 1107 | -------------------------------------------------------------------------------- /src/signaltower.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | Signal Tower 39 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | Signal Tower 50 | 52 | 53 | 54 | Josh "Cheeseness" Bush 55 | 56 | 57 | Originally created for the official shirt for the 2014 Open Source Developers' Conference held at Griffith University. 58 | 59 | 60 | signal tower radio transmission transmit 61 | 62 | 63 | 2014-10-22 64 | 65 | 67 | 69 | 71 | 73 | 74 | 75 | 76 | 80 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/signaltowerdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | Signal Tower 39 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | Signal Tower 50 | 52 | 53 | 54 | Josh "Cheeseness" Bush 55 | 56 | 57 | Originally created for the official shirt for the 2014 Open Source Developers' Conference held at Griffith University. 58 | 59 | 60 | signal tower radio transmission transmit 61 | 62 | 63 | 2014-10-22 64 | 65 | 67 | 69 | 71 | 73 | 74 | 75 | 76 | 81 | 82 | --------------------------------------------------------------------------------