├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── rocketchat-notifications-feedback-requests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── COPYRIGHTS.md ├── Dockerfile ├── Jenkinsfile ├── LICENSE.txt ├── README.md ├── config └── sample.config.yaml ├── docker-compose.yml ├── docs ├── api.md ├── configuration.md ├── development_setup.md ├── faq.md ├── img │ ├── UML-Class-Diagram.drawio │ ├── UML-Class-Diagram.png │ ├── UML-Component-Diagram.drawio │ ├── UML-Component-Diagram.png │ ├── UML-Sequence-Diagram.drawio │ ├── UML-Sequence-Diagram.png │ ├── oqt_website_step1.png │ └── oqt_website_step2.png ├── indicator.md ├── indicators.md ├── topic.md └── vector_datasets.md ├── ohsome_quality_api ├── __init__.py ├── api │ ├── __init__.py │ ├── api.py │ ├── request_context.py │ ├── request_models.py │ ├── response_models.py │ └── static │ │ ├── favicon-32x32.png │ │ ├── redoc.standalone.js │ │ ├── swagger-ui-bundle.js │ │ └── swagger-ui.css ├── attributes │ ├── __init__.py │ ├── attributes.yaml │ ├── definitions.py │ └── models.py ├── config.py ├── definitions.py ├── geodatabase │ ├── __init__.py │ ├── client.py │ ├── regions_as_geojson.sql │ ├── select_coverage.sql │ ├── select_intersection.sql │ └── select_shdi.sql ├── indicators │ ├── __init__.py │ ├── attribute_completeness │ │ ├── __init__.py │ │ ├── indicator.py │ │ └── templates.yaml │ ├── base.py │ ├── building_comparison │ │ ├── __init__.py │ │ ├── datasets.yaml │ │ ├── indicator.py │ │ ├── query.sql │ │ └── templates.yaml │ ├── currentness │ │ ├── __init__.py │ │ ├── indicator.py │ │ ├── templates.yaml │ │ └── thresholds.yaml │ ├── definitions.py │ ├── indicators.yaml │ ├── land_cover_thematic_accuracy │ │ ├── __init__.py │ │ ├── indicator.py │ │ ├── query-multi-classes.sql │ │ ├── query-single-class.sql │ │ └── templates.yaml │ ├── mapping_saturation │ │ ├── __init__.py │ │ ├── indicator.py │ │ ├── models.py │ │ ├── ssdoubles.R │ │ └── templates.yaml │ ├── minimal │ │ ├── __init__.py │ │ ├── indicator.py │ │ └── templates.yaml │ ├── models.py │ └── road_comparison │ │ ├── __init__.py │ │ ├── datasets.yaml │ │ ├── indicator.py │ │ ├── query.sql │ │ └── templates.yaml ├── ohsome │ ├── __init__.py │ └── client.py ├── oqt.py ├── projects │ ├── __init__.py │ ├── definitions.py │ ├── models.py │ └── projects.yaml ├── quality_dimensions │ ├── __init__.py │ ├── definitions.py │ ├── models.py │ └── quality_dimensions.yaml ├── topics │ ├── __init__.py │ ├── definitions.py │ ├── models.py │ └── presets.yaml └── utils │ ├── __init__.py │ ├── exceptions.py │ ├── helper.py │ ├── helper_asyncio.py │ ├── helper_geo.py │ └── validators.py ├── pipeline_config.groovy ├── pyproject.toml ├── regression-tests ├── README.md ├── __run_hurl_tests_for_stage.sh ├── building-comparison.hurl ├── building-comparison.json ├── buildingcount_bbox.json ├── buildingcount_bbox_attributecompleteness.hurl ├── buildingcount_bbox_currentness.hurl ├── buildingcount_bbox_housenumber.json ├── hospitals_adminarea.json ├── hospitals_adminarea_mappingsaturation__no_features.hurl ├── long-running │ ├── roads_bbox.json │ └── roads_bbox_currentness.hurl ├── metadata.hurl ├── road-comparison.hurl ├── road-comparison.json ├── roads_polygon.json ├── roads_polygon_attributecompleteness.hurl ├── roads_polygon_mappingsaturation.hurl ├── roads_polygon_maxspeed.json ├── run_hurl_tests_DEV.sh ├── run_hurl_tests_PROD.sh └── run_hurl_tests_TEST.sh ├── scripts ├── create_new_github_release.sh ├── functions.sh ├── release.sh ├── run_mapping_saturation_models.py └── update_swagger_scripts.sh ├── sonar-project.properties ├── tests ├── __init__.py ├── approvals │ └── integrationtests │ │ ├── api │ │ ├── test_indicators.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt │ │ ├── test_indicators.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt │ │ ├── test_indicators.py::test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt │ │ ├── test_indicators.py::test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt │ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers0-schema0].approved.txt │ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers1-schema1].approved.txt │ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt │ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].received.txt │ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt │ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].received.txt │ │ ├── test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers0-schema0].approved.txt │ │ ├── test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers1-schema1].approved.txt │ │ ├── test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt │ │ ├── test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt │ │ ├── test_indicators_land_cover_thematic_accuracy.py-test_invalid_class.approved.json │ │ └── test_indicators_land_cover_thematic_accuracy.py-test_invalid_topic.approved.json │ │ └── indicators │ │ ├── test_attribute_completeness.py-TestCalculation-test_calculate[indicator0].approved.txt │ │ ├── test_attribute_completeness.py-TestCalculation-test_calculate[indicator1].approved.txt │ │ ├── test_attribute_completeness.py-TestFigure-test_create_figure[indicator0].approved.json │ │ ├── test_attribute_completeness.py-TestFigure-test_create_figure[indicator1].approved.json │ │ ├── test_attribute_completeness.py-test_create_description_attribute_filter.approved.txt │ │ ├── test_attribute_completeness.py-test_create_description_attribute_keys_multiple.approved.txt │ │ ├── test_attribute_completeness.py-test_create_description_attribute_keys_single.approved.txt │ │ ├── test_building_comparison.py-TestCalculate-test_calculate.approved.txt │ │ ├── test_building_comparison.py-TestCalculate-test_calculate_above_one_th.approved.txt │ │ ├── test_building_comparison.py-TestCalculate-test_calculate_above_one_th_and_expected.approved.txt │ │ ├── test_building_comparison.py-TestCalculate-test_calculate_no_intersection.approved.txt │ │ ├── test_building_comparison.py-TestCalculate-test_calculate_some_intersection.approved.txt │ │ ├── test_building_comparison.py-TestFigure-test_create_figure.approved.json │ │ ├── test_building_comparison.py-TestFigure-test_create_figure_above_one_th.approved.json │ │ ├── test_building_comparison.py-TestFigure-test_create_figure_building_area_zero.approved.json │ │ ├── test_currentness.py-TestCalculation-test_calculate.approved.txt │ │ ├── test_currentness.py-TestCalculation-test_low_contributions.approved.txt │ │ ├── test_currentness.py-TestCalculation-test_months_without_edit.approved.txt │ │ ├── test_currentness.py-TestCalculation-test_no_amenities.approved.txt │ │ ├── test_currentness.py-TestCalculation-test_no_subway_stations.approved.txt │ │ ├── test_currentness.py-TestFigure-test_create_figure.approved.json │ │ ├── test_currentness.py-TestFigure-test_outdated_features_plotting.approved.txt │ │ ├── test_indicators-test_indicators_attribute_completeness_with_invalid_attribute_for_topic-headers0-schema0.approved.txt │ │ ├── test_indicators-test_indicators_attribute_completeness_with_invalid_attribute_for_topic-headers1-schema1.approved.txt │ │ ├── test_land-cover-thematic-accuracy.py-test_calculate.approved.txt │ │ ├── test_land-cover-thematic-accuracy.py-test_calculate.received.txt │ │ ├── test_land_cover_thematic_accuracy.py-test_calculate_multi_class │ │ ├── description.approved.txt │ │ └── report.approved.txt │ │ ├── test_land_cover_thematic_accuracy.py-test_calculate_single_class │ │ ├── description.approved.txt │ │ └── report.approved.txt │ │ ├── test_land_cover_thematic_accuracy.py-test_figure_multi_class.approved.json │ │ ├── test_land_cover_thematic_accuracy.py-test_figure_single_class.approved.json │ │ ├── test_mapping_saturation.py-TestCalculation-test_calculate.approved.txt │ │ ├── test_mapping_saturation.py-TestFigure-test_create_figure.approved.json │ │ ├── test_road_comparison.py-TestCalculate-test_calculate.approved.txt │ │ ├── test_road_comparison.py-TestCalculate-test_calculate_no_intersection.approved.txt │ │ ├── test_road_comparison.py-TestCalculate-test_calculate_reference_lenght_0.approved.txt │ │ ├── test_road_comparison.py-TestFigure-test_create_figure.approved.json │ │ └── test_road_comparison.py-TestFigure-test_create_figure_building_area_zero.approved.json ├── approvaltests_namers.py ├── approvaltests_reporters.py ├── approvaltests_scrubbers.py ├── conftest.py ├── fixtures │ ├── empty.png │ ├── feature-collection-germany-heidelberg.geojson │ ├── feature-collection-heidelberg-bahnstadt-bergheim-weststadt.geojson │ ├── feature-germany-berlin-friedrichshain-kreuzberg.geojson │ ├── feature-germany-heidelberg.geojson │ ├── feature-malta.geojson │ ├── land-cover-thematic-accuracy-db-fetch-results.json │ └── land-cover-thematic-accuracy-single-class-db-fetch-results.json ├── integrationtests │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── response_schema.py │ │ ├── test_indicators.py │ │ ├── test_indicators_attribute_completeness.py │ │ ├── test_indicators_land_cover_thematic_accuracy.py │ │ ├── test_indicators_mapping_saturation_data.py │ │ ├── test_metadata.py │ │ ├── test_metadata_attributes.py │ │ ├── test_metadata_indicators.py │ │ ├── test_metadata_projects.py │ │ ├── test_metadata_quality_dimensions.py │ │ ├── test_metadata_topics.py │ │ ├── test_ohsome_timeout.py │ │ └── test_response_models.py │ ├── conftest.py │ ├── fixtures │ │ ├── algeria-touggourt-feature.geojson │ │ ├── algeria-touggourt-hexcells.geojson │ │ ├── europe.geojson │ │ ├── heidelberg-altstadt-feature.geojson │ │ ├── niger-kanan-bakache.geojson │ │ ├── rasters │ │ │ ├── GHS_BUILT_LDS2014_GLOBE_R2018A_54009_1K_V2_0.tif │ │ │ ├── GHS_POP_E2015_GLOBE_R2019A_54009_1K_V1_0.tif │ │ │ ├── GHS_SMOD_POP2015_GLOBE_R2019A_54009_1K_V2_0.tif │ │ │ └── VNL_v2_npp_2020_global_vcmslcfg_c202102150000.average_masked.tif │ │ └── vcr_cassettes │ │ │ ├── api │ │ │ ├── test_indicators.yaml │ │ │ ├── test_indicators_attribute_completeness.yaml │ │ │ └── test_response_models.yaml │ │ │ ├── indicators │ │ │ ├── test_attribute_completeness.yaml │ │ │ ├── test_building_comparison.yaml │ │ │ ├── test_currentness.yaml │ │ │ ├── test_mapping_saturation.yaml │ │ │ └── test_minimal.yaml │ │ │ ├── test_ohsome_client.yaml │ │ │ └── test_oqt.yaml │ ├── indicators │ │ ├── __init__.py │ │ ├── test_attribute_completeness.py │ │ ├── test_building_comparison.py │ │ ├── test_currentness.py │ │ ├── test_land_cover_thematic_accuracy.py │ │ ├── test_mapping_saturation.py │ │ ├── test_minimal.py │ │ └── test_road_comparison.py │ ├── test_base_indicator.py │ ├── test_geodatabase.py │ ├── test_ohsome_client.py │ ├── test_oqt.py │ ├── test_postgres.py │ └── utils.py └── unittests │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── test_request_models.py │ ├── test_request_models_data.py │ └── test_response_models.py │ ├── fixtures │ ├── GHS_BUILT_R2018A-Heidelberg.tif │ ├── config.yaml │ ├── heidelberg-altstadt-feature.geojson │ ├── heidelberg-altstadt-featurecollection.geojson │ ├── heidelberg-altstadt-wsf2019.tif │ ├── nodata.tif │ ├── ohsome-response-200-invalid.geojson │ ├── ohsome-response-200-valid.geojson │ └── ohsome-response-400-time.json │ ├── mapping_saturation │ ├── __init__.py │ ├── fixtures.py │ └── test_models.py │ ├── test_api.py │ ├── test_attribute_definitions.py │ ├── test_attributes.py │ ├── test_config.py │ ├── test_definitions.py │ ├── test_helper.py │ ├── test_helper_asyncio.py │ ├── test_helper_geo.py │ ├── test_indicators_definitions.py │ ├── test_load_metadata.py │ ├── test_logging.py │ ├── test_ohsome_client.py │ ├── test_project.py │ ├── test_project_definitions.py │ ├── test_quality_dimension.py │ ├── test_quality_dimension_definitions.py │ ├── test_topic.py │ ├── test_topics_definitions.py │ ├── test_validators.py │ ├── test_version.py │ └── utils.py └── uv.lock /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please add the **type of change** as label. If your PR is not ready for review and merge, please add `🚧 ` to the PR title. 2 | 3 | ### Description 4 | Please add a clear and concise description of what your PR solves. 5 | 6 | ### Corresponding issue 7 | Closes # 8 | 9 | ### New or changed dependencies 10 | - 11 | 12 | ### Checklist 13 | - [ ] I have ensured my branch is mergeable with `main` (e.g. through `git rebase main` if necessary) 14 | - [ ] My code follows the [style guide](https://github.com/GIScience/ohsome-quality-api/blob/main/CONTRIBUTING.md#style-guide) and was checked with [pre-commit](https://github.com/GIScience/ohsome-quality-api/blob/main/CONTRIBUTING.md#pre-commit) before committing 15 | - [ ] I have commented my code 16 | - [ ] I have added sufficient unit and integration [tests](https://github.com/GIScience/ohsome-quality-api/blob/main/docs/development_setup.md#tests) 17 | - [ ] I have added new Hurl regression tests and checked all existing [tests](https://github.com/GIScience/ohsome-quality-api/blob/main/regression-tests/README.md) 18 | - [ ] I have updated the [CHANGELOG.md](https://github.com/GIScience/ohsome-quality-api/blob/main/CHANGELOG.md) 19 | 20 | Please check all finished tasks. If some tasks do not apply to your PR, please cross their text out (by using `~...~`) and remove their checkboxes. 21 | -------------------------------------------------------------------------------- /.github/workflows/rocketchat-notifications-feedback-requests.yml: -------------------------------------------------------------------------------- 1 | name: Rocket.Chat Pull Request push notifications 2 | 3 | on: 4 | pull_request: 5 | branches: [ master, main ] 6 | types: [ labeled ] 7 | issues: 8 | types: [ labeled ] 9 | 10 | jobs: 11 | rocketchat_pullrequest_notification: 12 | name: Someone labeled their Pull Request 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.pull_request && contains('comments welcome|help wanted|waiting for review', github.event.label.name) }} 15 | steps: 16 | - name: Push notification for a Pull Request labeled as "comments welcome" 17 | if: ${{ github.event.label.name == 'comments welcome' }} 18 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} would like to get feedback on pull request ${{ github.event.pull_request.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}" 19 | - name: Push notification for a Pull Request labeled as "help wanted" 20 | if: ${{ github.event.label.name == 'help wanted' }} 21 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} needs help with pull request ${{ github.event.pull_request.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}" 22 | - name: Push notification for a Pull Request labeled as "waiting for review" 23 | if: ${{ github.event.label.name == 'waiting for review' }} 24 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} is waiting for a code review of pull request ${{ github.event.pull_request.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}" 25 | rocketchat_issue_notification: 26 | name: Someone labeled their Issue 27 | runs-on: ubuntu-latest 28 | if: ${{ github.event.issue && contains('comments welcome|help wanted', github.event.label.name) }} 29 | steps: 30 | - name: Push notification for an Issue labeled as "comments welcome" 31 | if: ${{ github.event.label.name == 'comments welcome' }} 32 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} would like to get feedback on issue ${{ github.event.issue.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}" 33 | - name: Push notification for an Issue labeled as "help wanted" 34 | if: ${{ github.event.label.name == 'help wanted' }} 35 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} needs help with issue ${{ github.event.issue.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}" 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv*/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # pyenv config 117 | .python-version 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | 143 | # Pycharm 144 | .idea/ 145 | 146 | # Ruff 147 | .ruff_cache/ 148 | 149 | # oqapi 150 | /data/* 151 | /config/* 152 | !/config/sample.config.yaml 153 | /tests/*/fixtures/approved/*.received.txt 154 | /regression-tests/report/ 155 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.11.9 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | # Run the formatter. 9 | - id: ruff-format 10 | args: ["--check", "--diff"] 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v4.4.0 13 | hooks: 14 | - id: check-added-large-files 15 | args: ["--maxkb=1024"] 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## How to Contribute 4 | 5 | Please contribute to this repository through creating [issues](https://github.com/GIScience/ohsome-quality-api/issues/new) and [pull requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). 6 | 7 | 8 | ### Issues 9 | 10 | Bugs reports and enhancement suggestions are tracked via issues. Each issue can contain following information: 11 | 12 | - A clear and descriptive title 13 | - Description 14 | - Current behavior and expected behavior 15 | - Error message and stack trace 16 | 17 | Issues should serve as the basis for creating a merge request. They should have at least one tag associated with them. 18 | 19 | 20 | ### Merge Requests 21 | 22 | Merge requests are created to address one single issue or multiple. Either the assignee or the creator of the merge request is responsible for merging. 23 | Each merge request has to be approved by at least one reviewer before merging it. A person can be assigned as reviewer by either mark them as such or asking for a review by tagging the person in the description/comment of the merge request. 24 | 25 | A merge request can be made even if the branch is not ready to yet to be merged. When doing so please mark them as Work-in-Progress by writing `WIP` at the beginning of the title. This way people/reviewer know that currently someone is working to address an issue. This gives them the opportunity share their thoughts knowing that the merge request is still subject to change and does not need a full review yet. 26 | 27 | The [CHANGELOG.md](CHANGELOG.md) describes changes made in a merge request. It should contain a short description of the performed changes, as well as (a) link(s) to issue(s) or merge request. 28 | 29 | 30 | #### Review Process 31 | 32 | 1. Dev makes a PR/MR. 33 | 2. Rev reviews and raises some comments. 34 | 3. Dev addresses the comments and leaves responses explaining what has to be done. In cases where Dev just implemented Rev's suggestion, a simple "Done" is sufficient. 35 | 4. Rev reviews the changes and 36 | - If Rev is happy with a change, then Rev resolves the comment. 37 | - If Rev is still unsatisfied with a change, then Rev adds another comment explaining what is still missing. 38 | 5. Restart from 3 until all comments are resolved. 39 | 40 | 41 | ### Git Workflow 42 | 43 | All development work is based on the main branch (`main`). Merge requests are expected to target the main branch. 44 | 45 | 46 | ## Style Guide 47 | 48 | ### Tools 49 | 50 | This project uses [`ruff`](https://github.com/astral-sh/ruff) to ensure consistent code style. See the `pyproject.toml` file for configuration. 51 | 52 | 53 | #### A Note on the Configuration 54 | 55 | ```bash 56 | uv run ruff format . 57 | uv run ruff check --fix . 58 | ``` 59 | 60 | 61 | ### Pre-Commit 62 | 63 | In addition, [pre-commit](https://pre-commit.com/) is set up to run those tools prior to any git commit. In contrast to above described commands running these hooks will not apply any changes to the code base. Instead, 'pre-commit' checks if there would be any changes to be made. In that case simply run above commands manually. 64 | 65 | 66 | ## Tests 67 | 68 | Please provide [tests](/docs/development_setup.md#tests). 69 | 70 | 71 | ## Miscellaneous 72 | 73 | - How are indicators structured? -> [docs/indicator.md](/docs/indicator.md). 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build python app 2 | FROM python:3.13-slim AS builder 3 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 4 | 5 | WORKDIR /app 6 | 7 | # install R (with build dependencies) 8 | RUN rm -f /etc/apt/apt.conf.d/docker-clean; \ 9 | echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache 10 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 11 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 12 | apt update \ 13 | && apt install -y --no-upgrade --no-install-recommends \ 14 | r-base-dev \ 15 | build-essential 16 | 17 | ENV UV_LINK_MODE=copy \ 18 | UV_HTTP_TIMEOUT=300 19 | 20 | # install only the dependencies 21 | RUN --mount=type=cache,target=/root/.cache/uv \ 22 | --mount=type=bind,source=uv.lock,target=uv.lock \ 23 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ 24 | uv sync --locked --no-install-project --no-editable --no-dev 25 | 26 | # add project files and install the project 27 | COPY ohsome_quality_api ohsome_quality_api 28 | 29 | RUN --mount=type=cache,target=/root/.cache/uv \ 30 | --mount=type=bind,source=uv.lock,target=uv.lock \ 31 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ 32 | uv sync --locked --no-editable --no-dev 33 | 34 | FROM python:3.13-slim 35 | 36 | WORKDIR /app 37 | 38 | ENV VIRTUAL_ENV=/app/.venv \ 39 | PATH="/app/.venv/bin:$PATH" 40 | 41 | # install R (without build dependencies) 42 | RUN rm -f /etc/apt/apt.conf.d/docker-clean; \ 43 | echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache 44 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 45 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 46 | apt update \ 47 | && apt install -y --no-upgrade --no-install-recommends \ 48 | r-base 49 | 50 | # copy environment but not the source code 51 | COPY --from=builder --chown=app:app $VIRTUAL_ENV $VIRTUAL_ENV 52 | 53 | EXPOSE 8000/tcp 54 | 55 | # run the application 56 | CMD ["/app/.venv/bin/uvicorn","ohsome_quality_api.api.api:app","--host","0.0.0.0"] 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ohsome quality API 2 | 3 | [![Build Status](https://jenkins.heigit.org/buildStatus/icon?job=OQAPI/main)](https://jenkins.heigit.org/job/OQAPI/job/main/) 4 | [![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=ohsome-quality-api&metric=alert_status)](https://sonarcloud.io/dashboard?id=ohsome-quality-api) 5 | [![Docker Image Version](https://img.shields.io/docker/v/heigit/ohsome-quality-api)](https://hub.docker.com/r/heigit/ohsome-quality-api) 6 | [![LICENSE](https://img.shields.io/badge/license-AGPL--v3-orange)](LICENSE.txt) 7 | [![Dashboard](https://img.shields.io/website?url=https%3A%2F%2Fdashboard.ohsome.org&label=dashboard)](https://dashboard.ohsome.org/#backend=oqtApi) 8 | [![status: active](https://github.com/GIScience/badges/raw/master/status/active.svg)](https://github.com/GIScience/badges#active) 9 | 10 | The ohsome quality API computes and provides data quality estimations (indicators) for OpenStreetMap. 11 | 12 | Thanks to the [ohsome dashboard](https://dashboard.ohsome.org) generating quality estimations of OpenStreetMap data for an arbitrary region is as easy as pie. 13 | 14 | The software is developed by [Heidelberg Institute for Geoinformation Technology (HeiGIT)](https://heigit.org/) and based on the [ohsome platform](https://heigit.org/big-spatial-data-analytics-en/ohsome/). 15 | 16 | ## Usage 17 | 18 | ### Dashboard 19 | 20 | For quick insights, you can start exploring quality estimations through the [ohsome dashboard](https://dashboard.ohsome.org). 21 | 22 | ### API 23 | 24 | Check out the interactive [Swagger API documentation](https://api.quality.ohsome.org/v1/docs) and the [Jupyter Notebook examples](https://github.com/GIScience/ohsome-quality-api-examples). 25 | 26 | ## About 27 | 28 | On our [website](https://api.quality.ohsome.org) you can find more details about the ohsome quality API. If you want to read up on the history of ohsome quality API, which was named ohsome quality analyst (OQT) in the past, or want to stay up-to-date, take a look at our [blog posts](https://heigit.org/tag/ohsome-quality-api-en). 29 | 30 | ## Contributing 31 | 32 | Contributions of any form are more than welcome! Please take a look at our [contributing 33 | guidelines](CONTRIBUTING.md) for details on our git workflow and style guide. 34 | 35 | ## Development 36 | 37 | Please refer to the [development setup documentation](/docs/development_setup.md) for guidance. 38 | -------------------------------------------------------------------------------- /config/sample.config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Database connection parameters; 3 | postgres_host: localhost 4 | postgres_port: 5445 5 | postgres_db: oqapi 6 | postgres_user: oqapi 7 | postgres_password: oqapi 8 | # Restrict size of input geometry 9 | geom_size_limit: 1000 10 | # Python logging level 11 | log_level: INFO 12 | # ohsome API URL 13 | ohsome_api: https://api.ohsome.org/v1 14 | # Limit number of concurrent Indicator computations 15 | concurrent_computations: 4 16 | # User-Agent header for request to the ohsome API 17 | # Default: 'ohsome-quality-api/{version}' 18 | user_agent: ohsome-quality-api 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | api: 4 | build: . 5 | environment: 6 | OQAPI_CONFIG: /config/config.yaml 7 | command: uv run python scripts/start_api.py --host 0.0.0.0 8 | volumes: 9 | - ./config:/config 10 | ports: 11 | - "127.0.0.1:8080:8080" 12 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | Please take a look at the [interactive Swagger UI](https://api.quality.ohsome.org/v1/docs) to explore the API. 4 | 5 | We also provide a [Jupyter Notebook](https://github.com/GIScience/ohsome-quality-api-examples) with examples on how to use the ohsome quality API with Python. 6 | 7 | Have a look at this [blog post](https://heigit.org/de/visualizing-oqt-api-results-in-qgis-2/) to learn how to visualize the ohsome quality API Response in QGIS. 8 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Indicators 4 | 5 | ### Mapping Saturation 6 | 7 | #### How does the Mapping Saturation relate to mapping activity? 8 | 9 | A saturation of 100 % means that the gradient or slope of the last bit (i.e. last 3 years) of the fitted curve is 1.0 (horizontal line). This again means that no mapping has been done in this period. Hopefully because there is nothing to map. That is the premise of the Mapping Saturation indicator: Each aggregation of features (e.g. length of roads or count of building) has a maximum. After increased mapping activity saturation is reached near this maximum. 10 | 11 | #### Why does the curve of Mapping Saturation decline sometimes? 12 | 13 | In case that features or tags are deleted the fitted curve can decline. 14 | -------------------------------------------------------------------------------- /docs/img/UML-Class-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/UML-Class-Diagram.png -------------------------------------------------------------------------------- /docs/img/UML-Component-Diagram.drawio: -------------------------------------------------------------------------------- 1 | 7Vxbd6I6FP41rnXOgy6uoo9t7W2mnV70TKd9OStK1EyReCC2Or/+JBAEkhS1ItrO0D7ATkLCvnz7IwnWzJPJ/DwA0/E1dqFXMzR3XjM7NcMwbMOosX/NXcQS3dHNWDIKkMtlqaCLfkEu1Lh0hlwY5ioSjD2CpnnhAPs+HJCcDAQBfs1XG2Iv3+sUjKAk6A6AJ0sfkEvGXKprWlpwAdFozLtu2bygDwbPowDPfN5fzTCH0REXT0ByL14/HAMXv2ZE5mnNPAkwJvHZZH4CPabcRG1xu7M3SpfjDqBP1mlgheb1FQTnc6ePrwkYn7rjbt2O7/ICvBlMHiMaLFkkCooeEbKbaDXz+HWMCOxOwYCVvlKfoLIxmXj0Sqen8qD4OF9gQOA8I+KDPId4AkmwoFV4ab3NFcY9qm7wQb6m5nEcXmectYzFhYC7xGh571Qt9IRrRq2l57vwafQI4eTsy9MPs33x9dK16npToaamR/s97tOTETu5uevRGg84eIZBuCwNkuJEQvvvpzJB1UxJiLrmkYdGPhURzNQL+JUHh6xVSJWP/FGPlXVaqeAqKu4YqeSe60aPZGMwZd0MZn3ILuNA1JlNXRTQyEKYdRLiGVPy8RD7pMtHZuVtPESed4I9HESDNoc2+2O3JAF+hpmSZnTwm2Xk8VGOt7TyzmIkYZtxFstUOItp78pZWpJZoUsRh1/igIzxCPvAO02lx/kYS+tc4cjKTOs/ISELDp9gRvCacUftGQxgwXB5cBEQjCApqMdDgD1LoV0C6AGCXvLoWrqSZdw6ur2UFJ/4/AS7s0jP6lBKIkfL1BhQRULqr8fqmBRjYDg0BgNVDLjNftNuluPrui44e1N29mXeyjr7znzdlNNFBc4O54j8YM0bNr96zJR05vzO0cUiufDp82YascvHbFnaLLpK2r0/sJprBpauH1RkmeZHMWkFpjHLNg1veosRHfMysE0hsKXkFI+UtxIMvBzG+20u05uIzXxuNDWsQ0PTxAsqDr0EGfUcLqYwWToyOh+ScjhSkJxcfXrKYTqHFiRGe69BojU2DJPo6hYGiD4+s+6WsZOwhT3kru2wLXG9NHzu4ZRaIvzsIaQ3heTu7D2EZCjbc0w5G3HylCw6VbDFJJJWhpxVFV1s2jmPslrr0cWjIACLTLUpqxAW9CPMxFlNYYZxs/r0JB5BqdzV3Mt8y5avoJv5exWua1XjuhQNG047c+Q92Xashmllinfj12L8WMV+Td/Hiurvxq8T02VA+tJ3aU4jOPj0OdPU8xo/gJypSzqvFGbaGZhJQecNoKEXJTLO1rqMs10RhghpxhRsHj+RhBLSfXRh7cBqN7LI1Go1d4I+YrdmBWCyH4JXgc85ZbvcuiChXE21PhwVyb/M7o6HtHYCDZuGniHQDd1Zkfg1rah+Plbl1i2B3OTxpW29D7bEQS2nZCqaLE5sWfT2mNARMCCMHqzaNTD00PRiFWhsMLPryDsGLAVZEBVX3mRHW6EiYb/AiYdgZNa/HmA/pOr5+88Wgd1sEdCFgGkruKRRKZe0JPe46V50ZGMvDbLwEE0Sgbk6lPpxOrnqLwXLbUo3M0JvA7k8sa7SLC2jb8ZmyRvStWHLtUri+EYeHpsKjt9SmGVpztLtYq2BbBUkdMkaWnREJSAgR2zTG3MKD4QhGiTiM+SlE2tuUsnHzOBMwsu3y+MJiu5hKmy77SJyxO3Hshn7cdNkjafljZdaOLVfsYNUYFmj9Jmi7WJWW51q8TikvdFK0R6hP0l2F0nW1ESyu+8saxmSZ9Bh/25ZVhcWcA8gy8oTmzc8QH+vSU1r75Oalr2PLFjl9r2SV9+TZZLV/Mc4rCwp8x86BhcQ0Afhpw88cQW+0sB7Quajc/d99hN+aw17858utJ8Ue4n2EYguCMdR+9JeGm7Cc/94CF4feuGvL91u55rmxPq6O1b2FzNFw1YQSxe9JNTxO4xnuI5qjNvExSgp/PZPr1szaO/a+VHnOj67uevV7+GIkrwwQ0WR4jORTC+Kjgu/MClueg/CKESVYz6/6P57e3NbytiklsrBbn7/axQO3hj+comUKjmceaRQyat19wYu+phAQ0UDV9DGhA2238RHEWglyqixP5nKL+VlwKXAGO01mbxexvdXY1fTRr2HYV3Xe5e9U2PcAV/rMpOXTLM9K8ngLfT6+DULte9nDFUtioqrirqYuzZYFW0YWubIk9YdrjYoTS+/LCgA4QosKKAZmguHyEckBlf2xSgByKdn+QkAqur/ZjAksUVBxAzDxg6Q97dED1vYVm/ZRkNejlECSLJeVjp+rPGVq0x/cuxI8fqSnK+zI2OLNw7FDq6iQFmJMxkj2AWUd0s4skQvaL8TjixxT6GYZHaMP29/+ZsJ9ezGLI0aOQKfyXRGqP+wT9IZNoVTOEBDFEloFaCCEQ34rNhjcNZYH2cOD6JaHwqiTKvZUHxkrpoWazuNXYHUOu+EBwpSqm2mRdF0KCAlrEDammDbdUHKtrXiG5UHUsrphMNY3Sp/OkH5sIbsawf2XW3RsFXfv7Bk0WdrBuxXQ3w2JLRMKAVp4JAA3zlowK8LiyCm6is2FSN1djUB+GE+Wig/qIt+hyYb1EX1dh7T9DL9ZZ4YptPfPzJP/wc= -------------------------------------------------------------------------------- /docs/img/UML-Component-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/UML-Component-Diagram.png -------------------------------------------------------------------------------- /docs/img/UML-Sequence-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/UML-Sequence-Diagram.png -------------------------------------------------------------------------------- /docs/img/oqt_website_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/oqt_website_step1.png -------------------------------------------------------------------------------- /docs/img/oqt_website_step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/oqt_website_step2.png -------------------------------------------------------------------------------- /docs/indicator.md: -------------------------------------------------------------------------------- 1 | # Indicator 2 | 3 | An indicator estimates data quality for OpenStreetMap (OSM). It takes an area of interest in form of a GeoJSON Feature and a Topic object describing the set of OSM features as input. 4 | 5 | ## Base Class 6 | 7 | Each particular indicator class inherits from the `BaseIndicator` class of the `ohsome_quality_api/indicators/base.py` module. Following class diagram gives an overview of abstract methods and properties of this class: 8 | 9 | ![UML class diagram](./img/UML-Class-Diagram.png) 10 | 11 | ### Metadata 12 | 13 | Metadata is automatically loaded from a `metadata.yaml` file found in the module of a particular indicator. 14 | Metadata describes basic information about the indicator: The indicator name, for which projects the indicator can be used, the quality dimension, a short description of what it does and how it works, and a standardized interpretation of its possible results. 15 | 16 | ### Topic 17 | 18 | Please take a look at the [topic documentation](topic.md). 19 | 20 | ### Result 21 | 22 | The result object consists of following attributes: 23 | 24 | - `description (str)`: The result description 25 | - `timestamp (datetime)`: Timestamp of the creation of the indicator 26 | - `timestamp_osm (datetime)`: Timestamp of the used OSM data (e.g. the latest timestamp of the ohsome API results) 27 | - `value (float)`: The result value 28 | - `class (int)`: The result class. An integer between 1 and 5. It maps to following result labels: 29 | - `1`: `red` 30 | - `2`/`3`: `yellow` 31 | - `4`/`5`: `green` 32 | - `figure (dict)`: A plotly figure 33 | - `label (str)`: Traffic lights like quality label (`green`, `yellow` or `red`) 34 | 35 | 36 | ## Indicator Class 37 | 38 | A particular indicator class (child) need to implement three functions: 39 | 1. `preprocess` 40 | 2. `calculate` 41 | 3. `create_figure` 42 | 43 | 44 | Following sequence diagram shows when those functions are called during the lifecycle of the indicator: 45 | 46 | ![UML Sequence Diagram](img/UML-Sequence-Diagram.png) 47 | 48 | 49 | ### `preprocess` 50 | 51 | This function is used to fetch and preprocess the data needed for an indicator. Usually this involves querying the ohsome API by using the module `ohsome/client.py` which does so using Pythons async/await pattern 52 | 53 | All data created during the preprocessing will be stored as attributes of the indicator object. 54 | 55 | ### `calculate` 56 | 57 | This functions does the computation of the indicator. At the end the result object should be fully initialized and all attributes set (except the figure). 58 | 59 | ### `create_figure` 60 | 61 | This function creates plotly figure base on the indicator result. 62 | -------------------------------------------------------------------------------- /docs/indicators.md: -------------------------------------------------------------------------------- 1 | # Indicators 2 | 3 | ## Mapping Saturation 4 | 5 | Calculate the saturation within the last 3 years. 6 | Time period is one month since 2008. 7 | 8 | 9 | Different statistical models are used to find out if saturation is reached. 10 | 11 | 12 | ### Methods and Data 13 | 14 | - Intrinsic approach. 15 | 16 | Premise: 17 | 18 | The added number of OSM objects of a specific feature class per time period decreases as the 19 | number of mapped objects converges against the (unknown) true number of objects. 20 | 21 | Each aggregation of features (e.g. length of roads or count of building) 22 | has a maximum. After increased mapping activity saturation is reached near this 23 | maximum. 24 | 25 | 26 | 27 | ### References 28 | 29 | - Gröchenig S et al. (2014): Digging into the history of VGI data-sets: results from 30 | a worldwide study on OpenStreetMap mapping activity 31 | (https://doi.org/10.1080/17489725.2014.978403) 32 | - Barrington-Leigh C and Millard-Ball A (2017): The world’s user-generated road map 33 | is more than 80% complete 34 | (https://doi.org/10.1371/journal.pone.0180698 pmid:28797037) 35 | - Josephine Brückner, Moritz Schott, Alexander Zipf, and Sven Lautenbach (2021): 36 | Assessing shop completeness in OpenStreetMap for two federal states in Germany 37 | (https://doi.org/10.5194/agile-giss-2-20-2021) 38 | -------------------------------------------------------------------------------- /docs/topic.md: -------------------------------------------------------------------------------- 1 | # Topic 2 | 3 | A topic describes the request which should be made to the [ohsome API](https://api.ohsome.org). Each topic is representative of a specific set of features, aggregated information or user statistics derived from the OpenStreetMap database. Each topic is defined by the ohsome API `endpoint`, an `aggregation_type` and the `filter` parameter. In addition, each topic preset has a key, name, description, a list of valid indicators and a list of projects the topic belongs to. Topic presets are written down as YAML file at `ohsome_quality_api/topics/presets.yaml` 4 | 5 | ## Example 6 | 7 | ```yaml 8 | building-count: 9 | name: Building Count 10 | description: >- 11 | All buildings as defined by all objects tagged with 'building=*'. 12 | endpoint: elements 13 | aggregation_type: count 14 | filter: building=* and geometry:polygon 15 | indicators: 16 | - mapping-saturation 17 | - currentness 18 | - attribute-completeness 19 | projects: 20 | - core 21 | ``` 22 | 23 | ## How to Add a New Topic? 24 | 25 | First create an ohsome API query to retrieve desired information from the ohsome API. Helpful resources for this task are: 26 | - The Swagger UI of the ohsome API: 27 | https://api.ohsome.org/v1/swagger-ui.html 28 | - ohsome API documentation on the `aggregation_type` and `endpoint` parameters: 29 | https://docs.ohsome.org/ohsome-api/stable/endpoints.html 30 | - ohsome API documentation on the `filter` parameter: 31 | https://docs.ohsome.org/ohsome-api/stable/filter.html 32 | 33 | Second translate the query parameters into a topic preset and extent this file: 34 | `ohsome_quality_api/topics/presets.yaml`. 35 | -------------------------------------------------------------------------------- /docs/vector_datasets.md: -------------------------------------------------------------------------------- 1 | # Vector datasets (PostGIS Database) 2 | 3 | The ohsome quality API operates on vector datasets stored in a PostGIS database. 4 | 5 | 6 | ### The Human Development Index (SHDI) 7 | 8 | Information: 9 | - https://globaldatalab.org/shdi/ 10 | - product: GDL Shapefiles V4, epoch: 1990–2017, resolution: Subnational, coordinate system: EPSG:4326 11 | -------------------------------------------------------------------------------- /ohsome_quality_api/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.9.0" 2 | __title__ = "ohsome quality API" 3 | __description__ = "Data quality estimations for OpenStreetMap" 4 | __author__ = "ohsome team" 5 | __email__ = "ohsome@heigit.org" 6 | __homepage__ = "https://api.quality.ohsome.org/" 7 | -------------------------------------------------------------------------------- /ohsome_quality_api/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/api/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/api/request_context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | from dataclasses import dataclass 3 | 4 | from fastapi import Request 5 | 6 | 7 | @dataclass 8 | class RequestContext: 9 | path_parameters: dict 10 | 11 | 12 | request_context: ContextVar[RequestContext] = ContextVar("request_context") 13 | 14 | 15 | async def set_request_context(request: Request): 16 | """Set request context for the duration of a request. 17 | 18 | After leaving the context manager (after the request is processed) 19 | the request context is reset again. 20 | """ 21 | token = request_context.set(RequestContext(path_parameters=request.path_params)) 22 | try: 23 | yield 24 | finally: 25 | request_context.reset(token) 26 | -------------------------------------------------------------------------------- /ohsome_quality_api/api/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/api/static/favicon-32x32.png -------------------------------------------------------------------------------- /ohsome_quality_api/attributes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/attributes/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/attributes/definitions.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import Enum 3 | from typing import List 4 | 5 | import yaml 6 | 7 | from ohsome_quality_api.attributes.models import Attribute 8 | from ohsome_quality_api.topics.definitions import get_topic_preset, load_topic_presets 9 | from ohsome_quality_api.utils.helper import get_module_dir 10 | 11 | 12 | def load_attributes() -> dict[str, dict[str, Attribute]]: 13 | """Read attributes from YAML file.""" 14 | directory = get_module_dir("ohsome_quality_api.attributes") 15 | file = os.path.join(directory, "attributes.yaml") 16 | with open(file, "r") as f: 17 | raw = yaml.safe_load(f) 18 | attributes = {} 19 | for topic_key, value in raw.items(): 20 | attributes[topic_key] = {} 21 | for attribute_key, value_ in value.items(): 22 | attributes[topic_key][attribute_key] = Attribute(**value_) 23 | return attributes 24 | 25 | 26 | def get_attributes() -> dict[str, dict[str, Attribute]]: 27 | return load_attributes() 28 | 29 | 30 | def get_attribute(topic_key, a_key: str | None) -> Attribute: 31 | attributes = get_attributes() 32 | try: 33 | # TODO: Workaround to be able to display indicator in dashboard. 34 | # Remove if dashboard handles attribution key selection. 35 | if a_key is None: 36 | return next(iter(attributes[topic_key].values())) 37 | return attributes[topic_key][a_key] 38 | except KeyError as error: 39 | raise KeyError("Invalid topic or attribute key(s).") from error 40 | 41 | 42 | # TODO: Remove since it is the same as above? 43 | def get_attribute_preset(topic_key: str) -> List[Attribute]: 44 | """Get ohsome API parameters of a list of Attributes based on topic key.""" 45 | attributes = load_attributes() 46 | try: 47 | return attributes[topic_key] 48 | except KeyError as error: 49 | topics = load_topic_presets() 50 | raise KeyError( 51 | "Invalid topic key. Valid topic keys are: " + str(topics.keys()) 52 | ) from error 53 | 54 | 55 | def build_attribute_filter(attribute_key: List[str] | str, topic_key: str) -> str: 56 | """Build attribute filter for ohsome API query.""" 57 | attributes = get_attributes() 58 | try: 59 | if isinstance(attribute_key, str): 60 | return get_topic_preset(topic_key).filter + " and (" + attribute_key + ")" 61 | else: 62 | attribute_filter = get_topic_preset(topic_key).filter 63 | for key in attribute_key: 64 | attribute_filter += " and (" + attributes[topic_key][key].filter + ")" 65 | return attribute_filter 66 | except KeyError as error: 67 | raise KeyError("Invalid topic or attribute key(s).") from error 68 | 69 | 70 | attribute_keys = { 71 | inner_key 72 | for outer_dict in load_attributes().values() 73 | for inner_key in outer_dict.keys() 74 | } 75 | 76 | AttributeEnum = Enum("AttributeEnum", {name: name for name in attribute_keys}) 77 | -------------------------------------------------------------------------------- /ohsome_quality_api/attributes/models.py: -------------------------------------------------------------------------------- 1 | """Pydantic Models for Attributes.""" 2 | 3 | from pydantic import BaseModel, ConfigDict 4 | 5 | 6 | class Attribute(BaseModel): 7 | filter: str 8 | name: str 9 | description: str 10 | model_config = ConfigDict( 11 | extra="forbid", 12 | frozen=True, 13 | title="Attribute", 14 | ) 15 | -------------------------------------------------------------------------------- /ohsome_quality_api/definitions.py: -------------------------------------------------------------------------------- 1 | """Global Variables and Functions.""" 2 | 3 | import glob 4 | from enum import Enum 5 | from types import MappingProxyType 6 | from typing import Literal 7 | 8 | import yaml 9 | 10 | from ohsome_quality_api.indicators.models import IndicatorMetadata 11 | from ohsome_quality_api.utils.helper import ( 12 | get_module_dir, 13 | ) 14 | 15 | ATTRIBUTION_TEXTS = MappingProxyType( 16 | { 17 | "OSM": "© OpenStreetMap contributors", 18 | "GHSL": "© European Union, 1995-2022, Global Human Settlement Topic Data", 19 | "VNL": "Earth Observation Group Nighttime Light Data", 20 | "EUBUCCO": "European building stock characteristics in a common and open " 21 | + "database", 22 | "Microsoft Buildings": "Microsoft Building Footprints (ODbL)", 23 | } 24 | ) 25 | 26 | ATTRIBUTION_URL = ( 27 | "https://github.com/GIScience/ohsome-quality-api/blob/main/COPYRIGHTS.md" 28 | ) 29 | 30 | 31 | # default colors of the Sematic UI CSS Framework 32 | # used by the ohsome dashboard 33 | class Color(Enum): 34 | RED = "#DB2828" 35 | ORANGE = "#F2711C" 36 | YELLOW = "#FBBD08" 37 | OLIVE = "#B5CC18" 38 | GREEN = "#21BA45" 39 | TEAL = "#00B5AD" 40 | BLUE = "#2185D0" 41 | VIOLET = "#6435C9" 42 | PURPLE = "#A333C8" 43 | PINK = "#E03997" 44 | BROWN = "#A5673F" 45 | GREY = "#767676" 46 | BLACK = "#1B1C1D" 47 | 48 | 49 | def load_metadata( 50 | module_name: Literal["indicators"], 51 | ) -> dict[str, IndicatorMetadata]: 52 | """Read metadata of all indicators from YAML files. 53 | 54 | Those text files are located in the directory of each indicator. 55 | 56 | Args: 57 | module_name: indicators. 58 | Returns: 59 | A Dict with the class names of the indicators 60 | as keys and metadata as values. 61 | """ 62 | assert module_name == "indicators" 63 | directory = get_module_dir("ohsome_quality_api.{}".format(module_name)) 64 | files = glob.glob(directory + "/**/metadata.yaml", recursive=True) 65 | raw = {} 66 | for file in files: 67 | with open(file, "r") as f: 68 | raw.update(yaml.safe_load(f)) # Merge dicts 69 | metadata = {} 70 | match module_name: 71 | case "indicators": 72 | for k, v in raw.items(): 73 | metadata[k] = IndicatorMetadata(**v) 74 | return metadata 75 | 76 | 77 | def get_attribution(data_keys: list) -> str: 78 | """Return attribution text. Individual attributions are separated by semicolons.""" 79 | assert set(data_keys) <= {"OSM", "GHSL", "VNL", "EUBUCCO", "Microsoft Buildings"} 80 | filtered = dict(filter(lambda d: d[0] in data_keys, ATTRIBUTION_TEXTS.items())) 81 | return "; ".join([str(v) for v in filtered.values()]) 82 | -------------------------------------------------------------------------------- /ohsome_quality_api/geodatabase/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/geodatabase/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/geodatabase/regions_as_geojson.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | row_to_json(feature_collection) 3 | FROM ( 4 | SELECT 5 | 'FeatureCollection' AS "type", 6 | array_to_json(array_agg(feature)) AS "features" 7 | FROM ( 8 | SELECT 9 | 'Feature' AS "type", 10 | /* Make sure to conform with RFC7946 by using WGS84 lon, lat*/ 11 | ST_AsGeoJSON(ST_Transform(geom, 4326), 6)::json AS "geometry", 12 | ( 13 | SELECT 14 | json_strip_nulls(row_to_json(t)) 15 | FROM ( 16 | SELECT 17 | ogc_fid as id, 18 | name 19 | ) AS t 20 | ) AS "properties" 21 | FROM 22 | regions 23 | ) AS feature 24 | ) AS feature_collection 25 | -------------------------------------------------------------------------------- /ohsome_quality_api/geodatabase/select_coverage.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | ST_AsGeoJSON (ST_Transform (geom, 4326)) as geom 3 | FROM 4 | {table_name}; 5 | -------------------------------------------------------------------------------- /ohsome_quality_api/geodatabase/select_intersection.sql: -------------------------------------------------------------------------------- 1 | WITH bpoly AS ( 2 | SELECT 3 | ST_Setsrid (ST_GeomFromGeoJSON ($1), 4326) AS geom 4 | ) 5 | SELECT 6 | -- ratio of area within coverage (empty if outside, between 0-1 if intersection) 7 | ST_Area (ST_Intersection (bpoly.geom, coverage.geom)) / ST_Area (bpoly.geom) as area_ratio, 8 | ST_AsGeoJSON (ST_Intersection (bpoly.geom, coverage.geom)) AS geom 9 | FROM 10 | bpoly, 11 | {table_name} coverage 12 | WHERE 13 | ST_Intersects (bpoly.geom, coverage.geom) 14 | -------------------------------------------------------------------------------- /ohsome_quality_api/geodatabase/select_shdi.sql: -------------------------------------------------------------------------------- 1 | WITH bpoly AS ( 2 | SELECT 3 | ST_Setsrid (public.ST_GeomFromGeoJSON (geojson), 4326) AS geom, 4 | -- Row number is used to make sure order of result is the same as order of input 5 | -- (Probably unnecessary) 6 | row_number() OVER () AS rownumber 7 | FROM 8 | unnest(cast($1 AS text[])) AS geojson 9 | ) 10 | SELECT 11 | SUM(ST_Area (ST_Intersection (shdi.geom, bpoly.geom)::geography) / ST_Area 12 | (bpoly.geom::geography) * shdi.shdi) AS shdi, 13 | rownumber as rownumber 14 | FROM 15 | shdi, 16 | bpoly 17 | WHERE 18 | ST_Intersects (shdi.geom, bpoly.geom) 19 | GROUP BY 20 | bpoly.geom, 21 | bpoly.rownumber 22 | ORDER BY 23 | bpoly.rownumber; 24 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/attribute_completeness/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/attribute_completeness/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/attribute_completeness/templates.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | label_description: 3 | red: >- 4 | The attribute completeness is low (<25%). 5 | yellow: >- 6 | The attribute completeness is medium (25%-75%). 7 | green: >- 8 | The attribute completeness is high (>75%). 9 | undefined: >- 10 | The quality level could not be calculated for this indicator. 11 | result_description: >- 12 | $result% of all "$topic" features (all: $all) in your area of interest have the selected additional $tags (matched: $matched). 13 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/building_comparison/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/building_comparison/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/building_comparison/datasets.yaml: -------------------------------------------------------------------------------- 1 | EUBUCCO: 2 | name: EUBUCCO 3 | link: https://docs.eubucco.com/ 4 | date: Nov 3, 2022 5 | description: >- 6 | EUBUCCO is a dataset of building footprints for Europe. 7 | It is derived from administrative datasets. 8 | color: PURPLE 9 | coverage: 10 | simple: eubucco_coverage_simple 11 | inversed: eubucco_coverage_inversed 12 | table_name: eubucco 13 | 14 | Microsoft Buildings: 15 | name: Microsoft Building Footprints 16 | link: https://planetarycomputer.microsoft.com/dataset/ms-buildings 17 | date: July 5, 2022 18 | description: >- 19 | Microsoft Building Footprints is a dataset of building footprints for the world. 20 | It is derived from satellite imagery. 21 | color: ORANGE 22 | coverage: 23 | simple: microsoft_buildings_coverage_simple 24 | inversed: microsoft_buildings_coverage_inversed 25 | table_name: microsoft_buildings 26 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/building_comparison/query.sql: -------------------------------------------------------------------------------- 1 | WITH bpoly AS ( 2 | SELECT 3 | -- split mutlipolygon into list of polygons for more efficient processing 4 | (ST_DUMP (ST_Setsrid (ST_GeomFromGeoJSON ($1), 4326))).geom AS geom 5 | ) 6 | SELECT 7 | SUM({table_name}.area) as area 8 | FROM {table_name}, 9 | bpoly 10 | WHERE 11 | ST_Intersects ({table_name}.centroid, bpoly.geom); 12 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/building_comparison/templates.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | label_description: 3 | red: >- 4 | The completeness of OSM buildings in your area-of-interest is low. 5 | yellow: >- 6 | The completeness of OSM buildings in your area-of-interest is medium. 7 | green: >- 8 | The completeness of OSM buildings in your area-of-interest is high. 9 | undefined: >- 10 | Comparison could not be made. 11 | result_description: >- 12 | The completeness in comparison to $dataset is $ratio%. 13 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/currentness/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/currentness/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/currentness/templates.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | label_description: 3 | red: >- 4 | Many features are out-of-date. 5 | yellow: >- 6 | Some features are up-to-date and some features are out-of-date. 7 | green: >- 8 | Most features are up-to-date. 9 | undefined: >- 10 | The quality level could not be calculated for this indicator. 11 | result_description: >- 12 | In the area of interest $up_to_date_contrib_rel% of the $num_of_elements features were edited (created or modified) for the last time in the period between $from_timestamp and $to_timestamp. 13 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/currentness/thresholds.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # thresholds are age in number of months since latest timestamp (today) 3 | 4 | roads: 5 | thresholds: 6 | up_to_date: 48 7 | out_of_date: 96 8 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 9 | 10 | railway-length: 11 | thresholds: 12 | up_to_date: 96 13 | out_of_date: 132 14 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 15 | 16 | bus-stops: 17 | thresholds: 18 | up_to_date: 48 19 | out_of_date: 96 20 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 21 | 22 | tram-stops: 23 | thresholds: 24 | up_to_date: 48 25 | out_of_date: 96 26 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 27 | 28 | subway-station: 29 | thresholds: 30 | up_to_date: 48 31 | out_of_date: 96 32 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 33 | 34 | mapaction-rail-length: 35 | thresholds: 36 | up_to_date: 96 37 | out_of_date: 132 38 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 39 | 40 | mapaction-major-roads-length: 41 | thresholds: 42 | up_to_date: 48 43 | out_of_date: 96 44 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 45 | 46 | local-food-shops: 47 | thresholds: 48 | up_to_date: 24 49 | out_of_date: 48 50 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 51 | 52 | fast-food-restaurants: 53 | thresholds: 54 | up_to_date: 24 55 | out_of_date: 48 56 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 57 | 58 | restaurants: 59 | thresholds: 60 | up_to_date: 24 61 | out_of_date: 48 62 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 63 | 64 | convenience-stores: 65 | thresholds: 66 | up_to_date: 24 67 | out_of_date: 48 68 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 69 | 70 | pubs-and-biergartens: 71 | thresholds: 72 | up_to_date: 24 73 | out_of_date: 48 74 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 75 | 76 | alcohol-and-beverages: 77 | thresholds: 78 | up_to_date: 24 79 | out_of_date: 48 80 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 81 | 82 | sweets-and-pasteries: 83 | thresholds: 84 | up_to_date: 24 85 | out_of_date: 48 86 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests 87 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/definitions.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import Enum 3 | 4 | import yaml 5 | from geojson import FeatureCollection 6 | 7 | from ohsome_quality_api.indicators.models import IndicatorMetadata 8 | from ohsome_quality_api.projects.definitions import ProjectEnum 9 | from ohsome_quality_api.topics.definitions import load_topic_presets 10 | from ohsome_quality_api.utils.helper import ( 11 | get_class_from_key, 12 | get_module_dir, 13 | ) 14 | 15 | 16 | def get_indicator_keys() -> list[str]: 17 | return [str(t) for t in load_indicators().keys()] 18 | 19 | 20 | def load_indicators() -> dict[str, IndicatorMetadata]: 21 | directory = get_module_dir("ohsome_quality_api.indicators") 22 | file = os.path.join(directory, "indicators.yaml") 23 | with open(file, "r") as f: 24 | raw = yaml.safe_load(f) 25 | indicators = {} 26 | for k, v in raw.items(): 27 | indicators[k] = IndicatorMetadata(**v) 28 | return indicators 29 | 30 | 31 | def get_indicator_metadata(project: ProjectEnum = None) -> dict[str, IndicatorMetadata]: 32 | indicators = load_indicators() 33 | if project is not None: 34 | return {k: v for k, v in indicators.items() if project in v.projects} 35 | else: 36 | return indicators 37 | 38 | 39 | def get_indicator(indicator_key: str) -> IndicatorMetadata: 40 | indicators = get_indicator_metadata() 41 | try: 42 | return indicators[indicator_key] 43 | except KeyError as error: 44 | raise KeyError( 45 | "Invalid project key. Valid project keys are: " + str(indicators.keys()) 46 | ) from error 47 | 48 | 49 | def get_valid_indicators(topic_key: str) -> tuple: 50 | """Get valid Indicator/Topic combination of a Topic.""" 51 | td = load_topic_presets() 52 | return tuple(td[topic_key].indicators) 53 | 54 | 55 | async def get_coverage(indicator_key: str, inverse: bool = False) -> FeatureCollection: 56 | indicator_class = get_class_from_key(class_type="indicator", key=indicator_key) 57 | features = await indicator_class.coverage(inverse) 58 | return FeatureCollection(features=features) 59 | 60 | 61 | IndicatorEnum = Enum("IndicatorEnum", {name: name for name in get_indicator_keys()}) 62 | IndicatorEnumRequest = Enum( 63 | "IndicatorEnum", 64 | {name: name for name in get_indicator_keys() if name != "attribute-completeness"}, 65 | ) 66 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/indicators.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | mapping-saturation: 3 | name: Mapping Saturation 4 | projects: 5 | - core 6 | - corine-land-cover 7 | - expanse 8 | - experimental 9 | - idealvgi 10 | - mapaction 11 | - sketchmap 12 | - bkg 13 | - unicef 14 | quality_dimension: completeness 15 | description: >- 16 | Calculate if mapping has saturated. 17 | High saturation has been reached if the growth of the fitted curve is 18 | minimal. 19 | minimal: 20 | name: Minimal 21 | projects: 22 | - misc 23 | quality_dimension: minimal 24 | description: >- 25 | An minimal Indicator for testing purposes. 26 | currentness: 27 | name: Currentness 28 | projects: 29 | - core 30 | - bkg 31 | - unicef 32 | quality_dimension: currentness 33 | description: >- 34 | Estimate currentness of features by classifying contributions based on 35 | topic specific temporal thresholds into three groups: up-to-date, 36 | in-between and out-of-date. 37 | road-comparison: 38 | name: Road Comparison 39 | projects: 40 | - bkg 41 | - core 42 | - unicef 43 | quality_dimension: completeness 44 | description: >- 45 | Compare the road length of OSM roads with the road length of 46 | reference data. 47 | building-comparison: 48 | name: Building Comparison 49 | projects: 50 | - bkg 51 | - core 52 | quality_dimension: completeness 53 | description: >- 54 | Comparison of OSM buildings with the buildings of reference datasets. 55 | attribute-completeness: 56 | name: Attribute Completeness 57 | projects: 58 | - bkg 59 | - core 60 | quality_dimension: completeness 61 | description: >- 62 | Derive the ratio of OSM features compared to features which 63 | match additional expected tags (e.g. amenity=hospital vs 64 | amenity=hospital and wheelchair=yes). 65 | land-cover-thematic-accuracy: 66 | name: Land Cover Thematic Accuracy 67 | projects: 68 | - bkg 69 | quality_dimension: thematic-accuracy 70 | description: >- 71 | Thematic accuracy Open Street Map land cover data in comparison to the CORINE Land 73 | Cover (CLC) dataset. 74 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/land_cover_thematic_accuracy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/land_cover_thematic_accuracy/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/land_cover_thematic_accuracy/query-multi-classes.sql: -------------------------------------------------------------------------------- 1 | WITH bpoly AS ( 2 | SELECT 3 | -- split mutlipolygon into list of polygons for more efficient processing 4 | (ST_Dump (ST_SetSRID (ST_GeomFromGeoJSON ($1), 4326))).geom AS geometry 5 | ) 6 | SELECT 7 | CLC_class as clc_class_corine, 8 | osm_CLC_class as clc_class_osm, 9 | SUM ( 10 | CASE WHEN ST_Within (o.geometry, b.geometry) THEN 11 | area 12 | ELSE 13 | ST_Area (ST_Intersection (o.geometry, b.geometry)::geography) 14 | END) AS area 15 | FROM 16 | osm_corine_2021_deu_intersection o, 17 | bpoly b 18 | WHERE 19 | ST_Intersects (o.geometry, b.geometry) 20 | --AND osm_CLC_class != 0 21 | --AND osm_CLC_class != 50 22 | GROUP BY 1,2 23 | ; 24 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/land_cover_thematic_accuracy/query-single-class.sql: -------------------------------------------------------------------------------- 1 | -- TODO: will index on classes make this query faster? 2 | WITH bpoly AS ( 3 | SELECT 4 | -- split mutlipolygon into list of polygons for more efficient processing 5 | (ST_Dump(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326))).geom AS geometry 6 | ) 7 | SELECT 8 | CLC_class as clc_class_corine, 9 | osm_CLC_class as clc_class_osm, 10 | CASE 11 | WHEN ST_Within(o.geometry, b.geometry) THEN area 12 | ELSE ST_Area(ST_Intersection(o.geometry, b.geometry)::geography) 13 | END AS area 14 | FROM 15 | osm_corine_2021_deu_intersection o, 16 | bpoly b 17 | WHERE 18 | ST_Intersects(o.geometry, b.geometry) 19 | AND ( 20 | o.CLC_class = $2 OR o.osm_CLC_class = $2 21 | ); 22 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/land_cover_thematic_accuracy/templates.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | label_description: 3 | red: >- 4 | In your area-of-interes the thematic accuracy of $clc_class is $f1_score % 5 | (low agreement between OSM and CORINE). This suggests that OSM has a poor 6 | classification quality or incomplete land cover data. 7 | yellow: >- 8 | In your area-of-interes the thematic accuracy of $clc_class is $f1_score % 9 | (medium agreement between OSM and CORINE). This suggests that OSM does 10 | capture some land cover categories but may lack detail or accuracy. 11 | green: >- 12 | In your area-of-interes the thematic accuracy of $clc_class is $f1_score % 13 | (high agreement between OSM and CORINE). This suggests that OSM provides a 14 | reliable representation of land cover. 15 | undefined: >- 16 | The F1-score could not be calculated, possibly due to missing or incompatible data in the selected region. 17 | result_description: >- 18 | The F1-Score is used as 19 | statistical metric which considers both the correctness and completeness of 20 | the land cover categories. 21 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/mapping_saturation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/mapping_saturation/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/mapping_saturation/ssdoubles.R: -------------------------------------------------------------------------------- 1 | # title: "Self-starting two-steps-sigmoidal function" 2 | # author: "Josephine Brückner" 3 | 4 | doubleSFun <- function(input, e, f, k, b, Z, c, ...) { 5 | e+(f-e)*1/2*(tanh(k*(input-b))+1)+(Z-f)*1/2*(tanh(k*(input-c))+1) 6 | } 7 | 8 | doubleS.init <- function(mCall, LHS, data, ...) 9 | { xy <- sortedXyData(mCall[["input"]], LHS, data) 10 | x <- xy[, "x"]; y <- xy[, "y"] 11 | 12 | e <- min(y) 13 | Z <- max(y) 14 | f <- max(y)/2 15 | b <- x[which.min(y)] 16 | c <- max(x)*0.5 17 | k <- 10 18 | model <- "NA" 19 | count <- 0 20 | 21 | while(model[1]=="NA") 22 | { k <- k/10 23 | count <- count + 1 24 | 25 | model <- tryCatch( 26 | nls(y ~ doubleSFun(x, e, f, k, b, Z, c), start=c(e=min(y), f=max(y)/2, b=x[which.min(y)], Z=max(y), c=c, k=k), data), 27 | error = function(e) paste0("NA")) 28 | 29 | if (count > 100) 30 | {break} 31 | } 32 | 33 | start <- c(e, f, k, b, Z, c) 34 | names(start) <- mCall[c("e", "f", "k", "b", "Z", "c")] 35 | start 36 | } 37 | 38 | 39 | SSdoubleS <- selfStart(doubleSFun, doubleS.init, parameters=c("e", "f", "k", "b", "Z", "c")) 40 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/mapping_saturation/templates.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | label_description: 3 | red: >- 4 | No saturation identified (Saturation ≤ 30%). 5 | yellow: >- 6 | Saturation is in progress (30% < Saturation ≤ 97%). 7 | green: >- 8 | High saturation has been reached (97% < Saturation ≤ 100%). 9 | undefined: >- 10 | Saturation could not be calculated. 11 | result_description: >- 12 | The saturation of the last 3 years is $saturation%. 13 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/minimal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/minimal/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/minimal/indicator.py: -------------------------------------------------------------------------------- 1 | """An Indicator for testing purposes.""" 2 | 3 | from string import Template 4 | 5 | import dateutil.parser 6 | from geojson import Feature 7 | 8 | from ohsome_quality_api.indicators.base import BaseIndicator 9 | from ohsome_quality_api.ohsome import client as ohsome_client 10 | from ohsome_quality_api.topics.models import BaseTopic as Topic 11 | 12 | 13 | class Minimal(BaseIndicator): 14 | def __init__(self, topic: Topic, feature: Feature) -> None: 15 | super().__init__(topic=topic, feature=feature) 16 | self.count = 0 17 | 18 | async def preprocess(self) -> None: 19 | query_results = await ohsome_client.query(self.topic, self.feature) 20 | self.count = query_results["result"][0]["value"] 21 | self.result.timestamp_osm = dateutil.parser.isoparse( 22 | query_results["result"][0]["timestamp"] 23 | ) 24 | 25 | def calculate(self) -> None: 26 | description = Template(self.templates.result_description).substitute() 27 | self.result.value = 1.0 28 | self.result.description = ( 29 | description + self.templates.label_description["green"] 30 | ) 31 | 32 | def create_figure(self) -> None: 33 | # Do nothing ... 34 | return None 35 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/minimal/templates.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | label_description: 3 | red: >- 4 | Bad data quality. 5 | yellow: >- 6 | Medium data quality. 7 | green: >- 8 | Good data quality. 9 | undefined: >- 10 | The quality level could not be calculated for this indicator. 11 | result_description: >- 12 | Some description of the result. 13 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from typing import Literal 3 | 4 | from pydantic import BaseModel, ConfigDict, Field, computed_field 5 | 6 | from ohsome_quality_api.projects.definitions import ProjectEnum 7 | from ohsome_quality_api.quality_dimensions.definitions import QualityDimensionEnum 8 | from ohsome_quality_api.utils.helper import snake_to_lower_camel 9 | 10 | 11 | class IndicatorMetadata(BaseModel): 12 | """Metadata of an indicator as defined in the metadata.yaml file.""" 13 | 14 | name: str 15 | description: str 16 | projects: list[ProjectEnum] 17 | quality_dimension: QualityDimensionEnum 18 | model_config = ConfigDict( 19 | alias_generator=snake_to_lower_camel, 20 | title="Metadata", 21 | frozen=True, 22 | extra="forbid", 23 | populate_by_name=True, 24 | ) 25 | 26 | 27 | class IndicatorTemplates(BaseModel): 28 | """Result text templates of an indicator as defined in the templates.yaml file.""" 29 | 30 | label_description: dict[str, str] 31 | result_description: str 32 | 33 | 34 | class Result(BaseModel): 35 | """The result of the Indicator. 36 | 37 | Attributes: 38 | timestamp (datetime): Timestamp of the creation of the indicator 39 | timestamp_osm (datetime): Timestamp of the used OSM data 40 | (e.g. Latest timestamp of the ohsome API results) 41 | label (str): Traffic lights like quality label: `green`, `yellow` or `red`. The 42 | value is determined by the result classes 43 | value (float): The result value 44 | class_ (int): The result class. An integer between 1 and 5. It maps to the 45 | result labels. This value is used by the reports to determine an overall 46 | result. 47 | description (str): The result description. 48 | """ 49 | 50 | description: str 51 | timestamp: datetime = Field(default=datetime.now(timezone.utc)) 52 | timestamp_osm: datetime | None = Field(default=None, alias="timestampOSM") 53 | value: float | None = None 54 | class_: Literal[1, 2, 3, 4, 5] | None = None 55 | figure: dict | None = None 56 | model_config = ConfigDict( 57 | alias_generator=snake_to_lower_camel, 58 | extra="forbid", 59 | populate_by_name=True, 60 | ) 61 | 62 | @computed_field 63 | @property 64 | def label(self) -> Literal["green", "yellow", "red", "undefined"]: 65 | labels = {1: "red", 2: "yellow", 3: "yellow", 4: "green", 5: "green"} 66 | return labels.get(self.class_, "undefined") 67 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/road_comparison/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/road_comparison/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/road_comparison/datasets.yaml: -------------------------------------------------------------------------------- 1 | Microsoft Roads: 2 | name: Microsoft Roads 3 | link: https://github.com/microsoft/RoadDetections 4 | date: February 27, 2023 5 | description: >- 6 | Microsoft Road Detections is a dataset of road world-wide. 7 | It is derived from satellite imagery. 8 | color: ORANGE 9 | coverage: 10 | simple: microsoft_roads_coverage_simple 11 | inversed: microsoft_roads_coverage_inversed 12 | table_name: microsoft_roads_midpoint 13 | processing_date: 2024-04-05 14 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/road_comparison/query.sql: -------------------------------------------------------------------------------- 1 | WITH bpoly AS ( 2 | SELECT 3 | -- split mutlipolygon into list of polygons for more efficient processing 4 | (ST_DUMP (ST_Setsrid (ST_GeomFromGeoJSON ($1), 4326))).geom AS geom 5 | ) 6 | SELECT 7 | SUM(cr.covered), 8 | SUM(cr.length) 9 | FROM 10 | bpoly 11 | LEFT JOIN {table_name} cr ON ST_Intersects (cr.midpoint, bpoly.geom); 12 | -------------------------------------------------------------------------------- /ohsome_quality_api/indicators/road_comparison/templates.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | label_description: 3 | red: >- 4 | The completeness of OSM roads in your area-of-interest is low. 5 | yellow: >- 6 | The completeness of OSM roads in your area-of-interest is medium. 7 | green: >- 8 | The completeness of OSM roads in your area-of-interest is high. 9 | 10 | undefined: >- 11 | Comparison could not be made. 12 | result_description: >- 13 | The completeness in comparison to $dataset is $ratio%. 14 | $dataset have a total length of $length_total. 15 | $length_matched of $dataset are covered by OSM roads. 16 | -------------------------------------------------------------------------------- /ohsome_quality_api/ohsome/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/ohsome/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/oqt.py: -------------------------------------------------------------------------------- 1 | """Controller for computing Indicators.""" 2 | 3 | import logging 4 | from typing import Coroutine 5 | 6 | from geojson import Feature, FeatureCollection 7 | 8 | from ohsome_quality_api.indicators.base import BaseIndicator as Indicator 9 | from ohsome_quality_api.topics.models import BaseTopic as Topic 10 | from ohsome_quality_api.topics.models import TopicData, TopicDefinition 11 | from ohsome_quality_api.utils.helper import get_class_from_key 12 | from ohsome_quality_api.utils.helper_asyncio import gather_with_semaphore 13 | from ohsome_quality_api.utils.validators import validate_area 14 | 15 | 16 | async def create_indicator( 17 | key: str, 18 | bpolys: FeatureCollection, 19 | topic: TopicData | TopicDefinition, 20 | include_figure: bool = True, 21 | **kwargs, 22 | ) -> list[Indicator]: 23 | """Create indicator(s) for features of a GeoJSON FeatureCollection. 24 | 25 | Indicators are computed asynchronously utilizing semaphores. 26 | Properties of the input GeoJSON are preserved. 27 | """ 28 | tasks: list[Coroutine] = [] 29 | for i, feature in enumerate(bpolys.features): 30 | if "id" not in feature.keys(): 31 | feature["id"] = i 32 | # Disable size limit for the Mapping Saturation indicator 33 | # TODO: Remove size restriction 34 | if isinstance(topic, TopicDefinition) and key not in [ 35 | "mapping-saturation", 36 | "currentness", 37 | "building-comparison", 38 | "road-comparison", 39 | "attribute-completeness", 40 | "land-cover-thematic-accuracy", 41 | ]: 42 | validate_area(feature) 43 | tasks.append( 44 | _create_indicator( 45 | key, 46 | feature, 47 | topic, 48 | include_figure, 49 | **kwargs, 50 | ) 51 | ) 52 | return await gather_with_semaphore(tasks) 53 | 54 | 55 | async def _create_indicator( 56 | key: str, 57 | feature: Feature, 58 | topic: Topic, 59 | include_figure: bool = True, 60 | **kwargs, 61 | ) -> Indicator: 62 | """Create an indicator from scratch.""" 63 | 64 | logging.info("Indicator key: {0:4}".format(key)) 65 | logging.info("Topic key: {0:4}".format(topic.key)) 66 | logging.info("Feature id: {0:4}".format(feature.get("id", "None"))) 67 | 68 | indicator_class = get_class_from_key(class_type="indicator", key=key) 69 | indicator = indicator_class( 70 | topic, 71 | feature, 72 | **kwargs, 73 | ) 74 | 75 | logging.info("Run preprocessing") 76 | await indicator.preprocess() 77 | 78 | logging.info("Run calculation") 79 | indicator.calculate() 80 | 81 | if include_figure: 82 | logging.info("Run figure creation") 83 | indicator.create_figure() 84 | else: 85 | indicator.result.figure = None 86 | 87 | return indicator 88 | -------------------------------------------------------------------------------- /ohsome_quality_api/projects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/projects/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/projects/definitions.py: -------------------------------------------------------------------------------- 1 | """Global Variables and Functions.""" 2 | 3 | import os 4 | from enum import Enum 5 | 6 | import yaml 7 | 8 | from ohsome_quality_api.projects.models import Project 9 | from ohsome_quality_api.utils.helper import get_module_dir 10 | 11 | 12 | def get_project_keys() -> list[str]: 13 | return [str(t) for t in load_projects().keys()] 14 | 15 | 16 | def load_projects() -> dict[str, Project]: 17 | """Read definitions of projects. 18 | 19 | Returns: 20 | A dict with all projects included. 21 | """ 22 | directory = get_module_dir("ohsome_quality_api.projects") 23 | file = os.path.join(directory, "projects.yaml") 24 | with open(file, "r") as f: 25 | raw = yaml.safe_load(f) 26 | projects = {} 27 | for k, v in raw.items(): 28 | projects[k] = Project(**v) 29 | return projects 30 | 31 | 32 | def get_project_metadata() -> dict[str, Project]: 33 | projects = load_projects() 34 | return projects 35 | 36 | 37 | def get_project(project_key: str) -> Project: 38 | projects = get_project_metadata() 39 | try: 40 | return projects[project_key] 41 | except KeyError as error: 42 | raise KeyError( 43 | "Invalid project key. Valid project keys are: " + str(projects.keys()) 44 | ) from error 45 | 46 | 47 | ProjectEnum = Enum("ProjectEnum", {key: key for key in get_project_keys() + ["all"]}) 48 | -------------------------------------------------------------------------------- /ohsome_quality_api/projects/models.py: -------------------------------------------------------------------------------- 1 | """Pydantic Models for Projects.""" 2 | 3 | from pydantic import BaseModel, ConfigDict 4 | 5 | 6 | class Project(BaseModel): 7 | name: str 8 | description: str 9 | model_config = ConfigDict( 10 | title="Project", 11 | frozen=True, 12 | extra="forbid", 13 | populate_by_name=True, 14 | ) 15 | -------------------------------------------------------------------------------- /ohsome_quality_api/projects/projects.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # TODO description 3 | 4 | core: 5 | name: TODO 6 | description: >- 7 | something that is still a TODO 8 | 9 | mapaction: 10 | name: TODO 11 | description: >- 12 | something that is still a TODO 13 | 14 | sketchmap: 15 | name: TODO 16 | description: >- 17 | something that is still a TODO 18 | 19 | unicef: 20 | name: TODO 21 | description: >- 22 | something that is still a TODO 23 | 24 | misc: 25 | name: TODO 26 | description: >- 27 | something that is still a TODO 28 | 29 | experimental: 30 | name: TODO 31 | description: >- 32 | something that is still a TODO 33 | 34 | corine-land-cover: 35 | name: TODO 36 | description: >- 37 | something that is still a TODO 38 | 39 | idealvgi: 40 | name: TODO 41 | description: >- 42 | something that is still a TODO 43 | 44 | expanse: 45 | name: TODO 46 | description: >- 47 | something that is still a TODO 48 | 49 | bkg: 50 | name: TODO 51 | description: >- 52 | something that is still a TODO 53 | -------------------------------------------------------------------------------- /ohsome_quality_api/quality_dimensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/quality_dimensions/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/quality_dimensions/definitions.py: -------------------------------------------------------------------------------- 1 | """Global Variables and Functions.""" 2 | 3 | import os 4 | from enum import Enum 5 | 6 | import yaml 7 | 8 | from ohsome_quality_api.quality_dimensions.models import QualityDimension 9 | from ohsome_quality_api.utils.helper import get_module_dir 10 | 11 | 12 | def load_quality_dimensions() -> dict[str, QualityDimension]: 13 | """Read definitions of quality dimensions. 14 | 15 | Returns: 16 | A dict with all quality dimensions included. 17 | """ 18 | directory = get_module_dir("ohsome_quality_api.quality_dimensions") 19 | file = os.path.join(directory, "quality_dimensions.yaml") 20 | with open(file, "r") as f: 21 | raw = yaml.safe_load(f) 22 | quality_dimensions = {} 23 | for k, v in raw.items(): 24 | quality_dimensions[k] = QualityDimension(**v) 25 | return quality_dimensions 26 | 27 | 28 | def get_quality_dimensions() -> dict[str, QualityDimension]: 29 | return load_quality_dimensions() 30 | 31 | 32 | def get_quality_dimension(qd_key: str) -> QualityDimension: 33 | quality_dimensions = get_quality_dimensions() 34 | try: 35 | return quality_dimensions[qd_key] 36 | except KeyError as error: 37 | raise KeyError( 38 | "Invalid quality dimension key. Valid quality dimension keys are: " 39 | + str(quality_dimensions.keys()) 40 | ) from error 41 | 42 | 43 | def get_quality_dimension_keys() -> list[str]: 44 | return [str(t) for t in load_quality_dimensions().keys()] 45 | 46 | 47 | QualityDimensionEnum = Enum( 48 | "QualityDimensionEnum", {key: key for key in get_quality_dimension_keys()} 49 | ) 50 | -------------------------------------------------------------------------------- /ohsome_quality_api/quality_dimensions/models.py: -------------------------------------------------------------------------------- 1 | """Pydantic Models for Quality Dimensions.""" 2 | 3 | from pydantic import BaseModel, ConfigDict 4 | 5 | 6 | class QualityDimension(BaseModel): 7 | name: str 8 | description: str 9 | source: str | None = None 10 | model_config = ConfigDict( 11 | title="Topic", 12 | frozen=True, 13 | extra="forbid", 14 | populate_by_name=True, 15 | ) 16 | -------------------------------------------------------------------------------- /ohsome_quality_api/quality_dimensions/quality_dimensions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | completeness: 4 | name: Completeness 5 | description: >- 6 | The degree to which subject data associated with an entity has values for all expected attributes 7 | and related entity instances in a specific context of use. 8 | source: 9 | https://www.iso.org/standard/78900.html 10 | 11 | currentness: 12 | name: Currentness 13 | description: >- 14 | The degree to which data has attributes that are of the right age in a specific context of use. 15 | source: 16 | https://www.iso.org/standard/35736.html 17 | 18 | thematic-accuracy: 19 | name: Thematic Accuracy 20 | description: >- 21 | The degree to which attributes of data are correct (agree with "truth" of the reference dataset). 22 | source: 23 | https://onlinelibrary.wiley.com/doi/full/10.1155/2014/372349 24 | 25 | minimal: 26 | name: Minimal 27 | description: >- 28 | A minimal quality dimension definition for testing purposes. 29 | 30 | none: 31 | name: None 32 | description: >- 33 | No specific quality dimension 34 | -------------------------------------------------------------------------------- /ohsome_quality_api/topics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/topics/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/topics/definitions.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import Enum 3 | 4 | import yaml 5 | 6 | from ohsome_quality_api.projects.definitions import ProjectEnum 7 | from ohsome_quality_api.topics.models import TopicDefinition 8 | from ohsome_quality_api.utils.helper import get_module_dir 9 | 10 | 11 | def load_topic_presets() -> dict[str, TopicDefinition]: 12 | """Read ohsome API parameters of all topic from YAML file.""" 13 | directory = get_module_dir("ohsome_quality_api.topics") 14 | file = os.path.join(directory, "presets.yaml") 15 | with open(file, "r") as f: 16 | raw = yaml.safe_load(f) 17 | topics = {} 18 | for k, v in raw.items(): 19 | v["filter"] = v.pop("filter") 20 | v["key"] = k 21 | topics[k] = TopicDefinition(**v) 22 | return topics 23 | 24 | 25 | def get_topic_keys() -> list[str]: 26 | return [str(t) for t in load_topic_presets().keys()] 27 | 28 | 29 | def get_topic_presets(project: ProjectEnum = None) -> dict[str, TopicDefinition]: 30 | topics = load_topic_presets() 31 | if project is not None: 32 | return {k: v for k, v in topics.items() if project in v.projects} 33 | else: 34 | return topics 35 | 36 | 37 | def get_topic_preset(topic_key: str) -> TopicDefinition: 38 | """Get ohsome API parameters of a single topic based on topic key.""" 39 | topics = load_topic_presets() 40 | try: 41 | return topics[topic_key] 42 | except KeyError as error: 43 | raise KeyError( 44 | "Invalid topic key. Valid topic keys are: " + str(topics.keys()) 45 | ) from error 46 | 47 | 48 | def get_valid_topics(indicator_name: str) -> tuple: 49 | """Get valid Indicator/Topic combination of an Indicator.""" 50 | td = load_topic_presets() 51 | return tuple(topic for topic in td if indicator_name in td[topic].indicators) 52 | 53 | 54 | TopicEnum = Enum("TopicEnum", {name: name for name in get_topic_keys()}) 55 | -------------------------------------------------------------------------------- /ohsome_quality_api/topics/models.py: -------------------------------------------------------------------------------- 1 | """Pydantic Models for Topics 2 | 3 | Note: 4 | The topic key, name, description and the ohsome API endpoint and filter are defined 5 | in the `presets.yaml` file in the `topic` module. 6 | """ 7 | 8 | from typing import Literal 9 | 10 | from pydantic import BaseModel, ConfigDict 11 | 12 | from ohsome_quality_api.projects.definitions import ProjectEnum 13 | from ohsome_quality_api.utils.helper import snake_to_lower_camel 14 | 15 | 16 | class BaseTopic(BaseModel): 17 | key: str 18 | name: str 19 | description: str 20 | model_config = ConfigDict( 21 | alias_generator=snake_to_lower_camel, 22 | extra="forbid", 23 | frozen=True, 24 | populate_by_name=True, 25 | title="Topic", 26 | ) 27 | 28 | 29 | class TopicDefinition(BaseTopic): 30 | """Includes the ohsome API endpoint and parameters needed to retrieve the data.""" 31 | 32 | endpoint: Literal["elements"] 33 | aggregation_type: Literal["area", "count", "length", "perimeter"] 34 | filter: str 35 | indicators: list[str] 36 | projects: list[ProjectEnum] 37 | source: str | None = None 38 | ratio_filter: str | None = None 39 | 40 | 41 | class TopicData(BaseTopic): 42 | """Includes the data associated with the topic.""" 43 | 44 | data: dict 45 | -------------------------------------------------------------------------------- /ohsome_quality_api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/utils/__init__.py -------------------------------------------------------------------------------- /ohsome_quality_api/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exception classes.""" 2 | 3 | from schema import SchemaError 4 | 5 | 6 | class OhsomeApiError(Exception): 7 | """Request to ohsome API failed.""" 8 | 9 | def __init__(self, message): 10 | self.name = "OhsomeApiError" 11 | self.message = message 12 | 13 | 14 | class SizeRestrictionError(ValueError): 15 | """Exception raised if size of input GeoJSON Geometry is too big.""" 16 | 17 | def __init__(self, geom_size_limit, geom_size): 18 | self.name = "SizeRestrictionError" 19 | self.message = ( 20 | f"Input GeoJSON Geometry is too big ({geom_size} km²). " 21 | f"The area should be less than {geom_size_limit} km²." 22 | ) 23 | 24 | 25 | class DatabaseError(Exception): 26 | pass 27 | 28 | 29 | class EmptyRecordError(DatabaseError): 30 | def __init__(self): 31 | self.name = "EmptyRecordError" 32 | self.message = "Query returned no record." 33 | 34 | 35 | class TopicDataSchemaError(Exception): 36 | def __init__(self, message, schema_error: SchemaError): 37 | self.name = "TopicDataSchemaError" 38 | self.message = "{0}\n{1}".format(message, schema_error) 39 | -------------------------------------------------------------------------------- /ohsome_quality_api/utils/helper_asyncio.py: -------------------------------------------------------------------------------- 1 | """Helper functions for `asyncio`.""" 2 | 3 | import asyncio 4 | from typing import Coroutine 5 | 6 | 7 | async def gather_with_semaphore(tasks: list, *args, **kwargs) -> Coroutine: 8 | """A wrapper around `gather` to limit the number of tasks executed at a time.""" 9 | # Semaphore needs to initiated inside of the event loop 10 | semaphore = asyncio.Semaphore(4) 11 | 12 | async def sem_task(task): 13 | async with semaphore: 14 | return await task 15 | 16 | return await asyncio.gather(*(sem_task(task) for task in tasks), *args, **kwargs) 17 | 18 | 19 | def filter_exceptions(results: list) -> list[Exception]: 20 | """Return all exceptions""" 21 | return [i for i in results if isinstance(i, Exception)] 22 | -------------------------------------------------------------------------------- /ohsome_quality_api/utils/helper_geo.py: -------------------------------------------------------------------------------- 1 | from geojson import Feature 2 | from geojson.utils import coords 3 | from pyproj import Geod 4 | 5 | 6 | def calculate_area(feature: Feature) -> float: 7 | """Calculate area in meter.""" 8 | geod = Geod(ellps="WGS84") 9 | coordinates = tuple(coords(feature)) 10 | lons = tuple(c[0] for c in coordinates) 11 | lats = tuple(c[1] for c in coordinates) 12 | area, _ = geod.polygon_area_perimeter(lons=lons, lats=lats) 13 | return area 14 | -------------------------------------------------------------------------------- /ohsome_quality_api/utils/validators.py: -------------------------------------------------------------------------------- 1 | from geojson import Feature 2 | 3 | from ohsome_quality_api.config import get_config_value 4 | from ohsome_quality_api.utils.exceptions import ( 5 | SizeRestrictionError, 6 | ) 7 | from ohsome_quality_api.utils.helper_geo import calculate_area 8 | 9 | 10 | def validate_area(feature: Feature): 11 | """Check area size of feature against size limit configuration value.""" 12 | size_limit = float(get_config_value("geom_size_limit")) 13 | area = calculate_area(feature) / (1000 * 1000) # sqkm 14 | if area > size_limit: 15 | raise SizeRestrictionError(size_limit, area) 16 | -------------------------------------------------------------------------------- /pipeline_config.groovy: -------------------------------------------------------------------------------- 1 | libraries{ 2 | misc 3 | rocketchat 4 | } 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ohsome-quality-api" 3 | version = "1.9.0" 4 | description = "Data quality estimations for OpenStreetMap." 5 | authors = [{email="ohsome@heigit.org"}] 6 | keywords = [ 7 | "osm", 8 | "gis", 9 | "geography", 10 | "metric", 11 | "ohsome", 12 | "quality", 13 | ] 14 | requires-python = ">=3.12" 15 | dependencies = [ 16 | "async-lru>=2.0.5", 17 | "asyncpg>=0.30.0", 18 | "fastapi>=0.115.12", 19 | "geojson>=3.2.0", 20 | "geojson-pydantic>=2.0.0", 21 | "httpx>=0.28.1", 22 | "kaleido==0.2.1", 23 | "plotly>=6.0.1", 24 | "pydantic>=2.11.4", 25 | "pyproj>=3.7.1", 26 | "python-dateutil>=2.9.0.post0", 27 | "pyyaml>=6.0.2", 28 | "rpy2>=3.5.17", 29 | "schema>=0.7.7", 30 | "scikit-learn>=1.6.1", 31 | "uvicorn>=0.34.2", 32 | ] 33 | [project.urls] 34 | Homepage = "https://api.quality.ohsome.org" 35 | Repository = "https://github.com/GIScience/ohsome-quality-api" 36 | 37 | [dependency-groups] 38 | dev = [ 39 | "approvaltests>=14.5.0", 40 | "click>=8.2.0", 41 | "fastapi[standard]>=0.115.12", 42 | "pre-commit>=4.2.0", 43 | "pygments>=2.19.1", # add syntax highlighting to pytest 44 | "pytest>=8.3.5", 45 | "pytest-asyncio>=0.26.0", 46 | "pytest-cov>=6.1.1", 47 | "pytest-mock>=3.14.0", 48 | "pytest-sugar>=1.0.0", 49 | "pytest-xdist>=3.6.1", 50 | "ruff==0.11.9", # fixed because of pre-commit 51 | "vcrpy>=7.0.0", 52 | ] 53 | 54 | [build-system] 55 | requires = ["uv_build>=0.7.5"] 56 | build-backend = "uv_build" 57 | 58 | [tool.uv.build-backend] 59 | module-root = "" 60 | 61 | [tool.ruff.lint] 62 | select = [ 63 | "E", # pycodestyle Error 64 | "F", # Pyflakes 65 | "I", # isort 66 | "N", # pep8-nameing 67 | "Q", # flake8-quotes 68 | "W", # pycodestyle Warning 69 | "C90", # mccabe 70 | ] 71 | 72 | [tool.ruff.lint.per-file-ignores] 73 | "ohsome_quality_api/indicators/mapping_saturation/models.py" = [ 74 | "N803", 75 | "N806", 76 | ] 77 | 78 | [tool.pytest.ini_options] 79 | testpaths = ["tests"] 80 | asyncio_default_fixture_loop_scope = "function" 81 | 82 | -------------------------------------------------------------------------------- /regression-tests/README.md: -------------------------------------------------------------------------------- 1 | # Regression tests with hurl 2 | 3 | Hurl is an HTTP command line client and HTTP test tool: 4 | 5 | https://hurl.dev 6 | 7 | In order to run the HTTP tests in this directory, `hurl` must be installed. 8 | The tests can be run against the different stages using the following scripts: 9 | 10 | * [run_hurl_tests_DEV.sh](./run_hurl_tests_DEV.sh) 11 | * [run_hurl_tests_TEST.sh](./run_hurl_tests_TEST.sh) 12 | * [run_hurl_tests_PROD.sh](./run_hurl_tests_PROD.sh) 13 | 14 | Please note that you may get different results depending on the supported endpoints and connected spatial database of the system running on a given stage. 15 | 16 | An HTML-report with the results is generated here: 17 | 18 | [report/index.html](./report/index.html) 19 | 20 | 21 | -------------------------------------------------------------------------------- /regression-tests/__run_hurl_tests_for_stage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # requires the env variable $HURL_BASE_URL 4 | 5 | rm -rf report 6 | mkdir report 7 | 8 | hurl *.hurl --report-html report 9 | printf "\n\nhurl report: file://$PWD/report/index.html\n" 10 | 11 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 12 | xdg-open report/index.html 13 | elif [[ "$OSTYPE" == "darwin"* ]]; then 14 | open report/index.html 15 | else 16 | printf "\nOS could not be detected. Please open report manually!\n" 17 | fi -------------------------------------------------------------------------------- /regression-tests/building-comparison.hurl: -------------------------------------------------------------------------------- 1 | POST {{BASE_URL}}/indicators/building-comparison 2 | accept: application/json 3 | file, building-comparison.json; 4 | HTTP 200 5 | [Captures] 6 | duration_in_ms: duration 7 | -------------------------------------------------------------------------------- /regression-tests/buildingcount_bbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "building-count", 3 | "bpolys": { 4 | "type": "FeatureCollection", 5 | "features": [ 6 | { 7 | "type": "Feature", 8 | "id": "box 1", 9 | "bbox": [ 10 | 8.748488, 11 | 49.47397, 12 | 8.7532061, 13 | 49.4767595 14 | ], 15 | "properties": { 16 | "id": "box 1" 17 | }, 18 | "geometry": { 19 | "type": "Polygon", 20 | "coordinates": [ 21 | [ 22 | [ 23 | 8.748488, 24 | 49.47397 25 | ], 26 | [ 27 | 8.7532061, 28 | 49.47397 29 | ], 30 | [ 31 | 8.7532061, 32 | 49.4767595 33 | ], 34 | [ 35 | 8.748488, 36 | 49.4767595 37 | ], 38 | [ 39 | 8.748488, 40 | 49.47397 41 | ] 42 | ] 43 | ] 44 | } 45 | } 46 | ] 47 | } 48 | } -------------------------------------------------------------------------------- /regression-tests/buildingcount_bbox_attributecompleteness.hurl: -------------------------------------------------------------------------------- 1 | POST {{BASE_URL}}/indicators/attribute-completeness 2 | accept: application/json 3 | [Options] 4 | verbose: true 5 | file,buildingcount_bbox_housenumber.json; 6 | 7 | HTTP * 8 | 9 | [Asserts] 10 | duration < 20000 11 | status == 200 12 | bytes count > 1700 13 | 14 | jsonpath "$.result[0].metadata.name" == "Attribute Completeness" 15 | jsonpath "$.result[0].topic.name" == "Building Count" 16 | jsonpath "$.result[0].result.description" matches /^85\.\d{1}% of all "building count" features \(all: 6\d{1} elements\) in your area of interest have the selected additional attribute house number address \(matched: 5\d{1} elements\)\. The attribute completeness is high \(>75%\)\.$/ 17 | jsonpath "$.result[0].result.figure.data[0].gauge.steps[0].color" == "tomato" 18 | jsonpath "$.result[0].result.label" == "green" 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /regression-tests/buildingcount_bbox_currentness.hurl: -------------------------------------------------------------------------------- 1 | POST {{BASE_URL}}/indicators/currentness 2 | accept: application/json 3 | [Options] 4 | verbose: true 5 | file,buildingcount_bbox.json; 6 | 7 | HTTP * 8 | 9 | [Asserts] 10 | duration < 20000 11 | status == 200 12 | bytes count > 20000 13 | 14 | jsonpath "$.result[0].topic.name" == "Building Count" 15 | jsonpath "$.result[0].result.description" matches /^In the area of interest 1\d{1}% of the 6\d{1} features were edited \(created or modified\) for the last time in the period between .. ... 20.. and .. ... 20... 16 | |Many features are out-of-date.$/ 17 | jsonpath "$.result[0].result.figure.data[1].marker.color" == "#21BA45" 18 | jsonpath "$.result[0].result.label" == "red" 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /regression-tests/buildingcount_bbox_housenumber.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "building-count", 3 | "attributes": ["address-housenumber"], 4 | "bpolys": { 5 | "type": "FeatureCollection", 6 | "features": [ 7 | { 8 | "type": "Feature", 9 | "id": "box 1", 10 | "bbox": [ 11 | 8.748488, 12 | 49.47397, 13 | 8.7532061, 14 | 49.4767595 15 | ], 16 | "properties": { 17 | "id": "box 1" 18 | }, 19 | "geometry": { 20 | "type": "Polygon", 21 | "coordinates": [ 22 | [ 23 | [ 24 | 8.748488, 25 | 49.47397 26 | ], 27 | [ 28 | 8.7532061, 29 | 49.47397 30 | ], 31 | [ 32 | 8.7532061, 33 | 49.4767595 34 | ], 35 | [ 36 | 8.748488, 37 | 49.4767595 38 | ], 39 | [ 40 | 8.748488, 41 | 49.47397 42 | ] 43 | ] 44 | ] 45 | } 46 | } 47 | ] 48 | } 49 | } -------------------------------------------------------------------------------- /regression-tests/hospitals_adminarea_mappingsaturation__no_features.hurl: -------------------------------------------------------------------------------- 1 | POST {{BASE_URL}}/indicators/mapping-saturation 2 | accept: application/json 3 | [Options] 4 | verbose: true 5 | file,hospitals_adminarea.json; 6 | 7 | HTTP * 8 | 9 | [Asserts] 10 | duration < 20000 11 | status == 200 12 | bytes count > 1600 13 | 14 | jsonpath "$.result[0].topic.name" == "Hospitals" 15 | jsonpath "$.result[0].result.description" == "No features were mapped in this region." 16 | jsonpath "$.result[0].result.figure.data[0].labels[0]" == "The creation of the Indicator was unsuccessful." 17 | 18 | 19 | -------------------------------------------------------------------------------- /regression-tests/long-running/roads_bbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "roads", 3 | "bpolys": { 4 | "type": "FeatureCollection", 5 | "features": [ 6 | { 7 | "type": "Feature", 8 | "id": "box 1", 9 | "bbox": [ 10 | 8.748488, 11 | 49.47397, 12 | 8.7532061, 13 | 49.4767595 14 | ], 15 | "properties": { 16 | "id": "box 1" 17 | }, 18 | "geometry": { 19 | "type": "Polygon", 20 | "coordinates": [ 21 | [ 22 | [ 23 | 8.748488, 24 | 49.47397 25 | ], 26 | [ 27 | 8.7532061, 28 | 49.47397 29 | ], 30 | [ 31 | 8.7532061, 32 | 49.4767595 33 | ], 34 | [ 35 | 8.748488, 36 | 49.4767595 37 | ], 38 | [ 39 | 8.748488, 40 | 49.47397 41 | ] 42 | ] 43 | ] 44 | } 45 | } 46 | ] 47 | } 48 | } -------------------------------------------------------------------------------- /regression-tests/long-running/roads_bbox_currentness.hurl: -------------------------------------------------------------------------------- 1 | POST {{BASE_URL}}/indicators/currentness 2 | accept: application/json 3 | [Options] 4 | verbose: true 5 | file,roads_bbox.json; 6 | 7 | HTTP * 8 | 9 | [Asserts] 10 | duration < 300000 11 | status == 200 12 | bytes count > 20000 13 | 14 | jsonpath "$.result[0].topic.name" == "Roads" 15 | jsonpath "$.result[0].result.description" contains "Please note that in the area of interest less than 25 features of the selected topic are present today." 16 | jsonpath "$.result[0].result.figure.data[1].marker.color" == "#21BA45" 17 | jsonpath "$.result[0].result.label" == "green" 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /regression-tests/metadata.hurl: -------------------------------------------------------------------------------- 1 | GET {{BASE_URL}}/metadata?project=all 2 | accept: application/json 3 | [Options] 4 | verbose: true 5 | 6 | HTTP * 7 | 8 | [Asserts] 9 | duration < 2000 10 | status == 200 11 | bytes count > 25000 12 | 13 | jsonpath "$.result.indicators.mapping-saturation.description" contains "Calculate if mapping has saturated." 14 | jsonpath "$.result.indicators.currentness.description" contains "Estimate currentness of features" 15 | jsonpath "$.result.topics.building-count.aggregationType" == "count" 16 | jsonpath "$.result.topics.roads.aggregationType" == "length" 17 | jsonpath "$.result.qualityDimensions.minimal.name" == "Minimal" 18 | jsonpath "$.result.projects.core.description" == "something that is still a TODO" 19 | -------------------------------------------------------------------------------- /regression-tests/road-comparison.hurl: -------------------------------------------------------------------------------- 1 | POST {{BASE_URL}}/indicators/road-comparison 2 | accept: application/json 3 | file, road-comparison.json; 4 | HTTP 200 5 | [Captures] 6 | duration_in_ms: duration 7 | -------------------------------------------------------------------------------- /regression-tests/roads_polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "roads", 3 | "bpolys": { 4 | "type": "FeatureCollection", 5 | "features": [ 6 | { 7 | "type": "Feature", 8 | "id": "area 1", 9 | "properties": { 10 | "id": "area 1" 11 | }, 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 100.4674621, 18 | 5.1745498 19 | ], 20 | [ 21 | 100.4832462, 22 | 5.1827608 23 | ], 24 | [ 25 | 100.4952558, 26 | 5.1701022 27 | ], 28 | [ 29 | 100.4870207, 30 | 5.1547062 31 | ], 32 | [ 33 | 100.4678053, 34 | 5.1588118 35 | ], 36 | [ 37 | 100.4674621, 38 | 5.1745498 39 | ] 40 | ] 41 | ] 42 | } 43 | } 44 | ] 45 | } 46 | } -------------------------------------------------------------------------------- /regression-tests/roads_polygon_attributecompleteness.hurl: -------------------------------------------------------------------------------- 1 | POST {{BASE_URL}}/indicators/attribute-completeness 2 | accept: application/json 3 | [Options] 4 | verbose: true 5 | file,roads_polygon_maxspeed.json; 6 | 7 | HTTP * 8 | 9 | [Asserts] 10 | duration < 5000 11 | status == 200 12 | bytes count > 1900 13 | 14 | jsonpath "$.result[0].metadata.name" == "Attribute Completeness" 15 | jsonpath "$.result[0].topic.name" == "Roads" 16 | jsonpath "$.result[0].result.description" matches /^26\.\d{1}% of all "roads" features \(all: 103\d{1}\.\d{2} km\) in your area of interest have the selected additional attribute maxspeed \(matched: 27\d{1}\.\d{2} km\)\. The attribute completeness is medium \(25%-75%\)\.$/ 17 | jsonpath "$.result[0].result.figure.data[0].gauge.steps[0].color" == "tomato" 18 | jsonpath "$.result[0].result.label" == "yellow" 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /regression-tests/roads_polygon_mappingsaturation.hurl: -------------------------------------------------------------------------------- 1 | POST {{BASE_URL}}/indicators/mapping-saturation 2 | accept: application/json 3 | [Options] 4 | verbose: true 5 | file,roads_polygon.json; 6 | 7 | HTTP * 8 | 9 | [Asserts] 10 | duration < 20000 11 | status == 200 12 | bytes count > 24000 13 | 14 | jsonpath "$.result[0].topic.name" == "Roads" 15 | jsonpath "$.result[0].result.description" matches /^The saturation of the last 3 years is 98.\d{2}%.High saturation has been reached \(97% < Saturation ≤ 100%\).$/ 16 | jsonpath "$.result[0].result.figure.data[0].line.color" == "#2185D0" 17 | jsonpath "$.result[0].result.label" == "green" 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /regression-tests/roads_polygon_maxspeed.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "roads", 3 | "attributes": ["maxspeed"], 4 | "bpolys": { 5 | "type": "FeatureCollection", 6 | "features": [ 7 | { 8 | "type": "Feature", 9 | "id": "area 1", 10 | "properties": { 11 | "id": "area 1" 12 | }, 13 | "geometry": { 14 | "type": "Polygon", 15 | "coordinates": [ 16 | [ 17 | [ 18 | 8.643064, 19 | 49.4412261 20 | ], 21 | [ 22 | 8.6595344, 23 | 49.3665657 24 | ], 25 | [ 26 | 8.790611, 27 | 49.4280458 28 | ], 29 | [ 30 | 8.643064, 31 | 49.4412261 32 | ] 33 | ] 34 | ] 35 | } 36 | } 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /regression-tests/run_hurl_tests_DEV.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export HURL_BASE_URL=http://127.0.0.1:8080 4 | 5 | cd "$(dirname "$0")" 6 | ./__run_hurl_tests_for_stage.sh -------------------------------------------------------------------------------- /regression-tests/run_hurl_tests_PROD.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export HURL_BASE_URL=https://api.quality.ohsome.org/v1 4 | 5 | cd "$(dirname "$0")" 6 | ./__run_hurl_tests_for_stage.sh -------------------------------------------------------------------------------- /regression-tests/run_hurl_tests_TEST.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export HURL_BASE_URL=https://api.quality.ohsome.org/v1-test 4 | 5 | cd "$(dirname "$0")" 6 | ./__run_hurl_tests_for_stage.sh -------------------------------------------------------------------------------- /scripts/create_new_github_release.sh: -------------------------------------------------------------------------------- 1 | 2 | gh release create 1.4.0 -t "1.4.0" \ 3 | -n "See the [changelog](https://github.com/GIScience/ohsome-quality-api/blob/main/CHANGELOG.md#release-140) for release details." 4 | -------------------------------------------------------------------------------- /scripts/functions.sh: -------------------------------------------------------------------------------- 1 | # donation by mcauer as used in the 'ohsome-dashboard' project 2 | 3 | prompt_user() { 4 | local question="$1" # Capture the first argument as the question 5 | while true; do 6 | read -p "$question (y/n): " choice 7 | case "$choice" in 8 | y|Y ) 9 | echo "You chose Yes." 10 | return 0 # Exit the function and proceed 11 | ;; 12 | n|N ) 13 | echo "You chose No. Exiting." 14 | exit 0 # Exit the script 15 | ;; 16 | * ) 17 | echo "Invalid input. Please enter 'y' or 'n'." 18 | ;; 19 | esac 20 | done 21 | } 22 | 23 | 24 | run_sed() { 25 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 26 | sed --in-place=.bak "$1" "$2" 27 | elif [[ "$OSTYPE" == "darwin"* ]]; then 28 | sed -i .bak "$1" "$2" 29 | else 30 | printf "\nOS could not be detected. Please open report manually!\n" 31 | fi 32 | } 33 | 34 | run_open() { 35 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 36 | xdg-open "$1" 37 | elif [[ "$OSTYPE" == "darwin"* ]]; then 38 | open "$1" 39 | else 40 | printf "\nOS could not be detected. Please open URL manually!\n" 41 | fi 42 | } 43 | 44 | # Example usage 45 | #prompt_user "Do you want to continue?" 46 | 47 | # import in other script 48 | 49 | ## Get the directory of the current script 50 | #SCRIPT_DIR="$(dirname "$0")" 51 | # 52 | ## import user prompt 53 | #source "$SCRIPT_DIR/functions.sh" 54 | -------------------------------------------------------------------------------- /scripts/update_swagger_scripts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(dirname "$0")"/../ohsome_quality_api/api/static && \ 4 | wget https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js -O swagger-ui-bundle.js && \ 5 | wget https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css -O swagger-ui.css && \ 6 | wget https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js -O redoc.standalone.js 7 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=giscience 2 | sonar.projectKey=ohsome-quality-api 3 | sonar.projectName=ohsome quality API 4 | 5 | # settings for pull requests 6 | sonar.pullrequest.provider=github 7 | sonar.pullrequest.github.repository=GIScience/ohsome-quality-api 8 | sonar.pullrequest.github.endpoint=https://api.github.com/ 9 | 10 | # supported Python versions 11 | sonar.python.version=3.10, 3.11 12 | 13 | # disable PL/SQL 14 | sonar.plsql.file.suffixes="" 15 | 16 | # exclude static files 17 | sonar.exclusions=ohsome_quality_api/api/static/* 18 | 19 | # exclude non-API worker files from coverage report 20 | sonar.coverage.exclusions=database/**,scripts/**,tests/** 21 | 22 | # the fixtures file only contains values, no code that can be duplicated 23 | sonar.cpd.exclusions=tests/unittests/mapping_saturation/fixtures.py 24 | 25 | sonar.issue.ignore.multicriteria=e1 26 | # S117: local variable and function parameter names should comply with a naming convention 27 | # Ignore for math formula parameter 28 | sonar.issue.ignore.multicriteria.e1.ruleKey=python:S117 29 | # S1192: String literals should not be duplicated 30 | sonar.issue.ignore.multicriteria.e1.ruleKey=python:S1192 31 | sonar.issue.ignore.multicriteria.e1.resourceKey=ohsome_quality_api/indicators/mapping_saturation/models.py 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/__init__.py -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour, source 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour, source 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators.py::test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators.py::test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers0-schema0].approved.txt: -------------------------------------------------------------------------------- 1 | Querying the ohsome API failed! Invalid filter syntax. Please look at the additional info and examples about the filter parameter at https://docs.ohsome.org/ohsome-api. Detailed error message: line 1, column 63: whitespaces, EQUALS (=), NOT_EQUALS (!=) or in expected, f encountered. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers1-schema1].approved.txt: -------------------------------------------------------------------------------- 1 | Querying the ohsome API failed! Invalid filter syntax. Please look at the additional info and examples about the filter parameter at https://docs.ohsome.org/ohsome-api. Detailed error message: line 1, column 63: whitespaces, EQUALS (=), NOT_EQUALS (!=) or in expected, f encountered. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].received.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].received.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers0-schema0].approved.txt: -------------------------------------------------------------------------------- 1 | Querying the ohsome API failed! Invalid filter syntax. Please look at the additional info and examples about the filter parameter at https://docs.ohsome.org/ohsome-api. Detailed error message: line 1, column 63: whitespaces, EQUALS (=), NOT_EQUALS (!=) or in expected, f encountered. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers1-schema1].approved.txt: -------------------------------------------------------------------------------- 1 | Querying the ohsome API failed! Invalid filter syntax. Please look at the additional info and examples about the filter parameter at https://docs.ohsome.org/ohsome-api. Detailed error message: line 1, column 63: whitespaces, EQUALS (=), NOT_EQUALS (!=) or in expected, f encountered. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour, source 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour, source 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/api/test_indicators_land_cover_thematic_accuracy.py-test_invalid_class.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "1.9.0", 3 | "detail": [ 4 | { 5 | "ctx": { 6 | "expected": "'11', '12', '13', '14', '21', '22', '23', '24', '31', '32', '33', '41', '42', '51' or '52'" 7 | }, 8 | "input": "1", 9 | "loc": [ 10 | "body", 11 | "corineLandCoverClass" 12 | ], 13 | "msg": "Input should be '11', '12', '13', '14', '21', '22', '23', '24', '31', '32', '33', '41', '42', '51' or '52'", 14 | "type": "enum" 15 | } 16 | ], 17 | "type": "RequestValidationError" 18 | } 19 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_attribute_completeness.py-TestCalculation-test_calculate[indicator0].approved.txt: -------------------------------------------------------------------------------- 1 | 39.8% of all "buildings (count)" features (all: 30337 elements) in your area of interest have the selected additional attribute height of buildings (matched: 12083 elements). The attribute completeness is medium (25%-75%). 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_attribute_completeness.py-TestCalculation-test_calculate[indicator1].approved.txt: -------------------------------------------------------------------------------- 1 | 39.8% of all "buildings (count)" features (all: 30337 elements) in your area of interest have the selected additional attribute Height (matched: 12083 elements). The attribute completeness is medium (25%-75%). 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_attribute_completeness.py-TestFigure-test_create_figure[indicator0].approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "domain": { 5 | "x": [ 6 | 0, 7 | 1 8 | ], 9 | "y": [ 10 | 0, 11 | 1 12 | ] 13 | }, 14 | "gauge": { 15 | "axis": { 16 | "range": [ 17 | 0, 18 | 100 19 | ], 20 | "tickcolor": "darkblue", 21 | "tickfont": { 22 | "color": "black", 23 | "size": 20 24 | }, 25 | "ticksuffix": "%", 26 | "tickwidth": 1 27 | }, 28 | "bar": { 29 | "color": "black" 30 | }, 31 | "steps": [ 32 | { 33 | "color": "tomato", 34 | "range": [ 35 | 0, 36 | 25.0 37 | ] 38 | }, 39 | { 40 | "color": "gold", 41 | "range": [ 42 | 25.0, 43 | 75.0 44 | ] 45 | }, 46 | { 47 | "color": "darkseagreen", 48 | "range": [ 49 | 75.0, 50 | 100 51 | ] 52 | } 53 | ] 54 | }, 55 | "mode": "gauge+number", 56 | "number": { 57 | "suffix": "%" 58 | }, 59 | "type": "indicator", 60 | "value": 39.8293 61 | } 62 | ], 63 | "layout": { 64 | "autosize": true, 65 | "font": { 66 | "color": "black", 67 | "family": "Arial" 68 | }, 69 | "plot_bgcolor": "rgba(0,0,0,0)", 70 | "xaxis": { 71 | "fixedrange": true, 72 | "range": [ 73 | -1, 74 | 1 75 | ], 76 | "showgrid": false, 77 | "visible": false 78 | }, 79 | "yaxis": { 80 | "fixedrange": true, 81 | "range": [ 82 | 0, 83 | 1 84 | ], 85 | "showgrid": false, 86 | "visible": false 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_attribute_completeness.py-TestFigure-test_create_figure[indicator1].approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "domain": { 5 | "x": [ 6 | 0, 7 | 1 8 | ], 9 | "y": [ 10 | 0, 11 | 1 12 | ] 13 | }, 14 | "gauge": { 15 | "axis": { 16 | "range": [ 17 | 0, 18 | 100 19 | ], 20 | "tickcolor": "darkblue", 21 | "tickfont": { 22 | "color": "black", 23 | "size": 20 24 | }, 25 | "ticksuffix": "%", 26 | "tickwidth": 1 27 | }, 28 | "bar": { 29 | "color": "black" 30 | }, 31 | "steps": [ 32 | { 33 | "color": "tomato", 34 | "range": [ 35 | 0, 36 | 25.0 37 | ] 38 | }, 39 | { 40 | "color": "gold", 41 | "range": [ 42 | 25.0, 43 | 75.0 44 | ] 45 | }, 46 | { 47 | "color": "darkseagreen", 48 | "range": [ 49 | 75.0, 50 | 100 51 | ] 52 | } 53 | ] 54 | }, 55 | "mode": "gauge+number", 56 | "number": { 57 | "suffix": "%" 58 | }, 59 | "type": "indicator", 60 | "value": 39.8293 61 | } 62 | ], 63 | "layout": { 64 | "autosize": true, 65 | "font": { 66 | "color": "black", 67 | "family": "Arial" 68 | }, 69 | "plot_bgcolor": "rgba(0,0,0,0)", 70 | "xaxis": { 71 | "fixedrange": true, 72 | "range": [ 73 | -1, 74 | 1 75 | ], 76 | "showgrid": false, 77 | "visible": false 78 | }, 79 | "yaxis": { 80 | "fixedrange": true, 81 | "range": [ 82 | 0, 83 | 1 84 | ], 85 | "showgrid": false, 86 | "visible": false 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_attribute_completeness.py-test_create_description_attribute_filter.approved.txt: -------------------------------------------------------------------------------- 1 | 20.0% of all "buildings (count)" features (all: 10 elements) in your area of interest have the selected additional attribute Height (matched: 2 elements). 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_attribute_completeness.py-test_create_description_attribute_keys_multiple.approved.txt: -------------------------------------------------------------------------------- 1 | 20.0% of all "buildings (count)" features (all: 10 elements) in your area of interest have the selected additional attributes height of buildings, house number, street address (matched: 2 elements). 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_attribute_completeness.py-test_create_description_attribute_keys_single.approved.txt: -------------------------------------------------------------------------------- 1 | 20.0% of all "buildings (count)" features (all: 10 elements) in your area of interest have the selected additional attribute height of buildings (matched: 2 elements). 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate.approved.txt: -------------------------------------------------------------------------------- 1 | The completeness of OSM buildings in your area-of-interest is high. The completeness in comparison to EUBUCCO is 101.04%. The completeness in comparison to Microsoft Building Footprints is 101.04%. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate_above_one_th.approved.txt: -------------------------------------------------------------------------------- 1 | Comparison could not be made. OSM has substantivly more buildings than the reference datasets. The reference dataset is likely to miss many buildings. The completeness in comparison to EUBUCCO is 505.21%. The completeness in comparison to Microsoft Building Footprints is 505.21%. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate_above_one_th_and_expected.approved.txt: -------------------------------------------------------------------------------- 1 | The completeness of OSM buildings in your area-of-interest is medium. The completeness in comparison to EUBUCCO is 84.2%. The completeness in comparison to Microsoft Building Footprints is 84.2%. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate_no_intersection.approved.txt: -------------------------------------------------------------------------------- 1 | Comparison could not be made. EUBUCCO does not cover your area-of-interest. Microsoft Buildings does not cover your area-of-interest. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate_some_intersection.approved.txt: -------------------------------------------------------------------------------- 1 | The completeness of OSM buildings in your area-of-interest is high. EUBUCCO does not cover your area-of-interest. The completeness in comparison to Microsoft Building Footprints is 101.04%. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_building_comparison.py-TestFigure-test_create_figure.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "hoverinfo": "text", 5 | "hovertext": [ 6 | "OSM (Feb 02, 2025)", 7 | "OSM (Feb 02, 2025)" 8 | ], 9 | "marker": { 10 | "color": "#767676" 11 | }, 12 | "name": "OSM building area (5.05 km\u00b2, 5.05 km\u00b2)", 13 | "type": "bar", 14 | "x": [ 15 | "EUBUCCO", 16 | "Microsoft Building Footprints" 17 | ], 18 | "y": [ 19 | 5.05, 20 | 5.05 21 | ] 22 | }, 23 | { 24 | "hoverinfo": "text", 25 | "hovertext": [ 26 | "EUBUCCO (Nov 3, 2022)", 27 | "Microsoft Building Footprints (July 5, 2022)" 28 | ], 29 | "legendgroup": "Reference", 30 | "marker": { 31 | "color": [ 32 | "#A333C8", 33 | "#F2711C" 34 | ] 35 | }, 36 | "name": "EUBUCCO (5.0 km\u00b2)", 37 | "type": "bar", 38 | "x": [ 39 | "EUBUCCO", 40 | "Microsoft Building Footprints" 41 | ], 42 | "y": [ 43 | 5.0, 44 | 5.0 45 | ] 46 | } 47 | ], 48 | "layout": { 49 | "barmode": "group", 50 | "legend": { 51 | "entrywidth": 270, 52 | "orientation": "h", 53 | "x": 0.5, 54 | "xanchor": "center", 55 | "y": -0.1, 56 | "yanchor": "top" 57 | }, 58 | "shapes": [ 59 | { 60 | "fillcolor": "#F2711C", 61 | "layer": "below", 62 | "legendgroup": "Reference", 63 | "line": { 64 | "width": 0 65 | }, 66 | "name": "Microsoft Building Footprints (5.0 km\u00b2)", 67 | "showlegend": true, 68 | "type": "rect", 69 | "x0": 0, 70 | "x1": 0, 71 | "y0": 0, 72 | "y1": 0 73 | } 74 | ], 75 | "showlegend": true, 76 | "title": { 77 | "text": "Building Comparison" 78 | }, 79 | "yaxis": { 80 | "title": { 81 | "text": "Building Area [km\u00b2]" 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_building_comparison.py-TestFigure-test_create_figure_above_one_th.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "hoverinfo": "text", 5 | "hovertext": [ 6 | "OSM (Feb 02, 2025)", 7 | "OSM (Feb 02, 2025)" 8 | ], 9 | "marker": { 10 | "color": "#767676" 11 | }, 12 | "name": "OSM building area (5.05 km\u00b2, 5.05 km\u00b2)", 13 | "type": "bar", 14 | "x": [ 15 | "EUBUCCO", 16 | "Microsoft Building Footprints" 17 | ], 18 | "y": [ 19 | 5.05, 20 | 5.05 21 | ] 22 | }, 23 | { 24 | "hoverinfo": "text", 25 | "hovertext": [ 26 | "EUBUCCO (Nov 3, 2022)", 27 | "Microsoft Building Footprints (July 5, 2022)" 28 | ], 29 | "legendgroup": "Reference", 30 | "marker": { 31 | "color": [ 32 | "#A333C8", 33 | "#F2711C" 34 | ] 35 | }, 36 | "name": "EUBUCCO (5.0 km\u00b2)", 37 | "type": "bar", 38 | "x": [ 39 | "EUBUCCO", 40 | "Microsoft Building Footprints" 41 | ], 42 | "y": [ 43 | 5.0, 44 | 5.0 45 | ] 46 | } 47 | ], 48 | "layout": { 49 | "barmode": "group", 50 | "legend": { 51 | "entrywidth": 270, 52 | "orientation": "h", 53 | "x": 0.5, 54 | "xanchor": "center", 55 | "y": -0.1, 56 | "yanchor": "top" 57 | }, 58 | "shapes": [ 59 | { 60 | "fillcolor": "#F2711C", 61 | "layer": "below", 62 | "legendgroup": "Reference", 63 | "line": { 64 | "width": 0 65 | }, 66 | "name": "Microsoft Building Footprints (5.0 km\u00b2)", 67 | "showlegend": true, 68 | "type": "rect", 69 | "x0": 0, 70 | "x1": 0, 71 | "y0": 0, 72 | "y1": 0 73 | } 74 | ], 75 | "showlegend": true, 76 | "title": { 77 | "text": "Building Comparison" 78 | }, 79 | "yaxis": { 80 | "title": { 81 | "text": "Building Area [km\u00b2]" 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_building_comparison.py-TestFigure-test_create_figure_building_area_zero.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "hoverinfo": "none", 5 | "labels": [ 6 | "The creation of the Indicator was unsuccessful." 7 | ], 8 | "marker": { 9 | "colors": [ 10 | "rgba(0, 0, 0, 0)" 11 | ] 12 | }, 13 | "textposition": "inside", 14 | "texttemplate": "%{label}", 15 | "type": "pie", 16 | "values": [ 17 | 1 18 | ] 19 | } 20 | ], 21 | "layout": { 22 | "paper_bgcolor": "white", 23 | "plot_bgcolor": "white", 24 | "showlegend": false, 25 | "title": { 26 | "text": "Building Comparison" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_calculate.approved.txt: -------------------------------------------------------------------------------- 1 | In the area of interest 55% of the 29931 features were edited (created or modified) for the last time in the period between 02 Feb 2022 and 02 Feb 2025. 2 | Most features are up-to-date. 3 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_low_contributions.approved.txt: -------------------------------------------------------------------------------- 1 | Please note that in the area of interest less than 25 features of the selected topic are present today. In the area of interest 55% of the 20 features were edited (created or modified) for the last time in the period between 02 Feb 2022 and 02 Feb 2025. 2 | Most features are up-to-date. 3 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_months_without_edit.approved.txt: -------------------------------------------------------------------------------- 1 | Please note that there was no mapping activity for 13 months in this region. In the area of interest 55% of the 30 features were edited (created or modified) for the last time in the period between 02 Feb 2022 and 02 Feb 2025. 2 | Most features are up-to-date. 3 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_no_amenities.approved.txt: -------------------------------------------------------------------------------- 1 | In the area of interest no features of the selected topic are present today. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_no_subway_stations.approved.txt: -------------------------------------------------------------------------------- 1 | In the area of interest no features of the selected topic are present today. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_indicators-test_indicators_attribute_completeness_with_invalid_attribute_for_topic-headers0-schema0.approved.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_indicators-test_indicators_attribute_completeness_with_invalid_attribute_for_topic-headers1-schema1.approved.txt: -------------------------------------------------------------------------------- 1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_land-cover-thematic-accuracy.py-test_calculate.approved.txt: -------------------------------------------------------------------------------- 1 | precision recall f1-score support 2 | 3 | 0 0.00 0.00 0.00 0.0 4 | 11 0.94 0.86 0.90 13.681696054340627 5 | 12 0.84 0.87 0.85 4.868706606602494 6 | 13 0.00 0.00 0.00 0.0 7 | 14 0.13 0.42 0.20 0.3617314972878062 8 | 21 0.86 0.91 0.89 15.526750634345152 9 | 22 0.64 0.76 0.70 2.2997486561073313 10 | 23 0.75 0.44 0.55 6.712711587504546 11 | 24 0.00 0.00 0.00 0.084999664294624 12 | 31 0.82 0.92 0.87 10.490463269571688 13 | 32 0.00 0.00 0.00 0.2576135038588164 14 | 50 0.00 0.00 0.00 0.0 15 | 51 0.00 0.00 0.00 0.033118424422509736 16 | 17 | accuracy 0.82 54.31753989833559 18 | macro avg 0.38 0.40 0.38 54.31753989833559 19 | weighted avg 0.84 0.82 0.82 54.31753989833559 20 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_land-cover-thematic-accuracy.py-test_calculate.received.txt: -------------------------------------------------------------------------------- 1 | precision recall f1-score support 2 | 3 | 0 0.00 0.00 0.00 0.0 4 | 11 0.94 0.86 0.90 13.681696054340627 5 | 12 0.84 0.87 0.85 4.868706606602494 6 | 13 0.00 0.00 0.00 0.0 7 | 14 0.13 0.42 0.20 0.3617314972878062 8 | 21 0.86 0.91 0.89 15.526750634345152 9 | 22 0.64 0.76 0.70 2.2997486561073313 10 | 23 0.75 0.44 0.55 6.712711587504546 11 | 24 0.00 0.00 0.00 0.084999664294624 12 | 31 0.82 0.92 0.87 10.490463269571688 13 | 32 0.00 0.00 0.00 0.2576135038588164 14 | 50 0.00 0.00 0.00 0.0 15 | 51 0.00 0.00 0.00 0.033118424422509736 16 | 17 | accuracy 0.82 54.31753989833559 18 | macro avg 0.38 0.40 0.38 54.31753989833559 19 | weighted avg 0.84 0.82 0.82 54.31753989833559 20 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_calculate_multi_class/description.approved.txt: -------------------------------------------------------------------------------- 1 | In your area-of-interes the thematic accuracy of all CLC classes is 82.32 % (medium agreement between OSM and CORINE). This suggests that OSM does capture some land cover categories but may lack detail or accuracy. The F1-Score is used as statistical metric which considers both the correctness and completeness of the land cover categories. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_calculate_multi_class/report.approved.txt: -------------------------------------------------------------------------------- 1 | precision recall f1-score support 2 | 3 | 0 0.00 nan 0.00 0.0 4 | 11 0.94 0.86 0.90 13.681696054340627 5 | 12 0.84 0.87 0.85 4.868706606602494 6 | 13 0.00 nan 0.00 0.0 7 | 14 0.13 0.42 0.20 0.3617314972878062 8 | 21 0.86 0.91 0.89 15.526750634345152 9 | 22 0.64 0.76 0.70 2.2997486561073313 10 | 23 0.75 0.44 0.55 6.712711587504546 11 | 24 nan 0.00 0.00 0.084999664294624 12 | 31 0.82 0.92 0.87 10.490463269571688 13 | 32 0.00 0.00 0.00 0.2576135038588164 14 | 50 0.00 nan 0.00 0.0 15 | 51 nan 0.00 0.00 0.033118424422509736 16 | 17 | accuracy 0.82 54.31753989833559 18 | macro avg 0.45 0.52 0.38 54.31753989833559 19 | weighted avg 0.84 0.82 0.82 54.31753989833559 20 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_calculate_single_class/description.approved.txt: -------------------------------------------------------------------------------- 1 | In your area-of-interes the thematic accuracy of CLC class Pastures is 47.95 % (low agreement between OSM and CORINE). This suggests that OSM has a poor classification quality or incomplete land cover data. The F1-Score is used as statistical metric which considers both the correctness and completeness of the land cover categories. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_calculate_single_class/report.approved.txt: -------------------------------------------------------------------------------- 1 | precision recall f1-score support 2 | 3 | 0 0.00 0.00 0.00 1.000418439925055 4 | 1 0.75 0.44 0.55 6.712711587504546 5 | 6 | accuracy 0.38 7.713130027429601 7 | macro avg 0.37 0.22 0.28 7.713130027429601 8 | weighted avg 0.65 0.38 0.48 7.713130027429601 9 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_figure_single_class.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "colorbar": { 5 | "title": { 6 | "text": "Proportion" 7 | } 8 | }, 9 | "colorscale": [ 10 | [ 11 | 0.0, 12 | "#440154" 13 | ], 14 | [ 15 | 0.1111111111111111, 16 | "#482878" 17 | ], 18 | [ 19 | 0.2222222222222222, 20 | "#3e4989" 21 | ], 22 | [ 23 | 0.3333333333333333, 24 | "#31688e" 25 | ], 26 | [ 27 | 0.4444444444444444, 28 | "#26828e" 29 | ], 30 | [ 31 | 0.5555555555555556, 32 | "#1f9e89" 33 | ], 34 | [ 35 | 0.6666666666666666, 36 | "#35b779" 37 | ], 38 | [ 39 | 0.7777777777777778, 40 | "#6ece58" 41 | ], 42 | [ 43 | 0.8888888888888888, 44 | "#b5de2b" 45 | ], 46 | [ 47 | 1.0, 48 | "#fde725" 49 | ] 50 | ], 51 | "hovertemplate": "Predicted: %{x}
Actual: %{y}
Value: %{z:.2%}", 52 | "text": { 53 | "bdata": "AAAAAAAAAADkP/opHprAP/6TkLrPXd8/D0xyMCFV2D8=", 54 | "dtype": "f8", 55 | "shape": "2, 2" 56 | }, 57 | "texttemplate": "%{text:.2%}", 58 | "type": "heatmap", 59 | "x": [ 60 | "Other classes", 61 | "Agricultural areas
Pastures" 62 | ], 63 | "y": [ 64 | "Other classes", 65 | "Agricultural areas
Pastures" 66 | ], 67 | "z": { 68 | "bdata": "AAAAAAAAAADkP/opHprAP/6TkLrPXd8/D0xyMCFV2D8=", 69 | "dtype": "f8", 70 | "shape": "2, 2" 71 | } 72 | } 73 | ], 74 | "layout": { 75 | "xaxis": { 76 | "ticktext": [ 77 | "Other classes", 78 | "Agricultural areas
Pastures" 79 | ], 80 | "title": { 81 | "text": "Corine Land Cover Class in OSM" 82 | } 83 | }, 84 | "yaxis": { 85 | "ticktext": [ 86 | "Other classes", 87 | "Agricultural areas
Pastures" 88 | ], 89 | "title": { 90 | "text": "Corine Land Cover Class (actual)" 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_mapping_saturation.py-TestCalculation-test_calculate.approved.txt: -------------------------------------------------------------------------------- 1 | The saturation of the last 3 years is 99.5%. High saturation has been reached (97% < Saturation ≤ 100%). 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_road_comparison.py-TestCalculate-test_calculate.approved.txt: -------------------------------------------------------------------------------- 1 | Microsoft Roads has a road length of 0.03 km, of which 0.02 km are covered by roads in OSM. The completeness of OSM roads in your area-of-interest is medium. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_road_comparison.py-TestCalculate-test_calculate_no_intersection.approved.txt: -------------------------------------------------------------------------------- 1 | Reference dataset Microsoft Roads does not cover area-of-interest. Comparison could not be made. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_road_comparison.py-TestCalculate-test_calculate_reference_lenght_0.approved.txt: -------------------------------------------------------------------------------- 1 | Microsoft Roads does not contain roads for your area-of-interest. Comparison could not be made. 2 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_road_comparison.py-TestFigure-test_create_figure.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "hoverinfo": "text", 5 | "hovertext": "OSM Covered: 0.02 km (Apr 05, 2024)", 6 | "marker": { 7 | "color": "#767676", 8 | "line": { 9 | "color": "#767676", 10 | "width": 1 11 | } 12 | }, 13 | "name": "80.0% of Microsoft Roads are matched by OSM", 14 | "type": "bar", 15 | "width": 0.4, 16 | "x": [ 17 | "Microsoft Roads" 18 | ], 19 | "y": [ 20 | 80.0 21 | ] 22 | }, 23 | { 24 | "hoverinfo": "text", 25 | "hovertext": "Not OSM Covered: 0.01 km (Apr 05, 2024)", 26 | "marker": { 27 | "color": "rgba(0,0,0,0)", 28 | "line": { 29 | "color": "#767676", 30 | "width": 1 31 | } 32 | }, 33 | "name": "20.0% of Microsoft Roads are not matched by OSM", 34 | "textposition": "outside", 35 | "type": "bar", 36 | "width": 0.4, 37 | "x": [ 38 | "Microsoft Roads" 39 | ], 40 | "y": [ 41 | 20.0 42 | ] 43 | } 44 | ], 45 | "layout": { 46 | "barmode": "stack", 47 | "legend": { 48 | "entrywidth": 270, 49 | "orientation": "h", 50 | "x": 0.5, 51 | "xanchor": "center", 52 | "y": -0.1, 53 | "yanchor": "top" 54 | }, 55 | "title": { 56 | "text": "Road Comparison" 57 | }, 58 | "yaxis": { 59 | "title": { 60 | "text": "Matched road length [%]" 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/approvals/integrationtests/indicators/test_road_comparison.py-TestFigure-test_create_figure_building_area_zero.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "hoverinfo": "none", 5 | "labels": [ 6 | "The creation of the Indicator was unsuccessful." 7 | ], 8 | "marker": { 9 | "colors": [ 10 | "rgba(0, 0, 0, 0)" 11 | ] 12 | }, 13 | "textposition": "inside", 14 | "texttemplate": "%{label}", 15 | "type": "pie", 16 | "values": [ 17 | 1 18 | ] 19 | } 20 | ], 21 | "layout": { 22 | "paper_bgcolor": "white", 23 | "plot_bgcolor": "white", 24 | "showlegend": false, 25 | "title": { 26 | "text": "Road Comparison" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/approvaltests_namers.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Dict 4 | 5 | from approvaltests.namer.namer_base import NamerBase 6 | 7 | 8 | class PytestNamer(NamerBase): 9 | def __init__(self, extension: str = ".txt", postfix: str = ""): 10 | """An approval tests Namer for naming approved and received files. 11 | 12 | This Namer uses the `PYTEST_CURRENT_TEST` environment variable, which 13 | consist of the node ID and the current stage to derive names: 14 | `relative/path/to/test_file.py::TestClass::test_func[a] (call)` 15 | 16 | Above example will be translated to: 17 | `relative/path/to/test_file.py--TestClass--test_func[a]` 18 | 19 | Following changes are applied to the `PYTEST_CURRENT_TEST` environmen 20 | variables: 21 | - To avoid the forbidden character `:` in system paths, it is 22 | replaced by `-`. 23 | - During a pytest test session, stages can be setup, teardown or 24 | call. Approval tests should only be used during the call stage 25 | and therefore the ` (call)` postfix is removed. 26 | 27 | If verify is called multiple times use `postfix` parameter to 28 | differentiate names. 29 | """ 30 | self.nodeid: Path = Path(os.environ["PYTEST_CURRENT_TEST"]) 31 | self.postfix = postfix 32 | NamerBase.__init__(self, extension) 33 | 34 | def get_file_name(self) -> Path: 35 | """File name is pytest nodeid w/out directory name and pytest.""" 36 | file_name = str(self.nodeid.name).replace(" (call)", "").replace("::", "-") 37 | return Path(file_name) / self.postfix 38 | 39 | def get_directory(self) -> Path: 40 | """Directory is `tests/approval/{module}` derived from pytest nodeid.""" 41 | base_dir = Path(__file__).parent / "approvals" 42 | parts = self.nodeid.parent.parts 43 | directory = Path(*[p for p in parts if p not in ["tests"]]) 44 | return base_dir / directory 45 | 46 | def get_config(self) -> Dict[str, str]: 47 | return {} 48 | -------------------------------------------------------------------------------- /tests/approvaltests_scrubbers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | import re 4 | 5 | 6 | def round_fitted_y_values(figure: dict) -> dict: 7 | for data in figure["data"]: 8 | for i, number in enumerate(data["y"]): 9 | data["y"][i] = round(number) 10 | return figure 11 | 12 | 13 | def round_axis_range(figure: dict) -> dict: 14 | figure["layout"]["yaxis"]["range"][1] = math.ceil( 15 | figure["layout"]["yaxis"]["range"][1] 16 | ) 17 | return figure 18 | 19 | 20 | def replace_float_in_hovertext(figure: dict) -> dict: 21 | for data in figure["data"]: 22 | if "hovertext" in data.keys(): 23 | data["hovertext"] = re.sub( 24 | "[+-]?([0-9]*[.])?[0-9]+", 25 | "scrubbed", 26 | data["hovertext"], 27 | ) 28 | return figure 29 | 30 | 31 | def scrub_mapping_saturation_figure(figure: str): 32 | fig = json.loads(figure) 33 | fig = round_fitted_y_values(fig) 34 | fig = replace_float_in_hovertext(fig) 35 | fig = round_axis_range(fig) 36 | return json.dumps(fig) 37 | -------------------------------------------------------------------------------- /tests/fixtures/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/fixtures/empty.png -------------------------------------------------------------------------------- /tests/fixtures/feature-germany-berlin-friedrichshain-kreuzberg.geojson: -------------------------------------------------------------------------------- 1 | {"type": "Feature", "geometry": {"type":"MultiPolygon","coordinates":[[[[13.3682291,52.4933357],[13.3765623,52.4916704],[13.373518,52.4879703],[13.3740185,52.4851671],[13.3716065,52.484979],[13.3942612,52.4857753],[13.3946131,52.4840216],[13.4063457,52.4827923],[13.4078874,52.4888602],[13.4237047,52.4863837],[13.42535,52.488182],[13.4203999,52.4958638],[13.4392625,52.4896095],[13.4455486,52.494856],[13.4529296,52.4977068],[13.4633549,52.4954855],[13.4642795,52.4937189],[13.476147,52.4899443],[13.4786285,52.4870327],[13.481678,52.487638],[13.4829538,52.4860501],[13.4914434,52.4882828],[13.4730756,52.4990026],[13.4685734,52.4996572],[13.4711643,52.5051377],[13.4762658,52.5104393],[13.4758875,52.5148631],[13.4777288,52.5147204],[13.4723904,52.5205711],[13.4626951,52.5199276],[13.4552901,52.5212723],[13.4561572,52.5224576],[13.4521828,52.5277958],[13.4471683,52.5264084],[13.4422774,52.5310256],[13.4387482,52.5287778],[13.4236421,52.527915],[13.4197532,52.5255456],[13.4291879,52.5212028],[13.4259266,52.5183933],[13.4227803,52.5122343],[13.4294017,52.5085626],[13.4271997,52.5056674],[13.4140723,52.5040371],[13.4099694,52.5069278],[13.4080301,52.5061832],[13.4002283,52.5093816],[13.3992301,52.5080775],[13.3776503,52.5079658],[13.3749797,52.5033759],[13.3736093,52.504164],[13.3695143,52.498879],[13.3682291,52.4933357]]]]}, "properties": {"osm_id": -55764, "boundary": "administrative", "admin_level": 9, "parents": "-62422,-51477", "name": "Friedrichshain-Kreuzberg", "local_name": "Friedrichshain-Kreuzberg", "name_en": null}} 2 | -------------------------------------------------------------------------------- /tests/fixtures/feature-malta.geojson: -------------------------------------------------------------------------------- 1 | {"type": "Feature", "geometry": {"coordinates": [[[14.401600308089286, 35.98854348048188],[14.401600308089286,35.9033309465853],[14.495720596358126,35.9033309465853],[14.495720596358126,35.98854348048188],[14.401600308089286,35.98854348048188]]], "type": "Polygon"}} 2 | -------------------------------------------------------------------------------- /tests/integrationtests/__init__.py: -------------------------------------------------------------------------------- 1 | from ohsome_quality_api.config import configure_logging 2 | 3 | configure_logging() 4 | -------------------------------------------------------------------------------- /tests/integrationtests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/api/__init__.py -------------------------------------------------------------------------------- /tests/integrationtests/api/response_schema.py: -------------------------------------------------------------------------------- 1 | """This module defines schemata for API responses. 2 | 3 | Every API response must adhere to the general schema. 4 | Additionally, API responses need to adhere to one additional schema depending on which 5 | endpoint is used. 6 | """ 7 | 8 | from schema import Optional as Opt 9 | from schema import Or, Schema 10 | 11 | 12 | def get_indicator_properties_template(): 13 | return { 14 | "metadata": { 15 | "name": str, 16 | "description": str, 17 | }, 18 | "topic": { 19 | "name": str, 20 | "description": str, 21 | }, 22 | "result": { 23 | "timestamp": str, 24 | "timestampOSM": Or(str), 25 | "value": Or(float, str, int, None), 26 | "label": str, 27 | "description": str, 28 | }, 29 | } 30 | 31 | 32 | def get_general_schema() -> Schema: 33 | """General response schema. 34 | 35 | Every response should have this schema 36 | """ 37 | return Schema( 38 | { 39 | "apiVersion": str, 40 | "attribution": { 41 | "url": str, 42 | Opt("text"): str, 43 | }, 44 | }, 45 | ignore_extra_keys=True, 46 | ) 47 | 48 | 49 | def get_result_schema() -> Schema: 50 | """Response schema for all endpoints. 51 | 52 | Excluded is the endpoint `/indicator`. 53 | """ 54 | return Schema({"result": list}, ignore_extra_keys=True) 55 | 56 | 57 | def get_featurecollection_schema() -> Schema: 58 | """Response schema for responses of type FeatureCollection""" 59 | return Schema( 60 | {"type": "FeatureCollection", "features": list}, ignore_extra_keys=True 61 | ) 62 | 63 | 64 | def get_indicator_feature_schema() -> Schema: 65 | properties = get_indicator_properties_template() 66 | return Schema( 67 | { 68 | "type": "Feature", 69 | "geometry": dict, 70 | Opt("id"): Or(str, int), 71 | "properties": properties, 72 | }, 73 | ignore_extra_keys=True, 74 | ) 75 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_indicators_land_cover_thematic_accuracy.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from approvaltests import Options, verify_as_json 5 | 6 | from tests.approvaltests_namers import PytestNamer 7 | from tests.conftest import FIXTURE_DIR 8 | from tests.integrationtests.api.test_indicators import ( 9 | RESPONSE_SCHEMA_GEOJSON, 10 | RESPONSE_SCHEMA_JSON, 11 | ) 12 | from tests.integrationtests.utils import oqapi_vcr 13 | 14 | ENDPOINT = "/indicators/land-cover-thematic-accuracy" 15 | 16 | 17 | @pytest.fixture 18 | def mock_db_fetch(monkeypatch): 19 | async def fetch(*_): 20 | with open( 21 | FIXTURE_DIR / "land-cover-thematic-accuracy-db-fetch-results.json", "r" 22 | ) as file: 23 | return json.load(file) 24 | 25 | monkeypatch.setattr( 26 | "ohsome_quality_api.indicators.land_cover_thematic_accuracy.indicator.client.fetch", 27 | fetch, 28 | ) 29 | 30 | 31 | @oqapi_vcr.use_cassette 32 | @pytest.mark.parametrize( 33 | "headers,schema", 34 | [ 35 | ({"accept": "application/json"}, RESPONSE_SCHEMA_JSON), 36 | ({"accept": "application/geo+json"}, RESPONSE_SCHEMA_GEOJSON), 37 | ], 38 | ) 39 | def test_multi_class(client, bpolys, headers, schema, mock_db_fetch): 40 | # corine class parameter is optional (default all corine classes) 41 | parameters = {"bpolys": bpolys, "topic": "land-cover"} 42 | response = client.post(ENDPOINT, json=parameters, headers=headers) 43 | assert response.status_code == 200 44 | assert schema.is_valid(response.json()) 45 | 46 | 47 | @oqapi_vcr.use_cassette 48 | @pytest.mark.parametrize( 49 | "headers,schema", 50 | [ 51 | ({"accept": "application/json"}, RESPONSE_SCHEMA_JSON), 52 | ({"accept": "application/geo+json"}, RESPONSE_SCHEMA_GEOJSON), 53 | ], 54 | ) 55 | def test_single_class(client, bpolys, headers, schema, mock_db_fetch): 56 | # Corine class 23 are Pastures 57 | parameters = {"bpolys": bpolys, "topic": "land-cover", "corineLandCoverClass": "23"} 58 | response = client.post(ENDPOINT, json=parameters, headers=headers) 59 | assert response.status_code == 200 60 | assert schema.is_valid(response.json()) 61 | 62 | 63 | def test_invalid_topic(client, bpolys): 64 | parameters = {"bpolys": bpolys, "topic": "building-count"} 65 | response = client.post(ENDPOINT, json=parameters) 66 | assert response.status_code == 422 67 | verify_as_json(response.json(), options=Options().with_namer(PytestNamer())) 68 | 69 | 70 | def test_invalid_class(client, bpolys): 71 | parameters = {"bpolys": bpolys, "topic": "land-cover", "corineLandCoverClass": "1"} 72 | response = client.post(ENDPOINT, json=parameters) 73 | assert response.status_code == 422 74 | verify_as_json(response.json(), options=Options().with_namer(PytestNamer())) 75 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_indicators_mapping_saturation_data.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from schema import Optional, Or, Schema 4 | 5 | from tests.unittests.mapping_saturation.fixtures import VALUES_1 as DATA 6 | 7 | ENDPOINT = "/indicators/mapping-saturation/data" 8 | 9 | 10 | RESPONSE_SCHEMA_JSON = Schema( 11 | schema={ 12 | "apiVersion": str, 13 | "attribution": { 14 | "url": str, 15 | Optional("text"): str, 16 | }, 17 | "result": [ 18 | { 19 | Optional("id"): Or(str, int), 20 | "metadata": { 21 | "name": str, 22 | "description": str, 23 | }, 24 | "topic": { 25 | "name": str, 26 | "description": str, 27 | }, 28 | "result": { 29 | "timestamp": str, 30 | "timestampOSM": Or(str), 31 | "value": Or(float, str, int, None), 32 | "label": str, 33 | "description": str, 34 | "figure": dict, 35 | }, 36 | } 37 | ], 38 | }, 39 | name="json", 40 | ignore_extra_keys=True, 41 | ) 42 | 43 | 44 | def test_mapping_saturation_data(client, bpolys): 45 | """Test parameter Topic with custom data attached.""" 46 | timestamp_objects = [ 47 | datetime(2020, 7, 17, 9, 10, 0) + timedelta(days=1 * x) 48 | for x in range(DATA.size) 49 | ] 50 | timestamp_iso_string = [t.strftime("%Y-%m-%dT%H:%M:%S") for t in timestamp_objects] 51 | # Data is ohsome API response result for the topic 'building-count' and the bpolys 52 | # of for Heidelberg 53 | parameters = { 54 | "bpolys": bpolys, 55 | "topic": { 56 | "key": "foo", 57 | "name": "bar", 58 | "description": "", 59 | "data": { 60 | "result": [ 61 | {"value": v, "timestamp": t} 62 | for v, t in zip(DATA, timestamp_iso_string) 63 | ] 64 | }, 65 | }, 66 | } 67 | response = client.post(ENDPOINT, json=parameters) 68 | assert RESPONSE_SCHEMA_JSON.is_valid(response.json()) 69 | 70 | 71 | def test_mapping_saturation_data_invalid(client, bpolys): 72 | parameters = { 73 | "bpolys": bpolys, 74 | "topic": { 75 | "key": "foo", 76 | "name": "bar", 77 | "description": "", 78 | "data": {"result": [{"value": 1.0}]}, # Missing timestamp item 79 | }, 80 | } 81 | response = client.post(ENDPOINT, json=parameters) 82 | assert response.status_code == 422 83 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_metadata.py: -------------------------------------------------------------------------------- 1 | from ohsome_quality_api.indicators.definitions import IndicatorEnum 2 | from ohsome_quality_api.topics.definitions import TopicEnum 3 | 4 | 5 | def test_metadata( 6 | client, 7 | response_template, 8 | metadata_project_core, 9 | metadata_topic_building_count, 10 | metadata_indicator_mapping_saturation, 11 | metadata_quality_dimension, 12 | metadata_attribute_clc_leaf_type, 13 | ): 14 | response = client.get("/metadata") 15 | assert response.status_code == 200 16 | 17 | content = response.json() 18 | result = content.pop("result") 19 | assert content == response_template 20 | # check topics result 21 | assert ( 22 | metadata_topic_building_count["building-count"] 23 | == result["topics"]["building-count"] 24 | ) 25 | # check quality dimensions result 26 | assert ( 27 | metadata_quality_dimension["minimal"] == result["qualityDimensions"]["minimal"] 28 | ) 29 | # check projects result 30 | assert metadata_project_core["core"] == result["projects"]["core"] 31 | # check indicators result 32 | assert ( 33 | metadata_indicator_mapping_saturation["mapping-saturation"] 34 | == result["indicators"]["mapping-saturation"] 35 | ) 36 | assert ( 37 | metadata_attribute_clc_leaf_type["clc-leaf-type"] 38 | == result["attributes"]["clc-leaf-type"] 39 | ) 40 | 41 | 42 | def test_project_core( 43 | client, 44 | response_template, 45 | ): 46 | response = client.get("/metadata?project=core") 47 | assert response.status_code == 200 48 | 49 | content = response.json() 50 | result = content.pop("result") 51 | assert content == response_template 52 | for k in ("topics", "indicators"): 53 | for p in result[k].values(): 54 | assert "core" in p["projects"] 55 | # check topics result 56 | assert len(result["topics"]) > 0 57 | # check indicators result 58 | assert len(result["indicators"]) > 0 59 | 60 | 61 | def test_project_misc( 62 | client, 63 | response_template, 64 | ): 65 | response = client.get("/metadata?project=misc") 66 | assert response.status_code == 200 67 | 68 | content = response.json() 69 | result = content.pop("result") 70 | assert content == response_template 71 | for k in ("topics", "indicators"): 72 | for p in result[k].values(): 73 | assert "misc" in p["projects"] 74 | # check topics result 75 | assert len(result["topics"]) > 0 76 | # check indicators result 77 | assert len(result["indicators"]) > 0 78 | 79 | 80 | def test_project_all( 81 | client, 82 | response_template, 83 | ): 84 | response = client.get("/metadata?project=all") 85 | assert response.status_code == 200 86 | 87 | content = response.json() 88 | result = content.pop("result") 89 | assert content == response_template 90 | # check topics result 91 | assert len(result["topics"]) == len(TopicEnum) 92 | # check indicators result 93 | assert len(result["indicators"]) == len(IndicatorEnum) 94 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_metadata_attributes.py: -------------------------------------------------------------------------------- 1 | def test_metadata_attribute( 2 | client, 3 | response_template, 4 | metadata_attribute_clc_leaf_type, 5 | ): 6 | response = client.get("/metadata/attributes") 7 | assert response.status_code == 200 8 | 9 | content = response.json() 10 | result = content.pop("result") 11 | assert content == response_template 12 | assert metadata_attribute_clc_leaf_type["clc-leaf-type"] == result["clc-leaf-type"] 13 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_metadata_indicators.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import geojson 4 | import pytest 5 | 6 | from ohsome_quality_api.indicators.definitions import IndicatorEnum 7 | 8 | 9 | def test(client, response_template, metadata_indicator_mapping_saturation): 10 | response = client.get("/metadata/indicators/") 11 | assert response.status_code == 200 12 | 13 | content = response.json() 14 | result = content.pop("result") 15 | assert content == response_template 16 | assert ( 17 | metadata_indicator_mapping_saturation["mapping-saturation"] 18 | == result["mapping-saturation"] 19 | ) 20 | assert "minimal" not in result.keys() 21 | 22 | 23 | def test_by_key(client, response_template, metadata_indicator_minimal): 24 | response = client.get("/metadata/indicators/minimal") 25 | assert response.status_code == 200 26 | 27 | content = response.json() 28 | result = content.pop("result") 29 | assert content == response_template 30 | assert result == metadata_indicator_minimal 31 | 32 | 33 | def test_by_key_not_found_error(client): 34 | response = client.get("/metadata/indicators/foo") 35 | assert response.status_code == 422 36 | 37 | 38 | def test_project_core(client, response_template, metadata_indicator_mapping_saturation): 39 | response = client.get("/metadata/indicators/?project=core") 40 | assert response.status_code == 200 41 | 42 | content = response.json() 43 | result = content.pop("result") 44 | assert content == response_template 45 | assert ( 46 | metadata_indicator_mapping_saturation["mapping-saturation"] 47 | == result["mapping-saturation"] 48 | ) 49 | assert "minimal" not in result.keys() 50 | 51 | 52 | def test_project_all( 53 | client, 54 | response_template, 55 | ): 56 | response = client.get("/metadata/indicators/?project=all") 57 | assert response.status_code == 200 58 | 59 | content = response.json() 60 | result = content.pop("result") 61 | assert content == response_template 62 | assert len(result) == len(IndicatorEnum) 63 | 64 | 65 | @pytest.mark.skip(reason="Not yet implemented") 66 | def test_project_not_found_error(client): 67 | response = client.get("/metadata/indicators/?project=foo") 68 | assert response.status_code == 404 # Not Found 69 | 70 | 71 | def test_coverage_default(client): 72 | response = client.get("metadata/indicators/mapping-saturation/coverage") 73 | assert response.status_code == 200 74 | assert response.json()["features"][0]["geometry"] == { 75 | "coordinates": [ 76 | [ 77 | [-180, 90], 78 | [-180, -90], 79 | [180, -90], 80 | [180, 90], 81 | [-180, 90], 82 | ] 83 | ], 84 | "type": "Polygon", 85 | } 86 | 87 | 88 | @pytest.mark.skip(reason="Depends on database") 89 | def test_coverage(client): 90 | response = client.get("metadata/indicators/building-comparison/coverage") 91 | assert response.status_code == 200 92 | result = geojson.loads(json.dumps(response.json())) 93 | assert result.is_valid 94 | assert isinstance(result, geojson.FeatureCollection) 95 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_metadata_projects.py: -------------------------------------------------------------------------------- 1 | def test_metadata_projects(client, response_template, metadata_project_core): 2 | response = client.get("/metadata/projects") 3 | assert response.status_code == 200 4 | 5 | content = response.json() 6 | result = content.pop("result") 7 | assert content == response_template 8 | assert metadata_project_core["core"] == result["core"] 9 | # result shouldn't contain the all key 10 | assert "all" not in result 11 | 12 | 13 | def test_metadata_projects_by_key(client, response_template, metadata_project_core): 14 | response = client.get("/metadata/projects/core") 15 | assert response.status_code == 200 16 | 17 | content = response.json() 18 | result = content.pop("result") 19 | assert content == response_template 20 | assert result == metadata_project_core 21 | 22 | 23 | def test_metadata_projects_by_key_not_found_error(client): 24 | response = client.get("/metadata/projects/foo") 25 | assert response.status_code == 422 26 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_metadata_quality_dimensions.py: -------------------------------------------------------------------------------- 1 | def test_metadata_quality_dimensions( 2 | client, response_template, metadata_quality_dimension 3 | ): 4 | response = client.get("/metadata/quality-dimensions") 5 | assert response.status_code == 200 6 | 7 | content = response.json() 8 | result = content.pop("result") 9 | assert content == response_template 10 | assert metadata_quality_dimension["minimal"] == result["minimal"] 11 | 12 | 13 | def test_metadata_quality_dimensions_by_key( 14 | client, response_template, metadata_quality_dimension 15 | ): 16 | response = client.get("/metadata/quality-dimensions/minimal") 17 | assert response.status_code == 200 18 | 19 | content = response.json() 20 | result = content.pop("result") 21 | assert content == response_template 22 | assert result == metadata_quality_dimension 23 | 24 | 25 | def test_metadata_quality_dimensions_by_key_not_found_error(client): 26 | response = client.get("/metadata/quality-dimensions/foo") 27 | assert response.status_code == 422 28 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_metadata_topics.py: -------------------------------------------------------------------------------- 1 | from ohsome_quality_api.topics.definitions import TopicEnum 2 | 3 | 4 | def test_metadata_topic( 5 | client, 6 | response_template, 7 | metadata_topic_building_count, 8 | ): 9 | response = client.get("/metadata/topics") 10 | assert response.status_code == 200 11 | 12 | content = response.json() 13 | result = content.pop("result") 14 | assert content == response_template 15 | assert metadata_topic_building_count["building-count"] == result["building-count"] 16 | assert "minimal" not in result.keys() 17 | 18 | 19 | def test_metadata_topic_project_core( 20 | client, 21 | response_template, 22 | metadata_topic_building_count, 23 | ): 24 | response = client.get("/metadata/topics/?project=core") 25 | assert response.status_code == 200 26 | 27 | content = response.json() 28 | result = content.pop("result") 29 | assert content == response_template 30 | assert metadata_topic_building_count["building-count"] == result["building-count"] 31 | assert "minimal" not in result.keys() 32 | 33 | 34 | def test_metadata_topic_project_experimental(client, response_template): 35 | response = client.get("/metadata/topics/?project=experimental") 36 | assert response.status_code == 200 37 | 38 | content = response.json() 39 | result = content.pop("result") 40 | assert content == response_template 41 | assert "building-count" not in result.keys() 42 | assert "minimal" not in result.keys() 43 | 44 | 45 | def test_project_all( 46 | client, 47 | response_template, 48 | ): 49 | response = client.get("/metadata/topics/?project=all") 50 | assert response.status_code == 200 51 | 52 | content = response.json() 53 | result = content.pop("result") 54 | assert len(result) == len(TopicEnum) 55 | 56 | 57 | def test_metadata_topic_project_not_found_error(client): 58 | response = client.get("/metadata/topics/?project=foo") 59 | assert response.status_code == 422 60 | 61 | 62 | def test_metadata_topic_by_key( 63 | client, 64 | response_template, 65 | metadata_topic_building_count, 66 | ): 67 | response = client.get("/metadata/topics/building-count") 68 | assert response.status_code == 200 69 | 70 | content = response.json() 71 | result = content.pop("result") 72 | assert content == response_template 73 | assert result == metadata_topic_building_count 74 | 75 | 76 | def test_metadata_topic_by_key_not_found_error(client): 77 | response = client.get("/metadata/topics/foo") 78 | assert response.status_code == 422 79 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_ohsome_timeout.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing FastAPI Applications: 3 | https://fastapi.tiangolo.com/tutorial/testing/ 4 | 5 | Shared tests for `/indicator` endpoints using the `bpolys` parameter. 6 | Tests for the individual endpoints and using the `bpolys` parameter please see: 7 | - `test_api_indicator_geojson_io.py` 8 | """ 9 | 10 | import os 11 | from unittest import mock 12 | 13 | import httpx 14 | 15 | from tests.integrationtests.utils import AsyncMock 16 | 17 | 18 | # TODO: could/should this be converted to a parameterized test? 19 | def test_ohsome_timeout(client, bpolys): 20 | path = os.path.join( 21 | os.path.dirname(os.path.abspath(__file__)), 22 | "..", 23 | "..", 24 | "unittests", 25 | "fixtures", 26 | "ohsome-response-200-invalid.geojson", 27 | ) 28 | with open(path, "r") as f: 29 | invalid_response = f.read() 30 | with mock.patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_request: 31 | mock_request.return_value = httpx.Response( 32 | 200, 33 | content=invalid_response, 34 | request=httpx.Request("POST", "https://www.example.org/"), 35 | ) 36 | 37 | endpoint = "/indicators/minimal" 38 | parameters = { 39 | "bpolys": bpolys, 40 | "topic": "minimal", 41 | } 42 | response = client.post(endpoint, json=parameters) 43 | assert response.status_code == 422 44 | content = response.json() 45 | assert content["type"] == "OhsomeApiError" 46 | -------------------------------------------------------------------------------- /tests/integrationtests/api/test_response_models.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from ohsome_quality_api.api.response_models import ( 6 | CompIndicator, 7 | IndicatorGeoJSONResponse, 8 | IndicatorJSONResponse, 9 | ) 10 | from ohsome_quality_api.indicators.minimal.indicator import Minimal 11 | from tests.integrationtests.utils import oqapi_vcr 12 | 13 | 14 | class TestIndicatorResponseModels: 15 | @pytest.fixture(scope="class") 16 | @oqapi_vcr.use_cassette 17 | def indicator(self, topic_minimal, feature_germany_heidelberg): 18 | indicator = Minimal(topic_minimal, feature_germany_heidelberg) 19 | asyncio.run(indicator.preprocess()) 20 | indicator.calculate() 21 | indicator.create_figure() 22 | return indicator 23 | 24 | def test_indicator_property(self, indicator): 25 | raw_dict = indicator.as_dict(exclude_label=True) 26 | CompIndicator(**raw_dict) 27 | 28 | def test_indicator_json_response(self, indicator): 29 | raw_dict = indicator.as_dict(exclude_label=True) 30 | result_dict = {"result": [raw_dict]} 31 | IndicatorJSONResponse(**result_dict) 32 | 33 | def test_indicator_geojson_response(self, indicator): 34 | feature = indicator.as_feature(exclude_label=True) 35 | result_dict = {"type": "FeatureCollection", "features": [feature]} 36 | IndicatorGeoJSONResponse(**result_dict) 37 | -------------------------------------------------------------------------------- /tests/integrationtests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.integrationtests.utils import get_geojson_fixture 4 | 5 | 6 | @pytest.fixture 7 | def europe(): 8 | return get_geojson_fixture("europe.geojson") 9 | -------------------------------------------------------------------------------- /tests/integrationtests/fixtures/algeria-touggourt-hexcells.geojson: -------------------------------------------------------------------------------- 1 | {"type": "FeatureCollection", "features": [{"type": "Feature", "id": 4171694, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.932918, 33.019152], [5.874573, 32.995934], [5.81307, 33.024295], [5.809855, 33.075939], [5.868222, 33.0992], [5.929781, 33.070774], [5.932918, 33.019152]]]]}, "properties": {"area": 95930452.2125547}}, {"type": "Feature", "id": 4172685, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.868222, 33.0992], [5.809855, 33.075939], [5.748282, 33.104265], [5.74502, 33.155916], [5.803411, 33.179219], [5.865039, 33.150829], [5.868222, 33.0992]]]]}, "properties": {"area": 95932018.963268}}, {"type": "Feature", "id": 4171692, "geometry": {"type": "MultiPolygon", "coordinates": [[[[6.04363, 33.168571], [5.98511, 33.145517], [5.923495, 33.174009], [5.920345, 33.225623], [5.978889, 33.248721], [6.040559, 33.220161], [6.04363, 33.168571]]]]}, "properties": {"area": 95933529.0075574}}, {"type": "Feature", "id": 4170334, "geometry": {"type": "MultiPolygon", "coordinates": [[[[6.163817, 33.1629], [6.105231, 33.139974], [6.04363, 33.168571], [6.040559, 33.220161], [6.099167, 33.243133], [6.160824, 33.214467], [6.163817, 33.1629]]]]}, "properties": {"area": 95933383.7868543}}, {"type": "Feature", "id": 4170335, "geometry": {"type": "MultiPolygon", "coordinates": [[[[6.108257, 33.088391], [6.049759, 33.065381], [5.988213, 33.09391], [5.98511, 33.145517], [6.04363, 33.168571], [6.105231, 33.139974], [6.108257, 33.088391]]]]}, "properties": {"area": 95931857.5037036}}, {"type": "Feature", "id": 4172686, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.81307, 33.024295], [5.75479, 33.000954], [5.693273, 33.029215], [5.689979, 33.080882], [5.748282, 33.104265], [5.809855, 33.075939], [5.81307, 33.024295]]]]}, "properties": {"area": 95930542.1162593}}, {"type": "Feature", "id": 4171693, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.988213, 33.09391], [5.929781, 33.070774], [5.868222, 33.0992], [5.865039, 33.150829], [5.923495, 33.174009], [5.98511, 33.145517], [5.988213, 33.09391]]]]}, "properties": {"area": 95931910.750078}}, {"type": "Feature", "id": 4170336, "geometry": {"type": "MultiPolygon", "coordinates": [[[[6.052817, 33.013782], [5.994408, 32.99069], [5.932918, 33.019152], [5.929781, 33.070774], [5.988213, 33.09391], [6.049759, 33.065381], [6.052817, 33.013782]]]]}, "properties": {"area": 95930343.2759635}}, {"type": "Feature", "id": 4172684, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.923495, 33.174009], [5.865039, 33.150829], [5.803411, 33.179219], [5.800182, 33.230855], [5.858661, 33.254078], [5.920345, 33.225623], [5.923495, 33.174009]]]]}, "properties": {"area": 95933668.7242033}}]} 2 | -------------------------------------------------------------------------------- /tests/integrationtests/fixtures/europe.geojson: -------------------------------------------------------------------------------- 1 | {"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-15.468749999999998,37.16031654673677],[45.703125,37.16031654673677],[45.703125,70.61261423801925],[-15.468749999999998,70.61261423801925],[-15.468749999999998,37.16031654673677]]]}}]} 2 | -------------------------------------------------------------------------------- /tests/integrationtests/fixtures/heidelberg-altstadt-feature.geojson: -------------------------------------------------------------------------------- 1 | {"type": "Feature", "geometry": {"type":"Polygon","coordinates":[[[8.674092292785645,49.40427147224242],[8.695850372314453,49.40427147224242],[8.695850372314453,49.415552187316095],[8.674092292785645,49.415552187316095],[8.674092292785645,49.40427147224242]]]}} 2 | -------------------------------------------------------------------------------- /tests/integrationtests/fixtures/niger-kanan-bakache.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "geometry": { 4 | "type": "MultiPolygon", 5 | "coordinates": [ 6 | [ 7 | [ 8 | [ 9 | 7.818124294281006, 10 | 13.859122207420464 11 | ], 12 | [ 13 | 7.835311889648438, 14 | 13.859122207420464 15 | ], 16 | [ 17 | 7.835311889648438, 18 | 13.872538264248497 19 | ], 20 | [ 21 | 7.818124294281006, 22 | 13.872538264248497 23 | ], 24 | [ 25 | 7.818124294281006, 26 | 13.859122207420464 27 | ] 28 | ] 29 | ] 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/integrationtests/fixtures/rasters/GHS_BUILT_LDS2014_GLOBE_R2018A_54009_1K_V2_0.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/fixtures/rasters/GHS_BUILT_LDS2014_GLOBE_R2018A_54009_1K_V2_0.tif -------------------------------------------------------------------------------- /tests/integrationtests/fixtures/rasters/GHS_POP_E2015_GLOBE_R2019A_54009_1K_V1_0.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/fixtures/rasters/GHS_POP_E2015_GLOBE_R2019A_54009_1K_V1_0.tif -------------------------------------------------------------------------------- /tests/integrationtests/fixtures/rasters/GHS_SMOD_POP2015_GLOBE_R2019A_54009_1K_V2_0.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/fixtures/rasters/GHS_SMOD_POP2015_GLOBE_R2019A_54009_1K_V2_0.tif -------------------------------------------------------------------------------- /tests/integrationtests/fixtures/rasters/VNL_v2_npp_2020_global_vcmslcfg_c202102150000.average_masked.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/fixtures/rasters/VNL_v2_npp_2020_global_vcmslcfg_c202102150000.average_masked.tif -------------------------------------------------------------------------------- /tests/integrationtests/indicators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/indicators/__init__.py -------------------------------------------------------------------------------- /tests/integrationtests/indicators/test_minimal.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import plotly.graph_objects as pgo 4 | import plotly.io as pio 5 | import pytest 6 | 7 | from ohsome_quality_api.indicators.minimal.indicator import Minimal 8 | from tests.integrationtests.utils import oqapi_vcr 9 | 10 | 11 | class TestAttribution: 12 | @oqapi_vcr.use_cassette 13 | def test_attribution(self, topic_minimal, feature_germany_heidelberg): 14 | indicator = Minimal(topic_minimal, feature_germany_heidelberg) 15 | asyncio.run(indicator.preprocess()) 16 | assert indicator.attribution() is not None 17 | 18 | 19 | class TestPreprocess: 20 | @oqapi_vcr.use_cassette 21 | def test_preprocess(self, topic_minimal, feature_germany_heidelberg): 22 | indicator = Minimal(topic_minimal, feature_germany_heidelberg) 23 | asyncio.run(indicator.preprocess()) 24 | assert indicator.count is not None 25 | 26 | 27 | class TestCalculate: 28 | @pytest.fixture(scope="class") 29 | @oqapi_vcr.use_cassette 30 | def indicator(self, topic_minimal, feature_germany_heidelberg): 31 | i = Minimal(topic_minimal, feature_germany_heidelberg) 32 | asyncio.run(i.preprocess()) 33 | i.calculate() 34 | return i 35 | 36 | def test_calculate(self, indicator): 37 | assert indicator.result.value is not None 38 | assert indicator.result.label is not None 39 | assert indicator.result.description is not None 40 | assert indicator.result.timestamp is not None 41 | assert indicator.result.timestamp_osm is not None 42 | 43 | 44 | class TestFigure: 45 | @pytest.fixture(scope="class") 46 | @oqapi_vcr.use_cassette 47 | def indicator(self, topic_minimal, feature_germany_heidelberg): 48 | i = Minimal(topic_minimal, feature_germany_heidelberg) 49 | asyncio.run(i.preprocess()) 50 | i.calculate() 51 | return i 52 | 53 | @pytest.mark.skip(reason="Only for manual testing.") # comment for manual test 54 | def test_create_figure_manual(self, indicator): 55 | indicator.create_figure() 56 | pio.show(indicator.result.figure) 57 | 58 | def test_create_figure(self, indicator): 59 | indicator.create_figure() 60 | assert isinstance(indicator.result.figure, dict) 61 | pgo.Figure(indicator.result.figure) # test for valid Plotly figure 62 | -------------------------------------------------------------------------------- /tests/integrationtests/test_ohsome_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | 4 | import pytest 5 | 6 | from ohsome_quality_api.ohsome import client as ohsome_client 7 | from ohsome_quality_api.utils.exceptions import OhsomeApiError 8 | 9 | from .utils import oqapi_vcr 10 | 11 | 12 | @oqapi_vcr.use_cassette() 13 | def test_get_latest_ohsome_timestamp(): 14 | time = asyncio.run(ohsome_client.get_latest_ohsome_timestamp()) 15 | assert isinstance(time, datetime) 16 | 17 | 18 | @oqapi_vcr.use_cassette() 19 | def test_query_ohsome_api_exceptions_404(): 20 | url = "https://api.ohsome.org/v1/elements/lenght" # length is misspelled 21 | with pytest.raises(OhsomeApiError, match="Not Found.*"): 22 | asyncio.run(ohsome_client.query_ohsome_api(url, {})) 23 | 24 | 25 | @oqapi_vcr.use_cassette 26 | def test_query_ohsome_api_exceptions_400(): 27 | url = "https://api.ohsome.org/v1/elements/length" 28 | with pytest.raises(OhsomeApiError, match="Invalid filter syntax."): 29 | asyncio.run( 30 | ohsome_client.query_ohsome_api( 31 | url, {"bboxes": "8.67,49.39,8.71,49.42", "filter": "geometry:lie"} 32 | ) 33 | ) 34 | -------------------------------------------------------------------------------- /tests/integrationtests/test_postgres.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | from types import MappingProxyType 4 | from unittest import mock 5 | 6 | import asyncpg 7 | import pytest 8 | 9 | from ohsome_quality_api.geodatabase import client as pg_client 10 | 11 | pytestmark = pytest.mark.skip("dependency on database setup.") 12 | 13 | 14 | async def get_connection_context_manager(): 15 | async with pg_client.get_connection() as conn: 16 | return type(conn) 17 | 18 | 19 | class TestPostgres(unittest.TestCase): 20 | def test_connection(self): 21 | instance_type = asyncio.run(get_connection_context_manager()) 22 | self.assertEqual(instance_type, asyncpg.connection.Connection) 23 | 24 | @mock.patch( 25 | "ohsome_quality_api.config.get_config", 26 | ) 27 | def test_connection_fails(self, mock_get_config): 28 | """Test connection failure error due to wrong credentials""" 29 | mock_get_config.return_value = MappingProxyType( 30 | { 31 | "postgres_host": "foo", 32 | "postgres_port": "9999", 33 | "postgres_db": "bar", 34 | "postgres_user": "tis", 35 | "postgres_password": "fas", 36 | } 37 | ) 38 | with self.assertRaises(OSError): 39 | asyncio.run(get_connection_context_manager()) 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/integrationtests/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | from unittest.mock import MagicMock 4 | 5 | import geojson 6 | import vcr 7 | 8 | from ohsome_quality_api.topics.definitions import get_topic_preset 9 | from ohsome_quality_api.topics.models import TopicDefinition 10 | 11 | TEST_DIR = os.path.dirname(os.path.abspath(__file__)) 12 | FIXTURE_DIR = os.path.join(TEST_DIR, "fixtures") 13 | VCR_DIR = os.path.join(FIXTURE_DIR, "vcr_cassettes") 14 | 15 | 16 | class AsyncMock(MagicMock): 17 | async def __call__(self, *args, **kwargs): 18 | return super().__call__(*args, **kwargs) 19 | 20 | 21 | def filename_generator(function): 22 | """Return filename of function source file with the appropriate file-ending.""" 23 | test_path = inspect.getsourcefile(function) 24 | # path relative to TEST_DIR 25 | rel_test_path = os.path.relpath(os.path.dirname(test_path), TEST_DIR) 26 | filename_base = os.path.splitext(os.path.basename(test_path))[0] 27 | return os.path.join(rel_test_path, filename_base + "." + oqapi_vcr.serializer) 28 | 29 | 30 | def get_current_dir(): 31 | return os.path.dirname(os.path.abspath(__file__)) 32 | 33 | 34 | def get_fixture_dir(): 35 | return os.path.join(get_current_dir(), "fixtures") 36 | 37 | 38 | def get_geojson_fixture(name): 39 | path = os.path.join(get_fixture_dir(), name) 40 | with open(path, "r") as f: 41 | return geojson.load(f) 42 | 43 | 44 | def get_topic_fixture(name: str) -> TopicDefinition: 45 | return get_topic_preset(name) 46 | 47 | 48 | # usage example: 49 | # add this as parameter to vcr.VCR: 50 | # before_record_response=replace_body(["image/png"], dummy_png), 51 | def replace_body(content_types, replacement): 52 | def before_record_response(response): 53 | # header keys are sometimes in camel case 54 | response["headers"] = {k.lower(): v for k, v in response["headers"].items()} 55 | if any(ct in content_types for ct in response["headers"]["content-type"]): 56 | response["body"]["string"] = replacement 57 | return response 58 | 59 | return before_record_response 60 | 61 | 62 | # dummy_png created with PIL and this command: 63 | # >>> output = io.BytesIO() 64 | # >>> Image.new("RGB", (1, 1)).save(output, format="PNG") 65 | # >>> output.getvalue() 66 | dummy_png = ( 67 | b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\02\x00" 68 | b"\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xf6\x178U" 69 | b"\x00\x00\x00\x00IEND\xaeB`\x82" 70 | ) 71 | 72 | oqapi_vcr = vcr.VCR( 73 | cassette_library_dir=VCR_DIR, 74 | # details see https://vcrpy.readthedocs.io/en/latest/usage.html#record-modes 75 | record_mode=os.getenv("VCR_RECORD_MODE", default="new_episodes"), 76 | match_on=["method", "scheme", "host", "port", "path", "query", "body"], 77 | func_path_generator=filename_generator, 78 | before_record_response=replace_body(["image/png"], dummy_png), 79 | # ignore github.com. ApprovalTests 80 | ignore_hosts=["testserver", "github"], 81 | ignore_localhost=True, # do not record HTTP requests to local FastAPI test instance 82 | ) 83 | -------------------------------------------------------------------------------- /tests/unittests/__init__.py: -------------------------------------------------------------------------------- 1 | from ohsome_quality_api.config import configure_logging 2 | 3 | configure_logging() 4 | -------------------------------------------------------------------------------- /tests/unittests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/api/__init__.py -------------------------------------------------------------------------------- /tests/unittests/api/test_request_models_data.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ohsome_quality_api.api.request_models import ( 4 | IndicatorDataRequest, 5 | ) 6 | 7 | 8 | def test_indicator_data(bpolys): 9 | topic = {"key": "foo", "name": "bar", "description": "buz", "data": {}} 10 | IndicatorDataRequest( 11 | bpolys=bpolys, 12 | topic=topic, 13 | ) 14 | IndicatorDataRequest(bpolys=bpolys, topic=topic) 15 | 16 | 17 | def test_topic_data_valid(bpolys): 18 | topic = { 19 | "key": "foo", 20 | "name": "bar", 21 | "description": "buz", 22 | "data": {}, 23 | } 24 | IndicatorDataRequest(bpolys=bpolys, topic=topic) 25 | 26 | 27 | def test_topic_data_invalid(bpolys): 28 | for topic in ( 29 | {"key": "foo", "name": "bar", "data": {}}, 30 | {"key": "foo", "description": "bar", "data": {}}, 31 | {"key": "foo", "name": "bar", "description": "buz"}, 32 | {"key": "foo", "name": "bar", "description": "buz", "data": "fis"}, 33 | ): 34 | with pytest.raises(ValueError): 35 | IndicatorDataRequest(bpolys=bpolys, topic=topic) 36 | -------------------------------------------------------------------------------- /tests/unittests/fixtures/GHS_BUILT_R2018A-Heidelberg.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/fixtures/GHS_BUILT_R2018A-Heidelberg.tif -------------------------------------------------------------------------------- /tests/unittests/fixtures/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Database connection parameters; 3 | postgres_host: localhost 4 | postgres_port: 5445 5 | postgres_db: oqapi 6 | postgres_user: oqapi 7 | postgres_password: oqapi 8 | # Restrict size of input geometry 9 | geom_size_limit: 100 10 | # Python logging level 11 | log_level: INFO 12 | # ohsome API URL 13 | ohsome_api: https://api.ohsome.org/v1 14 | # Limit number of concurrent Indicator computations 15 | concurrent_computations: 4 16 | # User-Agent header for request to the ohsome API 17 | # Default: 'ohsome-quality-api/{version}' 18 | user_agent: ohsome-quality-api 19 | # Datasets and Feature IDs available in the database 20 | datasets: 21 | regions: # Name of relation with GEOM 22 | default: ogc_fid # Default unique id field 23 | other: [name] # More unique id fields (optional) 24 | -------------------------------------------------------------------------------- /tests/unittests/fixtures/heidelberg-altstadt-feature.geojson: -------------------------------------------------------------------------------- 1 | {"type": "Feature", "geometry": {"type":"Polygon","coordinates":[[[8.674092292785645,49.40427147224242],[8.695850372314453,49.40427147224242],[8.695850372314453,49.415552187316095],[8.674092292785645,49.415552187316095],[8.674092292785645,49.40427147224242]]]}} 2 | -------------------------------------------------------------------------------- /tests/unittests/fixtures/heidelberg-altstadt-featurecollection.geojson: -------------------------------------------------------------------------------- 1 | {"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type":"Polygon","coordinates":[[[8.674092292785645,49.40427147224242],[8.695850372314453,49.40427147224242],[8.695850372314453,49.415552187316095],[8.674092292785645,49.415552187316095],[8.674092292785645,49.40427147224242]]]}}]} 2 | -------------------------------------------------------------------------------- /tests/unittests/fixtures/heidelberg-altstadt-wsf2019.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/fixtures/heidelberg-altstadt-wsf2019.tif -------------------------------------------------------------------------------- /tests/unittests/fixtures/nodata.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/fixtures/nodata.tif -------------------------------------------------------------------------------- /tests/unittests/fixtures/ohsome-response-200-invalid.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "attribution" : { 3 | "url" : "https://ohsome.org/copyrights", 4 | "text" : "© OpenStreetMap contributors" 5 | }, 6 | "apiVersion" : "1.3.2", 7 | "metadata" : { 8 | "description" : "OSM data as GeoJSON features.", 9 | "requestUrl" : "https://api.ohsome.org/v1/elements/geometry?bboxes=-38.53%2C-13.01%2C-38.30%2C-12.91&clipGeometry=true&filter=type%3Away&properties=tags%2Cmetadata&showMetadata=yes&time=2008-01-01" 10 | }, 11 | "type" : "FeatureCollection", 12 | "features" : [{ 13 | "type" : "Feature", 14 | "geometry" : { 15 | "type" : "Polygon", 16 | "coordinates" : [ 17 | [ 18 | [ 19 | -38.336482, 20 | -12.913311199999999 21 | ], 22 | [ 23 | -38.3353886, 24 | -12.9130802 25 | ], 26 | [ 27 | -38.3351615, 28 | -12.913180299999999 29 | ], 30 | [ 31 | -38.3357467, 32 | -12.914635599999999 33 | ], 34 | [ 35 | -38.3358737, 36 | -12.9146472 37 | ], 38 | [ 39 | -38.336597499999996, 40 | -12.9136192 41 | ], 42 | [ 43 | -38.336482, 44 | -12.913311199999999 45 | ] 46 | ] 47 | ] 48 | }, 49 | "properties" : { 50 | "@changesetId" : 502107, 51 | "@lastEdit" : "2007-11-13T15:20:26Z", 52 | "@osmId" : "way/12331799", 53 | "@osmType" : "WAY", 54 | "@snapshotTimestamp" : "2008-01-01T00:00:00Z", 55 | "@version" : 1, 56 | "amenity" : "parking", 57 | "created_by" : "JOSM" 58 | } 59 | }{ 60 | "timestamp" : "2021-02-27T13:41:27.384738", 61 | "status" : 413, 62 | "message" : "The given query is too large in respect to the given timeout. Please use a smaller region and/or coarser time period.", 63 | "requestUrl" : "https://api.ohsome.org/v1/elements/geometry?bboxes=-38.53%2C-13.01%2C-38.30%2C-12.91&clipGeometry=true&filter=type%3Away&properties=tags%2Cmetadata&showMetadata=yes&time=2008-01-01" 64 | } 65 | -------------------------------------------------------------------------------- /tests/unittests/fixtures/ohsome-response-200-valid.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "attribution": { 3 | "url": "https://ohsome.org/copyrights", 4 | "text": "© OpenStreetMap contributors" 5 | }, 6 | "apiVersion": "1.6.2", 7 | "result": [ 8 | { 9 | "timestamp": "2014-01-01T00:00:00Z", 10 | "value": 42 11 | }, 12 | { 13 | "timestamp": "2015-01-01T00:00:00Z", 14 | "value": 42 15 | }, 16 | { 17 | "timestamp": "2016-01-01T00:00:00Z", 18 | "value": 43 19 | }, 20 | { 21 | "timestamp": "2017-01-01T00:00:00Z", 22 | "value": 43 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tests/unittests/fixtures/ohsome-response-400-time.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp" : "2021-06-08T08:19:49.333591", 3 | "status" : 400, 4 | "message" : "The provided time parameter is not ISO-8601 conform.", 5 | "requestUrl" : "https://api.ohsome.org/v1/contributions/count?bboxes=8.67%2C49.39%2C8.71%2C49.42&filter=type%3Away%20and%20natural%3D*&format=json&time=20140101%2C2015010" 6 | } -------------------------------------------------------------------------------- /tests/unittests/mapping_saturation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/mapping_saturation/__init__.py -------------------------------------------------------------------------------- /tests/unittests/mapping_saturation/test_models.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | import numpy as np 5 | from rpy2.rinterface_lib.embedded import RRuntimeError 6 | 7 | from ohsome_quality_api.indicators.mapping_saturation import models 8 | 9 | from . import fixtures 10 | 11 | 12 | class TestModels(unittest.TestCase): 13 | def setUp(self): 14 | self.ydata_1 = fixtures.VALUES_1 15 | self.xdata_1 = np.array(range(len(self.ydata_1))) 16 | self.ydata_2 = fixtures.VALUES_2 17 | self.xdata_2 = np.array(range(len(self.ydata_2))) 18 | self.expected_keys = [ 19 | "name", 20 | "function_formula", 21 | "asymptote", 22 | "mae", 23 | "xdata", 24 | "ydata", 25 | "coefficients", 26 | "fitted_values", 27 | ] 28 | 29 | def run_tests(self, model): 30 | self.assertIsNotNone(model.name) 31 | self.assertNotEqual(model.name, "") 32 | self.assertIsNotNone(model.function_formula) 33 | self.assertNotEqual(model.function_formula, "") 34 | 35 | self.assertTrue(model.coefficients) 36 | self.assertNotEqual(model.fitted_values.size, 0) 37 | self.assertFalse(np.isnan(np.sum(model.fitted_values))) 38 | self.assertTrue(np.isfinite(np.sum(model.fitted_values))) 39 | 40 | self.assertIsNotNone(model.asymptote) 41 | self.assertIsNotNone(model.mae) 42 | self.assertIsNotNone(model.asym_conf_int) 43 | 44 | md = model.as_dict() 45 | self.assertListEqual(self.expected_keys, list(md.keys())) 46 | 47 | def test_sigmoid(self): 48 | model = models.Sigmoid(self.xdata_1, self.ydata_1) 49 | self.run_tests(model) 50 | model = models.Sigmoid(self.xdata_2, self.ydata_2) 51 | self.run_tests(model) 52 | 53 | def test_sslogis(self): 54 | model = models.SSlogis(self.xdata_1, self.ydata_1) 55 | self.run_tests(model) 56 | model = models.SSlogis(self.xdata_2, self.ydata_2) 57 | self.run_tests(model) 58 | 59 | def test_ssdoubles(self): 60 | with self.assertRaises(RRuntimeError): 61 | models.SSdoubleS(self.xdata_1, self.ydata_1) 62 | model = models.SSdoubleS(self.xdata_2, self.ydata_2) 63 | self.run_tests(model) 64 | 65 | def test_ssfpl(self): 66 | model = models.SSfpl(self.xdata_1, self.ydata_1) 67 | self.run_tests(model) 68 | with self.assertRaises(RRuntimeError): 69 | models.SSfpl(self.xdata_2, self.ydata_2) 70 | 71 | def test_ssasymp(self): 72 | model = models.SSasymp(self.xdata_1, self.ydata_1) 73 | self.run_tests(model) 74 | model = models.SSasymp(self.xdata_2, self.ydata_2) 75 | self.run_tests(model) 76 | 77 | def test_ssmicmen(self): 78 | model = models.SSmicmen(self.xdata_1, self.ydata_1) 79 | self.run_tests(model) 80 | model = models.SSmicmen(self.xdata_2, self.ydata_2) 81 | self.run_tests(model) 82 | 83 | @mock.patch.multiple(models.BaseStatModel, __abstractmethods__=set()) 84 | def test_mae(self): 85 | model = models.BaseStatModel(np.array([2, 4]), np.array([2, 4])) 86 | model.fitted_values = np.array([4, 4]) # Mock 87 | result = model.mae 88 | self.assertEqual(result, 1.0) 89 | self.assertIsInstance(result, np.float64) 90 | 91 | 92 | if __name__ == "__main__": 93 | unittest.main() 94 | -------------------------------------------------------------------------------- /tests/unittests/test_api.py: -------------------------------------------------------------------------------- 1 | from ohsome_quality_api import __version__ as version 2 | from ohsome_quality_api.api.api import empty_api_response 3 | 4 | 5 | def test_empty_api_response(): 6 | response_template = { 7 | "apiVersion": version, 8 | "attribution": { 9 | "url": ( 10 | "https://github.com/GIScience/ohsome-quality-api/blob/main/" 11 | + "COPYRIGHTS.md" 12 | ), 13 | }, 14 | } 15 | assert response_template == empty_api_response() 16 | -------------------------------------------------------------------------------- /tests/unittests/test_attribute_definitions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ohsome_quality_api.attributes import definitions 4 | from ohsome_quality_api.attributes.models import Attribute 5 | 6 | 7 | @pytest.fixture() 8 | def attribute_key_string(): 9 | return "height" 10 | 11 | 12 | def test_get_attributes(): 13 | attributes = definitions.get_attributes() 14 | assert isinstance(attributes, dict) 15 | for key, value in attributes.items(): 16 | for k, v in value.items(): 17 | assert isinstance(k, str) 18 | assert isinstance(v, Attribute) 19 | 20 | 21 | def test_get_attribute(attribute_key_string, topic_key_building_count): 22 | attribute = definitions.get_attribute( 23 | topic_key_building_count, attribute_key_string 24 | ) 25 | assert isinstance(attribute, Attribute) 26 | 27 | 28 | def test_get_attribute_wrong_key(): 29 | with pytest.raises(KeyError): 30 | definitions.get_attribute("foo", "bar") 31 | 32 | 33 | def test_build_attribute_filter(attribute_key, topic_key_building_count): 34 | attribute = definitions.build_attribute_filter( 35 | attribute_key, topic_key_building_count 36 | ) 37 | assert isinstance(attribute, str) 38 | assert ( 39 | attribute == "building=* and building!=no and geometry:polygon" 40 | " and (height=* or building:levels=*)" 41 | ) 42 | 43 | 44 | def test_build_attribute_filter_multiple_attributes( 45 | attribute_key_multiple, topic_key_building_count 46 | ): 47 | attribute = definitions.build_attribute_filter( 48 | attribute_key_multiple, topic_key_building_count 49 | ) 50 | assert isinstance(attribute, str) 51 | 52 | 53 | def test_build_attribute_filter_wrong_key(): 54 | with pytest.raises(KeyError): 55 | definitions.build_attribute_filter("foo", "bar") 56 | 57 | 58 | def test_get_attribute_preset(topic_key_building_count): 59 | attribute = definitions.get_attribute_preset(topic_key_building_count) 60 | assert isinstance(attribute, dict) 61 | for key, value in attribute.items(): 62 | assert isinstance(value, Attribute) 63 | -------------------------------------------------------------------------------- /tests/unittests/test_attributes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import ValidationError 3 | 4 | from ohsome_quality_api.attributes.models import Attribute 5 | 6 | 7 | def test_attributes(): 8 | Attribute( 9 | name="My fancy Attribute", description="some description", filter="testfilter" 10 | ) 11 | 12 | 13 | def test_parameter_missing(): 14 | with pytest.raises(ValidationError): 15 | Attribute(name="some name", description="some description") 16 | with pytest.raises(ValidationError): 17 | Attribute(description="description") 18 | 19 | 20 | def test_extra_parameter(): 21 | with pytest.raises(ValidationError): 22 | Attribute( 23 | name="My fancy Attribute", 24 | description="some description", 25 | foo="bar", 26 | ) 27 | -------------------------------------------------------------------------------- /tests/unittests/test_definitions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ohsome_quality_api import definitions 4 | from ohsome_quality_api.indicators.models import ( 5 | IndicatorMetadata as IndicatorMetadata, 6 | ) 7 | 8 | 9 | class TestDefinitions(unittest.TestCase): 10 | def test_get_attribution(self): 11 | attribution = definitions.get_attribution(["OSM"]) 12 | self.assertEqual(attribution, "© OpenStreetMap contributors") 13 | 14 | attributions = definitions.get_attribution(["OSM", "GHSL", "VNL"]) 15 | self.assertEqual( 16 | attributions, 17 | ( 18 | "© OpenStreetMap contributors; © European Union, 1995-2022, " 19 | "Global Human Settlement Topic Data; " 20 | "Earth Observation Group Nighttime Light Data" 21 | ), 22 | ) 23 | 24 | self.assertRaises(AssertionError, definitions.get_attribution, ["MSO"]) 25 | -------------------------------------------------------------------------------- /tests/unittests/test_helper_asyncio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | 4 | from ohsome_quality_api.utils import helper_asyncio 5 | 6 | 7 | class TestHelper(unittest.TestCase): 8 | async def bar(self): 9 | raise ValueError() 10 | 11 | async def foo(self): 12 | return "OK" 13 | 14 | def setUp(self): 15 | self.tasks = [ 16 | self.foo(), 17 | self.foo(), 18 | self.foo(), 19 | self.foo(), 20 | self.foo(), 21 | ] 22 | 23 | def test_gather_with_semaphore(self): 24 | results = asyncio.run(helper_asyncio.gather_with_semaphore(self.tasks)) 25 | assert results == ["OK", "OK", "OK", "OK", "OK"] 26 | 27 | def test_gather_with_semaphore_return_exceptions(self): 28 | results = asyncio.run( 29 | helper_asyncio.gather_with_semaphore(self.tasks, return_exceptions=True) 30 | ) 31 | assert results == ["OK", "OK", "OK", "OK", "OK"] 32 | 33 | def test_gather_with_semaphore_return_exceptions_error(self): 34 | tasks = self.tasks + [self.bar()] 35 | results = asyncio.run( 36 | helper_asyncio.gather_with_semaphore(tasks, return_exceptions=True) 37 | ) 38 | assert len(results) == 6 39 | assert isinstance(results[-1], ValueError) 40 | 41 | def test_filter_exceptions(self): 42 | tasks = self.tasks + [self.bar()] 43 | results = asyncio.run( 44 | helper_asyncio.gather_with_semaphore(tasks, return_exceptions=True) 45 | ) 46 | exceptions = helper_asyncio.filter_exceptions(results) 47 | assert len(exceptions) == 1 48 | assert isinstance(exceptions[0], ValueError) 49 | -------------------------------------------------------------------------------- /tests/unittests/test_helper_geo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ohsome_quality_api.utils.helper_geo import calculate_area 4 | 5 | 6 | def test_calculate_area(feature_germany_heidelberg): 7 | expected = 108852960.62891776 # derived from PostGIS ST_AREA 8 | result = calculate_area(feature_germany_heidelberg) 9 | assert result == pytest.approx(expected, abs=1e-3) 10 | -------------------------------------------------------------------------------- /tests/unittests/test_indicators_definitions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest.mock import AsyncMock 3 | 4 | import geojson 5 | import pytest 6 | from geojson import Feature, Polygon 7 | 8 | from ohsome_quality_api.indicators import definitions, models 9 | 10 | 11 | @pytest.fixture(scope="class") 12 | def mock_get_reference_coverage(class_mocker): 13 | async_mock = AsyncMock( 14 | return_value=Feature( 15 | geometry=Polygon( 16 | coordinates=[ 17 | [ 18 | (-180, 90), 19 | (-180, -90), 20 | (180, -90), 21 | (180, 90), 22 | (-180, 90), 23 | ] 24 | ] 25 | ) 26 | ) 27 | ) 28 | class_mocker.patch( 29 | "ohsome_quality_api.indicators.building_comparison.indicator.db_client.get_reference_coverage", 30 | side_effect=async_mock, 31 | ) 32 | 33 | 34 | def test_get_indicator_keys(): 35 | names = definitions.get_indicator_keys() 36 | assert isinstance(names, list) 37 | 38 | 39 | def test_get_valid_indicators(): 40 | indicators = definitions.get_valid_indicators("building-count") 41 | assert indicators == ("mapping-saturation", "currentness", "attribute-completeness") 42 | 43 | 44 | def test_get_indicator_metadata(): 45 | indicators = definitions.get_indicator_metadata() 46 | assert isinstance(indicators, dict) 47 | for indicator in indicators.values(): 48 | assert isinstance(indicator, models.IndicatorMetadata) 49 | 50 | 51 | def test_get_indicator_metadata_filtered_by_project(): 52 | indicators = definitions.get_indicator_metadata("core") 53 | assert isinstance(indicators, dict) 54 | for indicator in indicators.values(): 55 | assert isinstance(indicator, models.IndicatorMetadata) 56 | assert indicator.projects == ["core"] 57 | 58 | 59 | def test_get_indicator(metadata_indicator_minimal): 60 | indicator = definitions.get_indicator("minimal") 61 | assert indicator == metadata_indicator_minimal["minimal"] 62 | 63 | 64 | def test_get_coverage(mock_get_reference_coverage): 65 | coverage = asyncio.run( 66 | definitions.get_coverage("building-comparison", inverse=False) 67 | ) 68 | assert coverage.is_valid 69 | assert isinstance(coverage, geojson.FeatureCollection) 70 | 71 | coverage = asyncio.run(definitions.get_coverage("building-comparison")) 72 | assert coverage.is_valid 73 | assert isinstance(coverage, geojson.FeatureCollection) 74 | 75 | coverage = asyncio.run( 76 | definitions.get_coverage("building-comparison", inverse=True) 77 | ) 78 | assert coverage.is_valid 79 | assert isinstance(coverage, geojson.FeatureCollection) 80 | 81 | coverage = asyncio.run(definitions.get_coverage("mapping-saturation")) 82 | assert coverage.is_valid 83 | assert isinstance(coverage, geojson.FeatureCollection) 84 | -------------------------------------------------------------------------------- /tests/unittests/test_load_metadata.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/test_load_metadata.py -------------------------------------------------------------------------------- /tests/unittests/test_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import unittest 5 | 6 | from ohsome_quality_api.config import configure_logging 7 | 8 | 9 | class TestLogging(unittest.TestCase): 10 | def setUp(self): 11 | self.log_level = os.environ.pop("OQAPI_LOG_LEVEL", None) 12 | 13 | def tearDown(self): 14 | if self.log_level is not None: 15 | os.environ["OQAPI_LOG_LEVEL"] = self.log_level 16 | else: 17 | os.environ.pop("OQAPI_LOG_LEVEL", None) 18 | 19 | def test_logging(self): 20 | configure_logging() 21 | with self.assertLogs(level="INFO") as captured: 22 | logging.info("Test info logging message") 23 | logging.debug("Test debug logging message") 24 | # Test that there is only one log message 25 | self.assertEqual(len(captured.records), 1) 26 | # Test not-formatted logging output message 27 | self.assertEqual(captured.records[0].getMessage(), "Test info logging message") 28 | 29 | def test_level(self): 30 | if "pydevd" in sys.modules or "pdb" in sys.modules: 31 | level = "DEBUG" 32 | else: 33 | level = "INFO" 34 | configure_logging() 35 | self.assertEqual(getattr(logging, level), logging.root.level) 36 | 37 | os.environ["OQAPI_LOG_LEVEL"] = "DEBUG" 38 | configure_logging() 39 | self.assertEqual(getattr(logging, "DEBUG"), logging.root.level) 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/unittests/test_project.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import ValidationError 3 | 4 | from ohsome_quality_api.projects.models import Project 5 | 6 | 7 | def test_project(): 8 | Project(name="My fancy Project", description="some description") 9 | 10 | 11 | def test_parameter_missing(): 12 | with pytest.raises(ValidationError): 13 | Project(name="some name") 14 | with pytest.raises(ValidationError): 15 | Project(description="description") 16 | 17 | 18 | def test_extra_parameter(): 19 | with pytest.raises(ValidationError): 20 | Project( 21 | name="My fancy Project", 22 | description="some description", 23 | foo="bar", 24 | ) 25 | -------------------------------------------------------------------------------- /tests/unittests/test_project_definitions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ohsome_quality_api.projects import definitions 4 | from ohsome_quality_api.projects.models import Project 5 | 6 | 7 | @pytest.fixture(params=["misc", "core", "experimental"]) 8 | def valid_project_keys(request): 9 | return request.param 10 | 11 | 12 | def test_get_projects(): 13 | qds = definitions.get_project_metadata() 14 | assert isinstance(qds, dict) 15 | for key, qd in qds.items(): 16 | assert isinstance(key, str) 17 | assert isinstance(qd, Project) 18 | 19 | 20 | def test_get_project(valid_project_keys): 21 | qd = definitions.get_project(valid_project_keys) 22 | assert isinstance(qd, Project) 23 | 24 | 25 | def test_get_project_wrong_key(): 26 | with pytest.raises(KeyError): 27 | definitions.get_project("foo") 28 | 29 | 30 | def test_get_project_keys_type(): 31 | keys = definitions.get_project_keys() 32 | assert isinstance(keys, list) 33 | for key in keys: 34 | assert isinstance(key, str) 35 | 36 | 37 | def test_get_project_keys_valid(valid_project_keys): 38 | keys = definitions.get_project_keys() 39 | assert valid_project_keys in keys 40 | -------------------------------------------------------------------------------- /tests/unittests/test_quality_dimension.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import ValidationError 3 | 4 | from ohsome_quality_api.quality_dimensions.models import QualityDimension 5 | 6 | 7 | def test_quality_dimension(): 8 | QualityDimension(name="My fancy QualityDimension", description="some description") 9 | 10 | 11 | def test_parameter_missing(): 12 | with pytest.raises(ValidationError): 13 | QualityDimension(name="some name") 14 | with pytest.raises(ValidationError): 15 | QualityDimension(description="description") 16 | 17 | 18 | def test_extra_parameter(): 19 | with pytest.raises(ValidationError): 20 | QualityDimension( 21 | name="My fancy QualityDimension", 22 | description="some description", 23 | foo="bar", 24 | ) 25 | -------------------------------------------------------------------------------- /tests/unittests/test_quality_dimension_definitions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ohsome_quality_api.quality_dimensions import definitions 4 | from ohsome_quality_api.quality_dimensions.models import QualityDimension 5 | 6 | 7 | @pytest.fixture(params=["minimal", "completeness", "currentness"]) 8 | def valid_quality_dimension_keys(request): 9 | return request.param 10 | 11 | 12 | def test_get_quality_dimensions(): 13 | qds = definitions.get_quality_dimensions() 14 | assert isinstance(qds, dict) 15 | for key, qd in qds.items(): 16 | assert isinstance(key, str) 17 | assert isinstance(qd, QualityDimension) 18 | 19 | 20 | def test_get_quality_dimension(valid_quality_dimension_keys): 21 | qd = definitions.get_quality_dimension(valid_quality_dimension_keys) 22 | assert isinstance(qd, QualityDimension) 23 | 24 | 25 | def test_get_quality_dimension_wrong_key(): 26 | with pytest.raises(KeyError): 27 | definitions.get_quality_dimension("foo") 28 | 29 | 30 | def test_get_quality_dimension_keys_type(): 31 | keys = definitions.get_quality_dimension_keys() 32 | assert isinstance(keys, list) 33 | for key in keys: 34 | assert isinstance(key, str) 35 | 36 | 37 | def test_get_quality_dimension_keys_valid(valid_quality_dimension_keys): 38 | keys = definitions.get_quality_dimension_keys() 39 | assert valid_quality_dimension_keys in keys 40 | -------------------------------------------------------------------------------- /tests/unittests/test_topics_definitions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ohsome_quality_api.topics import definitions, models 4 | 5 | 6 | def test_get_topic_keys(): 7 | names = definitions.get_topic_keys() 8 | assert isinstance(names, list) 9 | 10 | 11 | def test_get_valid_topics(): 12 | topics = definitions.get_valid_topics("minimal") 13 | assert topics == ("minimal",) 14 | 15 | 16 | def test_load_topic_definition(): 17 | topics = definitions.load_topic_presets() 18 | for topic in topics: 19 | assert isinstance(topics[topic], models.TopicDefinition) 20 | 21 | 22 | def test_get_topic_definition(): 23 | topic = definitions.get_topic_preset("minimal") 24 | assert isinstance(topic, models.TopicDefinition) 25 | 26 | 27 | def test_get_topic_definition_not_found_error(): 28 | with pytest.raises(KeyError): 29 | definitions.get_topic_preset("foo") 30 | with pytest.raises(KeyError): 31 | definitions.get_topic_preset(None) 32 | 33 | 34 | def test_get_topic_definitions(): 35 | topics = definitions.get_topic_presets() 36 | assert isinstance(topics, dict) 37 | for topic in topics.values(): 38 | assert isinstance(topic, models.TopicDefinition) 39 | 40 | 41 | def test_get_topic_definitions_with_project(): 42 | topics = definitions.get_topic_presets("core") 43 | assert isinstance(topics, dict) 44 | for topic in topics.values(): 45 | assert isinstance(topic, models.TopicDefinition) 46 | assert topic.project == "core" 47 | -------------------------------------------------------------------------------- /tests/unittests/test_validators.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from ohsome_quality_api.utils.exceptions import ( 6 | SizeRestrictionError, 7 | ) 8 | from ohsome_quality_api.utils.validators import ( 9 | validate_area, 10 | ) 11 | 12 | 13 | @mock.patch.dict( 14 | "os.environ", 15 | {"OQAPI_GEOM_SIZE_LIMIT": "1000"}, 16 | clear=True, 17 | ) 18 | def test_validate_area(feature_germany_heidelberg): 19 | # expect not exceptions 20 | validate_area(feature_germany_heidelberg) 21 | 22 | 23 | @mock.patch.dict( 24 | "os.environ", 25 | {"OQAPI_GEOM_SIZE_LIMIT": "1"}, 26 | clear=True, 27 | ) 28 | def test_validate_area_exception(feature_germany_heidelberg): 29 | with pytest.raises(SizeRestrictionError): 30 | validate_area(feature_germany_heidelberg) 31 | -------------------------------------------------------------------------------- /tests/unittests/test_version.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tomllib 3 | import unittest 4 | 5 | from ohsome_quality_api import __version__ as version 6 | 7 | 8 | class TestVersion(unittest.TestCase): 9 | def test_version(self): 10 | infile = os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), "..", "..", "pyproject.toml" 12 | ) 13 | with open(infile, "rb") as fo: 14 | project_file = tomllib.load(fo) 15 | pyproject_version = project_file["project"]["version"] 16 | self.assertEqual(pyproject_version, version) 17 | 18 | 19 | if __name__ == "__main__": 20 | unittest.main() 21 | -------------------------------------------------------------------------------- /tests/unittests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import geojson 4 | 5 | from ohsome_quality_api.attributes.definitions import get_attribute 6 | from ohsome_quality_api.attributes.models import Attribute 7 | from ohsome_quality_api.topics.definitions import get_topic_preset 8 | from ohsome_quality_api.topics.models import TopicDefinition 9 | 10 | 11 | def get_geojson_fixture(name): 12 | path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures", name) 13 | with open(path, "r") as f: 14 | return geojson.load(f) 15 | 16 | 17 | def get_topic_fixture(name: str) -> TopicDefinition: 18 | return get_topic_preset(name) 19 | 20 | 21 | def get_attribute_fixture(a_key: str, topic_key: str) -> Attribute: 22 | return get_attribute(a_key, topic_key) 23 | --------------------------------------------------------------------------------