OpenGeodeWeb-Back 6.17.0rc1__tar.gz → 6.17.0rc2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/PKG-INFO +3 -1
  2. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/pyproject.toml +2 -1
  3. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/requirements.txt +4 -0
  4. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/PKG-INFO +3 -1
  5. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/SOURCES.txt +12 -0
  6. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/requires.txt +2 -0
  7. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/app.py +6 -0
  8. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/basemap/basemap_mesh.py +93 -0
  9. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/basemap/basemap_providers.py +100 -0
  10. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/basemap/basemap_tiles.py +312 -0
  11. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/crs/crs_alignment.py +5 -63
  12. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/crs/crs_functions.py +0 -18
  13. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/blueprint_basemap.py +136 -0
  14. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/__init__.py +2 -0
  15. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/build.json +204 -0
  16. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/build.py +110 -0
  17. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/delete.json +30 -0
  18. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/delete.py +30 -0
  19. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/blueprint_routes.py +25 -1
  20. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/create/__init__.py +0 -0
  21. opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/crs/__init__.py +0 -0
  22. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/blueprint_crs.py +2 -12
  23. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/align.json +2 -15
  24. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/align.py +0 -9
  25. opengeodeweb_back-6.17.0rc2/tests/test_basemap.py +510 -0
  26. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_crs_routes.py +14 -98
  27. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_routes.py +22 -0
  28. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/LICENSE +0 -0
  29. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/README.md +0 -0
  30. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/setup.cfg +0 -0
  31. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/dependency_links.txt +0 -0
  32. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/entry_points.txt +0 -0
  33. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/top_level.txt +0 -0
  34. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/__init__.py +0 -0
  35. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/app_config.py +0 -0
  36. {opengeodeweb_back-6.17.0rc1/src/opengeodeweb_back/crs → opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/basemap}/__init__.py +0 -0
  37. {opengeodeweb_back-6.17.0rc1/src/opengeodeweb_back/routes → opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/crs}/__init__.py +0 -0
  38. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/crs/crs_assignment.py +0 -0
  39. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_functions.py +0 -0
  40. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/__init__.py +0 -0
  41. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/crs_coordinates.py +0 -0
  42. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_brep.py +0 -0
  43. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_cross_section.py +0 -0
  44. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_edged_curve2d.py +0 -0
  45. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_edged_curve3d.py +0 -0
  46. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_graph.py +0 -0
  47. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_grid2d.py +0 -0
  48. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_grid3d.py +0 -0
  49. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_horizon_stack3d.py +0 -0
  50. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_hybrid_solid3d.py +0 -0
  51. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_implicit_cross_section.py +0 -0
  52. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_implicit_structural_model.py +0 -0
  53. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_light_regular_grid2d.py +0 -0
  54. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_light_regular_grid3d.py +0 -0
  55. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_mesh.py +0 -0
  56. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_model.py +0 -0
  57. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_object.py +0 -0
  58. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_point_set2d.py +0 -0
  59. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_point_set3d.py +0 -0
  60. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_polygonal_surface2d.py +0 -0
  61. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_polygonal_surface3d.py +0 -0
  62. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_polyhedral_solid3d.py +0 -0
  63. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_raster_image2d.py +0 -0
  64. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_raster_image3d.py +0 -0
  65. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_regular_grid2d.py +0 -0
  66. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_regular_grid3d.py +0 -0
  67. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_section.py +0 -0
  68. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_solid_mesh3d.py +0 -0
  69. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_structural_model.py +0 -0
  70. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_surface_mesh2d.py +0 -0
  71. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_surface_mesh3d.py +0 -0
  72. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_tetrahedral_solid3d.py +0 -0
  73. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_triangulated_surface2d.py +0 -0
  74. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_triangulated_surface3d.py +0 -0
  75. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_vertex_set.py +0 -0
  76. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/py.typed +0 -0
  77. {opengeodeweb_back-6.17.0rc1/src/opengeodeweb_back/routes/create → opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes}/__init__.py +0 -0
  78. {opengeodeweb_back-6.17.0rc1/src/opengeodeweb_back/routes/crs → opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap}/__init__.py +0 -0
  79. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/blueprint_create.py +0 -0
  80. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/__init__.py +0 -0
  81. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/edged_curve.json +0 -0
  82. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/edged_curve.py +0 -0
  83. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/point_set.json +0 -0
  84. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/point_set.py +0 -0
  85. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/polygonal_surface.json +0 -0
  86. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/polygonal_surface.py +0 -0
  87. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/__init__.py +0 -0
  88. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/active.json +0 -0
  89. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/active.py +0 -0
  90. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/assign_geographic.json +0 -0
  91. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/assign_geographic.py +0 -0
  92. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/convert_geographic.json +0 -0
  93. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/convert_geographic.py +0 -0
  94. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/geographic_list.json +0 -0
  95. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/geographic_list.py +0 -0
  96. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/__init__.py +0 -0
  97. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/allowed_files.json +0 -0
  98. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/allowed_files.py +0 -0
  99. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/allowed_objects.json +0 -0
  100. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/allowed_objects.py +0 -0
  101. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/apply_time_series.json +0 -0
  102. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/apply_time_series.py +0 -0
  103. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/cell_attribute_names.json +0 -0
  104. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/cell_attribute_names.py +0 -0
  105. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/cell_attribute_range.json +0 -0
  106. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/cell_attribute_range.py +0 -0
  107. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/data_missing_files.json +0 -0
  108. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/data_missing_files.py +0 -0
  109. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/edge_attribute_names.json +0 -0
  110. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/edge_attribute_names.py +0 -0
  111. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/edge_attribute_range.json +0 -0
  112. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/edge_attribute_range.py +0 -0
  113. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/export_project.json +0 -0
  114. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/export_project.py +0 -0
  115. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/geode_object_inheritance.json +0 -0
  116. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/geode_object_inheritance.py +0 -0
  117. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/geode_objects_and_output_extensions.json +0 -0
  118. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/geode_objects_and_output_extensions.py +0 -0
  119. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/import_project.json +0 -0
  120. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/import_project.py +0 -0
  121. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/kill.json +0 -0
  122. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/kill.py +0 -0
  123. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.json +0 -0
  124. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.py +0 -0
  125. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_range.json +0 -0
  126. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_range.py +0 -0
  127. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.json +0 -0
  128. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.py +0 -0
  129. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_range.json +0 -0
  130. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_range.py +0 -0
  131. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.json +0 -0
  132. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.py +0 -0
  133. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_range.json +0 -0
  134. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_range.py +0 -0
  135. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.json +0 -0
  136. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.py +0 -0
  137. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_range.json +0 -0
  138. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_range.py +0 -0
  139. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/ping.json +0 -0
  140. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/ping.py +0 -0
  141. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.json +0 -0
  142. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.py +0 -0
  143. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polygon_attribute_range.json +0 -0
  144. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polygon_attribute_range.py +0 -0
  145. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.json +0 -0
  146. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.py +0 -0
  147. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_range.json +0 -0
  148. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_range.py +0 -0
  149. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/save_viewable_file.json +0 -0
  150. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/save_viewable_file.py +0 -0
  151. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/texture_coordinates.json +0 -0
  152. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/texture_coordinates.py +0 -0
  153. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/time_series_allowed_objects.json +0 -0
  154. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/time_series_allowed_objects.py +0 -0
  155. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/time_series_missing_files.json +0 -0
  156. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/time_series_missing_files.py +0 -0
  157. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/upload_file.json +0 -0
  158. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/upload_file.py +0 -0
  159. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/validate.json +0 -0
  160. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/validate.py +0 -0
  161. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.json +0 -0
  162. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.py +0 -0
  163. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/vertex_attribute_range.json +0 -0
  164. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/vertex_attribute_range.py +0 -0
  165. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/test_utils.py +0 -0
  166. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/typed_route.py +0 -0
  167. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/utils_functions.py +0 -0
  168. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_create_routes.py +0 -0
  169. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_events.py +0 -0
  170. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_geode_functions.py +0 -0
  171. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_typed_routes.py +0 -0
  172. {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_utils_functions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenGeodeWeb-Back
3
- Version: 6.17.0rc1
3
+ Version: 6.17.0rc2
4
4
  Summary: OpenGeodeWeb-Back is an open source framework that proposes handy python functions and wrappers for the OpenGeode ecosystem
5
5
  Author-email: Geode-solutions <team-web@geode-solutions.com>
6
6
  Project-URL: Homepage, https://github.com/Geode-solutions/OpenGeodeWeb-Back
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: asgiref==3.*
15
15
  Requires-Dist: blinker==1.*
16
+ Requires-Dist: certifi==2026.7.22
16
17
  Requires-Dist: click==8.*
17
18
  Requires-Dist: defusedxml==0.7.1
18
19
  Requires-Dist: flask[async]==3.*
@@ -28,6 +29,7 @@ Requires-Dist: opengeode-geosciencesio==5.13.1
28
29
  Requires-Dist: opengeode-inspector==6.12.4
29
30
  Requires-Dist: opengeode-inspector-geosciences==1.2.4
30
31
  Requires-Dist: opengeode-io==7.11.2
32
+ Requires-Dist: pillow==13.0.0
31
33
  Requires-Dist: types-defusedxml==0.7.0.20260504
32
34
  Requires-Dist: werkzeug==3.1.8
33
35
  Requires-Dist: opengeodeweb-microservice==1.*,>=1.3.2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "OpenGeodeWeb-Back"
7
- version = "6.17.0-rc.1"
7
+ version = "6.17.0-rc.2"
8
8
  dynamic = ["dependencies"]
9
9
  authors = [{ name = "Geode-solutions", email = "team-web@geode-solutions.com" }]
10
10
  description = "OpenGeodeWeb-Back is an open source framework that proposes handy python functions and wrappers for the OpenGeode ecosystem"
@@ -34,4 +34,5 @@ where = ["src"]
34
34
  "opengeodeweb_back.routes.models.schemas" = ["*.json"]
35
35
  "opengeodeweb_back.routes.create.schemas" = ["*.json"]
36
36
  "opengeodeweb_back.routes.crs.schemas" = ["*.json"]
37
+ "opengeodeweb_back.routes.basemap.schemas" = ["*.json"]
37
38
  "opengeodeweb_back" = ["py.typed"]
@@ -8,6 +8,8 @@ asgiref==3.*
8
8
  # via flask
9
9
  blinker==1.*
10
10
  # via flask
11
+ certifi==2026.7.22
12
+ # via -r requirements.in
11
13
  click==8.*
12
14
  # via flask
13
15
  defusedxml==0.7.1
@@ -62,6 +64,8 @@ opengeode-io==7.11.2
62
64
  # -r requirements.in
63
65
  # geode-viewables
64
66
  # opengeode-geosciencesio
67
+ pillow==13.0.0
68
+ # via -r requirements.in
65
69
  types-defusedxml==0.7.0.20260504
66
70
  # via -r requirements.in
67
71
  werkzeug==3.1.8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenGeodeWeb-Back
3
- Version: 6.17.0rc1
3
+ Version: 6.17.0rc2
4
4
  Summary: OpenGeodeWeb-Back is an open source framework that proposes handy python functions and wrappers for the OpenGeode ecosystem
5
5
  Author-email: Geode-solutions <team-web@geode-solutions.com>
6
6
  Project-URL: Homepage, https://github.com/Geode-solutions/OpenGeodeWeb-Back
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: asgiref==3.*
15
15
  Requires-Dist: blinker==1.*
16
+ Requires-Dist: certifi==2026.7.22
16
17
  Requires-Dist: click==8.*
17
18
  Requires-Dist: defusedxml==0.7.1
18
19
  Requires-Dist: flask[async]==3.*
@@ -28,6 +29,7 @@ Requires-Dist: opengeode-geosciencesio==5.13.1
28
29
  Requires-Dist: opengeode-inspector==6.12.4
29
30
  Requires-Dist: opengeode-inspector-geosciences==1.2.4
30
31
  Requires-Dist: opengeode-io==7.11.2
32
+ Requires-Dist: pillow==13.0.0
31
33
  Requires-Dist: types-defusedxml==0.7.0.20260504
32
34
  Requires-Dist: werkzeug==3.1.8
33
35
  Requires-Dist: opengeodeweb-microservice==1.*,>=1.3.2
@@ -16,6 +16,10 @@ src/opengeodeweb_back/py.typed
16
16
  src/opengeodeweb_back/test_utils.py
17
17
  src/opengeodeweb_back/typed_route.py
18
18
  src/opengeodeweb_back/utils_functions.py
19
+ src/opengeodeweb_back/basemap/__init__.py
20
+ src/opengeodeweb_back/basemap/basemap_mesh.py
21
+ src/opengeodeweb_back/basemap/basemap_providers.py
22
+ src/opengeodeweb_back/basemap/basemap_tiles.py
19
23
  src/opengeodeweb_back/crs/__init__.py
20
24
  src/opengeodeweb_back/crs/crs_alignment.py
21
25
  src/opengeodeweb_back/crs/crs_assignment.py
@@ -58,6 +62,13 @@ src/opengeodeweb_back/geode_objects/geode_triangulated_surface3d.py
58
62
  src/opengeodeweb_back/geode_objects/geode_vertex_set.py
59
63
  src/opengeodeweb_back/routes/__init__.py
60
64
  src/opengeodeweb_back/routes/blueprint_routes.py
65
+ src/opengeodeweb_back/routes/basemap/__init__.py
66
+ src/opengeodeweb_back/routes/basemap/blueprint_basemap.py
67
+ src/opengeodeweb_back/routes/basemap/schemas/__init__.py
68
+ src/opengeodeweb_back/routes/basemap/schemas/build.json
69
+ src/opengeodeweb_back/routes/basemap/schemas/build.py
70
+ src/opengeodeweb_back/routes/basemap/schemas/delete.json
71
+ src/opengeodeweb_back/routes/basemap/schemas/delete.py
61
72
  src/opengeodeweb_back/routes/create/__init__.py
62
73
  src/opengeodeweb_back/routes/create/blueprint_create.py
63
74
  src/opengeodeweb_back/routes/create/schemas/__init__.py
@@ -149,6 +160,7 @@ src/opengeodeweb_back/routes/schemas/vertex_attribute_names.json
149
160
  src/opengeodeweb_back/routes/schemas/vertex_attribute_names.py
150
161
  src/opengeodeweb_back/routes/schemas/vertex_attribute_range.json
151
162
  src/opengeodeweb_back/routes/schemas/vertex_attribute_range.py
163
+ tests/test_basemap.py
152
164
  tests/test_create_routes.py
153
165
  tests/test_crs_routes.py
154
166
  tests/test_events.py
@@ -1,5 +1,6 @@
1
1
  asgiref==3.*
2
2
  blinker==1.*
3
+ certifi==2026.7.22
3
4
  click==8.*
4
5
  defusedxml==0.7.1
5
6
  flask[async]==3.*
@@ -15,6 +16,7 @@ opengeode-geosciencesio==5.13.1
15
16
  opengeode-inspector==6.12.4
16
17
  opengeode-inspector-geosciences==1.2.4
17
18
  opengeode-io==7.11.2
19
+ pillow==13.0.0
18
20
  types-defusedxml==0.7.0.20260504
19
21
  werkzeug==3.1.8
20
22
  opengeodeweb-microservice==1.*,>=1.3.2
@@ -19,6 +19,7 @@ from werkzeug.exceptions import HTTPException
19
19
 
20
20
  from opengeodeweb_back import app_config, utils_functions
21
21
  from opengeodeweb_back.routes import blueprint_routes
22
+ from opengeodeweb_back.routes.basemap import blueprint_basemap
22
23
  from opengeodeweb_back.routes.create import blueprint_create
23
24
  from opengeodeweb_back.routes.crs import blueprint_crs
24
25
 
@@ -177,6 +178,11 @@ def register_ogw_back_blueprints(app: flask.Flask) -> None:
177
178
  url_prefix="/opengeodeweb_back/crs",
178
179
  name="opengeodeweb_crs",
179
180
  )
