From 81d7e7e1b3e12892b5cf24caecb1e5bb42b5ec78 Mon Sep 17 00:00:00 2001 From: Adrian Auer Date: Sat, 3 Jan 2026 12:23:43 +0100 Subject: [PATCH 1/3] adding local copies of map_desc repos and some initial docker stuff --- docker/Dockerfile | 31 ++ docker/entrypoint.bash | 5 + lib/mapdesc_msgs/CMakeLists.txt | 60 +++ lib/mapdesc_msgs/README.md | 7 + lib/mapdesc_msgs/msg/Area.msg | 5 + lib/mapdesc_msgs/msg/Box.msg | 4 + lib/mapdesc_msgs/msg/Dimension.msg | 3 + lib/mapdesc_msgs/msg/External.msg | 4 + lib/mapdesc_msgs/msg/LaneEdge.msg | 5 + lib/mapdesc_msgs/msg/LaneGraph.msg | 2 + lib/mapdesc_msgs/msg/LaneNode.msg | 3 + lib/mapdesc_msgs/msg/Map.msg | 13 + lib/mapdesc_msgs/msg/Marker.msg | 5 + lib/mapdesc_msgs/msg/Mesh.msg | 5 + lib/mapdesc_msgs/msg/Path.msg | 7 + lib/mapdesc_msgs/msg/Wall.msg | 3 + lib/mapdesc_msgs/package.xml | 29 ++ lib/mapdesc_msgs/srv/MapAreaCreate.srv | 7 + lib/mapdesc_msgs/srv/MapAreaDelete.srv | 8 + lib/mapdesc_msgs/srv/MapAreaList.srv | 6 + lib/mapdesc_msgs/srv/MapAreaUpdate.srv | 8 + lib/mapdesc_msgs/srv/MapCreate.srv | 8 + lib/mapdesc_msgs/srv/MapDelete.srv | 7 + lib/mapdesc_msgs/srv/MapExtCreate.srv | 7 + lib/mapdesc_msgs/srv/MapExtDelete.srv | 8 + lib/mapdesc_msgs/srv/MapExtList.srv | 6 + lib/mapdesc_msgs/srv/MapExtUpdate.srv | 8 + lib/mapdesc_msgs/srv/MapGet.srv | 6 + lib/mapdesc_msgs/srv/MapList.srv | 5 + lib/mapdesc_msgs/srv/MapMarkerCreate.srv | 7 + lib/mapdesc_msgs/srv/MapMarkerDelete.srv | 8 + lib/mapdesc_msgs/srv/MapMarkerList.srv | 6 + lib/mapdesc_msgs/srv/MapMarkerUpdate.srv | 8 + lib/mapdesc_msgs/srv/MapOverwrite.srv | 4 + lib/mapdesc_msgs/srv/MapPathCreate.srv | 7 + lib/mapdesc_msgs/srv/MapPathDelete.srv | 8 + lib/mapdesc_msgs/srv/MapPathList.srv | 6 + lib/mapdesc_msgs/srv/MapPathUpdate.srv | 8 + lib/mapdesc_msgs/srv/MapUpdate.srv | 8 + lib/mapdesc_msgs/srv/MapWallCreate.srv | 7 + lib/mapdesc_msgs/srv/MapWallDelete.srv | 8 + lib/mapdesc_msgs/srv/MapWallList.srv | 6 + lib/mapdesc_msgs/srv/MapWallUpdate.srv | 8 + .../.devcontainer/devcontainer.json | 20 + lib/mapdesc_ros/.gitignore | 2 + lib/mapdesc_ros/.gitlab-ci.yml | 34 ++ lib/mapdesc_ros/.gitmodules | 6 + lib/mapdesc_ros/Dockerfile | 21 ++ lib/mapdesc_ros/Dockerfile-ros-pip-pytest | 15 + lib/mapdesc_ros/Dockerfile_dev | 24 ++ lib/mapdesc_ros/README.md | 10 + lib/mapdesc_ros/compose.yml | 31 ++ .../data/simple_marker_map.yml | 124 ++++++ .../launch_testing/marker_launch_test.py | 141 +++++++ .../mapdesc_ros/launch/mapdesc.launch.py | 33 ++ .../mapdesc_ros/mapdesc_ros/__init__.py | 0 .../mapdesc_ros/mapdesc_ros/convert_data.py | 136 +++++++ .../mapdesc_ros/mapdesc_ros/map_desc.py | 355 ++++++++++++++++++ .../mapdesc_ros/mapdesc_ros/node.py | 40 ++ lib/mapdesc_ros/mapdesc_ros/package.xml | 21 ++ lib/mapdesc_ros/mapdesc_ros/pytest.ini | 6 + .../mapdesc_ros/resource/mapdesc_ros | 0 lib/mapdesc_ros/mapdesc_ros/setup.cfg | 8 + lib/mapdesc_ros/mapdesc_ros/setup.py | 33 ++ .../mapdesc_ros/test/test_copyright.py | 25 ++ .../mapdesc_ros/test/test_flake8.py | 25 ++ .../mapdesc_ros/test/test_mapdesc_service.py | 3 + lib/mapdesc_ros/roscrud.yml | 15 + lib/mapdesc_ros/run_tests.bash | 13 + lib/mapdesc_ros/test_docker.bash | 4 + 70 files changed, 1519 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/entrypoint.bash create mode 100644 lib/mapdesc_msgs/CMakeLists.txt create mode 100644 lib/mapdesc_msgs/README.md create mode 100644 lib/mapdesc_msgs/msg/Area.msg create mode 100644 lib/mapdesc_msgs/msg/Box.msg create mode 100644 lib/mapdesc_msgs/msg/Dimension.msg create mode 100644 lib/mapdesc_msgs/msg/External.msg create mode 100644 lib/mapdesc_msgs/msg/LaneEdge.msg create mode 100644 lib/mapdesc_msgs/msg/LaneGraph.msg create mode 100644 lib/mapdesc_msgs/msg/LaneNode.msg create mode 100644 lib/mapdesc_msgs/msg/Map.msg create mode 100644 lib/mapdesc_msgs/msg/Marker.msg create mode 100644 lib/mapdesc_msgs/msg/Mesh.msg create mode 100644 lib/mapdesc_msgs/msg/Path.msg create mode 100644 lib/mapdesc_msgs/msg/Wall.msg create mode 100644 lib/mapdesc_msgs/package.xml create mode 100644 lib/mapdesc_msgs/srv/MapAreaCreate.srv create mode 100644 lib/mapdesc_msgs/srv/MapAreaDelete.srv create mode 100644 lib/mapdesc_msgs/srv/MapAreaList.srv create mode 100644 lib/mapdesc_msgs/srv/MapAreaUpdate.srv create mode 100644 lib/mapdesc_msgs/srv/MapCreate.srv create mode 100644 lib/mapdesc_msgs/srv/MapDelete.srv create mode 100644 lib/mapdesc_msgs/srv/MapExtCreate.srv create mode 100644 lib/mapdesc_msgs/srv/MapExtDelete.srv create mode 100644 lib/mapdesc_msgs/srv/MapExtList.srv create mode 100644 lib/mapdesc_msgs/srv/MapExtUpdate.srv create mode 100644 lib/mapdesc_msgs/srv/MapGet.srv create mode 100644 lib/mapdesc_msgs/srv/MapList.srv create mode 100644 lib/mapdesc_msgs/srv/MapMarkerCreate.srv create mode 100644 lib/mapdesc_msgs/srv/MapMarkerDelete.srv create mode 100644 lib/mapdesc_msgs/srv/MapMarkerList.srv create mode 100644 lib/mapdesc_msgs/srv/MapMarkerUpdate.srv create mode 100644 lib/mapdesc_msgs/srv/MapOverwrite.srv create mode 100644 lib/mapdesc_msgs/srv/MapPathCreate.srv create mode 100644 lib/mapdesc_msgs/srv/MapPathDelete.srv create mode 100644 lib/mapdesc_msgs/srv/MapPathList.srv create mode 100644 lib/mapdesc_msgs/srv/MapPathUpdate.srv create mode 100644 lib/mapdesc_msgs/srv/MapUpdate.srv create mode 100644 lib/mapdesc_msgs/srv/MapWallCreate.srv create mode 100644 lib/mapdesc_msgs/srv/MapWallDelete.srv create mode 100644 lib/mapdesc_msgs/srv/MapWallList.srv create mode 100644 lib/mapdesc_msgs/srv/MapWallUpdate.srv create mode 100644 lib/mapdesc_ros/.devcontainer/devcontainer.json create mode 100644 lib/mapdesc_ros/.gitignore create mode 100644 lib/mapdesc_ros/.gitlab-ci.yml create mode 100644 lib/mapdesc_ros/.gitmodules create mode 100644 lib/mapdesc_ros/Dockerfile create mode 100644 lib/mapdesc_ros/Dockerfile-ros-pip-pytest create mode 100644 lib/mapdesc_ros/Dockerfile_dev create mode 100644 lib/mapdesc_ros/README.md create mode 100644 lib/mapdesc_ros/compose.yml create mode 100644 lib/mapdesc_ros/mapdesc_ros/integration_tests/data/simple_marker_map.yml create mode 100755 lib/mapdesc_ros/mapdesc_ros/integration_tests/launch_testing/marker_launch_test.py create mode 100644 lib/mapdesc_ros/mapdesc_ros/launch/mapdesc.launch.py create mode 100644 lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/__init__.py create mode 100644 lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/convert_data.py create mode 100755 lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/map_desc.py create mode 100644 lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/node.py create mode 100644 lib/mapdesc_ros/mapdesc_ros/package.xml create mode 100644 lib/mapdesc_ros/mapdesc_ros/pytest.ini create mode 100644 lib/mapdesc_ros/mapdesc_ros/resource/mapdesc_ros create mode 100644 lib/mapdesc_ros/mapdesc_ros/setup.cfg create mode 100644 lib/mapdesc_ros/mapdesc_ros/setup.py create mode 100644 lib/mapdesc_ros/mapdesc_ros/test/test_copyright.py create mode 100644 lib/mapdesc_ros/mapdesc_ros/test/test_flake8.py create mode 100644 lib/mapdesc_ros/mapdesc_ros/test/test_mapdesc_service.py create mode 100644 lib/mapdesc_ros/roscrud.yml create mode 100755 lib/mapdesc_ros/run_tests.bash create mode 100755 lib/mapdesc_ros/test_docker.bash diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..7772078 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,31 @@ +ARG ROS_DISTRO=jazzy +FROM ros:${ROS_DISTRO} + +RUN apt update +RUN apt install -y \ + ros-${ROS_DISTRO}-navigation2 \ + ros-${ROS_DISTRO}-nav2-bringup \ + # ros-${ROS_DISTRO}-slam-toolbox \ + # ros-${ROS_DISTRO}-nav2-amcl \ + # ros-${ROS_DISTRO}-map-server \ + # ros-${ROS_DISTRO}-robot-localization \ + && rm -rf /var/lib/apt/lists/* + + +ENV COLCON_WS=/root/colcon_ws +ENV COLCON_WS_SRC=/root/colcon_ws/src + +COPY ./docker/entrypoint.bash /entrypoint.bash +RUN chmod +x /entrypoint.bash +ENTRYPOINT ["/entrypoint.bash"] + +COPY ./ricbot_navigation ${COLCON_WS_SRC}/ricbot_navigation +COPY ./lib/mapdesc_msgs ${COLCON_WS_SRC}/mapdesc_msgs +COPY ./lib/mapdesc_ros ${COLCON_WS_SRC}/mapdesc_ros + + +RUN cd ${COLCON_WS}\ + && . /opt/ros/${ROS_DISTRO}/setup.sh\ + && colcon build --packages-select ricbot_navigation mapdesc_msgs mapdesc_ros + +CMD ["ros2", "launch", "ricbot_navigation", "ricbot_nav.launch.py"] diff --git a/docker/entrypoint.bash b/docker/entrypoint.bash new file mode 100644 index 0000000..5e86be3 --- /dev/null +++ b/docker/entrypoint.bash @@ -0,0 +1,5 @@ +#!/bin/bash + +source /opt/ros/${ROS_DISTRO}/setup.sh +source ${COLCON_WS}/install/setup.sh +exec "$@" diff --git a/lib/mapdesc_msgs/CMakeLists.txt b/lib/mapdesc_msgs/CMakeLists.txt new file mode 100644 index 0000000..4380c50 --- /dev/null +++ b/lib/mapdesc_msgs/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.8) +project(mapdesc_msgs) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) + +find_package(std_msgs REQUIRED) +find_package(diagnostic_msgs REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(builtin_interfaces REQUIRED) +find_package(rclcpp REQUIRED) + +find_package(rosidl_default_generators REQUIRED) +ament_export_dependencies(rosidl_default_runtime) + +rosidl_generate_interfaces(${PROJECT_NAME} "msg/Area.msg" + "msg/Box.msg" + "msg/Dimension.msg" + "msg/External.msg" + "msg/LaneEdge.msg" + "msg/LaneGraph.msg" + "msg/LaneNode.msg" + "msg/Map.msg" + "msg/Marker.msg" + "msg/Mesh.msg" + "msg/Path.msg" + "msg/Wall.msg" + "srv/MapAreaCreate.srv" + "srv/MapAreaDelete.srv" + "srv/MapAreaList.srv" + "srv/MapAreaUpdate.srv" + "srv/MapCreate.srv" + "srv/MapDelete.srv" + "srv/MapExtCreate.srv" + "srv/MapExtDelete.srv" + "srv/MapExtList.srv" + "srv/MapExtUpdate.srv" + "srv/MapGet.srv" + "srv/MapList.srv" + "srv/MapMarkerCreate.srv" + "srv/MapMarkerDelete.srv" + "srv/MapMarkerList.srv" + "srv/MapMarkerUpdate.srv" + "srv/MapOverwrite.srv" + "srv/MapPathCreate.srv" + "srv/MapPathDelete.srv" + "srv/MapPathList.srv" + "srv/MapPathUpdate.srv" + "srv/MapUpdate.srv" + "srv/MapWallCreate.srv" + "srv/MapWallDelete.srv" + "srv/MapWallList.srv" + "srv/MapWallUpdate.srv" + DEPENDENCIES diagnostic_msgs std_msgs geometry_msgs) + +ament_package() diff --git a/lib/mapdesc_msgs/README.md b/lib/mapdesc_msgs/README.md new file mode 100644 index 0000000..8fb3801 --- /dev/null +++ b/lib/mapdesc_msgs/README.md @@ -0,0 +1,7 @@ +# Map Description ROS 2 Messages +ROS 2 messages and services for the Map Description ROS 2 Wrapper. + + +## Motivation +Part of [Map Description ROS 2 Wrapper](../mapdesc_ros) but as own repository so you don't have to check out nor install the whole Map-Desc +with all its requirements (like OpenCV2) if you only want to talk to the Map Description ROS 2 wrapper from another machine. diff --git a/lib/mapdesc_msgs/msg/Area.msg b/lib/mapdesc_msgs/msg/Area.msg new file mode 100644 index 0000000..1ab05f0 --- /dev/null +++ b/lib/mapdesc_msgs/msg/Area.msg @@ -0,0 +1,5 @@ +string name +string type +string area_type +uint8[] color +Mesh data \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/Box.msg b/lib/mapdesc_msgs/msg/Box.msg new file mode 100644 index 0000000..3ab9d9e --- /dev/null +++ b/lib/mapdesc_msgs/msg/Box.msg @@ -0,0 +1,4 @@ +# should be box +string type +Dimension size +geometry_msgs/Pose pose \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/Dimension.msg b/lib/mapdesc_msgs/msg/Dimension.msg new file mode 100644 index 0000000..462685c --- /dev/null +++ b/lib/mapdesc_msgs/msg/Dimension.msg @@ -0,0 +1,3 @@ +float64 width +float64 height +float64 length \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/External.msg b/lib/mapdesc_msgs/msg/External.msg new file mode 100644 index 0000000..2a17523 --- /dev/null +++ b/lib/mapdesc_msgs/msg/External.msg @@ -0,0 +1,4 @@ +string name +string type +Mesh data +string[] filenames \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/LaneEdge.msg b/lib/mapdesc_msgs/msg/LaneEdge.msg new file mode 100644 index 0000000..30b6d4a --- /dev/null +++ b/lib/mapdesc_msgs/msg/LaneEdge.msg @@ -0,0 +1,5 @@ +LaneNode source +LaneNode target +int8 edge_type +string name +diagnostic_msgs/KeyValue params \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/LaneGraph.msg b/lib/mapdesc_msgs/msg/LaneGraph.msg new file mode 100644 index 0000000..8b807d5 --- /dev/null +++ b/lib/mapdesc_msgs/msg/LaneGraph.msg @@ -0,0 +1,2 @@ +LaneNode[] nodes +LaneEdge[] edges \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/LaneNode.msg b/lib/mapdesc_msgs/msg/LaneNode.msg new file mode 100644 index 0000000..bba2d37 --- /dev/null +++ b/lib/mapdesc_msgs/msg/LaneNode.msg @@ -0,0 +1,3 @@ +string name +geometry_msgs/Vector3 position +diagnostic_msgs/KeyValue params \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/Map.msg b/lib/mapdesc_msgs/msg/Map.msg new file mode 100644 index 0000000..ab8c989 --- /dev/null +++ b/lib/mapdesc_msgs/msg/Map.msg @@ -0,0 +1,13 @@ +string name +string description + +Dimension size +float64 resolution +geometry_msgs/Vector3 origin + +Marker[] marker +Area[] area +Wall[] wall +Path[] path +External[] ext +LaneGraph lane_graph \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/Marker.msg b/lib/mapdesc_msgs/msg/Marker.msg new file mode 100644 index 0000000..aa18f7d --- /dev/null +++ b/lib/mapdesc_msgs/msg/Marker.msg @@ -0,0 +1,5 @@ +geometry_msgs/Pose pose +string name +uint8[] color +string type +float32 radius \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/Mesh.msg b/lib/mapdesc_msgs/msg/Mesh.msg new file mode 100644 index 0000000..68753e3 --- /dev/null +++ b/lib/mapdesc_msgs/msg/Mesh.msg @@ -0,0 +1,5 @@ +# mesh +geometry_msgs/Vector3[] polygons +# box +Dimension size +geometry_msgs/Pose pose \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/Path.msg b/lib/mapdesc_msgs/msg/Path.msg new file mode 100644 index 0000000..cdd13e6 --- /dev/null +++ b/lib/mapdesc_msgs/msg/Path.msg @@ -0,0 +1,7 @@ +string name +int64[] points +geometry_msgs/Pose pose +geometry_msgs/Vector3 size +string color +bool distance_relative_to_ground +float64 radius \ No newline at end of file diff --git a/lib/mapdesc_msgs/msg/Wall.msg b/lib/mapdesc_msgs/msg/Wall.msg new file mode 100644 index 0000000..580d267 --- /dev/null +++ b/lib/mapdesc_msgs/msg/Wall.msg @@ -0,0 +1,3 @@ +Mesh data +string name +string type \ No newline at end of file diff --git a/lib/mapdesc_msgs/package.xml b/lib/mapdesc_msgs/package.xml new file mode 100644 index 0000000..929d251 --- /dev/null +++ b/lib/mapdesc_msgs/package.xml @@ -0,0 +1,29 @@ + + + + mapdesc_msgs + 0.0.1 + Messages for MapDesc. + Andreas Bresser + BSD-3 + + rclcpp + diagnostic_msgs + geometry_msgs + std_msgs + + builtin_interfaces + rosidl_default_runtime + + rosidl_default_generators + ament_cmake + + rosidl_default_generators + + rosidl_interface_packages + + + ament_cmake + + + diff --git a/lib/mapdesc_msgs/srv/MapAreaCreate.srv b/lib/mapdesc_msgs/srv/MapAreaCreate.srv new file mode 100644 index 0000000..03ef2e8 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapAreaCreate.srv @@ -0,0 +1,7 @@ +# This file has been generated by ROSCRUD + +# Create area on map with unique identifier (name) +string name +Area item +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapAreaDelete.srv b/lib/mapdesc_msgs/srv/MapAreaDelete.srv new file mode 100644 index 0000000..8b2cf56 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapAreaDelete.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Delete area at index on map +string name +uint32 index +--- +# Is False if map.name did not exist but was requested to delete or index out of range +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapAreaList.srv b/lib/mapdesc_msgs/srv/MapAreaList.srv new file mode 100644 index 0000000..d3997a9 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapAreaList.srv @@ -0,0 +1,6 @@ +# This file has been generated by ROSCRUD + +# get all area from map by name. +string name +--- +Area[] area \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapAreaUpdate.srv b/lib/mapdesc_msgs/srv/MapAreaUpdate.srv new file mode 100644 index 0000000..6633ea4 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapAreaUpdate.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Update on map at given index +string name +Area item +uint32 index +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapCreate.srv b/lib/mapdesc_msgs/srv/MapCreate.srv new file mode 100644 index 0000000..d6cc4db --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapCreate.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Create map with unique identifier (name) +Map map +--- +# is False if map.name already existed. +# use MapUpdate.srv instead for updating an existing map +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapDelete.srv b/lib/mapdesc_msgs/srv/MapDelete.srv new file mode 100644 index 0000000..25fff90 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapDelete.srv @@ -0,0 +1,7 @@ +# This file has been generated by ROSCRUD + +# Delete map +string name +--- +# Is False if map.name did not exist but was requested to delete +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapExtCreate.srv b/lib/mapdesc_msgs/srv/MapExtCreate.srv new file mode 100644 index 0000000..d0f2f2f --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapExtCreate.srv @@ -0,0 +1,7 @@ +# This file has been generated by ROSCRUD + +# Create ext on map with unique identifier (name) +string name +External item +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapExtDelete.srv b/lib/mapdesc_msgs/srv/MapExtDelete.srv new file mode 100644 index 0000000..a2b91d9 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapExtDelete.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Delete ext at index on map +string name +uint32 index +--- +# Is False if map.name did not exist but was requested to delete or index out of range +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapExtList.srv b/lib/mapdesc_msgs/srv/MapExtList.srv new file mode 100644 index 0000000..9aea42f --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapExtList.srv @@ -0,0 +1,6 @@ +# This file has been generated by ROSCRUD + +# get all ext from map by name. +string name +--- +External[] ext \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapExtUpdate.srv b/lib/mapdesc_msgs/srv/MapExtUpdate.srv new file mode 100644 index 0000000..b8bacbf --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapExtUpdate.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Update on map at given index +string name +External item +uint32 index +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapGet.srv b/lib/mapdesc_msgs/srv/MapGet.srv new file mode 100644 index 0000000..afbcc05 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapGet.srv @@ -0,0 +1,6 @@ +# This file has been generated by ROSCRUD + +# Get single map by its unique identifier (name) +string name +--- +Map map \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapList.srv b/lib/mapdesc_msgs/srv/MapList.srv new file mode 100644 index 0000000..0285cb8 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapList.srv @@ -0,0 +1,5 @@ +# This file has been generated by ROSCRUD + +--- +# get a list of all map +Map[] map \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapMarkerCreate.srv b/lib/mapdesc_msgs/srv/MapMarkerCreate.srv new file mode 100644 index 0000000..2c8f9a6 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapMarkerCreate.srv @@ -0,0 +1,7 @@ +# This file has been generated by ROSCRUD + +# Create marker on map with unique identifier (name) +string name +Marker item +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapMarkerDelete.srv b/lib/mapdesc_msgs/srv/MapMarkerDelete.srv new file mode 100644 index 0000000..4691a71 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapMarkerDelete.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Delete marker at index on map +string name +uint32 index +--- +# Is False if map.name did not exist but was requested to delete or index out of range +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapMarkerList.srv b/lib/mapdesc_msgs/srv/MapMarkerList.srv new file mode 100644 index 0000000..6f9b8ea --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapMarkerList.srv @@ -0,0 +1,6 @@ +# This file has been generated by ROSCRUD + +# get all marker from map by name. +string name +--- +Marker[] marker \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapMarkerUpdate.srv b/lib/mapdesc_msgs/srv/MapMarkerUpdate.srv new file mode 100644 index 0000000..422ee30 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapMarkerUpdate.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Update on map at given index +string name +Marker item +uint32 index +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapOverwrite.srv b/lib/mapdesc_msgs/srv/MapOverwrite.srv new file mode 100644 index 0000000..ce8e06d --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapOverwrite.srv @@ -0,0 +1,4 @@ +# This file has been generated by ROSCRUD + +bool allow_overwrite +--- \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapPathCreate.srv b/lib/mapdesc_msgs/srv/MapPathCreate.srv new file mode 100644 index 0000000..53f889f --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapPathCreate.srv @@ -0,0 +1,7 @@ +# This file has been generated by ROSCRUD + +# Create path on map with unique identifier (name) +string name +Path item +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapPathDelete.srv b/lib/mapdesc_msgs/srv/MapPathDelete.srv new file mode 100644 index 0000000..014f2dd --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapPathDelete.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Delete path at index on map +string name +uint32 index +--- +# Is False if map.name did not exist but was requested to delete or index out of range +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapPathList.srv b/lib/mapdesc_msgs/srv/MapPathList.srv new file mode 100644 index 0000000..545fd2e --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapPathList.srv @@ -0,0 +1,6 @@ +# This file has been generated by ROSCRUD + +# get all path from map by name. +string name +--- +Path[] path \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapPathUpdate.srv b/lib/mapdesc_msgs/srv/MapPathUpdate.srv new file mode 100644 index 0000000..1b4258e --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapPathUpdate.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Update on map at given index +string name +Path item +uint32 index +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapUpdate.srv b/lib/mapdesc_msgs/srv/MapUpdate.srv new file mode 100644 index 0000000..4679d0f --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapUpdate.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Update map +Map map +--- +# is False if map.name does not exist. +# use MapCreate.srv instead to create a new map entry. +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapWallCreate.srv b/lib/mapdesc_msgs/srv/MapWallCreate.srv new file mode 100644 index 0000000..e469040 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapWallCreate.srv @@ -0,0 +1,7 @@ +# This file has been generated by ROSCRUD + +# Create wall on map with unique identifier (name) +string name +Wall item +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapWallDelete.srv b/lib/mapdesc_msgs/srv/MapWallDelete.srv new file mode 100644 index 0000000..8b3a272 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapWallDelete.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Delete wall at index on map +string name +uint32 index +--- +# Is False if map.name did not exist but was requested to delete or index out of range +bool success \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapWallList.srv b/lib/mapdesc_msgs/srv/MapWallList.srv new file mode 100644 index 0000000..82e9bbc --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapWallList.srv @@ -0,0 +1,6 @@ +# This file has been generated by ROSCRUD + +# get all wall from map by name. +string name +--- +Wall[] wall \ No newline at end of file diff --git a/lib/mapdesc_msgs/srv/MapWallUpdate.srv b/lib/mapdesc_msgs/srv/MapWallUpdate.srv new file mode 100644 index 0000000..2fe3892 --- /dev/null +++ b/lib/mapdesc_msgs/srv/MapWallUpdate.srv @@ -0,0 +1,8 @@ +# This file has been generated by ROSCRUD + +# Update on map at given index +string name +Wall item +uint32 index +--- +bool success \ No newline at end of file diff --git a/lib/mapdesc_ros/.devcontainer/devcontainer.json b/lib/mapdesc_ros/.devcontainer/devcontainer.json new file mode 100644 index 0000000..51999d6 --- /dev/null +++ b/lib/mapdesc_ros/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "MapDesc", + "workspaceFolder": "/root/colcon_ws", + "dockerComposeFile": "../compose.yml", + "overrideCommand": true, + "service": "mapdesc_ros_dev", + "customizations":{ + "vscode": { + "extensions": [ + "MermaidChart.vscode-mermaid-chart", + "ms-python.python", + "ms-python.debugpy", + "ms-python.flake8", + "ms-vscode.cpptools", + "ms-vscode.cpptools-extension-pack", + "twxs.cmake" + ] + } + } +} diff --git a/lib/mapdesc_ros/.gitignore b/lib/mapdesc_ros/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/lib/mapdesc_ros/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/lib/mapdesc_ros/.gitlab-ci.yml b/lib/mapdesc_ros/.gitlab-ci.yml new file mode 100644 index 0000000..39f79c5 --- /dev/null +++ b/lib/mapdesc_ros/.gitlab-ci.yml @@ -0,0 +1,34 @@ +image: d-reg.hb.dfki.de:5000/robot-config/ros-pip-pytest:humble-0.0.1 + +variables: + GIT_SUBMODULE_STRATEGY: recursive + +stages: + - build + - test + +mapdesc_ros build: + stage: build + script: + - cd mapdesc + - pip3 install -r requirements.txt + - pip3 install . + - cd .. + - colcon build + +mapdesc_ros test: + stage: test + script: + - cd mapdesc + - pip3 install -r requirements.txt + - pip3 install . + - cd .. + - colcon build + - source ./install/setup.bash + # Basic Unit Tests + - colcon test --pytest-with-coverage --event-handlers console_cohesion+ --return-code-on-test-failure + # Integration tests + - launch_test mapdesc_ros/integration_tests/launch_testing/marker_launch_test.py + artifacts: + paths: + - build/mapdesc_ros/ diff --git a/lib/mapdesc_ros/.gitmodules b/lib/mapdesc_ros/.gitmodules new file mode 100644 index 0000000..c006add --- /dev/null +++ b/lib/mapdesc_ros/.gitmodules @@ -0,0 +1,6 @@ +[submodule "mapdesc"] + path = mapdesc + url = ../mapdesc +[submodule "mapdesc_msgs"] + path = mapdesc_msgs + url = ../mapdesc_msgs diff --git a/lib/mapdesc_ros/Dockerfile b/lib/mapdesc_ros/Dockerfile new file mode 100644 index 0000000..fe8f2aa --- /dev/null +++ b/lib/mapdesc_ros/Dockerfile @@ -0,0 +1,21 @@ +ARG ROS_DISTRO +FROM d-reg.hb.dfki.de/robot-config/ros-pip-pytest:${ROS_DISTRO}-0.0.1 + +ENV COLCON_WS=/root/colcon_ws +ENV COLCON_WS_SRC=/root/colcon_ws/src + +COPY ./mapdesc/requirements.txt /tmp/requirements.txt +RUN pip3 install -r /tmp/requirements.txt && rm /tmp/requirements.txt + +COPY ./mapdesc /opt/mapdesc +WORKDIR /opt/mapdesc +RUN pip3 install . + +WORKDIR ${COLCON_WS} +COPY ./mapdesc_ros ${COLCON_WS_SRC}/mapdesc_ros +COPY ./mapdesc_msgs ${COLCON_WS_SRC}/mapdesc_msgs +RUN . /opt/ros/${ROS_DISTRO}/setup.sh && colcon build + +COPY ./run_tests.bash /run_tests.bash +RUN chmod +x /run_tests.bash +CMD [ "bash", "-c", "/run_tests.bash"] \ No newline at end of file diff --git a/lib/mapdesc_ros/Dockerfile-ros-pip-pytest b/lib/mapdesc_ros/Dockerfile-ros-pip-pytest new file mode 100644 index 0000000..5d29dcf --- /dev/null +++ b/lib/mapdesc_ros/Dockerfile-ros-pip-pytest @@ -0,0 +1,15 @@ +ARG ROS_DISTRO +FROM ros:${ROS_DISTRO} + +RUN apt-get update -qq \ + && apt-get install -y \ + python3-pip \ + python3-opencv \ + ros-${ROS_DISTRO}-std-msgs \ + ros-${ROS_DISTRO}-geometry-msgs \ + ros-${ROS_DISTRO}-diagnostic-msgs \ + ament-cmake \ + && rm -rf /var/lib/apt/lists/* + +# we need to upgrade flake8 to supress a deprecation warning. +RUN pip3 install --upgrade flake8 pytest-cov opencv-python diff --git a/lib/mapdesc_ros/Dockerfile_dev b/lib/mapdesc_ros/Dockerfile_dev new file mode 100644 index 0000000..5a0d414 --- /dev/null +++ b/lib/mapdesc_ros/Dockerfile_dev @@ -0,0 +1,24 @@ +ARG ROS_DISTRO +FROM d-reg.hb.dfki.de/robot-config/ros-pip-pytest:${ROS_DISTRO}-0.0.1 + +ENV COLCON_WS=/root/colcon_ws +ENV COLCON_WS_SRC=/root/colcon_ws/src + +COPY ./mapdesc/requirements.txt /tmp/requirements.txt +RUN pip3 install -r /tmp/requirements.txt && rm /tmp/requirements.txt + +COPY ./mapdesc /opt/mapdesc +WORKDIR /opt/mapdesc +# For development we want to overwrite mapdesc from local +RUN pip3 install -e . + +WORKDIR ${COLCON_WS} +COPY ./mapdesc_ros ${COLCON_WS_SRC}/mapdesc_ros +COPY ./mapdesc_msgs ${COLCON_WS_SRC}/mapdesc_msgs +# For development we want to overwrite mapdesc_ros and mapdesc_msgs from local +RUN . /opt/ros/${ROS_DISTRO}/setup.sh \ + && colcon build --symlink-install + +COPY ./run_tests.bash /run_tests.bash +RUN chmod +x /run_tests.bash +CMD [ "bash", "-c", "/run_tests.bash"] \ No newline at end of file diff --git a/lib/mapdesc_ros/README.md b/lib/mapdesc_ros/README.md new file mode 100644 index 0000000..b5e598f --- /dev/null +++ b/lib/mapdesc_ros/README.md @@ -0,0 +1,10 @@ +# MapDesc ROS 2 wrapper +A lightweight ROS 2 wrapper for [mapdesc](../mapdesc/) + +This wrapper has been generated using ROSCrud + +# Unit testing +see mapdesc_ros/test + +# Integration testing +see https://github.com/ros2/launch/tree/master/launch_testing/ diff --git a/lib/mapdesc_ros/compose.yml b/lib/mapdesc_ros/compose.yml new file mode 100644 index 0000000..016b147 --- /dev/null +++ b/lib/mapdesc_ros/compose.yml @@ -0,0 +1,31 @@ +services: + mapdesc_ros: + image: d-reg.hb.dfki.de/robot-config/mapdesc_ros:humble-0.0.1 + build: + context: . + dockerfile: Dockerfile + args: + ROS_DISTRO: humble + + mapdesc_ros_dev: + # custom Dockerfile that installs without moving files + # (so we can easily overwrite them from volumes for easier + # development with Dev Container) + build: + context: . + dockerfile: Dockerfile_dev + args: + ROS_DISTRO: humble + volumes: + - ./mapdesc:/opt/mapdesc:rw + - ./mapdesc_ros:/root/colcon_ws/src/mapdesc_ros:rw + - ./mapdesc_msgs:/root/colcon_ws/src/mapdesc_msgs:rw + - ./run_tests.bash:/root/colcon_ws/run_tests.bash:rw + + ros_with_pip: + image: d-reg.hb.dfki.de/robot-config/ros-pip-pytest:humble-0.0.1 + build: + context: . + dockerfile: Dockerfile-ros-pip-pytest + args: + ROS_DISTRO: humble \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc_ros/integration_tests/data/simple_marker_map.yml b/lib/mapdesc_ros/mapdesc_ros/integration_tests/data/simple_marker_map.yml new file mode 100644 index 0000000..9692f6c --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/integration_tests/data/simple_marker_map.yml @@ -0,0 +1,124 @@ +{ + name: simple_map_marker, + description: A simple room with 4 walls and a big entrance., + marker: + [ + { + name: center_marker, + pose: { position: [ 0, 0, 0 ], orientation: [ 0, 0, 0, 1 ] }, + color: [ 255, 50, 50 ], + radius: 0.1 + }, + { + name: north_west, + radius: 0.2, + color: [ 255, 0, 0 ], + pose: + { + position: [ -6.788245960015084, 7.485528022376871, 0 ], + orientation: [ 0, 0, 0, 1 ] + } + }, + { + name: north_east, + radius: 0.2, + color: [ 255, 0, 0 ], + pose: + { + position: [ 7.239428524401198, 8.223826737355047, 0 ], + orientation: [ 0, 0, 0, 1 ] + } + }, + { + name: south_east, + radius: 0.2, + color: [ 255, 0, 0 ], + pose: + { + position: [ 6.829261498693715, -7.936709309296662, 0 ], + orientation: [ 0, 0, 0, 1 ] + } + } + ], + area: + [ + { + area_type: demonstation_area, + color: [ 255, 200, 200 ], + type: mesh, + data: + { + polygons: [ [ -7, -5 ], [ -2, -5 ], [ -2, 5 ], [ -7, 5 ] ], + pose: { orientation: [ 0, 0, 0, 1 ], position: [ -1, -1, 0 ] }, + size: [ 1, 1, 1 ] + }, + name: "" + }, + { + area_type: demonstation_area, + color: [ 100, 200, 200 ], + type: mesh, + data: + { + polygons: [ [ 7, -5 ], [ 4.5, -7 ], [ 2, -5 ], [ 2, 5 ], [ 4.5, 7 ], [ 7, 5 ] ], + pose: { orientation: [ 0, 0, 0, 1 ], position: [ 1, 1, 0 ] }, + size: [ 1, 1, 1 ] + }, + name: "" + }, + { + area_type: outside, + name: outside, + type: box, + data: + { + pose: { position: [ 0, 17, 0 ], orientation: [ 0, 0, 0, 1 ] }, + size: [ 12, 10, 2 ] + }, + color: [ 100, 200, 170 ] + } + ], + wall: + [ + { + type: box, + data: + { + pose: { position: [ 10, 0, 1.5 ], orientation: [ 0, 0, 0, 1 ] }, + size: [ 0.2, 20, 3 ] + } + }, + { + type: box, + data: + { + pose: { position: [ -10, 0, 1.5 ], orientation: [ 0, 0, 0, 1 ] }, + size: [ 0.2, 20, 3 ] + } + }, + { + type: box, + data: + { + pose: { position: [ 0, 10, 1.5 ], orientation: [ 0, 0, 0.7071068, 0.7071068 ] }, + size: [ 0.2, 20, 3 ] + } + }, + { + type: box, + data: + { + pose: { position: [ 7.5, -10, 1.5 ], orientation: [ 0, 0, 0.7071068, 0.7071068 ] }, + size: [ 0.2, 5, 3 ] + } + }, + { + type: box, + data: + { + pose: { position: [ -7.5, -10, 1.5 ], orientation: [ 0, 0, 0.7071068, 0.7071068 ] }, + size: [ 0.2, 5, 3 ] + } + } + ] +} diff --git a/lib/mapdesc_ros/mapdesc_ros/integration_tests/launch_testing/marker_launch_test.py b/lib/mapdesc_ros/mapdesc_ros/integration_tests/launch_testing/marker_launch_test.py new file mode 100755 index 0000000..7ed2510 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/integration_tests/launch_testing/marker_launch_test.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# based on https://github.com/ros2/launch_ros/blob/master/ +# launch_testing_ros/test/examples/check_msgs_launch_test.py + +# ROS 2 basics +from rclpy.task import Future +import rclpy + +# ROS 2 launchfile +import launch +import launch.actions +import launch_ros.actions +import launch_testing.actions + +# unit testing and pytest +import pytest +from threading import Event +from threading import Thread +import unittest + +# launch_testing +from launch_testing.io_handler import ActiveIoHandler +import launch_testing.markers + +# custom messages and services +from pathlib import Path +from mapdesc_msgs.srv import MapMarkerCreate, MapMarkerList, MapOverwrite + +BASE_PATH = Path(__file__).parent.absolute() + + +@pytest.mark.launch_test +@launch_testing.markers.keep_alive +def generate_test_description(): + map_yaml = str(BASE_PATH.parent / 'data' / 'simple_marker_map.yml') + return launch.LaunchDescription([ + launch_ros.actions.Node( + package='mapdesc_ros', + executable='mapdesc_service', + name='mapdesc_node', + parameters=[ + {'map_yaml': map_yaml} + ] + ), + launch_testing.actions.ReadyToTest() + ]) + + +class TestFixture(unittest.TestCase): + def marker_added_callback(self, future: Future): + """Callback that gets executed when the marker has been added.""" + assert future.result().success + self.add_marker_service_success_event.set() + + def marker_listed_callback(self, future: Future): + known_marker = [ + 'center_marker', 'north_west', 'north_east', 'south_east'] + marker = future.result().marker + marker_names = [m.name for m in marker] + if len(marker) == len(known_marker): + assert marker_names == known_marker + elif len(marker) == len(known_marker)+1: + # the other test has been executed first + assert marker_names == known_marker + ['new point'] + else: + assert False, 'number of stored marker does not match' + self.list_marker_service_success_event.set() + + def spin(self): + try: + while rclpy.ok() and not self.spinning.is_set(): + rclpy.spin_once(self.node, timeout_sec=0.1) + finally: + return + + def setUp(self): + rclpy.init() + self.node = rclpy.create_node('test_node') + self.list_marker_service_success_event = Event() + self.add_marker_service_success_event = Event() + self.spinning = Event() + # Add a spin thread + self.ros_spin_thread = Thread(target=self.spin) + self.ros_spin_thread.start() + + def wait_for_service(self, service_clz, service_name): + """wait for a service to become available.""" + self.cli = self.node.create_client(service_clz, service_name) + service_available = False + for _try in range(10): + if self.cli.wait_for_service(timeout_sec=.5): + service_available = True + self.node.get_logger().info( + f'service {service_name} is available 👍!') + break + self.node.get_logger().info( + f'service "{service_name}" not available, waiting again...') + + if not service_available: + raise RuntimeError(f'Service "{service_name}" not available ☠!') + + def list_marker(self): + """check if loaded marker are in the list.""" + self.wait_for_service(MapMarkerList, 'mapdesc/marker/list') + request = MapMarkerList.Request(name='simple_map_marker') + future = self.cli.call_async(request) + future.add_done_callback(self.marker_listed_callback) + + def check_overwrite_map(self): + self.wait_for_service(MapOverwrite, '/mapdesc/map/overwrite') + + def add_marker(self): + """test if we can add a new marker""" + self.wait_for_service(MapMarkerCreate, 'mapdesc/marker/create') + request = MapMarkerCreate.Request(name='simple_map_marker') + marker = request.item + marker.name = 'new point' + marker.pose.position.x = 10.0 + marker.pose.position.y = 12.0 + marker.pose.position.z = 0.5 + future = self.cli.call_async(request) + future.add_done_callback(self.marker_added_callback) + + def tearDown(self): + self.spinning.set() + self.ros_spin_thread.join() + self.node.destroy_client(self.cli) + self.node.destroy_node() + rclpy.shutdown() + + def test_check_if_service_called(self, proc_output: ActiveIoHandler): + self.add_marker() + service_called = self.add_marker_service_success_event.wait( + timeout=15.0) + assert service_called, 'Service to add marker not called!' + + def test_check_marker_listed(self, proc_output: ActiveIoHandler): + self.list_marker() + marker_listed = self.list_marker_service_success_event.wait( + timeout=15.0) + assert marker_listed, 'Service to list marker not called!' diff --git a/lib/mapdesc_ros/mapdesc_ros/launch/mapdesc.launch.py b/lib/mapdesc_ros/mapdesc_ros/launch/mapdesc.launch.py new file mode 100644 index 0000000..090ce28 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/launch/mapdesc.launch.py @@ -0,0 +1,33 @@ +from pathlib import Path +from launch import LaunchDescription +from launch.actions import OpaqueFunction +from launch_ros.actions import Node +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration + + +def launch_setup(context, *args, **kwargs): + """ + Generate launch description to start the wrapper for the mapdesc library. + """ + map_yaml = LaunchConfiguration('map_yaml').perform(context) + return [ + LaunchDescription([ + Node( + package='mapdesc_ros', + executable='mapdesc_service', + name='mapdesc_node', + parameters=[ + {'map_yaml': map_yaml} + ] + ) + ])] + + +def generate_launch_description(): + map_yaml = str(Path('/map_data') / 'rh1_eg.yml') + return LaunchDescription([ + DeclareLaunchArgument( + "map_yaml", default_value=map_yaml), + OpaqueFunction(function=launch_setup) + ]) diff --git a/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/__init__.py b/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/convert_data.py b/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/convert_data.py new file mode 100644 index 0000000..48b5772 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/convert_data.py @@ -0,0 +1,136 @@ +from geometry_msgs.msg import Pose as PoseMsg +from geometry_msgs.msg import Point as PointMsg +from geometry_msgs.msg import Vector3 as Vector3Msg +from geometry_msgs.msg import Quaternion as QuaternionMsg + +from mapdesc_msgs.msg import Area as AreaMsg +from mapdesc_msgs.msg import Box as BoxMsg +from mapdesc_msgs.msg import Map as MapMsg +from mapdesc_msgs.msg import Marker as MarkerMsg +from mapdesc_msgs.msg import Mesh as MeshMsg + +# mapdesc +from mapdesc.model.geom.pose import Pose +from mapdesc.model.geom.vector3 import Vector3 +from mapdesc.model.geom.quaternion import Quaternion + +from mapdesc.model.geom.box import Box +from mapdesc.model.geom.mesh import Mesh + +from mapdesc.model.area import Area +from mapdesc.model.map import Map +from mapdesc.model.marker import Marker + + +def pose_ros_to_mapdesc(pose: PoseMsg) -> Pose: + return Pose( + position=Vector3( + x=pose.position.x, + y=pose.position.y, + z=pose.position.z), + orientation=Quaternion( + x=pose.orientation.x, + y=pose.orientation.y, + z=pose.orientation.z, + w=pose.orientation.w) + ) + + +def pose_mapdesc_to_ros(pose: Pose) -> PoseMsg: + return PoseMsg( + position=PointMsg( + x=float(pose.position.x), + y=float(pose.position.y), + z=float(pose.position.z)), + orientation=QuaternionMsg( + x=float(pose.orientation.x), + y=float(pose.orientation.y), + z=float(pose.orientation.z), + w=float(pose.orientation.w)) + ) + + +def mesh_ros_to_mapdesc(data: MeshMsg | BoxMsg) -> Mesh | Box: + """Convert mapdesc_msg/MeshBox to mapdesc.model.Mesh/mapdesc.model.Box + """ + pose = pose_ros_to_mapdesc(data.pose) + if isinstance(data, MeshMsg): + return Mesh( + pose=pose, + polygons=data.polygons + ) + elif isinstance(data, BoxMsg): + return Box( + pose=pose, + size=data.size + ) + else: + raise RuntimeError('Not a valid ros mapdesc_msg mesh/box') + + +def mesh_mapdesc_to_ros(data: Mesh | Box) -> MeshMsg | BoxMsg: + """Convert mapdesc_msg/MeshBox to mapdesc.model.Mesh/mapdesc.model.Box + """ + if isinstance(data, Mesh): + return MeshMsg( + pose=pose_mapdesc_to_ros(data.pose), + polygons=[Vector3Msg( + x=float(p.x), y=float(p.y), z=0.0) + for p in data.polygons] + ) + elif isinstance(data, Box): + return BoxMsg( + pose=pose_mapdesc_to_ros(data.pose) + ) + else: + raise RuntimeError('Not a valid mapdesc mesh/box geometry') + + +def area_ros_to_mapdesc(area: AreaMsg) -> Area: + assert isinstance(area, AreaMsg) + return Area( + name=area.name, + type=area.type, + area_type=area.area_type, + color=area.color, + data=mesh_ros_to_mapdesc(area.data) + ) + + +def area_mapdesc_to_ros(area: Area) -> AreaMsg: + assert isinstance(area, Area) + return AreaMsg( + name=area.name, + type=area.type, + area_type=area.area_type, + color=area.color, + data=mesh_mapdesc_to_ros(area.data) + ) + + +def marker_ros_to_mapdesc(marker: MarkerMsg) -> Marker: + return Marker( + name=marker.name, + pose=pose_ros_to_mapdesc(marker.pose), + color=marker.color, + type=marker.type, + radius=marker.radius + ) + + +def marker_mapdesc_to_ros(marker: Marker) -> MarkerMsg: + return MarkerMsg( + name=marker.name, + pose=pose_mapdesc_to_ros(marker.pose), + color=marker.color, + type=marker.type, + radius=float(marker.radius) + ) + + +def map_ros_to_mapdesc(_map: Map): + return MapMsg( + name=_map.name, + marker=[marker_mapdesc_to_ros(m) for m in _map.marker], + # area=[area_mapdesc_to_ros(a) for a in _map.area] + ) diff --git a/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/map_desc.py b/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/map_desc.py new file mode 100755 index 0000000..9a8a27b --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/map_desc.py @@ -0,0 +1,355 @@ +import rclpy +from rclpy.node import Node + +from std_msgs.msg import String +from mapdesc_msgs.srv import \ + MapCreate, \ + MapDelete, \ + MapGet, \ + MapList, \ + MapUpdate, \ + MapOverwrite, \ + MapMarkerCreate, \ + MapMarkerDelete, \ + MapMarkerList, \ + MapMarkerUpdate, \ + MapAreaCreate, \ + MapAreaDelete, \ + MapAreaList, \ + MapAreaUpdate, \ + MapWallCreate, \ + MapWallDelete, \ + MapWallList, \ + MapWallUpdate, \ + MapPathCreate, \ + MapPathDelete, \ + MapPathList, \ + MapPathUpdate, \ + MapExtCreate, \ + MapExtDelete, \ + MapExtList, \ + MapExtUpdate +from mapdesc_msgs.msg import \ + Area, \ + External, \ + Map, \ + Marker, \ + Path, \ + Wall + +# This file has initially been generated using ROSCRUD + +PREFIX = 'mapdesc' + +# ROS CRUD services +MAP_CREATE = f'{PREFIX}/create' +MAP_DELETE = f'{PREFIX}/delete' +MAP_GET = f'{PREFIX}/get' +MAP_LIST = f'{PREFIX}/list' +MAP_UPDATE = f'{PREFIX}/update' +MAP_OVERWRITE = f'{PREFIX}/overwrite' +MAP_MARKER_CREATE = f'{PREFIX}/marker/create' +MAP_MARKER_DELETE = f'{PREFIX}/marker/delete' +MAP_MARKER_LIST = f'{PREFIX}/marker/list' +MAP_MARKER_UPDATE = f'{PREFIX}/marker/update' +MAP_AREA_CREATE = f'{PREFIX}/area/create' +MAP_AREA_DELETE = f'{PREFIX}/area/delete' +MAP_AREA_LIST = f'{PREFIX}/area/list' +MAP_AREA_UPDATE = f'{PREFIX}/area/update' +MAP_WALL_CREATE = f'{PREFIX}/wall/create' +MAP_WALL_DELETE = f'{PREFIX}/wall/delete' +MAP_WALL_LIST = f'{PREFIX}/wall/list' +MAP_WALL_UPDATE = f'{PREFIX}/wall/update' +MAP_PATH_CREATE = f'{PREFIX}/path/create' +MAP_PATH_DELETE = f'{PREFIX}/path/delete' +MAP_PATH_LIST = f'{PREFIX}/path/list' +MAP_PATH_UPDATE = f'{PREFIX}/path/update' +MAP_EXT_CREATE = f'{PREFIX}/ext/create' +MAP_EXT_DELETE = f'{PREFIX}/ext/delete' +MAP_EXT_LIST = f'{PREFIX}/ext/list' +MAP_EXT_UPDATE = f'{PREFIX}/ext/update' + + +# ROS Topics to inform data change +MAP_ADDED = f'{PREFIX}/added' +MAP_CHANGED = f'{PREFIX}/changed' +MAP_REMOVED = f'{PREFIX}/removed' +MAP_MARKER_ADDED = f'{PREFIX}/marker/added' +MAP_MARKER_CHANGED = f'{PREFIX}/marker/changed' +MAP_MARKER_REMOVED = f'{PREFIX}/marker/removed' +MAP_AREA_ADDED = f'{PREFIX}/area/added' +MAP_AREA_CHANGED = f'{PREFIX}/area/changed' +MAP_AREA_REMOVED = f'{PREFIX}/area/removed' +MAP_WALL_ADDED = f'{PREFIX}/wall/added' +MAP_WALL_CHANGED = f'{PREFIX}/wall/changed' +MAP_WALL_REMOVED = f'{PREFIX}/wall/removed' +MAP_PATH_ADDED = f'{PREFIX}/path/added' +MAP_PATH_CHANGED = f'{PREFIX}/path/changed' +MAP_PATH_REMOVED = f'{PREFIX}/path/removed' +MAP_EXT_ADDED = f'{PREFIX}/ext/added' +MAP_EXT_CHANGED = f'{PREFIX}/ext/changed' +MAP_EXT_REMOVED = f'{PREFIX}/ext/removed' + + +class MapNode(Node): + def __init__(self, name=None): + self.name = name if name else self.__class__.__name__ + super().__init__(self.name) + + # all map by name + self.data = {} + + # overwrite map in data if another map + # with name is received or log an error and ignore + # the new name + self.allow_overwrite = False + + # name of the id + self.id_name = 'name' + + # setup services for data manipulation + self.init_ros() + + def init_ros(self): + self.added_pub = self.create_publisher( + Map, MAP_ADDED, 10) + self.changed_pub = self.create_publisher( + Map, MAP_CHANGED, 10) + self.removed_pub = self.create_publisher( + String, MAP_REMOVED, 10) + self.marker_added_pub = self.create_publisher( + Marker, MAP_MARKER_ADDED, 10) + self.marker_changed_pub = self.create_publisher( + Marker, MAP_MARKER_CHANGED, 10) + self.marker_removed_pub = self.create_publisher( + String, MAP_MARKER_REMOVED, 10) + self.area_added_pub = self.create_publisher( + Area, MAP_AREA_ADDED, 10) + self.area_changed_pub = self.create_publisher( + Area, MAP_AREA_CHANGED, 10) + self.area_removed_pub = self.create_publisher( + String, MAP_AREA_REMOVED, 10) + self.wall_added_pub = self.create_publisher( + Wall, MAP_WALL_ADDED, 10) + self.wall_changed_pub = self.create_publisher( + Wall, MAP_WALL_CHANGED, 10) + self.wall_removed_pub = self.create_publisher( + String, MAP_WALL_REMOVED, 10) + self.path_added_pub = self.create_publisher( + Path, MAP_PATH_ADDED, 10) + self.path_changed_pub = self.create_publisher( + Path, MAP_PATH_CHANGED, 10) + self.path_removed_pub = self.create_publisher( + String, MAP_PATH_REMOVED, 10) + self.ext_added_pub = self.create_publisher( + External, MAP_EXT_ADDED, 10) + self.ext_changed_pub = self.create_publisher( + External, MAP_EXT_CHANGED, 10) + self.ext_removed_pub = self.create_publisher( + String, MAP_EXT_REMOVED, 10) + self.create_srv = self.create_service( + MapCreate, MAP_CREATE, + self.on_create) + self.delete_srv = self.create_service( + MapDelete, MAP_DELETE, + self.on_delete) + self.get_srv = self.create_service( + MapGet, MAP_GET, + self.on_get) + self.list_srv = self.create_service( + MapList, MAP_LIST, + self.on_list) + self.update_srv = self.create_service( + MapUpdate, MAP_UPDATE, + self.on_update) + self.overwrite_srv = self.create_service( + MapOverwrite, MAP_OVERWRITE, + self.on_overwrite) + self.create_marker_srv = self.create_service( + MapMarkerCreate, MAP_MARKER_CREATE, + self.on_attr_create('marker')) + self.delete_marker_srv = self.create_service( + MapMarkerDelete, MAP_MARKER_DELETE, + self.on_attr_delete('marker')) + self.list_marker_srv = self.create_service( + MapMarkerList, MAP_MARKER_LIST, + self.on_attr_list('marker')) + self.update_marker_srv = self.create_service( + MapMarkerUpdate, MAP_MARKER_UPDATE, + self.on_attr_update('marker')) + self.create_area_srv = self.create_service( + MapAreaCreate, MAP_AREA_CREATE, + self.on_attr_create('area')) + self.delete_area_srv = self.create_service( + MapAreaDelete, MAP_AREA_DELETE, + self.on_attr_delete('area')) + self.list_area_srv = self.create_service( + MapAreaList, MAP_AREA_LIST, + self.on_attr_list('area')) + self.update_area_srv = self.create_service( + MapAreaUpdate, MAP_AREA_UPDATE, + self.on_attr_update('area')) + self.create_wall_srv = self.create_service( + MapWallCreate, MAP_WALL_CREATE, + self.on_attr_create('wall')) + self.delete_wall_srv = self.create_service( + MapWallDelete, MAP_WALL_DELETE, + self.on_attr_delete('wall')) + self.list_wall_srv = self.create_service( + MapWallList, MAP_WALL_LIST, + self.on_attr_list('wall')) + self.update_wall_srv = self.create_service( + MapWallUpdate, MAP_WALL_UPDATE, + self.on_attr_update('wall')) + self.create_path_srv = self.create_service( + MapPathCreate, MAP_PATH_CREATE, + self.on_attr_create('path')) + self.delete_path_srv = self.create_service( + MapPathDelete, MAP_PATH_DELETE, + self.on_attr_delete('path')) + self.list_path_srv = self.create_service( + MapPathList, MAP_PATH_LIST, + self.on_attr_list('path')) + self.update_path_srv = self.create_service( + MapPathUpdate, MAP_PATH_UPDATE, + self.on_attr_update('path')) + self.create_ext_srv = self.create_service( + MapExtCreate, MAP_EXT_CREATE, + self.on_attr_create('ext')) + self.delete_ext_srv = self.create_service( + MapExtDelete, MAP_EXT_DELETE, + self.on_attr_delete('ext')) + self.list_ext_srv = self.create_service( + MapExtList, MAP_EXT_LIST, + self.on_attr_list('ext')) + self.update_ext_srv = self.create_service( + MapExtUpdate, MAP_EXT_UPDATE, + self.on_attr_update('ext')) + + def _get(self, _name): + if _name not in self.data: + return None + return self.data[_name] + + def on_attr_create(self, attr: str): + def _create(request, response): + _name = request.name + elem = self._get(_name) + if not elem: + self.get_logger().error( + f'Can not get map "{_name}"') + response.success = False + return response + getattr(elem, attr).append(request.item) + getattr(self, f'{attr}_added_pub').publish(request.item) + response.success = True + return response + return _create + + def on_attr_list(self, attr: str): + def _list(request, response): + _name = request.name + elem = self._get(_name) + data = getattr(elem, attr, []) + setattr(response, attr, data) + return response + return _list + + def on_attr_update(self, attr: str): + def _update(request, response): + _name = request.name + elem = self._get(_name) + idx = request.index + getattr(elem, attr)[idx] = request.item + getattr(self, f'{attr}_changed_pub').publish(request.item) + response.success = True + return response + return _update + + def on_attr_delete(self, attr: str): + def _delete(request, response): + _name = request.name + elem = self._get(_name) + idx = request.index + item = getattr(elem, attr)[idx] + getattr(elem, attr).remove(idx) + getattr(self, f'{attr}_removed_pub').publish(item) + return response + return _delete + + def on_create(self, request, response): + elem = request.map + _name = elem.name + if self._get(request.name) and not self.allow_overwrite: + self.get_logger().error( + 'Can not create, name ' + f'{_name} already exists.') + response.success = False + return response + self.data[_name] = elem + self.added_pub.publish(elem) + response.success = True + return response + + def on_delete(self, request, response): + _name = request.name + if not self._get(_name): + self.get_logger().error( + 'Can not delete, name ' + f'{_name} does not exist.') + response.success = False + return response + del self.data[_name] + self.removed_pub.publish(_name) + response.success = True + return response + + def on_get(self, request, response): + _name = request.name + elem = self._get(_name) + if elem: + response.map = elem + return response + else: + self.get_logger().info( + 'Mission ID ' + f'{request.name} does not exist!') + return response + + def on_list(self, request, response): + response.map = list(self.data.values()) + return response + + def on_update(self, request, response): + elem = request.map + _name = elem.name + if not self._get(request.name): + self.get_logger().error( + 'Can not update, name: ' + f'{_name} does not exist.') + response.success = False + return response + self.data[_name] = elem + self.changed_pub.publish(elem) + response.success = True + return response + + def on_overwrite(self, request, response): + """allow overwrite map in self.data""" + self.allow_overwrite = request.allow_overwrite + return response + + +def main(args=None): + rclpy.init(args=args) + + node = MapNode() + + rclpy.spin(node) + + node.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() diff --git a/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/node.py b/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/node.py new file mode 100644 index 0000000..3b0e7c8 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/mapdesc_ros/node.py @@ -0,0 +1,40 @@ +from pathlib import Path + +import rclpy + +from mapdesc.load.yaml import load_yaml + +from .convert_data import map_ros_to_mapdesc +from .map_desc import MapNode + + +class MapDescNode(MapNode): + def __init__(self): + super().__init__('mapdesc_node') + self.declare_parameter('map_yaml', '') + self.load_yaml_file() + + def load_yaml_file(self): + """Load yaml files and set as data""" + map_yaml = str(self.get_parameter('map_yaml').value) + if not map_yaml or map_yaml == '': + self.get_logger().warning('Map not set!') + return + if not Path(map_yaml).exists(): + self.get_logger().warning(f'Map does not exist: {map_yaml}') + return + map_mapdesc = load_yaml(map_yaml) + map_msg = map_ros_to_mapdesc(map_mapdesc) + self.data[map_msg.name] = map_msg + self.get_logger().info(f'Loaded map: {map_msg.name}') + + +def main(args=None): + rclpy.init(args=args) + node = MapDescNode() + rclpy.spin(node) + rclpy.shutdown() + + +if __name__ == '__main__': + main() diff --git a/lib/mapdesc_ros/mapdesc_ros/package.xml b/lib/mapdesc_ros/mapdesc_ros/package.xml new file mode 100644 index 0000000..0f88db2 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/package.xml @@ -0,0 +1,21 @@ + + + + mapdesc_ros + 0.0.0 + ROS 2 wrapper for the map description package + abresser + BSD-3 + + rclpy + geometry_msgs + std_msgs + + ament_copyright + ament_flake8 + python3-pytest + + + ament_python + + diff --git a/lib/mapdesc_ros/mapdesc_ros/pytest.ini b/lib/mapdesc_ros/mapdesc_ros/pytest.ini new file mode 100644 index 0000000..791ce85 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +# Set testpaths, otherwise pytest finds 'tests' in the examples directory +testpaths = test +# Add arguments for launch tests +addopts = --launch-args dut_arg:=test +junit_family=xunit2 \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc_ros/resource/mapdesc_ros b/lib/mapdesc_ros/mapdesc_ros/resource/mapdesc_ros new file mode 100644 index 0000000..e69de29 diff --git a/lib/mapdesc_ros/mapdesc_ros/setup.cfg b/lib/mapdesc_ros/mapdesc_ros/setup.cfg new file mode 100644 index 0000000..1003583 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/setup.cfg @@ -0,0 +1,8 @@ +[develop] +script_dir=$base/lib/mapdesc_ros +[install] +install_scripts=$base/lib/mapdesc_ros +[coverage:run] +# This will let coverage find files with 0% coverage (not hit by tests at all) +source = . +omit = setup.py \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc_ros/setup.py b/lib/mapdesc_ros/mapdesc_ros/setup.py new file mode 100644 index 0000000..6115554 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/setup.py @@ -0,0 +1,33 @@ +import os +from glob import glob +from setuptools import find_packages, setup + +package_name = 'mapdesc_ros' + + +setup( + name=package_name, + version='0.0.1', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ( + os.path.join('share', package_name, 'launch'), + glob(os.path.join('launch', '*.launch.py'))), + ], + install_requires=['setuptools'], + requires=['mapdesc'], + zip_safe=True, + maintainer='abresser', + maintainer_email='Andreas.Bresser@dfki.de', + description='ROS 2 wrapper for the map description', + license='BSD-3', + tests_require=['pytest', 'pytest-cov'], + entry_points={ + 'console_scripts': [ + 'mapdesc_service = mapdesc_ros.node:main', + ], + } +) diff --git a/lib/mapdesc_ros/mapdesc_ros/test/test_copyright.py b/lib/mapdesc_ros/mapdesc_ros/test/test_copyright.py new file mode 100644 index 0000000..97a3919 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/test/test_copyright.py @@ -0,0 +1,25 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_copyright.main import main +import pytest + + +# Remove the `skip` decorator once the source file(s) have a copyright header +@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/lib/mapdesc_ros/mapdesc_ros/test/test_flake8.py b/lib/mapdesc_ros/mapdesc_ros/test/test_flake8.py new file mode 100644 index 0000000..27ee107 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/lib/mapdesc_ros/mapdesc_ros/test/test_mapdesc_service.py b/lib/mapdesc_ros/mapdesc_ros/test/test_mapdesc_service.py new file mode 100644 index 0000000..34e4137 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc_ros/test/test_mapdesc_service.py @@ -0,0 +1,3 @@ +def test_get_marker(): + """Make sure the map we get from the ROS service has some marker.""" + assert 1 == 1 diff --git a/lib/mapdesc_ros/roscrud.yml b/lib/mapdesc_ros/roscrud.yml new file mode 100644 index 0000000..bdf18c5 --- /dev/null +++ b/lib/mapdesc_ros/roscrud.yml @@ -0,0 +1,15 @@ +# ROSCrud is a Tool to generate ROS 2 services to Create Read Update and Delete +# data, insprired by RESTful web services +# see https://en.wikipedia.org/wiki/Create,_read,_update_and_delete for details +- name: map + class_name: Map + id_name: name # equals the file name + package_name: mapdesc_ros + package_msgs_name: mapdesc_msgs + attributes: + - marker + - area + - wall + - path + - ext + # TODO: lanes - create getter/setter to set lane_nodes and lane_edges directly \ No newline at end of file diff --git a/lib/mapdesc_ros/run_tests.bash b/lib/mapdesc_ros/run_tests.bash new file mode 100755 index 0000000..5bcd8e8 --- /dev/null +++ b/lib/mapdesc_ros/run_tests.bash @@ -0,0 +1,13 @@ +#!/bin/bash +# run unit and integration tests INSIDE the docker container +source ./install/setup.bash +# TODO: move unit tests to an own folder +echo "" +echo "-- starting unit tests using colcon --" +echo "" +colcon test --pytest-with-coverage --event-handlers console_cohesion+ --return-code-on-test-failure +echo "" +echo "-- starting integration tests using launch_test --" +echo "" +# Note that you need to define each test individually +launch_test src/mapdesc_ros/integration_tests/launch_testing/marker_launch_test.py \ No newline at end of file diff --git a/lib/mapdesc_ros/test_docker.bash b/lib/mapdesc_ros/test_docker.bash new file mode 100755 index 0000000..d915a97 --- /dev/null +++ b/lib/mapdesc_ros/test_docker.bash @@ -0,0 +1,4 @@ +# run tests on local machine (no gitlab ci, but in a docker container) +docker compose build ros_with_pip +docker compose build mapdesc_ros +docker compose run mapdesc_ros \ No newline at end of file From b0fd39bde440a3c43f71df5e2bf10be300bf1785 Mon Sep 17 00:00:00 2001 From: Adrian Auer Date: Sat, 3 Jan 2026 13:15:50 +0100 Subject: [PATCH 2/3] needed to change lib structure due to submodules --- docker/Dockerfile | 3 +- lib/mapdesc_ros/mapdesc/.coveragerc | 2 + .../mapdesc/.devcontainer/devcontainer.json | 17 + lib/mapdesc_ros/mapdesc/.gitignore | 17 + lib/mapdesc_ros/mapdesc/.gitlab-ci.yml | 35 + lib/mapdesc_ros/mapdesc/CONTRIBUTING.md | 54 + lib/mapdesc_ros/mapdesc/LICENSE | 29 + lib/mapdesc_ros/mapdesc/README.md | 112 + lib/mapdesc_ros/mapdesc/clear_test.bash | 9 + lib/mapdesc_ros/mapdesc/compose.yml | 9 + .../mapdesc/doc/images/DFKI_RIC_RGB.jpg | Bin 0 -> 74283 bytes .../mapdesc/doc/images/mallmap.png | Bin 0 -> 23192 bytes .../doc/images/mapdesc_input_output.png | Bin 0 -> 138202 bytes .../doc/images/mapdesc_input_output.svg | 5356 +++++++++++++++++ lib/mapdesc_ros/mapdesc/doc/images/world.png | Bin 0 -> 503 bytes .../doc/tutorials/01_basic_tutorial.md | 53 + .../doc/tutorials/02_basic_api_usage.md | 91 + .../mapdesc/doc/tutorials/03_import_path.md | 35 + lib/mapdesc_ros/mapdesc/docker/Dockerfile | 7 + lib/mapdesc_ros/mapdesc/manifest.xml | 38 + lib/mapdesc_ros/mapdesc/mapdesc/__init__.py | 5 + lib/mapdesc_ros/mapdesc/mapdesc/cli.py | 148 + .../mapdesc/mapdesc/data/README.md | 2 + .../mapdesc/data/templates/model.config.j2 | 15 + .../mapdesc/data/templates/model.sdf.j2 | 54 + lib/mapdesc_ros/mapdesc/mapdesc/geo_data.py | 135 + .../mapdesc/mapdesc/load/__init__.py | 13 + .../mapdesc/mapdesc/load/geojson.py | 54 + lib/mapdesc_ros/mapdesc/mapdesc/load/osm.py | 44 + .../mapdesc/mapdesc/load/rosmap.py | 91 + lib/mapdesc_ros/mapdesc/mapdesc/load/sdf.py | 93 + lib/mapdesc_ros/mapdesc/mapdesc/load/yaml.py | 14 + .../mapdesc/mapdesc/model/__init__.py | 22 + lib/mapdesc_ros/mapdesc/mapdesc/model/area.py | 80 + lib/mapdesc_ros/mapdesc/mapdesc/model/ext.py | 19 + .../mapdesc/mapdesc/model/geom/__init__.py | 19 + .../mapdesc/mapdesc/model/geom/box.py | 61 + .../mapdesc/mapdesc/model/geom/capsule.py | 14 + .../mapdesc/mapdesc/model/geom/cylinder.py | 15 + .../mapdesc/mapdesc/model/geom/dimension.py | 67 + .../mapdesc/mapdesc/model/geom/mesh.py | 126 + .../mapdesc/mapdesc/model/geom/plane.py | 23 + .../mapdesc/mapdesc/model/geom/pose.py | 80 + .../mapdesc/mapdesc/model/geom/quaternion.py | 103 + .../mapdesc/mapdesc/model/geom/sphere.py | 12 + .../mapdesc/mapdesc/model/geom/vector2.py | 115 + .../mapdesc/mapdesc/model/geom/vector3.py | 96 + lib/mapdesc_ros/mapdesc/mapdesc/model/lane.py | 100 + lib/mapdesc_ros/mapdesc/mapdesc/model/map.py | 117 + .../mapdesc/mapdesc/model/marker.py | 41 + lib/mapdesc_ros/mapdesc/mapdesc/model/path.py | 38 + lib/mapdesc_ros/mapdesc/mapdesc/model/wall.py | 78 + .../mapdesc/mapdesc/save/__init__.py | 14 + lib/mapdesc_ros/mapdesc/mapdesc/save/png.py | 79 + .../mapdesc/mapdesc/save/rosmap.py | 28 + lib/mapdesc_ros/mapdesc/mapdesc/save/sdf.py | 24 + lib/mapdesc_ros/mapdesc/mapdesc/save/svg.py | 88 + lib/mapdesc_ros/mapdesc/mapdesc/save/yaml.py | 8 + lib/mapdesc_ros/mapdesc/mapdesc/util.py | 110 + lib/mapdesc_ros/mapdesc/pytest.bash | 5 + lib/mapdesc_ros/mapdesc/requirements.txt | 6 + lib/mapdesc_ros/mapdesc/setup.cfg | 21 + lib/mapdesc_ros/mapdesc/setup.py | 48 + .../mapdesc/test/geojson/geojson_drone.json | 34 + lib/mapdesc_ros/mapdesc/test/map/mallmap.png | Bin 0 -> 34636 bytes lib/mapdesc_ros/mapdesc/test/map/mallmap.yaml | 6 + .../test/simple_walls_sdf/model.config | 15 + .../mapdesc/test/simple_walls_sdf/model.sdf | 128 + .../mapdesc/test/test_conversions.py | 187 + .../mapdesc/test/test_dimension.py | 8 + lib/mapdesc_ros/mapdesc/test/test_lanes.py | 23 + .../mapdesc/test/test_load_save.py | 38 + lib/mapdesc_ros/mapdesc/test/test_map.py | 7 + lib/mapdesc_ros/mapdesc/test/test_mesh.py | 19 + lib/mapdesc_ros/mapdesc/test/test_plane.py | 11 + lib/mapdesc_ros/mapdesc/test/test_pose.py | 60 + .../mapdesc/test/test_quaternion.py | 99 + .../mapdesc/test/test_serialization.py | 10 + lib/mapdesc_ros/mapdesc/test/test_utils.py | 35 + lib/mapdesc_ros/mapdesc/test/test_vec2.py | 60 + lib/mapdesc_ros/mapdesc/test/test_vec3.py | 56 + lib/mapdesc_ros/mapdesc/test/test_wall.py | 15 + .../mapdesc/test/test_yaml_import.py | 20 + .../mapdesc/test/yaml/demonstrator_map.yml | 274 + .../mapdesc/test/yaml/hdp_2_agents_map.yml | 469 ++ .../mapdesc/test/yaml/simple_walls.yaml | 27 + lib/mapdesc_ros/mapdesc/test_cli.bash | 11 + .../mapdesc_msgs/CMakeLists.txt | 0 lib/{ => mapdesc_ros}/mapdesc_msgs/README.md | 0 .../mapdesc_msgs/msg/Area.msg | 0 .../mapdesc_msgs/msg/Box.msg | 0 .../mapdesc_msgs/msg/Dimension.msg | 0 .../mapdesc_msgs/msg/External.msg | 0 .../mapdesc_msgs/msg/LaneEdge.msg | 0 .../mapdesc_msgs/msg/LaneGraph.msg | 0 .../mapdesc_msgs/msg/LaneNode.msg | 0 .../mapdesc_msgs/msg/Map.msg | 0 .../mapdesc_msgs/msg/Marker.msg | 0 .../mapdesc_msgs/msg/Mesh.msg | 0 .../mapdesc_msgs/msg/Path.msg | 0 .../mapdesc_msgs/msg/Wall.msg | 0 .../mapdesc_msgs/package.xml | 0 .../mapdesc_msgs/srv/MapAreaCreate.srv | 0 .../mapdesc_msgs/srv/MapAreaDelete.srv | 0 .../mapdesc_msgs/srv/MapAreaList.srv | 0 .../mapdesc_msgs/srv/MapAreaUpdate.srv | 0 .../mapdesc_msgs/srv/MapCreate.srv | 0 .../mapdesc_msgs/srv/MapDelete.srv | 0 .../mapdesc_msgs/srv/MapExtCreate.srv | 0 .../mapdesc_msgs/srv/MapExtDelete.srv | 0 .../mapdesc_msgs/srv/MapExtList.srv | 0 .../mapdesc_msgs/srv/MapExtUpdate.srv | 0 .../mapdesc_msgs/srv/MapGet.srv | 0 .../mapdesc_msgs/srv/MapList.srv | 0 .../mapdesc_msgs/srv/MapMarkerCreate.srv | 0 .../mapdesc_msgs/srv/MapMarkerDelete.srv | 0 .../mapdesc_msgs/srv/MapMarkerList.srv | 0 .../mapdesc_msgs/srv/MapMarkerUpdate.srv | 0 .../mapdesc_msgs/srv/MapOverwrite.srv | 0 .../mapdesc_msgs/srv/MapPathCreate.srv | 0 .../mapdesc_msgs/srv/MapPathDelete.srv | 0 .../mapdesc_msgs/srv/MapPathList.srv | 0 .../mapdesc_msgs/srv/MapPathUpdate.srv | 0 .../mapdesc_msgs/srv/MapUpdate.srv | 0 .../mapdesc_msgs/srv/MapWallCreate.srv | 0 .../mapdesc_msgs/srv/MapWallDelete.srv | 0 .../mapdesc_msgs/srv/MapWallList.srv | 0 .../mapdesc_msgs/srv/MapWallUpdate.srv | 0 128 files changed, 9802 insertions(+), 2 deletions(-) create mode 100644 lib/mapdesc_ros/mapdesc/.coveragerc create mode 100644 lib/mapdesc_ros/mapdesc/.devcontainer/devcontainer.json create mode 100644 lib/mapdesc_ros/mapdesc/.gitignore create mode 100644 lib/mapdesc_ros/mapdesc/.gitlab-ci.yml create mode 100644 lib/mapdesc_ros/mapdesc/CONTRIBUTING.md create mode 100644 lib/mapdesc_ros/mapdesc/LICENSE create mode 100644 lib/mapdesc_ros/mapdesc/README.md create mode 100755 lib/mapdesc_ros/mapdesc/clear_test.bash create mode 100644 lib/mapdesc_ros/mapdesc/compose.yml create mode 100644 lib/mapdesc_ros/mapdesc/doc/images/DFKI_RIC_RGB.jpg create mode 100644 lib/mapdesc_ros/mapdesc/doc/images/mallmap.png create mode 100644 lib/mapdesc_ros/mapdesc/doc/images/mapdesc_input_output.png create mode 100644 lib/mapdesc_ros/mapdesc/doc/images/mapdesc_input_output.svg create mode 100644 lib/mapdesc_ros/mapdesc/doc/images/world.png create mode 100644 lib/mapdesc_ros/mapdesc/doc/tutorials/01_basic_tutorial.md create mode 100644 lib/mapdesc_ros/mapdesc/doc/tutorials/02_basic_api_usage.md create mode 100644 lib/mapdesc_ros/mapdesc/doc/tutorials/03_import_path.md create mode 100644 lib/mapdesc_ros/mapdesc/docker/Dockerfile create mode 100644 lib/mapdesc_ros/mapdesc/manifest.xml create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/__init__.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/cli.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/data/README.md create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/data/templates/model.config.j2 create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/data/templates/model.sdf.j2 create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/geo_data.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/load/__init__.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/load/geojson.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/load/osm.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/load/rosmap.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/load/sdf.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/load/yaml.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/__init__.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/area.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/ext.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/__init__.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/box.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/capsule.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/cylinder.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/dimension.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/mesh.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/plane.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/pose.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/quaternion.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/sphere.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/vector2.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/geom/vector3.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/lane.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/map.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/marker.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/path.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/model/wall.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/save/__init__.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/save/png.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/save/rosmap.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/save/sdf.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/save/svg.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/save/yaml.py create mode 100644 lib/mapdesc_ros/mapdesc/mapdesc/util.py create mode 100755 lib/mapdesc_ros/mapdesc/pytest.bash create mode 100644 lib/mapdesc_ros/mapdesc/requirements.txt create mode 100644 lib/mapdesc_ros/mapdesc/setup.cfg create mode 100644 lib/mapdesc_ros/mapdesc/setup.py create mode 100644 lib/mapdesc_ros/mapdesc/test/geojson/geojson_drone.json create mode 100755 lib/mapdesc_ros/mapdesc/test/map/mallmap.png create mode 100644 lib/mapdesc_ros/mapdesc/test/map/mallmap.yaml create mode 100644 lib/mapdesc_ros/mapdesc/test/simple_walls_sdf/model.config create mode 100644 lib/mapdesc_ros/mapdesc/test/simple_walls_sdf/model.sdf create mode 100644 lib/mapdesc_ros/mapdesc/test/test_conversions.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_dimension.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_lanes.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_load_save.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_map.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_mesh.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_plane.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_pose.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_quaternion.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_serialization.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_utils.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_vec2.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_vec3.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_wall.py create mode 100644 lib/mapdesc_ros/mapdesc/test/test_yaml_import.py create mode 100644 lib/mapdesc_ros/mapdesc/test/yaml/demonstrator_map.yml create mode 100644 lib/mapdesc_ros/mapdesc/test/yaml/hdp_2_agents_map.yml create mode 100644 lib/mapdesc_ros/mapdesc/test/yaml/simple_walls.yaml create mode 100755 lib/mapdesc_ros/mapdesc/test_cli.bash rename lib/{ => mapdesc_ros}/mapdesc_msgs/CMakeLists.txt (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/README.md (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/Area.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/Box.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/Dimension.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/External.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/LaneEdge.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/LaneGraph.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/LaneNode.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/Map.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/Marker.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/Mesh.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/Path.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/msg/Wall.msg (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/package.xml (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapAreaCreate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapAreaDelete.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapAreaList.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapAreaUpdate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapCreate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapDelete.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapExtCreate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapExtDelete.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapExtList.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapExtUpdate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapGet.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapList.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapMarkerCreate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapMarkerDelete.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapMarkerList.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapMarkerUpdate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapOverwrite.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapPathCreate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapPathDelete.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapPathList.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapPathUpdate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapUpdate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapWallCreate.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapWallDelete.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapWallList.srv (100%) rename lib/{ => mapdesc_ros}/mapdesc_msgs/srv/MapWallUpdate.srv (100%) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7772078..624634c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,8 +19,7 @@ COPY ./docker/entrypoint.bash /entrypoint.bash RUN chmod +x /entrypoint.bash ENTRYPOINT ["/entrypoint.bash"] -COPY ./ricbot_navigation ${COLCON_WS_SRC}/ricbot_navigation -COPY ./lib/mapdesc_msgs ${COLCON_WS_SRC}/mapdesc_msgs +COPY ./ ${COLCON_WS_SRC}/ricbot_navigation COPY ./lib/mapdesc_ros ${COLCON_WS_SRC}/mapdesc_ros diff --git a/lib/mapdesc_ros/mapdesc/.coveragerc b/lib/mapdesc_ros/mapdesc/.coveragerc new file mode 100644 index 0000000..10f391c --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = mapdesc/cli.py \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/.devcontainer/devcontainer.json b/lib/mapdesc_ros/mapdesc/.devcontainer/devcontainer.json new file mode 100644 index 0000000..cee6b83 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "MapDesc", + "workspaceFolder": "/app/", + "dockerComposeFile": "../compose.yml", + "overrideCommand": true, + "service": "mapdesc", + "customizations":{ + "vscode": { + "extensions": [ + "MermaidChart.vscode-mermaid-chart", + "ms-python.python", + "ms-python.debugpy", + "ms-python.flake8" + ] + } + } +} diff --git a/lib/mapdesc_ros/mapdesc/.gitignore b/lib/mapdesc_ros/mapdesc/.gitignore new file mode 100644 index 0000000..89c2228 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/.gitignore @@ -0,0 +1,17 @@ +# python cache / pip +*.egg-info/ +__pycache__/ +*.pyc +build/ + +# py.test +.pytest_cache/ + +# test coverage +.coverage +htmlcov/ + +# cli test script +/generated/* +# OSM cache +cache/ \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/.gitlab-ci.yml b/lib/mapdesc_ros/mapdesc/.gitlab-ci.yml new file mode 100644 index 0000000..8b7f678 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/.gitlab-ci.yml @@ -0,0 +1,35 @@ +image: hdgigante/python-opencv:4.9.0-alpine + +stages: +- build +- test +- deploy + +mapdesc build: + stage: build + script: + - pip3 install . + +mapdesc test: + stage: test + script: + - pip3 install . + - pip3 install pytest coverage + - python3 -m pytest + - coverage run --source mapdesc --omit=mapdesc/cli.py -m pytest . + - coverage report -m + - coverage html + artifacts: + paths: + - htmlcov/ + +software_catalogue_entry: + image: d-reg.hb.dfki.de:5000/ubuntu:overview_generator + stage: deploy + script: + - apt update + - apt install -y wget + - wget http://bob.dfki.uni-bremen.de/software_overview/generate.sh + - sh generate.sh $CI_PROJECT_NAMESPACE $CI_PROJECT_NAME $CI_PROJECT_URL + only: + - main diff --git a/lib/mapdesc_ros/mapdesc/CONTRIBUTING.md b/lib/mapdesc_ros/mapdesc/CONTRIBUTING.md new file mode 100644 index 0000000..5b78ab5 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Contributing to *MapDesc* + +Please inform the maintainer as early as possible about your planned +feature developments, extensions, or bugfixes that you are working on. +An easy way is to open an issue or a pull request in which you explain +what you are trying to do. + +## Pull Requests + +The preferred way to contribute to *MapDesc* is to fork the main repository on Gitlab, then submit a "pull request" +(PR): + +1. Fork the [project repository](git@github.com:PROJECT_PATH): + click on the 'Fork' button near the top of the page. This creates a copy of + the code under your account on the Gitlab server. + +3. Clone this copy to your local disk: + + $ git clone git@github.com:YourLogin/MapDesc.git + +4. Create a branch to hold your changes: + + $ git checkout -b my-feature + + and start making changes. Never work in the ``main`` branch! + +5. Work on this copy, on your computer, using Git to do the version + control. When you're done editing, do:: + + $ git add modified_files + $ git commit + + to record your changes in Git, then push them to Gitlab with:: + + $ git push -u origin my-feature + +Finally, go to the web page of the your fork of the repo, +and click 'Pull request' to send your changes to the maintainers for review. + +## Merge Policy + +Summary: maintainer can push minor changes directly, pull request + 1 reviewer for everything else. + +* Usually it is not possible to push directly to the `main` branch of WBC for anyone. Only tiny changes, urgent bugfixes, and maintenance commits can be pushed directly to the `main` branch by the maintainer without a review. "Tiny" means backwards compatibility is mandatory and all tests must succeed. No new feature must be added. + +* Developers have to submit pull requests. Those will be reviewed by at least one other developer and merged by the maintainer. New features must be documented and tested. Breaking changes must be discussed and announced in advance with deprecation warnings. + +* Any change of existing functionality requires that all unit tests must succeed. In addition, the [tutorials](https://github.com/PROJECT_PATH/doc/tutorials) should be executed and the results should be compared with the results obtained prior to making those changes. If the results differ, the changes should be reconsidered. + +* Adding new functionality requires the addition of unit tests. In pinciple, every class should be accompanied by at least one unit test that checks the common use case, and one unit tests that checks for the common error cases + +## Project Roadmap + +Check the [Issue Tracker](https://github.com/PROJECT_PATH/issues) for roadmap planning. \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/LICENSE b/lib/mapdesc_ros/mapdesc/LICENSE new file mode 100644 index 0000000..0a0bfdf --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2025, DFKI GmbH, Andreas Bresser +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/mapdesc_ros/mapdesc/README.md b/lib/mapdesc_ros/mapdesc/README.md new file mode 100644 index 0000000..d519383 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/README.md @@ -0,0 +1,112 @@ +# mapdesc + +**Map Desc**ription System for Robotics - generate and exports walls and other static objects from different sources to import into robotic simulations or as base for autonomous navigation. Can generate environments for navigation simulations (e.g. navsim-2d or Gazebo) from a given image as map using OpenCV or a map from a web-based editor. The map can also be exported into an image or YAML as input for the editor. + +MapDesc was initiated and is currently developed at the [Robotics Innovation Center](http://robotik.dfki-bremen.de/en/startpage.html) of the [German Research Center for Artificial Intelligence (DFKI)](http://www.dfki.de) in Bremen. + +![](doc/images/DFKI_RIC_RGB.jpg) + + +## Motivation +MapDesc allows you to import your data from different sources and export them into other formats + +### Inputs + +- OpenStreetMap (using OSMPythonTooly, you provide coordinates of a center point and a radius around it) +- ROS map (YAML and image file that is used by the [ROS map server](http://wiki.ros.org/map_server)) +- SDF-file environment description for gazebo (only parses collision box or mesh, ignores visual representation) +- YAML-description of walls, for example created by the [Map-Editor](../map-editor) (see format description) + +### Outputs +- SDF-files for gazebo simulation +- PNG-file (can be used as debug or to generate a PNG-file for the ROS map) +- SVG-file (can be used as debug to see the obstacle as individual objects) +- YAML-description of walls (see format description) that can be uploaded to the [Map-Editor](../map-editor) + + +![](doc/images/mapdesc_input_output.png) + +## Installation +**mapdesc** is written in python, so it can be installed for the current user using pip/pip3: + +```bash +# clone and cd into the mapdesc-directory +pip3 install . +``` + +### Dependencies +OpenCV2 is used for extracting and saving information from images. + +For Linux Ubuntu there are python-packages available: +```bash +sudo apt install python3-opencv +``` + +And for Linux Fedora: +```bash +sudo dnf install python3-opencv opencv +``` + +we also depend on these libraries: +- **pyyaml** to parse YAML-files like map descriptions and metadata. +- **jinja2** to generate SDF-files. +- **imutils** to get contours of an image when it gets loaded. +- **argcomplete** to autocomplete command-line arguments. + +## Getting Started +### Usage +see `mapdesc -h` + +Examples: + +```bash +# export rosmap yaml file and image to our own yaml-based format using opencv2 +mapdesc rosmap test/map/mallmap.yaml yaml test2.yml +# export a yaml-based description to an SDF file using jinja2 +mapdesc yaml test/yaml/simple_walls.yaml sdf test.sdf +``` + +### Troubleshooting +If you try to call `mapdesc` from your command line and you get the error message + +`mapdesc: command not found` + +you need to add your local bin-folder to your path, so just add + +`export PATH=~/.local/bin:$PATH` + +to your local `.bashrc` and source it again (`source ~/.bashrc`). + +## Format description + +The format is loosely based on the [SDFormat](http://sdformat.org/) geometry description to describe worlds in the robotic simulation [Gazebo](https://gazebosim.org/). See [SDF specification](http://sdformat.org/spec) for details. + +The format of the lanes-graph is loosely based on the ROS-messages for OpenRMF, see [RMF building map msgs](https://github.com/open-rmf/rmf_building_map_msgs/) for details. + +## Testing + +### Robotics / Gazebo +To test the generated SDF files with gazebo you can start gazebo with the SDF-folder as `GAZEBO_MODEL_PATH` variable, for example like this: `GAZEBO_MODEL_PATH=/home/user/mapdesc/generated/sdf/ gazebo` after you run the "`test_cli.bash`" file and the SDF-file you want to load is inside the generated-folder. + +Please check the unit tests [here](/test/). + +## Contributing + +Please use the [issue tracker](map_desc/issues) to submit bug reports and feature requests. Please use merge requests as described [here](/CONTRIBUTING.md) to add/adapt functionality. + +## License + +MapDesc is distributed under the [3-clause BSD license](https://opensource.org/licenses/BSD-3-Clause). + +## Maintainer / Authors / Contributers + +Andreas Bresser, andreas.bresser@dfki.de + +Copyright 2023, DFKI GmbH / Robotics Innovation Center diff --git a/lib/mapdesc_ros/mapdesc/clear_test.bash b/lib/mapdesc_ros/mapdesc/clear_test.bash new file mode 100755 index 0000000..c4952e5 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/clear_test.bash @@ -0,0 +1,9 @@ +#!/bin/bash +rm -Rf generated/sdf/test1/ +rm -Rf generated/sdf/test2/ +rm -f ./generated/mallmap.yml +rm -f generated/mallmap_bounding_box.yml +rm -f ./generated/test1.png +rm -f ./generated/test1.yml +rm -f ./generated/mallmap*.png +rm -f ./generated/hdp*.svg \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/compose.yml b/lib/mapdesc_ros/mapdesc/compose.yml new file mode 100644 index 0000000..439faf3 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/compose.yml @@ -0,0 +1,9 @@ +services: + mapdesc: + build: + context: ./ + dockerfile: ./docker/Dockerfile + environment: + - "PYTHONUNBUFFERED=1" + volumes: + - ./:/app/:rw \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/doc/images/DFKI_RIC_RGB.jpg b/lib/mapdesc_ros/mapdesc/doc/images/DFKI_RIC_RGB.jpg new file mode 100644 index 0000000000000000000000000000000000000000..33e223ebf06c9ae4cab84aa1219f57a658874453 GIT binary patch literal 74283 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

