cmake_minimum_required(VERSION 3.14.4)

project(mapviz_plugins
  LANGUAGES CXX)

if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 17)
endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

function(mapviz_plugins_ensure_interface_alias alias_target dependency_target)
  if(TARGET ${alias_target})
    return()
  endif()

  add_library(${alias_target} INTERFACE IMPORTED)
  set_target_properties(${alias_target} PROPERTIES
    INTERFACE_LINK_LIBRARIES ${dependency_target})
endfunction()

function(mapviz_plugins_ensure_opencv_component component)
  if(TARGET opencv_${component})
    return()
  endif()

  if(TARGET OpenCV::opencv_${component})
    mapviz_plugins_ensure_interface_alias(opencv_${component} OpenCV::opencv_${component})
    return()
  endif()

  if(TARGET PkgConfig::OpenCV)
    mapviz_plugins_ensure_interface_alias(opencv_${component} PkgConfig::OpenCV)
    return()
  endif()

  message(FATAL_ERROR "Unable to resolve OpenCV component target opencv_${component}")
endfunction()

set(MAPVIZ_PLUGINS_AUTOGEN_INCLUDE_DIR
  ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}_autogen/include)

find_package(ament_cmake REQUIRED)
find_package(ament_index_cpp REQUIRED)
find_package(cv_bridge REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(gps_msgs REQUIRED)
find_package(image_transport REQUIRED)

# image_transport (part of image_common) changed the ImageTransport
# constructor, and the create_subscription() free function, from taking
# rclcpp::Node::SharedPtr / rclcpp::Node* to taking rclcpp::Node& starting
# with image_common 6.4.0. Detect which API is available so the plugins can
# call the correct one.
if(image_transport_VERSION VERSION_LESS "6.4.0")
  set(MAPVIZ_PLUGINS_IMAGE_TRANSPORT_TAKES_NODE_REF OFF)
else()
  set(MAPVIZ_PLUGINS_IMAGE_TRANSPORT_TAKES_NODE_REF ON)
endif()

find_package(map_msgs REQUIRED)
find_package(mapviz REQUIRED)
find_package(marti_common_msgs REQUIRED)
find_package(marti_nav_msgs REQUIRED)
find_package(marti_sensor_msgs REQUIRED)
find_package(marti_visualization_msgs REQUIRED)
# Optional: nav2_msgs is not released for every distro (see package.xml).
find_package(nav2_msgs QUIET)
if(NOT nav2_msgs_FOUND)
  message(STATUS "nav2_msgs not found; the move_base plugin will not be built")
endif()
find_package(nav_msgs REQUIRED)
find_package(pluginlib REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(std_msgs REQUIRED)
find_package(stereo_msgs REQUIRED)
find_package(swri_image_util REQUIRED)
find_package(swri_math_util REQUIRED)
find_package(swri_route_util REQUIRED)
find_package(swri_transform_util REQUIRED)
find_package(tf2 REQUIRED)
find_package(tf2_geometry_msgs REQUIRED)
find_package(assimp REQUIRED)
find_package(urdf REQUIRED)
find_package(visualization_msgs REQUIRED)

# urdf renamed model.h to model.hpp in some distros (not yet in ROS Humble).
# Detect which header is actually available rather than assuming by distro.
include(CheckIncludeFileCXX)
set(CMAKE_REQUIRED_INCLUDES ${urdf_INCLUDE_DIRS})
check_include_file_cxx("urdf/model.hpp" MAPVIZ_PLUGINS_HAVE_URDF_MODEL_HPP)
unset(CMAKE_REQUIRED_INCLUDES)

### QT ###
set(QT_VERSION_MAJOR 5)
find_package(Qt5 QUIET
  COMPONENTS
    Core
    Gui
    OpenGL
    Widgets
)
if(NOT Qt5_FOUND)
  set(QT_VERSION_MAJOR 6)
  find_package(Qt6 REQUIRED
    COMPONENTS
      Core
      Gui
      OpenGL
      OpenGLWidgets
      Widgets
  )
endif()

find_package(OpenCV QUIET CONFIG COMPONENTS core imgcodecs imgproc)
if(NOT OpenCV_FOUND)
  find_package(PkgConfig REQUIRED)
  pkg_check_modules(OpenCV REQUIRED IMPORTED_TARGET opencv4)
endif()
foreach(component IN ITEMS core imgcodecs imgproc)
  mapviz_plugins_ensure_opencv_component(${component})
endforeach()

# Need to include the current dir to include the results of building Qt UI files
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOUIC_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/ui)
set(UI_FILES
  ui/attitude_indicator_config.ui
  ui/coordinate_picker_config.ui
  ui/disparity_config.ui
  ui/draw_marker_config.ui
  ui/draw_polygon_config.ui
  ui/float_config.ui
  ui/gps_config.ui
  ui/grid_config.ui
  ui/image_config.ui
  ui/laserscan_config.ui
  ui/marker_config.ui
  ui/measuring_config.ui
  ui/navsat_config.ui
  ui/occupancy_grid_config.ui
  ui/odometry_config.ui
  ui/path_config.ui
  ui/plan_route_config.ui
  ui/point_click_publisher_config.ui
  ui/pointcloud2_config.ui
  ui/pose_config.ui
  ui/robot_image_config.ui
  ui/route_config.ui
  ui/speedometer_config.ui
  ui/string_config.ui
  ui/textured_marker_config.ui
  ui/robot_model_config.ui
  ui/tf_frame_config.ui
  ui/topicselect.ui)

set(SRC_FILES
  src/attitude_indicator_plugin.cpp
  src/canvas_click_filter.cpp
  src/coordinate_picker_plugin.cpp
  src/disparity_plugin.cpp
  src/draw_marker_plugin.cpp
  src/draw_polygon_plugin.cpp
  src/float_plugin.cpp
  src/gps_plugin.cpp
  src/grid_plugin.cpp
  src/image_plugin.cpp
  src/laserscan_plugin.cpp
  src/marker_plugin.cpp
  src/measuring_plugin.cpp
  src/navsat_plugin.cpp
  src/occupancy_grid_plugin.cpp
  src/odometry_plugin.cpp
  src/path_plugin.cpp
  src/placeable_window_proxy.cpp
  src/plan_route_plugin.cpp
  src/point_click_publisher_plugin.cpp
  src/pointcloud2_plugin.cpp
  src/point_drawing_plugin.cpp
  src/pose_plugin.cpp
  src/robot_model_plugin.cpp
  src/robot_image_plugin.cpp
  src/route_plugin.cpp
  src/speedometer_plugin.cpp
  src/string_plugin.cpp
  src/textured_marker_plugin.cpp
  src/tf_frame_plugin.cpp
  src/topic_select.cpp
  src/topic_type_watcher.cpp)

set(HEADER_FILES
  include/${PROJECT_NAME}/attitude_indicator_plugin.hpp
  include/${PROJECT_NAME}/canvas_click_filter.hpp
  include/${PROJECT_NAME}/coordinate_picker_plugin.hpp
  include/${PROJECT_NAME}/disparity_plugin.hpp
  include/${PROJECT_NAME}/draw_marker_plugin.hpp
  include/${PROJECT_NAME}/draw_polygon_plugin.hpp
  include/${PROJECT_NAME}/float_plugin.hpp
  include/${PROJECT_NAME}/gps_plugin.hpp
  include/${PROJECT_NAME}/grid_plugin.hpp
  include/${PROJECT_NAME}/image_plugin.hpp
  include/${PROJECT_NAME}/laserscan_plugin.hpp
  include/${PROJECT_NAME}/marker_plugin.hpp
  include/${PROJECT_NAME}/measuring_plugin.hpp
  include/${PROJECT_NAME}/navsat_plugin.hpp
  include/${PROJECT_NAME}/occupancy_grid_plugin.hpp
  include/${PROJECT_NAME}/odometry_plugin.hpp
  include/${PROJECT_NAME}/path_plugin.hpp
  include/${PROJECT_NAME}/placeable_window_proxy.hpp
  include/${PROJECT_NAME}/plan_route_plugin.hpp
  include/${PROJECT_NAME}/point_click_publisher_plugin.hpp
  include/${PROJECT_NAME}/pointcloud2_plugin.hpp
  include/${PROJECT_NAME}/point_drawing_plugin.hpp
  include/${PROJECT_NAME}/pose_plugin.hpp
  include/${PROJECT_NAME}/robot_model_plugin.hpp
  include/${PROJECT_NAME}/robot_image_plugin.hpp
  include/${PROJECT_NAME}/route_plugin.hpp
  include/${PROJECT_NAME}/speedometer_plugin.hpp
  include/${PROJECT_NAME}/string_plugin.hpp
  include/${PROJECT_NAME}/textured_marker_plugin.hpp
  include/${PROJECT_NAME}/tf_frame_plugin.hpp
  include/${PROJECT_NAME}/topic_select.hpp
  include/${PROJECT_NAME}/topic_type_watcher.hpp)

set(MAPVIZ_PLUGINS_QT_LIBRARIES
  Qt${QT_VERSION_MAJOR}::Core
  Qt${QT_VERSION_MAJOR}::Gui
  Qt${QT_VERSION_MAJOR}::OpenGL
  Qt${QT_VERSION_MAJOR}::Widgets
)

if(QT_VERSION_MAJOR EQUAL 6)
  list(APPEND MAPVIZ_PLUGINS_QT_LIBRARIES Qt${QT_VERSION_MAJOR}::OpenGLWidgets)
endif()
set(MAPVIZ_PLUGINS_QT_EXPORT_DEPENDENCIES
  Qt${QT_VERSION_MAJOR}Core
  Qt${QT_VERSION_MAJOR}Gui
  Qt${QT_VERSION_MAJOR}OpenGL
  Qt${QT_VERSION_MAJOR}Widgets)

if(QT_VERSION_MAJOR EQUAL 6)
  list(APPEND MAPVIZ_PLUGINS_QT_EXPORT_DEPENDENCIES Qt${QT_VERSION_MAJOR}OpenGLWidgets)
endif()

add_library(${PROJECT_NAME} SHARED
    ${HEADER_FILES}
    ${SRC_FILES}
    ${UI_FILES}
)
if(nav2_msgs_FOUND)
  target_sources(${PROJECT_NAME} PRIVATE
    include/${PROJECT_NAME}/move_base_plugin.hpp
    src/move_base_plugin.cpp
    ui/move_base_config.ui)
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES
  AUTOMOC ON
  AUTOUIC ON
  AUTOUIC_SEARCH_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/ui"
  QT_MAJOR_VERSION ${QT_VERSION_MAJOR})

