diff --git a/3rd-party/cove/libvectorizer/Concurrency.h b/3rd-party/cove/libvectorizer/Concurrency.h index 652d6d206..a29059e0f 100644 --- a/3rd-party/cove/libvectorizer/Concurrency.h +++ b/3rd-party/cove/libvectorizer/Concurrency.h @@ -46,7 +46,7 @@ namespace Concurrency { * is const in order to ensure thread safety also in accessing this object * (cf. std::shared_ptr documentation). */ -class Progress : public ProgressObserver +class Progress final : public ProgressObserver { private: struct Data @@ -62,7 +62,7 @@ class Progress : public ProgressObserver Progress(Progress&& p) = delete; Progress& operator=(const Progress&) = delete; Progress& operator=(Progress&& p) = delete; - ~Progress() final = default; + ~Progress() override = default; int getPercentage() const noexcept; void setPercentage(int percentage) final; diff --git a/3rd-party/cove/tests/CMakeLists.txt b/3rd-party/cove/tests/CMakeLists.txt index 3bf77c25f..b4a9786dd 100644 --- a/3rd-party/cove/tests/CMakeLists.txt +++ b/3rd-party/cove/tests/CMakeLists.txt @@ -9,6 +9,10 @@ find_package(Qt5Test ${Qt5Core_VERSION} REQUIRED) set(CMAKE_CXX_STANDARD 14) set(CMAKE_AUTOMOC ON) +configure_file(test_config.h.in test_config.h) +add_library(cove-test-config INTERFACE) +target_include_directories(cove-test-config INTERFACE "${CMAKE_CURRENT_BINARY_DIR}") + add_executable(cove-ParallelImageProcessingTest ParallelImageProcessingTest.cpp ) @@ -23,6 +27,9 @@ endif() add_executable(cove-PolygonTest PolygonTest.cpp ) +target_link_libraries(cove-PolygonTest + PRIVATE cove-test-config +) add_test( NAME cove-PolygonTest COMMAND cove-PolygonTest diff --git a/3rd-party/cove/tests/PolygonTest.cpp b/3rd-party/cove/tests/PolygonTest.cpp index fdb4a4b55..482615822 100644 --- a/3rd-party/cove/tests/PolygonTest.cpp +++ b/3rd-party/cove/tests/PolygonTest.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,8 @@ #include "libvectorizer/Polygons.h" +#include "test_config.h" + // Benchmarking adds significant overhead when enabled. // With the focus on (CI) testing, we default to disabling benchmarking // by defining COVE_BENCHMARK as an empty macro here, and let CMake set @@ -46,6 +49,11 @@ # define COVE_BENCHMARK #endif +void PolygonTest::initTestCase() +{ + QDir::addSearchPath(QStringLiteral("testdata"), QDir(QString::fromUtf8(COVE_TEST_SOURCE_DIR)).absoluteFilePath(QStringLiteral("data"))); +} + void PolygonTest::testJoins_data() { QTest::addColumn("simpleOnly"); @@ -56,8 +64,8 @@ void PolygonTest::testJoins_data() QTest::addColumn("resultFile"); QTest::newRow("simple joins") - << true << 5.0 << 9 << 0.0 << "data/PolygonTest1-sample.png" - << "data/PolygonTest1-simple-joins-result.dat"; + << true << 5.0 << 9 << 0.0 << "testdata:PolygonTest1-sample.png" + << "testdata:PolygonTest1-simple-joins-result.dat"; } void PolygonTest::testJoins() @@ -72,7 +80,7 @@ void PolygonTest::testJoins() QFETCH(QString, imageFile); QFETCH(QString, resultFile); - QVERIFY(sampleImage.load(QFINDTESTDATA(imageFile))); + QVERIFY(sampleImage.load(imageFile)); polyTracer.setSimpleOnly(simpleOnly); polyTracer.setMaxDistance(maxDistance); @@ -85,8 +93,8 @@ void PolygonTest::testJoins() polys = polyTracer.createPolygonsFromImage(sampleImage); } - // saveResults(polys, QFINDTESTDATA(resultFile)); - compareResults(polys, QFINDTESTDATA(resultFile)); + // saveResults(polys, resultFile); + compareResults(polys, resultFile); } void PolygonTest::saveResults(const cove::PolygonList& polys, diff --git a/3rd-party/cove/tests/PolygonTest.h b/3rd-party/cove/tests/PolygonTest.h index 4e5a1c7c3..0d5e8e22c 100644 --- a/3rd-party/cove/tests/PolygonTest.h +++ b/3rd-party/cove/tests/PolygonTest.h @@ -26,6 +26,8 @@ class PolygonTest : public QObject { Q_OBJECT private slots: + void initTestCase(); + void testJoins_data(); void testJoins(); diff --git a/3rd-party/cove/tests/test_config.h.in b/3rd-party/cove/tests/test_config.h.in new file mode 100644 index 000000000..769981727 --- /dev/null +++ b/3rd-party/cove/tests/test_config.h.in @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Kai Pastor + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#ifndef COVE_TEST_CONFIG_H +#define COVE_TEST_CONFIG_H + +#define COVE_TEST_SOURCE_DIR "@CMAKE_CURRENT_SOURCE_DIR@" + +#endif diff --git a/CMakeLists.txt b/CMakeLists.txt index 2cda566ca..28ce79641 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ # # Copyright 2012, 2013, 2014 Thomas Schöps -# Copyright 2012-2020 Kai Pastor +# Copyright 2012-2021 Kai Pastor # # This file is part of OpenOrienteering. # @@ -27,10 +27,12 @@ if(CCACHE_PROGRAM) set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}") endif() +option(CMAKE_FIND_PACKAGE_PREFER_CONFIG "Lookup package config files before using find modules" ON) + # Project declaration -project(Mapper VERSION 0.9.4 LANGUAGES CXX C) -set(Mapper_COPYRIGHT "(C) 2020 The OpenOrienteering developers") +project(Mapper VERSION 0.9.5 LANGUAGES CXX C) +set(Mapper_COPYRIGHT "(C) 2021 The OpenOrienteering developers") math(EXPR Mapper_VERSION_CODE "${Mapper_VERSION_MAJOR} * 10000 + ${Mapper_VERSION_MINOR} * 100 + ${Mapper_VERSION_PATCH} * 2 + ${CMAKE_SIZEOF_VOID_P} / 4 - 1") @@ -63,8 +65,10 @@ endif() option(Mapper_DEBUG_TRANSLATIONS "Debug missing translations" OFF) -# Used for some Linux distributions which do not provide the polyclipping lib. -option(Mapper_BUILD_CLIPPER "Build the Clipper package from source" OFF) +# To improve developer experience, build clipper if it is not found +set(Mapper_BUILD_CLIPPER "auto" CACHE STRING + "Build the Clipper package from source, alternatives: ON, OFF" +) option(Mapper_USE_GDAL "Use the GDAL library" ON) @@ -138,10 +142,12 @@ elseif(ANDROID) set(MAPPER_DATA_DESTINATION "assets") set(MAPPER_ABOUT_DESTINATION "assets/doc") else() # LINUX and alike - set(MAPPER_RUNTIME_DESTINATION "bin") - set(MAPPER_LIBRARY_DESTINATION "lib/${Mapper_PACKAGE_NAME}") - set(MAPPER_DATA_DESTINATION "share/${Mapper_PACKAGE_NAME}") - set(MAPPER_ABOUT_DESTINATION "share/doc/${Mapper_PACKAGE_NAME}") + include(GNUInstallDirs) + set(MAPPER_RUNTIME_DESTINATION "${CMAKE_INSTALL_BINDIR}") + set(MAPPER_LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}/${Mapper_PACKAGE_NAME}") + set(MAPPER_DATA_DESTINATION "${CMAKE_INSTALL_DATADIR}/${Mapper_PACKAGE_NAME}") + string(REPLACE "/${PROJECT_NAME}" "/${Mapper_PACKAGE_NAME}" + MAPPER_ABOUT_DESTINATION "${CMAKE_INSTALL_DOCDIR}") endif() if(CMAKE_CROSSCOMPILING) @@ -174,6 +180,19 @@ add_custom_target(Mapper_prerequisites ) set(Mapper_prerequisites_FOUND TRUE) +if(Mapper_BUILD_CLIPPER STREQUAL "auto") + find_package(Polyclipping 6.1.3 MODULE) + if(NOT Polyclipping_FOUND) + message(WARNING + "System polyclipping is missing. Enabling embedded build.\n" + "Set Mapper_BUILD_CLIPPER=OFF to disable embedded build." + ) + set_property(CACHE Mapper_BUILD_CLIPPER PROPERTY VALUE "ON") + else() + set_property(CACHE Mapper_BUILD_CLIPPER PROPERTY VALUE "OFF") + endif() + set_property(CACHE Mapper_BUILD_CLIPPER PROPERTY TYPE "BOOL") +endif() if(Mapper_BUILD_CLIPPER) add_subdirectory(3rd-party/clipper) add_feature_info(Mapper_BUILD_CLIPPER 1 "version: ${CLIPPER_VERSION}") @@ -182,18 +201,20 @@ else() find_package(Polyclipping 6.1.3 MODULE REQUIRED) endif() -find_package(PROJ4 CONFIG QUIET) +# We must not require a minimum version of PROJ via find_package +# because PROJ config requires the major version to match exactly. +find_package(PROJ4 REQUIRED) if(NOT TARGET PROJ4::proj) - set(PROJ4_FOUND false) - find_package(PROJ4 MODULE REQUIRED) + include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FindPROJ.cmake") endif() -if(NOT PROJ4_VERSION OR PROJ4_VERSION VERSION_LESS 6.1) +if(NOT PROJ4_VERSION OR PROJ4_VERSION VERSION_LESS 4.9) + message(FATAL_ERROR "At least PROJ 4.9 is required") +elseif(PROJ4_VERSION VERSION_LESS 6.1) # New PROJ API missing or incomplete. # (proj_normalize_for_visualization() came in 6.1.) set_property(TARGET PROJ4::proj APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS ACCEPT_USE_OF_DEPRECATED_PROJ_API_H) -endif() -if(NOT PROJ4_VERSION OR PROJ4_VERSION VERSION_LESS 6.2.1) +elseif(PROJ4_VERSION VERSION_LESS 6.2.1) # Datum Potsdam issue, https://github.com/OSGeo/PROJ/pull/1573 set_property(TARGET PROJ4::proj APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS PROJ_ISSUE_1573) @@ -218,11 +239,6 @@ if(big_endian) endif() -if(UNIX AND NOT APPLE) -# set(CMAKE_INSTALL_RPATH "\$ORIGIN/../lib/${Mapper_PACKAGE_NAME}/lib") - set(CMAKE_INSTALL_RPATH "${MAPPER_LIBRARY_DESTINATION}/lib") -endif() - add_definitions(-D_USE_MATH_DEFINES -DUNICODE) if(Mapper_DEVELOPMENT_BUILD) @@ -274,7 +290,6 @@ endif() if (Mapper_USE_GDAL) add_subdirectory("src/gdal") endif() -add_subdirectory("src/libocad") if(NOT ANDROID) add_subdirectory("src/printsupport") endif() @@ -304,6 +319,7 @@ set(ci ci/filter-stderr.sed ci/publish.yml ci/publish-coverage.yml + ci/release-notes.yml ci/setup-common.yml ci/setup-macos.yml ci/setup-msys2.yml diff --git a/INSTALL.md b/INSTALL.md index fa3994ea7..f9418d932 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -95,6 +95,39 @@ make ``` +## Compiling for Windows (without OpenOrienteering superbuild) + +A development environment on 64-bit Windows can be set up and maintained easily +with the MSYS2 distribution. It provides up-to-date Windows packages of bash, +gcc, mingw-w64, CMake, Ninja, Qt, PROJ, GDAL and Doxygen. + +First of all, you need to install (and update) MSYS2, https://www.msys2.org/. +The next step is to install all dependencies used by Mapper at build time +and at run time. This will download more than 1.3 GB and take more than 9 GB +of disk spaced after installation. In an msys2 terminal window, type: + +``` +pacman -S git mingw-w64-x86_64-qt-creator mingw-w64-x86_64-proj mingw-w64-x86_64-gdal mingw-w64-x86_64-ninja mingw-w64-x86_64-cmake mingw-w64-x86_64-doxygen mingw-w64-x86_64-gdb +``` + +For development you will start with an mingw64 terminal. Clone the +OpenOrienteering Mapper repository (or use a source archive), as +written above. + +``` +git clone https://github.com/OpenOrienteering/mapper.git +``` + +Run Qt Creator: + +``` +qtcreator.exe & +``` + +Adjust the Qt Kit settings and set the CMake generator to Ninja. +Then open CMakeList.txt from the source directory. + + ## Compiling with OpenOrienteering superbuild The OpenOrienteering superbuild project diff --git a/ci/filter-stderr.sed b/ci/filter-stderr.sed index ffd1a4f4c..1c3a14d8f 100644 --- a/ci/filter-stderr.sed +++ b/ci/filter-stderr.sed @@ -1,6 +1,5 @@ # Cf. https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md /:.*[0-9]: [Ww]arning:/ { - /libocad/ b s,^\([a-zA-Z]:\)?/.*/1/s/,, s,^\([a-zA-Z]:\)?/.*/build/,, s,^,##vso[task.LogIssue type=warning;], diff --git a/ci/openorienteering-mapper-ci.cmake b/ci/openorienteering-mapper-ci.cmake index f87f25fe4..386ee9dc0 100644 --- a/ci/openorienteering-mapper-ci.cmake +++ b/ci/openorienteering-mapper-ci.cmake @@ -122,6 +122,7 @@ superbuild_package( "${CMAKE_COMMAND}" --build . --target package$,,/fast> $<$>: TEST_COMMAND + "${CMAKE_COMMAND}" -E env "LD_LIBRARY_PATH=${CMAKE_STAGING_PREFIX}/lib" "${CMAKE_CTEST_COMMAND}" -T Test --no-compress-output $<$: --exclude-regex symbol_set_t diff --git a/ci/setup-common.yml b/ci/setup-common.yml index 445687264..9d0280d0c 100644 --- a/ci/setup-common.yml +++ b/ci/setup-common.yml @@ -1,6 +1,6 @@ # This file is part of OpenOrienteering. -# Copyright 2019 Kai Pastor +# Copyright 2019-2021 Kai Pastor # # Redistribution and use is allowed according to the terms of the BSD license: # @@ -51,6 +51,8 @@ steps: if [ -z "${APP_ID_SUFFIX}" -a -n "${VERSION_DISPLAY}" ] ; then echo "##vso[task.setVariable variable=APP_ID_SUFFIX].${BUILD_SOURCEBRANCHNAME}" fi + # Fix Superbuild sdk_host quirk + echo "##vso[task.setVariable variable=ANDROID_NDK_ROOT]" env | sort displayName: 'Update environment' diff --git a/code-check-wrapper.sh b/code-check-wrapper.sh index 3b4354105..b90c87184 100755 --- a/code-check-wrapper.sh +++ b/code-check-wrapper.sh @@ -49,8 +49,10 @@ PATTERN= for I in \ action_grid_bar.cpp \ boolean_tool.cpp \ + color_wheel_widget.cpp \ combined_symbol.cpp \ configure_grid_dialog.cpp \ + course_file_format.cpp \ crs_param_widgets.cpp \ crs_template.cpp \ crs_template_implementation.cpp \ @@ -63,13 +65,17 @@ for I in \ georeferencing_dialog.cpp \ georeferencing_t.cpp \ icon_engine \ + iof_course_export \ key_button_bar.cpp \ + key_value_container \ + kml_course_export \ line_symbol.cpp \ main.cpp \ /map.cpp \ map_coord.cpp \ map_editor.cpp \ map_find_feature.cpp \ + map_printer \ map_widget.cpp \ mapper_proxystyle.cpp \ /object.cpp \ @@ -85,6 +91,8 @@ for I in \ renderable_implementation.cpp \ rotate_map_dialog.cpp \ settings_dialog.cpp \ + simple_course_dialog.cpp \ + simple_course_export.cpp \ stretch_map_dialog.cpp \ style_t.cpp \ /symbol.cpp \ diff --git a/codespell.sh b/codespell.sh index 94c4f6fe4..77b0a960c 100755 --- a/codespell.sh +++ b/codespell.sh @@ -7,7 +7,6 @@ while read WORD; do SKIP_LIST="${SKIP_LIST:+$SKIP_LIST,}$WORD"; done \ 3rd-party/cove/potrace packaging/linux/Mapper.desktop src/gdal/mapper-osmconf.ini - src/libocad src/printsupport/qt-5.5.1 src/printsupport/qt-5.12.4 END_SKIP_LIST diff --git a/doc/licensing/fedora-licensing.cmake b/doc/licensing/fedora-licensing.cmake index 3a7e83cf1..8107f4938 100644 --- a/doc/licensing/fedora-licensing.cmake +++ b/doc/licensing/fedora-licensing.cmake @@ -1,5 +1,5 @@ # -# Copyright 2017 Kai Pastor +# Copyright 2017-2021 Kai Pastor # # This file is part of OpenOrienteering. # @@ -17,20 +17,15 @@ # along with OpenOrienteering. If not, see . -# Based on Fedora 25. - include("linux-distribution.cmake") -set(explicit_copyright_gdal - "gdal" - "file:///usr/share/doc/gdal-libs/LICENSE.TXT" -) - set(system_copyright_dir "/usr/share") set(copyright_pattern "${system_copyright_dir}/doc/@package@/COPYING" "${system_copyright_dir}/doc/@package@/COPYRIGHT" + "${system_copyright_dir}/doc/@package@-libs/LICENSE.TXT" # gdal "${system_copyright_dir}/licenses/@package@/COPYING" "${system_copyright_dir}/licenses/@package@/Copyright.txt" "${system_copyright_dir}/licenses/@package@/LICENSE" + "${system_copyright_dir}/licenses/@package@-libs/LICENSE.TXT" # gdal ) diff --git a/doc/licensing/superbuild-licensing.cmake b/doc/licensing/superbuild-licensing.cmake index cb3e3e008..ce740b72d 100644 --- a/doc/licensing/superbuild-licensing.cmake +++ b/doc/licensing/superbuild-licensing.cmake @@ -37,17 +37,21 @@ if(NOT LICENSING_COPYRIGHT_DIR OR NOT LICENSING_COMMON_DIR) endif() -# Based on OpenOrienteering superbuild as of 2020-06-21 +# Based on OpenOrienteering superbuild as of 2020-06-21, plus libkml 2020-12-15 list(APPEND third_party_components + boost giflib libjpeg-turbo + libkml liblzma libopenjp2 libpcre3 libpng libtiff libwebp + minizip poppler + uriparser ) find_package(Qt5Core REQUIRED QUIET) if(NOT ${Qt5Core_VERSION} VERSION_LESS 5.9) diff --git a/doc/manual/CMakeLists.txt b/doc/manual/CMakeLists.txt index d50c0ced1..56470b02f 100644 --- a/doc/manual/CMakeLists.txt +++ b/doc/manual/CMakeLists.txt @@ -171,10 +171,16 @@ if(Mapper_MANUAL_QTHELP) DOC "The path of the sqlite3 executable" ) if (DEFINED ENV{SOURCE_DATE_EPOCH} AND SQLITE3_EXECUTABLE) - file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/timestamp.sql" - "update 'SettingsTable' set Value='$ENV{SOURCE_DATE_EPOCH}' where Key='CreationTime';\n" - "delete from 'SettingsTable' where Key='LastRegisterTime';\n" - ) + if(Qt5Core_VERSION VERSION_LESS 5.12.0) + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/timestamp.sql" + "UPDATE 'SettingsTable' SET Value='$ENV{SOURCE_DATE_EPOCH}' where Key='CreationTime';\n" + "DELETE FROM 'SettingsTable' WHERE Key='LastRegisterTime';\n" + ) + else() + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/timestamp.sql" + "UPDATE TimeStampTable SET TimeStamp=strftime('%Y-%m-%dT%H:%M:%S',datetime($ENV{SOURCE_DATE_EPOCH},'unixepoch'));\n" + ) + endif() set(source_date_commands COMMAND ${SQLITE3_EXECUTABLE} "${Mapper_HELP_COLLECTION}" < "timestamp.sql" ) diff --git a/doc/manual/Doxyfile-html.in b/doc/manual/Doxyfile-html.in index 77208adb5..bb8926423 100644 --- a/doc/manual/Doxyfile-html.in +++ b/doc/manual/Doxyfile-html.in @@ -38,8 +38,6 @@ SEARCHENGINE = NO HTML_HEADER = "@CMAKE_CURRENT_SOURCE_DIR@/header.html" HTML_FOOTER = "@CMAKE_CURRENT_SOURCE_DIR@/footer.html" HTML_STYLESHEET = "@CMAKE_CURRENT_SOURCE_DIR@/style.css" -HTML_EXTRA_FILES = "@CMAKE_CURRENT_SOURCE_DIR@/pages/attachment/scribble_1024.png" -HTML_EXTRA_FILES += "@CMAKE_CURRENT_SOURCE_DIR@/pages/attachment/scribble_2048.png" GENERATE_QHP = YES QCH_FILE = "../@Mapper_COMPRESSED_HELP@" diff --git a/doc/manual/pages/android-app.md b/doc/manual/pages/android-app.md index f673f64f7..668d71694 100644 --- a/doc/manual/pages/android-app.md +++ b/doc/manual/pages/android-app.md @@ -4,11 +4,14 @@ authors: - Kai Pastor - Thomas Schoeps keywords: Android -edited: 19 September 2020 +last_modified_date: 19 September 2020 redirect_from: - /android/ + - /android-app/ - '/Android Manual/' - /mapper-manual/pages/android-index.html +nav_order: 0.04 +has_children: true --- ## Preliminary remarks diff --git a/doc/manual/pages/android-pc.md b/doc/manual/pages/android-pc.md index 89c332620..5cdd2e570 100644 --- a/doc/manual/pages/android-pc.md +++ b/doc/manual/pages/android-pc.md @@ -1,5 +1,7 @@ --- title: Preparing a Map on the PC +parent: The Mapper App for Android +nav_order: 0.2 --- The Android app cannot be used to create new maps because it would be cumbersome to setup the base maps there. Instead, maps should be prepared with templates and possibly georeferencing using the PC version of Mapper first and then transferred to the mobile device for surveying. diff --git a/doc/manual/pages/android-requirements.md b/doc/manual/pages/android-requirements.md index 9c46d18ee..1898984a7 100644 --- a/doc/manual/pages/android-requirements.md +++ b/doc/manual/pages/android-requirements.md @@ -1,6 +1,8 @@ --- title: Device Requirements and Recommendations keywords: Android +parent: The Mapper App for Android +nav_order: 0.1 --- ## Android version diff --git a/doc/manual/pages/android-storage.md b/doc/manual/pages/android-storage.md index 36b3b9b6c..756142b5c 100644 --- a/doc/manual/pages/android-storage.md +++ b/doc/manual/pages/android-storage.md @@ -1,6 +1,8 @@ --- title: Storing Maps and Templates on Android Devices keywords: Android +parent: The Mapper App for Android +nav_order: 0.3 --- ## Storage locations diff --git a/doc/manual/pages/attachment/scribble_1024.png b/doc/manual/pages/attachment/scribble_1024.png deleted file mode 100644 index 2de8cacee..000000000 Binary files a/doc/manual/pages/attachment/scribble_1024.png and /dev/null differ diff --git a/doc/manual/pages/attachment/scribble_2048.png b/doc/manual/pages/attachment/scribble_2048.png deleted file mode 100644 index 4deb771c5..000000000 Binary files a/doc/manual/pages/attachment/scribble_2048.png and /dev/null differ diff --git a/doc/manual/pages/color_dock_widget.md b/doc/manual/pages/color_dock_widget.md index e1375d870..61747da1c 100644 --- a/doc/manual/pages/color_dock_widget.md +++ b/doc/manual/pages/color_dock_widget.md @@ -4,7 +4,9 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Colors -edited: 26 February 2013 +last_modified_date: 26 February 2013 +parent: Colors and Symbols +nav_order: 0.1 --- This window can be shown by clicking the menu item Symbols -> Color window. diff --git a/doc/manual/pages/colors_symbols.md b/doc/manual/pages/colors_symbols.md index 5bdb4573d..fc4b38701 100644 --- a/doc/manual/pages/colors_symbols.md +++ b/doc/manual/pages/colors_symbols.md @@ -1,6 +1,8 @@ --- title: Colors and Symbols -edited: 26 November 2015 +last_modified_date: 26 November 2015 +nav_order: 0.31 +has_children: true --- Modifying map colors and symbols diff --git a/doc/manual/pages/course_design.md b/doc/manual/pages/course_design.md index 6cb8eb1fb..5821d884e 100644 --- a/doc/manual/pages/course_design.md +++ b/doc/manual/pages/course_design.md @@ -1,6 +1,7 @@ --- title: Using the Course Design Symbol Set keywords: Courses, Symbol sets +nav_order: 0.51 --- [Templates]: templates.md diff --git a/doc/manual/pages/cove.md b/doc/manual/pages/cove.md index dca3aeb35..36b85503c 100644 --- a/doc/manual/pages/cove.md +++ b/doc/manual/pages/cove.md @@ -4,7 +4,9 @@ authors: - Kai Pastor - Libor Pecháček keywords: Templates -edited: 9 March 2020 +parent: Templates and Data +last_modified_date: 9 March 2020 +nav_order: 0.4 --- CoVe (for Contour Vectorizer) is the name of a tool which lets users generate diff --git a/doc/manual/pages/crt_files.md b/doc/manual/pages/crt_files.md index 0903472fc..c5cce1d35 100644 --- a/doc/manual/pages/crt_files.md +++ b/doc/manual/pages/crt_files.md @@ -2,7 +2,9 @@ title: CRT Files authors: - Kai Pastor -edited: 21 January 2018 +last_modified_date: 21 January 2018 +parent: Reference +nav_order: 0.5 --- ## Introduction diff --git a/doc/manual/pages/edit_menu.md b/doc/manual/pages/edit_menu.md index 9201a6b5a..028f31665 100644 --- a/doc/manual/pages/edit_menu.md +++ b/doc/manual/pages/edit_menu.md @@ -5,7 +5,10 @@ authors: - Thomas Schoeps - Kai Pastor keywords: Menus -edited: 21 January 2018 +parent: Menus +grand_parent: Reference +nav_order: 0.2 +last_modified_date: 21 January 2018 --- #### ![ ](../mapper-images/undo.png) Undo diff --git a/doc/manual/pages/faq.md b/doc/manual/pages/faq.md index d3c117071..b8fb5e4b7 100644 --- a/doc/manual/pages/faq.md +++ b/doc/manual/pages/faq.md @@ -6,7 +6,8 @@ authors: - Thomas Schoeps - Libor Pechacek keywords: FAQ -edited: 5 December 2017 +last_modified_date: 5 December 2017 +nav_order: 0.81 --- * TOC diff --git a/doc/manual/pages/file_menu.md b/doc/manual/pages/file_menu.md index 338c208ec..e36d350e2 100644 --- a/doc/manual/pages/file_menu.md +++ b/doc/manual/pages/file_menu.md @@ -5,7 +5,10 @@ authors: - Kai Pastor - Thomas Schoeps keywords: Menus -edited: 21 December 2019 +parent: Menus +grand_parent: Reference +nav_order: 0.1 +last_modified_date: 21 December 2019 --- These controls provide standard file management tools. diff --git a/doc/manual/pages/find_objects.md b/doc/manual/pages/find_objects.md index 33ffb3eae..17ab29e94 100644 --- a/doc/manual/pages/find_objects.md +++ b/doc/manual/pages/find_objects.md @@ -4,7 +4,9 @@ authors: - Mitchell Krome - Kai Pastor keywords: Tagging -edited: 21 January 2018 +last_modified_date: 21 January 2018 +parent: Objects +nav_order: 0.6 --- The "Find objects" dialog allows to find and select objects in the current map part based on their tags, text, or symbol name. diff --git a/doc/manual/pages/gdal.md b/doc/manual/pages/gdal.md index 0adfa99e2..23906d57c 100644 --- a/doc/manual/pages/gdal.md +++ b/doc/manual/pages/gdal.md @@ -1,7 +1,9 @@ --- title: Geospatial data support with GDAL -keywords: GDAL -edited: 20 September 2020 +keywords: GDAL, Templates +parent: Templates and Data +last_modified_date: 24 December 2020 +nav_order: 0.5 --- OpenOrienteering Mapper uses the [GDAL library](https://gdal.org) @@ -13,7 +15,10 @@ GDAL's vector data support used to be known as OGR, so you will sometimes find this name, too. Geospatial raster data and vector data can be used as templates. -But it also possible to open vector data files as maps +However, currently only graphical templates can be displayed; +Mapper does not yet process raw raster data, such as altitudes, +which first need to be rendered in other software. +It also possible to open vector data files as maps and to import vector data into map files. In addition, the [File menu](file_menu.md) offers the option to export a map diff --git a/doc/manual/pages/georeferencing.md b/doc/manual/pages/georeferencing.md index 95a3d98b0..3c57ac953 100644 --- a/doc/manual/pages/georeferencing.md +++ b/doc/manual/pages/georeferencing.md @@ -4,7 +4,8 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Georeferencing -edited: 25 January 2020 +nav_order: 0.12 +last_modified_date: 25 January 2020 --- - [Introduction](#introduction) diff --git a/doc/manual/pages/grid.md b/doc/manual/pages/grid.md index 057da04f0..319ad195a 100644 --- a/doc/manual/pages/grid.md +++ b/doc/manual/pages/grid.md @@ -4,7 +4,8 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Map -edited: 25 February 2013 +nav_order: 0.13 +last_modified_date: 25 February 2013 --- ![Grid icon](../mapper-images/grid.png) The map grid can be activated by clicking the corresponding button in the [view toolbar](toolbars.md#view-toolbar). Settings for the grid can be changed by clicking the arrow to the right of this button. diff --git a/doc/manual/pages/index.md b/doc/manual/pages/index.md index 4c3139f30..07a0f83b3 100644 --- a/doc/manual/pages/index.md +++ b/doc/manual/pages/index.md @@ -1,9 +1,10 @@ --- title: User Manual -edited: 21 December 2019 +last_modified_date: 31 January 2021 redirect_from: - / - /Home +nav_order: 0.01 --- {% if doxygen %} @@ -15,11 +16,14 @@ redirect_from: [Installation](installation.md){: .subpage} Instructions to download and install OpenOrienteering. -[Main window](main_window.md){: .subpage} -Explanation of the main drawing window. +[Introduction to the User Interface](main_window.md){: .subpage} +Explanation of the home screen and main drawing window. -[Reference](reference.md){: .subpage} -Toolbar and menu items, including explanations for [all drawing tools](toolbars.md#drawing-toolbar). +[The Touch Mode User Interface](touch-mode.md){: .subpage} +Using the Mapper app in "Touch Mode". + +[The Mapper app for Android](android-app.md){: .subpage} +Working with the Android version of Mapper. [Starting a new map](new_map.md){: .subpage} First steps. @@ -30,41 +34,20 @@ Defining the relationship between map paper coordinates and real world coordinat [Map grid](grid.md){: .subpage} Showing a helper grid in the map view. -[Templates](templates-index.md){: .subpage} -Working with templates. +[Objects](objects-index.md){: .subpage} +Organizing, tagging and finding map objects. -[Geospatial data](gdal.md){: .subpage} -Geospatial raster data and vector data support based on GDAL. +[Templates and Data](templates-index.md){: .subpage} +Using and processing images and geospatial data. [Colors and symbols](colors_symbols.md){: .subpage} Modifying map colors and symbols. -[Map parts](map_parts.md){: .subpage} -Organizing objects in maps. - -[Object tags](object_tags.md){: .subpage} -Attaching arbitrary key-value pairs to objects. - -[Find objects](find_objects.md){: .subpage} -Finding objects based on textual properties. - -[CRT files](crt_files.md){: .subpage} -Providing rules for assigning symbols. - -[CoVe, the Contour Vectorizer](cove.md){: .subpage} -Vectorizing line features in raster graphics templates. - -[Settings](settings.md){: .subpage} -Adjusting the program to your preferences. - [Course design](course_design.md){: .subpage} Using the course design symbol set. -[The Touch Mode User Interface](touch-mode.md){: .subpage} -Using the Mapper app in "Touch Mode" - -[The Mapper app for Android](android-app.md){: .subpage} -Working with the Android version of Mapper. +[Reference](reference.md){: .subpage} +Drawing tools, toolbars, menu items, settings, CRT files. [FAQ](faq.md){: .nosubpage} Frequently asked questions. diff --git a/doc/manual/pages/installation.md b/doc/manual/pages/installation.md index 20375ff1b..ff7608237 100644 --- a/doc/manual/pages/installation.md +++ b/doc/manual/pages/installation.md @@ -1,6 +1,7 @@ --- title: Installation -edited: 28 September 2019 +last_modified_date: 28 September 2019 +nav_order: 0.02 --- OpenOrienteering Mapper runs on Android, Windows, macOS and Linux. diff --git a/doc/manual/pages/main_window.md b/doc/manual/pages/main_window.md index 75b1817ae..23e9f2998 100644 --- a/doc/manual/pages/main_window.md +++ b/doc/manual/pages/main_window.md @@ -1,12 +1,35 @@ --- -title: Map Screen +title: Introduction to the User Interface authors: - Peter Hoban - Thomas Schoeps -edited: 24 February 2013 + - Kai Pastor +last_modified_date: 21 January 2021 +nav_order: 0.02 --- -This is the main window when drawing maps. By default it looks similar to this: +## The Desktop User Interface + +On computers running Windows, macOS or Linux, the default appearance +of OpenOrienteering Mapper is the "Desktop User Interface". + +### The Home Screen + +When Mapper is running with no opened map file, +the program shows the "Home" screen. +It features buttons for key actions (left side) +and a list of recently used files (right side). + +One special button is labeled "Touch mode". Using this button, +you can toggle the [Touch Mode User Interface](touch-mode.md). +This user interface is designed for mobile work with a touch screen, +and it is the only user interface offered on Android devices. +Note that the GNSS tracking feature is available only in touch mode +at the moment. + +### The Map Editor + +In the desktop user interface, the map editor window looks like this: ![ ](images/main_window.png) diff --git a/doc/manual/pages/map_menu.md b/doc/manual/pages/map_menu.md index 57c44a07c..326f8ecbd 100644 --- a/doc/manual/pages/map_menu.md +++ b/doc/manual/pages/map_menu.md @@ -5,7 +5,10 @@ authors: - Thomas Schoeps - Kai Pastor keywords: Menus -edited: 20 January 2018 +parent: Menus +grand_parent: Reference +nav_order: 0.5 +last_modified_date: 20 January 2018 --- #### Georeferencing... diff --git a/doc/manual/pages/map_parts.md b/doc/manual/pages/map_parts.md index add1195f6..98e6599ed 100644 --- a/doc/manual/pages/map_parts.md +++ b/doc/manual/pages/map_parts.md @@ -3,7 +3,9 @@ title: Map Parts authors: - Kai Pastor keywords: Map parts -edited: 10 January 2016 +last_modified_date: 10 January 2016 +parent: Objects +nav_order: 0.3 --- Map parts partition the map in different collections of objects which can be worked on independently. diff --git a/doc/manual/pages/mapping-introduction.md b/doc/manual/pages/mapping-introduction.md index a4763d2b5..70865eb9d 100644 --- a/doc/manual/pages/mapping-introduction.md +++ b/doc/manual/pages/mapping-introduction.md @@ -3,7 +3,8 @@ title: Introduction to mapping for orienteering author: - Thomas Schoeps - Kai Pastor -edited: 1 December 2015 +last_modified_date: 1 December 2015 +nav_exclude: true --- *Disclaimer:* This is is mostly an empty place holder, not a complete introduction to orienteering mapping. Contributions welcome. diff --git a/doc/manual/pages/menus.md b/doc/manual/pages/menus.md new file mode 100644 index 000000000..764165e03 --- /dev/null +++ b/doc/manual/pages/menus.md @@ -0,0 +1,15 @@ +--- +title: Menus +keywords: Menus +parent: Reference +nav_order: 0.1 +has_children: true +--- + + - [File Menu](file_menu.md){: .subpage} + - [Edit Menu](edit_menu.md){: .subpage} + - [View Menu](view_menu.md){: .subpage} + - [Tools Menu](tools_menu.md){: .subpage} + - [Map Menu](map_menu.md){: .subpage} + - [Symbols Menu](symbols_menu.md){: .subpage} + - [Templates Menu](templates_menu.md){: .subpage} diff --git a/doc/manual/pages/new_map.md b/doc/manual/pages/new_map.md index a69b6ac99..a93c09815 100644 --- a/doc/manual/pages/new_map.md +++ b/doc/manual/pages/new_map.md @@ -4,7 +4,8 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Map -edited: 26 February 2013 +nav_order: 0.11 +last_modified_date: 26 February 2013 --- Choose the menu item File -> New... to show the new map dialog. diff --git a/doc/manual/pages/object_tags.md b/doc/manual/pages/object_tags.md index 2312daf28..50e976fea 100644 --- a/doc/manual/pages/object_tags.md +++ b/doc/manual/pages/object_tags.md @@ -5,7 +5,9 @@ authors: - Fraser Mills - Kai Pastor keywords: Tagging -edited: 21 January 2018 +last_modified_date: 21 January 2018 +parent: Objects +nav_order: 0.5 --- ## About object tags diff --git a/doc/manual/pages/objects-index.md b/doc/manual/pages/objects-index.md new file mode 100644 index 000000000..b114a0ff1 --- /dev/null +++ b/doc/manual/pages/objects-index.md @@ -0,0 +1,16 @@ +--- +title: Objects +last_modified_date: 31 January 2021 +nav_order: 0.15 +has_children: true +--- + +[Map parts](map_parts.md){: .subpage} +Organizing objects in maps. + +[Object tags](object_tags.md){: .subpage} +Attaching arbitrary key-value pairs to objects. + +[Find objects](find_objects.md){: .subpage} +Finding objects based on textual properties. + diff --git a/doc/manual/pages/reference.md b/doc/manual/pages/reference.md index c49c68735..4398abdf0 100644 --- a/doc/manual/pages/reference.md +++ b/doc/manual/pages/reference.md @@ -1,17 +1,19 @@ --- title: Reference keywords: Reference -edited: 26 November 2015 +last_modified_date: 26 November 2015 +nav_order: 0.61 +has_children: true --- - - [Toolbars](toolbars.md){: .subpage} - - [Advanced drawing tools](toolbars.md#advanced-drawing-toolbar) +[Menus](menus.md){: .subpage} +[Toolbars](toolbars.md){: .subpage} + - [Advanced drawing tools](toolbars.md#advanced-drawing-toolbar) + +[Settings](settings.md){: .subpage} +Adjusting the program to your preferences. + +[CRT files](crt_files.md){: .subpage} +Providing rules for assigning symbols. - - [File Menu](file_menu.md){: .subpage} - - [Edit Menu](edit_menu.md){: .subpage} - - [View Menu](view_menu.md){: .subpage} - - [Tools Menu](tools_menu.md){: .subpage} - - [Map Menu](map_menu.md){: .subpage} - - [Symbols Menu](symbols_menu.md){: .subpage} - - [Templates Menu](templates_menu.md){: .subpage} diff --git a/doc/manual/pages/settings.md b/doc/manual/pages/settings.md index 2ed616a33..8571ef34b 100644 --- a/doc/manual/pages/settings.md +++ b/doc/manual/pages/settings.md @@ -4,7 +4,9 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Settings -edited: 25 February 2013 +last_modified_date: 25 February 2013 +parent: Reference +nav_order: 0.3 --- In the settings dialog, the program can be adjusted to suit your application. diff --git a/doc/manual/pages/symbol_dock_widget.md b/doc/manual/pages/symbol_dock_widget.md index 99a559264..2f4b576c4 100644 --- a/doc/manual/pages/symbol_dock_widget.md +++ b/doc/manual/pages/symbol_dock_widget.md @@ -4,7 +4,9 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Symbols -edited: 24 February 2013 +last_modified_date: 24 February 2013 +parent: Colors and Symbols +nav_order: 0.2 --- ![ ](images/symbol_dock_widget.png) diff --git a/doc/manual/pages/symbol_replace_dialog.md b/doc/manual/pages/symbol_replace_dialog.md index 2bd3f7169..7db56fdb2 100644 --- a/doc/manual/pages/symbol_replace_dialog.md +++ b/doc/manual/pages/symbol_replace_dialog.md @@ -4,7 +4,9 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Templates -edited: 26 February 2013 +last_modified_date: 26 February 2013 +parent: Colors and Symbols +nav_order: 0.3 --- This dialog enables to replace the symbol set in the current map file by the set of another file. It is possible to accurately select a replacement symbol for each old symbol. Show it by clicking the menu item Symbols -> Replace symbol set..., then select the map file from which to take the new symbol set. Note that this is not a way to e.g. rescale a map from 1:10000 to 1:15000, use Map -> Scale map... instead. diff --git a/doc/manual/pages/symbols_menu.md b/doc/manual/pages/symbols_menu.md index 2e407c990..c48606b00 100644 --- a/doc/manual/pages/symbols_menu.md +++ b/doc/manual/pages/symbols_menu.md @@ -5,7 +5,10 @@ authors: - Thomas Schoeps - Kai Pastor keywords: Menus -edited: 20 January 2018 +parent: Menus +grand_parent: Reference +nav_order: 0.6 +last_modified_date: 20 January 2018 --- #### ![ ](../mapper-images/symbols.png) Symbol window diff --git a/doc/manual/pages/template_adjust.md b/doc/manual/pages/template_adjust.md index 22c48fb12..430a7c0b4 100644 --- a/doc/manual/pages/template_adjust.md +++ b/doc/manual/pages/template_adjust.md @@ -4,7 +4,9 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Templates -edited: 25 February 2013 +parent: Templates and Data +nav_order: 0.2 +last_modified_date: 25 February 2013 --- ![ ](images/template_adjust.png) diff --git a/doc/manual/pages/templates-index.md b/doc/manual/pages/templates-index.md index 322b8abcf..d07487738 100644 --- a/doc/manual/pages/templates-index.md +++ b/doc/manual/pages/templates-index.md @@ -1,13 +1,20 @@ --- -title: Templates +title: Templates and Data keywords: Templates -edited: 26 November 2015 +last_modified_date: 31 January 2021 +nav_order: 0.21 +has_children: true --- -Working with templates - -[Templates](templates.md){: .subpage} +[Introduction to Templates](templates.md){: .subpage} Types of templates, loading and positioning [Adjusting template positions](template_adjust.md){: .subpage} For non-georeferenced templates + +[CoVe, the Contour Vectorizer](cove.md){: .subpage} +Vectorizing line features in raster graphics templates. + +[Geospatial data](gdal.md){: .subpage} +Geospatial raster data and vector data support based on GDAL. + diff --git a/doc/manual/pages/templates.md b/doc/manual/pages/templates.md index 798baa1a7..75b3d79e1 100644 --- a/doc/manual/pages/templates.md +++ b/doc/manual/pages/templates.md @@ -4,7 +4,9 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Templates -edited: 21 December 2019 +parent: Templates and Data +nav_order: 0.1 +last_modified_date: 21 December 2019 --- Images, tracks recorded with GPS receivers and other layers which are used to provide base information for the mapper are called templates. They can be loaded into the map file using the template setup window which is available via the menu item Templates -> Template Setup Window. Templates can also be "abused" to display information on the final map, for example sponsor logos which are only available as raster images. diff --git a/doc/manual/pages/templates_menu.md b/doc/manual/pages/templates_menu.md index 44e8786d7..441c52081 100644 --- a/doc/manual/pages/templates_menu.md +++ b/doc/manual/pages/templates_menu.md @@ -5,7 +5,10 @@ authors: - Thomas Schoeps - Kai Pastor keywords: Menus -edited: 20 January 2018 +parent: Menus +grand_parent: Reference +nav_order: 0.7 +last_modified_date: 20 January 2018 --- #### ![ ](../mapper-images/templates.png) Template setup window diff --git a/doc/manual/pages/toolbars.md b/doc/manual/pages/toolbars.md index cffdc58de..18fc5d9db 100644 --- a/doc/manual/pages/toolbars.md +++ b/doc/manual/pages/toolbars.md @@ -5,7 +5,9 @@ authors: - Kai Pastor - Thomas Schoeps keywords: Toolbars -edited: 5 June 2018 +parent: Reference +nav_order: 0.2 +last_modified_date: 5 June 2018 to-do: - Split this page and update ALL context help in Mapper. - Update context help for zoom-in and -out in Mapper. diff --git a/doc/manual/pages/tools_menu.md b/doc/manual/pages/tools_menu.md index 6150f340c..ae4d10ca8 100644 --- a/doc/manual/pages/tools_menu.md +++ b/doc/manual/pages/tools_menu.md @@ -4,7 +4,10 @@ authors: - Peter Hoban - Thomas Schoeps keywords: Menus -edited: 26 February 2013 +parent: Menus +grand_parent: Reference +nav_order: 0.4 +last_modified_date: 26 February 2013 --- All tools accessed through this menu are also available on the toolbar. So for documentation, see the [drawing toolbars reference](toolbars.md#drawing-toolbar). diff --git a/doc/manual/pages/touch-mode.md b/doc/manual/pages/touch-mode.md index 318b88cc6..b8d639074 100644 --- a/doc/manual/pages/touch-mode.md +++ b/doc/manual/pages/touch-mode.md @@ -4,7 +4,8 @@ authors: - Kai Pastor - Thomas Schoeps keywords: Touch Mode, Android -edited: 20 September 2020 +last_modified_date: 20 September 2020 +nav_order: 0.03 --- ## Using the Mapper app in "Touch Mode" diff --git a/doc/manual/pages/view_menu.md b/doc/manual/pages/view_menu.md index 9a49a8790..ce229f906 100644 --- a/doc/manual/pages/view_menu.md +++ b/doc/manual/pages/view_menu.md @@ -5,7 +5,10 @@ authors: - Thomas Schoeps - Kai Pastor keywords: Menus -edited: 20 January 2018 +parent: Menus +grand_parent: Reference +nav_order: 0.3 +last_modified_date: 20 January 2018 --- #### ![ ](../mapper-images/move.png) Pan diff --git a/doc/manual/preprocess-markdown-html.cmake.in b/doc/manual/preprocess-markdown-html.cmake.in index 2f3b7ba56..7e7998a1d 100644 --- a/doc/manual/preprocess-markdown-html.cmake.in +++ b/doc/manual/preprocess-markdown-html.cmake.in @@ -104,7 +104,7 @@ foreach(file ${input_files}) #set(output "${output}\n\n\\addindex ${keywords}") endif() - get_yaml_field(edited ${frontmatter} "edited") + get_yaml_field(edited ${frontmatter} "last_modified_date") if(edited) set(output "${output}\n\n---\nUpdated on ${edited}") else() diff --git a/packaging/CMakeLists.txt b/packaging/CMakeLists.txt index 90e62afc5..4a981c681 100644 --- a/packaging/CMakeLists.txt +++ b/packaging/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright 2012-2019 Kai Pastor +# Copyright 2012-2020 Kai Pastor # # This file is part of OpenOrienteering. # @@ -234,17 +234,37 @@ unset(MAPPER_LIB_HINTS) unset(MAPPER_LIBS) if(Mapper_PACKAGE_PROJ) - if(PROJ4_DIR AND NOT PROJ4_ROOT) - # Cf. find_package documentation - string(REGEX REPLACE "/CMake$|/cmake$" "" PROJ4_ROOT "${PROJ4_DIR}") # U - string(REGEX REPLACE "/PROJ4[^/]*$|/proj4[^/]*$" "" PROJ4_ROOT "${PROJ4_ROOT}") # U, W - string(REGEX REPLACE "/cmake$" "" PROJ4_ROOT "${PROJ4_ROOT}") # U - string(REGEX REPLACE "/lib/[^/]*$|/lib$|/share$" "" PROJ4_ROOT "${PROJ4_ROOT}") # U, W - elseif(NOT PROJ4_ROOT) - message(FATAL_ERROR "PROJ4_ROOT must be set when Mapper_PACKAGE_PROJ is enabled.") + if(NOT PROJ_DATA_DIR) + unset(PROJ_DATA_DIR CACHE) + if(PROJ4_ROOT) + set(proj_data_paths "${PROJ4_ROOT}/share/proj") + elseif(PROJ4_DIR) + # Cf. find_package documentation + string(REGEX REPLACE "/CMake$|/cmake$" "" proj_data_paths "${PROJ4_DIR}") # U + string(REGEX REPLACE "/PROJ4[^/]*$|/proj4[^/]*$" "" proj_data_paths "${proj_data_paths}") # U, W + string(REGEX REPLACE "/cmake$" "" proj_data_paths "${proj_data_paths}") # U + string(REGEX REPLACE "/lib/[^/]*$|/lib$|/share$" "" proj_data_paths "${proj_data_paths}") # U, W + set(proj_data_paths "${proj_data_paths}/share/proj") + elseif(PROJ4_INCLUDE_DIRS) + string(REGEX REPLACE "/include$" "/share/proj" proj_data_paths ${PROJ4_INCLUDE_DIRS}) # MSYS2 et al. + else() + set(proj_data_paths PATHS "${CMAKE_INSTALL_PREFIX}/share/proj") + endif() + find_path(PROJ_DATA_DIR + DOC "The PROJ data files directory" + NAMES epsg proj.db + PATHS ${proj_data_paths} + NO_DEFAULT_PATH + ) + endif() + if(NOT PROJ_DATA_DIR) + message(SEND_ERROR + "PROJ_DATA_DIR must be found or set " + "when Mapper_BUILD_PACKAGE and Mapper_PACKAGE_PROJ are enabled." + ) endif() install( - DIRECTORY "${PROJ4_ROOT}/share/proj" + DIRECTORY "${PROJ_DATA_DIR}" DESTINATION "${MAPPER_DATA_DESTINATION}") list(APPEND MAPPER_LIB_HINTS ${PROJ4_ROOT}/bin) endif() @@ -254,19 +274,33 @@ if(Mapper_PACKAGE_GDAL) unset(GDAL_CONFIG CACHE) find_program(GDAL_CONFIG gdal-config ONLY_CMAKE_FIND_ROOT_PATH) if(GDAL_CONFIG) - exec_program(${GDAL_CONFIG} ARGS --datadir OUTPUT_VARIABLE gdal_data_dir) - endif() - if(gdal_data_dir) - # Search in CMAKE_FIND_ROOT_PATH - find_path(GDAL_DATA_DIR - NAMES ellipsoid.csv - HINTS ${gdal_data_dir} + execute_process( + COMMAND "${GDAL_CONFIG}" --datadir + OUTPUT_VARIABLE gdal_data_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE gdal_config_result ) endif() + if(GDAL_CONFIG AND gdal_config_result EQUAL 0) + set(gdal_data_paths "${gdal_data_dir}") + elseif(GDAL_INCLUDE_DIRS) + string(REGEX REPLACE "/include$" "/share/gdal" gdal_data_paths ${GDAL_INCLUDE_DIRS}) # MSYS2 et al. + else() + set(gdal_data_paths PATHS "${CMAKE_INSTALL_PREFIX}/share/gdal") + endif() + find_path(GDAL_DATA_DIR + DOC "The GDAL data files directory" + NAMES ellipsoid.csv gdalvrt.xsd + PATHS ${gdal_data_paths} + NO_DEFAULT_PATH + ) endif() if(NOT GDAL_DATA_DIR) - message(FATAL_ERROR "The gdal-config script must be available, or GDAL_DATA_DIR must be set, " - "when Mapper_PACKAGE_GDAL is enabled.") + message(SEND_ERROR + "The gdal-config script must be available, " + "or GDAL_DATA_DIR must be set, " + "when Mapper_BUILD_PACKAGE and Mapper_PACKAGE_GDAL are enabled." + ) endif() install( DIRECTORY "${GDAL_DATA_DIR}/" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a6d7d3f34..48130fcb5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -120,16 +120,19 @@ set(Mapper_Common_SRCS core/symbols/symbol_icon_decorator.cpp core/symbols/text_symbol.cpp + fileformats/course_file_format.cpp fileformats/file_format.cpp fileformats/file_format_registry.cpp fileformats/file_import_export.cpp - fileformats/ocad8_file_format.cpp + fileformats/iof_course_export.cpp + fileformats/kml_course_export.cpp fileformats/ocd_file_export.cpp fileformats/ocd_file_format.cpp fileformats/ocd_file_import.cpp fileformats/ocd_georef_fields.cpp fileformats/ocd_icon.cpp fileformats/ocd_types.cpp + fileformats/simple_course_export.cpp fileformats/xml_file_format.cpp gui/about_dialog.cpp @@ -147,6 +150,7 @@ set(Mapper_Common_SRCS gui/print_widget.cpp gui/select_crs_dialog.cpp gui/settings_dialog.cpp + gui/simple_course_dialog.cpp gui/task_dialog.cpp gui/text_browser_dialog.cpp gui/touch_cursor.cpp @@ -176,6 +180,7 @@ set(Mapper_Common_SRCS gui/widgets/action_grid_bar.cpp gui/widgets/color_dropdown.cpp gui/widgets/color_list_widget.cpp + gui/widgets/color_wheel_widget.cpp gui/widgets/compass_display.cpp gui/widgets/crs_param_widgets.cpp gui/widgets/crs_selector.cpp @@ -185,6 +190,7 @@ set(Mapper_Common_SRCS gui/widgets/key_button_bar.cpp gui/widgets/mapper_proxystyle.cpp gui/widgets/measure_widget.cpp + gui/widgets/paint_on_template_settings_page.cpp gui/widgets/pie_menu.cpp gui/widgets/segmented_button_layout.cpp gui/widgets/settings_page.cpp @@ -249,6 +255,7 @@ set(Mapper_Common_SRCS util/encoding.cpp util/item_delegates.cpp + util/key_value_container.cpp util/mapper_service_proxy.cpp util/matrix.cpp util/overriding_shortcut.cpp @@ -268,7 +275,6 @@ set(Mapper_Common_HEADERS core/renderables/renderable_implementation.h fileformats/file_import_export.h # translations - fileformats/ocad8_file_format_p.h fileformats/ocd_file_import.h # translations fileformats/ocd_types.h fileformats/ocd_types_v8.h @@ -294,7 +300,6 @@ add_dependencies(Mapper_Common Mapper_prerequisites ) target_link_libraries(Mapper_Common - libocad Polyclipping::Polyclipping PROJ4::proj Qt5::Widgets diff --git a/src/core/georeferencing.cpp b/src/core/georeferencing.cpp index f2bee4d21..43b28fec6 100644 --- a/src/core/georeferencing.cpp +++ b/src/core/georeferencing.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 Kai Pastor + * Copyright 2012-2021 Kai Pastor * * This file is part of OpenOrienteering. * @@ -52,6 +52,10 @@ #include "fileformats/xml_file_format.h" #include "util/xml_stream_util.h" +#ifdef MAPPER_USE_GDAL +# include "gdal/gdal_manager.h" // IWYU pragma: keep +#endif + // ### A namespace which collects various string constants of type QLatin1String. ### @@ -214,8 +218,11 @@ namespace if (proj_data.exists()) { static auto const location = proj_data.absoluteFilePath().toLocal8Bit(); - static auto* const data = location.constData(); - proj_context_set_search_paths(nullptr, 1, &data); + static const char* const data[2] = { location.constData(), nullptr }; + proj_context_set_search_paths(nullptr, 1, data); +#if defined(MAPPER_USE_GDAL)// && !defined(QT_TESTLIB_LIB) + GdalManager::setProjSearchPaths(data); +#endif } #endif // ACCEPT_USE_OF_DEPRECATED_PROJ_API_H } @@ -282,12 +289,7 @@ ProjTransform& ProjTransform::operator=(ProjTransform&& other) noexcept // static ProjTransform ProjTransform::crs(const QString& crs_spec) { - ProjTransform result; - auto crs_spec_latin1 = crs_spec.toLatin1(); - if (!crs_spec_latin1.contains("+no_defs")) - crs_spec_latin1.append(" +no_defs"); - result.pj = pj_init_plus(crs_spec_latin1); - return result; + return ProjTransform(crs_spec); } bool ProjTransform::isValid() const noexcept @@ -304,10 +306,12 @@ QPointF ProjTransform::forward(const LatLon& lat_lon, bool* ok) const { static auto const geographic_crs = ProjTransform(Georeferencing::geographic_crs_spec); - double easting = qDegreesToRadians(lat_lon.longitude()), northing = qDegreesToRadians(lat_lon.latitude()); + auto point = isGeographic() + ? QPointF{lat_lon.longitude(), lat_lon.latitude()} + : QPointF{qDegreesToRadians(lat_lon.longitude()), qDegreesToRadians(lat_lon.latitude())}; if (geographic_crs.isValid()) { - auto ret = pj_transform(geographic_crs.pj, pj, 1, 1, &easting, &northing, nullptr); + auto ret = pj_transform(geographic_crs.pj, pj, 1, 1, &point.rx(), &point.ry(), nullptr); if (ok) *ok = (ret == 0); } @@ -315,7 +319,7 @@ QPointF ProjTransform::forward(const LatLon& lat_lon, bool* ok) const { *ok = false; } - return {easting, northing}; + return point; } LatLon ProjTransform::inverse(const QPointF& projected_coords, bool* ok) const @@ -333,7 +337,7 @@ LatLon ProjTransform::inverse(const QPointF& projected_coords, bool* ok) const { *ok = false; } - return LatLon::fromRadiant(northing, easting); + return isGeographic() ? LatLon{northing, easting} : LatLon::fromRadiant(northing, easting); } QString ProjTransform::errorText() const @@ -344,6 +348,20 @@ QString ProjTransform::errorText() const #else +namespace { + +QByteArray withTypeCrs(QByteArray crs_spec_utf8) +{ + if ((crs_spec_utf8.startsWith("+proj=") || crs_spec_utf8.startsWith("+init=")) + && !crs_spec_utf8.contains("+type=crs")) + { + crs_spec_utf8.append(" +type=crs"); + } + return crs_spec_utf8; +} + +} + ProjTransform::ProjTransform(ProjTransformData* pj) noexcept : pj{pj} {} @@ -358,12 +376,14 @@ ProjTransform::ProjTransform(const QString& crs_spec) if (crs_spec.isEmpty()) return; - auto spec_latin1 = crs_spec.toLatin1(); + static auto const geographic_crs_spec_utf8 = Georeferencing::geographic_crs_spec.toUtf8(); + + auto crs_spec_utf8 = crs_spec.toUtf8(); #ifdef PROJ_ISSUE_1573 // Cf. https://github.com/OSGeo/PROJ/pull/1573 - spec_latin1.replace("+datum=potsdam", "+ellps=bessel +nadgrids=@BETA2007.gsb"); + crs_spec_utf8.replace("+datum=potsdam", "+ellps=bessel +nadgrids=@BETA2007.gsb"); #endif - pj = proj_create_crs_to_crs(PJ_DEFAULT_CTX, Georeferencing::geographic_crs_spec.toLatin1(), spec_latin1, nullptr); + pj = proj_create_crs_to_crs(PJ_DEFAULT_CTX, geographic_crs_spec_utf8, crs_spec_utf8, nullptr); if (pj) operator=({proj_normalize_for_visualization(PJ_DEFAULT_CTX, pj)}); } @@ -384,7 +404,7 @@ ProjTransform& ProjTransform::operator=(ProjTransform&& other) noexcept ProjTransform ProjTransform::crs(const QString& crs_spec) { ProjTransform result; - auto crs_spec_utf8 = crs_spec.toUtf8(); + auto crs_spec_utf8 = withTypeCrs(crs_spec.toUtf8().trimmed()); #ifdef PROJ_ISSUE_1573 // Cf. https://github.com/OSGeo/PROJ/pull/1573 crs_spec_utf8.replace("+datum=potsdam", "+ellps=bessel +nadgrids=@BETA2007.gsb"); @@ -400,10 +420,17 @@ bool ProjTransform::isValid() const noexcept bool ProjTransform::isGeographic() const { - if (!isValid()) - return false; + auto type = pj ? proj_get_type(pj) : PJ_TYPE_UNKNOWN; + if (type == PJ_TYPE_BOUND_CRS) + { + // "Coordinates referring to a BoundCRS are expressed into its source/base CRS." + // (https://proj.org/development/reference/cpp/crs.html) + auto* base_crs = proj_get_source_crs(nullptr, pj); + type = proj_get_type(base_crs); + proj_destroy(base_crs); + } - switch (proj_get_type(pj)) + switch (type) { case PJ_TYPE_GEOGRAPHIC_CRS: case PJ_TYPE_GEOGRAPHIC_2D_CRS: @@ -412,7 +439,6 @@ bool ProjTransform::isGeographic() const default: return false; } - } QPointF ProjTransform::forward(const LatLon& lat_lon, bool* ok) const @@ -529,7 +555,7 @@ Georeferencing& Georeferencing::operator=(const Georeferencing& other) bool Georeferencing::isGeographic() const { - return ProjTransform::crs(getProjectedCRSSpec()).isGeographic(); + return getState() == Geospatial && ProjTransform::crs(getProjectedCRSSpec()).isGeographic(); } @@ -663,10 +689,7 @@ void Georeferencing::load(QXmlStreamReader& xml, bool load_scale_only) if (!projected_crs_spec.isEmpty()) { proj_transform = {projected_crs_spec}; - if (proj_transform.isValid()) - { - state = Normal; - } + state = proj_transform.isValid() ? Geospatial : BrokenGeospatial; updateGridCompensation(); if (!georef_element.hasAttribute(literal::auxiliary_scale_factor)) { @@ -733,7 +756,7 @@ void Georeferencing::save(QXmlStreamWriter& xml) const } } - if (state == Normal) + if (getState() == Geospatial) { XmlElementWriter crs_element(xml, literal::geographic_crs); crs_element.writeAttribute(literal::id, literal::geographic_coordinates); @@ -758,14 +781,14 @@ void Georeferencing::save(QXmlStreamWriter& xml) const } -void Georeferencing::setState(Georeferencing::State value) +void Georeferencing::setState(Georeferencing::State const value) { - if (state != value) + if (getState() != value) { state = value; updateTransformation(); - if (state != Normal) + if (value == Local) { setProjectedCRS(QStringLiteral("Local"), {}); } @@ -857,20 +880,18 @@ void Georeferencing::setMapRefPoint(const MapCoord& point) void Georeferencing::setProjectedRefPoint(const QPointF& point, bool update_grivation, bool update_scale_factor) { - if (projected_ref_point != point || state == Normal) + if (projected_ref_point != point || getState() == Geospatial) { projected_ref_point = point; bool ok = {}; LatLon new_geo_ref_point; - switch (state) + switch (getState()) { - default: - qWarning("Undefined georeferencing state"); - Q_FALLTHROUGH(); case Local: + case BrokenGeospatial: break; - case Normal: + case Geospatial: new_geo_ref_point = toGeographicCoords(point, &ok); if (ok && new_geo_ref_point != geographic_ref_point) { @@ -915,7 +936,7 @@ void Georeferencing::updateGridCompensation() convergence = 0.0; grid_scale_factor = 1.0; - if (state != Normal || !isValid()) + if (getState() != Geospatial) return; const double delta = 1000.0; // meters @@ -980,11 +1001,11 @@ void Georeferencing::updateGridCompensation() void Georeferencing::setGeographicRefPoint(LatLon lat_lon, bool update_grivation, bool update_scale_factor) { bool geo_ref_point_changed = geographic_ref_point != lat_lon; - if (geo_ref_point_changed || state == Normal) + if (geo_ref_point_changed || getState() == Geospatial) { geographic_ref_point = lat_lon; - if (state != Normal) - setState(Normal); + if (getState() == Local) + setState(BrokenGeospatial); bool ok = {}; QPointF new_projected_ref = toProjectedCoords(lat_lon, &ok); @@ -1069,7 +1090,7 @@ const QTransform& Georeferencing::projectedToMap() const bool Georeferencing::setProjectedCRS(const QString& id, QString spec, std::vector params) { // Default return value if no change is necessary - bool ok = (state == Normal || projected_crs_spec.isEmpty()); + bool ok = (getState() == Geospatial || projected_crs_spec.isEmpty()); for (const auto& substitution : spec_substitutions) { @@ -1093,17 +1114,21 @@ bool Georeferencing::setProjectedCRS(const QString& id, QString spec, std::vecto { projected_crs_parameters.clear(); proj_transform = {}; - ok = (state != Normal); + ok = (getState() == Local); + if (!ok) + setState(BrokenGeospatial); } else { projected_crs_parameters.swap(params); // params was passed by value! proj_transform = {projected_crs_spec}; ok = proj_transform.isValid(); - if (ok && state != Normal) - setState(Normal); + if (ok) + setState(Geospatial); + else + setState(BrokenGeospatial); } - if (isValid() && !isLocal()) + if (getState() == Geospatial) updateGridCompensation(); emit projectionChanged(); @@ -1166,10 +1191,10 @@ MapCoordF Georeferencing::toMapCoordF(const Georeferencing* other, const MapCoor return map_coords; } - if (isLocal() || other->isLocal()) + if (getState() != Geospatial || other->getState() != Geospatial) { if (ok) - *ok = true; + *ok = getState() != BrokenGeospatial && other->getState() != BrokenGeospatial; return toMapCoordF(other->toProjectedCoords(map_coords)); } @@ -1216,6 +1241,18 @@ QString Georeferencing::degToDMS(double val) QDebug operator<<(QDebug dbg, const Georeferencing &georef) { + auto state = [](auto state) -> const char* { + switch (state) + { + case Georeferencing::Local: + return "local"; + case Georeferencing::BrokenGeospatial: + return "broken"; + case Georeferencing::Geospatial: + return "geospatial"; + } + return "unknown"; + }; dbg.nospace() << "Georeferencing(1:" << georef.scale_denominator << " " << georef.combined_scale_factor @@ -1223,12 +1260,9 @@ QDebug operator<<(QDebug dbg, const Georeferencing &georef) << " " << georef.grivation << "deg, " << georef.projected_crs_id << " (" << georef.projected_crs_spec - << ") " << QString::number(georef.projected_ref_point.x(), 'f', 8) << "," << QString::number(georef.projected_ref_point.y(), 'f', 8); - if (georef.isLocal()) - dbg.nospace() << ", local)"; - else - dbg.nospace() << ", geographic)"; - + << ") " << QString::number(georef.projected_ref_point.x(), 'f', 8) << "," << QString::number(georef.projected_ref_point.y(), 'f', 8) + << ", " << state(georef.getState()) + << ")"; return dbg.space(); } diff --git a/src/core/georeferencing.h b/src/core/georeferencing.h index c27c20b59..144295a2d 100644 --- a/src/core/georeferencing.h +++ b/src/core/georeferencing.h @@ -96,13 +96,17 @@ struct ProjTransform * of the coordinate reference system of the projected coordinates. The actual * geographic transformation is done by the PROJ library for geographic * projections. - * - * If no (valid) specification is given, the projected coordinates are regarded - * as local coordinates. Local coordinates cannot be converted to other - * geographic coordinate systems. The georeferencing is "local". * * Conversions between "map coordinates" and "geographic coordinates" use the * projected coordinates as intermediate step. + * + * If no geospatial specification of the projected coordinate references system + * is given, the projected coordinates are regarded as "local coordinates". The + * georeferencing is "local". + * + * If the georeferencing is local, or if the specification of the coordinate + * reference system is invalid, coordinates cannot be converted to other + * geospatial coordinate systems. */ class Georeferencing : public QObject // clazy:exclude=copyable-polymorphic { @@ -115,11 +119,14 @@ friend QDebug operator<<(QDebug dbg, const Georeferencing& georef); enum State { /// Only conversions between map and local projected coordinates are possible. - Local = 1, + Local, + + /// Only conversions between map and local projected coordinates are possible, + /// because the given projected CRS is not usable. + BrokenGeospatial, - /// All coordinate conversions are possible (if there is no error in the - /// crs specification). - Normal = 2 + /// All coordinate conversions are possible. + Geospatial, }; @@ -203,24 +210,6 @@ friend QDebug operator<<(QDebug dbg, const Georeferencing& georef); Georeferencing& operator=(const Georeferencing& other); - /** - * Returns if the georeferencing settings are valid. - * - * This means that coordinates can be converted between map and projected - * coordinates. isLocal() can be checked to determine if a conversion - * to geographic coordinates is also possible. - */ - bool isValid() const; - - /** - * Returns true if this georeferencing is local. - * - * A georeferencing is local if no (valid) coordinate system specification - * is given for the projected coordinates. A local georeferencing cannot - * convert coordinates from and to geographic coordinate systems. - */ - bool isLocal() const; - /** * Returns true if the "projected CRS" is actually geographic. * @@ -232,17 +221,21 @@ friend QDebug operator<<(QDebug dbg, const Georeferencing& georef); /** * Returns the georeferencing state. */ - State getState() const; + State getState() const noexcept { return state; } + + /** + * Switches the georeferencing to local state. + */ + void setLocalState() { setState(Georeferencing::Local); } +protected: /** * Sets the georeferencing state. - * - * This is only necessary to decrease the state to Local, as otherwise it - * will be automatically changed when setting the respective values. */ void setState(State value); +public: /** * Returns the principal scale denominator. * @@ -423,6 +416,10 @@ friend QDebug operator<<(QDebug dbg, const Georeferencing& georef); * configuration of geographic reference point, projected reference point, * declination and grivation. * + * If the spec is an empty (zero-length) string, the georeferencing will + * be in Local state when this function returns. In all other case, it will + * be either in Geospatial or BrokenGeospatial state. + * * @param id an identifier * @param spec the PROJ specification of the CRS * @param params parameter values (ignore for empty spec) @@ -720,24 +717,6 @@ double Georeferencing::roundDeclination(double value) return floor(value*100.0+0.5)/100.0; } -inline -bool Georeferencing::isValid() const -{ - return state == Local || proj_transform.isValid(); -} - -inline -bool Georeferencing::isLocal() const -{ - return state == Local; -} - -inline -Georeferencing::State Georeferencing::getState() const -{ - return state; -} - inline unsigned int Georeferencing::getScaleDenominator() const { diff --git a/src/core/map.cpp b/src/core/map.cpp index 9619cd3b6..59a26d7b7 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -1985,7 +1985,7 @@ void Map::loadTemplateFilesAsync(MapView& view, std::functiongetTemplateState() == Template::Unloaded && view.getTemplateVisibility(temp.get()).visible) { - QTimer::singleShot(10, temp.get(), ([this, &view, &temp, log = std::move(listener)]() { + QTimer::singleShot(10, temp.get(), ([this, &view, temp = temp.get(), log = std::move(listener)]() { log(qApp->translate("OpenOrienteering::MainWindow", "Opening %1") .arg(temp->getTemplateFilename())); if (temp->getTemplateState() != Template::Loaded) diff --git a/src/core/map.h b/src/core/map.h index 9fe041eb6..237b9a76e 100644 --- a/src/core/map.h +++ b/src/core/map.h @@ -91,7 +91,6 @@ class Map : public QObject Q_OBJECT friend class MapTest; friend class MapRenderables; -friend class OCAD8FileImport; friend class XMLFileImporter; friend class XMLFileExporter; public: diff --git a/src/core/map_part.h b/src/core/map_part.h index 6da2bfe9c..396073b86 100644 --- a/src/core/map_part.h +++ b/src/core/map_part.h @@ -67,7 +67,6 @@ using SelectionInfoVector = std::vector> ; */ class MapPart { -friend class OCAD8FileImport; public: /** * Creates a new map part with the given name for a map. diff --git a/src/core/map_printer.cpp b/src/core/map_printer.cpp index bc0af5138..a5bea1a9a 100644 --- a/src/core/map_printer.cpp +++ b/src/core/map_printer.cpp @@ -1,6 +1,6 @@ /* * Copyright 2012, 2013 Thomas Schöps - * Copyright 2012-2018 Kai Pastor + * Copyright 2012-2020 Kai Pastor * * This file is part of OpenOrienteering. * @@ -22,18 +22,24 @@ #include "map_printer.h" #include +#include #include #include +#include #include +#include #include #include #include #include -#include #include // IWYU pragma: keep #include +#include #include +#include +#include +#include #include #include #include @@ -51,6 +57,7 @@ #include "core/georeferencing.h" #include "core/map.h" #include "core/map_color.h" +#include "core/map_grid.h" #include "core/map_view.h" #include "core/renderables/renderable.h" #include "templates/template.h" @@ -391,6 +398,12 @@ const QPrinterInfo* MapPrinter::imageTarget() return &image_target; } +const QPrinterInfo* MapPrinter::kmzTarget() +{ + static QPrinterInfo kmz_target; // TODO: set name and features? + return &kmz_target; +} + // QPageSize (::key(), ::name()) made this list mostly obsolete. // But we keep it in v0.9 for loading maps where we used names @@ -454,6 +467,8 @@ void MapPrinter::setTarget(const QPrinterInfo* new_target) target = new_target; else if (new_target == imageTarget()) target = new_target; + else if (new_target == kmzTarget()) + target = new_target; else { // We don't own this target, so we need to make a copy. @@ -461,7 +476,8 @@ void MapPrinter::setTarget(const QPrinterInfo* new_target) target = &target_copy; } - if (old_target == imageTarget() || new_target == imageTarget()) + if (old_target == imageTarget() || new_target == imageTarget() + || old_target == kmzTarget() || new_target == kmzTarget()) { // No page margins. Will emit pageFormatChanged( ). setCustomPageSize(page_format.page_rect.size()); @@ -532,12 +548,18 @@ std::unique_ptr MapPrinter::makePrinter() const return printer; } -bool MapPrinter::isPrinter() const +bool MapPrinter::isPrinter() const noexcept { - bool is_printer = target - && target != imageTarget() - && target != pdfTarget(); - return is_printer; + return isPrinter(target); +} + +// static +bool MapPrinter::isPrinter(const QPrinterInfo* const target) noexcept +{ + return target + && target != imageTarget() + && target != kmzTarget() + && target != pdfTarget(); } // slot @@ -547,7 +569,7 @@ void MapPrinter::setPrintArea(const QRectF& area) { print_area = area; - if (target == imageTarget() && print_area.size() != page_format.paper_dimensions) + if ((target == imageTarget() || target == kmzTarget()) && print_area.size() != page_format.paper_dimensions) setCustomPageSize(print_area.size() * scale_adjustment); updatePageBreaks(); @@ -627,7 +649,7 @@ void MapPrinter::setOverlap(qreal h_overlap, qreal v_overlap) void MapPrinter::updatePaperDimensions() { - if (target == imageTarget() && page_format.page_size == QPageSize::Custom) + if ((target == imageTarget() || target == kmzTarget()) && page_format.page_size == QPageSize::Custom) { // No margins, no need to query QPrinter. page_format.page_rect = QRectF(QPointF(0.0, 0.0), page_format.paper_dimensions); @@ -639,7 +661,7 @@ void MapPrinter::updatePaperDimensions() QPrinter* printer = target ? new QPrinter(*target, QPrinter::HighResolution) : new QPrinter(QPrinter::HighResolution); - if (!printer->isValid() || target == imageTarget() || target == pdfTarget()) + if (!printer->isValid() || target == imageTarget() || target == kmzTarget() || target == pdfTarget()) printer->setOutputFormat(QPrinter::PdfFormat); if (page_format.page_size == QPageSize::Custom) @@ -657,7 +679,7 @@ void MapPrinter::updatePaperDimensions() page_format.page_rect = printer->paperRect(QPrinter::Millimeter); page_format.paper_dimensions = page_format.page_rect.size(); - if ( target != imageTarget() && target != pdfTarget() && + if ( target != imageTarget() && target != kmzTarget() && target != pdfTarget() && page_format.page_size != QPageSize::Custom ) { page_format.page_rect = printer->pageRect(QPrinter::Millimeter); @@ -852,7 +874,8 @@ void MapPrinter::updatePageBreaks() const qreal right_bound = print_area.right() - h_overlap - 0.05; if (page_width >= 0.01) { - for (h_pos += page_width; h_pos < right_bound; h_pos += page_width) + auto const max_size = std::size_t(std::ceil((right_bound - h_pos) / page_width)); + for (h_pos += page_width; h_page_pos.size() < max_size; h_pos += page_width) h_page_pos.push_back(h_pos); // Center the print area on the pages total area. @@ -870,7 +893,8 @@ void MapPrinter::updatePageBreaks() const qreal bottom_bound = print_area.bottom() - v_overlap - 0.05; if (page_height >= 0.01) { - for (v_pos += page_height; v_pos < bottom_bound; v_pos += page_height) + auto const max_size = std::size_t(std::ceil((bottom_bound - v_pos) / page_height)); + for (v_pos += page_height; v_page_pos.size() < max_size; v_pos += page_height) v_page_pos.push_back(v_pos); // Don't pre-calculate offset to avoid FP precision problems @@ -896,7 +920,7 @@ void MapPrinter::takePrinterSettings(const QPrinter* printer) if (!printer) return; MapPrinterPageFormat f(*printer); - if (target == pdfTarget() || target == imageTarget()) + if (target == pdfTarget() || target == imageTarget() || target == kmzTarget()) { f.page_rect = QRectF(QPointF(0.0, 0.0), f.paper_dimensions); } @@ -921,6 +945,20 @@ void MapPrinter::takePrinterSettings(const QPrinter* printer) void MapPrinter::drawPage(QPainter* device_painter, const QRectF& page_extent, QImage* page_buffer) const +{ + // Determine transformation and clipping for page extent and region + const qreal units_per_mm = options.resolution / 25.4; + // Translate for top left page margin + auto transform = QTransform::fromScale(units_per_mm, units_per_mm); + transform.translate(page_format.page_rect.left(), page_format.page_rect.top()); + // Convert native map scale to print scale + transform.scale(scale_adjustment, scale_adjustment); + // Translate and clip for margins and print area + transform.translate(-page_extent.left(), -page_extent.top()); + drawPage(device_painter, page_extent, transform, page_buffer); +} + +void MapPrinter::drawPage(QPainter* device_painter, const QRectF& page_extent, const QTransform& page_extent_transform, QImage* page_buffer) const { // Logical units per mm const qreal units_per_mm = options.resolution / 25.4; @@ -930,18 +968,6 @@ void MapPrinter::drawPage(QPainter* device_painter, const QRectF& page_extent, Q | QPainter::Antialiasing | QPainter::SmoothPixmapTransform; - // Determine transformation and clipping for page extent and region - const auto page_extent_transform = [this, units_per_mm, page_extent]() { - // Translate for top left page margin - auto transform = QTransform::fromScale(units_per_mm, units_per_mm); - transform.translate(page_format.page_rect.left(), page_format.page_rect.top()); - // Convert native map scale to print scale - transform.scale(scale_adjustment, scale_adjustment); - // Translate and clip for margins and print area - transform.translate(-page_extent.left(), -page_extent.top()); - return transform; - }(); - const auto page_region_used = page_extent.intersected(print_area); @@ -964,7 +990,7 @@ void MapPrinter::drawPage(QPainter* device_painter, const QRectF& page_extent, Q * When the target is an image, use the temporary image to enforce the given * resolution. */ - const bool use_buffer_for_map = rasterModeSelected() || target == imageTarget() || engineWillRasterize(); + const bool use_buffer_for_map = rasterModeSelected() || target == imageTarget() || target == kmzTarget() || engineWillRasterize(); bool use_page_buffer = use_buffer_for_map; auto first_front_template = map.getFirstFrontTemplate(); @@ -1232,7 +1258,7 @@ void MapPrinter::drawSeparationPages(QPrinter* printer, QPainter* device_painter // Translate and clip for margins and print area device_painter->translate(-page_extent.left(), -page_extent.top()); - device_painter->setClipRect(page_extent.intersected(print_area), Qt::ReplaceClip); + device_painter->setClipRect(page_extent.intersected(print_area).adjusted(-10, 10, 10, 10), Qt::ReplaceClip); bool need_new_page = false; for (int i = map.getNumColors() - 1; i >= 0; --i) diff --git a/src/core/map_printer.h b/src/core/map_printer.h index 65d521142..01a09e880 100644 --- a/src/core/map_printer.h +++ b/src/core/map_printer.h @@ -1,6 +1,6 @@ /* * Copyright 2012, 2013 Thomas Schöps - * Copyright 2012-2019 Kai Pastor + * Copyright 2012-2020 Kai Pastor * * This file is part of OpenOrienteering. * @@ -27,11 +27,11 @@ #include #include -#include +#include #include -#include #include #include +#include #ifdef QT_PRINTSUPPORT_LIB # include @@ -44,6 +44,8 @@ class QHash; class QImage; class QPainter; class QPrinter; +class QRectF; +class QSizeF; class QXmlStreamReader; class QXmlStreamWriter; @@ -229,6 +231,9 @@ Q_OBJECT /** Returns a QPrinterInfo pointer which signals printing to a raster file. */ static const QPrinterInfo* imageTarget(); + /** Returns a QPrinterInfo pointer which signals printing to a KML ground overlay. */ + static const QPrinterInfo* kmzTarget(); + /** Returns a reference to a hash which maps paper sizes to names as C strings. * These strings are not translated. * @@ -254,7 +259,10 @@ Q_OBJECT } /** Returns true if a real printer is configured. */ - bool isPrinter() const; + bool isPrinter() const noexcept; + + /** Returns true if the target is not representing a virtual printer. */ + static bool isPrinter(const QPrinterInfo* target) noexcept; /** Returns the page format specification. */ const MapPrinterPageFormat& getPageFormat() const @@ -361,6 +369,8 @@ Q_OBJECT * buffer but refers to the logical coordinates of device_painter. */ void drawPage(QPainter* device_painter, const QRectF& page_extent, QImage* page_buffer = nullptr) const; + void drawPage(QPainter* device_painter, const QRectF& page_extent, const QTransform& page_extent_transform, QImage* page_buffer = nullptr) const; + /** Draws the separations as distinct pages to the printer. */ void drawSeparationPages(QPrinter* printer, QPainter* device_painter, const QRectF& page_extent) const; diff --git a/src/core/objects/boolean_tool.cpp b/src/core/objects/boolean_tool.cpp index a579b1762..354031829 100644 --- a/src/core/objects/boolean_tool.cpp +++ b/src/core/objects/boolean_tool.cpp @@ -1,6 +1,6 @@ /* * Copyright 2012, 2013 Thomas Schöps - * Copyright 2014-2020 Kai Pastor + * Copyright 2014-2021 Kai Pastor * * This file is part of OpenOrienteering. * @@ -480,7 +480,8 @@ void BooleanTool::executeForLine(const PathObject* area, const PathObject* line, // First segment auto middle_length = intersections[0].length / 2; auto middle = SplitPathCoord::at(path_coords, middle_length); - if (area->isPointInsideArea(middle.pos) == (op == BooleanTool::Intersection)) + if (0.0 < intersections[0].length + && area->isPointInsideArea(middle.pos) == (op == BooleanTool::Intersection)) { PathObject* segment = line->duplicate(); segment->changePathBounds(0, 0.0, intersections[0].length); @@ -492,7 +493,8 @@ void BooleanTool::executeForLine(const PathObject* area, const PathObject* line, { middle_length = (intersections[i].length + intersections[i+1].length) / 2; auto middle = SplitPathCoord::at(path_coords, middle_length); - if (area->isPointInsideArea(middle.pos) == (op == BooleanTool::Intersection)) + if (intersections[i].length < intersections[i+1].length + && area->isPointInsideArea(middle.pos) == (op == BooleanTool::Intersection)) { PathObject* segment = line->duplicate(); segment->changePathBounds(0, intersections[i].length, intersections[i+1].length); @@ -503,7 +505,8 @@ void BooleanTool::executeForLine(const PathObject* area, const PathObject* line, // Last segment middle_length = (part.length() + intersections.back().length) / 2; middle = SplitPathCoord::at(path_coords, middle_length); - if (area->isPointInsideArea(middle.pos) == (op == BooleanTool::Intersection)) + if (intersections.back().length < part.length() + && area->isPointInsideArea(middle.pos) == (op == BooleanTool::Intersection)) { PathObject* segment = line->duplicate(); segment->changePathBounds(0, intersections.back().length, part.length()); diff --git a/src/core/objects/object.cpp b/src/core/objects/object.cpp index 3b89d0233..7e0804c7f 100644 --- a/src/core/objects/object.cpp +++ b/src/core/objects/object.cpp @@ -161,9 +161,6 @@ bool Object::equals(const Object* other, bool compare_symbol) const } } - if (object_tags != other->object_tags) - return false; - if (qAbs(getRotation() - other->getRotation()) >= 0.000001) // six decimal places in XML return false; @@ -189,7 +186,11 @@ bool Object::equals(const Object* other, bool compare_symbol) const return false; } - return true; + if (object_tags.empty()) + return other->object_tags.empty(); + + using std::begin; using std::end; + return std::is_permutation(object_tags.begin(), object_tags.end(), other->object_tags.begin(), other->object_tags.end()); } @@ -324,9 +325,12 @@ Object* Object::load(QXmlStreamReader& xml, Map* map, const SymbolDictionary& sy bool conversion_ok; const auto id_converted = symbol_id.toInt(&conversion_ok); if (!symbol_id.isEmpty() && !conversion_ok) + { + delete object; throw FileFormatException(::OpenOrienteering::ImportExport::tr("Malformed symbol ID '%1' at line %2 column %3.") .arg(symbol_id).arg(xml.lineNumber()) .arg(xml.columnNumber())); + } object->symbol = symbol_dict[id_converted]; // FIXME: cannot work for forward references // NOTE: object->symbol may be nullptr. } @@ -560,6 +564,12 @@ void Object::scale(double factor_x, double factor_y) setOutputDirty(); } +// virtual +void Object::rotatePatternOrigin(const MapCoordF& /*center*/, qreal /*sin_angle*/, qreal /*cos_angle*/) +{ + qDebug("Unexpected call to %s", Q_FUNC_INFO); +} + void Object::rotateAround(const MapCoordF& center, qreal angle) { auto sin_angle = std::sin(angle); @@ -572,7 +582,12 @@ void Object::rotateAround(const MapCoordF& center, qreal angle) coord.setY(center.y() - sin_angle * center_to_coord.x() + cos_angle * center_to_coord.y()); } - if (symbol->isRotatable()) + if (symbol->hasRotatableFillPattern()) + { + rotation += angle; + rotatePatternOrigin(center, sin_angle, cos_angle); + } + else if (symbol->isRotatable()) { rotation += angle; } @@ -591,7 +606,12 @@ void Object::rotate(qreal angle) coord.setY(- sin_angle * old_coord.x() + cos_angle * old_coord.y()); } - if (symbol->isRotatable()) + if (symbol->hasRotatableFillPattern()) + { + rotation += angle; + rotatePatternOrigin(MapCoordF{}, sin_angle, cos_angle); + } + else if (symbol->isRotatable()) { rotation += angle; } @@ -670,7 +690,7 @@ Object* Object::getObjectForType(Object::Type type, const Symbol* symbol) return nullptr; } -void Object::setTags(const Object::Tags& tags) +void Object::setTags(const KeyValueContainer& tags) { if (object_tags != tags) { @@ -684,11 +704,18 @@ void Object::setTags(const Object::Tags& tags) } } +QString OpenOrienteering::Object::getTag(const QString& key) const +{ + auto const it = object_tags.find(key); + return it == object_tags.end() ? QString{} : it->value; +} + void Object::setTag(const QString& key, const QString& value) { - if (!object_tags.contains(key) || object_tags.value(key) != value) + auto it = object_tags.find(key); + if (it == object_tags.end() || it->value != value) { - object_tags.insert(key, value); + object_tags.insert_or_assign(it, key, value); if (map) { map->setObjectsDirty(); @@ -700,9 +727,10 @@ void Object::setTag(const QString& key, const QString& value) void Object::removeTag(const QString& key) { - if (object_tags.contains(key)) + auto it = object_tags.find(key); + if (it != object_tags.end()) { - object_tags.remove(key); + object_tags.erase(it); if (map) map->setObjectsDirty(); } @@ -1074,6 +1102,14 @@ void PathObject::partSizeChanged(PathPartVector::iterator part, MapCoordVector:: } } +// override +void PathObject::rotatePatternOrigin(const MapCoordF& center, qreal sin_angle, qreal cos_angle) +{ + auto const center_to_coord = MapCoordF(pattern_origin.x() - center.x(), pattern_origin.y() - center.y()); + pattern_origin.setX(center.x() + cos_angle * center_to_coord.x() + sin_angle * center_to_coord.y()); + pattern_origin.setY(center.y() - sin_angle * center_to_coord.x() + cos_angle * center_to_coord.y()); +} + void PathObject::transform(const QTransform& t) { diff --git a/src/core/objects/object.h b/src/core/objects/object.h index b44c2d705..2757316a1 100644 --- a/src/core/objects/object.h +++ b/src/core/objects/object.h @@ -27,7 +27,6 @@ #include #include -#include #include #include // IWYU pragma: no_include @@ -37,6 +36,7 @@ #include "core/virtual_path.h" #include "core/renderables/renderable.h" #include "core/symbols/symbol.h" +#include "util/key_value_container.h" class QTransform; class QXmlStreamReader; @@ -66,7 +66,6 @@ class VirtualCoordVector; class Object // clazy:exclude=copyable-polymorphic { friend class ObjectRenderables; -friend class OCAD8FileImport; friend class XMLImportExport; public: /** Enumeration of possible object types. */ @@ -210,6 +209,12 @@ friend class XMLImportExport; */ virtual void scale(double factor_x, double factor_y); +protected: + /** Rotates the pattern origin around the center point. + * The angle must be given in radians. */ + virtual void rotatePatternOrigin(const MapCoordF& center, qreal sin_angle, qreal cos_angle); + +public: /** Rotates the whole object around the center point. * The angle must be given in radians. */ void rotateAround(const MapCoordF& center, qreal angle); @@ -291,14 +296,11 @@ friend class XMLImportExport; static Object* getObjectForType(Type type, const Symbol* symbol = nullptr); - /** Defines a type which maps keys to values, to be used for tagging objects. */ - typedef QHash Tags; - /** Returns a const reference to the object's tags. */ - const Tags& tags() const; + const KeyValueContainer& tags() const; /** Replaces the object's tags. */ - void setTags(const Tags& tags); + void setTags(const KeyValueContainer& tags); /** Returns the value of the given tag key. */ QString getTag(const QString& key) const; @@ -324,7 +326,7 @@ friend class XMLImportExport; const Symbol* symbol = nullptr; MapCoordVector coords; Map* map = nullptr; - Tags object_tags; + KeyValueContainer object_tags; private: qreal rotation = 0; ///< The object's rotation (in radians). @@ -903,6 +905,9 @@ class PathObject : public Object // clazy:exclude=copyable-polymorphic void partSizeChanged(PathPartVector::iterator part, MapCoordVector::difference_type change); + void rotatePatternOrigin(const MapCoordF& center, qreal sin_angle, qreal cos_angle) override; + + void prepareDeleteBezierPoint(MapCoordVector::size_type pos, int delete_bezier_point_action); /** @@ -1160,17 +1165,11 @@ Map* Object::getMap() const } inline -const Object::Tags& Object::tags() const +const KeyValueContainer& Object::tags() const { return object_tags; } -inline -QString Object::getTag(const QString& key) const -{ - return object_tags.value(key); -} - //### PathPart inline code ### @@ -1293,7 +1292,7 @@ double ObjectPathCoord::findClosestPointTo(const MapCoordF& map_coord) inline constexpr ObjectPathCoord::operator bool() const { - return bool { object }; + return static_cast(object); } } // namespace OpenOrienteering diff --git a/src/core/objects/object_query.cpp b/src/core/objects/object_query.cpp index 2e71c86de..6e5c9881b 100644 --- a/src/core/objects/object_query.cpp +++ b/src/core/objects/object_query.cpp @@ -25,11 +25,11 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -39,6 +39,7 @@ #include "core/objects/object.h" #include "core/objects/text_object.h" #include "core/symbols/symbol.h" +#include "util/key_value_container.h" // ### Local utilities ### @@ -397,24 +398,31 @@ QString ObjectQuery::labelFor(ObjectQuery::Operator op) bool ObjectQuery::operator()(const Object* object) const { - const auto& object_tags = object->tags(); - switch(op) { case OperatorIs: - return object_tags.contains(tags.key) && object_tags.value(tags.key) == tags.value; + return [](auto const& container, auto const& tags) { + auto const it = container.find(tags.key); + return it != container.end() && it->value == tags.value; + } (object->tags(), tags); case OperatorIsNot: // If the object does have the tag, not is true - return !object_tags.contains(tags.key) || object_tags.value(tags.key) != tags.value; + return [](auto const& container, auto const& tags) { + auto const it = container.find(tags.key); + return it == container.end() || it->value != tags.value; + } (object->tags(), tags); case OperatorContains: - return object_tags.contains(tags.key) && object_tags.value(tags.key).contains(tags.value); + return [](auto const& container, auto const& tags) { + auto const it = container.find(tags.key); + return it != container.end() && it->value.contains(tags.value); + } (object->tags(), tags); case OperatorSearch: if (object->getSymbol() && object->getSymbol()->getName().contains(tags.value, Qt::CaseInsensitive)) return true; - for (auto it = object_tags.begin(), last = object_tags.end(); it != last; ++it) + for (auto const& current : object->tags()) { - if (it.key().contains(tags.value, Qt::CaseInsensitive) - || it.value().contains(tags.value, Qt::CaseInsensitive)) + if (current.key.contains(tags.value, Qt::CaseInsensitive) + || current.value.contains(tags.value, Qt::CaseInsensitive)) return true; } return false; diff --git a/src/core/symbols/area_symbol.cpp b/src/core/symbols/area_symbol.cpp index 8b5f74b85..f972bfcfd 100644 --- a/src/core/symbols/area_symbol.cpp +++ b/src/core/symbols/area_symbol.cpp @@ -165,9 +165,6 @@ bool AreaSymbol::FillPattern::equals(const AreaSymbol::FillPattern& other, Qt::C return false; } - if (name.compare(other.name, case_sensitivity) != 0) - return false; - return true; } @@ -771,7 +768,7 @@ qreal AreaSymbol::dimensionForIcon() const } - +// override bool AreaSymbol::hasRotatableFillPattern() const { return std::any_of(begin(patterns), end(patterns), [](auto& pattern){ diff --git a/src/core/symbols/area_symbol.h b/src/core/symbols/area_symbol.h index 886c922d1..c066470b1 100644 --- a/src/core/symbols/area_symbol.h +++ b/src/core/symbols/area_symbol.h @@ -64,7 +64,6 @@ class AreaSymbol : public Symbol { friend class AreaSymbolSettings; friend class PointSymbolEditorWidget; -friend class OCAD8FileImport; public: /** Describes a fill pattern. */ struct FillPattern @@ -283,7 +282,7 @@ friend class OCAD8FileImport; inline void setNumFillPatterns(int count) {patterns.resize(std::size_t(count));} inline FillPattern& getFillPattern(int i) {return patterns[std::size_t(i)];} inline const FillPattern& getFillPattern(int i) const {return patterns[std::size_t(i)];} - bool hasRotatableFillPattern() const; + bool hasRotatableFillPattern() const override; SymbolPropertiesWidget* createPropertiesWidget(SymbolSettingDialog* dialog) override; protected: diff --git a/src/core/symbols/combined_symbol.cpp b/src/core/symbols/combined_symbol.cpp index 38f696a47..5141f8db8 100644 --- a/src/core/symbols/combined_symbol.cpp +++ b/src/core/symbols/combined_symbol.cpp @@ -408,4 +408,13 @@ void CombinedSymbol::setPart(int i, const Symbol* symbol, bool is_private) } +// override +bool CombinedSymbol::hasRotatableFillPattern() const +{ + return std::any_of(begin(parts), end(parts), [](auto const* part) { + return part && part->hasRotatableFillPattern(); + }); +} + + } // namespace OpenOrienteering diff --git a/src/core/symbols/combined_symbol.h b/src/core/symbols/combined_symbol.h index 464e4254d..fb61f441a 100644 --- a/src/core/symbols/combined_symbol.h +++ b/src/core/symbols/combined_symbol.h @@ -58,7 +58,6 @@ class CombinedSymbol : public Symbol { friend class CombinedSymbolSettings; friend class PointSymbolEditorWidget; -friend class OCAD8FileImport; public: CombinedSymbol(); ~CombinedSymbol() override; @@ -113,6 +112,8 @@ friend class OCAD8FileImport; inline bool isPartPrivate(int i) const {return private_parts[i];} inline void setPartPrivate(int i, bool set_private) {private_parts[i] = set_private;} + bool hasRotatableFillPattern() const override; + SymbolPropertiesWidget* createPropertiesWidget(SymbolSettingDialog* dialog) override; protected: diff --git a/src/core/symbols/line_symbol.cpp b/src/core/symbols/line_symbol.cpp index 93bf2873c..36b62be6d 100644 --- a/src/core/symbols/line_symbol.cpp +++ b/src/core/symbols/line_symbol.cpp @@ -2032,25 +2032,22 @@ bool LineSymbol::equalsImpl(const Symbol* other, Qt::CaseSensitivity case_sensit return false; } } + + auto const symbols_are_equal = [](const PointSymbol* lhs, const PointSymbol* rhs) -> bool { + return ((!lhs || lhs->isEmpty()) && (!rhs || rhs->isEmpty())) + || (lhs && rhs && lhs->equals(rhs)); + }; - if (bool(start_symbol) != bool(line->start_symbol)) - return false; - if (start_symbol && !start_symbol->equals(line->start_symbol)) + if (!symbols_are_equal(start_symbol, line->start_symbol)) return false; - if (bool(mid_symbol) != bool(line->mid_symbol)) - return false; - if (mid_symbol && !mid_symbol->equals(line->mid_symbol)) + if (!symbols_are_equal(mid_symbol, line->mid_symbol)) return false; - if (bool(end_symbol) != bool(line->end_symbol)) - return false; - if (end_symbol && !end_symbol->equals(line->end_symbol)) + if (!symbols_are_equal(end_symbol, line->end_symbol)) return false; - if (bool(dash_symbol) != bool(line->dash_symbol)) - return false; - if (dash_symbol && !dash_symbol->equals(line->dash_symbol)) + if (!symbols_are_equal(dash_symbol, line->dash_symbol)) return false; if (suppress_dash_symbol_at_ends != line->suppress_dash_symbol_at_ends) return false; diff --git a/src/core/symbols/line_symbol.h b/src/core/symbols/line_symbol.h index 655295131..40d77038c 100644 --- a/src/core/symbols/line_symbol.h +++ b/src/core/symbols/line_symbol.h @@ -96,7 +96,6 @@ class LineSymbol : public Symbol { friend class LineSymbolSettings; friend class PointSymbolEditorWidget; -friend class OCAD8FileImport; public: enum CapStyle { diff --git a/src/core/symbols/point_symbol.h b/src/core/symbols/point_symbol.h index 8b0177fc9..0a4838845 100644 --- a/src/core/symbols/point_symbol.h +++ b/src/core/symbols/point_symbol.h @@ -62,7 +62,6 @@ class PointSymbol : public Symbol { friend class PointSymbolSettings; friend class PointSymbolEditorWidget; -friend class OCAD8FileImport; friend class XMLImportExport; public: /** Constructs an empty point symbol. */ diff --git a/src/core/symbols/symbol.cpp b/src/core/symbols/symbol.cpp index 3a5101f82..a8019c9d6 100644 --- a/src/core/symbols/symbol.cpp +++ b/src/core/symbols/symbol.cpp @@ -869,6 +869,11 @@ QString Symbol::getNumberAsString() const } +// virtual +bool Symbol::hasRotatableFillPattern() const +{ + return false; +} void Symbol::setRotatable(bool value) { diff --git a/src/core/symbols/symbol.h b/src/core/symbols/symbol.h index c895eb98b..037a73bbe 100644 --- a/src/core/symbols/symbol.h +++ b/src/core/symbols/symbol.h @@ -494,6 +494,11 @@ class Symbol */ bool isRotatable() const { return is_rotatable; } + /** + * Returns if the objects has fill patterns which can be rotated in arbitrary directions. + */ + virtual bool hasRotatableFillPattern() const; + protected: /** * Sets the rotatability state of the symbol. diff --git a/src/core/symbols/text_symbol.h b/src/core/symbols/text_symbol.h index 367d7e7e9..14fa32a5b 100644 --- a/src/core/symbols/text_symbol.h +++ b/src/core/symbols/text_symbol.h @@ -60,7 +60,6 @@ class TextSymbol : public Symbol { friend class TextSymbolSettings; friend class PointSymbolEditorWidget; -friend class OCAD8FileImport; public: /** Modes for text framing */ enum FramingMode diff --git a/src/fileformats/course_file_format.cpp b/src/fileformats/course_file_format.cpp new file mode 100644 index 000000000..3351b4bd2 --- /dev/null +++ b/src/fileformats/course_file_format.cpp @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Kai Pastor + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#include "course_file_format.h" + +// IWYU pragma: no_include +#include + +#include + +#include "fileformats/file_import_export.h" +#include "fileformats/iof_course_export.h" +#include "fileformats/kml_course_export.h" + + +namespace OpenOrienteering { + +template +std::unique_ptr makeFileFormat(FileFormat::FileType type, const char* id) +{ + auto builder = [](const QString& path, const Map* map, const MapView* view) { + return std::make_unique(path, map, view); + }; + auto const description = Exporter::formatDescription(); + auto const file_extension = Exporter::filenameExtension(); + return std::make_unique(type, id, description, file_extension, builder); +} + + +// static +std::vector> CourseFileFormat::makeAll() +{ + std::vector> result; + result.reserve(2); + + result.push_back(makeFileFormat(SimpleCourseFile, "simple-iof-course")); + result.push_back(makeFileFormat(SimpleCourseFile, "simple-kml-course")); + return result; +} + + +CourseFileFormat::CourseFileFormat(FileType type, const char* id, const QString& description, const QString& file_extension, ExporterBuilder exporter_builder) +: FileFormat { type, id, description, file_extension, FileFormat::Feature::FileExport | FileFormat::Feature::WritingLossy } +, make_exporter { exporter_builder } +{ + // Nothing +} + + +std::unique_ptr CourseFileFormat::makeExporter(const QString& path, const Map* map, const MapView* view) const +{ + return make_exporter(path, map, view); +} + + +} // namespace OpenOrienteering diff --git a/src/fileformats/ocad8_file_format.h b/src/fileformats/course_file_format.h similarity index 53% rename from src/fileformats/ocad8_file_format.h rename to src/fileformats/course_file_format.h index 6a4a5b813..0b576858a 100644 --- a/src/fileformats/ocad8_file_format.h +++ b/src/fileformats/course_file_format.h @@ -1,6 +1,5 @@ /* - * Copyright 2012, 2013 Pete Curtis - * Copyright 2018 Kai Pastor + * Copyright 2021 Kai Pastor * * This file is part of OpenOrienteering. * @@ -18,36 +17,53 @@ * along with OpenOrienteering. If not, see . */ -#ifndef OPENORIENTEERING_OCAD8_FILE_FORMAT_H -#define OPENORIENTEERING_OCAD8_FILE_FORMAT_H +#ifndef OPENORIENTEERING_COURSE_FILE_FORMAT_H +#define OPENORIENTEERING_COURSE_FILE_FORMAT_H +#include #include +#include -#include "file_format.h" +#include "fileformats/file_format.h" + +class QString; -class QIODevice; namespace OpenOrienteering { class Exporter; -class Importer; class Map; class MapView; - -/** Representation of the format used by OCAD 8. +/** + * A family of formats representing courses. */ -class OCAD8FileFormat : public FileFormat +class CourseFileFormat : public FileFormat { public: - OCAD8FileFormat(); + using ExporterBuilder = std::function (const QString&, const Map*, const MapView*)>; + + /** + * Returns a container of all supported variants of this format. + */ + static std::vector> makeAll(); + - ImportSupportAssumption understands(const char* buffer, int size) const override; - std::unique_ptr makeImporter(const QString& path, Map *map, MapView *view) const override; + /** + * Constructs a new CourseFileFormat. + */ + CourseFileFormat(FileType type, const char* id, const QString& description, const QString& file_extension, ExporterBuilder exporter_builder); + + + /// \copydoc FileFormat::makeExporter() std::unique_ptr makeExporter(const QString& path, const Map* map, const MapView* view) const override; + +private: + ExporterBuilder make_exporter; + }; } // namespace OpenOrienteering -#endif // OCAD8_FILE_IMPORT_H +#endif // OPENORIENTEERING_COURSE_FILE_FORMAT_H diff --git a/src/fileformats/file_format.cpp b/src/fileformats/file_format.cpp index e8dc30c64..b6a76f2a2 100644 --- a/src/fileformats/file_format.cpp +++ b/src/fileformats/file_format.cpp @@ -1,6 +1,6 @@ /* * Copyright 2012, 2013 Pete Curtis - * Copyright 2018 Kai Pastor + * Copyright 2018-2020 Kai Pastor * * This file is part of OpenOrienteering. * @@ -20,7 +20,17 @@ #include "file_format.h" -#include "file_import_export.h" +#include +#include + +#include +#include +#include +#include +#include + +#include "mapper_config.h" +#include "fileformats/file_import_export.h" namespace OpenOrienteering { @@ -37,6 +47,21 @@ const char* FileFormatException::what() const noexcept } +// static +FileFormatException FileFormatException::internalError(const char* function_info) +{ + return FileFormatException { + QCoreApplication::translate("OpenOrienteering::Util", + "Internal error detected!" + " Please report this issue." + "\nVersion: %1" + "\nLocation: %2") + .arg(QString::fromUtf8(APP_VERSION), + QString::fromUtf8(function_info)) + }; +} + + // ### FileFormat ### @@ -59,7 +84,7 @@ FileFormat::~FileFormat() = default; void FileFormat::addExtension(const QString& file_extension) { file_extensions << file_extension; - format_filter = QString::fromLatin1("%1 (*.%2)").arg(format_description, file_extensions.join(QString::fromLatin1(" *."))); + format_filter.clear(); } @@ -80,6 +105,30 @@ FileFormat::ImportSupportAssumption FileFormat::understands(const char* /*buffer } +QString FileFormat::fixupExtension(QString filepath) const +{ + auto const& extensions = fileExtensions(); + if (!extensions.empty()) + { + using std::begin; using std::end; + auto const has_extension = std::any_of(begin(extensions), end(extensions), [&filepath](const auto& extension) { + return filepath.endsWith(extension, Qt::CaseInsensitive) + && filepath.midRef(filepath.length() - extension.length() - 1, 1) == QLatin1String("."); + }); + if (!has_extension) + { + if (!filepath.endsWith(QLatin1Char('.'))) + filepath.append(QLatin1Char('.')); + filepath.append(primaryExtension()); + } + + // Ensure that the file name matches the format. + Q_ASSERT(extensions.contains(QFileInfo(filepath).suffix())); + } + return filepath; +} + + std::unique_ptr FileFormat::makeImporter(const QString& /*path*/, Map* /*map*/, MapView* /*view*/) const { qWarning("Format '%s' does not support import", format_id); @@ -92,5 +141,20 @@ std::unique_ptr FileFormat::makeExporter(const QString& /*path*/, cons return nullptr; } +const QString& OpenOrienteering::FileFormat::filter() const +{ + if (format_filter.isEmpty()) + { + auto const label = [](QString description) { + description.replace(QLatin1Char('('), QLatin1Char('[')); + description.replace(QLatin1Char(')'), QLatin1Char(']')); + return description; + } (format_description); + auto const extensions = file_extensions.join(QStringLiteral(" *.")); + format_filter = label + QLatin1String(" (*.") + extensions + QLatin1String(")"); + } + return format_filter; +} + } // namespace OpenOrienteering diff --git a/src/fileformats/file_format.h b/src/fileformats/file_format.h index 4b6a3308b..05d80b933 100644 --- a/src/fileformats/file_format.h +++ b/src/fileformats/file_format.h @@ -1,6 +1,6 @@ /* * Copyright 2012, 2013 Pete Curtis - * Copyright 2018 Kai Pastor + * Copyright 2018-2020 Kai Pastor * * This file is part of OpenOrienteering. * @@ -70,12 +70,37 @@ class FileFormatException : public std::exception // clazy:exclude=copyable-pol */ const char* what() const noexcept override; + + /** + * Returns an exception object representing an internal error + * in the given function. + * + * This is a helper function used by the FILEFORMAT_ASSERT macro. + */ + static FileFormatException internalError(const char* function_info); + + private: QString const msg; QByteArray const msg_c; }; +/** + * Checks if the condition is true, and raises an exception otherwise. + * + * This macro is to be used by importer and exporter implementations instead + * of plain assert or Q_ASSERT: In most cases, program consistency is not + * affected by internal errors of importers or exporters, and so there is no + * reason to abort the program (debug builds) or to let it run into a crash + * (release build). However, on hot paths, evaluating the condition must not + * be expensive. + */ +#define FILEFORMAT_ASSERT(condition) \ + if (Q_UNLIKELY(!(condition))) throw FileFormatException::internalError(Q_FUNC_INFO); + + + /** Describes a file format understood by this application. Each file format has an ID * (an internal, non-translated string); a description (translated); a file extension * (non-translated); and methods to indicate their support for import or export. Formats @@ -113,6 +138,7 @@ class FileFormat { MapFile = 0x01, OgrFile = 0x02, ///< Geospatial vector data supported by OGR + SimpleCourseFile = 0x04,///< Course interchange formats AllFiles = MapFile, ///< All types which can be handled by an editor. @@ -271,6 +297,15 @@ class FileFormat virtual ImportSupportAssumption understands(const char* buffer, int size) const; + /** + * Returns a filepath which ends with one of the formats extensions. + * + * If the filepath does not already end with one the extensions, + * this function appends the primary extension (separated by dot). + */ + QString fixupExtension(QString filepath) const; + + /** * Creates an Importer that will read a map file from the given stream. * @@ -290,7 +325,7 @@ class FileFormat const char* format_id; QString format_description; QStringList file_extensions; - QString format_filter; + mutable QString format_filter; Features format_features; }; @@ -361,12 +396,6 @@ const QStringList& FileFormat::fileExtensions() const return file_extensions; } -inline -const QString& FileFormat::filter() const -{ - return format_filter; -} - } // namespace OpenOrienteering diff --git a/src/fileformats/file_format_registry.cpp b/src/fileformats/file_format_registry.cpp index 347db619e..7791f37f3 100644 --- a/src/fileformats/file_format_registry.cpp +++ b/src/fileformats/file_format_registry.cpp @@ -1,6 +1,6 @@ /* * Copyright 2012, 2013 Pete Curtis - * Copyright 2013, 2015-2018 Kai Pastor + * Copyright 2013, 2015-2020 Kai Pastor * * This file is part of OpenOrienteering. * @@ -123,8 +123,10 @@ const FileFormat *FileFormatRegistry::findFormatByFilter(const QString& filter, // Compare only before closing ')'. Needed for QTBUG 51712 workaround in // file_dialog.cpp, and warranted by Q_ASSERT in registerFormat(). return findFormat([predicate, filter](auto format) { + auto const label_len_filter = filter.lastIndexOf(QLatin1String(" (")); + auto const label_len_format = format->filter().lastIndexOf(QLatin1String(" (")); return (format->*predicate)() - && filter.startsWith(format->filter().leftRef(format->filter().length()-1)); + && filter.leftRef(label_len_filter) == format->filter().leftRef(label_len_format); }); } diff --git a/src/fileformats/file_import_export.cpp b/src/fileformats/file_import_export.cpp index 67177681c..8583ea3c8 100644 --- a/src/fileformats/file_import_export.cpp +++ b/src/fileformats/file_import_export.cpp @@ -145,7 +145,7 @@ void Importer::importFailed() void Importer::validate() { auto const& georef = map->getGeoreferencing(); - if (georef.isValid() && !georef.isLocal()) + if (georef.getState() == Georeferencing::Geospatial) { auto const expected = georef.getProjectedRefPoint(); auto const actual = georef.toProjectedCoords(georef.getGeographicRefPoint()); @@ -248,7 +248,7 @@ void Importer::validate() if (!error_string.isEmpty()) { addWarning(tr("Warnings when loading template '%1':\n%2") - .arg(temp->getTemplateFilename(), temp->errorString())); + .arg(temp->getTemplateFilename(), error_string)); } } @@ -291,26 +291,29 @@ bool Exporter::doExport() } if (!device_->isOpen() && !device_->open(QIODevice::WriteOnly)) { - addWarning(tr("Cannot save file\n%1:\n%2").arg(path, device_->errorString())); + addWarning(device_->errorString()); return false; } } + auto success = true; + // Save the map try { if (!exportImplementation()) { Q_ASSERT(!warnings().empty()); - return false; + success = false; } - if (managed_file && !managed_file->commit()) + if (success && managed_file && !managed_file->commit()) { - addWarning(tr("Cannot save file\n%1:\n%2").arg(path, managed_file->errorString())); - return false; + addWarning(managed_file->errorString()); + success = false; } #ifdef Q_OS_ANDROID // Make the MediaScanner aware of the *updated* file. - if (auto* file_device = qobject_cast(device_)) + auto* file_device = qobject_cast(device_); + if (success && file_device) { const auto file_info = QFileInfo(file_device->fileName()); Android::mediaScannerScanFile(file_info.absolutePath()); @@ -319,11 +322,31 @@ bool Exporter::doExport() } catch (std::exception &e) { - addWarning(tr("Cannot save file\n%1:\n%2").arg(path, QString::fromLocal8Bit(e.what()))); - return false; + addWarning(QString::fromLocal8Bit(e.what())); + success = false; } - return true; + // Save modified templates + for (auto i = 0; i < map->getNumTemplates(); ++i) + { + auto const* temp = map->getTemplate(i); + auto const filename = temp->getTemplateFilename(); + try + { + if (temp->hasUnsavedChanges() && !temp->saveTemplateFile()) + { + addWarning(tr("Cannot save file\n%1:\n%2").arg(filename, temp->errorString())); + success = false; + } + } + catch (std::exception &e) + { + addWarning(tr("Cannot save file\n%1:\n%2").arg(filename, QString::fromLocal8Bit(e.what()))); + success = false; + } + } + + return success; } diff --git a/src/fileformats/iof_course_export.cpp b/src/fileformats/iof_course_export.cpp new file mode 100644 index 000000000..a3e282aa1 --- /dev/null +++ b/src/fileformats/iof_course_export.cpp @@ -0,0 +1,163 @@ +/* + * Copyright 2021 Kai Pastor + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#include "iof_course_export.h" + +#include +#include +#include +#include +#include + +#include "mapper_config.h" +#include "core/georeferencing.h" +#include "core/latlon.h" +#include "core/map.h" +#include "core/map_coord.h" +#include "core/objects/object.h" +#include "fileformats/simple_course_export.h" +#include "util/xml_stream_util.h" + + +namespace OpenOrienteering { + +// static +QString IofCourseExport::formatDescription() +{ + return OpenOrienteering::ImportExport::tr("IOF Data Standard 3.0"); +} + +// static +QString IofCourseExport::filenameExtension() +{ + return QStringLiteral("xml"); +} + + +IofCourseExport::~IofCourseExport() = default; + +IofCourseExport::IofCourseExport(const QString& path, const Map* map, const MapView* view) +: Exporter(path, map, view) +{} + + +bool IofCourseExport::exportImplementation() +{ + SimpleCourseExport course_export(*map); + auto* object = course_export.findObjectForExport(); + + if (!course_export.canExport(object)) + { + addWarning(course_export.errorString()); + return false; + } + + simple_course = &course_export; + + QXmlStreamWriter writer(device()); + writer.setAutoFormatting(true); + xml = &writer; + xml->writeStartDocument(); + writeXml(*object); + xml = nullptr; + + simple_course = nullptr; + return true; +} + + +void IofCourseExport::writeXml(const PathObject& object) +{ + auto const stamp = QDateTime::currentDateTime(); + xml->writeDefaultNamespace(QLatin1String("http://www.orienteering.org/datastandard/3.0")); + + XmlElementWriter course_data(*xml, QLatin1String("CourseDate")); + course_data.writeAttribute(QLatin1String("iofVersion"), QLatin1String("3.0")); + course_data.writeAttribute(QLatin1String("creator"), QLatin1String("OpenOrienteering Mapper " APP_VERSION)); + course_data.writeAttribute(QLatin1String("createTime"), stamp.toString(Qt::ISODate)); + { + XmlElementWriter event(*xml, QLatin1String("Event")); + xml->writeTextElement(QLatin1String("Name"), simple_course->eventName()); + } + { + XmlElementWriter event(*xml, QLatin1String("RaceCourseData")); + writeControls(object.getRawCoordinateVector()); + writeCourse(object.getRawCoordinateVector()); + } +} + +void IofCourseExport::writeControls(const std::vector& coords) +{ + auto next = [](auto current) { + return current + (current->isCurveStart() ? 3 : 1); + }; + + writeControl(coords.front(), QLatin1String("S1")); + auto code_number = simple_course->firstCode(); + for (auto current = next(coords.begin()); current != coords.end() - 1; current = next(current)) + { + auto const name = QString::number(code_number); + writeControl(*current, name); + ++code_number; + } + writeControl(coords.back(), QLatin1String("F1")); +} + +void IofCourseExport::writeCourse(const std::vector& coords) +{ + auto next = [](auto current) { + return current + (current->isCurveStart() ? 3 : 1); + }; + + XmlElementWriter event(*xml, QLatin1String("Course")); + xml->writeTextElement(QLatin1String("Name"), simple_course->courseName()); + writeCourseControl(QLatin1String("Start"), QLatin1String("S1")); + auto code_number = simple_course->firstCode(); + for (auto current = next(coords.begin()); current != coords.end() - 1; current = next(current)) + { + auto const name = QString::number(code_number); + writeCourseControl(QLatin1String("Control"), name); + ++code_number; + } + writeCourseControl(QLatin1String("Finish"), QLatin1String("F1")); +} + +void IofCourseExport::writeControl(const MapCoord& coord, const QString& id) +{ + XmlElementWriter control(*xml, QLatin1String("Control")); + xml->writeTextElement(QLatin1String("Id"), id); + writePosition(map->getGeoreferencing().toGeographicCoords(MapCoordF(coord))); +} + +void IofCourseExport::writeCourseControl(const QString& type, const QString& id) +{ + XmlElementWriter course_control(*xml, QLatin1String("CourseControl")); + course_control.writeAttribute(QLatin1String("type"), type); + xml->writeTextElement(QLatin1String("Control"), id); +} + +void IofCourseExport::writePosition(const LatLon& latlon) +{ + XmlElementWriter position(*xml, QLatin1String("CourseControl")); + position.writeAttribute(QLatin1String("lng"), latlon.longitude(), 7); + position.writeAttribute(QLatin1String("lat"), latlon.latitude(), 7); +} + + +} // namespace OpenOrienteering diff --git a/src/fileformats/iof_course_export.h b/src/fileformats/iof_course_export.h new file mode 100644 index 000000000..b47b2df85 --- /dev/null +++ b/src/fileformats/iof_course_export.h @@ -0,0 +1,81 @@ +/* + * Copyright 2021 Kai Pastor + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#ifndef OPENORIENTEERING_IOF_COURSE_EXPORT_H +#define OPENORIENTEERING_IOF_COURSE_EXPORT_H + +#include + +#include "fileformats/file_import_export.h" + +class QString; +class QXmlStreamWriter; + +namespace OpenOrienteering { + +class LatLon; +class Map; +class MapView; +class MapCoord; +class PathObject; +class SimpleCourseExport; + + +/** + * This class generates IOF Interface Standard 3.0 course files. + * + * This export handles a single path object and outputs placemarks for start + * (S1), finish (F1), and controls in between. Event name, course name, and + * the code number of the first control are taken from transient map properties + * in collaboration with the SimpleCourseExport class. + */ +class IofCourseExport : public Exporter +{ +public: + static QString formatDescription(); + static QString filenameExtension(); + + ~IofCourseExport(); + + IofCourseExport(const QString& path, const Map* map, const MapView* view); + +protected: + bool exportImplementation() override; + + void writeXml(const PathObject& object); + + void writeControls(const std::vector& coords); + + void writeCourse(const std::vector& coords); + + void writeControl(const MapCoord& coord, const QString& id); + + void writeCourseControl(const QString& type, const QString& id); + + void writePosition(const LatLon& latlon); + +private: + QXmlStreamWriter* xml = nullptr; + SimpleCourseExport* simple_course = nullptr; +}; + + +} // namespace OpenOrienteering + +#endif // OPENORIENTEERING_KML_COURSE_EXPORT_H diff --git a/src/fileformats/kml_course_export.cpp b/src/fileformats/kml_course_export.cpp new file mode 100644 index 000000000..b7be4d98c --- /dev/null +++ b/src/fileformats/kml_course_export.cpp @@ -0,0 +1,143 @@ +/* + * Copyright 2020-2021 Kai Pastor + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#include "kml_course_export.h" + +#include +#include +#include +#include +#include + +#include "mapper_config.h" +#include "core/georeferencing.h" +#include "core/latlon.h" +#include "core/map.h" +#include "core/map_coord.h" +#include "core/objects/object.h" +#include "fileformats/simple_course_export.h" +#include "util/xml_stream_util.h" + + +namespace OpenOrienteering { + +// static +QString KmlCourseExport::formatDescription() +{ + return OpenOrienteering::ImportExport::tr("KML"); +} + +// static +QString KmlCourseExport::filenameExtension() +{ + return QStringLiteral("kml"); +} + + +KmlCourseExport::~KmlCourseExport() = default; + +KmlCourseExport::KmlCourseExport(const QString& path, const Map* map, const MapView* view) +: Exporter(path, map, view) +{} + + +// override +bool KmlCourseExport::exportImplementation() +{ + auto course_export = SimpleCourseExport(*map); + auto const* const object = course_export.findObjectForExport(); + if (!course_export.canExport(object)) + { + addWarning(course_export.errorString()); + return false; + } + + simple_course = &course_export; + + QXmlStreamWriter writer(device()); + writer.setAutoFormatting(true); + xml = &writer; + xml->writeStartDocument(); + writeKml(*object); + xml = nullptr; + + simple_course = nullptr; + return true; +} + + +void KmlCourseExport::writeKml(const PathObject& object) +{ + auto const stamp = QDateTime::currentDateTime(); + xml->writeDefaultNamespace(QLatin1String("http://www.opengis.net/kml/2.2")); + xml->writeComment(QLatin1String("Generator: OpenOrienteering Mapper " APP_VERSION)); + xml->writeComment(QLatin1String("Created: ") + stamp.toString(Qt::ISODate)); + + XmlElementWriter kml(*xml, QLatin1String("kml")); + { + XmlElementWriter document(*xml, QLatin1String("Document")); + xml->writeTextElement(QLatin1String("name"), simple_course->eventName()); + { + XmlElementWriter folder(*xml, QLatin1String("Folder")); + xml->writeTextElement(QLatin1String("name"), simple_course->courseName()); + xml->writeTextElement(QLatin1String("open"), QLatin1String("1")); + writeKmlPlacemarks(object.getRawCoordinateVector()); + } + } +} + +void KmlCourseExport::writeKmlPlacemarks(const std::vector& coords) +{ + auto next = [](auto current) { + return current + (current->isCurveStart() ? 3 : 1); + }; + + writeKmlPlacemark(coords.front(), QLatin1String("S1"), QLatin1String("Start")); + auto code_number = simple_course->firstCode(); + for (auto current = next(coords.begin()); current != coords.end() - 1; current = next(current)) + { + auto const name = QString::number(code_number); + writeKmlPlacemark(*current, name, QLatin1String("Control ") + name); + ++code_number; + } + writeKmlPlacemark(coords.back(), QLatin1String("F1"), QLatin1String("Finish")); +} + +void KmlCourseExport::writeKmlPlacemark(const MapCoord& coord, const QString& name, const QString& description) +{ + XmlElementWriter placemark(*xml, QLatin1String("Placemark")); + xml->writeTextElement(QLatin1String("name"), name); + xml->writeTextElement(QLatin1String("description"), description); + { + XmlElementWriter point(*xml, QLatin1String("Point")); + writeCoordinates(map->getGeoreferencing().toGeographicCoords(MapCoordF(coord))); + } +} + +void KmlCourseExport::writeCoordinates(const LatLon& latlon) +{ + XmlElementWriter coordinates(*xml, QLatin1String("coordinates")); + xml->writeCharacters(QString::number(latlon.longitude(), 'f', 7)); + xml->writeCharacters(QLatin1String(",")); + xml->writeCharacters(QString::number(latlon.latitude(), 'f', 7)); + xml->writeCharacters(QLatin1String(",0")); +} + + +} // namespace OpenOrienteering diff --git a/src/fileformats/kml_course_export.h b/src/fileformats/kml_course_export.h new file mode 100644 index 000000000..41037586b --- /dev/null +++ b/src/fileformats/kml_course_export.h @@ -0,0 +1,77 @@ +/* + * Copyright 2020-2021 Kai Pastor + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#ifndef OPENORIENTEERING_KML_EXPORT_EXPORT_H +#define OPENORIENTEERING_KML_EXPORT_EXPORT_H + +#include + +#include "fileformats/file_import_export.h" + +class QString; +class QXmlStreamWriter; + +namespace OpenOrienteering { + +class LatLon; +class Map; +class MapCoord; +class MapView; +class PathObject; +class SimpleCourseExport; + + +/** + * This class generates KML course files for MapRunF. + * + * This export handles a single path object and outputs placemarks for start + * (S1), finish (F1), and controls in between. Event name, course name, and + * the code number of the first control are taken from transient map properties + * in collaboration with the SimpleCourseExport class. + */ +class KmlCourseExport : public Exporter +{ +public: + static QString formatDescription(); + static QString filenameExtension(); + + ~KmlCourseExport(); + + KmlCourseExport(const QString& path, const Map* map, const MapView* view); + +protected: + bool exportImplementation() override; + + void writeKml(const PathObject& object); + + void writeKmlPlacemarks(const std::vector& coords); + + void writeKmlPlacemark(const MapCoord& coord, const QString& name, const QString& description); + + void writeCoordinates(const LatLon& latlon); + +private: + QXmlStreamWriter* xml = nullptr; + SimpleCourseExport* simple_course = nullptr; +}; + + +} // namespace OpenOrienteering + +#endif // OPENORIENTEERING_KML_EXPORT_EXPORT_H diff --git a/src/fileformats/ocad8_file_format.cpp b/src/fileformats/ocad8_file_format.cpp deleted file mode 100644 index 3604b7f60..000000000 --- a/src/fileformats/ocad8_file_format.cpp +++ /dev/null @@ -1,2899 +0,0 @@ -/* - * Copyright 2012, 2013 Pete Curtis - * Copyright 2013-2020 Kai Pastor - * - * This file is part of OpenOrienteering. - * - * OpenOrienteering is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * OpenOrienteering is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with OpenOrienteering. If not, see . - */ - -#include "ocad8_file_format.h" -#include "ocad8_file_format_p.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "settings.h" -#include "core/georeferencing.h" -#include "core/map.h" -#include "core/map_color.h" -#include "core/map_coord.h" -#include "core/map_part.h" -#include "core/map_view.h" -#include "core/objects/object.h" -#include "core/objects/text_object.h" -#include "core/symbols/symbol.h" -#include "core/symbols/area_symbol.h" -#include "core/symbols/combined_symbol.h" -#include "core/symbols/line_symbol.h" -#include "core/symbols/point_symbol.h" -#include "core/symbols/text_symbol.h" -#include "fileformats/xml_file_format.h" -#include "fileformats/file_import_export.h" -#include "fileformats/ocd_file_format.h" -#include "templates/template.h" -#include "templates/template_image.h" -#include "templates/template_map.h" -#include "util/encoding.h" -#include "util/util.h" - - -namespace OpenOrienteering { - -// ### OCAD8FileFormat ### - -OCAD8FileFormat::OCAD8FileFormat() - : FileFormat(MapFile, - "OCAD78", - ::OpenOrienteering::ImportExport::tr("OCAD Versions 7, 8"), - QString::fromLatin1("ocd"), - Feature::FileOpen | Feature::FileImport | Feature::ReadingLossy | - Feature::FileSave | Feature::FileSaveAs | Feature::WritingLossy ) -{ - // Nothing -} - - -FileFormat::ImportSupportAssumption OCAD8FileFormat::understands(const char* buffer, int size) const -{ - // The first two bytes of the file must be AD 0C. - if (size < 2) - return Unknown; - else if (quint8(buffer[0]) == 0xAD && buffer[1] == 0x0C) - return FullySupported; - else - return NotSupported; -} - - -std::unique_ptr OCAD8FileFormat::makeImporter(const QString& path, Map *map, MapView *view) const -{ - return std::make_unique(path, map, view); -} - -std::unique_ptr OCAD8FileFormat::makeExporter(const QString& path, const Map* map, const MapView* view) const -{ - return std::make_unique(path, map, view); -} - - - -// ### OCAD8FileImport ### - -OCAD8FileImport::OCAD8FileImport(const QString& path, Map* map, MapView* view) : Importer(path, map, view), file(nullptr) -{ - ocad_init(); - const QByteArray enc_name = Settings::getInstance().getSetting(Settings::General_Local8BitEncoding).toByteArray(); - encoding_1byte = Util::codecForName(enc_name); - if (!encoding_1byte) encoding_1byte = QTextCodec::codecForLocale(); - encoding_2byte = QTextCodec::codecForName("UTF-16LE"); - offset_x = offset_y = 0; -} - -OCAD8FileImport::~OCAD8FileImport() -{ - ocad_shutdown(); -} - -bool OCAD8FileImport::isRasterImageFile(const QString &filename) -{ - int dot_pos = filename.lastIndexOf(QLatin1Char('.')); - if (dot_pos < 0) - return false; - QString extension = filename.right(filename.length() - dot_pos - 1).toLower(); - return QImageReader::supportedImageFormats().contains(extension.toLatin1()); -} - -bool OCAD8FileImport::importImplementation() -{ - //qint64 start = QDateTime::currentMSecsSinceEpoch(); - - auto& device = *this->device(); - u32 size = device.bytesAvailable(); - u8* buffer = (u8*)malloc(size); - if (!buffer) - throw FileFormatException(tr("Could not allocate buffer.")); - if (device.read((char*)buffer, size) != size) - throw FileFormatException(device.errorString()); - int err = ocad_file_open_memory(&file, buffer, size); - if (err != 0) throw FileFormatException(tr("libocad returned %1").arg(err)); - - if (file->header->major <= 5 || file->header->major >= 9) - throw FileFormatException(tr("OCAD files of version %1 are not supported!").arg(file->header->major)); - - //qDebug() << "file version is" << file->header->major << ", type is" - // << ((file->header->ftype == 2) ? "normal" : "other"); - //qDebug() << "map scale is" << file->setup->scale; - - map->setProperty(OcdFileFormat::versionProperty(), file->header->major); - - // Scale and georeferencing parameters - Georeferencing georef; - georef.setScaleDenominator(file->setup->scale); - georef.setProjectedRefPoint(QPointF(file->setup->offsetx, file->setup->offsety)); - if (qAbs(file->setup->angle) >= 0.01) /* degrees */ - { - georef.setGrivation(file->setup->angle); - } - map->setGeoreferencing(georef); - - map->setMapNotes(convertCString((const char*)file->buffer + file->header->infopos, file->header->infosize, false)); - - // TODO: print parameters - - // Load the separations to a temporary stack - std::vector< MapColor* > separations; - int num_separations = ocad_separation_count(file); -#if 1 - addWarning(tr("%n color separation(s) were skipped, reason: Import disabled.", "", num_separations)); - num_separations = 0; -#endif - if (num_separations < 0) - { - addWarning(tr("Could not load the spot color definitions, error: %1").arg(num_separations)); - num_separations = 0; - } - separations.reserve(num_separations); - for (int i = 0; i < num_separations; i++) - { - const OCADColorSeparation *ocad_separation = ocad_separation_at(file, i); - MapColor* color = new MapColor(convertPascalString(ocad_separation->sep_name), MapColor::Reserved); - color->setSpotColorName(convertPascalString(ocad_separation->sep_name).toUpper()); - // OCD stores CMYK values as integers from 0-200. - const MapColorCmyk cmyk( - 0.005f * ocad_separation->cyan, - 0.005f * ocad_separation->magenta, - 0.005f * ocad_separation->yellow, - 0.005f * ocad_separation->black ); - color->setCmyk(cmyk); - color->setOpacity(1.0f); - separations.push_back(color); - } - - // Load colors - int num_colors = ocad_color_count(file); - for (int i = 0; i < num_colors; i++) - { - OCADColor *ocad_color = ocad_color_at(file, i); - MapColor* color = new MapColor(convertPascalString(ocad_color->name), map->color_set->colors.size()); - // OCD stores CMYK values as integers from 0-200. - MapColorCmyk cmyk( - 0.005f * ocad_color->cyan, - 0.005f * ocad_color->magenta, - 0.005f * ocad_color->yellow, - 0.005f * ocad_color->black ); - color->setCmyk(cmyk); - color->setOpacity(1.0f); - - SpotColorComponents components; - for (int j = 0; j < num_separations; ++j) - { - const u8& ocad_halftone = ocad_color->spot[j]; - if (ocad_halftone <= 200) - { - float halftone = 0.005f * ocad_halftone; - components.reserve(std::size_t(num_separations)); // reserves only once for same capacity - components.push_back(SpotColorComponent(separations[j], halftone)); // clazy:exclude=reserve-candidates - } - } - if (!components.empty()) - { - color->setSpotColorComposition(components); - const MapColorCmyk cmyk(color->getCmyk()); - color->setCmykFromSpotColors(); - if (cmyk != color->getCmyk()) - // The color's CMYK was customized. - color->setCmyk(cmyk); - } - - if (i == 0 && color->isBlack() && color->getName() == QLatin1String("Registration black") - && XMLFileFormat::active_version >= 6 ) - { - delete color; color = nullptr; - color_index[ocad_color->number] = Map::getRegistrationColor(); - addWarning(tr("Color \"Registration black\" is imported as a special color.")); - // NOTE: This does not make a difference in output - // as long as no spot colors are created, - // but as a special color, it is protected from modification, - // and it will be saved as number 0 in OCD export. - } - else - { - map->color_set->colors.push_back(color); - color_index[ocad_color->number] = color; - } - } - - // Insert the spot colors into the map - for (int i = 0; i < num_separations; ++i) - { - map->addColor(separations[i], map->color_set->colors.size()); - } - - // Load symbols - for (OCADSymbolIndex *idx = ocad_symidx_first(file); idx; idx = ocad_symidx_next(file, idx)) - { - for (int i = 0; i < 256; i++) - { - OCADSymbol *ocad_symbol = ocad_symbol_at(file, idx, i); - if (ocad_symbol && ocad_symbol->number != 0) - { - Symbol *symbol = nullptr; - if (ocad_symbol->type == OCAD_POINT_SYMBOL) - { - symbol = importPointSymbol((OCADPointSymbol *)ocad_symbol); - } - else if (ocad_symbol->type == OCAD_LINE_SYMBOL) - { - symbol = importLineSymbol((OCADLineSymbol *)ocad_symbol); - } - else if (ocad_symbol->type == OCAD_AREA_SYMBOL) - { - symbol = importAreaSymbol((OCADAreaSymbol *)ocad_symbol); - } - else if (ocad_symbol->type == OCAD_TEXT_SYMBOL) - { - symbol = importTextSymbol((OCADTextSymbol *)ocad_symbol); - } - else if (ocad_symbol->type == OCAD_RECT_SYMBOL) - { - RectangleInfo* rect = importRectSymbol((OCADRectSymbol *)ocad_symbol); - map->symbols.push_back(rect->border_line); - if (rect->has_grid) - { - map->symbols.push_back(rect->inner_line); - map->symbols.push_back(rect->text); - } - continue; - } - - - if (symbol) - { - map->symbols.push_back(symbol); - symbol_index[ocad_symbol->number] = symbol; - } - else - { - addWarning(tr("Unable to import symbol \"%3\" (%1.%2)") - .arg(ocad_symbol->number / 10).arg(ocad_symbol->number % 10) - .arg(convertPascalString(ocad_symbol->name))); - } - } - } - } - - if (!loadSymbolsOnly()) - { - // Load objects - - // Place all objects into a single OCAD import part - MapPart* part = new MapPart(tr("OCAD import layer"), map); - for (OCADObjectIndex *idx = ocad_objidx_first(file); idx; idx = ocad_objidx_next(file, idx)) - { - for (int i = 0; i < 256; i++) - { - OCADObjectEntry *entry = ocad_object_entry_at(file, idx, i); - OCADObject *ocad_obj = ocad_object(file, entry); - if (ocad_obj) - { - Object *object = importObject(ocad_obj, part); - if (object) { - part->objects.push_back(object); - } - } - } - } - delete map->parts[0]; - map->parts[0] = part; - map->current_part_index = 0; - - // Load templates - map->templates.clear(); - for (OCADStringIndex *idx = ocad_string_index_first(file); idx; idx = ocad_string_index_next(file, idx)) - { - for (int i = 0; i < 256; i++) - { - OCADStringEntry *entry = ocad_string_entry_at(file, idx, i); - if (entry->type != 0 && entry->size > 0) - importString(entry); - } - } - map->first_front_template = map->templates.size(); // Templates in front of the map are not supported by OCD - - // Fill view with relevant fields from OCD file - if (view) - { - if (file->setup->zoom >= MapView::zoom_out_limit && file->setup->zoom <= MapView::zoom_in_limit) - view->setZoom(file->setup->zoom); - - s32 buf[3]; - ocad_point(buf, &file->setup->center); - MapCoord center_pos; - convertPoint(center_pos, buf[0], buf[1]); - view->setCenter(center_pos); - } - - // TODO: read template visibilities - /* - int num_template_visibilities; - file->read((char*)&num_template_visibilities, sizeof(int)); - - for (int i = 0; i < num_template_visibilities; ++i) - { - int pos; - file->read((char*)&pos, sizeof(int)); - - TemplateVisibility* vis = getTemplateVisibility(map->getTemplate(pos)); - file->read((char*)&vis->visible, sizeof(bool)); - file->read((char*)&vis->opacity, sizeof(float)); - } - } - */ - - // Undo steps are not supported in OCAD - } - - ocad_file_close(file); - - //qint64 elapsed = QDateTime::currentMSecsSinceEpoch() - start; - //qDebug() << "OCAD map imported:"<getNumSymbols()<<"symbols and"<getNumObjects()<<"objects in"<currentMapPartIndexChanged(map->current_part_index); - emit map->currentMapPartChanged(map->getPart(map->current_part_index)); - - return true; -} - -void OCAD8FileImport::setStringEncodings(const char *narrow, const char *wide) { - encoding_1byte = QTextCodec::codecForName(narrow); - encoding_2byte = QTextCodec::codecForName(wide); -} - -Symbol *OCAD8FileImport::importPointSymbol(const OCADPointSymbol *ocad_symbol) -{ - PointSymbol *symbol = importPattern(ocad_symbol->ngrp, (OCADPoint *)ocad_symbol->pts); - fillCommonSymbolFields(symbol, (OCADSymbol *)ocad_symbol); - symbol->setRotatable(ocad_symbol->base_flags & 1); - - return symbol; -} - -Symbol *OCAD8FileImport::importLineSymbol(const OCADLineSymbol *ocad_symbol) -{ - LineSymbol* line_for_borders = nullptr; - - // Import a main line? - LineSymbol* main_line = nullptr; - if (ocad_symbol->dmode == 0 || ocad_symbol->width > 0) - { - main_line = new LineSymbol(); - line_for_borders = main_line; - fillCommonSymbolFields(main_line, (OCADSymbol *)ocad_symbol); - - // Basic line options - main_line->line_width = convertSize(ocad_symbol->width); - main_line->color = convertColor(ocad_symbol->color); - - // Cap and join styles - if (ocad_symbol->ends == 0) - { - main_line->cap_style = LineSymbol::FlatCap; - main_line->join_style = LineSymbol::BevelJoin; - } - else if (ocad_symbol->ends == 1) - { - main_line->cap_style = LineSymbol::RoundCap; - main_line->join_style = LineSymbol::RoundJoin; - } - else if (ocad_symbol->ends == 2) - { - main_line->cap_style = LineSymbol::PointedCap; - main_line->join_style = LineSymbol::BevelJoin; - } - else if (ocad_symbol->ends == 3) - { - main_line->cap_style = LineSymbol::PointedCap; - main_line->join_style = LineSymbol::RoundJoin; - } - else if (ocad_symbol->ends == 4) - { - main_line->cap_style = LineSymbol::FlatCap; - main_line->join_style = LineSymbol::MiterJoin; - } - else if (ocad_symbol->ends == 6) - { - main_line->cap_style = LineSymbol::PointedCap; - main_line->join_style = LineSymbol::MiterJoin; - } - - main_line->start_offset = convertSize(ocad_symbol->bdist); - main_line->end_offset = convertSize(ocad_symbol->edist); - if (main_line->cap_style == LineSymbol::PointedCap) - { - // Note: While the property in the file may be different - // (cf. what is set in the first place), OC*D always - // draws round joins if the line cap is pointed! - main_line->join_style = LineSymbol::RoundJoin; - } - - // Handle the dash pattern - if( ocad_symbol->gap > 0 || ocad_symbol->gap2 > 0 ) - { - main_line->dashed = true; - - // Detect special case - if (ocad_symbol->gap2 > 0 && ocad_symbol->gap == 0) - { - main_line->dash_length = convertSize(ocad_symbol->len - ocad_symbol->gap2); - main_line->break_length = convertSize(ocad_symbol->gap2); - if (!(ocad_symbol->elen >= ocad_symbol->len / 2 - 1 && ocad_symbol->elen <= ocad_symbol->len / 2 + 1)) - addWarning(tr("In dashed line symbol %1, the end length cannot be imported correctly.").arg(0.1 * ocad_symbol->number)); - if (ocad_symbol->egap != 0) - addWarning(tr("In dashed line symbol %1, the end gap cannot be imported correctly.").arg(0.1 * ocad_symbol->number)); - } - else - { - if (ocad_symbol->len != ocad_symbol->elen) - { - if (ocad_symbol->elen >= ocad_symbol->len / 2 - 1 && ocad_symbol->elen <= ocad_symbol->len / 2 + 1) - main_line->half_outer_dashes = true; - else - addWarning(tr("In dashed line symbol %1, main and end length are different (%2 and %3). Using %4.") - .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->len).arg(ocad_symbol->elen).arg(ocad_symbol->len)); - } - - main_line->dash_length = convertSize(ocad_symbol->len); - main_line->break_length = convertSize(ocad_symbol->gap); - if (ocad_symbol->gap2 > 0) - { - main_line->dashes_in_group = 2; - if (ocad_symbol->gap2 != ocad_symbol->egap) - addWarning(tr("In dashed line symbol %1, gaps D and E are different (%2 and %3). Using %4.") - .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->gap2).arg(ocad_symbol->egap).arg(ocad_symbol->gap2)); - main_line->in_group_break_length = convertSize(ocad_symbol->gap2); - main_line->dash_length = (main_line->dash_length - main_line->in_group_break_length) / 2; - } - } - } - else - { - main_line->segment_length = convertSize(ocad_symbol->len); - main_line->end_length = convertSize(ocad_symbol->elen); - } - } - - // Import a 'framing' line? - LineSymbol* framing_line = nullptr; - if (ocad_symbol->fwidth > 0) - { - framing_line = new LineSymbol(); - if (!line_for_borders) - line_for_borders = framing_line; - fillCommonSymbolFields(framing_line, (OCADSymbol *)ocad_symbol); - - // Basic line options - framing_line->line_width = convertSize(ocad_symbol->fwidth); - framing_line->color = convertColor(ocad_symbol->fcolor); - - // Cap and join styles - if (ocad_symbol->fstyle == 0) - { - framing_line->cap_style = LineSymbol::FlatCap; - framing_line->join_style = LineSymbol::BevelJoin; - } - else if (ocad_symbol->fstyle == 1) - { - framing_line->cap_style = LineSymbol::RoundCap; - framing_line->join_style = LineSymbol::RoundJoin; - } - else if (ocad_symbol->fstyle == 4) - { - framing_line->cap_style = LineSymbol::FlatCap; - framing_line->join_style = LineSymbol::MiterJoin; - } - } - - // Import a 'double' line? - bool has_border_line = ocad_symbol->lwidth > 0 || ocad_symbol->rwidth > 0; - LineSymbol *double_line = nullptr; - if (ocad_symbol->dmode != 0 && (ocad_symbol->dflags & 1 || (has_border_line && !line_for_borders))) - { - double_line = new LineSymbol(); - line_for_borders = double_line; - fillCommonSymbolFields(double_line, (OCADSymbol *)ocad_symbol); - - double_line->line_width = convertSize(ocad_symbol->dwidth); - if (ocad_symbol->dflags & 1) - double_line->color = convertColor(ocad_symbol->dcolor); - else - double_line->color = nullptr; - - double_line->cap_style = LineSymbol::FlatCap; - double_line->join_style = LineSymbol::MiterJoin; - - double_line->segment_length = convertSize(ocad_symbol->len); - double_line->end_length = convertSize(ocad_symbol->elen); - } - - // Border lines - if (has_border_line) - { - Q_ASSERT(line_for_borders); - line_for_borders->have_border_lines = true; - LineSymbolBorder& border = line_for_borders->getBorder(); - LineSymbolBorder& right_border = line_for_borders->getRightBorder(); - - // Border color and width - border.color = convertColor(ocad_symbol->lcolor); - border.width = convertSize(ocad_symbol->lwidth); - border.shift = convertSize(ocad_symbol->lwidth) / 2 + (convertSize(ocad_symbol->dwidth) - line_for_borders->line_width) / 2; - - right_border.color = convertColor(ocad_symbol->rcolor); - right_border.width = convertSize(ocad_symbol->rwidth); - right_border.shift = convertSize(ocad_symbol->rwidth) / 2 + (convertSize(ocad_symbol->dwidth) - line_for_borders->line_width) / 2; - - // The borders may be dashed - if (ocad_symbol->dgap > 0 && ocad_symbol->dmode > 1) - { - border.dashed = true; - border.dash_length = convertSize(ocad_symbol->dlen); - border.break_length = convertSize(ocad_symbol->dgap); - - // If ocad_symbol->dmode == 2, only the left border should be dashed - if (ocad_symbol->dmode > 2) - { - right_border.dashed = border.dashed; - right_border.dash_length = border.dash_length; - right_border.break_length = border.break_length; - } - } - } - - // Create point symbols along line; middle ("normal") dash, corners, start, and end. - LineSymbol* symbol_line = main_line ? main_line : double_line; // Find the line to attach the symbols to - if (!symbol_line) - { - main_line = new LineSymbol(); - symbol_line = main_line; - fillCommonSymbolFields(main_line, (OCADSymbol *)ocad_symbol); - - main_line->segment_length = convertSize(ocad_symbol->len); - main_line->end_length = convertSize(ocad_symbol->elen); - } - OCADPoint * symbolptr = (OCADPoint *)ocad_symbol->pts; - symbol_line->mid_symbol = importPattern( ocad_symbol->smnpts, symbolptr); - symbol_line->mid_symbols_per_spot = ocad_symbol->snum; - symbol_line->mid_symbol_distance = convertSize(ocad_symbol->sdist); - symbolptr += ocad_symbol->smnpts; - if( ocad_symbol->ssnpts > 0 ) - { - //symbol_line->dash_symbol = importPattern( ocad_symbol->ssnpts, symbolptr); - symbolptr += ocad_symbol->ssnpts; - } - if( ocad_symbol->scnpts > 0 ) - { - symbol_line->dash_symbol = importPattern( ocad_symbol->scnpts, symbolptr); - symbol_line->dash_symbol->setName(QCoreApplication::translate("OpenOrienteering::LineSymbolSettings", "Dash symbol")); - symbolptr += ocad_symbol->scnpts; - } - if( ocad_symbol->sbnpts > 0 ) - { - symbol_line->start_symbol = importPattern( ocad_symbol->sbnpts, symbolptr); - symbol_line->start_symbol->setName(QCoreApplication::translate("OpenOrienteering::LineSymbolSettings", "Start symbol")); - symbolptr += ocad_symbol->sbnpts; - } - if( ocad_symbol->senpts > 0 ) - { - symbol_line->end_symbol = importPattern( ocad_symbol->senpts, symbolptr); - } - // FIXME: not really sure how this translates... need test cases - symbol_line->minimum_mid_symbol_count = 0; //1 + ocad_symbol->smin; - symbol_line->minimum_mid_symbol_count_when_closed = 0; //1 + ocad_symbol->smin; - symbol_line->show_at_least_one_symbol = false; // NOTE: this works in a different way than OCAD's 'at least X symbols' setting (per-segment instead of per-object) - - // Suppress dash symbol at line ends if both start symbol and end symbol exist, - // but don't create a warning unless a dash symbol is actually defined - // and the line symbol is not Mapper's 799 Simple orienteering course. - if (symbol_line->start_symbol && symbol_line->end_symbol) - { - symbol_line->setSuppressDashSymbolAtLineEnds(true); - if (symbol_line->dash_symbol && symbol_line->getNumberComponent(0) != 799) - addWarning(tr("Line symbol %1: suppressing dash symbol at line ends.").arg(QString::number(0.1 * ocad_symbol->number) + QLatin1Char(' ') + symbol_line->getName())); - } - - // TODO: taper fields (tmode and tlast) - - if (!main_line && !framing_line) - return double_line; - else if (!double_line && !framing_line) - return main_line; - else if (!main_line && !double_line) - return framing_line; - else - { - CombinedSymbol* full_line = new CombinedSymbol(); - fillCommonSymbolFields(full_line, (OCADSymbol *)ocad_symbol); - full_line->setNumParts(3); - int part = 0; - if (main_line) - { - full_line->setPart(part++, main_line, true); - main_line->setHidden(false); - main_line->setProtected(false); - } - if (double_line) - { - full_line->setPart(part++, double_line, true); - double_line->setHidden(false); - double_line->setProtected(false); - } - if (framing_line) - { - full_line->setPart(part++, framing_line, true); - framing_line->setHidden(false); - framing_line->setProtected(false); - } - full_line->setNumParts(part); - return full_line; - } -} - -Symbol *OCAD8FileImport::importAreaSymbol(const OCADAreaSymbol *ocad_symbol) -{ - AreaSymbol *symbol = new AreaSymbol(); - fillCommonSymbolFields(symbol, (OCADSymbol *)ocad_symbol); - - // Basic area symbol fields: minimum_area, color - symbol->minimum_area = 0; - symbol->color = ocad_symbol->fill ? convertColor(ocad_symbol->color) : nullptr; - symbol->patterns.clear(); - AreaSymbol::FillPattern *pat = nullptr; - - // Hatching - if (ocad_symbol->hmode > 0) - { - int n = symbol->patterns.size(); symbol->patterns.resize(n + 1); pat = &(symbol->patterns[n]); - pat->type = AreaSymbol::FillPattern::LinePattern; - pat->angle = convertRotation(ocad_symbol->hangle1); - pat->flags = AreaSymbol::FillPattern::Option::Rotatable; - pat->line_spacing = convertSize(ocad_symbol->hdist + ocad_symbol->hwidth); - pat->line_offset = 0; - pat->line_color = convertColor(ocad_symbol->hcolor); - pat->line_width = convertSize(ocad_symbol->hwidth); - if (ocad_symbol->hmode == 2) - { - // Second hatch, same as the first, just a different angle - symbol->patterns.push_back(*pat); - symbol->patterns.back().angle = convertRotation(ocad_symbol->hangle2); - } - } - - if (ocad_symbol->pmode > 0) - { - // OCAD 8 has a "staggered" pattern mode, where successive rows are shifted width/2 relative - // to each other. We need to simulate this in Mapper with two overlapping patterns, each with - // twice the height. The second is then offset by width/2, height/2. - auto spacing = convertSize(ocad_symbol->pheight); - if (ocad_symbol->pmode == 2) spacing *= 2; - int n = symbol->patterns.size(); symbol->patterns.resize(n + 1); pat = &(symbol->patterns[n]); - pat->type = AreaSymbol::FillPattern::PointPattern; - pat->angle = convertRotation(ocad_symbol->pangle); - pat->flags = AreaSymbol::FillPattern::Option::Rotatable; - pat->point_distance = convertSize(ocad_symbol->pwidth); - pat->line_spacing = spacing; - pat->line_offset = 0; - pat->offset_along_line = 0; - // FIXME: somebody needs to own this symbol and be responsible for deleting it - // Right now it looks like a potential memory leak - pat->point = importPattern(ocad_symbol->npts, (OCADPoint *)ocad_symbol->pts); - if (ocad_symbol->pmode == 2) - { - int n = symbol->patterns.size(); symbol->patterns.resize(n + 1); pat = &(symbol->patterns[n]); - pat->type = AreaSymbol::FillPattern::PointPattern; - pat->angle = convertRotation(ocad_symbol->pangle); - pat->flags = AreaSymbol::FillPattern::Option::Rotatable; - pat->point_distance = convertSize(ocad_symbol->pwidth); - pat->line_spacing = spacing; - pat->line_offset = pat->line_spacing / 2; - pat->offset_along_line = pat->point_distance / 2; - pat->point = importPattern(ocad_symbol->npts, (OCADPoint *)ocad_symbol->pts); - } - } - - return symbol; -} - -Symbol *OCAD8FileImport::importTextSymbol(const OCADTextSymbol *ocad_symbol) -{ - TextSymbol *symbol = new TextSymbol(); - fillCommonSymbolFields(symbol, (OCADSymbol *)ocad_symbol); - - symbol->font_family = convertPascalString(ocad_symbol->font); // FIXME: font mapping? - symbol->color = convertColor(ocad_symbol->color); - double d_font_size = (0.1 * ocad_symbol->dpts) / 72.0 * 25.4; - symbol->font_size = qRound(1000 * d_font_size); - symbol->bold = (ocad_symbol->bold >= 550) ? true : false; - symbol->italic = (ocad_symbol->italic) ? true : false; - symbol->underline = false; - symbol->paragraph_spacing = convertSize(ocad_symbol->pspace); - symbol->character_spacing = ocad_symbol->cspace / 100.0; - symbol->kerning = false; - symbol->line_below = ocad_symbol->under; - symbol->line_below_color = convertColor(ocad_symbol->ucolor); - symbol->line_below_width = convertSize(ocad_symbol->uwidth); - symbol->line_below_distance = convertSize(ocad_symbol->udist); - symbol->custom_tabs.resize(ocad_symbol->ntabs); - for (int i = 0; i < ocad_symbol->ntabs; ++i) - symbol->custom_tabs[i] = convertSize(ocad_symbol->tab[i]); - - int halign = (int)TextObject::AlignHCenter; - if (ocad_symbol->halign == 0) - halign = (int)TextObject::AlignLeft; - else if (ocad_symbol->halign == 1) - halign = (int)TextObject::AlignHCenter; - else if (ocad_symbol->halign == 2) - halign = (int)TextObject::AlignRight; - else if (ocad_symbol->halign == 3) - { - // TODO: implement justified alignment - addWarning(tr("During import of text symbol %1: ignoring justified alignment").arg(0.1 * ocad_symbol->number)); - } - text_halign_map[symbol] = halign; - - if (ocad_symbol->bold != 400 && ocad_symbol->bold != 700) - { - addWarning(tr("During import of text symbol %1: ignoring custom weight (%2)") - .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->bold)); - } - if (ocad_symbol->cspace != 0) - { - addWarning(tr("During import of text symbol %1: custom character spacing is set, its implementation does not match OCAD's behavior yet") - .arg(0.1 * ocad_symbol->number)); - } - if (ocad_symbol->wspace != 100) - { - addWarning(tr("During import of text symbol %1: ignoring custom word spacing (%2%)") - .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->wspace)); - } - if (ocad_symbol->indent1 != 0 || ocad_symbol->indent2 != 0) - { - addWarning(tr("During import of text symbol %1: ignoring custom indents (%2/%3)") - .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->indent1).arg(ocad_symbol->indent2)); - } - - if (ocad_symbol->fmode > 0) - { - symbol->framing = true; - symbol->framing_color = convertColor(ocad_symbol->fcolor); - if (ocad_symbol->fmode == 1) - { - symbol->framing_mode = TextSymbol::ShadowFraming; - symbol->framing_shadow_x_offset = convertSize(ocad_symbol->fdx); - symbol->framing_shadow_y_offset = -1 * convertSize(ocad_symbol->fdy); - } - else if (ocad_symbol->fmode == 2) - { - symbol->framing_mode = TextSymbol::LineFraming; - symbol->framing_line_half_width = convertSize(ocad_symbol->fdpts); - } - else - { - addWarning(tr("During import of text symbol %1: ignoring text framing (mode %2)") - .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->fmode)); - } - } - - symbol->updateQFont(); - - // Convert line spacing - double absolute_line_spacing = d_font_size * 0.01 * ocad_symbol->lspace; - symbol->line_spacing = absolute_line_spacing / (symbol->getFontMetrics().lineSpacing() / symbol->calculateInternalScaling()); - - return symbol; -} -OCAD8FileImport::RectangleInfo* OCAD8FileImport::importRectSymbol(const OCADRectSymbol* ocad_symbol) -{ - RectangleInfo rect; - rect.border_line = new LineSymbol(); - fillCommonSymbolFields(rect.border_line, (OCADSymbol *)ocad_symbol); - rect.border_line->line_width = convertSize(ocad_symbol->width); - rect.border_line->color = convertColor(ocad_symbol->color); - rect.border_line->cap_style = LineSymbol::FlatCap; - rect.border_line->join_style = LineSymbol::RoundJoin; - rect.corner_radius = 0.001 * convertSize(ocad_symbol->corner); - rect.has_grid = ocad_symbol->flags & 1; - - if (rect.has_grid) - { - rect.inner_line = new LineSymbol(); - fillCommonSymbolFields(rect.inner_line, (OCADSymbol *)ocad_symbol); - rect.inner_line->setNumberComponent(2, 1); - rect.inner_line->line_width = qRound(1000 * 0.15); - rect.inner_line->color = rect.border_line->color; - - rect.text = new TextSymbol(); - fillCommonSymbolFields(rect.text, (OCADSymbol *)ocad_symbol); - rect.text->setNumberComponent(2, 2); - rect.text->font_family = QString::fromLatin1("Arial"); - rect.text->font_size = qRound(1000 * (15 / 72.0 * 25.4)); - rect.text->color = rect.border_line->color; - rect.text->bold = true; - rect.text->updateQFont(); - - rect.number_from_bottom = ocad_symbol->flags & 2; - rect.cell_width = 0.001 * convertSize(ocad_symbol->cwidth); - rect.cell_height = 0.001 * convertSize(ocad_symbol->cheight); - rect.unnumbered_cells = ocad_symbol->gcells; - rect.unnumbered_text = convertPascalString(ocad_symbol->gtext); - } - - return &rectangle_info.insert(ocad_symbol->number, rect).value(); -} - -PointSymbol *OCAD8FileImport::importPattern(s16 npts, OCADPoint *pts) -{ - PointSymbol *symbol = new PointSymbol(); - symbol->setRotatable(true); - OCADPoint *p = pts, *end = pts + npts; - while (p < end) { - OCADSymbolElement *elt = (OCADSymbolElement *)p; - int element_index = symbol->getNumElements(); - bool multiple_elements = p + (2 + elt->npts) < end || p > pts; - if (elt->type == OCAD_DOT_ELEMENT) - { - int inner_radius = (int)convertSize(elt->diameter) / 2; - if (inner_radius > 0) - { - PointSymbol* element_symbol = multiple_elements ? (new PointSymbol()) : symbol; - element_symbol->inner_color = convertColor(elt->color); - element_symbol->inner_radius = inner_radius; - element_symbol->outer_color = nullptr; - element_symbol->outer_width = 0; - if (multiple_elements) - { - element_symbol->setRotatable(false); - PointObject* element_object = new PointObject(element_symbol); - element_object->coords.resize(1); - symbol->addElement(element_index, element_object, element_symbol); - } - } - } - else if (elt->type == OCAD_CIRCLE_ELEMENT) - { - int inner_radius = (int)convertSize(elt->diameter) / 2 - (int)convertSize(elt->width); - int outer_width = (int)convertSize(elt->width); - if (outer_width > 0 && inner_radius > 0) - { - PointSymbol* element_symbol = (multiple_elements) ? (new PointSymbol()) : symbol; - element_symbol->inner_color = nullptr; - element_symbol->inner_radius = inner_radius; - element_symbol->outer_color = convertColor(elt->color); - element_symbol->outer_width = outer_width; - if (multiple_elements) - { - element_symbol->setRotatable(false); - PointObject* element_object = new PointObject(element_symbol); - element_object->coords.resize(1); - symbol->addElement(element_index, element_object, element_symbol); - } - } - } - else if (elt->type == OCAD_LINE_ELEMENT) - { - LineSymbol* element_symbol = new LineSymbol(); - element_symbol->line_width = convertSize(elt->width); - element_symbol->color = convertColor(elt->color); - PathObject* element_object = new PathObject(element_symbol); - fillPathCoords(element_object, false, elt->npts, elt->pts); - element_object->recalculateParts(); - symbol->addElement(element_index, element_object, element_symbol); - } - else if (elt->type == OCAD_AREA_ELEMENT) - { - AreaSymbol* element_symbol = new AreaSymbol(); - element_symbol->color = convertColor(elt->color); - PathObject* element_object = new PathObject(element_symbol); - fillPathCoords(element_object, true, elt->npts, elt->pts); - element_object->recalculateParts(); - symbol->addElement(element_index, element_object, element_symbol); - } - p += (2 + elt->npts); - } - return symbol; -} - - -void OCAD8FileImport::fillCommonSymbolFields(Symbol *symbol, const OCADSymbol *ocad_symbol) -{ - // common fields are name, number, description, helper_symbol, hidden/protected status - symbol->setName(convertPascalString(ocad_symbol->name)); - symbol->setNumberComponent(0, ocad_symbol->number / 10); - symbol->setNumberComponent(1, ocad_symbol->number % 10); - symbol->setNumberComponent(2, -1); - symbol->setIsHelperSymbol(false); // no such thing in OCAD - if (ocad_symbol->status & 1) - symbol->setProtected(true); - if (ocad_symbol->status & 2) - symbol->setHidden(true); -} - -Object *OCAD8FileImport::importObject(const OCADObject* ocad_object, MapPart* part) -{ - Symbol* symbol; - if (!symbol_index.contains(ocad_object->symbol)) - { - if (!rectangle_info.contains(ocad_object->symbol)) - { - if (ocad_object->type == 1) - symbol = map->getUndefinedPoint(); - else if (ocad_object->type == 2 || ocad_object->type == 3) - symbol = map->getUndefinedLine(); - else if (ocad_object->type == 4 || ocad_object->type == 5) - symbol = map->getUndefinedText(); - else - { - addWarning(tr("Unable to load object")); - return nullptr; - } - } - else - { - if (!importRectangleObject(ocad_object, part, rectangle_info[ocad_object->symbol])) - addWarning(tr("Unable to import rectangle object")); - return nullptr; - } - } - else - symbol = symbol_index[ocad_object->symbol]; - - if (symbol->getType() == Symbol::Point) - { - PointObject *p = new PointObject(); - p->symbol = symbol; - - // extra properties: rotation - PointSymbol* point_symbol = reinterpret_cast(symbol); - if (point_symbol->isRotatable()) - p->setRotation(convertRotation(ocad_object->angle)); - else if (ocad_object->angle != 0) - { - if (!point_symbol->isSymmetrical()) - { - point_symbol->setRotatable(true); - p->setRotation(convertRotation(ocad_object->angle)); - } - } - - // only 1 coordinate is allowed, enforce it even if the OCAD object claims more. - fillPathCoords(p, false, 1, ocad_object->pts); - p->setMap(map); - return p; - } - else if (symbol->getType() == Symbol::Text) - { - TextObject *t = new TextObject(symbol); - - // extra properties: rotation, horizontalAlignment, verticalAlignment, text - t->setRotation(convertRotation(ocad_object->angle)); - t->setHorizontalAlignment((TextObject::HorizontalAlignment)text_halign_map.value(symbol)); - t->setVerticalAlignment(TextObject::AlignBaseline); - - const char *text_ptr = (const char *)(ocad_object->pts + ocad_object->npts); - std::size_t text_len = sizeof(OCADPoint) * ocad_object->ntext; - if (ocad_object->unicode) t->setText(convertWideCString(text_ptr, text_len, true)); - else t->setText(convertCString(text_ptr, text_len, true)); - - // Text objects need special path translation - if (!fillTextPathCoords(t, reinterpret_cast(symbol), ocad_object->npts, (OCADPoint *)ocad_object->pts)) - { - addWarning(tr("Not importing text symbol, couldn't figure out path' (npts=%1): %2") - .arg(ocad_object->npts).arg(t->getText())); - delete t; - return nullptr; - } - t->setMap(map); - return t; - } - else if (symbol->getType() == Symbol::Line || symbol->getType() == Symbol::Area || symbol->getType() == Symbol::Combined) { - PathObject *p = new PathObject(symbol); - - p->setPatternRotation(convertRotation(ocad_object->angle)); - - // Normal path - fillPathCoords(p, symbol->getType() == Symbol::Area, ocad_object->npts, ocad_object->pts); - p->recalculateParts(); - p->setMap(map); - return p; - } - - return nullptr; -} - -bool OCAD8FileImport::importRectangleObject(const OCADObject* ocad_object, MapPart* part, const OCAD8FileImport::RectangleInfo& rect) -{ - if (ocad_object->npts != 4) - return false; - - // Convert corner points -#ifdef Q_CC_CLANG -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warray-bounds" -#endif - s32 buf[3]; - ocad_point(buf, &(ocad_object->pts[3])); - MapCoord top_left; - convertPoint(top_left, buf[0], buf[1]); - ocad_point(buf, &(ocad_object->pts[0])); - MapCoord bottom_left; - convertPoint(bottom_left, buf[0], buf[1]); - ocad_point(buf, &(ocad_object->pts[2])); - MapCoord top_right; - convertPoint(top_right, buf[0], buf[1]); - ocad_point(buf, &(ocad_object->pts[1])); - MapCoord bottom_right; - convertPoint(bottom_right, buf[0], buf[1]); -#ifdef Q_CC_CLANG -#pragma clang diagnostic pop -#endif - - MapCoordF top_left_f = MapCoordF(top_left); - MapCoordF top_right_f = MapCoordF(top_right); - MapCoordF bottom_left_f = MapCoordF(bottom_left); - MapCoordF bottom_right_f = MapCoordF(bottom_right); - MapCoordF right = MapCoordF(top_right.x() - top_left.x(), top_right.y() - top_left.y()); - double angle = right.angle(); - MapCoordF down = MapCoordF(bottom_left.x() - top_left.x(), bottom_left.y() - top_left.y()); - right.normalize(); - down.normalize(); - - // Create border line - MapCoordVector coords; - if (rect.corner_radius == 0) - { - coords.emplace_back(top_left); - coords.emplace_back(top_right); - coords.emplace_back(bottom_right); - coords.emplace_back(bottom_left); - } - else - { - double handle_radius = (1 - BEZIER_KAPPA) * rect.corner_radius; - coords.emplace_back(top_right_f - right * rect.corner_radius, MapCoord::CurveStart); - coords.emplace_back(top_right_f - right * handle_radius); - coords.emplace_back(top_right_f + down * handle_radius); - coords.emplace_back(top_right_f + down * rect.corner_radius); - coords.emplace_back(bottom_right_f - down * rect.corner_radius, MapCoord::CurveStart); - coords.emplace_back(bottom_right_f - down * handle_radius); - coords.emplace_back(bottom_right_f - right * handle_radius); - coords.emplace_back(bottom_right_f - right * rect.corner_radius); - coords.emplace_back(bottom_left_f + right * rect.corner_radius, MapCoord::CurveStart); - coords.emplace_back(bottom_left_f + right * handle_radius); - coords.emplace_back(bottom_left_f - down * handle_radius); - coords.emplace_back(bottom_left_f - down * rect.corner_radius); - coords.emplace_back(top_left_f + down * rect.corner_radius, MapCoord::CurveStart); - coords.emplace_back(top_left_f + down * handle_radius); - coords.emplace_back(top_left_f + right * handle_radius); - coords.emplace_back(top_left_f + right * rect.corner_radius); - } - PathObject *border_path = new PathObject(rect.border_line, coords, map); - border_path->parts().front().setClosed(true, false); - part->objects.push_back(border_path); - - if (rect.has_grid && rect.cell_width > 0 && rect.cell_height > 0) - { - // Calculate grid sizes - double width = top_left.distanceTo(top_right); - double height = top_left.distanceTo(bottom_left); - int num_cells_x = qMax(1, qRound(width / rect.cell_width)); - int num_cells_y = qMax(1, qRound(height / rect.cell_height)); - - float cell_width = width / num_cells_x; - float cell_height = height / num_cells_y; - - // Create grid lines - coords.resize(2); - for (int x = 1; x < num_cells_x; ++x) - { - coords[0] = MapCoord(top_left_f + x * cell_width * right); - coords[1] = MapCoord(bottom_left_f + x * cell_width * right); - - PathObject *path = new PathObject(rect.inner_line, coords, map); - part->objects.push_back(path); - } - for (int y = 1; y < num_cells_y; ++y) - { - coords[0] = MapCoord(top_left_f + y * cell_height * down); - coords[1] = MapCoord(top_right_f + y * cell_height * down); - - PathObject *path = new PathObject(rect.inner_line, coords, map); - part->objects.push_back(path); - } - - // Create grid text - if (height >= rect.cell_height / 2) - { - for (int y = 0; y < num_cells_y; ++y) - { - for (int x = 0; x < num_cells_x; ++x) - { - int cell_num; - QString cell_text; - - if (rect.number_from_bottom) - cell_num = y * num_cells_x + x + 1; - else - cell_num = (num_cells_y - 1 - y) * num_cells_x + x + 1; - - if (cell_num > num_cells_x * num_cells_y - rect.unnumbered_cells) - cell_text = rect.unnumbered_text; - else - cell_text = QString::number(cell_num); - - TextObject* object = new TextObject(rect.text); - object->setMap(map); - object->setText(cell_text); - object->setRotation(-angle); - object->setHorizontalAlignment(TextObject::AlignLeft); - object->setVerticalAlignment(TextObject::AlignTop); - double position_x = (x + 0.07f) * cell_width; - double position_y = (y + 0.04f) * cell_height + rect.text->getFontMetrics().ascent() / rect.text->calculateInternalScaling() - rect.text->getFontSize(); - object->setAnchorPosition(top_left_f + position_x * right + position_y * down); - part->objects.push_back(object); - - //pts[0].Y -= rectinfo.gridText.FontAscent - rectinfo.gridText.FontEmHeight; - } - } - } - } - - return true; -} - -void OCAD8FileImport::importString(OCADStringEntry *entry) -{ - OCADCString *ocad_str = ocad_string(file, entry); - if (entry->type == 8) - { - // Template - importTemplate(ocad_str); - } - - // TODO: parse more types of strings, maybe the print parameters? -} - -Template *OCAD8FileImport::importTemplate(OCADCString* ocad_str) -{ - Template* templ = nullptr; - QByteArray data(ocad_str->str); // copies the data. - QString filename = encoding_1byte->toUnicode(data.left(data.indexOf('\t', 0))); - QString clean_path = QDir::cleanPath(QString(filename).replace(QLatin1Char('\\'), QLatin1Char('/'))); - QString extension = QFileInfo(clean_path).suffix(); - if (extension.compare(QLatin1String("ocd"), Qt::CaseInsensitive) == 0) - { - templ = new TemplateMap(clean_path, map); - } - else if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) - { - templ = new TemplateImage(clean_path, map); - } - else - { - addWarning(tr("Unable to import template: background \"%1\" doesn't seem to be a raster image").arg(filename)); - return nullptr; - } - - OCADBackground background = importBackground(data); - MapCoord c; - convertPoint(c, background.trnx, background.trny); - templ->setTemplatePosition(c); - templ->setTemplateRotation(M_PI / 180 * background.angle); - templ->setTemplateScaleX(convertTemplateScale(background.sclx)); - templ->setTemplateScaleY(convertTemplateScale(background.scly)); - templ->setTemplateShear(0.0); - - map->templates.insert(map->templates.begin(), std::unique_ptr