181
+ app.register_blueprint(
182
+ blueprint_basemap.routes,
183
+ url_prefix="/opengeodeweb_back/basemap",
184
+ name="opengeodeweb_basemap",
185
+ )
180
186
 
181
187
 
182
188
  def run_server(app: Flask) -> None:
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ # Standard library imports
4
+ import math
5
+ from typing import TYPE_CHECKING
6
+
7
+ # Third party imports
8
+ import opengeode as og
9
+ import opengeode_geosciences as og_geosciences
10
+
11
+ # Local application imports
12
+ from opengeodeweb_back.basemap.basemap_tiles import HALF_WORLD
13
+ from opengeodeweb_back.crs.crs_functions import convert_xy_corners
14
+ from opengeodeweb_back.geode_objects.geode_triangulated_surface3d import (
15
+ GeodeTriangulatedSurface3D,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from opengeodeweb_back.basemap.basemap_tiles import TileRange
20
+
21
+ GRID_SIZE = 32
22
+ MESH_NAME = "basemap"
23
+ UV_ATTRIBUTE = "basemap_uv"
24
+ WEB_MERCATOR = og_geosciences.GeographicCoordinateSystemInfo(
25
+ "EPSG", "3857", "WGS 84 / Pseudo-Mercator"
26
+ )
27
+ OUTSIDE_MESSAGE = "Area outside the map projection limits"
28
+
29
+
30
+ class OutsideProjectionError(Exception):
31
+ pass
32
+
33
+
34
+ def bounds_to_3857(
35
+ bounds: tuple[float, float, float, float],
36
+ crs: og_geosciences.GeographicCoordinateSystemInfo,
37
+ ) -> tuple[float, float, float, float]:
38
+ """Bounding box in Web Mercator of the four corners of bounds, given in crs."""
39
+ try:
40
+ corners = convert_xy_corners(bounds, crs, WEB_MERCATOR)
41
+ except RuntimeError as error: # PROJ rejects coordinates outside either CRS domain
42
+ raise OutsideProjectionError(OUTSIDE_MESSAGE) from error
43
+ xs = [x for x, _ in corners]
44
+ ys = [y for _, y in corners]
45
+ projected = (min(xs), min(ys), max(xs), max(ys))
46
+ if any(not abs(value) <= HALF_WORLD for value in projected): # also rejects NaN
47
+ raise OutsideProjectionError(OUTSIDE_MESSAGE)
48
+ return projected
49
+
50
+
51
+ def build_mesh(
52
+ tiles: TileRange,
53
+ crs: og_geosciences.GeographicCoordinateSystemInfo,
54
+ z_value: float,
55
+ ) -> GeodeTriangulatedSurface3D:
56
+ """Grid covering the tiles, with texture coordinates, converted from Web Mercator to crs."""
57
+ x0, y0, x1, y1 = tiles.bounds_3857()
58
+ mesh = GeodeTriangulatedSurface3D()
59
+ builder = mesh.builder()
60
+ builder.set_name(MESH_NAME)
61
+ attributes = mesh.triangulated_surface.vertex_attribute_manager()
62
+ values = og.AttributeValuesArrayDouble2()
63
+ values.default_value = [0.0, 0.0]
64
+ uv_id = attributes.create_attribute_variable_arraydouble2(
65
+ UV_ATTRIBUTE, values, og.AttributeProperties()
66
+ )
67
+ uv = attributes.find_attribute_variable_arraydouble2(uv_id)
68
+ last = GRID_SIZE - 1
69
+ for row in range(GRID_SIZE):
70
+ for column in range(GRID_SIZE):
71
+ u, v = column / last, row / last
72
+ point = og.Point3D([x0 + u * (x1 - x0), y0 + v * (y1 - y0), z_value])
73
+ uv.set_value(builder.create_point(point), [u, v])
74
+ for row in range(last):
75
+ for column in range(last):
76
+ corner = row * GRID_SIZE + column
77
+ builder.create_triangle([corner, corner + 1, corner + GRID_SIZE + 1])
78
+ builder.create_triangle([corner, corner + GRID_SIZE + 1, corner + GRID_SIZE])
79
+ builder.compute_polygon_adjacencies()
80
+ mesh.assign_crs(WEB_MERCATOR.authority_code(), WEB_MERCATOR)
81
+ # Assigning then converting to the same CRS name is rejected by OpenGeode
82
+ if crs.authority_code() == WEB_MERCATOR.authority_code():
83
+ return mesh
84
+ try:
85
+ mesh.convert_crs(crs.authority_code(), crs)
86
+ except RuntimeError as error: # PROJ rejects tiles outside the project CRS domain
87
+ raise OutsideProjectionError(OUTSIDE_MESSAGE) from error
88
+ surface = mesh.triangulated_surface
89
+ for vertex in range(surface.nb_vertices()):
90
+ point = surface.point(vertex)
91
+ if not all(math.isfinite(point.value(axis)) for axis in range(3)):
92
+ raise OutsideProjectionError(OUTSIDE_MESSAGE)
93
+ return mesh
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ # Standard library imports
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Container, Iterable
10
+
11
+ USER_AGENT = "Vease (+https://geode-solutions.com)"
12
+ # Every source is cut in 256 px Web Mercator tiles: they share one tile grid
13
+ TILE_SIZE = 256
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Source:
18
+ url_template: str
19
+ max_zoom: int
20
+ attribution: str
21
+
22
+
23
+ _GEOPLATEFORME = (
24
+ "https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}"
25
+ "&STYLE=normal&TILEMATRIXSET=PM&TILEMATRIX={{z}}&TILEROW={{y}}&TILECOL={{x}}&FORMAT={format}"
26
+ )
27
+ _IGN = "© IGN"
28
+
29
+ SOURCES: dict[str, Source] = {
30
+ "eox_s2cloudless_2016": Source(
31
+ "https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless_3857/default/g/{z}/{y}/{x}.jpg",
32
+ 17,
33
+ "Sentinel-2 cloudless 2016 by EOX IT Services GmbH "
34
+ "(Contains modified Copernicus Sentinel data 2016)",
35
+ ),
36
+ "ign_orthophotos": Source(
37
+ _GEOPLATEFORME.format(layer="ORTHOIMAGERY.ORTHOPHOTOS", format="image/jpeg"), 19, _IGN
38
+ ),
39
+ # Volunteer servers: to be replaced before production, see the basemap design
40
+ "osm": Source(
41
+ "https://tile.openstreetmap.org/{z}/{x}/{y}.png", 19, "© OpenStreetMap contributors"
42
+ ),
43
+ "ign_plan": Source(
44
+ _GEOPLATEFORME.format(layer="GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2", format="image/png"),
45
+ 19,
46
+ _IGN,
47
+ ),
48
+ "ign_roads": Source(
49
+ _GEOPLATEFORME.format(layer="TRANSPORTNETWORKS.ROADS", format="image/png"), 18, _IGN
50
+ ),
51
+ "eox_overlay": Source(
52
+ "https://tiles.maps.eox.at/wmts/1.0.0/overlay_3857/default/g/{z}/{y}/{x}.png",
53
+ 14,
54
+ "© OpenStreetMap contributors, rendering © EOX",
55
+ ),
56
+ }
57
+
58
+ # The layers of each map, from the bottom up. A layer draws the first of its sources serving a
59
+ # tile: the next one is only fetched when it serves none
60
+ MAPS: dict[str, tuple[tuple[str, ...], ...]] = {
61
+ # The IGN covers the world imagery or map where it has tiles (France)
62
+ "satellite": (("eox_s2cloudless_2016",), ("ign_orthophotos",)),
63
+ "plan": (("osm",), ("ign_plan",)),
64
+ # Transparent: both sources would draw the same roads twice, EOX outside France
65
+ "routes": (("ign_roads", "eox_overlay"),),
66
+ }
67
+
68
+
69
+ def url_template(source: str) -> str:
70
+ # Local servers for tests: one variable per source, else one for all where {source} is replaced
71
+ template = os.environ.get(f"BASEMAP_TILE_URL_{source.upper()}")
72
+ if template is not None:
73
+ return template
74
+ shared = os.environ.get("BASEMAP_TILE_URL")
75
+ if shared is not None:
76
+ return shared.replace("{source}", source)
77
+ return SOURCES[source].url_template
78
+
79
+
80
+ def sorted_maps(maps: Iterable[str]) -> list[str]:
81
+ requested = set(maps)
82
+ return [name for name in MAPS if name in requested]
83
+
84
+
85
+ def map_layers(maps: list[str], served: Container[str]) -> dict[str, list[str]]:
86
+ """The served sources of each sorted map, from the bottom up: maps without any are left out."""
87
+ layers = {
88
+ name: [source for layer in MAPS[name] for source in layer if source in served]
89
+ for name in maps
90
+ }
91
+ return {name: sources for name, sources in layers.items() if sources}
92
+
93
+
94
+ def max_zoom(sources: Iterable[str]) -> int:
95
+ # The grid follows the finest source: coarser ones are overzoomed
96
+ return max(SOURCES[source].max_zoom for source in sources)
97
+
98
+
99
+ def attribution(sources: Iterable[str]) -> str:
100
+ return " | ".join(dict.fromkeys(SOURCES[source].attribution for source in sources))
@@ -0,0 +1,312 @@
1
+ from __future__ import annotations
2
+
3
+ # Standard library imports
4
+ import contextlib
5
+ import hashlib
6
+ import http.client
7
+ import io
8
+ import math
9
+ import os
10
+ import ssl
11
+ import tempfile
12
+ import threading
13
+ import time
14
+ import urllib.error
15
+ import urllib.parse
16
+ import urllib.request
17
+ from concurrent.futures import ThreadPoolExecutor, wait
18
+ from dataclasses import dataclass
19
+ from http import HTTPStatus
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING
22
+
23
+ # Third party imports
24
+ import certifi
25
+ from PIL import Image
26
+
27
+ # Local application imports
28
+ from opengeodeweb_back import geode_functions
29
+ from opengeodeweb_back.basemap.basemap_providers import (
30
+ SOURCES,
31
+ TILE_SIZE,
32
+ USER_AGENT,
33
+ url_template,
34
+ )
35
+
36
+ if TYPE_CHECKING:
37
+ from collections.abc import Iterator
38
+
39
+ # Folder of a basemap data holding its tiles: only basemaps have one
40
+ TILES_FOLDER = "tiles"
41
+ HALF_WORLD = 20037508.342789244
42
+ DEFAULT_MAX_PIXELS = 2048
43
+ PNG_SIGNATURE = b"\x89PNG"
44
+ JPEG_QUALITY = 90
45
+ # Cached in place of a tile the server does not have, so it is not asked again
46
+ MISSING_TILE = b"missing"
47
+ FETCH_WORKERS = 8
48
+ MAX_REQUESTS_PER_HOST = 8
49
+ FETCH_TIMEOUT_SECONDS = 10
50
+ # Whole download of a build: a hanging server must not hold the request for minutes
51
+ FETCH_DEADLINE_SECONDS = 30
52
+ # IncompleteRead and other HTTPException are not OSError
53
+ DOWNLOAD_ERRORS = (OSError, http.client.HTTPException)
54
+ # Packaged binaries ship their own OpenSSL, which may not find the system certificates
55
+ SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
56
+
57
+
58
+ _HOST_LIMITS: dict[str, threading.Semaphore] = {}
59
+ _HOST_LIMITS_LOCK = threading.Lock()
60
+
61
+
62
+ def _host_limit(url: str) -> threading.Semaphore:
63
+ # Sources fetched in parallel share the connections their server accepts
64
+ host = urllib.parse.urlparse(url).netloc
65
+ with _HOST_LIMITS_LOCK:
66
+ return _HOST_LIMITS.setdefault(host, threading.Semaphore(MAX_REQUESTS_PER_HOST))
67
+
68
+
69
+ def _tile_span(z: int) -> float:
70
+ return 2 * HALF_WORLD / (1 << z)
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class TileRange:
75
+ z: int
76
+ x_min: int
77
+ x_max: int
78
+ y_min: int
79
+ y_max: int
80
+
81
+ def bounds_3857(self) -> tuple[float, float, float, float]:
82
+ span = _tile_span(self.z)
83
+ # Tile rows grow southward from the north edge of the world
84
+ return (
85
+ -HALF_WORLD + self.x_min * span,
86
+ HALF_WORLD - (self.y_max + 1) * span,
87
+ -HALF_WORLD + (self.x_max + 1) * span,
88
+ HALF_WORLD - self.y_min * span,
89
+ )
90
+
91
+ def tiles(self) -> list[tuple[int, int]]:
92
+ return [
93
+ (x, y)
94
+ for x in range(self.x_min, self.x_max + 1)
95
+ for y in range(self.y_min, self.y_max + 1)
96
+ ]
97
+
98
+
99
+ def _range_at(bounds_3857: tuple[float, float, float, float], z: int) -> TileRange:
100
+ span = _tile_span(z)
101
+ last = (1 << z) - 1
102
+ x_min, y_min, x_max, y_max = bounds_3857
103
+
104
+ def column(x: float) -> int:
105
+ return min(last, max(0, math.floor((x + HALF_WORLD) / span)))
106
+
107
+ def row(y: float) -> int:
108
+ return min(last, max(0, math.floor((HALF_WORLD - y) / span)))
109
+
110
+ return TileRange(z, column(x_min), column(x_max), row(y_max), row(y_min))
111
+
112
+
113
+ def tile_range(
114
+ bounds_3857: tuple[float, float, float, float],
115
+ max_zoom: int,
116
+ max_pixels: int = DEFAULT_MAX_PIXELS,
117
+ ) -> TileRange:
118
+ """Most detailed tile range whose mosaic side stays within max_pixels."""
119
+ best = _range_at(bounds_3857, 0)
120
+ for z in range(1, max_zoom + 1):
121
+ candidate = _range_at(bounds_3857, z)
122
+ columns = candidate.x_max - candidate.x_min + 1
123
+ rows = candidate.y_max - candidate.y_min + 1
124
+ if max(columns, rows) * TILE_SIZE > max_pixels:
125
+ break
126
+ best = candidate
127
+ return best
128
+
129
+
130
+ def _cache_folder() -> Path:
131
+ default = Path(tempfile.gettempdir()) / "opengeodeweb_basemap_cache"
132
+ return Path(os.environ.get("BASEMAP_CACHE_FOLDER", str(default)))
133
+
134
+
135
+ def _is_image(content: bytes) -> bool:
136
+ # Error pages come back as HTTP 200 with a text body
137
+ return content.startswith((PNG_SIGNATURE, b"\xff\xd8"))
138
+
139
+
140
+ def _read_cache(cached: Path) -> bytes | None:
141
+ try:
142
+ content = cached.read_bytes()
143
+ except OSError:
144
+ return None
145
+ # A file broken by a crash or a disk error is downloaded again
146
+ return content if content == MISSING_TILE or _is_image(content) else None
147
+
148
+
149
+ def _write_cache(cached: Path, content: bytes) -> None:
150
+ # Best effort: a tile that cannot be cached is still used
151
+ try:
152
+ cached.parent.mkdir(parents=True, exist_ok=True)
153
+ descriptor, temporary = tempfile.mkstemp(dir=cached.parent, prefix=f".{cached.name}.")
154
+ except OSError:
155
+ return
156
+ try:
157
+ with os.fdopen(descriptor, "wb") as file:
158
+ file.write(content)
159
+ # Atomic: a concurrent build never reads a half-written tile
160
+ Path(temporary).replace(cached)
161
+ except OSError:
162
+ with contextlib.suppress(OSError):
163
+ Path(temporary).unlink()
164
+
165
+
166
+ def _fetch_tile(source: str, z: int, x: int, y: int) -> bytes | None:
167
+ # The template hash keeps tiles of an overridden URL apart
168
+ template = url_template(source)
169
+ template_hash = hashlib.sha256(template.encode()).hexdigest()[:12]
170
+ cached = _cache_folder() / source / template_hash / str(z) / str(x) / str(y)
171
+ cached_content = _read_cache(cached)
172
+ if cached_content is not None:
173
+ return None if cached_content == MISSING_TILE else cached_content
174
+ # Open tile servers ask every application to name itself
175
+ url = template.format(z=z, x=x, y=y)
176
+ request = urllib.request.Request( # noqa: S310 URL from the source table or the environment
177
+ url, headers={"User-Agent": USER_AGENT}
178
+ )
179
+ try:
180
+ with (
181
+ _host_limit(url),
182
+ urllib.request.urlopen( # noqa: S310
183
+ request, timeout=FETCH_TIMEOUT_SECONDS, context=SSL_CONTEXT
184
+ ) as response,
185
+ ):
186
+ content: bytes = response.read()
187
+ except urllib.error.HTTPError as error:
188
+ error.close()
189
+ # Outside its area (the IGN outside France) a server answers 404: unlike a timeout or a
190
+ # network error, asking again would give the same answer
191
+ if error.code == HTTPStatus.NOT_FOUND:
192
+ _write_cache(cached, MISSING_TILE)
193
+ return None
194
+ except DOWNLOAD_ERRORS:
195
+ return None
196
+ if not _is_image(content):
197
+ return None
198
+ _write_cache(cached, content)
199
+ return content
200
+
201
+
202
+ def _overzoomed(
203
+ parent: bytes | None, depth: int, children: list[tuple[int, int]]
204
+ ) -> dict[tuple[int, int], bytes | None]:
205
+ """The sub-squares of a parent tile covering its children, `depth` zooms deeper."""
206
+ if parent is None:
207
+ return dict.fromkeys(children)
208
+ side = TILE_SIZE >> depth
209
+ images: dict[tuple[int, int], bytes | None] = {}
210
+ try:
211
+ # Decoded once for all its children
212
+ with Image.open(io.BytesIO(parent)) as opened:
213
+ transparent = opened.mode in ("RGBA", "LA", "PA") or "transparency" in opened.info
214
+ # Palette and bilevel modes ignore BILINEAR: they become true colour first
215
+ image = opened.convert("RGBA" if transparent else "RGB")
216
+ # Opaque tiles stay JPEG: smaller and faster to encode than PNG, which ignores the quality
217
+ image_format = "PNG" if transparent else "JPEG"
218
+ for x, y in children:
219
+ left = (x % (1 << depth)) * side
220
+ top = (y % (1 << depth)) * side
221
+ part = image.crop((left, top, left + side, top + side)).resize(
222
+ (TILE_SIZE, TILE_SIZE), Image.Resampling.BILINEAR
223
+ )
224
+ output = io.BytesIO()
225
+ part.save(output, format=image_format, quality=JPEG_QUALITY)
226
+ images[(x, y)] = output.getvalue()
227
+ except OSError: # UnidentifiedImageError included
228
+ return dict.fromkeys(children)
229
+ return images
230
+
231
+
232
+ @contextlib.contextmanager
233
+ def _downloads(workers: int) -> Iterator[ThreadPoolExecutor]:
234
+ executor = ThreadPoolExecutor(workers)
235
+ try:
236
+ yield executor
237
+ finally:
238
+ # Past the deadline the requests still running end on their own: the build does not wait
239
+ executor.shutdown(wait=False, cancel_futures=True)
240
+
241
+
242
+ def _source_tiles(
243
+ source: str, tiles: TileRange, downloads: ThreadPoolExecutor, deadline: float
244
+ ) -> dict[tuple[int, int], bytes | None]:
245
+ # Past its max zoom a source has no tile: its parent tiles are enlarged instead
246
+ depth = max(0, tiles.z - SOURCES[source].max_zoom)
247
+ z = tiles.z - depth
248
+ children: dict[tuple[int, int], list[tuple[int, int]]] = {}
249
+ for x, y in tiles.tiles():
250
+ children.setdefault((x >> depth, y >> depth), []).append((x, y))
251
+ futures = {parent: downloads.submit(_fetch_tile, source, z, *parent) for parent in children}
252
+ done, _ = wait(futures.values(), timeout=max(0.0, deadline - time.monotonic()))
253
+ images: dict[tuple[int, int], bytes | None] = {}
254
+ for parent, future in futures.items():
255
+ # An unfinished tile counts as missing
256
+ content = future.result() if future in done else None
257
+ if depth == 0:
258
+ images[parent] = content
259
+ else:
260
+ images |= _overzoomed(content, depth, children[parent])
261
+ return images
262
+
263
+
264
+ def fetch_tiles(source: str, tiles: TileRange) -> dict[tuple[int, int], bytes | None]:
265
+ with _downloads(FETCH_WORKERS) as downloads:
266
+ return _source_tiles(source, tiles, downloads, time.monotonic() + FETCH_DEADLINE_SECONDS)
267
+
268
+
269
+ def _first_served(
270
+ layer: tuple[str, ...], tiles: TileRange, downloads: ThreadPoolExecutor, deadline: float
271
+ ) -> tuple[str, dict[tuple[int, int], bytes | None]] | None:
272
+ for source in layer:
273
+ images = _source_tiles(source, tiles, downloads, deadline)
274
+ if any(images.values()):
275
+ return source, images
276
+ return None
277
+
278
+
279
+ def fetch_layers(
280
+ layers: list[tuple[str, ...]], tiles: TileRange
281
+ ) -> dict[str, dict[tuple[int, int], bytes | None]]:
282
+ """The tiles of the first source serving any in each layer, by source.
283
+
284
+ Every layer at once: the slowest server sets the wait, not the sum of them.
285
+ """
286
+ deadline = time.monotonic() + FETCH_DEADLINE_SECONDS
287
+ with (
288
+ _downloads(FETCH_WORKERS * max(1, len(layers))) as downloads,
289
+ ThreadPoolExecutor(max(1, len(layers))) as executor,
290
+ ):
291
+ served = executor.map(
292
+ lambda layer: _first_served(layer, tiles, downloads, deadline), layers
293
+ )
294
+ return dict(item for item in served if item is not None)
295
+
296
+
297
+ def _extension(content: bytes) -> str:
298
+ # The viewer picks its image reader from the file extension
299
+ return "png" if content.startswith(PNG_SIGNATURE) else "jpg"
300
+
301
+
302
+ def save_tiles(tiles: TileRange, images: dict[tuple[int, int], bytes | None], folder: Path) -> None:
303
+ """Writes each tile as <column>_<row>.<ext>, counted from the north-west corner."""
304
+ folder.mkdir(parents=True, exist_ok=True)
305
+ for (x, y), content in images.items():
306
+ if content is not None:
307
+ name = f"{x - tiles.x_min}_{y - tiles.y_min}.{_extension(content)}"
308
+ (folder / name).write_bytes(content)
309
+
310
+
311
+ def is_basemap(data_id: str) -> bool:
312
+ return Path(geode_functions.data_file_path(data_id, TILES_FOLDER)).is_dir()