├── icon.rc.in ├── version.txt.in ├── src ├── tilefactory │ ├── rust │ │ ├── .gitignore │ │ ├── build.rs │ │ ├── Cargo.toml │ │ ├── lib.rs │ │ └── CMakeLists.txt │ ├── coverageratio.h │ ├── include │ │ └── tilefactory │ │ │ ├── itilesource.h │ │ │ ├── triangulator.h │ │ │ ├── pos.h │ │ │ ├── catalog.h │ │ │ ├── mercator.h │ │ │ ├── oesenctilesource.h │ │ │ ├── georect.h │ │ │ ├── chartclipper.h │ │ │ ├── chart.h │ │ │ └── tilefactory.h │ ├── pos.cpp │ ├── filehelper.h │ ├── triangulator.cpp │ ├── CMakeLists.txt │ ├── georect.cpp │ ├── filehelper.cpp │ ├── coverageratio.cpp │ ├── mercator.cpp │ ├── chartdata.capnp │ ├── catalog.cpp │ ├── oesenctilesource.cpp │ └── chartclipper.cpp ├── scene │ ├── geometrynode.cpp │ ├── geometrynode.h │ ├── polygon │ │ ├── polygonshader.h │ │ ├── polygon.frag │ │ ├── polygon.vert │ │ ├── polygonmaterial.h │ │ ├── polygonnode.h │ │ ├── polygonmaterial.cpp │ │ ├── polygonshader.cpp │ │ └── polygonnode.cpp │ ├── test │ │ ├── CMakeLists.txt │ │ └── fontimage_test.cpp │ ├── line │ │ ├── line.frag │ │ ├── lineshader.h │ │ ├── linematerial.h │ │ ├── linenode.h │ │ ├── linematerial.cpp │ │ ├── line.vert │ │ ├── lineshader.cpp │ │ └── linenode.cpp │ ├── rootnode.h │ ├── annotations │ │ ├── zoomsweeper.h │ │ ├── annotationmaterial.h │ │ ├── annotationmaterial.cpp │ │ ├── annotation.vert │ │ ├── annotationshader.h │ │ ├── annotationnode.h │ │ ├── annotation.frag │ │ ├── symbolimage.h │ │ ├── annotationshader.cpp │ │ ├── annotationnode.cpp │ │ └── annotater.h │ ├── rootnode.cpp │ ├── tiledata.h │ ├── tilefactorywrapper.cpp │ ├── materialcreator.h │ ├── include │ │ └── scene │ │ │ ├── annotations │ │ │ ├── fontimage.h │ │ │ └── types.h │ │ │ ├── tilefactorywrapper.h │ │ │ └── scene.h │ ├── tessellator.h │ ├── materialcreator.cpp │ └── CMakeLists.txt ├── licenses.h ├── tileinfobackend.h ├── maptilemodel.h ├── mercatorwrapper.h ├── licenses.cpp ├── usersettings.h ├── tileinfobackend.cpp ├── chartmodel.h ├── usersettings.cpp ├── main.cpp ├── maptilemodel.cpp └── maptile.h ├── .gitignore ├── version.h.in ├── chocolatey.config ├── qml ├── XdgFileDialogWrapper.qml ├── qmldir ├── qml.qrc ├── about.md ├── ChartSelector.qml ├── FolderDialog.qml ├── StatusBar.qml ├── About.qml └── TileInfo.qml ├── snap ├── gui │ └── nautograf.desktop └── snapcraft.yaml ├── deploy-qt-windows.cmake.in ├── symbols ├── test_symbol.svg ├── underwater_rocks │ ├── awash.svg │ ├── always_submerged.svg │ └── cover_and_uncovers.svg ├── buoys │ ├── super_buoy.svg │ ├── can.svg │ ├── spar_port.svg │ ├── spar_starboard.svg │ ├── conical.svg │ ├── barrel.svg │ ├── pillar.svg │ └── spherical.svg ├── beacons │ └── beacon_norwegian.svg └── symbols.qrc ├── .commitlintrc.mjs ├── privacy_policy.md ├── .clang-format ├── vcpkg.json ├── cmake ├── InstallLicense.cmake └── GetVersionFromGit.cmake ├── create_icon.sh ├── external └── CMakeLists.txt ├── .gitmodules ├── AppxManifest.xml.in ├── CMakePresets.json ├── README.md ├── CMakeLists.txt └── .github └── workflows └── ci.yml /icon.rc.in: -------------------------------------------------------------------------------- 1 | Icon ICON icon.ico 2 | -------------------------------------------------------------------------------- /version.txt.in: -------------------------------------------------------------------------------- 1 | @NAUTOGRAF_VERSION_DESCRIBE@ 2 | 3 | -------------------------------------------------------------------------------- /src/tilefactory/rust/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeLists.txt.user 2 | CMakeUserPresets.json 3 | build* 4 | *.png 5 | *.ico 6 | -------------------------------------------------------------------------------- /version.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define NAUTOGRAF_VERSION "@NAUTOGRAF_VERSION_DESCRIBE@" 4 | -------------------------------------------------------------------------------- /chocolatey.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /qml/XdgFileDialogWrapper.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.seatronomy.nautograf 3 | 4 | // Only used on Linux when compiled with option USE_XDG_FILE_DIALOG 5 | 6 | XdgFileDialog {} 7 | -------------------------------------------------------------------------------- /snap/gui/nautograf.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Nautograf 4 | Comment=Tile-based nautical chart viewer 5 | Exec=nautograf 6 | Icon=${SNAP}/usr/share/icons/hicolor/scalable/apps/nautograf.svg 7 | -------------------------------------------------------------------------------- /qml/qmldir: -------------------------------------------------------------------------------- 1 | module Nautograf 2 | Viewer 1.0 Viewer.qml 3 | ChartSelector 1.0 ChartSelector.qml 4 | ViewerMenu 1.0 ViewerMenu.qml 5 | ViewerMenuItem 1.0 ViewerMenuItem.qml 6 | StatusBar 1.0 StatusBar.qml 7 | TileInfo 1.0 TileInfo.qml 8 | -------------------------------------------------------------------------------- /src/scene/geometrynode.cpp: -------------------------------------------------------------------------------- 1 | #include "geometrynode.h" 2 | 3 | GeometryNode::GeometryNode(const QString &id) 4 | : m_id(id) 5 | { 6 | } 7 | 8 | const QString &GeometryNode::id() const 9 | { 10 | return m_id; 11 | } 12 | -------------------------------------------------------------------------------- /src/tilefactory/rust/build.rs: -------------------------------------------------------------------------------- 1 | use cxx_build::CFG; 2 | 3 | #[allow(unused_must_use)] 4 | fn main() { 5 | CFG.include_prefix = "tilefactory_rust"; 6 | cxx_build::bridge("lib.rs"); 7 | println!("cargo:rerun-if-changed=lib.rs"); 8 | } 9 | -------------------------------------------------------------------------------- /deploy-qt-windows.cmake.in: -------------------------------------------------------------------------------- 1 | set(WINDEPLOYQT "@WINDEPLOYQT@") 2 | set(CMAKE_SOURCE_DIR "@CMAKE_SOURCE_DIR@") 3 | 4 | execute_process( 5 | COMMAND ${WINDEPLOYQT} --no-compiler-runtime --qmldir ${CMAKE_SOURCE_DIR}/qml ${EXECUTABLE_DIR} 6 | COMMAND_ERROR_IS_FATAL ANY 7 | ) 8 | -------------------------------------------------------------------------------- /src/scene/geometrynode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class GeometryNode : public QSGNode 7 | { 8 | public: 9 | GeometryNode() = delete; 10 | GeometryNode(const QString &id); 11 | const QString &id() const; 12 | 13 | private: 14 | QString m_id; 15 | }; 16 | -------------------------------------------------------------------------------- /symbols/test_symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scene/polygon/polygonshader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class PolygonShader : public QSGMaterialShader 6 | { 7 | public: 8 | PolygonShader(); 9 | bool updateUniformData(RenderState &state, 10 | QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; 11 | }; 12 | -------------------------------------------------------------------------------- /.commitlintrc.mjs: -------------------------------------------------------------------------------- 1 | const scopes = [ 2 | 'scene', 3 | 'tilefactory', 4 | ]; 5 | 6 | export default { 7 | extends: ['@commitlint/config-conventional'], 8 | 9 | rules: { 10 | 'header-max-length': [2, 'always', 72], 11 | 'subject-case': [2, 'always', 'sentence-case'], 12 | 'scope-enum': [2, 'always', scopes], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/scene/test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(GTest CONFIG REQUIRED) 2 | include(GoogleTest) 3 | 4 | add_executable(fontimage_test 5 | fontimage_test.cpp 6 | ) 7 | 8 | target_link_libraries(fontimage_test 9 | PUBLIC 10 | GTest::gtest 11 | GTest::gtest_main 12 | scene 13 | ) 14 | 15 | gtest_discover_tests(fontimage_test) 16 | -------------------------------------------------------------------------------- /privacy_policy.md: -------------------------------------------------------------------------------- 1 | # Privacy policy 2 | 3 | 1. This application does not transmit any information to third parties. 4 | 2. This application will generate and store "map tiles" on the device it 5 | is installed. These map tiles will indicate the geographic areas that have 6 | been viewed in the application. The map tiles are kept until the application 7 | is uninstalled. 8 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: WebKit 2 | Standard: Cpp11 3 | ColumnLimit: 0 4 | PointerBindsToType: false 5 | BreakBeforeBinaryOperators: NonAssignment 6 | AlignAfterOpenBracket: true 7 | AlwaysBreakTemplateDeclarations: true 8 | SortIncludes: true 9 | BreakBeforeBraces: Custom 10 | BraceWrapping: 11 | AfterClass: true 12 | AfterFunction: true 13 | AfterStruct: true 14 | -------------------------------------------------------------------------------- /symbols/underwater_rocks/awash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scene/polygon/polygon.frag: -------------------------------------------------------------------------------- 1 | #version 440 2 | 3 | layout(location = 0) in vec4 color; 4 | 5 | layout(location = 0) out vec4 fragColor; 6 | 7 | layout(std140, binding = 0) uniform buf { 8 | mat4 qt_Matrix; 9 | float qt_Opacity; 10 | } ubuf; 11 | 12 | void main() 13 | { 14 | float opacity = ubuf.qt_Opacity * color.a; 15 | fragColor = vec4(color.rgb * opacity, opacity); 16 | } 17 | -------------------------------------------------------------------------------- /symbols/underwater_rocks/always_submerged.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nautograf", 3 | "version": "0.1.0", 4 | "builtin-baseline": "a1703aa3e8b3fccd8c98ab9685444c2a3f007aab", 5 | "dependencies": ["capnproto", "tinyxml2", "earcut-hpp", "clipper2", 6 | { 7 | "name": "freetype", 8 | "default-features": false, 9 | "features": ["zlib"] 10 | }, 11 | "libpng", 12 | "nlohmann-json", 13 | "quadtree"] 14 | } 15 | -------------------------------------------------------------------------------- /src/tilefactory/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tilefactory_rust" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | crate-type = ["staticlib"] 9 | path = "lib.rs" 10 | 11 | [dependencies] 12 | cxx = "=1.0.173" 13 | geo = { version = "0.28.0", default-features = false } 14 | 15 | [build-dependencies] 16 | cxx-build = "=1.0.173" 17 | -------------------------------------------------------------------------------- /src/scene/polygon/polygon.vert: -------------------------------------------------------------------------------- 1 | #version 440 2 | 3 | layout(location = 0) in vec3 aPos; 4 | layout(location = 1) in vec4 colorIn; 5 | 6 | layout(location = 0) out vec4 color; 7 | 8 | layout(std140, binding = 0) uniform buf { 9 | mat4 qt_Matrix; 10 | float qt_Opacity; 11 | } ubuf; 12 | 13 | out gl_PerVertex { vec4 gl_Position; }; 14 | 15 | void main() 16 | { 17 | gl_Position = ubuf.qt_Matrix * vec4(aPos.xyz, 1.0); 18 | color = colorIn; 19 | } 20 | -------------------------------------------------------------------------------- /qml/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | About.qml 5 | FolderDialog.qml 6 | Viewer.qml 7 | ChartSelector.qml 8 | StatusBar.qml 9 | TileInfo.qml 10 | XdgFileDialogWrapper.qml 11 | about.md 12 | graphics/title.svg 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/tilefactory/coverageratio.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "tilefactory/chart.h" 6 | #include "tilefactory/georect.h" 7 | 8 | class CoverageRatio 9 | { 10 | public: 11 | CoverageRatio(const GeoRect &rect); 12 | void accumulate(const capnp::List::Reader &coverages); 13 | float ratio() const; 14 | 15 | private: 16 | float coveredArea() const; 17 | GeoRect m_rect; 18 | Clipper2Lib::PathsD m_coverage; 19 | }; 20 | -------------------------------------------------------------------------------- /src/scene/line/line.frag: -------------------------------------------------------------------------------- 1 | #version 440 2 | 3 | layout(location = 1) in vec3 color; 4 | layout(location = 2) in vec2 normal; 5 | 6 | layout(std140, binding = 0) uniform buf { 7 | mat4 modelView; 8 | mat4 projection; 9 | float qt_Opacity; 10 | float width; 11 | } ubuf; 12 | 13 | layout(location = 0) out vec4 fragColor; 14 | 15 | void main() 16 | { 17 | float alpha = smoothstep(1.0, 0.0, length(normal)) * ubuf.qt_Opacity; 18 | fragColor = vec4(color.rgb * alpha, alpha) ; 19 | } 20 | -------------------------------------------------------------------------------- /src/scene/rootnode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class MaterialCreator; 6 | class QSGTexture; 7 | class QQuickWindow; 8 | 9 | class RootNode : public QSGTransformNode 10 | { 11 | public: 12 | RootNode(const QImage &symbolImage, 13 | const QImage &fontImage, 14 | const QQuickWindow *window); 15 | ~RootNode(); 16 | MaterialCreator *materialCreator() const; 17 | 18 | private: 19 | MaterialCreator *m_materialCreator = nullptr; 20 | }; 21 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/itilesource.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "chart.h" 7 | #include "georect.h" 8 | 9 | #include "tilefactory_export.h" 10 | 11 | class TILEFACTORY_EXPORT ITileSource 12 | { 13 | public: 14 | virtual std::shared_ptr create(const GeoRect &boundingBox, 15 | int pixelsPerLongitude) = 0; 16 | virtual GeoRect extent() const = 0; 17 | virtual int scale() const = 0; 18 | }; 19 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/triangulator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "tilefactory_export.h" 7 | 8 | class TILEFACTORY_EXPORT Triangulator 9 | { 10 | public: 11 | Triangulator() = delete; 12 | using Point = std::array; 13 | static std::vector calc(const std::vector> &polygons); 14 | 15 | private: 16 | static Point getPoint(const std::vector> &polygon, int index); 17 | }; 18 | -------------------------------------------------------------------------------- /src/scene/polygon/polygonmaterial.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class PolygonMaterial : public QSGMaterial 7 | { 8 | public: 9 | enum class BlendMode { 10 | Opaque, 11 | Alpha 12 | }; 13 | PolygonMaterial(BlendMode blendMode = BlendMode::Opaque); 14 | QSGMaterialType *type() const override; 15 | int compare(const QSGMaterial *other) const override; 16 | QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override; 17 | }; 18 | -------------------------------------------------------------------------------- /src/scene/polygon/polygonnode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QSGMaterial; 6 | 7 | class PolygonNode : public QSGGeometryNode 8 | { 9 | public: 10 | struct Vertex 11 | { 12 | float x; 13 | float y; 14 | float z; 15 | uchar red; 16 | uchar green; 17 | uchar blue; 18 | uchar alpha; 19 | }; 20 | PolygonNode(QSGMaterial *material, const QList &vertices); 21 | void updateVertices(const QList &vertices); 22 | }; 23 | -------------------------------------------------------------------------------- /src/scene/test/fontimage_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "scene/annotations/fontimage.h" 7 | 8 | TEST(FontImageTest, SaveAtlasToFile) 9 | { 10 | FontImage fontImage; 11 | const QImage &image = fontImage.image(); 12 | image.save("atlas.png"); 13 | } 14 | 15 | TEST(FontImageTest, EmptyTextGivesZeroBoundingBox) 16 | { 17 | FontImage fontImage; 18 | QRectF boundingBox = fontImage.boundingBox(QString(), 12); 19 | ASSERT_TRUE(boundingBox.isNull()); 20 | } 21 | -------------------------------------------------------------------------------- /src/tilefactory/pos.cpp: -------------------------------------------------------------------------------- 1 | #include "tilefactory/pos.h" 2 | 3 | Pos::Pos(double lat, double lon) 4 | : m_lat(lat) 5 | , m_lon(lon) 6 | { 7 | } 8 | 9 | bool Pos::operator!=(const Pos &pos) const 10 | { 11 | return (m_lat != pos.lat() || m_lon != pos.lon()); 12 | } 13 | 14 | bool Pos::operator==(const Pos &pos) const 15 | { 16 | return (m_lat == pos.lat() && m_lon == pos.lon()); 17 | } 18 | 19 | std::ostream &operator<<(std::ostream &os, const Pos &pos) 20 | { 21 | os << pos.lat() << " " << pos.lon(); 22 | return os; 23 | } 24 | -------------------------------------------------------------------------------- /symbols/underwater_rocks/cover_and_uncovers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /symbols/buoys/super_buoy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /symbols/beacons/beacon_norwegian.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scene/line/lineshader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class LineShader : public QSGMaterialShader 9 | { 10 | public: 11 | LineShader() 12 | { 13 | setShaderFileName(VertexStage, QLatin1String(":/scene/line/line.vert.qsb")); 14 | setShaderFileName(FragmentStage, QLatin1String(":/scene/line/line.frag.qsb")); 15 | } 16 | bool updateUniformData(RenderState &state, 17 | QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; 18 | }; 19 | -------------------------------------------------------------------------------- /src/scene/line/linematerial.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class LineMaterial : public QSGMaterial 7 | { 8 | public: 9 | LineMaterial(); 10 | QSGMaterialType *type() const override; 11 | 12 | int compare(const QSGMaterial *other) const override; 13 | QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override; 14 | 15 | void setWidth(float width) 16 | { 17 | uniforms.width = width; 18 | uniforms.dirty = true; 19 | } 20 | 21 | struct Uniforms 22 | { 23 | float width = 0; 24 | bool dirty = false; 25 | }; 26 | 27 | Uniforms uniforms; 28 | }; 29 | -------------------------------------------------------------------------------- /symbols/symbols.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | test_symbol.svg 5 | buoys/barrel.svg 6 | buoys/can.svg 7 | buoys/conical.svg 8 | buoys/pillar.svg 9 | buoys/spar_port.svg 10 | buoys/spar_starboard.svg 11 | buoys/spherical.svg 12 | buoys/super_buoy.svg 13 | beacons/beacon_norwegian.svg 14 | underwater_rocks/always_submerged.svg 15 | underwater_rocks/awash.svg 16 | underwater_rocks/cover_and_uncovers.svg 17 | 18 | -------------------------------------------------------------------------------- /symbols/buoys/can.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /qml/about.md: -------------------------------------------------------------------------------- 1 | Nautograf is an experimental viewer for marine vector charts and is inspired by [OpenCPN](https://opencpn.org/). 2 | 3 | This software is released under the GNU General Public License and comes with no warranty. Using this software for navigation is highly discouraged. 4 | 5 | This software depends on other libraries such as [The Qt Framework](https://www.qt.io/product/qt6). The Qt framework is licensed under the "GNU Lesser General Public License". 6 | 7 | Please open the "Licenses" tab to review the licenses of the other dependent software libraries. 8 | 9 | For more information, see the [GitHub repository for the project](https://github.com/hornang/nautograf). 10 | 11 | Copyright 2024 Stig Hornang 12 | -------------------------------------------------------------------------------- /cmake/InstallLicense.cmake: -------------------------------------------------------------------------------- 1 | function(install_license) 2 | set(oneValueArgs FILE) 3 | cmake_parse_arguments(INSTALL_LICENSE "" "${oneValueArgs}" "" ${ARGN}) 4 | 5 | if(NOT EXISTS ${INSTALL_LICENSE_FILE}) 6 | message(FATAL_ERROR "Can not find license file ${INSTALL_LICENSE_FILE}") 7 | endif() 8 | 9 | cmake_path(GET INSTALL_LICENSE_FILE PARENT_PATH DIR) 10 | cmake_path(GET DIR FILENAME NAME) 11 | 12 | configure_file( 13 | ${INSTALL_LICENSE_FILE} 14 | "${CMAKE_BINARY_DIR}/licenses/${NAME}.txt" 15 | COPYONLY 16 | ) 17 | 18 | install( 19 | FILES "${CMAKE_BINARY_DIR}/licenses/${NAME}.txt" 20 | DESTINATION licenses 21 | ) 22 | 23 | endfunction() 24 | -------------------------------------------------------------------------------- /symbols/buoys/spar_port.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scene/line/linenode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class LineMaterial; 10 | 11 | class LineNode : public QSGGeometryNode 12 | { 13 | public: 14 | struct Vertex 15 | { 16 | float xPos; 17 | float yPos; 18 | float zPos; 19 | float xOffset; 20 | float yOffset; 21 | uchar red; 22 | uchar green; 23 | uchar blue; 24 | uchar alpha; 25 | }; 26 | LineNode(QSGMaterial *material, 27 | const QList &vertices); 28 | 29 | public: 30 | void updateVertices(const QList &vertices); 31 | }; 32 | -------------------------------------------------------------------------------- /symbols/buoys/spar_starboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scene/line/linematerial.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "linematerial.h" 4 | #include "lineshader.h" 5 | 6 | LineMaterial::LineMaterial() 7 | { 8 | setFlag(QSGMaterial::Blending); 9 | } 10 | 11 | QSGMaterialType *LineMaterial::type() const 12 | { 13 | static QSGMaterialType type; 14 | return &type; 15 | } 16 | 17 | int LineMaterial::compare(const QSGMaterial *o) const 18 | { 19 | Q_ASSERT(o && type() == o->type()); 20 | const auto *other = static_cast(o); 21 | return other == this ? 0 : 1; // ### TODO: compare state??? <- From custommaterial example 22 | } 23 | 24 | QSGMaterialShader *LineMaterial::createShader(QSGRendererInterface::RenderMode) const 25 | { 26 | return new LineShader(); 27 | } 28 | -------------------------------------------------------------------------------- /create_icon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | INPUT=$1 6 | OUTPUT=$2 7 | IMAGEMAGICK=$3 8 | 9 | FILENAME=$(basename -- "$INPUT") 10 | EXTENSION="${FILENAME##*.}" 11 | FILENAME="${FILENAME%.*}" 12 | 13 | WIDTHS=(16 32 44 64 128 150 256) 14 | 15 | for WIDTH in "${WIDTHS[@]}" 16 | do 17 | rsvg-convert --output="${FILENAME}_${WIDTH}.png" -w $WIDTH "$INPUT" 18 | done 19 | 20 | PNGS=() 21 | for WIDTH in "${WIDTHS[@]}"; do 22 | PNGS+=("${FILENAME}_${WIDTH}.png") 23 | done 24 | 25 | PNGS_ARG="${PNGS[@]}" 26 | 27 | if [[ "$IMAGEMAGICK" == *convert ]]; then 28 | # We don't call use subcommand on a system with the legacy "convert" executable 29 | convert $PNGS_ARG "$OUTPUT" 30 | else 31 | "$IMAGEMAGICK" convert $PNGS_ARG "$OUTPUT" 32 | fi 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/scene/annotations/zoomsweeper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "scene/annotations/types.h" 8 | 9 | class ZoomSweeper 10 | { 11 | 12 | public: 13 | ZoomSweeper(float maxZoom, const QRect ®ion); 14 | void calcSymbols(std::vector &symbols); 15 | void calcLabels(const std::vector &symbols, 16 | std::vector &labels); 17 | 18 | private: 19 | static constexpr float m_zoomRatios[] = { 5, 4, 3, 2, 1, 0 }; 20 | 21 | struct TestZoom 22 | { 23 | QTransform transform; 24 | float zoom = 0; 25 | }; 26 | 27 | TestZoom m_testZooms[std::size(m_zoomRatios)]; 28 | QRect m_region; 29 | }; 30 | -------------------------------------------------------------------------------- /symbols/buoys/conical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scene/line/line.vert: -------------------------------------------------------------------------------- 1 | #version 440 2 | 3 | layout(location = 0) in vec2 aPos; 4 | layout(location = 1) in vec2 aNormal; 5 | layout(location = 2) in vec3 colorIn; 6 | 7 | layout(location = 1) out vec3 color; 8 | layout(location = 2) out vec2 normal; 9 | 10 | layout(std140, binding = 0) uniform buf { 11 | mat4 modelView; 12 | mat4 projection; 13 | float qt_Opacity; 14 | float width; 15 | } ubuf; 16 | 17 | out gl_PerVertex { vec4 gl_Position; }; 18 | 19 | void main() 20 | { 21 | float u_linewidth = ubuf.width; 22 | vec4 delta = vec4(0.5 * aNormal * u_linewidth, 0, 0); 23 | vec4 center = ubuf.modelView * vec4(aPos.x, aPos.y, 0.0, 1.0); 24 | 25 | gl_Position = ubuf.projection * (center + delta); 26 | color = colorIn; 27 | normal = aNormal; 28 | } 29 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/pos.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "tilefactory_export.h" 6 | 7 | class TILEFACTORY_EXPORT Pos 8 | { 9 | public: 10 | Pos() = default; 11 | Pos(double lat, double lon); 12 | bool operator!=(const Pos &pos) const; 13 | bool operator==(const Pos &pos) const; 14 | inline double lat() const { return m_lat; } 15 | inline double lon() const { return m_lon; } 16 | inline void setLat(double lat) { m_lat = lat; } 17 | inline void setLon(double lon) { m_lon = lon; } 18 | 19 | private: 20 | /// North/South coordinate in degrees 21 | double m_lat = 0; 22 | 23 | /// East/West coordinate in degrees 24 | double m_lon = 0; 25 | }; 26 | 27 | std::ostream &operator<<(std::ostream &os, const Pos &pos); 28 | -------------------------------------------------------------------------------- /src/scene/annotations/annotationmaterial.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class AnnotationMaterial : public QSGMaterial 7 | { 8 | public: 9 | AnnotationMaterial(QSGTexture *texture); 10 | QSGMaterialType *type() const override; 11 | 12 | int compare(const QSGMaterial *other) const override; 13 | QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override; 14 | QSGTexture *texture() const { return m_texture; } 15 | void setZoom(float zoom) 16 | { 17 | uniforms.zoom = zoom; 18 | uniforms.dirty = true; 19 | } 20 | 21 | struct 22 | { 23 | float zoom = 0; 24 | bool dirty = false; 25 | } uniforms; 26 | 27 | private: 28 | QSGTexture *m_texture = nullptr; 29 | }; 30 | -------------------------------------------------------------------------------- /src/tilefactory/filehelper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "tilefactory/georect.h" 6 | 7 | class FileHelper 8 | { 9 | public: 10 | static std::string tileId(const GeoRect &boundingBox, int pixelsPerLongitude); 11 | static std::string chartTypeIdToString(uint64_t typeId); 12 | static std::string getTileDir(const std::string &tileDir, uint64_t typeId); 13 | static std::string tileFileName(const std::string &tileDir, 14 | const std::string &name, 15 | const std::string &id); 16 | static std::string internalChartFileName(const std::string &tileDir, 17 | const std::string &name, 18 | int pixelsPerLon); 19 | }; 20 | -------------------------------------------------------------------------------- /symbols/buoys/barrel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scene/polygon/polygonmaterial.cpp: -------------------------------------------------------------------------------- 1 | #include "polygonmaterial.h" 2 | #include "polygonshader.h" 3 | 4 | PolygonMaterial::PolygonMaterial(BlendMode blendMode) 5 | { 6 | if (blendMode == BlendMode::Alpha) { 7 | setFlag(QSGMaterial::Blending); 8 | } 9 | } 10 | 11 | QSGMaterialType *PolygonMaterial::type() const 12 | { 13 | static QSGMaterialType type; 14 | return &type; 15 | } 16 | 17 | int PolygonMaterial::compare(const QSGMaterial *o) const 18 | { 19 | Q_ASSERT(o && type() == o->type()); 20 | const auto *other = static_cast(o); 21 | return other == this ? 0 : 1; // ### TODO: compare state??? <- From custommaterial example 22 | } 23 | 24 | QSGMaterialShader *PolygonMaterial::createShader(QSGRendererInterface::RenderMode) const 25 | { 26 | return new PolygonShader(); 27 | } 28 | -------------------------------------------------------------------------------- /symbols/buoys/pillar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /external/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(${USE_XDG_FILE_DIALOG}) 2 | add_subdirectory(xdg-file-dialog) 3 | endif() 4 | 5 | set(MSDFGEN_BUILD_STANDALONE OFF) 6 | set(MSDFGEN_USE_SKIA OFF) 7 | set(MSDFGEN_INSTALL ON) 8 | set(MSDFGEN_DYNAMIC_RUNTIME ON) 9 | 10 | add_subdirectory(msdfgen) 11 | install_license(FILE ${CMAKE_CURRENT_SOURCE_DIR}/msdfgen/LICENSE.txt) 12 | 13 | set(MSDF_ATLAS_MSDFGEN_EXTERNAL ON) 14 | set(MSDF_ATLAS_NO_ARTERY_FONT ON) 15 | set(MSDF_ATLAS_INSTALL ON) 16 | set(MSDF_ATLAS_BUILD_STANDALONE OFF) 17 | set(MSDF_ATLAS_DYNAMIC_RUNTIME ON) 18 | 19 | add_subdirectory(msdf-atlas-gen) 20 | install_license(FILE ${CMAKE_CURRENT_SOURCE_DIR}/msdf-atlas-gen/LICENSE.txt) 21 | 22 | add_subdirectory(cutlines) 23 | add_subdirectory(msdf-atlas-read) 24 | 25 | add_subdirectory(MercatorTile) 26 | install_license(FILE ${CMAKE_CURRENT_SOURCE_DIR}/MercatorTile/LICENSE) 27 | 28 | add_subdirectory(oesenc) 29 | -------------------------------------------------------------------------------- /src/scene/annotations/annotationmaterial.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "annotationmaterial.h" 4 | #include "annotationshader.h" 5 | 6 | AnnotationMaterial::AnnotationMaterial(QSGTexture *texture) 7 | : m_texture(texture) 8 | { 9 | setFlag(QSGMaterial::RequiresFullMatrix | QSGMaterial::Blending); 10 | } 11 | 12 | QSGMaterialType *AnnotationMaterial::type() const 13 | { 14 | static QSGMaterialType type; 15 | return &type; 16 | } 17 | 18 | int AnnotationMaterial::compare(const QSGMaterial *o) const 19 | { 20 | Q_ASSERT(o && type() == o->type()); 21 | const auto *other = static_cast(o); 22 | return other == this ? 0 : 1; // ### TODO: compare state??? <- From custommaterial example 23 | } 24 | 25 | QSGMaterialShader *AnnotationMaterial::createShader(QSGRendererInterface::RenderMode) const 26 | { 27 | return new AnnotationShader(); 28 | } 29 | -------------------------------------------------------------------------------- /symbols/buoys/spherical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/scene/annotations/annotation.vert: -------------------------------------------------------------------------------- 1 | #version 440 2 | 3 | layout(location = 0) in vec2 aPos; 4 | layout(location = 1) in vec2 offset; 5 | layout(location = 2) in vec2 aTexCoord; 6 | layout(location = 3) in vec3 colorIn; 7 | layout(location = 4) in float minZoomIn; 8 | 9 | layout(location = 0) out vec2 vTexCoord; 10 | layout(location = 1) out vec3 color; 11 | layout(location = 2) out float minZoom; 12 | 13 | layout(std140, binding = 0) uniform buf { 14 | mat4 modelView; 15 | mat4 projection; 16 | float qt_Opacity; 17 | float zoom; 18 | } ubuf; 19 | 20 | out gl_PerVertex { vec4 gl_Position; }; 21 | 22 | void main() 23 | { 24 | vec4 center = ubuf.modelView * vec4(aPos.x, aPos.y, 0.0, 1.0); 25 | center.x += offset.x; 26 | center.y += offset.y; 27 | 28 | gl_Position = ubuf.projection * center; 29 | 30 | vTexCoord = aTexCoord; 31 | minZoom = minZoomIn; 32 | color = colorIn; 33 | } 34 | -------------------------------------------------------------------------------- /src/scene/annotations/annotationshader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class AnnotationShader : public QSGMaterialShader 9 | { 10 | public: 11 | AnnotationShader() 12 | { 13 | setShaderFileName(VertexStage, QLatin1String(":/scene/annotations/annotation.vert.qsb")); 14 | setShaderFileName(FragmentStage, QLatin1String(":/scene/annotations/annotation.frag.qsb")); 15 | } 16 | bool updateUniformData(RenderState &state, 17 | QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; 18 | 19 | void updateSampledImage(QSGMaterialShader::RenderState &state, 20 | int binding, 21 | QSGTexture **texture, 22 | QSGMaterial *newMaterial, 23 | QSGMaterial *oldMaterial) override; 24 | }; 25 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/catalog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "tilefactory_export.h" 11 | 12 | class TILEFACTORY_EXPORT Catalog 13 | { 14 | public: 15 | enum class Type { 16 | Invalid, 17 | Oesu, 18 | Oesenc, 19 | Unencrypted, 20 | }; 21 | 22 | Catalog(oesenc::ServerControl *serverControl, std::string_view dir); 23 | std::shared_ptr openChart(std::string_view fileName); 24 | std::vector chartFileNames() const; 25 | Type type() const; 26 | 27 | private: 28 | std::unordered_map m_oesuKeys; 29 | std::string m_oesencKey; 30 | std::filesystem::path m_dir; 31 | Type m_type = Type::Invalid; 32 | oesenc::ServerControl *m_serverControl; 33 | std::shared_ptr m_currentStream; 34 | }; 35 | -------------------------------------------------------------------------------- /src/scene/polygon/polygonshader.cpp: -------------------------------------------------------------------------------- 1 | #include "polygonshader.h" 2 | #include "polygonmaterial.h" 3 | 4 | PolygonShader::PolygonShader() 5 | { 6 | setShaderFileName(VertexStage, QLatin1String(":/scene/polygon/polygon.vert.qsb")); 7 | setShaderFileName(FragmentStage, QLatin1String(":/scene/polygon/polygon.frag.qsb")); 8 | } 9 | 10 | bool PolygonShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) 11 | { 12 | bool changed = false; 13 | QByteArray *uniformBuffer = state.uniformData(); 14 | Q_ASSERT(uniformBuffer->size() == 68); 15 | 16 | if (state.isMatrixDirty()) { 17 | memcpy(uniformBuffer->data(), state.combinedMatrix().constData(), 64); 18 | changed = true; 19 | } 20 | 21 | if (state.isOpacityDirty()) { 22 | const float opacity = state.opacity(); 23 | memcpy(uniformBuffer->data() + 64, &opacity, 4); 24 | changed = true; 25 | } 26 | 27 | return changed; 28 | } 29 | -------------------------------------------------------------------------------- /src/tilefactory/rust/lib.rs: -------------------------------------------------------------------------------- 1 | use ffi::*; 2 | use geo::{self, Simplify}; 3 | 4 | #[cxx::bridge(namespace = "tilefactory_rust")] 5 | mod ffi { 6 | #[derive(Clone, Copy)] 7 | struct Pos { 8 | x: f64, 9 | y: f64, 10 | } 11 | 12 | extern "Rust" { 13 | fn simplify(input: &Vec, epsilon: f64) -> Vec; 14 | } 15 | } 16 | 17 | fn simplify(input: &Vec, epsilon: f64) -> Vec { 18 | let mut coord_vector = Vec::new(); 19 | 20 | for val in input { 21 | coord_vector.push(geo::Coord { x: val.x, y: val.y }); 22 | } 23 | 24 | let geo_line_string = geo::LineString::new(coord_vector); 25 | let simplified_geo_line_string = geo_line_string.simplify(&epsilon); 26 | let mut simplified_coord_vector: Vec = Vec::new(); 27 | 28 | for value in simplified_geo_line_string { 29 | simplified_coord_vector.push(Pos { 30 | x: value.x, 31 | y: value.y, 32 | }); 33 | } 34 | 35 | simplified_coord_vector 36 | } 37 | -------------------------------------------------------------------------------- /src/scene/annotations/annotationnode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class AnnotationMaterial; 10 | 11 | class AnnotationNode : public QSGGeometryNode 12 | { 13 | public: 14 | struct Vertex 15 | { 16 | float xCenter; 17 | float yCenter; 18 | float xOffset; 19 | float yOffset; 20 | float xTexture; 21 | float yTexture; 22 | uchar red; 23 | uchar green; 24 | uchar blue; 25 | uchar alpha; // For padding purposes. Ignored by shader 26 | float minZoom; 27 | }; 28 | AnnotationNode(const QString &tileId, 29 | QSGMaterial *material, 30 | const QList &vertices); 31 | const QString &id() const { return m_id; } 32 | 33 | public: 34 | void updateVertices(const QList &vertices); 35 | 36 | private: 37 | QString m_id; 38 | }; 39 | -------------------------------------------------------------------------------- /src/scene/rootnode.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "annotations/annotationmaterial.h" 5 | #include "annotations/symbolimage.h" 6 | #include "line/linematerial.h" 7 | #include "materialcreator.h" 8 | #include "polygon/polygonmaterial.h" 9 | #include "rootnode.h" 10 | 11 | RootNode::RootNode(const QImage &symbolImage, 12 | const QImage &fontImage, 13 | const QQuickWindow *window) 14 | { 15 | QSGTexture *symbolTexture = window->createTextureFromImage(symbolImage); 16 | symbolTexture->setFiltering(QSGTexture::Linear); 17 | 18 | QSGTexture *fontTexture = window->createTextureFromImage(fontImage); 19 | fontTexture->setFiltering(QSGTexture::Linear); 20 | 21 | m_materialCreator = new MaterialCreator(symbolTexture, fontTexture); 22 | } 23 | 24 | MaterialCreator *RootNode::materialCreator() const 25 | { 26 | return m_materialCreator; 27 | } 28 | 29 | RootNode::~RootNode() 30 | { 31 | if (m_materialCreator) { 32 | delete m_materialCreator; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/licenses.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Licenses : public QObject 6 | { 7 | Q_OBJECT 8 | Q_PROPERTY(QStringList names MEMBER m_names NOTIFY licensesChanged) 9 | Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged) 10 | Q_PROPERTY(QString currentLicense READ currentLicense NOTIFY currentLicenseChanged) 11 | 12 | public: 13 | Licenses(); 14 | QStringList names() const; 15 | 16 | int index() const; 17 | QString currentLicense() const; 18 | void setIndex(int newIndex); 19 | void load(int index); 20 | 21 | public slots: 22 | 23 | signals: 24 | void indexChanged(); 25 | void currentLicenseChanged(); 26 | void licensesChanged(); 27 | 28 | private: 29 | QString m_licenseDir; 30 | void setLicenseDir(const QString &path); 31 | 32 | struct License 33 | { 34 | QString name; 35 | QString file; 36 | }; 37 | 38 | QStringList m_names; 39 | QString m_currentLicense; 40 | QList m_licenses; 41 | int m_index = -1; 42 | }; 43 | -------------------------------------------------------------------------------- /src/scene/tiledata.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "annotations/annotationnode.h" 4 | #include "line/linenode.h" 5 | #include "polygon/polygonnode.h" 6 | 7 | struct GeometryLayer 8 | { 9 | struct LineGroup 10 | { 11 | struct Style 12 | { 13 | bool operator==(const Style &other) const 14 | { 15 | return width == other.width; 16 | } 17 | 18 | enum class Width { 19 | Thin, 20 | Medium, 21 | Thick, 22 | }; 23 | 24 | Width width = Width::Medium; 25 | }; 26 | 27 | QList vertices; 28 | Style style = { Style::Width::Medium }; 29 | }; 30 | 31 | QList polygonVertices; 32 | QList lineGroups; 33 | }; 34 | 35 | struct TileData 36 | { 37 | // Per layer (chart) geometric objects 38 | QList geometryLayers; 39 | 40 | // Symbolism and text for all layers 41 | QList symbolVertices; 42 | QList textVertices; 43 | }; 44 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vcpkg"] 2 | path = vcpkg 3 | url = https://github.com/microsoft/vcpkg.git 4 | [submodule "external/oesenc"] 5 | path = external/oesenc 6 | url = https://github.com/hornang/oesenc.git 7 | [submodule "msdfgen"] 8 | path = external/msdfgen 9 | url = https://github.com/hornang/msdfgen.git 10 | [submodule "cutlines"] 11 | path = external/cutlines 12 | url = https://github.com/hornang/cutlines.git 13 | [submodule "MercatorTile"] 14 | path = external/MercatorTile 15 | url = https://github.com/hornang/MercatorTile 16 | [submodule "external/xdg-file-dialog"] 17 | path = external/xdg-file-dialog 18 | url = https://github.com/hornang/xdg-file-dialog.git 19 | [submodule "external/msdf-atlas-gen"] 20 | path = external/msdf-atlas-gen 21 | url = https://github.com/Chlumsky/msdf-atlas-gen.git 22 | [submodule "external/msdf-atlas-read"] 23 | path = external/msdf-atlas-read 24 | url = https://github.com/hornang/msdf-atlas-read.git 25 | [submodule "external/slippy-map-tiles-rs/slippy-map-tiles-rs"] 26 | path = external/slippy-map-tiles-rs/slippy-map-tiles-rs 27 | url = https://github.com/amandasaurus/slippy-map-tiles-rs.git 28 | -------------------------------------------------------------------------------- /src/scene/line/lineshader.cpp: -------------------------------------------------------------------------------- 1 | #include "lineshader.h" 2 | #include "linematerial.h" 3 | #include "linenode.h" 4 | 5 | bool LineShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) 6 | { 7 | bool changed = false; 8 | QByteArray *uniformBuffer = state.uniformData(); 9 | Q_ASSERT(uniformBuffer->size() == 136); 10 | 11 | if (state.isMatrixDirty()) { 12 | memcpy(uniformBuffer->data(), state.modelViewMatrix().constData(), 64); 13 | memcpy(uniformBuffer->data() + 64, state.projectionMatrix().constData(), 64); 14 | changed = true; 15 | } 16 | 17 | if (state.isOpacityDirty()) { 18 | const float opacity = state.opacity(); 19 | memcpy(uniformBuffer->data() + 128, &opacity, 4); 20 | changed = true; 21 | } 22 | 23 | LineMaterial *material = static_cast(newMaterial); 24 | if (oldMaterial != newMaterial || material->uniforms.dirty) { 25 | memcpy(uniformBuffer->data() + 132, &material->uniforms.width, 4); 26 | material->uniforms.dirty = false; 27 | changed = true; 28 | } 29 | 30 | return changed; 31 | } 32 | -------------------------------------------------------------------------------- /src/scene/annotations/annotation.frag: -------------------------------------------------------------------------------- 1 | #version 440 2 | 3 | layout(location = 0) in vec2 vTexCoord; 4 | layout(location = 1) in vec3 color; 5 | layout(location = 2) in float minZoom; 6 | 7 | layout(std140, binding = 0) uniform buf { 8 | mat4 modelView; 9 | mat4 projection; 10 | float qt_Opacity; 11 | float zoom; 12 | } ubuf; 13 | 14 | layout(binding = 1) uniform sampler2D ourTexture; 15 | layout(location = 0) out vec4 fragColor; 16 | 17 | const float smoothing = 2.0 / 16.0; 18 | const float borderSmoothing = 3.0 / 16.0; 19 | const float outlineWidth = 4.0 / 16.0; 20 | const float edgeCenter = 0.5; 21 | const float outerEdgeCenter = edgeCenter - outlineWidth; 22 | const vec4 u_outlineColor = vec4(1, 1, 1, 1); 23 | 24 | void main() 25 | { 26 | float dist = texture(ourTexture, vTexCoord).r; 27 | float alpha = smoothstep(outerEdgeCenter - smoothing, outerEdgeCenter + smoothing, dist) * ubuf.qt_Opacity; 28 | float zoomRevealAlpha = smoothstep(0, 1, ((ubuf.zoom - minZoom) + 1)); 29 | alpha *= zoomRevealAlpha; 30 | float border = smoothstep(edgeCenter - borderSmoothing, edgeCenter + borderSmoothing, dist); 31 | fragColor = vec4(mix(u_outlineColor.rgb, color.rgb, border) * alpha, alpha); 32 | } 33 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/mercator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "tilefactory_export.h" 4 | 5 | class TILEFACTORY_EXPORT Mercator 6 | { 7 | public: 8 | static double mercatorWidth(double leftLongitude, 9 | double rightLongitude, 10 | double pixelsPerLongitude); 11 | 12 | static double mercatorNormalizedHeight(double topLat, double bottomLat); 13 | 14 | static double mercatorHeightInverse(double topLatitude, 15 | double height, 16 | double pixelsPerLongitude); 17 | 18 | static double mercatorNormalizedWidth(double leftLon, double rightLon); 19 | 20 | static double mercatorWidthInverse(double leftLongitude, 21 | double pixels, 22 | double pixelsPerLongitude); 23 | 24 | static double mercatorWidthInverse(double pixels, double pixelsPerLon); 25 | 26 | static double mercatorHeight(double topLatitude, 27 | double bottomLatitude, 28 | double pixelsPerLongitude); 29 | static double toLatitude(double y); 30 | static double toLongitude(double x); 31 | }; 32 | -------------------------------------------------------------------------------- /src/scene/tilefactorywrapper.cpp: -------------------------------------------------------------------------------- 1 | #include "scene/tilefactorywrapper.h" 2 | 3 | std::vector> TileFactoryWrapper::create(TileRecipe recipe) 4 | { 5 | return m_tileDataCallback(recipe.rect, recipe.pixelsPerLongitude); 6 | } 7 | 8 | void TileFactoryWrapper::setTileDataCallback(TileDataCallback tileDataCallback) 9 | { 10 | m_tileDataCallback = tileDataCallback; 11 | } 12 | 13 | std::vector TileFactoryWrapper::chartInfos(TileRecipe recipe) 14 | { 15 | assert(m_chartInfoCallback); 16 | return m_chartInfoCallback(recipe.rect, recipe.pixelsPerLongitude); 17 | } 18 | 19 | void TileFactoryWrapper::setChartInfoCallback(ChartInfoCallback callback) 20 | { 21 | m_chartInfoCallback = callback; 22 | } 23 | 24 | void TileFactoryWrapper::triggerTileDataChanged(const std::vector &_tileIds) 25 | { 26 | QStringList tileIds; 27 | 28 | for (const std::string &tileId : _tileIds) { 29 | tileIds.push_back(QString::fromStdString(tileId)); 30 | } 31 | emit tileDataChanged(tileIds); 32 | } 33 | 34 | void TileFactoryWrapper::setTileSettingsCb(TileSettingsCallback callback) 35 | { 36 | m_tileSettingsCallback = callback; 37 | } 38 | 39 | void TileFactoryWrapper::setTileSettings(const std::string &tileId, TileFactory::TileSettings tileSettings) 40 | { 41 | assert(m_tileSettingsCallback); 42 | 43 | m_tileSettingsCallback(tileId, tileSettings); 44 | } 45 | -------------------------------------------------------------------------------- /src/scene/annotations/symbolimage.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "chartdata.capnp.h" 7 | #include "scene/annotations/types.h" 8 | 9 | class SymbolImage 10 | { 11 | public: 12 | SymbolImage(const QString &baseDir); 13 | SymbolImage(const SymbolImage &other) = delete; 14 | void load(); 15 | const QImage image() const { return m_image; } 16 | std::optional underwaterRock(const ChartData::UnderwaterRock::Reader &rock) const; 17 | std::optional lateralBuoy(const ChartData::BuoyLateral::Reader &buoy) const; 18 | std::optional beacon(const ChartData::Beacon::Reader &beacon) const; 19 | std::optional testSymbol() const; 20 | 21 | private: 22 | enum class Laterality { 23 | Port, 24 | Starboard 25 | }; 26 | 27 | static QString underwaterRockHash(ChartData::WaterLevelEffect waterLevelEffect); 28 | static QString buoyHash(ChartData::BuoyShape buoyShape, Laterality laterality); 29 | static QString beaconHash(ChartData::BeaconShape shape); 30 | static QRectF toNormalized(const QRect &rect, const QSize &image); 31 | QRect render(QImage &image, 32 | const QRect &lastRect, 33 | int &rowHeight, 34 | const QString &filename); 35 | 36 | QHash m_symbols; 37 | QImage m_image; 38 | QString m_baseDir; 39 | }; 40 | -------------------------------------------------------------------------------- /src/scene/materialcreator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class LineMaterial; 7 | class PolygonMaterial; 8 | class AnnotationMaterial; 9 | class QSGTexture; 10 | 11 | #include "tiledata.h" 12 | 13 | class MaterialCreator 14 | { 15 | using LineStyle = GeometryLayer::LineGroup::Style; 16 | 17 | public: 18 | MaterialCreator() = delete; 19 | MaterialCreator(QSGTexture *symbolTexture, QSGTexture *fontTexture); 20 | void setZoom(float zoom); 21 | PolygonMaterial *opaquePolygonMaterial(); 22 | PolygonMaterial *blendedPolygonMaterial(); 23 | AnnotationMaterial *symbolMaterial(); 24 | AnnotationMaterial *textMaterial(); 25 | LineMaterial *lineMaterial(const LineStyle &style); 26 | 27 | private: 28 | struct LineStyleHash 29 | { 30 | auto operator()(const LineStyle &p) const -> size_t 31 | { 32 | return std::hash {}(size_t(p.width)); 33 | } 34 | }; 35 | 36 | float m_scale = 0; 37 | QSGTexture *m_symbolTexture = nullptr; 38 | QSGTexture *m_fontTexture = nullptr; 39 | std::unique_ptr m_opaquePolygonMaterial; 40 | std::unique_ptr m_blendedPolygonMaterial; 41 | std::unique_ptr m_symbolMaterial; 42 | std::unique_ptr m_textMaterial; 43 | std::unordered_map, LineStyleHash> m_lineMaterials; 44 | }; 45 | -------------------------------------------------------------------------------- /cmake/GetVersionFromGit.cmake: -------------------------------------------------------------------------------- 1 | find_package(Git REQUIRED) 2 | 3 | function(get_version_from_git) 4 | set(oneValueArgs SEMANTIC DESCRIBE) 5 | cmake_parse_arguments(GET_VERSION "" "${oneValueArgs}" "" ${ARGN}) 6 | 7 | execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --dirty 8 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 9 | OUTPUT_VARIABLE GIT_DESCRIBE_VERSION 10 | RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE 11 | OUTPUT_STRIP_TRAILING_WHITESPACE) 12 | 13 | if(NOT GIT_DESCRIBE_ERROR_CODE) 14 | string(REGEX MATCH "^v(([0-9]+\.[0-9]+\.[0-9]+).*$)" MATCH_OUTPUT ${GIT_DESCRIBE_VERSION}) 15 | if (NOT ${MATCH_OUTPUT} STREQUAL "") 16 | set(DESCRIBE ${CMAKE_MATCH_1}) 17 | set(SEMANTIC ${CMAKE_MATCH_2}) 18 | else() 19 | message(WARNING "Failed to parse tag " ${GIT_DESCRIBE_VERSION}) 20 | endif() 21 | else() 22 | message(WARNING "Failed to run git command " ${GIT_DESCRIBE_VERSION} "Error code " ${GIT_DESCRIBE_ERROR_CODE}) 23 | endif() 24 | 25 | if (NOT DEFINED SEMANTIC OR NOT DEFINED DESCRIBE) 26 | message(WARNING "Failed to determine VERSION_STRING from Git tags. Using default version 0.0.0") 27 | set(SEMANTIC "0.0.0") 28 | set(DESCRIBE "0.0.0-unknown") 29 | endif() 30 | 31 | set(${GET_VERSION_SEMANTIC} ${SEMANTIC} PARENT_SCOPE) 32 | set(${GET_VERSION_DESCRIBE} ${DESCRIBE} PARENT_SCOPE) 33 | endfunction() 34 | 35 | -------------------------------------------------------------------------------- /src/tileinfobackend.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "scene/tilefactorywrapper.h" 6 | #include "tilefactory/tilefactory.h" 7 | 8 | class TileInfoBackend : public QObject 9 | { 10 | Q_OBJECT 11 | Q_PROPERTY(TileFactoryWrapper *tileFactory READ tileFactory WRITE setTileFactory NOTIFY tileFactoryChanged) 12 | Q_PROPERTY(QVariantMap tileRef READ tileRef WRITE setTileRef NOTIFY tileRefChanged) 13 | Q_PROPERTY(QString tileId READ tileId NOTIFY tileIdChanged) 14 | Q_PROPERTY(QVariantList chartInfos READ chartInfos NOTIFY chartInfosChanged) 15 | 16 | public: 17 | TileInfoBackend(); 18 | TileFactoryWrapper *tileFactory() const; 19 | void setTileFactory(TileFactoryWrapper *newTileFactory); 20 | const QString &tileId() const; 21 | void setTileId(const QString &newTileId); 22 | const QVariantMap &tileRef() const; 23 | void setTileRef(const QVariantMap &newTileRef); 24 | QVariantList chartInfos() const; 25 | 26 | public slots: 27 | void tileDataChanged(const QStringList &tileIds); 28 | void setChartEnabled(const QString &chart, bool enabled); 29 | 30 | signals: 31 | void tileFactoryChanged(); 32 | void tileIdChanged(); 33 | void tileRefChanged(); 34 | void chartInfosChanged(); 35 | 36 | private: 37 | void update(); 38 | TileFactoryWrapper *m_tileFactory = nullptr; 39 | QVariantMap m_tileRef; 40 | std::vector m_chartInfos; 41 | QString m_tileId; 42 | }; 43 | -------------------------------------------------------------------------------- /src/tilefactory/triangulator.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tilefactory/triangulator.h" 4 | 5 | Triangulator::Point Triangulator::getPoint(const std::vector> &polygon, int index) 6 | { 7 | for (const std::vector &points : polygon) { 8 | if (index >= points.size()) { 9 | index -= points.size(); 10 | continue; 11 | } 12 | return { points[index][0], points[index][1] }; 13 | } 14 | 15 | return {}; 16 | } 17 | 18 | std::vector Triangulator::calc(const std::vector> &polygon) 19 | { 20 | // From https://github.com/mapbox/earcut.hpp 21 | 22 | // The number type to use for tessellation 23 | using Coord = double; 24 | 25 | // The index type. Defaults to uint32_t, but you can also pass uint16_t if you know that your 26 | // data won't have more than 65536 vertices. 27 | using N = uint32_t; 28 | 29 | // Run tessellation 30 | // Returns array of indices that refer to the vertices of the input polygon. 31 | // e.g: the index 6 would refer to {25, 75} in this example. 32 | // Three subsequent indices form a triangle. Output triangles are clockwise. 33 | std::vector indices = mapbox::earcut(polygon); 34 | 35 | std::vector result; 36 | result.resize(indices.size()); 37 | 38 | int counter = 0; 39 | for (const auto &index : indices) { 40 | result[counter] = getPoint(polygon, index); 41 | counter++; 42 | } 43 | 44 | return result; 45 | } 46 | -------------------------------------------------------------------------------- /src/scene/include/scene/annotations/fontimage.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include "types.h" 17 | 18 | class FontWorker; 19 | 20 | class FontImage : public QObject 21 | { 22 | Q_OBJECT 23 | 24 | public: 25 | FontImage(); 26 | ~FontImage(); 27 | FontImage(const FontImage &other) = delete; 28 | 29 | struct Atlas 30 | { 31 | QImage image; 32 | std::unordered_map geometries; 33 | }; 34 | 35 | QImage image() const 36 | { 37 | if (!m_atlas.has_value()) { 38 | return {}; 39 | } 40 | return m_atlas->image; 41 | } 42 | QList glyphs(const QString &text, float pixelSize, FontType type = FontType::Soundings) const; 43 | QSize atlasSize() const 44 | { 45 | if (!m_atlas.has_value()) { 46 | return {}; 47 | } 48 | return m_atlas->image.size(); 49 | } 50 | QRectF boundingBox(const QString &text, float pixelSize, FontType type = FontType::Soundings) const; 51 | static QString locateFontFile(FontType type); 52 | 53 | signals: 54 | void atlasChanged(); 55 | 56 | public slots: 57 | void setAtlas(const FontImage::Atlas &cache); 58 | 59 | private: 60 | FontWorker *m_worker = nullptr; 61 | std::optional m_atlas; 62 | }; 63 | -------------------------------------------------------------------------------- /src/scene/include/scene/tilefactorywrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "tilefactory/georect.h" 12 | #include "tilefactory/tilefactory.h" 13 | 14 | #include "scene_export.h" 15 | 16 | class Chart; 17 | 18 | class SCENE_EXPORT TileFactoryWrapper : public QObject 19 | { 20 | Q_OBJECT 21 | 22 | public: 23 | struct TileRecipe 24 | { 25 | GeoRect rect; 26 | double pixelsPerLongitude; 27 | }; 28 | 29 | using TileDataCallback = std::function>(GeoRect, double)>; 30 | using ChartInfoCallback = std::function(GeoRect, double)>; 31 | using TileSettingsCallback = std::function; 32 | 33 | void setTileSettings(const std::string &tileId, TileFactory::TileSettings tileSettings); 34 | void setTileDataCallback(TileDataCallback callback); 35 | void setChartInfoCallback(ChartInfoCallback callback); 36 | void setTileSettingsCb(TileSettingsCallback callback); 37 | std::vector> create(TileRecipe recipe); 38 | std::vector chartInfos(TileRecipe recipe); 39 | 40 | void triggerTileDataChanged(const std::vector &tileIds); 41 | 42 | signals: 43 | void tileDataChanged(QStringList tileIds); 44 | 45 | private: 46 | TileDataCallback m_tileDataCallback; 47 | ChartInfoCallback m_chartInfoCallback; 48 | TileSettingsCallback m_tileSettingsCallback; 49 | }; 50 | -------------------------------------------------------------------------------- /qml/ChartSelector.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Layouts 4 | import QtQuick.Controls.Universal 2.12 5 | 6 | ColumnLayout { 7 | id: root 8 | 9 | property alias model: listView.model 10 | 11 | CheckBox { 12 | text: "All" 13 | Layout.fillWidth: true 14 | nextCheckState: function() { return checkState } 15 | checked: root.model.allEnabled 16 | onClicked: root.model.setAllChartsEnabled(!checked) 17 | } 18 | 19 | ScrollView { 20 | clip: true 21 | Layout.fillHeight: true 22 | Layout.fillWidth: true 23 | 24 | ScrollBar.horizontal.policy: ScrollBar.AlwaysOff 25 | 26 | ListView { 27 | id: listView 28 | delegate: RowLayout { 29 | width: listView.width 30 | CheckBox { 31 | nextCheckState: function() { return checkState } 32 | checked: model.enabled && model.ok 33 | enabled: model.ok 34 | onClicked: root.model.setChartEnabled(index, !checked) 35 | font.family: "fixed" 36 | 37 | property string symbol: { 38 | if (!model.ok) { 39 | return "❌"; 40 | } else { 41 | return "" 42 | } 43 | } 44 | text: symbol + model.name 45 | } 46 | Label { 47 | visible: model.ok 48 | text: "1:" + model.scale 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/oesenctilesource.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "itilesource.h" 9 | #include "oesenc/chartfile.h" 10 | #include "tilefactory/chart.h" 11 | 12 | #include "tilefactory_export.h" 13 | 14 | class Catalog; 15 | 16 | class TILEFACTORY_EXPORT OesencTileSource : public ITileSource 17 | { 18 | public: 19 | OesencTileSource(Catalog *catalogue, 20 | std::string_view name, 21 | std::string_view baseTileDir); 22 | 23 | bool isValid() const; 24 | ~OesencTileSource(); 25 | GeoRect extent() const override; 26 | int scale() const override { return m_scale; } 27 | std::shared_ptr create(const GeoRect &boundingBox, int pixelsPerLongitude) override; 28 | 29 | private: 30 | bool convertChartToInternalFormat(float lineEpsilon, int pixelsPerLon); 31 | void readOesencMetaData(const oesenc::ChartFile *chart); 32 | static GeoRect fromOesencRect(const oesenc::Rect &src); 33 | 34 | /*! 35 | Generate tile data for the given boundingBox 36 | 37 | The actual ChartFile will not be opened until the first call to this function 38 | */ 39 | std::shared_ptr generateTile(const GeoRect &boundingBox, int pixelsPerLongitude); 40 | std::unordered_map> m_tileMutexes; 41 | std::mutex m_tileMutexesMutex; 42 | std::string m_tileDir; 43 | std::string m_name; 44 | bool m_valid = false; 45 | GeoRect m_extent; 46 | std::mutex m_internalChartMutex; 47 | Catalog *m_catalogue = nullptr; 48 | int m_scale = 0; 49 | }; 50 | -------------------------------------------------------------------------------- /AppxManifest.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | Nautograf 12 | @MSIX_PUBLISHER_NAME@ 13 | logo_44.png 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/scene/polygon/polygonnode.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "polygonmaterial.h" 4 | #include "polygonnode.h" 5 | 6 | static const QSGGeometry::Attribute attributes[2] = { 7 | QSGGeometry::Attribute::createWithAttributeType(0, 3, 8 | QSGGeometry::FloatType, 9 | QSGGeometry::PositionAttribute), 10 | QSGGeometry::Attribute::createWithAttributeType(1, 4, 11 | QSGGeometry::UnsignedByteType, 12 | QSGGeometry::ColorAttribute), 13 | }; 14 | 15 | static const QSGGeometry::AttributeSet attributeSet = { 2, 16, attributes }; 16 | static_assert(sizeof(PolygonNode::Vertex) == 16, "Incorrect sizeof(PolygonNode::Vertex)"); 17 | 18 | PolygonNode::PolygonNode(QSGMaterial *material, 19 | const QList &vertices) 20 | { 21 | setMaterial(material); 22 | QSGGeometry *geometry = new QSGGeometry(attributeSet, vertices.length()); 23 | memcpy(geometry->vertexData(), vertices.constData(), vertices.length() * sizeof(Vertex)); 24 | geometry->setDrawingMode(QSGGeometry::DrawTriangles); 25 | geometry->setVertexDataPattern(QSGGeometry::StaticPattern); 26 | setGeometry(geometry); 27 | 28 | setFlag(OwnsGeometry, true); 29 | setFlag(OwnedByParent, true); 30 | } 31 | 32 | void PolygonNode::updateVertices(const QList &vertices) 33 | { 34 | geometry()->allocate(vertices.length()); 35 | memcpy(geometry()->vertexData(), vertices.constData(), vertices.length() * sizeof(Vertex)); 36 | markDirty(DirtyGeometry); 37 | } 38 | -------------------------------------------------------------------------------- /src/scene/line/linenode.cpp: -------------------------------------------------------------------------------- 1 | #include "linenode.h" 2 | 3 | static const QSGGeometry::Attribute attributes[] = { 4 | QSGGeometry::Attribute::createWithAttributeType(0, 3, QSGGeometry::FloatType, QSGGeometry::PositionAttribute), 5 | QSGGeometry::Attribute::createWithAttributeType(1, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), 6 | QSGGeometry::Attribute::createWithAttributeType(2, 4, QSGGeometry::UnsignedByteType, QSGGeometry::ColorAttribute), 7 | }; 8 | static const QSGGeometry::AttributeSet attributeSet = { static_cast(std::size(attributes)), 9 | sizeof(LineNode::Vertex), 10 | attributes }; 11 | 12 | static_assert(sizeof(LineNode::Vertex) == 24, "Incorrect sizeof(LineNode::Vertex)"); 13 | 14 | LineNode::LineNode(QSGMaterial *material, 15 | const QList &vertices) 16 | { 17 | setMaterial(material); 18 | 19 | QSGGeometry *geometry = new QSGGeometry(attributeSet, vertices.length()); 20 | geometry->setDrawingMode(QSGGeometry::DrawTriangles); 21 | memcpy(geometry->vertexData(), 22 | vertices.constData(), 23 | vertices.length() * sizeof(LineNode::Vertex)); 24 | 25 | setGeometry(geometry); 26 | setFlag(OwnsGeometry, true); 27 | setFlag(OwnedByParent, true); 28 | } 29 | 30 | void LineNode::updateVertices(const QList &vertices) 31 | { 32 | geometry()->allocate(vertices.length()); 33 | memcpy(geometry()->vertexData(), 34 | vertices.constData(), 35 | vertices.length() * sizeof(LineNode::Vertex)); 36 | markDirty(DirtyGeometry | DirtyMaterial); 37 | } 38 | -------------------------------------------------------------------------------- /qml/FolderDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qt.labs.platform as QtLabs 3 | import org.seatronomy.nautograf 4 | 5 | Item { 6 | id: root 7 | 8 | property string title 9 | property bool useXdgFileDialog: false 10 | property variant parentWindow: undefined 11 | property string folder 12 | 13 | signal accepted 14 | signal error(var message) 15 | 16 | visible: false 17 | 18 | Connections { 19 | target: loader.item 20 | ignoreUnknownSignals: true 21 | 22 | function onAccepted() { 23 | root.folder = loader.item.folder; 24 | root.accepted(); 25 | } 26 | 27 | function onVisibleChanged() { 28 | root.visible = loader.item.visible; 29 | } 30 | 31 | function onErrorOccurred() { 32 | root.error("Please ensure that xdg-desktop-portal-gnome or other portal implementation is installed."); 33 | } 34 | } 35 | 36 | Loader { 37 | id: loader 38 | 39 | onLoaded: { 40 | if (root.useXdgFileDialog) { 41 | item.visible = Qt.binding(function() { return root.visible }) 42 | item.folder = root.folder; 43 | item.title = root.title; 44 | item.parentWindow = parentWindow; 45 | } 46 | } 47 | 48 | source: useXdgFileDialog ? "XdgFileDialogWrapper.qml" : "" 49 | sourceComponent: !useXdgFileDialog ? labsFileDialog : undefined 50 | } 51 | 52 | Component { 53 | id: labsFileDialog 54 | 55 | QtLabs.FolderDialog { 56 | currentFolder: root.folder 57 | title: root.title 58 | visible: root.visible 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tilefactory/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(CapnProto CONFIG REQUIRED) 2 | install_license(FILE ${CapnProto_DIR}/copyright) 3 | 4 | find_package(PkgConfig REQUIRED) 5 | pkg_check_modules(Clipper2 REQUIRED IMPORTED_TARGET Clipper2) 6 | install_license(FILE ${Clipper2_INCLUDE_DIRS}/../share/clipper2/copyright) 7 | 8 | find_path(EARCUT_HPP_INCLUDE_DIRS "mapbox/earcut.hpp") 9 | 10 | capnp_generate_cpp(CAPNP_SRCS CAPNP_HDRS chartdata.capnp) 11 | 12 | add_library(tilefactory 13 | include/tilefactory/catalog.h 14 | include/tilefactory/itilesource.h 15 | include/tilefactory/mercator.h 16 | include/tilefactory/georect.h 17 | include/tilefactory/oesenctilesource.h 18 | include/tilefactory/tilefactory.h 19 | include/tilefactory/chart.h 20 | include/tilefactory/chartclipper.h 21 | include/tilefactory/pos.h 22 | include/tilefactory/triangulator.h 23 | 24 | catalog.cpp 25 | chart.cpp 26 | coverageratio.h 27 | coverageratio.cpp 28 | filehelper.cpp 29 | filehelper.h 30 | georect.cpp 31 | chartclipper.cpp 32 | mercator.cpp 33 | oesenctilesource.cpp 34 | tilefactory.cpp 35 | triangulator.cpp 36 | pos.cpp 37 | ${CAPNP_SRCS} 38 | ${CAPNP_HDRS} 39 | ) 40 | 41 | include(GenerateExportHeader) 42 | generate_export_header(tilefactory) 43 | 44 | target_include_directories(tilefactory 45 | PUBLIC 46 | ${CMAKE_CURRENT_BINARY_DIR} 47 | ${CMAKE_CURRENT_SOURCE_DIR}/include 48 | 49 | PRIVATE 50 | ${EARCUT_HPP_INCLUDE_DIRS} 51 | ) 52 | 53 | add_subdirectory(rust) 54 | 55 | target_link_libraries(tilefactory 56 | CapnProto::capnp 57 | PkgConfig::Clipper2 58 | oesenc 59 | cutlines 60 | mercatortile 61 | tilefactory-rust-bridge 62 | ) 63 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/georect.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "pos.h" 6 | 7 | #include "tilefactory_export.h" 8 | 9 | class TILEFACTORY_EXPORT GeoRect 10 | { 11 | public: 12 | GeoRect() = default; 13 | GeoRect(double top, double bottom, double left, double right); 14 | GeoRect(const GeoRect &other) = default; 15 | GeoRect &operator=(const GeoRect &other) = default; 16 | bool operator==(const GeoRect &other); 17 | bool contains(double lat, double lon) const; 18 | Pos topLeft() const { return Pos(m_top, m_left); } 19 | Pos topRight() const { return Pos(m_top, m_right); } 20 | Pos bottomLeft() const { return Pos(m_bottom, m_left); } 21 | Pos bottomRight() const { return Pos(m_bottom, m_right); } 22 | double lonSpan() const { return m_right - m_left; } 23 | double latSpan() const { return m_top - m_bottom; } 24 | double top() const { return m_top; } 25 | double bottom() const { return m_bottom; } 26 | double right() const { return m_right; } 27 | double left() const { return m_left; } 28 | double width() const { return m_right - m_left; } 29 | double height() const { return m_top - m_bottom; } 30 | bool isNull() const { return m_top == 0 && m_bottom == 0 && m_left == 0 && m_right == 0; } 31 | /*! 32 | * Returns true of other rectangle is totally inside this Rectangle 33 | */ 34 | bool encloses(const GeoRect &other) const; 35 | bool intersects(const GeoRect &rect) const; 36 | GeoRect intersection(const GeoRect &rect) const; 37 | 38 | private: 39 | double m_top = 0; 40 | double m_bottom = 0; 41 | double m_left = 0; 42 | double m_right = 0; 43 | }; 44 | 45 | bool operator==(const GeoRect &a, const GeoRect &b); 46 | std::ostream &operator<<(std::ostream &os, const GeoRect &rect); 47 | -------------------------------------------------------------------------------- /src/maptilemodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "tilefactory/georect.h" 4 | #include "tilefactory/pos.h" 5 | #include "tilefactory/tilefactory.h" 6 | 7 | #include 8 | #include 9 | 10 | class MapTileModel : public QAbstractListModel 11 | { 12 | Q_OBJECT 13 | Q_PROPERTY(QSizeF viewPort READ viewPort WRITE setViewPort NOTIFY viewPortChanged) 14 | Q_PROPERTY(int count READ count NOTIFY countChanged) 15 | 16 | public: 17 | enum Role { 18 | TileRefRole, 19 | TileIdRole, 20 | TopLatitude, 21 | LeftLongitude, 22 | BottomLatitude, 23 | RightLongitude, 24 | }; 25 | 26 | MapTileModel(const std::shared_ptr &tileFactory); 27 | QSizeF viewPort() const; 28 | int rowCount(const QModelIndex &parent = QModelIndex()) const; 29 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; 30 | QHash roleNames() const; 31 | void chartsChanged(const std::vector &rects); 32 | int count() const; 33 | void scheduleUpdate(); 34 | 35 | public slots: 36 | void setViewPort(const QSizeF &viewPort); 37 | void setPanZoom(double lon, double lat, double pixelsPerLon); 38 | QVariantMap tileRefAtPos(float lat, float lon); 39 | void update(); 40 | 41 | signals: 42 | void viewPortChanged(QSizeF viewPort); 43 | void countChanged(int count); 44 | 45 | private: 46 | static QVariantMap createTileRef(const QString &tileId, 47 | const GeoRect &boundingBox, 48 | int maxPixelsPerLon); 49 | std::shared_ptr m_tileFactory; 50 | Pos m_center; 51 | double m_pixelsPerLon = 50000; 52 | QHash m_roleNames; 53 | QVector m_tiles; 54 | QSizeF m_viewPort; 55 | }; 56 | -------------------------------------------------------------------------------- /src/tilefactory/rust/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CARGO_MANIFEST ${CMAKE_CURRENT_SOURCE_DIR}/Cargo.toml) 2 | set(CARGO_TARGET_DIR ${CMAKE_CURRENT_BINARY_DIR}/target) 3 | 4 | set(WRAPPER_LIB_SOURCE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/lib.rs) 5 | set(BRIDGE_CPP ${CARGO_TARGET_DIR}/cxxbridge/tilefactory_rust/lib.rs.cc) 6 | 7 | if(CMAKE_BUILD_TYPE STREQUAL "Release") 8 | set(CARGO_TARGET_SUBDIR "release") 9 | set(CARGO_RELEASE_ARG "--release") 10 | else() 11 | set(CARGO_TARGET_SUBDIR "debug") 12 | if(MSVC) 13 | set(CARGO_BUILD_ENV_PRELUDE set CFLAGS=-MDd && set CXXFLAGS=-MDd &&) 14 | endif() 15 | endif() 16 | 17 | set(WRAPPER_LIB ${CARGO_TARGET_DIR}/${CARGO_TARGET_SUBDIR}/${CMAKE_STATIC_LIBRARY_PREFIX}tilefactory_rust${CMAKE_STATIC_LIBRARY_SUFFIX}) 18 | 19 | set(CARGO_BUILD_COMMAND cargo build ${CARGO_RELEASE_ARG} --target-dir ${CARGO_TARGET_DIR} --manifest-path ${CARGO_MANIFEST}) 20 | set(CARGO_BUILD_COMMAND_WITH_ENV ${CARGO_BUILD_ENV_PRELUDE} ${CARGO_BUILD_COMMAND}) 21 | 22 | add_custom_command( 23 | OUTPUT ${BRIDGE_CPP} ${WRAPPER_LIB} 24 | COMMAND ${CARGO_BUILD_COMMAND_WITH_ENV} 25 | DEPENDS ${WRAPPER_LIB_SOURCE_FILE} 26 | USES_TERMINAL 27 | COMMENT "Running cargo..." 28 | ) 29 | 30 | add_library(tilefactory-rust-bridge STATIC 31 | ${BRIDGE_CPP} 32 | ) 33 | 34 | target_include_directories(tilefactory-rust-bridge 35 | PUBLIC 36 | ${CARGO_TARGET_DIR}/cxxbridge 37 | ) 38 | 39 | set_target_properties( 40 | tilefactory-rust-bridge 41 | PROPERTIES ADDITIONAL_CLEAN_FILES ${CARGO_TARGET_DIR} 42 | ) 43 | 44 | target_link_libraries(tilefactory-rust-bridge ${WRAPPER_LIB}) 45 | 46 | if(WIN32) 47 | target_link_libraries(tilefactory-rust-bridge 48 | userenv ws2_32 bcrypt ntdll 49 | ) 50 | elseif(UNIX) 51 | target_link_libraries(tilefactory-rust-bridge 52 | pthread 53 | dl 54 | ) 55 | endif() 56 | -------------------------------------------------------------------------------- /src/mercatorwrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "tilefactory/mercator.h" 6 | 7 | class MercatorWrapper : public QObject 8 | { 9 | Q_OBJECT 10 | 11 | public slots: 12 | double mercatorWidth(double leftLon, double rightLon, double pixelsPerLon) 13 | { 14 | return Mercator::mercatorWidth(leftLon, rightLon, pixelsPerLon); 15 | } 16 | 17 | double mercatorHeight(double topLat, double bottomLat, double pixelsPerLon) 18 | { 19 | return Mercator::mercatorHeight(topLat, bottomLat, pixelsPerLon); 20 | } 21 | 22 | double mercatorWidthInverse(double leftLon, double pixels, double pixelsPerLon) 23 | { 24 | return Mercator::mercatorWidthInverse(leftLon, pixels, pixelsPerLon); 25 | } 26 | 27 | double mercatorHeightInverse(double topLat, double pixels, double pixelsPerLon) 28 | { 29 | return Mercator::mercatorHeightInverse(topLat, pixels, pixelsPerLon); 30 | } 31 | 32 | double pixelsPerLonInterval(double leftLon, double rightLon, double pixels) 33 | { 34 | double pixelsPerLongitudeRadians = pixels / Mercator::mercatorNormalizedWidth(leftLon, rightLon); 35 | return pixelsPerLongitudeRadians * M_PI / 180.; 36 | } 37 | 38 | double pixelsPerLatInterval(const double topLat, const double bottomLat, const double height) 39 | { 40 | double pixelsPerLongitudeRadians = height / Mercator::mercatorNormalizedHeight(topLat, bottomLat); 41 | return pixelsPerLongitudeRadians * M_PI / 180.0; 42 | } 43 | 44 | double offsetLat(double lat, double offset, double pixelsPerLon) 45 | { 46 | return Mercator::mercatorHeightInverse(lat, offset, pixelsPerLon); 47 | } 48 | 49 | double offsetLon(double lon, double offset, double pixelsPerLon) 50 | { 51 | return Mercator::mercatorWidthInverse(lon, offset, pixelsPerLon); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/scene/include/scene/annotations/types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | struct TextureSymbol 12 | { 13 | QRectF coords; 14 | QSize size; 15 | 16 | // This is the pixel position within the symbol that should be placed on 17 | // geographical position in the map. E.g for a stake the bottom of the 18 | // symbol is to be placed, not the center. 19 | QPointF center; 20 | 21 | // Rectangle within the symbol which should not be overlapped 22 | // For many symbols it is simply QRectF(0, 0, size.width(), size.height()) 23 | QRectF roi; 24 | QColor color; 25 | 26 | bool operator!=(const TextureSymbol &other) const 27 | { 28 | return (this->coords != other.coords || this->size != other.size); 29 | } 30 | 31 | bool operator==(const TextureSymbol &other) const 32 | { 33 | return (this->coords == other.coords && this->size == other.size); 34 | } 35 | }; 36 | 37 | enum class FontType { 38 | Unknown, 39 | Soundings, 40 | Normal, 41 | }; 42 | 43 | // This only applies to symbol collisions. Labels are always checked. 44 | enum class CollisionRule { 45 | NoCheck, 46 | Always, 47 | OnlyWithSameType 48 | }; 49 | 50 | struct Label 51 | { 52 | QString text; 53 | FontType font; 54 | QColor color; 55 | float pointSize; 56 | }; 57 | 58 | struct AnnotationLabel 59 | { 60 | Label label; 61 | QPointF pos; 62 | std::optional parentSymbolIndex; 63 | QPointF offset; 64 | QRectF boundingBox; 65 | std::optional minZoom; 66 | }; 67 | 68 | struct AnnotationSymbol 69 | { 70 | QPointF pos; 71 | std::optional symbol; 72 | std::optional minZoom; 73 | int priority = 0; 74 | CollisionRule collisionRule; 75 | }; 76 | -------------------------------------------------------------------------------- /src/scene/annotations/annotationshader.cpp: -------------------------------------------------------------------------------- 1 | #include "annotationshader.h" 2 | #include "annotationmaterial.h" 3 | #include "annotationnode.h" 4 | 5 | bool AnnotationShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) 6 | { 7 | bool changed = false; 8 | QByteArray *uniformBuffer = state.uniformData(); 9 | Q_ASSERT(uniformBuffer->size() == 136); 10 | 11 | if (state.isMatrixDirty()) { 12 | memcpy(uniformBuffer->data(), state.modelViewMatrix().constData(), 64); 13 | memcpy(uniformBuffer->data() + 64, state.projectionMatrix().constData(), 64); 14 | changed = true; 15 | } 16 | 17 | if (state.isOpacityDirty()) { 18 | const float opacity = state.opacity(); 19 | memcpy(uniformBuffer->data() + 128, &opacity, 4); 20 | changed = true; 21 | } 22 | 23 | auto *annotationMaterial = static_cast(newMaterial); 24 | if (oldMaterial != newMaterial || annotationMaterial->uniforms.dirty) { 25 | memcpy(uniformBuffer->data() + 132, &annotationMaterial->uniforms.zoom, 4); 26 | annotationMaterial->uniforms.dirty = false; 27 | changed = true; 28 | } 29 | 30 | return changed; 31 | } 32 | 33 | void AnnotationShader::updateSampledImage(QSGMaterialShader::RenderState &state, 34 | int binding, 35 | QSGTexture **texture, 36 | QSGMaterial *newMaterial, 37 | QSGMaterial *oldMaterial) 38 | { 39 | 40 | auto *material = static_cast(newMaterial); 41 | 42 | if (*texture != material->texture()) { 43 | if (material->texture()) { 44 | *texture = material->texture(); 45 | (*texture)->commitTextureOperations(state.rhi(), state.resourceUpdateBatch()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/tilefactory/georect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tilefactory/georect.h" 4 | 5 | GeoRect::GeoRect(double top, double bottom, double left, double right) 6 | : m_top(top) 7 | , m_bottom(bottom) 8 | , m_left(left) 9 | , m_right(right) 10 | { 11 | } 12 | 13 | bool GeoRect::operator==(const GeoRect &other) 14 | { 15 | return m_left == other.m_left && m_right == other.m_right && m_top == other.m_top && m_bottom == other.m_bottom; 16 | } 17 | 18 | bool GeoRect::contains(double lat, double lon) const 19 | { 20 | return lon >= m_left && lon <= m_right && lat < m_top && lat > m_bottom; 21 | } 22 | 23 | bool GeoRect::intersects(const GeoRect &other) const 24 | { 25 | if (m_left > other.m_right || m_right < other.m_left || m_bottom > other.m_top || m_top < other.m_bottom) { 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | 32 | GeoRect GeoRect::intersection(const GeoRect &other) const 33 | { 34 | if (!intersects(other)) { 35 | return GeoRect(); 36 | } 37 | 38 | GeoRect output = *this; 39 | 40 | output.m_top = std::min(output.m_top, other.m_top); 41 | output.m_bottom = std::max(output.m_bottom, other.m_bottom); 42 | output.m_left = std::max(output.m_left, other.m_left); 43 | output.m_right = std::min(output.m_right, other.m_right); 44 | 45 | return output; 46 | } 47 | 48 | bool GeoRect::encloses(const GeoRect &other) const 49 | { 50 | return (m_left <= other.m_left 51 | && m_right >= other.m_right 52 | && m_top >= other.m_top 53 | && m_bottom <= other.m_bottom); 54 | } 55 | 56 | std::ostream &operator<<(std::ostream &os, const GeoRect &rect) 57 | { 58 | os << "top: " << rect.top() << " bottom: " << rect.bottom() << " left: " << rect.left() << " right: " << rect.right(); 59 | return os; 60 | } 61 | 62 | bool operator==(const GeoRect &a, const GeoRect &b) 63 | { 64 | return a.left() == b.left() 65 | && a.right() == b.right() 66 | && a.top() == b.top() 67 | && a.bottom() == b.bottom(); 68 | } 69 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/chartclipper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "chartdata.capnp.h" 8 | #include "tilefactory/georect.h" 9 | #include "tilefactory/pos.h" 10 | 11 | #include "tilefactory_export.h" 12 | 13 | class TILEFACTORY_EXPORT ChartClipper 14 | { 15 | public: 16 | using Line = std::vector; 17 | 18 | struct Polygon 19 | { 20 | Line main; 21 | std::vector holes; 22 | }; 23 | 24 | struct Config 25 | { 26 | GeoRect box; 27 | GeoRect chartBoundingBox; 28 | int maxPixelsPerLongitude = 0; 29 | float latitudeMargin = 0; 30 | float longitudeMargin = 0; 31 | float latitudeResolution = 0; 32 | float longitudeResolution = 0; 33 | bool inflateAtChartEdges = false; 34 | }; 35 | 36 | static std::vector clipPolygon(const ChartData::Polygon::Reader &polygon, 37 | Config clipConfig); 38 | static Clipper2Lib::Path64 toClipperPath(const capnp::List::Reader &points, 39 | const GeoRect &roi, double xRes, double yRes); 40 | static Line toLine(const Clipper2Lib::Path64 &path, 41 | const GeoRect &roi, 42 | double xRes, 43 | double yRes); 44 | 45 | private: 46 | static int inRange(double value, double min, double max, double margin); 47 | static Line inflateAtChartEdges(const Line &area, Config clipConfig); 48 | static inline Clipper2Lib::Point64 toIntPoint(const Pos &pos, 49 | const GeoRect &roi, 50 | double xRes, 51 | double yRes); 52 | static inline Pos fromIntPoint(const Clipper2Lib::Point64 &intPoint, 53 | const GeoRect &clip, 54 | double xRes, double yRes); 55 | }; 56 | -------------------------------------------------------------------------------- /src/licenses.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "licenses.h" 5 | 6 | Licenses::Licenses() 7 | { 8 | const QString installPath = QCoreApplication::applicationDirPath() + "/../licenses"; 9 | const QString devPath = QStringLiteral("licenses"); 10 | 11 | if (QFile::exists(installPath)) { 12 | setLicenseDir(installPath); 13 | } else if (QFile::exists(devPath)) { 14 | setLicenseDir(devPath); 15 | } else { 16 | qWarning() << "Unable to find license dir"; 17 | } 18 | } 19 | 20 | void Licenses::setLicenseDir(const QString &path) 21 | { 22 | m_licenses.clear(); 23 | m_names.clear(); 24 | 25 | QStringList licenseFiles = QDir(path).entryList({ QStringLiteral("*.txt") }, 26 | QDir::Files | QDir::NoDotAndDotDot); 27 | 28 | for (const auto &licenseFile : licenseFiles) { 29 | License license { licenseFile.left(licenseFile.size() - 4), licenseFile }; 30 | m_licenses.append(license); 31 | m_names.append(license.name); 32 | } 33 | m_licenseDir = path; 34 | emit licensesChanged(); 35 | setIndex(0); 36 | } 37 | 38 | QString Licenses::currentLicense() const 39 | { 40 | return m_currentLicense; 41 | } 42 | 43 | void Licenses::load(int index) 44 | { 45 | if (index < 0 || index > m_licenses.size() - 1) { 46 | return; 47 | } 48 | 49 | QFile file(m_licenseDir + "/" + m_licenses.at(index).file); 50 | 51 | if (!file.open(QIODevice::ReadOnly)) { 52 | return; 53 | } 54 | 55 | QByteArray data = file.readAll(); 56 | file.close(); 57 | m_currentLicense = QString::fromUtf8(data); 58 | emit currentLicenseChanged(); 59 | } 60 | 61 | QStringList Licenses::names() const 62 | { 63 | return m_names; 64 | } 65 | 66 | int Licenses::index() const 67 | { 68 | return m_index; 69 | } 70 | 71 | void Licenses::setIndex(int newIndex) 72 | { 73 | if (m_index == newIndex) 74 | return; 75 | 76 | m_index = newIndex; 77 | emit indexChanged(); 78 | load(newIndex); 79 | } 80 | -------------------------------------------------------------------------------- /src/scene/annotations/annotationnode.cpp: -------------------------------------------------------------------------------- 1 | #include "annotationnode.h" 2 | 3 | static const QSGGeometry::Attribute attributes[] = { 4 | QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute), 5 | QSGGeometry::Attribute::createWithAttributeType(1, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), 6 | QSGGeometry::Attribute::createWithAttributeType(2, 2, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute), 7 | QSGGeometry::Attribute::createWithAttributeType(3, 4, QSGGeometry::UnsignedByteType, QSGGeometry::ColorAttribute), 8 | QSGGeometry::Attribute::createWithAttributeType(4, 1, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute) 9 | }; 10 | static const QSGGeometry::AttributeSet attributeSet = { static_cast(std::size(attributes)), 11 | sizeof(AnnotationNode::Vertex), 12 | attributes }; 13 | 14 | static_assert(sizeof(AnnotationNode::Vertex) == 32, "Incorrect sizeof(AnnotationNode::Vertex)"); 15 | 16 | AnnotationNode::AnnotationNode(const QString &id, 17 | QSGMaterial *material, 18 | const QList &vertices) 19 | : m_id(id) 20 | { 21 | setMaterial(material); 22 | 23 | QSGGeometry *geometry = new QSGGeometry(attributeSet, vertices.length()); 24 | geometry->setDrawingMode(QSGGeometry::DrawTriangles); 25 | memcpy(geometry->vertexData(), 26 | vertices.constData(), 27 | vertices.length() * sizeof(AnnotationNode::Vertex)); 28 | 29 | setGeometry(geometry); 30 | setFlag(OwnsGeometry, true); 31 | setFlag(OwnedByParent, true); 32 | } 33 | 34 | void AnnotationNode::updateVertices(const QList &vertices) 35 | { 36 | geometry()->allocate(vertices.length()); 37 | memcpy(geometry()->vertexData(), 38 | vertices.constData(), 39 | vertices.length() * sizeof(AnnotationNode::Vertex)); 40 | markDirty(DirtyGeometry | DirtyMaterial); 41 | } 42 | -------------------------------------------------------------------------------- /src/tilefactory/filehelper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "filehelper.h" 6 | 7 | std::string FileHelper::tileId(const GeoRect &boundingBox, int pixelsPerLongitude) 8 | { 9 | std::stringstream ss; 10 | ss << boundingBox.top(); 11 | ss << boundingBox.left(); 12 | ss << boundingBox.bottom(); 13 | ss << boundingBox.right(); 14 | ss << pixelsPerLongitude; 15 | unsigned int h1 = std::hash {}(ss.str()); 16 | 17 | static const std::string_view hex_chars = "0123456789abcdef"; 18 | 19 | std::mt19937 mt(h1); 20 | 21 | std::string uuid; 22 | unsigned int len = 16; 23 | uuid.reserve(len); 24 | 25 | while (uuid.size() < len) { 26 | auto n = mt(); 27 | for (auto i = std::mt19937::max(); i & 0x8 && uuid.size() < len; i >>= 4) { 28 | uuid += hex_chars[n & 0xf]; 29 | n >>= 4; 30 | } 31 | } 32 | 33 | return uuid; 34 | } 35 | 36 | std::string FileHelper::getTileDir(const std::string &tileDir, uint64_t typeId) 37 | { 38 | std::stringstream ss; 39 | ss << std::hex << typeId; 40 | std::string s = ss.str(); 41 | std::filesystem::path dir; 42 | dir.append(tileDir); 43 | dir.append(ss.str()); 44 | std::filesystem::create_directories(dir); 45 | return dir.string(); 46 | } 47 | 48 | std::string FileHelper::tileFileName(const std::string &tileDir, 49 | const std::string &name, 50 | const std::string &id) 51 | { 52 | std::filesystem::path path(tileDir); 53 | return (path / name / (id + std::string(".bin"))).string(); 54 | } 55 | 56 | std::string FileHelper::internalChartFileName(const std::string &tileDir, 57 | const std::string &name, 58 | int pixelsPerLon) 59 | { 60 | std::filesystem::path path(tileDir); 61 | std::stringstream ss; 62 | ss << pixelsPerLon; 63 | std::string baseName = "all_" + ss.str() + ".bin"; 64 | return (path / name / baseName).string(); 65 | } 66 | -------------------------------------------------------------------------------- /src/tilefactory/coverageratio.cpp: -------------------------------------------------------------------------------- 1 | #include "tilefactory/triangulator.h" 2 | 3 | #include "coverageratio.h" 4 | 5 | CoverageRatio::CoverageRatio(const GeoRect &rect) 6 | : m_rect(rect) 7 | { 8 | } 9 | 10 | float CoverageRatio::coveredArea() const 11 | { 12 | std::vector> lines; 13 | 14 | for (Clipper2Lib::PathD path : m_coverage) { 15 | std::vector line; 16 | 17 | for (Clipper2Lib::PointD point : path) { 18 | line.push_back({ point.x, point.y }); 19 | } 20 | lines.push_back(line); 21 | } 22 | 23 | std::vector triangles = Triangulator::calc(lines); 24 | 25 | assert((triangles.size() % 3) == 0); 26 | 27 | float area = 0; 28 | 29 | for (int i = 0; i < triangles.size() / 3; i++) { 30 | const Triangulator::Point &a = triangles[i * 3]; 31 | const Triangulator::Point &b = triangles[i * 3 + 1]; 32 | const Triangulator::Point &c = triangles[i * 3 + 2]; 33 | 34 | // https://en.wikipedia.org/wiki/Triangle#Using_coordinates 35 | float arg = a[0] * (b[1] - c[1]) 36 | + b[0] * (c[1] - a[1]) 37 | + c[0] * (a[1] - b[1]); 38 | area += 0.5f * fabs(arg); 39 | } 40 | 41 | return area; 42 | } 43 | 44 | float CoverageRatio::ratio() const 45 | { 46 | return coveredArea() / (m_rect.width() * m_rect.height()); 47 | } 48 | 49 | void CoverageRatio::accumulate(const capnp::List::Reader &coverages) 50 | { 51 | Clipper2Lib::PathsD input = m_coverage; 52 | 53 | for (const ChartData::CoverageArea::Reader &coverage : coverages) { 54 | for (const ChartData::Polygon::Reader &polygon : coverage.getPolygons()) { 55 | Clipper2Lib::PathD newCoveragePath; 56 | for (const ChartData::Position::Reader &pos : polygon.getMain()) { 57 | newCoveragePath.push_back({ pos.getLatitude(), pos.getLongitude() }); 58 | } 59 | input.push_back(newCoveragePath); 60 | } 61 | } 62 | 63 | m_coverage = Clipper2Lib::Union(input, Clipper2Lib::FillRule::EvenOdd, 5); 64 | } 65 | -------------------------------------------------------------------------------- /src/scene/tessellator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "annotations/annotationnode.h" 11 | #include "annotations/symbolimage.h" 12 | #include "polygon/polygonnode.h" 13 | #include "scene/annotations/fontimage.h" 14 | #include "scene/tilefactorywrapper.h" 15 | #include "tilefactory/chart.h" 16 | 17 | #include "tiledata.h" 18 | 19 | /*! 20 | Processes chart data for a tile an creates vertices for scene graph 21 | */ 22 | class Tessellator : public QObject 23 | { 24 | Q_OBJECT 25 | public: 26 | Tessellator(TileFactoryWrapper *tileFactory, 27 | TileFactoryWrapper::TileRecipe, 28 | std::shared_ptr symbolImage, 29 | std::shared_ptr fontImage); 30 | 31 | void fetchAgain(); 32 | QList textVertices() const { return m_data.textVertices; } 33 | QList symbolVertices() const { return m_data.symbolVertices; } 34 | bool isReady() const { return m_ready; } 35 | bool hasDataChanged() const { return m_dataChanged; } 36 | bool isRemoved() const { return m_removed; } 37 | void setRemoved(); 38 | void resetDataChanged() { m_dataChanged = false; } 39 | void setTileFactory(TileFactoryWrapper *tileFactoryWrapper); 40 | const TileData &data() const { return m_data; } 41 | QString id() const { return m_id; } 42 | void setId(const QString &tileId); 43 | static int pixelsPerLon(); 44 | QList createTileVertices(const QColor &color) const; 45 | 46 | public slots: 47 | void finished(); 48 | 49 | signals: 50 | void dataChanged(const QString &id); 51 | 52 | private: 53 | GeometryLayer createLoadingIndicatorLayer() const; 54 | QFuture m_result; 55 | QFutureWatcher m_watcher; 56 | QString m_id; 57 | TileData m_data; 58 | TileFactoryWrapper::TileRecipe m_recipe; 59 | TileFactoryWrapper *m_tileFactory = nullptr; 60 | std::shared_ptr m_symbolImage; 61 | std::shared_ptr m_fontImage; 62 | bool m_removed = false; 63 | bool m_ready = false; 64 | bool m_dataChanged = true; 65 | bool m_fetchAgain = false; 66 | }; 67 | -------------------------------------------------------------------------------- /src/usersettings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | static const QString orgName = QStringLiteral("Seatronomy"); 8 | static const QString appName = QStringLiteral("Nautograf"); 9 | 10 | class UserSettings : public QObject 11 | { 12 | Q_OBJECT 13 | 14 | Q_PROPERTY(double lat READ lat WRITE setLat NOTIFY latChanged) 15 | Q_PROPERTY(double lon READ lon WRITE setLon NOTIFY lonChanged) 16 | Q_PROPERTY(double pixelsPerLon READ pixelsPerLon WRITE setPixelsPerLon NOTIFY pixelsPerLonChanged) 17 | 18 | Q_PROPERTY(QWindow::Visibility windowState READ windowState WRITE setWindowState NOTIFY windowStateChanged) 19 | Q_PROPERTY(QRect geometry READ geometry WRITE setGeometry NOTIFY geometryChanged) 20 | Q_PROPERTY(bool showLegacyDebugView READ showLegacyDebugView WRITE setShowLegacyDebugView NOTIFY showLegacyDebugViewChanged) 21 | Q_PROPERTY(bool showLegacyRenderer READ showLegacyRenderer WRITE setLegacyRender NOTIFY showLegacyRendererChanged) 22 | 23 | public: 24 | UserSettings(); 25 | ~UserSettings(); 26 | QWindow::Visibility windowState() const; 27 | void setWindowState(QWindow::Visibility newWindowState); 28 | const QRect &geometry() const; 29 | void setGeometry(const QRect &newGeometry); 30 | 31 | bool showLegacyDebugView() const; 32 | void setShowLegacyDebugView(bool newShowLegacyDebugView); 33 | 34 | bool showLegacyRenderer() const; 35 | void setLegacyRender(bool newLegacyRender); 36 | 37 | double lat() const; 38 | void setLat(const double &newLat); 39 | 40 | double lon() const; 41 | void setLon(const double &newLon); 42 | 43 | double pixelsPerLon() const; 44 | void setPixelsPerLon(const double &newPixelsPerLon); 45 | 46 | private: 47 | static const QString showLegacyRendererKey; 48 | void write(); 49 | void read(); 50 | QWindow::Visibility m_windowState = QWindow::Windowed; 51 | QRect m_geometry = QRect(200, 200, 1280, 1024); 52 | double m_lat = 61; 53 | double m_lon = 4.5; 54 | double m_pixelsPerLon = 200; 55 | bool m_debugView; 56 | bool m_showLegacyRenderer = false; 57 | 58 | signals: 59 | void latChanged(); 60 | void lonChanged(); 61 | void pixelsPerLonChanged(); 62 | void windowStateChanged(); 63 | void geometryChanged(); 64 | void showLegacyDebugViewChanged(); 65 | void showLegacyRendererChanged(); 66 | }; 67 | -------------------------------------------------------------------------------- /src/scene/annotations/annotater.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "scene/annotations/fontimage.h" 10 | #include "scene/annotations/types.h" 11 | #include "symbolimage.h" 12 | #include 13 | 14 | class Annotater 15 | { 16 | public: 17 | Annotater(std::shared_ptr &fontImage, 18 | std::shared_ptr &symbolImage, 19 | int pixelsPerLon); 20 | 21 | struct Annotations 22 | { 23 | std::vector symbols; 24 | std::vector labels; 25 | 26 | Annotations &operator+=(const Annotations &other) 27 | { 28 | size_t initialSymbolsSize = symbols.size(); 29 | 30 | symbols.insert(symbols.end(), 31 | other.symbols.begin(), 32 | other.symbols.end()); 33 | 34 | size_t initialLabelsSize = labels.size(); 35 | 36 | labels.insert(labels.end(), 37 | other.labels.begin(), 38 | other.labels.end()); 39 | for (int i = initialLabelsSize; i < labels.size(); i++) { 40 | if (labels[i].parentSymbolIndex.has_value()) { 41 | labels[i].parentSymbolIndex.value() += initialSymbolsSize; 42 | } 43 | } 44 | 45 | return *this; 46 | } 47 | }; 48 | 49 | Annotations getAnnotations(const std::vector> &charts); 50 | 51 | private: 52 | template 53 | Annotations getAnnotations( 54 | const typename capnp::List::Reader &elements, 55 | std::function(const typename T::Reader &)> getSymbol, 56 | std::function(const typename T::Reader &)> getPosition, 57 | std::function getLabel, 58 | int priority = 0, 59 | CollisionRule collissionRule = CollisionRule::NoCheck) const; 60 | QPointF posToMercator(const ChartData::Position::Reader &pos) const; 61 | QString getDepthString(float depth) const; 62 | 63 | private: 64 | QLocale m_locale = QLocale::system(); 65 | std::shared_ptr m_fontImage; 66 | std::shared_ptr m_symbolImage; 67 | int m_pixelsPerLon = 0; 68 | }; 69 | -------------------------------------------------------------------------------- /src/scene/materialcreator.cpp: -------------------------------------------------------------------------------- 1 | #include "materialcreator.h" 2 | #include "annotations/annotationmaterial.h" 3 | #include "line/linematerial.h" 4 | #include "polygon/polygonmaterial.h" 5 | #include "tiledata.h" 6 | 7 | MaterialCreator::MaterialCreator(QSGTexture *symbolTexture, 8 | QSGTexture *fontTexture) 9 | : m_symbolTexture(symbolTexture) 10 | , m_fontTexture(fontTexture) 11 | { 12 | } 13 | 14 | void MaterialCreator::setZoom(float zoom) 15 | { 16 | m_scale = zoom; 17 | 18 | if (m_symbolMaterial) { 19 | m_symbolMaterial->setZoom(zoom); 20 | } 21 | 22 | if (m_textMaterial) { 23 | m_textMaterial->setZoom(zoom); 24 | } 25 | } 26 | 27 | PolygonMaterial *MaterialCreator::opaquePolygonMaterial() 28 | { 29 | if (!m_opaquePolygonMaterial) { 30 | m_opaquePolygonMaterial = std::make_unique(PolygonMaterial::BlendMode::Opaque); 31 | } 32 | 33 | return m_opaquePolygonMaterial.get(); 34 | } 35 | 36 | PolygonMaterial *MaterialCreator::blendedPolygonMaterial() 37 | { 38 | if (!m_blendedPolygonMaterial) { 39 | m_blendedPolygonMaterial = std::make_unique(PolygonMaterial::BlendMode::Alpha); 40 | } 41 | 42 | return m_blendedPolygonMaterial.get(); 43 | } 44 | 45 | AnnotationMaterial *MaterialCreator::symbolMaterial() 46 | { 47 | if (!m_symbolMaterial) { 48 | m_symbolMaterial = std::make_unique(m_symbolTexture); 49 | m_symbolMaterial->setZoom(m_scale); 50 | } 51 | 52 | return m_symbolMaterial.get(); 53 | } 54 | 55 | AnnotationMaterial *MaterialCreator::textMaterial() 56 | { 57 | if (!m_textMaterial) { 58 | m_textMaterial = std::make_unique(m_fontTexture); 59 | m_textMaterial->setZoom(m_scale); 60 | } 61 | 62 | return m_textMaterial.get(); 63 | } 64 | 65 | using LineStyle = GeometryLayer::LineGroup::Style; 66 | 67 | LineMaterial *MaterialCreator::lineMaterial(const LineStyle &style) 68 | { 69 | if (!m_lineMaterials.contains(style)) { 70 | auto lineMaterial = std::make_unique(); 71 | switch (style.width) { 72 | case LineStyle::Width::Thin: 73 | lineMaterial->setWidth(1.5); 74 | break; 75 | case LineStyle::Width::Medium: 76 | lineMaterial->setWidth(2.5); 77 | break; 78 | case LineStyle::Width::Thick: 79 | lineMaterial->setWidth(4); 80 | break; 81 | } 82 | m_lineMaterials[style] = std::move(lineMaterial); 83 | } 84 | 85 | return m_lineMaterials[style].get(); 86 | } 87 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 20, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "ninja-multi-vcpkg", 11 | "displayName": "Ninja Multi-Config", 12 | "description": "Configure with vcpkg toolchain and generate Ninja project files for all configurations", 13 | "binaryDir": "${sourceDir}/builds/${presetName}", 14 | "generator": "Ninja Multi-Config", 15 | "cacheVariables": { 16 | "CMAKE_TOOLCHAIN_FILE": { 17 | "type": "FILEPATH", 18 | "value": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake" 19 | }, 20 | "BUILD_TESTING": false, 21 | "BUILD_SHARED_LIBS": true 22 | } 23 | }, 24 | { 25 | "name": "ninja-multi-vcpkg-static", 26 | "inherits": "ninja-multi-vcpkg", 27 | "cacheVariables": { 28 | "BUILD_SHARED_LIBS": false 29 | } 30 | }, 31 | { 32 | "name": "ninja-vcpkg", 33 | "binaryDir": "${sourceDir}/builds/${presetName}", 34 | "generator": "Ninja", 35 | "hidden": true, 36 | "cacheVariables": { 37 | "CMAKE_TOOLCHAIN_FILE": { 38 | "type": "FILEPATH", 39 | "value": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake" 40 | }, 41 | "BUILD_TESTING": false, 42 | "BUILD_SHARED_LIBS": true 43 | } 44 | }, 45 | { 46 | "name": "ninja-vcpkg-static", 47 | "inherits": "ninja-vcpkg", 48 | "cacheVariables": { 49 | "BUILD_SHARED_LIBS": false 50 | } 51 | }, 52 | { 53 | "name": "ninja-debug-vcpkg", 54 | "inherits": "ninja-vcpkg", 55 | "cacheVariables": { 56 | "CMAKE_BUILD_TYPE": "Debug" 57 | } 58 | }, 59 | { 60 | "name": "ninja-debug-vcpkg-static", 61 | "inherits": "ninja-vcpkg-static", 62 | "cacheVariables": { 63 | "CMAKE_BUILD_TYPE": "Debug" 64 | } 65 | }, 66 | { 67 | "name": "ninja-release-vcpkg", 68 | "inherits": "ninja-vcpkg", 69 | "cacheVariables": { 70 | "CMAKE_BUILD_TYPE": "Release" 71 | } 72 | }, 73 | { 74 | "name": "ninja-release-vcpkg-static", 75 | "inherits": "ninja-vcpkg-static", 76 | "cacheVariables": { 77 | "CMAKE_BUILD_TYPE": "Release" 78 | } 79 | } 80 | ], 81 | "buildPresets": [ 82 | { 83 | "name": "debug", 84 | "configurePreset": "ninja-debug-vcpkg" 85 | }, 86 | { 87 | "name": "release", 88 | "configurePreset": "ninja-release-vcpkg" 89 | }, 90 | { 91 | "name": "static-debug", 92 | "configurePreset": "ninja-debug-vcpkg-static" 93 | }, 94 | { 95 | "name": "static-release", 96 | "configurePreset": "ninja-release-vcpkg-static" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /src/scene/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Qt6 2 | COMPONENTS Core Concurrent Quick ShaderTools 3 | REQUIRED 4 | ) 5 | 6 | find_path(QUADTREE_INCLUDE_DIRS "quadtree/Box.h") 7 | 8 | if(NOT DEFINED SYMBOLS_DIR) 9 | message(FATAL_ERROR "SYMBOLS_DIR not defined") 10 | endif() 11 | 12 | add_library(scene 13 | annotations/annotationmaterial.cpp 14 | annotations/annotationmaterial.h 15 | annotations/annotationnode.cpp 16 | annotations/annotationnode.h 17 | annotations/annotationshader.cpp 18 | annotations/annotationshader.h 19 | annotations/fontimage.cpp 20 | include/scene/annotations/fontimage.h 21 | include/scene/annotations/types.h 22 | annotations/symbolimage.cpp 23 | annotations/symbolimage.h 24 | annotations/annotater.cpp 25 | annotations/annotater.h 26 | annotations/zoomsweeper.cpp 27 | annotations/zoomsweeper.h 28 | 29 | line/linematerial.cpp 30 | line/linematerial.h 31 | line/linenode.cpp 32 | line/linenode.h 33 | line/lineshader.cpp 34 | line/lineshader.h 35 | 36 | polygon/polygonmaterial.cpp 37 | polygon/polygonmaterial.h 38 | polygon/polygonnode.cpp 39 | polygon/polygonnode.h 40 | polygon/polygonshader.cpp 41 | polygon/polygonshader.h 42 | 43 | geometrynode.cpp 44 | geometrynode.h 45 | materialcreator.cpp 46 | materialcreator.h 47 | rootnode.cpp 48 | rootnode.h 49 | 50 | scene.cpp 51 | include/scene/scene.h 52 | 53 | tessellator.cpp 54 | tessellator.h 55 | 56 | tilefactorywrapper.cpp 57 | include/scene/tilefactorywrapper.h 58 | 59 | tiledata.h 60 | ) 61 | 62 | qt6_add_shaders(scene "shaders" 63 | PREFIX 64 | "scene" 65 | FILES 66 | "annotations/annotation.vert" 67 | "annotations/annotation.frag" 68 | "polygon/polygon.frag" 69 | "polygon/polygon.vert" 70 | "line/line.frag" 71 | "line/line.vert" 72 | ) 73 | 74 | include(GenerateExportHeader) 75 | generate_export_header(scene) 76 | 77 | target_compile_definitions(scene PRIVATE 78 | SYMBOLS_DIR="${SYMBOLS_DIR}" 79 | APP_VERSION="${NAUTOGRAF_VERSION_DESCRIBE}" 80 | ) 81 | 82 | set_target_properties(scene PROPERTIES AUTOMOC TRUE) 83 | 84 | target_include_directories(scene 85 | PRIVATE 86 | . 87 | PUBLIC 88 | ${CMAKE_CURRENT_BINARY_DIR} 89 | include 90 | ) 91 | 92 | target_link_libraries(scene PUBLIC 93 | Qt6::Core 94 | Qt6::Quick 95 | msdf-atlas-gen 96 | msdf-atlas-read 97 | ) 98 | 99 | target_link_libraries(scene PRIVATE 100 | Qt6::Concurrent 101 | msdfgen-core 102 | msdfgen-ext 103 | tilefactory 104 | cutlines 105 | ) 106 | 107 | if(UNIX) 108 | target_link_libraries(scene PRIVATE 109 | fontconfig 110 | ) 111 | endif() 112 | 113 | target_include_directories(scene PRIVATE ${QUADTREE_INCLUDE_DIRS}) 114 | 115 | if(BUILD_TESTING) 116 | add_subdirectory(test) 117 | endif() 118 | -------------------------------------------------------------------------------- /src/tilefactory/mercator.cpp: -------------------------------------------------------------------------------- 1 | #define _USE_MATH_DEFINES 2 | #include 3 | #include 4 | 5 | #include "tilefactory/mercator.h" 6 | 7 | static const double latLimit = atan(sinh(M_PI)) * 180. / M_PI; 8 | 9 | double Mercator::mercatorNormalizedHeight(double topLat, double bottomLat) 10 | { 11 | double phiTop = topLat * M_PI / 180.; 12 | double phiBottom = bottomLat * M_PI / 180.; 13 | 14 | double logArgTop = tan(M_PI / 4 + phiTop / 2); 15 | double logArgBottom = tan(M_PI / 4 + phiBottom / 2); 16 | 17 | assert(logArgTop > 0); 18 | assert(logArgBottom > 0); 19 | 20 | double yTop = log(logArgTop); 21 | double yBottom = log(logArgBottom); 22 | 23 | return yTop - yBottom; 24 | } 25 | 26 | double Mercator::mercatorHeight(double topLat, double bottomLat, double pixelsPerLon) 27 | { 28 | double pixelsPerLonRadians = pixelsPerLon * 180. / M_PI; 29 | return pixelsPerLonRadians * mercatorNormalizedHeight(topLat, bottomLat); 30 | } 31 | 32 | double Mercator::mercatorHeightInverse(const double topLatitude, 33 | const double height, 34 | const double pixelsPerLongitude) 35 | { 36 | double pixelsPerLongitudeRadians = pixelsPerLongitude * 180. / M_PI; 37 | double phiTop = topLatitude * M_PI / 180.; 38 | double logArg = tan(M_PI / 4 + phiTop / 2); 39 | assert(logArg > 0); 40 | double yTop = log(logArg); 41 | double expArg = yTop - height / pixelsPerLongitudeRadians; 42 | double expResult = exp(expArg); 43 | double phiBottom = 2 * atan(expResult) - M_PI / 2; 44 | return phiBottom * 180 / M_PI; 45 | } 46 | 47 | double Mercator::toLongitude(double x) 48 | { 49 | return x * 360. - 180.; 50 | } 51 | 52 | double Mercator::toLatitude(double y) 53 | { 54 | double expArg = M_PI * (1 - 2 * y); 55 | double expResult = exp(expArg); 56 | double phiBottom = 2 * (atan(expResult) - M_PI / 4); 57 | return phiBottom * 180 / M_PI; 58 | } 59 | 60 | double Mercator::mercatorNormalizedWidth(double leftLon, double rightLon) 61 | { 62 | double phiLeft = leftLon * M_PI / 180.; 63 | double phiRight = rightLon * M_PI / 180.; 64 | 65 | return phiRight - phiLeft; 66 | } 67 | 68 | double Mercator::mercatorWidth(double leftLongitude, 69 | double rightLongitude, 70 | double pixelsPerLongitude) 71 | { 72 | double pixelsPerLongitudeRadians = pixelsPerLongitude * 180. / M_PI; 73 | double phiLeft = leftLongitude * M_PI / 180.; 74 | double phiRight = rightLongitude * M_PI / 180.; 75 | 76 | return pixelsPerLongitudeRadians * mercatorNormalizedWidth(leftLongitude, rightLongitude); 77 | } 78 | 79 | double Mercator::mercatorWidthInverse(double leftLongitude, 80 | double pixels, 81 | double pixelsPerLongitude) 82 | { 83 | return leftLongitude + pixels / pixelsPerLongitude; 84 | } 85 | 86 | double Mercator::mercatorWidthInverse(double pixels, double pixelsPerLongitude) 87 | { 88 | return pixels / pixelsPerLongitude; 89 | } 90 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/chart.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "chartdata.capnp.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "oesenc/s57.h" 13 | #include "tilefactory/chartclipper.h" 14 | #include "tilefactory/georect.h" 15 | #include "tilefactory/pos.h" 16 | 17 | #include "tilefactory_export.h" 18 | 19 | class TILEFACTORY_EXPORT Chart 20 | { 21 | public: 22 | static std::shared_ptr open(const std::string &filename); 23 | static bool write(capnp::MallocMessageBuilder *message, const std::string &filename); 24 | static std::unique_ptr 25 | buildFromS57(const std::vector &objects, 26 | const GeoRect &boundingBox, 27 | const std::string &name, 28 | int scale); 29 | 30 | Chart() = delete; 31 | ~Chart(); 32 | Chart(Chart &&) = delete; 33 | Chart(const Chart &) = delete; 34 | 35 | std::unique_ptr buildClipped(ChartClipper::Config config) const; 36 | 37 | static uint64_t typeId() { return ChartData::_capnpPrivate::typeId; } 38 | 39 | ChartData::Reader root() const 40 | { 41 | assert(m_capnpReader); 42 | return m_capnpReader->getRoot(); 43 | } 44 | 45 | int nativeScale() const { return root().getNativeScale(); } 46 | std::string name() const { return root().getName(); } 47 | GeoRect boundingBox() const; 48 | capnp::List::Reader coastLines() const { return root().getCoastLines(); } 49 | capnp::List::Reader coverage() const { return root().getCoverage(); } 50 | capnp::List::Reader landAreas() const { return root().getLandAreas(); } 51 | capnp::List::Reader depthAreas() const { return root().getDepthAreas(); } 52 | capnp::List::Reader depthContours() const { return root().getDepthContours(); } 53 | capnp::List::Reader builtUpAreas() const { return root().getBuiltUpAreas(); } 54 | capnp::List::Reader builtUpPoints() const { return root().getBuiltUpPoints(); } 55 | capnp::List::Reader landRegions() const { return root().getLandRegions(); } 56 | capnp::List::Reader soundings() const { return root().getSoundings(); } 57 | capnp::List::Reader beacons() const { return root().getBeacons(); } 58 | capnp::List::Reader underwaterRocks() const { return root().getUnderwaterRocks(); } 59 | capnp::List::Reader lateralBuoys() const { return root().getLateralBuoys(); } 60 | capnp::List::Reader pontoons() const { return root().getPontoons(); } 61 | capnp::List::Reader shorelineConstructions() const { return root().getShorelineConstructions(); } 62 | capnp::List::Reader roads() const { return root().getRoads(); } 63 | 64 | private: 65 | Chart(FILE *fd); 66 | void read(const std::string &filename); 67 | std::unique_ptr<::capnp::PackedFdMessageReader> m_capnpReader; 68 | FILE *m_file = nullptr; 69 | }; 70 | -------------------------------------------------------------------------------- /qml/StatusBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Layouts 1.3 3 | import QtQuick.Controls.Universal 4 | import QtQuick.Controls 5 | 6 | import org.seatronomy.nautograf 7 | 8 | ToolBar { 9 | id: root 10 | 11 | property string catalogPath 12 | property variant catalogStatus 13 | property bool serverError: false 14 | property real mouseLat 15 | property real mouseLon 16 | property real pixelsPerLongitude: 1 17 | 18 | position: ToolBar.Footer 19 | 20 | signal openCatalogSelector 21 | 22 | RowLayout { 23 | id: row 24 | 25 | anchors.fill: parent 26 | 27 | ToolButton { 28 | onClicked: root.openCatalogSelector() 29 | 30 | icon.name: CanUseIconTheme ? "folder-open" : "" 31 | text: !CanUseIconTheme ? "📁" : "" 32 | } 33 | 34 | Label { 35 | id: catalogPath 36 | 37 | elide: Text.ElideRight 38 | Layout.fillWidth: true 39 | text: root.catalogPath 40 | } 41 | 42 | RowLayout { 43 | visible: ChartModel.waitingForServer 44 | 45 | BusyIndicator {} 46 | 47 | Label { 48 | text: qsTr("Connecting to oexserverd") 49 | } 50 | } 51 | 52 | ToolSeparator { 53 | visible: ChartModel.waitingForServer 54 | } 55 | 56 | Label { 57 | visible: root.serverError 58 | text: qsTr("No oexserverd") 59 | } 60 | 61 | ToolSeparator { 62 | visible: root.serverError 63 | } 64 | 65 | Label { 66 | id: catalogStatus 67 | 68 | visible: root.catalogStatus !== ChartModel.NotLoaded && 69 | !(ChartModel.waitingForServer && root.catalogStatus === ChartModel.Invalid) 70 | text: { 71 | switch(root.catalogStatus) { 72 | case ChartModel.Oesu: 73 | return qsTr("🔐 OESU"); 74 | case ChartModel.Oesenc: 75 | return qsTr("🔐 OESENC"); 76 | case ChartModel.Unencrypted: 77 | return qsTr("🔓 Unencrypted"); 78 | case ChartModel.Invalid: 79 | return qsTr("⛔ Invalid catalog"); 80 | default: 81 | return ""; 82 | } 83 | } 84 | } 85 | 86 | ToolSeparator { 87 | visible: catalogStatus.visible 88 | } 89 | 90 | Label { 91 | Layout.leftMargin: 20 92 | property real value: 52246 / (root.pixelsPerLongitude / 2560 * 0.6) 93 | property string pixelsPerLongitude: Number(root.pixelsPerLongitude).toLocaleString(Qt.locale(), 'f', 0) 94 | text: "1 : " + Number(value).toLocaleString(Qt.locale(), 'f', 0) + " (" + pixelsPerLongitude + ")" 95 | } 96 | 97 | Label { 98 | Layout.preferredWidth: 130 99 | text: Number(root.mouseLat).toLocaleString(Qt.locale(), 'f', 7) 100 | } 101 | 102 | Label { 103 | Layout.rightMargin: 20 104 | Layout.preferredWidth: 130 105 | text: Number(root.mouseLon).toLocaleString(Qt.locale(), 'f', 7) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /qml/About.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Layouts 4 | import QtQuick.Controls.Universal 5 | import org.seatronomy.nautograf 6 | 7 | Dialog { 8 | id: dialog 9 | 10 | anchors.centerIn: Overlay.overlay 11 | visible: true 12 | modal: true 13 | standardButtons: Dialog.Close 14 | background: Rectangle { 15 | property int offset: 3 16 | x: -offset 17 | y: -offset 18 | width: parent.width + offset * 2 19 | height: parent.height + offset * 2 20 | radius: offset * 3 21 | color: dialog.footer.background.color // How to avoid this hack? 22 | } 23 | 24 | header: ColumnLayout { 25 | Image { 26 | source: "graphics/title.svg" 27 | Layout.alignment: Qt.AlignCenter 28 | Layout.topMargin: 10 29 | } 30 | 31 | Label { 32 | text: "v" + AppVersion 33 | Layout.alignment: Qt.AlignCenter 34 | } 35 | 36 | TabBar { 37 | id: bar 38 | 39 | Layout.fillWidth: true 40 | 41 | TabButton { 42 | text: qsTr("About") 43 | } 44 | 45 | TabButton { 46 | text: qsTr("Licenses") 47 | } 48 | } 49 | } 50 | 51 | StackLayout { 52 | currentIndex: bar.currentIndex 53 | 54 | ColumnLayout { 55 | spacing: 20 56 | 57 | TextEdit { 58 | Layout.maximumWidth: 500 59 | Layout.minimumHeight: implicitHeight 60 | wrapMode: Text.WordWrap 61 | color: Universal.foreground 62 | textFormat: Text.MarkdownText 63 | selectByMouse: true 64 | onLinkActivated: function(url) { 65 | Qt.openUrlExternally(url); 66 | } 67 | text: AboutText 68 | } 69 | 70 | Label { 71 | visible: !CanReadEncryptedCatalog 72 | font.bold: true 73 | text: "This version is compiled without support for chart descryption via oexserverd" 74 | } 75 | } 76 | 77 | ColumnLayout { 78 | id: licensesLayout 79 | 80 | Licenses { 81 | id: licenseModel 82 | 83 | index: comboBox.currentIndex 84 | } 85 | 86 | ComboBox { 87 | id: comboBox 88 | 89 | Layout.alignment: Qt.AlignLeft 90 | Layout.fillWidth: true 91 | Layout.bottomMargin: 20 92 | model: licenseModel.names 93 | } 94 | 95 | Flickable { 96 | id: flickable 97 | 98 | visible: parent.visible 99 | clip: true 100 | Layout.fillWidth: true 101 | Layout.fillHeight: true 102 | Layout.minimumHeight: 400 103 | Layout.preferredWidth: licenseText.width 104 | contentHeight: licenseText.height 105 | ScrollBar.vertical: ScrollBar {} 106 | 107 | Label { 108 | id: licenseText 109 | 110 | font.family: "fixed" 111 | text: licenseModel.currentLicense 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/tileinfobackend.cpp: -------------------------------------------------------------------------------- 1 | #include "scene/scene.h" 2 | 3 | #include "tileinfobackend.h" 4 | 5 | TileInfoBackend::TileInfoBackend() 6 | { 7 | } 8 | 9 | TileFactoryWrapper *TileInfoBackend::tileFactory() const 10 | { 11 | return m_tileFactory; 12 | } 13 | 14 | void TileInfoBackend::setTileFactory(TileFactoryWrapper *newTileFactory) 15 | { 16 | if (m_tileFactory == newTileFactory) 17 | return; 18 | 19 | if (m_tileFactory) { 20 | disconnect(m_tileFactory, nullptr, this, nullptr); 21 | } 22 | 23 | m_tileFactory = newTileFactory; 24 | emit tileFactoryChanged(); 25 | 26 | if (m_tileFactory) { 27 | connect(m_tileFactory, &TileFactoryWrapper::tileDataChanged, 28 | this, &TileInfoBackend::tileDataChanged); 29 | } 30 | } 31 | 32 | void TileInfoBackend::setTileRef(const QVariantMap &tileRef) 33 | { 34 | if (m_tileRef == tileRef) { 35 | return; 36 | } 37 | 38 | m_tileRef = tileRef; 39 | QString newTileId; 40 | 41 | if (m_tileRef.contains("tileId")) { 42 | newTileId = m_tileRef["tileId"].toString(); 43 | } 44 | 45 | if (m_tileId != newTileId) { 46 | m_tileId = newTileId; 47 | emit tileIdChanged(); 48 | } 49 | 50 | update(); 51 | } 52 | 53 | void TileInfoBackend::update() 54 | { 55 | if (m_tileRef.isEmpty()) { 56 | m_chartInfos.clear(); 57 | emit chartInfosChanged(); 58 | return; 59 | } 60 | 61 | if (!m_tileFactory) { 62 | return; 63 | } 64 | 65 | std::optional tileRecipe = Scene::parseTileRef(m_tileRef); 66 | 67 | if (!tileRecipe.has_value()) { 68 | return; 69 | } 70 | 71 | m_chartInfos = m_tileFactory->chartInfos(tileRecipe.value()); 72 | emit chartInfosChanged(); 73 | } 74 | 75 | void TileInfoBackend::setChartEnabled(const QString &chart, bool enabled) 76 | { 77 | QStringList disabledCharts; 78 | 79 | for (const TileFactory::ChartInfo &chartInfo : m_chartInfos) { 80 | if (!chartInfo.enabled) { 81 | disabledCharts.append(QString::fromStdString(chartInfo.name)); 82 | } 83 | } 84 | 85 | if (enabled && disabledCharts.contains(chart)) { 86 | disabledCharts.removeAll(chart); 87 | } 88 | 89 | if (!enabled && !disabledCharts.contains(chart)) { 90 | disabledCharts.append(chart); 91 | } 92 | 93 | TileFactory::TileSettings tileSettings; 94 | 95 | for (const QString &disabledChart : disabledCharts) { 96 | tileSettings.disabledCharts.push_back(disabledChart.toStdString()); 97 | } 98 | 99 | m_tileFactory->setTileSettings(m_tileId.toStdString(), tileSettings); 100 | } 101 | 102 | const QVariantMap &TileInfoBackend::tileRef() const 103 | { 104 | return m_tileRef; 105 | } 106 | 107 | QVariantList TileInfoBackend::chartInfos() const 108 | { 109 | QVariantList output; 110 | 111 | for (const TileFactory::ChartInfo &chartInfo : m_chartInfos) { 112 | QVariantMap entry; 113 | entry["name"] = QString::fromStdString(chartInfo.name); 114 | entry["enabled"] = chartInfo.enabled; 115 | output.append(entry); 116 | } 117 | 118 | return output; 119 | } 120 | 121 | void TileInfoBackend::tileDataChanged(const QStringList &tileIds) 122 | { 123 | update(); 124 | } 125 | 126 | const QString &TileInfoBackend::tileId() const 127 | { 128 | return m_tileId; 129 | } 130 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: nautograf 2 | base: core22 3 | architectures: 4 | - build-on: [amd64] 5 | - build-on: [arm64] 6 | - build-on: [armhf] 7 | adopt-info: nautograf 8 | summary: Tile-based nautical chart viewer 9 | description: > 10 | Nautograf is a viewer for marine vector charts. It is inspired by 11 | OpenCPN which is the only nautical chart software that is open 12 | source and runs with offline chart data. Nautograf only supports 13 | chart data from https://www.o-charts.org. Please visit the GitHub 14 | repository for more information: https://github.com/hornang/nautograf 15 | grade: stable 16 | confinement: strict 17 | apps: 18 | nautograf: 19 | command-chain: 20 | - bin/graphics-core22-wrapper 21 | command: usr/bin/nautograf 22 | extensions: [gnome] 23 | environment: 24 | QT_PLUGIN_PATH: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/qt6/plugins 25 | QML2_IMPORT_PATH: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/qt6/qml 26 | plugs: 27 | - home 28 | 29 | # Statements related to mesa-core22 is from this forum post: 30 | # https://discourse.ubuntu.com/t/the-graphics-core22-snap-interface/34663 31 | 32 | plugs: 33 | graphics-core22: 34 | interface: content 35 | target: $SNAP/graphics 36 | default-provider: mesa-core22 37 | 38 | layout: 39 | /usr/share/libdrm: 40 | bind: $SNAP/graphics/libdrm 41 | /usr/share/drirc.d: 42 | symlink: $SNAP/graphics/drirc.d 43 | 44 | parts: 45 | nautograf: 46 | override-pull: | 47 | craftctl default 48 | craftctl set version=$(git describe --tags | sed 's/^v\?\(.*\)$/\1/') 49 | source: . 50 | plugin: cmake 51 | build-environment: 52 | - VCPKG_FORCE_SYSTEM_BINARIES: 1 # Must be set for building on arm 53 | cmake-parameters: 54 | - -DCMAKE_TOOLCHAIN_FILE=${CRAFT_PART_SRC}/vcpkg/scripts/buildsystems/vcpkg.cmake 55 | - -DBUILD_TESTING=off 56 | - -DUSE_XDG_FILE_DIALOG=on 57 | - -DUSE_OEXSERVERD=off 58 | - -DCMAKE_INSTALL_PREFIX=/usr 59 | build-packages: 60 | # curl, zip, unzip and tar is needed by vcpkg: 61 | # https://github.com/microsoft/vcpkg/blob/master/scripts/bootstrap.sh#L88-L91 62 | - curl 63 | - zip 64 | - unzip 65 | - tar 66 | 67 | - build-essential 68 | - cargo 69 | - ninja-build 70 | - libglu1-mesa-dev 71 | - libxmu-dev 72 | - libxi-dev 73 | - libfontconfig-dev 74 | - libgl-dev 75 | - imagemagick 76 | - librsvg2-bin 77 | - qt6-base-dev 78 | - qt6-declarative-dev 79 | - libqt6shadertools6-dev 80 | - libqt6svg6-dev 81 | - libqt6opengl6-dev 82 | - pkg-config 83 | 84 | stage-packages: 85 | - libqt6core6 86 | - libqt6gui6 87 | - libqt6opengl6 88 | - libqt6qmlmodels6 89 | - libqt6quick6 90 | - libqt6widgets6 91 | - libqt6svg6 92 | - qt6-qpa-plugins 93 | - qt6-gtk-platformtheme 94 | - qt6-wayland 95 | - qml6-module-qtquick-controls 96 | - qml6-module-qtquick-layouts 97 | - qml6-module-qtquick-window 98 | - qml6-module-qt-labs-platform 99 | - qml6-module-qtqml-workerscript 100 | - qml6-module-qtquick-templates 101 | 102 | graphics-core22: 103 | after: [nautograf] 104 | source: https://github.com/MirServer/graphics-core22.git 105 | plugin: dump 106 | override-prime: | 107 | craftctl default 108 | ${CRAFT_PART_SRC}/bin/graphics-core22-cleanup mesa-core22 nvidia-core22 109 | prime: 110 | - bin/graphics-core22-wrapper 111 | -------------------------------------------------------------------------------- /src/chartmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include "scene/tilefactorywrapper.h" 16 | #include "tilefactory/catalog.h" 17 | #include "tilefactory/tilefactory.h" 18 | 19 | class OesencTileSource; 20 | 21 | class ChartModel : public QAbstractListModel 22 | { 23 | Q_OBJECT 24 | Q_PROPERTY(bool allEnabled READ allEnabled NOTIFY allEnabledChanged) 25 | Q_PROPERTY(float loadingProgress READ loadingProgress NOTIFY loadingProgressChanged) 26 | Q_PROPERTY(bool waitingForServer MEMBER m_waitingForServer NOTIFY waitingForServerChanged) 27 | Q_PROPERTY(bool serverError MEMBER m_serverError NOTIFY serverErrorChanged) 28 | Q_PROPERTY(QString dir READ dir NOTIFY dirChanged) 29 | Q_PROPERTY(CatalogType catalogType READ catalogType NOTIFY catalogTypeChanged) 30 | 31 | public: 32 | enum class CatalogType { 33 | NotLoaded, 34 | Oesu, 35 | Oesenc, 36 | Unencrypted, 37 | Invalid, 38 | }; 39 | Q_ENUM(CatalogType) 40 | 41 | enum class Role : int { 42 | Name, 43 | Scale, 44 | Enabled, 45 | Ok, 46 | }; 47 | 48 | ChartModel(std::shared_ptr tileFactory); 49 | QHash roleNames() const; 50 | bool allEnabled() const; 51 | int rowCount(const QModelIndex &parent = QModelIndex()) const; 52 | QVariant data(const QModelIndex &index, int role) const; 53 | QString dir() const { return m_dir; } 54 | 55 | float loadingProgress() const; 56 | 57 | bool serverError() const; 58 | QString cryptReaderStatus() const; 59 | CatalogType catalogType() const; 60 | 61 | public slots: 62 | void populateModel(const QString &dir); 63 | void setChartEnabled(int index, bool enabled); 64 | void setAllChartsEnabled(bool enabled); 65 | void setUrl(const QUrl &url); 66 | void writeVisibleCharts(); 67 | void clear(); 68 | 69 | signals: 70 | void loadChart(const QString &chart); 71 | void chartListAvailable(const QStringList &charts); 72 | void allEnabledChanged(); 73 | void dirChanged(); 74 | void loadingProgressChanged(); 75 | void waitingForServerChanged(); 76 | void serverErrorChanged(); 77 | void catalogTypeChanged(); 78 | void catalogExtentCalculated(double top, double bottom, double left, double right); 79 | 80 | private: 81 | void setDir(const QString &dir); 82 | void enableOesencServerControl(); 83 | void setWaitingForServerFalse(); 84 | QHash readVisibleCharts(); 85 | void addSource(const std::shared_ptr &tileSource, bool enabled); 86 | void updateAllEnabled(); 87 | bool loadNextFromQueue(); 88 | QQueue m_chartsToLoad; 89 | std::unique_ptr m_oesencServerControl; 90 | std::shared_ptr m_tileFactory; 91 | QHash m_roleNames; 92 | std::vector m_sourceCache; 93 | QString m_tileDir; 94 | QString m_dirBeeingLoaded; 95 | QString m_currentChartBeeingLoaded; 96 | QString m_dir; 97 | QByteArray m_key; 98 | QTimer m_serverPollTimer; 99 | std::unique_ptr m_catalog; 100 | std::chrono::milliseconds m_serverPollDuration { 0 }; 101 | int m_initalQueueSize = 0; 102 | float m_loadingProgress = 1; 103 | bool m_loadingCharts = false; 104 | bool m_allEnabled = false; 105 | bool m_waitingForServer = true; 106 | bool m_serverError = false; 107 | }; 108 | -------------------------------------------------------------------------------- /src/tilefactory/include/tilefactory/tilefactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "itilesource.h" 9 | #include "tilefactory/georect.h" 10 | #include "tilefactory/pos.h" 11 | 12 | #include "tilefactory_export.h" 13 | 14 | class TILEFACTORY_EXPORT TileFactory 15 | { 16 | public: 17 | TileFactory() = default; 18 | 19 | struct Tile 20 | { 21 | std::string tileId; 22 | GeoRect boundingBox; 23 | int maxPixelsPerLon; 24 | }; 25 | 26 | struct ChartInfo 27 | { 28 | std::string name; 29 | GeoRect boundingBox; 30 | bool enabled; 31 | }; 32 | 33 | struct TileSettings 34 | { 35 | std::vector disabledCharts; 36 | }; 37 | 38 | /*! 39 | Helper structure to add enable property to hide certain charts from 40 | rendering even if they are loaded. 41 | */ 42 | struct Source 43 | { 44 | std::string name; 45 | std::shared_ptr tileSource; 46 | bool enabled = true; 47 | }; 48 | 49 | /*! 50 | Returns the tiles within the given viewport that intersects with the 51 | loaded charts. 52 | 53 | \param center The geodetic position of the viewport's center 54 | \param pixelsPerLon Pixels per longitude for the viewport. 55 | \param width Width of viewport in pixels. 56 | \param height Height of viewport in pixels. 57 | */ 58 | std::vector tiles(const Pos ¢er, float pixelsPerLon, int width, int height); 59 | 60 | /*! 61 | Removes all chart sources. 62 | 63 | This function may block if a source is currently beeing used by another 64 | thread through \ref tileData. 65 | */ 66 | void clear(); 67 | 68 | /*! 69 | Returns a list of tile data for the given tile geo rectangle. 70 | 71 | This will trigger creation of the data if it is not already cached to 72 | disk. Therefore the function could take some time before it returns. 73 | */ 74 | std::vector> tileData(const GeoRect &rect, double pixelsPerLongitude); 75 | std::vector chartInfo(const GeoRect &rect, double pixelsPerLongitude); 76 | 77 | void setUpdateCallback(std::function updateCallback) { m_updateCallback = updateCallback; } 78 | void setChartsChangedCb(std::function roi)> chartsChangedCb) { m_chartsChangedCb = chartsChangedCb; } 79 | void setSources(const std::vector &sources); 80 | 81 | using TileDataChangedCallback = std::function)>; 82 | void setTileDataChangedCallback(TileDataChangedCallback tileDataChangedCallback) { m_tileDataChangedCallback = tileDataChangedCallback; } 83 | void setTileSettings(const std::string &tileId, TileSettings tileSettings); 84 | void setChartEnabled(const std::string &name, bool enabled); 85 | std::vector setAllChartsEnabled(bool enabled); 86 | std::optional totalExtent(); 87 | 88 | private: 89 | std::vector sourceCandidates(const GeoRect &rect, double pixelsPerLon); 90 | bool chartEnabledForTile(const std::string &chart, const std::string &tileId) const; 91 | bool hasSource(const std::string &id); 92 | static std::vector tilesInViewport(const GeoRect &rect, int zoom); 93 | std::function m_updateCallback; 94 | std::function roi)> m_chartsChangedCb; 95 | TileDataChangedCallback m_tileDataChangedCallback; 96 | std::vector m_sources; 97 | std::mutex m_sourcesMutex; 98 | std::vector m_previousTileLocations; 99 | std::vector m_previousTiles; 100 | std::unordered_map m_tileSettings; 101 | }; 102 | -------------------------------------------------------------------------------- /src/scene/include/scene/scene.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "tilefactory/chart.h" 10 | #include "tilefactory/georect.h" 11 | #include "tilefactorywrapper.h" 12 | 13 | #include "scene_export.h" 14 | 15 | class SymbolImage; 16 | class FontImage; 17 | class Tessellator; 18 | class TileData; 19 | 20 | class SCENE_EXPORT Scene : public QQuickItem 21 | { 22 | Q_OBJECT 23 | Q_PROPERTY(TileFactoryWrapper *tileFactory READ tileFactory WRITE setTileFactory NOTIFY tileFactoryChanged) 24 | Q_PROPERTY(QAbstractListModel *tileModel READ tileModel WRITE setModel NOTIFY tileModelChanged) 25 | Q_PROPERTY(double lat READ lat WRITE setLat NOTIFY latChanged) 26 | Q_PROPERTY(double lon READ lon WRITE setLon NOTIFY lonChanged) 27 | Q_PROPERTY(double pixelsPerLon READ pixelsPerLon WRITE setPixelsPerLon NOTIFY pixelsPerLonChanged) 28 | Q_PROPERTY(QString focusedTile READ focusedTile WRITE setFocusedTile NOTIFY setFocusedTileChanged) 29 | Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) 30 | 31 | public: 32 | Scene(QQuickItem *parent = 0); 33 | 34 | QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *); 35 | void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry); 36 | TileFactoryWrapper *tileFactory() const; 37 | void setTileFactory(TileFactoryWrapper *newTileFactory); 38 | 39 | double lat() const; 40 | void setLat(double newLat); 41 | 42 | double lon() const; 43 | void setLon(double newLon); 44 | 45 | double pixelsPerLon() const; 46 | void setPixelsPerLon(double newPixelsPerLon); 47 | 48 | QAbstractListModel *tileModel() const; 49 | void setModel(QAbstractListModel *newTileModel); 50 | 51 | static std::optional parseTileRef(const QVariantMap &tileRef); 52 | const QString &focusedTile() const; 53 | void setFocusedTile(const QString &newFocusedTile); 54 | 55 | const QColor &accentColor() const; 56 | void setAccentColor(const QColor &newAccentColor); 57 | 58 | public slots: 59 | void rowsInserted(const QModelIndex &parent, int first, int last); 60 | void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last); 61 | void dataChanged(const QModelIndex &topLeft, 62 | const QModelIndex &bottomRight, 63 | const QList &roles); 64 | void tessellatorDone(const QString &tileId); 65 | void tileDataChanged(const QStringList &tileIds); 66 | void atlasChanged(); 67 | 68 | signals: 69 | void tileFactoryChanged(); 70 | void latChanged(); 71 | void lonChanged(); 72 | void pixelsPerLonChanged(); 73 | void tileModelChanged(); 74 | void setFocusedTileChanged(); 75 | 76 | void accentColorChanged(); 77 | 78 | private: 79 | template 80 | void removeStaleNodes(QSGNode *parent) const; 81 | void markChildrensDirtyMaterial(QSGNode *parent); 82 | void addTessellatorsFromModel(TileFactoryWrapper *tileFactory, int first, int last); 83 | void updateBox(); 84 | void getData(); 85 | void fetchAll(); 86 | void initializeTessellators(); 87 | 88 | QHash> m_tessellators; 89 | QSet m_tessellatorsWithPendingData; 90 | std::shared_ptr m_symbolImage; 91 | std::shared_ptr m_fontImage; 92 | 93 | double m_lat = 0; 94 | double m_lon = 0; 95 | double m_pixelsPerLon = 300; 96 | QRectF m_boundingRect; 97 | 98 | /// The viewport in mercator coordinates 99 | QRectF m_box; 100 | 101 | QString m_focusedTile; 102 | bool m_focusedTileChanged = false; 103 | 104 | TileFactoryWrapper *m_tileFactory = nullptr; 105 | QAbstractListModel *m_tileModel = nullptr; 106 | bool m_tessellatorRemoved = false; 107 | float m_zoom = -1.0; 108 | QColor m_accentColor; 109 | QColor m_overlayColor; 110 | bool m_atlasChanged = false; 111 | }; 112 | -------------------------------------------------------------------------------- /src/tilefactory/chartdata.capnp: -------------------------------------------------------------------------------- 1 | @0xcfa67771551e48ef; 2 | 3 | struct ChartData { 4 | name @0: Text; 5 | nativeScale @1: Int32; 6 | coverage @2 :List(CoverageArea); 7 | landAreas @3 :List(LandArea); 8 | builtUpAreas @4 :List(BuiltUpArea); 9 | builtUpPoints @5 :List(BuiltUpPoint); 10 | depthAreas @6 :List(DepthArea); 11 | soundings @7 :List(Sounding); 12 | beacons @8 :List(Beacon); 13 | underwaterRocks @9 :List(UnderwaterRock); 14 | roads @10 :List(Road); 15 | lateralBuoys @11 :List(BuoyLateral); 16 | landRegions @12 :List(LandRegion); 17 | topLeft @13: Position; 18 | bottomRight @14: Position; 19 | coastLines @15: List(CoastLine); 20 | pontoons @16: List(Pontoon); 21 | depthContours @17: List(DepthContour); 22 | shorelineConstructions @18: List(ShorelineConstruction); 23 | 24 | struct CoverageArea { 25 | polygons @0 :List(Polygon); 26 | } 27 | 28 | struct LandArea { 29 | name @0: Text; 30 | polygons @1 :List(Polygon); 31 | centroid @2: Position; 32 | } 33 | 34 | struct BuiltUpArea { 35 | name @0: Text; 36 | polygons @1 :List(Polygon); 37 | centroid @2: Position; 38 | } 39 | 40 | struct CoastLine { 41 | lines @0 :List(Line); 42 | } 43 | 44 | struct DepthContour { 45 | lines @0 :List(Line); 46 | } 47 | 48 | struct BuiltUpPoint { 49 | name @0: Text; 50 | position @1: Position; 51 | } 52 | 53 | struct LandRegion { 54 | name @0: Text; 55 | position @1: Position; 56 | } 57 | 58 | struct DepthArea { 59 | depth @0 :Float32; 60 | polygons @1 :List(Polygon); 61 | } 62 | 63 | struct Polygon { 64 | main @0 :List(Position); 65 | holes @1 :List(List(Position)); 66 | } 67 | 68 | struct Pontoon { 69 | name @0: Text; 70 | polygons @1 :List(Polygon); 71 | lines @2 :List(Line); 72 | } 73 | 74 | struct ShorelineConstruction { 75 | name @0: Text; 76 | polygons @1 :List(Polygon); 77 | lines @2 :List(Line); 78 | } 79 | 80 | struct Position { 81 | latitude @0: Float64; 82 | longitude @1: Float64; 83 | } 84 | 85 | struct Sounding { 86 | depth @0 :Float32; 87 | position @1: Position; 88 | } 89 | 90 | struct Beacon { 91 | position @0: Position; 92 | shape @1: BeaconShape; 93 | name @2: Text; 94 | } 95 | 96 | struct BuoyLateral { 97 | position @0: Position; 98 | category @1: CategoryOfLateralMark; 99 | shape @2: BuoyShape; 100 | name @3: Text; 101 | color @4: Color; 102 | } 103 | 104 | enum BuoyShape { 105 | unknown @0; 106 | conical @1; 107 | can @2; 108 | spherical @3; 109 | pillar @4; 110 | spar @5; 111 | barrel @6; 112 | superBuoy @7; 113 | iceBuoy @8; 114 | } 115 | 116 | enum Color { 117 | unknown @0; 118 | white @1; 119 | black @2; 120 | red @3; 121 | green @4; 122 | blue @5; 123 | yellow @6; 124 | grey @7; 125 | brown @8; 126 | amber @9; 127 | violet @10; 128 | orange @11; 129 | magenta @12; 130 | pink @13; 131 | } 132 | 133 | enum CategoryOfLateralMark { 134 | unknown @0; 135 | port @1; 136 | starboard @2; 137 | channelToStarboard @3; 138 | channelToPort @4; 139 | } 140 | 141 | enum BeaconShape { 142 | unknown @0; 143 | stake @1; 144 | withy @2; 145 | beaconTower @3; 146 | latticeBeacon @4; 147 | pileBeacon @5; 148 | cairn @6; 149 | boyant @7; 150 | } 151 | 152 | enum WaterLevelEffect { 153 | unknown @0; 154 | partlySubmergedAtHighWater @1; 155 | alwaysDry @2; 156 | alwaysSubmerged @3; 157 | coversAndUncovers @4; 158 | awash @5; 159 | subjectToFlooding @6; 160 | floating @7; 161 | } 162 | 163 | enum CategoryOfRoad { 164 | unknown @0; 165 | motorway @1; 166 | majorRoad @2; 167 | minorRoad @3; 168 | track @4; 169 | majorStreet @5; 170 | minorStreet @6; 171 | crossing @7; 172 | } 173 | 174 | struct UnderwaterRock { 175 | position @0: Position; 176 | depth @1: Float32; 177 | waterlevelEffect @2: WaterLevelEffect; 178 | } 179 | 180 | struct Depth { 181 | depth @0 :Float32; 182 | position @1: Position; 183 | } 184 | 185 | struct Line { 186 | positions @0 :List(Position); 187 | } 188 | 189 | struct Road { 190 | name @0: Text; 191 | category @1: CategoryOfRoad; 192 | lines @2 :List(Line); 193 | polygons @3 :List(Polygon); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /qml/TileInfo.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Window 3 | import QtQuick.Controls 4 | import QtQuick.Layouts 5 | import QtQuick.Controls.Universal 2.12 6 | import org.seatronomy.nautograf 1.0 7 | 8 | Pane { 9 | id: root 10 | 11 | property alias chartNames: repeater.model 12 | property alias selectedTile: tileInfoBackend.tileId 13 | property variant tileRef 14 | 15 | signal tileSelected(var tileId) 16 | signal close 17 | 18 | function selectTile(tileRef) { 19 | tileInfoBackend.tileRef = tileRef; 20 | } 21 | 22 | onTileRefChanged: selectTile(tileRef) 23 | 24 | Keys.onEscapePressed: function(event) { 25 | if (event.key === Qt.Key_Escape) { 26 | root.close(); 27 | event.accepted = true; 28 | } 29 | } 30 | 31 | 32 | TileInfoBackend { 33 | id: tileInfoBackend 34 | 35 | tileFactory: TileFactory 36 | } 37 | 38 | Button { 39 | text: "✕" 40 | anchors.top: parent.top 41 | anchors.right: parent.right 42 | onClicked: root.close() 43 | } 44 | 45 | SplitView { 46 | orientation: Qt.Vertical 47 | anchors.fill: parent 48 | anchors.topMargin: 50 49 | 50 | ListView { 51 | id: tileList 52 | 53 | model: MapTileModel 54 | SplitView.preferredHeight: 400 55 | spacing: 2 56 | 57 | delegate: Pane { 58 | height: 40 59 | padding: 0 60 | width: tileList.width 61 | 62 | RowLayout { 63 | anchors.fill: parent 64 | 65 | MouseArea { 66 | Layout.fillHeight: true 67 | Layout.fillWidth: true 68 | 69 | onClicked: { 70 | if (model.tileRef.tileId === tileInfoBackend.tileId) { 71 | root.selectTile({}); 72 | root.tileSelected(""); 73 | tileList.currentIndex = -1; 74 | } else { 75 | root.selectTile(model.tileRef); 76 | root.tileSelected(model.tileRef.tileId); 77 | tileList.currentIndex = index; 78 | } 79 | } 80 | 81 | Rectangle { 82 | anchors.fill: parent 83 | visible: model.tileId === tileInfoBackend.tileId 84 | color: Universal.accent 85 | } 86 | 87 | Label { 88 | anchors { 89 | verticalCenter: parent.verticalCenter 90 | left: parent.left 91 | margins: 5 92 | } 93 | text: model.tileId 94 | font.family: "Lucida Console" 95 | } 96 | 97 | } 98 | 99 | TextEdit { 100 | id: textEditForCopyToClipboard 101 | 102 | visible: false 103 | } 104 | 105 | Button { 106 | onClicked: { 107 | textEditForCopyToClipboard.text = model.tileId; 108 | textEditForCopyToClipboard.selectAll(); 109 | textEditForCopyToClipboard.copy(); 110 | } 111 | 112 | icon.name: CanUseIconTheme ? "edit-copy" : "" 113 | text: !CanUseIconTheme ? "📋" : "" 114 | } 115 | } 116 | 117 | } 118 | } 119 | 120 | ColumnLayout { 121 | TabBar { 122 | id: bar 123 | 124 | Layout.fillWidth: true 125 | 126 | TabButton { 127 | text: qsTr("Charts") 128 | } 129 | } 130 | 131 | StackLayout { 132 | Layout.fillWidth: true 133 | currentIndex: bar.currentIndex 134 | 135 | ListView { 136 | id: repeater 137 | 138 | Layout.fillWidth: true 139 | Layout.fillHeight: true 140 | model: tileInfoBackend.chartInfos 141 | 142 | delegate: CheckBox { 143 | id: checkbox 144 | 145 | font.family: "Lucida Console" 146 | checkable: true 147 | checked: modelData["enabled"] 148 | onClicked: tileInfoBackend.setChartEnabled(modelData["name"], checked); 149 | text: modelData["name"] 150 | } 151 | } 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Nautograf](qml/graphics/title.svg) 2 | 3 | ## Introduction 4 | 5 | Nautograf is an experimental viewer for marine vector charts. It is inspired by [OpenCPN](https://www.opencpn.org) which is the only nautical chart software that is open source and runs with offline chart data. The aim of Nautograf is to offer a user experience similar to OpenCPN but with better zooming and panning, better rendering quality, and a more modern codebase. 6 | 7 | Nautograf is designed for vector charts. This kind of data is not open to the public for all areas. Therefore Nautograf currently only supports _oesu_ charts from [o-charts.org](https://www.o-charts.org) where you can buy cheap charts for most areas. Support for S-57 charts, such as those provided by https://www.noaa.gov/ may be added in the future. 8 | 9 | ## Installing 10 | 11 | Releases are available from The Microsoft Store and the Snap Store. Snap Store provides binaries for amd64, armhf and arm64. 12 | 13 | [](https://apps.microsoft.com/detail/Nautograf/9NP97HF6LW08) 14 | [![Get it from the snap store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/nautograf) 15 | 16 | The version in the Snap Store only supports unencrypted charts. Binaries are also available as artifacts in the releases page at GitHub. 17 | 18 | ## Development status 19 | 20 | ![CI status badge](https://github.com/hornang/nautograf/actions/workflows/ci.yml/badge.svg) 21 | 22 | Nautograf is a spare time project so progress is varying and non-deterministic. It is likely that it will never come up to a point of being sufficient for navigation. The current feature set is limited and **not sufficient for nautical navigation**. 23 | 24 | Nonetheless, the current implementation can display the most prominent S-57 features such as land areas, depth areas and depth numbers. Chart navigation works by using conventional _zoom to cursor_ or pinch zoom on touch capable devices. 25 | 26 | ![Screenshot of chart](https://dashboard.snapcraft.io/site_media/appmedia/2024/05/apps.38393.13722934716828675.035bc3c5-f1f3-4e06-9b6a-e24c9982c4a1-1717097890.jpeg) 27 | 28 | Nautograf currently only supports _oesu_ charts from [o-charts.org](https://www.o-charts.org). Nautograf uses OpenCPN's decryption engine _oexserverd_ to decrypt the charts on the fly. You can read more about that in the [oesenc-export](https://github.com/hornang/oesenc-export) repository. 29 | 30 | ## Design 31 | 32 | Most online map viewers are based on [tiled web maps](https://en.wikipedia.org/wiki/Tiled_web_map). Tiling is an important feature to support smooth zooming and panning for the user. Online map viewers usually renders tiles that are already generated and hosted from a server. Nautograf is also tile based, but has to create those tiles internally because the chart data is provided by the user. 33 | 34 | Because tile creation can take some time it runs in multiple threads. Each tile being generated is put in a queue and waits for the next available thread. Generated tile data is cached to disk. This means that each time you navigate to a *new area* it will take some time before the area shows. Keeping this delay as low as possible is a key factor to improve usability of the application. 35 | 36 | ### Internal tile format 37 | 38 | Before tiles are rendered the source data format is transformed to a [Cap'n Proto](https://capnproto.org/) based format. The internal format is defined in a [schema file](src/tilefactory/chartdata.capnp). The schema defines C++ classes and serialization/deserialization code. This provides both a way of caching data to disk and also an internal data model used when rendering. On Windows you will find the cached tiles under `C:\Users\Username\AppData\Local\nautograf\cache\tiles`. There is no garbage collection so delete the data if it gets too large. 39 | 40 | ### Rendering 41 | 42 | At the start of the project rendered was done using [QPainter](https://doc.qt.io/qt-6/qpainter.html). This provided good quality, but was too slow for a proper pan/zoom experience. Therefore the render code is now based on [The Qt Quick Scene Graph](https://doc.qt.io/qt-6/qtquick-visualcanvas-scenegraph.html). This approach is very similar to the approach used by WebGL based map applications. 43 | 44 | ## Background 45 | 46 | Historically, electronic nautical charts have not received significant attention from open source or web technology communities. Nautical navigation, being a niche area compared to urban and road navigation, has led to a lag in technological advancements in nautical charting when compared to popular services like Google Maps and OpenStreetMap. 47 | 48 | Access to free nautical chart data remains limited, often relying on authorities' willingness to share map data with the public. This scarcity of freely available nautical data poses challenges for non-profit research and innovation within this area. 49 | 50 | Currently, the primary open source navigation tool for nautical maps is [OpenCPN](https://www.opencpn.org). To address the lack of readily available vector chart data, the creators of OpenCPN established the o-charts.org store in 2013. This store offers charts in OpenCPN's proprietary format, sourced from hydrographic offices. The o-charts incorporate end-user encryption that is decrypted within the closed-source component of OpenCPN's plugin, previously known as oesenc_pi and now as [o-charts_pi](https://github.com/bdbcat/o-charts_pi). 51 | -------------------------------------------------------------------------------- /src/tilefactory/catalog.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "tilefactory/catalog.h" 11 | 12 | using namespace std; 13 | 14 | namespace { 15 | 16 | vector listFilesWithExtension(string_view dir, string_view extension) 17 | { 18 | vector list; 19 | for (const filesystem::path &entry : std::filesystem::directory_iterator(dir)) { 20 | if (entry.extension() == extension) { 21 | list.push_back(entry); 22 | } 23 | } 24 | return list; 25 | } 26 | 27 | Catalog::Type detectCatalogType(oesenc::ServerControl *serverControl, string_view &dir) 28 | { 29 | if (!std::filesystem::is_directory(dir)) { 30 | return Catalog::Type::Invalid; 31 | } 32 | 33 | vector oesencFiles = listFilesWithExtension(dir, ".oesenc"); 34 | vector oesuFiles = listFilesWithExtension(dir, ".oesu"); 35 | vector bothFiles = oesencFiles; 36 | bothFiles.insert(bothFiles.begin(), oesuFiles.begin(), oesuFiles.end()); 37 | 38 | if (bothFiles.empty()) { 39 | return Catalog::Type::Invalid; 40 | } 41 | 42 | auto stream = std::ifstream(bothFiles.front(), std::ios::binary); 43 | oesenc::ChartFile chartFile(stream); 44 | if (chartFile.readHeaders()) { 45 | return Catalog::Type::Unencrypted; 46 | } 47 | 48 | if (serverControl == nullptr || !serverControl->isReady()) { 49 | return Catalog::Type::Invalid; 50 | } 51 | 52 | if (!oesencFiles.empty()) { 53 | auto key = oesenc::KeyListReader::readOesencKey(dir); 54 | auto stream = oesenc::ServerReader::openOesenc(serverControl->pipeName(), 55 | (dir / oesencFiles.front()).string(), 56 | key); 57 | oesenc::ChartFile chartFile(*stream); 58 | if (chartFile.readHeaders()) { 59 | return Catalog::Type::Oesenc; 60 | } 61 | } 62 | 63 | if (!oesuFiles.empty()) { 64 | auto keys = oesenc::KeyListReader::readOesuKeys(dir); 65 | std::string baseName = oesuFiles.front().stem().string(); 66 | if (keys.find(baseName) == keys.end()) { 67 | return Catalog::Type::Invalid; 68 | } 69 | auto stream = oesenc::ServerReader::openOesu(serverControl->pipeName(), 70 | (dir / oesuFiles.front()).string(), 71 | keys[baseName]); 72 | oesenc::ChartFile chartFile(*stream); 73 | if (chartFile.readHeaders()) { 74 | return Catalog::Type::Oesu; 75 | } 76 | } 77 | 78 | return Catalog::Type::Invalid; 79 | } 80 | } 81 | 82 | Catalog::Catalog(oesenc::ServerControl *serverControl, string_view dir) 83 | : m_dir(dir) 84 | , m_serverControl(serverControl) 85 | { 86 | m_type = detectCatalogType(m_serverControl, dir); 87 | 88 | if (m_type == Type::Invalid) { 89 | return; 90 | } 91 | 92 | if (serverControl != nullptr && serverControl->isReady()) { 93 | m_oesuKeys = oesenc::KeyListReader::readOesuKeys(dir); 94 | m_oesencKey = oesenc::KeyListReader::readOesencKey(dir); 95 | } 96 | } 97 | 98 | Catalog::Type Catalog::type() const 99 | { 100 | return m_type; 101 | } 102 | 103 | vector Catalog::chartFileNames() const 104 | { 105 | vector oesencFiles = listFilesWithExtension(m_dir.string(), ".oesenc"); 106 | vector oesuFiles = listFilesWithExtension(m_dir.string(), ".oesu"); 107 | 108 | vector bothFiles = oesencFiles; 109 | bothFiles.insert(bothFiles.begin(), oesuFiles.begin(), oesuFiles.end()); 110 | 111 | vector names; 112 | names.resize(bothFiles.size()); 113 | 114 | std::transform(bothFiles.cbegin(), bothFiles.cend(), names.begin(), [](const filesystem::path &path) { 115 | return path.filename().string(); 116 | }); 117 | return names; 118 | } 119 | 120 | std::shared_ptr Catalog::openChart(std::string_view fileName) 121 | { 122 | // The user of this class should ensure that there is only one std::istream 123 | // reading from the oexserverd process. 124 | // 125 | // The following expression asserts if that rule is broken 126 | assert(m_currentStream.use_count() <= 1); 127 | 128 | filesystem::path filePath = m_dir / std::string(fileName); 129 | string fileNameWithoutExtension = filePath.stem().string(); 130 | 131 | switch (m_type) { 132 | case Type::Oesu: 133 | assert(m_serverControl); 134 | if (m_oesuKeys.find(fileNameWithoutExtension) == m_oesuKeys.end()) { 135 | return nullptr; 136 | } 137 | m_currentStream = oesenc::ServerReader::openOesu(m_serverControl->pipeName(), 138 | filePath.string(), 139 | m_oesuKeys[fileNameWithoutExtension]); 140 | return m_currentStream; 141 | case Type::Oesenc: 142 | assert(m_serverControl); 143 | m_currentStream = oesenc::ServerReader::openOesenc(m_serverControl->pipeName(), filePath.string(), m_oesencKey); 144 | return m_currentStream; 145 | case Type::Unencrypted: 146 | return make_shared(filePath, std::ios::binary); 147 | default: 148 | return nullptr; 149 | } 150 | 151 | return nullptr; 152 | } 153 | -------------------------------------------------------------------------------- /src/usersettings.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "usersettings.h" 6 | 7 | const double maxLatFromSettings = 85.0; 8 | 9 | static const QString viewportKey = QStringLiteral("Viewport"); 10 | const QString UserSettings::showLegacyRendererKey = QStringLiteral("ShowLegacyRenderer"); 11 | 12 | UserSettings::UserSettings() 13 | { 14 | read(); 15 | } 16 | 17 | UserSettings::~UserSettings() 18 | { 19 | write(); 20 | } 21 | 22 | void UserSettings::read() 23 | { 24 | QSettings settings(orgName, appName); 25 | 26 | auto rectVariant = settings.value("Geometry"); 27 | if (settings.contains("Geometry")) { 28 | m_geometry = rectVariant.toRect(); 29 | emit geometryChanged(); 30 | } 31 | auto windowStateVariant = settings.value("WindowState"); 32 | bool ok; 33 | auto windowState = windowStateVariant.toInt(&ok); 34 | if (ok) { 35 | // Not really guaranteed to work if enum changes 36 | m_windowState = static_cast(windowState); 37 | if (m_windowState == QWindow::Hidden) { 38 | // Hidden not allowed 39 | m_windowState = QWindow::Windowed; 40 | } 41 | emit windowStateChanged(); 42 | } 43 | settings.beginGroup(viewportKey); 44 | 45 | QVariant latValue = settings.value("Latitude"); 46 | QVariant lonValue = settings.value("Longitude"); 47 | QVariant pixelsPerLongitudeValue = settings.value("PixelsPerLongitude"); 48 | 49 | if (latValue.isValid() && lonValue.isValid() && pixelsPerLongitudeValue.isValid()) { 50 | double lon = lonValue.toDouble(); 51 | double lat = latValue.toDouble(); 52 | double pixelsPerLongitude = pixelsPerLongitudeValue.toDouble(); 53 | 54 | if (std::isfinite(lon) && std::isfinite(lat) && std::isfinite(pixelsPerLongitude)) { 55 | lat = std::clamp(lat, -maxLatFromSettings, maxLatFromSettings); 56 | lon = std::clamp(lon, -180, 180); 57 | 58 | m_lat = lat; 59 | emit latChanged(); 60 | m_lon = lon; 61 | emit lonChanged(); 62 | m_pixelsPerLon = pixelsPerLongitude; 63 | emit pixelsPerLonChanged(); 64 | } else { 65 | qWarning() << "Invalid viewport" << lat << lon << pixelsPerLongitude; 66 | } 67 | } 68 | 69 | bool debugView = settings.value("DebugView").toBool(); 70 | if (m_debugView != debugView) { 71 | m_debugView = debugView; 72 | emit showLegacyDebugViewChanged(); 73 | } 74 | 75 | bool showLegacyRenderer = settings.value(showLegacyRendererKey).toBool(); 76 | if (m_showLegacyRenderer != showLegacyRenderer) { 77 | m_showLegacyRenderer = showLegacyRenderer; 78 | emit showLegacyRendererChanged(); 79 | } 80 | } 81 | 82 | void UserSettings::write() 83 | { 84 | QSettings settings(orgName, appName); 85 | if (m_windowState == QWindow::Windowed) { 86 | settings.setValue("Geometry", m_geometry); 87 | } 88 | settings.setValue("WindowState", m_windowState); 89 | settings.beginGroup(viewportKey); 90 | settings.setValue("Latitude", m_lat); 91 | settings.setValue("Longitude", m_lon); 92 | settings.setValue("PixelsPerLongitude", m_pixelsPerLon); 93 | settings.setValue("DebugView", m_debugView); 94 | settings.setValue(showLegacyRendererKey, m_showLegacyRenderer); 95 | } 96 | 97 | QWindow::Visibility UserSettings::windowState() const 98 | { 99 | return m_windowState; 100 | } 101 | 102 | void UserSettings::setWindowState(QWindow::Visibility newWindowState) 103 | { 104 | if (m_windowState == newWindowState || newWindowState == QWindow::Hidden) 105 | return; 106 | m_windowState = newWindowState; 107 | emit windowStateChanged(); 108 | } 109 | 110 | const QRect &UserSettings::geometry() const 111 | { 112 | return m_geometry; 113 | } 114 | 115 | void UserSettings::setGeometry(const QRect &newGeometry) 116 | { 117 | if (m_geometry == newGeometry) 118 | return; 119 | m_geometry = newGeometry; 120 | emit geometryChanged(); 121 | } 122 | 123 | bool UserSettings::showLegacyDebugView() const 124 | { 125 | return m_debugView; 126 | } 127 | 128 | void UserSettings::setShowLegacyDebugView(bool newShowLegacyDebugView) 129 | { 130 | if (m_debugView == newShowLegacyDebugView) 131 | return; 132 | m_debugView = newShowLegacyDebugView; 133 | emit showLegacyDebugViewChanged(); 134 | } 135 | 136 | bool UserSettings::showLegacyRenderer() const 137 | { 138 | return m_showLegacyRenderer; 139 | } 140 | 141 | void UserSettings::setLegacyRender(bool newLegacyRender) 142 | { 143 | if (m_showLegacyRenderer == newLegacyRender) 144 | return; 145 | m_showLegacyRenderer = newLegacyRender; 146 | emit showLegacyRendererChanged(); 147 | } 148 | 149 | double UserSettings::lat() const 150 | { 151 | return m_lat; 152 | } 153 | 154 | void UserSettings::setLat(const double &newLat) 155 | { 156 | if (m_lat == newLat) 157 | return; 158 | m_lat = newLat; 159 | emit latChanged(); 160 | } 161 | 162 | double UserSettings::lon() const 163 | { 164 | return m_lon; 165 | } 166 | 167 | void UserSettings::setLon(const double &newLon) 168 | { 169 | if (m_lon == newLon) 170 | return; 171 | m_lon = newLon; 172 | emit lonChanged(); 173 | } 174 | 175 | double UserSettings::pixelsPerLon() const 176 | { 177 | return m_pixelsPerLon; 178 | } 179 | 180 | void UserSettings::setPixelsPerLon(const double &newPixelsPerLon) 181 | { 182 | if (m_pixelsPerLon == newPixelsPerLon) 183 | return; 184 | m_pixelsPerLon = newPixelsPerLon; 185 | emit pixelsPerLonChanged(); 186 | } 187 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #ifdef USE_XDG_FILE_DIALOG 7 | #include 8 | #endif 9 | 10 | #include "chartmodel.h" 11 | #include "licenses.h" 12 | #include "maptile.h" 13 | #include "maptilemodel.h" 14 | #include "mercatorwrapper.h" 15 | #include "oesenc/chartfile.h" 16 | #include "scene/scene.h" 17 | #include "scene/tilefactorywrapper.h" 18 | #include "tilefactory/tilefactory.h" 19 | #include "tileinfobackend.h" 20 | #include "usersettings.h" 21 | #include "version.h" 22 | 23 | #ifndef QML_DIR 24 | #define QML_DIR "qrc:/qml" 25 | #endif 26 | 27 | #if defined(USE_OEXSERVERD) && defined(Q_OS_WIN) 28 | void addDirToPath(const QString &dir) 29 | { 30 | const char *pathVariable = "PATH"; 31 | QByteArray path = qgetenv(pathVariable); 32 | path += QByteArray(";") + dir.toLocal8Bit(); 33 | qputenv(pathVariable, path); 34 | } 35 | 36 | void addOexserverdToPath() 37 | { 38 | const auto appDataFolder = QStandardPaths::locate(QStandardPaths::HomeLocation, 39 | "AppData", 40 | QStandardPaths::LocateDirectory); 41 | if (appDataFolder.isEmpty()) { 42 | qWarning() << "Unable to find AppData folder"; 43 | return; 44 | } 45 | 46 | const auto oexserverdDir = QDir(appDataFolder).filePath("Local/opencpn/plugins"); 47 | addDirToPath(oexserverdDir); 48 | } 49 | #endif 50 | 51 | int main(int argc, char *argv[]) 52 | { 53 | QGuiApplication application(argc, argv); 54 | QCoreApplication::setApplicationName("Nautograf"); 55 | setlocale(LC_NUMERIC, "C"); 56 | 57 | const QString iconPath = ":/icon.ico"; 58 | application.setWindowIcon(QIcon(iconPath)); 59 | 60 | std::shared_ptr tileFactory = std::make_shared(); 61 | MapTileModel mapTileModel(tileFactory); 62 | 63 | #if defined(USE_OEXSERVERD) && defined(Q_OS_WIN) 64 | // oexserverd.exe must be in PATH for oesenc::ServerControl to find it. 65 | // See ChartModel:m_oesencServerControl 66 | addOexserverdToPath(); 67 | #endif 68 | 69 | ChartModel chartModel(tileFactory); 70 | 71 | tileFactory->setUpdateCallback([&]() { 72 | mapTileModel.scheduleUpdate(); 73 | }); 74 | 75 | tileFactory->setChartsChangedCb([&](const std::vector &rects) { 76 | mapTileModel.chartsChanged(rects); 77 | }); 78 | 79 | TileFactoryWrapper tileFactoryWrapper; 80 | tileFactoryWrapper.setTileDataCallback([&](GeoRect rect, double pixelsPerLongitude) -> std::vector> { 81 | return tileFactory->tileData(rect, pixelsPerLongitude); 82 | }); 83 | tileFactoryWrapper.setChartInfoCallback([&](GeoRect rect, double pixelsPerLongitude) -> std::vector { 84 | return tileFactory->chartInfo(rect, pixelsPerLongitude); 85 | }); 86 | 87 | tileFactory->setTileDataChangedCallback([&](const std::vector &tileIds) { 88 | tileFactoryWrapper.triggerTileDataChanged(tileIds); 89 | }); 90 | 91 | tileFactoryWrapper.setTileSettingsCb([&](const std::string &tileId, TileFactory::TileSettings tileSettings) { 92 | tileFactory->setTileSettings(tileId, tileSettings); 93 | }); 94 | 95 | UserSettings userSettings; 96 | 97 | qmlRegisterType("org.seatronomy.nautograf", 1, 0, "MapTile"); 98 | qmlRegisterType("org.seatronomy.nautograf", 1, 0, "Scene"); 99 | qmlRegisterType("org.seatronomy.nautograf", 1, 0, "TileInfoBackend"); 100 | qmlRegisterType("org.seatronomy.nautograf", 1, 0, "Licenses"); 101 | 102 | qmlRegisterSingletonInstance("org.seatronomy.nautograf", 1, 0, "MapTileModel", &mapTileModel); 103 | qmlRegisterSingletonInstance("org.seatronomy.nautograf", 1, 0, "TileFactory", &tileFactoryWrapper); 104 | qmlRegisterSingletonInstance("org.seatronomy.nautograf", 1, 0, "ChartModel", &chartModel); 105 | qmlRegisterSingletonInstance("org.seatronomy.nautograf", 1, 0, "UserSettings", &userSettings); 106 | 107 | MercatorWrapper mercatorWrapper; 108 | qmlRegisterSingletonInstance("org.seatronomy.nautograf", 1, 0, "Mercator", &mercatorWrapper); 109 | 110 | QQmlApplicationEngine engine; 111 | 112 | QString aboutFile = QStringLiteral(QML_DIR) + "/about.md"; 113 | if (aboutFile.startsWith("qrc")) { 114 | aboutFile = aboutFile.right(aboutFile.length() - 3); 115 | } 116 | 117 | QFile file(aboutFile); 118 | if (file.open(QIODevice::ReadOnly)) { 119 | auto aboutText = file.readAll(); 120 | engine.rootContext()->setContextProperty("AboutText", aboutText); 121 | file.close(); 122 | } 123 | engine.rootContext()->setContextProperty("AppVersion", QStringLiteral(NAUTOGRAF_VERSION)); 124 | 125 | #ifdef USE_OEXSERVERD 126 | constexpr bool canReadEncryptedCatalog = true; 127 | #else 128 | constexpr bool canReadEncryptedCatalog = false; 129 | #endif 130 | engine.rootContext()->setContextProperty("CanReadEncryptedCatalog", canReadEncryptedCatalog); 131 | 132 | #ifdef USE_XDG_FILE_DIALOG 133 | qmlRegisterType("org.seatronomy.nautograf", 1, 0, "XdgFileDialog"); 134 | constexpr bool useXdgFileDialog = true; 135 | #else 136 | constexpr bool useXdgFileDialog = false; 137 | #endif 138 | engine.rootContext()->setContextProperty("UseXdgFileDialog", useXdgFileDialog); 139 | 140 | #ifdef Q_OS_LINUX 141 | constexpr bool canUseIconTheme = true; 142 | #else 143 | constexpr bool canUseIconTheme = false; 144 | #endif 145 | engine.rootContext()->setContextProperty("CanUseIconTheme", canUseIconTheme); 146 | 147 | engine.load(QStringLiteral(QML_DIR) + "/main.qml"); 148 | 149 | int result = application.exec(); 150 | qDebug() << "Waiting for threads to finish..."; 151 | QThreadPool::globalInstance()->waitForDone(); 152 | return result; 153 | } 154 | -------------------------------------------------------------------------------- /src/maptilemodel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | // #include 5 | 6 | #include "maptile.h" 7 | #include "maptilemodel.h" 8 | #include "tilefactory/mercator.h" 9 | 10 | MapTileModel::MapTileModel(const std::shared_ptr &tileFactory) 11 | : m_tileFactory(tileFactory) 12 | { 13 | m_roleNames[Role::TileRefRole] = "tileRef"; 14 | m_roleNames[Role::TileIdRole] = "tileId"; 15 | m_roleNames[Role::TopLatitude] = "topLatitude"; 16 | m_roleNames[Role::LeftLongitude] = "leftLongitude"; 17 | m_roleNames[Role::BottomLatitude] = "bottomLatitude"; 18 | m_roleNames[Role::RightLongitude] = "rightLongitude"; 19 | update(); 20 | } 21 | 22 | void MapTileModel::setPanZoom(double lon, double lat, double pixelsPerLon) 23 | { 24 | Pos topLeft(lat, lon); 25 | 26 | if (m_center == topLeft && m_pixelsPerLon == pixelsPerLon) { 27 | return; 28 | } 29 | 30 | m_center = topLeft; 31 | m_pixelsPerLon = pixelsPerLon; 32 | 33 | update(); 34 | } 35 | 36 | void MapTileModel::chartsChanged(const std::vector &rects) 37 | { 38 | 39 | QList affectedTiles; 40 | 41 | int i = 0; 42 | for (const auto &tile : m_tiles) { 43 | for (const auto &rect : rects) { 44 | if (tile.boundingBox.intersects(rect)) { 45 | if (!affectedTiles.contains(i)) { 46 | affectedTiles.append(i); 47 | } 48 | } 49 | } 50 | i++; 51 | } 52 | 53 | for (const auto &affectedTile : affectedTiles) { 54 | emit dataChanged(createIndex(affectedTile, 0), createIndex(affectedTile, 0)); 55 | } 56 | } 57 | 58 | void MapTileModel::scheduleUpdate() 59 | { 60 | QMetaObject::invokeMethod(this, &MapTileModel::update, Qt::QueuedConnection); 61 | } 62 | 63 | void MapTileModel::update() 64 | { 65 | std::vector tiles; 66 | 67 | if (m_viewPort.isValid()) { 68 | tiles = m_tileFactory->tiles(m_center, m_pixelsPerLon, m_viewPort.width(), m_viewPort.height()); 69 | } 70 | 71 | QHash correct; 72 | 73 | for (const TileFactory::Tile &tile : tiles) { 74 | correct[tile.tileId] = tile; 75 | } 76 | 77 | for (int i = 0; i < m_tiles.length(); i++) { 78 | if (!correct.contains(m_tiles[i].tileId)) { 79 | beginRemoveRows(QModelIndex(), i, i); 80 | m_tiles.remove(i); 81 | endRemoveRows(); 82 | i--; 83 | } 84 | } 85 | 86 | for (const TileFactory::Tile &tile : m_tiles) { 87 | correct.remove(tile.tileId); 88 | } 89 | 90 | if (!correct.isEmpty()) { 91 | beginInsertRows(QModelIndex(), m_tiles.size(), m_tiles.size() + correct.size() - 1); 92 | QList keys = correct.keys(); 93 | for (const std::string &key : keys) { 94 | TileFactory::Tile tile = correct[key]; 95 | m_tiles.append(tile); 96 | } 97 | endInsertRows(); 98 | } 99 | emit countChanged(count()); 100 | } 101 | 102 | int MapTileModel::count() const 103 | { 104 | return m_tiles.size(); 105 | } 106 | 107 | void MapTileModel::setViewPort(const QSizeF &viewPort) 108 | { 109 | if (m_viewPort == viewPort) 110 | return; 111 | 112 | m_viewPort = viewPort; 113 | emit viewPortChanged(m_viewPort); 114 | update(); 115 | } 116 | 117 | int MapTileModel::rowCount(const QModelIndex &parent) const 118 | { 119 | if (!parent.isValid()) { 120 | return m_tiles.length(); 121 | } 122 | return 0; 123 | } 124 | 125 | QVariantMap MapTileModel::createTileRef(const QString &tileId, 126 | const GeoRect &boundingBox, 127 | int maxPixelsPerLon) 128 | { 129 | QVariantMap tileRef; 130 | tileRef["tileId"] = tileId; 131 | tileRef["topLatitude"] = boundingBox.top(); 132 | tileRef["leftLongitude"] = boundingBox.left(); 133 | tileRef["bottomLatitude"] = boundingBox.bottom(); 134 | tileRef["rightLongitude"] = boundingBox.right(); 135 | tileRef["maxPixelsPerLongitude"] = maxPixelsPerLon; 136 | return tileRef; 137 | } 138 | 139 | QVariant MapTileModel::data(const QModelIndex &index, int role) const 140 | { 141 | const int row = index.row(); 142 | 143 | if (row < 0 || row > m_tiles.size() - 1) { 144 | return QVariant(); 145 | } 146 | 147 | const TileFactory::Tile &tile = m_tiles.at(index.row()); 148 | 149 | switch (role) { 150 | case TileRefRole: { 151 | QVariantMap id; 152 | id["tileId"] = QString::fromStdString(tile.tileId); 153 | id["topLatitude"] = tile.boundingBox.top(); 154 | id["leftLongitude"] = tile.boundingBox.left(); 155 | id["bottomLatitude"] = tile.boundingBox.bottom(); 156 | id["rightLongitude"] = tile.boundingBox.right(); 157 | id["maxPixelsPerLongitude"] = tile.maxPixelsPerLon; 158 | return id; 159 | } break; 160 | case TileIdRole: 161 | return QString::fromStdString(tile.tileId); 162 | case TopLatitude: 163 | return tile.boundingBox.top(); 164 | case LeftLongitude: 165 | return tile.boundingBox.left(); 166 | case BottomLatitude: 167 | return tile.boundingBox.bottom(); 168 | case RightLongitude: 169 | return tile.boundingBox.right(); 170 | } 171 | 172 | return QVariant(); 173 | } 174 | 175 | QHash MapTileModel::roleNames() const 176 | { 177 | return m_roleNames; 178 | } 179 | 180 | QSizeF MapTileModel::viewPort() const 181 | { 182 | return m_viewPort; 183 | } 184 | 185 | QVariantMap MapTileModel::tileRefAtPos(float lat, float lon) 186 | { 187 | for (const TileFactory::Tile &tile : m_tiles) { 188 | if (tile.boundingBox.contains(lat, lon)) { 189 | return createTileRef(QString::fromStdString(tile.tileId), 190 | tile.boundingBox, 191 | tile.maxPixelsPerLon); 192 | } 193 | } 194 | return {}; 195 | } 196 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | 3 | include(cmake/GetVersionFromGit.cmake) 4 | get_version_from_git( 5 | SEMANTIC NAUTOGRAF_SEMVER 6 | DESCRIBE NAUTOGRAF_VERSION_DESCRIBE 7 | ) 8 | 9 | project(nautograf LANGUAGES CXX VERSION ${NAUTOGRAF_SEMVER}) 10 | 11 | include(cmake/InstallLicense.cmake) 12 | install_license(FILE ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE) 13 | 14 | configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.txt.in 15 | ${CMAKE_CURRENT_BINARY_DIR}/version.txt @ONLY) 16 | 17 | configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in 18 | ${CMAKE_CURRENT_BINARY_DIR}/version.h @ONLY) 19 | 20 | set(CMAKE_CXX_STANDARD 20) 21 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 22 | 23 | find_package(Qt6 24 | COMPONENTS 25 | REQUIRED Gui Core Network Concurrent Quick Svg QuickControls2 ShaderTools 26 | CMAKE_FIND_ROOT_PATH_BOTH 27 | ) 28 | 29 | find_program(IMAGEMAGICK 30 | NAMES magick convert 31 | ) 32 | 33 | find_program(RSVG-CONVERT 34 | NAMES rsvg-convert 35 | ) 36 | 37 | find_package(UnixCommands) 38 | 39 | if(CMAKE_SYSTEM_NAME STREQUAL "Linux") 40 | option(USE_XDG_FILE_DIALOG "Use xdg-file-dialog library when user selects chart catalog" OFF) 41 | endif() 42 | 43 | add_subdirectory(external) 44 | add_subdirectory(src/tilefactory) 45 | 46 | option(RESOURCE_DEV "Load QML from file system not from Qt resource" "") 47 | 48 | if(${RESOURCE_DEV}) 49 | set(SYMBOLS_DIR "${CMAKE_SOURCE_DIR}/symbols") 50 | else() 51 | qt_add_resources(QML_RESOURCES qml/qml.qrc) 52 | qt_add_resources(SYMBOL_RESOURCES symbols/symbols.qrc) 53 | set(SYMBOLS_DIR ":/symbols") 54 | endif() 55 | 56 | add_subdirectory(src/scene) 57 | 58 | if (NOT ${BASH} STREQUAL "BASH-NOTFOUND" 59 | AND NOT ${IMAGEMAGICK} STREQUAL "IMAGEMAGICK-NOTFOUND" 60 | AND NOT ${RSVG-CONVERT} STREQUAL "RSVG-CONVERT-NOTFOUND") 61 | configure_file( 62 | icon.rc.in 63 | ${CMAKE_BINARY_DIR}/icon.rc 64 | ) 65 | 66 | set(ICON_GENERATED TRUE) 67 | set(APP_ICON_RESOURCE_WINDOWS "${CMAKE_BINARY_DIR}/icon.rc") 68 | 69 | add_custom_command( 70 | OUTPUT ${CMAKE_BINARY_DIR}/icon.ico 71 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/logo.svg ${CMAKE_CURRENT_SOURCE_DIR}/create_icon.sh 72 | COMMAND ${BASH} "${CMAKE_CURRENT_SOURCE_DIR}/create_icon.sh" ${CMAKE_CURRENT_SOURCE_DIR}/logo.svg ${CMAKE_BINARY_DIR}/icon.ico "${IMAGEMAGICK}" 73 | WORKING_DIRECTORY ${CMAKE_BINARY_DIR} 74 | ) 75 | else() 76 | message(WARNING "Cannot create application icon. Missing bash or imagemagick") 77 | endif() 78 | 79 | add_executable(nautograf WIN32 80 | src/main.cpp 81 | src/licenses.h 82 | src/licenses.cpp 83 | src/mercatorwrapper.h 84 | src/maptile.h 85 | src/maptile.cpp 86 | src/maptilemodel.h 87 | src/maptilemodel.cpp 88 | src/chartmodel.cpp 89 | src/chartmodel.h 90 | src/tileinfobackend.cpp 91 | src/tileinfobackend.h 92 | src/usersettings.cpp 93 | src/usersettings.h 94 | ${QML_RESOURCES} 95 | ${SYMBOL_RESOURCES} 96 | ${APP_ICON_RESOURCE_WINDOWS} 97 | ) 98 | 99 | if(ICON_GENERATED) 100 | qt_add_resources(nautograf "icon" 101 | PREFIX "/" 102 | BASE ${CMAKE_BINARY_DIR} 103 | FILES ${CMAKE_BINARY_DIR}/icon.ico 104 | ) 105 | 106 | if(CMAKE_SYSTEM_NAME STREQUAL "Linux") 107 | install( 108 | FILES 109 | ${CMAKE_CURRENT_SOURCE_DIR}/logo.svg 110 | DESTINATION 111 | "share/icons/hicolor/scalable/apps/" 112 | RENAME 113 | ${CMAKE_PROJECT_NAME}.svg 114 | ) 115 | endif() 116 | endif() 117 | 118 | option(USE_OEXSERVERD "Use oexserverd to decrypt chart catalogs" ON) 119 | 120 | if(${USE_OEXSERVERD}) 121 | target_compile_definitions(nautograf PRIVATE USE_OEXSERVERD) 122 | endif() 123 | 124 | target_include_directories(nautograf PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/src) 125 | target_include_directories(nautograf PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) 126 | set_target_properties(nautograf PROPERTIES AUTOMOC TRUE) 127 | 128 | if(${RESOURCE_DEV}) 129 | target_compile_definitions(nautograf PRIVATE QML_DIR="${CMAKE_SOURCE_DIR}/qml") 130 | endif() 131 | 132 | target_include_directories(nautograf PRIVATE src) 133 | 134 | target_link_libraries(nautograf PRIVATE 135 | Qt6::Core 136 | Qt6::Quick 137 | Qt6::QuickControls2 138 | Qt6::Concurrent 139 | Qt6::Svg 140 | Qt::Gui 141 | tilefactory 142 | scene 143 | ) 144 | 145 | install( 146 | TARGETS nautograf scene tilefactory oesenc 147 | RUNTIME_DEPENDENCY_SET dependency_set 148 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Runtime 149 | ) 150 | 151 | if(${USE_XDG_FILE_DIALOG}) 152 | target_compile_definitions(nautograf PRIVATE USE_XDG_FILE_DIALOG) 153 | 154 | target_link_libraries(nautograf PRIVATE xdg-file-dialog) 155 | 156 | install( 157 | TARGETS xdg-file-dialog 158 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Runtime 159 | ) 160 | endif() 161 | 162 | install( 163 | IMPORTED_RUNTIME_ARTIFACTS nautograf 164 | RUNTIME_DEPENDENCY_SET dependency_set 165 | ) 166 | 167 | install( 168 | RUNTIME_DEPENDENCY_SET dependency_set 169 | PRE_INCLUDE_REGEXES "tinyxml2" "freetype" "bz2" "brotli" "libpng" "zlib" 170 | PRE_EXCLUDE_REGEXES ".*" 171 | 172 | # tinyxml exists as runtime dependency for more than one target. Exclude 173 | # one of them to avoid conflict during install. 174 | POST_EXCLUDE_REGEXES ".*/scene/.*tinyxml2.*" 175 | ) 176 | 177 | if(WIN32) 178 | find_program(WINDEPLOYQT windeployqt HINTS "${_qt_bin_dir}") 179 | 180 | configure_file( 181 | "${CMAKE_CURRENT_SOURCE_DIR}/AppxManifest.xml.in" 182 | "${CMAKE_CURRENT_BINARY_DIR}/AppxManifest.xml") 183 | 184 | configure_file( 185 | "${CMAKE_CURRENT_SOURCE_DIR}/deploy-qt-windows.cmake.in" 186 | "${CMAKE_CURRENT_BINARY_DIR}/deploy-qt-windows.cmake" 187 | @ONLY) 188 | 189 | 190 | set(CPACK_PRE_BUILD_SCRIPTS 191 | ${CMAKE_CURRENT_BINARY_DIR}/deploy-qt-windows.cmake 192 | ) 193 | 194 | install( 195 | FILES ${CMAKE_CURRENT_BINARY_DIR}/AppxManifest.xml 196 | DESTINATION . 197 | ) 198 | 199 | # Replace this if statement with a more explicit relation between the variable 200 | # beeing checked and the existence of the PNG files. 201 | if(ICON_GENERATED) 202 | install( 203 | FILES 204 | ${CMAKE_CURRENT_BINARY_DIR}/logo_44.png 205 | ${CMAKE_CURRENT_BINARY_DIR}/logo_150.png 206 | DESTINATION . 207 | ) 208 | endif() 209 | endif() 210 | 211 | set(CPACK_GENERATOR "ZIP") 212 | include(CPack) 213 | -------------------------------------------------------------------------------- /src/maptile.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "tilefactory/chart.h" 4 | #include "tilefactory/georect.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class TileFactoryWrapper; 12 | 13 | class MapTile : public QQuickPaintedItem 14 | { 15 | Q_OBJECT 16 | Q_PROPERTY(QVariantMap tileRef READ tileRef WRITE setTileRef NOTIFY tileRefChanged) 17 | Q_PROPERTY(TileFactoryWrapper *tileFactory READ tileFactory WRITE setTileFactory NOTIFY tileFactoryChanged) 18 | Q_PROPERTY(double lat READ lat WRITE setLat NOTIFY latChanged) 19 | Q_PROPERTY(double lon READ lon WRITE setLon NOTIFY lonChanged) 20 | Q_PROPERTY(double pixelsPerLon READ pixelsPerLon WRITE setPixelsPerLon NOTIFY pixelsPerLonChanged) 21 | Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) 22 | Q_PROPERTY(int margin READ margin CONSTANT) 23 | Q_PROPERTY(bool noData READ noData NOTIFY noDataChanged) 24 | Q_PROPERTY(bool error READ error NOTIFY errorChanged) 25 | Q_PROPERTY(QStringList charts READ charts NOTIFY chartsChanged) 26 | 27 | public: 28 | MapTile(QQuickItem *parent = 0); 29 | ~MapTile(); 30 | 31 | double lat() const; 32 | void setLat(double newLat); 33 | 34 | double lon() const; 35 | void setLon(double newLon); 36 | 37 | double pixelsPerLon() const; 38 | void setPixelsPerLon(double newPixelsPerLon); 39 | 40 | void paint(QPainter *painter); 41 | const QVariantMap &tileRef() const; 42 | void setTileRef(const QVariantMap &newTileId); 43 | TileFactoryWrapper *tileFactory() const; 44 | void setTileFactory(TileFactoryWrapper *newTileFactory); 45 | bool loading() const; 46 | int margin() const; 47 | QStringList charts() const; 48 | bool chartVisible(int index) const; 49 | bool noData() const; 50 | bool error() const; 51 | 52 | public slots: 53 | void setChartVisibility(const QString &name, bool visible); 54 | bool chartVisible(const QString &name) const; 55 | void renderFinished(); 56 | 57 | private: 58 | struct RenderConfig { 59 | /// Geodetic position of paint target's origin 60 | Pos topLeft; 61 | 62 | /// Ofset everything with pixels 63 | QPointF offset; 64 | 65 | qreal pixelsPerLongitude; 66 | QSizeF size; 67 | int scale = 0; 68 | QStringList hiddenCharts; 69 | }; 70 | 71 | void render(double lat, double lon, double pixelsPerLon); 72 | static std::vector> getChartData(TileFactoryWrapper *tileFactory, 73 | const GeoRect &boundingBox, 74 | int pixelsPerLongitude); 75 | /*! 76 | Renders the tile onto a QImage 77 | 78 | The tile is provided as a shared_ptr to ensure the life-time of the tile. 79 | */ 80 | static QPair>> renderTile(TileFactoryWrapper *tileFactory, 81 | RenderConfig renderConfig, 82 | const GeoRect &boundingBox, 83 | int maxPixelsPerLongitude); 84 | static void paintLandArea(const ::capnp::List::Reader &areas, 85 | const RenderConfig &renderConfig, 86 | QPainter *painter); 87 | 88 | static void paintDepthAreas(const ::capnp::List::Reader &depthAreas, 89 | const RenderConfig &renderConfig, 90 | QPainter *painter); 91 | 92 | static void paintBuiltUpArea(const ::capnp::List::Reader &areas, 93 | const RenderConfig &renderConfig, 94 | QPainter *painter); 95 | 96 | static void clipAroundCoverage(const capnp::List::Reader &areas, 97 | const RenderConfig &renderConfig, 98 | QPainter *painter); 99 | 100 | static inline QPointF toMercator(const Pos &topLeft, 101 | qreal pixelsPerLongitude, 102 | const Pos &position); 103 | 104 | static inline QPointF toMercator(const Pos &topLeft, 105 | qreal pixelsPerLongitude, 106 | const double &lat, 107 | const double &lon); 108 | 109 | static void paintRoads(const capnp::List::Reader &roads, 110 | const RenderConfig &renderConfig, 111 | QPainter *painter); 112 | 113 | static QPolygonF fromCapnpToPolygon(const capnp::List::Reader src, 114 | const RenderConfig &renderConfig); 115 | 116 | static QPolygonF reversePolygon(const QPolygonF &input); 117 | static QSizeF sizeFromPixelsPerLon(const GeoRect &boundingBox, double pixelsPerLon); 118 | static void rasterClip(const QRectF &outer, const QRectF &inner, QPainter *painter); 119 | static QColor depthColor(qreal depth); 120 | 121 | void updateGeometry(); 122 | 123 | QImage m_image; 124 | GeoRect m_boundingBox; 125 | QString m_tileId; 126 | QFuture>>> m_renderResult; 127 | QFutureWatcher>>> m_renderResultWatcher; 128 | QSize m_imageSize; 129 | QVariantMap m_tileRef; 130 | 131 | double m_lat = 0; 132 | double m_lon = 0; 133 | double m_pixelsPerLon = 300; 134 | 135 | double m_updatedLat = 0; 136 | double m_updatedLon = 0; 137 | double m_updatedPixelsPerLon = 0; 138 | 139 | TileFactoryWrapper *m_tileFactory = nullptr; 140 | QStringList m_charts; 141 | 142 | QStringList m_hiddenCharts; 143 | 144 | QList m_chartVisibility; 145 | std::vector> m_chartDatas; 146 | bool m_loading = false; 147 | bool m_pendingRender = false; 148 | int m_maxPixelsPerLongitude = 3000; 149 | bool m_noData = false; 150 | bool m_error = false; 151 | 152 | signals: 153 | void chartsChanged(); 154 | void latChanged(); 155 | void lonChanged(); 156 | void pixelsPerLonChanged(); 157 | void tileRefChanged(); 158 | void tileFactoryChanged(TileFactoryWrapper *tileFactory); 159 | void loadingChanged(bool loading); 160 | void marginChanged(); 161 | void symbolsChanged(); 162 | void noDataChanged(bool noData); 163 | void errorChanged(); 164 | }; 165 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build packages 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | schedule: 13 | - cron: '30 10 * * 0' 14 | 15 | permissions: 16 | contents: read 17 | pull-requests: read 18 | 19 | jobs: 20 | commitlint: 21 | runs-on: ubuntu-latest 22 | name: 'Lint commit messages' 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: wagoid/commitlint-github-action@v6 26 | 27 | windows: 28 | name: 'Windows' 29 | outputs: 30 | package-name: ${{ steps.install-to-directory.outputs.package-name }} 31 | runs-on: windows-latest 32 | env: 33 | SYSTEM_NAME: win64 34 | 35 | steps: 36 | - name: Check out repository 37 | uses: actions/checkout@v3 38 | with: 39 | submodules: true 40 | fetch-depth: 0 41 | 42 | - name: Install Qt 43 | uses: jurplel/install-qt-action@v3 44 | with: 45 | version: '6.7.2' 46 | cache: true 47 | arch: 'win64_msvc2019_64' 48 | modules: "qtshadertools" 49 | 50 | - uses: lukka/get-cmake@latest 51 | with: 52 | cmakeVersion: "3.29.0" 53 | 54 | # To ensure the right link.exe is found by cargo 55 | - uses: TheMrMilchmann/setup-msvc-dev@v3 56 | with: 57 | arch: x64 58 | 59 | - name: Restore artifacts, or setup vcpkg (do not install any package) 60 | uses: lukka/run-vcpkg@v11 61 | 62 | - name: Install choco dependencies 63 | uses: crazy-max/ghaction-chocolatey@v1 64 | with: 65 | args: install chocolatey.config -y --no-progress 66 | 67 | - name: Run CMake with vcpkg.json manifest 68 | uses: lukka/run-cmake@v10 69 | with: 70 | configurePreset: 'ninja-release-vcpkg' 71 | buildPreset: 'release' 72 | 73 | - name: Install to directory 74 | id: install-to-directory 75 | shell: bash 76 | run: > 77 | cd "${{ github.workspace }}/builds/ninja-release-vcpkg"; 78 | PACKAGE_NAME=nautograf_$(cat version.txt)_$SYSTEM_NAME; 79 | echo "package-name=$PACKAGE_NAME" >> $GITHUB_OUTPUT; 80 | cmake --install . --prefix "$(pwd)/install/$PACKAGE_NAME"; 81 | rm -r "$(pwd)/install/$PACKAGE_NAME/lib" "$(pwd)/install/$PACKAGE_NAME/include" 82 | 83 | - name: Deploy Qt libraries to install dir 84 | shell: bash 85 | run: > 86 | cmake -DEXECUTABLE_DIR="${{ github.workspace }}/builds/ninja-release-vcpkg/install/${{ steps.install-to-directory.outputs.package-name }}/bin" 87 | -P "${{ github.workspace }}/builds/ninja-release-vcpkg/deploy-qt-windows.cmake" 88 | 89 | - name: Upload package 90 | uses: actions/upload-artifact@v4 91 | with: 92 | name: ${{ steps.install-to-directory.outputs.package-name }} 93 | path: ${{ github.workspace }}/builds/ninja-release-vcpkg/install 94 | 95 | ubuntu-22-04: 96 | name: 'Ubuntu 22.04' 97 | outputs: 98 | package-name: ${{ steps.install-to-directory.outputs.package-name }} 99 | runs-on: ubuntu-22.04 100 | env: 101 | SYSTEM_NAME: ubuntu-22.04 102 | steps: 103 | - name: Check out repository 104 | uses: actions/checkout@v3 105 | with: 106 | submodules: true 107 | fetch-depth: 0 108 | 109 | - name: Install Qt 110 | uses: jurplel/install-qt-action@v3 111 | with: 112 | version: '6.7.2' 113 | cache: 'true' 114 | modules: 'qtshadertools' 115 | 116 | - uses: lukka/get-cmake@latest 117 | with: 118 | cmakeVersion: "3.29.0" 119 | 120 | - name: Install dependencies 121 | run: > 122 | sudo apt-get install 123 | libglu1-mesa-dev 124 | libxmu-dev 125 | libxi-dev 126 | libgl-dev 127 | imagemagick 128 | librsvg2-bin 129 | libpthread-stubs0-dev 130 | 131 | - name: Restore artifacts, or setup vcpkg (do not install any package) 132 | uses: lukka/run-vcpkg@v11 133 | 134 | - name: Run CMake with vcpkg.json manifest 135 | uses: lukka/run-cmake@v10 136 | with: 137 | configurePreset: 'ninja-release-vcpkg-static' 138 | buildPreset: 'static-release' 139 | 140 | - name: Install to directory 141 | id: install-to-directory 142 | shell: bash 143 | run: > 144 | cd "${{ github.workspace }}/builds/ninja-release-vcpkg-static"; 145 | PACKAGE_NAME=nautograf_$(cat version.txt)_$SYSTEM_NAME; 146 | echo "package-name=$PACKAGE_NAME" >> $GITHUB_OUTPUT; 147 | cmake --install . --prefix "$(pwd)/install/$PACKAGE_NAME"; 148 | rm -r "$(pwd)/install/$PACKAGE_NAME/lib" "$(pwd)/install/$PACKAGE_NAME/include" 149 | 150 | - name: Upload package 151 | uses: actions/upload-artifact@v4 152 | with: 153 | name: ${{ steps.install-to-directory.outputs.package-name }} 154 | path: ${{ github.workspace }}/builds/ninja-release-vcpkg-static/install 155 | 156 | ubuntu-snap: 157 | name: 'Snapcraft core22' 158 | outputs: 159 | package-name: ${{ steps.snapcraft.outputs.snap }} 160 | runs-on: ubuntu-latest 161 | steps: 162 | - name: Check out Git repository 163 | uses: actions/checkout@v3 164 | with: 165 | submodules: true 166 | fetch-depth: 0 167 | 168 | - name: Build snap 169 | id: snapcraft 170 | uses: snapcore/action-build@v1 171 | with: 172 | snapcraft-args: '-v' 173 | 174 | - uses: actions/upload-artifact@v4 175 | with: 176 | name: ${{ steps.snapcraft.outputs.snap }} 177 | path: ${{ github.workspace }}/${{ steps.snapcraft.outputs.snap }} 178 | 179 | create-release: 180 | if: startsWith(github.event.ref, 'refs/tags/v') 181 | name: 'Draft release' 182 | needs: [windows, ubuntu-22-04, ubuntu-snap] 183 | runs-on: ubuntu-latest 184 | permissions: 185 | write-all 186 | steps: 187 | - uses: actions/download-artifact@v4 188 | with: 189 | name: ${{ needs.windows.outputs.package-name }} 190 | 191 | - uses: actions/download-artifact@v4 192 | with: 193 | name: ${{ needs.ubuntu-22-04.outputs.package-name }} 194 | 195 | - uses: actions/download-artifact@v4 196 | with: 197 | name: ${{ needs.ubuntu-snap.outputs.package-name }} 198 | 199 | - name: zip ubuntu and win64 artifacts 200 | run: > 201 | zip -r ${{ needs.windows.outputs.package-name }}.zip ${{ needs.windows.outputs.package-name }}; 202 | zip -r ${{ needs.ubuntu-22-04.outputs.package-name }}.zip ${{ needs.ubuntu-22-04.outputs.package-name }} 203 | 204 | - name: Draft release 205 | uses: ncipollo/release-action@v1.13.0 206 | with: 207 | artifacts: '*.zip,*.snap' 208 | draft: true 209 | -------------------------------------------------------------------------------- /src/tilefactory/oesenctilesource.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "filehelper.h" 11 | #include "oesenc/serverreader.h" 12 | #include "tilefactory/catalog.h" 13 | #include "tilefactory/chartclipper.h" 14 | #include "tilefactory/mercator.h" 15 | #include "tilefactory/oesenctilesource.h" 16 | 17 | using namespace std; 18 | 19 | namespace { 20 | constexpr int clippingMarginInPixels = 6; 21 | mutex catalogueMutex; 22 | } 23 | 24 | OesencTileSource::OesencTileSource(Catalog *catalogue, string_view name, 25 | string_view baseTileDir) 26 | : m_name(name) 27 | , m_tileDir(FileHelper::getTileDir(string(baseTileDir), Chart::typeId())) 28 | , m_catalogue(catalogue) 29 | { 30 | lock_guard guard(catalogueMutex); 31 | auto stream = m_catalogue->openChart(name); 32 | oesenc::ChartFile chart = oesenc::ChartFile(*stream); 33 | 34 | if (chart.readHeaders()) { 35 | readOesencMetaData(&chart); 36 | m_valid = true; 37 | } 38 | } 39 | 40 | void OesencTileSource::readOesencMetaData(const oesenc::ChartFile *chart) 41 | { 42 | assert(chart); 43 | m_scale = chart->nativeScale(); 44 | m_extent = fromOesencRect(chart->extent()); 45 | } 46 | 47 | GeoRect OesencTileSource::fromOesencRect(const oesenc::Rect &src) 48 | { 49 | return GeoRect(src.top(), src.bottom(), src.left(), src.right()); 50 | } 51 | 52 | bool OesencTileSource::convertChartToInternalFormat(float lineEpsilon, int pixelsPerLon) 53 | { 54 | std::string decimatedFileName = FileHelper::internalChartFileName(m_tileDir, 55 | m_name, 56 | pixelsPerLon); 57 | 58 | const lock_guard lock(m_internalChartMutex); 59 | 60 | if (filesystem::exists(decimatedFileName)) { 61 | return true; 62 | } 63 | 64 | std::unique_ptr capnpMessage; 65 | 66 | using Line = vector; 67 | 68 | oesenc::ChartFile::Config oesencConfig; 69 | oesencConfig.vectorEdgeDecimator = [=](const Line &line) -> Line { 70 | if (line.size() < 2) { 71 | return line; 72 | } 73 | 74 | ::rust::Vec input; 75 | input.reserve(line.size()); 76 | for (const oesenc::Position &pos : line) { 77 | input.push_back({ pos.latitude(), pos.longitude() }); 78 | } 79 | 80 | ::rust::Vec simplified = tilefactory_rust::simplify(input, lineEpsilon); 81 | 82 | for (const oesenc::Position &pos : line) { 83 | input.push_back({ pos.latitude(), pos.longitude() }); 84 | } 85 | 86 | Line simplifiedLine; 87 | simplifiedLine.reserve(simplified.size()); 88 | 89 | for (const tilefactory_rust::Pos &pos : simplified) { 90 | simplifiedLine.push_back({ pos.x, pos.y }); 91 | } 92 | 93 | return simplifiedLine; 94 | }; 95 | 96 | lock_guard guard(catalogueMutex); 97 | shared_ptr stream = m_catalogue->openChart(m_name); 98 | unique_ptr oesencChart = make_unique(*stream, oesencConfig); 99 | 100 | if (!oesencChart->read()) { 101 | return false; 102 | } 103 | 104 | readOesencMetaData(oesencChart.get()); 105 | capnpMessage = Chart::buildFromS57(oesencChart->s57(), m_extent, m_name, m_scale); 106 | 107 | filesystem::path targetPath = decimatedFileName; 108 | 109 | if (!filesystem::exists(targetPath.parent_path())) { 110 | error_code errorCode; 111 | if (!filesystem::create_directory(targetPath.parent_path(), errorCode)) { 112 | cerr << "Failed to create dir " << targetPath.parent_path() << endl; 113 | } 114 | } 115 | 116 | if (!Chart::write(capnpMessage.get(), decimatedFileName)) { 117 | return false; 118 | } 119 | 120 | return true; 121 | } 122 | 123 | bool OesencTileSource::isValid() const 124 | { 125 | return m_valid; 126 | } 127 | 128 | OesencTileSource::~OesencTileSource() 129 | { 130 | } 131 | 132 | GeoRect OesencTileSource::extent() const 133 | { 134 | return m_extent; 135 | } 136 | 137 | shared_ptr OesencTileSource::create(const GeoRect &boundingBox, 138 | int pixelsPerLongitude) 139 | { 140 | const string id = FileHelper::tileId(boundingBox, pixelsPerLongitude); 141 | 142 | shared_ptr tileMutex; 143 | { 144 | lock_guard guard(m_tileMutexesMutex); 145 | auto it = m_tileMutexes.find(id); 146 | if (it == m_tileMutexes.end()) { 147 | m_tileMutexes[id] = make_shared(); 148 | } 149 | tileMutex = m_tileMutexes[id]; 150 | } 151 | 152 | lock_guard tileGuard(*tileMutex.get()); 153 | string tilefile = FileHelper::tileFileName(m_tileDir, m_name, id); 154 | 155 | if (filesystem::exists(tilefile)) { 156 | shared_ptr tile = Chart::open(tilefile); 157 | 158 | if (!tile) { 159 | cerr << "Failed to create chart from: " << tilefile << endl; 160 | return {}; 161 | } 162 | 163 | lock_guard guard(m_tileMutexesMutex); 164 | m_tileMutexes.erase(id); 165 | return tile; 166 | } 167 | 168 | auto tile = generateTile(boundingBox, pixelsPerLongitude); 169 | lock_guard guard(m_tileMutexesMutex); 170 | m_tileMutexes.erase(id); 171 | return tile; 172 | } 173 | 174 | shared_ptr OesencTileSource::generateTile(const GeoRect &boundingBox, 175 | int pixelsPerLongitude) 176 | { 177 | double longitudeMargin = Mercator::mercatorWidthInverse(boundingBox.left(), 178 | clippingMarginInPixels, 179 | pixelsPerLongitude) 180 | - boundingBox.left(); 181 | double latitudeMargin = boundingBox.top() 182 | - Mercator::mercatorHeightInverse(boundingBox.top(), 183 | clippingMarginInPixels, 184 | pixelsPerLongitude); 185 | 186 | double latitudeResolution = boundingBox.top() 187 | - Mercator::mercatorHeightInverse(boundingBox.top(), 188 | 2, 189 | pixelsPerLongitude); 190 | double longitudeResolution = Mercator::mercatorWidthInverse(boundingBox.left(), 191 | 2, 192 | pixelsPerLongitude) 193 | - boundingBox.left(); 194 | 195 | string internalChartFileName = FileHelper::internalChartFileName(m_tileDir, 196 | m_name, 197 | pixelsPerLongitude); 198 | 199 | if (!filesystem::exists(internalChartFileName)) { 200 | float epsilon = 2 * min(longitudeResolution, latitudeResolution); 201 | if (!convertChartToInternalFormat(epsilon, pixelsPerLongitude)) { 202 | cerr << "Failed to convert chart to internal format" << endl; 203 | return {}; 204 | } 205 | } 206 | 207 | shared_ptr entireChart = Chart::open(internalChartFileName); 208 | 209 | if (!entireChart) { 210 | cerr << "Failed to open " << internalChartFileName << endl; 211 | return {}; 212 | } 213 | 214 | assert(entireChart->nativeScale() != 0); 215 | 216 | string id = FileHelper::tileId(boundingBox, pixelsPerLongitude); 217 | string tileFile = FileHelper::tileFileName(m_tileDir, m_name, id); 218 | 219 | ChartClipper::Config clipConfig; 220 | clipConfig.box = boundingBox; 221 | clipConfig.latitudeMargin = latitudeMargin; 222 | clipConfig.longitudeMargin = longitudeMargin; 223 | clipConfig.longitudeResolution = longitudeResolution; 224 | clipConfig.latitudeResolution = latitudeResolution; 225 | clipConfig.maxPixelsPerLongitude = pixelsPerLongitude; 226 | 227 | unique_ptr clippedChart = entireChart->buildClipped(clipConfig); 228 | 229 | assert(clippedChart); 230 | 231 | if (!Chart::write(clippedChart.get(), tileFile)) { 232 | cerr << "Failed to write " << tileFile << endl; 233 | return {}; 234 | } 235 | 236 | return Chart::open(tileFile); 237 | } 238 | -------------------------------------------------------------------------------- /src/tilefactory/chartclipper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tilefactory/chartclipper.h" 4 | #include "tilefactory/georect.h" 5 | #include "tilefactory/pos.h" 6 | 7 | Clipper2Lib::Path64 ChartClipper::toClipperPath(const capnp::List::Reader &positions, 8 | const GeoRect &roi, double xRes, double yRes) 9 | { 10 | Clipper2Lib::Path64 path; 11 | Clipper2Lib::Point64 prevPoint; 12 | 13 | for (const auto &p : positions) { 14 | Pos pos(p.getLatitude(), p.getLongitude()); 15 | Clipper2Lib::Point64 point = toIntPoint(pos, roi, xRes, yRes); 16 | if (point == prevPoint) { 17 | continue; 18 | } 19 | path.push_back(point); 20 | } 21 | 22 | return path; 23 | } 24 | 25 | ChartClipper::Line ChartClipper::toLine(const Clipper2Lib::Path64 &path, 26 | const GeoRect &roi, double xRes, double yRes) 27 | { 28 | ChartClipper::Line polygon; 29 | 30 | for (const Clipper2Lib::Point64 &intPoint : path) { 31 | Pos p = fromIntPoint(intPoint, roi, xRes, yRes); 32 | polygon.push_back(Pos(p.lat(), p.lon())); 33 | } 34 | 35 | return polygon; 36 | } 37 | 38 | std::vector ChartClipper::clipPolygon(const ChartData::Polygon::Reader &polygon, 39 | Config clipConfig) 40 | { 41 | assert(clipConfig.longitudeResolution > 0); 42 | assert(clipConfig.latitudeResolution > 0); 43 | assert(!clipConfig.box.isNull()); 44 | assert(!clipConfig.chartBoundingBox.isNull()); 45 | 46 | const GeoRect &boundingBox = clipConfig.box; 47 | GeoRect clipRect(boundingBox.top() + clipConfig.latitudeMargin, 48 | boundingBox.bottom() - clipConfig.latitudeMargin, 49 | boundingBox.left() - clipConfig.longitudeMargin, 50 | boundingBox.right() + clipConfig.longitudeMargin); 51 | 52 | int xRes = (clipConfig.box.right() - clipConfig.box.left()) / clipConfig.longitudeResolution; 53 | int yRes = (clipConfig.box.top() - clipConfig.box.bottom()) / clipConfig.latitudeResolution; 54 | 55 | // Add a fudge factor here to increase resolution 56 | xRes *= 2; 57 | yRes *= 2; 58 | 59 | Clipper2Lib::Paths64 paths; 60 | 61 | GeoRect geoRect(clipRect.top(), clipRect.bottom(), clipRect.left(), clipRect.right()); 62 | 63 | paths.push_back(toClipperPath(polygon.getMain(), clipRect, xRes, yRes)); 64 | 65 | Clipper2Lib::Path64 clipPath; 66 | clipPath.push_back(Clipper2Lib::Point64(0, 0)); 67 | clipPath.push_back(Clipper2Lib::Point64(0, yRes)); 68 | clipPath.push_back(Clipper2Lib::Point64(xRes, yRes)); 69 | clipPath.push_back(Clipper2Lib::Point64(xRes, 0)); 70 | 71 | Clipper2Lib::Paths64 clipPaths; 72 | clipPaths.push_back(clipPath); 73 | Clipper2Lib::Paths64 solution = Clipper2Lib::Intersect(paths, 74 | clipPaths, 75 | Clipper2Lib::FillRule::EvenOdd); 76 | std::vector output; 77 | 78 | if (solution.empty()) { 79 | return {}; 80 | } 81 | 82 | Clipper2Lib::Paths64 holePaths; 83 | 84 | for (const capnp::List::Reader &hole : polygon.getHoles()) { 85 | holePaths.push_back(toClipperPath(hole, clipRect, xRes, yRes)); 86 | } 87 | 88 | for (const Clipper2Lib::Path64 &mainAreas : solution) { 89 | Polygon area; 90 | auto polygon = toLine(mainAreas, geoRect, xRes, yRes); 91 | 92 | if (clipConfig.inflateAtChartEdges) { 93 | polygon = inflateAtChartEdges(polygon, clipConfig); 94 | } 95 | 96 | area.main = polygon; 97 | 98 | if (holePaths.empty()) { 99 | output.push_back(area); 100 | continue; 101 | } 102 | 103 | Clipper2Lib::Paths64 holeResults = Clipper2Lib::Intersect(holePaths, 104 | { mainAreas }, 105 | Clipper2Lib::FillRule::EvenOdd); 106 | 107 | // The holes should already be cut so that they do not intersect with 108 | // the main contour. However, some investigation revealed that the 109 | // triangulation algorithm in the scene library failed to triangulate 110 | // in rare occasions, likely because it does not support holes that 111 | // intersect or touch the parent contour. 112 | // 113 | // To address this issue, the following statement "deflates" all holes 114 | // by one unit to ensure a minimum space from the parent area or contour. 115 | static int holeDeflateAmount = -1; 116 | holeResults = Clipper2Lib::InflatePaths(holeResults, 117 | holeDeflateAmount, 118 | Clipper2Lib::JoinType::Round, 119 | Clipper2Lib::EndType::Polygon); 120 | 121 | for (const Clipper2Lib::Path64 &path : holeResults) { 122 | area.holes.push_back(toLine(path, geoRect, xRes, yRes)); 123 | } 124 | output.push_back(area); 125 | } 126 | return output; 127 | } 128 | 129 | inline Clipper2Lib::Point64 ChartClipper::toIntPoint(const Pos &pos, const GeoRect &roi, double xRes, double yRes) 130 | { 131 | int x = (pos.lon() - roi.left()) / (roi.right() - roi.left()) * xRes; 132 | int y = (pos.lat() - roi.bottom()) / (roi.top() - roi.bottom()) * yRes; 133 | return Clipper2Lib::Point64(x, y); 134 | } 135 | 136 | inline Pos ChartClipper::fromIntPoint(const Clipper2Lib::Point64 &intPoint, const GeoRect &roi, double xRes, double yRes) 137 | { 138 | double lon = static_cast(intPoint.x) / xRes * (roi.right() - roi.left()) + roi.left(); 139 | double lat = static_cast(intPoint.y) / yRes * (roi.top() - roi.bottom()) + roi.bottom(); 140 | return Pos(lat, lon); 141 | } 142 | 143 | ChartClipper::Line ChartClipper::inflateAtChartEdges(const Line &line, Config clipConfig) 144 | { 145 | Pos previousPosition = line.back(); 146 | int longitudeState = inRange(previousPosition.lon(), 147 | clipConfig.chartBoundingBox.left(), 148 | clipConfig.chartBoundingBox.right(), 149 | clipConfig.longitudeResolution); 150 | int latitudeState = inRange(previousPosition.lat(), 151 | clipConfig.chartBoundingBox.bottom(), 152 | clipConfig.chartBoundingBox.top(), 153 | clipConfig.latitudeResolution); 154 | bool outside = (longitudeState != 0) || (latitudeState != 0); 155 | 156 | Line output; 157 | 158 | for (Pos position : line) { 159 | Pos newPosition = position; 160 | 161 | int newLongitudeState = inRange(position.lon(), 162 | clipConfig.chartBoundingBox.left(), 163 | clipConfig.chartBoundingBox.right(), 164 | clipConfig.longitudeResolution); 165 | 166 | int newLatitudeState = inRange(position.lat(), 167 | clipConfig.chartBoundingBox.bottom(), 168 | clipConfig.chartBoundingBox.top(), 169 | clipConfig.latitudeResolution); 170 | 171 | bool newOutside = (newLongitudeState != 0) || (newLatitudeState != 0); 172 | 173 | if (newOutside && !outside) { 174 | output.push_back(position); 175 | } 176 | 177 | if (newLongitudeState == 1) { 178 | newPosition.setLon(clipConfig.chartBoundingBox.right() + 2 * clipConfig.longitudeMargin); 179 | } else if (newLongitudeState == -1) { 180 | newPosition.setLon(clipConfig.chartBoundingBox.left() - 2 * clipConfig.longitudeMargin); 181 | } 182 | 183 | if (newLatitudeState == 1) { 184 | newPosition.setLat(clipConfig.chartBoundingBox.top() + 2 * clipConfig.latitudeMargin); 185 | } else if (newLatitudeState == -1) { 186 | newPosition.setLat(clipConfig.chartBoundingBox.bottom() - 2 * clipConfig.latitudeMargin); 187 | } 188 | 189 | if (!newOutside && outside) { 190 | output.push_back(previousPosition); 191 | } 192 | outside = newOutside; 193 | 194 | output.push_back(newPosition); 195 | previousPosition = position; 196 | } 197 | return output; 198 | } 199 | 200 | int ChartClipper::inRange(double value, double min, double max, double margin) 201 | { 202 | if (value <= min + margin) { 203 | return -1; 204 | } else if (value >= max - margin) { 205 | return 1; 206 | } else { 207 | return 0; 208 | } 209 | } 210 | --------------------------------------------------------------------------------