target_include_directories(${PROJECT_NAME}
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include/${PROJECT_NAME}>)

target_compile_definitions(${PROJECT_NAME}
  PUBLIC
    "PLUGINLIB__DISABLE_BOOST_FUNCTIONS")
target_compile_definitions(${PROJECT_NAME}
  PRIVATE
    WFlags=WindowFlags)

# cv_bridge 3.5.0 (ROS 2 Iron) started transitioning some files from .h to .hpp extensions.
# Public because image_plugin.hpp and disparity_plugin.hpp choose their include
# with it, so anything including them (such as the tests) needs it too.
if(cv_bridge_VERSION VERSION_LESS "3.5.0")
  target_compile_definitions(${PROJECT_NAME} PUBLIC USE_CVBRIDGE_H_FILES)
endif()

if(MAPVIZ_PLUGINS_IMAGE_TRANSPORT_TAKES_NODE_REF)
  target_compile_definitions(${PROJECT_NAME} PRIVATE
    MAPVIZ_IMAGE_TRANSPORT_TAKES_NODE_REF
  )
endif()

if(MAPVIZ_PLUGINS_HAVE_URDF_MODEL_HPP)
  target_compile_definitions(${PROJECT_NAME} PRIVATE MAPVIZ_HAVE_URDF_MODEL_HPP)
