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.
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/PKG-INFO +3 -1
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/pyproject.toml +2 -1
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/requirements.txt +4 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/PKG-INFO +3 -1
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/SOURCES.txt +12 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/requires.txt +2 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/app.py +6 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/basemap/basemap_mesh.py +93 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/basemap/basemap_providers.py +100 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/basemap/basemap_tiles.py +312 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/crs/crs_alignment.py +5 -63
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/crs/crs_functions.py +0 -18
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/blueprint_basemap.py +136 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/__init__.py +2 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/build.json +204 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/build.py +110 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/delete.json +30 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap/schemas/delete.py +30 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/blueprint_routes.py +25 -1
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/create/__init__.py +0 -0
- opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/crs/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/blueprint_crs.py +2 -12
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/align.json +2 -15
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/align.py +0 -9
- opengeodeweb_back-6.17.0rc2/tests/test_basemap.py +510 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_crs_routes.py +14 -98
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_routes.py +22 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/LICENSE +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/README.md +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/setup.cfg +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/dependency_links.txt +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/entry_points.txt +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/top_level.txt +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/app_config.py +0 -0
- {opengeodeweb_back-6.17.0rc1/src/opengeodeweb_back/crs → opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/basemap}/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1/src/opengeodeweb_back/routes → opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/crs}/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/crs/crs_assignment.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_functions.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/crs_coordinates.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_brep.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_cross_section.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_edged_curve2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_edged_curve3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_graph.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_grid2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_grid3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_horizon_stack3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_hybrid_solid3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_implicit_cross_section.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_implicit_structural_model.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_light_regular_grid2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_light_regular_grid3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_mesh.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_model.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_object.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_point_set2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_point_set3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_polygonal_surface2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_polygonal_surface3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_polyhedral_solid3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_raster_image2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_raster_image3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_regular_grid2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_regular_grid3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_section.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_solid_mesh3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_structural_model.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_surface_mesh2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_surface_mesh3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_tetrahedral_solid3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_triangulated_surface2d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_triangulated_surface3d.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/geode_objects/geode_vertex_set.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/py.typed +0 -0
- {opengeodeweb_back-6.17.0rc1/src/opengeodeweb_back/routes/create → opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes}/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1/src/opengeodeweb_back/routes/crs → opengeodeweb_back-6.17.0rc2/src/opengeodeweb_back/routes/basemap}/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/blueprint_create.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/edged_curve.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/edged_curve.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/point_set.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/point_set.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/polygonal_surface.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/create/schemas/polygonal_surface.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/active.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/active.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/assign_geographic.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/assign_geographic.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/convert_geographic.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/convert_geographic.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/geographic_list.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/crs/schemas/geographic_list.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/__init__.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/allowed_files.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/allowed_files.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/allowed_objects.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/allowed_objects.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/apply_time_series.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/apply_time_series.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/cell_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/cell_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/cell_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/cell_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/data_missing_files.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/data_missing_files.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/edge_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/edge_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/edge_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/edge_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/export_project.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/export_project.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/geode_object_inheritance.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/geode_object_inheritance.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/geode_objects_and_output_extensions.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/geode_objects_and_output_extensions.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/import_project.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/import_project.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/kill.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/kill.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/ping.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/ping.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polygon_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polygon_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/save_viewable_file.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/save_viewable_file.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/texture_coordinates.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/texture_coordinates.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/time_series_allowed_objects.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/time_series_allowed_objects.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/time_series_missing_files.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/time_series_missing_files.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/upload_file.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/upload_file.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/validate.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/validate.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/vertex_attribute_range.json +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/routes/schemas/vertex_attribute_range.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/test_utils.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/typed_route.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/opengeodeweb_back/utils_functions.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_create_routes.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_events.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_geode_functions.py +0 -0
- {opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/tests/test_typed_routes.py +0 -0
- {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.
|
|
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.
|
|
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
|
{opengeodeweb_back-6.17.0rc1 → opengeodeweb_back-6.17.0rc2}/src/OpenGeodeWeb_Back.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenGeodeWeb-Back
|
|
3
|
-
Version: 6.17.
|
|
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()
|