├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake └── tiledwebmapsConfig.cmake ├── images └── map.jpg ├── include └── tiledwebmaps │ ├── affine.h │ ├── affine │ ├── named_axes.h │ ├── rigid.h │ ├── rotation.h │ └── scaled_rigid.h │ ├── bin.h │ ├── cache.h │ ├── disk.h │ ├── geo.h │ ├── http.h │ ├── layout.h │ ├── lru.h │ ├── proj.h │ ├── python.h │ ├── tiledwebmaps.h │ └── tileloader.h ├── python ├── CMakeLists.txt ├── backend │ ├── CMakeLists.txt │ └── main.cpp ├── build_wheel │ ├── build_all.sh │ └── build_in_docker.sh ├── copy_proj_data.py ├── scripts │ ├── download_massgis.py │ ├── download_nconemap.py │ ├── download_openbb.py │ ├── download_opendc.py │ ├── download_opennrw.py │ ├── download_opensaxony.py │ └── to_bin.py ├── setup.cfg ├── setup.py ├── test │ └── test_tiledwebmaps.py └── tiledwebmaps │ ├── __init__.py │ ├── geo.py │ ├── presets.py │ └── util.py └── test ├── CMakeLists.txt └── tiledwebmaps.cpp /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.2.0] 4 | 5 | Large refactor of the library. 6 | 7 | ### Added 8 | 9 | - Added option for storing layout in ``layout.yaml`. 10 | - Added scripts for bulk downloading imagery. 11 | - Added ``min_zoom`` and ``max_zoom`` property to all tileloaders. 12 | 13 | ### Changed 14 | 15 | - Removed cosy1 dependency. 16 | - Improved tile loading speed. 17 | 18 | 19 | 20 | ## [0.1.2] 21 | 22 | ### Changed 23 | 24 | - Improved error handling and reporting. 25 | 26 | ### Fixed 27 | 28 | - Fixed bug that caused "Illegal instruction (core dumped)" on some devices. (Remove -march=native) -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.17 FATAL_ERROR) 2 | project(tiledwebmaps LANGUAGES CXX) 3 | 4 | enable_testing() 5 | 6 | include(GNUInstallDirs) 7 | 8 | set(PACKAGE_NAME tiledwebmaps) 9 | set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/${PACKAGE_NAME}/cmake) 10 | set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR}/${PACKAGE_NAME}) 11 | 12 | 13 | # Base library 14 | add_library(tiledwebmaps INTERFACE) 15 | target_compile_features(tiledwebmaps INTERFACE cxx_std_17) 16 | 17 | find_package(xtl REQUIRED) 18 | find_package(xtensor REQUIRED) 19 | find_package(xtensor-io REQUIRED) 20 | find_package(xtensor-blas REQUIRED) 21 | find_package(xtensor-interfaces REQUIRED) 22 | find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs) 23 | find_package(PROJ REQUIRED) 24 | find_package(CURL REQUIRED) 25 | find_package(curlcpp REQUIRED) 26 | 27 | target_link_libraries(tiledwebmaps INTERFACE 28 | xtensor 29 | xtensor-io 30 | xtensor-blas 31 | xtensor-interfaces::base 32 | xtensor-interfaces::opencv 33 | PROJ::proj 34 | CURL::libcurl 35 | curlcpp::curlcpp 36 | ${OpenCV_LIBS} 37 | ) 38 | target_include_directories(tiledwebmaps INTERFACE 39 | $ 40 | $ 41 | ${OpenCV_INCLUDE_DIRS} 42 | ) 43 | 44 | install( 45 | TARGETS tiledwebmaps 46 | EXPORT install_targets 47 | ) 48 | 49 | 50 | 51 | ######################## TESTS ######################## 52 | 53 | add_custom_target(tests) 54 | add_subdirectory(test) 55 | 56 | 57 | 58 | ######################## PYTHON ######################## 59 | 60 | option(tiledwebmaps_BUILD_PYTHON_INTERFACE "Build python interface" ON) 61 | if(tiledwebmaps_BUILD_PYTHON_INTERFACE) 62 | add_subdirectory(python) 63 | endif() 64 | 65 | 66 | 67 | ######################## INSTALL ######################## 68 | 69 | # Install headers 70 | set(INSTALL_HEADERS_AS_SYMLINK OFF CACHE BOOL "Installs headers as a symlink to the source directory instead of copying all files to install directory") 71 | if(INSTALL_HEADERS_AS_SYMLINK) 72 | install(CODE "file(MAKE_DIRECTORY ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR})") 73 | install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/include/tiledwebmaps ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/tiledwebmaps)") 74 | install(CODE "message(\"-- Installed symlink: ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/tiledwebmaps -> ${CMAKE_CURRENT_SOURCE_DIR}/include/tiledwebmaps\")") 75 | else() 76 | install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 77 | endif() 78 | 79 | # Install targets file 80 | install(EXPORT install_targets 81 | FILE 82 | ${PACKAGE_NAME}Targets.cmake 83 | NAMESPACE 84 | ${PACKAGE_NAME}:: 85 | DESTINATION 86 | ${INSTALL_CONFIGDIR} 87 | ) 88 | # Install ${PACKAGE_NAME}Config.cmake 89 | include(CMakePackageConfigHelpers) 90 | configure_package_config_file( 91 | ${CMAKE_CURRENT_SOURCE_DIR}/cmake/${PACKAGE_NAME}Config.cmake 92 | ${CMAKE_CURRENT_BINARY_DIR}/${PACKAGE_NAME}Config.cmake 93 | INSTALL_DESTINATION ${INSTALL_CONFIGDIR} 94 | ) 95 | install(FILES 96 | ${CMAKE_CURRENT_BINARY_DIR}/${PACKAGE_NAME}Config.cmake 97 | DESTINATION ${INSTALL_CONFIGDIR} 98 | ) 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023- Florian Fervers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TiledWebMaps 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![PyPI version shields.io](https://img.shields.io/pypi/v/tiledwebmaps.svg)](https://pypi.python.org/pypi/tiledwebmaps/) 4 | 5 | > A lightweight library for retrieving map images from a [tile provider](https://en.wikipedia.org/wiki/Tiled_web_map) with arbitrary resolution, location and bearing. 6 | 7 | ## Install 8 | 9 | ``` 10 | pip install tiledwebmaps 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Example 16 | 17 | ```python 18 | import tiledwebmaps as twm 19 | 20 | # Create a tileloader to access imagery from Massachusetts (MassGIS) 21 | tileloader = twm.Http( 22 | "https://tiles.arcgis.com/tiles/hGdibHYSPO59RG1h/arcgis/rest/services/orthos2021/MapServer/tile/{zoom}/{y}/{x}", 23 | twm.Layout.XYZ(), # Common layout used by many tile providers: 256x256 tiles in epsg:3857 projection 24 | min_zoom=0, # Minimum zoom level specified by MassGIS 25 | max_zoom=23, # Maximum zoom level specified by MassGIS 26 | ) 27 | 28 | # Load an aerial image with a given resolution, location and bearing 29 | image = tileloader.load( 30 | latlon=(42.360995, -71.051685), # Center of the image 31 | bearing=0.0, # Bearing pointing upwards in the image (= angle measured clockwise from north) 32 | meters_per_pixel=0.5, 33 | shape=(512, 512), 34 | ) 35 | 36 | # Save the image to disk 37 | import imageio 38 | imageio.imwrite("map.jpg", image) 39 | ``` 40 | 41 | The tileloader fetches multiple tiles from [MassGIS](https://www.mass.gov/orgs/massgis-bureau-of-geographic-information) and combines and transforms them to the correct location, bearing and resolution. This creates the following image ([same location in Bing Maps](https://www.bing.com/maps/?cp=42.360995%7E-71.051683&lvl=18.5&style=a)): 42 | 43 | 44 | 45 | A list of tile providers can for example be found at https://osmlab.github.io/editor-layer-index which is maintained by the OpenStreetMaps community. The above parameters for MassGIS are copied from [here](https://github.com/osmlab/editor-layer-index/blob/gh-pages/sources/north-america/us/ma/MassGIS_2021_Aerial.geojson). Please ensure that you comply with the terms of use of the respective tile providers, which may include attribution requirements and rate limits. Some tile providers also charge payment for tile requests. We are not responsible for charges incured when using this library! 46 | 47 | Requesting individual tiles from a tile provider for large regions via ``twm.Http`` is slow and puts high demand on the tile provider's servers. If possible, please prefer using bulk download scripts (see [below](#bulk-downloading)). 48 | 49 | ### Caching 50 | 51 | Tiles can be saved on disk to avoid downloading the same tile multiple times: 52 | 53 | ```python 54 | http_tileloader = twm.Http( 55 | "https://tiles.arcgis.com/tiles/hGdibHYSPO59RG1h/arcgis/rest/services/orthos2021/MapServer/tile/{zoom}/{y}/{x}", 56 | twm.Layout.XYZ(), 57 | min_zoom=0, 58 | max_zoom=23, 59 | ) 60 | cached_tileloader = twm.DiskCached(http_tileloader, "/path/to/map/folder") 61 | ``` 62 | 63 | `cached_tileloader` will check if a tile is already present on disk before calling `http_tileloader`, and store missing tiles after downloading them. 64 | 65 | Tiles can also be cached in memory using an [LRU cache](https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU): 66 | 67 | ```python 68 | cached_tileloader = twm.LRUCached(http_tileloader, size=100) 69 | ``` 70 | 71 | Not all tile providers allow caching or storing tiles on disk! Please check the terms of use of the tile provider before using this feature. 72 | 73 | ### Bulk downloading 74 | 75 | [This folder](https://github.com/fferflo/tiledwebmaps/tree/master/python/scripts) contains scripts for downloading aerial image tiles for regions that provide options for bulk downloading. This is preferred over requesting individual tiles via ``twm.Http`` as it is faster and puts less demand on the tile provider's servers. 76 | 77 | ```bash 78 | # Example for download_massgis, replace with download_{openbb|opendc|opennrw|opensaxony|nconemap} for other regions 79 | 80 | # 1. Install additional dependencies required by the download scripts: 81 | pip install tiledwebmaps[scripts] 82 | 83 | # 2. Download script 84 | wget https://github.com/fferflo/tiledwebmaps/blob/master/python/scripts/download_massgis.py 85 | 86 | # 3. Run script 87 | python download_massgis.py --path PATH_TO_DOWNLOAD_FOLDER --shape TILESIZE 88 | ``` 89 | 90 | The aerial imagery is downloaded to ``PATH_TO_DOWNLOAD_FOLDER`` and stored as tiles with size ``TILESIZE x TILESIZE``. A tileloader for the downloaded imagery can be instantiated as follows: 91 | 92 | ```python 93 | import tiledwebmaps as twm 94 | tileloader = twm.from_yaml("PATH_TO_DOWNLOAD_FOLDER") 95 | ``` -------------------------------------------------------------------------------- /cmake/tiledwebmapsConfig.cmake: -------------------------------------------------------------------------------- 1 | get_filename_component(tiledwebmaps_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) 2 | 3 | if(NOT TARGET tiledwebmaps::tiledwebmaps) 4 | find_package(xtl REQUIRED) 5 | find_package(xtensor REQUIRED) 6 | find_package(xtensor-blas REQUIRED) 7 | find_package(xtensor-interfaces REQUIRED) 8 | find_package(OpenCV REQUIRED) 9 | find_package(PROJ REQUIRED) 10 | find_package(CURL REQUIRED) 11 | find_package(curlcpp REQUIRED) 12 | 13 | include("${tiledwebmaps_CMAKE_DIR}/tiledwebmapsTargets.cmake") 14 | endif() 15 | -------------------------------------------------------------------------------- /images/map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fferflo/tiledwebmaps/45d2b7734b62bd72ef84f945403d212708f0ac26/images/map.jpg -------------------------------------------------------------------------------- /include/tiledwebmaps/affine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | -------------------------------------------------------------------------------- /include/tiledwebmaps/affine/named_axes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace tiledwebmaps { 9 | 10 | template 11 | class NamedAxesTransformation; 12 | 13 | template 14 | class NamedAxes 15 | { 16 | public: 17 | NamedAxes() 18 | { 19 | } 20 | 21 | NamedAxes(std::initializer_list> axes_names) 22 | : m_axes_names(TRank) 23 | { 24 | if (axes_names.size() != TRank) 25 | { 26 | throw std::invalid_argument(XTI_TO_STRING("Expected " << TRank << " axes, got " << axes_names.size() << " axes")); 27 | } 28 | for (size_t i = 0; i < TRank; i++) 29 | { 30 | const std::initializer_list& axis_initializer_list = axes_names.begin()[i]; 31 | if (axis_initializer_list.size() != 2) 32 | { 33 | throw std::invalid_argument(XTI_TO_STRING("Expected 2 names per axis, got " << axis_initializer_list.size() << " names")); 34 | } 35 | m_axes_names[i].first = axis_initializer_list.begin()[0]; 36 | m_axes_names[i].second = axis_initializer_list.begin()[1]; 37 | } 38 | } 39 | 40 | const std::pair& operator[](size_t i) const 41 | { 42 | return m_axes_names[i]; 43 | } 44 | 45 | bool operator==(const NamedAxes& other) const 46 | { 47 | return this->m_axes_names == other.m_axes_names; 48 | } 49 | 50 | bool operator!=(const NamedAxes& other) const 51 | { 52 | return !(*this == other); 53 | } 54 | 55 | xti::vecXi get_vector(std::string direction) const 56 | { 57 | xti::vecXi vector; 58 | vector.fill(0); 59 | for (size_t i = 0; i < TRank; i++) 60 | { 61 | if (m_axes_names[i].first == direction) 62 | { 63 | vector[i] = 1; 64 | break; 65 | } 66 | else if (m_axes_names[i].second == direction) 67 | { 68 | vector[i] = -1; 69 | break; 70 | } 71 | } 72 | if (xt::all(xt::equal(vector, 0))) 73 | { 74 | throw std::invalid_argument("Invalid axis direction"); 75 | } 76 | return vector; 77 | } 78 | 79 | template 80 | friend class NamedAxesTransformation; 81 | 82 | public: 83 | std::vector> m_axes_names; 84 | }; 85 | 86 | template 87 | std::ostream& operator<<(std::ostream& stream, const NamedAxes& axes) 88 | { 89 | stream << "NamedAxes["; 90 | for (size_t i = 0; i < TRank; i++) 91 | { 92 | if (i > 0) 93 | { 94 | stream << ", "; 95 | } 96 | stream << axes[i].first << "-" << axes[i].second; 97 | } 98 | stream << "]"; 99 | return stream; 100 | } 101 | 102 | template 103 | class NamedAxesTransformation : public Rotation 104 | { 105 | public: 106 | NamedAxesTransformation(const NamedAxes& axes1, const NamedAxes& axes2) 107 | : Rotation() 108 | , m_axes1(axes1) 109 | , m_axes2(axes2) 110 | { 111 | this->get_rotation().fill(0); 112 | for (size_t i1 = 0; i1 < TRank; i1++) 113 | { 114 | const auto& axis1 = axes1[i1]; 115 | for (size_t i2 = 0; i2 < TRank; i2++) 116 | { 117 | const auto& axis2 = axes2[i2]; 118 | if (axis1.first == axis2.first) 119 | { 120 | if (axis1.second != axis2.second) 121 | { 122 | throw std::invalid_argument("Named axes do not correspond"); 123 | } 124 | this->get_rotation()(i2, i1) = 1; 125 | } 126 | else if (axis1.first == axis2.second) 127 | { 128 | if (axis1.second != axis2.first) 129 | { 130 | throw std::invalid_argument("Named axes do not correspond"); 131 | } 132 | this->get_rotation()(i2, i1) = -1; 133 | } 134 | } 135 | } 136 | for (size_t i = 0; i < TRank; i++) 137 | { 138 | if (xt::all(xt::equal(xt::row(this->get_rotation(), i), 0)) || xt::all(xt::equal(xt::col(this->get_rotation(), i), 0))) 139 | { 140 | throw std::invalid_argument("Named axes do not correspond"); 141 | } 142 | } 143 | } 144 | 145 | private: 146 | NamedAxes m_axes1; 147 | NamedAxes m_axes2; 148 | }; 149 | 150 | } // end of ns tiledwebmaps 151 | -------------------------------------------------------------------------------- /include/tiledwebmaps/affine/rigid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace tiledwebmaps { 14 | 15 | template 16 | class Rigid 17 | { 18 | private: 19 | xti::matXT m_rotation; 20 | xti::vecXT m_translation; 21 | 22 | public: 23 | Rigid() 24 | : m_rotation(xt::eye(TRank)) 25 | { 26 | m_translation.fill(0); 27 | } 28 | 29 | Rigid(xti::matXT transformation_matrix) 30 | : m_rotation(xt::view(transformation_matrix, xt::range(0, TRank), xt::range(0, TRank))) 31 | , m_translation(xt::view(transformation_matrix, xt::range(0, TRank), TRank)) 32 | { 33 | // TODO: check that all other elements of matrix are 0, with epsilon 34 | // if (xt::view(transformation_matrix, TRank, xt::range(0, TRank)) != 0 || transformation_matrix(TRank, TRank) != 1) 35 | } 36 | 37 | Rigid(xti::matXT rotation, xti::vecXT translation) 38 | : m_rotation(rotation) 39 | , m_translation(translation) 40 | { 41 | } 42 | 43 | template > 44 | Rigid(TScalar angle, xti::vecXT translation) 45 | : m_rotation(angle_to_rotation_matrix(angle)) 46 | , m_translation(translation) 47 | { 48 | } 49 | 50 | template 51 | Rigid(const Rigid& other) 52 | : m_rotation(other.m_rotation) 53 | , m_translation(other.m_translation) 54 | { 55 | } 56 | 57 | template 58 | Rigid& operator=(const Rigid& other) 59 | { 60 | this->m_rotation = other.m_rotation; 61 | this->m_translation = other.m_translation; 62 | return *this; 63 | } 64 | 65 | auto transform(xti::vecXT point) const 66 | { 67 | return xt::linalg::dot(m_rotation, point) + m_translation; 68 | } 69 | 70 | template 71 | auto transform_all(TTensor&& points) const 72 | { 73 | if (points.shape()[1] != TRank) 74 | { 75 | throw std::invalid_argument(XTI_TO_STRING("Points tensor must have shape (n, " << TRank << "), got shape " << xt::adapt(points.shape()))); 76 | } 77 | return xt::transpose(xt::eval(xt::linalg::dot(m_rotation, xt::transpose(xt::eval(std::forward(points)), {1, 0})) + xt::view(m_translation, xt::all(), xt::newaxis())), {1, 0}); 78 | } 79 | 80 | auto transform_inverse(xti::vecXT point) const 81 | { 82 | return xt::linalg::dot(xt::transpose(m_rotation, {1, 0}), point - m_translation); 83 | } 84 | 85 | template 86 | auto transform_all_inverse(TTensor&& points) const 87 | { 88 | if (points.shape()[1] != TRank) 89 | { 90 | throw std::invalid_argument(XTI_TO_STRING("Points tensor must have shape (n, " << TRank << "), got shape " << xt::adapt(points.shape()))); 91 | } 92 | return xt::transpose(xt::eval(xt::linalg::dot(xt::transpose(m_rotation, {1, 0}), xt::transpose(xt::eval(std::forward(points)), {1, 0}) - xt::view(m_translation, xt::all(), xt::newaxis())), {1, 0})); 93 | } 94 | 95 | Rigid inverse() const 96 | { 97 | Rigid result; 98 | result.get_rotation() = xt::transpose(m_rotation, {1, 0}); 99 | result.get_translation() = xt::linalg::dot(result.get_rotation(), -m_translation); 100 | return result; 101 | } 102 | 103 | Rigid& operator*=(const Rigid& right) 104 | { 105 | m_translation = this->transform(right.get_translation()); 106 | m_rotation = xt::linalg::dot(m_rotation, right.get_rotation()); 107 | return *this; 108 | } 109 | 110 | xti::matXT& get_rotation() 111 | { 112 | return m_rotation; 113 | } 114 | 115 | const xti::matXT& get_rotation() const 116 | { 117 | return m_rotation; 118 | } 119 | 120 | xti::vecXT& get_translation() 121 | { 122 | return m_translation; 123 | } 124 | 125 | const xti::vecXT& get_translation() const 126 | { 127 | return m_translation; 128 | } 129 | 130 | xti::matXT to_matrix() const 131 | { 132 | xti::matXT result; 133 | for (int32_t r = 0; r < TRank; r++) 134 | { 135 | for (int32_t c = 0; c < TRank; c++) 136 | { 137 | result(r, c) = m_rotation(r, c); 138 | } 139 | result(r, TRank) = m_translation(r); 140 | result(TRank, r) = 0; 141 | } 142 | result(TRank, TRank) = 1; 143 | return result; 144 | } 145 | 146 | template 147 | friend class Rigid; 148 | }; 149 | 150 | template 151 | Rigid operator*(const Rigid& left, const Rigid& right) 152 | { 153 | return Rigid(xt::linalg::dot(left.get_rotation(), right.get_rotation()), left.transform(right.get_translation())); 154 | } 155 | 156 | template 157 | Rigid operator/(const Rigid& left, const Rigid& right) 158 | { 159 | return left * right.inverse(); 160 | } 161 | 162 | template 163 | Rigid slerp(const Rigid& first, const Rigid& second, TScalar alpha) 164 | { 165 | return Rigid( 166 | slerp(first.get_rotation(), second.get_rotation(), alpha), 167 | first.get_translation() + alpha * (second.get_translation() - first.get_translation()) 168 | ); 169 | } 170 | 171 | template 172 | std::ostream& operator<<(std::ostream& stream, const Rigid& transform) 173 | { 174 | return stream << "Rigid(t=" << transform.get_translation() << " R=" << transform.get_rotation() << ")"; 175 | } 176 | 177 | } // tiledwebmaps 178 | -------------------------------------------------------------------------------- /include/tiledwebmaps/affine/rotation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace tiledwebmaps { 12 | 13 | template >> 14 | TScalar radians(TScalar degrees) 15 | { 16 | return degrees / 180 * xt::numeric_constants::PI; 17 | } 18 | 19 | template >> 20 | auto radians(TTensor&& tensor) 21 | { 22 | return std::forward(tensor) / 180 * xt::numeric_constants>::PI; 23 | } 24 | 25 | template >> 26 | TScalar degrees(TScalar degrees) 27 | { 28 | return degrees * 180 / xt::numeric_constants::PI; 29 | } 30 | 31 | template >> 32 | auto degrees(TTensor&& tensor) 33 | { 34 | return std::forward(tensor) * 180 / xt::numeric_constants>::PI; 35 | } 36 | 37 | template 38 | xti::mat2T> angle_to_rotation_matrix(TScalar angle) 39 | { 40 | return {{std::cos(angle), -std::sin(angle)}, {std::sin(angle), std::cos(angle)}}; 41 | } 42 | 43 | template 44 | auto rotation_matrix_to_angle(TRotationMatrix&& rotation_matrix) 45 | { 46 | return std::atan2(rotation_matrix(1, 0), rotation_matrix(0, 0)); 47 | } 48 | 49 | template 50 | xti::vecXT rotation_matrix_to_quaternion(xti::mat3T rotation_matrix) // wxyz 51 | { 52 | TScalar q0 = std::sqrt(std::max(TScalar(0.25) * (1 + rotation_matrix(0, 0) + rotation_matrix(1, 1) + rotation_matrix(2, 2)), TScalar(0))); 53 | TScalar q1 = std::sqrt(std::max(TScalar(0.25) * (1 + rotation_matrix(0, 0) - rotation_matrix(1, 1) - rotation_matrix(2, 2)), TScalar(0))); 54 | TScalar q2 = std::sqrt(std::max(TScalar(0.25) * (1 - rotation_matrix(0, 0) + rotation_matrix(1, 1) - rotation_matrix(2, 2)), TScalar(0))); 55 | TScalar q3 = std::sqrt(std::max(TScalar(0.25) * (1 - rotation_matrix(0, 0) - rotation_matrix(1, 1) + rotation_matrix(2, 2)), TScalar(0))); 56 | 57 | #define PSIGN(i, j) ((rotation_matrix(i, j) + rotation_matrix(j, i) >= 0) ? 1.0f : -1.0f) 58 | #define NSIGN(i, j) ((rotation_matrix(i, j) - rotation_matrix(j, i) >= 0) ? 1.0f : -1.0f) 59 | 60 | if (q0 >= q1 && q0 >= q2 && q0 >= q3) 61 | { 62 | // q0 *= 1.0f; 63 | q1 *= NSIGN(2, 1); 64 | q2 *= NSIGN(0, 2); 65 | q3 *= NSIGN(1, 0); 66 | } 67 | else if (q1 >= q0 && q1 >= q2 && q1 >= q3) 68 | { 69 | q0 *= NSIGN(2, 1); 70 | // q1 *= 1.0f; 71 | q2 *= PSIGN(1, 0); 72 | q3 *= PSIGN(0, 2); 73 | } 74 | else if (q2 >= q0 && q2 >= q1 && q2 >= q3) 75 | { 76 | q0 *= NSIGN(0, 2); 77 | q1 *= PSIGN(1, 0); 78 | // q2 *= 1.0f; 79 | q3 *= PSIGN(2, 1); 80 | } 81 | else // if (q3 >= q0 && q3 >= q1 && q3 >= q2) 82 | { 83 | q0 *= NSIGN(1, 0); 84 | q1 *= PSIGN(2, 0); 85 | q2 *= PSIGN(2, 1); 86 | // q3 *= 1.0f; 87 | } 88 | 89 | #undef PSIGN 90 | #undef NSIGN 91 | 92 | xti::vecXT quaternion({q0, q1, q2, q3}); 93 | quaternion = quaternion / xt::linalg::norm(quaternion); 94 | return quaternion; 95 | } 96 | 97 | template 98 | xti::mat3T quaternion_to_rotation_matrix(xti::vecXT quaternion) // wxyz 99 | { 100 | xti::mat3T rotation_matrix; 101 | rotation_matrix(0, 0) = 1 - 2 * quaternion(2) * quaternion(2) - 2 * quaternion(3) * quaternion(3); 102 | rotation_matrix(1, 1) = 1 - 2 * quaternion(1) * quaternion(1) - 2 * quaternion(3) * quaternion(3); 103 | rotation_matrix(2, 2) = 1 - 2 * quaternion(1) * quaternion(1) - 2 * quaternion(2) * quaternion(2); 104 | rotation_matrix(0, 1) = 2 * quaternion(1) * quaternion(2) - 2 * quaternion(3) * quaternion(0); 105 | rotation_matrix(1, 0) = 2 * quaternion(1) * quaternion(2) + 2 * quaternion(3) * quaternion(0); 106 | rotation_matrix(2, 0) = 2 * quaternion(1) * quaternion(3) - 2 * quaternion(2) * quaternion(0); 107 | rotation_matrix(0, 2) = 2 * quaternion(1) * quaternion(3) + 2 * quaternion(2) * quaternion(0); 108 | rotation_matrix(1, 2) = 2 * quaternion(2) * quaternion(3) - 2 * quaternion(1) * quaternion(0); 109 | rotation_matrix(2, 1) = 2 * quaternion(2) * quaternion(3) + 2 * quaternion(1) * quaternion(0); 110 | return rotation_matrix; 111 | } 112 | 113 | template 114 | xti::vecXT slerp(xti::vecXT quaternion1, xti::vecXT quaternion2, TScalar alpha) 115 | { 116 | TScalar dot = xt::linalg::dot(quaternion1, quaternion2)(); 117 | 118 | if (dot < 0) 119 | { 120 | dot = -dot; 121 | quaternion2 = -quaternion2; 122 | } 123 | 124 | xti::vecXT result; 125 | if (dot > 0.9999) 126 | { 127 | result = quaternion1 + alpha * (quaternion2 - quaternion1); 128 | } 129 | else 130 | { 131 | TScalar theta_0 = std::acos(dot); 132 | TScalar sin_theta_0 = std::sin(theta_0); 133 | 134 | TScalar theta = theta_0 * alpha; 135 | TScalar sin_theta = std::sin(theta); 136 | 137 | TScalar s1 = std::cos(theta) - dot * sin_theta / sin_theta_0; 138 | TScalar s2 = sin_theta / sin_theta_0; 139 | 140 | result = s1 * quaternion1 + s2 * quaternion2; 141 | } 142 | result = result / xt::linalg::norm(result); 143 | 144 | return result; 145 | } 146 | 147 | template 148 | xti::matXT slerp(xti::matXT rotation_matrix1, xti::matXT rotation_matrix2, TScalar alpha) 149 | { 150 | return quaternion_to_rotation_matrix(slerp(rotation_matrix_to_quaternion(rotation_matrix1), rotation_matrix_to_quaternion(rotation_matrix2), alpha)); 151 | } 152 | 153 | template 154 | xti::vecXT axisangle_to_quaternion(xti::vecXT axis, TScalar angle) 155 | { 156 | axis = axis / (TScalar) xt::linalg::norm(axis); 157 | TScalar theta = angle / 2; 158 | TScalar sin_theta = std::sin(theta); 159 | 160 | xti::vecXT result; 161 | result(0) = std::cos(theta); 162 | result(1) = axis(0) * sin_theta; 163 | result(2) = axis(1) * sin_theta; 164 | result(3) = axis(2) * sin_theta; 165 | 166 | return result; 167 | } 168 | 169 | template 170 | xti::matXT axisangle_to_rotation_matrix(xti::vecXT axis, TScalar angle) 171 | { 172 | return quaternion_to_rotation_matrix(axisangle_to_quaternion(axis, angle)); 173 | } 174 | 175 | template 176 | auto normalize_angle(TScalar angle, TScalar lower = -xt::numeric_constants::PI, TScalar upper = xt::numeric_constants::PI) 177 | { 178 | static const TScalar pi = xt::numeric_constants::PI; 179 | while (angle >= upper) 180 | { 181 | angle -= 2 * pi; 182 | } 183 | while (angle < lower) 184 | { 185 | angle += 2 * pi; 186 | } 187 | return angle; 188 | } 189 | 190 | template 191 | auto angle_between_vectors(TVec1&& vec1, TVec2&& vec2, bool clockwise = false) 192 | { 193 | auto angle = std::atan2(vec2(1), vec2(0)) - std::atan2(vec1(1), vec1(0)); 194 | 195 | return clockwise ? -angle : angle; 196 | } 197 | 198 | template 199 | xti::matXT rpy_to_rotation_matrix(TScalar r, TScalar p, TScalar y) 200 | { 201 | // https://en.wikipedia.org/wiki/Rotation_matrix#General_3D_rotations 202 | TScalar a = y; 203 | TScalar b = p; 204 | TScalar c = r; 205 | xti::matXT rotation_matrix; 206 | rotation_matrix(0, 0) = std::cos(a) * std::cos(b); 207 | rotation_matrix(0, 1) = std::cos(a) * std::sin(b) * std::sin(c) - std::sin(a) * std::cos(c); 208 | rotation_matrix(0, 2) = std::cos(a) * std::sin(b) * std::cos(c) + std::sin(a) * std::sin(c); 209 | rotation_matrix(1, 0) = std::sin(a) * std::cos(b); 210 | rotation_matrix(1, 1) = std::sin(a) * std::sin(b) * std::sin(c) + std::cos(a) * std::cos(c); 211 | rotation_matrix(1, 2) = std::sin(a) * std::sin(b) * std::cos(c) - std::cos(a) * std::sin(c); 212 | rotation_matrix(2, 0) = -std::sin(b); 213 | rotation_matrix(2, 1) = std::cos(b) * std::sin(c); 214 | rotation_matrix(2, 2) = std::cos(b) * std::cos(c); 215 | return rotation_matrix; 216 | } 217 | 218 | template 219 | xti::matXT rpy_to_rotation_matrix(xti::vecXT rpy) 220 | { 221 | return rpy_to_rotation_matrix(rpy(0), rpy(1), rpy(2)); 222 | } 223 | 224 | template 225 | class Rotation 226 | { 227 | private: 228 | xti::matXT m_rotation; 229 | 230 | public: 231 | Rotation() 232 | : m_rotation(xt::eye(TRank)) 233 | { 234 | } 235 | 236 | template > 237 | Rotation(TScalar angle) 238 | : m_rotation(angle_to_rotation_matrix(angle)) 239 | { 240 | } 241 | 242 | Rotation(xti::matXT transformation_matrix) 243 | : m_rotation(xt::view(transformation_matrix, xt::range(0, TRank), xt::range(0, TRank))) 244 | { 245 | // TODO: check that all other elements of matrix are 0, with epsilon 246 | // if (xt::view(transformation_matrix, TRank, xt::range(0, TRank)) != 0 || transformation_matrix(TRank, TRank) != 1) 247 | } 248 | 249 | Rotation(xti::matXT rotation) 250 | : m_rotation(rotation) 251 | { 252 | } 253 | 254 | template 255 | Rotation(const Rotation& other) 256 | : m_rotation(other.m_rotation) 257 | { 258 | } 259 | 260 | template 261 | Rotation& operator=(const Rotation& other) 262 | { 263 | this->m_rotation = other.m_rotation; 264 | return *this; 265 | } 266 | 267 | auto transform(xti::vecXT point) const 268 | { 269 | return xt::linalg::dot(m_rotation, point); 270 | } 271 | 272 | template 273 | auto transform_all(TTensor&& points) const 274 | { 275 | if (points.shape()[1] != TRank) 276 | { 277 | throw std::invalid_argument(XTI_TO_STRING("Points tensor must have shape (n, " << TRank << "), got shape " << xt::adapt(points.shape()))); 278 | } 279 | return xt::transpose(xt::eval(xt::linalg::dot(m_rotation, xt::transpose(xt::eval(std::forward(points)), {1, 0}))), {1, 0}); 280 | } 281 | 282 | auto transform_inverse(xti::vecXT point) const 283 | { 284 | return xt::linalg::dot(xt::transpose(m_rotation, {1, 0}), point); 285 | } 286 | 287 | template 288 | auto transform_all_inverse(TTensor&& points) const 289 | { 290 | if (points.shape()[1] != TRank) 291 | { 292 | throw std::invalid_argument(XTI_TO_STRING("Points tensor must have shape (n, " << TRank << "), got shape " << xt::adapt(points.shape()))); 293 | } 294 | return xt::transpose(xt::eval(xt::linalg::dot(xt::transpose(m_rotation, {1, 0}), xt::transpose(xt::eval(std::forward(points)), {1, 0})), {1, 0})); 295 | } 296 | 297 | Rotation inverse() const 298 | { 299 | Rotation result; 300 | result.get_rotation() = xt::transpose(m_rotation, {1, 0}); 301 | return result; 302 | } 303 | 304 | Rotation& operator*=(const Rotation& right) 305 | { 306 | m_rotation = xt::linalg::dot(m_rotation, right.get_rotation()); 307 | return *this; 308 | } 309 | 310 | xti::matXT& get_rotation() 311 | { 312 | return m_rotation; 313 | } 314 | 315 | const xti::matXT& get_rotation() const 316 | { 317 | return m_rotation; 318 | } 319 | 320 | bool flips() const 321 | { 322 | return xt::linalg::det(m_rotation) < 0; 323 | } 324 | 325 | template 326 | friend class Rotation; 327 | }; 328 | 329 | template 330 | Rotation operator*(const Rotation& left, const Rotation& right) 331 | { 332 | return Rotation(xt::linalg::dot(left.get_rotation(), right.get_rotation())); 333 | } 334 | 335 | template 336 | Rotation operator/(const Rotation& left, const Rotation& right) 337 | { 338 | return left * right.inverse(); 339 | } 340 | 341 | template 342 | std::ostream& operator<<(std::ostream& stream, const Rotation& transform) 343 | { 344 | return stream << "Rotation(" << " R=" << transform.get_rotation() << ")"; 345 | } 346 | 347 | } // tiledwebmaps 348 | -------------------------------------------------------------------------------- /include/tiledwebmaps/affine/scaled_rigid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace tiledwebmaps { 15 | 16 | template 17 | class ScaledRigid 18 | { 19 | private: 20 | using Scale = std::conditional_t>; 21 | 22 | static Scale scale_one() 23 | { 24 | if constexpr (TSingleScale) 25 | { 26 | return 1; 27 | } 28 | else 29 | { 30 | return xt::ones({TRank}); 31 | } 32 | } 33 | 34 | xti::matXT m_rotation; 35 | xti::vecXT m_translation; 36 | Scale m_scale; 37 | 38 | public: 39 | ScaledRigid() 40 | : m_rotation(xt::eye(TRank)) 41 | , m_scale(scale_one()) 42 | { 43 | m_translation.fill(0); 44 | } 45 | 46 | template 47 | ScaledRigid(const Rigid& other) 48 | : m_rotation(other.get_rotation()) 49 | , m_translation(other.get_translation()) 50 | , m_scale(scale_one()) 51 | { 52 | } 53 | 54 | template 55 | ScaledRigid(const Rotation& other) 56 | : m_rotation(other.get_rotation()) 57 | , m_scale(scale_one()) 58 | { 59 | m_translation.fill(0); 60 | } 61 | 62 | ScaledRigid(xti::matXT rotation, xti::vecXT translation, Scale scale = scale_one()) 63 | : m_rotation(rotation) 64 | , m_translation(translation) 65 | , m_scale(scale) 66 | { 67 | } 68 | 69 | template > 70 | ScaledRigid(TScalar angle, xti::vecXT translation, Scale scale) 71 | : m_rotation(angle_to_rotation_matrix(angle)) 72 | , m_translation(translation) 73 | , m_scale(scale) 74 | { 75 | } 76 | 77 | template 78 | ScaledRigid(const ScaledRigid& other) 79 | : m_rotation(other.m_rotation) 80 | , m_translation(other.m_translation) 81 | , m_scale(other.m_scale) 82 | { 83 | } 84 | 85 | template 86 | ScaledRigid& operator=(const ScaledRigid& other) 87 | { 88 | this->m_rotation = other.m_rotation; 89 | this->m_translation = other.m_translation; 90 | this->m_scale = other.m_scale; 91 | return *this; 92 | } 93 | 94 | auto transform(xti::vecXT point) const 95 | { 96 | return m_scale * xt::linalg::dot(m_rotation, point) + m_translation; 97 | } 98 | 99 | template 100 | auto transform_all(TTensor&& points) const 101 | { 102 | if (points.shape()[1] != TRank) 103 | { 104 | throw std::invalid_argument(XTI_TO_STRING("Points tensor must have shape (n, " << TRank << "), got shape " << xt::adapt(points.shape()))); 105 | } 106 | return xt::transpose(xt::eval(m_scale * xt::linalg::dot(m_rotation, xt::transpose(xt::eval(std::forward(points)), {1, 0})) + xt::view(m_translation, xt::all(), xt::newaxis())), {1, 0}); 107 | } 108 | 109 | auto transform_inverse(xti::vecXT point) const 110 | { 111 | return xt::linalg::dot(xt::transpose(m_rotation, {1, 0}), point - m_translation) / m_scale; 112 | } 113 | 114 | template 115 | auto transform_all_inverse(TTensor&& points) const 116 | { 117 | if (points.shape()[1] != TRank) 118 | { 119 | throw std::invalid_argument(XTI_TO_STRING("Points tensor must have shape (n, " << TRank << "), got shape " << xt::adapt(points.shape()))); 120 | } 121 | return xt::transpose(xt::eval(xt::linalg::dot(xt::transpose(m_rotation, {1, 0}), xt::transpose(xt::eval(std::forward(points)), {1, 0}) - xt::view(m_translation, xt::all(), xt::newaxis())), {1, 0})) / m_scale; 122 | } 123 | 124 | ScaledRigid inverse() const 125 | { 126 | ScaledRigid result; 127 | result.get_rotation() = xt::transpose(m_rotation, {1, 0}); 128 | result.get_translation() = xt::linalg::dot(result.get_rotation(), -m_translation) / m_scale; 129 | result.get_scale() = 1 / m_scale; 130 | return result; 131 | } 132 | 133 | ScaledRigid& operator*=(const ScaledRigid& right) 134 | { 135 | m_translation = this->transform(right.get_translation()); 136 | m_rotation = xt::linalg::dot(m_rotation, right.get_rotation()); 137 | m_scale = m_scale * right.m_scale; 138 | return *this; 139 | } 140 | 141 | xti::matXT& get_rotation() 142 | { 143 | return m_rotation; 144 | } 145 | 146 | const xti::matXT& get_rotation() const 147 | { 148 | return m_rotation; 149 | } 150 | 151 | xti::vecXT& get_translation() 152 | { 153 | return m_translation; 154 | } 155 | 156 | const xti::vecXT& get_translation() const 157 | { 158 | return m_translation; 159 | } 160 | 161 | Scale& get_scale() 162 | { 163 | return m_scale; 164 | } 165 | 166 | const Scale& get_scale() const 167 | { 168 | return m_scale; 169 | } 170 | 171 | xti::matXT to_matrix() const 172 | { 173 | xti::matXT result; 174 | for (int32_t r = 0; r < TRank; r++) 175 | { 176 | for (int32_t c = 0; c < TRank; c++) 177 | { 178 | if constexpr (TSingleScale) 179 | { 180 | result(r, c) = m_rotation(r, c) * m_scale; 181 | } 182 | else 183 | { 184 | result(r, c) = m_rotation(r, c) * m_scale[c]; 185 | } 186 | } 187 | result(r, TRank) = m_translation(r); 188 | result(TRank, r) = 0; 189 | } 190 | result(TRank, TRank) = 1; 191 | return result; 192 | } 193 | 194 | template 195 | friend class ScaledRigid; 196 | }; 197 | 198 | template 199 | ScaledRigid operator*(const ScaledRigid& left, const ScaledRigid& right) 200 | { 201 | return ScaledRigid(xt::linalg::dot(left.get_rotation(), right.get_rotation()), left.transform(right.get_translation()), left.get_scale() * right.get_scale()); 202 | } 203 | 204 | template 205 | ScaledRigid operator/(const ScaledRigid& left, const ScaledRigid& right) 206 | { 207 | return left * right.inverse(); 208 | } 209 | 210 | template 211 | std::ostream& operator<<(std::ostream& stream, const ScaledRigid& transform) 212 | { 213 | return stream << "ScaledRigid(t=" << transform.get_translation() << " R=" << transform.get_rotation() << " s=" << transform.get_scale() << ")"; 214 | } 215 | 216 | } // tiledwebmaps 217 | -------------------------------------------------------------------------------- /include/tiledwebmaps/bin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace tiledwebmaps { 19 | 20 | class Bin : public TileLoader 21 | { 22 | public: 23 | Bin(std::filesystem::path path, const Layout& layout) 24 | : TileLoader(layout) 25 | , m_path(path) 26 | , m_file_pointer(NULL) 27 | { 28 | if (!std::filesystem::exists(path / "images.dat")) 29 | { 30 | throw FileNotFoundException(path / "images.dat"); 31 | } 32 | auto npz = xt::load_npz(path / "images-meta.npz"); 33 | 34 | xt::xtensor zoom = npz["zoom"].template cast(); 35 | xt::xtensor x = npz["x"].template cast(); 36 | xt::xtensor y = npz["y"].template cast(); 37 | xt::xtensor offset = npz["offset"].template cast(); 38 | 39 | for (int i = 0; i < zoom.size(); i++) 40 | { 41 | m_tiles[std::make_tuple(zoom(i), x(i), y(i))] = std::make_tuple(offset(i), offset(i + 1) - offset(i)); 42 | } 43 | 44 | m_min_zoom = xt::amin(zoom)(); 45 | m_max_zoom = xt::amax(zoom)(); 46 | } 47 | 48 | Bin(const Bin& other) 49 | : TileLoader(other) 50 | , m_path(other.m_path) 51 | , m_file_pointer(NULL) 52 | , m_tiles(other.m_tiles) 53 | { 54 | } 55 | 56 | Bin(Bin&& other) 57 | : TileLoader(other) 58 | , m_path(std::move(other.m_path)) 59 | , m_file_pointer(other.m_file_pointer) 60 | , m_tiles(std::move(other.m_tiles)) 61 | { 62 | other.m_file_pointer = NULL; 63 | } 64 | 65 | virtual ~Bin() 66 | { 67 | std::lock_guard lock(m_mutex); 68 | if (m_file_pointer != NULL) 69 | { 70 | std::fclose(m_file_pointer); 71 | } 72 | } 73 | 74 | Bin& operator=(const Bin& other) 75 | { 76 | if (this != &other) 77 | { 78 | std::lock_guard lock(m_mutex); 79 | m_path = other.m_path; 80 | m_file_pointer = NULL; 81 | m_tiles = other.m_tiles; 82 | } 83 | return *this; 84 | } 85 | 86 | Bin& operator=(Bin&& other) 87 | { 88 | if (this != &other) 89 | { 90 | std::lock_guard lock(m_mutex); 91 | m_path = std::move(other.m_path); 92 | m_file_pointer = other.m_file_pointer; 93 | other.m_file_pointer = NULL; 94 | m_tiles = std::move(other.m_tiles); 95 | } 96 | return *this; 97 | } 98 | 99 | virtual void make_forksafe() 100 | { 101 | std::lock_guard lock(m_mutex); 102 | if (m_file_pointer != NULL) 103 | { 104 | std::fclose(m_file_pointer); 105 | m_file_pointer = NULL; 106 | } 107 | } 108 | 109 | int get_min_zoom() const 110 | { 111 | return m_min_zoom; 112 | } 113 | 114 | int get_max_zoom() const 115 | { 116 | return m_max_zoom; 117 | } 118 | 119 | cv::Mat load(xti::vec2i tile, int zoom) 120 | { 121 | if (zoom > m_max_zoom) 122 | { 123 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is higher than the maximum zoom level " + XTI_TO_STRING(m_max_zoom) + "."); 124 | } 125 | if (zoom < m_min_zoom) 126 | { 127 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is lower than the minimum zoom level " + XTI_TO_STRING(m_min_zoom) + "."); 128 | } 129 | auto it = m_tiles.find(std::make_tuple(zoom, tile[0], tile[1])); 130 | if (it == m_tiles.end()) 131 | { 132 | throw LoadTileException("Tile not found in bin file"); 133 | } 134 | int64_t offset = std::get<0>(it->second); 135 | int64_t size = std::get<1>(it->second); 136 | 137 | std::vector buffer(size); 138 | { 139 | std::lock_guard lock(m_mutex); 140 | if (m_file_pointer == NULL) 141 | { 142 | m_file_pointer = std::fopen((m_path / "images.dat").string().c_str(), "rb"); 143 | if (m_file_pointer == NULL) 144 | { 145 | throw LoadFileException(m_path / "images.dat", "Failed to open file"); 146 | } 147 | } 148 | if (std::fseek(m_file_pointer, offset, SEEK_SET) != 0) 149 | { 150 | throw LoadFileException(m_path / "images.dat", "Failed to seek to offset " + std::to_string(offset)); 151 | } 152 | if (std::fread(buffer.data(), 1, size, m_file_pointer) != size) 153 | { 154 | throw LoadFileException(m_path / "images.dat", "Failed to read " + std::to_string(size) + " bytes from offset " + std::to_string(offset)); 155 | } 156 | } 157 | 158 | cv::Mat data_cv(1, buffer.size(), xti::opencv::pixeltype::get(1), buffer.data()); 159 | if (data_cv.data == NULL) 160 | { 161 | throw ImreadException("Failed to convert data array of file " + m_path.string() + " to cv mat"); 162 | } 163 | 164 | cv::Mat image = cv::imdecode(data_cv, cv::IMREAD_COLOR); 165 | if (image.data == NULL) 166 | { 167 | throw ImreadException("Failed to decode image from file " + m_path.string()); 168 | } 169 | 170 | cv::cvtColor(image, image, cv::COLOR_BGR2RGB); 171 | 172 | try 173 | { 174 | this->to_tile(image, true); 175 | } 176 | catch (LoadTileException ex) 177 | { 178 | throw LoadFileException(m_path, std::string("Loaded invalid tile. ") + ex.what()); 179 | } 180 | return image; 181 | } 182 | 183 | private: 184 | std::filesystem::path m_path; 185 | FILE* m_file_pointer; 186 | std::map, std::tuple> m_tiles; 187 | int m_min_zoom; 188 | int m_max_zoom; 189 | std::mutex m_mutex; 190 | }; 191 | 192 | } // end of ns tiledwebmaps 193 | -------------------------------------------------------------------------------- /include/tiledwebmaps/cache.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace tiledwebmaps { 11 | 12 | class CacheFailure : public std::exception 13 | { 14 | public: 15 | virtual const char* what() const throw () 16 | { 17 | return "Cache failure"; 18 | } 19 | }; 20 | 21 | class Cache 22 | { 23 | public: 24 | virtual cv::Mat load(xti::vec2i tile, int zoom) = 0; 25 | 26 | virtual void save(const cv::Mat& image, xti::vec2i tile, int zoom) = 0; 27 | 28 | virtual bool contains(xti::vec2i tile, int zoom) const = 0; 29 | }; 30 | 31 | class CachedTileLoader : public TileLoader 32 | { 33 | public: 34 | CachedTileLoader(std::shared_ptr loader, std::shared_ptr cache) 35 | : TileLoader(loader->get_layout()) 36 | , m_cache(cache) 37 | , m_loader(loader) 38 | { 39 | } 40 | 41 | int get_min_zoom() const 42 | { 43 | return m_loader->get_min_zoom(); 44 | } 45 | 46 | int get_max_zoom() const 47 | { 48 | return m_loader->get_max_zoom(); 49 | } 50 | 51 | cv::Mat load(xti::vec2i tile_coord, int zoom) 52 | { 53 | if (m_cache->contains(tile_coord, zoom)) 54 | { 55 | try 56 | { 57 | return m_cache->load(tile_coord, zoom); 58 | } 59 | catch (CacheFailure e) 60 | { 61 | } 62 | } 63 | 64 | cv::Mat image = m_loader->load(tile_coord, zoom); 65 | m_cache->save(image, tile_coord, zoom); 66 | return image; 67 | } 68 | 69 | std::shared_ptr get_cache() const 70 | { 71 | return m_cache; 72 | } 73 | 74 | virtual void make_forksafe() 75 | { 76 | m_loader->make_forksafe(); 77 | } 78 | 79 | private: 80 | std::shared_ptr m_loader; 81 | std::shared_ptr m_cache; 82 | }; 83 | 84 | class WithDefault : public TileLoader 85 | { 86 | public: 87 | WithDefault(std::shared_ptr tileloader, xti::vec3i color) 88 | : TileLoader(tileloader->get_layout()) 89 | , m_tileloader(tileloader) 90 | , m_color(color) 91 | { 92 | } 93 | 94 | int get_min_zoom() const 95 | { 96 | return m_tileloader->get_min_zoom(); 97 | } 98 | 99 | int get_max_zoom() const 100 | { 101 | return m_tileloader->get_max_zoom(); 102 | } 103 | 104 | cv::Mat load(xti::vec2i tile_coord, int zoom) 105 | { 106 | if (zoom > get_max_zoom()) 107 | { 108 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is higher than the maximum zoom level " + XTI_TO_STRING(get_max_zoom()) + "."); 109 | } 110 | if (zoom < get_min_zoom()) 111 | { 112 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is lower than the minimum zoom level " + XTI_TO_STRING(get_min_zoom()) + "."); 113 | } 114 | try 115 | { 116 | return m_tileloader->load(tile_coord, zoom); 117 | } 118 | catch (LoadTileException e) 119 | { 120 | } 121 | catch (CacheFailure e) 122 | { 123 | } 124 | 125 | return cv::Mat(m_tileloader->get_layout().get_tile_shape_px()(1), m_tileloader->get_layout().get_tile_shape_px()(0), CV_8UC3, cv::Scalar(m_color(0), m_color(1), m_color(2))); 126 | } 127 | 128 | virtual void make_forksafe() 129 | { 130 | m_tileloader->make_forksafe(); 131 | } 132 | 133 | private: 134 | std::shared_ptr m_tileloader; 135 | xti::vec3i m_color; 136 | }; 137 | 138 | } // end of ns tiledwebmaps 139 | -------------------------------------------------------------------------------- /include/tiledwebmaps/disk.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace tiledwebmaps { 20 | 21 | bool ends_with(std::string_view str, std::string_view suffix) 22 | { 23 | return str.size() >= suffix.size() && 0 == str.compare(str.size() - suffix.size(), suffix.size(), suffix); 24 | } 25 | 26 | class ImreadException : public CacheFailure 27 | { 28 | public: 29 | ImreadException(std::string message) 30 | : m_message(message) 31 | { 32 | } 33 | 34 | virtual const char* what() const throw () 35 | { 36 | return m_message.c_str(); 37 | } 38 | 39 | private: 40 | std::string m_message; 41 | }; 42 | 43 | cv::Mat safe_imread(std::filesystem::path path) 44 | { 45 | if (!std::filesystem::exists(path)) 46 | { 47 | throw ImreadException(std::string("File does not exist: ") + path.string()); 48 | } 49 | std::ifstream file(path.string(), std::ios::binary | std::ios::ate); 50 | std::streamsize size = file.tellg(); 51 | if (size == 0) 52 | { 53 | throw ImreadException(std::string("File is empty: ") + path.string()); 54 | } 55 | file.seekg(0, std::ios::beg); 56 | 57 | std::vector buffer(size); 58 | if (!file.read((char*) buffer.data(), size)) 59 | { 60 | throw ImreadException(std::string("Failed to read bytes of file ") + path.string()); 61 | } 62 | 63 | if (ends_with(path.string(), ".jpg") || ends_with(path.string(), ".jpeg")) 64 | { 65 | #define HEX(x) std::setw(2) << std::setfill('0') << std::hex << (int) (x) 66 | if (buffer[0] != 0xFF || buffer[1] != 0xD8) 67 | { 68 | throw ImreadException(XTI_TO_STRING("Loaded jpeg with invalid start marker " << HEX(buffer[0]) << " " << HEX(buffer[1]) << " from file " << path.string())); 69 | } 70 | else if (buffer[buffer.size() - 2] != 0xFF || buffer[buffer.size() - 1] != 0xD9) 71 | { 72 | throw ImreadException(XTI_TO_STRING("Loaded jpeg with invalid end marker " << HEX(buffer[buffer.size() - 2]) << " " << HEX(buffer[buffer.size() - 1]) << " from file " << path.string())); 73 | } 74 | #undef HEX 75 | } 76 | 77 | cv::Mat data_cv(1, buffer.size(), xti::opencv::pixeltype::get(1), buffer.data()); 78 | if (data_cv.data == NULL) 79 | { 80 | throw ImreadException("Failed to convert data array of file " + path.string() + " to cv mat"); 81 | } 82 | 83 | cv::Mat image_cv = cv::imdecode(data_cv, cv::IMREAD_COLOR); 84 | if (image_cv.data == NULL) 85 | { 86 | throw ImreadException("Failed to decode image from file " + path.string()); 87 | } 88 | 89 | return image_cv; 90 | } 91 | 92 | class WriteFileException : public std::exception 93 | { 94 | public: 95 | WriteFileException(std::filesystem::path path, std::string message) 96 | : m_message(std::string("Failed to write file ") + path.string() + ". Reason: " + message) 97 | { 98 | } 99 | 100 | WriteFileException(std::filesystem::path path) 101 | : m_message(std::string("Failed to write file ") + path.string()) 102 | { 103 | } 104 | 105 | virtual const char* what() const throw () 106 | { 107 | return m_message.c_str(); 108 | } 109 | 110 | private: 111 | std::string m_message; 112 | }; 113 | 114 | class LoadFileException : public LoadTileException 115 | { 116 | public: 117 | LoadFileException(std::filesystem::path path, std::string message) 118 | : m_message(std::string("Failed to load file ") + path.string() + ". Reason: " + message) 119 | { 120 | } 121 | 122 | virtual const char* what() const throw () 123 | { 124 | return m_message.c_str(); 125 | } 126 | 127 | private: 128 | std::string m_message; 129 | }; 130 | 131 | class FileNotFoundException : public LoadFileException 132 | { 133 | public: 134 | FileNotFoundException(std::filesystem::path path) 135 | : LoadFileException(path, "File not found") 136 | { 137 | } 138 | }; 139 | 140 | class Disk : public TileLoader, public Cache 141 | { 142 | public: 143 | struct Mutex 144 | { 145 | std::shared_mutex mutex; 146 | 147 | Mutex() 148 | { 149 | } 150 | 151 | Mutex(const Mutex& other) 152 | { 153 | } 154 | 155 | Mutex& operator=(const Mutex& other) 156 | { 157 | return *this; 158 | } 159 | }; 160 | 161 | Disk(std::filesystem::path path, const Layout& layout, int min_zoom, int max_zoom, float wait_after_last_modified = 1.0) 162 | : TileLoader(layout) 163 | , Cache() 164 | , m_min_zoom(min_zoom) 165 | , m_max_zoom(max_zoom) 166 | , m_wait_after_last_modified(wait_after_last_modified) 167 | { 168 | if (path.string().find("{") == std::string::npos) 169 | { 170 | path = path / "{zoom}" / "{x}" / "{y}.jpg"; 171 | } 172 | m_path = path; 173 | } 174 | 175 | int get_min_zoom() const 176 | { 177 | return m_min_zoom; 178 | } 179 | 180 | int get_max_zoom() const 181 | { 182 | return m_max_zoom; 183 | } 184 | 185 | std::filesystem::path get_path(xti::vec2i tile, int zoom) const 186 | { 187 | if (zoom > m_max_zoom) 188 | { 189 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is higher than the maximum zoom level " + XTI_TO_STRING(m_max_zoom) + "."); 190 | } 191 | return replace_placeholders(m_path, this->get_layout(), tile, zoom); 192 | } 193 | 194 | bool contains(xti::vec2i tile, int zoom) const 195 | { 196 | return zoom <= m_max_zoom && std::filesystem::exists(get_path(tile, zoom)); 197 | } 198 | 199 | cv::Mat load(xti::vec2i tile, int zoom) 200 | { 201 | if (zoom > m_max_zoom) 202 | { 203 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is higher than the maximum zoom level " + XTI_TO_STRING(m_max_zoom) + "."); 204 | } 205 | if (zoom < m_min_zoom) 206 | { 207 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is lower than the minimum zoom level " + XTI_TO_STRING(m_min_zoom) + "."); 208 | } 209 | std::shared_lock lock(m_mutex.mutex); 210 | 211 | std::filesystem::path path = get_path(tile, zoom); 212 | if (!std::filesystem::exists(path)) 213 | { 214 | throw FileNotFoundException(path); 215 | } 216 | 217 | auto last_modified_time = std::filesystem::last_write_time(path); 218 | auto now_time = std::filesystem::__file_clock::now(); 219 | auto sleep_duration = std::chrono::duration(m_wait_after_last_modified) - (now_time - last_modified_time); 220 | if (sleep_duration > std::chrono::seconds(0)) 221 | { 222 | std::this_thread::sleep_for(sleep_duration); 223 | } 224 | 225 | cv::Mat image_cv = safe_imread(path); 226 | 227 | try 228 | { 229 | this->to_tile(image_cv, true); 230 | } 231 | catch (LoadTileException ex) 232 | { 233 | throw LoadFileException(path, std::string("Loaded invalid tile. ") + ex.what()); 234 | } 235 | return image_cv; 236 | } 237 | 238 | void save(const cv::Mat& image, xti::vec2i tile, int zoom) 239 | { 240 | if (zoom > m_max_zoom) 241 | { 242 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is higher than the maximum zoom level " + XTI_TO_STRING(m_max_zoom) + "."); 243 | } 244 | if (zoom < m_min_zoom) 245 | { 246 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is lower than the minimum zoom level " + XTI_TO_STRING(m_min_zoom) + "."); 247 | } 248 | std::lock_guard lock(m_mutex.mutex); 249 | 250 | std::filesystem::path path = get_path(tile, zoom); 251 | 252 | std::filesystem::path parent_path = path.parent_path(); 253 | if (!std::filesystem::exists(parent_path)) 254 | { 255 | std::filesystem::create_directories(parent_path); 256 | } 257 | 258 | cv::Mat image_bgr; 259 | cv::cvtColor(image, image_bgr, cv::COLOR_RGB2BGR); 260 | 261 | if (!cv::imwrite(path.string(), image_bgr)) 262 | { 263 | throw WriteFileException(path); 264 | } 265 | } 266 | 267 | std::filesystem::path get_path() const 268 | { 269 | return m_path; 270 | } 271 | 272 | private: 273 | std::filesystem::path m_path; 274 | int m_min_zoom; 275 | int m_max_zoom; 276 | float m_wait_after_last_modified; 277 | Mutex m_mutex; 278 | }; 279 | 280 | } // end of ns tiledwebmaps 281 | -------------------------------------------------------------------------------- /include/tiledwebmaps/geo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace tiledwebmaps::geo { 8 | 9 | static const double EARTH_RADIUS_METERS = 6.378137e6; 10 | 11 | class CompassAxes : public NamedAxes<2> 12 | { 13 | public: 14 | static std::string opposite_of(std::string direction) 15 | { 16 | if (direction == "north") 17 | { 18 | return "south"; 19 | } 20 | else if (direction == "south") 21 | { 22 | return "north"; 23 | } 24 | else if (direction == "east") 25 | { 26 | return "west"; 27 | } 28 | else if (direction == "west") 29 | { 30 | return "east"; 31 | } 32 | else 33 | { 34 | throw std::invalid_argument(XTI_TO_STRING("Got invalid compass direction " << direction)); 35 | } 36 | } 37 | 38 | CompassAxes() 39 | { 40 | } 41 | 42 | CompassAxes(std::string axis1, std::string axis2) 43 | : NamedAxes<2>({{axis1, opposite_of(axis1)}, {axis2, opposite_of(axis2)}}) 44 | { 45 | } 46 | }; 47 | 48 | static const CompassAxes epsg4326_axes("north", "east"); 49 | 50 | inline xti::vec2d move_from_latlon(xti::vec2d latlon, double bearing, double distance) 51 | { 52 | static const double pi = xt::numeric_constants::PI; 53 | bearing = tiledwebmaps::radians(bearing); 54 | 55 | xti::vec2d latlon_rad = tiledwebmaps::radians(latlon); 56 | 57 | double angular_distance = distance / EARTH_RADIUS_METERS; 58 | 59 | double target_lat = std::asin( 60 | std::sin(latlon_rad[0]) * std::cos(angular_distance) + 61 | std::cos(latlon_rad[0]) * std::sin(angular_distance) * std::cos(bearing) 62 | ); 63 | double target_lon = latlon_rad[1] + std::atan2( 64 | std::sin(bearing) * std::sin(angular_distance) * std::cos(latlon_rad[0]), 65 | std::cos(angular_distance) - std::sin(latlon_rad[0]) * std::sin(target_lat) 66 | ); 67 | target_lon = tiledwebmaps::normalize_angle(target_lon); 68 | 69 | return xti::vec2d({target_lat, target_lon}) * (180.0 / pi); 70 | } 71 | 72 | inline xti::vec2d meters_per_deg_at_latlon(xti::vec2d latlon) 73 | { 74 | double distance = 1.0; 75 | xti::vec2d latlon2 = move_from_latlon(move_from_latlon(latlon, 90.0, distance), 0.0, distance); 76 | xti::vec2d diff_deg = xt::abs(latlon - latlon2); 77 | return distance / diff_deg; 78 | // xti::vec2d latlon_rad = tiledwebmaps::radians(latlon); 79 | // double meter_per_deg_lat = 111132.954 - 559.822 * std::cos(2 * latlon_rad(0)) + 1.175 * std::cos(4 * latlon_rad(0)); 80 | // double meter_per_deg_lon = 111132.954 * std::cos(latlon_rad(0)); 81 | // return xti::vec2d({meter_per_deg_lat, meter_per_deg_lon}); 82 | } 83 | 84 | } // end of ns tiledwebmaps::geo 85 | -------------------------------------------------------------------------------- /include/tiledwebmaps/http.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace tiledwebmaps { 15 | 16 | class Http : public TileLoader 17 | { 18 | public: 19 | struct Mutex 20 | { 21 | std::mutex mutex; 22 | 23 | Mutex() 24 | { 25 | } 26 | 27 | Mutex(const Mutex& other) 28 | { 29 | } 30 | 31 | Mutex& operator=(const Mutex& other) 32 | { 33 | return *this; 34 | } 35 | }; 36 | 37 | Http(std::string url, const Layout& layout, int min_zoom, int max_zoom, int retries = 10, float wait_after_error = 1.5, bool verify_ssl = true, std::optional capath = std::optional(), std::optional cafile = std::optional(), std::map header = std::map(), bool allow_multithreading = false) 38 | : TileLoader(layout) 39 | , m_url(url) 40 | , m_min_zoom(min_zoom) 41 | , m_max_zoom(max_zoom) 42 | , m_retries(retries) 43 | , m_wait_after_error(wait_after_error) 44 | , m_verify_ssl(verify_ssl) 45 | , m_capath(capath) 46 | , m_cafile(cafile) 47 | , m_header(header) 48 | , m_allow_multithreading(allow_multithreading) 49 | , m_mutex() 50 | { 51 | } 52 | 53 | int get_min_zoom() const 54 | { 55 | return m_min_zoom; 56 | } 57 | 58 | int get_max_zoom() const 59 | { 60 | return m_max_zoom; 61 | } 62 | 63 | cv::Mat load(xti::vec2i tile, int zoom) 64 | { 65 | if (zoom > m_max_zoom) 66 | { 67 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is higher than the maximum zoom level " + XTI_TO_STRING(m_max_zoom) + "."); 68 | } 69 | if (zoom < m_min_zoom) 70 | { 71 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is lower than the minimum zoom level " + XTI_TO_STRING(m_min_zoom) + "."); 72 | } 73 | auto lock = m_allow_multithreading ? std::unique_lock() : std::unique_lock(m_mutex.mutex); 74 | 75 | std::string url = this->get_url(tile, zoom); 76 | 77 | LoadTileException last_ex; 78 | for (int tries = 0; tries < m_retries; ++tries) 79 | { 80 | if (tries > 0) 81 | { 82 | std::this_thread::sleep_for(std::chrono::duration(m_wait_after_error)); 83 | } 84 | try 85 | { 86 | // Retrieve data from url 87 | curl::curl_easy request; 88 | request.add(url.c_str()); 89 | request.add(1L); 90 | 91 | curl::curl_header curl_header; 92 | for (const auto& pair : m_header) 93 | { 94 | curl_header.add(pair.first + ": " + pair.second); 95 | } 96 | request.add(curl_header.get()); 97 | 98 | std::ostringstream header_stream, body_stream; 99 | curl::curl_ios curl_header_stream(header_stream); 100 | curl::curl_ios curl_body_stream(body_stream); 101 | request.add(curl_header_stream.get_function()); 102 | request.add(curl_header_stream.get_stream()); 103 | request.add(curl_body_stream.get_function()); 104 | request.add(curl_body_stream.get_stream()); 105 | if (!m_verify_ssl) 106 | { 107 | request.add(0); 108 | request.add(0); 109 | } 110 | if (m_capath) 111 | { 112 | request.add(m_capath->string().c_str()); 113 | } 114 | else if (m_cafile) 115 | { 116 | request.add(m_cafile->string().c_str()); 117 | } 118 | 119 | request.perform(); 120 | 121 | // Convert data to image 122 | std::string data = body_stream.str(); 123 | if (data.length() == 0) 124 | { 125 | last_ex = LoadTileException("Failed to download image from url " + url + ". Received no data."); 126 | continue; 127 | } 128 | cv::Mat data_cv(1, data.length(), xti::opencv::pixeltype::get(1), data.data()); 129 | if (data_cv.data == NULL) 130 | { 131 | last_ex = LoadTileException("Failed to download image from url " + url); 132 | continue; 133 | } 134 | cv::Mat image_cv = cv::imdecode(data_cv, cv::IMREAD_COLOR); 135 | if (image_cv.data == NULL) 136 | { 137 | last_ex = LoadTileException("Failed to decode downloaded image from url " + url + ". Received " + XTI_TO_STRING(data.length()) + " bytes: " + data); 138 | continue; 139 | } 140 | try 141 | { 142 | to_tile(image_cv, true); 143 | } 144 | catch (LoadTileException ex) 145 | { 146 | last_ex = LoadTileException(std::string("Downloaded invalid tile. ") + ex.what()); 147 | continue; 148 | } 149 | return image_cv; 150 | } 151 | catch (curl::curl_easy_exception ex) 152 | { 153 | last_ex = LoadTileException(std::string("Failed to download image. Reason: ") + ex.what()); 154 | } 155 | } 156 | throw last_ex; 157 | } 158 | 159 | std::string get_url(xti::vec2i tile, int zoom) const 160 | { 161 | if (zoom > m_max_zoom) 162 | { 163 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is higher than the maximum zoom level " + XTI_TO_STRING(m_max_zoom) + "."); 164 | } 165 | if (zoom < m_min_zoom) 166 | { 167 | throw LoadTileException("Zoom level " + XTI_TO_STRING(zoom) + " is lower than the minimum zoom level " + XTI_TO_STRING(m_min_zoom) + "."); 168 | } 169 | return replace_placeholders(m_url, this->get_layout(), tile, zoom); 170 | } 171 | 172 | private: 173 | std::string m_url; 174 | int m_min_zoom; 175 | int m_max_zoom; 176 | int m_retries; 177 | float m_wait_after_error; 178 | bool m_verify_ssl; 179 | std::optional m_capath; 180 | std::optional m_cafile; 181 | std::map m_header; 182 | bool m_allow_multithreading; 183 | Mutex m_mutex; 184 | }; 185 | 186 | } // end of ns tiledwebmaps 187 | -------------------------------------------------------------------------------- /include/tiledwebmaps/layout.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace tiledwebmaps { 12 | 13 | static const tiledwebmaps::geo::CompassAxes pixel_axes("south", "east"); 14 | 15 | class Layout 16 | { 17 | public: 18 | // https://gist.github.com/tmcw/4954720 19 | static Layout XYZ(std::shared_ptr proj_context) 20 | { 21 | std::shared_ptr crs = std::make_shared(proj_context, "epsg:3857"); 22 | std::shared_ptr epsg4326_to_crs = std::make_shared(std::make_shared(proj_context, "epsg:4326"), crs); 23 | 24 | auto area_of_use = crs->get_area_of_use(); 25 | 26 | xti::vec2d lower_crs = epsg4326_to_crs->transform(area_of_use.lower_latlon); 27 | xti::vec2d upper_crs = epsg4326_to_crs->transform(area_of_use.upper_latlon); 28 | 29 | lower_crs(1) = lower_crs(0); 30 | upper_crs(1) = upper_crs(0); 31 | 32 | xti::vec2d tile_shape_crs = upper_crs - lower_crs; 33 | xti::vec2d origin_crs = lower_crs; 34 | xti::vec2d size_crs = upper_crs - lower_crs; 35 | 36 | return Layout( 37 | std::make_shared(proj_context, "epsg:3857"), 38 | xti::vec2i({256, 256}), 39 | tile_shape_crs, 40 | lower_crs, 41 | upper_crs - lower_crs, 42 | tiledwebmaps::geo::CompassAxes("east", "south") 43 | ); 44 | } 45 | 46 | Layout(std::shared_ptr crs, xti::vec2i tile_shape_px, xti::vec2d tile_shape_crs, xti::vec2d origin_crs, std::optional size_crs, tiledwebmaps::geo::CompassAxes tile_axes) 47 | : Layout(crs, std::make_shared(std::make_shared(crs->get_context(), "epsg:4326"), crs), tile_shape_px, tile_shape_crs, origin_crs, size_crs, tile_axes) 48 | { 49 | } 50 | 51 | Layout(std::shared_ptr crs, std::shared_ptr epsg4326_to_crs, xti::vec2i tile_shape_px, xti::vec2d tile_shape_crs, xti::vec2d origin_crs, std::optional size_crs, tiledwebmaps::geo::CompassAxes tile_axes) 52 | : m_crs(crs) 53 | , m_epsg4326_to_crs(std::move(epsg4326_to_crs)) 54 | , m_tile_shape_px(tile_shape_px) 55 | , m_tile_shape_crs(tile_shape_crs) 56 | , m_origin_crs(origin_crs) 57 | , m_size_crs(size_crs) 58 | , m_tile_axes(tile_axes) 59 | , m_crs_to_tile_axes(crs->get_axes(), tile_axes) 60 | , m_tile_to_pixel_axes(m_tile_axes, pixel_axes) 61 | { 62 | if (m_tile_shape_px(0) != m_tile_shape_px(1)) 63 | { 64 | throw std::runtime_error("tile_shape_px must be square"); 65 | } 66 | if (m_tile_shape_crs(0) != m_tile_shape_crs(1)) 67 | { 68 | throw std::runtime_error("tile_shape_crs must be square"); 69 | } 70 | tiledwebmaps::NamedAxesTransformation crs_to_tile_axes(crs->get_axes(), m_tile_axes); 71 | tiledwebmaps::NamedAxesTransformation tile_to_pixel_axes(m_tile_axes, pixel_axes); 72 | 73 | m_tile_to_crs = tiledwebmaps::ScaledRigid( 74 | crs_to_tile_axes.inverse().get_rotation(), 75 | m_origin_crs, 76 | 1.0 77 | ); 78 | if (xt::any(crs_to_tile_axes.get_rotation() < 0)) 79 | { 80 | m_tile_to_crs.get_translation() += xt::maximum(-crs_to_tile_axes.transform(size_crs.value()), 0.0); 81 | } 82 | 83 | m_tile_to_pixel = tiledwebmaps::ScaledRigid( 84 | tile_to_pixel_axes.get_rotation(), 85 | xti::vec2d({0.0, 0.0}),// xt::maximum(-(scale * tile_to_pixel_axes.transform(xt::abs(crs_to_tile_axes.transform(m_size_crs))) - 1), 0.0), 86 | tile_shape_px(0) 87 | ); 88 | } 89 | 90 | const std::shared_ptr& get_epsg4326_to_crs() const 91 | { 92 | return m_epsg4326_to_crs; 93 | } 94 | 95 | xti::vec2d epsg4326_to_crs(xti::vec2d coords_epsg4326) const 96 | { 97 | return m_epsg4326_to_crs->transform(coords_epsg4326); 98 | } 99 | 100 | xti::vec2d crs_to_epsg4326(xti::vec2d coords_crs) const 101 | { 102 | return m_epsg4326_to_crs->transform_inverse(coords_crs); 103 | } 104 | 105 | xti::vec2d crs_to_tile(xti::vec2d coords_crs, double scale) const 106 | { 107 | return tile_to_crs(scale).transform_inverse(coords_crs); 108 | } 109 | 110 | xti::vec2d crs_to_tile(xti::vec2d coords_crs, int zoom) const 111 | { 112 | double scale = (double) std::pow(2.0, zoom) / m_tile_shape_crs(0); 113 | return crs_to_tile(coords_crs, scale); 114 | } 115 | 116 | tiledwebmaps::ScaledRigid tile_to_crs(double scale) const 117 | { 118 | return tiledwebmaps::ScaledRigid( 119 | m_tile_to_crs.get_rotation(), 120 | m_tile_to_crs.get_translation(), 121 | 1.0 / scale 122 | ); 123 | } 124 | 125 | xti::vec2d tile_to_crs(xti::vec2d coords_tile, double scale) const 126 | { 127 | return tile_to_crs(scale).transform(coords_tile); 128 | } 129 | 130 | xti::vec2d tile_to_crs(xti::vec2d coords_tile, int zoom) const 131 | { 132 | double scale = (double) std::pow(2.0, zoom) / m_tile_shape_crs(0); 133 | return tile_to_crs(coords_tile, scale); 134 | } 135 | 136 | tiledwebmaps::ScaledRigid tile_to_pixel(double scale) const 137 | { 138 | return tiledwebmaps::ScaledRigid( 139 | m_tile_to_pixel.get_rotation(), 140 | m_tile_to_pixel.get_translation(), // xt::maximum(-(scale * m_tile_to_pixel_axes.transform(xt::abs(m_crs_to_tile_axes.transform(m_size_crs))) - 1), 0.0), 141 | m_tile_shape_px(0) 142 | ); 143 | } 144 | 145 | xti::vec2d tile_to_pixel(xti::vec2d coords_tile, double scale) const 146 | { 147 | return tile_to_pixel(scale).transform(coords_tile); 148 | } 149 | 150 | xti::vec2d tile_to_pixel(xti::vec2d coords_tile, int zoom) const 151 | { 152 | double scale = (double) std::pow(2.0, zoom) / m_tile_shape_crs(0); 153 | return tile_to_pixel(coords_tile, scale); 154 | } 155 | 156 | xti::vec2d pixel_to_tile(xti::vec2d coords_pixel, double scale) const 157 | { 158 | return tile_to_pixel(scale).transform_inverse(coords_pixel); 159 | } 160 | 161 | xti::vec2d pixel_to_tile(xti::vec2d coords_pixel, int zoom) const 162 | { 163 | double scale = (double) std::pow(2.0, zoom) / m_tile_shape_crs(0); 164 | return pixel_to_tile(coords_pixel, scale); 165 | } 166 | 167 | template 168 | xti::vec2d epsg4326_to_tile(xti::vec2d coords_epsg4326, T zoom_or_scale) const 169 | { 170 | return crs_to_tile(epsg4326_to_crs(coords_epsg4326), zoom_or_scale); 171 | } 172 | 173 | template 174 | xti::vec2d tile_to_epsg4326(xti::vec2d coords_tile, T zoom_or_scale) const 175 | { 176 | return crs_to_epsg4326(tile_to_crs(coords_tile, zoom_or_scale)); 177 | } 178 | 179 | template 180 | xti::vec2d epsg4326_to_pixel(xti::vec2d coords_epsg4326, T zoom_or_scale) const 181 | { 182 | return tile_to_pixel(epsg4326_to_tile(coords_epsg4326, zoom_or_scale), zoom_or_scale); 183 | } 184 | 185 | template 186 | xti::vec2d pixel_to_epsg4326(xti::vec2d coords_pixel, T zoom_or_scale) const 187 | { 188 | return tile_to_epsg4326(pixel_to_tile(coords_pixel, zoom_or_scale), zoom_or_scale); 189 | } 190 | 191 | template 192 | xti::vec2d pixels_per_meter_at_latlon(xti::vec2d latlon, T zoom_or_scale) const 193 | { 194 | static const double f = 0.1; 195 | xti::vec2d center_tile = epsg4326_to_tile(latlon, zoom_or_scale); 196 | xti::vec2d f_tile_size_deg = xt::abs(tile_to_epsg4326(center_tile + 0.5 * f, zoom_or_scale) - tile_to_epsg4326(center_tile - 0.5 * f, zoom_or_scale)); 197 | xti::vec2d f_tile_size_meter = f_tile_size_deg * tiledwebmaps::geo::meters_per_deg_at_latlon(latlon); 198 | xti::vec2d f_tile_size_px = f * xt::cast(m_tile_shape_px); 199 | xti::vec2d pixels_per_meter = xt::abs(m_tile_to_pixel_axes.transform(f_tile_size_px / f_tile_size_meter)); 200 | return pixels_per_meter; 201 | } 202 | 203 | float get_meridian_convergence(xti::vec2d latlon) const 204 | { 205 | xti::vec2d latlon2({latlon(0) + 0.0001, latlon(1)}); 206 | xti::vec2d crs1 = m_epsg4326_to_crs->transform(latlon); 207 | xti::vec2d crs2 = m_epsg4326_to_crs->transform(latlon2); 208 | xti::vec2d true_north = crs2 - crs1; 209 | xti::vec2d north = m_crs->get_vector("north"); 210 | return angle_between_vectors(north, true_north); 211 | } 212 | 213 | std::shared_ptr get_crs() const 214 | { 215 | return m_crs; 216 | } 217 | 218 | xti::vec2i get_tile_shape_px() const 219 | { 220 | return m_tile_shape_px; 221 | } 222 | 223 | xti::vec2d get_tile_shape_crs() const 224 | { 225 | return m_tile_shape_crs; 226 | } 227 | 228 | xti::vec2d get_origin_crs() const 229 | { 230 | return m_origin_crs; 231 | } 232 | 233 | std::optional get_size_crs() const 234 | { 235 | return m_size_crs; 236 | } 237 | 238 | tiledwebmaps::geo::CompassAxes get_tile_axes() const 239 | { 240 | return m_tile_axes; 241 | } 242 | 243 | bool operator==(const Layout& other) const 244 | { 245 | return *this->m_crs == *other.m_crs && this->m_tile_shape_px == other.m_tile_shape_px && this->m_tile_shape_crs == other.m_tile_shape_crs && this->m_origin_crs == other.m_origin_crs && this->m_size_crs == other.m_size_crs && this->m_tile_axes == other.m_tile_axes; 246 | } 247 | 248 | bool operator!=(const Layout& other) const 249 | { 250 | return !(*this == other); 251 | } 252 | 253 | private: 254 | std::shared_ptr m_crs; 255 | std::shared_ptr m_epsg4326_to_crs; 256 | xti::vec2i m_tile_shape_px; 257 | xti::vec2d m_tile_shape_crs; 258 | xti::vec2d m_origin_crs; 259 | std::optional m_size_crs; 260 | tiledwebmaps::geo::CompassAxes m_tile_axes; 261 | tiledwebmaps::NamedAxesTransformation m_crs_to_tile_axes; 262 | tiledwebmaps::NamedAxesTransformation m_tile_to_pixel_axes; 263 | 264 | tiledwebmaps::ScaledRigid m_tile_to_crs; 265 | tiledwebmaps::ScaledRigid m_tile_to_pixel; 266 | }; 267 | 268 | } // end of ns tiledwebmaps 269 | -------------------------------------------------------------------------------- /include/tiledwebmaps/lru.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace tiledwebmaps { 15 | 16 | class LRU : public Cache 17 | { 18 | public: 19 | using Key = std::tuple; // tile-x, tile-y, zoom 20 | 21 | LRU(int size) 22 | : Cache() 23 | , m_size(size) 24 | { 25 | } 26 | 27 | bool contains(xti::vec2i tile, int zoom) const 28 | { 29 | return m_key_to_tile.count(Key(tile(0), tile(1), zoom)) > 0; 30 | } 31 | 32 | cv::Mat load(xti::vec2i tile, int zoom) 33 | { 34 | std::lock_guard guard(m_mutex); 35 | 36 | Key key(tile(0), tile(1), zoom); 37 | auto key_it = std::find(m_keys.begin(), m_keys.end(), key); 38 | if (key_it == m_keys.end()) 39 | { 40 | throw CacheFailure(); 41 | } 42 | m_keys.erase(key_it); 43 | m_keys.push_back(key); 44 | return m_key_to_tile[key].clone(); 45 | } 46 | 47 | void save(const cv::Mat& image, xti::vec2i tile, int zoom) 48 | { 49 | std::lock_guard guard(m_mutex); 50 | 51 | Key key(tile(0), tile(1), zoom); 52 | auto key_it = std::find(m_keys.begin(), m_keys.end(), key); 53 | if (key_it != m_keys.end()) 54 | { 55 | m_keys.erase(key_it); 56 | } 57 | m_keys.push_back(key); 58 | m_key_to_tile[key] = image.clone(); 59 | if (m_keys.size() > m_size) 60 | { 61 | m_key_to_tile.erase(m_key_to_tile.find(m_keys.front())); 62 | m_keys.pop_front(); 63 | } 64 | if (m_keys.size() > m_size || m_key_to_tile.size() > m_size) 65 | { 66 | std::cout << "Assertion failure in tiledwebmaps::LRU" << std::endl; 67 | exit(-1); 68 | } 69 | } 70 | 71 | private: 72 | int m_size; 73 | std::map m_key_to_tile; 74 | std::list m_keys; 75 | std::mutex m_mutex; 76 | }; 77 | 78 | } // end of ns tiledwebmaps 79 | -------------------------------------------------------------------------------- /include/tiledwebmaps/proj.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace tiledwebmaps::proj { 13 | 14 | class Transformer; 15 | class CRS; 16 | class Context; 17 | 18 | class Exception : public std::exception 19 | { 20 | public: 21 | Exception(std::string message, Context& context); 22 | 23 | Exception(std::string message, std::shared_ptr context) 24 | : Exception(message, *context) 25 | { 26 | } 27 | 28 | Exception(std::string message) 29 | : m_message(message) 30 | { 31 | } 32 | 33 | virtual const char* what() const throw () 34 | { 35 | return m_message.c_str(); 36 | } 37 | 38 | private: 39 | std::string m_message; 40 | }; 41 | 42 | class Context final 43 | { 44 | public: 45 | Context(bool use_default_context = false, std::optional proj_data_path = std::optional()) 46 | { 47 | if (!use_default_context) 48 | { 49 | m_handle = proj_context_create(); 50 | if (!m_handle) 51 | { 52 | throw Exception("Failed to create context."); 53 | } 54 | 55 | if (proj_data_path) 56 | { 57 | const char* paths[1]; 58 | paths[0] = proj_data_path->c_str(); 59 | proj_context_set_search_paths(m_handle, 1, paths); 60 | } 61 | } 62 | else 63 | { 64 | if (proj_data_path) 65 | { 66 | throw Exception("proj_data_path cannot be given for default context"); 67 | } 68 | m_handle = NULL; 69 | } 70 | } 71 | 72 | ~Context() 73 | { 74 | if (m_handle != NULL) 75 | { 76 | proj_context_destroy(m_handle); 77 | m_handle = NULL; 78 | } 79 | } 80 | 81 | Context(const Context&) = delete; 82 | Context& operator=(const Context&) = delete; 83 | 84 | friend class tiledwebmaps::proj::CRS; 85 | friend class tiledwebmaps::proj::Transformer; 86 | friend class Exception; 87 | 88 | private: 89 | PJ_CONTEXT* m_handle; 90 | std::mutex m_mutex; 91 | }; 92 | 93 | Exception::Exception(std::string message, Context& context) 94 | { 95 | std::lock_guard lock(context.m_mutex); 96 | int32_t error = proj_context_errno(context.m_handle); 97 | std::string str; 98 | if (error == 0) 99 | { 100 | str = "Unknown"; 101 | } 102 | else 103 | { 104 | str = proj_context_errno_string(context.m_handle, error); 105 | } 106 | m_message = message + "\nReason: " + str; 107 | } 108 | 109 | class CRS 110 | { 111 | public: 112 | struct AreaOfUse 113 | { 114 | xti::vec2d lower_latlon; 115 | xti::vec2d upper_latlon; 116 | }; 117 | 118 | struct AxisInfo 119 | { 120 | size_t axis_index; 121 | std::string out_name; 122 | std::string out_abbrev; 123 | std::string out_direction; 124 | double out_unit_conv_factor; 125 | std::string out_unit_name; 126 | std::string out_unit_auth_name; 127 | std::string out_unit_code; 128 | }; 129 | 130 | CRS(std::string description) 131 | : CRS(std::make_shared(), description) 132 | { 133 | } 134 | 135 | CRS(std::shared_ptr context, std::string description) 136 | : m_context(context) 137 | , m_description(description) 138 | { 139 | std::lock_guard lock(m_context->m_mutex); 140 | 141 | // Create CRS 142 | PJ* handle = proj_create(context->m_handle, description.c_str()); 143 | if (!handle) 144 | { 145 | throw Exception("Failed to create CRS.", context); 146 | } 147 | m_handle = std::shared_ptr(handle, [](PJ* handle){proj_destroy(handle);}); 148 | 149 | // Create CS 150 | PJ* handle_cs = proj_crs_get_coordinate_system(m_context->m_handle, m_handle.get()); 151 | if (!handle_cs) 152 | { 153 | throw Exception("Failed to acquire cs via proj_crs_get_coordinate_system"); 154 | } 155 | m_handle_cs = std::shared_ptr(handle_cs, [](PJ* handle_cs){proj_destroy(handle_cs);}); 156 | 157 | // Get axes info 158 | int axis_num = proj_cs_get_axis_count(m_context->m_handle, m_handle_cs.get()); 159 | if (axis_num < 0) 160 | { 161 | throw Exception("Failed to get axis num via proj_cs_get_axis_count", m_context); 162 | } 163 | if (axis_num != 2) 164 | { 165 | throw Exception(XTI_TO_STRING("Expected number of axes 2, got " << axis_num)); 166 | } 167 | std::vector axes_info; 168 | for (size_t axis_index = 0; axis_index < axis_num; ++axis_index) 169 | { 170 | const char* out_name; 171 | const char* out_abbrev; 172 | const char* out_direction; 173 | double out_unit_conv_factor; 174 | const char* out_unit_name; 175 | const char* out_unit_auth_name; 176 | const char* out_unit_code; 177 | if (!proj_cs_get_axis_info(m_context->m_handle, m_handle_cs.get(), axis_index, &out_name, &out_abbrev, &out_direction, &out_unit_conv_factor, &out_unit_name, &out_unit_auth_name, &out_unit_code)) 178 | { 179 | throw Exception("Failed to get axis info via proj_cs_get_axis_info", m_context); 180 | } 181 | axes_info.push_back(AxisInfo{axis_index, out_name, out_abbrev, out_direction, out_unit_conv_factor, out_unit_name, out_unit_auth_name, out_unit_code}); 182 | } 183 | m_axes = geo::CompassAxes(axes_info[0].out_direction, axes_info[1].out_direction); 184 | 185 | // Get area of use 186 | if (!proj_get_area_of_use(m_context->m_handle, m_handle.get(), &m_area_of_use.lower_latlon(1), &m_area_of_use.lower_latlon(0), &m_area_of_use.upper_latlon(1), &m_area_of_use.upper_latlon(0), NULL)) 187 | { 188 | throw Exception("Failed to get area-of-use.", m_context); 189 | } 190 | } 191 | 192 | std::string get_description() const 193 | { 194 | return m_description; 195 | } 196 | 197 | AreaOfUse get_area_of_use() const 198 | { 199 | return m_area_of_use; 200 | } 201 | 202 | std::shared_ptr get_context() const 203 | { 204 | return m_context; 205 | } 206 | 207 | const geo::CompassAxes& get_axes() const 208 | { 209 | return m_axes; 210 | } 211 | 212 | bool operator==(const CRS& other) const 213 | { 214 | std::lock_guard lock(m_context->m_mutex); 215 | return proj_is_equivalent_to_with_ctx(m_context->m_handle, this->m_handle.get(), other.m_handle.get(), PJ_COMP_EQUIVALENT); 216 | } 217 | 218 | bool operator!=(const CRS& other) const 219 | { 220 | return !(*this == other); 221 | } 222 | 223 | xti::vec2d get_vector(std::string direction) const 224 | { 225 | return m_axes.get_vector(direction); 226 | } 227 | 228 | // float get_meridian_convergence(xti::vec2d latlon) 229 | // { 230 | // return get_factors(latlon).meridian_convergence; // radians 231 | // } 232 | 233 | friend class Transformer; 234 | 235 | private: 236 | std::shared_ptr m_context; 237 | std::string m_description; 238 | std::shared_ptr m_handle; 239 | std::shared_ptr m_handle_cs; 240 | AreaOfUse m_area_of_use; 241 | geo::CompassAxes m_axes; 242 | 243 | // PJ_FACTORS get_factors(xti::vec2d latlon) const 244 | // { 245 | // std::lock_guard lock(m_context->m_mutex); 246 | // PJ_COORD input_proj = proj_coord(tiledwebmaps::radians(latlon(1)), tiledwebmaps::radians(latlon(0)), 0, 0); 247 | // PJ_FACTORS factors = proj_factors(m_handle.get(), input_proj); 248 | 249 | // return factors; 250 | // } 251 | }; 252 | 253 | class Transformer 254 | { 255 | public: 256 | struct ParamCRS 257 | { 258 | std::function(std::shared_ptr)> get; 259 | 260 | ParamCRS(std::shared_ptr crs) 261 | : get([crs](std::shared_ptr context){return crs;}) 262 | { 263 | } 264 | 265 | ParamCRS(std::string desc) 266 | : get([desc](std::shared_ptr context){return std::make_shared(context, desc);}) 267 | { 268 | } 269 | 270 | ParamCRS(const char* desc) 271 | : ParamCRS(std::string(desc)) 272 | { 273 | } 274 | }; 275 | 276 | Transformer(ParamCRS from_crs, ParamCRS to_crs) 277 | : Transformer(from_crs.get(std::make_shared())->get_context(), from_crs, to_crs) 278 | { 279 | } 280 | 281 | Transformer(std::shared_ptr context, ParamCRS from_crs, ParamCRS to_crs) 282 | : m_context(context) 283 | , m_from_crs(from_crs.get(context)) 284 | , m_to_crs(to_crs.get(context)) 285 | , m_axes_transformation(NamedAxesTransformation(m_from_crs->get_axes(), m_to_crs->get_axes())) 286 | { 287 | std::lock_guard lock(m_context->m_mutex); 288 | PJ* handle = proj_create_crs_to_crs_from_pj(context->m_handle, m_from_crs->m_handle.get(), m_to_crs->m_handle.get(), NULL, NULL); 289 | if (!handle) 290 | { 291 | throw Exception("Failed to create Transformer.", context); 292 | } 293 | m_handle = std::shared_ptr(handle, [](PJ* handle){proj_destroy(handle);}); 294 | } 295 | 296 | xti::vec2d transform(xti::vec2d input) const 297 | { 298 | return transform(input, PJ_FWD); 299 | } 300 | 301 | double transform_angle(double angle) const 302 | { 303 | return rotation_matrix_to_angle(xt::linalg::dot(m_axes_transformation.get_rotation(), angle_to_rotation_matrix(angle))); 304 | } 305 | 306 | xti::vec2d transform_inverse(xti::vec2d input) const 307 | { 308 | return transform(input, PJ_INV); 309 | } 310 | 311 | double transform_angle_inverse(double angle) const 312 | { 313 | return rotation_matrix_to_angle(xt::linalg::dot(xt::transpose(m_axes_transformation.get_rotation(), {1, 0}), angle_to_rotation_matrix(angle))); 314 | } 315 | 316 | xti::vec2d operator()(xti::vec2d input) const 317 | { 318 | return transform(input); 319 | } 320 | 321 | std::shared_ptr get_context() const 322 | { 323 | return m_context; 324 | } 325 | 326 | std::shared_ptr get_from_crs() const 327 | { 328 | return m_from_crs; 329 | } 330 | 331 | std::shared_ptr get_to_crs() const 332 | { 333 | return m_to_crs; 334 | } 335 | 336 | std::shared_ptr inverse() const 337 | { 338 | return std::make_shared(m_context, m_to_crs, m_from_crs); 339 | } 340 | 341 | bool operator==(const Transformer& other) const 342 | { 343 | return this->m_from_crs == other.m_from_crs && this->m_to_crs == other.m_to_crs; 344 | } 345 | 346 | bool operator!=(const Transformer& other) const 347 | { 348 | return !(*this == other); 349 | } 350 | 351 | private: 352 | std::shared_ptr m_context; 353 | std::shared_ptr m_from_crs; 354 | std::shared_ptr m_to_crs; 355 | std::shared_ptr m_handle; 356 | NamedAxesTransformation m_axes_transformation; 357 | 358 | xti::vec2d transform(xti::vec2d input, PJ_DIRECTION direction) const 359 | { 360 | std::lock_guard lock(m_context->m_mutex); 361 | PJ_COORD input_proj = proj_coord(input(0), input(1), 0, 0); 362 | PJ_COORD output_proj = proj_trans(m_handle.get(), direction, input_proj); 363 | return xti::vec2d({output_proj.v[0], output_proj.v[1]}); 364 | } 365 | }; 366 | 367 | tiledwebmaps::ScaledRigid eastnorthmeters_at_latlon_to_epsg3857(xti::vec2d latlon, const Transformer& epsg4326_to_epsg3857) 368 | { 369 | double mercator_scale = std::cos(latlon[0] / 180.0 * xt::numeric_constants::PI); 370 | return tiledwebmaps::ScaledRigid(xt::eye(2), xti::vec2d({0, 0}), 1.0 / mercator_scale) 371 | * tiledwebmaps::ScaledRigid(xt::eye(2), epsg4326_to_epsg3857(latlon) * mercator_scale); 372 | } 373 | 374 | tiledwebmaps::ScaledRigid geopose_to_epsg3857(xti::vec2d latlon, double bearing, const Transformer& epsg4326_to_epsg3857) 375 | { 376 | tiledwebmaps::ScaledRigid transform = tiledwebmaps::proj::eastnorthmeters_at_latlon_to_epsg3857(latlon, epsg4326_to_epsg3857); 377 | transform.get_rotation() = tiledwebmaps::angle_to_rotation_matrix(epsg4326_to_epsg3857.transform_angle(tiledwebmaps::radians(bearing))); 378 | return transform; 379 | } 380 | 381 | } // end of ns tiledwebmaps::proj 382 | -------------------------------------------------------------------------------- /include/tiledwebmaps/python.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace py = pybind11; 9 | 10 | namespace pybind11::detail { 11 | 12 | template 13 | struct type_caster> 14 | { 15 | public: 16 | using Type = tiledwebmaps::Rigid; 17 | PYBIND11_TYPE_CASTER(Type, const_name("Rigid")); 18 | 19 | bool load(py::handle src, bool) 20 | { 21 | if (!src || !py::hasattr(src, "rotation") || !py::hasattr(src, "translation")) 22 | { 23 | return false; 24 | } 25 | 26 | xt::xtensor rotation = src.attr("rotation").cast>(); // TODO: assert batchsize == () 27 | xt::xtensor translation = src.attr("translation").cast>(); 28 | 29 | value = tiledwebmaps::Rigid(rotation, translation); 30 | 31 | return true; 32 | } 33 | 34 | static py::handle cast(tiledwebmaps::Rigid src, py::return_value_policy /* policy */, py::handle /* parent */) 35 | { 36 | py::module_ tiledwebmaps = py::module_::import("tiledwebmaps"); 37 | py::object dest = tiledwebmaps.attr("Rigid")(src.get_rotation(), src.get_translation()); 38 | return dest.release(); 39 | } 40 | }; 41 | 42 | template 43 | struct type_caster> 44 | { 45 | public: 46 | using Type = tiledwebmaps::ScaledRigid; 47 | PYBIND11_TYPE_CASTER(Type, const_name("ScaledRigid")); 48 | 49 | bool load(py::handle src, bool) 50 | { 51 | if (!src || !py::hasattr(src, "rotation") || !py::hasattr(src, "translation")) 52 | { 53 | return false; 54 | } 55 | 56 | xt::xtensor rotation = src.attr("rotation").cast>(); 57 | xt::xtensor translation = src.attr("translation").cast>(); 58 | TElementType scale = 1; 59 | if (py::hasattr(src, "scale")) 60 | { 61 | scale = src.attr("scale").cast(); 62 | } 63 | 64 | value = tiledwebmaps::ScaledRigid(rotation, translation, scale); 65 | 66 | return true; 67 | } 68 | 69 | static py::handle cast(tiledwebmaps::ScaledRigid src, py::return_value_policy /* policy */, py::handle /* parent */) 70 | { 71 | py::module_ tiledwebmaps = py::module_::import("tiledwebmaps"); 72 | py::object dest = tiledwebmaps.attr("ScaledRigid")(src.get_rotation(), src.get_translation(), src.get_scale()); 73 | return dest.release(); 74 | } 75 | }; 76 | 77 | } // end of ns pybind11::detail 78 | -------------------------------------------------------------------------------- /include/tiledwebmaps/tiledwebmaps.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | -------------------------------------------------------------------------------- /include/tiledwebmaps/tileloader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace tiledwebmaps { 14 | 15 | class LoadTileException : public std::exception 16 | { 17 | public: 18 | LoadTileException(std::string message) 19 | : m_message(message) 20 | { 21 | } 22 | 23 | LoadTileException() 24 | { 25 | } 26 | 27 | const char* what() const noexcept override 28 | { 29 | return m_message.c_str(); 30 | } 31 | 32 | private: 33 | std::string m_message; 34 | }; 35 | 36 | class TileLoader 37 | { 38 | public: 39 | TileLoader(const Layout& layout) 40 | : m_layout(layout) 41 | { 42 | } 43 | 44 | virtual ~TileLoader() = default; 45 | 46 | virtual cv::Mat load(xti::vec2i tile, int zoom) = 0; 47 | 48 | const Layout& get_layout() const 49 | { 50 | return m_layout; 51 | } 52 | 53 | virtual void make_forksafe() 54 | { 55 | } 56 | 57 | virtual int get_min_zoom() const = 0; 58 | 59 | virtual int get_max_zoom() const = 0; 60 | 61 | int get_zoom(xti::vec2d latlon, float meters_per_pixel) const 62 | { 63 | int zoom = get_min_zoom(); 64 | while (zoom < get_max_zoom() && 1.0 / xt::amax(get_layout().pixels_per_meter_at_latlon(latlon, zoom))() >= 0.5 * meters_per_pixel) 65 | { 66 | zoom++; 67 | } 68 | return zoom; 69 | } 70 | 71 | protected: 72 | void to_tile(cv::Mat& input, bool bgr_to_rgb = true) const 73 | { 74 | xti::vec2i got_tile_shape({(int) input.rows, (int) input.cols}); 75 | if (got_tile_shape != m_layout.get_tile_shape_px()) 76 | { 77 | throw LoadTileException("Expected tile shape " + XTI_TO_STRING(m_layout.get_tile_shape_px()) + ", got tile shape " + XTI_TO_STRING(got_tile_shape)); 78 | } 79 | 80 | if (input.channels() == 3) 81 | { 82 | if (bgr_to_rgb) 83 | { 84 | cv::cvtColor(input, input, cv::COLOR_BGR2RGB); 85 | } 86 | } 87 | else if (input.channels() == 4) 88 | { 89 | if (bgr_to_rgb) 90 | { 91 | cv::cvtColor(input, input, cv::COLOR_BGRA2RGB); 92 | } 93 | else 94 | { 95 | cv::cvtColor(input, input, cv::COLOR_BGRA2BGR); 96 | } 97 | } 98 | else 99 | { 100 | throw LoadTileException("Expected 3 or 4 color channels, got " + std::to_string(input.channels())); 101 | } 102 | } 103 | 104 | private: 105 | Layout m_layout; 106 | }; 107 | 108 | uint64_t get_time() 109 | { 110 | return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); 111 | } 112 | 113 | cv::Mat load(TileLoader& tileloader, xti::vec2i min_tile, xti::vec2i max_tile, int zoom) 114 | { 115 | xti::vec2i tiles_num = max_tile - min_tile; 116 | xti::vec2i pixels_num = xt::abs(tileloader.get_layout().tile_to_pixel(tiles_num, zoom)); 117 | 118 | xti::vec2i corner1 = tileloader.get_layout().tile_to_pixel(min_tile, zoom); 119 | xti::vec2i corner2 = tileloader.get_layout().tile_to_pixel(max_tile, zoom); 120 | xti::vec2i image_min_pixel = xt::minimum(corner1, corner2); 121 | xti::vec2i image_max_pixel = xt::maximum(corner1, corner2); 122 | 123 | cv::Mat image(pixels_num(0), pixels_num(1), CV_8UC3, cv::Scalar(0, 0, 0)); 124 | for (int t0 = min_tile(0); t0 < max_tile(0); t0++) 125 | { 126 | for (int t1 = min_tile(1); t1 < max_tile(1); t1++) 127 | { 128 | xti::vec2i tile({t0, t1}); 129 | cv::Mat tile_image = tileloader.load(tile, zoom); 130 | 131 | xti::vec2i corner1 = tileloader.get_layout().tile_to_pixel(tile, zoom); 132 | xti::vec2i corner2 = tileloader.get_layout().tile_to_pixel(tile + 1, zoom); 133 | xti::vec2i min_pixel = xt::minimum(corner1, corner2) - image_min_pixel; 134 | xti::vec2i max_pixel = xt::maximum(corner1, corner2) - image_min_pixel; 135 | 136 | cv::Rect roi(min_pixel(1), min_pixel(0), max_pixel(1) - min_pixel(1), max_pixel(0) - min_pixel(0)); 137 | cv::Mat image_roi = image(roi); 138 | tile_image.copyTo(image_roi); 139 | } 140 | } 141 | 142 | return image; 143 | } 144 | 145 | cv::Mat load(TileLoader& tileloader, xti::vec2i tile, int zoom) 146 | { 147 | return tileloader.load(tile, zoom); 148 | } 149 | 150 | cv::Mat load_metric(TileLoader& tileloader, xti::vec2d latlon, float bearing, float meters_per_pixel, xti::vec2i shape, int zoom) 151 | { 152 | // Load source image 153 | xti::vec2f dest_pixels = shape; 154 | xti::vec2f dest_meters = dest_pixels * meters_per_pixel; 155 | xti::vec2f src_meters = dest_meters; 156 | xti::vec2f src_pixels_per_meter = tileloader.get_layout().pixels_per_meter_at_latlon(latlon, zoom); 157 | float src_pixels_per_meter1 = 0.5 * (src_pixels_per_meter(0) + src_pixels_per_meter(1)); 158 | src_pixels_per_meter = xti::vec2f({src_pixels_per_meter1, src_pixels_per_meter1}); // TODO: why is this necessary? 159 | xti::vec2f src_pixels = src_meters * src_pixels_per_meter; 160 | 161 | float rotation_factor = std::fmod(tiledwebmaps::radians(bearing), xt::numeric_constants::PI / 2); 162 | if (rotation_factor < 0) 163 | { 164 | rotation_factor += xt::numeric_constants::PI / 2; 165 | } 166 | rotation_factor = std::sqrt(2.0f) * std::sin(rotation_factor + xt::numeric_constants::PI / 4); 167 | src_pixels = src_pixels * rotation_factor; 168 | 169 | xti::vec2d global_center_pixel = tileloader.get_layout().epsg4326_to_pixel(latlon, zoom); 170 | xti::vec2d global_min_pixel = global_center_pixel - src_pixels / 2; 171 | xti::vec2d global_max_pixel = global_center_pixel + src_pixels / 2; 172 | 173 | xti::vec2i global_tile_corner1 = tileloader.get_layout().pixel_to_tile(global_min_pixel, zoom); 174 | xti::vec2i global_tile_corner2 = tileloader.get_layout().pixel_to_tile(global_max_pixel, zoom); 175 | xti::vec2i global_min_tile = xt::minimum(global_tile_corner1, global_tile_corner2); 176 | xti::vec2i global_max_tile = xt::maximum(global_tile_corner1, global_tile_corner2) + 1; 177 | 178 | cv::Mat src_image = load(tileloader, global_min_tile, global_max_tile, zoom); 179 | 180 | if (xt::amin(src_pixels_per_meter)() > 1.0 / meters_per_pixel) 181 | { 182 | double sigma = (xt::amin(src_pixels_per_meter)() * meters_per_pixel - 1) / 2; 183 | size_t kernel_size = static_cast(std::ceil(sigma) * 4) + 1; 184 | cv::GaussianBlur(src_image, src_image, cv::Size(kernel_size, kernel_size), sigma, sigma); 185 | } 186 | 187 | // Sample dest image 188 | xti::vec2d global_srcimagemin_pixel = xt::minimum(tileloader.get_layout().tile_to_pixel(global_min_tile, zoom), tileloader.get_layout().tile_to_pixel(global_max_tile, zoom)); 189 | xti::vec2d destim_center_pixel = xt::cast(shape) / 2; 190 | xti::vec2d srcim_center_pixel = global_center_pixel - global_srcimagemin_pixel; 191 | float angle_dest_to_src = -tiledwebmaps::radians(bearing) + tileloader.get_layout().get_meridian_convergence(latlon); 192 | tiledwebmaps::ScaledRigid dest_to_center; 193 | dest_to_center.get_translation() = -destim_center_pixel; 194 | tiledwebmaps::ScaledRigid dest_pixels_to_meters; 195 | dest_pixels_to_meters.get_scale() = xti::vec2f({meters_per_pixel, meters_per_pixel}); 196 | tiledwebmaps::ScaledRigid rotate_dest_to_src; 197 | rotate_dest_to_src.get_rotation() = tiledwebmaps::angle_to_rotation_matrix(angle_dest_to_src); // TODO: epsg4326_to_epsg....transform_angle()? 198 | tiledwebmaps::ScaledRigid src_meters_to_pixels; 199 | src_meters_to_pixels.get_scale() = src_pixels_per_meter; 200 | tiledwebmaps::ScaledRigid src_from_center; 201 | src_from_center.get_translation() = srcim_center_pixel; 202 | 203 | tiledwebmaps::ScaledRigid transform = src_from_center * src_meters_to_pixels * rotate_dest_to_src * dest_pixels_to_meters * dest_to_center; 204 | xti::mat2f sR = transform.get_rotation(); 205 | for (int r = 0; r < 2; r++) 206 | { 207 | for (int c = 0; c < 2; c++) 208 | { 209 | sR(r, c) *= transform.get_scale()(r); 210 | } 211 | } 212 | xti::vec2f t = transform.get_translation(); 213 | 214 | cv::Size newsize((size_t) shape(1), (size_t) shape(0)); 215 | cv::Mat map_x(newsize, CV_32FC1); 216 | cv::Mat map_y(newsize, CV_32FC1); 217 | for (int r = 0; r < shape(0); r++) 218 | { 219 | float point0 = r; 220 | float sR00t0 = sR(0, 0) * point0 + t(0); 221 | float sR10t0 = sR(1, 0) * point0 + t(1); 222 | for (int c = 0; c < shape(1); c++) 223 | { 224 | float point1 = c; 225 | map_y.at(r, c) = sR00t0 + sR(0, 1) * point1; 226 | map_x.at(r, c) = sR10t0 + sR(1, 1) * point1; 227 | } 228 | } 229 | cv::Mat dest_image; 230 | cv::remap(src_image, dest_image, map_x, map_y, cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0)); // BORDER_REPLICATE 231 | 232 | return dest_image; 233 | } 234 | 235 | cv::Mat load_metric(TileLoader& tileloader, xti::vec2d latlon, float bearing, float meters_per_pixel, xti::vec2i shape) 236 | { 237 | return load_metric(tileloader, latlon, bearing, meters_per_pixel, shape, tileloader.get_zoom(latlon, meters_per_pixel)); 238 | } 239 | 240 | std::string replace_placeholders(std::string url, const Layout& layout, xti::vec2i tile, int zoom) 241 | { 242 | xti::vec2d crs_lower = layout.tile_to_crs(tile, zoom); 243 | xti::vec2d crs_upper = layout.tile_to_crs(tile + 1, zoom); 244 | if (crs_lower(0) > crs_upper(0)) 245 | { 246 | std::swap(crs_lower(0), crs_upper(0)); 247 | } 248 | if (crs_lower(1) > crs_upper(1)) 249 | { 250 | std::swap(crs_lower(1), crs_upper(1)); 251 | } 252 | xti::vec2d crs_center = layout.tile_to_crs(tile + 0.5, zoom); 253 | xti::vec2d crs_size = crs_upper - crs_lower; 254 | 255 | xti::vec2d px_lower = layout.tile_to_pixel(tile, zoom); 256 | xti::vec2d px_upper = layout.tile_to_pixel(tile + 1, zoom); 257 | if (px_lower(0) > px_upper(0)) 258 | { 259 | std::swap(px_lower(0), px_upper(0)); 260 | } 261 | if (px_lower(1) > px_upper(1)) 262 | { 263 | std::swap(px_lower(1), px_upper(1)); 264 | } 265 | xti::vec2d px_center = layout.tile_to_pixel(tile + 0.5, zoom); 266 | xti::vec2i px_size = layout.get_tile_shape_px(); 267 | 268 | xti::vec2i tile_lower = tile; 269 | xti::vec2d tile_center = tile + 0.5; 270 | xti::vec2i tile_upper = tile + 1; 271 | 272 | xti::vec2d latlon_lower = layout.tile_to_epsg4326(tile, zoom); 273 | xti::vec2d latlon_upper = layout.tile_to_epsg4326(tile + 1, zoom); 274 | if (latlon_lower(0) > latlon_upper(0)) 275 | { 276 | std::swap(latlon_lower(0), latlon_upper(0)); 277 | } 278 | if (latlon_lower(1) > latlon_upper(1)) 279 | { 280 | std::swap(latlon_lower(1), latlon_upper(1)); 281 | } 282 | xti::vec2d latlon_center = layout.tile_to_epsg4326(tile + 0.5, zoom); 283 | xti::vec2d latlon_size = latlon_upper - latlon_lower; 284 | 285 | std::string quad = ""; 286 | for (int32_t bit = zoom; bit > 0; bit--) 287 | { 288 | char digit = '0'; 289 | auto mask = 1 << (bit - 1); 290 | if ((tile(0) & mask) != 0) 291 | { 292 | digit += 1; 293 | } 294 | if ((tile(1) & mask) != 0) 295 | { 296 | digit += 2; 297 | } 298 | quad += digit; 299 | } 300 | 301 | std::string crs = layout.get_crs()->get_description(); 302 | std::string bbox = std::to_string(crs_lower(0)) + "," + std::to_string(crs_lower(1)) + "," + std::to_string(crs_upper(0)) + "," + std::to_string(crs_upper(1)); 303 | 304 | url = std::regex_replace(url, std::regex("\\{crs_lower_x\\}"), std::to_string(crs_lower(0))); 305 | url = std::regex_replace(url, std::regex("\\{crs_lower_y\\}"), std::to_string(crs_lower(1))); 306 | url = std::regex_replace(url, std::regex("\\{crs_upper_x\\}"), std::to_string(crs_upper(0))); 307 | url = std::regex_replace(url, std::regex("\\{crs_upper_y\\}"), std::to_string(crs_upper(1))); 308 | url = std::regex_replace(url, std::regex("\\{crs_center_x\\}"), std::to_string(crs_center(0))); 309 | url = std::regex_replace(url, std::regex("\\{crs_center_y\\}"), std::to_string(crs_center(1))); 310 | url = std::regex_replace(url, std::regex("\\{crs_size_x\\}"), std::to_string(crs_size(0))); 311 | url = std::regex_replace(url, std::regex("\\{crs_size_y\\}"), std::to_string(crs_size(1))); 312 | 313 | url = std::regex_replace(url, std::regex("\\{px_lower_x\\}"), std::to_string(px_lower(0))); 314 | url = std::regex_replace(url, std::regex("\\{px_lower_y\\}"), std::to_string(px_lower(1))); 315 | url = std::regex_replace(url, std::regex("\\{px_upper_x\\}"), std::to_string(px_upper(0))); 316 | url = std::regex_replace(url, std::regex("\\{px_upper_y\\}"), std::to_string(px_upper(1))); 317 | url = std::regex_replace(url, std::regex("\\{px_center_x\\}"), std::to_string(px_center(0))); 318 | url = std::regex_replace(url, std::regex("\\{px_center_y\\}"), std::to_string(px_center(1))); 319 | url = std::regex_replace(url, std::regex("\\{px_size_x\\}"), std::to_string(px_size(0))); 320 | url = std::regex_replace(url, std::regex("\\{px_size_y\\}"), std::to_string(px_size(1))); 321 | 322 | url = std::regex_replace(url, std::regex("\\{tile_lower_x\\}"), std::to_string(tile_lower(0))); 323 | url = std::regex_replace(url, std::regex("\\{tile_lower_y\\}"), std::to_string(tile_lower(1))); 324 | url = std::regex_replace(url, std::regex("\\{tile_upper_x\\}"), std::to_string(tile_upper(0))); 325 | url = std::regex_replace(url, std::regex("\\{tile_upper_y\\}"), std::to_string(tile_upper(1))); 326 | url = std::regex_replace(url, std::regex("\\{tile_center_x\\}"), std::to_string(tile_center(0))); 327 | url = std::regex_replace(url, std::regex("\\{tile_center_y\\}"), std::to_string(tile_center(1))); 328 | 329 | url = std::regex_replace(url, std::regex("\\{lat_lower\\}"), std::to_string(latlon_lower(0))); 330 | url = std::regex_replace(url, std::regex("\\{lon_lower\\}"), std::to_string(latlon_lower(1))); 331 | url = std::regex_replace(url, std::regex("\\{lat_upper\\}"), std::to_string(latlon_upper(0))); 332 | url = std::regex_replace(url, std::regex("\\{lon_upper\\}"), std::to_string(latlon_upper(1))); 333 | url = std::regex_replace(url, std::regex("\\{lat_center\\}"), std::to_string(latlon_center(0))); 334 | url = std::regex_replace(url, std::regex("\\{lon_center\\}"), std::to_string(latlon_center(1))); 335 | url = std::regex_replace(url, std::regex("\\{lat_size\\}"), std::to_string(latlon_size(0))); 336 | url = std::regex_replace(url, std::regex("\\{lon_size\\}"), std::to_string(latlon_size(1))); 337 | 338 | url = std::regex_replace(url, std::regex("\\{zoom\\}"), std::to_string(zoom)); 339 | url = std::regex_replace(url, std::regex("\\{quad\\}"), quad); 340 | 341 | url = std::regex_replace(url, std::regex("\\{x\\}"), std::to_string(tile_lower(0))); 342 | url = std::regex_replace(url, std::regex("\\{y\\}"), std::to_string(tile_lower(1))); 343 | url = std::regex_replace(url, std::regex("\\{z\\}"), std::to_string(zoom)); 344 | url = std::regex_replace(url, std::regex("\\{width\\}"), std::to_string(px_size(0))); 345 | url = std::regex_replace(url, std::regex("\\{height\\}"), std::to_string(px_size(1))); 346 | url = std::regex_replace(url, std::regex("\\{bbox\\}"), bbox); 347 | url = std::regex_replace(url, std::regex("\\{proj\\}"), crs); 348 | url = std::regex_replace(url, std::regex("\\{crs\\}"), crs); 349 | 350 | return url; 351 | } 352 | 353 | } // end of ns tiledwebmaps 354 | -------------------------------------------------------------------------------- /python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(backend) 2 | 3 | execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/tiledwebmaps) 4 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/tiledwebmaps/__init__.py ${CMAKE_CURRENT_BINARY_DIR}/tiledwebmaps/__init__.py) 5 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/tiledwebmaps/geo.py ${CMAKE_CURRENT_BINARY_DIR}/tiledwebmaps/geo.py) 6 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/tiledwebmaps/presets.py ${CMAKE_CURRENT_BINARY_DIR}/tiledwebmaps/presets.py) 7 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/tiledwebmaps/util.py ${CMAKE_CURRENT_BINARY_DIR}/tiledwebmaps/util.py) 8 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/test ${CMAKE_CURRENT_BINARY_DIR}/test) 9 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/setup.py ${CMAKE_CURRENT_BINARY_DIR}/setup.py) 10 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/setup.cfg ${CMAKE_CURRENT_BINARY_DIR}/setup.cfg) 11 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/../README.md ${CMAKE_CURRENT_BINARY_DIR}/README.md) 12 | execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE ${CMAKE_CURRENT_BINARY_DIR}/LICENSE) 13 | 14 | find_package(Python REQUIRED COMPONENTS Interpreter) 15 | execute_process(COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/copy_proj_data.py ${CMAKE_CURRENT_BINARY_DIR}/tiledwebmaps COMMAND_ERROR_IS_FATAL ANY) 16 | 17 | add_test(NAME pytest WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/python" COMMAND pytest) 18 | -------------------------------------------------------------------------------- /python/backend/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(Python3_FIND_STRATEGY LOCATION) 2 | find_package(Python REQUIRED COMPONENTS NumPy) 3 | find_package(pybind11 REQUIRED) 4 | find_package(xtl REQUIRED) 5 | find_package(xtensor REQUIRED) 6 | find_package(xtensor-python REQUIRED) 7 | 8 | pybind11_add_module(backend main.cpp) 9 | target_link_libraries(backend PUBLIC tiledwebmaps xtensor-python Python::NumPy) 10 | 11 | set_target_properties(backend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/python/tiledwebmaps") 12 | -------------------------------------------------------------------------------- /python/backend/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace py = pybind11; 9 | 10 | thread_local std::shared_ptr proj_context = std::make_shared(); 11 | 12 | thread_local std::shared_ptr epsg4326_to_epsg3857 = std::make_shared( 13 | std::make_shared(proj_context, "epsg:4326"), 14 | std::make_shared(proj_context, "epsg:3857") 15 | ); 16 | thread_local std::shared_ptr epsg3857_to_epsg4326 = epsg4326_to_epsg3857->inverse(); 17 | 18 | PYBIND11_MODULE(backend, m) 19 | { 20 | // ********************************************************************************************** 21 | // ******************************************** PROJ ******************************************** 22 | // ********************************************************************************************** 23 | auto proj = m.def_submodule("proj"); 24 | 25 | py::class_>(proj, "CRS") 26 | .def(py::init([](std::string desc){ 27 | return std::make_shared(proj_context, desc); 28 | }), 29 | py::arg("desc") 30 | ) 31 | .def("get_vector", &tiledwebmaps::proj::CRS::get_vector, 32 | py::arg("name") 33 | ) 34 | .def_property_readonly("area_of_use", [](const tiledwebmaps::proj::CRS& crs){ 35 | auto area_of_use = crs.get_area_of_use(); 36 | return std::make_pair(area_of_use.lower_latlon, area_of_use.upper_latlon); 37 | } 38 | ) 39 | .def(py::pickle( 40 | [](const tiledwebmaps::proj::CRS& x){ // __getstate__ 41 | return py::make_tuple( 42 | x.get_description() 43 | ); 44 | }, 45 | [](py::tuple t){ // __setstate__ 46 | if (t.size() != 1) 47 | { 48 | throw std::runtime_error("Invalid state"); 49 | } 50 | return std::make_shared( 51 | proj_context, 52 | t[0].cast() 53 | ); 54 | } 55 | )) 56 | ; 57 | 58 | py::class_>(proj, "Transformer") 59 | .def(py::init([](std::string from_crs, std::string to_crs){ 60 | return std::make_shared(proj_context, from_crs, to_crs); 61 | }), 62 | py::arg("from_crs"), 63 | py::arg("to_crs") 64 | ) 65 | .def(py::init([](std::shared_ptr from_crs, std::string to_crs){ 66 | return std::make_shared(proj_context, from_crs, to_crs); 67 | }), 68 | py::arg("from_crs"), 69 | py::arg("to_crs") 70 | ) 71 | .def(py::init([](std::string from_crs, std::shared_ptr to_crs){ 72 | return std::make_shared(proj_context, from_crs, to_crs); 73 | }), 74 | py::arg("from_crs"), 75 | py::arg("to_crs") 76 | ) 77 | .def(py::init([](std::shared_ptr from_crs, std::shared_ptr to_crs){ 78 | return std::make_shared(proj_context, from_crs, to_crs); 79 | }), 80 | py::arg("from_crs"), 81 | py::arg("to_crs") 82 | ) 83 | .def("transform", [](const tiledwebmaps::proj::Transformer& transformer, xti::vec2d coords){ 84 | return transformer.transform(coords); 85 | }, 86 | py::arg("coords") 87 | ) 88 | .def("transform_inverse", [](const tiledwebmaps::proj::Transformer& transformer, xti::vec2d coords){ 89 | return transformer.transform_inverse(coords); 90 | }, 91 | py::arg("coords") 92 | ) 93 | .def("transform_angle", [](const tiledwebmaps::proj::Transformer& transformer, double angle){ 94 | return transformer.transform_angle(angle); 95 | }, 96 | py::arg("angle") 97 | ) 98 | .def("transform_angle_inverse", [](const tiledwebmaps::proj::Transformer& transformer, double angle){ 99 | return transformer.transform_angle_inverse(angle); 100 | }, 101 | py::arg("angle") 102 | ) 103 | .def("__call__", [](const tiledwebmaps::proj::Transformer& transformer, xti::vec2d coords){ 104 | return transformer.transform(coords); 105 | }, 106 | py::arg("coords") 107 | ) 108 | .def("inverse", &tiledwebmaps::proj::Transformer::inverse) 109 | .def_property_readonly("from_crs", &tiledwebmaps::proj::Transformer::get_from_crs) 110 | .def_property_readonly("to_crs", &tiledwebmaps::proj::Transformer::get_to_crs) 111 | .def(py::pickle( 112 | [](const tiledwebmaps::proj::Transformer& x){ // __getstate__ 113 | return py::make_tuple( 114 | x.get_from_crs(), 115 | x.get_to_crs() 116 | ); 117 | }, 118 | [](py::tuple t){ // __setstate__ 119 | if (t.size() != 2) 120 | { 121 | throw std::runtime_error("Invalid state"); 122 | } 123 | return std::make_shared( 124 | proj_context, 125 | t[0].cast>(), 126 | t[1].cast>() 127 | ); 128 | } 129 | )) 130 | ; 131 | 132 | proj.def("eastnorthmeters_at_latlon_to_epsg3857", [](xti::vec2d latlon){ 133 | return tiledwebmaps::proj::eastnorthmeters_at_latlon_to_epsg3857(latlon, *epsg4326_to_epsg3857).to_matrix(); 134 | }, 135 | py::arg("latlon") 136 | ); 137 | proj.def("geopose_to_epsg3857", [](xti::vec2d latlon, double bearing){ 138 | return tiledwebmaps::proj::geopose_to_epsg3857(latlon, bearing, *epsg4326_to_epsg3857).to_matrix(); 139 | }, 140 | py::arg("latlon"), 141 | py::arg("bearing") 142 | ); 143 | 144 | proj.attr("__setattr__")("epsg4326_to_epsg3857", epsg4326_to_epsg3857); 145 | proj.attr("__setattr__")("epsg3857_to_epsg4326", epsg3857_to_epsg4326); 146 | 147 | 148 | 149 | 150 | // ********************************************************************************************** 151 | // ***************************************** TILEDWEBMAPS *************************************** 152 | // ********************************************************************************************** 153 | py::class_>(m, "NamedAxes2") 154 | .def(py::init([](std::pair axis1, std::pair axis2){ 155 | return tiledwebmaps::NamedAxes<2>({{axis1.first, axis1.second}, {axis2.first, axis2.second}}); 156 | }), 157 | py::arg("axis1"), 158 | py::arg("axis2") 159 | ) 160 | .def("__getitem__", [](const tiledwebmaps::NamedAxes<2>& axes, size_t index){return axes[index];}) 161 | ; 162 | m.def("NamedAxesTransformation", [](tiledwebmaps::NamedAxes<2> axes1, tiledwebmaps::NamedAxes<2> axes2) -> tiledwebmaps::Rotation{ 163 | return tiledwebmaps::NamedAxesTransformation(axes1, axes2); 164 | }); 165 | 166 | auto geo = m.def_submodule("geo"); 167 | py::class_>(geo, "CompassAxes") 168 | .def(py::init(), 169 | py::arg("axis1"), 170 | py::arg("axis2") 171 | ) 172 | .def_property_readonly("axis1", [](const tiledwebmaps::geo::CompassAxes& axes){return axes[0];}) 173 | .def_property_readonly("axis2", [](const tiledwebmaps::geo::CompassAxes& axes){return axes[1];}) 174 | ; 175 | 176 | py::class_>(m, "Layout", py::dynamic_attr()) 177 | .def(py::init, xti::vec2i, xti::vec2d, xti::vec2d, std::optional, tiledwebmaps::geo::CompassAxes>(), 178 | py::arg("crs") = std::make_shared("epsg:3857"), 179 | py::arg("tile_shape_px") = xti::vec2i({256, 256}), 180 | py::arg("tile_shape_crs") = xti::vec2d({1.0, 1.0}), 181 | py::arg("origin_crs") = xti::vec2d({0.0, 0.0}), 182 | py::arg("size_crs") = std::optional(), 183 | py::arg("tile_axes") = tiledwebmaps::geo::CompassAxes("east", "south") 184 | ) 185 | .def("crs_to_tile", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_crs, double scale){ 186 | return layout.crs_to_tile(coords_crs, scale); 187 | }, 188 | py::arg("coords"), 189 | py::arg("scale") 190 | ) 191 | .def("crs_to_tile", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_crs, int zoom){ 192 | return layout.crs_to_tile(coords_crs, zoom); 193 | }, 194 | py::arg("coords"), 195 | py::arg("zoom") 196 | ) 197 | .def("tile_to_crs", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_tile, double scale){ 198 | return layout.tile_to_crs(coords_tile, scale); 199 | }, 200 | py::arg("coords"), 201 | py::arg("scale") 202 | ) 203 | .def("tile_to_crs", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_tile, int zoom){ 204 | return layout.tile_to_crs(coords_tile, zoom); 205 | }, 206 | py::arg("coords"), 207 | py::arg("zoom") 208 | ) 209 | .def("tile_to_pixel", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_tile, double scale){ 210 | return layout.tile_to_pixel(coords_tile, scale); 211 | }, 212 | py::arg("coords"), 213 | py::arg("scale") 214 | ) 215 | .def("tile_to_pixel", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_tile, int zoom){ 216 | return layout.tile_to_pixel(coords_tile, zoom); 217 | }, 218 | py::arg("coords"), 219 | py::arg("zoom") 220 | ) 221 | .def("pixel_to_tile", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_pixel, double scale){ 222 | return layout.pixel_to_tile(coords_pixel, scale); 223 | }, 224 | py::arg("coords"), 225 | py::arg("scale") 226 | ) 227 | .def("pixel_to_tile", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_pixel, int zoom){ 228 | return layout.pixel_to_tile(coords_pixel, zoom); 229 | }, 230 | py::arg("coords"), 231 | py::arg("zoom") 232 | ) 233 | .def("epsg4326_to_tile", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_epsg4326, double scale){ 234 | return layout.epsg4326_to_tile(coords_epsg4326, scale); 235 | }, 236 | py::arg("coords"), 237 | py::arg("scale") 238 | ) 239 | .def("epsg4326_to_tile", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_epsg4326, int zoom){ 240 | return layout.epsg4326_to_tile(coords_epsg4326, zoom); 241 | }, 242 | py::arg("coords"), 243 | py::arg("zoom") 244 | ) 245 | .def("tile_to_epsg4326", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_tile, double scale){ 246 | return layout.tile_to_epsg4326(coords_tile, scale); 247 | }, 248 | py::arg("coords"), 249 | py::arg("scale") 250 | ) 251 | .def("tile_to_epsg4326", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_tile, int zoom){ 252 | return layout.tile_to_epsg4326(coords_tile, zoom); 253 | }, 254 | py::arg("coords"), 255 | py::arg("zoom") 256 | ) 257 | .def("epsg4326_to_pixel", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_epsg4326, double scale){ 258 | return layout.epsg4326_to_pixel(coords_epsg4326, scale); 259 | }, 260 | py::arg("coords"), 261 | py::arg("scale") 262 | ) 263 | .def("epsg4326_to_pixel", [](const tiledwebmaps::Layout& layout, xt::xarray coords_epsg4326, int zoom){ 264 | if (coords_epsg4326.dimension() == 1) 265 | { 266 | coords_epsg4326 = layout.epsg4326_to_pixel(coords_epsg4326, zoom); 267 | } 268 | else if (coords_epsg4326.dimension() == 2) 269 | { 270 | if (coords_epsg4326.shape()[1] != 2) 271 | { 272 | throw std::runtime_error("coords must be a 1D or 2D array with 2 columns"); 273 | } 274 | for (auto i = 0; i < coords_epsg4326.shape()[0]; ++i) 275 | { 276 | xti::vec2d x({coords_epsg4326(i, 0), coords_epsg4326(i, 1)}); 277 | x = layout.epsg4326_to_pixel(x, zoom); 278 | coords_epsg4326(i, 0) = x[0]; 279 | coords_epsg4326(i, 1) = x[1]; 280 | } 281 | } 282 | else 283 | { 284 | throw std::runtime_error("coords must be a 1D or 2D array"); 285 | } 286 | return coords_epsg4326; 287 | }, 288 | py::arg("coords"), 289 | py::arg("zoom") 290 | ) 291 | .def("pixel_to_epsg4326", [](const tiledwebmaps::Layout& layout, xti::vec2d coords_pixel, double scale){ 292 | return layout.pixel_to_epsg4326(coords_pixel, scale); 293 | }, 294 | py::arg("coords"), 295 | py::arg("scale") 296 | ) 297 | .def("pixel_to_epsg4326", [](const tiledwebmaps::Layout& layout, xt::xarray coords_pixel, int zoom){ 298 | if (coords_pixel.dimension() == 1) 299 | { 300 | coords_pixel = layout.pixel_to_epsg4326(coords_pixel, zoom); 301 | } 302 | else if (coords_pixel.dimension() == 2) 303 | { 304 | if (coords_pixel.shape()[1] != 2) 305 | { 306 | throw std::runtime_error("coords must be a 1D or 2D array with 2 columns"); 307 | } 308 | for (auto i = 0; i < coords_pixel.shape()[0]; ++i) 309 | { 310 | xti::vec2d x({coords_pixel(i, 0), coords_pixel(i, 1)}); 311 | x = layout.pixel_to_epsg4326(x, zoom); 312 | coords_pixel(i, 0) = x[0]; 313 | coords_pixel(i, 1) = x[1]; 314 | } 315 | } 316 | else 317 | { 318 | throw std::runtime_error("coords must be a 1D or 2D array"); 319 | } 320 | return coords_pixel; 321 | }, 322 | py::arg("coords"), 323 | py::arg("zoom") 324 | ) 325 | .def("pixels_per_meter_at_latlon", [](const tiledwebmaps::Layout& layout, xti::vec2d latlon, double scale){ 326 | return layout.pixels_per_meter_at_latlon(latlon, scale); 327 | }, 328 | py::arg("latlon"), 329 | py::arg("scale") 330 | ) 331 | .def("pixels_per_meter_at_latlon", [](const tiledwebmaps::Layout& layout, xti::vec2d latlon, int zoom){ 332 | return layout.pixels_per_meter_at_latlon(latlon, zoom); 333 | }, 334 | py::arg("latlon"), 335 | py::arg("zoom") 336 | ) 337 | .def("get_meridian_convergence", &tiledwebmaps::Layout::get_meridian_convergence, 338 | py::arg("latlon") 339 | ) 340 | .def_property_readonly("crs", &tiledwebmaps::Layout::get_crs) 341 | .def_property_readonly("tile_shape_px", &tiledwebmaps::Layout::get_tile_shape_px) 342 | .def_property_readonly("tile_shape_crs", &tiledwebmaps::Layout::get_tile_shape_crs) 343 | .def_property_readonly("origin_crs", &tiledwebmaps::Layout::get_origin_crs) 344 | .def_property_readonly("size_crs", &tiledwebmaps::Layout::get_size_crs) 345 | .def_property_readonly("tile_axes", &tiledwebmaps::Layout::get_tile_axes) 346 | .def_property_readonly("epsg4326_to_crs", &tiledwebmaps::Layout::get_epsg4326_to_crs) 347 | .def_property_readonly("crs_to_epsg4326", [](const tiledwebmaps::Layout& layout){return layout.get_epsg4326_to_crs()->inverse();}) 348 | .def_static("XYZ", [](){ 349 | return tiledwebmaps::Layout::XYZ(proj_context); 350 | }, 351 | "Creates a new XYZ tiles layout.\n" 352 | "\n" 353 | "Uses the epsg:3857 map projection and axis order east-south.\n" 354 | "\n" 355 | "Returns:\n" 356 | " The XYZ tiles layout." 357 | ) 358 | ; 359 | 360 | py::class_>(m, "TileLoader", py::dynamic_attr()) 361 | .def("load", [](tiledwebmaps::TileLoader& tile_loader, xti::vec2s tile, int zoom){ 362 | py::gil_scoped_release gil; 363 | cv::Mat image = tile_loader.load(tile, zoom); 364 | xt::xtensor image2 = xti::from_opencv(image); 365 | return image2; 366 | }, 367 | py::arg("tile"), 368 | py::arg("zoom") 369 | ) 370 | .def("load", [](tiledwebmaps::TileLoader& tile_loader, xti::vec2s min_tile, xti::vec2s max_tile, int zoom){ 371 | py::gil_scoped_release gil; 372 | cv::Mat image = tiledwebmaps::load(tile_loader, min_tile, max_tile, zoom); 373 | xt::xtensor image2 = xti::from_opencv(image); 374 | return image2; 375 | }, 376 | py::arg("min_tile"), 377 | py::arg("max_tile"), 378 | py::arg("zoom") 379 | ) 380 | .def("load", [](tiledwebmaps::TileLoader& tile_loader, xti::vec2d latlon, double bearing, double meters_per_pixel, xti::vec2s shape, std::optional zoom){ 381 | py::gil_scoped_release gil; 382 | cv::Mat image; 383 | if (zoom) 384 | { 385 | image = tiledwebmaps::load_metric(tile_loader, latlon, bearing, meters_per_pixel, shape, *zoom); 386 | } 387 | else 388 | { 389 | image = tiledwebmaps::load_metric(tile_loader, latlon, bearing, meters_per_pixel, shape); 390 | } 391 | xt::xtensor image2 = xti::from_opencv(image); 392 | return image2; 393 | }, 394 | py::arg("latlon"), 395 | py::arg("bearing"), 396 | py::arg("meters_per_pixel"), 397 | py::arg("shape"), 398 | py::arg("zoom") = std::optional(), 399 | "Load an image with the given location, bearing and resolution.\n" 400 | "\n" 401 | "Parameters:\n" 402 | " latlon: Latitude and longitude, center of the returned image\n" 403 | " bearing: Orientation of the returned image, in degrees from north clockwise\n" 404 | " meters_per_pixel: Pixel resolution in meters per pixel\n" 405 | " shape: Shape of the returned image\n" 406 | " zoom: Zoom level at which images are retrieved from the tileloader. If None, chooses the next zoom level above 2 * meters_per_pixel. Defaults to None.\n" 407 | "Returns:\n" 408 | " The loaded image.\n" 409 | ) 410 | .def_property_readonly("layout", &tiledwebmaps::TileLoader::get_layout) 411 | .def("make_forksafe", &tiledwebmaps::TileLoader::make_forksafe) 412 | .def("get_zoom", &tiledwebmaps::TileLoader::get_zoom, 413 | py::arg("latlon"), 414 | py::arg("meters_per_pixel") 415 | ) 416 | .def_property_readonly("max_zoom", &tiledwebmaps::TileLoader::get_max_zoom) 417 | .def_property_readonly("min_zoom", &tiledwebmaps::TileLoader::get_min_zoom) 418 | ; 419 | 420 | py::class_, tiledwebmaps::TileLoader>(m, "Http") 421 | .def(py::init([](std::string url, tiledwebmaps::Layout layout, int min_zoom, int max_zoom, int retries, float wait_after_error, bool verify_ssl, std::optional capath, std::optional cafile, std::map header, bool allow_multithreading){ 422 | if (!capath && !cafile) 423 | { 424 | auto ssl = py::module::import("ssl"); 425 | auto default_verify_paths = ssl.attr("get_default_verify_paths")(); 426 | std::vector capaths; 427 | auto capath_py = default_verify_paths.attr("capath"); 428 | if (!capath_py.is_none()) 429 | { 430 | capaths.push_back(capath_py.cast()); 431 | } 432 | auto openssl_capath_py = default_verify_paths.attr("openssl_capath"); 433 | if (!openssl_capath_py.is_none()) 434 | { 435 | capaths.push_back(openssl_capath_py.cast()); 436 | } 437 | for (auto& capath2 : capaths) 438 | { 439 | if (std::filesystem::exists(capath2)) 440 | { 441 | capath = capath2; 442 | break; 443 | } 444 | } 445 | } 446 | if (!capath && !cafile) 447 | { 448 | auto ssl = py::module::import("ssl"); 449 | auto default_verify_paths = ssl.attr("get_default_verify_paths")(); 450 | std::vector cafiles; 451 | auto cafile_py = default_verify_paths.attr("cafile"); 452 | if (!cafile_py.is_none()) 453 | { 454 | cafiles.push_back(cafile_py.cast()); 455 | } 456 | auto openssl_cafile_py = default_verify_paths.attr("openssl_cafile"); 457 | if (!openssl_cafile_py.is_none()) 458 | { 459 | cafiles.push_back(openssl_cafile_py.cast()); 460 | } 461 | for (auto& cafile2 : cafiles) 462 | { 463 | if (std::filesystem::exists(cafile2)) 464 | { 465 | cafile = cafile2; 466 | break; 467 | } 468 | } 469 | } 470 | return tiledwebmaps::Http(url, layout, min_zoom, max_zoom, retries, wait_after_error, verify_ssl, capath, cafile, header, allow_multithreading); 471 | }), 472 | py::arg("url"), 473 | py::arg("layout"), 474 | py::arg("min_zoom"), 475 | py::arg("max_zoom"), 476 | py::arg("retries") = 10, 477 | py::arg("wait_after_error") = 1.5, 478 | py::arg("verify_ssl") = true, 479 | py::arg("capath") = std::optional(), 480 | py::arg("cafile") = std::optional(), 481 | py::arg("header") = std::map(), 482 | py::arg("allow_multithreading") = false, 483 | "Create an Http tileloader that loads images from the given url.\n" 484 | "\n" 485 | "The url can contain the following placeholders that will be replaced by the parameters of the loaded tile:\n" 486 | " {crs_lower_x} {crs_lower_y}: Lower corner of the tile in the crs of the tileloader\n" 487 | " {crs_upper_x} {crs_upper_y}: Upper corner of the tile in the crs of the tileloader\n" 488 | " {crs_center_x} {crs_center_y}: Center of the tile in the crs of the tileloader\n" 489 | " {crs_size_x} {crs_size_y}: Size of the tile in the crs of the tileloader\n" 490 | " {px_lower_x} {px_lower_y}: Lower corner of the tile in pixels\n" 491 | " {px_upper_x} {px_upper_y}: Upper corner of the tile in pixels\n" 492 | " {px_center_x} {px_center_y}: Center of the tile in pixels\n" 493 | " {px_size_x} {px_size_y}: Size of the tile in pixels\n" 494 | " {tile_lower_x} {tile_lower_y}: Lower corner of the tile in tile coordinates at the given zoom level\n" 495 | " {tile_upper_x} {tile_upper_y}: Upper corner of the tile in tile coordinates at the given zoom level (equal to lower corner plus 1)\n" 496 | " {tile_center_x} {tile_center_y}: Center of the tile in tile coordinates at the given zoom level (equal to lower corner plus 0.5)\n" 497 | " {lat_lower} {lon_lower}: Lower corner of the tile as latitude and longitude\n" 498 | " {lat_upper} {lon_upper}: Upper corner of the tile as latitude and longitude\n" 499 | " {lat_center} {lon_center}: Center of the tile as latitude and longitude\n" 500 | " {lat_size} {lon_size}: Size of the tile as latitude and longitude\n" 501 | " {zoom}: Zoom level of the tile\n" 502 | " {quad}: Tile identifier defined by Bing Maps\n" 503 | " {crs}: String id of the crs\n" 504 | " {x}: Alias for {tile_lower_x}\n" 505 | " {y}: Alias for {tile_lower_y}\n" 506 | " {z}: Alias for {zoom}\n" 507 | " {width}: Alias for {px_size_x}\n" 508 | " {height}: Alias for {px_size_y}\n" 509 | " {proj}: Alias for {crs}\n" 510 | " {bbox}: Alias for {crs_lower_x},{crs_lower_y},{crs_upper_x},{crs_upper_y}" 511 | "\n" 512 | "Parameters:\n" 513 | " url: The string url with placeholders.\n" 514 | " min_zoom: The minimum zoom level that the tileloader will load.\n" 515 | " max_zoom: The maximum zoom level that the tileloader will load.\n" 516 | " layout: The layout of the tiles loaded by this tileloader. Defaults to tiledwebmaps.Layout.XYZ().\n" 517 | " retries: Number of times that the http request will be retried before throwing an error. Defaults to 10.\n" 518 | " wait_after_error: Seconds to wait before retrying the http request. Defaults to 1.5.\n" 519 | " verify_ssl: Whether to verify the ssl host/peer. Defaults to True.\n" 520 | " capath: Set the capath of the curl request if given. Defaults to None.\n" 521 | " cafile: Set the cafile of the curl request if given. Defaults to None.\n" 522 | " header: Header of the curl request. Defaults to {}.\n" 523 | " allow_multithreading: True if multiple threads are allowed to use this tileloader concurrently. Defaults to False.\n" 524 | "\n" 525 | "Returns:\n" 526 | " The created Http tileloader.\n" 527 | ) 528 | ; 529 | 530 | py::class_, tiledwebmaps::TileLoader>(m, "Bin") 531 | .def(py::init([](std::string path, tiledwebmaps::Layout layout){ 532 | return tiledwebmaps::Bin(path, layout); 533 | }), 534 | py::arg("path"), 535 | py::arg("layout") = tiledwebmaps::Layout::XYZ(proj_context) 536 | ) 537 | ; 538 | 539 | py::class_>(m, "Cache") 540 | .def("load", &tiledwebmaps::Cache::load, 541 | py::arg("tile"), 542 | py::arg("zoom") 543 | ) 544 | .def("save", &tiledwebmaps::Cache::save, 545 | py::arg("image"), 546 | py::arg("tile"), 547 | py::arg("zoom") 548 | ) 549 | .def("contains", &tiledwebmaps::Cache::contains, 550 | py::arg("tile"), 551 | py::arg("zoom") 552 | ) 553 | ; 554 | py::class_, tiledwebmaps::TileLoader>(m, "CachedTileLoader", py::dynamic_attr()) 555 | .def(py::init, std::shared_ptr>()) 556 | .def_property_readonly("cache", &tiledwebmaps::CachedTileLoader::get_cache) 557 | ; 558 | 559 | py::class_, tiledwebmaps::TileLoader, tiledwebmaps::Cache>(m, "Disk", py::dynamic_attr()) 560 | .def(py::init(), 561 | py::arg("path"), 562 | py::arg("layout"), 563 | py::arg("min_zoom"), 564 | py::arg("max_zoom"), 565 | py::arg("wait_after_last_modified") = 1.0, 566 | "Returns a new tileloader that loads tiles from disk.\n" 567 | "\n" 568 | "Parameters:\n" 569 | " path: The path to the saved tiles, including placeholders. If it does not include placeholders, appends \"/zoom/x/y.jpg\".\n" 570 | " layout: The layout of the tiles loaded by this tileloader. Defaults to tiledwebmaps.Layout.XYZ().\n" 571 | " min_zoom: The minimum zoom level that the tileloader will load.\n" 572 | " max_zoom: The maximum zoom level that the tileloader will load.\n" 573 | " wait_after_last_modified: Waits this many seconds after the last modification of a tile before loading it. Defaults to 1.0.\n" 574 | "\n" 575 | "Returns:\n" 576 | " A new tileloader that loads tiles from disk.\n" 577 | ) 578 | .def_property_readonly("path", [](const tiledwebmaps::Disk& disk){return disk.get_path().string();}) 579 | ; 580 | m.def("DiskCached", [](std::shared_ptr loader, std::string path, float wait_after_last_modified){ 581 | return std::make_shared(loader, std::make_shared(path, loader->get_layout(), loader->get_min_zoom(), loader->get_max_zoom(), wait_after_last_modified)); 582 | }, 583 | py::arg("loader"), 584 | py::arg("path"), 585 | py::arg("wait_after_last_modified") = 1.0, 586 | "Returns a new tileloader that caches tiles from the given tileloader on disk.\n" 587 | "\n" 588 | "Parameters:\n" 589 | " loader: The tileloader whose tiles will be cached.\n" 590 | " path: The path to where the cached tiles will be saved, including placeholders. If it does not include placeholders, appends \"/zoom/x/y.jpg\".\n" 591 | " wait_after_last_modified: Waits this many seconds after the last modification of a tile before loading it. Defaults to 1.0.\n" 592 | "\n" 593 | "Returns:\n" 594 | " A new tileloader that caches tiles from the given tileloader on disk.\n" 595 | ); 596 | py::class_, tiledwebmaps::Cache>(m, "LRU", py::dynamic_attr()) 597 | .def(py::init(), 598 | py::arg("size") 599 | ) 600 | ; 601 | m.def("LRUCached", [](std::shared_ptr loader, int size){ 602 | return std::make_shared(loader, std::make_shared(size)); 603 | }, 604 | py::arg("loader"), 605 | py::arg("size"), 606 | "Returns a new tileloader that caches tiles from the given tileloader in a LRU (least-recently-used) cache.\n" 607 | "\n" 608 | "Parameters:\n" 609 | " loader: The tileloader whose tiles will be cached.\n" 610 | " size: The maximum number of tiles that will be cached.\n" 611 | "\n" 612 | "Returns:\n" 613 | " A new tileloader that caches tiles from the given tileloader in a LRU cache.\n" 614 | ); 615 | py::class_, tiledwebmaps::TileLoader>(m, "WithDefault", py::dynamic_attr()) 616 | .def(py::init, xti::vec3i>(), 617 | py::arg("loader"), 618 | py::arg("color") = xti::vec3i({255, 255, 255}), 619 | "Returns a new tileloader that returns default tiles with the given color if the given tileloader does not contain a tile.\n" 620 | "\n" 621 | "Parameters:\n" 622 | " loader: The tileloader whose tiles will be returned if they exist.\n" 623 | " color: Color that the default tile will be filled with. Defaults to [255, 255, 255].\n" 624 | "\n" 625 | "Returns:\n" 626 | " A new tileloader that returns default tiles if the given tileloader does not contain a tile.\n" 627 | ) 628 | ; 629 | 630 | 631 | py::register_exception(m, "LoadTileException"); 632 | py::register_exception(m, "WriteFileException"); 633 | py::register_exception(m, "LoadFileException"); 634 | py::register_exception(m, "FileNotFoundException"); 635 | } 636 | -------------------------------------------------------------------------------- /python/build_wheel/build_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -u -x 3 | 4 | BUILD_PLATFORM="manylinux2014_x86_64" 5 | OUTPUT_PATH="$1" 6 | SCRIPTS_PATH="$(dirname "$(readlink -fm "$0")")" 7 | TWM_PATH="$(dirname "$(dirname "$(dirname "$(readlink -fm "$0")")")")" 8 | 9 | echo $TWM_PATH 10 | 11 | mkdir -p $OUTPUT_PATH 12 | 13 | docker pull quay.io/pypa/$BUILD_PLATFORM 14 | 15 | for MINOR in 8 9 10 11 12; do 16 | docker run -t --rm -e BUILD_PLATFORM=$BUILD_PLATFORM -e BUILD_PYTHON_ROOT_PATH=/opt/python/cp3$MINOR-cp3$MINOR -e BUILD_MINOR=$MINOR -v $TWM_PATH:/tiledwebmaps -v $OUTPUT_PATH:/io -v $SCRIPTS_PATH:/scripts quay.io/pypa/$BUILD_PLATFORM sh /scripts/build_in_docker.sh 17 | done 18 | -------------------------------------------------------------------------------- /python/build_wheel/build_in_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -u -x 3 | 4 | $BUILD_PYTHON_ROOT_PATH/bin/python -m pip install cython numpy 5 | 6 | yum install -y openssl-devel libtiff-devel blas-devel lapack-devel 7 | 8 | git clone https://github.com/opencv/opencv && cd opencv && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DBUILD_opencv_python=OFF -DBUILD_opencv_dnn=OFF -DBUILD_opencv_video=OFF -DBUILD_opencv_highgui=OFF -DBUILD_opencv_ml=OFF -DBUILD_opencv_flann=OFF -DBUILD_opencv_video=OFF -DBUILD_opencv_videoio=OFF -DBUILD_opencv_features2d=OFF -DBUILD_opencv_gapi=OFF -DBUILD_opencv_photo=OFF -DCMAKE_CXX_STANDARD=14 -DWITH_CUDA=OFF -DCUDA_FAST_MATH=ON -DBUILD_EXAMPLES=OFF -DBUILD_TESTS=OFF -DBUILD_opencv_apps=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_PROTOBUF=OFF -DWITH_PROTOBUF=OFF -DWITH_VTK=OFF -DWITH_GTK=OFF -DBUILD_JAVA=OFF -DWITH_QUIRC=OFF -DWITH_ADE=OFF .. && make -j32 && make install -j32 && cd ../.. && rm -rf opencv 9 | git clone https://github.com/curl/curl && cd curl && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DCURL_CA_BUNDLE=none -DCURL_CA_PATH=none . && make -j32 && make install -j32 && cd .. && rm -rf curl 10 | git clone https://github.com/JosephP91/curlcpp && cd curlcpp && cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCURLCPP_USE_PKGCONFIG=OFF . && make -j32 && make install -j32 && cd .. && rm -rf curlcpp 11 | git clone https://github.com/pybind/pybind11 && cd pybind11 && cmake -DCMAKE_BUILD_TYPE=Release -DPYBIND11_TEST=OFF -DPYTHON_EXECUTABLE=/usr/local/bin/python3.$BUILD_MINOR -DPYBIND11_PYTHON_VERSION=3.$BUILD_MINOR . && make -j32 && make install -j32 && cd .. && rm -rf pybind11 12 | git clone https://github.com/OSGeo/PROJ && cd PROJ && cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF . && make -j32 && make install -j32 && cd .. && rm -rf PROJ 13 | git clone https://github.com/gulrak/filesystem && cd filesystem && cmake -DCMAKE_BUILD_TYPE=Release -DGHC_FILESYSTEM_BUILD_TESTING=OFF -DGHC_FILESYSTEM_BUILD_EXAMPLES=OFF -DGHC_FILESYSTEM_WITH_INSTALL=ON . && make -j32 && make install -j32 && cd .. && rm -rf filesystem 14 | git clone https://github.com/xtensor-stack/xtl && cd xtl && cmake -DCMAKE_BUILD_TYPE=Release . && make -j32 && make install -j32 && cd .. && rm -rf xtl 15 | git clone https://github.com/xtensor-stack/xtensor && cd xtensor && cmake -DCMAKE_BUILD_TYPE=Release . && make -j32 && make install -j32 && cd .. && rm -rf xtensor 16 | git clone https://github.com/xtensor-stack/xtensor-io && cd xtensor-io && cmake -DCMAKE_BUILD_TYPE=Release . && make -j32 && make install -j32 && cd .. && rm -rf xtensor-io 17 | git clone https://github.com/xtensor-stack/xtensor-python && cd xtensor-python && cmake -DCMAKE_BUILD_TYPE=Release -DPYTHON_EXECUTABLE=/usr/local/bin/python3.$BUILD_MINOR -DPYBIND11_PYTHON_VERSION=3.$BUILD_MINOR . && make -j32 && make install -j32 && cd .. && rm -rf xtensor-python 18 | git clone https://github.com/xtensor-stack/xtensor-blas && cd xtensor-blas && cmake -DCMAKE_BUILD_TYPE=Release . && make -j32 && make install -j32 && cd .. && rm -rf xtensor-blas 19 | git clone https://github.com/catchorg/Catch2 && cd Catch2 && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release .. && make -j32 && make install -j32 && cd ../.. && rm -rf Catch2 20 | git clone https://github.com/fferflo/xtensor-interfaces && cd xtensor-interfaces && cmake -DCMAKE_BUILD_TYPE=Release . && make -j32 && make install -j32 && cd .. && rm -rf xtensor-interfaces 21 | 22 | 23 | cp -r /tiledwebmaps /tiledwebmaps2 24 | 25 | cd /tiledwebmaps2 && mkdir build && cd build 26 | cmake -DCMAKE_BUILD_TYPE=Release -DPython_ROOT_DIR=$BUILD_PYTHON_ROOT_PATH .. 27 | make -j32 28 | cd python 29 | 30 | $BUILD_PYTHON_ROOT_PATH/bin/python setup.py bdist_wheel 31 | rename py3- cp3$BUILD_MINOR- dist/*.whl 32 | auditwheel repair dist/*.whl --plat $BUILD_PLATFORM 33 | cp wheelhouse/* /io -------------------------------------------------------------------------------- /python/copy_proj_data.py: -------------------------------------------------------------------------------- 1 | import shutil, os, sys 2 | 3 | src_proj_path = shutil.which("proj") 4 | assert not src_proj_path is None 5 | src_proj_path = os.path.join(os.path.dirname(os.path.dirname(src_proj_path)), "share", "proj") 6 | assert len(os.listdir(src_proj_path)) > 0 7 | 8 | dest_proj_path = os.path.join(sys.argv[1], "proj_data") 9 | if os.path.isdir(dest_proj_path): 10 | shutil.rmtree(dest_proj_path) 11 | 12 | print(f"Copying from {src_proj_path} to {dest_proj_path}") 13 | shutil.copytree(src_proj_path, dest_proj_path) -------------------------------------------------------------------------------- /python/scripts/download_massgis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse, os, requests, tqdm, pandas, pyunpack, shutil, multiprocessing 4 | 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument("--path", type=str, required=True) 7 | parser.add_argument("--shape", type=int, default=None) 8 | parser.add_argument("--workers", type=int, default=32) 9 | args = parser.parse_args() 10 | 11 | import tiledwebmaps as twm 12 | import xml.etree.ElementTree as ET 13 | from PIL import Image 14 | Image.MAX_IMAGE_PIXELS = None 15 | import imageio.v2 as imageio 16 | import tinypl as pl 17 | 18 | download_path = os.path.join(args.path, "download") 19 | if not os.path.exists(download_path): 20 | os.makedirs(download_path) 21 | 22 | utm18_path = os.path.join(args.path, "utm18") 23 | if not os.path.exists(utm18_path): 24 | os.makedirs(utm18_path) 25 | utm19_path = os.path.join(args.path, "utm19") 26 | if not os.path.exists(utm19_path): 27 | os.makedirs(utm19_path) 28 | 29 | 30 | 31 | # url = "https://s3.us-east-1.amazonaws.com/download.massgis.digital.mass.gov/images/coq2021_15cm_jp2/COQ2021INDEX_POLY.xlsx" 32 | # metafile = os.path.join(download_path, "COQ2021INDEX_POLY.xlsx") 33 | # twm.util.download(url, metafile) 34 | # urls = pandas.read_excel(metafile)["URL"].tolist() 35 | 36 | url = "https://s3.us-east-1.amazonaws.com/download.massgis.digital.mass.gov/images/coq2021_15cm_jp2/COQ2021INDEX_POLY.zip" 37 | metafile = os.path.join(download_path, "COQ2021INDEX_POLY.zip") 38 | twm.util.download(url, metafile) 39 | twm.util.extract(metafile, download_path) 40 | metafile = os.path.join(download_path, "COQ2021INDEX_POLY.dbf") 41 | from dbfread import DBF 42 | table = DBF(metafile, load=True) 43 | # Some urls are broken and contain an invalid tilename. Fix the tilename: 44 | urls = ["/".join(record["URL"].split("/")[:-1]) + "/" + record["TILENAME"] + ".zip" for record in table] 45 | 46 | if args.shape is None: 47 | partition = 1 48 | else: 49 | partition = 10000 // args.shape 50 | if partition * args.shape != 10000: 51 | print("--shape must be a divisor of 10000") 52 | sys.exit(-1) 53 | 54 | shape = (10000 // partition, 10000 // partition) 55 | tile_shape_crs = [1500.0 / partition, 1500.0 / partition] 56 | 57 | layout18 = twm.Layout( 58 | crs=twm.proj.CRS("epsg:6347"), 59 | tile_shape_px=shape, 60 | tile_shape_crs=tile_shape_crs, 61 | tile_axes=twm.geo.CompassAxes("east", "north"), 62 | ) 63 | 64 | layout19 = twm.Layout( 65 | crs=twm.proj.CRS("epsg:6348"), 66 | tile_shape_px=shape, 67 | tile_shape_crs=tile_shape_crs, 68 | tile_axes=twm.geo.CompassAxes("east", "north"), 69 | ) 70 | 71 | import yaml 72 | layout_yaml = { 73 | "crs": "epsg:6347", 74 | "tile_shape_px": [shape[0], shape[1]], 75 | "tile_shape_crs": tile_shape_crs, 76 | "tile_axes": ["east", "north"], 77 | "path": "{zoom}/{x}/{y}.jpg", 78 | "min_zoom": 0, 79 | "max_zoom": 0, 80 | } 81 | with open(os.path.join(utm18_path, "layout.yaml"), "w") as f: 82 | yaml.dump(layout_yaml, f, default_flow_style=False) 83 | 84 | layout_yaml = { 85 | "crs": "epsg:6348", 86 | "tile_shape_px": [shape[0], shape[1]], 87 | "tile_shape_crs": tile_shape_crs, 88 | "tile_axes": ["east", "north"], 89 | "path": "{zoom}/{x}/{y}.jpg", 90 | "min_zoom": 0, 91 | "max_zoom": 0, 92 | } 93 | with open(os.path.join(utm19_path, "layout.yaml"), "w") as f: 94 | yaml.dump(layout_yaml, f, default_flow_style=False) 95 | 96 | print(f"Partitioning into {partition} tiles per side") 97 | 98 | 99 | utm18_to_epsg4326 = twm.proj.Transformer("epsg:6347", "epsg:4326") 100 | utm19_to_epsg4326 = twm.proj.Transformer("epsg:6348", "epsg:4326") 101 | 102 | 103 | pipe = urls 104 | pipe = pl.thread.mutex(pipe) 105 | 106 | lock = multiprocessing.Lock() 107 | lock2 = multiprocessing.Lock() 108 | def process(url): 109 | file = os.path.join(download_path, os.path.basename(url)) 110 | 111 | imagefile = file.replace('.zip', '.jp2') 112 | metafile = f"{imagefile}.aux.xml" 113 | 114 | if not os.path.isfile(metafile) or os.path.isfile(imagefile): 115 | is_utm_18 = file.split("/")[-1].startswith("18") 116 | with lock: 117 | twm.util.download(url, file) 118 | twm.util.extract(file, download_path) 119 | image = imageio.imread(imagefile)[:, :, :3] 120 | 121 | tree = ET.parse(f"{imagefile}.aux.xml") 122 | root = tree.getroot() 123 | 124 | lower_utm = [float(x) for x in list(root.iter("{http://www.opengis.net/gml}lowerCorner"))[0].text.split(" ")] 125 | upper_utm = [float(x) for x in list(root.iter("{http://www.opengis.net/gml}upperCorner"))[0].text.split(" ")] 126 | 127 | utm_to_epsg4326 = utm18_to_epsg4326 if is_utm_18 else utm19_to_epsg4326 128 | 129 | lower_latlon = utm_to_epsg4326(lower_utm) 130 | upper_latlon = utm_to_epsg4326(upper_utm) 131 | latlon = 0.5 * (lower_latlon + upper_latlon) 132 | 133 | for image, tile in twm.util.to_tiles(image, latlon, layout18 if is_utm_18 else layout19, partition): 134 | path = os.path.join(utm18_path if is_utm_18 else utm19_path, "0", f"{tile[0]}") 135 | if not os.path.isdir(path): 136 | with lock2: 137 | if not os.path.isdir(path): 138 | os.makedirs(path) 139 | imageio.imwrite(os.path.join(path, f"{tile[1]}.jpg"), image, quality=100) 140 | 141 | os.remove(imagefile) 142 | pipe = pl.process.map(pipe, process, workers=args.workers) 143 | 144 | for _ in tqdm.tqdm(pipe, total=len(urls), desc="Processing tiles"): 145 | pass 146 | 147 | shutil.rmtree(download_path) 148 | 149 | twm.util.add_zooms(utm18_path, workers=args.workers) 150 | twm.util.add_zooms(utm19_path, workers=args.workers) -------------------------------------------------------------------------------- /python/scripts/download_nconemap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse, os, requests, tqdm, shutil, sys, multiprocessing, cv2 4 | import imageio.v2 as imageio 5 | import tiledwebmaps as twm 6 | from datetime import datetime 7 | import tinypl as pl 8 | import numpy as np 9 | 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument("--path", type=str, required=True) 12 | parser.add_argument("--shape", type=int, default=None) 13 | parser.add_argument("--workers", type=int, default=32) 14 | args = parser.parse_args() 15 | 16 | if shutil.which("gdal_retile.py") is None: 17 | print("This script requires gdal to run. On ubuntu, please run: sudo apt install gdal-bin python3-gdal") 18 | sys.exit(-1) 19 | 20 | if shutil.which("docker") is None: 21 | print("This script requires docker to run. Please install docker.") 22 | sys.exit(-1) 23 | 24 | areas = """ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | """ 126 | areas = [a[a.index(">") + 1:a.index("<", 1)] for a in areas.split("\n") if a.startswith("