endif()

target_link_libraries(${PROJECT_NAME} PUBLIC
    cv_bridge::cv_bridge
    ${geometry_msgs_TARGETS}
    ${gps_msgs_TARGETS}
    image_transport::image_transport
    ${map_msgs_TARGETS}
    ${mapviz_TARGETS}
    ${marti_common_msgs_TARGETS}
    ${marti_nav_msgs_TARGETS}
    ${marti_sensor_msgs_TARGETS}
    ${marti_visualization_msgs_TARGETS}
    ${nav_msgs_TARGETS}
    opencv_core
    ${MAPVIZ_PLUGINS_QT_LIBRARIES}
    rclcpp::rclcpp
    rclcpp_action::rclcpp_action
    ${sensor_msgs_TARGETS}
    ${std_msgs_TARGETS}
    ${stereo_msgs_TARGETS}
    ${swri_route_util_TARGETS}
    ${swri_transform_util_TARGETS}
    tf2::tf2
    ${visualization_msgs_TARGETS})

if(nav2_msgs_FOUND)
  target_link_libraries(${PROJECT_NAME} PUBLIC ${nav2_msgs_TARGETS})
endif()

target_link_libraries(${PROJECT_NAME} PRIVATE
    ament_index_cpp::ament_index_cpp
    assimp::assimp
    opencv_imgcodecs
    opencv_imgproc
    pluginlib::pluginlib
    sensor_msgs::sensor_msgs_library
    ${swri_image_util_TARGETS}
    ${swri_math_util_TARGETS}
    tf2_geometry_msgs::tf2_geometry_msgs
    ${urdf_TARGETS})

