Intersection Algorithm - HY-OHTUPROJ-OSRM/osrm-project GitHub Wiki

The Polygonal-Intersections repository contains an algorithm to calculate new speeds for road segments given a set of roadblocks and speed zones. The affected segments and their new speed values can then be given to OSRM as dynamic traffic data. The algorithm comes with a GUI application for testing as well as a separate CLI that's to be called from routing-api.

Build instructions

CLI

  1. If you're on Windows, install a build of GCC such as MinGW-w64 and make sure the folder containing g++ is in Path.

  2. Run make cli

The program will be placed bin/cli.

GUI

⚠️ Note: Building the GUI application using the provided Makefile is currently only supported on Windows.

The GUI can be built on Windows as follows:

  1. Install a build of GCC for Windows such as MinGW-w64.

  2. Run make gui_dirs

  3. Place the C and C++ standard library binaries from the compiler in the bin/gui folder that was created.

  4. The GUI uses the SFML library. Unless the compiler you use is listed on the library's download page, you will need to build it from source.

  5. Place the DLLs from SFML in bin/gui and add the include folder from the SFML zip file to Path.

  6. Make sure you have the folder containing g++.exe in Path.

  7. Run make gui. (If you don't want to do the previous step, you can also run make gui CXX=[path to g++]/g++.)

The program will be placed in bin/gui.

Testing

Components of the algorithm can be tested with unit tests using the following commands:

  • make tests: Builds the unit tests.

  • make run_tests: Builds and runs the unit tests.

Coverage

A test coverage report can be generated using either LCOV or Gcovr. Generating the report with Gcovr will include branch coverage information, but since not all branches in the code are actually relevant, the simpler line and function coverage report generated by LCOV is recommended instead. The following commands are supported:

  • make coverage.info: Builds and runs the unit tests and generates a machine-readable coverage report using LCOV.

  • make cov: Builds and runs the unit tests and generates a coverage report in HTML using LCOV. The report can be viewed by opening htmlcov/index.html in a browser.

  • make gcovr: Builds and runs the unit tests and generates a coverage report using Gcovr. The report can be viewed by opening gcovr/coverage.html in a browser.

The coverage report from the latest commit in the repository is on Codecov.

CLI Usage

The program Polygonal-Intersections-CLI takes no command line arguments. Instead, the roadblocks, speed zones and line segments are sent via standard input in the following format.

Input

The header of the input contains the number of polygons and paths.

Offset Size Description
0x00 8 Number of roadblock polygons
0x08 8 Number of roadblock polygonal chains
0x10 8 Number of speed zones
0x18 8 Number of paths
0x20 List of roadblock polygons
List of roadblock polygonal chains
List of speed zones
List of paths

Each roadblock (both polygons and polygonal chains) on the corresponding lists are given in the following format:

Offset Size Description
0x00 8 Number of vertices
0x08 Vertex list

Each vertex on the vertex list is represented as a pair of 32-bit signed integers. The list of polygonal chain roadblocks immediately follows the list of polygon roadblocks without any padding in between. The latter is immediately followed by the list of speed zones. Each speed zone on that list is in given in the following format:

Offset Size Description
0x00 1 Type
0x01 8 Effect value
0x09 8 Number of vertices
0x11 Vertex list

The effect value is a 64-bit floating point number. The following speed zone types are supported:

Speed zone type Name Description
0 Offset The effect value is added to the speed
1 Factor The speed is multiplied by the effect value
2 Cap If the speed is higher than the effect value, it gets replaced with it
3-255 Constant The speed is replaced with the effect value

The path list immediately follows the list of speed zones. Each path on it is given in the following format:

Offset Size Description
0x00 2 Original speed
0x02 8 Number of vertices
0x0A Node list

The original speed of the path is given as an unsigned 16-bit integer. Each node on the node list is given in the following format:

Offset Size Description
0x00 8 node ID
0x08 4 x-coordinate
0x0C 4 y-coordinate

The node ID is an unsigned 64-bit integer. It's used to identify nodes in the output. Like before, coordinates are 32-bit signed integers.

Output

The program sends its results to standard output. The output represents all of the given line segments whose speeds are affected by the roadblocks and speed zones. Each of those segments is only identified by the node IDs of its endpoints, not by their coordinates. Each affected segment is sent in the following format:

Offset Size Description
0x00 8 start node ID
0x08 8 end node ID
0x10 8 new speed

Once the program finishes successfully, all of the affected segments will have been sent. The node IDs in each segment are in the same order that they were in the input. The new speed is given as a 64-bit floating point number.

Algorithm

The main algorithm is contained in the function TrafficZones::for_each_intersecting_segment. It's called for a single path at once, and it receives its nodes by calling the read_node parameter. For each segment in the path whose speed is affected by the roadblocks and speed zones, it calls the write_segment parameter. This way the segments are written to standard output as soon as they're calculated, and that calculation begins as soon as the segments are read.

The algorithm first checks if the starting point of the path is inside a roadblock polygon sets the in_roadblock variable accordingly. Then it iterates all segments of the chain. On each iteration, it first checks if the segment is blocked by a roadblock, before calculating the more expensive average speed given by the speed zones. It uses the in_roadblock variable to keep track of whether the current node is inside a roadblock polygon or not.

Checking if a point is inside a polygon

The algorithm to determine if a given point is inside a polygon is in the function Polygon::contains. It casts a ray vertically downwards from the point and counts the number of times it crosses from one side of the polygon to the other modulo 2. If that number is even, the point is considered the be outside of the polygon, and otherwise it's considered to be inside. Some special cases are handled to ensure that points on the edge of the polygon are considered inside, and vertical edges overlapping with the ray are handled properly. There is a short description of this algorithm on Wikipedia.

The directional limit of the point-in-polygon function

The function Polygon::limit_of_contains computes the directional limit of Polygon::contains along a given vector. In other words, it returns true if every neighborhood of the point contains another point in the given direction, and false otherwise. It's implement similarly to Polygon::contains, but the direction of the ray is the direction of the given vector and the edge cases are different.

Checking if two line segments intersect

The algorithm to determine if two line segments intersect is in the function find_intersection. It interprets the line segments as images of linear interpolation functions $l_{1}, l_{2}: [0, 1] \cap \mathbb{Q} \rightarrow \mathbb{Q}^{2}$. First the algorithm checks if the line segments are parallel by calculating the determinant of the $2 \times 2$ matrix whose columns are the direction vectors of the segments (this operation is also known as a "wedge product").

In the case where the line segments aren't parallel, it calculates the parameters $t_{1}$ and $t_{2}$ for the corresponding extended functions $\mathbb{Q} \rightarrow \mathbb{Q}^{2}$ at the intersection point and then checks if both parameters are within $[0, 1]$. If they are, the line segments intersect and the algorithm returns the parameter $t_{1}$ of the intersection point for the first line segment, i.e. the unique value for $t_{1}$ with which $l_{1}(t_{1})$ is on the second line segment. Otherwise they don't intersect, and the function returns std::nullopt.

If the line segments are parallel, the algorithm checks if they are also collinear. If they are collinear and overlap with each other, the algorithm returns an interval of all possible values for $t_{1}$ with which $l_{1}(t_{1})$ is on the second line segment. Otherwise they don't intersect and std::nullopt is returned.

Finding the parts of a line segment that are inside different speed zones

The IntervalMap::insert_at_intersections function finds all intersections between a line segment and a polygon as parameters to the line segment and inserts them to the IntervalMap data structure which maps intervals of rational numbers to speed zones.

Calculating the new average speed for a line segment based on speed zones

The LineSegment::new_average_speed function calculates the new average speed for a line segment given its original speed and a collection of speed zones. It calls IntervalMap::insert_at_intersections repeatedly to insert the intersections with each speed zone to the same interval map. The intersections with the zone that would give the lowest speed to the segment are inserted last. This makes it so that when two or more speed zones overlap, the lowest speed given by them is used at each point.

Time complexity

Excluding logarithmic factors, the time complexity of the current algorithm is $O(nm)$ where $n$ is the total number of nodes in the paths and $m$ is the total number of vertices in the roadblocks and speed zones. However, it might be possible to improve this in the future.

Existing algorithms to look into

The following algorithms calculate all $k$ intersections of $n$ line segments:

There is also the Shamos–Hoey algorithm that determines if at least one intersection exists in $O(n log n)$ time. Perhaps a modified version of one of these algorithms could be used to improve the time complexity of the current algorithm?

Minimum and maximum values for coordinates

Each coordinate of a vertex needs to be within $[1-2^{30}, 2^{30}-1]$. Otherwise integer overflow may occur, and the results may not be correct. Currently the coordinates aren't validated, but this could change in the future. Another option would be receiving all coordinates in floating point and then scaling and rounding them so that the one with the largest absolute value maps to $\pm(2^{30}-1)$.

Self-intersecting polygons

If a polygon intersects itself, the notions of "inside" and "outside" become ambiguous. The current algorithm uses the even-odd rule to decide this. It would also be possible to require all polygons to be simple and validate them before running the algorithm.