Mj_*wPg%{~d z=i0Lo<+*rv`fFTzw3(z;phaXa78{%#KuBkIMI}9h4z?Ac%sVZw-PJCRl8t zNg=nQv)->JV*Y|d z#N#o>IY!+->B|H>KWzvP{;R!bWF8*mXkmv!8~TCd-J*RH>{%tsNioSI;!!C ziPowOXquCUdPLB-JVpvdDPtO0xHg?;IBhHRo+OO0mR7lLB+yCDmP4aYv^2+j`zAGA z_{J4K)kT%y-=cQ561^IOtrFAjzhDMKZcWn5U9VGSR9IV>c4!P58WI*2rtS&iu4PsP zq$yCC_qL#HW_eyKIq?glNtD_sqZ{xXOTDWQ_M1{xb}~wTq$b^?!eBos&0`nc>mr`u=^8jm8OZR${L(hyvf?F(!M?7E{D67qw4z{`#B zD#3Yu=L+X~h9o*%>6X({ZS}ltjl5qo+jU2^n|pR2lO+V%y86=TX>Sv`n2hE1E|f zF7}(>8?x@UpDfH9&s8*j2jw0?(9ol6Jx?2Ez_nChdH) z=HrvROli-C5uyTo?@>+lWB_|_IkCwDdr|ye(qiVd`i;I)aEDm)eQ9O%R>;~)VZBGE zX;C+k>SM7+x)M73tgc;3JH`dQa+eyUqBWPn>BnBZY|WZYWW_mJ1MZncN4Nue?_W|p z7nq5D%F_q>CP`AC-94&HUqfjz8Y1H`Z1`5#?n>i^<&xuzBvtTLq{y1?l`tpYN{dO3 z*L*w?+fjK@6b8HDv^`en%q>$pf^Z1-(%Q|W2hp@dSyeMa2DFPK=kO0_h2|2D`3TEw z@8c*(8Gb%UqMucC8Ju!P%`K;YE%lklY6d0mZKwHLti1KhHGEl&TKY{j&Tjs;>gy^P zi7!=-+scly_JcphKl`S?>o*@r*JmRxc0}ct`ji+^lB?$~!s~g9ru;!CaV@RIF=Ako11mcrUQL}`^Mgv#zDz*>?}t&9EAV6>5H5{6(}#kmNuXJJz3HOxM=ZOOTSUu`p`IvC5{x-+ z+n@SuDZc<8+&_6!y~%PM*h^ns1JAP~F9kO$K2v*seTim`k&~JIrI7OXiJ5;=X;JOl z4*OQk(-mBIE)l{6T(mKbl);S)9N#6@`d*`evHj0{%a1w>o*iCYE_;{u?wd7XoT3^` zqz`1)mfFZuuiB`D`ZxrrZo-mF+QU3xE97#ty4iZQic@-btW5qiN=>zRbt`Mm;rhGf zj&sI^+GXm+p-e96s~>G6p9@!fw(I(~=!Z*0opfqty_mG#5lw2_4bP$UVQ}(HC%I@! z(?v!$l;u}}2YSC5K1mx|7j_3@F(RZK3yR6m@!?}s&EB*HiR4@9gyTDc11%pvCDZpa zr+`OyN59`vZj~s-b|r0QrxX-C-kf8lrkF=d6Jd})_xv}?a9lQr905LK?a*117y319 zHQ(+o^CReSqF;+|9$k9SGT~T8(rytB-c6Vr>9pamOmspkk2|;d_1IM{V?&9D(ALLh z+J-1`mwR#DeU(Ek0UH8w#W%75aMw}zou}$@gQ>}_hmR}xly%zf-jTMyn-dm}Z!~bB ze-4);M2aZ2vgv;3-U7LYF{5H~p+@l-Pg@zuT2_nwdP<>F1flJ~?2``_L0MY|eN~Om z&Y7ib|CChm25NR%eh}t_^98LI3<421BROKKq2`v`H{s*5C8}~=#fOeKwcxujt;Q`9 z(-O69R&{*gK*o^F05x)Z`e@d|nnU(mneI6=>O4O8ad7)SMRJ*D;$cudHIH|WU}A=Y zzvNN;K{*~r2OZ#+aJI1FA7YJY;vJ6u@O1>Zg3~LsKktU3LzCV0*8{0i)HIR8g>?+F z+U5h=4ZbEFlvwAWmoI+&R%OXenl<}!vt0Y}U6fW^pw{Z*GZ(;nBI@xfBZ+R;T6JD8dxlIp3BSBC9JDIYvEfO zTAGz{D9?z{iMc@-OU{og;qmn%RM^0)&@aGcH&lhkA(Kh&zV~Lo0Q`Sgf)6;!=f>mk zc|N|}8kk`cCpagWmdE+bCpP=%ZdW}h%?n|Z-`P>1dyPOK$US2rpb!jpjjbvMR%qCi#v5a#y&L#>Z z^EOYdAF|I@uDX7SofVIX{gCbv= zF=TQ6ZcYD7Hu<<=6Uy6qOv=*Q0!a!N2lz*8^#~p9gS~$wBJ9^L*JL$J8WQCY+RDY zVj!LU5M!6<(rFR-$08CE+9f2*inFHg-XRdM{tIwI$gWkufX=6&GF`=BeVZD(GS({J ziF{LRkW$?Vdn=0KI_i6jmD@Cj?lxI6NGe5zxWzD5K+>f2E$mDd?_ z8>k+$m@&y*ffeV`etL4-!Clhcim~ZfrCRF+yO=zdO%o~*jo`#UN>IDeZyW-v_BsyL#_wSaHb)Xh^mXPoo`^s;^BXqB-%2aedByyud|jeV)XjV558@m;#;z!CF26Sl)fAtN zs))vkV#A?}!Ku_)K`{O(O__w!=N?%UW}p=%V>loE8oe493%oW=-p2k@rOs7 zr~)@4rC#Wa-nnux`a0ZUL1{Ts`-ausdWn0~-?(!WgoN~HUX-~`tW%TTcO&hk)`HnYoab!^o=_NnrjmBlu|8v5j+m zUZAwY>?iRsxAW#tIKrn7{Zy|$_hhefLH*-ZJ(seZ7b=@|?qhOk_cL)bmQUvZ&%YBv(&!7rnzD;T z(j}X?DgygYjuRmZzM*B*S~;fE+a)voww`f^B*ZSwdHfx-EVT0>yuLsWR}V_ zT50>8^HD%=(e`HC8`2-E5<6sc&Dbh_vQ=XVC7%EM5pU)TKwrP8XGA!?*02sAgF;fjN6Owdsdpd;#*^#ICsW*eeK_+udXIb|<)z z)O?3+Qt!pLRda~!X4T)EQ^_Y=rR?`!0iJuZ!p0e|b;&7XhdEcjsYt%xBU1E;O^Hox zYHnOqpWYQKREJQtF+IFE57r`!(!3F_agtMh!bg$*jY%j0s&1QFHhm6W==F@=E>$ZF zW@b{C+9wfgYHWk|ndCH+#H;cB^m?-c0W8?Ym4;eU%5_LU^eoR?xz>%e@UhxAMv!p(cFxVaA%%okwFZ=}?$b zfsCp2#v@1Fww0|a)Yr8=;z!axM28AKb1lef7P?W!8m3*yMiZCLO=e{Dhk7)sDGXrO zG@+J;>?b)=nw(nl{?e#piSQq`6Su9dJxZv&AZ0>Zcr1rSw4u4|`sgjU+hM`ab9Wx3 zk00|es2MDP+96x!lzT6;ZfB;REj%%zLA^KZ(*RD}qZ`xC2FZO?sx=JkbFFB*9vj-; ztg(H>=CE{qJ^kE7h$5Ym2q6av))n;Ul?3_a+sxp7D`y#R6w^jjcyW`_YnV-SsuFc& z#1bzBe*IA=Nr zPZ17b!6p|xtB4Qz0x;1yonbodSo>Zn9NA&?Dt+V?!dKNrmTp77aye-+5Z>PGqa;^% z1r_cUPx2=%LYrn-Wke$CRv`nJye+=7E;s=q{N-e`>W9AdsdmaA6E7EU>0h6BdcE>K zjH~!a`y-^}IaO9yCSoW2t99CD@``=TfiM|$ZuW6S=_kDV{@YZ9%0hmZrzIku!c(H~ z*#UEeg7}j=b&Xtq=EZbD_vO-fd4P|X(^B|Se_HiHip0d7Z6=L9T@mL)$kQlPZjpwZ zWa6B=-NQxZ-K8Qp%|!{*!fTOfQBrGnU>ae7K&_tXm1CVd=)Mh(7=*<;RQmT&-prv{ zEwfYT8L893{8_oJheKjgvWh}>ltkIImV)x(2l7I(5)4wa_yp|5Ux2{NCgT^+vloXel|}_BT~MI6Na?hlhbUwlmgu? zi>(oxa^7y^QG<@+XgQB6_jDX)uptz~^&S=_wy5U54c{_FSs(F|Tyq!+MYW%21kT+` zXm5_JEpKGl$31>84$?W(-YR{xnN)dcBTct-5Ekvi=P$ZE5cz3c!$sq zkI|%k=Ry>Hwmzzt;_;kU8xO?mmS=_!`Wnz|ISQ5ZvPH#(6FEtylF*yvL>M~D5;x-B z<|(9ISoEhheC4?AZ-b}`d-_T9B6(1GL&8kjH+T1Gk9I7&nr%+vRI@{hd&EVCZHn>0 z9y`JIO{p#`&iZVwe9gpw@2uu<3gaYlMe&KvZwx66;9vTr50;KlZN^8`8G@+T?7rJW zgjplK)=z@M?L};DsU2poSQ%2N2{^A&6JYrEIXVlac=y`pXzZC2&TJL0kXPERhc;J6 z@UFMj&^L0ht3$8IU5-lDOh>Oa>Gl=g=kuHP z>Rs^AY#vb#YlWv~nb}n7y9+Gc5e+P@8ktdBoW{}Pnag*M<68(7-!CXq`3hrBUQ3mW{c5pJ3K5O~m8+4yr)EiRxmAlCYmKa3JzFbhSG2>Wy+qiRywo+d z)Z2n$i(dfEm@!Ol_QeL4QFq4VEIQ zK=`oz&unX|Mtz6^zPaY$PthkVcOKxasejtwqu>R*)9eKr6@L$CSBsJ(YaU*MDM=4LVQm=B}GB2P&4AyF*+`Xo(5u#O|@@aZD{@U_vn$47w(c4(+T@~a&#yd&q z)ECmBpi`=SpNJyijYy8$ZJ0*NH}TEXep*y+M&(uN_2^Te2vxZt;EK`TF-%08!n4@Z zgr(Xt=rSX3TGUIcx$o%r#5?F}jCQi(s6@eRjA{w%af>_GnDBGY)I^a7bYXH>`IjJ=J||s4<2Afov*G!K0WH&N2(0AlM^UHu+S7wEZwLwK8G{Br$>gTs^n8lvfsv z-y2Bn0!OCVX>7l}aW4W470iN`nO>I?T5Wmu`oRvQh7Vz_+d&So5&-$XIHLF3a|qhvai z;|{Z%1*4Zx#vaqMJ+glB*DFIS(cFv$Mue<<%7ofpQAL45&l_F6E8>^vc*8yx*7G8etn}Gb4@W=g&!|E>P9H*Q3ntQ$e+z&Cp ztWLC$W}P~x=8q(tJSTGxjW)xlFFh%FrSBk$CMUPh$)+=YPq zl`g*LNqy7fFJsa@d(9EFf24kh$~c{DC|180z-_dF!>E!B7m4$IkmWhu(1afe{!TM_ zL}2fmRFp=oF3k4) z4*Y5vS^X4d3HClcankQDJ;3K+F2Lf=!7664e*3vI_*2knK2WfIUrqnyU>%vJS>%bZ(ASZgaKk92>I8RT@&=&f&;zI^8$2wU088j7M!(poJ(jNKHu!cDPq7$SlJd=6e~J9k_^Qh( z4o^Eq8CSM-G~hGWzl98cmf=eEbze}Gx-3wLSf@+$)jLd?tJ{&Ed;$M z+Q}n}W4$)42ZBQ`)50CROMRvas1j=0Z*1P)A=i0PMSe9u#bd#s-{H6Mgdy~aQ1ht4 ztztQ5M+unvEU_&_tup#W`3HLZ8Kvg6adg}?ROjEssw>Yn1i~2$T-q82t{hFW&r(eK zPTR@hi+CC@Bqs=kX%CrnF#3UfmyB%-5s?T|d^(}j>P+VC;9e%X2KX-$;iR`wt&To7?GA zN|mtV5{O}N+ZYn&??sdyIRr5|W_aVR6%CoP9nU!q+-gT*_gs~Uhfyf?$8%TaW@`C! zQzO6gZE8@^{{J|83!u7|Z3}ec9-QD1+=B<#;O_43?hxGF3GS}J3GNcy-Q6{K{wDX_ z^X_>s_t&pq)kST(*IM0ccK7T##~j_ORD;Vnv5#|dekoa-Ln!|w@}ShJ3f5;iOmbMM zJWQ#cpeR|)T4SszqGA&|*=B5;n#e9i!nA|}gc7^w2&U_h5^O+vcTYw1TI%v|yyt%@ znC?P639%ZbJYg=`qDZMIII+Pj+FewbPAWTgdD{?)iZoHxy7+RNf6p!>Z}D}p$>m0M zVH{>XViJj#leVOZEyX-5OptNghIF(X{ox>2bq$kzOMK8%LU|@sIDNA;#*s}@;EsW^ zPP?O$B6*CZrXhM2!ZOpRf|;C@4Td=hwhRxl$aG`@Roir5GB&9j=$rJiz932wgzJ9S z%?rc#2;XVOMSUj3SmUVc@**OtlMAt`7sLb0#Mm5AA@ya0)>$f`!Ya1WIB9AsA#%P^ z0-t0jLMY~A40V_~AcNESX@kDoGTSmfVFkk&*^DiU-rgU4d%gbw-jg*>niWNauS_}>C!zm*bla|l+#PAzP~KUl+A=hN2fiTO3Zr}+gqa)9!c z>rPag7eP#ulvUFq8Bn1)swM3FI7zqGX1;(tS)DqL*VC^ugUzxg!=mz4i+J?Yz~2t` zKd_=NA>L7rk5f-2s~uuMkK>AY_O^OrVEUbT6=RD?s!jBt5uz7lx(3S%$SLWj07-I~ zlHMKJ?vHP4575K)vZS|jY(H!rKEyX`=l&Z@le!aMYAxbdDakUpM!nIF?0IszrdiiiEv%}D z7$bMSrp|XOy#E_vz5KLDZGM#jpRxKn6toNQvH`r_wd_gp2*`Sq%w)2v>fe<=_6MUZ8T}1d4<2EgW-%kn=!e_iAgYRF9y|+*($Gc6y3K z*h10JW?)MttGHggyi5&yJIXB=cAEp-S{-+#8KB*O76V3{ z!`ffg?IZZU^*&ecnxplqtg!`tUY0S3nbR`R4bG0hItc&BE|KLNHoFrA|2@EB;?q`O zVzrl@@?at8M0P$kXrTTkWvWY_d>`dT@hKQcsLN?fIn1{je>Nm7ug)b%CN$Pyy!mX9 z?W#UJ13F?&S(ncWGzVIk3ciA!)*ZGN;%)F$3&^u|N4G~fHFJzd4!{d!dpfJmM#UFC z!Ft`QnQ{)oCGEL^+$kuNZT9VBJYyGLmJnsy6aQ)b&f0 zSBl$Lqhw7tQe1gJy)Oy|B@ITm8?yTf!$k1*Rbmr*S zdW*Y-({RzgEHzPGZZ^|M^6JfON+qVV`v#IJt;LF>mHFi~fNOkpWMzOiq3^-dp!=wu z^Bs+CIr4Y{`cegtIM5h@ zI9p`Dy|2Ln(j;%4YXL zCW;r|MxI5wicgq{2U2nj0CCyVZf(bjWmEM8uXykRSe?~xsV7A*+hOjoOeLI`l0C2j zUR;!mXh2hdBBysQ5xPK0l0_lUOdBUqY^bafo#Q;OoMjt(MQuzhTnobYIEG7AKQn0I z77a0srWlPaT{!mxqr-51X<;V2J+-$||NA~WQt}=+GbdW`0wCgvB7+cBeoyaOiAskE ztEY5O*w*slt9Ppk94qQp7zCd#cHllW8;(dm5L$dMlN- -yx0CwG_$>nw#Vm0(;? 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 WUETi%|KfB^cIL*l7gxXh#{U3%4c7qx literal 0 HcmV?d00001 diff --git a/lib/mapdesc_ros/mapdesc/doc/images/mapdesc_input_output.png b/lib/mapdesc_ros/mapdesc/doc/images/mapdesc_input_output.png new file mode 100644 index 0000000000000000000000000000000000000000..8da63a5531cf5c41f7f554a497fb4a7cf9ec445a GIT binary patch literal 138202 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 &}v$FOW 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 literal 0 HcmV?d00001 diff --git a/lib/mapdesc_ros/mapdesc/doc/images/mapdesc_input_output.svg b/lib/mapdesc_ros/mapdesc/doc/images/mapdesc_input_output.svg new file mode 100644 index 0000000..4c6a5a1 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/doc/images/mapdesc_input_output.svg @@ -0,0 +1,5356 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + YAML + + + + SVG + + + + PNG + + + + SDF + + + + mapdesc + + + + + + + + + + + + OpenStreetMap + + + + ROS map + + + + SDF + + + + YAML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/mapdesc_ros/mapdesc/doc/images/world.png b/lib/mapdesc_ros/mapdesc/doc/images/world.png new file mode 100644 index 0000000000000000000000000000000000000000..316e0eadcab4a02e979f657f1d5fa2387dc9ccd3 GIT binary patch literal 503 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`(m>Eakt!T9!|W0r~|!x4v< z|NF0*Nlasu?=F14P-)E%f6aCFDMy_vMNTc=@vlYH>!YXUy7!x%H0?BxeywE_*$<>r z!!(YnFWs^4JlE16OGK`JkNg-Q*ZN6goqgibdauI2irVYGM-=Wo-u2oxEav^jM^*iz z*Q?j;xR-qNs~k=#v#HFYhu7R#RJuDMtaJl$w^=f5iH{= n?h9?WAdXoqVowLxf8%!*&YtmS*Qdw82xahe^>bP0l+XkKU-sSX literal 0 HcmV?d00001 diff --git a/lib/mapdesc_ros/mapdesc/doc/tutorials/01_basic_tutorial.md b/lib/mapdesc_ros/mapdesc/doc/tutorials/01_basic_tutorial.md new file mode 100644 index 0000000..5262000 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/doc/tutorials/01_basic_tutorial.md @@ -0,0 +1,53 @@ +# Basic tutorial +mapdesc provides a command line interface (cli) as well as a Python API. + +In this first tutorial we will look at the CLI, for the API take a look at [02: Basic API usage](02_basic_api_usage.md) + +## Bring a map to Gazebo simulation +mapdesc allows you convert data from one source to another. + +A typical use case would be to create an SDF-file form a recorded ROS map, so you could easily create a simulation environment of your recorded map for testing your robot in simulation first. + +First store your ROS 2 map using the [ROS map_server](http://wiki.ros.org/map_server) for ROS 1 or ROS 2. You can then load the map and export it to SDF. + +Replace `your_ros_map.yml` with the filename of the file from your map. +```bash +mapdesc rosmap your_ros_map.yml sdf ./generated/sdf/test2 +``` + +Then you could either copy the exported SDF to your `.gazebo/models`-folder or just run gazebo with the `GAZEBO_MODEL_PATH` set to the parent folder of where you generated the sdf-file: +```bash +GAZEBO_MODEL_PATH=/home/user/mapdesc/generated/sdf/ gazebo +``` +in Gazebo click on the "Insert"-tab. There you should see the model that you can insert in your gazebo world. + +## Load map to ROS map_server +1. Generate a ROSMap for a mapdesc description (or import it from another format like a YAML) +1. You can then use the YAML-file in your ROS 2 nav2 as part of the [map-server parameter](https://navigation.ros.org/configuration/packages/configuring-map-server.html#map-server-parameters) + + \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/doc/tutorials/02_basic_api_usage.md b/lib/mapdesc_ros/mapdesc/doc/tutorials/02_basic_api_usage.md new file mode 100644 index 0000000..5d7a5d7 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/doc/tutorials/02_basic_api_usage.md @@ -0,0 +1,91 @@ +# Examples + +1. Creating a map +2. Import and export data + +## Creating a map: + +For the beginning we will generate a simple map with two objects as `Marker` and an obstacle `Wall`. Let’s take a look at the mapdesc folder, for now the model and save folder are relevant. + + +`model` – contains the modules to describe the map, includes basic geometric descriptions for obstacles (for example box, cylinder) as well as their position and orientation (for example pose, vector2, quaternion...) +`save` – contains the modules to generate image files etc. from the model objects + +1. Import the required libraries: + + ```python + from mapdesc.model import Map, Marker, Wall + from mapdesc.model.geom import Pose, Vector3, Box, Dimenstion + from mapdesc import save + ``` + +2. Define what should be on the map, in this case 2 Marker named 'storage' and 'production' and a Wall named 'wall'. The map size is variable and will be set according to the position and dimensions of the outer objects. + + ```python + storage = Marker( + name = 'storage', + radius = 0.2, + pose = Pose( + position = Vector3(0,3,0) + ), + color = [255,0,0] + ) + + production = Marker( + name = 'production', + radius = 0.2, + pose = Pose( + position = Vector3(6,5,0) + ), + color = [0,0,255] + ) + wall = Wall(data = Box( + pose = Pose( + position = Vector3(3,5,0) + ), + size = Dimension(1.0,2.0,1.0) + ) + ) + ``` +3. Now we can generate the map that will contain the marker and the wall. + + ```python + world = Map( + name='world', + marker=[ + storage, production + ], + wall=[wall] + ) + ``` + +4. Almost done! In order to have an output we need to call the desired function from the save module. For example to export the whole map into a .png image. + ```python + save.save_png(world, 'world.png') + ``` + +5. The result will be saved as world.png below the folder where you run your code. It should look like this: +![](../images/world.png) + + +## Import and export data +In this example we will import a map recorded using the [ROS map_server](http://wiki.ros.org/map_server) and export it to an image file. +1. Import the required libraries: + ```python + from mapdesc.load.rosmap import load_rosmap + from mapdesc.save import save_png + ``` +2. Use the specific function to import the .yaml file + ```python + testmap = load_rosmap('./test/map/mallmap.yaml') + ``` + +3. Export it to .png. + ```python + save_png(testmap, './mallmap.png') + ``` +4. The output should like: + + ![](../images/mallmap.png) + +5. Be aware that the exported file may look slightly different to the imported one. Due to the fact that the import file is converted into its abstracted structure and then reconstructed to a file, so that some pixel information will not be identical, especially for circles or rotated objects we loose information. \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/doc/tutorials/03_import_path.md b/lib/mapdesc_ros/mapdesc/doc/tutorials/03_import_path.md new file mode 100644 index 0000000..016534d --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/doc/tutorials/03_import_path.md @@ -0,0 +1,35 @@ +# Import path from GeoJSON. + +For details on the GeoJSON format see https://www.rfc-editor.org/rfc/rfc7946 + +## Use Case #1: Create a path for a drone in a hilly area +### Create GeoJSON file +1. Go to https://geojson.io +1. Select the "Draw LineString"-tool on the right or press "l" +1. Create a path on the map +1. Click on "Save"/"GeoJSON" + + + +### Download and import from OSM +TODO: Describe how to Download and import + +### Alternative: Download from ??? +TODO: Describe how to Download GeoTIFF from NASA EarthData + +TODO: Convert/crop data using [geotiff_util](https://git.hb.dfki.de/samler-ki/simulation/geotiff_util/) + +### Import and manipulate data in MapEditor + +TODO: describe how to import the data into the MapEditor + +## Use Case #2: Create a path for a moon rover +### Create GeoJSON file + +1. Go to https://quickmap.lroc.asu.edu +1. Select the "Draw/Search tool" icon on the left +1. Click on the path "Arc" next to "Select Tool" +1. Create a path by clicking points on the map. Double-click when you are done. +1. Click on "Export Query Features"/Download Button (📥) in the center of the left "Draw & Query" tool. + +TODO: create mapdesc-file diff --git a/lib/mapdesc_ros/mapdesc/docker/Dockerfile b/lib/mapdesc_ros/mapdesc/docker/Dockerfile new file mode 100644 index 0000000..5983cc9 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/docker/Dockerfile @@ -0,0 +1,7 @@ +FROM hdgigante/python-opencv:4.9.0-alpine + +WORKDIR /app +COPY . /app/ + +RUN pip3 install pytest coverage +RUN pip3 install -e . \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/manifest.xml b/lib/mapdesc_ros/mapdesc/manifest.xml new file mode 100644 index 0000000..74d06cc --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/manifest.xml @@ -0,0 +1,38 @@ + + MapDesc + + **Map Desc**ription System for Robotics - generate and exports walls and other static objects from different sources to import into robotic simulations or as base for autonomous navigation. Can generate environments for navigation simulations (e.g. navsim-2d or Gazebo) from a given image as map using OpenCV or a map from a web-based editor. The map can also be exported into an image or yaml as input for the editor. + + Andreas Bresser/andreas.bresser@dfki.de + Andreas Bresser/andreas.bresser@dfki.de + + BSD-3-Clause + https://git.hb.dfki.de/phanes/mapdesc/ + + + + utilities + ROS1 + ROS2 + mapping + navigation + + + 0 + single project + + + active + + + + + python + ros + Development Status :: 3 - Alpha + + diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/__init__.py b/lib/mapdesc_ros/mapdesc/mapdesc/__init__.py new file mode 100644 index 0000000..82e286e --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/__init__.py @@ -0,0 +1,5 @@ +__version__ = '0.1' + +from . import model +from . import load +from . import save diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/cli.py b/lib/mapdesc_ros/mapdesc/mapdesc/cli.py new file mode 100644 index 0000000..3a9158b --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/cli.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +# PYTHON_ARGCOMPLETE_OK + +"""mapdesc command line tool.""" + +import argparse +import logging +import os +import sys + +from .load import LOAD +from .save import SAVE + +CONFIG_FORMATTER = '%(asctime)s %(name)s[%(levelname)s] %(message)s' +LOAD_ARGS = { + 'yaml': [('load_file', 'YAML filename (.yml/.yaml)')], + 'rosmap': [('load_file', 'ROS filename (.yml/.yaml), NOT the png')], + 'geojson': [ + ('load_file', 'Load GeoJSON file (.geojson)'), + ('planet', 'Planet (normally "earth")')], + 'sdf': [('load_file', 'SDF file (.xml/.sdf)')], + 'osm': [ + ('lat', 'latitude of coordinate'), + ('lon', 'longitude of coordinate'), + ('radius', 'radius around the given coordinate'), + ('planet', 'Planet (normally "earth")') + ] +} +SAVE_ARGS = { + 'png': [('file_name', 'PNG image (.png)')], + 'rosmap': [ + ( + 'file_name', + 'Name of YAML-File, will create a png from the map' + ) + ], + 'sdf': [ + ( + 'folder_name', + 'folder name of the model, will create a .sdf and config file' + ) + ], + 'svg': [('file_name', 'SVG file (.svg)')], + 'yaml': [ + ( + 'file_name', + 'Name of YAML-File, lossless, ' + 'based on the description with all information' + ) + ], +} + + +def setup_logging(): + log_level = os.environ.get('LOG_LEVEL', 'INFO') + log_level = getattr(logging, log_level) + logging.basicConfig(level=log_level, format=CONFIG_FORMATTER) + + +def print_help(): + print( + 'usage: mapdesc [-h] [LOAD_TYPE] [LOAD_PARAMS...] ' + '[SAVE_TYPE] [SAVE_PARAMS...]:') + print(__doc__) + print('') + print('LOAD_TYPE can be one of these (with LOAD_PARAMS):') + for key, arg in LOAD_ARGS.items(): + print(f' {key}: ') + for sarg in arg: + print(f' {sarg[0]}: {sarg[1]}') + print('') + print('SAVE_TYPE can be one of these (with SAVE_PARAMS):') + for key, arg in SAVE_ARGS.items(): + print(f' {key}: ') + for sarg in arg: + print(f' {sarg[0]}: {sarg[1]}') + print('') + print('examples:') + print('# convert yaml file to sdf') + print('mapdesc yaml test/yaml/simple_walls.yaml sdf ./generated/sdf/test1') + print('# get buildings from OSM and save as svg') + print('mapdesc osm 53.0762098 8.8075270 80 svg bremen_city.svg') + + +def main(): + setup_logging() + + parser = argparse.ArgumentParser(description=__doc__) + + parser.add_argument( + 'load_type', choices=LOAD_ARGS.keys(), + help='Type of loading operation') + parser.add_argument( + '--recenter', '-r', default=False, + action=argparse.BooleanOptionalAction + ) + parser.add_argument( + '--bounding_box', default=False, + action=argparse.BooleanOptionalAction + ) + # boxify creates a box from meshes that have 4 points as polygon that + # perfectly align as box + parser.add_argument( + '--boxify', '-b', default=False, + action=argparse.BooleanOptionalAction + ) + + parser.add_argument( + 'load_params', nargs='+', help='Parameters for loading operation') + + parser.add_argument( + 'save_type', choices=SAVE_ARGS.keys(), help='Type of saving operation') + parser.add_argument( + 'save_params', nargs='+', help='Parameters for saving operation') + + args = parser.parse_args() + + if len(LOAD_ARGS[args.load_type]) != len(args.load_params): + print( + f'Error: {args.load_type} operation requires ' + f'{len(LOAD_ARGS[args.load_type])} parameters ' + f'({len(args.load_params)} given)') + parser.print_help() + sys.exit(1) + + if len(SAVE_ARGS[args.save_type]) != len(args.save_params): + print( + f'Error: {args.save_type} operation requires ' + f'{len(SAVE_ARGS[args.save_type])} parameters ' + f'({len(args.save_params)} given)') + parser.print_help() + sys.exit(1) + + _map = LOAD[args.load_type](*args.load_params) + if not _map: + return + if args.recenter: + _map.recenter() + if args.bounding_box: + _map.bounding_box() + if args.boxify: + _map.boxify() + SAVE[args.save_type](_map, *args.save_params) + + +if __name__ == '__main__': + main() diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/data/README.md b/lib/mapdesc_ros/mapdesc/mapdesc/data/README.md new file mode 100644 index 0000000..b5cafd5 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/data/README.md @@ -0,0 +1,2 @@ +# ROS 2 template +Generate ROS 2 data. \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/data/templates/model.config.j2 b/lib/mapdesc_ros/mapdesc/mapdesc/data/templates/model.config.j2 new file mode 100644 index 0000000..1a35591 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/data/templates/model.config.j2 @@ -0,0 +1,15 @@ + + + {{ map.name }} + 1.0 + model.sdf + + + {{ author['name'] }} + {{ author['email'] }} + + + + {{ map.description }} + + diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/data/templates/model.sdf.j2 b/lib/mapdesc_ros/mapdesc/mapdesc/data/templates/model.sdf.j2 new file mode 100644 index 0000000..30b9474 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/data/templates/model.sdf.j2 @@ -0,0 +1,54 @@ + + + + 0 0 0 0 0 0 + {% for wall in map.walls %} + + + + {% if wall.type == 'box' -%} + + {{ wall.size.width }} {{ wall.size.length }} {{ wall.size.height }} + + {%- elif wall.type in ['polyline', 'mesh'] -%} + + {% for point in wall.points -%} + {{ point.x }} {{ point.y }} + {% endfor -%} + + {% endif %} + + 0 0 {{ wall.size.height / 2 }} 0 0 0 + + + + {% if wall.type == 'box' -%} + + {{ wall.size.width }} {{ wall.size.length }} {{ wall.size.height }} + + {%- elif wall.type == 'polyline' -%} + + {% for point in wall.points -%} + {{ point.x }} {{ point.y }} + {% endfor -%} + + {% endif %} + + 0 0 {{ wall.size.height / 2 }} 0 0 0 + + + 1 1 1 1 + + + 0 + + + {{ wall.pose.position.x }} {{ wall.pose.position.y }} {{ wall.pose.position.z }} {{ wall.pose.euler_orientation().x }} {{ wall.pose.euler_orientation().y }} {{ wall.pose.euler_orientation().z }} + + {% endfor %} + 1 + + diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/geo_data.py b/lib/mapdesc_ros/mapdesc/mapdesc/geo_data.py new file mode 100644 index 0000000..a61d0d8 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/geo_data.py @@ -0,0 +1,135 @@ +import math + + +PROJECTIONS = { + 'moon': + 'PROJCS["Equirectangular Moon",' + 'GEOGCS["GCS_Moon",DATUM["D_Moon",' + 'SPHEROID["Moon_localRadius",1737400,0]],' + 'PRIMEM["Reference_Meridian",0],' + 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]]],' + 'PROJECTION["Equirectangular"],' + 'PARAMETER["standard_parallel_1",0],' + 'PARAMETER["central_meridian",0],' + 'PARAMETER["false_easting",0],' + 'PARAMETER["false_northing",0],' + 'UNIT["metre",1,AUTHORITY["EPSG","9001"]],' + 'AXIS["Easting",EAST],' + 'AXIS["Northing",NORTH]]' +} + +# Volumetric mean radius of planets and moons (approx. in meter) +RADIUS_IN_METERS = { + 'sun': 696342000, + + 'mercury': 2439700, + 'venus': 6051800, + 'earth': 6378137, + 'moon': 1737400, + 'mars': 3389500, + # Moons of Mars + 'phobos': 11266, + 'deimos': 6200, + + 'jupiter': 69911000, + # Moons of Jupiter + 'io': 1821600, + 'europa': 1560800, + 'ganymede': 2634100, + 'callisto': 2410300, + + 'saturn': 58232000, + # Moons of Saturn + 'mimas': 198200, + 'enceladus': 252100, + 'tethys': 531100, + 'dione': 561400, + 'rhea': 764300, + 'titan': 2574700, + 'iapetus': 734500, + + 'uranus': 25362000, + # Moons of Uranus + 'miranda': 235800, + 'ariel': 578900, + 'umbriel': 584700, + 'titania': 788400, + 'oberon': 761400, + + 'neptune': 24622000, + # Moon of Neptune + 'triton': 1353400, + + # dwarf planets + 'pluto': 1188300, + # Moons of pluto + 'charon': 606000, + 'nix': 24900, # diameter 49.8 × 33.2 × 31.1 + 'hydra': 25500, # diameter 50.9 × 36.1 × 30.9 + 'kerberos': 9500, # diameter 19 × 10 × 9 + 'styx': 8000, # diameter 16 × 9 × 8 + + 'eris': 1163000, + 'haumea': 780000, + 'makemake': 715000, + 'gongong': 615000, + 'quaora': 545000, + 'sedna': 500000, + 'ceres': 469700, + 'orcus': 435000, + 'vesta': 262700, + 'pallas': 255500, + 'hygiea': 216500, + 'juno': 135700, + 'chiron': 58350, + 'pholus': 49500, + 'nessus': 29000, +} + +SIGNS = { + 'sun': '☉', + 'mercury': '☿', + 'venus': '♀︎', + 'earth': '🜨', + 'moon': '☾', + 'mars': '♂︎', + 'jupiter': 'J', + + 'saturn': '♄', + 'uranus': '⛢', + + 'neptune': '♆', + + # dwarf planets + 'pluto': '♇', + 'eris': '⯰', + 'sedna': '⯲', + 'ceres': '⚳', + 'vesta': '⚶', + 'pallas': '⚴', + 'hygiea': '⯚', + 'juno': '⚵', + 'chiron': '⚷', + 'pholus': '⯛', + 'nessus': '⯜', +} + +CIRCUMFERENCE_IN_METERS = {} +for name, radius in RADIUS_IN_METERS.items(): + CIRCUMFERENCE_IN_METERS[name] = radius * math.pi * 2 + +METERS_PER_DEGREE_LATITUDE = {} +for name, circ in CIRCUMFERENCE_IN_METERS.items(): + METERS_PER_DEGREE_LATITUDE[name] = circ / 360 + + +def meters_per_degree_longitude(lat, body): + # A degree of longitude is widest at the equator at 111.321 km and + # gradually shrinks to zero at the poles. + return METERS_PER_DEGREE_LATITUDE[body] * math.cos(lat / 180 * math.pi) + + +def lon_lat_to_point(lat, lon, build_lat, buid_lon, body): + lonToMeter = meters_per_degree_longitude(build_lat, body) + latToMeter = METERS_PER_DEGREE_LATITUDE[body] + return (lon - buid_lon) * lonToMeter, (lat - build_lat) * latToMeter diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/load/__init__.py b/lib/mapdesc_ros/mapdesc/mapdesc/load/__init__.py new file mode 100644 index 0000000..03839ca --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/load/__init__.py @@ -0,0 +1,13 @@ +from .rosmap import load_rosmap +from .yaml import load_yaml +from .osm import load_osm +from .sdf import load_sdf +from .geojson import load_geojson + +LOAD = { + 'yaml': load_yaml, + 'rosmap': load_rosmap, + 'osm': load_osm, + 'sdf': load_sdf, + 'geojson': load_geojson +} diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/load/geojson.py b/lib/mapdesc_ros/mapdesc/mapdesc/load/geojson.py new file mode 100644 index 0000000..5af8f7c --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/load/geojson.py @@ -0,0 +1,54 @@ +from ..model import Map, Path +from pathlib import Path as PathLib +import json +import logging +from ..geo_data import lon_lat_to_point + +logger = logging.getLogger(__name__) + + +def parse_coordinates(first_lat, first_lon, coords, planet: str = 'earth'): + distances = [] + if not first_lat: + first_lon, first_lat = coords[0][0], coords[0][1] + coords = coords[1:] + for lon, lat in coords: + distances.append( + lon_lat_to_point(lat, lon, first_lat, first_lon, body=planet)) + return first_lon, first_lat, distances + + +def get_path_from_geojson(path, planet: str = 'earth'): + paths = [] + first_lat, first_lon = None, None + with open(str(path), encoding='utf-8') as fd: + data = json.load(fd) + for feature in data['features']: + if 'geometry' not in feature: + continue + if 'coordinates' not in feature['geometry']: + continue + if feature['geometry']['type'] == 'Point': + continue + if len(feature['geometry']['coordinates']) == 1: + continue + path = Path( + name='unknown path', + ) + paths.append(path) + first_lon, first_lat, coords = \ + parse_coordinates( + first_lat, first_lon, + feature['geometry']['coordinates'], + planet) + path.points = coords + return paths + + +def load_geojson(input_path=None, planet: str = 'earth'): + input_geojson = PathLib(input_path) + if not input_geojson.exists(): + raise RuntimeError('file/folder to load does not exist') + _map = Map() + _map.path = get_path_from_geojson(input_geojson, planet) + return _map diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/load/osm.py b/lib/mapdesc_ros/mapdesc/mapdesc/load/osm.py new file mode 100644 index 0000000..10b7eb3 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/load/osm.py @@ -0,0 +1,44 @@ +# load data description from OSM coordinages +from ..model import Map, Wall +from ..model.geom import Mesh +from OSMPythonTools.overpass import Overpass, overpassQueryBuilder +import numpy as np +from ..geo_data import lon_lat_to_point + + +def load_osm(lat: float, lon: float, radius: float, body: str = 'earth'): + lat = float(lat) + lon = float(lon) + radius = float(radius) + + north = lat + radius/111320 + south = lat - radius/111320 + east = lon + radius/111320/np.cos(lat*np.pi/180) + west = lon - radius/111320/np.cos(lat*np.pi/180) + + # Build the query using the OverpassQueryBuilder + query = overpassQueryBuilder( + bbox=(south, west, north, east), + elementType=['way', 'relation', 'node'], + selector='"building"="yes"', + out='body', + ) + + overpass = Overpass() + results = overpass.query(query).ways() + + # TODO: save buildings + # building_nodes = {} + + walls = [] + if results: + for way in results: + nodes = way.nodes() + points = [ + lon_lat_to_point( + lat, lon, n.lat(), n.lon(), body) for n in nodes] + mesh = Mesh(polygons=points) + wall = Wall(mesh, name=way.id) + walls.append(wall) + + return Map(wall=walls) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/load/rosmap.py b/lib/mapdesc_ros/mapdesc/mapdesc/load/rosmap.py new file mode 100644 index 0000000..395df82 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/load/rosmap.py @@ -0,0 +1,91 @@ +# load data description +import logging +import math +from pathlib import Path +import yaml +try: + import cv2 + from imutils import contours + import imutils + CV2_AVAILABLE = True +except ImportError: + CV2_AVAILABLE = False +from ..model import Map, Wall +from ..model.geom import Box, Mesh, Dimension, Vector2, Vector3 + +logger = logging.getLogger(__name__) + + +def _shapes_from_image(res, path): + """Generate list of x, y shapes from contours of an image + """ + if not CV2_AVAILABLE: + raise RuntimeError('Can not modify image, OpenCV2 not available') + img = cv2.imread(str(path)) + gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + edges = cv2.Canny(gray_img, 20, 100) + cnts = cv2.findContours( + edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) + cnts = imutils.grab_contours(cnts) + (cnts, _) = contours.sort_contours(cnts) + shapes = [] + for cnt in cnts: + # approx = cv2.approxPolyDP(cnt, 0.03*cv2.arcLength(cnt, True), True) + approx = cv2.approxPolyDP(cnt, 0.01*cv2.arcLength(cnt, True), True) + + coords = [(a[0][0], a[0][1]) for a in approx] + shapes.append(coords) + return shapes + + +def load_rosmap(yaml_file, height=2.0): + """Find contours in an image and create map from it. + """ + _map = Map() + _map.wall = [] + with open(yaml_file, encoding='utf-8') as f: + data = yaml.safe_load(f) + res = data['resolution'] + _map.origin.position = Vector3.from_any(data['origin']) + # get data from image + img_path = Path(yaml_file).absolute().parent / data['image'] + shapes = _shapes_from_image(res, img_path) + for coords in shapes: + points = [(x * res, y * res) for x, y in coords] + center = Mesh.calculate_position([ + Vector2(x, y) for x, y in points]) + if len(points) == 4 and \ + math.isclose( + math.dist(points[0], points[1]), + math.dist(points[2], points[3])) and \ + math.isclose( + math.dist(points[1], points[2]), + math.dist(points[3], points[0])): + + wall = Wall(data=Box()) + + center_top = (points[0][0] + points[1][0]), \ + (points[0][1] + points[1][1]) + center_btm = (points[2][0] + points[3][0]), \ + (points[2][1] + points[3][1]) + width = math.dist(center_top, center_btm) / 2 + + center_lft = (points[1][0] + points[2][0]), \ + (points[1][1] + points[2][1]) + center_rgt = (points[3][0] + points[0][0]), \ + (points[3][1] + points[0][1]) + length = math.dist(center_lft, center_rgt) / 2 + wall.data.size = Dimension(width, length, height) + else: + wall = Wall(data=Mesh()) + wall.data.polygons = [( + Vector2(*p))-center for p in points] + wall.data.size = Dimension(1.0, 1.0, height) + # our coordinates are relative (we subtracted center earlier so) + # we have to save it as the walls position + wall.data.pose.position = center + _map.wall.append(wall) + if not _map.wall: + logger.error('No walls found, aborting!') + return None + return _map diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/load/sdf.py b/lib/mapdesc_ros/mapdesc/mapdesc/load/sdf.py new file mode 100644 index 0000000..5c2b1c0 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/load/sdf.py @@ -0,0 +1,93 @@ +# load data description from SDL file +# see http://sdformat.org/tutorials +from ..model import Map, Wall +from ..model.geom import Box, Pose, Dimension, Vector3, \ + Quaternion, Mesh +from ..util import euler_to_quaternion +import logging +import xml.etree.ElementTree as ET +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def parse_pose(pose: str): + pose = [float(x) for x in pose.split(' ')] + position = Vector3(*[float(x) for x in pose[0:3]]) + rotation = euler_to_quaternion(*pose[3:6]) + return Pose(position=position, orientation=Quaternion(*rotation)) + + +def parse_box_size(link_element): + size = link_element.find('collision/geometry/box/size').text.strip() + return Dimension(*[float(x) for x in size.split(' ')]) + + +def parse_link(link_element): + if link_element.find('collision/geometry/box'): + wall_type = 'box' + wall_data = Box( + pose=parse_pose(link_element.find('pose').text.strip()), + size=parse_box_size(link_element)) + elif link_element.find('collision/geometry/polyline'): + wall_type = 'polygon' + wall_data = Mesh( + polygons=[ + [float(y) for y in x.text.strip().split(' ')] for x in + link_element.findall('collision/geometry/polyline/point')]) + else: + raise RuntimeError('unknown xml geometry') + # we ignore the pose + return Wall( + name=link_element.attrib['name'], + data=wall_data, + type=wall_type, + ) + + +def get_walls_from_sdf(path): + walls = [] + with open(str(path), encoding='utf-8') as fd: + xml_string = fd.read() + root = ET.fromstring(xml_string) + model_element = root.find('model') + # we assume our model is static and + # everything is stored inside a link, + # we also ignore the initial pose of the model + # links are translated into walls + for link in model_element.findall('link'): + wall = parse_link(link) + if wall: + walls.append(wall) + return walls + + +def parse_sdf_dir(path): + with open(str(path / 'model.config'), encoding='utf-8') as fd: + xml_string = fd.read() + root = ET.fromstring(xml_string) + name_el = root.find('name') + desc_el = root.find('description') + sdf_el = root.find('sdf') + + description = desc_el.text.strip() if desc_el is not None else '' + name = name_el.text.strip() if name_el is not None else '' + sdf_file = sdf_el.text.strip() if sdf_el is not None else 'model.sdf' + + _map = Map(name=name, description=description) + _map.walls = get_walls_from_sdf(str(path / sdf_file)) + return _map + + +def load_sdf(input_path=None): + """Load from custom sdf format. + """ + input_sdf = Path(input_path) + if not input_sdf.exists(): + raise RuntimeError('file/folder to load does not exist') + if input_sdf.is_dir(): + _map = parse_sdf_dir(input_sdf) + elif str(input_path)[:4] in ['.sdf', '.xml']: + _map = Map() + _map.walls = get_walls_from_sdf(str(input_sdf)) + return _map diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/load/yaml.py b/lib/mapdesc_ros/mapdesc/mapdesc/load/yaml.py new file mode 100644 index 0000000..ad364e7 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/load/yaml.py @@ -0,0 +1,14 @@ +# load data description +from ..model import Map +import yaml +import logging + +logger = logging.getLogger(__name__) + + +def load_yaml(input_file=None): + """Load from custom yaml format. + """ + with open(input_file, encoding='utf-8') as fd: + yaml_data = yaml.safe_load(fd) + return Map(**yaml_data) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/__init__.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/__init__.py new file mode 100644 index 0000000..f628e32 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/__init__.py @@ -0,0 +1,22 @@ +# Data Description + +# In this file we allow unused imports. +# flake8: noqa + +from .path import Path + +# special description for maps +from .wall import Wall + +# roads/lanes +from .lane import BIDIRECTIONAL, UNIDIRECTIONAL +from .lane import LaneEdge, LaneGraph, LaneNode + +# regions for annotation or behaivor +from .area import Area + +# special points on the map to annotate for the user or for behavior +from .marker import Marker + +# map/graph +from .map import Map diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/area.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/area.py new file mode 100644 index 0000000..b22f597 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/area.py @@ -0,0 +1,80 @@ +# An area defines a region by a rectangle or polygon on the map, +# either as annotation for the user without function or +# for the planning system to change agent behavior when it enters a +# region. +# For example go to the next task when the agent moves +# into a goal area or change from a behavior where the robot +# follows a strict line to a behavior where the robot +# explores the map further. +from .geom.box import Box +from .geom.mesh import Mesh +from dataclasses import dataclass, field + + +TYPES = { + 'mesh': Mesh, + 'box': Box +} + + +@dataclass +class Area: + data: Box | Mesh + # geometric type (mesh or box) + type: str = 'mesh' + # custom type of your are, can for example be 'recharging' or 'unload' + # so other tools can use it to execute behavior based on this type + area_type: str = 'unnamed' + # name to identify area + name: str = None + # color as RGB array with values from 0 to 255 + color: list = field(default_factory=lambda: [50, 50, 255]) + + @property + def center(self): + return self.data.pose.position + + @property + def points(self): + return self.data.points + + @property + def size(self): + return self.data.size + + @property + def pose(self): + return self.data.pose + + def local_points(self): + return self.data.local_points() + + def __post_init__(self): + if isinstance(self.data, dict): + clz = TYPES[self.type] + # pylint: disable=not-a-mapping + self.data = clz(**self.data) + + def __iter__(self): + yield ('data', dict(self.data)) + yield ('type', self.type) + yield ('area_type', self.area_type) + if self.name: + yield ('name', self.name) + if self.color: + yield ('color', self.color) + + def copy(self): + return Area(**dict(self)) + + def bounding_box(self): + if self.type != 'mesh': + return + self.data = self.data.bounding_box() + self.type = 'box' + + def boxify(self): + if self.type != 'mesh': + return + self.data = self.data.boxify() + self.type = 'box' diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/ext.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/ext.py new file mode 100644 index 0000000..b2e456a --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/ext.py @@ -0,0 +1,19 @@ +# An external object, +from dataclasses import dataclass +# bounding box +from .geom.box import Box + + +@dataclass +class Ext: + name: str = None + type: str = 'gltf' # 'geotiff' | 'obj' | 'fbx' | 'ifc' | 'gltf' + data: Box + # list of strings + filenames: list + + def __iter__(self): + yield ('name', self.name) + yield ('type', self.type) + yield ('data', dict(self.data)) + yield ('filenames', list(self.filenames)) \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/__init__.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/__init__.py new file mode 100644 index 0000000..2d4759b --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/__init__.py @@ -0,0 +1,19 @@ +# In this file we allow unused imports. +# flake8: noqa + +# dots/points/vectors/nodes +from .vector2 import Vector2 # x, y +from .vector3 import Vector3 # x, y, z +from .quaternion import Quaternion # w, y, z, w +from .pose import Pose # Vector3 position and Quaternion orientation + +# physical description +from .dimension import Dimension + +# generic visual Objects +from .box import Box +from .plane import Plane +from .sphere import Sphere +from .capsule import Capsule +from .cylinder import Cylinder +from .mesh import Mesh # a 2D/3D list of points diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/box.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/box.py new file mode 100644 index 0000000..6725849 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/box.py @@ -0,0 +1,61 @@ +import dataclasses +import numpy as np + +from .vector2 import Vector2 +from .vector3 import Vector3 +from .dimension import Dimension +from .pose import Pose + + +@dataclasses.dataclass +class Box: + pose: Pose = dataclasses.field(default_factory=Pose) + size: Dimension = dataclasses.field(default_factory=Dimension) + + def __post_init__(self): + if isinstance(self.pose, dict): + # pylint: disable=not-a-mapping + self.pose = Pose(**self.pose) + if isinstance(self.size, dict): + # pylint: disable=not-a-mapping + self.size = Dimension(**self.size) + elif isinstance(self.size, (list, tuple, set)): + self.size = Dimension(*self.size) + + @property + def points(self): + # return points counter clockwise sorted points defining the outer + # hull in 2d + return [ + Vector2(-self.size.width/2, self.size.length/2), + Vector2(-self.size.width/2, -self.size.length/2), + Vector2(self.size.width/2, -self.size.length/2), + Vector2(self.size.width/2, self.size.length/2) + ] + + def local_points(self): + # apply pose (translate by position and rotate by orientation) + points = [] + # 1. rotate by orientation + # all points are centered/we rotate around x=0, y=0 + # returns a 3x3 matrix that we can multiply with our coordinates + matrix = self.pose.orientation.rotation_matrix() + for point in self.points: + if isinstance(point, Vector3): + point = list(point) + elif isinstance(point, Vector2): + point = list(point) + [0.0] + dot = np.dot(matrix, point) + points.append(Vector2(*dot[:2])) + # 2. translate by position (only x and y coordinates) + pose_2d = Vector3(self.pose.position.x, self.pose.position.y) + points = [p + pose_2d for p in points] + return points + + def __iter__(self): + yield ('pose', dict(self.pose)) + if self.size: + yield ('size', tuple(self.size)) + + def copy(self): + return Box(**dict(self)) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/capsule.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/capsule.py new file mode 100644 index 0000000..5225f6c --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/capsule.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass + +from .box import Box + + +@dataclass +class Capsule(Box): + radius: float = 1.0 + length: float = 1.0 + + def __iter__(self): + yield from super().__iter__() + yield ('radius', self.radius) + yield ('length', self.length) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/cylinder.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/cylinder.py new file mode 100644 index 0000000..3a18c3d --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/cylinder.py @@ -0,0 +1,15 @@ +import dataclasses + + +from .box import Box + + +@dataclasses.dataclass +class Cylinder(Box): + radius: float = 1.0 + length: float = 1.0 + + def __iter__(self): + yield from super().__iter__() + yield ('radius', self.radius) + yield ('length', self.length) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/dimension.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/dimension.py new file mode 100644 index 0000000..8188c52 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/dimension.py @@ -0,0 +1,67 @@ +import dataclasses + + +@dataclasses.dataclass +class Dimension: + # Dimensions in meter, defaults to a 1x1x1 meter cube + width: float = 1.0 + length: float = 1.0 + height: float = 1.0 + + def __init__(self, width=1.0, length=1.0, height=1.0, *_, **__): + self.width = width + self.length = length + self.height = height + + def __iter__(self): + """ Helper to create a tuple from this """ + yield float(self.width) + yield float(self.length) + yield float(self.height) + + def __add__(self, o): + if isinstance(o, Dimension): + return Dimension( + self.width + o.width, + self.length + o.length, + self.height + o.height) + else: + return Dimension( + self.width + o, + self.length + o, + self.height + o) + + def __sub__(self, o): + if isinstance(o, Dimension): + return Dimension( + self.width - o.width, + self.length - o.length, + self.height - o.height) + else: + return Dimension( + self.width - o, + self.length - o, + self.height - o) + + def __mul__(self, o): + if isinstance(o, Dimension): + return Dimension( + self.width * o.width, + self.length * o.length, + self.height * o.height) + else: + return Dimension( + self.width * o, + self.length * o, + self.height * o) + + def __neg__(self): + return Dimension(-self.width, -self.length, -self.height) + + def null(self): + """check if all sides are 0.""" + return self.width == self.height == self.length == 0.0 + + def copy(self): + """create new Dimension instance with same data.""" + return Dimension(self.width, self.length, self.height) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/mesh.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/mesh.py new file mode 100644 index 0000000..b0c6c63 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/mesh.py @@ -0,0 +1,126 @@ +import math + +from dataclasses import dataclass, field + +from .box import Box +from .dimension import Dimension +from .vector2 import Vector2 +from .vector3 import Vector3 +from .quaternion import Quaternion +from ...util import ccw_sort, calculate_slope, \ + dot_product, euler_to_quaternion +from .pose import list_to_vector, Pose + + +@dataclass +class Mesh(Box): + polygons: list = field(default_factory=list) + + def __post_init__(self): + super().__post_init__() + self.polygons = [ + list_to_vector(poly) if isinstance(poly, (list, tuple)) else poly + for poly in self.polygons + ] + + def ccw_sort(self): + """sort polygon points counter-clockwise.""" + self.polygons = ccw_sort(self.polygons) + + @property + def points(self): + return self.polygons + + @staticmethod + def calculate_position(points, clz=Vector2): + # if you like to not use the zero-vector as position for the mesh + # but the center of it you can use this function to calculate the + # new position. Make sure to subtract the result of this vector + # from each point. + if clz == Vector2: + return Vector2( + sum([p.x for p in points]) / len(points), + sum([p.y for p in points]) / len(points)) + elif clz == Vector3: + return Vector3( + sum([p.x for p in points]) / len(points), + sum([p.y for p in points]) / len(points), + sum([p.z for p in points]) / len(points)) + else: + raise RuntimeError( + 'Only Vector2 or Vector3 are valid options as class, not ' + f'{clz}') + + def recenter(self): + clz = self.polygons[0].__class__ + center = Mesh.calculate_position(self.polygons, clz) + for idx, _ in enumerate(self.polygons): + self.polygons[idx] -= center + self.pose.position.x += center.x + self.pose.position.y += center.y + + def is_rectangle(self): + p = self.polygons + if len(p) != 4: + return False + slopes = [ + calculate_slope(p[0], p[1]), + calculate_slope(p[1], p[2]), + calculate_slope(p[2], p[3]), + calculate_slope(p[3], p[0]) + ] + if slopes[0] != slopes[2] or slopes[1] != slopes[3]: + return False + # check for right angle using dot product + if dot_product(p[0], p[1], p[2]) != 0: + return False + return True + + def boxify(self): + if not self.is_rectangle(): + # the mesh has to consist of exactly 4 polygons + return False + # recalculate center + + self.recenter() + + p = self.polygons + # calculate center of points right to the center + midpoint = p[0].midpoint(p[1]) + width = midpoint.distance(Vector2()) * 2 + length = p[1].midpoint(p[2]).distance(Vector2()) * 2 + + rot_z = math.atan2(midpoint.y, midpoint.x) + quat = euler_to_quaternion(0, 0, rot_z) + self.pose.orientation = Quaternion(*quat) + + return Box(size=Dimension(width, length), pose=self.pose) + + def bounding_box(self): + """create a new bounding box around the points.""" + min_x = min([p.x for p in self.polygons]) + max_x = max([p.x for p in self.polygons]) + min_y = min([p.y for p in self.polygons]) + max_y = max([p.y for p in self.polygons]) + + size = Dimension() + size.width = float(max_x - min_x) + + pose = Pose(orientation=self.pose.orientation.copy()) + pose.position.x = float(min_x + max_x) / 2 + + pose.position.y = float(min_y + max_y) / 2 + pose.position.z = float(pose.position.z) + size.length = float(max_y - min_y) + + if isinstance(self.polygons[0], Vector3): + min_z = min([p.z for p in self.polygons]) + max_z = max([p.z for p in self.polygons]) + pose.position.z = float(min_z + max_z) / 2 + size.height = float(max_z - min_z) + + return Box(pose=pose, size=size) + + def __iter__(self): + yield from super().__iter__() + yield ('polygons', [list(p) for p in self.polygons]) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/plane.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/plane.py new file mode 100644 index 0000000..b39ef56 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/plane.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass, field + +from .vector3 import Vector3 +from .box import Box + + +# TODO: let the Box inerhit plane - a box is a plane with a height +# or have them separated? +@dataclass +class Plane(Box): + normal: Vector3 = field(default_factory=Vector3()) + + def __post_init__(self): + super().__post_init__() + if isinstance(self.normal, (list, tuple)): + self.normal = Vector3(*self.normal) + # default for dimension is a 1x1x1 cube, so we have to set it to 0. + self.size.height = 0.0 + + def __iter__(self): + yield from super().__iter__() + yield ('normal', tuple(self.normal)) + yield ('size', tuple(self.size)[0:2]) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/pose.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/pose.py new file mode 100644 index 0000000..b0d1411 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/pose.py @@ -0,0 +1,80 @@ +from dataclasses import dataclass, field +from .quaternion import Quaternion +from .vector2 import Vector2 +from .vector3 import Vector3 + + +def list_to_vector(data): + """Creates a Vector or Quaternion from a list by the list size.""" + if len(data) == 2: + return Vector2(*data) + if len(data) == 3: + return Vector3(*data) + if len(data) == 4: + return Quaternion(*data) + raise RuntimeError(f'data for vector should be of length 2-4: {data}') + + +def dict_to_vector(data): + """Creates a Vector or Quaternion from a dict. + + Needs at least x and y for a 2d pose, creates a 3D-vector for x, y, z + and a Quaternion if w is also given. + """ + if 'x' not in data or 'y' not in data: + raise RuntimeError('x and y not set, not a valid pose.') + if 'w' in data: + return Quaternion(**data) + if 'z' in data: + return Vector3(**data) + return Vector2(**data) + + +def any_to_vector(data): + """converts any data to a vector or Quaternion if possible. + + see @dict_to_vector and @list_to_vector. + """ + if isinstance(data, (list, tuple, set)): + return list_to_vector(data) + if isinstance(data, dict): + return dict_to_vector(data) + if isinstance(data, (Vector2, Vector3, Quaternion)): + return data + raise RuntimeError(f'unknown vector type {type(data)} ({data})') + + +@dataclass +class Pose: + """Describes the pose of an object. + + (defined by its position and orientation).""" + position: Vector3 = field(default_factory=Vector3) + orientation: Quaternion = field(default_factory=Quaternion) + _euler_orientation: Vector3 = field(default_factory=Vector3) + + def __post_init__(self): + if self.orientation: + self.orientation = any_to_vector(self.orientation) + if self.position: + self.position = any_to_vector(self.position) + + def euler_orientation(self): + """Create euler orientation from Quaternion. + + Caches the generated euler orientation to a local _euler_orientation. + Note that it might be different from the given orientation-quaternion. + """ + if isinstance(self.orientation, Quaternion): + self._euler_orientation = self.orientation.to_euler() + elif isinstance(self.orientation, Vector3): + self._euler_orientation = self.orientation + return self._euler_orientation + + def __iter__(self): + yield ('orientation', tuple(self.orientation)) + yield ('position', tuple(self.position)) + + def copy(self): + """copy values from current Pose to a new Pose-instance.""" + return Pose(**dict(self)) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/quaternion.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/quaternion.py new file mode 100644 index 0000000..5dacfcd --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/quaternion.py @@ -0,0 +1,103 @@ +from dataclasses import dataclass + +from ...util import quaternion_to_euler +from .vector3 import Vector3 + + +@dataclass +class Quaternion(Vector3): + # a quaternion describes rotation in radians. + w: float = 1.0 + + def __init__(self, x=0.0, y=0.0, z=0.0, w=1.0, *args, **kwargs): + super().__init__(x, y, z) + self.w = w + + def __iter__(self): + """ Helper to create a tuple from this """ + yield self.x + yield self.y + yield self.z + yield self.w + + def __getitem__(self, idx): + if idx in [3, 'w']: + return self.w + return super().__getitem__(idx) + + def __add__(self, o): + if isinstance(o, Quaternion): + return Quaternion( + self.x + o.x, self.y + o.y, self.z + o.z, self.w + o.w) + else: + return Quaternion(*[val + o for val in self]) + + def __sub__(self, o): + if isinstance(o, Quaternion): + return Quaternion( + self.x - o.x, self.y - o.y, self.z - o.z, self.w - o.w) + else: + return Quaternion(*[val - o for val in self]) + + def __mul__(self, o): + """ Scalar multiplication """ + if isinstance(o, Quaternion): + return Quaternion( + self.x * o.x, self.y * o.y, self.z * o.z, self.w * o.w) + else: + return Quaternion(*[val * o for val in self]) + + def __neg__(self): + """ Returns a vector pointing in the opposite direction """ + return Quaternion(*[-val for val in self]) + + def normalize(self): + """ Normalizes the vector to have unit length """ + mag = self.magnitude() + if not mag: + raise RuntimeError( + f'Can not normalize null quaternion {self}') + return Quaternion(*[q / mag for q in self]) + + def copy(self): + return Quaternion(self.x, self.y, self.z, self.w) + + @staticmethod + def from_any(lst): + if isinstance(lst, Quaternion): + return lst + if isinstance(lst, (list, tuple)): + return Quaternion(*lst[:4]) + elif isinstance(lst, dict): + return Quaternion(**lst) + else: + raise RuntimeError('given data neither list nor dict') + + def rotation_matrix(self) -> list: + # see https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation# + # Conversion_to_and_from_the_matrix_representation + + # get a normalized copy of the quaternion + quat = self.normalize() + + # Calculate the elements of the rotation matrix + w, x, y, z = quat.w, quat.x, quat.y, quat.z + xx = x * x + xy = x * y + xz = x * z + yy = y * y + yz = y * z + zz = z * z + wx = w * x + wy = w * y + wz = w * z + + # Construct and return the rotation matrix + return [[1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy)], + [2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx)], + [2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)]] + + def to_euler(self): + e = Vector3() + e.x, e.y, e.z = quaternion_to_euler(self.x, self.y, self.z, self.w) + return e diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/sphere.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/sphere.py new file mode 100644 index 0000000..1241ac9 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/sphere.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +from .box import Box + + +@dataclass +class Sphere(Box): + radius: float = 1.0 + + def __iter__(self): + yield from super().__iter__() + yield ('radius', self.radius) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/vector2.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/vector2.py new file mode 100644 index 0000000..2f9a190 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/vector2.py @@ -0,0 +1,115 @@ +import dataclasses +import math + + +@dataclasses.dataclass +class Vector2: + x: float = 0.0 + y: float = 0.0 + + def __init__(self, x=0.0, y=0.0, *args, **kwargs): + self.x = x + self.y = y + + def __getitem__(self, idx): + if idx in [0, 'x']: + return self.x + elif idx in [1, 'y']: + return self.y + elif idx in [2, 'z']: + # just in case someone wanted a vector3 instead + return 0.0 + else: + raise RuntimeError(f'unknown key "{idx}"') + + def serialize(self): + """Serializes the vector as tuple.""" + return self.to_tuple() + + def to_tuple(self): + """generates a tuple using the dataclasses.astuple helper function.""" + return tuple(float(i) for i in dataclasses.astuple(self)) + + def __iter__(self): + """The default iteration for vectors behaves like a list or tuple.""" + yield float(self.x) + yield float(self.y) + + def __add__(self, o): + if isinstance(o, Vector2): + return Vector2(self.x + o.x, self.y + o.y) + else: + return Vector2(*[val + o for val in self]) + + def __round__(self, num): + return Vector2( + round(self.x, num), + round(self.y, num)) + + def __sub__(self, o): + if isinstance(o, Vector2): + return Vector2(self.x - o.x, self.y - o.y) + else: + return Vector2(*[val - o for val in self]) + + def __mul__(self, o): + """ Scalar multiplication """ + if isinstance(o, Vector2): + return Vector2(self.x * o.x, self.y * o.y) + else: + return Vector2(*[val * o for val in self]) + + def __neg__(self): + """ Returns the vector pointing in the opposite direction """ + return Vector2(-self.x, -self.y) + + def magnitude(self): + """ Calculate the euclidean length of the vector """ + return math.sqrt(sum([val**2 for val in self])) + + def normalize(self): + """ Normalizes the vector to have unit length """ + return Vector2(*[q / self.magnitude() for q in self]) + + def null(self): + """Check if both, x and y are 0.""" + return self.x == self.y == 0 + + def copy(self): + """create a new vector2 instance.""" + return Vector2(*self) + + @staticmethod + def from_any(_any): + if isinstance(_any, (Vector2)): + return _any + if isinstance(_any, (list, tuple, set)): + return Vector2(*_any) + if isinstance(_any, dict): + return Vector2(**_any) + raise RuntimeError('given data neither list nor dict') + + def is_close(self, other, rel_tol=0.01, abs_tol=0.01, threshold=None): + """ Check if a Vector2 is close to another Vector2. """ + if not other: + return False + if isinstance(other, dict): + x, y = other['x'], other['y'] + elif isinstance(other, (list, tuple, set)): + x, y = other + else: + x, y = other.x, other.y + if threshold: + return abs(self.x - x) + abs(self.y + y) < threshold + return math.isclose( + self.x, x, rel_tol=rel_tol, abs_tol=abs_tol) and \ + math.isclose( + self.y, y, rel_tol=rel_tol, abs_tol=abs_tol) + + def distance(self, other): + """ Calculate the distance to the given position """ + return (self - other).magnitude() + + def midpoint(self, other): + """Return a midpoint between the two points.""" + return Vector2((self.x + other.x) / 2, (self.y + other.y) / 2) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/vector3.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/vector3.py new file mode 100644 index 0000000..d9bfb43 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/geom/vector3.py @@ -0,0 +1,96 @@ +import dataclasses +import math +from .vector2 import Vector2 + + +@dataclasses.dataclass +class Vector3(Vector2): + z: float = 0.0 + + def __init__(self, x=0.0, y=0.0, z=0.0, *args, **kwargs): + super().__init__(x, y) + self.z = z + + def __getitem__(self, idx): + if idx in [2, 'z']: + return self.z + return super().__getitem__(idx) + + def __iter__(self): + """ Helper to create a tuple from this """ + yield self.x + yield self.y + yield self.z + + def __add__(self, o): + if isinstance(o, Vector3): + return Vector3(self.x + o.x, self.y + o.y, self.z + o.z) + else: + return Vector3(*[val + o for val in self]) + + def __sub__(self, o): + if isinstance(o, Vector3): + return Vector3(self.x - o.x, self.y - o.y, self.z - o.z) + else: + return Vector3(*[val - o for val in self]) + + def __mul__(self, o): + """ Scalar multiplication """ + if isinstance(o, Vector3): + return Vector3(self.x * o.x, self.y * o.y, self.z * o.z) + else: + return Vector3(*[val * o for val in self]) + + def __neg__(self): + """ Returns a vector pointing in the opposite direction """ + return Vector3(*[-val for val in self]) + + def __round__(self, num): + return Vector3( + round(self.x, num), + round(self.y, num), + round(self.z, num)) + + def normalize(self): + """ Normalizes the vector to have unit length """ + return Vector3(*[q / self.magnitude() for q in self]) + + def copy(self): + return Vector3(self.x, self.y, self.y) + + def is_close(self, o, rel_tol=0.01, abs_tol=0.01, threshold=None): + """ Check if a Vector3 is close to another Vector3. """ + if not o: + return False + if isinstance(o, dict): + x, y, z = o['x'], o['y'], o['z'] + elif isinstance(o, (list, tuple, set)): + x, y, z = o + else: + x, y, z = o.x, o.y, o.z + if threshold: + return abs(self.x - x) + abs(self.y - y) + abs(self.z - z) < \ + threshold + return math.isclose( + self.x, x, rel_tol=rel_tol, abs_tol=abs_tol) and \ + math.isclose( + self.y, y, rel_tol=rel_tol, abs_tol=abs_tol) and \ + math.isclose( + self.z, z, rel_tol=rel_tol, abs_tol=abs_tol) + + @staticmethod + def from_any(lst): + if isinstance(lst, Vector3): + return lst + if isinstance(lst, (list, tuple, set)): + return Vector3(*lst) + if isinstance(lst, dict): + return Vector3(**lst) + raise RuntimeError('given data neither list nor dict') + + def midpoint(self, other): + """Return a midpoint between the two points.""" + return Vector3( + (self.x + other.x) / 2, + (self.y + other.y) / 2, + (self.z + other.z) / 2) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/lane.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/lane.py new file mode 100644 index 0000000..77d0a6f --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/lane.py @@ -0,0 +1,100 @@ +# lane inspired by +# https://github.com/open-rmf/rmf_building_map_msgs/ +from dataclasses import dataclass, field +from .geom.vector3 import Vector3 + + +BIDIRECTIONAL = 0 +UNIDIRECTIONAL = 1 + + +@dataclass +class LaneGraph: + nodes: list = field(default_factory=list) # list of LaneNode + edges: list = field(default_factory=list) # list of edges + _last_new_number: int = 0 # used to optimize generation of unique ids + + def _unique_node_name(self): + """Generate a new unique name for a node.""" + known_names = [n.name for n in self.nodes] + while True: + new_name = f'node_{self._last_new_number}' + if new_name in known_names: + self._last_new_number += 1 + else: + return new_name + + def __post_init__(self): + self.nodes = [ + LaneNode(**n) if isinstance(n, dict) else n + for n in self.nodes + ] + self.edges = [ + LaneEdge(**e) if isinstance(e, dict) else e + for e in self.edges + ] + # generate unique id for all nodes if necessary + for node in self.nodes: + if not node.name: + node.name = self._unique_node_name() + for edge in self.edges: + edge.graph = self + edge.update_nodes() + + def __iter__(self): + yield ('nodes', [dict(node) for node in self.nodes]) + yield ('edges', [dict(edge) for edge in self.edges]) + + +@dataclass +class LaneNode: + # node in the navigation graph (similar to marker.Marker) + position: Vector3 = field(default_factory=Vector3) + name: str = None + # additional information about this node that the user can define + # as key-value pair + params: dict = field(default_factory=dict) + + def __post_init__(self): + self.position = Vector3.from_any(self.position) + + def __iter__(self): + yield ('name', self.name) + yield ('position', list(self.position)) + if self.params: + yield ('params', self.params) + + +@dataclass +class LaneEdge: + source: LaneNode = None + target: LaneNode = None + graph: LaneGraph = None + + # Unidirectional lanes only allow driving from source to target + # not from target to source + edge_type: int = BIDIRECTIONAL # enum UNIDIRECTIONAL or BIDIRECTIONAL + + # optional name for the edge + name: str = None + + # additional information about this edge that the user can define + params: dict = None + + def update_nodes(self): + """Set source and target nodes based on their poisitons + in the graph. + """ + if isinstance(self.source, int): + self.source = self.graph.nodes[self.source] + if isinstance(self.target, int): + self.target = self.graph.nodes[self.target] + + def __iter__(self): + yield ('source', self.graph.nodes.index(self.source)) + yield ('target', self.graph.nodes.index(self.target)) + if self.name: + yield ('name', self.name) + if self.params: + yield ('params', self.params) + yield ('edge_type', self.edge_type) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/map.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/map.py new file mode 100644 index 0000000..61619b0 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/map.py @@ -0,0 +1,117 @@ +from dataclasses import dataclass, field + +from .geom.dimension import Dimension +from .geom.vector3 import Vector3 +from .wall import Wall +from .marker import Marker +from .area import Area +from .lane import LaneGraph +from .path import Path + + +@dataclass +class Map: + # name has to be unique as its also the file-name + name: str = 'undefined' + # optional description of the map + description: str = 'some map' + + # size of the map + size: Dimension = field(default_factory=Dimension) + # resolution - important for export for the ROS 2 mapserver + resolution: float = 0.05 + # optional origin of the 0:0 position + # (mostly imporant for the ROS 2 mapserver) + origin: Vector3 = field(default_factory=Vector3) + + # obstacles/walls, for example useful to model a building + wall: list = field(default_factory=list) # list[Wall] + # a marker is a single points with a direction + marker: list = field(default_factory=list) + # the area is an annotation of a region + area: list = field(default_factory=list) # list[Area] + # the path consists of multiple points connected to each other + path: list = field(default_factory=list) + + # ext is short for external mesh, useful for more complex environments. + ext: list = field(default_factory=list) + + lane_graph: LaneGraph = None + + def __post_init__(self): + if isinstance(self.size, (list, tuple)): + self.size = Dimension(*self.size) + if isinstance(self.origin, list): + # pylint: disable=not-a-mapping + self.origin = Vector3(*self.origin) + if isinstance(self.origin, dict): + # pylint: disable=not-a-mapping + self.origin = Vector3(**self.origin) + if self.marker: + self.marker = [ + Marker(**m) if isinstance(m, dict) else m + for m in self.marker + ] + if self.area: + self.area = [ + Area(**a) if isinstance(a, dict) else a + for a in self.area + ] + if self.wall: + self.wall = [ + Wall(**w) if isinstance(w, dict) else w + for w in self.wall + ] + if self.path: + self.path = [ + Path(**p) if isinstance(p, dict) else p + for p in self.path + ] + + # TODO: self.ext + + if isinstance(self.lane_graph, dict): + self.lane_graph = LaneGraph(**self.lane_graph) + + def __iter__(self): + yield ('name', self.name) + yield ('description', self.description) + if self.wall: + yield ('wall', [dict(w) for w in self.wall]) + yield ('resolution', self.resolution) + yield ('origin', list(self.origin)) + if self.lane_graph: + yield ('lane_graph', dict(self.lane_graph)) + if self.area: + yield ('area', [dict(a) for a in self.area]) + if self.marker: + yield ('marker', [dict(m) for m in self.marker]) + if self.path: + yield ('path', [dict(m) for m in self.path]) + if self.ext: + yield ('ext', [dict(m) for m in self.ext]) + + def _meshes(self): + """returns all items that have a mesh.""" + meshes = [] + if self.area: + meshes += self.area + if self.wall: + meshes += self.wall + return [m for m in meshes if m.type == 'mesh'] + + def recenter(self): + """Recenter walls and areas""" + for item in self._meshes(): + if item.type == 'mesh': + item.data.recenter() + + def boxify(self): + """If there is a mesh with 4 points that align to make it a box""" + for item in self._meshes(): + item.boxify() + + def bounding_box(self): + """recenter walls and areas.""" + for item in self._meshes(): + item.bounding_box() diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/marker.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/marker.py new file mode 100644 index 0000000..cf9eda6 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/marker.py @@ -0,0 +1,41 @@ +# annotations or special points on the map, can for example be used +# by navigation as a goal (similar to lane.LaneNode) +from dataclasses import dataclass, field +from .geom.pose import Pose + + +@dataclass +class Marker: + name: str = 'new marker' + + # marker can have an orientation + pose: Pose = field(default_factory=Pose) + + # additional information about this marker that the user can define + params: dict = field(default_factory=dict) + + # color the marker has on the map, defaults to red + color: list = field(default_factory=lambda: [255, 50, 50]) + + # radius of the marker (if we print the map) + radius: float = 1.0 + + # type of the marker (point, sphere, box, ...) + type: str = 'point' + + def __post_init__(self): + if isinstance(self.pose, dict): + # pylint: disable=not-a-mapping + self.pose = Pose(**self.pose) + + def __iter__(self): + yield ('name', self.name) + yield ('pose', dict(self.pose)) + if self.color: + yield ('color', self.color) + if self.radius is not None: + yield ('radius', self.radius) + if self.params: + yield ('params', self.params) + if self.type: + yield ('type', self.type) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/path.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/path.py new file mode 100644 index 0000000..bc6b5dd --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/path.py @@ -0,0 +1,38 @@ +"""A simple path is just a list of (way-)points. + +For a more defined behavior use lanes (see lane.py). +""" +from dataclasses import dataclass, field +from .geom.pose import Pose +from .geom.vector3 import Vector3 + + +@dataclass +class Path: + name: str + points: list = field(default_factory=list) + pose: Pose = field(default_factory=Pose) + size: Vector3 = None + color: str = 'red' + distance_relative_to_ground: bool = True + radius: float = 0.3 + + def __post_init__(self): + if not self.size: + self.size = Vector3(1, 1, 1) + + def __iter__(self): + yield ('name', self.name) + yield ('points', self.points) + if self.pose: + yield ('pose', dict(self.pose)) + if self.size: + yield ('size', list(self.size)) + if self.color: + yield ('color', self.color) + if self.distance_relative_to_ground: + yield ( + 'distance_relative_to_ground', + self.distance_relative_to_ground) + if self.radius: + yield ('radius', self.radius) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/model/wall.py b/lib/mapdesc_ros/mapdesc/mapdesc/model/wall.py new file mode 100644 index 0000000..0e3d1b1 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/model/wall.py @@ -0,0 +1,78 @@ +from dataclasses import dataclass +from .geom.box import Box +from .geom.mesh import Mesh + + +TYPES = { + 'mesh': Mesh, + 'box': Box +} + + +@dataclass +class Wall: + # required, either a Box or a Mesh + data: Box | Mesh + + # optional name for the wall + name: str = None + + # optional list of holes in the wall + holes: list = None + + # type, is set based on given data + type: str = None + + def __post_init__(self): + if isinstance(self.data, dict): + clz = TYPES[self.type] + # pylint: disable=not-a-mapping + self.data = clz(**self.data) + else: + self.type = self.data.__class__.__name__.lower() + self.holes = self.holes if self.holes else [] + + @property + def center(self): + return self.data.pose.position + + @property + def points(self): + return self.data.points + + @property + def size(self): + return self.data.size + + @property + def pose(self): + return self.data.pose + + def local_points(self): + return self.data.local_points() + + def __iter__(self): + yield ('data', dict(self.data)) + yield ('type', self.type) + if self.name: + yield ('name', self.name) + if self.holes: + yield ('holes', self.holes) + + def bounding_box(self): + if self.type != 'mesh': + return + box = self.data.bounding_box() + if not box: + return + self.data = box + self.type = 'box' + + def boxify(self): + if self.type != 'mesh': + return + box = self.data.boxify() + if not box: + return + self.data = box + self.type = 'box' diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/save/__init__.py b/lib/mapdesc_ros/mapdesc/mapdesc/save/__init__.py new file mode 100644 index 0000000..2631a25 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/save/__init__.py @@ -0,0 +1,14 @@ +from .png import save_png +from .sdf import save_sdf +from .yaml import save_yaml +from .svg import save_svg +from .rosmap import save_rosmap + + +SAVE = { + 'sdf': save_sdf, + 'yaml': save_yaml, + 'png': save_png, + 'svg': save_svg, + 'rosmap': save_rosmap +} diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/save/png.py b/lib/mapdesc_ros/mapdesc/mapdesc/save/png.py new file mode 100644 index 0000000..ea7d4f4 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/save/png.py @@ -0,0 +1,79 @@ +try: + import cv2 + CV2_AVAILABLE = True +except ImportError: + CV2_AVAILABLE = False +import numpy as np +from ..util import image_dimensions +import logging + +logger = logging.getLogger(__name__) + + +def rgb_to_bgr(rgb): + return [rgb[2], rgb[1], rgb[0]] + + +def save_png(_map, output_file, obstacles_only=False): + if not CV2_AVAILABLE: + raise RuntimeError('Can not save as PNG, OpenCV2 not avaialbe') + width, height, offset_x, offset_y = image_dimensions(_map) + res = _map.resolution + img = np.zeros((int(height / res), int(width / res), 3), dtype=np.uint8) + img.fill(255) + if _map.area and not obstacles_only: + for area in _map.area: + pts = [ + ( + int((p.x + offset_x) / res), + int((p.y + offset_y) / res) + ) for p in area.local_points() + ] + pts = np.array( + [pts], + dtype=np.int32) + color = rgb_to_bgr(area.color) if area.color \ + else [255, 100, 100] + cv2.fillPoly(img, [pts], color) + + for wall in _map.wall: + pts = [ + ( + int((p.x + offset_x) / res), + int((p.y + offset_y) / res) + ) for p in wall.local_points() + ] + pts = np.array( + [pts], + dtype=np.int32) + color = [0, 0, 0] + cv2.fillPoly(img, [pts], color) + + if _map.lane_graph and not obstacles_only: + for edge in _map.lane_graph.edges: + s = edge.source.position + t = edge.target.position + x1 = int((s.x + offset_x) / res) + y1 = int((s.y + offset_y) / res) + x2 = int((t.x + offset_x) / res) + y2 = int((t.y + offset_y) / res) + thickness = 1 + color = (0, 0, 255) + start = (x1, y1) + end = (x2, y2) + cv2.line(img, start, end, color, thickness) + + if not obstacles_only: + for marker in _map.marker: + pos = marker.pose.position + x = int((pos.x + offset_x) / res) + y = int((pos.y + offset_y) / res) + color = rgb_to_bgr(marker.color) if marker.color else (0, 0, 255) + radius = (marker.radius if marker.radius else 1.0) / res + cv2.circle(img, (x, y), int(radius), color) + if width == 0 or height == 0: + logger.error( + 'Can not safe image %s, invalid dimensions %i:%i', + output_file, width, height) + return + cv2.imwrite(str(output_file), img) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/save/rosmap.py b/lib/mapdesc_ros/mapdesc/mapdesc/save/rosmap.py new file mode 100644 index 0000000..c497e38 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/save/rosmap.py @@ -0,0 +1,28 @@ +from .png import save_png +from ..model import Map +import yaml +from pathlib import Path +import logging + +logger = logging.getLogger(__name__) + + +def save_rosmap(_map: Map, yaml_file: str): + path = Path(yaml_file) + if path.suffix not in ['.yaml', '.yml']: + logger.error('Not a yaml file!') + return + base = yaml_file[:-len(path.suffix)] + png_file = Path(f'{base}.png') + save_png(_map, png_file, obstacles_only=True) + yaml_data = { + 'image': str(png_file.relative_to(png_file.parent)), + 'resolution': _map.resolution, + 'origin': list(_map.origin), + 'negate': 0, + 'occupied_thresh': 0.65, + 'free_thresh': 0.196 + } + with open(str(yaml_file), 'w', encoding='utf-8') as fd: + yaml.safe_dump( + yaml_data, fd, default_style=None, default_flow_style=None) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/save/sdf.py b/lib/mapdesc_ros/mapdesc/mapdesc/save/sdf.py new file mode 100644 index 0000000..a6445a4 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/save/sdf.py @@ -0,0 +1,24 @@ +import os +import jinja2 +from pathlib import Path + + +BASE_DIR = Path(os.path.dirname(__file__)).absolute() +TEMPLATE_DIR = BASE_DIR.parent / 'data' / 'templates' +templateLoader = jinja2.FileSystemLoader(searchpath=TEMPLATE_DIR) +templateEnv = jinja2.Environment(loader=templateLoader) +TEMPLATE_SDF = "model.sdf.j2" +TEMPLATE_CFG = "model.config.j2" +template_sdf = templateEnv.get_template(TEMPLATE_SDF) +template_cfg = templateEnv.get_template(TEMPLATE_CFG) + + +def save_sdf(_map, output_path): + output_path = Path(output_path) + if not output_path.exists(): + os.mkdir(output_path) + with open(output_path / 'model.sdf', 'w', encoding='UTF-8') as model: + model.write(template_sdf.render(map=_map)) + with open(output_path / 'model.config', 'w', encoding='UTF-8') as model: + author = {'name': 'MapDesc Generator', 'email': 'noone@example.com'} + model.write(template_cfg.render(map=_map, author=author)) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/save/svg.py b/lib/mapdesc_ros/mapdesc/mapdesc/save/svg.py new file mode 100644 index 0000000..53b7e35 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/save/svg.py @@ -0,0 +1,88 @@ +import math +from ..util import image_dimensions + + +SVG_TPL = ''' + + {items} + +''' +WALL_STYLE = 'fill:black;' +SVG_LINE = '' +SVG_CIRCLE = '' +LANE_STYLE = 'stroke:red;stroke-width:1' + + +def save_svg(_map, output_file): + width, height, offset_x, offset_y = image_dimensions(_map) + res = _map.resolution + + svg_data = SVG_TPL.format( + width=math.ceil(width / res), height=math.ceil(height / res), + offset_x=0, offset_y=0, + items='{items}') + + svg_items = [] + if _map.area: + svg_items.append('') + for area in _map.area: + color = area.color if area.color else [100, 100, 255] + style = 'fill:#{:02x}{:02x}{:02x};'.format(*color) + svg_poly = ''.format( + style=style, points=''.join([ + f'{round((p.x + offset_x) / res):.0f},' + f'{round((p.y + offset_y) / res):.0f} ' + for p in area.local_points()])) + svg_items.append(svg_poly) + else: + svg_items.append('') + + if _map.wall: + svg_items.append('') + for wall in _map.wall: + svg_poly = ''.format( + style=WALL_STYLE, points=''.join([ + f'{round((p.x + offset_x) / res):.0f},' + f'{round((p.y + offset_y) / res):.0f} ' + for p in wall.local_points()])) + svg_items.append(svg_poly) + else: + svg_items.append('') + + # TODO: path! + + if _map.lane_graph: + svg_items.append('') + for edge in _map.lane_graph.edges: + s = edge.source.position + t = edge.target.position + x1 = (s.x + offset_x) / res + y1 = (s.y + offset_y) / res + x2 = (t.x + offset_x) / res + y2 = (t.y + offset_y) / res + svg_items.append(SVG_LINE.format( + x1=x1, x2=x2, y1=y1, y2=y2, style=LANE_STYLE)) + else: + svg_items.append('') + + if _map.marker: + svg_items.append('') + for marker in _map.marker: + pos = marker.pose.position + x = int((pos.x + offset_x) / res) + y = int((pos.y + offset_y) / res) + color = marker.color if marker.color else [0, 0, 255] + stroke = '#{:02x}{:02x}{:02x}'.format(*color) + radius = (marker.radius if marker.radius else 1.0) / res + _style = 'fill:none' + svg_items.append(SVG_CIRCLE.format( + style=_style, cx=x, cy=y, r=radius, stroke_width=1, + stroke=stroke)) + else: + svg_items.append('') + + with open(output_file, 'w', encoding='utf-8') as fp: + fp.write(svg_data.format(items='\n '.join(svg_items))) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/save/yaml.py b/lib/mapdesc_ros/mapdesc/mapdesc/save/yaml.py new file mode 100644 index 0000000..f5f69e0 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/save/yaml.py @@ -0,0 +1,8 @@ +import yaml +from ..model import Map + + +def save_yaml(_map: Map, output_file: str): + with open(output_file, 'w', encoding='utf-8') as f: + yaml.safe_dump( + dict(_map), f, default_style=None, default_flow_style=None) diff --git a/lib/mapdesc_ros/mapdesc/mapdesc/util.py b/lib/mapdesc_ros/mapdesc/mapdesc/util.py new file mode 100644 index 0000000..886abe2 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/mapdesc/util.py @@ -0,0 +1,110 @@ +import math + + +def dot_product(p1, p2, p3): + """calculates the dot-product of two vectors from 3 points. + + Useful to determine if it is a right angle.""" + v1 = (p2.x - p1.x, p2.y - p1.y) + v2 = (p3.x - p2.x, p3.y - p2.y) + return v1[0] * v2[0] + v1[1] * v2[1] + + +def calculate_slope(p1, p2): + """calculate slope of line between 2 points.""" + if p1.x == p2.x: + return float('inf') # Use infinity to represent vertical lines + return (p2.y - p1.y) / (p2.x - p1.x) + + +def bounding_box(points: list) -> list: + """get bounding box around all given points.""" + x_min = min([p[0] for p in points]) + y_min = min([p[1] for p in points]) + x_max = max([p[0] for p in points]) + y_max = max([p[1] for p in points]) + return x_min, y_min, x_max, y_max + + +def ccw_sort(points: list): + """sort points counter-clockwise. + + We assume the coordinates are taken from the center and calculate the + degree of each point to order by angle. + """ + points_distance = [ + ( + math.atan2(p.x, p.y) % (math.pi * 2), + p + ) for p in points + ] + points_distance.sort(key=lambda x: x[0], reverse=True) + return [p[1] for p in points_distance] + + +def image_dimensions(_map) -> list: + """Get image dimensions for SVG and PNG export.""" + if _map.size.width > 1 and _map.size.length > 1: + return _map.size.width, _map.size.length, 0, 0 + else: + points = [p for w in _map.wall for p in w.local_points()] + if _map.marker: + # subtract and add radius to get top-left and bottom-right + # corner of the marker + points += [m.pose.position + m.radius for m in _map.marker] + points += [m.pose.position - m.radius for m in _map.marker] + if _map.lane_graph: + points += [n.position for n in _map.lane_graph.nodes] + if not points: + print('WARN: no obstacles given to calculate bounding box') + return 0, 0, 0, 0 + x_min, y_min, x_max, y_max = bounding_box(points) + # image size is calculated by the points, we set the position + # of the image by the top-left and bottom-right bounding box + # that the points form. Only works if we have at least 2 points. + width, height = x_max - x_min, y_max - y_min + # we move all points between min and max, so we need to move all + # points by that (shift all points by an offset so they are inside + # the image) + return width, height, -x_min, -y_min + + +def euler_to_quaternion(roll: float, pitch: float, yaw: float) -> set: + # roll (X), pitch (Y), yaw (Z), + # Abbreviations for the various angular functions based on + # https://en.wikipedia.org/wiki/ + # Conversion_between_quaternions_and_Euler_angles + cy = math.cos(yaw * 0.5) + sy = math.sin(yaw * 0.5) + cp = math.cos(pitch * 0.5) + sp = math.sin(pitch * 0.5) + cr = math.cos(roll * 0.5) + sr = math.sin(roll * 0.5) + + return ( + sr * cp * cy - cr * sp * sy, # x + cr * sp * cy + sr * cp * sy, # y + cr * cp * sy - sr * sp * cy, # z + cr * cp * cy + sr * sp * sy # w + ) + + +def quaternion_to_euler(x: float, y: float, z: float, w: float): + # roll (x-axis rotation) + sinr_cosp = 2 * (w * x + y * z) + cosr_cosp = 1 - 2 * (x * x + y * y) + roll = math.atan2(sinr_cosp, cosr_cosp) + + # pitch (y-axis rotation) + sinp = 2 * (w * y - z * x) + # math.asin has to be between -1 and 1, so we return 90°(π/2) as limit + sinp = max(-1, sinp) + sinp = min(1, sinp) + pitch = math.asin(sinp) + + # yaw (z-axis rotation) + siny_cosp = 2 * (w * z + x * y) + cosy_cosp = 1 - 2 * (y * y + z * z) + yaw = math.atan2(siny_cosp, cosy_cosp) + + return roll, pitch, yaw diff --git a/lib/mapdesc_ros/mapdesc/pytest.bash b/lib/mapdesc_ros/mapdesc/pytest.bash new file mode 100755 index 0000000..7b5da28 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/pytest.bash @@ -0,0 +1,5 @@ +#!/bin/bash +# run pytest and coverage +python3 -m coverage run --source mapdesc -m pytest . +python3 -m coverage report -m +python3 -m coverage html \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/requirements.txt b/lib/mapdesc_ros/mapdesc/requirements.txt new file mode 100644 index 0000000..bc137e2 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/requirements.txt @@ -0,0 +1,6 @@ +argcomplete +pyyaml +jinja2 +imutils +numpy>=1.24 +OSMPythonTools \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/setup.cfg b/lib/mapdesc_ros/mapdesc/setup.cfg new file mode 100644 index 0000000..ac4a813 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/setup.cfg @@ -0,0 +1,21 @@ +[bumpversion] +current_version = 0.1 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version='{current_version}' +replace = version='{new_version}' + +[bumpversion:file:mapdesc/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + +[bdist_wheel] +universal = 1 + +[flake8] +exclude = docs + +[aliases] +test = pytest \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/setup.py b/lib/mapdesc_ros/mapdesc/setup.py new file mode 100644 index 0000000..6e1d434 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/setup.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import os +from setuptools import setup, find_packages + + +def package_files(directory): + """add package_files for setup.""" + paths = [] + for (path, _, filenames) in os.walk(directory): + for filename in filenames: + paths.append(os.path.join('..', path, filename)) + return paths + + +EXTRA_FILES = package_files('mapdesc/data') +SHORT_DESC = "Map Description and format conversion for robotics applications." + + +with open("README.md", "r", encoding='utf-8') as fh: + long_description = fh.read() + +setup( + name="mapdesc", + description=SHORT_DESC, + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/dfki-ric/mapdesc", + version="0.1", + license="BSD-3", + author="Andreas Bresser", + packages=find_packages(), + tests_require=[], + include_package_data=True, + package_data={'': EXTRA_FILES}, + install_requires=[ + 'argcomplete', + 'jinja2', + 'pyyaml', + 'imutils', + 'numpy>=1.24', + 'OSMPythonTools' + ], + entry_points={ + 'console_scripts': [ + 'mapdesc = mapdesc.cli:main', + ], + }, +) diff --git a/lib/mapdesc_ros/mapdesc/test/geojson/geojson_drone.json b/lib/mapdesc_ros/mapdesc/test/geojson/geojson_drone.json new file mode 100644 index 0000000..5c3558f --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/geojson/geojson_drone.json @@ -0,0 +1,34 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + 8.85854569154364, + 53.11180309646721 + ], + [ + 8.85921278666325, + 53.1115805149382 + ], + [ + 8.859445451991974, + 53.1118074607991 + ], + [ + 8.858734732122826, + 53.11205950023157 + ], + [ + 8.858532967658363, + 53.11181509837945 + ] + ], + "type": "LineString" + } + } + ] + } \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/test/map/mallmap.png b/lib/mapdesc_ros/mapdesc/test/map/mallmap.png new file mode 100755 index 0000000000000000000000000000000000000000..28b9a37453adb199046f8f77ab430960b86e21e6 GIT binary patch literal 34636 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 literal 0 HcmV?d00001 diff --git a/lib/mapdesc_ros/mapdesc/test/map/mallmap.yaml b/lib/mapdesc_ros/mapdesc/test/map/mallmap.yaml new file mode 100644 index 0000000..56c0d3c --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/map/mallmap.yaml @@ -0,0 +1,6 @@ +image: mallmap.png +resolution: 0.05 +origin: [-20.5, -17.5, 0.0] +negate: 0 +occupied_thresh: 0.65 +free_thresh: 0.196 diff --git a/lib/mapdesc_ros/mapdesc/test/simple_walls_sdf/model.config b/lib/mapdesc_ros/mapdesc/test/simple_walls_sdf/model.config new file mode 100644 index 0000000..8750280 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/simple_walls_sdf/model.config @@ -0,0 +1,15 @@ + + + Simple Walls + 1.0 + model.sdf + + + MapDesc Generator + noone@example.com + + + + A simple room with 4 walls and an entrance for a door + + \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/test/simple_walls_sdf/model.sdf b/lib/mapdesc_ros/mapdesc/test/simple_walls_sdf/model.sdf new file mode 100644 index 0000000..6f0b626 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/simple_walls_sdf/model.sdf @@ -0,0 +1,128 @@ + + + + 0 0 0 0 0 0 + + + + + + 0.2 20 3 + + + 0 0 1.5 0 0 0 + + + + + 0.2 20 3 + + + 0 0 1.5 0 0 0 + + + 1 1 1 1 + + + 0 + + + -10 0 1.5 0 0 0 + + + + + + + 0.2 20 3 + + + 0 0 1.5 0 0 0 + + + + + 0.2 20 3 + + + 0 0 1.5 0 0 0 + + + 1 1 1 1 + + + 0 + + + 10 0 1.5 0.0 0.0 0.0 + + + + + + + 0.2 20 3 + + + 0 0 1.5 0 0 0 + + + + + 0.2 20 3 + + + 0 0 1.5 0 0 0 + + + 1 1 1 1 + + + 0 + + + 0 10 1.5 0.0 0.0 1.570494235572534 + + + + + + + 0.2 20 3 + + + 0 0 1.5 0 0 0 + + + + + 0.2 20 3 + + + 0 0 1.5 0 0 0 + + + 1 1 1 1 + + + 0 + + + 0 -10 1.5 0.0 0.0 1.570494235572534 + + + 1 + + \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/test/test_conversions.py b/lib/mapdesc_ros/mapdesc/test/test_conversions.py new file mode 100644 index 0000000..6f1a929 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_conversions.py @@ -0,0 +1,187 @@ +from mapdesc.model.geom import Vector2, Vector3, Quaternion, Dimension + + +def test_from_to_list(): + vec = Vector2(*[1, 2, 'ignore_me']) + assert vec.x == 1 + assert vec.y == 2 + assert list(vec) == [1.0, 2.0] + + vec = Vector3(*[1, 2, 3, 'ignore_me']) + assert vec.x == 1 + assert vec.y == 2 + assert vec.z == 3 + assert list(vec) == [1.0, 2.0, 3.0] + + quat = Quaternion(*[1, 2, 3, 4, 'ignore_me']) + assert quat.x == 1 + assert quat.y == 2 + assert quat.z == 3 + assert quat.w == 4 + assert list(quat) == [1.0, 2.0, 3.0, 4.0] + + dim = Dimension(*[1, 2, 3, 'ignore_me']) + assert dim.width == 1 + assert dim.length == 2 + assert dim.height == 3 + assert list(dim) == [1.0, 2.0, 3.0] + + +def test_null(): + num = [1, 2, 3, 4, 'ignore_me'] + for clz in [Vector2, Vector3, Quaternion]: + vec = clz(*num) + assert not vec.null() + vec = clz() + assert vec.null() + + dim = Dimension(*[1, 2, 3]) + assert not dim.null() + dim = Dimension() + assert not dim.null() + dim = Dimension(0, 0, 0) + assert dim.null() + + +def test_sub(): + num = [10, 9, 8, 7, 'ignore_me'] + + vec = Vector2(*num) - 3 + assert vec.x == 7 + assert vec.y == 6 + vec -= Vector2(1, 1) + assert vec.x == 6 + assert vec.y == 5 + + vec = Vector3(*num) - 2 + assert vec.x == 8 + assert vec.y == 7 + assert vec.z == 6 + vec -= Vector3(1, 1, 1) + assert vec.x == 7 + assert vec.y == 6 + assert vec.z == 5 + + quat = Quaternion(*num) - 2 + assert quat.x == 8 + assert quat.y == 7 + assert quat.z == 6 + assert quat.w == 5 + quat -= Quaternion(-1, 1, -2, -3) + assert quat.x == 9 + assert quat.y == 6 + assert quat.z == 8 + assert quat.w == 8 + + dim = Dimension(*num) - 2 + assert dim.width == 8 + assert dim.length == 7 + assert dim.height == 6 + dim -= Dimension(-2, -4, -6) + assert dim.width == 10 + assert dim.length == 11 + assert dim.height == 12 + dim -= dim + assert dim.null() + + +def test_mul(): + num = [1, 2, 3, 4, 'ignore_me'] + + vec = Vector2(*num) * 2 + assert vec.x == 2 + assert vec.y == 4 + vec *= vec + assert vec.x == 4 + assert vec.y == 16 + + vec = Vector3(*num) * 2 + assert vec.x == 2 + assert vec.y == 4 + assert vec.z == 6 + vec *= vec + assert vec.x == 4 + assert vec.y == 16 + assert vec.z == 36 + + quat = Quaternion(*num) * 2 + assert quat.x == 2 + assert quat.y == 4 + assert quat.z == 6 + assert quat.w == 8 + quat *= quat + assert quat.x == 4 + assert quat.y == 16 + assert quat.z == 36 + assert quat.w == 64 + + dim = Dimension(*num) * 2 + assert dim.width == 2 + assert dim.length == 4 + assert dim.height == 6 + dim *= dim + assert dim.width == 4 + assert dim.length == 16 + assert dim.height == 36 + + +def test_add(): + num = [1, 2, 3, 4, 'ignore_me'] + + vec = Vector2(*num) + 2 + assert vec.x == 3 + assert vec.y == 4 + vec += vec + assert vec.x == 6 + assert vec.y == 8 + + vec = Vector3(*num) + 4 + assert vec.x == 5 + assert vec.y == 6 + assert vec.z == 7 + vec += vec + assert vec.x == 10 + assert vec.y == 12 + assert vec.z == 14 + + quat = Quaternion(*num) + 6 + assert quat.x == 7 + assert quat.y == 8 + assert quat.z == 9 + assert quat.w == 10 + quat += quat + assert quat.x == 14 + assert quat.y == 16 + assert quat.z == 18 + assert quat.w == 20 + + dim = Dimension(*num) + 2 + assert dim.width == 3 + assert dim.length == 4 + assert dim.height == 5 + dim += dim + assert dim.width == 6 + assert dim.length == 8 + assert dim.height == 10 + + +def test_neg(): + vec = -Vector2(*[1, 2, 'ignore_me']) + assert vec.x == -1 + assert vec.y == -2 + + vec = -Vector3(*[1, 2, 3, 'ignore_me']) + assert vec.x == -1 + assert vec.y == -2 + assert vec.z == -3 + + quat = -Quaternion(*[1, 2, 3, 4, 'ignore_me']) + assert quat.x == -1 + assert quat.y == -2 + assert quat.z == -3 + assert quat.w == -4 + + dim = -Dimension(*[1, 2, 3, 'ignore_me']) + assert dim.width == -1 + assert dim.length == -2 + assert dim.height == -3 diff --git a/lib/mapdesc_ros/mapdesc/test/test_dimension.py b/lib/mapdesc_ros/mapdesc/test/test_dimension.py new file mode 100644 index 0000000..8c982f1 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_dimension.py @@ -0,0 +1,8 @@ +from mapdesc.model.geom import Dimension + + +def test_dimension(): + dim = Dimension() + assert dim.length == 1 + assert dim.height == 1 + assert dim.width == 1 diff --git a/lib/mapdesc_ros/mapdesc/test/test_lanes.py b/lib/mapdesc_ros/mapdesc/test/test_lanes.py new file mode 100644 index 0000000..f1af573 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_lanes.py @@ -0,0 +1,23 @@ +from mapdesc.model import LaneGraph + + +def test_lane_from_dict(): + graph_data = { + 'nodes': [ + {'position': [1, 1, 1]}, + {'position': [2, 2, 2]}, + {'position': [3, 3, 3]} + ], + 'edges': [ + {'edge_type': 0, 'source': 0, 'target': 1}, + {'edge_type': 0, 'source': 1, 'target': 2} + ] + } + graph = LaneGraph(**graph_data) + assert graph.nodes[0].position.x == 1 + assert graph.edges[1].target.name == 'node_2' + idx = 0 + for node in graph_data['nodes']: + node['name'] = f'node_{idx}' + idx += 1 + assert dict(graph) == graph_data diff --git a/lib/mapdesc_ros/mapdesc/test/test_load_save.py b/lib/mapdesc_ros/mapdesc/test/test_load_save.py new file mode 100644 index 0000000..70bec88 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_load_save.py @@ -0,0 +1,38 @@ +from mapdesc.load.rosmap import load_rosmap +from mapdesc.load.yaml import load_yaml +from mapdesc.save import save_sdf, save_png, save_svg, save_rosmap +from pathlib import Path + + +def test_gen_sdf(): + _map = load_rosmap('./test/map/mallmap.yaml') + assert _map.wall + save_sdf(_map, './generated/sdf/mallmap_unittest_sdf1') + + +def test_gen_sdf2(): + _map = load_yaml('./test/yaml/simple_walls.yaml') + save_sdf(_map, './generated/sdf/mallmap_unittest_sdf2') + + +def test_gen_png(): + mall_map = load_rosmap('./test/map/mallmap.yaml') + assert mall_map.wall + save_png(mall_map, './generated/mallmap_unittest_png.png') + + hdp_map = load_yaml('./test/yaml/hdp_2_agents_map.yml') + assert hdp_map.wall + save_png(hdp_map, './generated/hdp_unittest_png.png') + + +def test_gen_svg(): + _map = load_yaml('./test/yaml/hdp_2_agents_map.yml') + save_svg(_map, './generated/hdp2_unittest_svg.svg') + + +def test_rosmap(): + _map = load_yaml('./test/yaml/hdp_2_agents_map.yml') + save_rosmap(_map, './generated/fail.test') + save_rosmap(_map, './generated/hdp_rosmap.yaml') + assert Path('./generated/hdp_rosmap.png').exists() + assert Path('./generated/hdp_rosmap.yaml').exists() diff --git a/lib/mapdesc_ros/mapdesc/test/test_map.py b/lib/mapdesc_ros/mapdesc/test/test_map.py new file mode 100644 index 0000000..5f59292 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_map.py @@ -0,0 +1,7 @@ +from mapdesc.load.yaml import load_yaml + + +def test_map_to_dict(): + _map = load_yaml('./test/yaml/hdp_2_agents_map.yml') + dict_map = dict(_map) + assert len(dict_map['wall']) == 15 diff --git a/lib/mapdesc_ros/mapdesc/test/test_mesh.py b/lib/mapdesc_ros/mapdesc/test/test_mesh.py new file mode 100644 index 0000000..d8b3c30 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_mesh.py @@ -0,0 +1,19 @@ +from mapdesc.model.geom.mesh import Mesh + + +def test_recenter(): + mesh = Mesh( + polygons=[ + (1, 2), + (11, 2), + (11, 11), + (1, 11) + ] + ) + mesh.recenter() + assert mesh.pose.position.x == 6 + assert mesh.pose.position.y == 6.5 + assert mesh.pose.position.z == 0 + + assert mesh.polygons[0].x == -5 + assert mesh.polygons[0].y == -4.5 diff --git a/lib/mapdesc_ros/mapdesc/test/test_plane.py b/lib/mapdesc_ros/mapdesc/test/test_plane.py new file mode 100644 index 0000000..de9aaed --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_plane.py @@ -0,0 +1,11 @@ +from mapdesc.model.geom import Dimension, Plane + + +def test_plane(): + plane = Plane( + normal=[1, 1, 1], + size=[2, 2]) + assert isinstance(plane.size, Dimension) + assert plane.size.height == 0 + plane_dict = dict(plane) + assert plane_dict['size'] == (2, 2) diff --git a/lib/mapdesc_ros/mapdesc/test/test_pose.py b/lib/mapdesc_ros/mapdesc/test/test_pose.py new file mode 100644 index 0000000..da80d49 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_pose.py @@ -0,0 +1,60 @@ +import math +import pytest +from mapdesc.model.geom import Pose, Vector2, Vector3, Quaternion +from mapdesc.model.geom.pose import any_to_vector, dict_to_vector + + +def test_euler_orientation(): + p = Pose(orientation=Vector3( + math.pi / 180 * 90, + math.pi / 180 * 45, + math.pi / 180 * 180)) + euler = p.euler_orientation() + assert tuple(euler) == (math.pi / 2, math.pi / 4, math.pi) + # check caching + euler = p.euler_orientation() + assert tuple(euler) == (math.pi / 2, math.pi / 4, math.pi) + + p = Pose(orientation=Quaternion(0, 0, 0, 1)) + euler = p.euler_orientation() + assert tuple(euler) == (0, 0, 0) + + p = Pose(orientation=Quaternion(1, 0, 0, 0)) + euler = p.euler_orientation() + assert tuple(euler) == (math.pi, 0, 0) + + p = Pose(orientation=Quaternion(0.707, 0, 0, 0.707)) + euler = p.euler_orientation() + assert tuple(euler) == (pytest.approx(math.pi / 2, .001), 0, 0) + + +def test_pose_dict(): + p = Pose() + # test post-init + assert isinstance(p.orientation, Quaternion) + # test __iter__ + assert '_euler_orientation' not in dict(p) + assert 'orientation' in dict(p) + # note that dict("custom_object") calls __iter__() on all nested + # structures (like position and orientation). + assert dict(p)['orientation'] == (0, 0, 0, 1) + + +def test_any_to_vector(): + my_dict = {'x': 1, 'y': 2} + vec2 = any_to_vector(my_dict) + assert isinstance(vec2, Vector2) + + my_dict['z'] = 3 + vec3 = any_to_vector(my_dict) + assert isinstance(vec3, Vector3) + + my_dict['w'] = 4 + quat = any_to_vector(my_dict) + assert isinstance(quat, Quaternion) + + my_dict = {'x': 1} + with pytest.raises(RuntimeError) as exc_info: + dict_to_vector(my_dict) + err_str = 'x and y not set, not a valid pose.' + assert exc_info.value.args[0] == err_str diff --git a/lib/mapdesc_ros/mapdesc/test/test_quaternion.py b/lib/mapdesc_ros/mapdesc/test/test_quaternion.py new file mode 100644 index 0000000..173548d --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_quaternion.py @@ -0,0 +1,99 @@ +from mapdesc.model.geom import Box, Dimension, Pose, \ + Quaternion, Vector2, Mesh +import pytest + + +def test_from_any(): + original = Quaternion(1, 2, 3, 1) + vec = Quaternion.from_any([1, 2, 3, 1]) + assert vec.to_tuple() == original.to_tuple() + vec = Quaternion.from_any({'x': 1, 'y': 2, 'z': 3, 'w': 1}) + assert vec.to_tuple() == original.to_tuple() + try: + Quaternion.from_any('invalid') + assert False + except RuntimeError: + pass + + +def test_rotaion(): + # test to rotate a mesh + # 1. rotate simple box by 45 degree [] + box = Box( + size=Dimension( + width=2.0, length=5.0, height=1.0 + ), + pose=Pose( + # roate by 45 degree + orientation=Quaternion(z=0.383, w=0.924) + ) + ) + # apply roation + points = box.local_points() + points = [round(p, 3) for p in points] + assert Vector2(1.062, -2.474) in points + assert Vector2(2.475, -1.059) in points + assert Vector2(-1.062, 2.474) in points + assert Vector2(-2.475, 1.059) in points + + # 2. rotate complex shape by 23 degree [] + polygons = [ + Vector2(-20, -30), + Vector2(10, -40), + Vector2(50, -20), + Vector2(100, 60), + Vector2(-70, 10), + Vector2(-10, 0) + ] + + mesh = Mesh( + polygons=polygons, + pose=Pose( + position=Vector2(80, 100), + # roate by 23 + orientation=Quaternion(z=0.2, w=0.98) + )) + points = mesh.local_points() + points = [round(p, 3) for p in points] + assert points == [ + Vector2(x=73.355, y=64.562), + Vector2(x=104.874, y=67.117), + Vector2(x=133.838, y=101.192), + Vector2(x=148.493, y=194.386), + Vector2(x=11.679, y=81.771), + Vector2(x=70.8, y=96.082)] + + +def test_getitem(): + q = Quaternion(x=3, y=4, z=5, w=6) + assert q['w'] == 6 + assert q[2] == 5 + assert q[0] == q['x'] == 3 + assert q[1] == 4 + with pytest.raises(RuntimeError) as exc_info: + assert not q['a'] # a should not be a valid subscription + assert exc_info.value.args[0] == 'unknown key "a"' + with pytest.raises(RuntimeError) as exc_info: + assert q[-1] + assert exc_info.value.args[0] == 'unknown key "-1"' + + +def test_copy(): + q = Quaternion(x=3, y=4, z=5, w=6) + q2 = q.copy() + q2.x = 1 + q2.y = 2 + q2.z = 3 + q2.w = 4 + assert q.x == 3 + assert q.y == 4 + assert q.z == 5 + assert q.w == 6 + + +def test_normalize(): + vec = Quaternion(0, 0, 0, 0) + with pytest.raises(RuntimeError) as exc_info: + vec.normalize() + assert exc_info.value.args[0].startswith( + 'Can not normalize null quaternion') diff --git a/lib/mapdesc_ros/mapdesc/test/test_serialization.py b/lib/mapdesc_ros/mapdesc/test/test_serialization.py new file mode 100644 index 0000000..46d537e --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_serialization.py @@ -0,0 +1,10 @@ +import json +from mapdesc.model import Marker + + +def test_to_json(): + model = Marker() + assert json.dumps(dict(model)) == '{"name": "new marker", '\ + '"pose": {"orientation": [0.0, 0.0, 0.0, 1.0], "position": '\ + '[0.0, 0.0, 0.0]}, "color": [255, 50, 50], "radius": 1.0, '\ + '"type": "point"}' diff --git a/lib/mapdesc_ros/mapdesc/test/test_utils.py b/lib/mapdesc_ros/mapdesc/test/test_utils.py new file mode 100644 index 0000000..575f4f5 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_utils.py @@ -0,0 +1,35 @@ +import math +from mapdesc import util +from mapdesc.model.geom import Quaternion, Vector2, Mesh +import pytest + + +def test_quaternion(): + assert \ + (0.0, 0.0, 0.707107, 0.707107) == \ + pytest.approx(util.euler_to_quaternion(0, 0, math.pi / 180 * 90)) + + quat = Quaternion(0, 0, 0.707107, 0.707107) + euler = quat.to_euler() + assert euler.x == 0 + assert euler.y == 0 + assert euler.z == pytest.approx(math.pi / 180 * 90) + + +def test_sort_points(): + # these points are clockwise sort + cw_points = [Vector2(x, y) for x, y in [[0, 0], [0, 1], [1, 0], [1, 1]]] + # these are counter clockwise + # TODO: check, if this is correct, we probably don't want to go + # from 0.0 to 0.1! + ccw_points = [Vector2(x, y) for x, y in [[1, 0], [1, 1], [0, 0], [0, 1]]] + assert util.ccw_sort(cw_points) == ccw_points + + mesh = Mesh(polygons=cw_points) + mesh.ccw_sort() + assert mesh.polygons == ccw_points + + +def test_bounding_box(): + points = [[1, 2], [-3, 99], [-6, -100]] + assert util.bounding_box(points) == (-6, -100, 1, 99) diff --git a/lib/mapdesc_ros/mapdesc/test/test_vec2.py b/lib/mapdesc_ros/mapdesc/test/test_vec2.py new file mode 100644 index 0000000..b96e433 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_vec2.py @@ -0,0 +1,60 @@ +from mapdesc.model.geom import Vector2 +import json + + +def test_is_close(): + a = Vector2(1, 2) + b = Vector2(3, 4) + assert not a.is_close(b) + assert not a.is_close(None) + assert not a.is_close({'x': 3, 'y': 5}) + assert not a.is_close({3, 5}) + assert a.is_close({3, 5}, threshold=10) + + +def test_distance(): + a = Vector2(1, 2) + b = Vector2(3, 4) + assert a.distance(b) == 2.8284271247461903 + + +def test_normalize(): + a = Vector2(1, 2).normalize() + # we are using the euclidean length, so we do not normalize based on + # min and max (so the solution is not 0.5, 1.0) + assert a.x == 0.4472135954999579 + assert a.y == 0.8944271909999159 + + +def test_serialize(): + a = Vector2(1, 2) + assert "[1.0, 2.0]" == json.dumps(a.serialize()) + + +def test_from_any(): + original = Vector2(1, 2) + vec = Vector2.from_any([1, 2]) + assert vec.to_tuple() == original.to_tuple() + vec = Vector2.from_any({'x': 1, 'y': 2}) + assert vec.to_tuple() == original.to_tuple() + try: + Vector2.from_any('invalid') + assert False + except RuntimeError: + pass + + +def test_copy(): + vec = Vector2(x=3, y=4) + vec2 = vec.copy() + vec2.x = 1 + vec2.y = 2 + assert vec.x == 3 + assert vec.y == 4 + + +def test_round(): + vec = Vector2(1.1111111, 2.22222222) + vec2 = round(vec, 2) + assert vec.x == 1.1111111 + assert vec2 == Vector2(1.11, 2.22) diff --git a/lib/mapdesc_ros/mapdesc/test/test_vec3.py b/lib/mapdesc_ros/mapdesc/test/test_vec3.py new file mode 100644 index 0000000..972359d --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_vec3.py @@ -0,0 +1,56 @@ +from mapdesc.model.geom import Vector3 + + +def test_is_close(): + a = Vector3(1, 2, 5) + b = Vector3(3, 4, 6) + assert not a.is_close(b) + assert not a.is_close(None) + assert not a.is_close({'x': 3, 'y': 5, 'z': 8}) + assert not a.is_close({3, 5, 6}) + assert a.is_close({3, 5, 6}, threshold=10) + assert not a.is_close({3, 11, 10}, threshold=10) + + +def test_to_tuple(): + a = Vector3(1, 2, 5) + # check inherited to_tuple + assert a.to_tuple() == tuple((1.0, 2.0, 5.0)) + + +def test_normalize(): + a = Vector3(1, 2, 3).normalize() + assert a.x == 0.2672612419124244 + assert a.y == 0.5345224838248488 + assert a.z == 0.8017837257372732 + + +def test_from_any(): + original = Vector3(1, 2, 3) + vec = Vector3.from_any([1, 2, 3]) + assert vec.to_tuple() == original.to_tuple() + vec = Vector3.from_any({'x': 1, 'y': 2, 'z': 3}) + assert vec.to_tuple() == original.to_tuple() + try: + Vector3.from_any('invalid') + assert False + except RuntimeError: + pass + + +def test_copy(): + vec = Vector3(x=3, y=4, z=5) + vec2 = vec.copy() + vec2.x = 1 + vec2.y = 2 + vec2.z = 3 + assert vec.x == 3 + assert vec.y == 4 + assert vec.z == 5 + + +def test_round(): + vec = Vector3(1.1111111, 2.22222222, 3.333333) + vec2 = round(vec, 2) + assert vec.x == 1.1111111 + assert vec2 == Vector3(1.11, 2.22, 3.33) diff --git a/lib/mapdesc_ros/mapdesc/test/test_wall.py b/lib/mapdesc_ros/mapdesc/test/test_wall.py new file mode 100644 index 0000000..d9f443a --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_wall.py @@ -0,0 +1,15 @@ +from mapdesc import model + + +def test_wall_box(): + wall = model.Wall(data=model.geom.Box()) + # assure a default length is set + assert wall.data.size.length == 1.0 + # assure the type is the given box + assert wall.type == 'box' + + +def test_wall_mesh(): + wall = model.Wall(data=model.geom.Mesh()) + assert wall.type == 'mesh' + assert wall.data.size.width == 1.0 diff --git a/lib/mapdesc_ros/mapdesc/test/test_yaml_import.py b/lib/mapdesc_ros/mapdesc/test/test_yaml_import.py new file mode 100644 index 0000000..58cef88 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/test_yaml_import.py @@ -0,0 +1,20 @@ +import os +from pathlib import Path +from mapdesc import load + + +BASE_PATH = Path(os.path.dirname(__file__)).absolute() + + +def test_load_yaml(): + _map = load.load_yaml( + input_file=BASE_PATH / 'yaml' / 'simple_walls.yaml') + assert len(_map.wall) == 4 + assert _map.wall[0].pose.position.x == -10 + + # Hi Digit Pro 4.0 production map as complex example + _map = load.load_yaml( + input_file=BASE_PATH / 'yaml' / 'hdp_2_agents_map.yml') + assert len(_map.area) == 7 + assert len(_map.lane_graph.edges) == 74 + assert len(_map.lane_graph.nodes) == 23 diff --git a/lib/mapdesc_ros/mapdesc/test/yaml/demonstrator_map.yml b/lib/mapdesc_ros/mapdesc/test/yaml/demonstrator_map.yml new file mode 100644 index 0000000..a5f0a53 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/yaml/demonstrator_map.yml @@ -0,0 +1,274 @@ +area: + # Demonstrator Area + - name: demonstation_area + color: [255, 200, 200] + type: mesh + data: + polygons: + - [-34, 5] + - [21, 5] + - [21, 23] + - [-34, 23] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + + # Storage Areas + - name: components_storage + color: [100, 100, 255] + type: mesh + data: + polygons: + - [6, -25] + - [40, -25] + - [40, -8] + - [6, -8] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + + - name: parts_storage + color: [255, 165, 0] + type: mesh + data: + polygons: + - [-16, -25] + - [5, -25] + - [5, -8] + - [-16, -8] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + + # Arrival Areas + - name: components_arrival_area + color: [173, 216, 230] + type: mesh + data: + polygons: + - [21, 5] + - [38, 5] + - [38, 23] + - [21, 23] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + + - name: delivery_drop_off_area + color: [173, 216, 230] + type: mesh + data: + polygons: + - [-36, -23] + - [-21, -23] + - [-21, -10] + - [-36, -10] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + + + # Tools and Parts Boxes + - name: tools_box + color: [0, 0, 139] + type: mesh + data: + polygons: + - [22, 21] + - [24, 21] + - [24, 23] + - [22, 23] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + + - name: parts_box + color: [0, 100, 0] + type: mesh + data: + polygons: + - [25, 21] + - [28, 21] + - [28, 23] + - [25, 23] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + +wall: + # Main Walls + # Main Wall 00 + - data: + polygons: + - [40, -25] + - [41, -25] + - [41, 25] + - [40, 25] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Main Wall 01 + - data: + polygons: + - [-41, 25] + - [41, 25] + - [41, 26] + - [-41, 26] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Main Wall 02 + - data: + polygons: + - [-41, 23] + - [-40, 23] + - [-40, 25] + - [-41, 25] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Main Wall 03 + - data: + polygons: + - [-41, -13] + - [-40, -13] + - [-40, 5] + - [-41, 5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Main Wall 04 + - data: + polygons: + - [-41, -25] + - [-40, -25] + - [-40, -23] + - [-41, -23] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Main Wall 05 + - data: + polygons: + - [-41, -26] + - [41, -26] + - [41, -25] + - [-41, -25] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Components Storage + # Wall 00 + - data: + polygons: + - [38, -8] + - [40, -8] + - [40, -7] + - [38, -7] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Wall 01 + - data: + polygons: + - [5, -8] + - [21, -8] + - [21, -7] + - [5, -7] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Wall 02 + - data: + polygons: + - [5, -25] + - [6, -25] + - [6, -7] + - [5, -7] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Parts Storage + # Wall 00 + - data: + polygons: + - [3, -8] + - [5, -8] + - [5, -7] + - [3, -7] + + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Wall 01 + - data: + polygons: + - [-16, -8] + - [-5, -8] + - [-5, -7] + - [-16, -7] + + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Wall 02 + - data: + polygons: + - [-17, -25] + - [-16, -25] + - [-16, -7] + - [-17, -7] + + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Demonstation Walls + # Wall bottom + - data: + polygons: + - [-34, 22] + - [21, 22] + - [21, 23] + - [-34, 23] + + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh + + # Wall top + - data: + polygons: + - [-34, 5] + - [21, 5] + - [21, 6] + - [-34, 6] + + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/test/yaml/hdp_2_agents_map.yml b/lib/mapdesc_ros/mapdesc/test/yaml/hdp_2_agents_map.yml new file mode 100644 index 0000000..44fa807 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/yaml/hdp_2_agents_map.yml @@ -0,0 +1,469 @@ +area: +- area_type: storage + color: [100, 255, 100] + data: + polygons: + - [-34.375, 0.0] + - [-15.625, 0.0] + - [-15.625, 12.5] + - [-34.375, 12.5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] +- area_type: storage + color: [100, 255, 100] + data: + polygons: + - [-34.375, -12.5] + - [-15.625, -12.5] + - [-15.625, 0.0] + - [-34.375, 0.0] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] +- area_type: storage + color: [100, 255, 100] + data: + polygons: + - [15.625, -12.5] + - [34.375, -12.5] + - [34.375, 12.5] + - [15.625, 12.5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] +- area_type: assembly + color: [100, 100, 255] + data: + polygons: + - [2.5, 2.5] + - [10.0, 2.5] + - [10.0, 10.0] + - [2.5, 10.0] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] +- area_type: assembly + color: [100, 100, 255] + data: + polygons: + - [-10.0, 2.5] + - [-2.5, 2.5] + - [-2.5, 10.0] + - [-10.0, 10.0] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] +- area_type: assembly + color: [100, 100, 255] + data: + polygons: + - [-10.0, -10.0] + - [-2.5, -10.0] + - [-2.5, -2.5] + - [-10.0, -2.5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] +- area_type: assembly + color: [100, 100, 255] + data: + polygons: + - [2.5, -10.0] + - [10.0, -10.0] + - [10.0, -2.5] + - [2.5, -2.5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] +description: Some map. +lane_graph: + edges: + - {edge_type: 1, source: 0, target: 20} + - {edge_type: 1, source: 0, target: 4} + - {edge_type: 1, source: 0, target: 15} + - {edge_type: 1, source: 0, target: 3} + - {edge_type: 1, source: 0, target: 2} + - {edge_type: 1, source: 0, target: 1} + - {edge_type: 1, source: 1, target: 8} + - {edge_type: 1, source: 1, target: 18} + - {edge_type: 1, source: 1, target: 0} + - {edge_type: 1, source: 1, target: 5} + - {edge_type: 1, source: 1, target: 13} + - {edge_type: 1, source: 2, target: 0} + - {edge_type: 1, source: 2, target: 5} + - {edge_type: 1, source: 2, target: 6} + - {edge_type: 1, source: 2, target: 20} + - {edge_type: 1, source: 3, target: 0} + - {edge_type: 1, source: 3, target: 7} + - {edge_type: 1, source: 3, target: 22} + - {edge_type: 1, source: 3, target: 6} + - {edge_type: 1, source: 3, target: 17} + - {edge_type: 1, source: 4, target: 0} + - {edge_type: 1, source: 4, target: 8} + - {edge_type: 1, source: 4, target: 15} + - {edge_type: 1, source: 4, target: 7} + - {edge_type: 1, source: 5, target: 18} + - {edge_type: 1, source: 5, target: 2} + - {edge_type: 1, source: 5, target: 1} + - {edge_type: 1, source: 5, target: 9} + - {edge_type: 1, source: 6, target: 22} + - {edge_type: 1, source: 6, target: 3} + - {edge_type: 1, source: 6, target: 2} + - {edge_type: 1, source: 6, target: 10} + - {edge_type: 1, source: 7, target: 3} + - {edge_type: 1, source: 7, target: 4} + - {edge_type: 1, source: 7, target: 12} + - {edge_type: 1, source: 7, target: 17} + - {edge_type: 1, source: 8, target: 4} + - {edge_type: 1, source: 8, target: 11} + - {edge_type: 1, source: 8, target: 13} + - {edge_type: 1, source: 8, target: 1} + - {edge_type: 1, source: 9, target: 5} + - {edge_type: 1, source: 10, target: 6} + - {edge_type: 1, source: 11, target: 8} + - {edge_type: 1, source: 11, target: 12} + - {edge_type: 1, source: 12, target: 11} + - {edge_type: 1, source: 12, target: 7} + - {edge_type: 1, source: 13, target: 8} + - {edge_type: 1, source: 13, target: 14} + - {edge_type: 1, source: 13, target: 1} + - {edge_type: 1, source: 14, target: 15} + - {edge_type: 1, source: 14, target: 13} + - {edge_type: 1, source: 15, target: 0} + - {edge_type: 1, source: 15, target: 4} + - {edge_type: 1, source: 15, target: 14} + - {edge_type: 1, source: 15, target: 16} + - {edge_type: 1, source: 16, target: 15} + - {edge_type: 1, source: 16, target: 17} + - {edge_type: 1, source: 17, target: 3} + - {edge_type: 1, source: 17, target: 16} + - {edge_type: 1, source: 17, target: 7} + - {edge_type: 1, source: 18, target: 5} + - {edge_type: 1, source: 18, target: 19} + - {edge_type: 1, source: 18, target: 1} + - {edge_type: 1, source: 19, target: 20} + - {edge_type: 1, source: 19, target: 18} + - {edge_type: 1, source: 20, target: 0} + - {edge_type: 1, source: 20, target: 19} + - {edge_type: 1, source: 20, target: 2} + - {edge_type: 1, source: 20, target: 21} + - {edge_type: 1, source: 21, target: 22} + - {edge_type: 1, source: 21, target: 20} + - {edge_type: 1, source: 22, target: 3} + - {edge_type: 1, source: 22, target: 6} + - {edge_type: 1, source: 22, target: 21} + nodes: + - name: node_1 + position: {x: 0.0, y: 0.0} + - name: node_2 + position: {x: 0.0, y: 11.25} + - name: node_3 + position: {x: -12.5, y: 0.0} + - name: node_4 + position: {x: 0.0, y: -11.25} + - name: node_5 + position: {x: 12.5, y: 0.0} + - name: node_6 + position: {x: -12.5, y: 11.25} + - name: node_7 + position: {x: -12.5, y: -11.25} + - name: node_8 + position: {x: 12.5, y: -11.25} + - name: node_9 + position: {x: 12.5, y: 11.25} + - name: node_10 + position: {x: -25.0, y: 6.25} + - name: node_11 + position: {x: -25.0, y: -6.25} + - name: node_12 + position: {x: 25.0, y: 6.25} + - name: node_13 + position: {x: 25.0, y: -6.25} + - name: node_14 + position: {x: 6.25, y: 11.25} + - name: node_15 + position: {x: 6.25, y: 6.25} + - name: node_16 + position: {x: 6.25, y: 0.0} + - name: node_17 + position: {x: 6.25, y: -6.25} + - name: node_18 + position: {x: 6.25, y: -11.25} + - name: node_19 + position: {x: -6.25, y: 11.25} + - name: node_20 + position: {x: -6.25, y: 6.25} + - name: node_21 + position: {x: -6.25, y: 0.0} + - name: node_22 + position: {x: -6.25, y: -6.25} + - name: node_23 + position: {x: -6.25, y: -11.25} +marker: +- color: [0, 0, 255] + name: actor_1 + params: {actor_type: wing_spawn, type: actor} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-29.6875, 3.125] +- color: [0, 0, 255] + name: actor_2 + params: {actor_type: wing_spawn, type: actor} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-29.6875, 6.25] +- color: [0, 0, 255] + name: actor_3 + params: {actor_type: wing_spawn, type: actor} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-29.6875, 9.375] +- color: [0, 0, 255] + name: actor_4 + params: {actor_type: movable_spawn, type: actor} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-29.6875, -9.375] +- color: [0, 0, 255] + name: actor_5 + params: {actor_type: movable_spawn, type: actor} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-29.6875, -6.25] +- color: [0, 0, 255] + name: actor_6 + params: {actor_type: movable_spawn, type: actor} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-29.6875, -3.125] +- color: [0, 0, 255] + name: actor_7 + params: {actor_type: despawn, type: actor} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [29.6875, 0.0] +- color: [255, 50, 50] + name: agent_1 + params: {agent_type: mir_100, type: agent_spawn} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0] +- color: [255, 50, 50] + name: agent_2 + params: {agent_type: mir_100, type: agent_spawn} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-1.25, 0.0] +- color: [255, 50, 50] + name: actor_3 + params: {agent_type: generic_arm, type: agent_spawn} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [8.125, 6.25] +- color: [255, 50, 50] + name: actor_4 + params: {agent_type: generic_arm, type: agent_spawn} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-8.125, 6.25] +- color: [255, 50, 50] + name: actor_5 + params: {agent_type: generic_arm, type: agent_spawn} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [-8.125, -6.25] +- color: [255, 50, 50] + name: actor_6 + params: {agent_type: generic_arm, type: agent_spawn} + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [8.125, -6.25] +name: some_graph +origin: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] +resolution: 0.05 +wall: +- data: + polygons: + - [2.375, 2.375] + - [5.125, 2.375] + - [5.125, 2.6250000000000004] + - [2.6250000000000004, 2.6250000000000004] + - [2.6250000000000004, 9.875] + - [5.125, 9.875] + - [5.125, 10.125] + - [2.375, 10.125] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [7.375, 2.375] + - [10.125, 2.375] + - [10.125, 10.125] + - [7.375, 10.125] + - [7.375, 9.875] + - [9.874999999999998, 9.875] + - [9.874999999999998, 2.6250000000000004] + - [7.375, 2.6250000000000004] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-10.125, 2.375] + - [-7.375, 2.375] + - [-7.375, 2.6250000000000004] + - [-9.874999999999998, 2.6250000000000004] + - [-9.874999999999998, 9.875] + - [-7.375, 9.875] + - [-7.375, 10.125] + - [-10.125, 10.125] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-5.125, 2.375] + - [-2.375, 2.375] + - [-2.375, 10.125] + - [-5.125, 10.125] + - [-5.125, 9.875] + - [-2.6250000000000004, 9.875] + - [-2.6250000000000004, 2.6250000000000004] + - [-5.125, 2.6250000000000004] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-10.125, -10.125] + - [-7.375, -10.125] + - [-7.375, -9.875] + - [-9.874999999999998, -9.875] + - [-9.874999999999998, -2.6250000000000004] + - [-7.375, -2.6250000000000004] + - [-7.375, -2.375] + - [-10.125, -2.375] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-5.125, -10.125] + - [-2.375, -10.125] + - [-2.375, -2.375] + - [-5.125, -2.375] + - [-5.125, -2.6250000000000004] + - [-2.6250000000000004, -2.6250000000000004] + - [-2.6250000000000004, -9.875] + - [-5.125, -9.875] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [2.375, -10.125] + - [5.125, -10.125] + - [5.125, -9.875] + - [2.6250000000000004, -9.875] + - [2.6250000000000004, -2.6250000000000004] + - [5.125, -2.6250000000000004] + - [5.125, -2.375] + - [2.375, -2.375] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [7.375, -10.125] + - [10.125, -10.125] + - [10.125, -2.375] + - [7.375, -2.375] + - [7.375, -2.6250000000000004] + - [9.874999999999998, -2.6250000000000004] + - [9.874999999999998, -9.875] + - [7.375, -9.875] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-34.375, 12.375] + - [34.375, 12.375] + - [34.375, 12.625] + - [-34.375, 12.625] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-34.375, -12.625] + - [34.375, -12.625] + - [34.375, -12.375] + - [-34.375, -12.375] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [34.25, -12.5] + - [34.5, -12.5] + - [34.5, 12.5] + - [34.25, 12.5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-34.5, -12.5] + - [-34.25, -12.5] + - [-34.25, 12.5] + - [-34.5, 12.5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-34.375, -0.125] + - [-15.625, -0.125] + - [-15.625, 0.125] + - [-34.375, 0.125] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [-15.75, -7.5] + - [-15.5, -7.5] + - [-15.5, 7.5] + - [-15.75, 7.5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh +- data: + polygons: + - [15.5, -7.5] + - [15.75, -7.5] + - [15.75, 7.5] + - [15.5, 7.5] + pose: + orientation: [0.0, 0.0, 0.0, 1.0] + position: [0.0, 0.0, 0.0] + type: mesh diff --git a/lib/mapdesc_ros/mapdesc/test/yaml/simple_walls.yaml b/lib/mapdesc_ros/mapdesc/test/yaml/simple_walls.yaml new file mode 100644 index 0000000..ee776dd --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test/yaml/simple_walls.yaml @@ -0,0 +1,27 @@ +name: "Simple Walls" +description: "A simple room with 4 walls and an entrance for a door" +wall: + - type: "box" + data: + pose: + position: [-10, 0, 1.5] + orientation: [0, 0, 0] # x y z - euler + size: [0.2, 20, 3] + - type: "box" + data: + pose: + position: [10, 0, 1.5] + orientation: [0, 0, 0, 1] # x y z w - quaternion + size: [0.2, 20, 3] + - type: "box" + data: + pose: + position: [0, 10, 1.5] + orientation: [0, 0, 0.707, 0.707] + size: [0.2, 20, 3] + - type: "box" + data: + pose: + position: [0, -10, 1.5] + orientation: [0, 0, 0.707, 0.707] + size: [0.2, 20, 3] \ No newline at end of file diff --git a/lib/mapdesc_ros/mapdesc/test_cli.bash b/lib/mapdesc_ros/mapdesc/test_cli.bash new file mode 100755 index 0000000..7f84603 --- /dev/null +++ b/lib/mapdesc_ros/mapdesc/test_cli.bash @@ -0,0 +1,11 @@ +#!/bin/bash +mapdesc yaml test/yaml/simple_walls.yaml sdf ./generated/sdf/test1 +mapdesc rosmap test/map/mallmap.yaml yaml ./generated/mallmap.yml +mapdesc rosmap test/map/mallmap.yaml yaml -b ./generated/mallmap_bounding_box.yml +mapdesc rosmap test/map/mallmap.yaml sdf ./generated/sdf/test2 +mapdesc rosmap test/map/mallmap.yaml png ./generated/test1.png +mapdesc yaml test/yaml/hdp_2_agents_map.yml rosmap ./generated/hdp_rosmap.yaml +mapdesc yaml --recenter test/yaml/demonstrator_map.yml yaml ./generated/demonstrator_map_recenter.yml +mapdesc yaml --boxify test/yaml/demonstrator_map.yml yaml ./generated/demonstrator_map_box.yml +# mapdesc osm 53.0762098 8.8075270 80 earth svg ./generated/bremen_city.svg +# mapdesc osm 53.0762098 8.8075270 80 earth yaml ./generated/bremen_city.yml \ No newline at end of file diff --git a/lib/mapdesc_msgs/CMakeLists.txt b/lib/mapdesc_ros/mapdesc_msgs/CMakeLists.txt similarity index 100% rename from lib/mapdesc_msgs/CMakeLists.txt rename to lib/mapdesc_ros/mapdesc_msgs/CMakeLists.txt diff --git a/lib/mapdesc_msgs/README.md b/lib/mapdesc_ros/mapdesc_msgs/README.md similarity index 100% rename from lib/mapdesc_msgs/README.md rename to lib/mapdesc_ros/mapdesc_msgs/README.md diff --git a/lib/mapdesc_msgs/msg/Area.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/Area.msg similarity index 100% rename from lib/mapdesc_msgs/msg/Area.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/Area.msg diff --git a/lib/mapdesc_msgs/msg/Box.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/Box.msg similarity index 100% rename from lib/mapdesc_msgs/msg/Box.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/Box.msg diff --git a/lib/mapdesc_msgs/msg/Dimension.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/Dimension.msg similarity index 100% rename from lib/mapdesc_msgs/msg/Dimension.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/Dimension.msg diff --git a/lib/mapdesc_msgs/msg/External.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/External.msg similarity index 100% rename from lib/mapdesc_msgs/msg/External.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/External.msg diff --git a/lib/mapdesc_msgs/msg/LaneEdge.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/LaneEdge.msg similarity index 100% rename from lib/mapdesc_msgs/msg/LaneEdge.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/LaneEdge.msg diff --git a/lib/mapdesc_msgs/msg/LaneGraph.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/LaneGraph.msg similarity index 100% rename from lib/mapdesc_msgs/msg/LaneGraph.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/LaneGraph.msg diff --git a/lib/mapdesc_msgs/msg/LaneNode.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/LaneNode.msg similarity index 100% rename from lib/mapdesc_msgs/msg/LaneNode.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/LaneNode.msg diff --git a/lib/mapdesc_msgs/msg/Map.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/Map.msg similarity index 100% rename from lib/mapdesc_msgs/msg/Map.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/Map.msg diff --git a/lib/mapdesc_msgs/msg/Marker.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/Marker.msg similarity index 100% rename from lib/mapdesc_msgs/msg/Marker.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/Marker.msg diff --git a/lib/mapdesc_msgs/msg/Mesh.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/Mesh.msg similarity index 100% rename from lib/mapdesc_msgs/msg/Mesh.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/Mesh.msg diff --git a/lib/mapdesc_msgs/msg/Path.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/Path.msg similarity index 100% rename from lib/mapdesc_msgs/msg/Path.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/Path.msg diff --git a/lib/mapdesc_msgs/msg/Wall.msg b/lib/mapdesc_ros/mapdesc_msgs/msg/Wall.msg similarity index 100% rename from lib/mapdesc_msgs/msg/Wall.msg rename to lib/mapdesc_ros/mapdesc_msgs/msg/Wall.msg diff --git a/lib/mapdesc_msgs/package.xml b/lib/mapdesc_ros/mapdesc_msgs/package.xml similarity index 100% rename from lib/mapdesc_msgs/package.xml rename to lib/mapdesc_ros/mapdesc_msgs/package.xml diff --git a/lib/mapdesc_msgs/srv/MapAreaCreate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapAreaCreate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapAreaCreate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapAreaCreate.srv diff --git a/lib/mapdesc_msgs/srv/MapAreaDelete.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapAreaDelete.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapAreaDelete.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapAreaDelete.srv diff --git a/lib/mapdesc_msgs/srv/MapAreaList.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapAreaList.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapAreaList.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapAreaList.srv diff --git a/lib/mapdesc_msgs/srv/MapAreaUpdate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapAreaUpdate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapAreaUpdate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapAreaUpdate.srv diff --git a/lib/mapdesc_msgs/srv/MapCreate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapCreate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapCreate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapCreate.srv diff --git a/lib/mapdesc_msgs/srv/MapDelete.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapDelete.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapDelete.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapDelete.srv diff --git a/lib/mapdesc_msgs/srv/MapExtCreate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapExtCreate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapExtCreate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapExtCreate.srv diff --git a/lib/mapdesc_msgs/srv/MapExtDelete.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapExtDelete.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapExtDelete.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapExtDelete.srv diff --git a/lib/mapdesc_msgs/srv/MapExtList.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapExtList.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapExtList.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapExtList.srv diff --git a/lib/mapdesc_msgs/srv/MapExtUpdate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapExtUpdate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapExtUpdate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapExtUpdate.srv diff --git a/lib/mapdesc_msgs/srv/MapGet.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapGet.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapGet.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapGet.srv diff --git a/lib/mapdesc_msgs/srv/MapList.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapList.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapList.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapList.srv diff --git a/lib/mapdesc_msgs/srv/MapMarkerCreate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapMarkerCreate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapMarkerCreate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapMarkerCreate.srv diff --git a/lib/mapdesc_msgs/srv/MapMarkerDelete.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapMarkerDelete.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapMarkerDelete.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapMarkerDelete.srv diff --git a/lib/mapdesc_msgs/srv/MapMarkerList.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapMarkerList.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapMarkerList.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapMarkerList.srv diff --git a/lib/mapdesc_msgs/srv/MapMarkerUpdate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapMarkerUpdate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapMarkerUpdate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapMarkerUpdate.srv diff --git a/lib/mapdesc_msgs/srv/MapOverwrite.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapOverwrite.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapOverwrite.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapOverwrite.srv diff --git a/lib/mapdesc_msgs/srv/MapPathCreate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapPathCreate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapPathCreate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapPathCreate.srv diff --git a/lib/mapdesc_msgs/srv/MapPathDelete.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapPathDelete.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapPathDelete.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapPathDelete.srv diff --git a/lib/mapdesc_msgs/srv/MapPathList.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapPathList.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapPathList.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapPathList.srv diff --git a/lib/mapdesc_msgs/srv/MapPathUpdate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapPathUpdate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapPathUpdate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapPathUpdate.srv diff --git a/lib/mapdesc_msgs/srv/MapUpdate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapUpdate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapUpdate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapUpdate.srv diff --git a/lib/mapdesc_msgs/srv/MapWallCreate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapWallCreate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapWallCreate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapWallCreate.srv diff --git a/lib/mapdesc_msgs/srv/MapWallDelete.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapWallDelete.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapWallDelete.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapWallDelete.srv diff --git a/lib/mapdesc_msgs/srv/MapWallList.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapWallList.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapWallList.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapWallList.srv diff --git a/lib/mapdesc_msgs/srv/MapWallUpdate.srv b/lib/mapdesc_ros/mapdesc_msgs/srv/MapWallUpdate.srv similarity index 100% rename from lib/mapdesc_msgs/srv/MapWallUpdate.srv rename to lib/mapdesc_ros/mapdesc_msgs/srv/MapWallUpdate.srv From 3e4ff22bb95109bcae53d1d7dd0536b60cf55b03 Mon Sep 17 00:00:00 2001 From: Adrian Auer Date: Sat, 3 Jan 2026 14:02:27 +0100 Subject: [PATCH 3/3] tryed to fix problem with no installaation of mapdesc --- docker/Dockerfile | 9 +++++++++ lib/mapdesc_ros/mapdesc_ros/setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 624634c..016ecb1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,9 +19,18 @@ COPY ./docker/entrypoint.bash /entrypoint.bash RUN chmod +x /entrypoint.bash ENTRYPOINT ["/entrypoint.bash"] + + COPY ./ ${COLCON_WS_SRC}/ricbot_navigation COPY ./lib/mapdesc_ros ${COLCON_WS_SRC}/mapdesc_ros +##### mapdesc installation - this should be done automatically with colcon if deps are defined correctly, but for some reason it is not +# RUN pip3 install -r ${COLCON_WS_SRC}/mapdesc_ros/mapdesc/requirements.txt + +# COPY ./lib/mapdesc_ros/mapdesc /opt/mapdesc +# WORKDIR /opt/mapdesc +# RUN pip3 install . +##### RUN cd ${COLCON_WS}\ && . /opt/ros/${ROS_DISTRO}/setup.sh\ diff --git a/lib/mapdesc_ros/mapdesc_ros/setup.py b/lib/mapdesc_ros/mapdesc_ros/setup.py index 6115554..f73ba44 100644 --- a/lib/mapdesc_ros/mapdesc_ros/setup.py +++ b/lib/mapdesc_ros/mapdesc_ros/setup.py @@ -17,7 +17,7 @@ os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*.launch.py'))), ], - install_requires=['setuptools'], + install_requires=['setuptools', 'mapdesc'], requires=['mapdesc'], zip_safe=True, maintainer='abresser',