├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs └── images │ ├── example.png │ └── graph.png ├── examples ├── 1_build_graph_from_shapefiles.py ├── 2_calculate_shortest_distance.py ├── 3_plot_path_on_interactive_map.py └── README.md ├── pyvisgraph ├── __init__.py ├── graph.py ├── shortest_path.py ├── vis_graph.py └── visible_vertices.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── test_deploy.py └── test_pvg.py └── tox.ini /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved. 5 | 6 | ## Using the issue tracker 7 | 8 | The issue tracker is the preferred channel for bug reports, features requests 9 | and submitting pull requests. 10 | 11 | ### Bug reports 12 | 13 | A bug is a demonstrable problem that is caused by the code in the repository. Good bug 14 | reports are extremely helpful - thank you! 15 | 16 | A good bug report shouldn't leave others needing to chase you up for more information. 17 | Please try to be as detailed as possible in your report. What is your environment? What 18 | steps will reproduce the issue? What OS experience the problem? What would you expect 19 | to be the outcome? All these details will help people to fix any potential bugs. 20 | 21 | Example: 22 | 23 | > Short and descriptive example bug report title 24 | > 25 | > A summary of the issue and the OS environment in which it occurs. If suitable, include the steps required to reproduce the bug. 26 | > 27 | > This is the first step 28 | > This is the second step 29 | > Further steps, etc. 30 | > Any data, like the relevant `Point`s and so on 31 | > 32 | > Any other information you want to share that is relevant to the issue being reported. This might include the lines of code that you have identified as causing the bug, and potential solutions (and your opinions on their merits). 33 | 34 | Below is an actual (slightly modified) example submitted by @tjansson60: 35 | 36 | > Floating point representation error in angle2() 37 | > 38 | > While processing the shoreline set GSHHS_c_L1 with 742 shapes a 39 | > problem was encountered where cos_value = 1.00000000000048 which meant 40 | > that the acos raised an ValueError: math domain error and the graph 41 | > could not be built. 42 | > 43 | > Examples of values in angle2 when problem was first encountered: 44 | > point_a = (112.96, -25.49) 45 | > point_b = (45.00, -25.49) 46 | > point_c = (45.00, -25.49) 47 | > a = 6.938890000001394e-07 48 | > b = 4618.002849783456 49 | > c = 4618.11606498842 50 | > cos_value = 1.00000000000048 51 | > 52 | > A possible solution is to check if the arguments are very close 53 | > to -1 or 1 and returns the expected values. If the values are not 54 | > close to either of these values the ValueError is still raised. 55 | 56 | ### Feature requests 57 | Feature requests are welcome. But take a moment to find out whether your idea fits 58 | with the scope and aims of the project. It's up to you to make a strong case to convince 59 | the project's developers of the merits of this feature. Please provide as much detail 60 | and context as possible. 61 | 62 | ### Pull requests 63 | Good pull requests - patches, improvements, new features - are a fantastic help. They 64 | should remain focused in scope and avoid containing unrelated commits. 65 | 66 | Please ask first before embarking on any significant pull request (e.g. implementing 67 | features, refactoring code, porting to a different language), otherwise you risk spending 68 | a lot of time working on something that the project's developers might not want to merge 69 | into the project. 70 | 71 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to license 72 | your work under the same license as that used by the project. 73 | 74 | ## Commit message rules 75 | 1. Separate subject from body with a blank line 76 | 2. Limit the subject line to 50 characters 77 | 3. Use the imperative mood in the subject line (i.e. `Fix bug xxx`, not `Fixed bug xxx`) 78 | 4. Use the body to explain *what* and *why* vs. *how* 79 | 5. Add references to GitHub issues/PRs at the bottom 80 | 81 | Example commit message: 82 | 83 | ``` 84 | Summarize changes in around 50 characters or less 85 | 86 | More detailed explanatory text, if necessary.In some contexts, the 87 | first line is treated as the subject of the commit and the rest of 88 | the text as the body. The blank line separating the summary from 89 | the body is critical (unless you omit the body entirely); various 90 | tools like `log`, `shortlog` and `rebase` can get confused if you 91 | run the two together. 92 | 93 | Explain the problem that this commit is solving. Focus on why you 94 | are making this change as opposed to how (the code explains that). 95 | Are there side effects or other unintuitive consequences of this 96 | change? Here's the place to explain them. 97 | 98 | Further paragraphs come after blank lines. 99 | 100 | - Bullet points are okay, too 101 | 102 | - Typically a hyphen or asterisk is used for the bullet, preceded 103 | by a single space, with blank lines in between 104 | 105 | References to specific GitHub issues/PRs should be added like below. 106 | When you then view the commit on GitHub, there will automatically 107 | be links to the issues/PRs. Note that when using the 'Resolves' 108 | reference, GitHub will automatically close the referenced issue. 109 | 110 | Resolves: #123 111 | See also: #456, #789 112 | ``` 113 | 114 | In many cases a commit will not require both a subject and a body. Sometimes a single line is fine, especially 115 | when the change is so simple that no further context is necessary. For example: 116 | 117 | ``` 118 | Fix typo in README.MD 119 | ``` 120 | 121 | See: [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) 122 | 123 | ## Git branch naming conventions 124 | 125 | Git branches should be named as follows: `/` 126 | 127 | ### type 128 | 129 | ``` 130 | bug - Code changes linked to a known issue. 131 | ft - New feature. 132 | ``` 133 | 134 | ### name 135 | 136 | Always use dashes to seperate words, and keep it short. 137 | 138 | ### Examples 139 | 140 | ``` 141 | ft/progressbar 142 | bug/angle2-floatingpoint 143 | ``` 144 | 145 | ## Creating a Fork 146 | 147 | Just head over to the [Pyvisgraph](https://github.com/TaipanRex/pyvisgraph) GitHub page 148 | and click the "Fork" button. Once you've done that, you need to clone the forked repo to your local machine: 149 | 150 | ```shell 151 | git clone git@github.com:USERNAME/pyvisgraph.git 152 | ``` 153 | 154 | ## Keeping Your Fork Up to Date 155 | 156 | While this isn't an absolutely necessary step, if you plan on doing anything more than just a 157 | tiny quick fix, you'll want to make sure you keep your fork up to date by tracking the 158 | original "upstream" repo that you forked. To do this, you'll need to add a remote: 159 | 160 | ```shell 161 | # Add 'upstream' repo to list of remotes 162 | git remote add upstream https://github.com/TaipanRex/pyvisgraph.git 163 | 164 | # Verify the new remote named 'upstream' 165 | git remote -v 166 | ``` 167 | 168 | Whenever you want to update your fork with the latest upstream changes, you'll need to first 169 | fetch the upstream repo's branches and latest commits to bring them into your repository: 170 | 171 | ```shell 172 | # Fetch from upstream remote 173 | git fetch upstream 174 | 175 | # View all branches, including those from upstream 176 | git branch -va 177 | ``` 178 | 179 | Now, checkout your own master branch and merge the upstream repo's master branch: 180 | 181 | ```shell 182 | # Checkout your master branch and merge upstream 183 | git checkout master 184 | git merge upstream/master 185 | ``` 186 | 187 | If there are no unique commits on the local master branch, git will simply perform a fast-forward. 188 | However, if you have been making changes on master (in the vast majority of cases you probably 189 | shouldn't be - [see the next section](#doing-your-work), you may have to deal with conflicts. 190 | When doing so, be careful to respect the changes made upstream. 191 | 192 | Now, your local master branch is up-to-date with everything modified upstream. 193 | 194 | ## Doing Your Work 195 | 196 | ### Create a Branch 197 | 198 | Whenever you begin work on a new feature or bugfix, it's important that you create a new branch. 199 | Not only is it proper git workflow, but it also keeps your changes organized and separated 200 | from the master branch so that you can easily submit and manage multiple pull requests for 201 | very task you complete. 202 | 203 | To create a new branch and start working on it (note the [branch naming conventions](#git-branch-naming-conventions)): 204 | 205 | ```shell 206 | # Checkout the master branch - you want your new branch to come from master 207 | git checkout master 208 | 209 | # Create a new branch named /, f.ex. ft/newfeature 210 | git branch ft/newfeature 211 | 212 | # Switch to your new branch 213 | git checkout ft/newfeature 214 | ``` 215 | 216 | Now you can start making changes to the code. 217 | 218 | ## Submitting a Pull Request 219 | 220 | ### Cleaning Up Your Work 221 | 222 | Prior to submitting your pull request, you might want to do a few things to clean up your 223 | branch and make it as simple as possible for the original repo's maintainer to test, 224 | accept, and merge your work. 225 | 226 | If any commits have been made to the upstream master branch, you should rebase your 227 | development branch so that merging it will be a simple fast-forward that won't require 228 | any conflict resolution work. 229 | 230 | ```shell 231 | # Fetch upstream master and merge with your repo's master branch 232 | git fetch upstream 233 | git checkout master 234 | git merge upstream/master 235 | 236 | # If there were any new commits, rebase your development branch 237 | git checkout ft/newfeature 238 | git rebase master 239 | ``` 240 | 241 | Now, it may be desirable to squash some of your smaller commits down into a small number 242 | of larger more cohesive commits. You can do this with an interactive rebase: 243 | 244 | ```shell 245 | # Rebase all commits on your development branch 246 | git checkout ft/newfeature 247 | git rebase -i master 248 | ``` 249 | 250 | This will open up a text editor where you can specify which commits to squash. 251 | 252 | ### Submitting 253 | 254 | Once you've committed and pushed all of your changes to GitHub, go to the page for your 255 | fork on GitHub, select your development branch, and click the pull request button. If 256 | you need to make any adjustments to your pull request, just push the updates to GitHub. 257 | Your pull request will automatically track the changes on your development branch and update. 258 | 259 | ## Accepting and merging a Pull Request 260 | 261 | The following sections are written from the perspective of the repository owner who is 262 | handling an incoming pull request. Thus, where the "forker" was referring to the original 263 | repository as upstream, we're now looking at it as the owner of that original repository 264 | and the standard origin remote. 265 | 266 | ### Checking out a Pull Request locally 267 | 268 | ```shell 269 | # Creates a local branch . is the Github PR number. 270 | git fetch origin pull//head: 271 | ``` 272 | 273 | If you want to push the branch to the repo: 274 | 275 | ```shell 276 | git push origin 277 | ``` 278 | 279 | See: [Checking out pull requests locally](https://help.github.com/articles/checking-out-pull-requests-locally/) 280 | 281 | ### Automatically Merging a Pull Request 282 | 283 | In cases where the merge would be a simple fast-forward, you can automatically do the merge by just clicking the 284 | button on the pull request page on GitHub. 285 | 286 | ### Manually Merging a Pull Request 287 | 288 | The target branch must [pulled locally](#checking-out-a-pull-request-locally) first. 289 | 290 | ```shell 291 | # Checkout the branch you're merging to in the target repo 292 | git checkout master 293 | 294 | # Merge the development branch 295 | git merge ft/newfeature 296 | 297 | # Push master with the new feature merged into it 298 | git push origin master 299 | ``` 300 | 301 | ### Cherry picking commits from a Pull Request 302 | 303 | If you only want to merge certain commits from a Pull Request, use `cherry-pick`. Again make sure the 304 | target branch is [pulled locally](#checking-out-a-pull-request-locally) first. 305 | 306 | ```shell 307 | # Make sure you are on the branch you want to apply the commit to. 308 | git checkout master 309 | git cherry-pick 310 | ``` 311 | 312 | See: [What does cherry picking a commit with git mean](https://stackoverflow.com/questions/9339429/what-does-cherry-picking-a-commit-with-git-mean) 313 | 314 | ### Deleting development branches 315 | 316 | When you are done with a development branch, you're free to delete it. If it has been merged, all commit history and 317 | graphical log will be preserved. 318 | 319 | ```shell 320 | git branch -d newfeature 321 | 322 | # If you want to delete a remote branch (this will also close any Pull Request tied to it) 323 | git push -d origin branchname 324 | ``` 325 | 326 | ## Releasing a new version on PyPi 327 | 328 | _Note: with the release of [pypi.org](www.pypi.org), below will become deprecated. [This](https://packaging.python.org/tutorials/packaging-projects/) explains how to update the workflow._ 329 | 330 | Make sure all changes have been commited, we will then make a release commit. Change `setup.py` 331 | in two places with the new version number: `version = 'x.x.x',` and `download_url = 'https://github.com/TaipanRex/pyvisgraph/tarball/x.x.x',`. 332 | 333 | ```shell 334 | git add setup.py 335 | git commit -m "push x.x.x" 336 | # Write release notes in tag annotation. First line should be release number, x.x.x 337 | git tag -a x.x.x 338 | git push origin master 339 | git push --tags origin master 340 | sudo python setup.py sdist upload -r pypi 341 | ``` 342 | 343 | See: [How to submit a package to PyPI](http://peterdowns.com/posts/first-time-with-pypi.html) 344 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christian August Reksten-Monsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include LICENSE.txt 4 | include CONTRIBUTING.md 5 | include setup.py 6 | include setup.cfg 7 | include README.md 8 | include examples/1_build_graph_from_shapefiles.py 9 | include examples/2_calculate_shortest_distance.py 10 | include examples/3_plot_path_on_interactive_map.py 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyvisgraph - Python Visibility Graph 2 | 3 | [![MIT License](https://img.shields.io/github/license/taipanrex/pyvisgraph.svg?style=flat)](/LICENSE.txt) 4 | [![PyPI](https://img.shields.io/pypi/v/pyvisgraph.svg?style=flat)](https://pypi.python.org/pypi/pyvisgraph) 5 | 6 | Given a set of simple obstacle polygons, build a visibility graph and find 7 | the shortest path between two points. 8 | 9 | ![Figure 1](docs/images/graph.png) 10 | 11 | Pyvisgraph is a MIT-licensed Python package for building visibility graphs from 12 | a list of simple obstacle polygons. The visibility graph algorithm (D.T. Lee) 13 | runs in O(n^2 log n) time. The shortest path is found using Djikstra's 14 | algorithm. 15 | 16 | To see how visibility graphs work interactively, take a look at the 17 | [Visibility Graph Simulator](https://github.com/TaipanRex/visgraph_simulator) 18 | built with Pyvisgraph. 19 | 20 | ## Installing Pyvisgraph 21 | ``` 22 | $ pip install pyvisgraph 23 | ``` 24 | Pyvisgraph supports Python 2 and 3. 25 | 26 | ## Usage 27 | Here is an example of building a visibility graph given a list of 28 | simple polygons: 29 | ``` 30 | >>> import pyvisgraph as vg 31 | >>> polys = [[vg.Point(0.0,1.0), vg.Point(3.0,1.0), vg.Point(1.5,4.0)], 32 | >>> [vg.Point(4.0,4.0), vg.Point(7.0,4.0), vg.Point(5.5,8.0)]] 33 | >>> g = vg.VisGraph() 34 | >>> g.build(polys) 35 | >>> shortest = g.shortest_path(vg.Point(1.5,0.0), vg.Point(4.0, 6.0)) 36 | >>> print shortest 37 | [Point(1.50, 0.00), Point(3.00, 1.00), Point(4.00, 6.00)] 38 | ``` 39 | Once the visibility graph is built, it can be saved and subsequently loaded. 40 | This is useful for large graphs where build time is long. `pickle` is used 41 | for saving and loading. 42 | ``` 43 | >>> g.save('graph.pk1') 44 | >>> g2 = VisGraph() 45 | >>> g2.load('graph.pk1') 46 | ``` 47 | For obstacles with a large number of points, Pyvisgraph can take advantage of 48 | processors with multiple cores using the `multiprocessing` module. Simply 49 | add the number of workers (processes) to the `build` method: 50 | ``` 51 | >>> g.build(polys, workers=4) 52 | ``` 53 | Pyvisgraph also has some useful helper functions: 54 | * `g.update([list of Points])`: Updates the visibility graph 55 | by checking visibility of each `Point` in the list. 56 | * `g.point_in_polygon(Point)`: Check if `Point` is in the interior of any of 57 | the obstacle polygons. Returns the polygon_id of said polygon, -1 if not 58 | inside any polygon. 59 | * `g.closest_point(Point, polygon_id)`: Return the closest point outside 60 | polygon with polygon_id from Point. 61 | 62 | For further examples, please look at the scripts provided in the `examples` 63 | folder. 64 | 65 | ## Example & performance 66 | This example uses a shapefile representing world shorelines as obstacles. 67 | Two vessels were picked randomly and their current location found 68 | using [AIS](https://en.wikipedia.org/wiki/Automatic_identification_system). Red 69 | lines are laden voyage legs (carrying cargo) and dotted blue lines are ballast 70 | legs (no cargo, moving to load destination). Pyvisgraph has the following 71 | performance on a Microsoft Surface Pro 3 (Intel i7-4650U @ 1.7Ghz, 8GB DDR3 72 | RAM), where time is in seconds: 73 | ``` 74 | Shoreline obstacle graph [points: 4335 edges: 4335] 75 | Using 4 worker processes... 76 | Time to create visibility graph: 554.683238029 77 | Visibility graph edges: 118532 78 | Time to update visgraph & find shortest path: 1.09287905693 79 | Shorest path nodes: 19 80 | Time to find shortest path between existing points: 0.508340835571 81 | ``` 82 | For one vessel the origin and destination Points were not part of the built 83 | visibility graph and had to first be computed. For the second vessel, these 84 | Points were first added to the visibility graph using `update`, then finding 85 | the shortest path is faster. Using Matplotlib basemap to visualize the routes: 86 | ![Figure 2](docs/images/example.png) 87 | 88 | For more information about the implementation, see these series of articles: 89 | * [Distance Tables Part 1: Defining the Problem](https://taipanrex.github.io/2016/09/17/Distance-Tables-Part-1-Defining-the-Problem.html) 90 | * [Distance Tables Part 2: Lee's Visibility Graph Algorithm](https://taipanrex.github.io/2016/10/19/Distance-Tables-Part-2-Lees-Visibility-Graph-Algorithm.html) 91 | * More to come... 92 | -------------------------------------------------------------------------------- /docs/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaipanRex/pyvisgraph/9472e0df7ca7bfa5f45932ca348222bdc1f7e688/docs/images/example.png -------------------------------------------------------------------------------- /docs/images/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaipanRex/pyvisgraph/9472e0df7ca7bfa5f45932ca348222bdc1f7e688/docs/images/graph.png -------------------------------------------------------------------------------- /examples/1_build_graph_from_shapefiles.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | import pyvisgraph as vg 25 | import shapefile 26 | 27 | # In this example a visibility graph will be created from the GSHHS shorelines 28 | # available at: https://www.ngdc.noaa.gov/mgg/shorelines/gshhs.html 29 | # 1. Download the shorelines as shape files and select the wanted resolution 30 | # and level (in this example we use Crude, L1). 31 | # 2. Copy all the related files (shx, shp, prj, dbf) to the folder that contains 32 | # this script. 33 | 34 | input_shapefile = shapefile.Reader('GSHHS_c_L1') 35 | output_graphfile = 'GSHHS_c_L1.graph' 36 | # Number of CPU cores on host computer. If you don't know how many cores you 37 | # have, use 'cat /proc/cpuinfo | grep processor | wc -l' on Linux. On Windows, 38 | # press Ctrl + Shift + Esc, press Performance tab. Look for 'logical processors'. 39 | workers = 4 40 | 41 | # Get the shoreline shapes from the shape file. Broadly speaking the GSHHS 42 | # shapes correspond to the shorelines of continents, countries and islands. 43 | shapes = input_shapefile.shapes() 44 | print('The shapefile contains {} shapes.'.format(len(shapes))) 45 | 46 | # Create a list of polygons, where each polygon corresponds to a shape 47 | polygons = [] 48 | for shape in shapes: 49 | polygon = [] 50 | for point in shape.points: 51 | polygon.append(vg.Point(point[0], point[1])) 52 | polygons.append(polygon) 53 | 54 | # Start building the visibility graph 55 | graph = vg.VisGraph() 56 | print('Starting building visibility graph') 57 | graph.build(polygons, workers=workers) 58 | print('Finished building visibility graph') 59 | 60 | # Save the visibility graph to a file 61 | graph.save(output_graphfile) 62 | print('Saved visibility graph to file: {}'.format(output_graphfile)) 63 | -------------------------------------------------------------------------------- /examples/2_calculate_shortest_distance.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | import pyvisgraph as vg 25 | from haversine import haversine 26 | 27 | # In this example we will find the shortest path between two points on a 28 | # sphere, i.e. on earth. To calculate the total distance of that path, we 29 | # need to use the great circle formula. We use the haversine package for this. 30 | 31 | # Example points 32 | start_point = vg.Point(12.568337, 55.676098) # Copenhagen 33 | end_point = vg.Point(103.851959, 1.290270) # Singapore 34 | 35 | # Load the visibility graph file. If you do not have this, please run 36 | # 1_build_graph_from_shapefiles.py first. 37 | graph = vg.VisGraph() 38 | graph.load('GSHHS_c_L1.graph') 39 | 40 | # Get the shortest path 41 | shortest_path = graph.shortest_path(start_point, end_point) 42 | 43 | # Calculate the total distance of the shortest path in km 44 | path_distance = 0 45 | prev_point = shortest_path[0] 46 | for point in shortest_path[1:]: 47 | # Add miles=True to the end of the haversine call to get result in miles 48 | path_distance += haversine((prev_point.y, prev_point.x), (point.y, point.x)) 49 | prev_point = point 50 | # If you want to total distance in nautical miles: 51 | # path_distance = path_distance*0.539957 52 | 53 | print('Shortest path distance: {}'.format(path_distance)) 54 | -------------------------------------------------------------------------------- /examples/3_plot_path_on_interactive_map.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | import pyvisgraph as vg 25 | import folium 26 | 27 | # In this example we will calculate the shortest path between two points 28 | # and plot this on a interactive map, using the folium package. 29 | 30 | # Example points 31 | start_point = vg.Point(12.568337, 55.676098) # Copenhagen 32 | end_point = vg.Point(103.851959, 1.290270) # Singapore 33 | 34 | # Load the visibility graph file If you do not have this, please run 35 | # 1_build_graph_from_shapefiles.py first. 36 | graph = vg.VisGraph() 37 | graph.load('GSHHS_c_L1.graph') 38 | 39 | # Calculate the shortest path 40 | shortest_path = graph.shortest_path(start_point, end_point) 41 | 42 | # Plot of the path using folium 43 | geopath = [[point.y, point.x] for point in shortest_path] 44 | geomap = folium.Map([0, 0], zoom_start=2) 45 | for point in geopath: 46 | folium.Marker(point, popup=str(point)).add_to(geomap) 47 | folium.PolyLine(geopath).add_to(geomap) 48 | 49 | # Add a Mark on the start and positions in a different color 50 | folium.Marker(geopath[0], popup=str(start_point), icon=folium.Icon(color='red')).add_to(geomap) 51 | folium.Marker(geopath[-1], popup=str(end_point), icon=folium.Icon(color='red')).add_to(geomap) 52 | 53 | # Save the interactive plot as a map 54 | output_name = 'example_shortest_path_plot.html' 55 | geomap.save(output_name) 56 | print('Output saved to: {}'.format(output_name)) 57 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | * **1_build_graph_from_shapefiles.py** - Builds a visibility graph from GSHHS shapefiles and stores it in a file. 4 | * **2_calculate_shortest_distance.py** - Calculates and prints the shortest path between to points on earth by sea 5 | using the GSHHS graphfile. 6 | * **3_plot_path_on_interactive_map.py** - Plots the shortest path on a interactive map saved as HTML using the GSHHS 7 | visibility graph. 8 | -------------------------------------------------------------------------------- /pyvisgraph/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from pyvisgraph.graph import Point, Edge, Graph 3 | from pyvisgraph.vis_graph import VisGraph 4 | -------------------------------------------------------------------------------- /pyvisgraph/graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | from collections import defaultdict 25 | 26 | 27 | class Point(object): 28 | __slots__ = ('x', 'y', 'polygon_id') 29 | 30 | def __init__(self, x, y, polygon_id=-1): 31 | self.x = float(x) 32 | self.y = float(y) 33 | self.polygon_id = polygon_id 34 | 35 | def __eq__(self, point): 36 | return point and self.x == point.x and self.y == point.y 37 | 38 | def __ne__(self, point): 39 | return not self.__eq__(point) 40 | 41 | def __lt__(self, point): 42 | """ This is only needed for shortest path calculations where heapq is 43 | used. When there are two points of equal distance, heapq will 44 | instead evaluate the Points, which doesnt work in Python 3 and 45 | throw a TypeError.""" 46 | return hash(self) < hash(point) 47 | 48 | def __str__(self): 49 | return "(%.2f, %.2f)" % (self.x, self.y) 50 | 51 | def __hash__(self): 52 | return self.x.__hash__() ^ self.y.__hash__() 53 | 54 | def __repr__(self): 55 | return "Point(%.2f, %.2f)" % (self.x, self.y) 56 | 57 | 58 | class Edge(object): 59 | __slots__ = ('p1', 'p2') 60 | 61 | def __init__(self, point1, point2): 62 | self.p1 = point1 63 | self.p2 = point2 64 | 65 | def get_adjacent(self, point): 66 | if point == self.p1: 67 | return self.p2 68 | return self.p1 69 | 70 | def __contains__(self, point): 71 | return self.p1 == point or self.p2 == point 72 | 73 | def __eq__(self, edge): 74 | if self.p1 == edge.p1 and self.p2 == edge.p2: 75 | return True 76 | if self.p1 == edge.p2 and self.p2 == edge.p1: 77 | return True 78 | return False 79 | 80 | def __ne__(self, edge): 81 | return not self.__eq__(edge) 82 | 83 | def __str__(self): 84 | return "({}, {})".format(self.p1, self.p2) 85 | 86 | def __repr__(self): 87 | return "Edge({!r}, {!r})".format(self.p1, self.p2) 88 | 89 | def __hash__(self): 90 | return self.p1.__hash__() ^ self.p2.__hash__() 91 | 92 | 93 | class Graph(object): 94 | """ 95 | A Graph is represented by a dict where the keys are Points in the Graph 96 | and the dict values are sets containing Edges incident on each Point. 97 | A separate set *edges* contains all Edges in the graph. 98 | 99 | The input must be a list of polygons, where each polygon is a list of 100 | in-order (clockwise or counter clockwise) Points. If only one polygon, 101 | it must still be a list in a list, i.e. [[Point(0,0), Point(2,0), 102 | Point(2,1)]]. 103 | 104 | *polygons* dictionary: key is a integer polygon ID and values are the 105 | edges that make up the polygon. Note only polygons with 3 or more Points 106 | will be classified as a polygon. Non-polygons like just one Point will be 107 | given a polygon ID of -1 and not maintained in the dict. 108 | """ 109 | 110 | def __init__(self, polygons): 111 | self.graph = defaultdict(set) 112 | self.edges = set() 113 | self.polygons = defaultdict(set) 114 | pid = 0 115 | for polygon in polygons: 116 | if polygon[0] == polygon[-1] and len(polygon) > 1: 117 | polygon.pop() 118 | for i, point in enumerate(polygon): 119 | sibling_point = polygon[(i + 1) % len(polygon)] 120 | edge = Edge(point, sibling_point) 121 | if len(polygon) > 2: 122 | point.polygon_id = pid 123 | sibling_point.polygon_id = pid 124 | self.polygons[pid].add(edge) 125 | self.add_edge(edge) 126 | if len(polygon) > 2: 127 | pid += 1 128 | 129 | def get_adjacent_points(self, point): 130 | return [edge.get_adjacent(point) for edge in self[point]] 131 | 132 | def get_points(self): 133 | return list(self.graph) 134 | 135 | def get_edges(self): 136 | return self.edges 137 | 138 | def add_edge(self, edge): 139 | self.graph[edge.p1].add(edge) 140 | self.graph[edge.p2].add(edge) 141 | self.edges.add(edge) 142 | 143 | def __contains__(self, item): 144 | if isinstance(item, Point): 145 | return item in self.graph 146 | if isinstance(item, Edge): 147 | return item in self.edges 148 | return False 149 | 150 | def __getitem__(self, point): 151 | if point in self.graph: 152 | return self.graph[point] 153 | return set() 154 | 155 | def __str__(self): 156 | res = "" 157 | for point in self.graph: 158 | res += "\n" + str(point) + ": " 159 | for edge in self.graph[point]: 160 | res += str(edge) 161 | return res 162 | 163 | def __repr__(self): 164 | return self.__str__() 165 | -------------------------------------------------------------------------------- /pyvisgraph/shortest_path.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | from heapq import heapify, heappush, heappop 25 | from pyvisgraph.visible_vertices import edge_distance 26 | 27 | try: 28 | dict.iteritems 29 | except AttributeError: 30 | # Python 3 31 | def iteritems(d): 32 | return iter(d.items()) 33 | else: 34 | # Python 2 35 | def iteritems(d): 36 | return d.iteritems() 37 | 38 | 39 | def dijkstra(graph, origin, destination, add_to_visgraph): 40 | D = {} 41 | P = {} 42 | Q = priority_dict() 43 | Q[origin] = 0 44 | 45 | for v in Q: 46 | D[v] = Q[v] 47 | if v == destination: break 48 | 49 | edges = graph[v] 50 | if add_to_visgraph != None and len(add_to_visgraph[v]) > 0: 51 | edges = add_to_visgraph[v] | graph[v] 52 | for e in edges: 53 | w = e.get_adjacent(v) 54 | elength = D[v] + edge_distance(v, w) 55 | if w in D: 56 | if elength < D[w]: 57 | raise ValueError 58 | elif w not in Q or elength < Q[w]: 59 | Q[w] = elength 60 | P[w] = v 61 | return (D, P) 62 | 63 | 64 | def shortest_path(graph, origin, destination, add_to_visgraph=None): 65 | D, P = dijkstra(graph, origin, destination, add_to_visgraph) 66 | path = [] 67 | while 1: 68 | path.append(destination) 69 | if destination == origin: break 70 | destination = P[destination] 71 | path.reverse() 72 | return path 73 | 74 | 75 | class priority_dict(dict): 76 | """Dictionary that can be used as a priority queue. 77 | 78 | Keys of the dictionary are items to be put into the queue, and values 79 | are their respective priorities. All dictionary methods work as expected. 80 | The advantage over a standard heapq-based priority queue is that priorities 81 | of items can be efficiently updated (amortized O(1)) using code as 82 | 'thedict[item] = new_priority.' 83 | 84 | Note that this is a modified version of 85 | https://gist.github.com/matteodellamico/4451520 where sorted_iter() has 86 | been replaced with the destructive sorted iterator __iter__ from 87 | https://gist.github.com/anonymous/4435950 88 | """ 89 | def __init__(self, *args, **kwargs): 90 | super(priority_dict, self).__init__(*args, **kwargs) 91 | self._rebuild_heap() 92 | 93 | def _rebuild_heap(self): 94 | self._heap = [(v, k) for k, v in iteritems(self)] 95 | heapify(self._heap) 96 | 97 | def smallest(self): 98 | heap = self._heap 99 | v, k = heap[0] 100 | while k not in self or self[k] != v: 101 | heappop(heap) 102 | v, k = heap[0] 103 | return k 104 | 105 | def pop_smallest(self): 106 | heap = self._heap 107 | v, k = heappop(heap) 108 | while k not in self or self[k] != v: 109 | v, k = heappop(heap) 110 | del self[k] 111 | return k 112 | 113 | def __setitem__(self, key, val): 114 | super(priority_dict, self).__setitem__(key, val) 115 | 116 | if len(self._heap) < 2 * len(self): 117 | heappush(self._heap, (val, key)) 118 | else: 119 | self._rebuild_heap() 120 | 121 | def setdefault(self, key, val): 122 | if key not in self: 123 | self[key] = val 124 | return val 125 | return self[key] 126 | 127 | def update(self, *args, **kwargs): 128 | super(priority_dict, self).update(*args, **kwargs) 129 | self._rebuild_heap() 130 | 131 | def __iter__(self): 132 | def iterfn(): 133 | while len(self) > 0: 134 | x = self.smallest() 135 | yield x 136 | del self[x] 137 | return iterfn() 138 | -------------------------------------------------------------------------------- /pyvisgraph/vis_graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | from timeit import default_timer 25 | from sys import stdout, version_info 26 | from multiprocessing import Pool 27 | from tqdm import tqdm 28 | from warnings import warn 29 | 30 | from pyvisgraph.graph import Graph, Edge 31 | from pyvisgraph.shortest_path import shortest_path 32 | from pyvisgraph.visible_vertices import visible_vertices, point_in_polygon 33 | from pyvisgraph.visible_vertices import closest_point 34 | 35 | PYTHON3 = version_info[0] == 3 36 | if PYTHON3: 37 | xrange = range 38 | import pickle 39 | else: 40 | import cPickle as pickle 41 | 42 | 43 | class VisGraph(object): 44 | 45 | def __init__(self): 46 | self.graph = None 47 | self.visgraph = None 48 | 49 | def load(self, filename): 50 | """Load obstacle graph and visibility graph. """ 51 | with open(filename, 'rb') as load: 52 | self.graph, self.visgraph = pickle.load(load) 53 | 54 | def save(self, filename): 55 | """Save obstacle graph and visibility graph. """ 56 | with open(filename, 'wb') as output: 57 | pickle.dump((self.graph, self.visgraph), output, -1) 58 | 59 | def build(self, input, workers=1, status=True): 60 | """Build visibility graph based on a list of polygons. 61 | 62 | The input must be a list of polygons, where each polygon is a list of 63 | in-order (clockwise or counter clockwise) Points. It only one polygon, 64 | it must still be a list in a list, i.e. [[Point(0,0), Point(2,0), 65 | Point(2,1)]]. 66 | Take advantage of processors with multiple cores by setting workers to 67 | the number of subprocesses you want. Defaults to 1, i.e. no subprocess 68 | will be started. 69 | Set status=False to turn off the statusbar when building. 70 | """ 71 | 72 | self.graph = Graph(input) 73 | self.visgraph = Graph([]) 74 | 75 | points = self.graph.get_points() 76 | batch_size = 10 77 | 78 | if workers == 1: 79 | for batch in tqdm([points[i:i + batch_size] 80 | for i in xrange(0, len(points), batch_size)], 81 | disable=not status): 82 | for edge in _vis_graph(self.graph, batch): 83 | self.visgraph.add_edge(edge) 84 | else: 85 | pool = Pool(workers) 86 | batches = [(self.graph, points[i:i + batch_size]) 87 | for i in xrange(0, len(points), batch_size)] 88 | 89 | results = list(tqdm(pool.imap(_vis_graph_wrapper, batches), total=len(batches), 90 | disable=not status)) 91 | for result in results: 92 | for edge in result: 93 | self.visgraph.add_edge(edge) 94 | 95 | def find_visible(self, point): 96 | """Find vertices visible from point.""" 97 | 98 | return visible_vertices(point, self.graph) 99 | 100 | def update(self, points, origin=None, destination=None): 101 | """Update visgraph by checking visibility of Points in list points.""" 102 | 103 | for p in points: 104 | for v in visible_vertices(p, self.graph, origin=origin, 105 | destination=destination): 106 | self.visgraph.add_edge(Edge(p, v)) 107 | 108 | def shortest_path(self, origin, destination): 109 | """Find and return shortest path between origin and destination. 110 | 111 | Will return in-order list of Points of the shortest path found. If 112 | origin or destination are not in the visibility graph, their respective 113 | visibility edges will be found, but only kept temporarily for finding 114 | the shortest path. 115 | """ 116 | 117 | origin_exists = origin in self.visgraph 118 | dest_exists = destination in self.visgraph 119 | if origin_exists and dest_exists: 120 | return shortest_path(self.visgraph, origin, destination) 121 | orgn = None if origin_exists else origin 122 | dest = None if dest_exists else destination 123 | add_to_visg = Graph([]) 124 | if not origin_exists: 125 | for v in visible_vertices(origin, self.graph, destination=dest): 126 | add_to_visg.add_edge(Edge(origin, v)) 127 | if not dest_exists: 128 | for v in visible_vertices(destination, self.graph, origin=orgn): 129 | add_to_visg.add_edge(Edge(destination, v)) 130 | return shortest_path(self.visgraph, origin, destination, add_to_visg) 131 | 132 | def point_in_polygon(self, point): 133 | """Return polygon_id if point in a polygon, -1 otherwise.""" 134 | 135 | return point_in_polygon(point, self.graph) 136 | 137 | def closest_point(self, point, polygon_id, length=0.001): 138 | """Return closest Point outside polygon from point. 139 | 140 | Note method assumes point is inside the polygon, no check is 141 | performed. 142 | """ 143 | 144 | return closest_point(point, self.graph, polygon_id, length) 145 | 146 | 147 | def _vis_graph_wrapper(args): 148 | try: 149 | return _vis_graph(*args) 150 | except KeyboardInterrupt: 151 | pass 152 | 153 | def _vis_graph(graph, points): 154 | visible_edges = [] 155 | for p1 in points: 156 | for p2 in visible_vertices(p1, graph, scan='half'): 157 | visible_edges.append(Edge(p1, p2)) 158 | return visible_edges 159 | -------------------------------------------------------------------------------- /pyvisgraph/visible_vertices.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | from __future__ import division 25 | from math import pi, sqrt, atan, acos 26 | from pyvisgraph.graph import Point 27 | 28 | INF = 10000 29 | CCW = 1 30 | CW = -1 31 | COLLINEAR = 0 32 | """Due to floating point representation error, some functions need to 33 | truncate floating point numbers to a certain tolerance.""" 34 | COLIN_TOLERANCE = 10 35 | T = 10**COLIN_TOLERANCE 36 | T2 = 10.0**COLIN_TOLERANCE 37 | 38 | def visible_vertices(point, graph, origin=None, destination=None, scan='full'): 39 | """Returns list of Points in graph visible by point. 40 | 41 | If origin and/or destination Points are given, these will also be checked 42 | for visibility. scan 'full' will check for visibility against all points in 43 | graph, 'half' will check for visibility against half the points. This saves 44 | running time when building a complete visibility graph, as the points 45 | that are not checked will eventually be 'point'. 46 | """ 47 | edges = graph.get_edges() 48 | points = graph.get_points() 49 | if origin: points.append(origin) 50 | if destination: points.append(destination) 51 | points.sort(key=lambda p: (angle(point, p), edge_distance(point, p))) 52 | 53 | # Initialize open_edges with any intersecting edges on the half line from 54 | # point along the positive x-axis 55 | open_edges = OpenEdges() 56 | point_inf = Point(INF, point.y) 57 | for edge in edges: 58 | if point in edge: continue 59 | if edge_intersect(point, point_inf, edge): 60 | if on_segment(point, edge.p1, point_inf): continue 61 | if on_segment(point, edge.p2, point_inf): continue 62 | open_edges.insert(point, point_inf, edge) 63 | 64 | visible = [] 65 | prev = None 66 | prev_visible = None 67 | for p in points: 68 | if p == point: continue 69 | if scan == 'half' and angle(point, p) > pi: break 70 | 71 | # Update open_edges - remove clock wise edges incident on p 72 | if open_edges: 73 | for edge in graph[p]: 74 | if ccw(point, p, edge.get_adjacent(p)) == CW: 75 | open_edges.delete(point, p, edge) 76 | 77 | # Check if p is visible from point 78 | is_visible = False 79 | # ...Non-collinear points 80 | if prev is None or ccw(point, prev, p) != COLLINEAR or not on_segment(point, prev, p): 81 | if len(open_edges) == 0: 82 | is_visible = True 83 | elif not edge_intersect(point, p, open_edges.smallest()): 84 | is_visible = True 85 | # ...For collinear points, if previous point was not visible, p is not 86 | elif not prev_visible: 87 | is_visible = False 88 | # ...For collinear points, if previous point was visible, need to check 89 | # that the edge from prev to p does not intersect any open edge. 90 | else: 91 | is_visible = True 92 | for edge in open_edges: 93 | if prev not in edge and edge_intersect(prev, p, edge): 94 | is_visible = False 95 | break 96 | if is_visible and edge_in_polygon(prev, p, graph): 97 | is_visible = False 98 | 99 | # Check if the visible edge is interior to its polygon 100 | if is_visible and p not in graph.get_adjacent_points(point): 101 | is_visible = not edge_in_polygon(point, p, graph) 102 | 103 | if is_visible: visible.append(p) 104 | 105 | # Update open_edges - Add counter clock wise edges incident on p 106 | for edge in graph[p]: 107 | if (point not in edge) and ccw(point, p, edge.get_adjacent(p)) == CCW: 108 | open_edges.insert(point, p, edge) 109 | 110 | prev = p 111 | prev_visible = is_visible 112 | return visible 113 | 114 | 115 | def polygon_crossing(p1, poly_edges): 116 | """Returns True if Point p1 is internal to the polygon. The polygon is 117 | defined by the Edges in poly_edges. Uses crossings algorithm and takes into 118 | account edges that are collinear to p1.""" 119 | p2 = Point(INF, p1.y) 120 | intersect_count = 0 121 | for edge in poly_edges: 122 | if p1.y < edge.p1.y and p1.y < edge.p2.y: continue 123 | if p1.y > edge.p1.y and p1.y > edge.p2.y: continue 124 | if p1.x > edge.p1.x and p1.x > edge.p2.x: continue 125 | # Deal with points collinear to p1 126 | edge_p1_collinear = (ccw(p1, edge.p1, p2) == COLLINEAR) 127 | edge_p2_collinear = (ccw(p1, edge.p2, p2) == COLLINEAR) 128 | if edge_p1_collinear and edge_p2_collinear: continue 129 | if edge_p1_collinear or edge_p2_collinear: 130 | collinear_point = edge.p1 if edge_p1_collinear else edge.p2 131 | if edge.get_adjacent(collinear_point).y > p1.y: 132 | intersect_count += 1 133 | elif edge_intersect(p1, p2, edge): 134 | intersect_count += 1 135 | if intersect_count % 2 == 0: 136 | return False 137 | return True 138 | 139 | 140 | def edge_in_polygon(p1, p2, graph): 141 | """Return true if the edge from p1 to p2 is interior to any polygon 142 | in graph.""" 143 | if p1.polygon_id != p2.polygon_id: 144 | return False 145 | if p1.polygon_id == -1 or p2.polygon_id == -1: 146 | return False 147 | mid_point = Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2) 148 | return polygon_crossing(mid_point, graph.polygons[p1.polygon_id]) 149 | 150 | 151 | def point_in_polygon(p, graph): 152 | """Return true if the point p is interior to any polygon in graph.""" 153 | for polygon in graph.polygons: 154 | if polygon_crossing(p, graph.polygons[polygon]): 155 | return polygon 156 | return -1 157 | 158 | 159 | def unit_vector(c, p): 160 | magnitude = edge_distance(c, p) 161 | return Point((p.x - c.x) / magnitude, (p.y - c.y) / magnitude) 162 | 163 | 164 | def closest_point(p, graph, polygon_id, length=0.001): 165 | """Assumes p is interior to the polygon with polygon_id. Returns the 166 | closest point c outside the polygon to p, where the distance from c to 167 | the intersect point from p to the edge of the polygon is length.""" 168 | polygon_edges = graph.polygons[polygon_id] 169 | close_point = None 170 | close_edge = None 171 | close_dist = None 172 | # Finds point closest to p, but on a edge of the polygon. 173 | # Solution from http://stackoverflow.com/a/6177788/4896361 174 | for i, e in enumerate(polygon_edges): 175 | num = ((p.x-e.p1.x)*(e.p2.x-e.p1.x) + (p.y-e.p1.y)*(e.p2.y-e.p1.y)) 176 | denom = ((e.p2.x - e.p1.x)**2 + (e.p2.y - e.p1.y)**2) 177 | u = num/denom 178 | pu = Point(e.p1.x + u*(e.p2.x - e.p1.x), e.p1.y + u*(e.p2.y- e.p1.y)) 179 | pc = pu 180 | if u < 0: 181 | pc = e.p1 182 | elif u > 1: 183 | pc = e.p2 184 | d = edge_distance(p, pc) 185 | if i == 0 or d < close_dist: 186 | close_dist = d 187 | close_point = pc 188 | close_edge = e 189 | 190 | # Extend the newly found point so it is outside the polygon by `length`. 191 | if close_point in close_edge: 192 | c = close_edge.p1 if close_point == close_edge.p1 else close_edge.p2 193 | edges = list(graph[c]) 194 | v1 = unit_vector(c, edges[0].get_adjacent(c)) 195 | v2 = unit_vector(c, edges[1].get_adjacent(c)) 196 | vsum = unit_vector(Point(0, 0), Point(v1.x + v2.x, v1.y + v2.y)) 197 | close1 = Point(c.x + (vsum.x * length), c.y + (vsum.y * length)) 198 | close2 = Point(c.x - (vsum.x * length), c.y - (vsum.y * length)) 199 | if point_in_polygon(close1, graph) == -1: 200 | return close1 201 | return close2 202 | else: 203 | v = unit_vector(p, close_point) 204 | return Point(close_point.x + v.x*length, close_point.y + v.y*length) 205 | 206 | 207 | def edge_distance(p1, p2): 208 | """Return the Euclidean distance between two Points.""" 209 | return sqrt((p2.x - p1.x)**2 + (p2.y - p1.y)**2) 210 | 211 | 212 | def intersect_point(p1, p2, edge): 213 | """Return intersect Point where the edge from p1, p2 intersects edge""" 214 | if p1 in edge: return p1 215 | if p2 in edge: return p2 216 | if edge.p1.x == edge.p2.x: 217 | if p1.x == p2.x: 218 | return None 219 | pslope = (p1.y - p2.y) / (p1.x - p2.x) 220 | intersect_x = edge.p1.x 221 | intersect_y = pslope * (intersect_x - p1.x) + p1.y 222 | return Point(intersect_x, intersect_y) 223 | 224 | if p1.x == p2.x: 225 | eslope = (edge.p1.y - edge.p2.y) / (edge.p1.x - edge.p2.x) 226 | intersect_x = p1.x 227 | intersect_y = eslope * (intersect_x - edge.p1.x) + edge.p1.y 228 | return Point(intersect_x, intersect_y) 229 | 230 | pslope = (p1.y - p2.y) / (p1.x - p2.x) 231 | eslope = (edge.p1.y - edge.p2.y) / (edge.p1.x - edge.p2.x) 232 | if eslope == pslope: 233 | return None 234 | intersect_x = (eslope * edge.p1.x - pslope * p1.x + p1.y - edge.p1.y) / (eslope - pslope) 235 | intersect_y = eslope * (intersect_x - edge.p1.x) + edge.p1.y 236 | return Point(intersect_x, intersect_y) 237 | 238 | 239 | def point_edge_distance(p1, p2, edge): 240 | """Return the Eucledian distance from p1 to intersect point with edge. 241 | Assumes the line going from p1 to p2 intersects edge before reaching p2.""" 242 | ip = intersect_point(p1, p2, edge) 243 | if ip is not None: 244 | return edge_distance(p1, ip) 245 | return 0 246 | 247 | 248 | def angle(center, point): 249 | """Return the angle (radian) of point from center of the radian circle. 250 | ------p 251 | | / 252 | | / 253 | c|a/ 254 | """ 255 | dx = point.x - center.x 256 | dy = point.y - center.y 257 | if dx == 0: 258 | if dy < 0: 259 | return pi * 3 / 2 260 | return pi / 2 261 | if dy == 0: 262 | if dx < 0: 263 | return pi 264 | return 0 265 | if dx < 0: 266 | return pi + atan(dy / dx) 267 | if dy < 0: 268 | return 2 * pi + atan(dy / dx) 269 | return atan(dy / dx) 270 | 271 | 272 | def angle2(point_a, point_b, point_c): 273 | """Return angle B (radian) between point_b and point_c. 274 | c 275 | / \ 276 | / B\ 277 | a-------b 278 | """ 279 | a = (point_c.x - point_b.x)**2 + (point_c.y - point_b.y)**2 280 | b = (point_c.x - point_a.x)**2 + (point_c.y - point_a.y)**2 281 | c = (point_b.x - point_a.x)**2 + (point_b.y - point_a.y)**2 282 | cos_value = (a + c - b) / (2 * sqrt(a) * sqrt(c)) 283 | return acos(int(cos_value*T)/T2) 284 | 285 | 286 | def ccw(A, B, C): 287 | """Return 1 if counter clockwise, -1 if clock wise, 0 if collinear """ 288 | # Rounding this way is faster than calling round() 289 | area = int(((B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x))*T)/T2 290 | if area > 0: return 1 291 | if area < 0: return -1 292 | return 0 293 | 294 | 295 | def on_segment(p, q, r): 296 | """Given three colinear points p, q, r, the function checks if point q 297 | lies on line segment 'pr'.""" 298 | if (q.x <= max(p.x, r.x)) and (q.x >= min(p.x, r.x)): 299 | if (q.y <= max(p.y, r.y)) and (q.y >= min(p.y, r.y)): 300 | return True 301 | return False 302 | 303 | 304 | def edge_intersect(p1, q1, edge): 305 | """Return True if edge from A, B interects edge. 306 | http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/""" 307 | p2 = edge.p1 308 | q2 = edge.p2 309 | o1 = ccw(p1, q1, p2) 310 | o2 = ccw(p1, q1, q2) 311 | o3 = ccw(p2, q2, p1) 312 | o4 = ccw(p2, q2, q1) 313 | 314 | # General case 315 | if (o1 != o2 and o3 != o4): 316 | return True 317 | # p1, q1 and p2 are colinear and p2 lies on segment p1q1 318 | if o1 == COLLINEAR and on_segment(p1, p2, q1): 319 | return True 320 | # p1, q1 and p2 are colinear and q2 lies on segment p1q1 321 | if o2 == COLLINEAR and on_segment(p1, q2, q1): 322 | return True 323 | # p2, q2 and p1 are colinear and p1 lies on segment p2q2 324 | if o3 == COLLINEAR and on_segment(p2, p1, q2): 325 | return True 326 | # p2, q2 and q1 are colinear and q1 lies on segment p2q2 327 | if o4 == COLLINEAR and on_segment(p2, q1, q2): 328 | return True 329 | return False 330 | 331 | 332 | class OpenEdges(object): 333 | def __init__(self): 334 | self._open_edges = [] 335 | 336 | def insert(self, p1, p2, edge): 337 | self._open_edges.insert(self._index(p1, p2, edge), edge) 338 | 339 | def delete(self, p1, p2, edge): 340 | index = self._index(p1, p2, edge) - 1 341 | if self._open_edges[index] == edge: 342 | del self._open_edges[index] 343 | 344 | def smallest(self): 345 | return self._open_edges[0] 346 | 347 | def _less_than(self, p1, p2, edge1, edge2): 348 | """Return True if edge1 is smaller than edge2, False otherwise.""" 349 | if edge1 == edge2: 350 | return False 351 | if not edge_intersect(p1, p2, edge2): 352 | return True 353 | edge1_dist = point_edge_distance(p1, p2, edge1) 354 | edge2_dist = point_edge_distance(p1, p2, edge2) 355 | if edge1_dist > edge2_dist: 356 | return False 357 | if edge1_dist < edge2_dist: 358 | return True 359 | # If the distance is equal, we need to compare on the edge angles. 360 | if edge1_dist == edge2_dist: 361 | if edge1.p1 in edge2: 362 | same_point = edge1.p1 363 | else: 364 | same_point = edge1.p2 365 | angle_edge1 = angle2(p1, p2, edge1.get_adjacent(same_point)) 366 | angle_edge2 = angle2(p1, p2, edge2.get_adjacent(same_point)) 367 | if angle_edge1 < angle_edge2: 368 | return True 369 | return False 370 | 371 | def _index(self, p1, p2, edge): 372 | lo = 0 373 | hi = len(self._open_edges) 374 | while lo < hi: 375 | mid = (lo+hi)//2 376 | if self._less_than(p1, p2, edge, self._open_edges[mid]): 377 | hi = mid 378 | else: 379 | lo = mid + 1 380 | return lo 381 | 382 | def __len__(self): 383 | return len(self._open_edges) 384 | 385 | def __getitem__(self, index): 386 | return self._open_edges[index] 387 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm >= 4.23.4 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('requirements.txt', 'r') as fr: 4 | required = fr.read().splitlines() 5 | 6 | setup( 7 | name = 'pyvisgraph', 8 | packages = ['pyvisgraph'], 9 | version = '0.2.1', 10 | description = 'Given a set of simple obstacle polygons, build a visibility graph and find the shortest path between two points.', 11 | author = 'Christian Reksten-Monsen', 12 | author_email = 'christian@reksten-monsen.com', 13 | url = 'https://github.com/TaipanRex/pyvisgraph', 14 | download_url = 'https://github.com/TaipanRex/pyvisgraph/tarball/0.2.1', 15 | install_requires = required, 16 | keywords = ['visibility', 'graph', 'shortest'], 17 | classifiers = [], 18 | ) 19 | -------------------------------------------------------------------------------- /tests/test_deploy.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | import shapefile 25 | import pyvisgraph as vg 26 | 27 | class TestVisGraphBuild: 28 | 29 | def setup_method(self, method): 30 | self.sf = shapefile.Reader("examples/shapefiles/GSHHS_c_L1.dbf") 31 | self.shapes = self.sf.shapes() 32 | self.polys = [] 33 | self.polys.append([vg.Point(a[0], a[1]) for a in self.shapes[4].points]) 34 | 35 | 36 | def test_build_1_core(self): 37 | world = vg.VisGraph() 38 | world.build(self.polys) 39 | assert len(world.graph.get_points()) == 310 40 | assert len(world.graph.get_edges()) == 310 41 | assert len(world.visgraph.get_edges()) == 1156 42 | s = "Graph points: {} edges: {}, visgraph edges: {}" 43 | print(s.format(len(world.graph.get_points()), 44 | len(world.graph.get_edges()), 45 | len(world.visgraph.get_edges()))) 46 | 47 | def test_build_2_core(self): 48 | world = vg.VisGraph() 49 | world.build(self.polys, workers=2) 50 | assert len(world.graph.get_points()) == 310 51 | assert len(world.graph.get_edges()) == 310 52 | assert len(world.visgraph.get_edges()) == 1156 53 | s = "Graph points: {} edges: {}, visgraph edges: {}" 54 | print(s.format(len(world.graph.get_points()), 55 | len(world.graph.get_edges()), 56 | len(world.visgraph.get_edges()))) 57 | 58 | 59 | class TestVisGraphMethods: 60 | 61 | def setup_method(self, method): 62 | self.world = vg.VisGraph() 63 | self.world.load('world.pk1') 64 | self.origin = vg.Point(100, -20) 65 | self.destination = vg.Point(25, 75) 66 | 67 | def test_shortest_path(self): 68 | shortest = self.world.shortest_path(self.origin, self.destination) 69 | assert len(shortest) == 19 70 | 71 | def test_shortest_path_not_update_visgraph(self): 72 | shortest = self.world.shortest_path(self.origin, self.destination) 73 | assert self.origin not in self.world.visgraph 74 | assert self.destination not in self.world.visgraph 75 | 76 | def test_shortest_path_not_update_graph(self): 77 | shortest = self.world.shortest_path(self.origin, self.destination) 78 | assert self.origin not in self.world.graph 79 | assert self.destination not in self.world.graph 80 | 81 | def test_update(self): 82 | self.world.update([self.origin, self.destination]) 83 | assert self.origin in self.world.visgraph 84 | assert self.destination in self.world.visgraph 85 | 86 | def test_update_not_update_graph(self): 87 | self.world.update([self.origin, self.destination]) 88 | assert self.origin not in self.world.graph 89 | assert self.destination not in self.world.graph 90 | -------------------------------------------------------------------------------- /tests/test_pvg.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Christian August Reksten-Monsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | from __future__ import division 25 | from pyvisgraph.graph import Graph, Point, Edge 26 | from pyvisgraph.visible_vertices import edge_intersect, point_edge_distance 27 | from pyvisgraph.visible_vertices import visible_vertices, angle, point_in_polygon 28 | from pyvisgraph.visible_vertices import intersect_point, edge_distance 29 | from math import pi, degrees, cos, sin 30 | import pyvisgraph as vg 31 | 32 | ''' 33 | setup_module(module): only run once when file is executed 34 | teardown_module(module): 35 | setup_function(function): 36 | 37 | In a class: 38 | setup(self): gets run last when a method is called 39 | setup_class(cls): 40 | setup_method(self, method): 41 | def test_subscribe_when_already_registered(): 42 | # GIVEN a user is already subscribed to a newsletter 43 | # WHEN a user subscribes to the newsletter 44 | # THEN no changes are made to the email list 45 | # and the user is told they already are subscribed 46 | ''' 47 | 48 | 49 | class TestUndirectedGraph: 50 | 51 | def setup_method(self, method): 52 | self.point_a = Point(0.0, 1.0) 53 | self.point_b = Point(1.0, 2.0) 54 | self.point_c = Point(0.0, 1.0) 55 | self.point_d = Point(1.0, 2.0) 56 | self.edge_a = Edge(self.point_a, self.point_b) 57 | self.edge_b = Edge(self.point_b, self.point_a) 58 | self.edge_c = Edge(self.point_c, self.point_d) 59 | self.edge_d = Edge(self.point_d, self.point_c) 60 | 61 | def test_point_equality(self): 62 | assert self.point_a == Point(0.0, 1.0) 63 | assert self.point_a != Point(1.0, 0.0) 64 | assert self.edge_a == self.edge_b 65 | assert self.edge_a == self.edge_a 66 | assert self.edge_a == self.edge_c 67 | assert self.edge_a == self.edge_d 68 | 69 | 70 | def test_angle_function(): 71 | center = Point(1.0, 1.0) 72 | point_a = Point(3.0, 1.0) 73 | point_b = Point(1.0, 0) 74 | point_c = Point(0.0, 2.0) 75 | point_d = Point(2.0, 2.0) 76 | point_e = Point(2.0, 0.0) 77 | point_f = Point(0.0, 0.0) 78 | assert angle(center, point_a) == 0 79 | assert angle(center, point_b) == pi * 3 / 2 80 | assert degrees(angle(center, point_c)) == 135 81 | assert degrees(angle(center, point_d)) == 45 82 | assert degrees(angle(center, point_e)) == 315 83 | assert degrees(angle(center, point_f)) == 225 84 | 85 | 86 | """This tests for floating point representation errors""" 87 | def test_angle2_function(): 88 | polys = [[Point(353.6790486272709, 400.99387840984855), 89 | Point(351.1303807396073, 398.8696192603927), 90 | Point(349.5795890382704, 397.8537806679034), 91 | Point(957.1067811865476, -207.10678118654744), 92 | Point(-457.10678118654766, -207.10678118654744), 93 | Point(-457.10678118654744, 1207.1067811865476), 94 | Point(957.1067811865476, 1207.1067811865473), 95 | Point(353.52994294901674, 606.0798841165788), 96 | Point(354.0988628008279, 604.098862800828), 97 | Point(354.52550331744527, 601.3462324760635), 98 | Point(352.6969055662087, 602.6943209889012), 99 | Point(351.22198101804634, 603.781672670995), 100 | Point(247.0, 500.0), 101 | Point(341.8964635104416, 405.50444716676054), 102 | Point(349.24224903733045, 410.671256247085), 103 | Point(350.84395848060774, 407.17766877398697)]] 104 | v = vg.VisGraph() 105 | v.build(polys) 106 | 107 | 108 | def test_edge_intersect_function(): 109 | point_a = Point(3.0, 5.0) 110 | point_b = Point(5.0, 3.0) 111 | point_c = Point(4.0, 2.0) 112 | point_d = Point(4.0, 5.0) 113 | point_e = Point(5.0, 4.0) 114 | point_f = Point(3.0, 4.0) 115 | point_g = Point(4.0, 1.0) 116 | point_h = Point(6.0, 4.0) 117 | point_i = Point(4.0, 4.0) 118 | edge = Edge(point_a, point_b) 119 | assert edge_intersect(point_c, point_d, edge) is True 120 | assert edge_intersect(point_c, point_e, edge) is True 121 | assert edge_intersect(point_f, point_e, edge) is True 122 | assert edge_intersect(point_g, point_b, edge) is True 123 | assert edge_intersect(point_c, point_h, edge) is True 124 | assert edge_intersect(point_h, point_i, edge) is True 125 | 126 | 127 | def test_point_edge_distance_function(): 128 | point_a = Point(3.0, 1.0) 129 | point_b = Point(3.0, 5.0) 130 | point_c = Point(2.0, 2.0) 131 | point_d = Point(4.0, 4.0) 132 | point_e = Point(1.0, 1.0) 133 | point_f = Point(1.0, 2.0) 134 | point_g = Point(3.0, 4.0) 135 | point_h = Point(2.0, 5.0) 136 | edge = Edge(point_a, point_b) 137 | edge2 = Edge(point_c, point_d) 138 | edge3 = Edge(point_e, point_b) 139 | assert point_edge_distance(point_c, point_d, edge) == 1.4142135623730951 140 | assert point_edge_distance(point_a, point_b, edge2) == 2.0 141 | assert point_edge_distance(point_f, point_g, edge3) == 1.4142135623730951 142 | assert point_edge_distance(point_h, point_g, edge3) == 0.9428090415820635 143 | 144 | 145 | def test_point_in_polygon(): 146 | g = vg.VisGraph() 147 | point_a = Point(0,0) 148 | point_b = Point(4,0) 149 | point_c = Point(2,4) 150 | point_d = Point(1,0.5) 151 | g.build([[point_a, point_b, point_c]]) 152 | assert g.point_in_polygon(point_d) != -1 153 | 154 | 155 | class TestClosestPoint: 156 | 157 | def setup_method(self, method): 158 | self.g = vg.VisGraph() 159 | self.point_a = Point(0,0) 160 | self.point_b = Point(4,0) 161 | self.point_c = Point(2,4) 162 | self.point_d = Point(1,0.5) 163 | self.g.build([[self.point_a, self.point_b, self.point_c]]) 164 | 165 | def test_closest_point(self): 166 | pid = self.g.point_in_polygon(self.point_d) 167 | cp = self.g.closest_point(self.point_d, pid) 168 | assert self.g.point_in_polygon(cp) == -1 169 | 170 | def test_closest_point_length(self): 171 | pid = self.g.point_in_polygon(self.point_d) 172 | cp = self.g.closest_point(self.point_d, pid, length=0.5) 173 | ip = intersect_point(self.point_d, cp, Edge(self.point_a, self.point_b)) 174 | assert edge_distance(ip, cp) == 0.5 175 | 176 | def test_closest_point_edge_point(self): 177 | """Test where the cp is a end-point of a polygon edge. Can end up with 178 | cp extending into polygon instead of outside it.""" 179 | g = vg.VisGraph() 180 | g.build([[Point(0,1), Point(2,0), Point(1,1), Point(2,2)]]) 181 | p = Point(1,0.9) 182 | pid = g.point_in_polygon(p) 183 | cp = g.closest_point(p, pid, length=0.001) 184 | assert g.point_in_polygon(cp) == -1 185 | 186 | 187 | class TestCollinear: 188 | 189 | def setup_method(self, method): 190 | self.point_a = Point(0.0, 1.0) 191 | self.point_b = Point(1.0, 0.0) 192 | self.point_c = Point(2.0, 3.0) 193 | self.point_d = Point(3.0, 2.0) 194 | self.point_e = Point(3.5, 0.5) 195 | self.point_f = Point(4.5, 3.5) 196 | 197 | def test_collin1(self): 198 | graph = Graph([[self.point_a, self.point_b, self.point_c], 199 | [self.point_d, self.point_e, self.point_f]]) 200 | visible = visible_vertices(Point(1,4), graph, None, None) 201 | assert visible == [self.point_a, self.point_c, self.point_d, self.point_f] 202 | 203 | def test_collin2(self): 204 | self.point_g = Point(2.0, 5.0) 205 | self.point_h = Point(3.0, 5.0) 206 | graph = Graph([[self.point_g, self.point_h, self.point_c], 207 | [self.point_d, self.point_e, self.point_f]]) 208 | visible = visible_vertices(Point(1,4), graph, None, None) 209 | assert visible == [self.point_g, self.point_e, self.point_c, self.point_d] 210 | 211 | def test_collin3(self): 212 | point_g = Point(2.0, 2.0) 213 | point_h = Point(3.5, 5.0) 214 | point_i = Point(2.5, 2.0) 215 | graph = Graph([[self.point_a, self.point_b, self.point_c], 216 | [point_g, point_h, point_i], 217 | [self.point_d, self.point_e, self.point_f]]) 218 | visible = visible_vertices(Point(1,4), graph, None, None) 219 | assert visible == [point_h, self.point_a, self.point_c] 220 | 221 | def test_collin4(self): 222 | graph = Graph([[Point(1,1), Point(2,3), Point(3,1),Point(2,2)], 223 | [Point(2,4)]]) 224 | visible = visible_vertices(Point(2,1), graph, None, None) 225 | assert visible == [Point(3,1), Point(2,2), Point(1,1)] 226 | 227 | def test_collin5(self): 228 | r = 0.2 # Radius of polygon 229 | n = 4 # Sides of polygon 230 | c = Point(1.0, 1.0) # Center of polygon 231 | verts = [] 232 | for i in range(n): 233 | verts.append(Point(r * cos(2*pi * i/n - pi/4) + c.x, 234 | r * sin(2*pi * i/n - pi/4) + c.y)) 235 | g = vg.VisGraph() 236 | g.build([verts]) 237 | s = Point(0, 0) 238 | t = Point(1.7, 1.7) 239 | shortest = g.shortest_path(s, t) 240 | visible = visible_vertices(t, g.graph, s, None) 241 | assert verts[3] not in visible 242 | assert verts[1] not in shortest 243 | assert verts[3] not in shortest 244 | 245 | """ See https://github.com/TaipanRex/pyvisgraph/issues/20. 246 | This tests colinearity case #1 using point_in_polygon.""" 247 | def test_collin6(self): 248 | graph = Graph([[Point(0,0), Point(2,1), Point(0,2)]]) 249 | pip = point_in_polygon(Point(1,1), graph) 250 | assert pip > -1 251 | 252 | """ See https://github.com/TaipanRex/pyvisgraph/issues/20. 253 | This tests colinearity case #2 using point_in_polygon.""" 254 | def test_collin7(self): 255 | graph = Graph([[Point(0,0), Point(1,1), Point(2,0), Point(2,2), Point(0,2)]]) 256 | pip = point_in_polygon(Point(0.5,1), graph) 257 | assert pip > -1 258 | 259 | """ See https://github.com/TaipanRex/pyvisgraph/issues/20. 260 | This tests colinearity case #3 using point_in_polygon.""" 261 | def test_collin8(self): 262 | graph = Graph([[Point(0,0), Point(2,0), Point(2,2), Point(1,1), Point(0,2)]]) 263 | pip = point_in_polygon(Point(0.5,1), graph) 264 | assert pip > -1 265 | 266 | """ See https://github.com/TaipanRex/pyvisgraph/issues/20. 267 | This tests colinearity case #4 using point_in_polygon.""" 268 | def test_collin9(self): 269 | graph = Graph([[Point(0,0), Point(1,0), Point(1,1), Point(2,1), Point(2,2), 270 | Point(0,2)]]) 271 | pip = point_in_polygon(Point(0.5,1), graph) 272 | assert pip > -1 273 | 274 | """ See https://github.com/TaipanRex/pyvisgraph/issues/20. 275 | This tests colinearity case #5 using point_in_polygon.""" 276 | def test_collin10(self): 277 | graph = Graph([[Point(0,0), Point(1,0), Point(1,1), Point(2,1), Point(2,0), 278 | Point(3,0), Point(3,2), Point(0,2)]]) 279 | pip = point_in_polygon(Point(0.5,1), graph) 280 | assert pip > -1 281 | 282 | """ See https://github.com/TaipanRex/pyvisgraph/issues/20. 283 | This tests colinearity case #6 using point_in_polygon.""" 284 | def test_collin11(self): 285 | graph = Graph([[Point(0,0), Point(3,0), Point(3,2), Point(2,3), Point(2,1), 286 | Point(1,1), Point(1,2), Point(0,2)]]) 287 | pip = point_in_polygon(Point(0.5,1), graph) 288 | assert pip > -1 -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35 3 | [testenv] 4 | deps= 5 | pytest 6 | pyshp 7 | tqdm 8 | commands=py.test -v --tb=short 9 | --------------------------------------------------------------------------------