diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 7375b2cf02..52bf542095 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -57,7 +57,7 @@ jobs: cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CONAN=ON -DENABLE_NODE_BINDINGS=ON .. cmake --build . --config Release - + # TODO: MSVC goes out of memory when building our tests # - name: Run tests # shell: bash @@ -331,7 +331,7 @@ jobs: build_node_package: true continue-on-error: true node: 22 - runs-on: macos-15 # arm64 + runs-on: macos-15 # arm64 BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ @@ -419,7 +419,7 @@ jobs: fi # See: https://github.com/actions/toolkit/issues/946#issuecomment-1590016041 # We need it to be able to access system folders while restoring cached Boost below - - name: Give tar root ownership + - name: Give tar root ownership if: runner.os == 'Linux' && matrix.ENABLE_CONAN != 'ON' run: sudo chown root /bin/tar && sudo chmod u+s /bin/tar @@ -429,7 +429,7 @@ jobs: id: install-boost with: boost_version: 1.85.0 - + - name: Install dev dependencies run: | # workaround for issue that GitHub Actions seems to not adding it to PATH after https://github.com/actions/runner-images/pull/6499 @@ -577,7 +577,7 @@ jobs: uses: actions/upload-artifact@v4 if: failure() with: - name: logs + name: logs-${{ matrix.name }} path: test/logs/ - name: Build Node package diff --git a/CHANGELOG.md b/CHANGELOG.md index e97f8cc878..81b4ab8ee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ +# Changelog + # Unreleased - Changes from 6.0.0 + - Features + - ADDED: Pedestrian area routing [#7161](https://github.com/Project-OSRM/osrm-backend/pull/7161) # 6.0.0 - Changes from 6.0.0 RC2: None @@ -182,7 +186,7 @@ - CHANGED: Docker build, enabled arm64 build layer [#6172](https://github.com/Project-OSRM/osrm-backend/pull/6172) - CHANGED: Docker build, enabled apt-get update/install caching in separate layer for build phase [#6175](https://github.com/Project-OSRM/osrm-backend/pull/6175) - FIXED: Bump CI complete meta job to ubuntu-20.04 [#6323](https://github.com/Project-OSRM/osrm-backend/pull/6323) - - CHANGED: Node packages are now scoped by @project-osrm [#6386](https://github.com/Project-OSRM/osrm-backend/issues/6386) + - CHANGED: Node packages are now scoped by \@project-osrm [#6386](https://github.com/Project-OSRM/osrm-backend/issues/6386) - Routing: - CHANGED: Lazily generate optional route path data [#6045](https://github.com/Project-OSRM/osrm-backend/pull/6045) - FIXED: Completed support for no_entry and no_exit turn restrictions. [#5988](https://github.com/Project-OSRM/osrm-backend/pull/5988) @@ -217,7 +221,7 @@ - Build: - CHANGED: Node binaries now use Github Releases for hosting [#6030](https://github.com/Project-OSRM/osrm-backend/pull/6030) - Misc: - - FIXED: Upgrade to @mapbox/node-pre-gyp fix various bugs with Node 12/14 [#5991](https://github.com/Project-OSRM/osrm-backend/pull/5991) + - FIXED: Upgrade to \@mapbox/node-pre-gyp fix various bugs with Node 12/14 [#5991](https://github.com/Project-OSRM/osrm-backend/pull/5991) - FIXED: `valid` type in documentation examples [#5990](https://github.com/Project-OSRM/osrm-backend/issues/5990) - FIXED: Remove redundant loading of .osrm.cell_metrics [#6019](https://github.com/Project-OSRM/osrm-backend/issues/6019) - CHANGED: Increase PackedOSMIDs size to 34 bits. This breaks the **data format** [#6020](https://github.com/Project-OSRM/osrm-backend/issues/6020) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b01a4a77d..b8816a4c01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,18 +1,21 @@ -# Everyone +# Contributing + +## Everyone Please take some time to review our [code of conduct](CODE-OF-CONDUCT.md) to help guide your interactions with others on this project. -# User +## User Before you open a new issue, please search for older ones that cover the same issue. In general "me too" comments/issues are frowned upon. You can add a :+1: emoji reaction to the issue if you want to express interest in this. -# Developer +## Developer We use `clang-format` version `15` to consistently format the code base. There is a helper script under `scripts/format.sh`. The format is automatically checked by the `mason-linux-release` job of a Travis CI build. To save development time a local hook `.git/hooks/pre-push` + ``` #!/bin/sh @@ -28,16 +31,16 @@ if [ x"$remote" = xorigin ] ; then fi fi ``` + could check code format, modify a local repository and reject push due to unstaged formatting changes. -Also `pre-push` hook rejects direct pushes to `origin/master`. +Also `pre-push` hook rejects direct pushes to `origin/master`. ⚠️ `scripts/format.sh` checks all local files that match `*.cpp` or `*.hpp` patterns. - In general changes that affect the API and/or increase the memory consumption need to be discussed first. Often we don't include changes that would increase the memory consumption a lot if they are not generally usable (e.g. elevation data is a good example). -## Pull Request +### Pull Request Every pull-request that changes the API needs to update the docs in `docs/http.md` and add an entry to `CHANGELOG.md`. Breaking changes need to have a BREAKING prefix. See the [releasing documentation](docs/releasing.md) on how this affects the version. @@ -57,25 +60,25 @@ If you do have commit access there are in general two accepted styles to merging 1. Make sure the branch is up to date with `master`. Run `git rebase master` to find out. 2. Once that is ensured you can either: - - Click the nice green merge button (for a non-fast-forward merge) - - Merge by hand using a fast-forward merge + +- Click the nice green merge button (for a non-fast-forward merge) +- Merge by hand using a fast-forward merge Which merge you prefer is up to personal preference. In general it is recommended to use fast-forward merges because it creates a history that is sequential and easier to understand. -# Maintainer +## Maintainer -## Doing a release +### Doing a release There is an in-depth guide around how to push out a release once it is ready [here](docs/releasing.md). -## The API +### The API Changes to the API need to be discussed and signed off by the team. Breaking changes even more so than additive changes. -## Milestones +### Milestones If a pull request or an issue is applicable for the current or next milestone, depends on the target version number. Since we use semantic versioning we restrict breaking changes to major releases. After a Release Candidate is released we usually don't change the API anymore if it is not critical. Bigger code changes after a RC was released should also be avoided. - diff --git a/Doxyfile.in b/Doxyfile.in index dd51b16d72..1ca9c4a914 100644 --- a/Doxyfile.in +++ b/Doxyfile.in @@ -13,14 +13,14 @@ QUIET = YES INPUT = @CMAKE_CURRENT_SOURCE_DIR@ USE_MDFILE_AS_MAINPAGE = @CMAKE_CURRENT_SOURCE_DIR@/README.md -FILE_PATTERNS = *.h *.hpp *.c *.cc *.cpp *.md +FILE_PATTERNS = *.h *.hpp *.c *.cc *.cpp *.md *.dox RECURSIVE = YES EXCLUDE = @CMAKE_CURRENT_SOURCE_DIR@/third_party \ @CMAKE_CURRENT_SOURCE_DIR@/build \ @CMAKE_CURRENT_SOURCE_DIR@/node_modules \ @CMAKE_CURRENT_SOURCE_DIR@/unit_tests \ - @CMAKE_CURRENT_SOURCE_DIR@/benchmarks \ + @CMAKE_CURRENT_SOURCE_DIR@/benchmarks \ @CMAKE_CURRENT_SOURCE_DIR@/features SOURCE_BROWSER = YES @@ -40,5 +40,4 @@ CALLER_GRAPH = YES DOT_IMAGE_FORMAT = svg INTERACTIVE_SVG = YES DOT_GRAPH_MAX_NODES = 500 -DOT_TRANSPARENT = YES DOT_MULTI_TARGETS = YES diff --git a/README.md b/README.md index f0079952d8..e6aae111cb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -## Open Source Routing Machine - +# Open Source Routing Machine [![osrm-backend CI](https://github.com/Project-OSRM/osrm-backend/actions/workflows/osrm-backend.yml/badge.svg)](https://github.com/Project-OSRM/osrm-backend/actions/workflows/osrm-backend.yml) [![Discord](https://img.shields.io/discord/1034487840219860992)](https://discord.gg/es9CdcCXcb) High performance routing engine written in C++ designed to run on OpenStreetMap data. The following services are available via HTTP API, C++ library interface and NodeJs wrapper: + - Nearest - Snaps coordinates to the street network and returns the nearest matches - Route - Finds the fastest route between coordinates - Table - Computes the duration or distances of the fastest route between all pairs of supplied coordinates @@ -18,6 +18,7 @@ To quickly try OSRM use our [demo server](http://map.project-osrm.org) which com For a quick introduction about how the road network is represented in OpenStreetMap and how to map specific road network features have a look at [the OSM wiki on routing](https://wiki.openstreetmap.org/wiki/Routing) or [this guide about mapping for navigation](https://web.archive.org/web/20221206013651/https://labs.mapbox.com/mapping/mapping-for-navigation/). Related [Project-OSRM](https://github.com/Project-OSRM) repositories: + - [osrm-frontend](https://github.com/Project-OSRM/osrm-frontend) - User-facing frontend with map. The demo server runs this on top of the backend - [osrm-text-instructions](https://github.com/Project-OSRM/osrm-text-instructions) - Text instructions from OSRM route response - [osrm-backend-docker](https://github.com/project-osrm/osrm-backend/pkgs/container/osrm-backend) - Ready to use Docker images @@ -28,7 +29,6 @@ Related [Project-OSRM](https://github.com/Project-OSRM) repositories: - [Hosted documentation](http://project-osrm.org) - [osrm-routed HTTP API documentation](docs/http.md) -- [libosrm API documentation](docs/libosrm.md) ## Contact @@ -41,6 +41,7 @@ Related [Project-OSRM](https://github.com/Project-OSRM) repositories: The easiest and quickest way to setup your own routing engine is to use Docker images we provide. There are two pre-processing pipelines available: + - Contraction Hierarchies (CH) - Multi-Level Dijkstra (MLD) @@ -86,13 +87,13 @@ After adding yourself to the `docker` group make sure to log out and back in aga We support the following images in the Container Registry: -Name | Description ------|------ -`latest` | `master` compiled with release flag -`latest-assertions` | `master` compiled with with release flag, assertions enabled and debug symbols -`latest-debug` | `master` compiled with debug flag -`` | specific tag compiled with release flag -`-debug` | specific tag compiled with debug flag +| Name | Description | +| ------------------- | ------------------------------------------------------------------------------ | +| `latest` | `master` compiled with release flag | +| `latest-assertions` | `master` compiled with with release flag, assertions enabled and debug symbols | +| `latest-debug` | `master` compiled with debug flag | +| `` | specific tag compiled with release flag | +| `-debug` | specific tag compiled with debug flag | ### Building from Source @@ -166,7 +167,6 @@ For usage details have a look [these API docs](docs/nodejs/api.md). An exemplary implementation by a 3rd party with Docker and Node.js can be found [here](https://github.com/door2door-io/osrm-express-server-demo). - ## References in publications When using the code in a (scientific) publication, please cite diff --git a/cucumber.js b/cucumber.js index 9cec411756..34c85ec0d2 100644 --- a/cucumber.js +++ b/cucumber.js @@ -1,7 +1,7 @@ module.exports = { default: '--strict --tags ~@stress --tags ~@todo --tags ~@mld --require features/support --require features/step_definitions', - ch: '--strict --tags ~@stress --tags ~@todo --tags ~@mld -f progress --require features/support --require features/step_definitions', + ch: '--strict --tags ~@stress --tags ~@todo --tags ~@mld --require features/support --require features/step_definitions', todo: '--strict --tags @todo --require features/support --require features/step_definitions', all: '--strict --require features/support --require features/step_definitions', - mld: '--strict --tags ~@stress --tags ~@todo --tags ~@ch --require features/support --require features/step_definitions -f progress' + mld: '--strict --tags ~@stress --tags ~@todo --tags ~@ch --require features/support --require features/step_definitions' }; diff --git a/docs/areas.md b/docs/areas.md new file mode 100644 index 0000000000..b869e749ae --- /dev/null +++ b/docs/areas.md @@ -0,0 +1,109 @@ +# How to route inside pedestrian areas {#pedestrian_areas} + +How to route inside pedestrian areas, or over the interior of an area where you can +travel freely in all directions. + +%OSRM can create routes crossing the interior of an area by generating virtual ways +between every pair of entry points to the area. This process is called @em meshing. The +generated ways follow lines of sight, avoid obstacles, and use existing nodes. An entry +point is where another way connects to the perimeter of the area. + +This feature is still EXPERIMENTAL. + +## Configuration + +To opt-in to this feature, you must declare an algorithm to be used for area meshing. +Find your LUA profile's @ref setup function and insert this line: + +```lua +function setup() + ... + area_manager:init('visgraph+dijkstra') + ... +end +``` + +Note: Only the `visgraph+dijkstra` algorithm is available at present. + +All areas to be meshed must be registered with the @ref AreaManager. In OpenStreetMap +areas are mapped either as a closed way or as a multipolygon relation. Both flavours +must be configured separately. + +### Meshing closed ways + +To mesh a closed way you must register it in your @ref process_way function. Insert +following lines into your existing `process_way` function, immediately after the "quick +initial test": + +```lua +function process_way(profile, way, result, relations) + ... + if way:has_tag('highway', 'pedestrian') and way:has_true_tag('area') then + -- register the way + area_manager:way(way) + return + end + ... +end +``` + +(Note that open ways cannot be meshed and will be ignored.) + +### Meshing multipolygon relations + +To mesh a multipolygon relation you must register it in the @ref process_relation +function. The `process_relation` function is a newly introduced function that is called +for every relation in the input file. You'll have to create the function like this: + +```lua +function process_relation(profile, relation, relations) + if relation:has_tag('type', 'multipolygon') and relation:has_tag('highway', 'pedestrian') then + -- register the relation + area_manager:relation(relation) + end +end +``` + +And you must also return the `process_relation` function at the end of your profile: + +```lua +return { + setup = setup, + process_way = process_way, + process_node = process_node, + process_relation = process_relation, -- << add this line + ... +} +``` + +At this point you have a working basic configuration. Remember that you must run +`osrm-extract` before your changes become effective. + +### Processing the generated ways + +While not necessary, you may want to apply further processing to the @em generated ways. +The generated ways are passed to the @ref process_way function in the usual fashion. +They have the same tags as the original way or relation, except: + +- the `area` tag is removed on ways, +- the `type` tag is removed on relations, +- an `osrm:virtual=yes` tag is added. + +You can pick generated ways like this: + +```lua +function process_way(profile, way, result, relations) + ... + if way:has_key('osrm:virtual') then + -- do something with the way here + end + ... +end +``` + +@sa AreaManager +
A complete example profile is found in the file: [profiles/foot_area.lua](../profiles/foot_area.lua). +
https://wiki.openstreetmap.org/wiki/Relation:multipolygon +
https://wiki.openstreetmap.org/wiki/Key:area +
https://wiki.openstreetmap.org/wiki/Tag:highway%3Dpedestrian#Squares_and_plazas diff --git a/docs/lua-types.dox b/docs/lua-types.dox new file mode 100644 index 0000000000..8d48b22f0f --- /dev/null +++ b/docs/lua-types.dox @@ -0,0 +1,466 @@ +/** + * @defgroup LUA LUA Types + * + * This page is about types as seen from inside the LUA scripting language. + * + * @{ + */ + +class Boolean; +class Float; +class Integer; +class String; +class Table; +class Location; + +/** + * @brief The type of an OSM object + */ +enum class item_type +{ + node = 1, /**< An OSM node */ + way = 2, /**< An OSM way */ + relation = 3 /**< An OSM relation */ +}; + +/** @brief Represents an OSM object + + Example: + + ```.lua + if object:type() == item_type.way and object:id() == 42 then + local highway = object:get_key_by_value("highway") + local oneway = object:has_true_tag("oneway") + ... + end + ``` + */ +class OSMObject +{ + public: + /** @brief Return the id of the OSM object. */ + Integer id(); + + /** @brief Return the item type of the OSM object. */ + item_type type(); + + /** @brief Return the version of the OSM object. */ + Integer version(); + + /** @brief Return the value of the tag identified by key. If the key is not found, return + * `nil`. */ + String get_value_by_key(String key); + + /** @brief Return the value of the tag identified by key. If the key is not found, return the + default value. */ + String get_value_by_key(String key, String default_value); + + /** @brief Return true if a tag identified by the given key is found. */ + Boolean has_key(String key); + + /** @brief Return true if a tag with the given key and value is found. */ + Boolean has_tag(String key, String value); + + /** + * @brief Return true if a tag with the given key and a truthy value is found. + * + * Truthy values are: "yes", "true", and "1". + */ + Boolean has_true_tag(String key); + + /** + * @brief Return true if a tag with the given key and a falsey value is found. + * + * Falsey values are: "no", "false", and "0". + */ + Boolean has_false_tag(String key); +}; + +/** + * @brief Represents a node + */ +class Node : public OSMObject +{ + public: + /** @brief Return the Location of the node. */ + Location location(); + + /** + @brief Return location data for the node. + + Location data is compiled by geographic region. + + ```.lua + local driving_side = way:get_location_tag('driving_side') + ``` + */ + String get_location_tag(String key); +}; + +/** + * @brief Represents a way + */ +class Way : public OSMObject +{ + public: + /** + * @brief Return the number of nodes. + */ + Integer size(); + + /** + * @brief Return true if the way is closed. + */ + Boolean is_closed(); + + /** + * @brief Return the nodes. + * @return A table of Node objects. + */ + Table get_nodes(); + + /** + * @brief Return location data for the way. + */ + String get_location_tag(String key); +}; + +/** + * @brief Represents a relation + */ +class Relation : public OSMObject +{ + public: + /** @brief Return the role the given objects has in the relation or nil. */ + String get_role(OSMObject); + + /** + * @brief Return the members of the relation. + * @return A table of RelationMember objects. + */ + Table get_members(); +}; + +/** + * @brief Represents a relation member + */ +class RelationMember +{ + public: + /** @brief Return the OSM id of the member. */ + Integer ref(); + /** @brief Return the @ref item_type of the member: node, way, or relation. */ + item_type type(); + /** @brief Return the role of the member, eg. `outer`. */ + String role(); +}; + +/** @brief A storage container for relations. + +This container stores all relations that have one of the types configured in @ref +relation_types plus all multipolygon relations registered for area meshing. It is +passed as parameter `relations` to the @ref process_node, @ref process_way, and @ref +process_relation functions. By querying this container you can find out if a node or +way or relation is member of any stored relation. + +Example: + +```lua +for _, rel_id in pairs(relations:get_relations(way)) + local rel = relations:relation(rel_id) + if rel:get_value_by_key('type') == 'route' then + local role = rel:get_role(way) + local dir = rel:get_value_by_key('direction') + ... + end +end +``` +*/ +class Relations +{ + public: + /** + * @brief Return the ids of the relations that contain the given object. + * + * OSMObject may be a Node, Way or Relation. + * + * @return A table of Integer. + */ + Table get_relations(OSMObject); + /** + * @brief Return the relation with the given id. + */ + Relation relation(Integer relation_id); +}; + +/** + * @brief Represents the location of a node. + */ +class Location +{ + public: + /** @brief Return the latitude. */ + Float lat(); + /** @brief Return the longitude. */ + Float lon(); + /** @brief Return true if the location is valid. */ + Boolean valid(); +}; + +/** + * @brief The type of the obstacle + */ +enum class obstacle_type +{ + none, /**< No obstacle */ + /** + * This and the following values have the same semantics as in the `highway` tag in + * OSM. + */ + barrier, + traffic_signals, + stop, + give_way, + crossing, + traffic_calming, + mini_roundabout, + turning_loop, + turning_circle +}; + +/** + * @brief The direction of the obstacle + * + * The direction always refers to the way that the node is in. If the node is at either + * end of a way the direction may be undefined. + */ +enum class obstacle_direction +{ + none, /**< No obstacle */ + forward, /**< The obstacle is present only in the forward direction. */ + backward, /**< The obstacle is present only in the backward direction. */ + both /**< The obstacle is present in both directions. */ +}; + +/** @brief An obstacle on the road or a place where you can turn around. + +This may be a completely impassable obstacle like a barrier, a temporary obstacle like a +traffic light or a stop sign, or an obstacle that just slows you down like a +traffic_calming. The obstacle may be present in both directions or in one direction +only. + +This also represents a good turning point like a `mini_roundabout`, `turning_loop`, or +`turning_circle`. + +An object of this type is immutable once constructed. + +```lua +local obs = Obstacle.new( + obstacle_type.traffic_signals, + obstacle_direction.forward, + 2.5, + 0 +) +assert(obs.duration == 2.5) +``` +*/ + +class Obstacle +{ + public: + /** The type of the obstacle */ + obstacle_type type; + /** The direction of the obstacle */ + obstacle_direction direction; + /** The expected delay in seconds */ + Float duration; + /** The obstacle weight */ + Float weight; +}; + +/** @brief A registry for obstacles. + +The LUA global variable `obstacle_map` is an instance of this class. + +The canonical workflow is: to store obstacles in @ref process_node and retrieve them in +@ref process_turn. + +Note: In the course of processing, between the `process_node()` stage and the +`process_turn()` stage, the extractor switches from using OSM nodes to using internal +nodes. Both types have different ids. You can only store OSM nodes and only retrieve +internal nodes. This implies that, in `process_node()`, you cannot retrieve an obstacle +you have just stored. +*/ + +class ObstacleMap +{ + public: + /** + @brief Register a new obstacle + + Call this function inside @ref process_node to register an obstacle on a node. You + can register as many different obstacles as you wish on any given node. It is your + responsibility to register the same obstacle only once. + + In a following step -- likely in @ref process_turn -- you can retrieve all + obstacles registered at any given node. This function works with OSM nodes. + + Example: + + ```lua + function process_node(profile, node, result, relations) + ... + obstacle_map:add(node, + Obstacle.new( + obstacle_type.traffic_signal, + obstacle_direction.forward, + 2, 0)) + end + ``` + */ + void add(Node, Obstacle); + + /** + @brief Return true if there are any obstacles + + Return true if there are any obstacles at node `to` when coming from node + `from` and having the type `type`. + + You will likely call this function inside @ref process_turn. + Note that this works only with internal nodes, not with OSM nodes. + + Usage examples: + + ```lua + function process_turn(profile, turn) + if obstacle_map:any(turn.via) then + ... + end + if obstacle_map:any(turn.from, turn.via, obstacle_type.traffic_signal) then + turn.duration = turn.duration + 2 + end + end + ``` + + @param from The leading node. Optional. + @param to The node with the obstacle. + @param type The obstacle type. Defaults to all types. May be a bitwise-or + combination of types. + + @return True if there are any obstacles satisfiying the given criteria. + */ + Boolean any(Node from, Node to, obstacle_type type); + /** @overload */ + Boolean any(Node from, Node to); + /** @overload */ + Boolean any(Node to); + + /** @brief Return all obstacles located at node. + + This function retrieves all registered obstacles at node `to` when coming from the node + `from` and having the type `type`. + + You will likely call this function inside @ref process_turn. Note that this works only + with internal nodes, not with OSM nodes. + + @param from The leading node. Optional. + @param to The node with the obstacle. + @param type The obstacle type. Defaults to all types. May be a bitwise-or + combination of types. + + @return A table of Obstacle objects. + + Example: + + ```lua + function process_turn(profile, turn) + for _, obs in pairs(obstacle_map:get(turn.via)) do + if obs.type == obstacle_type.barrier then + turn.duration = turn.duration + obs.duration + end + end + for _, obs in pairs(obstacle_map:get( + turn.from, turn.via, obstacle_type.traffic_signal)) do + turn.duration = turn.duration + obs.duration + end + end + ``` + */ + Table get(Node from, Node to, obstacle_type type); + /** @overload */ + Table get(Node from, Node to); + /** @overload */ + Table get(Node to); +}; + +/** + * @brief A registry for pedestrian areas. + * + * The LUA global variable `area_manager` is an instance of this class. + */ +class AreaManager +{ + public: + /** + * @brief Initialize the area manager + * + * Call this function inside @ref setup to initialize the area manager. + * The supplied algorithm will be used for meshing. + * + * @param algorithm_name The algorithm to use for meshing. At present only one + * algorithm is supported: 'visgraph+dijkstra'. + */ + void init(String algorithm_name); + + /** + * @brief Register a multipolygon relation for meshing. + * + * Call this function inside @ref process_relation to register a relation for + * meshing. The relation must be a multipolygon relation. + */ + void relation(Relation); + + /** + * @brief Register a closed way for meshing. + * + * Call this function inside @ref process_way to register a way for + * meshing. The way must be closed. + */ + void way(Way); + + /** + * @brief Return the registered relations for this node. + * + * Call this functions inside @ref process_node. If this node is a member of a + * relation that was registered for meshing, those relations will be returned. + * + * @return A Table of Relation objects. + */ + Table get_relations(Node); + + /** + @brief Return the registered relations for this way. + + Call this functions inside @ref process_way. If this way is a member of a + relation that was registered for meshing, those relations will be returned. + + @return A Table of Relation objects. + */ + Table get_relations(Way); +}; + +/** + * @brief Return true if the value is truthy. + * + * Truthy values are: "yes", "true", and "1". + */ +Boolean is_true(String value); // NOLINT + +/** + * @brief Return true if the value is falsey. + * + * Falsey values are: "no", "false", and "0". + */ +Boolean is_false(String value); // NOLINT + +/** @} */ diff --git a/docs/profiles.md b/docs/profiles.md index b6c2905689..dc58891471 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -1,4 +1,9 @@ # OSRM profiles + +@sa +@ref LUA
+@subpage pedestrian_areas + OSRM supports "profiles". Profiles representing routing behavior for different transport modes like car, bike and foot. You can also create profiles for variations like a fastest/shortest car profile or fastest/safest/greenest bicycles profile. A profile describes whether or not it's possible to route along a particular type of way, whether we can pass a particular node, and how quickly we'll be traveling when we do. This feeds into the way the routing graph is created and thus influences the output routes. @@ -35,7 +40,7 @@ A profile can also define various local functions it needs. Looking at [car.lua](../profiles/car.lua) as an example, at the top of the file the api version is defined and then required library files are included. -Then follows the `setup` function, which is called once when the profile is loaded. It returns a big hash table of configurations, specifying things like what speed to use for different way types. The configurations are used later in the various processing functions. Many adjustments can be done just by modifying this configuration table. +Then follows the @ref setup function, which is called once when the profile is loaded. It returns a big hash table of configurations, specifying things like what speed to use for different way types. The configurations are used later in the various processing functions. Many adjustments can be done just by modifying this configuration table. The setup function is also where you can do other setup, like loading an elevation data source if you want to consider that when processing ways. @@ -67,6 +72,7 @@ If you set rate = speed on all ways, the result will be fastest path routing. If you want to prioritize certain streets, increase the rate on these. ## Elements + ### api_version A profile should set `api_version` at the top of your profile. This is done to ensure that older profiles are still supported when the api changes. If `api_version` is not defined, 0 will be assumed. The current api version is 4. @@ -86,226 +92,77 @@ guidance.lua | Function for processing guidance attributes They all return a table of functions when you use `require` to load them. You can either store this table and reference its functions later, or if you need only a single function you can store that directly. -### setup() +### setup() {#setup} The `setup` function is called once when the profile is loaded and must return a table of configurations. It's also where you can do other global setup, like loading data sources that are used during processing. Note that processing of data is parallelized and several unconnected LUA interpreters will be running at the same time. The `setup` function will be called once for each. Each LUA interpreter will have its own set of globals. +@anchor properties The following global properties can be set under `properties` in the hash you return in the `setup` function: -Attribute | Type | Notes --------------------------------------|----------|---------------------------------------------------------------------------- -weight_name | String | Name used in output for the routing weight property (default `'duration'`) -weight_precision | Unsigned | Decimal precision of edge weights (default `1`) -left_hand_driving | Boolean | Are vehicles assumed to drive on the left? (used in guidance, default `false`) -use_turn_restrictions | Boolean | Are turn restrictions followed? (default `false`) -continue_straight_at_waypoint | Boolean | Must the route continue straight on at a via point, or are U-turns allowed? (default `true`) -max_speed_for_map_matching | Float | Maximum vehicle speed to be assumed in matching (in m/s) -max_turn_weight | Float | Maximum turn penalty weight -force_split_edges | Boolean | True value forces a split of forward and backward edges of extracted ways and guarantees that `process_segment` will be called for all segments (default `false`) - +Attribute | Type | Notes +------------------------------|----------|---------------------------------------------------------------------------- +weight_name | String | Name used in output for the routing weight property (default `"duration"`) +weight_precision | Unsigned | Decimal precision of edge weights (default `1`) +left_hand_driving | Boolean | Are vehicles assumed to drive on the left? (used in guidance, default `false`) +use_turn_restrictions | Boolean | Are turn restrictions followed? (default `false`) +continue_straight_at_waypoint | Boolean | Must the route continue straight on at a via point, or are U-turns allowed? (default `true`) +max_speed_for_map_matching | Float | Maximum vehicle speed to be assumed in matching (in m/s) +max_turn_weight | Float | Maximum turn penalty weight +force_split_edges | Boolean | True value forces a split of forward and backward edges of extracted ways and guarantees that `process_segment` will be called for all segments (default `false`) The following additional global properties can be set in the hash you return in the `setup` function: -Attribute | Type | Notes --------------------------------------|------------------|---------------------------------------------------------------------------- -excludable | Sequence of Sets | Determines which class-combinations are supported by the `exclude` option at query time. E.g. `Sequence{Set{"ferry", "motorway"}, Set{"motorway"}}` will allow you to exclude ferries and motorways, or only motorways. -classes | Sequence | Determines the allowed classes that can be referenced using `{forward,backward}_classes` on the way in the `process_way` function. -restrictions | Sequence | Determines which turn restrictions will be used for this profile. -suffix_list | Set | List of name suffixes needed for determining if "Highway 101 NW" the same road as "Highway 101 ES". -relation_types | Sequence | Determines which relations should be cached for processing in this profile. It contains relations types - -### process_node(profile, node, result, relations) -Process an OSM node to determine whether this node is an obstacle, if it can be passed at all and whether passing it incurs a delay. - -Argument | Description ----------|------------------------------------------------------- -profile | The configuration table you returned in `setup`. -node | The input node to process (read-only). -result | The output that you will modify. -relations| Storage of relations to access relations, where `node` is a member. - -The following attributes can be set on `result`: -(Note: for new code use the `obstacle_map`. - -Attribute | Type | Notes -----------------|---------|--------------------------------------------------------- -barrier | Boolean | Is it an impassable barrier? -traffic_lights | Boolean | Is it a traffic light (incurs delay in `process_turn`)? - -### Obstacle -A user type that represents an obstacle on the road or a place where you can turn -around. - -This may be a completely impassable obstacle like a barrier, a temporary obstacle like a -traffic light or a stop sign, or an obstacle that just slows you down like a -traffic_calming. The obstacle may be present in both directions or in one direction -only. - -This also represents a good turning point like a mini_roundabout, turning_loop, or -turning_circle. - -An object of this type is immutable once constructed. - -```lua -local obs = Obstacle.new( - obstacle_type.traffic_signals, - obstacle_direction.forward, - 2.5, - 0 -) -assert(obs.duration == 2.5) -``` +Attribute | Type | Notes +--------------------------------------|------------------|---------------------------------------------------------------------------- +excludable | Sequence of Sets | Determines which class-combinations are supported by the `exclude` option at query time. E.g. `Sequence{Set{"ferry", "motorway"}, Set{"motorway"}}` will allow you to exclude ferries and motorways, or only motorways. +classes | Sequence | Determines the allowed classes that can be referenced using `{forward,backward}_classes` on the way in the `process_way` function. +restrictions | Sequence | Determines which turn restrictions will be used for this profile. +suffix_list | Set | List of name suffixes needed for determining if "Highway 101 NW" the same road as "Highway 101 ES". +relation_types @anchor relation_types | Sequence | Determines which relation types should be stored for reference. Only these relation types will be queryable using the `relations` parameter in @ref process_node, @ref process_way, and @ref process_relation. -Member | Mode | Type | Notes -----------|-----------|--------------------|---------------------------------- -type | read-only | obstacle_type | eg. `obstacle_type.barrier` -direction | read-only | obstacle_direction | eg. `obstacle_direction.forward` -duration | read-only | float | The expected delay in seconds -weight | read-only | float | The weight - -#### obstacle_type -An enum with the following keys: - -Keys | -----------------| -none | -barrier | -traffic_signals | -stop | -give_way | -crossing | -traffic_calming | -mini_roundabout | -turning_loop | -turning_circle | - -#### obstacle_direction -An enum with the following keys: - -Keys | ----------| -none | -forward | -backward | -both | - -### obstacle_map -A global user type. It stores obstacles. - -The canonical workflow is: to store obstacles in `process_node()` and retrieve them in -`process_turn()`. - -Note: In the course of processing, between the `process_node()` stage and the -`process_turn()` stage, the extractor switches from using OSM nodes to using -internal nodes. Both types have different ids. You can only store OSM nodes and only -retrieve internal nodes. This implies that, in `process_node()`, you cannot retrieve an -obstacle you have just stored. - -#### obstacle_map:add(node, obstacle) -Call this function inside `process_node()` to register an obstacle on a node. You can -register as many different obstacles as you wish on any given node. It is your -responsibility to register the same obstacle only once. - -In a following step -- likely in `process_turn()` -- you can retrieve all obstacles -registered at any given node. This function works with OSM nodes. - -Argument | Type | Notes ----------|----------|-------------------------------------------- -node | OSMNode | The same node as passed to `process_node`. -obstacle | Obstacle | The obstacle - -Usage example: +### process_relation(profile, relation, relations) {#process_relation} -```lua -function process_node(profile, node, result, relations) - ... - obstacle_map:add(node, - Obstacle.new( - obstacle_type.traffic_signal, - obstacle_direction.forward, - 2, 0)) -end -``` +@note This function is called only if @ref pedestrian_areas "area meshing" is +configured. At present the only thing you can do here is to register areas for meshing. -#### obstacle_map:any(from, to, type) -Return true if there are any obstacles at node `to` when coming from node -`from` and having the type `type`. +The `process_relation` function is called for every relation in an early stage, before +@ref process_node and @ref process_way are being called. -You will likely call this function inside `process_turn()`. -Note that this works only with internal nodes, not with OSM nodes. +Argument | Type | Description +----------|------------|------------------------------------------- +profile | Properties | The @ref properties configured in `setup`. +relation | Relation | The relation to process. +relations | Relations | Contains the parent relations of `relation`. -```lua -bool obstacle_map:any(to) -bool obstacle_map:any(from, to) -bool obstacle_map:any(from, to, type) -``` - -Argument | Type | Notes ----------|---------------|------------------------------------------------------------------------------------- -from | Node | The leading node. Optional. -to | Node | The node with the obstacle. -type | obstacle_type | The obstacle type. Defaults to all types. May be a bitwise-or combination of types. -returns | bool | True if there are any obstacles satisfiying the given criteria. - -Usage examples: - -```lua -function process_turn(profile, turn) - if obstacle_map:any(turn.via) then - ... - end - if obstacle_map:any(turn.from, turn.via, obstacle_type.traffic_signal) then - turn.duration = turn.duration + 2 - end -end -``` +### process_node(profile, node, result, relations) {#process_node} +Process an OSM node to determine whether this node is an obstacle, if it can be passed at all and whether passing it incurs a delay. -#### obstacle_map:get(from, to, type) -This function retrieves all registered obstacles at node `to` when coming from the node -`from` and having the type `type`. +Argument | Type | Description +----------|------------|------------------------------------------- +profile | Properties | The @ref properties configured in `setup`. +node | Node | The input node to process (read-only). +result | | The output that you will modify. +relations | Relations | Contains the parent relations of `node`. -You will likely call this function inside `process_turn()`. -Note that this works only with internal nodes, not with OSM nodes. +@note For new code use ObstacleMap instead of `result`. -```lua -obstacle_map:get(to) -obstacle_map:get(from, to) -obstacle_map:get(from, to, type) -``` - -Argument | Type | Notes ----------|---------------|------------------------------------------------------------------------------------- -from | Node | The leading node. Optional. -to | Node | The node with the obstacle. -type | obstacle_type | The obstacle type. Defaults to all types. May be a bitwise-or combination of types. -returns | table | A table of `Obstacle`s. - -Usage examples: +The following attributes can be set on `result`: -```lua -function process_turn(profile, turn) - for _, obs in pairs(obstacle_map:get(turn.via)) do - if obs.type == obstacle_type.barrier then - turn.duration = turn.duration + obs.duration - end - end - for _, obs in pairs(obstacle_map:get( - turn.from, turn.via, obstacle_type.traffic_signal)) do - turn.duration = turn.duration + obs.duration - end -end -``` +Attribute | Type | Notes +----------------|---------|-------------------------------------------------------- +barrier | Boolean | Is it an impassable barrier? +traffic_lights | Boolean | Is it a traffic light (incurs delay in `process_turn`)? -### process_way(profile, way, result, relations) +### process_way(profile, way, result, relations) {#process_way} Given an OpenStreetMap way, the `process_way` function will either return nothing (meaning we are not going to route over this way at all), or it will set up a result hash. -Argument | Description ----------|------------------------------------------------------- -profile | The configuration table you returned in `setup`. -way | The input way to process (read-only). -result | The output that you will modify. -relations| Storage of relations to access relations, where `way` is a member. +Argument | Type | Description +----------|------------|------------------------------------------------- +profile | Properties | The @ref properties configured in `setup`. +way | Way | The input way to process (read-only). +result | | The output that you will modify. +relations | Relations | Contains the parent relations of `way`. Importantly it will set `result.forward_mode` and `result.backward_mode` to indicate the travel mode in each direction, as well as set `result.forward_speed` and `result.backward_speed` to integer values representing the speed for traversing the way. @@ -347,7 +204,7 @@ road_classification.road_priority_class | Enum | Guidance: order in priority road_classification.may_be_ignored | Boolean | Guidance: way is non-highway road_classification.num_lanes | Unsigned | Guidance: total number of lanes in way -### process_segment(profile, segment) +### process_segment(profile, segment) {#process_segment} The `process_segment` function is called for every segment of OSM ways. A segment is a straight line between two OSM nodes. On OpenStreetMap way cannot have different tags on different parts of a way. Instead you would split the way into several smaller ways. However many ways are long. For example, many ways pass hills without any change in tags. @@ -368,7 +225,7 @@ distance | Read | Float | Length of segment weight | Read/write | Float | Routing weight for this segment duration | Read/write | Float | Duration for this segment -### process_turn(profile, turn) +### process_turn(profile, turn) {#process_turn} The `process_turn` function is called for every possible turn in the network. Based on the angle and type of turn you assign the weight and duration of the movement. The following attributes can be read and set on the result in `process_turn`: @@ -409,10 +266,10 @@ weight | Read/write | Float | duration | Read/write | Float | Penalty to be applied for this turn (duration in deciseconds) -#### `from`, `via`, and `to` -Use these node IDs to retrieve obstacles. See: `obstacle_map:get`. +#### from, via, and to +Use these node IDs to retrieve obstacles. See: @ref ObstacleMap::get. -#### `source_road`, `target_road`, `roads_on_the_right`, and `roads_on_the_left` +#### source_road, target_road, roads_on_the_right, and roads_on_the_left The information of `source_road`, `target_road`, `roads_on_the_right`, and `roads_on_the_left` that can be read are as follows: @@ -455,7 +312,7 @@ When turning from `a` to `b` via `x`, Note that indices of arrays in lua are 1-based. -#### `highway_turn_classification` and `access_turn_classification` +#### highway_turn_classification and access_turn_classification When setting appropriate turn weights and duration, information about the highway and access tags of roads that are involved in the turn are necessary. The lua turn function `process_turn` does not have access to the original osrm tags anymore. However, `highway_turn_classification` and `access_turn_classification` can be set during setup. The classification set during setup can be later used in `process_turn`. **Example** @@ -495,7 +352,7 @@ Forks can be emitted between roads of similar priority category only. Obvious ch ### Using raster data OSRM has built-in support for loading an interpolating raster data in ASCII format. This can be used e.g. for factoring in elevation when computing routes. -Use `raster:load()` in your `setup` function to load data and store the source in your configuration hash: +Use `raster:load()` in your @ref setup function to load data and store the source in your configuration hash: ```lua function setup() @@ -551,3 +408,5 @@ There are a few helper functions defined in the global scope that profiles can u - `trimLaneString` - `applyAccessTokens` - `canonicalizeStringList` +- @ref is_true +- @ref is_false diff --git a/features/bicycle/alley.feature b/features/bicycle/alley.feature index db7b624a7e..968401b3d3 100644 --- a/features/bicycle/alley.feature +++ b/features/bicycle/alley.feature @@ -27,8 +27,7 @@ Feature: Bicycle - Route around alleys | cf | residential | | When I route I should get - | from | to | a:nodes | weight | # | - | a | f | 1:2:3:6 | 196.2 | Avoids d,e,f | - | a | e | 1:2:5 | 172.2 | Take the alley b,e if neccessary | - | d | f | 4:1:2:3:6 | 248.4 | Avoids the alley d,e,f | - + | from | to | a:nodes | weight | # | + | a | f | abcf | 196.2 | Avoids d,e,f | + | a | e | abe | 172.2 | Take the alley b,e if neccessary | + | d | f | dabcf | 248.4 | Avoids the alley d,e,f | diff --git a/features/car/traffic_light_penalties.feature b/features/car/traffic_light_penalties.feature index 5311599b6d..340cd756b6 100644 --- a/features/car/traffic_light_penalties.feature +++ b/features/car/traffic_light_penalties.feature @@ -294,8 +294,8 @@ Feature: Car - Handle traffic lights When I route I should get | from | to | route | speed | weights | time | distances | a:datasources | a:nodes | a:speed | a:duration | a:weight | - | a | c | abc,abc | 60 km/h | 24.2,0 | 24.2s | 400m,0m | 1:0 | 1:2:3 | 18:18 | 11.1:11.1 | 11.1:11.1 | - | c | a | abc,abc | 60 km/h | 24.2,0 | 24.2s | 400m,0m | 0:1 | 3:2:1 | 18:18 | 11.1:11.1 | 11.1:11.1 | + | a | c | abc,abc | 60 km/h | 24.2,0 | 24.2s | 400m,0m | 1:0 | abc | 18:18 | 11.1:11.1 | 11.1:11.1 | + | c | a | abc,abc | 60 km/h | 24.2,0 | 24.2s | 400m,0m | 0:1 | cba | 18:18 | 11.1:11.1 | 11.1:11.1 | @traffic @@ -326,8 +326,8 @@ Feature: Car - Handle traffic lights When I route I should get | from | to | route | speed | weights | time | distances | a:datasources | a:nodes | a:speed | a:duration | a:weight | - | a | c | abc,abc | 60 km/h | 24.2,0 | 24.2s | 400m,0m | 1:0 | 1:2:3 | 18:18 | 11.1:11.1 | 11.1:11.1 | - | c | a | abc,abc | 65 km/h | 22.2,0 | 22.2s | 400m,0m | 0:1 | 3:2:1 | 18:18 | 11.1:11.1 | 11.1:11.1 | + | a | c | abc,abc | 60 km/h | 24.2,0 | 24.2s | 400m,0m | 1:0 | abc | 18:18 | 11.1:11.1 | 11.1:11.1 | + | c | a | abc,abc | 65 km/h | 22.2,0 | 22.2s | 400m,0m | 0:1 | cba | 18:18 | 11.1:11.1 | 11.1:11.1 | @traffic @@ -358,8 +358,8 @@ Feature: Car - Handle traffic lights When I route I should get | from | to | route | speed | weights | time | distances | a:datasources | a:nodes | a:speed | a:duration | a:weight | - | a | c | abc,abc | 65 km/h | 22.2,0 | 22.2s | 400m,0m | 1:0 | 1:2:3 | 18:18 | 11.1:11.1 | 11.1:11.1 | - | c | a | abc,abc | 60 km/h | 24.2,0 | 24.2s | 400m,0m | 0:1 | 3:2:1 | 18:18 | 11.1:11.1 | 11.1:11.1 | + | a | c | abc,abc | 65 km/h | 22.2,0 | 22.2s | 400m,0m | 1:0 | abc | 18:18 | 11.1:11.1 | 11.1:11.1 | + | c | a | abc,abc | 60 km/h | 24.2,0 | 24.2s | 400m,0m | 0:1 | cba | 18:18 | 11.1:11.1 | 11.1:11.1 | Scenario: Car - Traffic signal straight direction with edge compression diff --git a/features/foot/area_mesh.feature b/features/foot/area_mesh.feature new file mode 100644 index 0000000000..e3b0c8b071 --- /dev/null +++ b/features/foot/area_mesh.feature @@ -0,0 +1,126 @@ +@routing @foot @area +Feature: Foot - Pedestrian areas + + Background: + Given the profile "foot_area" + Given a grid size of 50 meters + Given the extract extra arguments "--verbosity DEBUG" + Given the query options + | annotations | nodes | + + Scenario: Foot - Route across a closed way area + Given the node map + """ + e-a---b-f + | | + h-d---c-g + """ + + And the ways + | nodes | highway | area | name | + | abcda | pedestrian | yes | Plaza | + | ea | pedestrian | | A | + | bf | pedestrian | | B | + | hd | pedestrian | | C | + | cg | pedestrian | | D | + + When I route I should get + | from | to | a:nodes | route | + | e | g | eacg | A,Plaza,D | + | g | e | gcae | D,Plaza,A | + | h | f | hdbf | C,Plaza,B | + | f | h | fbdh | B,Plaza,C | + + Scenario: Foot - Do not route across a closed way w/o area + Given the node map + """ + e-a--b-f + | \ + h-d---c-g + """ + + And the ways + | nodes | highway | + | abcda | pedestrian | + | ea | pedestrian | + | bf | pedestrian | + | hd | pedestrian | + | cg | pedestrian | + + When I route I should get + | from | to | a:nodes | + | e | g | eabcg | + | g | e | gcbae | + | h | f | hdabf | + | f | h | fbadh | + + Scenario: Foot - Route across a multipolygon area + Given the node map + """ + e-a-------b-f + | u-v | + | x-w | + h-d-------c-g + """ + + And the ways + | nodes | highway | name | + | abcda | (nil) | | + | uvwxu | (nil) | | + | ea | pedestrian | A | + | bf | pedestrian | B | + | hd | pedestrian | C | + | cg | pedestrian | D | + + And the relations + | type | highway | name | way:outer | way:inner | + | multipolygon | pedestrian | Plaza | abcda | uvwxu | + + When I route I should get + | from | to | a:nodes | route | + | e | g | eavcg | A,Plaza,D | + | g | e | gcvae | D,Plaza,A | + | f | h | fbwdh | B,Plaza,C | + | h | f | hdwbf | C,Plaza,B | + + Scenario: Foot - Route across a complex multipolygon area + Given the node map + """ + g-a---------------b-h + | z-y | + | | | | + l-f | | v--u | + | w-x | | | + | | | c-i + | | | | + | s--t | + k-e---------------d-j + """ + + And the ways + | nodes | highway | + | abcdefa | (nil) | + | zwxyz | (nil) | + | vstuv | (nil) | + | ag | pedestrian | + | bh | pedestrian | + | ci | pedestrian | + | dj | pedestrian | + | ek | pedestrian | + | fl | pedestrian | + + And the relations + | type | highway | way:outer | way:inner | + | multipolygon | pedestrian | abcdefa | vstuv,zwxyz | + + When I route I should get + | from | to | a:nodes | + | g | h | gabh | + | g | i | gauci | + | g | j | gaysdj | + | l | h | lfzbh | + | l | i | lfwxvuci | + | l | j | lfwsdj | + | k | h | kebh | + | k | i | ketci | + | k | j | kedj | diff --git a/features/lib/osm.js b/features/lib/osm.js index 18d688cfa5..fe5314628f 100644 --- a/features/lib/osm.js +++ b/features/lib/osm.js @@ -80,6 +80,7 @@ class DB { this.relations.forEach((r) => { var relation = xml.ele('relation', { id: r.id, + version: 1, user: r.OSM_USER, timestamp: r.OSM_TIMESTAMP, uid: r.OSM_UID diff --git a/features/support/data.js b/features/support/data.js index e124407150..bd08ffc8e0 100644 --- a/features/support/data.js +++ b/features/support/data.js @@ -117,6 +117,7 @@ module.exports = function () { var node = new OSM.Node(id, this.OSM_USER, this.OSM_TIMESTAMP, this.OSM_UID, lon, lat, {name: name}); this.OSMDB.addNode(node); this.nameNodeHash[name] = node; + this.idNodeHash[id] = node; }; this.addLocation = (name, lon, lat) => { @@ -137,6 +138,16 @@ module.exports = function () { return fromNode; }; + this.findNodeById = (id) => { + return this.idNodeHash[id.toString()]; + }; + + this.nodeNameById = (id) => { + const fromNode = this.findNodeById(id) || {}; + const tags = fromNode.tags || {}; + return tags.name || '?'; + }; + // find a node based on an array containing lon/lat this.findNodeByLocation = (node_location) => { var searched_coordinate = new classes.Location(node_location[0],node_location[1]); @@ -167,6 +178,7 @@ module.exports = function () { this.resetOSM = () => { this.OSMDB.clear(); this.nameNodeHash = {}; + this.idNodeHash = {}; this.locationHash = {}; this.shortcutsHash = {}; this.nameWayHash = {}; diff --git a/features/support/shared_steps.js b/features/support/shared_steps.js index 54f74a3df7..eb1455ea19 100644 --- a/features/support/shared_steps.js +++ b/features/support/shared_steps.js @@ -178,6 +178,10 @@ module.exports = function () { if (annotation && !annotation[a_type]) return cb(new Error('Annotation not found in response', a_type)); got[k] = annotation && annotation[a_type] || ''; + // replaces node ids by their names: "0:1:2:3" -> "abcd" + if (a_type == 'nodes') { + got[k] = got[k].split(':').map(this.nodeNameById).join(''); + } } else if (k.match(/^am:/)) { let a_type = k.slice(3); if (metadata_whitelist.indexOf(a_type) == -1) diff --git a/features/testbot/annotations.feature b/features/testbot/annotations.feature index cff8461b24..f7c6dc4f5b 100644 --- a/features/testbot/annotations.feature +++ b/features/testbot/annotations.feature @@ -23,9 +23,9 @@ Feature: Annotations When I route I should get | from | to | route | a:speed | a:weight | a:nodes | - | h | j | hk,jk,jk | 6.7:6.7 | 15:15 | 1:4:3 | - | i | m | il,lm,lm | 6.7:6.7 | 15:15 | 2:5:6 | - | j | m | jk,lm | 6.7:6.7:6.7 | 15:15:15 | 3:4:5:6 | + | h | j | hk,jk,jk | 6.7:6.7 | 15:15 | hkj | + | i | m | il,lm,lm | 6.7:6.7 | 15:15 | ilm | + | j | m | jk,lm | 6.7:6.7:6.7 | 15:15:15 | jklm | Scenario: There should be different forward/reverse datasources @@ -114,5 +114,5 @@ Feature: Annotations | bearings | 90,5;180,5 | When I route I should get - | from | to | route | a:speed | a:distance | a:duration | a:nodes | - | a | c | abc,abc | 10:10 | 249.9876189:299.962882 | 25:30 | 1:2:3 | + | from | to | route | a:speed | a:distance | a:duration | a:nodes | + | a | c | abc,abc | 10:10 | 249.9876189:299.962882 | 25:30 | abc | diff --git a/features/testbot/traffic_speeds.feature b/features/testbot/traffic_speeds.feature index db3b2372b1..f6e7248af9 100644 --- a/features/testbot/traffic_speeds.feature +++ b/features/testbot/traffic_speeds.feature @@ -136,13 +136,13 @@ Feature: Traffic - speeds When I route I should get | from | to | route | speed | weights | a:datasources | a:speed | a:nodes| - | a | b | fb,fb | 36 km/h | 328.9,0 | 0 | 10 | 6:2 | - | a | c | fb,bc,bc | 30 km/h | 328.9,737.2,0 | 0:1 | 10:7.5 | 6:2:3 | - | b | c | bc,bc | 27 km/h | 737.2,0 | 1 | 7.5 | 2:3 | - | a | d | fb,df,df | 36 km/h | 139.8,486.8,0 | 0:0 | 10:10 | 2:6:4 | - | d | c | dc,dc | 36 km/h | 955.4,0 | 0 | 10 | 4:3 | - | g | b | fb,fb | 36 km/h | 164.4,0 | 0 | 10 | 6:2 | - | a | g | fb,fb | 36 km/h | 164.5,0 | 0 | 10 | 6:2 | + | a | b | fb,fb | 36 km/h | 328.9,0 | 0 | 10 | fb | + | a | c | fb,bc,bc | 30 km/h | 328.9,737.2,0 | 0:1 | 10:7.5 | fbc | + | b | c | bc,bc | 27 km/h | 737.2,0 | 1 | 7.5 | bc | + | a | d | fb,df,df | 36 km/h | 139.8,486.8,0 | 0:0 | 10:10 | bfd | + | d | c | dc,dc | 36 km/h | 955.4,0 | 0 | 10 | dc | + | g | b | fb,fb | 36 km/h | 164.4,0 | 0 | 10 | fb | + | a | g | fb,fb | 36 km/h | 164.5,0 | 0 | 10 | fb | Scenario: Verify that negative values cause an error, they're not valid at all diff --git a/include/extractor/area/area_manager.hpp b/include/extractor/area/area_manager.hpp new file mode 100644 index 0000000000..5a839f4c74 --- /dev/null +++ b/include/extractor/area/area_manager.hpp @@ -0,0 +1,83 @@ +#ifndef OSRM_EXTRACTOR_AREA_AREA_MANAGER_HPP +#define OSRM_EXTRACTOR_AREA_AREA_MANAGER_HPP + +#include "extractor/extraction_relation.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace osrm::extractor::area +{ + +/** + * @brief A registry for area objects. + * + * This class backs the `area_manager` global variable in all LUA scripts. Call + * AreaManager::way() if you want to turn a closed way into an area, call + * AreaMananger::relation() if you want to turn a multipolygon relation into an area. + */ +class AreaManager + : public osmium::relations::RelationsManager +{ + osmium::area::AssemblerConfig m_assembler_config; + using MutexType = tbb::mutex; + /** Mutex to protect relations_manager */ + MutexType mutex; + + using relation_ids = std::vector; + std::string algorithm_name; + bool enabled{false}; + + public: + AreaManager(ExtractionRelationContainer &c) : relations_stash{c} + { + m_assembler_config.debug_level = 0; + }; + + void init(const char *algorithm_name); + void way(const osmium::Way &way); + void relation(const osmium::Relation &relation); + void prepare_for_lookup(); + bool is_registered_closed_way(osmium::object_id_type way_id) const; + relation_ids get_relations_for_node(const osmium::Node &) const; + relation_ids get_relations_for_way(const osmium::Way &) const; + + void complete_relation(const osmium::Relation &relation); + void after_way(const osmium::Way &way); + bool is_enabled() { return enabled; } + + std::size_t number_of_ways{0}; + std::size_t number_of_relations{0}; + + /** + * This collects the closed ways and the outer ring members of the relations we have + * registered for meshing. They are also used for collecting the intersecting ways. + */ + tbb::concurrent_vector registered_closed_ways; + /** Map of way_id -> rel_id: if way is a member of rel */ + tbb::concurrent_map m_way_relation; + /** Map of node_id -> rel_id: if node is a member of rel */ + tbb::concurrent_map m_node_relation; + /** + * @brief Contains every node of every way we have seen. + * + * This is used later to find all intersecting ways. + */ + tbb::concurrent_set node_ids; + /** + * @brief Storage for relations + * + * Registered relations are also stored here. + */ + ExtractionRelationContainer &relations_stash; +}; + +} // namespace osrm::extractor::area + +#endif // AREA_MANAGER.HPP diff --git a/include/extractor/area/area_mesher.hpp b/include/extractor/area/area_mesher.hpp new file mode 100644 index 0000000000..e724584260 --- /dev/null +++ b/include/extractor/area/area_mesher.hpp @@ -0,0 +1,106 @@ +#ifndef OSRM_EXTRACTOR_AREA_AREA_MESHER_HPP +#define OSRM_EXTRACTOR_AREA_AREA_MESHER_HPP + +#include "extractor/extraction_relation.hpp" +#include "typedefs.hpp" + +#include "extractor/extraction_containers.hpp" +#include "util/typedefs.hpp" + +#include +#include + +#include +#include + +namespace osmium +{ +namespace memory +{ +class Buffer; +}; + +}; // namespace osmium + +namespace osrm::extractor::area +{ + +class AreaManager; + +/** + * @brief A class that "meshes" areas + * + * This class "meshes" an area by creating OSM ways for each shortest path between all + * pairs of entry points to the area. It first generates a @ref VisibilityGraph + * "visibility map", then uses @ref Dijkstra "Dijkstra's shortest-path algorithm" to + * reduce the number of edges. The generated ways are returned in an + * osmium::memory::Buffer. + */ +class AreaMesher +{ + public: + void init(const AreaManager &manager, const extractor::ExtractionContainers &containers); + OsmiumMultiPolygon area_builder(const osmium::Area &area); + NodeRefSet get_entry_points(const OsmiumPolygon &poly); + NodeRefSet get_obstacle_vertices(const OsmiumPolygon &poly); + void mesh_area(const osmium::Area &area, + osmium::memory::Buffer &out_buffer, + ExtractionRelationContainer &relations); + + void mesh_buffer(const osmium::memory::Buffer &in_buffer, + osmium::memory::Buffer &out_buffer, + ExtractionRelationContainer &relations); + osmium::memory::Buffer read(); + + int added_ways{0}; + /** Refuse to mesh more vertices */ + size_t max_vertices{100}; + + private: + using NodeIDVector = std::vector; + using WayNodeIDOffsets = std::vector; + using WayIDVector = std::vector; + + std::set run_dijkstra(const OsmiumPolygon &poly, + std::set &vis_map, + const NodeRefSet &entry_points); + + osmium::object_id_type get_relations(const osmium::Area &area, + const ExtractionRelationContainer &relations); + + std::unordered_multimap node_id2way_index; + osmium::object_id_type next_way_id{(1ULL << 34) - 1}; // see: packed_osm_ids.hpp +#ifndef NDEBUG + osmium::object_id_type next_node_id{(1ULL << 34) - 1}; // see: packed_osm_ids.hpp +#endif +}; + +/** + * @brief Implements a reader for a buffer. + * + * This class allows you to read from a osmium::memory::Buffer in the same way you + * would read an OSM file using a osmium::io::Reader. + */ +class BufferReader +{ + osmium::memory::Buffer::const_iterator iter; + const osmium::memory::Buffer::const_iterator end; + enum class status + { + okay = 0, // normal reading + error = 1, // some error occurred while reading + closed = 2, // close() called + eof = 3 // eof of file was reached without error + }; + status m_status{status::okay}; + + public: + BufferReader(const osmium::memory::Buffer &in_buffer) + : iter{in_buffer.cbegin()}, end{in_buffer.cend()} {}; + + osmium::memory::Buffer read(); +}; + +} // namespace osrm::extractor::area + +#endif // AREA_MESHER.HPP diff --git a/include/extractor/area/dijkstra.hpp b/include/extractor/area/dijkstra.hpp new file mode 100644 index 0000000000..c12d988d14 --- /dev/null +++ b/include/extractor/area/dijkstra.hpp @@ -0,0 +1,167 @@ +#ifndef OSRM_EXTRACTOR_AREA_DIJKSTRA_HPP +#define OSRM_EXTRACTOR_AREA_DIJKSTRA_HPP + +#include "index_priority_queue.hpp" + +#include +#include +#include +#include + +namespace osrm::extractor::area +{ + +/** + * @brief Implements the Dijkstra shortest-path algorithm. + * + * @tparam vertex_t The type of a vertex. + */ +template class Dijkstra +{ + struct Edge + { + Edge(size_t other, double weight) + : other{other}, weight{weight} {}; // for macos-x64 clang-14 + size_t other; + double weight; + }; + std::vector vertices; + std::map seen_vertices; + + std::vector distances; + std::vector predecessors; + std::vector> adj; + + /** + * @brief Initialize the data structures before each run. + */ + void init_data() + { + double inf = std::numeric_limits::infinity(); + distances.resize(vertices.size()); + predecessors.resize(vertices.size()); + for (size_t i = 0; i < vertices.size(); ++i) + { + distances[i] = inf; + predecessors[i] = i; + } + } + + public: + /** + * @brief Add one vertex to the graph. + * + * If a vertex is already present it will not be inserted again and the index of the + * present vertex will be returned. + * + * @param v The vertex to add + * @return The index of the vertex. + */ + size_t add_vertex(const vertex_t &v) + { + size_t pos = vertices.size(); + auto [_, inserted] = seen_vertices.emplace(v, pos); + if (inserted) + { + vertices.push_back(v); + return pos; + } + return seen_vertices.at(v); + }; + + /** + * @brief Return the index of the vertex. + * + * @param v The vertex + * @return size_t The index of the vertex. + */ + size_t index_of(const vertex_t &v) { return seen_vertices.at(v); } + + /** + * @brief Get the vertex object + * + * @param i The index of the vertex + * @return const vertex_t& The vertex + */ + const vertex_t &get_vertex(size_t i) { return vertices.at(i); }; + + /** + * @brief Add one edge to the graph. + * + * It is the caller's responsibility to avoid inserting duplicate edges. + * + * @param u The first vertex. + * @param v The second vertex. + * @param weight The weight of the edge. + */ + void add_edge(const vertex_t &u, const vertex_t &v, double weight) + { + size_t iu = add_vertex(u); + size_t iv = add_vertex(v); + adj.resize(vertices.size()); + adj[iu].emplace_back(iv, weight); + adj[iv].emplace_back(iu, weight); + } + + const std::vector &get_predecessors() { return predecessors; } + const std::vector &get_distances() { return distances; } + + /** + * @brief Run the Dijkstra shortest-path algorithm starting at vertex s. + * + * After this function completes the predecessor for each vertex will be stored in + * {@code predecessors} and the distance from {@code s} to each vertex will be + * stored in {@code distances}. + * + * @param s The index of the "start" vertex. + */ + void run(size_t s) + { + init_data(); + + IndexPriorityQueue pq(vertices.size(), + [this](size_t u, size_t v) -> bool + { return distances[u] < distances[v]; }); + + distances[s] = 0; + pq.insert(s); + + while (!pq.empty()) + { + size_t u = pq.pop(); + double dist_u = distances[u]; + for (Edge e : adj[u]) + { + size_t v = e.other; + if (dist_u + e.weight < distances[v]) + { + distances[v] = dist_u + e.weight; + predecessors[v] = u; + pq.insert_or_decrease(v); + } + } + } + } + + /** + * @brief Return the number of vertices. + */ + size_t num_vertices() { return vertices.size(); } + + /** + * @brief Return the number of edges. + */ + size_t num_edges() + { + size_t n = 0; + for (auto &a : adj) + { + n += a.size(); + } + return n / 2; + } +}; + +} // namespace osrm::extractor::area + +#endif diff --git a/include/extractor/area/index_priority_queue.hpp b/include/extractor/area/index_priority_queue.hpp new file mode 100644 index 0000000000..b49793d80b --- /dev/null +++ b/include/extractor/area/index_priority_queue.hpp @@ -0,0 +1,227 @@ +#ifndef OSRM_EXTRACTOR_AREA_INDEX_PRIORITY_QUEUE_HPP +#define OSRM_EXTRACTOR_AREA_INDEX_PRIORITY_QUEUE_HPP + +#include "util/log.hpp" + +#include +#include +#include +#include + +namespace osrm::extractor::area +{ + +/** + * @brief An indexed priority queue with key update operations. + * + * Usage: Store your data items in a vector external to this class. Instantiate this + * class with the size of that vector and a function that compares two items given their + * indices. Manipulate items in this queue by their index in the vector. + * + * By using an appropriate compare function you can build either a min-queue or a + * max-queue. To build a min-queue you must pass a function that behaves like + * {@code std::less} and to build a max-queue you must pass a function that behaves like + * {@code std::greater}. + * + * @code + * std::vector distances = {69, 42, ...}; + * IndexPriorityQueue min_pq( + * distances.size(), + * [&distances](size_t u, size_t v) { return distances[u] < distances[v]; } + * ); + * min_pq.insert(0); // 69 + * min_pq.insert(1); // 42 + * assert(min_pq.pop() == 1) // 42 + * assert(min_pq.pop() == 0) // 69 + * @endcode + * + * Adapted from *Sedgewick and Wayne. Algorithms, Fourth Edition. Addison Wesley. 2011.* + * Chapter 2.4 + */ +template class IndexPriorityQueue +{ + public: + /** + * @brief Construct a new IndexPriorityQueue object + * + * @param size The number of items in the external vector. + * @param comp A function with signature {@code bool(size_t u, size_t v)} that + * compares the item referenced by {@code u} with the item referenced by + * {@code v}. To construct a priority queue that has the lowest item on + * top the function should behave like {@code std::less}. + */ + IndexPriorityQueue(size_t size, comp_t comp) : vector_size{size}, comp{comp} + { + pq.resize(size + 1); + qp.resize(size + 1); + for (size_t i = 0; i <= size; ++i) + { + qp[i] = none; + } + }; + + /** + * @brief Insert the item with the index {@code i}. + * + * @param i The index of the item + */ + void insert(size_t i) + { + assert(!contains(i)); + // util::Log(logDEBUG) << "Heap::insert(" << i << ")"; + ++n; + qp[i] = n; + pq[n] = i; + swim(n); + } + + /** + * @brief Remove the item on top of the queue and return its index. + * + * @return size_t The index of the item on top. + */ + size_t pop() + { + assert(!empty()); + size_t min = pq[1]; + exchange(1, n--); + sink(1); + qp[min] = none; + pq[n + 1] = none; + // util::Log(logDEBUG) << "Heap::pop() yields " << min; + return min; + } + + /** + * @brief Remove the item with index {@code i}. + * + * @param i The index of the item to remove + */ + void remove(size_t i) + { + size_t index = qp[i]; + exchange(index, n--); + swim(index); + sink(index); + qp[i] = none; + } + + /** + * @brief Adjust the heap after the item at index {@code i} had its key decreased. + * + * @param i The index of the item + */ + void decrease(size_t i) + { + // util::Log(logDEBUG) << "Heap::decrease(" << i << ")"; + assert(contains(i)); + swim(qp[i]); + } + + /** + * @brief Adjust the heap after the item at index {@code i} had its key increased. + * + * @param i The index of the item + */ + void increase(size_t i) { sink(qp[i]); } + + /** + * @brief Adjust the heap after the item at index {@code i} had its key updated. + * + * @param i The index of the item + */ + void update(size_t i) + { + swim(qp[i]); + sink(qp[i]); + } + + /** + * @brief Insert or decreas the item at index {@code i}. + * + * @param i The index of the item + */ + void insert_or_decrease(size_t i) + { + if (contains(i)) + { + decrease(i); + } + else + { + insert(i); + } + } + + /** + * @brief Return the index of the item on top. + */ + size_t top() { return pq[1]; } + + /** + * @brief Return true if {@code i} is a valid index. + */ + bool contains(size_t i) { return qp[i] != none; } + + /** + * @brief Return true if the queue is empty. + */ + + bool empty() { return n == 0; } + /** + * @brief Return the number of items in the queue. + */ + size_t size() { return n; } + + private: + const size_t vector_size; + comp_t comp; + + /** + * Binary heap of the indexes into an external vector of items. + * + * The index is 1-based (for easier math). + * {@code external_vector[pq[1]]} is the element on top of the queue. + */ + std::vector pq{}; + /** Inverse of pq: qp[pq[i]] = pq[qp[i]] = i */ + std::vector qp{}; + /** The number of items on the heap */ + size_t n{0}; + + const size_t none{~0UL}; + + bool compare(size_t i, size_t j) { return comp(pq[j], pq[i]); } + + void swim(size_t i) + { + while (i > 1 && compare(i / 2, i)) + { + exchange(i, i / 2); + i = i / 2; + } + } + void sink(size_t i) + { + while (i * 2 <= n) + { + size_t j = i * 2; + if (j < n && compare(j, j + 1)) + ++j; + if (!compare(i, j)) + break; + exchange(i, j); + i = j; + } + } + void exchange(size_t i, size_t j) + { + std::swap(pq[i], pq[j]); + qp[pq[i]] = i; + qp[pq[j]] = j; + } +}; + +} // namespace osrm::extractor::area + +#endif diff --git a/include/extractor/area/typedefs.hpp b/include/extractor/area/typedefs.hpp new file mode 100644 index 0000000000..1e0eabe922 --- /dev/null +++ b/include/extractor/area/typedefs.hpp @@ -0,0 +1,134 @@ +#ifndef OSRM_EXTRACTOR_AREA_TYPEDEFS_HPP +#define OSRM_EXTRACTOR_AREA_TYPEDEFS_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace osrm::extractor::area +{ + +namespace bg = boost::geometry; + +using OsmiumPolygon = bg::model::polygon; +using OsmiumMultiPolygon = bg::model::multi_polygon; +using NodeRefSet = std::set; + +/** + * @brief A segment between two nodes. + */ +struct OsmiumSegment : public std::pair +{ + OsmiumSegment(const osmium::NodeRef &first, const osmium::NodeRef &second) + { + if (first.ref() < second.ref()) + { + this->first = first; + this->second = second; + } + else + { + this->first = second; + this->second = first; + } + } +}; + +} // namespace osrm::extractor::area + +// +// The following traits adapt the point-ish types we use to boost::geometry. +// + +namespace boost +{ + +namespace geometry::traits +{ + +using namespace osrm::extractor::area; + +// osmium::NodeRef + +template <> struct tag +{ + using type = point_tag; // osmium::NodeRef represents a point +}; +template <> struct dimension : boost::mpl::int_<2> // it has 2 dimensions +{ +}; +template <> struct coordinate_type +{ + using type = double; // it actually stores the value multiplied by + // (double)osmium::detail::coordinate_precision in an int32_t + // but this is what we retrieve +}; +template <> struct coordinate_system +{ + using type = bg::cs::spherical_equatorial; // it is spherical +}; + +template <> struct access +{ + static inline double get(const osmium::NodeRef &p) { return p.location().lon_without_check(); } + static inline void set(osmium::NodeRef &p, double value) { p.location().set_lon(value); } +}; + +template <> struct access +{ + static inline double get(const osmium::NodeRef &p) { return p.location().lat_without_check(); } + static inline void set(osmium::NodeRef &p, double value) { p.location().set_lat(value); } +}; + +// osmium::OuterRing + +template <> struct tag +{ + using type = ring_tag; // osmium::OuterRing represents a ring type +}; +template <> struct point_order +{ + static const order_selector value = counterclockwise; +}; +template <> struct closure +{ + static const closure_selector value = closed; +}; + +// osmium::InnerRing + +template <> struct tag +{ + using type = ring_tag; // osmium::InnerRing represents a ring type +}; +template <> struct point_order +{ + static const order_selector value = clockwise; +}; +template <> struct closure +{ + static const closure_selector value = closed; +}; + +} // namespace geometry::traits + +template <> struct range_value +{ + using type = osmium::NodeRef; // it is a ring of NodeRefs +}; +template <> struct range_value +{ + using type = osmium::NodeRef; // it is a ring of NodeRefs +}; + +} // namespace boost + +#endif // OSRM_EXTRACTOR_AREA_TYPEDEFS_HPP diff --git a/include/extractor/area/util.hpp b/include/extractor/area/util.hpp new file mode 100644 index 0000000000..d5134deb66 --- /dev/null +++ b/include/extractor/area/util.hpp @@ -0,0 +1,253 @@ +#ifndef OSRM_EXTRACTOR_AREA_UTIL_HPP +#define OSRM_EXTRACTOR_AREA_UTIL_HPP + +#include +#include +#include +#include +#include + +namespace osrm::extractor::area +{ +namespace bg = boost::geometry; + +/** + * @brief Return twice the area enclosed by the three given points. + * + * The result is: + * + * - positive if a, b, c form a counter-clockwise circuit, + * - negative if they form a clockwise circuit, and + * - 0 if they are collinear. + */ +template double area2(const TPoint *a, const TPoint *b, const TPoint *c) +{ + return ((bg::get<0>(*b) - bg::get<0>(*a)) * (bg::get<1>(*c) - bg::get<1>(*a))) - + ((bg::get<0>(*c) - bg::get<0>(*a)) * (bg::get<1>(*b) - bg::get<1>(*a))); +} +/** + * @brief Return true if the point c is strictly to the left of the line formed by + * points a and b. + */ +template bool left(const TPoint *a, const TPoint *b, const TPoint *c) +{ + return area2(a, b, c) > 0; +} +/** + * @brief Return true if the point c is to the left of the line formed by points a and b + * or on that line. + */ +template bool leftOrOn(const TPoint *a, const TPoint *b, const TPoint *c) +{ + return area2(a, b, c) >= 0; +} +/** + * @brief Return true if the point c is strictly to the right of the line formed by + * points a and b. + */ +template bool right(const TPoint *a, const TPoint *b, const TPoint *c) +{ + return area2(a, b, c) < 0; +} +/** + * @brief Return true if the point c is to the right of the line formed by points a and + * b or on that line. + */ +template bool rightOrOn(const TPoint *a, const TPoint *b, const TPoint *c) +{ + return area2(a, b, c) <= 0; +} +/** + * @brief Return true if points a, b, and c are collinear. + */ +template bool collinear(const TPoint *a, const TPoint *b, const TPoint *c) +{ + return area2(a, b, c) == 0; +} + +/** + * @brief Calculate segment-segment or ray-segment intersection. + * + * If ray_segment is false, the function returns true if the segment between a and + * b intersects the segment between c and d. + * + * If ray_segment is true, the function returns true if a ray from a through b intersects + * the segment between c and d. + * + * The intersection point is returned in i if i is not null. + * + * The intersection test does not include endpoints. To include endpoints, use @ref + * intersect_closed(). + * + * Adapted from: *O'Rourke. Computational Geometry in C. 2nd ed. Cambridge 1998* Code 7.2 + * + * @tparam TPoint The point type. See: boost::geometry. + * @tparam TComp The comparison function to use. + * @param a The first point of the first segment or the origin of the ray. + * @param b The second point of the first segment or the ray. + * @param c The first point of the second segment. + * @param d The second point of the second segment. + * @param i Returns the intersection point if not `nullptr`. + * @param ray_segment If true calculate the ray-segment intersection. + * @return True if an intersection was found. + */ +template > +bool intersect(const TPoint *a, + const TPoint *b, + const TPoint *c, + const TPoint *d, + TPoint *i = nullptr, + bool ray_segment = false) +{ + using Coord = double; + Coord num, denom; + auto comp = TComp{}; + + // clang-format off + + denom = bg::get<0>(*a) * (Coord)(bg::get<1>(*d) - bg::get<1>(*c)) + + bg::get<0>(*b) * (Coord)(bg::get<1>(*c) - bg::get<1>(*d)) + + bg::get<0>(*d) * (Coord)(bg::get<1>(*b) - bg::get<1>(*a)) + + bg::get<0>(*c) * (Coord)(bg::get<1>(*a) - bg::get<1>(*b)); + + // if denom is zero the ray and the segment are parallel + if (denom == 0) return false; + + num = bg::get<0>(*a) * (Coord)(bg::get<1>(*d) - bg::get<1>(*c)) + + bg::get<0>(*c) * (Coord)(bg::get<1>(*a) - bg::get<1>(*d)) + + bg::get<0>(*d) * (Coord)(bg::get<1>(*c) - bg::get<1>(*a)); + + // the parameter of the intersection point: a + s * (b - a) + double s = num / denom; + + if (comp(s, 0.0)) + return false; + + if (!ray_segment && comp(1.0, s)) + return false; + + num = -((bg::get<0>(*a) * (Coord)(bg::get<1>(*c) - bg::get<1>(*b))) + + (bg::get<0>(*b) * (Coord)(bg::get<1>(*a) - bg::get<1>(*c))) + + (bg::get<0>(*c) * (Coord)(bg::get<1>(*b) - bg::get<1>(*a)))); + + // clang-format on + + // the parameter of the intersection point: c + t * (d - c) + double t = num / denom; + + if (comp(t, 0.0) || comp(1.0, t)) + return false; + + if (i) + { + bg::set<0>(*i, bg::get<0>(*a) + (s * (bg::get<0>(*b) - bg::get<0>(*a)))); + bg::set<1>(*i, bg::get<1>(*a) + (s * (bg::get<1>(*b) - bg::get<1>(*a)))); + } + return true; +}; + +/** + * @brief Same as @ref intersect() but includes segmenmt endpoint. + */ +template > +bool intersect_closed(const TPoint *a, + const TPoint *b, + const TPoint *c, + const TPoint *d, + TPoint *i = nullptr, + bool ray_segment = false) +{ + return intersect>(a, b, c, d, i, ray_segment); +} + +/** + * @brief Return true if a -> b lies in the closed cone clockwise determined by a -> a0 + * and a -> a1. + * + * Adapted from: *O'Rourke. Computational Geometry in C. 2nd ed. Cambridge 1998* Code 1.11 + */ +template +bool in_closed_cone(const TPoint *a0, const TPoint *a, const TPoint *a1, const TPoint *b) +{ + if (leftOrOn(a, a1, a0)) + // angle <= 180° + return leftOrOn(a, b, a0) && leftOrOn(b, a, a1); + return !(leftOrOn(a, b, a1) && leftOrOn(b, a, a0)); +} + +/** + * @brief Return true if a -> b lies in the open cone clockwise determined by a -> a0 + * and a -> a1. + * + * Adapted from: *O'Rourke. Computational Geometry in C. 2nd ed. Cambridge 1998* Code 1.11 + */ +template +bool in_open_cone(const TPoint *a0, const TPoint *a, const TPoint *a1, const TPoint *b) +{ + if (leftOrOn(a, a1, a0)) + // angle <= 180° + return left(a, b, a0) && left(b, a, a1); + return !(left(a, b, a1) && left(b, a, a0)); +} + +/** + * @brief Call `function` for each pair in a ring. + * + * Note: `function` will also be called on the pair (last, first). + * + * @tparam range_t The ring type + * @tparam fun_t The function type, void(range_t, range_t) + * @param range The ring + * @param function The function to call + */ +template +void for_each_pair_in_ring(range_t &range, fun_t function) +{ + size_t size = range.size(); + for (size_t i = 0; i < size; ++i) + { + function(range[i], range[(i + 1) % size]); + } +} + +/** + * @brief Call `function` for each triplet in a ring. + * + * Note: `function` will also be called on the triplets (n-2, n-1, 0) and (n-1, 0, 1). + * + * @tparam range_t The ring type + * @tparam fun_t The function type, void(range_t, range_t, range_t) + * @param range The ring + * @param function The function to call + */ +template +void for_each_triplet_in_ring(range_t &range, fun_t function) +{ + size_t size = range.size(); + auto mod = [size](size_t i) { return (i + size) % size; }; + for (size_t i = 0; i < size; ++i) + { + function(range[mod(i - 1)], range[i], range[mod(i + 1)]); + } +} + +/** + * @brief Call `function` for each ring in the polygon. + * + * @tparam polygon_t The polygon type + * @tparam fun_t The function type, void(polygon_t::ring_t) + * @param poly The polygon + * @param function The function + */ +template void for_each_ring(polygon_t &poly, fun_t function) +{ + function(poly.outer()); + for (auto &inner : poly.inners()) + { + function(inner); + } +} + +} // namespace osrm::extractor::area + +#endif diff --git a/include/extractor/area/visibility_graph.hpp b/include/extractor/area/visibility_graph.hpp new file mode 100644 index 0000000000..739475b618 --- /dev/null +++ b/include/extractor/area/visibility_graph.hpp @@ -0,0 +1,459 @@ +#ifndef OSRM_EXTRACTOR_AREA_VISIBILITY_GRAPH_HPP +#define OSRM_EXTRACTOR_AREA_VISIBILITY_GRAPH_HPP + +#include "typedefs.hpp" +#include "util.hpp" +#include "util/log.hpp" + +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace osrm::extractor::area +{ +namespace +{ +namespace srs = boost::geometry::srs; + +srs::projection> osm_mercator; +} // namespace + +/** + * @brief Implements the visibility graph + * + * This implementation follows the outline in Chapter 15 of: *de Berg, Cheong, van + * Kreveld, Overmars. Computational Geometry. Third Edition. Springer 2008.* + * + * tldr: To compute the visibility graph we do a circular sweep around the observer. We + * maintain the sweep status in tau. Edges are not supposed to cross each other. + * + * Define "interesting" as a vertex that either: + * - is an entrance to the area, + * - is reflex (sticks into the area). + * + * For each interesting vertex in turn: + * - put the observer at the vertex + * - sort all other vertices in clockwise order around the observer + * - shoot a ray from the observer through the first vertex and initialize the status in tau + * - sweep the ray clockwise around the observer and + * - for each vertex intersected by the ray + * - report the vertex if it is visible + * - update the status in tau + */ +class VisibilityGraph +{ + public: + /** A Vertex of the visibility graph */ + struct Vertex + { + // The std::sort code expects a default constructor, using pointers here instead + // of references allows the compiler to generate one. + Vertex() = default; + Vertex(const osmium::NodeRef &n) : node{n} { osm_mercator.forward(n, *this); }; + + osmium::object_id_type ref() const noexcept { return node.ref(); }; + const osmium::NodeRef &toNodeRef() const noexcept { return node; }; + + /** Projected coordinates X,Y */ + int64_t point[2]{0, 0}; + + const Vertex *prev{nullptr}; + const Vertex *next{nullptr}; + + double angle{0.0}; + double distance{0.0}; + bool visible{false}; // is the vertex visible + + private: + osmium::NodeRef node; + + friend bool operator<(const Vertex &a, const Vertex &b) noexcept + { + // sort in clockwise order, shorter distance breaks ties + return a.angle > b.angle || (a.angle == b.angle && a.distance < b.distance); + }; + friend bool operator==(const Vertex &a, const Vertex &b) noexcept + { + return a.ref() == b.ref(); + } + }; + + /** An edge of the visibilty graph */ + struct Segment + { + const Vertex *first; + const Vertex *second; + double distance{0}; + + Segment(const Vertex *first, const Vertex *second) + : first{first}, second{second}, distance{0} {}; + + friend bool operator<(const Segment &a, const Segment &b) noexcept + { + return a.distance < b.distance; + }; + friend inline bool operator==(const Segment &a, const Segment &b) noexcept + { + return (a.first == b.first && a.second == b.second); + } + }; + + // An open polygon of Vertex + using VertexPoly = bg::model::polygon; + + std::set run(const OsmiumPolygon &poly, NodeRefSet &work_set); + + std::vector visible_vertices(VertexPoly &poly, const Vertex &observer); + + bool visible(const Vertex *observer, + const Vertex *prev_w, + const Vertex *w, + const std::vector &tau); +}; + +} // namespace osrm::extractor::area + +namespace boost::geometry::traits +{ + +using namespace osrm::extractor::area; + +// The coordinates are projected into OSM-Mercator and then multipiled by COEFF so to +// store them in a pair of int64_t. Integer arithmetic avoids a host of nasty bugs. +const double COEFF = 100.0; + +template <> struct tag +{ + using type = point_tag; +}; +template <> struct dimension : boost::mpl::int_<2> +{ +}; +template <> struct coordinate_type +{ + using type = double; +}; +template <> struct coordinate_system +{ + using type = bg::cs::cartesian; // Point is projected into mercator +}; +template struct access +{ + static inline double get(const VisibilityGraph::Vertex &v) { return v.point[K] / COEFF; } + static inline void set(VisibilityGraph::Vertex &v, const double &value) + { + v.point[K] = value * COEFF; + } +}; + +} // namespace boost::geometry::traits + +namespace osrm::extractor::area +{ + +/** + * @brief Calculate the visibility graph. + * + * For each node in the work_set, and for each vertex of poly visible from that + * node, it returns the segment from the node to the vertex. + */ +std::set VisibilityGraph::run(const OsmiumPolygon &poly, NodeRefSet &work_set) +{ + // copy the NodeRef polygon into a Vertex polygon + VertexPoly vpoly; + + std::transform(poly.outer().begin(), + poly.outer().end(), + std::back_inserter(vpoly.outer()), + [](const auto n) { return Vertex{n}; }); + for (auto &inner : poly.inners()) + { + bg::model::ring vinner; + std::transform(inner.begin(), + inner.end(), + std::back_inserter(vinner), + [](const auto n) { return Vertex{n}; }); + vpoly.inners().push_back(vinner); + } + + // for each ring, for each vertex, link it to the previous and the next one + for_each_ring(vpoly, + [](auto &ring) + { + for_each_pair_in_ring(ring, + [](Vertex &v, Vertex &next) + { + v.next = &next; + next.prev = &v; + }); + }); + + // for each node in the working set, find the visible vertices + std::set result; + for (const osmium::NodeRef &observer : work_set) + { + for (const Vertex *w : visible_vertices(vpoly, observer)) + { + if (w->visible && work_set.contains(w->ref())) + { + result.emplace(OsmiumSegment(observer, w->toNodeRef())); + } + } + } + util::Log(logDEBUG) << "Found " << result.size() << " lines of sight."; + return result; +} + +/** + * @brief Return all vertices of poly that the observer can see. + * + * @param poly the polygon + * @param observer the vertex where the observer stands + */ +std::vector VisibilityGraph::visible_vertices(VertexPoly &poly, + const Vertex &observer) +{ + util::Log(logDEBUG) << "Calling visible_vertices on node: " << observer.ref(); + + // 1. Initialize vertices_cw. + // Insert all vertices (except the observer itself) into vertices_cw. Sort + // vertices_cw in clockwise order around the observer. + + // The vertices sorted in clockwise order around the observer. + std::vector vertices_cw; + + for_each_ring(poly, + [&](auto &ring) + { + for (Vertex &v : ring) + { + if (v != observer) + { + vertices_cw.push_back(&v); + } + } + }); + util::Log(logDEBUG) << "Vertices CW: " << vertices_cw.size(); + + for (Vertex *v : vertices_cw) + { + v->distance = bg::comparable_distance(observer, *v); + double dx = bg::get<0>(*v) - bg::get<0>(observer); + double dy = bg::get<1>(*v) - bg::get<1>(observer); + // See: pseudoangles + // https://stackoverflow.com/questions/16542042 + // https://computergraphics.stackexchange.com/questions/10522 + v->angle = std::copysign(1. - (dx / (fabs(dx) + fabs(dy))), dy); + } + std::sort(vertices_cw.begin(), + vertices_cw.end(), + [](const Vertex *a, const Vertex *b) { return *a < *b; }); + + util::Log(logDEBUG) << "Sorted vertices_cw:"; + for (const Vertex *v : vertices_cw) + { + util::Log(logDEBUG) << " OSM id: " << v->ref() << " at " << v->angle; + } + + // 2. Initialize the sweep status tau. + // + // Let rho be a ray starting at the observer and going through the first vertex. + // Find all segments that are intersected by rho and store them in tau in the + // same order they are intersected by rho. (Actually store them in pending edges + // from where they will be later transferred into tau.) + // + // In the textbook the observer lies outside of any obstacles, but our observer + // stands on the polygon boundary. So we must take care not to insert edges + // adjacent to the observer vertex because those edges will not obscure anything + // but interfere with distance ordering. + + // The sweep status. An ordered collection of edges sorted by increasing distance. + std::vector tau; + // Edges about to be inserted in tau. + std::deque pending_edges; + + const Vertex *q = vertices_cw[0]; + for (const Vertex *v : vertices_cw) + { + if (&observer != v->prev && &observer != v->next && + intersect(&observer, q, v, v->next, (Vertex *)nullptr, true)) + { + pending_edges.emplace_back(v, v->next); + } + }; + util::Log(logDEBUG) << "Starting sweep with " << pending_edges.size() << " pending edges."; + + // 3. Sweep the ray rho clockwise and stop at each vertex. + // rho is implicitly defined by: observer -> w -> infinity + // At each vertex do: + // - insert pending edges into tau in order of distance + // - test the visibility of the vertex + // - remove from tau any incident edge to the ccw of the sweep ray + // - add to the pending edges any incident edge to the cw of the sweep ray + // + + // The previously visited vertex in CW order. + const Vertex *prev_w = nullptr; + for (Vertex *w : vertices_cw) + { + util::Log(logDEBUG) << "Now at node id:" << w->ref() << " with " << tau.size() + << " edges in tau"; + + // update tau with pending edges + if (!pending_edges.empty()) + { + // update the distances of the segments in the status (the order of the + // segments does not change because segments are not supposed to intersect) + Vertex intersection; + for (Segment &s : tau) + { + if (intersect(&observer, w, s.first, s.second, &intersection, true)) + { + s.distance = bg::comparable_distance(observer, intersection); + util::Log(logDEBUG) + << " Updated distance of edge id:" << s.first->ref() + << " -> id:" << s.second->ref() << " in tau to: " << s.distance; + } + }; + // insert pending edges into tau in the correct order + while (!pending_edges.empty()) + { + Segment &s = pending_edges[0]; + util::Log(logDEBUG) << " Pending edge unstashed: id:" << s.first->ref() + << " -> id:" << s.second->ref(); + util::Log(logDEBUG) + << " intersection test with: id:" << observer.ref() + << " -> id:" << w->ref(); + if (intersect(&observer, w, s.first, s.second, &intersection, true)) + { + s.distance = bg::comparable_distance(observer, intersection); + auto at = tau.insert(std::lower_bound(tau.begin(), tau.end(), s), s); + util::Log(logDEBUG) + << " and inserted into tau at pos " + << std::distance(tau.begin(), at) << " and distance " << s.distance; + } + else + { + util::Log(logDEBUG) << " and dropped"; + } + pending_edges.pop_front(); + } + } + + // test visibility p -> w + util::Log(logDEBUG) << " Testing visibility of node id:" << w->ref() << " with " + << tau.size() << " edges in tau."; + for (Segment &s : tau) + { + util::Log(logDEBUG) << " Edge id:" << s.first->ref() << " -> id:" << s.second->ref() + << " distance: " << s.distance; + } + w->visible = visible(&observer, prev_w, w, tau); + prev_w = w; + util::Log(logDEBUG) << " Result: " << w->visible; + + // update tau + // + // Delete the old edges to the CCW (left) of the sweep ray. Then insert the new + // edges to the CW (right) of the sweep ray. There is a difficulty here that + // the textbook elegantly glosses over: tau is supposed to store the intersected + // edges in order of increasing distance, but if there are *two* new edges to + // insert their order is not apparent yet. Our only choice is to handle those + // edges later. + auto update_tau = [&](const Vertex *u, const Vertex *v, bool left) + { + if (left) + { + util::Log(logDEBUG) << " Erasing edge: id:" << u->ref() << " -> id:" << v->ref(); + std::erase_if(tau, + [u, v](const Segment &s) + { + bool delendus = (*s.first == *u) && (*s.second == *v); + if (delendus) + util::Log(logDEBUG) + << " Edge erased from tau: id:" << u->ref() + << " -> id:" << v->ref(); + return delendus; + }); + } + else + { + pending_edges.emplace_back(u, v); + util::Log(logDEBUG) + << " Pending edge stashed: id:" << u->ref() << " -> id:" << v->ref(); + } + }; + update_tau(w->prev, w, leftOrOn(&observer, w, w->prev)); + update_tau(w, w->next, leftOrOn(&observer, w, w->next)); + } + return vertices_cw; // visible vertices have "visible" set +} + +/** + * @brief Return true if the observer can see vertex w. + * + * @param observer the observer at the center of the circular clockwise sweep + * @param prev_w the previous vertex in clockwise order around the sweep center + * @param w the current vertex + * @param tau maintains the sweep status + */ +bool VisibilityGraph::visible(const Vertex *observer, + const Vertex *prev_w, // the last visited vertex + const Vertex *w, + const std::vector &tau) +{ + // Check if observer -> w is inside the polygon immediately before intersecting the + // edge. This condition also checks if the ray falls entirely outside the outer + // ring. + // + // if observer -> w intersects the interior of the obstacle of which w is a + // vertex, locally at w then w is not visible + if (in_open_cone(w->next, w, w->prev, observer)) + { + util::Log(logDEBUG) << " Invisible because in cone"; + return false; + } + + // Handle the simple case first: the ray really did turn some since the last vertex + // + // if prev_w is not on the ray observer -> w + if (!prev_w || !collinear(observer, prev_w, w)) + { + // If there is an edge in tau and observer -> w intersects it, then w is not visible. + if (tau.size() > 0 && intersect(observer, w, tau[0].first, tau[0].second)) + { + util::Log(logDEBUG) << " Invisible because obstructed by tau[0]"; + return false; + } + return true; + } + + // The special cases follow: the ray did not turn because the last two vertices are + // collinear with the observer + + // if prev_w was not visible, then w is not visible either, because w is farther + // away from the observer + if (!prev_w->visible) + { + return false; + } + + // if prev_w was visible, search for any edge that obstructs prev_w -> w + for (const Segment &s : tau) + { + if (intersect(prev_w, w, s.first, s.second)) + return false; + } + return true; +} + +} // namespace osrm::extractor::area + +#endif // OSRM_EXTRACTOR_AREA_VISIBILITY_GRAPH_HPP diff --git a/include/extractor/extraction_helper_functions.hpp b/include/extractor/extraction_helper_functions.hpp index 94b52db15b..e39a72001c 100644 --- a/include/extractor/extraction_helper_functions.hpp +++ b/include/extractor/extraction_helper_functions.hpp @@ -133,6 +133,16 @@ inline std::string canonicalizeStringList(std::string strlist, const std::string return strlist; } +inline bool is_true_value(const char *value) +{ + return value && (!strcmp(value, "yes") || !strcmp(value, "true") || !strcmp(value, "1")); +} + +inline bool is_false_value(const char *value) +{ + return value && (!strcmp(value, "no") || !strcmp(value, "false") || !strcmp(value, "0")); +} + } // namespace osrm::extractor #endif // EXTRACTION_HELPER_FUNCTIONS_HPP diff --git a/include/extractor/extraction_relation.hpp b/include/extractor/extraction_relation.hpp index 94012b6e0e..e16151acd2 100644 --- a/include/extractor/extraction_relation.hpp +++ b/include/extractor/extraction_relation.hpp @@ -3,202 +3,256 @@ #include "util/exception.hpp" -#include - #include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include #include namespace osrm::extractor { -struct ExtractionRelation +namespace { - class OsmIDTyped - { - public: - OsmIDTyped(osmium::object_id_type _id, osmium::item_type _type) : id(_id), type(_type) {} - - std::uint64_t GetID() const { return std::uint64_t(id); } - osmium::item_type GetType() const { return type; } - - std::uint64_t Hash() const { return id ^ (static_cast(type) << 56); } - - private: - osmium::object_id_type id; - osmium::item_type type; - }; +using string = boost::flyweights::flyweight; +using rel_id_type = osmium::object_id_type; +using member_id_type = osmium::object_id_type; +} // namespace + +/** + * @brief A replacement for `osmium::RelationMember` + * + * A replacement is needed because `osmium::RelationMember` has has no copy-constructor, + * but SOL/LUA wants a copy-constructor. + * + * Care has been taken to minimize memory consumption. This implementation uses a + * flyweight-pattern to store the very repetitive OSM member role. + */ +class RelationMember +{ + string m_role; + member_id_type m_ref; + osmium::item_type m_type; - using AttributesList = std::vector>; - using MembersRolesList = std::vector>; + public: + RelationMember(member_id_type id, osmium::item_type type) : m_ref{id}, m_type{type} {}; + RelationMember(const osmium::RelationMember &o) + : m_role(o.role()), m_ref{o.ref()}, m_type{o.type()} {}; - explicit ExtractionRelation(const OsmIDTyped &_id) : id(_id) {} + member_id_type ref() const noexcept { return m_ref; } + osmium::item_type type() const noexcept { return m_type; } + const string &role() const noexcept { return m_role; } - void Clear() + friend bool operator==(const RelationMember &m, const RelationMember &o) { - attributes.clear(); - members_role.clear(); + return m.ref() == o.ref() && m.type() == o.type(); } - - const char *GetAttr(const std::string &attr) const + friend bool operator<(const RelationMember &m, const RelationMember &o) { - auto it = std::lower_bound( - attributes.begin(), attributes.end(), std::make_pair(attr, std::string())); + return m.ref() < o.ref() || (m.ref() == o.ref() && m.type() < m.type()); + } +}; + +/** + * @brief A replacement for `osmium::Relation` + * + * A replacement is needed because in-memory storage for `osmium::Relation` in an + * `osmium::Stash` is expensive. + * + * Care has been taken to minimize memory consumption. This implementation uses a + * flyweight-pattern to store the very repetitive OSM tags. + * + */ +class Relation +{ + using members_t = std::vector; - if (it != attributes.end() && (*it).first == attr) - return (*it).second.c_str(); + std::map m_tags; + members_t m_members; + member_id_type m_id; + osmium::object_version_type m_version; + bool sorted{false}; - return nullptr; - } + public: + /** + * @brief Construct a new `Relation` object from an `osmium::Relation` + * + * @param o The `osmium::Relation` + */ + Relation(const osmium::Relation &o) : m_id{o.id()}, m_version(o.version()) + { + for (const osmium::Tag &tag : o.tags()) + { + m_tags.emplace(tag.key(), tag.value()); + } + for (const osmium::RelationMember &member : o.cmembers()) + { + m_members.emplace_back(member); + } + }; - void Prepare() + member_id_type id() const noexcept { return m_id; }; + osmium::item_type type() const noexcept { return osmium::item_type::relation; }; + osmium::object_version_type version() const noexcept { return m_version; }; + + /** + * @brief Return the tag value for the given key or the given default. + * + * @param key The key + * @param default_value The default value + * @return const char* The tag value, or the given default value if the tag does not + * exist + */ + const char *get_value_by_key_default(const char *key, const char *default_value) const { - std::sort(attributes.begin(), attributes.end()); - std::sort(members_role.begin(), members_role.end()); - } + // Nitpick: if somebody searches for "higway", it will be added to the flyweights + auto found = m_tags.find(string(key)); + if (found != m_tags.end()) + return found->second.get().c_str(); + return default_value; + }; - void AddMember(const OsmIDTyped &member_id, const char *role) + /** + * @brief Return the tag value for the given key or `nullptr`. + * + * @param key The tag key + * @return const char* The tag value, or nullptr if the tag does not exist + */ + const char *get_value_by_key(const char *key) const { - members_role.emplace_back(std::make_pair(member_id.Hash(), std::string(role))); + return get_value_by_key_default(key, nullptr); } - const char *GetRole(const OsmIDTyped &member_id) const + /** + * @brief Return the role that the given object has in the relation + * + * @tparam T Any type that has T.id() and T.type() + * @param o The relation member + * @return const char* The role, eg. "outer" + */ + template const char *get_member_role(const T &o) { - const auto hash = member_id.Hash(); - auto it = std::lower_bound( - members_role.begin(), members_role.end(), std::make_pair(hash, std::string())); - - if (it != members_role.end() && (*it).first == hash) - return (*it).second.c_str(); - - return nullptr; + return get_member_role(RelationMember(o.id(), o.type())); } - OsmIDTyped id; - AttributesList attributes; - MembersRolesList members_role; + const char *get_member_role(const RelationMember &m); + const members_t &members() { return m_members; }; }; -// It contains data of all parsed relations for each node/way element +/** + * @brief A storage container for Relation objects + * + * This is the object passed to the @ref process_node, @ref process_way, and @ref + * process_relation functions as the `relations` argument. It answers the question: + * Which relations do contain this OSM object? + * + * To save on memory this object stores only the relation types specified in + * @ref relation_types and the multipolygon relations registered for meshing. + */ class ExtractionRelationContainer { - public: - using AttributesMap = ExtractionRelation::AttributesList; - using OsmIDTyped = ExtractionRelation::OsmIDTyped; - using RelationList = std::vector; - using RelationIDList = std::vector; - using RelationRefMap = std::unordered_map; - - ExtractionRelationContainer() = default; - ExtractionRelationContainer(ExtractionRelationContainer &&) = default; - ExtractionRelationContainer(const ExtractionRelationContainer &) = delete; - - void AddRelation(ExtractionRelation &&rel) - { - rel.Prepare(); - - BOOST_ASSERT(relations_data.find(rel.id.GetID()) == relations_data.end()); - relations_data.insert(std::make_pair(rel.id.GetID(), std::move(rel))); - } - - void AddRelationMember(const OsmIDTyped &relation_id, const OsmIDTyped &member_id) - { - switch (member_id.GetType()) - { - case osmium::item_type::node: - node_refs[member_id.GetID()].push_back(relation_id); - break; + using rel_ids_t = tbb::concurrent_vector; + using parent_map_t = tbb::concurrent_map; - case osmium::item_type::way: - way_refs[member_id.GetID()].push_back(relation_id); - break; + tbb::concurrent_map relations; + std::vector parents; - case osmium::item_type::relation: - rel_refs[member_id.GetID()].push_back(relation_id); - break; + static rel_ids_t empty_rel_list; - default: - break; - }; - } + parent_map_t &p(osmium::item_type t) { return parents[(size_t)t]; } + const parent_map_t &p(osmium::item_type t) const { return parents[(size_t)t]; } - void Merge(ExtractionRelationContainer &&other) + public: + ExtractionRelationContainer(ExtractionRelationContainer &&) = delete; + ExtractionRelationContainer(const ExtractionRelationContainer &) = delete; + ExtractionRelationContainer() : parents(4){}; + + /** + * @brief Add a relation to the container + * + * Add a relation to the container and register all its members. + * + * @param rel The relation to add + */ + void add_relation(const osmium::Relation &rel) { - for (auto it : other.relations_data) + if (relations.contains(rel.id())) + return; + for (auto const &m : rel.members()) { - const auto res = relations_data.insert(std::make_pair(it.first, std::move(it.second))); - BOOST_ASSERT(res.second); - (void)res; // prevent unused warning in release + add_relation_member(rel.id(), m.ref(), m.type()); } - - auto MergeRefMap = [&](RelationRefMap &source, RelationRefMap &target) - { - for (auto it : source) - { - auto &v = target[it.first]; - v.insert(v.end(), it.second.begin(), it.second.end()); - } - }; - - MergeRefMap(other.way_refs, way_refs); - MergeRefMap(other.node_refs, node_refs); - MergeRefMap(other.rel_refs, rel_refs); + relations.emplace(rel.id(), rel); } - std::size_t GetRelationsNum() const { return relations_data.size(); } - - const RelationIDList &GetRelations(const OsmIDTyped &member_id) const + /** + * @brief Register a member of the given relation + * + * @param relation_id The id of the relation + * @param member_id The id of the member + * @param member_type The type of the member + */ + void add_relation_member(rel_id_type relation_id, + member_id_type member_id, + osmium::item_type member_type) { - auto getFromMap = [this](std::uint64_t id, - const RelationRefMap &map) -> const RelationIDList & - { - auto it = map.find(id); - if (it != map.end()) - return it->second; - - return empty_rel_list; - }; - - switch (member_id.GetType()) - { - case osmium::item_type::node: - return getFromMap(member_id.GetID(), node_refs); - - case osmium::item_type::way: - return getFromMap(member_id.GetID(), way_refs); - - case osmium::item_type::relation: - return getFromMap(member_id.GetID(), rel_refs); - - default: - break; - } + p(member_type)[member_id].push_back(relation_id); + } - return empty_rel_list; + /** + * @brief Return the number of relations in the container + */ + std::size_t get_relations_num() const { return relations.size(); } + + const rel_ids_t &_get_relations_for(member_id_type member_id, + osmium::item_type member_type) const; + + /** + * @brief Return the relations that contain the given OSM object. + * + * @param member The OSM object + * @return The ids of the relations + */ + template const rel_ids_t &get_relations_for(const T &member) const + { + return _get_relations_for(member.id(), member.type()); } - const ExtractionRelation &GetRelationData(const ExtractionRelation::OsmIDTyped &rel_id) const + /** + * @brief Get the Relation object for the given relation id + * + * @param rel_id The relation id + * @return The relation object + */ + const Relation &get_relation(rel_id_type rel_id) const { - auto it = relations_data.find(rel_id.GetID()); - if (it == relations_data.end()) - throw osrm::util::exception("Can't find relation data for " + - std::to_string(rel_id.GetID())); + auto it = relations.find(rel_id); + if (it == relations.end()) + throw osrm::util::exception("Can't find relation data for " + std::to_string(rel_id)); return it->second; } - - private: - RelationIDList empty_rel_list; - std::unordered_map relations_data; - - // each map contains list of relation id's, that has keyed id as a member - RelationRefMap way_refs; - RelationRefMap node_refs; - RelationRefMap rel_refs; }; +const char *get_osmium_member_role(const osmium::Relation &rel, const osmium::OSMObject &o); +std::vector get_osmium_relation_members(const osmium::Relation &rel); + } // namespace osrm::extractor #endif // EXTRACTION_RELATION_HPP diff --git a/include/extractor/extractor.hpp b/include/extractor/extractor.hpp index 4d30eb7e4b..5e7e67ef6c 100644 --- a/include/extractor/extractor.hpp +++ b/include/extractor/extractor.hpp @@ -35,13 +35,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "extractor/maneuver_override.hpp" #include "extractor/packed_osm_ids.hpp" -#include "guidance/guidance_processing.hpp" -#include "guidance/turn_data_container.hpp" - -#include "util/guidance/bearing_class.hpp" -#include "util/guidance/entry_class.hpp" -#include "util/guidance/turn_lanes.hpp" - #include "restriction_graph.hpp" #include "util/typedefs.hpp" diff --git a/include/extractor/node_locations_for_ways.hpp b/include/extractor/node_locations_for_ways.hpp new file mode 100644 index 0000000000..8259be59e5 --- /dev/null +++ b/include/extractor/node_locations_for_ways.hpp @@ -0,0 +1,32 @@ +#ifndef NODE_LOCATIONS_FOR_WAYS_HPP +#define NODE_LOCATIONS_FOR_WAYS_HPP + +#include "extractor/query_node.hpp" + +#include +#include + +namespace osrm::extractor +{ + +/** + * Handler to add node locations to ways. + * + * Compatible to osmium::handler::NodeLocationsForWays but uses the node locations we + * already have in memory. + */ +class NodeLocationsForWays : public osmium::handler::Handler +{ + std::vector &all_nodes_list; + osmium::Location get_node_location(OSMNodeID osm_id) const; + + public: + NodeLocationsForWays(std::vector &all_nodes_list); + void way(const osmium::Way &way); + void prepare_for_lookup(); + bool error{false}; +}; + +} // namespace osrm::extractor + +#endif // NODE_LOCATIONS_FOR_WAYS_HPP diff --git a/include/extractor/scripting_environment.hpp b/include/extractor/scripting_environment.hpp index 662aea2352..0b6790f789 100644 --- a/include/extractor/scripting_environment.hpp +++ b/include/extractor/scripting_environment.hpp @@ -1,7 +1,9 @@ #ifndef SCRIPTING_ENVIRONMENT_HPP #define SCRIPTING_ENVIRONMENT_HPP -#include "extractor/internal_extractor_edge.hpp" +#include "extractor/area/area_manager.hpp" +#include "extractor/extraction_node.hpp" +#include "extractor/extraction_way.hpp" #include "extractor/maneuver_override.hpp" #include "extractor/obstacles.hpp" #include "extractor/profile_properties.hpp" @@ -35,11 +37,20 @@ namespace extractor class RestrictionParser; class ManeuverOverrideRelationParser; class ExtractionRelationContainer; -struct ExtractionNode; -struct ExtractionWay; struct ExtractionTurn; struct ExtractionSegment; +// The data structure that is passed through the tbb::parallel pipeline. +struct ScriptingResults +{ + // this keeps the buffer alive as long as the pipeline runs + std::shared_ptr osmium_buffer; + std::vector> resulting_nodes; + std::vector> resulting_ways; + std::vector resulting_restrictions; + std::vector resulting_maneuver_overrides; +}; + /** * Abstract class that handles processing osmium ways, nodes and relation objects by applying * user supplied profiles. @@ -47,7 +58,7 @@ struct ExtractionSegment; class ScriptingEnvironment { public: - ScriptingEnvironment() = default; + ScriptingEnvironment() : m_area_manager(m_relations_stash){}; ScriptingEnvironment(const ScriptingEnvironment &) = delete; ScriptingEnvironment &operator=(const ScriptingEnvironment &) = delete; virtual ~ScriptingEnvironment() = default; @@ -62,19 +73,20 @@ class ScriptingEnvironment virtual void ProcessTurn(ExtractionTurn &turn) = 0; virtual void ProcessSegment(ExtractionSegment &segment) = 0; + virtual void ProcessRelation(ScriptingResults &result_buffer) = 0; virtual void - ProcessElements(const osmium::memory::Buffer &buffer, + ProcessElements(ScriptingResults &result_buffer, const RestrictionParser &restriction_parser, - const ManeuverOverrideRelationParser &maneuver_override_parser, - const ExtractionRelationContainer &relations, - std::vector> &resulting_nodes, - std::vector> &resulting_ways, - std::vector &resulting_restrictions, - std::vector &resulting_maneuver_overrides) = 0; + const ManeuverOverrideRelationParser &maneuver_override_parser) = 0; virtual bool HasLocationDependentData() const = 0; - ObstacleMap m_obstacle_map; // The obstacle map shared by all threads. + /** The `relations` parameter to @ref process_way etc. */ + ExtractionRelationContainer m_relations_stash; + /** The `obstacle map` global shared by all threads. */ + ObstacleMap m_obstacle_map; + /** The `area_manager` global shared by all threads. */ + area::AreaManager m_area_manager; }; } // namespace extractor } // namespace osrm diff --git a/include/extractor/scripting_environment_lua.hpp b/include/extractor/scripting_environment_lua.hpp index 26da7a3b50..03ba7b82e4 100644 --- a/include/extractor/scripting_environment_lua.hpp +++ b/include/extractor/scripting_environment_lua.hpp @@ -1,7 +1,6 @@ #ifndef SCRIPTING_ENVIRONMENT_LUA_HPP #define SCRIPTING_ENVIRONMENT_LUA_HPP -#include "extractor/extraction_relation.hpp" #include "extractor/location_dependent_data.hpp" #include "extractor/raster_source.hpp" #include "extractor/scripting_environment.hpp" @@ -31,6 +30,7 @@ struct LuaScriptingContext final void ProcessWay(const osmium::Way &, ExtractionWay &result, const ExtractionRelationContainer &relations); + void ProcessRelation(const osmium::Relation &, const ExtractionRelationContainer &relations); ProfileProperties properties; RasterContainer raster_sources; @@ -40,11 +40,13 @@ struct LuaScriptingContext final bool has_node_function = false; bool has_way_function = false; bool has_segment_function = false; + bool has_relation_function = false; sol::protected_function turn_function; sol::protected_function way_function; sol::protected_function node_function; sol::protected_function segment_function; + sol::protected_function relation_function; int api_version = 4; sol::table profile_table; @@ -83,15 +85,10 @@ class Sol2ScriptingEnvironment final : public ScriptingEnvironment void ProcessTurn(ExtractionTurn &turn) override; void ProcessSegment(ExtractionSegment &segment) override; - void - ProcessElements(const osmium::memory::Buffer &buffer, - const RestrictionParser &restriction_parser, - const ManeuverOverrideRelationParser &maneuver_override_parser, - const ExtractionRelationContainer &relations, - std::vector> &resulting_nodes, - std::vector> &resulting_ways, - std::vector &resulting_restrictions, - std::vector &resulting_maneuver_overrides) override; + void ProcessRelation(ScriptingResults &result_buffer) override; + void ProcessElements(ScriptingResults &result_buffer, + const RestrictionParser &restriction_parser, + const ManeuverOverrideRelationParser &maneuver_override_parser) override; bool HasLocationDependentData() const override { return !location_dependent_data.empty(); } diff --git a/profiles/foot_area.lua b/profiles/foot_area.lua new file mode 100644 index 0000000000..8fcf6d1b70 --- /dev/null +++ b/profiles/foot_area.lua @@ -0,0 +1,284 @@ +-- Foot profile with area meshing + +api_version = 4 + +Set = require('lib/set') +Sequence = require('lib/sequence') +Handlers = require("lib/way_handlers") +find_access_tag = require("lib/access").find_access_tag + +function setup() + local walking_speed = 5 + area_manager:init('visgraph+dijkstra') + + return { + properties = { + weight_name = 'duration', + max_speed_for_map_matching = 40/3.6, -- kmph -> m/s + call_tagless_node_function = false, + traffic_signal_penalty = 2, + u_turn_penalty = 2, + continue_straight_at_waypoint = false, + use_turn_restrictions = false, + }, + + default_mode = mode.walking, + default_speed = walking_speed, + oneway_handling = 'specific', -- respect 'oneway:foot' but not 'oneway' + + barrier_blacklist = Set { + 'yes', + 'wall', + 'fence' + }, + + access_tag_whitelist = Set { + 'yes', + 'foot', + 'permissive', + 'designated' + }, + + access_tag_blacklist = Set { + 'no', + 'agricultural', + 'forestry', + 'private', + 'delivery', + }, + + restricted_access_tag_list = Set { }, + + restricted_highway_whitelist = Set { }, + + construction_whitelist = Set {}, + + access_tags_hierarchy = Sequence { + 'foot', + 'access' + }, + + -- tags disallow access to in combination with highway=service + service_access_tag_blacklist = Set { }, + + restrictions = Sequence { + 'foot' + }, + + -- list of suffixes to suppress in name change instructions + suffix_list = Set { + 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'North', 'South', 'West', 'East' + }, + + avoid = Set { + 'impassable', + 'proposed' + }, + + speeds = Sequence { + highway = { + primary = walking_speed, + primary_link = walking_speed, + secondary = walking_speed, + secondary_link = walking_speed, + tertiary = walking_speed, + tertiary_link = walking_speed, + unclassified = walking_speed, + residential = walking_speed, + road = walking_speed, + living_street = walking_speed, + service = walking_speed, + track = walking_speed, + path = walking_speed, + steps = walking_speed, + pedestrian = walking_speed, + platform = walking_speed, + footway = walking_speed, + pier = walking_speed, + }, + + railway = { + platform = walking_speed + }, + + amenity = { + parking = walking_speed, + parking_entrance= walking_speed + }, + + man_made = { + pier = walking_speed + }, + + leisure = { + track = walking_speed + } + }, + + route_speeds = { + ferry = 5 + }, + + bridge_speeds = { + }, + + surface_speeds = { + fine_gravel = walking_speed*0.75, + gravel = walking_speed*0.75, + pebblestone = walking_speed*0.75, + mud = walking_speed*0.5, + sand = walking_speed*0.5 + }, + + tracktype_speeds = { + }, + + smoothness_speeds = { + }, + } +end + +function process_node(profile, node, result, relations) + -- parse access and barrier tags + local access = find_access_tag(node, profile.access_tags_hierarchy) + if access then + if profile.access_tag_blacklist[access] then + result.barrier = true + end + else + local barrier = node:get_value_by_key("barrier") + if barrier then + -- make an exception for rising bollard barriers + local bollard = node:get_value_by_key("bollard") + local rising_bollard = bollard and "rising" == bollard + + if profile.barrier_blacklist[barrier] and not rising_bollard then + result.barrier = true + end + end + end + + -- check if node is a traffic light + local tag = node:get_value_by_key("highway") + if "traffic_signals" == tag then + -- Direction should only apply to vehicles + result.traffic_lights = true + end +end + +-- main entry point for processsing a way +function process_way(profile, way, result, relations) + -- the intial filtering of ways based on presence of tags + -- affects processing times significantly, because all ways + -- have to be checked. + -- to increase performance, prefetching and intial tag check + -- is done in directly instead of via a handler. + + -- in general we should try to abort as soon as + -- possible if the way is not routable, to avoid doing + -- unnecessary work. this implies we should check things that + -- commonly forbids access early, and handle edge cases later. + + -- data table for storing intermediate values during processing + local data = { + -- prefetch tags + highway = way:get_value_by_key('highway'), + bridge = way:get_value_by_key('bridge'), + route = way:get_value_by_key('route'), + leisure = way:get_value_by_key('leisure'), + man_made = way:get_value_by_key('man_made'), + railway = way:get_value_by_key('railway'), + platform = way:get_value_by_key('platform'), + amenity = way:get_value_by_key('amenity'), + public_transport = way:get_value_by_key('public_transport') + } + + -- perform an quick initial check and abort if the way is + -- obviously not routable. here we require at least one + -- of the prefetched tags to be present, ie. the data table + -- cannot be empty + if next(data) == nil then -- is the data table empty? + return + end + + if data.highway == 'pedestrian' and way:has_true_tag('area') then + area_manager:way(way) + return + end + + local handlers = Sequence { + -- set the default mode for this profile. if can be changed later + -- in case it turns we're e.g. on a ferry + WayHandlers.default_mode, + + -- check various tags that could indicate that the way is not + -- routable. this includes things like status=impassable, + -- toll=yes and oneway=reversible + WayHandlers.blocked_ways, + + -- determine access status by checking our hierarchy of + -- access tags, e.g: motorcar, motor_vehicle, vehicle + WayHandlers.access, + + -- check whether forward/backward directons are routable + WayHandlers.oneway, + + -- check whether forward/backward directons are routable + WayHandlers.destinations, + + -- check whether we're using a special transport mode + WayHandlers.ferries, + WayHandlers.movables, + + -- compute speed taking into account way type, maxspeed tags, etc. + WayHandlers.speed, + WayHandlers.surface, + + -- handle turn lanes and road classification, used for guidance + WayHandlers.classification, + + -- handle various other flags + WayHandlers.roundabouts, + WayHandlers.startpoint, + + -- set name, ref and pronunciation + WayHandlers.names, + + -- set weight properties of the way + WayHandlers.weights + } + + WayHandlers.run(profile, way, result, data, handlers) +end + +function process_relation(profile, relation) + if relation:has_tag('type', 'multipolygon') and relation:has_tag('highway', 'pedestrian') then + area_manager:relation(relation) + end +end + +function process_turn (profile, turn) + turn.duration = 0. + + if turn.is_u_turn then + turn.duration = turn.duration + profile.properties.u_turn_penalty + end + + if turn.has_traffic_light then + turn.duration = profile.properties.traffic_signal_penalty + end + if profile.properties.weight_name == 'routability' then + -- penalize turns from non-local access only segments onto local access only tags + if not turn.source_restricted and turn.target_restricted then + turn.weight = turn.weight + 3000 + end + end +end + +return { + setup = setup, + process_way = process_way, + process_node = process_node, + process_turn = process_turn, + process_relation = process_relation +} diff --git a/scripts/cucumber_test_matrix.sh b/scripts/cucumber_test_matrix.sh index 4d5c0457fd..e26bd34d38 100755 --- a/scripts/cucumber_test_matrix.sh +++ b/scripts/cucumber_test_matrix.sh @@ -12,7 +12,7 @@ do for loadmethod in "${loadmethods[@]}" do set -x - node ./node_modules/cucumber/bin/cucumber.js features/ -p $profile -m $loadmethod + node ./node_modules/cucumber/bin/cucumber.js features/ -p $profile -m $loadmethod -f summary { set +x; } 2>/dev/null done done diff --git a/src/extractor/area/area_manager.cpp b/src/extractor/area/area_manager.cpp new file mode 100644 index 0000000000..84d5b95cc9 --- /dev/null +++ b/src/extractor/area/area_manager.cpp @@ -0,0 +1,219 @@ +#include "extractor/area/area_manager.hpp" + +#include "util/log.hpp" + +#include + +namespace osrm::extractor::area +{ + +/** + * @brief Initialize the area manager + * + * @param algorithm_name The alogorithm to use for meshing. At present only one + * algorithm is supported: 'visgraph+dijkstra'. + */ +void AreaManager::init(const char *algorithm_name) +{ + MutexType::scoped_lock lock(mutex); + if (!enabled) + { + this->algorithm_name = algorithm_name; + enabled = true; + } +} + +/** + * @brief Register the given closed way for area-building. + * + * This function is thread-safe. + * + * The way must be closed, else it will be silently ignored. + * + * This member function is named way() so the manager can be used as a handler for the + * first pass through a data file. + * + * @param way Way we want to build. + */ +void AreaManager::way(const osmium::Way &way) +{ + // the way must be closed + if (!way.ends_have_same_id()) + { + return; + } + + util::Log(logDEBUG) << "Registering way: " << way.get_value_by_key("name", "noname") + << " id: " << way.id(); + registered_closed_ways.push_back(way.id()); + ++number_of_ways; +} + +/** + * @brief Register the given relation for area-building. + * + * This function is thread-safe. + * + * The relation must be tagged with `type=multipolygon`, else it will be silently + * ignored. + * + * This member function is named relation() so the manager can be used as a handler for + * the first pass through a data file. + * + * @param relation Relation we want to build. + */ +void AreaManager::relation(const osmium::Relation &relation) +{ + const char *type = relation.get_value_by_key("type"); + + // the relation must be a multipolygon + if (type == nullptr || std::strcmp(type, "multipolygon") != 0) + { + return; + } + + util::Log(logDEBUG) << "Registering relation: " << relation.get_value_by_key("name", "noname") + << " id: " << relation.id(); + + relations_stash.add_relation(relation); + ++number_of_relations; + + // osmium is not thread-safe + MutexType::scoped_lock lock(mutex); + auto rel_handle = relations_database().add(relation); + + std::size_t n = 0; + for (auto &member : rel_handle->members()) + { + switch (member.type()) + { + case osmium::item_type::node: + member_database(member.type()).track(rel_handle, member.ref(), n); + m_node_relation[member.ref()] = relation.id(); + break; + case osmium::item_type::way: + member_database(member.type()).track(rel_handle, member.ref(), n); + m_way_relation[member.ref()] = relation.id(); + break; + default: + member.set_ref(0); // set member id to zero to indicate we are not interested + } + ++n; + } +} + +/** + * @brief Sort the members databases to prepare them for reading. + * + * Usually this is called between the first and second pass reading through an OSM data + * file. + */ +void AreaManager::prepare_for_lookup() +{ + RelationsManager::prepare_for_lookup(); + std::sort(registered_closed_ways.begin(), registered_closed_ways.end()); +} + +/** + * @brief Return true if the given id belongs to a registered way. + */ +inline bool AreaManager::is_registered_closed_way(osmium::object_id_type osm_id) const +{ + return std::binary_search(registered_closed_ways.begin(), registered_closed_ways.end(), osm_id); +} + +/** + * @brief Return the registered relations for the given way. + * + * Call this from the LUA @ref process_way function to find out if the way is a member + * of any relation that you registered in the LUA @ref process_relation function. + * + * @param way The given way + * @return A list of relations + */ +AreaManager::relation_ids AreaManager::get_relations_for_way(const osmium::Way &way) const +{ + relation_ids result; + auto found = m_way_relation.find(way.id()); + if (found != m_way_relation.end()) + result.push_back(found->second); + return result; +} + +/** + * @brief Return the registered relations for the given node. + * + * Call this from the LUA @ref process_node function to find out if the node is a member + * of any relation that you registered in the LUA @ref process_relation function. + * + * Note: not overloaded to avoid a hideous cast when registering on sol. + * + * @param node The given node + * @return A list of relations + */ +AreaManager::relation_ids AreaManager::get_relations_for_node(const osmium::Node &node) const +{ + relation_ids result; + auto found = m_node_relation.find(node.id()); + if (found != m_node_relation.end()) + result.push_back(found->second); + return result; +} + +/** + * @brief Second-pass handler for ways. + * + * This is called when a registered way was found in the input. + */ +void AreaManager::after_way(const osmium::Way &way) +{ + if (is_registered_closed_way(way.id())) + { + const char *name = way.get_value_by_key("name", "noname"); + util::Log(logDEBUG) << "Completing way: " << name << " id: " << way.id(); + for (const osmium::NodeRef &noderef : way.nodes()) + { + node_ids.emplace(noderef.ref()); + } + osmium::area::Assembler assembler{m_assembler_config}; + assembler(way, this->buffer()); + } +} + +/** + * @brief Second-pass handler for relations. + * + * This is called when all member ways of a registered relation were found in the input. + */ +void AreaManager::complete_relation(const osmium::Relation &relation) +{ + const char *name = relation.get_value_by_key("name", "noname"); + util::Log(logDEBUG) << "Completing relation: " << name << " id: " << relation.id(); + + std::vector ways; + ways.reserve(relation.members().size()); + for (const auto &member : relation.members()) + { + if (member.ref() != 0) + { + const osmium::Way *way = get_member_way(member.ref()); + assert(way != nullptr); + ways.push_back(way); + for (const osmium::NodeRef &noderef : way->nodes()) + { + node_ids.emplace(noderef.ref()); + } + } + } + try + { + osmium::area::Assembler assembler{m_assembler_config}; + assembler(relation, ways, this->buffer()); + } + catch (const osmium::invalid_location &) + { + // XXX ignore + } +} + +} // namespace osrm::extractor::area diff --git a/src/extractor/area/area_mesher.cpp b/src/extractor/area/area_mesher.cpp new file mode 100644 index 0000000000..1a8c5e003b --- /dev/null +++ b/src/extractor/area/area_mesher.cpp @@ -0,0 +1,517 @@ +// FIXME: include entrances in entry points +// FIXME: include crossing roads inside area + +#include "extractor/area/area_mesher.hpp" + +#include "extractor/area/dijkstra.hpp" +#include "extractor/area/typedefs.hpp" +#include "extractor/area/util.hpp" +#include "extractor/area/visibility_graph.hpp" +#include "util/log.hpp" +#include "util/typedefs.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace osrm::extractor::area +{ + +namespace +{ + +/** + * @brief Copies tags from the area to the generated ways. + * + * - removes the `area` tag + * - adds an `osrm:virtual` tag + */ +void copy_tags(osmium::builder::Builder &parent, const osmium::TagList &tags) +{ + osmium::builder::TagListBuilder builder{parent}; + + for (const auto &tag : tags) + { + if (!std::strcmp(tag.key(), "area")) + { + continue; + } + builder.add_tag(tag); + } + builder.add_tag("osrm:virtual", "yes"); +} + +/** + * @brief Return the id of the OSM object that created the area. + * + * @param area The area + * @return The id in the format "r42" or "w69" + */ +std::string area_id(const osmium::Area &area) +{ + std::stringstream strstream; + strstream << (area.id() % 2 == 1 ? "r" : "w") << area.orig_id(); + return strstream.str(); +} + +} // namespace + +/** + * @brief Build a boost multi-polygon from an osmium::Area. + * + * It is hard to adapt an osmium::Area to a boost::multi_polygon because the latter is a + * vector of boost::polygon, while libosmium has no equivalent for boost::polygon. So + * whenever we need to use boost we use a boost polygon model and copy our data into it. + * + * Note that the built polygon is an open one. + */ +OsmiumMultiPolygon AreaMesher::area_builder(const osmium::Area &area) +{ + OsmiumMultiPolygon mp; + for (const osmium::OuterRing &outer : area.outer_rings()) + { + OsmiumPolygon poly; + std::copy(outer.cbegin(), outer.cend() - 1, std::back_inserter(poly.outer())); + for (const osmium::InnerRing &inner : area.inner_rings(outer)) + { + OsmiumPolygon::ring_type vinner(inner.cbegin(), inner.cend() - 1); + poly.inners().push_back(vinner); + } + mp.emplace_back(poly); + } + return mp; +} + +/** + * @brief Prepares the search for intersecting ways. + * + * We need to know by which nodes an area can actually be entered or exited. + * With this knowledge we can substantially reduce the number of ways to generate. + * + * This function generates a multi-map from node ids to ways (actually to the index into + * ExtractionContainers::ways_list). + */ +void AreaMesher::init(const AreaManager &manager, const extractor::ExtractionContainers &containers) +{ + // For all nodes in any of the registered ways, find out which other ways use these + // nodes. + + const size_t number_of_ways = containers.ways_list.size(); + for (size_t i = 0; i < number_of_ways; ++i) + { + auto start = containers.used_node_id_list.begin() + containers.way_node_id_offsets[i]; + auto end = containers.used_node_id_list.begin() + containers.way_node_id_offsets[i + 1]; + + for (auto it = start; it != end; ++it) + { + if (manager.node_ids.contains(from_alias(*it))) + node_id2way_index.emplace(*it, containers.ways_list[i]); + } + } +}; + +/** + * @brief Return the nodes by which the area can be entered or exited. + * + * For an area to be meshed there must be at least 2 incident ways. If there is only + * one way crossing the area, or running along the perimeter, the area need not be + * meshed. + * + * If a way joins the perimeter, runs along it, and then leaves it, only two nodes will + * be reported. + * + * Note that an entry point can at the same time be classified as visibility-blocking + * obstacle, see below. + */ +NodeRefSet AreaMesher::get_entry_points(const OsmiumPolygon &poly) +{ + NodeRefSet entry_nodes; + std::set all_ways; + + const auto &outer = poly.outer(); + std::size_t size = outer.size(); + + auto get_ways_crossing_node = [this, &outer](int index) -> std::set + { + std::set result; + auto [wno_begin, wno_end] = + node_id2way_index.equal_range(to_alias(outer[index].ref())); + for (auto wno_iter = wno_begin; wno_iter != wno_end; ++wno_iter) + { + result.emplace(wno_iter->second); + } + return result; + }; + + std::set last_ways = get_ways_crossing_node(0); + + for (std::size_t i = 1; i <= size; ++i) + { + std::set current_ways = get_ways_crossing_node(i % size); + + // find the difference between current and last ways + auto first1 = current_ways.begin(); + auto last1 = current_ways.end(); + auto first2 = last_ways.begin(); + auto last2 = last_ways.end(); + int new_ways = 0; + int gone_ways = 0; + + while (first1 != last1 && first2 != last2) + { + if (*first1 < *first2) + { + ++new_ways; + ++first1; + } + else if (*first1 > *first2) + { + ++gone_ways; + ++first2; + } + else + { + ++first1; + ++first2; + } + } + + if (new_ways || first1 != last1) + { + // a new way + entry_nodes.emplace(outer[i % size]); + } + if (gone_ways || first2 != last2) + { + // eg. a way that was running along the border has left us + entry_nodes.emplace(outer[(i - 1) % size]); + } + + last_ways = current_ways; + all_ways.insert(current_ways.begin(), current_ways.end()); + } + if (entry_nodes.size() >= 2 and all_ways.size() >= 3) + return entry_nodes; + return NodeRefSet{}; +} + +/** + * Return the vertices which potentially block visibility. + * + * Specifically return: + * + * - all convex vertices on the inner rings, and + * - all reflex vertices on the outer ring. + * + * Only these vertices plus the entry points to the area need to be considered when + * building the visibility graph. Note that a blocking vertex can at the same time be + * an entry point. + */ +NodeRefSet AreaMesher::get_obstacle_vertices(const OsmiumPolygon &poly) +{ + NodeRefSet obstacle_vertices; + + for_each_ring(poly, + [&](const auto &ring) + { + for_each_triplet_in_ring(ring, + [&](const osmium::NodeRef &prev, + const osmium::NodeRef &n, + const osmium::NodeRef &next) + { + if (right(&prev, &n, &next)) + { + obstacle_vertices.emplace(n); + } + }); + }); + + util::Log(logDEBUG) << "Obstacle vertices: " << obstacle_vertices.size(); + return obstacle_vertices; +} + +/** + * @brief Meshes one area into a buffer. + * + * @param area The area to mesh + * @param out_buffer The buffer to receive the ways of the meshed area + * @param relations The registry of relations + */ +void AreaMesher::mesh_area(const osmium::Area &area, + osmium::memory::Buffer &out_buffer, + ExtractionRelationContainer &relations) +{ + util::Log(logDEBUG) << "Meshing area: " << area.get_value_by_key("name", "noname") + << " id: " << area_id(area); + + auto rel_ids = relations.get_relations_for(area); + util::Log(logDEBUG) << " Found " << rel_ids.size() << " parent relations."; + + // add the segments to the output buffer + auto add_to_buffer = + [&](const std::set &segments, osmium::memory::Buffer &out_buffer) + { + using namespace osmium::builder::attr; + + for (const OsmiumSegment &s : segments) + { + util::Log(logDEBUG) << " Reporting: " << s.first.ref() << " -> " << s.second.ref(); + auto builder = osmium::builder::WayBuilder(out_buffer); + builder.set_id(next_way_id); + builder.set_version(1); + copy_tags(builder, area.tags()); + builder.add_node_refs({s.first.ref(), s.second.ref()}); + for (auto rel_id : rel_ids) + { + // if the original item was part of a relation, the generated ways + // should be part of that relation too, eg. a hiking route crossing a + // pedestrian area + relations.add_relation_member(rel_id, next_way_id, osmium::item_type::way); + } + --next_way_id; + ++added_ways; + } + out_buffer.commit(); + }; + +#ifndef NDEBUG + auto debug_nodes = + [&](const NodeRefSet &nodes, const char *klass, osmium::memory::Buffer &out_buffer) + { + for (const osmium::NodeRef node : nodes) + { + auto builder = osmium::builder::NodeBuilder(out_buffer); + builder.set_id(next_node_id); + builder.set_version(1); + builder.add_tags({{"osrm:debug:class", klass}}); + builder.set_location(node.location()); + --next_node_id; + } + out_buffer.commit(); + }; + + const auto write_debug = [&](const char *basename, const std::set &segments) + { + // write debug file + std::stringstream strstream; + strstream << basename << "-" << area_id(area) << ".osm"; + std::filesystem::path path = std::filesystem::temp_directory_path() / strstream.str(); + osmium::io::Writer writer(path, osmium::io::overwrite::allow); + osmium::memory::Buffer debug_buffer{16 * 1024}; + add_to_buffer(segments, debug_buffer); + debug_buffer.commit(); + writer(std::move(debug_buffer)); + writer.close(); + }; +#endif + + for (const OsmiumPolygon &poly : area_builder(area)) + { + NodeRefSet entry_points = get_entry_points(poly); + if (entry_points.size() < 2) + { + util::Log(logDEBUG) << " Poly outer size: " << poly.outer().size(); + util::Log(logDEBUG) << " Poly inner rings: " << poly.inners().size(); + util::Log(logDEBUG) << " Not enough entry points: " << entry_points.size(); + continue; + } + + // The vertices that should be in the visibility map. + NodeRefSet work_vertices = get_obstacle_vertices(poly); + + util::Log(logDEBUG) << " Starting VisibilityGraph on area with " << entry_points.size() + << " entry points and " << work_vertices.size() << " obstacle points."; + +#ifndef NDEBUG + debug_nodes(entry_points, "entry_points", out_buffer); + debug_nodes(work_vertices, "obstacle_points", out_buffer); +#endif + + work_vertices.insert(entry_points.begin(), entry_points.end()); + + // safety valve + if (work_vertices.size() > max_vertices) + { + util::Log(logWARNING) << "Too many vertices (" << work_vertices.size() << "/" + << max_vertices << ") for meshing. OSM id: " << area_id(area); + continue; + } + + // Note: we cannot reduce the area to just the work_vertices. Assume a round + // place with two entries opposite each other and a fountain in the middle. If + // we reduce the area to a line between the entries the fountain will block + // everything. + VisibilityGraph gv; + std::set vis_map = gv.run(poly, work_vertices); + util::Log(logDEBUG) << " After running VisibilityGraph we have " << vis_map.size() + << " visible edges."; + +#ifndef NDEBUG + write_debug("osrm-area-routing-visgraph-debug", vis_map); +#endif + + // std::set segments = run_dijkstra(OsmiumPolygon(), vis_map, entry_points); + std::set segments = run_dijkstra(poly, vis_map, entry_points); + util::Log(logDEBUG) << " After running Dijkstra there are " << segments.size() + << " edges left."; + add_to_buffer(segments, out_buffer); + out_buffer.commit(); + +#ifndef NDEBUG + write_debug("osrm-area-routing-dijkstra-debug", segments); +#endif + } +}; + +/** + * @brief Runs the Dijkstra shortest-path algorithm on the visibility graph. + * + * @param poly The area as polygon + * @param vis_map The visibility graph + * @param entry_points The entry points to the area + * @return The resulting ways to add to the router + */ +std::set AreaMesher::run_dijkstra(const OsmiumPolygon &poly, + std::set &vis_map, + const NodeRefSet &entry_points) +{ + Dijkstra d; + + std::set poly_segments; + + // Add the segments in the polygon. + for_each_ring(poly, + [&](auto &ring) + { + for_each_pair_in_ring(ring, + [&](const osmium::NodeRef &u, const osmium::NodeRef &v) + { + poly_segments.emplace(OsmiumSegment(u, v)); + double weight = bg::distance(u, v); + d.add_edge(u, v, weight); + }); + }); + + util::Log(logDEBUG) << " The polygon has " << poly_segments.size() << " edges:"; + util::Log(logDEBUG) << " The vis_map has " << vis_map.size() << " edges:"; + + // Add the segments of the visibility graph. Avoid duplicates. + for (const OsmiumSegment &s : vis_map) + { + if (!poly_segments.contains(s)) + { + double weight = bg::distance(s.first, s.second); + d.add_edge(s.first, s.second, weight); + util::Log(logDEBUG) << " " << s.first.ref() << " -> " << s.second.ref() << " " + << weight; + } + } + + util::Log(logDEBUG) << "Running Dijkstra on: " << entry_points.size() << " entry points, " + << d.num_vertices() << " vertices and " << d.num_edges() << " edges."; + + std::set result; + + using index_t = size_t; + for (const osmium::NodeRef &entry_point : entry_points) + { + index_t u = d.index_of(entry_point); + d.run(u); + + const std::vector &predecessors(d.get_predecessors()); + + // starting from each exit point report all generated edges that are on the + // shortest path from the entry point + + for (const osmium::NodeRef &exit_point : entry_points) + { + util::Log(logDEBUG) << " Collecting segments from " << entry_point.ref() << " -> " + << exit_point.ref(); + index_t v = d.index_of(exit_point); + while (v != u && v != predecessors.at(v)) + { + auto s = OsmiumSegment(d.get_vertex(v), d.get_vertex(predecessors.at(v))); + util::Log(logDEBUG) + << " Collecting: " << s.first.ref() << " -> " << s.second.ref(); + result.emplace(s); + v = predecessors.at(v); + } + } + } + return result; +} + +/** + * @brief Meshes all areas in the input buffer. + * + * @param in_buffer The input buffer with areas + * @param out_buffer The output buffer with ways + * @param relations The registry of relations + */ +void AreaMesher::mesh_buffer(const osmium::memory::Buffer &in_buffer, + osmium::memory::Buffer &out_buffer, + ExtractionRelationContainer &relations) +{ + for (const auto &area : in_buffer.select()) + { + mesh_area(area, out_buffer, relations); + } +} + +/** + * @brief Return a buffer with areas. + * + * Fill the next buffer with up to 4 areas and return it. An invalid buffer signals + * that there are no more areas. After that, all calls will throw an + * @ref osrm::util::exception. + * + * @returns A buffer with areas + * @throws osrm::util::exception if there is an error. + */ +osmium::memory::Buffer BufferReader::read() +{ + if (m_status != status::okay) + { + throw osrm::util::exception( + "extractor::area::BufferReader: cannot read in status 'closed', 'eof', or 'error'"); + } + if (iter == end) + { + m_status = status::eof; + return osmium::memory::Buffer(); + } + + const size_t no_of_areas_per_buffer = 4; + + osmium::memory::Buffer out_buffer{16 * 1024, osmium::memory::Buffer::auto_grow::yes}; + size_t i = 0; + + while (true) + { + if (iter->type() == osmium::item_type::area) + { + out_buffer.add_item(*iter); + ++i; + } + ++iter; + if (iter == end || i >= no_of_areas_per_buffer) + { + out_buffer.commit(); + return out_buffer; + } + } +} + +} // namespace osrm::extractor::area diff --git a/src/extractor/extraction_relation.cpp b/src/extractor/extraction_relation.cpp new file mode 100644 index 0000000000..5c3666b588 --- /dev/null +++ b/src/extractor/extraction_relation.cpp @@ -0,0 +1,88 @@ +#include "extractor/extraction_relation.hpp" + +namespace osrm::extractor +{ + +/** + * @brief Return the role of the given object in the given relation + * + * @param rel The relation + * @param o The OSM object + * @return The role or nullptr + */ +const char *get_osmium_member_role(const osmium::Relation &rel, const osmium::OSMObject &o) +{ + for (const auto &member : rel.cmembers()) + { + if (member.ref() == o.id() && member.type() == o.type()) + return member.role(); + } + return nullptr; +}; + +/** + * @brief Return the members of a relation as `RelationMember`s + * + * This is a helper function for SOL/LUA: `osmium::RelationMember` is not + * copy-constructable. But SOL/LUA wants to copy the members. We turn the + * `osmium::RelationMember` into a copy-constructable `RelationMember`. + * + * @param rel The `osmium::Relation` + * @return The members as `RelationMember` + */ +std::vector get_osmium_relation_members(const osmium::Relation &rel) +{ + std::vector result; + for (const auto &member : rel.cmembers()) + { + result.push_back(RelationMember(member)); + } + return result; +}; + +const char *Relation::get_member_role(const RelationMember &m) +{ + if (!sorted) + { + std::sort(m_members.begin(), m_members.end()); + sorted = true; + } + auto it = std::lower_bound(m_members.begin(), m_members.end(), m); + if (it != m_members.end() && *it == m) + { + return it->role().get().c_str(); + } + return nullptr; +} + +ExtractionRelationContainer::rel_ids_t ExtractionRelationContainer::empty_rel_list{}; + +/** + * @brief Return the relations which contain the given member + * + * @param member_id The id of the member + * @param member_type The type of the member + * @return The ids of the relations + */ +const ExtractionRelationContainer::rel_ids_t & +ExtractionRelationContainer::_get_relations_for(member_id_type member_id, + osmium::item_type member_type) const +{ + if (member_type == osmium::item_type::area) + { + member_type = (member_id & 1) ? osmium::item_type::relation : osmium::item_type::way; + member_id /= 2; + } + assert(member_type == osmium::item_type::relation || member_type == osmium::item_type::way || + member_type == osmium::item_type::node); + + const parent_map_t &parents = p(member_type); + + auto it = parents.find(member_id); + if (it != parents.end()) + return it->second; + + return empty_rel_list; +} + +} // namespace osrm::extractor diff --git a/src/extractor/extractor.cpp b/src/extractor/extractor.cpp index 435a0e481b..533f45bbbb 100644 --- a/src/extractor/extractor.cpp +++ b/src/extractor/extractor.cpp @@ -1,18 +1,20 @@ #include "extractor/extractor.hpp" +#include "extractor/area/area_manager.hpp" +#include "extractor/area/area_mesher.hpp" #include "extractor/compressed_edge_container.hpp" #include "extractor/compressed_node_based_graph_edge.hpp" #include "extractor/edge_based_edge.hpp" #include "extractor/extraction_containers.hpp" -#include "extractor/extraction_node.hpp" #include "extractor/extraction_relation.hpp" -#include "extractor/extraction_way.hpp" #include "extractor/extractor_callbacks.hpp" #include "extractor/files.hpp" #include "extractor/maneuver_override_relation_parser.hpp" #include "extractor/name_table.hpp" #include "extractor/node_based_graph_factory.hpp" +#include "extractor/node_locations_for_ways.hpp" #include "extractor/node_restriction_map.hpp" +#include "extractor/profile_properties.hpp" #include "extractor/restriction_graph.hpp" #include "extractor/restriction_parser.hpp" #include "extractor/scripting_environment.hpp" @@ -38,19 +40,22 @@ #include +#include +#include +#include + #include #include #include +#include +#include #include #include -#include -#include #include #include #include #include -#include #include namespace osrm::extractor @@ -58,6 +63,17 @@ namespace osrm::extractor namespace { +template auto convertIDMapToVector(const Map &map) +{ + std::vector result(map.size()); + for (const auto &pair : map) + { + BOOST_ASSERT(pair.second < map.size()); + result[pair.second] = pair.first; + } + return result; +} + // Converts the class name map into a fixed mapping of index to name void SetClassNames(const std::vector &class_names, ExtractorCallbacks::ClassesMap &classes_map, @@ -363,7 +379,7 @@ Extractor::ParsedOSMData Extractor::ParseOSMData(ScriptingEnvironment &scripting const osmium::io::File input_file(config.input_path.string()); osmium::thread::Pool pool(number_of_threads); - util::Log() << "Parsing in progress.."; + util::Log() << "Parsing in progress ..."; TIMER_START(parsing); { // Parse OSM header @@ -393,59 +409,35 @@ Extractor::ParsedOSMData Extractor::ParseOSMData(ScriptingEnvironment &scripting } util::Log() << "timestamp: " << timestamp; } + ProfileProperties profile_properties = scripting_environment.GetProfileProperties(); // Extraction containers and restriction parser ExtractionContainers extraction_containers; ExtractorCallbacks::ClassesMap classes_map; LaneDescriptionMap turn_lane_map; - auto extractor_callbacks = - std::make_unique(extraction_containers, - classes_map, - turn_lane_map, - scripting_environment.GetProfileProperties()); - - // get list of supported relation types - auto relation_types = scripting_environment.GetRelations(); - std::sort(relation_types.begin(), relation_types.end()); + auto extractor_callbacks = std::make_unique( + extraction_containers, classes_map, turn_lane_map, profile_properties); std::vector restrictions = scripting_environment.GetRestrictions(); // setup restriction parser const RestrictionParser restriction_parser( - scripting_environment.GetProfileProperties().use_turn_restrictions, - config.parse_conditionals, - restrictions); + profile_properties.use_turn_restrictions, config.parse_conditionals, restrictions); const ManeuverOverrideRelationParser maneuver_override_parser; // OSM data reader - using SharedBuffer = std::shared_ptr; - struct ParsedBuffer - { - SharedBuffer buffer; - std::vector> resulting_nodes; - std::vector> resulting_ways; - std::vector> resulting_relations; - std::vector resulting_restrictions; - std::vector resulting_maneuver_overrides; - }; + using OsmiumBuffer = std::shared_ptr; - ExtractionRelationContainer relations; - - const auto buffer_reader = [](osmium::io::Reader &reader) + const auto reader_source = [](auto &reader) { - return tbb::filter( + return tbb::filter( tbb::filter_mode::serial_in_order, [&reader](tbb::flow_control &fc) { - if (auto buffer = reader.read()) - { - return std::make_shared(std::move(buffer)); - } - else - { + OsmiumBuffer buffer = std::make_shared(reader.read()); + if (!*buffer) fc.stop(); - return SharedBuffer{}; - } + return buffer; }); }; @@ -457,31 +449,36 @@ Extractor::ParsedOSMData Extractor::ParseOSMData(ScriptingEnvironment &scripting osmium_index_type location_cache; osmium_location_handler_type location_handler(location_cache); - tbb::filter location_cacher( + tbb::filter location_cache_filter( tbb::filter_mode::serial_in_order, - [&location_handler](SharedBuffer buffer) + [&location_handler](const OsmiumBuffer &buffer) { osmium::apply(buffer->begin(), buffer->end(), location_handler); return buffer; }); // OSM elements Lua parser - tbb::filter buffer_transformer( + tbb::filter process_elements_filter( tbb::filter_mode::parallel, - // NOLINTNEXTLINE(performance-unnecessary-value-param) - [&](const SharedBuffer buffer) + [&](const OsmiumBuffer &buffer) { - ParsedBuffer parsed_buffer; - parsed_buffer.buffer = buffer; - scripting_environment.ProcessElements(*buffer, - restriction_parser, - maneuver_override_parser, - relations, - parsed_buffer.resulting_nodes, - parsed_buffer.resulting_ways, - parsed_buffer.resulting_restrictions, - parsed_buffer.resulting_maneuver_overrides); - return parsed_buffer; + ScriptingResults results; + // keeps the buffer alive until the end of the pipe + results.osmium_buffer = buffer; + scripting_environment.ProcessElements( + results, restriction_parser, maneuver_override_parser); + return results; + }); + + // OSM elements Lua parser + tbb::filter process_relation_filter( + tbb::filter_mode::parallel, + [&](const OsmiumBuffer &buffer) -> void + { + ScriptingResults results; + // keeps the buffer alive until the end of the pipe + results.osmium_buffer = buffer; + scripting_environment.ProcessRelation(results); }); // Parsed nodes and ways handler @@ -489,82 +486,51 @@ Extractor::ParsedOSMData Extractor::ParseOSMData(ScriptingEnvironment &scripting unsigned number_of_ways = 0; unsigned number_of_restrictions = 0; unsigned number_of_maneuver_overrides = 0; - tbb::filter buffer_storage( + tbb::filter extractor_callbacks_filter( tbb::filter_mode::serial_in_order, - [&](const ParsedBuffer &parsed_buffer) + [&](const ScriptingResults &results) { - number_of_nodes += parsed_buffer.resulting_nodes.size(); + number_of_nodes += results.resulting_nodes.size(); // put parsed objects thru extractor callbacks - for (const auto &result : parsed_buffer.resulting_nodes) + for (const auto &result : results.resulting_nodes) { extractor_callbacks->ProcessNode(result.first, result.second); } - number_of_ways += parsed_buffer.resulting_ways.size(); - for (const auto &result : parsed_buffer.resulting_ways) + number_of_ways += results.resulting_ways.size(); + for (const auto &result : results.resulting_ways) { extractor_callbacks->ProcessWay(result.first, result.second); } - number_of_restrictions += parsed_buffer.resulting_restrictions.size(); - for (const auto &result : parsed_buffer.resulting_restrictions) + number_of_restrictions += results.resulting_restrictions.size(); + for (const auto &result : results.resulting_restrictions) { extractor_callbacks->ProcessRestriction(result); } - number_of_maneuver_overrides = parsed_buffer.resulting_maneuver_overrides.size(); - for (const auto &result : parsed_buffer.resulting_maneuver_overrides) + number_of_maneuver_overrides = results.resulting_maneuver_overrides.size(); + for (const auto &result : results.resulting_maneuver_overrides) { extractor_callbacks->ProcessManeuverOverride(result); } }); - tbb::filter> buffer_relation_cache( + osmium::TagsFilter tags_filter{false}; + tbb::filter stash_relations_filter( tbb::filter_mode::parallel, - // NOLINTNEXTLINE(performance-unnecessary-value-param) - [&](const SharedBuffer buffer) + [&](const OsmiumBuffer &buffer) -> void { if (!buffer) - return std::shared_ptr{}; + return; - auto relations = std::make_shared(); - for (auto entity = buffer->cbegin(), end = buffer->cend(); entity != end; ++entity) + for (const osmium::Relation &rel : buffer->select()) { - if (entity->type() != osmium::item_type::relation) - continue; - - const auto &rel = static_cast(*entity); - - const char *rel_type = rel.get_value_by_key("type"); - if (!rel_type || !std::binary_search(relation_types.begin(), - relation_types.end(), - std::string(rel_type))) - continue; - - ExtractionRelation extracted_rel({rel.id(), osmium::item_type::relation}); - for (auto const &t : rel.tags()) - extracted_rel.attributes.emplace_back(std::make_pair(t.key(), t.value())); - - for (auto const &m : rel.members()) + if (osmium::tags::match_any_of(rel.tags(), tags_filter)) { - ExtractionRelation::OsmIDTyped const mid(m.ref(), m.type()); - extracted_rel.AddMember(mid, m.role()); - relations->AddRelationMember(extracted_rel.id, mid); + scripting_environment.m_relations_stash.add_relation(rel); } - - relations->AddRelation(std::move(extracted_rel)); }; - return relations; - }); - - unsigned number_of_relations = 0; - tbb::filter, void> buffer_storage_relation( - tbb::filter_mode::serial_in_order, - // NOLINTNEXTLINE(performance-unnecessary-value-param) - [&](const std::shared_ptr parsed_relations) - { - number_of_relations += parsed_relations->GetRelationsNum(); - relations.Merge(std::move(*parsed_relations)); }); // Parse OSM elements with parallel transformer @@ -573,15 +539,52 @@ Extractor::ParsedOSMData Extractor::ParseOSMData(ScriptingEnvironment &scripting const auto read_meta = config.use_metadata ? osmium::io::read_meta::yes : osmium::io::read_meta::no; - { // Relations reading pipeline - util::Log() << "Parse relations ..."; + // get list of supported relation types + std::vector relation_types = scripting_environment.GetRelations(); + if (relation_types.empty()) + { + util::Log() << "No relation_types in properties.relation_types"; + } + else + { + util::Log() << "Stashing relations of following types ..."; + for (auto &rel_type : relation_types) + { + tags_filter.add_rule(true, osmium::TagMatcher("type", rel_type)); + util::Log() << " " << rel_type; + } + // Read the relations configured in `profile.relation_types`. We must read them + // first because they are passed as argument to the LUA process_* functions. + TIMER_START(parse_relations); + osmium::io::Reader reader(input_file, pool, osmium::osm_entity_bits::relation, read_meta); - tbb::parallel_pipeline( - num_threads, buffer_reader(reader) & buffer_relation_cache & buffer_storage_relation); + tbb::parallel_pipeline(num_threads, reader_source(reader) & stash_relations_filter); + + TIMER_STOP(parse_relations); + util::Log() << "... " << scripting_environment.m_relations_stash.get_relations_num() + << " relations in " << TIMER_SEC(parse_relations) << " seconds"; } + auto &area_manager = scripting_environment.m_area_manager; + if (area_manager.is_enabled()) + { + // Next we read the relations again and pass them to LUA's process_relation + // function, so they can be registered for meshing. Unfortunately we cannot + // combine this run with the run above because we need its results, for example: + // The user may want to mesh only those areas that are part of a hiking route. + util::Log() << "Register pedestrian areas ..."; + TIMER_START(areas_first_pass); - { // Nodes and ways reading pipeline - util::Log() << "Parse ways and nodes ..."; + osmium::io::Reader reader(input_file, pool, osmium::osm_entity_bits::relation, read_meta); + tbb::parallel_pipeline(num_threads, reader_source(reader) & process_relation_filter); + + TIMER_STOP(areas_first_pass); + util::Log() << "... " << area_manager.number_of_relations << " pedestrian areas in " + << TIMER_SEC(areas_first_pass) << " seconds"; + // At this point we know the relations and the way ids of their members. + } + { + util::Log() << "Parse ways and nodes and restrictions ..."; + TIMER_START(parse_ways); osmium::io::Reader reader(input_file, pool, osmium::osm_entity_bits::node | osmium::osm_entity_bits::way | @@ -590,17 +593,96 @@ Extractor::ParsedOSMData Extractor::ParseOSMData(ScriptingEnvironment &scripting const auto pipeline = scripting_environment.HasLocationDependentData() && config.use_locations_cache - ? buffer_reader(reader) & location_cacher & buffer_transformer & buffer_storage - : buffer_reader(reader) & buffer_transformer & buffer_storage; + ? reader_source(reader) & location_cache_filter & process_elements_filter & + extractor_callbacks_filter + : reader_source(reader) & process_elements_filter & extractor_callbacks_filter; tbb::parallel_pipeline(num_threads, pipeline); + TIMER_STOP(parse_ways); + util::Log() << "... in " << TIMER_SEC(parse_ways) << " seconds"; + } + + if (area_manager.number_of_ways + area_manager.number_of_relations) + { + util::Log() << "Parse pedestrian areas ..."; + TIMER_START(areas_second_pass); + + area_manager.prepare_for_lookup(); + + // After the first pass the manager knows which ways and which relations we want + // to mesh. In the second pass it will complete the relations with ways, nodes + // and locations. This is the second pass. + + tbb::filter manager_second_pass_filter( + tbb::filter_mode::serial_out_of_order, + [&area_manager](const OsmiumBuffer &buffer) + { osmium::apply(buffer->begin(), buffer->end(), area_manager.handler()); }); + + osmium::io::Reader reader(input_file, pool, osmium::osm_entity_bits::way, read_meta); + if (config.use_locations_cache) + { + auto area_location_handler = NodeLocationsForWays(extraction_containers.all_nodes_list); + area_location_handler.prepare_for_lookup(); + + tbb::filter area_location_cache_filter( + tbb::filter_mode::parallel, + [&area_location_handler](const OsmiumBuffer &buffer) + { + osmium::apply(buffer->begin(), buffer->end(), area_location_handler); + return buffer; + }); + + tbb::parallel_pipeline(num_threads, + reader_source(reader) & area_location_cache_filter & + manager_second_pass_filter); + } + else + { + // location data has been provided by preprocessing the OSM file + tbb::parallel_pipeline(num_threads, reader_source(reader) & manager_second_pass_filter); + } + reader.close(); + TIMER_STOP(areas_second_pass); + + util::Log() << "... " << area_manager.number_of_relations << " multipolygon relations and " + << area_manager.number_of_ways << " closed ways in " + << TIMER_SEC(areas_second_pass) << " seconds"; + + util::Log() << "Mesh pedestrian areas ..."; + TIMER_START(mesh); + // The manager has collected all information and assembled it into osmium::areas + // in a big buffer. + + area::AreaMesher mesher; + mesher.init(area_manager, extraction_containers); + + tbb::filter mesh_areas_filter( + tbb::filter_mode::parallel, + [&](const OsmiumBuffer &buffer) + { + osmium::memory::Buffer out_buffer{16 * 1024, + osmium::memory::Buffer::auto_grow::yes}; + mesher.mesh_buffer(*buffer, out_buffer, scripting_environment.m_relations_stash); + return std::make_shared(std::move(out_buffer)); + }); + + // Mesh areas and feed the resulting virtual ways through the usual pipeline + area::BufferReader areader(area_manager.buffer()); + const auto pipeline2 = reader_source(areader) & mesh_areas_filter & + process_elements_filter & extractor_callbacks_filter; + tbb::parallel_pipeline(num_threads, pipeline2); + TIMER_STOP(mesh); + + util::Log() << "... " << area_manager.number_of_ways + area_manager.number_of_relations + << " areas, yielding " << mesher.added_ways << " ways in " << TIMER_SEC(mesh) + << " seconds"; } TIMER_STOP(parsing); util::Log() << "Parsing finished after " << TIMER_SEC(parsing) << " seconds"; util::Log() << "Raw input contains " << number_of_nodes << " nodes, " << number_of_ways - << " ways, and " << number_of_relations << " relations, " << number_of_restrictions - << " restrictions"; + << " ways, and " << scripting_environment.m_relations_stash.get_relations_num() + << " relations, " << number_of_restrictions << " restrictions"; extractor_callbacks.reset(); @@ -613,7 +695,6 @@ Extractor::ParsedOSMData Extractor::ParseOSMData(ScriptingEnvironment &scripting extraction_containers.PrepareData(scripting_environment, config.GetPath(".osrm.names").string()); - auto profile_properties = scripting_environment.GetProfileProperties(); SetClassNames(scripting_environment.GetClassNames(), classes_map, profile_properties); auto excludable_classes = scripting_environment.GetExcludableClasses(); SetExcludableClasses(classes_map, excludable_classes, profile_properties); @@ -809,17 +890,6 @@ void Extractor::BuildRTree(std::vector edge_based_node_seg util::Log() << "finished r-tree construction in " << TIMER_SEC(construction) << " seconds"; } -template auto convertIDMapToVector(const Map &map) -{ - std::vector result(map.size()); - for (const auto &pair : map) - { - BOOST_ASSERT(pair.second < map.size()); - result[pair.second] = pair.first; - } - return result; -} - void Extractor::ProcessGuidanceTurns( const util::NodeBasedDynamicGraph &node_based_graph, const extractor::EdgeBasedNodeDataContainer &edge_based_node_container, diff --git a/src/extractor/node_locations_for_ways.cpp b/src/extractor/node_locations_for_ways.cpp new file mode 100644 index 0000000000..cee2ae2192 --- /dev/null +++ b/src/extractor/node_locations_for_ways.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include + +#include + +#include "extractor/node_locations_for_ways.hpp" + +namespace osrm::extractor +{ + +NodeLocationsForWays::NodeLocationsForWays(std::vector &all_nodes_list) + : all_nodes_list{all_nodes_list} +{ +} + +void NodeLocationsForWays::prepare_for_lookup() +{ + // this is duplicated work, but the alternative would be to use an + // osmium::handler::NodeLocationsForWays handler, which would read all nodes into + // memory again + tbb::parallel_sort(all_nodes_list.begin(), + all_nodes_list.end(), + [](const auto &left, const auto &right) + { return left.node_id < right.node_id; }); +} + +/** + * Get location of node with given id. + */ +osmium::Location NodeLocationsForWays::get_node_location(OSMNodeID osm_id) const +{ + auto iter = std::lower_bound(all_nodes_list.begin(), + all_nodes_list.end(), + osm_id, + [](auto it, auto val) { return it.node_id < val; }); + if (iter != all_nodes_list.end() && iter->node_id == osm_id) + { + return osmium::Location{from_alias(util::toFloating(iter->lon)), + from_alias(util::toFloating(iter->lat))}; + } + return osmium::Location(); +} + +/** + * Retrieve locations of all nodes in the way from the all_nodes_list and add them to + * the way object. + */ +void NodeLocationsForWays::way(const osmium::Way &cway) +{ + // Technically we don't change the way, we only complete the nodes with their + // locations. We need the parameter to be const because of the non-optimal + // interface of the osmium library. + osmium::Way &way = const_cast(cway); + for (auto &node_ref : way.nodes()) + { + node_ref.set_location(get_node_location(OSMNodeID{node_ref.positive_ref()})); + if (!node_ref.location().is_defined()) + { + error = true; + } + } +} + +} // namespace osrm::extractor diff --git a/src/extractor/scripting_environment_lua.cpp b/src/extractor/scripting_environment_lua.cpp index ca565751c8..e8c63afdb3 100644 --- a/src/extractor/scripting_environment_lua.cpp +++ b/src/extractor/scripting_environment_lua.cpp @@ -1,12 +1,12 @@ #include "extractor/scripting_environment_lua.hpp" +#include "extractor/area/area_manager.hpp" #include "extractor/extraction_helper_functions.hpp" #include "extractor/extraction_node.hpp" #include "extractor/extraction_relation.hpp" #include "extractor/extraction_segment.hpp" #include "extractor/extraction_turn.hpp" #include "extractor/extraction_way.hpp" -#include "extractor/graph_compressor.hpp" #include "extractor/internal_extractor_edge.hpp" #include "extractor/maneuver_override_relation_parser.hpp" #include "extractor/profile_properties.hpp" @@ -22,16 +22,16 @@ #include "util/typedefs.hpp" #include +#include +#include +#include +#include +#include +#include namespace sol { -template <> struct is_container : std::false_type -{ -}; -template <> struct is_container : std::false_type -{ -}; -template <> struct is_container : std::false_type +template <> struct is_container : std::false_type { }; } // namespace sol @@ -41,32 +41,39 @@ namespace osrm::extractor namespace { +template const char *get_value_by_key(const T &object, const char *key) +{ + const char *value = object.get_value_by_key(key); + return (value && *value) ? value : nullptr; +} + template -auto get_value_by_key(T const &object, const char *key) -> decltype(object.get_value_by_key(key)) +const char *get_value_by_key_default(const T &object, const char *key, const char *default_value) { - auto v = object.get_value_by_key(key); - if (v && *v) - { // non-empty string? - return v; - } - else - { - return nullptr; - } + const char *value = object.get_value_by_key(key, default_value); + return (value && *value) ? value : nullptr; } -template -const char *get_value_by_key(T const &object, const char *key, D const default_value) +template bool has_key(const T &object, const char *key) { - auto v = get_value_by_key(object, key); - if (v && *v) - { - return v; - } - else - { - return default_value; - } + const char *value = object.get_value_by_key(key); + return value && *value; +} + +template bool has_tag(const T &object, const char *key, const char *val) +{ + const char *value = object.get_value_by_key(key); + return value && !strcmp(val, value); +} + +template bool has_true_tag(const T &object, const char *key) +{ + return is_true_value(object.get_value_by_key(key)); +} + +template bool has_false_tag(const T &object, const char *key) +{ + return is_false_value(object.get_value_by_key(key)); } template double latToDouble(T const &object) @@ -86,7 +93,6 @@ struct to_lua_object : public boost::static_visitor auto operator()(boost::blank &) const { return sol::lua_nil; } sol::state &state; }; -} // namespace // Handle a lua error thrown in a protected function by printing the traceback and bubbling // exception up to caller. Lua errors are generally unrecoverable, so this exception should not be @@ -108,6 +114,7 @@ void handle_lua_error(const sol::protected_function_result &luares) } throw util::exception("Lua error (see stderr for traceback)"); } +} // namespace Sol2ScriptingEnvironment::Sol2ScriptingEnvironment( const std::string &file_name, @@ -126,6 +133,8 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) context.state["trimLaneString"] = trimLaneString; context.state["applyAccessTokens"] = applyAccessTokens; context.state["canonicalizeStringList"] = canonicalizeStringList; + context.state["is_true"] = is_true_value; + context.state["is_false"] = is_false_value; context.state.new_enum("mode", "inaccessible", @@ -209,6 +218,8 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) "interpolate", &RasterContainer::GetRasterInterpolateFromSource); + // the first parameter to the LUA process_* functions + // FIXME: shouldn't all of this be readonly? context.state.new_usertype( "ProfileProperties", "traffic_signal_penalty", @@ -228,7 +239,7 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) "weight_precision", &ProfileProperties::weight_precision, "weight_name", - sol::property(&ProfileProperties::SetWeightName, &ProfileProperties::GetWeightName), + sol::property(&ProfileProperties::GetWeightName, &ProfileProperties::SetWeightName), "max_turn_weight", sol::property(&ProfileProperties::GetMaxTurnWeight), "force_split_edges", @@ -250,11 +261,25 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) "valid", &osmium::Location::valid); - auto get_location_tag = [](auto &context, const auto &location, const char *key) + auto get_location_tag = [&context](const osmium::OSMObject &o, const char *key) { if (context.location_dependent_data.empty()) return sol::object(context.state); + osmium::Location location; + + if (o.type() == osmium::item_type::way) + { + // HEURISTIC: use a single node (last) of the way to localize the way + // For more complicated scenarios a proper merging of multiple tags + // at one or many locations must be provided + location = static_cast(o).nodes().back().location(); + } + else + { + location = static_cast(o).location(); + } + const LocationDependentData::point_t point{location.lon(), location.lat()}; if (!boost::geometry::equals(context.last_location_point, point)) { @@ -267,40 +292,94 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) return boost::apply_visitor(to_lua_object(context.state), value); }; - context.state.new_usertype( - "Way", - "get_value_by_key", - &get_value_by_key, + context.state.new_usertype("RelationMember", + "ref", + &RelationMember::ref, + "type", + &RelationMember::type, + "role", + &RelationMember::role); + + context.state.new_usertype( + "OSMObject", "id", - &osmium::Way::id, + &osmium::OSMObject::id, + "type", + &osmium::OSMObject::type, "version", - &osmium::Way::version, - "get_nodes", - [](const osmium::Way &way) { return sol::as_table(&way.nodes()); }, - "get_location_tag", - [&context, &get_location_tag](const osmium::Way &way, const char *key) - { - // HEURISTIC: use a single node (last) of the way to localize the way - // For more complicated scenarios a proper merging of multiple tags - // at one or many locations must be provided - const auto &nodes = way.nodes(); - const auto &location = nodes.back().location(); - return get_location_tag(context, location, key); - }); - - context.state.new_usertype( - "Node", - "location", - &osmium::Node::location, + &osmium::OSMObject::version, + "has_key", + &has_key, + "has_tag", + &has_tag, + "has_true_tag", + &has_true_tag, + "has_false_tag", + &has_false_tag, "get_value_by_key", - &get_value_by_key, + sol::overload(&get_value_by_key, + &get_value_by_key_default)); + + // This is the type you get from the "relations" container in functions + // `process_way` etc. This is a stored type not backed by an osmium::buffer. This + // type mimics an `osmium::Relation` so as to be indistinguishable from LUA. + // + // Implementation note: A different type is used because storing an + // `osmium::Relation` in an `osmium::stash` consumed too much memory. + context.state.new_usertype( + "ExtractionRelation", "id", - &osmium::Node::id, + &Relation::id, + "type", + &Relation::type, "version", - &osmium::Node::version, + &Relation::version, + "has_key", + &has_key, + "has_tag", + &has_tag, + "has_true_tag", + &has_true_tag, + "has_false_tag", + &has_false_tag, + "get_value_by_key", + sol::overload(&Relation::get_value_by_key, &Relation::get_value_by_key_default), + "get_role", + sol::overload(&Relation::get_member_role, + &Relation::get_member_role), + "get_members", + &Relation::members); + + // This is the type you get as argument in function `process_relation`. + // This is backed by an osmium::buffer. + context.state.new_usertype("Relation", + "get_role", + get_osmium_member_role, + "get_members", + get_osmium_relation_members, + sol::base_classes, + sol::bases()); + + context.state.new_usertype( + "Way", + "size", + [](const osmium::Way &way) { return way.nodes().size(); }, + "is_closed", + &osmium::Way::is_closed, + "get_nodes", + [](const osmium::Way &way) { return sol::as_table(&way.nodes()); }, "get_location_tag", - [&context, &get_location_tag](const osmium::Node &node, const char *key) - { return get_location_tag(context, node.location(), key); }); + get_location_tag, + sol::base_classes, + sol::bases()); + + context.state.new_usertype("Node", + "location", + &osmium::Node::location, + "get_location_tag", + get_location_tag, + sol::base_classes, + sol::bases()); context.state.new_enum("traffic_lights", "none", @@ -350,7 +429,20 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) [](const ObstacleMap &om, NodeID from, NodeID to, Obstacle::Type type) { return om.any(from, to, type); })); + context.state.new_usertype( + "AreaManager", + "init", + &area::AreaManager::init, + "get_relations", + sol::overload(&area::AreaManager::get_relations_for_node, + &area::AreaManager::get_relations_for_way), + "way", + &area::AreaManager::way, + "relation", + &area::AreaManager::relation); + context.state["obstacle_map"] = std::ref(m_obstacle_map); + context.state["area_manager"] = std::ref(m_area_manager); context.state.new_usertype( "ResultNode", @@ -484,56 +576,13 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) sol::property([](const ExtractionWay &way) { return way.access_turn_classification; }, [](ExtractionWay &way, int flag) { way.access_turn_classification = flag; })); - auto getTypedRefBySol = [](const sol::object &obj) -> ExtractionRelation::OsmIDTyped - { - if (obj.is()) - { - osmium::Way *way = obj.as(); - return {way->id(), osmium::item_type::way}; - } - - if (obj.is()) - { - ExtractionRelation *rel = obj.as(); - return rel->id; - } - - if (obj.is()) - { - osmium::Node *node = obj.as(); - return {node->id(), osmium::item_type::node}; - } - - return ExtractionRelation::OsmIDTyped(0, osmium::item_type::undefined); - }; - - context.state.new_usertype( - "OsmIDTyped", - "id", - &ExtractionRelation::OsmIDTyped::GetID, - "type", - &ExtractionRelation::OsmIDTyped::GetType); - - context.state.new_usertype( - "ExtractionRelation", - "id", - [](ExtractionRelation &rel) { return rel.id.GetID(); }, - "get_value_by_key", - [](ExtractionRelation &rel, const char *key) -> const char * { return rel.GetAttr(key); }, - "get_role", - [&getTypedRefBySol](ExtractionRelation &rel, const sol::object &obj) -> const char * - { return rel.GetRole(getTypedRefBySol(obj)); }); - context.state.new_usertype( "ExtractionRelationContainer", "get_relations", - [&getTypedRefBySol](ExtractionRelationContainer &cont, const sol::object &obj) - -> const ExtractionRelationContainer::RelationIDList & - { return cont.GetRelations(getTypedRefBySol(obj)); }, + sol::overload(&ExtractionRelationContainer::get_relations_for, + &ExtractionRelationContainer::get_relations_for), "relation", - [](ExtractionRelationContainer &cont, - const ExtractionRelation::OsmIDTyped &rel_id) -> const ExtractionRelation & - { return cont.GetRelationData(rel_id); }); + &ExtractionRelationContainer::get_relation); context.state.new_usertype( "NodeBasedEdgeClassification", @@ -944,6 +993,8 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) "duration", &ExtractionTurn::duration); initV2Context(); + context.relation_function = function_table.value()["process_relation"]; + context.has_relation_function = context.relation_function.valid(); break; } case 3: @@ -1024,21 +1075,33 @@ LuaScriptingContext &Sol2ScriptingEnvironment::GetSol2Context() return *ref; } +/** + * Calls the LUA process_relation function. + */ +void Sol2ScriptingEnvironment::ProcessRelation(ScriptingResults &results) +{ + auto &local_context = this->GetSol2Context(); + if (local_context.has_relation_function) + { + for (const osmium::Relation &relation : results.osmium_buffer->select()) + { + local_context.ProcessRelation(relation, m_relations_stash); + } + } +} + void Sol2ScriptingEnvironment::ProcessElements( - const osmium::memory::Buffer &buffer, + ScriptingResults &results, const RestrictionParser &restriction_parser, - const ManeuverOverrideRelationParser &maneuver_override_parser, - const ExtractionRelationContainer &relations, - std::vector> &resulting_nodes, - std::vector> &resulting_ways, - std::vector &resulting_restrictions, - std::vector &resulting_maneuver_overrides) + const ManeuverOverrideRelationParser &maneuver_override_parser) { ExtractionNode result_node; ExtractionWay result_way; auto &local_context = this->GetSol2Context(); - for (auto entity = buffer.cbegin(), end = buffer.cend(); entity != end; ++entity) + for (auto entity = results.osmium_buffer->cbegin(), end = results.osmium_buffer->cend(); + entity != end; + ++entity) { switch (entity->type()) { @@ -1049,10 +1112,10 @@ void Sol2ScriptingEnvironment::ProcessElements( if (local_context.has_node_function && (!node.tags().empty() || local_context.properties.call_tagless_node_function)) { - local_context.ProcessNode(node, result_node, relations); + local_context.ProcessNode(node, result_node, m_relations_stash); } result_node.node = nullptr; - resulting_nodes.push_back({node, result_node}); + results.resulting_nodes.push_back({node, result_node}); } break; case osmium::item_type::way: @@ -1062,23 +1125,24 @@ void Sol2ScriptingEnvironment::ProcessElements( result_way.clear(); if (local_context.has_way_function) { - local_context.ProcessWay(way, result_way, relations); + local_context.ProcessWay(way, result_way, m_relations_stash); } - resulting_ways.push_back({way, std::move(result_way)}); + results.resulting_ways.push_back({way, std::move(result_way)}); } break; case osmium::item_type::relation: { const auto &relation = static_cast(*entity); - auto results = restriction_parser.TryParse(relation); - if (!results.empty()) + auto restrictions = restriction_parser.TryParse(relation); + if (!restrictions.empty()) { - std::move( - results.begin(), results.end(), std::back_inserter(resulting_restrictions)); + std::move(restrictions.begin(), + restrictions.end(), + std::back_inserter(results.resulting_restrictions)); } - else if (auto result_res = maneuver_override_parser.TryParse(relation)) + else if (auto maneuvers = maneuver_override_parser.TryParse(relation)) { - resulting_maneuver_overrides.push_back(std::move(*result_res)); + results.resulting_maneuver_overrides.push_back(std::move(*maneuvers)); } } break; @@ -1346,6 +1410,24 @@ void Sol2ScriptingEnvironment::ProcessSegment(ExtractionSegment &segment) } } +void LuaScriptingContext::ProcessRelation(const osmium::Relation &relation, + const ExtractionRelationContainer &relations) +{ + BOOST_ASSERT(state.lua_state() != nullptr); + + sol::protected_function_result luares; + + switch (api_version) + { + case 4: + luares = relation_function(profile_table, std::ref(relation), std::cref(relations)); + break; + } + + if (!luares.valid()) + handle_lua_error(luares); +} + void LuaScriptingContext::ProcessNode(const osmium::Node &node, ExtractionNode &result, const ExtractionRelationContainer &relations) diff --git a/src/util/coordinate_calculation.cpp b/src/util/coordinate_calculation.cpp index 19fbc2d0eb..b80cee2b50 100644 --- a/src/util/coordinate_calculation.cpp +++ b/src/util/coordinate_calculation.cpp @@ -1,4 +1,5 @@ #include "util/coordinate_calculation.hpp" +#include "util/alias.hpp" #include "util/cheap_ruler.hpp" #include "util/coordinate.hpp" #include "util/trigonometry_table.hpp" @@ -55,15 +56,13 @@ static CheapRulerContainer cheap_ruler_container(1800); // Does not project the coordinates! std::uint64_t squaredEuclideanDistance(const Coordinate lhs, const Coordinate rhs) { - std::int64_t d_lon = static_cast(lhs.lon - rhs.lon); - std::int64_t d_lat = static_cast(lhs.lat - rhs.lat); + std::int64_t d_lon = from_alias(lhs.lon) - from_alias(rhs.lon); + std::int64_t d_lat = from_alias(lhs.lat) - from_alias(rhs.lat); - std::int64_t sq_lon = d_lon * d_lon; - std::int64_t sq_lat = d_lat * d_lat; + std::uint64_t sq_lon = d_lon * d_lon; + std::uint64_t sq_lat = d_lat * d_lat; - std::uint64_t result = static_cast(sq_lon + sq_lat); - - return result; + return sq_lon + sq_lat; } double greatCircleDistance(const Coordinate coordinate_1, const Coordinate coordinate_2) diff --git a/unit_tests/CMakeLists.txt b/unit_tests/CMakeLists.txt index c3cd7b09c4..6e0a01bee4 100644 --- a/unit_tests/CMakeLists.txt +++ b/unit_tests/CMakeLists.txt @@ -8,7 +8,8 @@ file(GLOB ContractorTestsSources file(GLOB ExtractorTestsSources extractor_tests.cpp - extractor/*.cpp) + extractor/*.cpp + extractor/area/*.cpp) file(GLOB PartitionTestsSources partitioner_tests.cpp diff --git a/unit_tests/extractor/area/dijkstra.cpp b/unit_tests/extractor/area/dijkstra.cpp new file mode 100644 index 0000000000..f5b410c92c --- /dev/null +++ b/unit_tests/extractor/area/dijkstra.cpp @@ -0,0 +1,75 @@ +#include "extractor/area/dijkstra.hpp" + +#include +#include +#include + +#include "util/log.hpp" + +BOOST_AUTO_TEST_SUITE(area_util_test) + +using namespace osrm; +using namespace osrm::extractor::area; + +BOOST_AUTO_TEST_CASE(area_dijkstra_test) +{ +#define CHECK_EQUAL_RANGES(a, b) \ + BOOST_CHECK_EQUAL_COLLECTIONS((a).begin(), (a).end(), (b).begin(), (b).end()) + + // osrm::util::LogPolicy::GetInstance().SetLevel(logDEBUG); + // osrm::util::LogPolicy::GetInstance().Unmute(); + + auto dist = [](const osmium::NodeRef &a, const osmium::NodeRef &b) + { + auto ax = a.location().lon(); + auto ay = a.location().lat(); + auto bx = b.location().lon(); + auto by = b.location().lat(); + auto dx = bx - ax; + auto dy = by - ay; + return sqrt((dx * dx) + (dy * dy)); + }; + + osmium::NodeRef u{0, {0, 0}}; + osmium::NodeRef v{1, {0, 1}}; + osmium::NodeRef w{2, {1, 1}}; + osmium::NodeRef x{3, {2, 1}}; + osmium::NodeRef y{4, {3, 1}}; + osmium::NodeRef z{5, {4, 1}}; + + Dijkstra d; + auto add = [&](const osmium::NodeRef &a, const osmium::NodeRef &b) + { d.add_edge(a, b, dist(a, b)); }; + + add(u, v); + add(v, w); + add(w, x); + add(x, y); + add(y, z); + BOOST_CHECK(d.num_edges() == 5); + BOOST_CHECK(d.num_vertices() == 6); + + std::vector expected{0, 0, 1, 2, 3, 4}; + BOOST_CHECK_NO_THROW(d.run(d.index_of(u))); + CHECK_EQUAL_RANGES(d.get_predecessors(), expected); + + add(u, w); + BOOST_CHECK(d.num_edges() == 6); + BOOST_CHECK(d.num_vertices() == 6); + + expected = {0, 0, 0, 2, 3, 4}; + BOOST_CHECK_NO_THROW(d.run(d.index_of(u))); + CHECK_EQUAL_RANGES(d.get_predecessors(), expected); + + add(u, x); + add(u, y); + add(u, z); + BOOST_CHECK(d.num_edges() == 9); + BOOST_CHECK(d.num_vertices() == 6); + + expected = {0, 0, 0, 0, 0, 0}; + BOOST_CHECK_NO_THROW(d.run(d.index_of(u))); + CHECK_EQUAL_RANGES(d.get_predecessors(), expected); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/unit_tests/extractor/area/index_priority_queue.cpp b/unit_tests/extractor/area/index_priority_queue.cpp new file mode 100644 index 0000000000..b4b208dd16 --- /dev/null +++ b/unit_tests/extractor/area/index_priority_queue.cpp @@ -0,0 +1,92 @@ +#include "extractor/area/index_priority_queue.hpp" + +#include +#include +#include + +BOOST_AUTO_TEST_SUITE(area_util_index_priority_queue) + +using namespace osrm; +using namespace osrm::extractor::area; + +namespace +{ +const std::vector distances{1, 3, 5, 7, 9, 8, 6, 4, 2, 0}; +auto min_comp = [](size_t u, size_t v) -> bool { return distances[u] < distances[v]; }; +auto max_comp = [](size_t u, size_t v) -> bool { return distances[u] > distances[v]; }; +} // namespace + +BOOST_AUTO_TEST_CASE(size_test) +{ + IndexPriorityQueue pq(distances.size(), min_comp); + BOOST_CHECK(pq.empty()); + BOOST_CHECK_EQUAL(pq.size(), 0); + pq.insert(0); + BOOST_CHECK(!pq.empty()); + BOOST_CHECK_EQUAL(pq.size(), 1); + pq.top(); + BOOST_CHECK(!pq.empty()); + BOOST_CHECK_EQUAL(pq.size(), 1); + pq.pop(); + BOOST_CHECK(pq.empty()); + BOOST_CHECK_EQUAL(pq.size(), 0); +} + +BOOST_AUTO_TEST_CASE(min_comp_test) +{ + // osrm::util::LogPolicy::GetInstance().SetLevel(logDEBUG); + // osrm::util::LogPolicy::GetInstance().Unmute(); + + IndexPriorityQueue pq(distances.size(), min_comp); + + for (size_t i = 0; i < distances.size(); ++i) + { + pq.insert(i); + } + for (size_t i = 0; i < distances.size(); ++i) + { + BOOST_CHECK_EQUAL(distances[pq.pop()], i); + } +} + +BOOST_AUTO_TEST_CASE(max_comp_test) +{ + IndexPriorityQueue pq(distances.size(), max_comp); + + for (size_t i = 0; i < distances.size(); ++i) + { + pq.insert(i); + } + for (size_t i = 0; i < distances.size(); ++i) + { + BOOST_CHECK_EQUAL(distances[pq.pop()], distances.size() - i - 1); + } +} + +BOOST_AUTO_TEST_CASE(decrease_test) +{ + // osrm::util::LogPolicy::GetInstance().SetLevel(logDEBUG); + // osrm::util::LogPolicy::GetInstance().Unmute(); + + auto copy = distances; + + IndexPriorityQueue pq(copy.size(), + [&](size_t u, size_t v) -> bool { return copy[u] < copy[v]; }); + for (size_t i = 0; i < distances.size(); ++i) + { + pq.insert(i); + } + copy[2] = -1; + pq.decrease(2); + BOOST_CHECK_EQUAL(copy[pq.top()], -1); + + copy[4] = -2; + pq.decrease(4); + BOOST_CHECK_EQUAL(copy[pq.top()], -2); + + BOOST_CHECK_EQUAL(copy[pq.pop()], -2); + BOOST_CHECK_EQUAL(copy[pq.pop()], -1); + BOOST_CHECK_EQUAL(copy[pq.pop()], 0); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/unit_tests/extractor/area/util.cpp b/unit_tests/extractor/area/util.cpp new file mode 100644 index 0000000000..7b6c63f24f --- /dev/null +++ b/unit_tests/extractor/area/util.cpp @@ -0,0 +1,65 @@ +#include "extractor/area/util.hpp" + +#include "extractor/area/typedefs.hpp" + +#include +#include +#include + +#include + +BOOST_GEOMETRY_REGISTER_BOOST_TUPLE_CS(bg::cs::cartesian) + +BOOST_AUTO_TEST_SUITE(area_util_test) + +using namespace osrm; +using namespace osrm::extractor::area; + +using point_t = boost::tuple; + +BOOST_AUTO_TEST_CASE(area_util_test_geometry) +{ + point_t o(0, 0); + point_t x(10, 0); + point_t y(0, 10); + point_t d(10, 10); + point_t dd(20, 20); + + BOOST_CHECK(left(&o, &x, &d)); + BOOST_CHECK(left(&o, &d, &y)); + BOOST_CHECK(left(&o, &x, &y)); + + BOOST_CHECK(!left(&o, &y, &d)); + BOOST_CHECK(!left(&o, &d, &x)); + BOOST_CHECK(!left(&o, &y, &x)); + + BOOST_CHECK(collinear(&o, &d, &dd)); + + // some degenerate cases + BOOST_CHECK(!left(&o, &d, &d)); + BOOST_CHECK(!left(&o, &o, &o)); + + // intersections + point_t expected(5, 5); + point_t i(0, 0); + BOOST_CHECK(intersect(&o, &d, &x, &y, &i)); + BOOST_CHECK(bg::equals(i, expected)); + + BOOST_CHECK(!intersect(&dd, &d, &x, &y, &i)); + BOOST_CHECK(intersect(&dd, &d, &x, &y, &i, true)); + BOOST_CHECK(bg::equals(i, expected)); + + BOOST_CHECK(!intersect(&o, &x, &x, &d)); + BOOST_CHECK(!intersect(&o, &y, &y, &d)); + BOOST_CHECK(!intersect(&o, &d, &d, &x)); + BOOST_CHECK(!intersect(&o, &d, &d, &y)); + + // in closed cone clockwise + BOOST_CHECK(in_closed_cone(&y, &o, &x, &y)); + BOOST_CHECK(in_closed_cone(&y, &o, &x, &d)); + BOOST_CHECK(in_closed_cone(&y, &o, &x, &x)); + + BOOST_CHECK(!in_closed_cone(&x, &o, &y, &d)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/unit_tests/library/extract.cpp b/unit_tests/library/extract.cpp index c4f1f0354f..701c738cd2 100644 --- a/unit_tests/library/extract.cpp +++ b/unit_tests/library/extract.cpp @@ -1,10 +1,10 @@ -#include - -#include "osrm/exception.hpp" #include "osrm/extractor.hpp" #include "osrm/extractor_config.hpp" +#include "util/log.hpp" #include +#include + #include // utility class to redirect stderr so we can test it @@ -48,6 +48,19 @@ BOOST_AUTO_TEST_CASE(test_extract_with_valid_config) BOOST_CHECK_NO_THROW(osrm::extract(config)); } +BOOST_AUTO_TEST_CASE(test_extract_with_valid_config_and_areas) +{ + // osrm::util::LogPolicy::GetInstance().SetLevel(logDEBUG); + // osrm::util::LogPolicy::GetInstance().Unmute(); + osrm::ExtractorConfig config; + config.input_path = OSRM_TEST_DATA_DIR "/monaco.osm.pbf"; + config.UseDefaultOutputNames(OSRM_TEST_DATA_DIR "/monaco.osm.pbf"); + config.profile_path = OSRM_TEST_DATA_DIR "/../../profiles/foot_area.lua"; + config.small_component_size = 1000; + config.requested_num_threads = std::thread::hardware_concurrency(); + BOOST_CHECK_NO_THROW(osrm::extract(config)); +} + BOOST_AUTO_TEST_CASE(test_setup_runtime_error) { osrm::ExtractorConfig config; diff --git a/unit_tests/mocks/mock_scripting_environment.hpp b/unit_tests/mocks/mock_scripting_environment.hpp index e56a6f9fa2..e822cd53a7 100644 --- a/unit_tests/mocks/mock_scripting_environment.hpp +++ b/unit_tests/mocks/mock_scripting_environment.hpp @@ -3,12 +3,10 @@ #include "extractor/extraction_segment.hpp" #include "extractor/extraction_turn.hpp" -#include "extractor/maneuver_override.hpp" #include "extractor/profile_properties.hpp" #include "extractor/scripting_environment.hpp" #include -#include #include namespace osrm::test @@ -33,14 +31,10 @@ class MockScriptingEnvironment : public extractor::ScriptingEnvironment void ProcessTurn(extractor::ExtractionTurn &) override final {} void ProcessSegment(extractor::ExtractionSegment &) override final {} - void ProcessElements(const osmium::memory::Buffer &, + void ProcessRelation(extractor::ScriptingResults &) override final{}; + void ProcessElements(extractor::ScriptingResults &, const extractor::RestrictionParser &, - const extractor::ManeuverOverrideRelationParser &, - const extractor::ExtractionRelationContainer &, - std::vector> &, - std::vector> &, - std::vector &, - std::vector &) override final + const extractor::ManeuverOverrideRelationParser &) override final { }