### Install the plugins ###
if(nav2_msgs_FOUND)
  install(DIRECTORY include/
    DESTINATION include/${PROJECT_NAME}
    FILES_MATCHING PATTERN "*.hpp")
else()
  install(DIRECTORY include/
    DESTINATION include/${PROJECT_NAME}
    FILES_MATCHING PATTERN "*.hpp"
    PATTERN "move_base_plugin.hpp" EXCLUDE)
endif()

install(DIRECTORY ${MAPVIZ_PLUGINS_AUTOGEN_INCLUDE_DIR}/
  DESTINATION include/${PROJECT_NAME}
  FILES_MATCHING PATTERN "ui_*.h")

install(TARGETS ${PROJECT_NAME}
  EXPORT export_${PROJECT_NAME}
  RUNTIME DESTINATION bin
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib)

pluginlib_export_plugin_description_file(mapviz mapviz_plugins.xml)
if(nav2_msgs_FOUND)
  pluginlib_export_plugin_description_file(mapviz move_base_plugin.xml)
endif()

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()

  find_package(ament_cmake_gtest REQUIRED)

  # The plugins build Qt widgets in their constructors, so the test needs a
  # platform plugin that works without a display.
  ament_add_gtest(test_plugin_config test/test_plugin_config.cpp
    ENV QT_QPA_PLATFORM=offscreen)
  if(TARGET test_plugin_config)
    # ament_add_gtest() already used the plain signature on this target.
    target_link_libraries(test_plugin_config ${PROJECT_NAME})
    # The plugin headers include their AUTOUIC generated ui_*.h, which land in
    # the library target's autogen directory rather than on its usage
    # requirements.
    target_include_directories(test_plugin_config
      PRIVATE ${MAPVIZ_PLUGINS_AUTOGEN_INCLUDE_DIR})
  endif()

  ament_add_gtest(test_plan_route test/test_plan_route.cpp
    ENV QT_QPA_PLATFORM=offscreen)
  if(TARGET test_plan_route)
    target_link_libraries(test_plan_route ${PROJECT_NAME})
    target_include_directories(test_plan_route
      PRIVATE ${MAPVIZ_PLUGINS_AUTOGEN_INCLUDE_DIR})
  endif()

  ament_add_gtest(test_plugin_round_trip test/test_plugin_round_trip.cpp
    ENV QT_QPA_PLATFORM=offscreen)
  if(TARGET test_plugin_round_trip)
    target_link_libraries(test_plugin_round_trip ${PROJECT_NAME})
    target_include_directories(test_plugin_round_trip
      PRIVATE ${MAPVIZ_PLUGINS_AUTOGEN_INCLUDE_DIR})
  endif()

  ament_add_gtest(test_plugin_signals test/test_plugin_signals.cpp
    ENV QT_QPA_PLATFORM=offscreen)
  if(TARGET test_plugin_signals)
    target_link_libraries(test_plugin_signals ${PROJECT_NAME})
    target_include_directories(test_plugin_signals
      PRIVATE ${MAPVIZ_PLUGINS_AUTOGEN_INCLUDE_DIR})
  endif()

  ament_add_gtest(test_malformed_messages test/test_malformed_messages.cpp
    ENV QT_QPA_PLATFORM=offscreen)
  if(TARGET test_malformed_messages)
    target_link_libraries(test_malformed_messages ${PROJECT_NAME})
    target_include_directories(test_malformed_messages
      PRIVATE ${MAPVIZ_PLUGINS_AUTOGEN_INCLUDE_DIR})
  endif()
endif()

ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)
ament_export_dependencies(
  cv_bridge
  geometry_msgs
  gps_msgs
  image_transport
  map_msgs
  mapviz
  marti_common_msgs
  marti_nav_msgs
  marti_sensor_msgs
  marti_visualization_msgs
  nav_msgs
  ${MAPVIZ_PLUGINS_QT_EXPORT_DEPENDENCIES}
  rclcpp
  rclcpp_action
  sensor_msgs
  std_msgs
  stereo_msgs
  swri_route_util
  swri_transform_util
  tf2
  visualization_msgs)
if(nav2_msgs_FOUND)
  ament_export_dependencies(nav2_msgs)
endif()
ament_package(CONFIG_EXTRAS cmake/mapviz_plugins-extras.cmake)
