├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── docker-image-master.yml │ ├── docker-image-publish.yml │ ├── lint-format.yml │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.rst ├── cjio ├── __init__.py ├── cityjson.py ├── cjio.py ├── convert.py ├── errors.py ├── floatEncoder.py ├── geom_help.py ├── subset.py └── utils.py ├── docker ├── Dockerfile_Cesium └── run.sh ├── docs ├── Makefile ├── design_document.ipynb ├── figures │ ├── rotterdamsubset.png │ ├── zurich.png │ └── zurichmlresult.png ├── make.bat ├── requirements_docs.txt └── source │ ├── api.rst │ ├── api_tutorial_basics.ipynb │ ├── api_tutorial_create.ipynb │ ├── api_tutorial_ml.ipynb │ ├── conf.py │ ├── includeme.rst │ ├── index.rst │ └── tutorials.rst ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── data │ ├── DH_01_subs.city.json │ ├── box.off │ ├── box.poly │ ├── cube.c.json │ ├── cube.json │ ├── cube.poly │ ├── delft.json │ ├── delft_1b.json │ ├── dummy │ │ ├── composite_solid_with_material.json │ │ ├── dummy.json │ │ ├── dummy_noappearance.json │ │ ├── multisurface_with_material.json │ │ └── rectangle.json │ ├── empty.yaml │ ├── material │ │ ├── mt-1-triangulated.json │ │ ├── mt-1.json │ │ ├── mt-2-triangulated.json │ │ └── mt-2.json │ ├── minimal.json │ ├── multi_lod.json │ ├── rotterdam │ │ ├── appearances │ │ │ └── 0320_4_15.jpg │ │ ├── rotterdam_one.json │ │ ├── rotterdam_subset.json │ │ ├── rotterdam_subset_cjio_test.json │ │ └── textures │ │ │ └── empty.texture │ ├── upgrade_all.py │ ├── upgrade_all.sh │ └── zurich │ │ └── zurich_subset_lod2.json ├── test_appearances.py ├── test_cityjson.py ├── test_cli.py ├── test_convert.py └── test_utils.py └── uid_entrypoint.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .pytest_cache 3 | .git 4 | .run 5 | docs 6 | tests 7 | tmp 8 | cjio.egg-info -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **cjio version** 14 | What does `cjio --version` print? Or did you install from a git branch? 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior? What error message do you see in the console? 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Desktop (please complete the following information):** 26 | - OS: [e.g. Windows] 27 | - Version [e.g. Windows 10] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-master.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Build the Docker image 18 | run: docker build . --file Dockerfile --tag tudelft3d/cjio:latest -------------------------------------------------------------------------------- /.github/workflows/docker-image-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | push_to_registry: 9 | name: Push Docker image to Docker Hub 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v2 14 | 15 | - name: Log in to Docker Hub 16 | uses: docker/login-action@v1 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | 21 | - name: Extract metadata (tags, labels) for Docker 22 | id: meta 23 | uses: docker/metadata-action@v3 24 | with: 25 | images: tudelft3d/cjio 26 | 27 | - name: Build and push Docker image 28 | uses: docker/build-push-action@v2 29 | with: 30 | context: . 31 | push: true 32 | tags: ${{ steps.meta.outputs.tags }} 33 | labels: ${{ steps.meta.outputs.labels }} 34 | -------------------------------------------------------------------------------- /.github/workflows/lint-format.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Format 2 | 3 | on: 4 | pull_request: 5 | branches: [ "develop", "master" ] 6 | 7 | jobs: 8 | 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-python@v5 16 | with: 17 | python-version: '3.11' 18 | - name: Install dependencies 19 | run: pip install ruff pytest 20 | - name: Lint 21 | run: ruff check tests ; ruff check cjio 22 | - name: Format 23 | run: ruff format tests --check ; ruff format cjio --check -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package to PyPI when a new release is created. 2 | # For more information see: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/# 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | name: Build distribution 📦 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check-out repository 17 | uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.x" 24 | - name: Install dependencies 25 | run: >- 26 | python3 -m 27 | pip install 28 | build 29 | --user 30 | - name: Build a binary wheel and a source tarball 31 | run: python3 -m build 32 | - name: Store the distribution packages 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: python-package-distributions 36 | path: dist/ 37 | - name: Verify dist directory 38 | run: ls -l dist 39 | pypi-publish: 40 | name: Publish to PyPI 41 | needs: 42 | - build 43 | runs-on: ubuntu-latest 44 | environment: 45 | name: pypi 46 | url: https://pypi.org/p/cjio 47 | permissions: 48 | id-token: write 49 | steps: 50 | - name: Download all the dists 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: python-package-distributions 54 | path: dist/ 55 | - name: Publish to PyPI 56 | uses: pypa/gh-action-pypi-publish@release/v1 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Data 2 | tmp 3 | /tests/data/zurich/zurich.json 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # pipenv 82 | Pipfile.lock 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # dotenv 91 | .env 92 | 93 | # virtualenv 94 | .venv 95 | venv/ 96 | ENV/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | 111 | # Eclipse 112 | .project 113 | .pydevproject 114 | 115 | # PyCharm 116 | .idea* 117 | /.run/ 118 | 119 | # KDE 120 | .directory 121 | 122 | # VS Code 123 | .vscode -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | # Optionally set the version of Python and requirements required to build your docs 13 | python: 14 | version: 3.7 15 | install: 16 | - requirements: docs/requirements_docs.txt -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.10.1] – 2025-05-08 4 | ### Changed 5 | - The command `medata_remove` was renamed to `metadata_extended_remove` and can be used to remove the deprecated extended metadata from older files 6 | - Fixed the `texture_update` and `texture_locate` commands and also the `save` command with the `--texture` flag. 7 | - Fixed the conversion from .poly input when the file has comments or 1- index. 8 | - Fixed `get_normal_newell(poly)` to reject faces with > 3 points 9 | 10 | ### Added 11 | - More tests and coverage 12 | - lint and format github action 13 | - --digit option for reprojections 14 | 15 | ### Removed: 16 | - Support for extended metadata: specifically the commands `metadata_create` and `metadata_update` 17 | - All API functionality is deprecated: cjio.models and some functions from cjio.cityjson have been removed. Also all related tests. 18 | 19 | 20 | ## [0.9.0] – 2023-09-28 21 | ### Changed 22 | - now CityJSON v2.0 is the default version 23 | - fixes for gltf export 24 | - fix the cjio subset and children 25 | ### Added 26 | - added an upgrade ops for v2.0 files 27 | 28 | 29 | ## [0.8.2] – 2023-07-13 30 | ### Changed 31 | - Removed the emojis in the console output for the function 'info' 32 | 33 | 34 | ## [0.8.1] – 2023-02-06 35 | ### Added 36 | - Added the `translate` paramter to `cityjson.compress` to manually set the tranlation properties instead of computing from the data. 37 | - The `cityjson.cityjson_for_features` and `cityjson.generate_features` methods. 38 | 39 | ### Changed 40 | #### gltf (glb) converter 41 | - `to_glb` sets a root transformation matrix for z-up to y-up, instead of swapping the vertex coordinates directly. 42 | - Takes a `do_triangulate` argument to completely skip triangulation. 43 | - Compute (smooth) normals. 44 | 45 | ## [0.8.0] – 2022-11-28 46 | ### Added 47 | - added functions for reading/writing CityJSONL (CityJSONFeatures) from stdin/stdout, so cjio can be part of a pipeline of operators processing 3D city models 🚀 48 | ### Changed 49 | - many small bugs fixed 50 | - The function that used an API are going to be deprecated in the upcoming releases, because the cjio API is under refactoring. The affected functions are `cityjson.save`, `cityjson.load`, `cityjson.save`, `cityjson.load_from_j`, `cityjson.get_cityobjects`, `cityjson.set_cityobjects`, `cityjson.to_dataframe`, `cityjson.reference_geometry`, `cityjson.add_to_j` and all members of the `cityjson.models` module. There is a new cityjson library under development, called cjlib, which will replace the relevant parts in cjio. 51 | 52 | 53 | ## [0.7.6] – 2022-09-12 54 | ### Changed 55 | - cjvalpy >=v0.3 is required to use the latest schemas (v1.1.2) 56 | - fix the parsing of the validation string 57 | 58 | 59 | ## [0.7.5] – 2022-08-08 60 | ### Added 61 | - Subset more than one CityObject type (#9) 62 | - `models.Geometry.reproject()` for reprojecting dereferenced geometry boundaries 63 | - Read from `stdin` and save to `stdout` 64 | - `--suppress_msg` to suppress all messages. Required when saving to `stdout` 65 | ### Fixed 66 | - Subset with BBOX does not modify the input model anymore (#10) 67 | - `cityjson.load()` does not fail on a `GeometryInstance`, however it does not load it either (#19) 68 | - Fixes to the *glb* exporter (#20, #57, #83), and fixed the coordinate system 69 | - `texture` and `material` are correctly removed from the geometries of the CityObjects with `textures/materials_remove` 70 | - `vertex-texture` is removed from the CityJSON with `textures_remove` 71 | - Docker image build (#77, #132) 72 | - Other minor fixes 73 | ### Changed 74 | - Export format is an argument, not an option (#35), e.g. `cjio ... export obj out.obj` 75 | - NumPy is a hard requirement 76 | - Require pyproj >= 3.0.0 (#142) 77 | - Refactor warnings and alert printing (#143) 78 | 79 | 80 | ## [0.7.4] - 2022-06-20 81 | ### Fixed 82 | - crash with new version of Click (>=8.1) (#140) 83 | ### Added 84 | - templates for bug reporting 85 | 86 | ## [0.7.3] - 2021-12-15 87 | ### Fixed 88 | - STL export (#127) 89 | 90 | ## [0.7.2] - 2021-12-02 91 | ### Fixed 92 | - String representation of the CityJSON class works again 93 | 94 | ## [0.7.1] - 2021-12-01 95 | ### Fixed 96 | - save operator was crashing for unknown reasons sometimes, this is fixed 97 | 98 | 99 | ## [0.7.0] - 2021-12-01 100 | ### Changed 101 | - Minimum required CityJSON version is 1.1 102 | - Many operators names changed, it's now "property-verb", so that all the operators related to textures for instance are together 103 | - The metadata are only updated (with lineage) when there is a [metadata-extended](https://github.com/cityjson/metadata-extended) property in the file, otherwise nothing is modified 104 | - The schema validator (operator `validate`) is not written in Python anymore and part of cjio, it's using [cjval](https://github.com/cityjson/cjval) and its [Python binding](https://github.com/cityjson/cjvalpy) (which needs to be installed). The validator is several orders of magniture faster too 105 | 106 | ### Added 107 | - A new operator `triangulate` that triangulates each surface of the input (de-triangulate coming soon) 108 | 109 | ### Fixed 110 | - Several bugs were fixed 111 | 112 | ### API 113 | - Loading a file with `cityjson.load()` removes the `transform` property from the file 114 | 115 | 116 | ## [0.6.10] - 2021-10-18 117 | ### Changed 118 | - Minimum required Python is 3.6 119 | 120 | ### Fixed 121 | - Click option is set to None when empty (#99) 122 | - Loading breaks on inconsistent semantics (#102) 123 | - extract_lod doesn't work with the improved LoD (#80) 124 | 125 | ### API changes 126 | - Added `CityJSON.load_from_j` 127 | - Make transformation the default on loading a cityjson 128 | - `CityJSON.add_to_j` includes `reference_geometry`, no need to call it separately 129 | 130 | ## [0.6.9] - 2021-07-06 131 | ### Changed 132 | - version with schemas 1.0.3 (where metadata schema is fixed) 133 | - fix bugs with operators `update_metadata_cmd()` and `get_metadata_cmd()` crashing 134 | 135 | 136 | ## [0.6.8] - 2021-03-19 137 | ### Changed 138 | - fix bug about datetime in schema but not put in metadata 139 | 140 | 141 | ## [0.6.7] - 2021-03-12 142 | ### Changed 143 | - fix bug: crash when validating files containing Extensions under Windows 144 | 145 | 146 | ## [0.6.0] - 2020-10-27 147 | ### Added 148 | - Convert to Binary glTF (glb) 149 | - Convert to Batched 3D Models (b3dm) - Output is probably incorrect though 150 | - Progress bar for the `reproject` command 151 | - Started a proof of concept for an API. You can read about the first struggles in `docs/design_document.ipynb`. Mainly implemented in `models` and a few additional methods in `cityjson`. Plus a bunch of tests for the API ([#13](https://github.com/cityjson/cjio/pull/13)) 152 | - Add tutorials and dedicated documentation 153 | - Docker image and Travis build for it ([#25](https://github.com/cityjson/cjio/pull/25)) 154 | - Generate metadata ([#56](https://github.com/cityjson/cjio/pull/56)) 155 | - STL export format ([#66](https://github.com/cityjson/cjio/pull/66)) 156 | ### Changed 157 | - click messages, warnings got their functions and placed into the `utils` module 158 | - only EPSG codes are supported for the CRS's URN 159 | - When `--indent` is passed to `save`, tabs are used instead of spaces. Results in smaller files. 160 | ### Fixes 161 | - Fix precision when removing duplicates ([#50](https://github.com/cityjson/cjio/pull/60)) 162 | 163 | 164 | ## [0.5.4] - 2019-06-18 165 | ### Changed 166 | - proper schemas are packaged 167 | - clean() operator added 168 | 169 | ## [0.5.2] - 2019-04-29 170 | ### Changed 171 | - CityJSON v1.0.0 supported 172 | - subset() operator: invert --> exclude (clearer for the users) 173 | 174 | 175 | ## [0.5.1] - 2019-02-06 176 | ### Changed 177 | - CityJSON schemas v0.9 added 178 | - cjio supports only CityJSON v0.9, there's an operator to upgrade files ('upgrade_version') 179 | - validate supports CityJSON Extensions from v0.9 180 | ### Added 181 | - new operators, like 'extract_lod', 'export' (to .obj), 'reproject' 182 | 183 | 184 | ## [0.4.0] - 2018-09-25 185 | ### Changed 186 | - CityJSON schemas v08 added 187 | - new operators 188 | - validate now supports CityJSON Extensions 189 | 190 | 191 | ## [0.2.1] - 2018-05-24 192 | ### Changed 193 | - schemas were not uploaded to pypi, now they are 194 | 195 | 196 | ## [0.2.0] - 2018-05-24 197 | ### Added 198 | - hosted on pypi 199 | - decompress 200 | - fix of bugs 201 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.12-slim-bullseye AS builder 2 | 3 | RUN useradd -u 1001 -G root app && \ 4 | chgrp -R 0 /etc/passwd && \ 5 | chmod -R g=u /etc/passwd && \ 6 | mkdir /app && \ 7 | chgrp -R 0 /app && \ 8 | chmod -R g=u /app 9 | 10 | RUN apt-get update && \ 11 | apt-get install -y --no-install-recommends \ 12 | curl \ 13 | build-essential \ 14 | gcc 15 | 16 | ARG PIP_VERSION="pip==22.3.0" 17 | ARG SETUPTOOL_VERSION="setuptools==65.5.0" 18 | RUN python -m venv /opt/venv 19 | ENV PATH="/opt/venv/bin:$PATH" 20 | RUN python3 -m pip install ${PIP_VERSION} ${SETUPTOOL_VERSION} 21 | 22 | # --- cjvalpy build from source because https://github.com/cityjson/cjio/issues/146 23 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 24 | ENV PATH="/root/.cargo/bin:$PATH" 25 | RUN pip install maturin==0.14 26 | 27 | ARG CJVALPY_VERSION="0.3.2" 28 | RUN curl -L -o cjvalpy.tar.gz https://github.com/cityjson/cjvalpy/archive/refs/tags/${CJVALPY_VERSION}.tar.gz && \ 29 | tar -xvf cjvalpy.tar.gz && \ 30 | cd cjvalpy-${CJVALPY_VERSION} && \ 31 | maturin build --release && \ 32 | cd ./target/wheels/ 33 | ARG WHEEL="cjvalpy-${CJVALPY_VERSION}/target/wheels/*.whl" 34 | RUN pip install ${WHEEL} && \ 35 | pip uninstall -y maturin 36 | # --- 37 | 38 | COPY setup.py setup.cfg README.rst LICENSE CHANGELOG.md /app/ 39 | COPY cjio /app/cjio 40 | RUN cd /app && \ 41 | pip install .[export,validate,reproject] && \ 42 | rm -rf /tmp/* && \ 43 | rm -rf /user/local/man && \ 44 | cjio --version 45 | 46 | FROM python:3.8.12-slim AS cjio 47 | LABEL org.opencontainers.image.authors="Balázs Dukai " 48 | LABEL org.opencontainers.image.licenses="MIT" 49 | LABEL org.opencontainers.image.url="https://github.com/cityjson/cjio" 50 | LABEL org.opencontainers.image.description="Python CLI to process and manipulate CityJSON files. The different operators can be chained to perform several processing operations in one step, the CityJSON model goes through them and different versions of the CityJSON model can be saved as files along the pipeline." 51 | LABEL org.opencontainers.image.title="cjio" 52 | 53 | RUN useradd -u 1001 -G root -s /bin/bash app && \ 54 | chgrp -R 0 /etc/passwd && \ 55 | chmod -R g=u /etc/passwd && \ 56 | mkdir /app && \ 57 | chgrp -R 0 /app && \ 58 | chmod -R g=u /app 59 | 60 | COPY --from=builder /opt/venv /opt/venv 61 | 62 | COPY --chown=1001:0 uid_entrypoint.sh /usr/local/bin/ 63 | ENV PATH="/opt/venv/bin:$PATH" 64 | 65 | RUN mkdir /data && \ 66 | chown 1001 /data && \ 67 | chgrp 0 /data && \ 68 | chmod g=u /data 69 | 70 | WORKDIR /data 71 | 72 | ENV LANG="C.UTF-8" 73 | 74 | USER 1001 75 | 76 | ENTRYPOINT ["/usr/local/bin/uid_entrypoint.sh"] 77 | 78 | CMD ["cjio"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 3D geoinformation group at TU Delft (https://3d.bk.tudelft.nl) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | cjio, or CityJSON/io 2 | ==================== 3 | 4 | |License: MIT| |image1| 5 | 6 | Python CLI to process and manipulate `CityJSON `_ files. 7 | The different operators can be chained to perform several processing operations in one step, the 8 | CityJSON model goes through them and different versions of the CityJSON model can be saved as files along the pipeline. 9 | 10 | Documentation 11 | ------------- 12 | 13 | `cjio.readthedocs.io `_ 14 | 15 | Installation 16 | ------------ 17 | 18 | It uses Python 3.7+ only. 19 | 20 | To install the latest release: 21 | 22 | .. code:: console 23 | 24 | pip install cjio 25 | 26 | .. note:: The commands ``export``, ``triangulate``, ``reproject``, and ``validate`` require extra packages that are not install by default. You can install these packages by specifying the 27 | commands for pip. 28 | 29 | .. code:: console 30 | 31 | pip install 'cjio[export,reproject,validate]' 32 | 33 | To install the development branch, and still develop with it: 34 | 35 | .. code:: console 36 | 37 | git checkout develop 38 | virtualenv venv 39 | . venv/bin/activate 40 | pip install --editable '.[develop]' 41 | 42 | **Note for Windows users** 43 | 44 | If your installation fails based on a *pyproj* or *pyrsistent* error there is a small hack to get around it. 45 | Based on the python version you have installed you can download a wheel (binary of a python package) of the problem package/s. 46 | A good website to use is `here `_. 47 | You then run: 48 | 49 | .. code:: console 50 | 51 | pip install [name of wheel file] 52 | 53 | You can then continue with: 54 | 55 | .. code:: console 56 | 57 | pip install cjio 58 | 59 | 60 | Supported CityJSON versions 61 | --------------------------- 62 | 63 | Currently it supports `CityJSON v2.0 `_, but v1.1 and v1.0 files can be upgraded automatically with the operator upgrade`. 64 | 65 | The operators (``cjio --version``) expect that your file is using the latest version `CityJSON schema `_. 66 | If your file uses an earlier version, you can upgrade it with the ``upgrade`` operator: ``cjio old.json upgrade save newfile.city.json`` 67 | 68 | 69 | Usage of the CLI 70 | ---------------- 71 | 72 | After installation, you have a small program called ``cjio``, to see its 73 | possibilities: 74 | 75 | .. code:: console 76 | 77 | cjio --help 78 | 79 | Commands: 80 | attribute_remove Remove an attribute. 81 | attribute_rename Rename an attribute. 82 | crs_assign Assign a (new) CRS (an EPSG). 83 | crs_reproject Reproject to a new EPSG. 84 | crs_translate Translate the coordinates. 85 | export Export to another format. 86 | info Output information about the dataset. 87 | lod_filter Filter only one LoD for a dataset. 88 | materials_remove Remove all materials. 89 | merge Merge the current CityJSON with other ones. 90 | metadata_extended_remove Remove the deprecated +metadata-extended properties. 91 | metadata_get Show the metadata of this dataset. 92 | print Print the (pretty formatted) JSON to the to the console. 93 | save Save to a CityJSON file. 94 | subset Create a subset, City Objects can be selected by: (1) IDs of City Objects; (2) bbox (3) City Object type(s) (4) randomly. 95 | textures_locate Print the location of the texture files. 96 | textures_remove Remove all textures. 97 | textures_update Update the location of the texture files. 98 | triangulate Triangulate every surface. 99 | upgrade Upgrade the CityJSON to the latest version. 100 | validate Validate the CityJSON: (1) against its schemas (2) against the (potential) Extensions schemas (3) extra validations 101 | vertices_clean Remove duplicate vertices + orphan vertices 102 | 103 | 104 | Or see the command-specific help by calling ``--help`` after a command: 105 | 106 | .. code:: console 107 | 108 | Usage: cjio INPUT subset [OPTIONS] 109 | 110 | Create a subset, City Objects can be selected by: (1) IDs of City Objects; 111 | (2) bbox; (3) City Object type(s); (4) randomly. 112 | 113 | These can be combined, except random which overwrites others. 114 | 115 | Option '--exclude' excludes the selected objects, or "reverse" the 116 | selection. 117 | 118 | Usage examples: 119 | 120 | cjio myfile.city.json subset --bbox 104607 490148 104703 490257 save out.city.json 121 | cjio myfile.city.json subset --radius 500.0 610.0 50.0 --exclude save out.city.json 122 | cjio myfile.city.json subset --id house12 save out.city.json 123 | cjio myfile.city.json subset --random 5 save out.city.json 124 | cjio myfile.city.json subset --cotype LandUse --cotype Building save out.city.json 125 | 126 | Options: 127 | --id TEXT The ID of the City Objects; can be used multiple times. 128 | --bbox FLOAT... 2D bbox: minx miny maxx maxy. 129 | --radius FLOAT... x y radius 130 | --random INTEGER Number of random City Objects to select. 131 | --cotype TEXT The City Object types; can be used multiple times. 132 | --exclude Excludes the selection, thus delete the selected 133 | object(s). 134 | --help Show this message and exit. 135 | 136 | 137 | Pipelines of operators 138 | ---------------------- 139 | 140 | The input 3D city model opened is passed through all the operators, and it gets modified by some operators. 141 | Operators like ``info`` and ``validate`` output information in the console and just pass the 3D city model to the next operator. 142 | 143 | .. code:: console 144 | 145 | cjio example.city.json subset --id house12 remove_materials save out.city.json 146 | cjio example.city.json remove_textures info 147 | cjio example.city.json upgrade validate save new.city.json 148 | cjio myfile.city.json merge '/home/elvis/temp/*.city.json' save all_merged.city.json 149 | 150 | 151 | stdin and stdout 152 | ---------------- 153 | 154 | Starting from v0.8, cjio allows to read/write from stdin/stdout (standard input/output streams). 155 | 156 | For reading, it accepts at this moment only `CityJSONL (text sequences with CityJSONFeatures) `_. 157 | Instead of putting the file name, ``stdin`` must be used. 158 | 159 | For writing, both CityJSON files and `CityJSONL files `_ can be piped to stdout. 160 | Instead of putting the file name, ``stdout`` must be used. 161 | Also, the different operators of cjio output messages/information, and those will get in the stdout stream, to avoid this add the flag ``--suppress_msg`` when reading the file, as shown below. 162 | 163 | .. code:: console 164 | 165 | cat mystream.city.jsonl | cjio --suppress_msg stdin remove_materials save stdout 166 | cjio --suppress_msg myfile.city.json remove_materials export jsonl stdout | less 167 | cat myfile.city.json | cjio --suppress_msg stdin crs_reproject 7415 export jsonl mystream.txt 168 | 169 | 170 | Generating Binary glTF 171 | ---------------------- 172 | 173 | Convert the CityJSON ``example.city.json`` to a glb file 174 | ``/home/elvis/gltfs/example.glb`` 175 | 176 | .. code:: console 177 | 178 | cjio example.json export glb /home/elvis/gltfs 179 | 180 | Convert the CityJSON ``example.city.json`` to a glb file 181 | ``/home/elvis/test.glb`` 182 | 183 | .. code:: console 184 | 185 | cjio example.city.json export glb /home/elvis/test.glb 186 | 187 | Usage of the API 188 | ---------------- 189 | 190 | `cjio.readthedocs.io/en/stable/tutorials.html `_ 191 | 192 | Docker 193 | ------ 194 | 195 | If docker is the tool of your choice, please read the following hints. 196 | 197 | To run cjio via docker simply call: 198 | 199 | .. code:: console 200 | 201 | docker run --rm -v :/data tudelft3d/cjio:latest cjio --help 202 | 203 | 204 | To give a simple example for the following lets assume you want to create a geojson which represents 205 | the bounding boxes of the files in your directory. Lets call this script *gridder.py*. It would look like this: 206 | 207 | .. code:: python 208 | 209 | from cjio import cityjson 210 | import glob 211 | import ntpath 212 | import json 213 | import os 214 | from shapely.geometry import box, mapping 215 | 216 | def path_leaf(path): 217 | head, tail = ntpath.split(path) 218 | return tail or ntpath.basename(head) 219 | 220 | files = glob.glob('./*.json') 221 | 222 | geo_json_dict = { 223 | "type": "FeatureCollection", 224 | "features": [] 225 | } 226 | 227 | for f in files: 228 | cj_file = open(f, 'r') 229 | cm = cityjson.reader(file=cj_file) 230 | theinfo = json.loads(cm.get_info()) 231 | las_polygon = box(theinfo['bbox'][0], theinfo['bbox'][1], theinfo['bbox'][3], theinfo['bbox'][4]) 232 | feature = { 233 | 'properties': { 234 | 'name': path_leaf(f) 235 | }, 236 | 'geometry': mapping(las_polygon) 237 | } 238 | geo_json_dict["features"].append(feature) 239 | geo_json_dict["crs"] = { 240 | "type": "name", 241 | "properties": { 242 | "name": "EPSG:{}".format(theinfo['epsg']) 243 | } 244 | } 245 | geo_json_file = open(os.path.join('./', 'grid.json'), 'w+') 246 | geo_json_file.write(json.dumps(geo_json_dict, indent=2)) 247 | geo_json_file.close() 248 | 249 | 250 | This script will produce for all files with postfix ".json" in the directory a bbox polygon using 251 | cjio and save the complete geojson result in grid.json in place. 252 | 253 | If you have a python script like this, simply put it inside your 254 | local data and call docker like this: 255 | 256 | .. code:: console 257 | 258 | docker run --rm -v :/data tudelft3d/cjio:latest python gridder.py 259 | 260 | This will execute your script in the context of the python environment inside the docker image. 261 | 262 | 263 | Example CityJSON datasets 264 | ------------------------- 265 | 266 | There are a few `example files on the CityJSON webpage `_. 267 | 268 | Alternatively, any `CityGML `_ file can be 269 | automatically converted to CityJSON with the open-source project 270 | `citygml-tools `_ (based on 271 | `citygml4j `_). 272 | 273 | 274 | Acknowledgements 275 | ---------------- 276 | 277 | The glTF exporter is adapted from Kavisha's 278 | `CityJSON2glTF `_. 279 | 280 | .. |License: MIT| image:: https://img.shields.io/badge/License-MIT-yellow.svg 281 | :target: https://github.com/tudelft3d/cjio/blob/master/LICENSE 282 | .. |image1| image:: https://badge.fury.io/py/cjio.svg 283 | :target: https://pypi.org/project/cjio/ 284 | -------------------------------------------------------------------------------- /cjio/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.10.1" 2 | import importlib.util 3 | 4 | 5 | loader = importlib.util.find_spec("triangle") 6 | MODULE_TRIANGLE_AVAILABLE = loader is not None 7 | 8 | loader = importlib.util.find_spec("mapbox_earcut") 9 | MODULE_EARCUT_AVAILABLE = loader is not None 10 | 11 | loader = importlib.util.find_spec("pyproj") 12 | MODULE_PYPROJ_AVAILABLE = loader is not None 13 | 14 | loader = importlib.util.find_spec("pandas") 15 | MODULE_PANDAS_AVAILABLE = loader is not None 16 | 17 | loader = importlib.util.find_spec("cjvalpy") 18 | MODULE_CJVAL_AVAILABLE = loader is not None 19 | -------------------------------------------------------------------------------- /cjio/errors.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | class CJInvalidOperation(Exception): 5 | def __init__(self, msg): 6 | self.msg = msg 7 | 8 | def __str__(self): 9 | return "InvalidOperation: %s" % self.msg 10 | 11 | 12 | class CJInvalidVersion(Exception): 13 | def __init__(self, msg): 14 | self.msg = msg 15 | 16 | def __str__(self): 17 | return "InvalidVersion: %s" % self.msg 18 | 19 | 20 | class CJWarning(Warning): 21 | def __init__(self, msg): 22 | super().__init__() 23 | self.msg = msg 24 | 25 | def __str__(self): 26 | return self.msg 27 | 28 | def warn(self): 29 | warnings.warn(self) 30 | -------------------------------------------------------------------------------- /cjio/floatEncoder.py: -------------------------------------------------------------------------------- 1 | class FloatEncoder(float): 2 | __repr__ = staticmethod(lambda x: format(x, ".6f")) 3 | -------------------------------------------------------------------------------- /cjio/geom_help.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | MODULE_TRIANGLE_AVAILABLE = True 6 | try: 7 | import triangle 8 | except ImportError: 9 | MODULE_TRIANGLE_AVAILABLE = False 10 | 11 | MODULE_EARCUT_AVAILABLE = True 12 | try: 13 | import mapbox_earcut 14 | except ImportError: 15 | MODULE_EARCUT_AVAILABLE = False 16 | 17 | 18 | def to_2d(p, n): 19 | # -- n must be normalised 20 | # p = np.array([1, 2, 3]) 21 | # newell = np.array([1, 3, 4.2]) 22 | # n = newell/math.sqrt(p[0]*p[0] + p[1]*p[1] + p[2]*p[2]) 23 | x3 = np.array([1.1, 1.1, 1.1]) # -- is this always a good value?? 24 | if (n == x3).all(): 25 | x3 += np.array([1, 2, 3]) 26 | x3 = x3 - np.dot(x3, n) * n 27 | # print(n, x3) 28 | x3 /= math.sqrt((x3**2).sum()) # make x a unit vector 29 | y3 = np.cross(n, x3) 30 | return (np.dot(p, x3), np.dot(p, y3)) 31 | 32 | 33 | def get_normal_newell(poly): 34 | """ 35 | Compute the normal vector of a polygon using Newell's method. 36 | """ 37 | n = np.array([0.0, 0.0, 0.0], dtype=np.float64) 38 | 39 | for i, p in enumerate(poly): 40 | if len(p) < 3: # -- if it does not have 3 points then skip 41 | continue 42 | ne = i + 1 43 | if ne == len(poly): 44 | ne = 0 45 | n[0] += (poly[i][1] - poly[ne][1]) * (poly[i][2] + poly[ne][2]) 46 | n[1] += (poly[i][2] - poly[ne][2]) * (poly[i][0] + poly[ne][0]) 47 | n[2] += (poly[i][0] - poly[ne][0]) * (poly[i][1] + poly[ne][1]) 48 | 49 | if (n == np.array([0.0, 0.0, 0.0])).all(): 50 | # print("one wrong") 51 | return (n, False) 52 | n = n / math.sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]) 53 | return (n, True) 54 | 55 | 56 | def triangulate_face(face, vnp, sloppy=False): 57 | if not sloppy: 58 | return triangulate_face_shewchuk(face, vnp) 59 | else: 60 | return triangulate_face_mapbox_earcut(face, vnp) 61 | 62 | 63 | ##-- with Shewchuk Triangle library 64 | def triangulate_face_shewchuk(face, vnp): 65 | # print(face) 66 | # -- remove duplicate vertices, which can *easily* make Triangle segfault 67 | for i, each in enumerate(face): 68 | if len(set(each)) < len(each): # -- there are duplicates 69 | re = [] 70 | for k in each: 71 | if re.count(k) == 0: 72 | re.append(k) 73 | face[i] = re 74 | # print(face) 75 | # print(len(face)) 76 | if (len(face) == 1) and (len(face[0]) <= 3): 77 | if len(face[0]) == 3: 78 | # -- if a triangle then do nothing 79 | return (np.array(face), True) 80 | else: 81 | # -- if collapsed then ignore and return false 82 | return (np.array(face), False) 83 | 84 | for i, ring in enumerate(face): 85 | if len(ring) < 3: 86 | # -- if a triangle then do nothing 87 | return (np.zeros(1), False) 88 | sf = np.array([], dtype=np.int64) 89 | for ring in face: 90 | sf = np.hstack((sf, np.array(ring))) 91 | sfv = vnp[sf] 92 | 93 | rings = np.zeros(len(face), dtype=np.int64) 94 | total = 0 95 | for i in range(len(face)): 96 | total += len(face[i]) 97 | rings[i] = total 98 | 99 | # 1. normal with Newell's method 100 | n, b = get_normal_newell(sfv) 101 | 102 | # 2. project to the plane to get xy 103 | sfv2d = np.zeros((sfv.shape[0], 2)) 104 | for i, p in enumerate(sfv): 105 | xy = to_2d(p, n) 106 | sfv2d[i][0] = xy[0] 107 | sfv2d[i][1] = xy[1] 108 | 109 | # -- 3. deal with segments/constraints, prepare the Triangle input 110 | sg = np.zeros((rings[-1], 2), dtype=np.int64) 111 | for i, e in enumerate(sg): 112 | sg[i][0] = i 113 | sg[i][1] = i + 1 114 | starti = 0 115 | for each in rings: 116 | sg[each - 1][1] = starti 117 | starti = each 118 | # -- deal with holes 119 | if len(rings) > 1: 120 | holes = np.zeros((len(rings) - 1, 2)) 121 | for k in range(len(rings) - 1): 122 | # -- basically triangulate the Triangle the Ring, and find the centre 123 | # -- of mass of the first triangle 124 | a = sfv2d[rings[k] : rings[k + 1]] 125 | sg1 = np.zeros((a.shape[0], 2), dtype=np.int64) 126 | for i, e in enumerate(sg1): 127 | sg1[i][0] = i 128 | sg1[i][1] = i + 1 129 | sg1[-1][1] = 0 130 | pcl = dict(vertices=a, segments=sg1) 131 | trl = triangle.triangulate(pcl, "p") 132 | t = trl["triangles"][0] 133 | c = np.average(a[t], axis=0) # -- find its centre of mass 134 | holes[k][0] = c[0] 135 | holes[k][1] = c[1] 136 | A = dict(vertices=sfv2d, segments=sg, holes=holes) 137 | else: 138 | A = dict(vertices=sfv2d, segments=sg) 139 | 140 | try: 141 | re = triangle.triangulate(A, "p") 142 | except Exception as e: 143 | print(e) 144 | print("Houston we have a problem...") 145 | # re = {} 146 | return (np.array(face), False) 147 | # -- check the output 148 | if "triangles" not in re: 149 | return ([], False) 150 | re = re["triangles"] 151 | for i, each in enumerate(re): 152 | try: 153 | re[i] = sf[each] 154 | except Exception as exp: 155 | print(exp) 156 | return (re, False) 157 | return (re, True) 158 | 159 | 160 | def triangulate_face_mapbox_earcut(face, vnp): 161 | sf = np.array([], dtype=np.int64) 162 | if (len(face) == 1) and (len(face[0]) == 3): 163 | return (np.array(face), True) 164 | for ring in face: 165 | sf = np.hstack((sf, np.array(ring))) 166 | sfv = vnp[sf] 167 | # print(sf) 168 | # print(sfv) 169 | rings = np.zeros(len(face), dtype=np.int32) 170 | total = 0 171 | for i in range(len(face)): 172 | total += len(face[i]) 173 | rings[i] = total 174 | # print(rings) 175 | 176 | # 1. normal with Newell's method 177 | n, b = get_normal_newell(sfv) 178 | 179 | # -- if already a triangle then return it 180 | if not b: 181 | return (n, False) 182 | # print ("Newell:", n) 183 | 184 | # 2. project to the plane to get xy 185 | sfv2d = np.zeros((sfv.shape[0], 2)) 186 | # print (sfv2d) 187 | for i, p in enumerate(sfv): 188 | xy = to_2d(p, n) 189 | # print("xy", xy) 190 | sfv2d[i][0] = xy[0] 191 | sfv2d[i][1] = xy[1] 192 | result = mapbox_earcut.triangulate_float64(sfv2d, rings) 193 | # print (result.reshape(-1, 3)) 194 | 195 | for i, each in enumerate(result): 196 | # print (sf[i]) 197 | result[i] = sf[each] 198 | 199 | # print (result.reshape(-1, 3)) 200 | return (result.reshape(-1, 3), True) 201 | 202 | 203 | def triangle_normal(tri, vertexlist, weighted=False): 204 | """Compute the triangle normal vector weighted by the triangle area. 205 | 206 | Returns None if the normal vector cannot be computed (mostly because the triangle 207 | is degenerate). 208 | """ 209 | v0, v1, v2 = tri[0], tri[1], tri[2] 210 | p0 = np.array((vertexlist[v0][0], vertexlist[v0][1], vertexlist[v0][2])) 211 | p1 = np.array((vertexlist[v1][0], vertexlist[v1][1], vertexlist[v1][2])) 212 | p2 = np.array((vertexlist[v2][0], vertexlist[v2][1], vertexlist[v2][2])) 213 | cross_prod = np.cross(p1 - p0, p2 - p0) 214 | magnitude = np.linalg.norm(cross_prod) 215 | if math.isclose(magnitude, 0.0): 216 | return None 217 | else: 218 | norm_vec = cross_prod / magnitude 219 | if not weighted: 220 | return norm_vec 221 | else: 222 | tri_area = magnitude * 0.5 223 | return norm_vec * tri_area 224 | 225 | 226 | def average_normal(normals): 227 | """Compute the smooth (average) normal vector from a list of vectors. 228 | Returns a numpy array of [x,y,z]. If the normal cannot be computed, then it returns 229 | a fake normal of [1.0, 0.0, 0.0]. 230 | """ 231 | s = sum(normals) 232 | n = np.linalg.norm(s) 233 | if math.isclose(n, 0.0): 234 | # Set a fake normal if length of the sum of normals is 235 | # 0. Can happen with opposite vectors. 236 | normal_vec = np.array([1.0, 0.0, 0.0]) 237 | else: 238 | normal_vec = s / n 239 | return normal_vec 240 | -------------------------------------------------------------------------------- /cjio/subset.py: -------------------------------------------------------------------------------- 1 | """CityModel subset functions""" 2 | 3 | 4 | def select_co_ids(j, IDs): 5 | IDs = list(IDs) 6 | re = set() 7 | for theid in IDs: 8 | if theid in j["CityObjects"] and "parents" not in j["CityObjects"][theid]: 9 | re.add(theid) 10 | 11 | # deal with CityObjectGroup 12 | for each in j["CityObjects"]: 13 | if each in IDs: 14 | if ( 15 | j["CityObjects"][each]["type"] == "CityObjectGroup" 16 | and "members" in j["CityObjects"][each] 17 | ): 18 | for member in j["CityObjects"][each]["members"]: 19 | re.add(member) 20 | 21 | # -- add children "recursively" 22 | children_list = [] 23 | for id in re: 24 | if "children" in j["CityObjects"][id]: 25 | for child in j["CityObjects"][id]["children"]: 26 | children_list.append(child) 27 | while len(children_list) > 0: 28 | c = children_list.pop() 29 | re.add(c) 30 | if "children" in j["CityObjects"][c]: 31 | for child in j["CityObjects"][c]["children"]: 32 | children_list.append(child) 33 | return re 34 | 35 | 36 | def process_geometry(j, j2): 37 | # -- update vertex indices 38 | oldnewids = {} 39 | newvertices = [] 40 | for each in j2["CityObjects"]: 41 | if "geometry" in j2["CityObjects"][each]: 42 | for geom in j2["CityObjects"][each]["geometry"]: 43 | update_array_indices( 44 | geom["boundaries"], oldnewids, j["vertices"], newvertices, -1 45 | ) 46 | j2["vertices"] = newvertices 47 | 48 | 49 | def process_templates(j, j2): 50 | dOldNewIDs = {} 51 | newones = [] 52 | for each in j2["CityObjects"]: 53 | if "geometry" in j2["CityObjects"][each]: 54 | for geom in j2["CityObjects"][each]["geometry"]: 55 | if geom["type"] == "GeometryInstance": 56 | t = geom["template"] 57 | if t in dOldNewIDs: 58 | geom["template"] = dOldNewIDs[t] 59 | else: 60 | geom["template"] = len(newones) 61 | dOldNewIDs[t] = len(newones) 62 | newones.append(j["geometry-templates"]["templates"][t]) 63 | if len(newones) > 0: 64 | j2["geometry-templates"] = {} 65 | j2["geometry-templates"]["vertices-templates"] = j["geometry-templates"][ 66 | "vertices-templates" 67 | ] 68 | j2["geometry-templates"]["templates"] = newones 69 | 70 | 71 | def process_appearance(j, j2): 72 | # -- materials 73 | dOldNewIDs = {} 74 | newmats = [] 75 | for each in j2["CityObjects"]: 76 | if "geometry" in j2["CityObjects"][each]: 77 | for geom in j2["CityObjects"][each]["geometry"]: 78 | if "material" in geom: 79 | for each in geom["material"]: 80 | if "value" in geom["material"][each]: 81 | v = geom["material"][each]["value"] 82 | if v in dOldNewIDs: 83 | geom["material"][each]["value"] = dOldNewIDs[v] 84 | else: 85 | geom["material"][each]["value"] = len(newmats) 86 | dOldNewIDs[v] = len(newmats) 87 | newmats.append(j["appearance"]["materials"][v]) 88 | if "values" in geom["material"][each]: 89 | update_array_indices( 90 | geom["material"][each]["values"], 91 | dOldNewIDs, 92 | j["appearance"]["materials"], 93 | newmats, 94 | -1, 95 | ) 96 | if len(newmats) > 0: 97 | j2["appearance"]["materials"] = newmats 98 | 99 | # -- textures references (first int in the arrays) 100 | dOldNewIDs = {} 101 | newtextures = [] 102 | for each in j2["CityObjects"]: 103 | if "geometry" in j2["CityObjects"][each]: 104 | for geom in j2["CityObjects"][each]["geometry"]: 105 | if "texture" in geom: 106 | for each in geom["texture"]: 107 | if "values" in geom["texture"][each]: 108 | update_array_indices( 109 | geom["texture"][each]["values"], 110 | dOldNewIDs, 111 | j["appearance"]["textures"], 112 | newtextures, 113 | 0, 114 | ) 115 | if len(newtextures) > 0: 116 | j2["appearance"]["textures"] = newtextures 117 | # -- textures vertices references (1+ int in the arrays) 118 | dOldNewIDs = {} 119 | newtextures = [] 120 | for each in j2["CityObjects"]: 121 | if "geometry" in j2["CityObjects"][each]: 122 | for geom in j2["CityObjects"][each]["geometry"]: 123 | if "texture" in geom: 124 | for each in geom["texture"]: 125 | if "values" in geom["texture"][each]: 126 | update_array_indices( 127 | geom["texture"][each]["values"], 128 | dOldNewIDs, 129 | j["appearance"]["vertices-texture"], 130 | newtextures, 131 | 1, 132 | ) 133 | if len(newtextures) > 0: 134 | j2["appearance"]["vertices-texture"] = newtextures 135 | 136 | 137 | def update_array_indices(a, dOldNewIDs, oldarray, newarray, slicearray): 138 | # -- slicearray: -1=none ; 0=use-only-first (for textures) ; 1=use-1+ (for textures) 139 | # -- a must be an array 140 | # -- issue with passing integer is that it's non-mutable, thus can't update 141 | # -- (or I don't know how...) 142 | for i, each in enumerate(a): 143 | if isinstance(each, list): 144 | update_array_indices(each, dOldNewIDs, oldarray, newarray, slicearray) 145 | elif each is not None: 146 | if ( 147 | (slicearray == -1) 148 | or (slicearray == 0 and i == 0) 149 | or (slicearray == 1 and i > 0) 150 | ): 151 | if each in dOldNewIDs: 152 | a[i] = dOldNewIDs[each] 153 | else: 154 | a[i] = len(newarray) 155 | dOldNewIDs[each] = len(newarray) 156 | newarray.append(oldarray[each]) 157 | -------------------------------------------------------------------------------- /cjio/utils.py: -------------------------------------------------------------------------------- 1 | """Various utility functions""" 2 | 3 | import os.path 4 | import click 5 | 6 | 7 | def verify_filename(filename): 8 | """Verify if the provided output filename is a file or a directory""" 9 | res = {"dir": False, "path": ""} 10 | if os.path.isdir(filename): 11 | res["dir"] = True 12 | absp = os.path.abspath(filename) 13 | if os.path.exists(absp): 14 | res["path"] = absp 15 | else: 16 | raise click.ClickException("Couldn't expand %s to absolute path" % filename) 17 | else: 18 | base = os.path.basename(filename) 19 | dirname = os.path.abspath(os.path.dirname(filename)) 20 | # parent directory must exist, we don't recurse further 21 | if not os.path.exists(dirname): 22 | raise click.ClickException('Path does not exist: "%s"' % (dirname)) 23 | fname, extension = os.path.splitext(base) 24 | res["path"] = os.path.join(dirname, base) 25 | if len(extension) == 0: 26 | res["dir"] = True 27 | else: 28 | res["dir"] = False 29 | return res 30 | -------------------------------------------------------------------------------- /docker/Dockerfile_Cesium: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | 3 | RUN mkdir /opt/cesium;\ 4 | wget https://github.com/AnalyticalGraphicsInc/cesium/releases/download/1.56.1/Cesium-1.56.1.zip -O /opt/cesium.zip -nv;\ 5 | unzip /opt/cesium.zip -d /opt/cesium;\ 6 | rm /opt/cesium.zip;\ 7 | cd /opt/cesium;\ 8 | npm install 9 | 10 | WORKDIR /opt/cesium 11 | EXPOSE 8080 12 | CMD ["npm", "start"] 13 | 14 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | docker run --rm --network=host --name=cesium -v $(pwd):/opt/cesium/cjio cesium 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -E -a 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = cjio 8 | SOURCEDIR = source 9 | BUILDDIR = ../../cjio-docs 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | api: 20 | sphinx-apidoc -f -o ./source ../cjio ../cjio/*cjio* ../cjio/*convert* ../cjio/*errors* ../cjio/*geom_help* ../cjio/*remove_textures* ../cjio/*subset* ../cjio/*utils* ../cjio/*validation* 21 | buildandcommithtml: html 22 | cd $(BUILDDIR)/html; git add . ; git commit -m "rebuilt docs"; git push origin gh-pages 23 | %: Makefile 24 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 25 | -------------------------------------------------------------------------------- /docs/figures/rotterdamsubset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityjson/cjio/35728539127f71b1d5ae458169ff84050e5e20ad/docs/figures/rotterdamsubset.png -------------------------------------------------------------------------------- /docs/figures/zurich.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityjson/cjio/35728539127f71b1d5ae458169ff84050e5e20ad/docs/figures/zurich.png -------------------------------------------------------------------------------- /docs/figures/zurichmlresult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityjson/cjio/35728539127f71b1d5ae458169ff84050e5e20ad/docs/figures/zurichmlresult.png -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements_docs.txt: -------------------------------------------------------------------------------- 1 | sphinx >= 3.2.1 2 | pydata-sphinx-theme 3 | Pygments >= 2.7.4 4 | nbsphinx >= 0.7.1 5 | jsonref 6 | click -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ============== 3 | 4 | cjio.cityjson module 5 | -------------------- 6 | 7 | .. automodule:: cjio.cityjson 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | cjio.models module 13 | ------------------ 14 | 15 | .. automodule:: cjio.models 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/api_tutorial_create.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "collapsed": true, 7 | "pycharm": { 8 | "name": "#%% md\n" 9 | } 10 | }, 11 | "source": [ 12 | "# Creating city models and objects\n", 13 | "\n", 14 | "In this tutorial we explore how to create new city models with using `cjio`'s\n", 15 | " API." 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 1, 21 | "metadata": { 22 | "pycharm": { 23 | "is_executing": false, 24 | "name": "#%%\n" 25 | } 26 | }, 27 | "outputs": [], 28 | "source": [ 29 | "from pathlib import Path\n", 30 | "\n", 31 | "from cjio import cityjson\n", 32 | "from cjio.models import CityObject, Geometry" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": { 38 | "pycharm": { 39 | "name": "#%% md\n" 40 | } 41 | }, 42 | "source": [ 43 | "Set up paths for the tutorial.\n", 44 | " " 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 2, 50 | "metadata": { 51 | "pycharm": { 52 | "is_executing": false, 53 | "name": "#%%\n" 54 | } 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "package_dir = Path(__name__).resolve().parent.parent.parent\n", 59 | "schema_dir = package_dir / 'cjio' / 'schemas'/ '1.0.0'\n", 60 | "data_dir = package_dir / 'tests' / 'data'" 61 | ] 62 | }, 63 | { 64 | "cell_type": "markdown", 65 | "metadata": { 66 | "pycharm": { 67 | "name": "#%% md\n" 68 | } 69 | }, 70 | "source": [ 71 | "## Creating a single CityObject\n", 72 | "\n", 73 | "We are building a single CityObject of type *Building*. This building has an \n", 74 | "LoD2 geometry, thus it has Semantic Surfaces. The geometric shape of the \n", 75 | "building is a simple cube (size 10x10x10), which is sufficient for this \n", 76 | "demonstration.\n", 77 | "\n", 78 | "The idea is that we create empty containers for the CityModel, CityObjects and\n", 79 | "Geometries, then fill those up and add to the CityModel." 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": { 85 | "pycharm": { 86 | "name": "#%% md\n" 87 | } 88 | }, 89 | "source": [ 90 | "We create an empty CityModel" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 3, 96 | "metadata": { 97 | "pycharm": { 98 | "is_executing": false, 99 | "name": "#%%\n" 100 | } 101 | }, 102 | "outputs": [ 103 | { 104 | "name": "stdout", 105 | "output_type": "stream", 106 | "text": [ 107 | "{\n", 108 | " \"cityjson_version\": \"1.0\",\n", 109 | " \"epsg\": null,\n", 110 | " \"bbox\": [\n", 111 | " 9000000000.0,\n", 112 | " 9000000000.0,\n", 113 | " 9000000000.0,\n", 114 | " -9000000000.0,\n", 115 | " -9000000000.0,\n", 116 | " -9000000000.0\n", 117 | " ],\n", 118 | " \"transform/compressed\": false,\n", 119 | " \"cityobjects_total\": 0,\n", 120 | " \"cityobjects_present\": [],\n", 121 | " \"materials\": false,\n", 122 | " \"textures\": false\n", 123 | "}\n" 124 | ] 125 | } 126 | ], 127 | "source": [ 128 | "cm = cityjson.CityJSON()\n", 129 | "print(cm)" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": { 135 | "pycharm": { 136 | "name": "#%% md\n" 137 | } 138 | }, 139 | "source": [ 140 | "An empty CityObject. Note that the ID is required." 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": 4, 146 | "metadata": { 147 | "pycharm": { 148 | "is_executing": false, 149 | "name": "#%%\n" 150 | } 151 | }, 152 | "outputs": [], 153 | "source": [ 154 | "co = CityObject(\n", 155 | " id='1'\n", 156 | ")" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": { 162 | "pycharm": { 163 | "name": "#%% md\n" 164 | } 165 | }, 166 | "source": [ 167 | "We can also add attributes" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 5, 173 | "metadata": { 174 | "pycharm": { 175 | "is_executing": false, 176 | "name": "#%%\n" 177 | } 178 | }, 179 | "outputs": [], 180 | "source": [ 181 | "co_attrs = {\n", 182 | " 'some_attribute': 42,\n", 183 | " 'other_attribute': 'bla bla'\n", 184 | "}\n", 185 | "co.attributes = co_attrs" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "metadata": { 191 | "pycharm": { 192 | "name": "#%% md\n" 193 | } 194 | }, 195 | "source": [ 196 | "Let's see what do we have" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 6, 202 | "metadata": { 203 | "pycharm": { 204 | "is_executing": false, 205 | "name": "#%%\n" 206 | } 207 | }, 208 | "outputs": [ 209 | { 210 | "name": "stdout", 211 | "output_type": "stream", 212 | "text": [ 213 | "{\n", 214 | " \"id\": \"1\",\n", 215 | " \"type\": null,\n", 216 | " \"attributes\": {\n", 217 | " \"some_attribute\": 42,\n", 218 | " \"other_attribute\": \"bla bla\"\n", 219 | " },\n", 220 | " \"children\": [],\n", 221 | " \"parents\": [],\n", 222 | " \"geometry_type\": [],\n", 223 | " \"geometry_lod\": [],\n", 224 | " \"semantic_surfaces\": []\n", 225 | "}\n" 226 | ] 227 | } 228 | ], 229 | "source": [ 230 | "print(co)" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": { 236 | "pycharm": { 237 | "name": "#%% md\n" 238 | } 239 | }, 240 | "source": [ 241 | "Instantiate a Geometry without boundaries and semantics" 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": 7, 247 | "metadata": { 248 | "pycharm": { 249 | "is_executing": false, 250 | "name": "#%%\n" 251 | } 252 | }, 253 | "outputs": [], 254 | "source": [ 255 | "geom = Geometry(type='Solid', lod=2)" 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "metadata": { 261 | "pycharm": { 262 | "name": "#%% md\n" 263 | } 264 | }, 265 | "source": [ 266 | "We build the boundary Solid of the cube\n", 267 | "The surfaces are in this order: WallSurface, WallSurface, WallSurface, WallSurface, GroundSurface, RoofSurface" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": 8, 273 | "metadata": { 274 | "pycharm": { 275 | "is_executing": false, 276 | "name": "#%%\n" 277 | } 278 | }, 279 | "outputs": [], 280 | "source": [ 281 | "bdry = [\n", 282 | " [[(0.0, 0.0, 0.0), (10.0, 0.0, 0.0), (10.0, 0.0, 10.0), (0.0, 0.0, 10.0)]],\n", 283 | " [[(10.0, 0.0, 0.0), (10.0, 10.0, 0.0), (10.0, 10.0, 10.0), (10.0, 0.0, 10.0)]],\n", 284 | " [[(10.0, 10.0, 0.0), (0.0, 10.0, 0.0), (0.0, 10.0, 10.0), (10.0, 10.0, 10.0)]],\n", 285 | " [[(0.0, 10.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 10.0), (0.0, 10.0, 10.0)]],\n", 286 | " [[(0.0, 0.0, 0.0), (0.0, 10.0, 0.0), (10.0, 10.0, 0.0), (10.0, 0.0, 0.0)]],\n", 287 | " [[(10.0, 0.0, 10.0), (10.0, 10.0, 10.0), (0.0, 10.0, 10.0), (0.0, 0.0, 10.0)]]\n", 288 | "]" 289 | ] 290 | }, 291 | { 292 | "cell_type": "markdown", 293 | "metadata": { 294 | "pycharm": { 295 | "name": "#%% md\n" 296 | } 297 | }, 298 | "source": [ 299 | "Add the boundary to the Geometry" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": 9, 305 | "metadata": { 306 | "pycharm": { 307 | "is_executing": false, 308 | "name": "#%%\n" 309 | } 310 | }, 311 | "outputs": [], 312 | "source": [ 313 | "geom.boundaries.append(bdry)" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": { 319 | "pycharm": { 320 | "name": "#%% md\n" 321 | } 322 | }, 323 | "source": [ 324 | "We build the SemanticSurfaces for the boundary. The `surfaces` attribute must\n", 325 | "contain at least the `surface_idx` and `type` keys, optionally `attributes`.\n", 326 | "We have three semantic surface types, WallSurface, GroundSurface, RoofSurface." 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": 10, 332 | "metadata": { 333 | "pycharm": { 334 | "is_executing": false, 335 | "name": "#%%\n" 336 | } 337 | }, 338 | "outputs": [], 339 | "source": [ 340 | "srf = {\n", 341 | " 0: {'surface_idx': [], 'type': 'WallSurface'},\n", 342 | " 1: {'surface_idx': [], 'type': 'GroundSurface'},\n", 343 | " 2: {'surface_idx': [], 'type': 'RoofSurface'}\n", 344 | "}" 345 | ] 346 | }, 347 | { 348 | "cell_type": "markdown", 349 | "metadata": { 350 | "pycharm": { 351 | "name": "#%% md\n" 352 | } 353 | }, 354 | "source": [ 355 | "We use the `surface_idx` to point to the surfaces of the boundary. Thus the\n", 356 | "index to a single boundary surface is composed as [Solid index, Shell index, Surface index].\n", 357 | "Consequently, in case of a CompositeSolid which first Solid, outer Shell,\n", 358 | "second Surface is a WallSurface, one element in the `surface_idx` would be\n", 359 | "`[0, 0, 1]`. Then assuming that there is only a single WallSurface in the\n", 360 | "mentioned CompositeSolid, the index to the WallSurfaces is composed as\n", 361 | "`{'surface_idx': [ [0, 0, 1] ], 'type': 'WallSurface'}`.\n", 362 | "In case of a Solid boundary type the *Solid index* is omitted from the elements\n", 363 | "of `surface_idx`. In case of a MultiSurface boundary type both the *Solid index*\n", 364 | "and *Shell index* are omitted from the elements of `surface_idx`.\n", 365 | "\n", 366 | "We create the surface index accordingly and assign them to the geometry." 367 | ] 368 | }, 369 | { 370 | "cell_type": "code", 371 | "execution_count": 11, 372 | "metadata": { 373 | "pycharm": { 374 | "is_executing": false, 375 | "name": "#%%\n" 376 | } 377 | }, 378 | "outputs": [], 379 | "source": [ 380 | "geom.surfaces[0] = {'surface_idx': [[0,0], [0,1], [0,2], [0,3]], 'type': 'WallSurface'}\n", 381 | "geom.surfaces[1] = {'surface_idx': [[0,4]], 'type': 'GroundSurface'}\n", 382 | "geom.surfaces[2] = {'surface_idx': [[0,5]], 'type': 'RoofSurface'}" 383 | ] 384 | }, 385 | { 386 | "cell_type": "markdown", 387 | "metadata": { 388 | "pycharm": { 389 | "name": "#%% md\n" 390 | } 391 | }, 392 | "source": [ 393 | "Then we test if it works." 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": 12, 399 | "metadata": { 400 | "pycharm": { 401 | "is_executing": false, 402 | "name": "#%%\n" 403 | } 404 | }, 405 | "outputs": [], 406 | "source": [ 407 | "ground = geom.get_surfaces('groundsurface')\n", 408 | "ground_boundaries = []\n", 409 | "for g in ground.values():\n", 410 | " ground_boundaries.append(geom.get_surface_boundaries(g))" 411 | ] 412 | }, 413 | { 414 | "cell_type": "markdown", 415 | "metadata": { 416 | "pycharm": { 417 | "name": "#%% md\n" 418 | } 419 | }, 420 | "source": [ 421 | "We have a list of generators" 422 | ] 423 | }, 424 | { 425 | "cell_type": "code", 426 | "execution_count": 13, 427 | "metadata": { 428 | "pycharm": { 429 | "is_executing": false, 430 | "name": "#%%\n" 431 | } 432 | }, 433 | "outputs": [], 434 | "source": [ 435 | "res = list(ground_boundaries[0])" 436 | ] 437 | }, 438 | { 439 | "cell_type": "markdown", 440 | "metadata": { 441 | "pycharm": { 442 | "name": "#%% md\n" 443 | } 444 | }, 445 | "source": [ 446 | "The generator creates a list of surfaces --> a MultiSurface" 447 | ] 448 | }, 449 | { 450 | "cell_type": "code", 451 | "execution_count": 14, 452 | "metadata": { 453 | "pycharm": { 454 | "is_executing": false, 455 | "name": "#%%\n" 456 | } 457 | }, 458 | "outputs": [], 459 | "source": [ 460 | "assert res[0] == bdry[4]\n", 461 | "\n", 462 | "# %%\n", 463 | "wall = geom.get_surfaces('wallsurface')\n", 464 | "wall_boundaries = []\n", 465 | "for w in wall.values():\n", 466 | " wall_boundaries.append(geom.get_surface_boundaries(w))" 467 | ] 468 | }, 469 | { 470 | "cell_type": "markdown", 471 | "metadata": { 472 | "pycharm": { 473 | "name": "#%% md\n" 474 | } 475 | }, 476 | "source": [ 477 | "We put everything together, first filling up the CityObject" 478 | ] 479 | }, 480 | { 481 | "cell_type": "code", 482 | "execution_count": 15, 483 | "metadata": { 484 | "pycharm": { 485 | "is_executing": false, 486 | "name": "#%%\n" 487 | } 488 | }, 489 | "outputs": [], 490 | "source": [ 491 | "co.geometry.append(geom)\n", 492 | "co.type = 'Building'" 493 | ] 494 | }, 495 | { 496 | "cell_type": "markdown", 497 | "metadata": { 498 | "pycharm": { 499 | "name": "#%% md\n" 500 | } 501 | }, 502 | "source": [ 503 | "Then adding the CityObject to the CityModel." 504 | ] 505 | }, 506 | { 507 | "cell_type": "code", 508 | "execution_count": 16, 509 | "metadata": { 510 | "pycharm": { 511 | "is_executing": false, 512 | "name": "#%%\n" 513 | } 514 | }, 515 | "outputs": [], 516 | "source": [ 517 | "cm.cityobjects[co.id] = co" 518 | ] 519 | }, 520 | { 521 | "cell_type": "markdown", 522 | "metadata": { 523 | "pycharm": { 524 | "name": "#%% md\n" 525 | } 526 | }, 527 | "source": [ 528 | "Let's validate the citymodel before writing it to a file. However, first we \n", 529 | "need to index the geometry boundaries and create the vertex list, second we \n", 530 | "need to add the cityobject and vertices to the internal json-store of the \n", 531 | "citymodel so the `validate()` method can validate them.\n", 532 | "\n", 533 | "Note: CityJSON version 1.0.0 only accepts the Geometry `lod` as a numeric \n", 534 | "value and not a string." 535 | ] 536 | }, 537 | { 538 | "cell_type": "code", 539 | "execution_count": 17, 540 | "metadata": { 541 | "pycharm": { 542 | "is_executing": false, 543 | "name": "#%%\n" 544 | }, 545 | "scrolled": true 546 | }, 547 | "outputs": [ 548 | { 549 | "data": { 550 | "text/plain": [ 551 | "[0.0, 0.0, 0.0, 10.0, 10.0, 10.0]" 552 | ] 553 | }, 554 | "execution_count": 17, 555 | "metadata": {}, 556 | "output_type": "execute_result" 557 | } 558 | ], 559 | "source": [ 560 | "cityobjects, vertex_lookup = cm.reference_geometry()\n", 561 | "cm.add_to_j(cityobjects,vertex_lookup)\n", 562 | "cm.update_bbox()\n", 563 | "#cm.validate(folder_schemas=schema_dir)" 564 | ] 565 | }, 566 | { 567 | "cell_type": "code", 568 | "execution_count": 18, 569 | "metadata": {}, 570 | "outputs": [ 571 | { 572 | "data": { 573 | "text/plain": [ 574 | "{\n", 575 | " \"cityjson_version\": \"1.0\",\n", 576 | " \"epsg\": null,\n", 577 | " \"bbox\": [\n", 578 | " 0.0,\n", 579 | " 0.0,\n", 580 | " 0.0,\n", 581 | " 10.0,\n", 582 | " 10.0,\n", 583 | " 10.0\n", 584 | " ],\n", 585 | " \"transform/compressed\": false,\n", 586 | " \"cityobjects_total\": 1,\n", 587 | " \"cityobjects_present\": [\n", 588 | " \"Building\"\n", 589 | " ],\n", 590 | " \"materials\": false,\n", 591 | " \"textures\": false\n", 592 | "}" 593 | ] 594 | }, 595 | "execution_count": 18, 596 | "metadata": {}, 597 | "output_type": "execute_result" 598 | } 599 | ], 600 | "source": [ 601 | "cm" 602 | ] 603 | }, 604 | { 605 | "cell_type": "markdown", 606 | "metadata": { 607 | "pycharm": { 608 | "name": "#%% md\n" 609 | } 610 | }, 611 | "source": [ 612 | "Finally, we write the citymodel to a CityJSON file." 613 | ] 614 | }, 615 | { 616 | "cell_type": "code", 617 | "execution_count": 19, 618 | "metadata": { 619 | "pycharm": { 620 | "is_executing": false, 621 | "name": "#%%\n" 622 | } 623 | }, 624 | "outputs": [], 625 | "source": [ 626 | "outfile = data_dir / 'test_create.json'\n", 627 | "cityjson.save(cm, outfile)" 628 | ] 629 | } 630 | ], 631 | "metadata": { 632 | "kernelspec": { 633 | "display_name": "Python 3", 634 | "language": "python", 635 | "name": "python3" 636 | }, 637 | "language_info": { 638 | "codemirror_mode": { 639 | "name": "ipython", 640 | "version": 3 641 | }, 642 | "file_extension": ".py", 643 | "mimetype": "text/x-python", 644 | "name": "python", 645 | "nbconvert_exporter": "python", 646 | "pygments_lexer": "ipython3", 647 | "version": "3.6.7" 648 | }, 649 | "pycharm": { 650 | "stem_cell": { 651 | "cell_type": "raw", 652 | "metadata": { 653 | "collapsed": false 654 | }, 655 | "source": [] 656 | } 657 | } 658 | }, 659 | "nbformat": 4, 660 | "nbformat_minor": 1 661 | } 662 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'cjio' 21 | copyright = '2025, 3D geoinformation group at TU Delft' 22 | author = 'Hugo Ledoux, Balázs Dukai, Gina Stavropoulou' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.10.1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.viewcode', 39 | 'sphinx.ext.githubpages', 40 | 'nbsphinx', 41 | ] 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = '.rst' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # List of patterns, relative to source directory, that match files and 56 | # directories to ignore when looking for source files. 57 | # This pattern also affects html_static_path and html_extra_path. 58 | exclude_patterns = [] 59 | 60 | # The name of the Pygments (syntax highlighting) style to use. 61 | pygments_style = 'sphinx' 62 | 63 | 64 | # -- Options for HTML output ------------------------------------------------- 65 | 66 | # The theme to use for HTML and HTML Help pages. See the documentation for 67 | # a list of builtin themes. 68 | # 69 | html_theme = "pydata_sphinx_theme" 70 | 71 | # Theme options are theme-specific and customize the look and feel of a theme 72 | # further. For a list of options available for each theme, see the 73 | # documentation. 74 | # 75 | 76 | 77 | # Add any paths that contain custom static files (such as style sheets) here, 78 | # relative to this directory. They are copied after the builtin static files, 79 | # so a file named "default.css" will overwrite the builtin "default.css". 80 | html_static_path = ['_static'] 81 | 82 | # -- Options for HTMLHelp output --------------------------------------------- 83 | 84 | # Output file base name for HTML help builder. 85 | htmlhelp_basename = 'cjiodoc' 86 | 87 | # -- Options for todo extension ---------------------------------------------- 88 | 89 | # If true, `todo` and `todoList` produce output, else they produce nothing. 90 | todo_include_todos = True 91 | -------------------------------------------------------------------------------- /docs/source/includeme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. cjio documentation master file, created by 2 | sphinx-quickstart on Thu May 2 15:57:04 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ================================ 7 | Welcome to cjio's documentation! 8 | ================================ 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | includeme 14 | tutorials 15 | api 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/source/tutorials.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | api_tutorial_basics.ipynb 8 | api_tutorial_ml.ipynb 9 | api_tutorial_create.ipynb -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.10.1 3 | commit = True 4 | tag = True 5 | 6 | [bdist_wheel] 7 | universal = 1 8 | 9 | [metadata] 10 | license_file = LICENSE 11 | 12 | [tool:pytest] 13 | log_cli = true 14 | addopts = --ignore=setup.py 15 | markers = 16 | balazs: tests that run against Balázs' local data 17 | 18 | [bumpversion:file:setup.py] 19 | search = version='{current_version}' 20 | replace = version='{new_version}' 21 | 22 | [bumpversion:file:cjio/__init__.py] 23 | search = __version__ = '{current_version}' 24 | replace = __version__ = '{new_version}' 25 | 26 | [bumpversion:file:docs/source/conf.py] 27 | search = release = '{current_version}' 28 | replace = release = '{new_version}' 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from pathlib import Path 3 | 4 | CURRENT_DIR = Path(__file__).parent 5 | 6 | with open("README.rst", "r") as fh: 7 | long_description = fh.read() 8 | 9 | setup( 10 | name="cjio", 11 | version="0.10.1", 12 | description="CLI to process and manipulate CityJSON files", 13 | long_description=long_description, 14 | long_description_content_type="text/x-rst", 15 | url="https://github.com/cityjson/cjio", 16 | author="Hugo Ledoux, Balázs Dukai, Gina Stavropoulou", 17 | author_email="h.ledoux@tudelft.nl, balazs.dukai@3dgi.nl, g.stavropoulou@tudelft.nl", 18 | python_requires=">=3.6", 19 | packages=["cjio"], 20 | # include_package_data=True, 21 | license="MIT", 22 | classifiers=[ 23 | # https://pypi.org/pypi?%3Aaction=list_classifiers 24 | "Development Status :: 3 - Alpha", 25 | "Intended Audience :: Science/Research", 26 | "Topic :: Scientific/Engineering :: GIS", 27 | "License :: OSI Approved :: MIT License", 28 | "Programming Language :: Python :: 3", 29 | "Operating System :: POSIX :: Linux", 30 | "Operating System :: MacOS :: MacOS X", 31 | "Operating System :: Microsoft :: Windows", 32 | ], 33 | install_requires=["numpy", "Click>=8.1.0"], 34 | extras_require={ 35 | "develop": [ 36 | "pytest", 37 | "bump2version", 38 | "coverage", 39 | ], 40 | "export": ["pandas", "mapbox-earcut", "triangle2"], 41 | "validate": ["cjvalpy>=0.3.0"], 42 | "reproject": ["pyproj>=3.0.0"], 43 | }, 44 | entry_points=""" 45 | [console_scripts] 46 | cjio=cjio.cjio:cli 47 | """, 48 | ) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityjson/cjio/35728539127f71b1d5ae458169ff84050e5e20ad/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import pytest 3 | 4 | from cjio import cityjson 5 | 6 | 7 | # ------------------------------------ add option for running the full test set 8 | def pytest_addoption(parser): 9 | parser.addoption( 10 | "--run-all", action="store_true", default=False, help="Run all tests" 11 | ) 12 | 13 | 14 | def pytest_configure(config): 15 | config.addinivalue_line("markers", "slow: mark test as slow to run") 16 | 17 | 18 | def pytest_collection_modifyitems(config, items): 19 | if config.getoption("--run-all"): 20 | return 21 | skip_slow = pytest.mark.skip(reason="need --run-all option to run") 22 | for item in items: 23 | if "slow" in item.keywords: 24 | item.add_marker(skip_slow) 25 | 26 | 27 | @pytest.fixture(scope="function") 28 | def data_dir(): 29 | package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 30 | yield os.path.join(package_dir, "tests", "data") 31 | 32 | 33 | @pytest.fixture(scope="function") 34 | def temp_texture_dir(): 35 | package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 36 | yield os.path.join(package_dir, "tests", "data", "rotterdam", "textures") 37 | 38 | 39 | @pytest.fixture(scope="function") 40 | def data_output_dir(): 41 | package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 42 | d = os.path.join(package_dir, "tmp") 43 | os.makedirs(d, exist_ok=True) 44 | yield d 45 | 46 | 47 | @pytest.fixture(scope="function") 48 | def delft(data_dir): 49 | p = os.path.join(data_dir, "delft.json") 50 | with open(p, "r") as f: 51 | yield cityjson.CityJSON(file=f) 52 | 53 | 54 | @pytest.fixture(scope="function") 55 | def sample_input_path(data_dir): 56 | p = os.path.join(data_dir, "delft.json") 57 | yield p 58 | 59 | 60 | @pytest.fixture(scope="function") 61 | def sample_wrong_suffix(data_dir): 62 | p = os.path.join(data_dir, "empty.yaml") 63 | yield p 64 | 65 | 66 | @pytest.fixture(scope="function") 67 | def sample_off_file(data_dir): 68 | p = os.path.join(data_dir, "box.off") 69 | yield p 70 | 71 | 72 | @pytest.fixture(scope="function") 73 | def sample_poly_0_index_file(data_dir): 74 | p = os.path.join(data_dir, "cube.poly") 75 | yield p 76 | 77 | 78 | @pytest.fixture(scope="function") 79 | def sample_poly_1_index_file(data_dir): 80 | p = os.path.join(data_dir, "box.poly") 81 | yield p 82 | 83 | 84 | @pytest.fixture(scope="function") 85 | def sample_with_ext_metadata_input_path(data_dir): 86 | p = os.path.join(data_dir, "material/mt-1-triangulated.json") 87 | yield p 88 | 89 | 90 | @pytest.fixture(scope="function") 91 | def wrong_input_path(data_dir): 92 | p = os.path.join(data_dir, "delft_no_file.json") 93 | yield p 94 | 95 | 96 | @pytest.fixture(scope="function") 97 | def delft_1b(data_dir): 98 | p = os.path.join(data_dir, "delft_1b.json") 99 | with open(p, "r") as f: 100 | yield cityjson.CityJSON(file=f) 101 | 102 | 103 | @pytest.fixture(scope="function") 104 | def rotterdam_subset_path(data_dir): 105 | p = os.path.join(data_dir, "rotterdam", "rotterdam_subset.json") 106 | yield p 107 | 108 | 109 | @pytest.fixture(scope="function") 110 | def rotterdam_subset(data_dir): 111 | p = os.path.join(data_dir, "rotterdam", "rotterdam_subset.json") 112 | with open(p, "r") as f: 113 | yield cityjson.CityJSON(file=f) 114 | 115 | 116 | @pytest.fixture(scope="function") 117 | def zurich_subset(data_dir): 118 | p = os.path.join(data_dir, "zurich", "zurich_subset_lod2.json") 119 | with open(p, "r") as f: 120 | yield cityjson.CityJSON(file=f) 121 | 122 | 123 | @pytest.fixture(scope="function") 124 | def dummy(data_dir): 125 | p = os.path.join(data_dir, "dummy", "dummy.json") 126 | with open(p, "r") as f: 127 | yield cityjson.CityJSON(file=f) 128 | 129 | 130 | @pytest.fixture(scope="function") 131 | def dummy_noappearance(data_dir): 132 | p = os.path.join(data_dir, "dummy", "dummy_noappearance.json") 133 | with open(p, "r") as f: 134 | yield cityjson.CityJSON(file=f) 135 | 136 | 137 | @pytest.fixture(scope="function") 138 | def cube(data_dir): 139 | p = os.path.join(data_dir, "cube.json") 140 | with open(p, "r") as f: 141 | yield cityjson.CityJSON(file=f) 142 | 143 | 144 | @pytest.fixture(scope="function") 145 | def multi_lod(data_dir): 146 | p = os.path.join(data_dir, "multi_lod.json") 147 | with open(p, "r") as f: 148 | yield cityjson.CityJSON(file=f) 149 | 150 | 151 | @pytest.fixture(scope="function") 152 | def multi_lod_path(data_dir): 153 | p = os.path.join(data_dir, "multi_lod.json") 154 | yield p 155 | 156 | 157 | @pytest.fixture(scope="function") 158 | def vertices(): 159 | yield [ 160 | (0.0, 1.0, 0.0), 161 | (1.0, 1.0, 0.0), 162 | (2.0, 1.0, 0.0), 163 | (3.0, 1.0, 0.0), 164 | (4.0, 1.0, 0.0), 165 | (5.0, 1.0, 0.0), 166 | ] 167 | 168 | 169 | @pytest.fixture( 170 | scope="function", 171 | params=[ 172 | ("material", "mt-1.json"), 173 | ("material", "mt-2.json"), 174 | ("dummy", "composite_solid_with_material.json"), 175 | ("dummy", "dummy.json"), 176 | ("dummy", "multisurface_with_material.json"), 177 | ], 178 | ) 179 | def materials(data_dir, request): 180 | p = os.path.join(data_dir, *request.param) 181 | with open(p, "r") as f: 182 | yield cityjson.CityJSON(file=f) 183 | 184 | 185 | @pytest.fixture(scope="function") 186 | def materials_two(data_dir): 187 | """Two models with materials for testing their merging""" 188 | cms = [] 189 | p = os.path.join(data_dir, "material", "mt-1.json") 190 | with open(p, "r") as f: 191 | cms.append(cityjson.CityJSON(file=f)) 192 | p = os.path.join(data_dir, "material", "mt-2.json") 193 | with open(p, "r") as f: 194 | cms.append(cityjson.CityJSON(file=f)) 195 | yield cms 196 | 197 | 198 | @pytest.fixture( 199 | scope="function", 200 | params=[ 201 | ("material", "mt-1-triangulated.json"), 202 | ("material", "mt-2-triangulated.json"), 203 | ], 204 | ) 205 | def triangulated(data_dir, request): 206 | p = os.path.join(data_dir, *request.param) 207 | with open(p, "r") as f: 208 | yield cityjson.CityJSON(file=f) 209 | -------------------------------------------------------------------------------- /tests/data/box.off: -------------------------------------------------------------------------------- 1 | OFF 2 | 8 6 0 3 | -0.500000 -0.500000 0.500000 4 | 0.500000 -0.500000 0.500000 5 | -0.500000 0.500000 0.500000 6 | 0.500000 0.500000 0.500000 7 | -0.500000 0.500000 -0.500000 8 | 0.500000 0.500000 -0.500000 9 | -0.500000 -0.500000 -0.500000 10 | 0.500000 -0.500000 -0.500000 11 | 4 0 1 3 2 12 | 4 2 3 5 4 13 | 4 4 5 7 6 14 | 4 6 7 1 0 15 | 4 1 7 5 3 16 | 4 6 0 2 4 -------------------------------------------------------------------------------- /tests/data/box.poly: -------------------------------------------------------------------------------- 1 | # Part 1 - node list 2 | # node count, 3 dim, no attribute, no boundary marker 3 | 8 3 0 0 4 | # Node index, node coordinates 5 | 1 0.0 0.0 0.0 6 | 2 1.0 0.0 0.0 7 | 3 1.0 1.0 0.0 8 | 4 0.0 1.0 0.0 9 | 5 0.0 0.0 1.0 10 | 6 1.0 0.0 1.0 11 | 7 1.0 1.0 1.0 12 | 8 0.0 1.0 1.0 13 | 14 | # Part 2 - facet list 15 | # facet count, no boundary marker 16 | 6 0 17 | # facets 18 | 1 # 1 polygon, no hole, no boundary marker 19 | 4 1 2 3 4 # front 20 | 1 21 | 4 5 6 7 8 # back 22 | 1 23 | 4 1 2 6 5 # bottom 24 | 1 25 | 4 2 3 7 6 # right 26 | 1 27 | 4 3 4 8 7 # top 28 | 1 29 | 4 4 1 5 8 # left 30 | 31 | # Part 3 - hole list 32 | 0 # no hole 33 | 34 | # Part 4 - region list 35 | 0 # no region -------------------------------------------------------------------------------- /tests/data/cube.c.json: -------------------------------------------------------------------------------- 1 | { 2 | "CityObjects": {"id-1": { 3 | "geometry": [{ 4 | "boundaries": [[ 5 | [[ 6 | 0, 7 | 1, 8 | 2, 9 | 3 10 | ]], 11 | [[ 12 | 4, 13 | 5, 14 | 6, 15 | 7 16 | ]], 17 | [[ 18 | 0, 19 | 3, 20 | 5, 21 | 4 22 | ]], 23 | [[ 24 | 3, 25 | 2, 26 | 6, 27 | 5 28 | ]], 29 | [[ 30 | 2, 31 | 1, 32 | 7, 33 | 6 34 | ]], 35 | [[ 36 | 1, 37 | 0, 38 | 4, 39 | 7 40 | ]] 41 | ]], 42 | "type": "Solid", 43 | "lod": "1" 44 | }], 45 | "type": "Building" 46 | }}, 47 | "version": "1.1", 48 | "type": "CityJSON", 49 | "vertices": [ 50 | [ 51 | 0, 52 | 0, 53 | 0 54 | ], 55 | [ 56 | 0, 57 | 1000, 58 | 0 59 | ], 60 | [ 61 | 1000, 62 | 1000, 63 | 0 64 | ], 65 | [ 66 | 1000, 67 | 0, 68 | 0 69 | ], 70 | [ 71 | 0, 72 | 0, 73 | 1000 74 | ], 75 | [ 76 | 1000, 77 | 0, 78 | 1000 79 | ], 80 | [ 81 | 1000, 82 | 1000, 83 | 1000 84 | ], 85 | [ 86 | 0, 87 | 1000, 88 | 1000 89 | ] 90 | ], 91 | "metadata": {"geographicalExtent": [ 92 | 0, 93 | 0, 94 | 0, 95 | 1, 96 | 1, 97 | 1 98 | ]}, 99 | "transform": { 100 | "scale": [ 101 | 0.001, 102 | 0.001, 103 | 0.001 104 | ], 105 | "translate": [ 106 | 0, 107 | 0, 108 | 0 109 | ] 110 | } 111 | } -------------------------------------------------------------------------------- /tests/data/cube.json: -------------------------------------------------------------------------------- 1 | { 2 | "CityObjects": {"id-1": { 3 | "geometry": [{ 4 | "boundaries": [[ 5 | [[ 6 | 0, 7 | 1, 8 | 2, 9 | 3 10 | ]], 11 | [[ 12 | 4, 13 | 5, 14 | 6, 15 | 7 16 | ]], 17 | [[ 18 | 0, 19 | 3, 20 | 5, 21 | 4 22 | ]], 23 | [[ 24 | 3, 25 | 2, 26 | 6, 27 | 5 28 | ]], 29 | [[ 30 | 2, 31 | 1, 32 | 7, 33 | 6 34 | ]], 35 | [[ 36 | 1, 37 | 0, 38 | 4, 39 | 7 40 | ]] 41 | ]], 42 | "type": "Solid", 43 | "lod": "1" 44 | }], 45 | "type": "Building" 46 | }}, 47 | "version": "1.1", 48 | "type": "CityJSON", 49 | "vertices": [ 50 | [ 51 | 0, 52 | 0, 53 | 0 54 | ], 55 | [ 56 | 0, 57 | 1000, 58 | 0 59 | ], 60 | [ 61 | 1000, 62 | 1000, 63 | 0 64 | ], 65 | [ 66 | 1000, 67 | 0, 68 | 0 69 | ], 70 | [ 71 | 0, 72 | 0, 73 | 1000 74 | ], 75 | [ 76 | 1000, 77 | 0, 78 | 1000 79 | ], 80 | [ 81 | 1000, 82 | 1000, 83 | 1000 84 | ], 85 | [ 86 | 0, 87 | 1000, 88 | 1000 89 | ] 90 | ], 91 | "metadata": {"geographicalExtent": [ 92 | 0, 93 | 0, 94 | 0, 95 | 1, 96 | 1, 97 | 1 98 | ]}, 99 | "transform": { 100 | "scale": [ 101 | 0.001, 102 | 0.001, 103 | 0.001 104 | ], 105 | "translate": [ 106 | 0, 107 | 0, 108 | 0 109 | ] 110 | } 111 | } -------------------------------------------------------------------------------- /tests/data/cube.poly: -------------------------------------------------------------------------------- 1 | 8 3 0 0 2 | 0 0.0 0.0 0.0 3 | 1 1.0 0.0 0.0 4 | 2 1.0 1.0 0.0 5 | 3 0.0 1.0 0.0 6 | 4 0.0 0.0 1.0 7 | 5 1.0 0.0 1.0 8 | 6 1.0 1.0 1.0 9 | 7 0.0 1.0 1.0 10 | 6 0 11 | 1 0 12 | 4 0 3 2 1 13 | 1 0 14 | 4 4 5 6 7 15 | 1 0 16 | 4 0 1 5 4 17 | 1 0 18 | 4 1 2 6 5 19 | 1 0 20 | 4 2 3 7 6 21 | 1 0 22 | 4 3 0 4 7 23 | 0 24 | 0 -------------------------------------------------------------------------------- /tests/data/dummy/composite_solid_with_material.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"1.1","CityObjects":{"onebuilding":{"type":"+GenericCityObject","geometry":[{"type":"CompositeSolid","lod":"1","boundaries":[[[[[0,1,2,3]],[[4,5,6,7]],[[0,3,5,4]],[[3,2,6,5]],[[2,1,7,6]],[[1,0,4,7]]]],[[[[3,2,8,9]],[[5,10,11,6]],[[3,9,10,5]],[[9,8,11,10]],[[8,2,6,11]],[[2,3,5,6]]]]],"material":{"irradiation":{"values":[[[0,0,1,null,1,8]],[[0,1,1,2,3,4]]]}},"texture":{"rgbTexture":{"values":[[[[[0,1,1,2,3]],[[0,3,4,5,3]],[[1,4,1,6,7]],[[0,1,1,2,3]],[[0,1,1,2,3]],[[0,1,1,2,3]]]],[[[[4,1,1,2,3]],[[3,3,4,5,3]],[[2,4,1,6,7]],[[1,1,1,2,3]],[[0,1,1,2,3]],[[10,1,1,2,3]]]]]}}}]}},"vertices":[[0,0,0],[0,1000,0],[1000,1000,0],[1000,0,0],[0,0,1000],[1000,0,1000],[1000,1000,1000],[0,1000,1000],[2000,1000,0],[2000,0,0],[2000,0,1000],[2000,1000,1000]],"transform":{"scale":[0.001,0.001,0.001],"translate":[0.0,0.0,0.0]},"extensions":{"Generic":{"url":"https://cityjson.org/extensions/download/generic.ext.json","version":"1.0"}}} -------------------------------------------------------------------------------- /tests/data/dummy/dummy.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"2.0","metadata":{"referenceSystem":"https://www.opengis.net/def/crs/EPSG/0/7415","geographicalExtent":[0.000000,0.000000,0.000000,1.000000,1.000000,1.000000]},"CityObjects":{"102636712":{"type":"Building","attributes":{"measuredHeight":22.300000,"roofType":"gable","yearOfConstruction":1904,"owner":"Elvis Presley"},"geometry":[{"type":"Solid","lod":"1.1","boundaries":[[[[3,0,0,0],[3,0,0]],[[1,2,4,5]],[[3,0,2,1]],[[0,0,4,2]],[[0,0,5,4]],[[0,3,1,5]]]],"material":{"irradiation":{"values":[[0,0,null,null,2,2]]},"irradiation-2":{"value":1}}},{"type":"Solid","lod":"2.1","boundaries":[[[[3,0,0,0]],[[1,2,4,5]],[[3,0,2,1]],[[0,0,4,2]],[[0,0,5,4]],[[0,3,1,5]]],[[[3,0,0,0]],[[1,2,4,5]],[[3,0,2,1]],[[0,0,4,2]]]],"texture":{"winter-textures":{"values":[[[[0,10,13,12,19]],[[null]],[[null]],[[0,1,2,6,5]],[[0,2,3,7,6]],[[0,3,0,4,7]]],[[[null]],[[null]],[[0,0,1,5,4]],[[0,1,2,6,5]]]]},"summer-textures":{"values":[[[[1,7,3,2,1]],[[null]],[[null]],[[1,1,2,6,5]],[[1,2,3,7,6]],[[1,3,0,4,7]]],[[[null]],[[null]],[[1,0,1,5,4]],[[1,1,2,6,5]]]]}}}]},"1243":{"type":"SolitaryVegetationObject","geometry":[{"type":"GeometryInstance","template":0,"boundaries":[0],"transformationMatrix":[2.000000,0.000000,0.000000,0.000000,0.000000,2.000000,0.000000,0.000000,0.000000,0.000000,2.000000,0.000000,0.000000,0.000000,0.000000,1.000000]}]}},"vertices":[[1000,0,0],[1000,1000,0],[0,1000,0],[0,0,0],[0,0,1000],[1000,0,1000],[1000,1000,1000]],"something-else":{"this":1.000000,"that":"blablabla"},"geometry-templates":{"templates":[{"type":"MultiSurface","lod":"2","boundaries":[[[0,3,2,1]],[[4,5,6,7]],[[0,1,5,4]]]},{"type":"MultiSurface","lod":"2","boundaries":[[[1,2,6,5]],[[2,3,7,6]],[[3,0,4,7]]]}],"vertices-templates":[[0.000000,0.500000,0.000000],[1.000000,0.000000,0.000000],[11.000000,0.000000,0.000000],[11.000000,10.000000,0.000000],[1.000000,12.000000,0.000000],[1.000000,40.000000,0.000000],[1.000000,1.000000,0.000000],[0.000000,1.000000,0.000000]]},"appearance":{"default-theme-texture":"summer-textures","default-theme-material":"irradiation","vertices-texture":[[0.000000,0.500000],[1.000000,0.000000],[1.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000],[0.000000,1.000000]],"textures":[{"type":"PNG","image":"myfacade.png"},{"type":"JPG","image":"myroof.jpg"},{"type":"JPG","image":"mymymy.jpg"}],"materials":[{"name":"irradiation-0-50","ambientIntensity":0.750000,"diffuseColor":[0.900000,0.100000,0.750000],"specularColor":[0.900000,0.100000,0.750000],"transparency":1.000000},{"name":"irradiation-51-80","diffuseColor":[0.900000,0.100000,0.750000],"shininess":0.000000,"transparency":0.500000,"isSmooth":true},{"name":"irradiation-81-100","diffuseColor":[0.190000,0.110000,0.175000],"shininess":0.200000,"transparency":0.900000,"isSmooth":true}]},"transform":{"scale":[0.001000,0.001000,0.001000],"translate":[0.000000,0.000000,0.000000]}} -------------------------------------------------------------------------------- /tests/data/dummy/dummy_noappearance.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"1.1","metadata":{"referenceSystem":"https://www.opengis.net/def/crs/EPSG/0/7415","geographicalExtent":[0.0,0.0,0.0,1.0,1.0,1.0]},"CityObjects":{"102636712":{"type":"Building","attributes":{"measuredHeight":22.3,"roofType":"gable","yearOfConstruction":1904,"owner":"Elvis Presley"},"geometry":[{"type":"Solid","lod":"1.1","boundaries":[[[[3,0,0,0],[3,0,0]],[[1,2,4,5]],[[3,0,2,1]],[[0,0,4,2]],[[0,0,5,4]],[[0,3,1,5]]]]},{"type":"Solid","lod":"2.1","boundaries":[[[[3,0,0,0]],[[1,2,4,5]],[[3,0,2,1]],[[0,0,4,2]],[[0,0,5,4]],[[0,3,1,5]]],[[[3,0,0,0]],[[1,2,4,5]],[[3,0,2,1]],[[0,0,4,2]]]]}]}},"vertices":[[1000,0,0],[1000,1000,0],[0,1000,0],[0,0,0],[0,0,1000],[1000,0,1000],[1000,1000,1000]],"something-else":{"this":1.0,"that":"blablabla"},"geometry-templates":{"templates":[{"type":"MultiSurface","lod":"2","boundaries":[[[0,3,2,1]],[[4,5,6,7]],[[0,1,5,4]]]},{"type":"MultiSurface","lod":"2","boundaries":[[[1,2,6,5]],[[2,3,7,6]],[[3,0,4,7]]]}],"vertices-templates":[[0.0,0.5,0.0],[1.0,0.0,0.0],[11.0,0.0,0.0],[11.0,10.0,0.0],[1.0,12.0,0.0],[1.0,40.0,0.0],[1.0,1.0,0.0],[0.0,1.0,0.0]]},"appearance":{"vertices-texture":[[0.0,0.5],[1.0,0.0],[1.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0],[0.0,1.0]]},"transform":{"scale":[0.001,0.001,0.001],"translate":[0.0,0.0,0.0]}} -------------------------------------------------------------------------------- /tests/data/dummy/multisurface_with_material.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"1.1","CityObjects":{"GMLID_6162422_289094_1279":{"type":"CityFurniture","attributes":{"function":"1090"},"geometry":[{"type":"MultiSurface","boundaries":[[[0,1,2,3]],[[1,0,4,5]],[[3,2,6,7]],[[2,1,5,6]],[[3,7,4,0]],[[8,9,10,11]],[[12,13,14,15]],[[13,8,11,14]],[[12,15,10,9]],[[16,17,18,19]],[[19,18,20,21]],[[22,23,21,20]],[[23,16,19,21]],[[22,20,18,17]],[[15,14,23,22]],[[15,22,17,10]],[[11,10,17,16]],[[7,12,9,4]],[[14,11,16,23]],[[6,5,8,13]],[[5,4,9,8]],[[7,6,13,12]],[[24,25,26,27]],[[28,29,30,31]],[[27,26,32,33]],[[34,35,29,28]],[[33,36,30,29,35,37,24,27]],[[34,28,31,38,32,26,25,39]],[[33,32,38,36]],[[39,37,35,34]],[[36,38,31,30]],[[25,24,37,39]],[[40,41,42,43]],[[44,45,42,41]],[[44,46,47,45]],[[46,40,43,47]],[[43,42,45,47]],[[40,46,44,41]],[[48,49,50,51]],[[51,50,52,53]],[[54,55,56,57]],[[58,59,55,54]],[[53,52,60,61]],[[53,61,56,55,59,62,48,51]],[[58,54,57,60,52,50,49,63]],[[61,60,57,56]],[[63,62,59,58]],[[49,48,62,63]],[[64,65,66,67]],[[65,68,69,66]],[[70,69,68,71]],[[64,71,68,65]],[[67,66,69,70]],[[67,70,71,64]],[[72,73,74,41]],[[72,75,76,73]],[[75,77,78,76]],[[77,41,74,78]],[[73,76,78,74]],[[72,41,77,75]],[[79,80,81,82]],[[83,81,82,84]],[[85,80,81,83]],[[86,79,80,85]],[[84,82,79,86]],[[87,88,89,90]],[[88,87,91,92]],[[92,91,93,94]],[[90,89,94,93]],[[89,88,92,94]],[[90,93,91,87]],[[95,96,97,98]],[[99,100,96,95]],[[101,102,100,99]],[[103,104,102,101]],[[104,103,105,106]],[[106,105,107,108]],[[108,107,109,110]],[[110,109,111,112]],[[112,111,113,114]],[[113,115,116,114]],[[116,115,117,118]],[[118,117,119,120]],[[121,122,120,119]],[[123,122,121,124]],[[125,126,123,124]],[[98,97,126,125]],[[116,118,120,122,123,126,97,96,100,102,104,106,108,110,112,114]],[[124,121,119,117,115,113,111,109,107,105,103,101,99,95,98,125]],[[86,127,84]],[[84,127,83]],[[83,127,85]],[[85,127,86]],[[128,129,130,131]],[[132,128,131,133]],[[134,132,133,135]],[[136,134,135,137]],[[137,138,139,136]],[[138,140,141,139]],[[140,142,143,141]],[[142,144,145,143]],[[144,146,147,145]],[[146,148,149,147]],[[148,150,151,149]],[[150,152,153,151]],[[154,153,152,155]],[[156,154,155,157]],[[158,156,157,159]],[[129,158,159,130]],[[157,155,152,150,148,146,144,142,140,138,137,135,133,131,130,159]],[[149,151,153,154,156,158,129,128,132,134,136,139,141,143,145,147]],[[160,161,162,163]],[[164,165,166,167]],[[167,166,168,169]],[[170,171,165,164]],[[172,173,171,170]],[[174,175,176,177]],[[178,179,180,181]],[[177,176,182,183]],[[184,185,162,161]],[[186,187,188,189]],[[190,164,167,191]],[[191,167,169,192]],[[193,194,195,196]],[[197,186,189,198]],[[199,200,201,202]],[[203,178,181,204]],[[205,195,194,206]],[[175,207,208,176]],[[176,208,209,182]],[[210,211,212,213]],[[214,172,170,215]],[[216,217,218,219]],[[215,170,164,190]],[[220,221,222,223]],[[217,224,225,218]],[[226,199,202,227]],[[224,228,229,225]],[[200,210,213,201]],[[230,226,227,231]],[[228,205,206,229]],[[198,230,231,197]],[[232,233,221,220]],[[204,181,234,235]],[[192,169,178,203]],[[184,236,236,237,214,215,190,191,192,203,204,235,209,208,207,238,185],[206,194,193,211,210,200,199,226,230,198,189,188,219,218,225,229],[233,233,239,240,222,222,221]],[[235,234,182,209]],[[241,237,236,242]],[[211,193,196,212]],[[243,244,240,239]],[[172,214,237,241]],[[169,168,179,178]],[[181,180,245,234]],[[234,245,183,182]],[[246,241,242,247]],[[173,172,241,246]],[[238,248,162,185]],[[248,238,207,175]],[[249,222,240,244]],[[222,249,223,222]],[[163,250,174,177,183,245,180,179,168,166,165,171,173,246,247,247,160]],[[248,250,163,162]],[[250,248,175,174]],[[247,251,161,160]],[[251,247,247,242]],[[251,236,184,161]],[[236,251,242,236]],[[233,252,243,239]],[[252,232,220,223,249,244,243]],[[232,252,233,233]],[[216,187,186,197,231,227,202,201,213,212,196,195,205,228,224,217]],[[219,188,187,216]],[[253,254,255,256]],[[257,258,259,260]],[[256,255,261,262]],[[263,264,258,257]],[[256,262,265,259,258,264,266,253]],[[257,260,267,261,255,254,268,263]],[[262,261,265]],[[261,267,265]],[[268,266,264,263]],[[260,259,265,267]],[[266,268,254,253]],[[269,270,271,272,273,274,275,276]],[[270,277,278,271]],[[279,280,278,277,281,282,283,284]],[[276,282,281,269]],[[269,281,277,270]],[[284,274,273,279]],[[275,283,282,276]],[[283,275,274,284]],[[280,272,271,278]],[[279,273,272,280]]],"material":{"visual":{"values":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,1,null,null,null,1,0,0,0,1,0,1,0,4,4,4,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0]},"vv":{"value":5}},"lod":"3"}]}},"vertices":[[9756,1614,532],[9751,1613,532],[9745,1622,532],[9748,1625,532],[9756,1614,584],[9751,1613,584],[9745,1622,584],[9748,1625,584],[9751,1613,636],[9756,1614,636],[9756,1614,687],[9751,1613,687],[9748,1625,636],[9745,1622,636],[9745,1622,687],[9748,1625,687],[9751,1613,740],[9756,1614,740],[9756,1614,792],[9751,1613,792],[9748,1625,792],[9745,1622,792],[9748,1625,740],[9745,1622,740],[9741,1611,854],[9744,1613,854],[9745,1610,856],[9743,1609,856],[9742,1617,863],[9738,1614,863],[9741,1611,866],[9744,1613,866],[9747,1609,860],[9744,1606,860],[9741,1618,860],[9737,1617,860],[9743,1609,863],[9738,1614,856],[9745,1610,863],[9742,1617,856],[9738,1625,419],[9742,1627,419],[9742,1627,856],[9738,1625,856],[9740,1630,419],[9740,1630,856],[9737,1629,419],[9737,1629,856],[9756,1605,822],[9753,1602,822],[9748,1608,827],[9753,1610,827],[9748,1609,833],[9751,1610,833],[9763,1589,844],[9766,1590,844],[9761,1597,849],[9759,1593,849],[9763,1588,838],[9766,1590,838],[9756,1598,849],[9759,1601,849],[9759,1601,822],[9756,1597,822],[9751,1605,837],[9740,1622,851],[9744,1623,851],[9754,1608,837],[9744,1617,860],[9747,1618,860],[9758,1602,844],[9754,1601,844],[9756,1608,419],[9756,1608,459],[9742,1627,459],[9776,1621,419],[9776,1621,459],[9761,1641,419],[9761,1641,459],[9756,1614,459],[9769,1623,459],[9761,1634,459],[9748,1625,459],[9761,1634,879],[9748,1625,879],[9769,1623,879],[9756,1614,879],[9756,1614,479],[9735,1601,479],[9729,1610,479],[9748,1625,479],[9756,1614,504],[9735,1601,504],[9748,1625,504],[9729,1610,504],[9740,1629,835],[9767,1589,835],[9766,1588,840],[9738,1627,840],[9743,1630,830],[9770,1590,830],[9747,1633,827],[9774,1593,827],[9751,1635,825],[9777,1596,825],[9754,1638,827],[9782,1598,827],[9759,1642,830],[9786,1601,830],[9761,1642,835],[9789,1604,835],[9763,1643,840],[9790,1604,840],[9761,1642,846],[9789,1604,846],[9759,1642,850],[9786,1601,850],[9754,1638,853],[9782,1598,853],[9751,1635,854],[9777,1596,854],[9747,1633,853],[9774,1593,853],[9770,1590,850],[9743,1630,850],[9740,1629,846],[9767,1589,846],[9759,1623,888],[9754,1614,857],[9756,1614,860],[9748,1609,860],[9748,1610,857],[9754,1615,856],[9748,1611,856],[9754,1617,854],[9747,1613,854],[9751,1619,854],[9745,1614,854],[9744,1617,854],[9751,1622,854],[9743,1618,856],[9750,1622,856],[9743,1619,857],[9748,1625,857],[9742,1619,860],[9748,1625,860],[9743,1619,863],[9748,1625,863],[9743,1618,865],[9750,1622,865],[9744,1617,866],[9751,1622,866],[9745,1614,866],[9751,1619,866],[9754,1617,866],[9747,1613,866],[9754,1615,865],[9748,1611,865],[9754,1614,863],[9748,1610,863],[9734,1631,851],[9732,1630,851],[9732,1630,866],[9734,1631,866],[9791,1544,851],[9793,1545,851],[9795,1542,854],[9793,1541,854],[9795,1542,859],[9793,1541,859],[9789,1548,847],[9791,1549,847],[9786,1550,847],[9788,1553,847],[9783,1560,867],[9782,1557,867],[9783,1554,870],[9786,1556,870],[9793,1541,863],[9795,1542,863],[9793,1545,867],[9791,1544,867],[9786,1550,870],[9788,1553,870],[9731,1630,851],[9731,1630,866],[9785,1553,851],[9783,1556,854],[9782,1554,854],[9783,1553,851],[9790,1542,851],[9791,1541,854],[9792,1540,859],[9786,1548,867],[9785,1549,867],[9786,1550,867],[9788,1549,867],[9786,1550,851],[9785,1549,851],[9789,1544,856],[9789,1544,859],[9791,1545,859],[9791,1545,856],[9791,1541,863],[9790,1542,867],[9785,1553,867],[9783,1553,867],[9780,1557,867],[9782,1553,870],[9785,1549,870],[9789,1544,862],[9788,1545,865],[9790,1546,865],[9791,1545,862],[9785,1549,847],[9788,1545,847],[9783,1557,856],[9782,1557,859],[9780,1557,859],[9780,1556,856],[9780,1561,859],[9777,1560,859],[9779,1558,863],[9780,1561,863],[9783,1557,862],[9780,1556,862],[9788,1545,854],[9790,1546,854],[9783,1556,865],[9782,1554,865],[9786,1548,851],[9788,1549,851],[9780,1561,854],[9779,1558,854],[9789,1548,870],[9788,1545,870],[9780,1557,851],[9782,1553,847],[9780,1557,866],[9732,1626,854],[9732,1626,863],[9783,1554,847],[9782,1557,851],[9735,1627,854],[9735,1627,863],[9791,1549,870],[9786,1556,847],[9783,1560,851],[9780,1558,866],[9780,1560,863],[9783,1560,866],[9780,1558,851],[9780,1560,854],[9753,1604,828],[9751,1602,828],[9750,1604,830],[9751,1605,830],[9754,1597,831],[9756,1597,831],[9754,1600,834],[9754,1598,834],[9750,1602,833],[9751,1604,833],[9754,1597,828],[9756,1598,828],[9754,1601,835],[9754,1601,827],[9751,1601,835],[9753,1600,827],[9756,1598,840],[9756,1597,843],[9758,1596,844],[9759,1593,844],[9760,1592,841],[9760,1593,838],[9758,1594,837],[9757,1597,837],[9754,1597,843],[9756,1594,844],[9759,1590,841],[9758,1593,844],[9754,1597,840],[9754,1597,837],[9757,1593,837],[9758,1592,838]],"transform":{"scale":[0.001,0.001,0.001],"translate":[0.56,0.64,7.579]},"appearance":{"materials":[{"name":"UUID_9bf2262a-054e-4054-b2b0-a0206db1a227","diffuseColor":[0.796875,0.0,0.0],"emissiveColor":[0.0,0.0,0.0],"specularColor":[1.0,1.0,1.0],"transparency":0.0},{"name":"UUID_8c940db7-0961-489c-84b6-9c87819fdf57","diffuseColor":[0.99609375,0.99609375,0.99609375],"emissiveColor":[0.0,0.0,0.0],"specularColor":[1.0,1.0,1.0],"transparency":0.0},{"name":"UUID_18756aa0-2d97-4399-b8cf-4c5499840cb7","diffuseColor":[0.19921875,0.19921875,0.19921875],"emissiveColor":[0.0,0.0,0.0],"specularColor":[1.0,1.0,1.0],"transparency":0.0},{"name":"UUID_79a9e956-70ed-4aa6-8883-3a2cb53166b0","diffuseColor":[0.49609375,0.49609375,0.49609375],"emissiveColor":[0.0,0.0,0.0],"specularColor":[1.0,1.0,1.0],"transparency":0.0},{"name":"UUID_c33c3767-e54d-4d3f-a5e5-c30608b2aeb5","diffuseColor":[0.0,0.296875,0.0],"emissiveColor":[0.0,0.0,0.0],"specularColor":[1.0,1.0,1.0],"transparency":0.0}]},"metadata":{"identifier":"340ea7fc-676a-4466-a346-0688a95f3419","referenceDate":"2021-08-10","geographicalExtent":[10.289000000000001,2.18,7.997999999999999,10.355,2.283,8.467]},"+metadata-extended":{"datasetCharacterSet":"UTF-8","datasetTopicCategory":"geoscientificInformation","distributionFormatVersion":"1.0","spatialRepresentationType":["vector"],"fileIdentifier":"ss.json","metadataStandard":"ISO 19115 - Geographic Information - Metadata","metadataStandardVersion":"ISO 19115:2014(E)","metadataCharacterSet":"UTF-8","metadataDateStamp":"2021-08-10","textures":"absent","materials":"present","cityfeatureMetadata":{"CityFurniture":{"uniqueFeatureCount":1,"aggregateFeatureCount":1,"presentLoDs":{"3":1}}},"presentLoDs":{"3":1},"thematicModels":["CityFurniture"]},"extensions":{"MetadataExtended":{"url":"https://raw.githubusercontent.com/cityjson/metadata-extended/0.5/metadata-extended.ext.json","version":"0.5"}}} -------------------------------------------------------------------------------- /tests/data/dummy/rectangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "CityObjects": 3 | { 4 | "id-1": 5 | { 6 | "geometry": 7 | [ 8 | { 9 | "boundaries": 10 | [ 11 | [ 12 | [ 13 | 0, 14 | 1, 15 | 2, 16 | 3 17 | ] 18 | ] 19 | ], 20 | "type": "MultiSurface", 21 | "lod": "1.0" 22 | } 23 | ], 24 | "type": "Building" 25 | } 26 | }, 27 | "version": "1.1", 28 | "type": "CityJSON", 29 | "vertices": 30 | [ 31 | [ 32 | 0, 33 | 0, 34 | 0 35 | ], 36 | [ 37 | 0, 38 | 1000, 39 | 0 40 | ], 41 | [ 42 | 1000, 43 | 1000, 44 | 0 45 | ], 46 | [ 47 | 1000, 48 | 0, 49 | 0 50 | ] 51 | ], 52 | "metadata": 53 | { 54 | "geographicalExtent": 55 | [ 56 | 0.0, 57 | 0.0, 58 | 0.0, 59 | 1.0, 60 | 1.0, 61 | 0.0 62 | ] 63 | }, 64 | "transform": 65 | { 66 | "scale": 67 | [ 68 | 0.001, 69 | 0.001, 70 | 0.001 71 | ], 72 | "translate": 73 | [ 74 | 0.0, 75 | 0.0, 76 | 0.0 77 | ] 78 | } 79 | } -------------------------------------------------------------------------------- /tests/data/empty.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityjson/cjio/35728539127f71b1d5ae458169ff84050e5e20ad/tests/data/empty.yaml -------------------------------------------------------------------------------- /tests/data/material/mt-1-triangulated.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"1.1","CityObjects":{"NL.IMBAG.Pand.0518100001755018":{"attributes":{"dak_type":"horizontal","data_area":0.6550000309944153,"data_coverage":0.07970529049634933,"documentnummer":"VMG20179462","geconstateerd":false,"gid":1080982,"h_dak_50p":2.0320000648498535,"h_dak_70p":2.0360000133514404,"h_dak_max":2.059999942779541,"h_dak_min":1.9859999418258667,"h_maaiveld":-0.48899999260902405,"identificatie":"NL.IMBAG.Pand.0518100001755018","kas_warenhuis":false,"lod11_replace":false,"ondergronds_type":"above ground","oorspronkelijkbouwjaar":1980,"pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","reconstruction_skipped":false,"rmse_lod12":0.025784239172935486,"rmse_lod13":0.04050220176577568,"rmse_lod22":0.019489331170916557,"rn":1,"status":"Pand in gebruik","t_run":40.32099914550781,"val3dity_codes_lod12":"[]","val3dity_codes_lod13":"[]","val3dity_codes_lod22":"[]","voorkomenidentificatie":1},"children":["NL.IMBAG.Pand.0518100001755018-0"],"type":"Building"},"NL.IMBAG.Pand.0518100001755018-0":{"attributes":{},"geometry":[{"boundaries":[[[[2,3,0]],[[0,1,2]],[[0,5,4]],[[4,1,0]],[[3,6,5]],[[5,0,3]],[[1,4,7]],[[7,2,1]],[[2,7,6]],[[6,3,2]],[[4,5,6]],[[6,7,4]]]],"lod":"1.2","semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"on_footprint_edge":true,"type":"WallSurface"},{"on_footprint_edge":false,"type":"WallSurface"}],"values":[[0,0,2,2,2,2,2,2,2,2,1,1]]},"type":"Solid","material":{"default":{"value":0}}}],"parents":["NL.IMBAG.Pand.0518100001755018"],"type":"BuildingPart"}},"appearance":{"materials":[{"name":"wall","ambientIntensity":0.4,"diffuseColor":[0.1,0.1,0.9],"emissiveColor":[0.1,0.1,0.9],"specularColor":[0.9,0.1,0.75],"shininess":0.0,"transparency":0.5,"isSmooth":true}]},"vertices":[[339152,-40239,-489],[336150,-39020,-489],[337104,-36670,-489],[340106,-37889,-489],[336150,-39020,2009],[339152,-40239,2009],[340106,-37889,2009],[337104,-36670,2009]],"transform":{"scale":[0.001,0.001,0.001],"translate":[76364.556,451840.245,0.0]},"metadata":{"geographicalExtent":[76700.70599999999,451800.006,-0.489,76704.662,451803.575,2.009]},"+metadata-extended":{"fileIdentifier":"mt-1-triangulate.json"},"extensions":{"MetadataExtended":{"url":"https://raw.githubusercontent.com/cityjson/metadata-extended/0.5/metadata-extended.ext.json","version":"0.5"}}} -------------------------------------------------------------------------------- /tests/data/material/mt-1.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"1.1","CityObjects":{"NL.IMBAG.Pand.0518100001755018":{"attributes":{"dak_type":"horizontal","data_area":0.6550000309944153,"data_coverage":0.07970529049634933,"documentnummer":"VMG20179462","geconstateerd":false,"gid":1080982,"h_dak_50p":2.0320000648498535,"h_dak_70p":2.0360000133514404,"h_dak_max":2.059999942779541,"h_dak_min":1.9859999418258667,"h_maaiveld":-0.48899999260902405,"identificatie":"NL.IMBAG.Pand.0518100001755018","kas_warenhuis":false,"lod11_replace":false,"ondergronds_type":"above ground","oorspronkelijkbouwjaar":1980,"pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","reconstruction_skipped":false,"rmse_lod12":0.025784239172935486,"rmse_lod13":0.04050220176577568,"rmse_lod22":0.019489331170916557,"rn":1,"status":"Pand in gebruik","t_run":40.32099914550781,"val3dity_codes_lod12":"[]","val3dity_codes_lod13":"[]","val3dity_codes_lod22":"[]","voorkomenidentificatie":1},"children":["NL.IMBAG.Pand.0518100001755018-0"],"type":"Building"},"NL.IMBAG.Pand.0518100001755018-0":{"attributes":{},"geometry":[{"boundaries":[[[[0,1,2,3]],[[4,1,0,5]],[[5,0,3,6]],[[7,2,1,4]],[[6,3,2,7]],[[6,7,4,5]]]],"lod":"1.2","semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"on_footprint_edge":true,"type":"WallSurface"},{"on_footprint_edge":false,"type":"WallSurface"}],"values":[[0,2,2,2,2,1]]},"type":"Solid","material":{"default":{"value":0}}}],"parents":["NL.IMBAG.Pand.0518100001755018"],"type":"BuildingPart"}},"appearance":{"materials":[{"name":"wall","ambientIntensity":0.4,"diffuseColor":[0.1,0.1,0.9],"emissiveColor":[0.1,0.1,0.9],"specularColor":[0.9,0.1,0.75],"shininess":0.0,"transparency":0.5,"isSmooth":true}]},"vertices":[[339152,-40239,-489],[336150,-39020,-489],[337104,-36670,-489],[340106,-37889,-489],[336150,-39020,2009],[339152,-40239,2009],[340106,-37889,2009],[337104,-36670,2009]],"transform":{"scale":[0.001,0.001,0.001],"translate":[76364.556,451840.245,0.0]},"metadata":{"geographicalExtent":[76700.70599999999,451800.006,-0.489,76704.662,451803.575,2.009]}} -------------------------------------------------------------------------------- /tests/data/material/mt-2-triangulated.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"1.1","CityObjects":{"NL.IMBAG.Pand.0518100001755019":{"attributes":{"dak_type":"horizontal","data_area":2.1875,"data_coverage":0.2661913335323334,"documentnummer":"VMG20179462","geconstateerd":false,"gid":1080983,"h_dak_50p":2.0309998989105225,"h_dak_70p":2.049999952316284,"h_dak_max":2.1110000610351562,"h_dak_min":1.9889999628067017,"h_maaiveld":-0.4860000014305115,"identificatie":"NL.IMBAG.Pand.0518100001755019","kas_warenhuis":false,"lod11_replace":false,"ondergronds_type":"above ground","oorspronkelijkbouwjaar":1980,"pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","reconstruction_skipped":false,"rmse_lod12":0.0315316803753376,"rmse_lod13":0.031793348491191864,"rmse_lod22":0.027490859851241112,"rn":1,"status":"Pand in gebruik","t_run":41.638999938964844,"val3dity_codes_lod12":"[]","val3dity_codes_lod13":"[]","val3dity_codes_lod22":"[]","voorkomenidentificatie":1},"children":["NL.IMBAG.Pand.0518100001755019-0"],"type":"Building"},"NL.IMBAG.Pand.0518100001755019-0":{"attributes":{},"geometry":[{"boundaries":[[[[2,3,0]],[[0,1,2]],[[0,5,4]],[[4,1,0]],[[3,6,5]],[[5,0,3]],[[1,4,7]],[[7,2,1]],[[2,7,6]],[[6,3,2]],[[4,5,6]],[[6,7,4]]]],"lod":"1.2","semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"on_footprint_edge":true,"type":"WallSurface"},{"on_footprint_edge":false,"type":"WallSurface"}],"values":[[0,0,2,2,2,2,2,2,2,2,1,1]]},"type":"Solid","material":{"color":{"values":[[0,0,0,0,0,0,0,0,0,0,0,0]]}}}],"parents":["NL.IMBAG.Pand.0518100001755019"],"type":"BuildingPart"}},"appearance":{"materials":[{"name":"roofandground","ambientIntensity":0.2,"diffuseColor":[0.9,0.1,0.75],"emissiveColor":[0.9,0.1,0.75],"specularColor":[0.9,0.1,0.75],"shininess":0.2,"transparency":0.5,"isSmooth":false}]},"vertices":[[338198,-42589,-486],[335196,-41370,-486],[336150,-39020,-486],[339152,-40239,-486],[335196,-41370,2041],[338198,-42589,2041],[339152,-40239,2041],[336150,-39020,2041]],"transform":{"scale":[0.001,0.001,0.001],"translate":[76364.556,451840.245,0.0]},"metadata":{"geographicalExtent":[76699.752,451797.656,-0.486,76703.708,451801.225,2.041]},"+metadata-extended":{"fileIdentifier":"mt-2-triangulated.json"},"extensions":{"MetadataExtended":{"url":"https://raw.githubusercontent.com/cityjson/metadata-extended/0.5/metadata-extended.ext.json","version":"0.5"}}} -------------------------------------------------------------------------------- /tests/data/material/mt-2.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"1.1","CityObjects":{"NL.IMBAG.Pand.0518100001755019":{"attributes":{"dak_type":"horizontal","data_area":2.1875,"data_coverage":0.2661913335323334,"documentnummer":"VMG20179462","geconstateerd":false,"gid":1080983,"h_dak_50p":2.0309998989105225,"h_dak_70p":2.049999952316284,"h_dak_max":2.1110000610351562,"h_dak_min":1.9889999628067017,"h_maaiveld":-0.4860000014305115,"identificatie":"NL.IMBAG.Pand.0518100001755019","kas_warenhuis":false,"lod11_replace":false,"ondergronds_type":"above ground","oorspronkelijkbouwjaar":1980,"pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","reconstruction_skipped":false,"rmse_lod12":0.0315316803753376,"rmse_lod13":0.031793348491191864,"rmse_lod22":0.027490859851241112,"rn":1,"status":"Pand in gebruik","t_run":41.638999938964844,"val3dity_codes_lod12":"[]","val3dity_codes_lod13":"[]","val3dity_codes_lod22":"[]","voorkomenidentificatie":1},"children":["NL.IMBAG.Pand.0518100001755019-0"],"type":"Building"},"NL.IMBAG.Pand.0518100001755019-0":{"attributes":{},"geometry":[{"boundaries":[[[[0,1,2,3]],[[4,1,0,5]],[[5,0,3,6]],[[7,2,1,4]],[[6,3,2,7]],[[6,7,4,5]]]],"lod":"1.2","semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"on_footprint_edge":true,"type":"WallSurface"},{"on_footprint_edge":false,"type":"WallSurface"}],"values":[[0,2,2,2,2,1]]},"type":"Solid","material":{"color":{"values":[[0,0,0,0,0,0]]}}}],"parents":["NL.IMBAG.Pand.0518100001755019"],"type":"BuildingPart"}},"appearance":{"materials":[{"name":"roofandground","ambientIntensity":0.2,"diffuseColor":[0.9,0.1,0.75],"emissiveColor":[0.9,0.1,0.75],"specularColor":[0.9,0.1,0.75],"shininess":0.2,"transparency":0.5,"isSmooth":false}]},"vertices":[[338198,-42589,-486],[335196,-41370,-486],[336150,-39020,-486],[339152,-40239,-486],[335196,-41370,2041],[338198,-42589,2041],[339152,-40239,2041],[336150,-39020,2041]],"transform":{"scale":[0.001,0.001,0.001],"translate":[76364.556,451840.245,0.0]},"metadata":{"geographicalExtent":[76699.752,451797.656,-0.486,76703.708,451801.225,2.041]}} -------------------------------------------------------------------------------- /tests/data/minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "CityJSON", 3 | "version": "1.1", 4 | "CityObjects": 5 | {}, 6 | "vertices": 7 | [], 8 | "transform": 9 | { 10 | "scale": 11 | [ 12 | 0.001, 13 | 0.001, 14 | 0.001 15 | ], 16 | "translate": 17 | [ 18 | 9000000000.0, 19 | 9000000000.0, 20 | 9000000000.0 21 | ] 22 | } 23 | } -------------------------------------------------------------------------------- /tests/data/multi_lod.json: -------------------------------------------------------------------------------- 1 | {"type":"CityJSON","version":"2.0","CityObjects":{"6751773":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[0,1,2]],[[2,1,3]],[[3,1,4]],[[5,1,6]],[[6,1,0]],[[7,3,8]],[[8,3,4]],[[6,0,9]],[[9,0,2]],[[8,4,5]],[[5,4,1]],[[9,2,7]],[[7,2,3]],[[9,7,5]],[[6,9,5]],[[5,7,8]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[0,1,2]],[[3,10,4]],[[4,10,11]],[[2,1,10]],[[10,1,11]],[[12,13,14]],[[14,13,1]],[[1,13,11]],[[14,1,15]],[[15,1,0]],[[16,17,12]],[[12,17,13]],[[18,3,19]],[[19,3,4]],[[20,2,17]],[[16,20,17]],[[17,2,10]],[[15,0,20]],[[20,0,2]],[[19,4,13]],[[13,4,11]],[[17,10,18]],[[18,10,3]],[[14,15,20]],[[12,14,16]],[[16,14,20]],[[17,18,19]],[[13,17,19]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[0,21,2]],[[4,10,11]],[[10,22,1]],[[3,10,4]],[[22,21,1]],[[2,21,22]],[[10,1,11]],[[23,24,25]],[[25,24,1]],[[1,24,11]],[[25,1,21]],[[26,25,21]],[[26,21,27]],[[27,21,0]],[[28,29,24]],[[23,28,24]],[[30,3,4]],[[31,30,4]],[[32,22,29]],[[28,32,29]],[[29,22,10]],[[27,0,2]],[[33,27,2]],[[33,2,22]],[[32,33,22]],[[31,4,11]],[[24,31,11]],[[29,10,30]],[[30,10,3]],[[33,32,26]],[[27,33,26]],[[25,28,23]],[[26,32,25]],[[25,32,28]],[[29,30,31]],[[24,29,31]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,0,0,0,2,2,2,2,2,2,2,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1]]}}],"attributes":{"fid":730210,"identificatie":"NL.IMBAG.Pand.0796100000236261","oorspronkelijk_bouwjaar":1965,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2019-01-09","documentnummer":"8618264","voorkomenidentificatie":2,"begingeldigheid":"2019-01-09","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2019-01-09T16:00:26.607000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":5.254000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"2128302":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[34,35,36]],[[34,37,35]],[[34,36,38]],[[39,37,40]],[[40,37,34]],[[40,34,41]],[[41,34,38]],[[42,36,43]],[[43,36,35]],[[41,38,42]],[[42,38,36]],[[43,35,39]],[[39,35,37]],[[40,43,39]],[[41,42,40]],[[40,42,43]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[34,35,36]],[[34,37,35]],[[34,36,38]],[[44,37,45]],[[45,37,34]],[[45,34,46]],[[46,34,38]],[[47,36,48]],[[48,36,35]],[[46,38,47]],[[47,38,36]],[[48,35,44]],[[44,35,37]],[[45,47,48]],[[45,46,47]],[[45,48,44]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[34,35,49]],[[34,37,35]],[[34,49,50]],[[49,36,50]],[[36,38,50]],[[51,37,34]],[[52,51,34]],[[52,34,50]],[[53,52,50]],[[54,49,55]],[[55,49,35]],[[56,38,36]],[[57,56,36]],[[53,50,56]],[[56,50,38]],[[57,36,49]],[[54,57,49]],[[55,35,37]],[[51,55,37]],[[57,54,53]],[[56,57,53]],[[55,51,52]],[[54,52,53]],[[54,55,52]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1]]}}],"attributes":{"fid":13745293,"identificatie":"NL.IMBAG.Pand.0796100000215775","oorspronkelijk_bouwjaar":1963,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2020-11-13","documentnummer":"NL-HtGH-VERSEON-10534131","voorkomenidentificatie":2,"begingeldigheid":"2020-11-13","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2020-11-13T16:07:01.477000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":4.706000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"596872":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[58,59,60]],[[61,58,62]],[[61,63,58]],[[58,63,59]],[[64,58,65]],[[65,58,60]],[[66,63,67]],[[67,63,61]],[[65,60,68]],[[68,60,59]],[[69,62,64]],[[64,62,58]],[[68,59,66]],[[66,59,63]],[[67,61,69]],[[69,61,62]],[[68,66,64]],[[65,68,64]],[[69,64,67]],[[64,66,67]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[58,59,60]],[[61,58,62]],[[61,63,58]],[[58,63,59]],[[70,58,71]],[[71,58,60]],[[72,63,73]],[[73,63,61]],[[71,60,74]],[[74,60,59]],[[75,62,70]],[[70,62,58]],[[74,59,72]],[[72,59,63]],[[73,61,75]],[[75,61,62]],[[70,73,75]],[[74,70,71]],[[74,72,70]],[[70,72,73]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[63,59,76]],[[58,76,60]],[[77,63,76]],[[77,76,58]],[[78,58,79]],[[61,78,62]],[[77,58,78]],[[78,79,62]],[[80,58,60]],[[81,80,60]],[[82,63,77]],[[83,84,82]],[[82,84,63]],[[85,86,87]],[[87,86,59]],[[59,86,76]],[[82,77,78]],[[88,82,78]],[[89,79,80]],[[80,79,58]],[[87,59,84]],[[84,59,63]],[[83,82,86]],[[85,83,86]],[[81,60,86]],[[86,60,76]],[[88,78,90]],[[90,78,61]],[[90,61,91]],[[91,61,62]],[[91,62,79]],[[89,91,79]],[[88,89,80]],[[86,80,81]],[[82,88,80]],[[82,80,86]],[[90,91,88]],[[88,91,89]],[[87,84,85]],[[85,84,83]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1]]}}],"attributes":{"fid":8759559,"identificatie":"NL.IMBAG.Pand.0796100001008481","oorspronkelijk_bouwjaar":2017,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2018-02-08","documentnummer":"7704563","voorkomenidentificatie":2,"begingeldigheid":"2018-02-08","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2018-02-08T10:01:08.773000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":4.691000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"uncertain","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"408703":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[92,93,94]],[[95,96,97]],[[96,94,93]],[[97,96,93]],[[98,94,96]],[[99,98,100]],[[100,98,96]],[[101,92,102]],[[102,92,94]],[[103,97,104]],[[104,97,93]],[[104,93,101]],[[101,93,92]],[[102,94,99]],[[99,94,98]],[[105,95,103]],[[103,95,97]],[[100,96,105]],[[105,96,95]],[[99,100,102]],[[104,100,103]],[[103,100,105]],[[104,102,100]],[[101,102,104]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[92,93,94]],[[95,96,97]],[[96,94,93]],[[97,96,93]],[[98,94,96]],[[106,98,107]],[[107,98,96]],[[108,92,109]],[[109,92,94]],[[110,97,111]],[[111,97,93]],[[111,93,108]],[[108,93,92]],[[109,94,106]],[[106,94,98]],[[112,95,110]],[[110,95,97]],[[107,96,112]],[[112,96,95]],[[106,107,109]],[[111,107,110]],[[110,107,112]],[[111,109,107]],[[108,109,111]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[92,93,94]],[[95,96,97]],[[96,94,93]],[[97,96,93]],[[98,94,96]],[[113,98,96]],[[114,113,96]],[[115,92,116]],[[116,92,94]],[[117,97,118]],[[118,97,93]],[[118,93,115]],[[115,93,92]],[[116,94,98]],[[113,116,98]],[[119,95,97]],[[117,119,97]],[[114,96,95]],[[119,114,95]],[[113,114,116]],[[118,114,117]],[[117,114,119]],[[118,116,114]],[[115,116,118]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1]]}}],"attributes":{"fid":6348664,"identificatie":"NL.IMBAG.Pand.0796100000210250","oorspronkelijk_bouwjaar":1990,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2010-04-20","documentnummer":"10.415","voorkomenidentificatie":1,"begingeldigheid":"2010-04-20","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2010-11-10T01:00:50.071000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":5.691000,"dak_type":"single horizontal","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"2499572":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[120,121,122]],[[123,120,122]],[[124,120,125]],[[125,120,123]],[[126,122,127]],[[127,122,121]],[[127,121,124]],[[124,121,120]],[[125,123,126]],[[126,123,122]],[[125,126,124]],[[124,126,127]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[120,121,122]],[[123,120,122]],[[128,120,129]],[[129,120,123]],[[130,122,131]],[[131,122,121]],[[131,121,128]],[[128,121,120]],[[129,123,130]],[[130,123,122]],[[129,130,128]],[[128,130,131]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[120,132,123]],[[122,133,121]],[[133,123,132]],[[121,133,132]],[[134,133,135]],[[135,133,122]],[[136,132,137]],[[137,132,120]],[[137,120,138]],[[138,120,123]],[[135,122,139]],[[139,122,121]],[[139,121,132]],[[136,139,132]],[[138,123,133]],[[134,138,133]],[[134,135,139]],[[136,134,139]],[[134,136,137]],[[138,134,137]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1]]}}],"attributes":{"fid":14518465,"identificatie":"NL.IMBAG.Pand.0796100000226180","oorspronkelijk_bouwjaar":1948,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2009-10-06","documentnummer":"09.0875","voorkomenidentificatie":1,"begingeldigheid":"2009-10-06","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2010-11-10T00:01:37.012000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":4.611000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"3374155":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[140,141,142]],[[143,140,142]],[[144,143,145]],[[145,143,142]],[[146,141,147]],[[147,141,140]],[[147,140,144]],[[144,140,143]],[[145,142,146]],[[146,142,141]],[[147,144,145]],[[146,147,145]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[140,141,148]],[[143,149,142]],[[148,141,149]],[[148,149,143]],[[150,143,151]],[[151,143,142]],[[152,141,153]],[[153,141,140]],[[154,155,156]],[[156,155,157]],[[153,140,155]],[[154,153,155]],[[155,140,148]],[[156,157,152]],[[152,157,141]],[[141,157,149]],[[155,148,150]],[[150,148,143]],[[151,142,157]],[[157,142,149]],[[151,157,150]],[[150,157,155]],[[153,154,152]],[[152,154,156]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[140,141,158]],[[148,158,149]],[[159,140,158]],[[143,149,142]],[[148,149,143]],[[159,158,148]],[[160,159,161]],[[162,160,161]],[[161,159,148]],[[163,164,165]],[[165,164,158]],[[158,164,149]],[[166,143,142]],[[167,166,142]],[[168,141,169]],[[169,141,140]],[[162,161,164]],[[163,162,164]],[[170,171,172]],[[173,174,172]],[[169,140,159]],[[160,169,159]],[[165,158,168]],[[168,158,141]],[[175,176,170]],[[170,176,171]],[[177,178,174]],[[173,177,174]],[[161,148,166]],[[166,148,143]],[[167,142,149]],[[164,167,149]],[[179,180,181]],[[181,180,182]],[[181,182,177]],[[177,182,178]],[[183,184,179]],[[179,184,180]],[[176,175,185]],[[184,183,185]],[[167,164,166]],[[166,164,161]],[[165,168,170]],[[170,168,175]],[[175,168,169]],[[175,169,160]],[[172,165,170]],[[162,182,180]],[[178,163,174]],[[172,174,165]],[[162,178,182]],[[174,163,165]],[[162,163,178]],[[180,184,185]],[[160,180,185]],[[160,185,175]],[[162,180,160]],[[171,176,185]],[[172,171,183]],[[183,171,185]],[[181,183,179]],[[172,183,181]],[[173,181,177]],[[172,181,173]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,3,3,3,3,2,2,2,2,3,3,3,3,2,2,2,2,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]}}],"attributes":{"fid":14026539,"identificatie":"NL.IMBAG.Pand.0796100000256407","oorspronkelijk_bouwjaar":1965,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2014-03-10","documentnummer":"3749388","voorkomenidentificatie":4,"begingeldigheid":"2014-03-10","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2014-03-18T10:34:14.729000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":5.194000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"7115146":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[186,187,188]],[[189,186,188]],[[190,186,191]],[[191,186,189]],[[191,189,192]],[[192,189,188]],[[193,187,190]],[[190,187,186]],[[192,188,193]],[[193,188,187]],[[191,192,190]],[[190,192,193]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[186,187,188]],[[189,186,188]],[[194,189,195]],[[195,189,188]],[[196,187,197]],[[197,187,186]],[[197,186,194]],[[194,186,189]],[[195,188,196]],[[196,188,187]],[[197,194,195]],[[196,197,195]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[186,187,198]],[[199,198,188]],[[199,186,198]],[[199,188,189]],[[200,199,201]],[[201,199,189]],[[202,188,198]],[[203,202,198]],[[201,189,202]],[[202,189,188]],[[204,187,205]],[[205,187,186]],[[205,186,199]],[[200,205,199]],[[203,198,204]],[[204,198,187]],[[201,202,200]],[[200,202,203]],[[203,204,200]],[[200,204,205]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1]]}}],"attributes":{"fid":6348693,"identificatie":"NL.IMBAG.Pand.0796100000210275","oorspronkelijk_bouwjaar":1956,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2002-03-05","documentnummer":"SOB0200300","voorkomenidentificatie":1,"begingeldigheid":"2002-03-05","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2010-11-10T01:00:49.697000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":5.790000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"3194274":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[206,207,208]],[[208,207,209]],[[210,208,211]],[[211,208,209]],[[212,207,213]],[[213,207,206]],[[213,206,210]],[[210,206,208]],[[211,209,212]],[[212,209,207]],[[211,212,210]],[[210,212,213]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[206,207,208]],[[208,207,209]],[[214,208,215]],[[215,208,209]],[[216,207,217]],[[217,207,206]],[[217,206,214]],[[214,206,208]],[[215,209,216]],[[216,209,207]],[[215,216,214]],[[214,216,217]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[206,207,208]],[[208,207,209]],[[218,208,209]],[[219,218,209]],[[220,207,221]],[[221,207,206]],[[221,206,208]],[[218,221,208]],[[219,209,220]],[[220,209,207]],[[219,220,218]],[[218,220,221]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"}],"values":[[0,0,2,2,2,2,2,2,2,2,1,1]]}}],"attributes":{"fid":207623,"identificatie":"NL.IMBAG.Pand.0796100000265918","oorspronkelijk_bouwjaar":1985,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2010-04-20","documentnummer":"10.415","voorkomenidentificatie":1,"begingeldigheid":"2010-04-20","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2010-11-09T23:00:31.848000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":4.208000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"2921895":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[222,223,224]],[[224,223,225]],[[226,227,228]],[[222,224,226]],[[222,226,229]],[[226,224,227]],[[226,230,229]],[[222,229,231]],[[232,229,233]],[[233,229,230]],[[234,226,235]],[[235,226,228]],[[236,225,237]],[[237,225,223]],[[238,231,232]],[[232,231,229]],[[239,222,238]],[[238,222,231]],[[233,230,234]],[[234,230,226]],[[240,227,241]],[[241,227,224]],[[235,228,240]],[[240,228,227]],[[237,223,239]],[[239,223,222]],[[241,224,236]],[[236,224,225]],[[236,237,241]],[[233,234,232]],[[241,234,240]],[[240,234,235]],[[241,239,234]],[[237,239,241]],[[239,232,234]],[[238,232,239]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[225,224,223]],[[227,226,224]],[[223,224,242]],[[242,224,226]],[[227,228,226]],[[226,229,222]],[[242,226,222]],[[229,231,222]],[[230,229,226]],[[243,229,244]],[[244,229,230]],[[245,242,246]],[[246,242,222]],[[247,248,249]],[[249,248,228]],[[228,248,226]],[[250,225,251]],[[251,225,223]],[[252,231,243]],[[243,231,229]],[[246,222,252]],[[252,222,231]],[[244,230,248]],[[248,230,226]],[[253,227,254]],[[254,227,224]],[[249,228,253]],[[253,228,227]],[[255,245,247]],[[247,245,248]],[[251,223,245]],[[255,251,245]],[[245,223,242]],[[254,224,250]],[[250,224,225]],[[244,248,243]],[[243,248,246]],[[246,248,245]],[[243,246,252]],[[251,255,254]],[[253,247,249]],[[253,254,247]],[[250,251,254]],[[254,255,247]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[256,224,257]],[[256,225,224]],[[224,226,257]],[[223,225,256]],[[226,224,227]],[[242,257,226]],[[227,228,226]],[[226,229,222]],[[242,226,222]],[[229,231,222]],[[230,229,226]],[[258,229,230]],[[259,258,230]],[[260,242,222]],[[261,260,222]],[[262,257,260]],[[263,262,260]],[[260,257,242]],[[264,265,266]],[[266,265,228]],[[228,265,226]],[[267,225,268]],[[268,225,223]],[[269,231,229]],[[258,269,229]],[[261,222,231]],[[269,261,231]],[[259,230,265]],[[265,230,226]],[[270,271,272]],[[273,270,272]],[[274,227,272]],[[272,227,224]],[[275,256,262]],[[276,275,262]],[[262,256,257]],[[276,262,277]],[[277,262,278]],[[266,228,227]],[[274,266,227]],[[279,280,271]],[[270,279,271]],[[277,278,280]],[[279,277,280]],[[263,260,265]],[[264,263,265]],[[268,223,256]],[[275,268,256]],[[273,272,267]],[[267,224,225]],[[267,272,224]],[[259,265,258]],[[258,265,261]],[[261,265,260]],[[258,261,269]],[[278,262,263]],[[278,263,264]],[[274,272,280]],[[278,264,266]],[[274,278,266]],[[280,278,274]],[[271,280,272]],[[273,267,270]],[[270,268,275]],[[270,267,268]],[[276,277,275]],[[275,279,270]],[[275,277,279]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,2,2,2,2,2,3,3,2,2,3,3,3,3,3,3,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]}}],"attributes":{"fid":738411,"identificatie":"NL.IMBAG.Pand.0796100000242693","oorspronkelijk_bouwjaar":1961,"status":"Pand in gebruik (niet ingemeten)","geconstateerd":false,"documentdatum":"2019-01-03","documentnummer":"8600475","voorkomenidentificatie":2,"begingeldigheid":"2019-01-03","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2019-01-06T22:00:22.627000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":5.207000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}},"8049533":{"type":"Building","geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[281,282,283]],[[284,281,285]],[[284,286,281]],[[281,286,282]],[[287,282,288]],[[288,282,286]],[[289,281,290]],[[290,281,283]],[[291,284,292]],[[292,284,285]],[[288,286,291]],[[291,286,284]],[[290,283,287]],[[287,283,282]],[[292,285,289]],[[289,285,281]],[[289,290,287]],[[289,287,288]],[[289,288,291]],[[292,289,291]]]]},{"type":"Solid","lod":"1.3","boundaries":[[[[281,282,283]],[[293,281,294]],[[293,286,281]],[[281,286,282]],[[284,294,285]],[[293,294,284]],[[295,282,296]],[[296,282,286]],[[297,281,298]],[[298,281,283]],[[299,284,300]],[[300,284,285]],[[300,285,301]],[[301,285,294]],[[302,303,304]],[[304,303,301]],[[303,293,299]],[[299,293,284]],[[296,286,303]],[[302,296,303]],[[303,286,293]],[[298,283,295]],[[295,283,282]],[[304,301,297]],[[297,301,281]],[[281,301,294]],[[299,300,301]],[[303,299,301]],[[298,295,297]],[[297,296,302]],[[297,295,296]],[[297,302,304]]]]},{"type":"Solid","lod":"2.2","boundaries":[[[[286,282,305]],[[281,306,283]],[[305,282,306]],[[305,306,281]],[[284,293,294]],[[284,294,285]],[[293,281,294]],[[305,281,293]],[[307,283,306]],[[308,307,306]],[[309,282,310]],[[310,282,286]],[[311,281,283]],[[307,311,283]],[[312,284,313]],[[313,284,285]],[[313,285,294]],[[314,313,294]],[[315,305,316]],[[316,305,317]],[[317,305,293]],[[316,317,318]],[[318,317,314]],[[317,293,312]],[[312,293,284]],[[310,286,305]],[[315,310,305]],[[308,306,309]],[[309,306,282]],[[318,314,311]],[[311,314,281]],[[281,314,294]],[[312,313,314]],[[317,312,314]],[[308,309,315]],[[315,309,310]],[[311,307,308]],[[311,308,315]],[[311,315,316]],[[318,311,316]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"type":"WallSurface"},{"type":"WallSurface"}],"values":[[0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1]]}}],"attributes":{"fid":17752313,"identificatie":"NL.IMBAG.Pand.0796100000245196","oorspronkelijk_bouwjaar":1985,"status":"Pand in gebruik","geconstateerd":false,"documentdatum":"2010-04-20","documentnummer":"10.415","voorkomenidentificatie":1,"begingeldigheid":"2010-04-20","eindgeldigheid":null,"tijdstipinactief":null,"tijdstipregistratielv":"2010-11-09T23:31:24.334000+01:00","tijdstipeindregistratielv":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"h_maaiveld":4.702000,"dak_type":"slanted","pw_datum":"2016-12-01","pw_actueel":"yes","pw_bron":"ahn3","reconstructie_methode":"tudelft3d-geoflow","versie_methode":"v21.03.1","kas_warenhuis":false,"ondergronds_type":"above ground","lod11_replace":false,"reconstruction_skipped":false}}},"vertices":[[410422,289580,2553],[416340,292700,2553],[413729,283318,2553],[423629,288481,2553],[420433,294788,2553],[416340,292700,9271],[410422,289580,9271],[423629,288481,9271],[420433,294788,9271],[413729,283318,9271],[419780,286474,2553],[416411,292736,2553],[416411,292736,9838],[416411,292736,5609],[416340,292700,9838],[410422,289580,9838],[419780,286474,9838],[419780,286474,5609],[423629,288481,5609],[420433,294788,5609],[413729,283318,9838],[413202,291046,2553],[416509,284768,2553],[416411,292736,7846],[416411,292736,5662],[416340,292700,7904],[413202,291046,10506],[410422,289580,8197],[419780,286474,7817],[419780,286474,5639],[423629,288481,5504],[420433,294788,5521],[416509,284768,10523],[413729,283318,8220],[531023,157704,2005],[536071,163168,2005],[540462,158599,2005],[530779,157956,2005],[535127,153398,2005],[530779,157956,9188],[531023,157704,9188],[535127,153398,9188],[540462,158599,9188],[536071,163168,9188],[530779,157956,9215],[531023,157704,9215],[535127,153398,9215],[540462,158599,9215],[536071,163168,9215],[538420,160723,2005],[533092,155534,2005],[530779,157956,7471],[531023,157704,7733],[533092,155534,9977],[538420,160723,9982],[536071,163168,7445],[535127,153398,7685],[540462,158599,7693],[291809,52321,1990],[292284,59308,1990],[294874,53757,1990],[288587,45263,1990],[293930,47753,1990],[283886,55387,1990],[291809,52321,7097],[294874,53757,7097],[283886,55387,7097],[288587,45263,7097],[292284,59308,7097],[293930,47753,7097],[291809,52321,6089],[294874,53757,6089],[283886,55387,6089],[288587,45263,6089],[292284,59308,6089],[293930,47753,6089],[293354,57015,1990],[284994,53001,1990],[286634,49469,1990],[291953,52011,1990],[291809,52321,8351],[294874,53757,8367],[284994,53001,5111],[284994,53001,6068],[283886,55387,5977],[293354,57015,6159],[293354,57015,5073],[292284,59308,6072],[286634,49469,8678],[291953,52011,8664],[288587,45263,4731],[293930,47753,4668],[100552,207962,2990],[101462,208090,2990],[101142,204000,2990],[106114,204858,2990],[105894,204828,2990],[106412,208790,2990],[105880,204669,2990],[105880,204669,5785],[105894,204828,5785],[100552,207962,5785],[101142,204000,5785],[106412,208790,5785],[101462,208090,5785],[106114,204858,5785],[105880,204669,5784],[105894,204828,5784],[100552,207962,5784],[101142,204000,5784],[106412,208790,5784],[101462,208090,5784],[106114,204858,5784],[105880,204669,5774],[105894,204828,5774],[100552,207962,5786],[101142,204000,5768],[106412,208790,5793],[101462,208090,5787],[106114,204858,5775],[318428,78782,1910],[322204,80102,1910],[325505,70663,1910],[321729,69342,1910],[318428,78782,6362],[321729,69342,6362],[325505,70663,6362],[322204,80102,6362],[318428,78782,6527],[321729,69342,6527],[325505,70663,6527],[322204,80102,6527],[320005,79333,1910],[323331,69902,1910],[323331,69902,7147],[325505,70663,4464],[320005,79333,7164],[318428,78782,5013],[321729,69342,4961],[322204,80102,4451],[409317,251365,2493],[412438,245461,2493],[403642,240835,2493],[400523,246741,2493],[400523,246741,9445],[403642,240835,9445],[412438,245461,9445],[409317,251365,9445],[402968,248026,2493],[406036,242094,2493],[400523,246741,5484],[403642,240835,5484],[412438,245461,9379],[409317,251365,9379],[402968,248026,9379],[402968,248026,5484],[406036,242094,9379],[406036,242094,5484],[409257,243788,2493],[406140,249694,2493],[406140,249694,10468],[402968,248026,5452],[402968,248026,7774],[406036,242094,7747],[406036,242094,5498],[409257,243788,10482],[400523,246741,5452],[403642,240835,5497],[412438,245461,7839],[409317,251365,7828],[408781,244690,10480],[408781,244690,10290],[408558,244577,10292],[406974,243775,10309],[406974,243775,8958],[407223,247643,10473],[407223,247643,10300],[405671,245672,10319],[405671,245672,8752],[406301,246876,10310],[406301,246876,9591],[405868,245923,10316],[405868,245923,8971],[406485,246706,10309],[406485,246706,9655],[407068,247447,10301],[303521,387014,3089],[310142,390536,3089],[313063,385045,3089],[306442,381522,3089],[303521,387014,8114],[306442,381522,8114],[313063,385045,8114],[310142,390536,8114],[306442,381522,8156],[313063,385045,8156],[310142,390536,8156],[303521,387014,8156],[311582,387829,3089],[304987,384257,3089],[304987,384257,9090],[306442,381522,6019],[313063,385045,5967],[311582,387829,9093],[310142,390536,6062],[303521,387014,6002],[237572,568900,1507],[240368,570226,1507],[238832,566230,1507],[241657,567575,1507],[238832,566230,4909],[241657,567575,4909],[240368,570226,4909],[237572,568900,4909],[238832,566230,4920],[241657,567575,4920],[240368,570226,4920],[237572,568900,4920],[238832,566230,5342],[241657,567575,5348],[240368,570226,3957],[237572,568900,3949],[566806,244864,2506],[563315,250254,2506],[571929,251100,2506],[569733,254314,2506],[573288,249111,2506],[572194,251268,2506],[573476,249229,2506],[571272,243350,2506],[575435,245968,2506],[568816,241760,2506],[571272,243350,9868],[575435,245968,9868],[573288,249111,9868],[573476,249229,9868],[569733,254314,9868],[563315,250254,9868],[568816,241760,9868],[566806,244864,9868],[572194,251268,9868],[571929,251100,9868],[566730,244982,2506],[571272,243350,5168],[575435,245968,5168],[566730,244982,5168],[566806,244864,5168],[573288,249111,10185],[573288,249111,5168],[573476,249229,10185],[569733,254314,10185],[563315,250254,10185],[568816,241760,5168],[572194,251268,10185],[571929,251100,10185],[566730,244982,10185],[565431,246987,2506],[566223,245764,2506],[571272,243350,5156],[575435,245968,5180],[566730,244982,5142],[566806,244864,5142],[566223,245764,8778],[566730,244982,8074],[573288,249111,8072],[573288,249111,5179],[573476,249229,8072],[569733,254314,8131],[563315,250254,8127],[568816,241760,5142],[571720,250968,11079],[571720,250968,9893],[571929,251100,9893],[571929,251100,11079],[572194,251268,9894],[565431,246987,11076],[566223,245764,10131],[572178,249478,10102],[572178,249478,8755],[571721,250626,10890],[571721,250626,9673],[214082,87180,2001],[213876,96957,2001],[217060,88254,2001],[212480,84977,2001],[214602,85740,2001],[208674,95050,2001],[213876,96957,10086],[208674,95050,10086],[214082,87180,10086],[217060,88254,10086],[212480,84977,10086],[214602,85740,10086],[212155,85838,2001],[214302,86571,2001],[213876,96957,10286],[208674,95050,10286],[214082,87180,10286],[217060,88254,10286],[212480,84977,5518],[214602,85740,5518],[214302,86571,5518],[212155,85838,10286],[212155,85838,5518],[214302,86571,10286],[210118,91228,2001],[215285,93106,2001],[217060,88254,7136],[215285,93106,11286],[213876,96957,8030],[208674,95050,8029],[214082,87180,7135],[212480,84977,4977],[214602,85740,4968],[214302,86571,5702],[210118,91228,11272],[212155,85838,6645],[212155,85838,5741],[214302,86571,6615]],"transform":{"scale":[0.001000,0.001000,0.001000],"translate":[153200.847921,414118.209990,2.701000]}} -------------------------------------------------------------------------------- /tests/data/rotterdam/appearances/0320_4_15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityjson/cjio/35728539127f71b1d5ae458169ff84050e5e20ad/tests/data/rotterdam/appearances/0320_4_15.jpg -------------------------------------------------------------------------------- /tests/data/rotterdam/rotterdam_one.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "CityJSON", 3 | "version": "1.1", 4 | "CityObjects": 5 | { 6 | "{CD98680D-A8DD-4106-A18E-15EE2A908D75}": 7 | { 8 | "type": "Building", 9 | "attributes": 10 | { 11 | "TerrainHeight": 2.93, 12 | "bron_tex": "UltraCAM-X 10cm juni 2008", 13 | "voll_tex": "complete", 14 | "bron_geo": "Lidar 15-30 punten - nov. 2008", 15 | "status": "1" 16 | }, 17 | "geometry": 18 | [ 19 | { 20 | "type": "MultiSurface", 21 | "boundaries": 22 | [ 23 | [ 24 | [ 25 | 0, 26 | 1, 27 | 2, 28 | 3, 29 | 4, 30 | 5, 31 | 6 32 | ] 33 | ], 34 | [ 35 | [ 36 | 7, 37 | 8, 38 | 8, 39 | 9, 40 | 10 41 | ] 42 | ], 43 | [ 44 | [ 45 | 11, 46 | 12, 47 | 13, 48 | 14 49 | ] 50 | ], 51 | [ 52 | [ 53 | 15, 54 | 16, 55 | 17, 56 | 18 57 | ] 58 | ], 59 | [ 60 | [ 61 | 1, 62 | 0, 63 | 19, 64 | 15 65 | ] 66 | ], 67 | [ 68 | [ 69 | 0, 70 | 6, 71 | 20, 72 | 19 73 | ] 74 | ], 75 | [ 76 | [ 77 | 6, 78 | 5, 79 | 12, 80 | 11 81 | ] 82 | ], 83 | [ 84 | [ 85 | 5, 86 | 4, 87 | 13, 88 | 12 89 | ] 90 | ], 91 | [ 92 | [ 93 | 3, 94 | 2, 95 | 7, 96 | 10 97 | ] 98 | ], 99 | [ 100 | [ 101 | 2, 102 | 1, 103 | 21, 104 | 22 105 | ] 106 | ], 107 | [ 108 | [ 109 | 9, 110 | 8, 111 | 18, 112 | 17 113 | ] 114 | ], 115 | [ 116 | [ 117 | 8, 118 | 8, 119 | 18, 120 | 18 121 | ] 122 | ], 123 | [ 124 | [ 125 | 8, 126 | 7, 127 | 23, 128 | 24 129 | ] 130 | ], 131 | [ 132 | [ 133 | 11, 134 | 14, 135 | 16, 136 | 20 137 | ] 138 | ] 139 | ], 140 | "semantics": 141 | { 142 | "values": 143 | [ 144 | 0, 145 | 0, 146 | 0, 147 | 1, 148 | 2, 149 | 2, 150 | 2, 151 | 2, 152 | 2, 153 | 2, 154 | 2, 155 | 2, 156 | 2, 157 | 2 158 | ], 159 | "surfaces": 160 | [ 161 | { 162 | "type": "RoofSurface" 163 | }, 164 | { 165 | "type": "GroundSurface" 166 | }, 167 | { 168 | "type": "WallSurface" 169 | } 170 | ] 171 | }, 172 | "texture": 173 | { 174 | "rgbTexture": 175 | { 176 | "values": 177 | [ 178 | [ 179 | [ 180 | 0, 181 | 0, 182 | 1, 183 | 2, 184 | 3, 185 | 4, 186 | 5, 187 | 6 188 | ] 189 | ], 190 | [ 191 | [ 192 | 1, 193 | 7, 194 | 8, 195 | 8, 196 | 9, 197 | 10 198 | ] 199 | ], 200 | [ 201 | [ 202 | 2, 203 | 11, 204 | 12, 205 | 13, 206 | 14 207 | ] 208 | ], 209 | [ 210 | [ 211 | null 212 | ] 213 | ], 214 | [ 215 | [ 216 | 1, 217 | 15, 218 | 16, 219 | 17, 220 | 18 221 | ] 222 | ], 223 | [ 224 | [ 225 | 1, 226 | 19, 227 | 20, 228 | 21, 229 | 22 230 | ] 231 | ], 232 | [ 233 | [ 234 | 3, 235 | 23, 236 | 24, 237 | 25, 238 | 26 239 | ] 240 | ], 241 | [ 242 | [ 243 | 2, 244 | 27, 245 | 28, 246 | 29, 247 | 30 248 | ] 249 | ], 250 | [ 251 | [ 252 | 1, 253 | 31, 254 | 32, 255 | 33, 256 | 34 257 | ] 258 | ], 259 | [ 260 | [ 261 | 2, 262 | 35, 263 | 36, 264 | 37, 265 | 38 266 | ] 267 | ], 268 | [ 269 | [ 270 | 4, 271 | 39, 272 | 40, 273 | 41, 274 | 42 275 | ] 276 | ], 277 | [ 278 | [ 279 | 5, 280 | 43, 281 | 43, 282 | 44, 283 | 44 284 | ] 285 | ], 286 | [ 287 | [ 288 | 3, 289 | 45, 290 | 46, 291 | 47, 292 | 48 293 | ] 294 | ], 295 | [ 296 | [ 297 | 4, 298 | 49, 299 | 50, 300 | 51, 301 | 52 302 | ] 303 | ] 304 | ] 305 | } 306 | }, 307 | "lod": "2" 308 | } 309 | ] 310 | } 311 | }, 312 | "vertices": 313 | [ 314 | [ 315 | 524038, 316 | 209058, 317 | 15311 318 | ], 319 | [ 320 | 523657, 321 | 208741, 322 | 15311 323 | ], 324 | [ 325 | 527714, 326 | 203869, 327 | 15311 328 | ], 329 | [ 330 | 532519, 331 | 207703, 332 | 15311 333 | ], 334 | [ 335 | 529417, 336 | 211476, 337 | 15311 338 | ], 339 | [ 340 | 525121, 341 | 207919, 342 | 15311 343 | ], 344 | [ 345 | 524122, 346 | 209126, 347 | 15311 348 | ], 349 | [ 350 | 527714, 351 | 203869, 352 | 11136 353 | ], 354 | [ 355 | 529970, 356 | 201158, 357 | 11136 358 | ], 359 | [ 360 | 534759, 361 | 204979, 362 | 11136 363 | ], 364 | [ 365 | 532519, 366 | 207703, 367 | 11136 368 | ], 369 | [ 370 | 524122, 371 | 209126, 372 | 12869 373 | ], 374 | [ 375 | 525121, 376 | 207919, 377 | 12869 378 | ], 379 | [ 380 | 529417, 381 | 211476, 382 | 12869 383 | ], 384 | [ 385 | 528421, 386 | 212688, 387 | 12869 388 | ], 389 | [ 390 | 523657, 391 | 208741, 392 | 0 393 | ], 394 | [ 395 | 528421, 396 | 212688, 397 | 0 398 | ], 399 | [ 400 | 534759, 401 | 204979, 402 | 0 403 | ], 404 | [ 405 | 529970, 406 | 201158, 407 | 0 408 | ], 409 | [ 410 | 524038, 411 | 209058, 412 | 0 413 | ], 414 | [ 415 | 524122, 416 | 209126, 417 | 0 418 | ], 419 | [ 420 | 523657, 421 | 208741, 422 | 15151 423 | ], 424 | [ 425 | 527714, 426 | 203869, 427 | 15151 428 | ], 429 | [ 430 | 527714, 431 | 203869, 432 | 10976 433 | ], 434 | [ 435 | 529970, 436 | 201158, 437 | 10976 438 | ] 439 | ], 440 | "transform": 441 | { 442 | "scale": 443 | [ 444 | 0.001, 445 | 0.001, 446 | 0.001 447 | ], 448 | "translate": 449 | [ 450 | 90409.32, 451 | 435440.44, 452 | 0.0 453 | ] 454 | }, 455 | "appearance": 456 | { 457 | "textures": 458 | [ 459 | { 460 | "type": "JPG", 461 | "image": "appearances/0320_4_15.jpg" 462 | }, 463 | { 464 | "type": "JPG", 465 | "image": "appearances/0320_4_16.jpg" 466 | }, 467 | { 468 | "type": "JPG", 469 | "image": "appearances/0320_4_18.jpg" 470 | }, 471 | { 472 | "type": "JPG", 473 | "image": "appearances/0320_4_19.jpg" 474 | }, 475 | { 476 | "type": "JPG", 477 | "image": "appearances/0320_4_13.jpg" 478 | }, 479 | { 480 | "type": "JPG", 481 | "image": "appearances/0320_4_17.jpg" 482 | } 483 | ], 484 | "vertices-texture": 485 | [ 486 | [ 487 | 0.033, 488 | 0.8854 489 | ], 490 | [ 491 | 0.0372, 492 | 0.8803 493 | ], 494 | [ 495 | 0.1023, 496 | 0.9349 497 | ], 498 | [ 499 | 0.0507, 500 | 0.9991 501 | ], 502 | [ 503 | 0.0003, 504 | 0.9573 505 | ], 506 | [ 507 | 0.0482, 508 | 0.9 509 | ], 510 | [ 511 | 0.0321, 512 | 0.8865 513 | ], 514 | [ 515 | 0.314, 516 | 0.3067 517 | ], 518 | [ 519 | 0.2779, 520 | 0.2764 521 | ], 522 | [ 523 | 0.3291, 524 | 0.2126 525 | ], 526 | [ 527 | 0.3654, 528 | 0.2427 529 | ], 530 | [ 531 | 0.2542, 532 | 0.25 533 | ], 534 | [ 535 | 0.2703, 536 | 0.2634 537 | ], 538 | [ 539 | 0.2225, 540 | 0.3206 541 | ], 542 | [ 543 | 0.2064, 544 | 0.3073 545 | ], 546 | [ 547 | 0.455, 548 | 0.0706 549 | ], 550 | [ 551 | 0.4507, 552 | 0.0756 553 | ], 554 | [ 555 | 0.3687, 556 | 0.0328 557 | ], 558 | [ 559 | 0.3729, 560 | 0.0279 561 | ], 562 | [ 563 | 0.8032, 564 | 0.3657 565 | ], 566 | [ 567 | 0.8023, 568 | 0.3668 569 | ], 570 | [ 571 | 0.7203, 572 | 0.3239 573 | ], 574 | [ 575 | 0.7212, 576 | 0.3228 577 | ], 578 | [ 579 | 0.6559, 580 | 0.4971 581 | ], 582 | [ 583 | 0.6721, 584 | 0.5104 585 | ], 586 | [ 587 | 0.6587, 588 | 0.5213 589 | ], 590 | [ 591 | 0.6426, 592 | 0.508 593 | ], 594 | [ 595 | 0.5318, 596 | 0.9429 597 | ], 598 | [ 599 | 0.4835, 600 | 0.9991 601 | ], 602 | [ 603 | 0.4704, 604 | 0.992 605 | ], 606 | [ 607 | 0.5186, 608 | 0.9359 609 | ], 610 | [ 611 | 0.9671, 612 | 0.1619 613 | ], 614 | [ 615 | 0.9159, 616 | 0.2256 617 | ], 618 | [ 619 | 0.8889, 620 | 0.222 621 | ], 622 | [ 623 | 0.9399, 624 | 0.1584 625 | ], 626 | [ 627 | 0.1395, 628 | 0.0753 629 | ], 630 | [ 631 | 0.2038, 632 | 0.1301 633 | ], 634 | [ 635 | 0.2028, 636 | 0.1305 637 | ], 638 | [ 639 | 0.1385, 640 | 0.0758 641 | ], 642 | [ 643 | 0.1218, 644 | 0.8882 645 | ], 646 | [ 647 | 0.071, 648 | 0.9515 649 | ], 650 | [ 651 | 0.0, 652 | 0.9421 653 | ], 654 | [ 655 | 0.0504, 656 | 0.8794 657 | ], 658 | [ 659 | 0.7683, 660 | 0.9476 661 | ], 662 | [ 663 | 0.6973, 664 | 0.9382 665 | ], 666 | [ 667 | 0.4918, 668 | 0.2573 669 | ], 670 | [ 671 | 0.5275, 672 | 0.2877 673 | ], 674 | [ 675 | 0.5265, 676 | 0.2881 677 | ], 678 | [ 679 | 0.4908, 680 | 0.2578 681 | ], 682 | [ 683 | 0.4863, 684 | 0.3843 685 | ], 686 | [ 687 | 0.4381, 688 | 0.4404 689 | ], 690 | [ 691 | 0.3698, 692 | 0.4039 693 | ], 694 | [ 695 | 0.4175, 696 | 0.3483 697 | ] 698 | ] 699 | }, 700 | "metadata": 701 | { 702 | "referenceSystem": "https://www.opengis.net/def/crs/EPSG/0/7415", 703 | "geographicalExtent": 704 | [ 705 | 90932.97700000001, 706 | 435641.598, 707 | 0.0, 708 | 90944.07900000001, 709 | 435653.128, 710 | 15.311 711 | ] 712 | } 713 | } -------------------------------------------------------------------------------- /tests/data/rotterdam/textures/empty.texture: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityjson/cjio/35728539127f71b1d5ae458169ff84050e5e20ad/tests/data/rotterdam/textures/empty.texture -------------------------------------------------------------------------------- /tests/data/upgrade_all.py: -------------------------------------------------------------------------------- 1 | import glob 2 | from cjio import cityjson 3 | 4 | to_upgrade_to = "1.1" 5 | 6 | for f in glob.glob("./**/*.json", recursive=True): 7 | # print(f) 8 | cj_file = open(f, "r") 9 | cm = cityjson.reader(file=cj_file) 10 | if cm.get_version() != to_upgrade_to: 11 | print(f) 12 | cm.upgrade_version(to_upgrade_to, 3) 13 | cityjson.save(cm, f) 14 | # re = cm.validate() 15 | # print(re) 16 | # print(cm) 17 | # break 18 | -------------------------------------------------------------------------------- /tests/data/upgrade_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | LOG_PATH="$SCRIPT_DIR/upgrade.txt" 5 | ORIGINAL_FILES_PATH="$SCRIPT_DIR/old/" 6 | 7 | mkdir -p "$ORIGINAL_FILES_PATH" 8 | 9 | for filepath in $(find $directory -type f -name "*.json") 10 | do 11 | 12 | file="$(basename -- "$filepath")" 13 | name="${file%%.*}" 14 | echo $file 15 | 16 | # rename original files and move to dedicated folder 17 | basepath="${filepath%/*}/" 18 | oldfilepath="$ORIGINAL_FILES_PATH$name"_old.json 19 | mv "$filepath" "$oldfilepath" 20 | 21 | # save upgraded files with names of original files 22 | cjio "$oldfilepath" upgrade save "$filepath" >> $LOG_PATH 23 | 24 | echo -e "\n ========== \n" >> $LOG_PATH 25 | 26 | done 27 | -------------------------------------------------------------------------------- /tests/test_appearances.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from copy import deepcopy 3 | 4 | import pytest 5 | 6 | from cjio import errors 7 | 8 | 9 | def test_get_textures_location_subdir(rotterdam_subset, data_dir): 10 | """Textures are in a subdirectory to the city model""" 11 | d = rotterdam_subset.get_textures_location() 12 | loc = os.path.join(data_dir, "rotterdam", "appearances") 13 | assert d == loc 14 | 15 | 16 | def test_get_textures_location_same(dummy, data_dir): 17 | """Textures are in same location with the city model""" 18 | d = dummy.get_textures_location() 19 | loc = os.path.join(data_dir, "dummy") 20 | assert d == loc 21 | 22 | 23 | def test_update_textures_relative(rotterdam_subset, data_dir): 24 | r = deepcopy(rotterdam_subset) 25 | npath = os.path.join(data_dir, "rotterdam", "textures") 26 | r.update_textures_location(npath, relative=True) 27 | dirs = [] 28 | for t in r.j["appearance"]["textures"]: 29 | dirs.append(os.path.dirname(t["image"])) 30 | assert all(p == "textures" for p in dirs) 31 | 32 | 33 | def test_update_textures_absolute(rotterdam_subset, data_dir): 34 | r = deepcopy(rotterdam_subset) 35 | npath = os.path.join(data_dir, "rotterdam", "textures") 36 | r.update_textures_location(npath, relative=False) 37 | dirs = [] 38 | for t in r.j["appearance"]["textures"]: 39 | dirs.append(os.path.dirname(t["image"])) 40 | assert all(p == npath for p in dirs) 41 | 42 | 43 | def test_update_textures_nodir(rotterdam_subset, data_dir): 44 | r = deepcopy(rotterdam_subset) 45 | npath = os.path.join(data_dir, "rotterdam", "not_existing_dir") 46 | with pytest.raises(NotADirectoryError): 47 | r.update_textures_location(npath, relative=False) 48 | 49 | 50 | def test_update_textures_url(rotterdam_subset): 51 | r = deepcopy(rotterdam_subset) 52 | npath = "http://www.balazs.com/images" 53 | r.update_textures_location(npath, relative=False) 54 | dirs = [] 55 | for t in r.j["appearance"]["textures"]: 56 | dirs.append(os.path.dirname(t["image"])) 57 | assert all(p == npath for p in dirs) 58 | 59 | 60 | def test_update_textures_none(dummy_noappearance): 61 | with pytest.raises(errors.CJInvalidOperation): 62 | dummy_noappearance.update_textures_location("somepath", relative=True) 63 | 64 | 65 | def test_remove_textures(rotterdam_subset): 66 | cm = rotterdam_subset 67 | cm.remove_textures() 68 | assert "appearance" not in cm.j 69 | assert all( 70 | "texture" not in geom 71 | for co in cm.j["CityObjects"].values() 72 | for geom in co["geometry"] 73 | ) 74 | -------------------------------------------------------------------------------- /tests/test_cityjson.py: -------------------------------------------------------------------------------- 1 | """Test the CityJSON class""" 2 | 3 | import pytest 4 | import copy 5 | from cjio import cityjson 6 | from math import isclose 7 | import json 8 | 9 | 10 | class TestCityJSON: 11 | def test_subset_ids(self, zurich_subset): 12 | # Parent ID 13 | subset = zurich_subset.get_subset_ids( 14 | ["UUID_583c776f-5b0c-4d42-9c37-5b94e0c21a30"] 15 | ) 16 | expected = [ 17 | "UUID_583c776f-5b0c-4d42-9c37-5b94e0c21a30", 18 | "UUID_60ae78b4-7632-49ca-89ed-3d1616d5eb80", 19 | "UUID_5bd1cee6-b3f0-40fb-a6ae-833e88305e31", 20 | ] 21 | assert set(expected).issubset(subset.j["CityObjects"]) 22 | # Child ID 23 | subset2 = zurich_subset.get_subset_ids( 24 | ["UUID_60ae78b4-7632-49ca-89ed-3d1616d5eb80"] 25 | ) 26 | expected = [] 27 | assert set(expected).issubset(set(subset2.j["CityObjects"])) 28 | 29 | def test_subset_bbox(self, delft): 30 | cm = delft 31 | extent = cm.j["metadata"]["geographicalExtent"] 32 | bbox = [ 33 | extent[0], 34 | extent[1], 35 | extent[3] - ((extent[3] - extent[0]) / 2), 36 | extent[4] - ((extent[4] - extent[1]) / 2), 37 | ] 38 | subset = cm.get_subset_bbox(bbox) 39 | for coid in subset.j["CityObjects"]: 40 | centroid = subset.get_centroid(coid) 41 | if centroid is not None: 42 | assert ( 43 | (centroid[0] >= bbox[0]) 44 | and (centroid[1] >= bbox[1]) 45 | and (centroid[0] < bbox[2]) 46 | and (centroid[1] < bbox[3]) 47 | ) 48 | 49 | def test_subset_bbox_loop(self, delft): 50 | """Issue #10""" 51 | _ = delft.update_bbox() 52 | subs_box = ( 53 | 84873.68845606346, 54 | 447503.6748565406, 55 | 84919.65679078053, 56 | 447548.4091420035, 57 | ) 58 | nr_cos = [] 59 | for i in range(4): 60 | s = delft.get_subset_bbox(subs_box) 61 | nr_cos.append(len(s.j["CityObjects"])) 62 | _f = nr_cos[0] 63 | assert all(i == _f for i in nr_cos) 64 | 65 | def test_subset_random(self, zurich_subset): 66 | subset = zurich_subset.get_subset_random(10) 67 | cnt = sum( 68 | 1 for co in subset.j["CityObjects"].values() if co["type"] == "Building" 69 | ) 70 | assert cnt == 10 71 | 72 | def test_subset_cotype(self, delft): 73 | subset = delft.get_subset_cotype(("Building", "LandUse")) 74 | types = [ 75 | "LandUse", 76 | "Building", 77 | "BuildingPart", 78 | "BuildingInstallation", 79 | "BuildingConstructiveElement", 80 | "BuildingFurniture", 81 | "BuildingStorey", 82 | "BuildingRoom", 83 | "BuildingUnit", 84 | ] 85 | 86 | for co in subset.j["CityObjects"]: 87 | assert subset.j["CityObjects"][co]["type"] in types 88 | 89 | def test_calculate_bbox(self): 90 | """Test the calculate_bbox function""" 91 | 92 | data = {"vertices": [[0, 0, 0], [1, 1, 1]]} 93 | 94 | cm = cityjson.CityJSON(j=data) 95 | bbox = cm.calculate_bbox() 96 | 97 | assert bbox == [0, 0, 0, 1, 1, 1] 98 | 99 | def test_calculate_bbox_with_transform(self): 100 | """Test the calculate_bbox function""" 101 | 102 | data = { 103 | "vertices": [[0, 0, 0], [1, 1, 1]], 104 | "transform": {"scale": [0.001, 0.001, 0.001], "translate": [100, 100, 100]}, 105 | } 106 | 107 | cm = cityjson.CityJSON(j=data) 108 | bbox = cm.calculate_bbox() 109 | 110 | assert bbox == [100, 100, 100, 100.001, 100.001, 100.001] 111 | 112 | def test_de_compression(self, delft): 113 | cm = copy.deepcopy(delft) 114 | assert cm.decompress() 115 | cm2 = copy.deepcopy(cm) 116 | cm.compress(3) 117 | assert cm.j["transform"]["scale"][0] == 0.001 118 | assert len(delft.j["vertices"]) == len(cm.j["vertices"]) 119 | v1 = cm2.j["vertices"][0][0] 120 | v2 = cm.j["vertices"][0][0] 121 | assert isinstance(v1, float) 122 | assert isinstance(v2, int) 123 | 124 | def test_de_compression_2(self, cube): 125 | cubec = copy.deepcopy(cube) 126 | cubec.decompress() 127 | assert cube.j["vertices"][0][0] == cubec.j["vertices"][0][0] 128 | assert cubec.compress(2) 129 | assert len(cube.j["vertices"]) == len(cubec.j["vertices"]) 130 | 131 | def test_reproject(self, delft_1b): 132 | cm = copy.deepcopy(delft_1b) 133 | cm.reproject(4937) # -- z values should stay the same 134 | x = ( 135 | cm.j["vertices"][0][0] * cm.j["transform"]["scale"][0] 136 | + cm.j["transform"]["translate"][0] 137 | ) 138 | y = ( 139 | cm.j["vertices"][0][1] * cm.j["transform"]["scale"][1] 140 | + cm.j["transform"]["translate"][1] 141 | ) 142 | z = ( 143 | cm.j["vertices"][0][2] * cm.j["transform"]["scale"][2] 144 | + cm.j["transform"]["translate"][2] 145 | ) 146 | print(x, y, z) 147 | assert x == pytest.approx(52.011288184126094) 148 | assert y == pytest.approx(4.36772776578513) 149 | assert z == pytest.approx(49.50418078666017) 150 | assert isclose( 151 | cm.j["metadata"]["geographicalExtent"][5] 152 | - cm.j["metadata"]["geographicalExtent"][2], 153 | 6.1, 154 | abs_tol=0.001, 155 | ) 156 | 157 | cm.reproject(7415) 158 | x = ( 159 | cm.j["vertices"][0][0] * cm.j["transform"]["scale"][0] 160 | + cm.j["transform"]["translate"][0] 161 | ) 162 | y = ( 163 | cm.j["vertices"][0][1] * cm.j["transform"]["scale"][1] 164 | + cm.j["transform"]["translate"][1] 165 | ) 166 | z = ( 167 | cm.j["vertices"][0][2] * cm.j["transform"]["scale"][2] 168 | + cm.j["transform"]["translate"][2] 169 | ) 170 | print(x, y, z) 171 | x_d = ( 172 | delft_1b.j["vertices"][0][0] * delft_1b.j["transform"]["scale"][0] 173 | + delft_1b.j["transform"]["translate"][0] 174 | ) 175 | y_d = ( 176 | delft_1b.j["vertices"][0][1] * delft_1b.j["transform"]["scale"][1] 177 | + delft_1b.j["transform"]["translate"][1] 178 | ) 179 | z_d = ( 180 | delft_1b.j["vertices"][0][2] * delft_1b.j["transform"]["scale"][2] 181 | + delft_1b.j["transform"]["translate"][2] 182 | ) 183 | assert x == pytest.approx(x_d) 184 | assert y == pytest.approx(y_d) 185 | assert z == pytest.approx(z_d) 186 | 187 | def test_convert_to_stl(self, delft): 188 | cm = copy.deepcopy(delft) 189 | _ = cm.export2stl(sloppy=True) 190 | 191 | def test_triangulate(self, materials): 192 | cm = materials 193 | cm.triangulate(sloppy=False) 194 | 195 | def test_is_triangulate(self, triangulated): 196 | cm = triangulated 197 | assert cm.is_triangulated() 198 | 199 | def test_convert_to_jsonl(self, rotterdam_subset): 200 | cm = copy.deepcopy(rotterdam_subset) 201 | jsonl = cm.export2jsonl() 202 | assert jsonl is not None 203 | jsonl.seek(0) 204 | for line in jsonl.readlines(): 205 | data = json.loads(line) 206 | 207 | assert "CityObjects" in data 208 | 209 | def test_filter_lod(self, multi_lod): 210 | cm = multi_lod 211 | cm.filter_lod("1.3") 212 | for coid in cm.j["CityObjects"]: 213 | if "geometry" in cm.j["CityObjects"][coid]: 214 | for geom in cm.j["CityObjects"][coid]["geometry"]: 215 | assert geom["lod"] == "1.3" 216 | 217 | def test_merge_materials(self, materials_two): 218 | """Testing #100 219 | Merging two files with materials. One has the member 'values', the other has the 220 | member 'value' on their CityObjects. 221 | """ 222 | cm1, cm2 = materials_two 223 | # cm1 contains the CityObject with 'value'. During the merge, the Material Object 224 | # from cm1 is appended to the list of Materials in cm2 225 | assert cm2.merge([cm1]) 226 | assert len(cm2.j["CityObjects"]) == 4 227 | # The value of 'value' in the CityObject from cm1 must be updated to point to the 228 | # correct Material Object in the materials list 229 | assert ( 230 | cm2.j["CityObjects"]["NL.IMBAG.Pand.0518100001755018-0"]["geometry"][0][ 231 | "material" 232 | ]["default"]["value"] 233 | == 1 234 | ) 235 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | from click.testing import CliRunner 4 | from cjio import cjio, cityjson, __version__ 5 | import pytest 6 | 7 | 8 | class TestCLI: 9 | def test_version_cli(self): 10 | runner = CliRunner() 11 | result = runner.invoke(cjio.cli, args=["--version"]) 12 | 13 | assert result.exit_code == 0 14 | assert __version__ in result.output 15 | 16 | def test_help_cli(self): 17 | runner = CliRunner() 18 | result = runner.invoke(cjio.cli, args=["--help"]) 19 | assert result.exit_code == 0 20 | assert "Options" in result.output 21 | 22 | def test_help_subcommand_cli(self): 23 | runner = CliRunner() 24 | result = runner.invoke(cjio.cli, args=["validate", "--help"]) 25 | assert result.exit_code == 0 26 | assert "Options" in result.output 27 | 28 | def test_attribute_remove_cli(self, sample_input_path, data_output_dir): 29 | p_out = os.path.join(data_output_dir, "attribute_remove.city.json") 30 | runner = CliRunner() 31 | result = runner.invoke( 32 | cjio.cli, 33 | args=[sample_input_path, "attribute_remove", "bgt_status", "save", p_out], 34 | ) 35 | 36 | assert result.exit_code == 0 37 | assert os.path.exists(p_out) 38 | 39 | os.remove(p_out) 40 | 41 | def test_attribute_rename_cli(self, sample_input_path, data_output_dir): 42 | p_out = os.path.join(data_output_dir, "attribute_rename.city.json") 43 | runner = CliRunner() 44 | result = runner.invoke( 45 | cjio.cli, 46 | args=[ 47 | sample_input_path, 48 | "attribute_rename", 49 | "hoek", 50 | "angle", 51 | "save", 52 | p_out, 53 | ], 54 | ) 55 | 56 | assert result.exit_code == 0 57 | assert os.path.exists(p_out) 58 | 59 | os.remove(p_out) 60 | 61 | def test_crs_assign_cli(self, sample_input_path, data_output_dir): 62 | p_out = os.path.join(data_output_dir, "crs_assign.city.json") 63 | runner = CliRunner() 64 | result = runner.invoke( 65 | cjio.cli, args=[sample_input_path, "crs_assign", "4326", "save", p_out] 66 | ) 67 | 68 | assert result.exit_code == 0 69 | assert os.path.exists(p_out) 70 | 71 | os.remove(p_out) 72 | 73 | def test_crs_reproject_cli(self, sample_input_path, data_output_dir): 74 | p_out = os.path.join(data_output_dir, "crs_reproject.city.json") 75 | runner = CliRunner() 76 | result = runner.invoke( 77 | cjio.cli, 78 | args=[ 79 | sample_input_path, 80 | "crs_reproject", 81 | "--digit", 82 | "7", 83 | "4979", 84 | "save", 85 | p_out, 86 | ], 87 | ) 88 | 89 | assert result.exit_code == 0 90 | assert os.path.exists(p_out) 91 | 92 | os.remove(p_out) 93 | 94 | def test_crs_translate_cli(self, sample_input_path, data_output_dir): 95 | p_out = os.path.join(data_output_dir, "crs_translate.city.json") 96 | runner = CliRunner() 97 | result = runner.invoke( 98 | cjio.cli, 99 | args=[ 100 | sample_input_path, 101 | "crs_translate", 102 | "--minxyz", 103 | "-1", 104 | "-1", 105 | "-1", 106 | "save", 107 | p_out, 108 | ], 109 | ) 110 | 111 | assert result.exit_code == 0 112 | assert os.path.exists(p_out) 113 | 114 | os.remove(p_out) 115 | 116 | def test_export_obj_cli(self, sample_input_path, data_output_dir): 117 | p_out = os.path.join(data_output_dir, "delft.obj") 118 | runner = CliRunner() 119 | result = runner.invoke( 120 | cjio.cli, args=[sample_input_path, "export", "obj", p_out] 121 | ) 122 | assert result.exit_code == 0 123 | assert os.path.exists(p_out) 124 | 125 | os.remove(p_out) 126 | 127 | def test_export_glb_cli(self, sample_input_path, data_output_dir): 128 | p_out = os.path.join(data_output_dir, "delft.glb") 129 | runner = CliRunner() 130 | result = runner.invoke( 131 | cjio.cli, args=[sample_input_path, "export", "glb", p_out] 132 | ) 133 | assert result.exit_code == 0 134 | assert os.path.exists(p_out) 135 | 136 | os.remove(p_out) 137 | 138 | def test_export_b3dm_cli(self, sample_input_path, data_output_dir): 139 | p_out = os.path.join(data_output_dir, "delft.b3dm") 140 | runner = CliRunner() 141 | result = runner.invoke( 142 | cjio.cli, args=[sample_input_path, "export", "b3dm", p_out] 143 | ) 144 | assert result.exit_code == 0 145 | assert os.path.exists(p_out) 146 | 147 | os.remove(p_out) 148 | 149 | def test_export_stl_cli(self, sample_input_path, data_output_dir): 150 | p_out = os.path.join(data_output_dir, "delft.stl") 151 | runner = CliRunner() 152 | result = runner.invoke( 153 | cjio.cli, args=[sample_input_path, "export", "stl", p_out] 154 | ) 155 | assert result.exit_code == 0 156 | assert os.path.exists(p_out) 157 | 158 | os.remove(p_out) 159 | 160 | def test_export_jsonl_cli(self, sample_input_path, data_output_dir): 161 | p_out = os.path.join(data_output_dir, "delft.city.jsonl") 162 | runner = CliRunner() 163 | result = runner.invoke( 164 | cjio.cli, args=[sample_input_path, "export", "jsonl", p_out] 165 | ) 166 | assert result.exit_code == 0 167 | assert os.path.exists(p_out) 168 | 169 | os.remove(p_out) 170 | 171 | def test_export_wrong_file_cli(self, wrong_input_path): 172 | p_out = os.path.join("tests", "data", "delft_non.city.json") 173 | runner = CliRunner() 174 | result = runner.invoke( 175 | cjio.cli, args=[wrong_input_path, "export", "jsonl", p_out] 176 | ) 177 | 178 | assert not os.path.exists(p_out) 179 | assert result.exit_code != 0 180 | 181 | def test_info_cli(self, sample_input_path): 182 | runner = CliRunner() 183 | result = runner.invoke(cjio.cli, args=[sample_input_path, "info"]) 184 | 185 | assert result.exit_code == 0 186 | 187 | def test_lod_filter_cli(self, multi_lod_path, data_output_dir): 188 | p_out = os.path.join(data_output_dir, "filtered_lod.city.json") 189 | runner = CliRunner() 190 | result = runner.invoke( 191 | cjio.cli, args=[multi_lod_path, "lod_filter", "1.2", "save", p_out] 192 | ) 193 | 194 | assert result.exit_code == 0 195 | assert os.path.exists(p_out) 196 | assert "1.2" in result.output 197 | 198 | os.remove(p_out) 199 | 200 | def test_materials_remove_cli(self, rotterdam_subset_path, data_output_dir): 201 | p_out = os.path.join(data_output_dir, "materials_remove.city.json") 202 | runner = CliRunner() 203 | result = runner.invoke( 204 | cjio.cli, args=[rotterdam_subset_path, "materials_remove", "save", p_out] 205 | ) 206 | 207 | assert result.exit_code == 0 208 | assert os.path.exists(p_out) 209 | 210 | os.remove(p_out) 211 | 212 | def test_merge_cli(self, sample_input_path, rotterdam_subset_path, data_output_dir): 213 | p_out = os.path.join(data_output_dir, "merge.city.json") 214 | runner = CliRunner() 215 | result = runner.invoke( 216 | cjio.cli, 217 | args=[sample_input_path, "merge", rotterdam_subset_path, "save", p_out], 218 | ) 219 | 220 | assert result.exit_code == 0 221 | assert os.path.exists(p_out) 222 | 223 | os.remove(p_out) 224 | 225 | def test_metadata_extended_remove_cli( 226 | self, sample_with_ext_metadata_input_path, data_output_dir 227 | ): 228 | p_out = os.path.join(data_output_dir, "cleaned_file.city.json") 229 | runner = CliRunner() 230 | result = runner.invoke( 231 | cjio.cli, 232 | args=[ 233 | sample_with_ext_metadata_input_path, 234 | "metadata_extended_remove", 235 | "save", 236 | p_out, 237 | ], 238 | ) 239 | assert result.exit_code == 0 240 | assert os.path.exists(p_out) 241 | 242 | os.remove(p_out) 243 | 244 | def test_metadata_get_cli(self, sample_input_path): 245 | runner = CliRunner() 246 | result = runner.invoke(cjio.cli, args=[sample_input_path, "metadata_get"]) 247 | 248 | assert result.exit_code == 0 249 | 250 | def test_print_cli(self, sample_input_path): 251 | runner = CliRunner() 252 | result = runner.invoke(cjio.cli, args=[sample_input_path, "print"]) 253 | assert result.exit_code == 0 254 | assert result.exit_code == 0 255 | assert "CityJSON" in result.output 256 | assert "EPSG" in result.output 257 | 258 | def test_save_cli(self, sample_input_path, data_output_dir): 259 | p_out = os.path.join(data_output_dir, "save.city.json") 260 | runner = CliRunner() 261 | result = runner.invoke( 262 | cjio.cli, args=[sample_input_path, "save", "--indent", p_out] 263 | ) 264 | 265 | assert result.exit_code == 0 266 | assert os.path.exists(p_out) 267 | 268 | os.remove(p_out) 269 | 270 | def test_save_with_texture_cli( 271 | self, rotterdam_subset_path, data_output_dir, temp_texture_dir 272 | ): 273 | p_out = os.path.join(data_output_dir, "save_texture.city.json") 274 | runner = CliRunner() 275 | result = runner.invoke( 276 | cjio.cli, 277 | args=[ 278 | rotterdam_subset_path, 279 | "save", 280 | "--textures", 281 | temp_texture_dir, 282 | "--indent", 283 | p_out, 284 | ], 285 | ) 286 | assert result.exit_code == 0 287 | assert os.path.exists(p_out) 288 | 289 | result2 = runner.invoke( 290 | cjio.cli, 291 | args=[ 292 | p_out, 293 | "textures_locate", 294 | ], 295 | ) 296 | assert result2.exit_code == 0 297 | assert temp_texture_dir in result2.output 298 | 299 | os.remove(p_out) 300 | 301 | def test_subset_id_cli(self, rotterdam_subset_path, data_output_dir): 302 | p_out = os.path.join(data_output_dir, "subset.city.json") 303 | runner = CliRunner() 304 | result = runner.invoke( 305 | cjio.cli, 306 | args=[ 307 | rotterdam_subset_path, 308 | "subset", 309 | "--id", 310 | "{23D8CA22-0C82-4453-A11E-B3F2B3116DB4}", 311 | "save", 312 | p_out, 313 | ], 314 | ) 315 | 316 | assert result.exit_code == 0 317 | assert os.path.exists(p_out) 318 | 319 | os.remove(p_out) 320 | 321 | def test_subset_bbox_cli(self, rotterdam_subset_path, data_output_dir): 322 | p_out = os.path.join(data_output_dir, "subset_bbox.city.json") 323 | runner = CliRunner() 324 | result = runner.invoke( 325 | cjio.cli, 326 | args=[ 327 | rotterdam_subset_path, 328 | "subset", 329 | "--bbox", 330 | 90970, 331 | 435620, 332 | 91000, 333 | 435650, 334 | "save", 335 | p_out, 336 | ], 337 | ) 338 | 339 | assert result.exit_code == 0 340 | assert os.path.exists(p_out) 341 | 342 | os.remove(p_out) 343 | 344 | def test_subset_radius_cli(self, rotterdam_subset_path, data_output_dir): 345 | p_out = os.path.join(data_output_dir, "subset_radius.city.json") 346 | runner = CliRunner() 347 | result = runner.invoke( 348 | cjio.cli, 349 | args=[ 350 | rotterdam_subset_path, 351 | "subset", 352 | "--radius", 353 | 90970, 354 | 435620, 355 | 20.0, 356 | "--exclude", 357 | "save", 358 | p_out, 359 | ], 360 | ) 361 | 362 | assert result.exit_code == 0 363 | assert os.path.exists(p_out) 364 | 365 | os.remove(p_out) 366 | 367 | def test_subset_random_cli(self, rotterdam_subset_path, data_output_dir): 368 | p_out = os.path.join(data_output_dir, "subset_random.city.json") 369 | runner = CliRunner() 370 | result = runner.invoke( 371 | cjio.cli, 372 | args=[ 373 | rotterdam_subset_path, 374 | "subset", 375 | "--random", 376 | 3, 377 | "save", 378 | p_out, 379 | ], 380 | ) 381 | 382 | assert result.exit_code == 0 383 | assert os.path.exists(p_out) 384 | 385 | os.remove(p_out) 386 | 387 | def test_subset_cotype_cli(self, sample_input_path, data_output_dir): 388 | p_out = os.path.join(data_output_dir, "subset_cotype.city.json") 389 | runner = CliRunner() 390 | result = runner.invoke( 391 | cjio.cli, 392 | args=[ 393 | sample_input_path, 394 | "subset", 395 | "--cotype", 396 | "Bridge", 397 | "save", 398 | p_out, 399 | ], 400 | ) 401 | 402 | assert result.exit_code == 0 403 | assert os.path.exists(p_out) 404 | 405 | os.remove(p_out) 406 | 407 | def test_textures_locate_cli_no_textures(self, sample_input_path): 408 | runner = CliRunner() 409 | result = runner.invoke(cjio.cli, args=[sample_input_path, "textures_locate"]) 410 | 411 | assert result.exit_code == 0 412 | assert "This file does not have textures" in result.output 413 | 414 | def test_textures_locate_cli_with_textures(self, rotterdam_subset_path): 415 | runner = CliRunner() 416 | result = runner.invoke( 417 | cjio.cli, args=[rotterdam_subset_path, "textures_locate"] 418 | ) 419 | 420 | assert result.exit_code == 0 421 | assert "rotterdam/appearances" in result.output 422 | 423 | def test_textures_remove_cli(self, rotterdam_subset_path, data_output_dir): 424 | p_out = os.path.join(data_output_dir, "textures_remove.city.json") 425 | runner = CliRunner() 426 | result = runner.invoke( 427 | cjio.cli, args=[rotterdam_subset_path, "textures_remove", "save", p_out] 428 | ) 429 | 430 | assert result.exit_code == 0 431 | assert os.path.exists(p_out) 432 | 433 | result2 = runner.invoke(cjio.cli, args=[p_out, "textures_locate"]) 434 | 435 | assert result2.exit_code == 0 436 | assert "This file does not have textures" in result2.output 437 | 438 | os.remove(p_out) 439 | 440 | def test_textures_update_cli( 441 | self, rotterdam_subset_path, data_output_dir, temp_texture_dir 442 | ): 443 | p_out = os.path.join(data_output_dir, "updated_textures.city.json") 444 | runner = CliRunner() 445 | result = runner.invoke( 446 | cjio.cli, 447 | args=[ 448 | rotterdam_subset_path, 449 | "textures_update", 450 | temp_texture_dir, 451 | "save", 452 | p_out, 453 | ], 454 | ) 455 | assert result.exit_code == 0 456 | assert os.path.exists(p_out) 457 | 458 | result2 = runner.invoke(cjio.cli, args=[p_out, "textures_locate"]) 459 | 460 | assert result2.exit_code == 0 461 | assert temp_texture_dir in result2.output 462 | 463 | os.remove(p_out) 464 | 465 | def test_triangulate_cli(self, sample_input_path, data_output_dir): 466 | p_out = os.path.join(data_output_dir, "triangulated.city.json") 467 | runner = CliRunner() 468 | result = runner.invoke( 469 | cjio.cli, args=[sample_input_path, "triangulate", "save", p_out] 470 | ) 471 | 472 | assert result.exit_code == 0 473 | assert os.path.exists(p_out) 474 | 475 | os.remove(p_out) 476 | 477 | def test_triangulate_sloppy_cli(self, sample_input_path, data_output_dir): 478 | p_out = os.path.join(data_output_dir, "triangulated.city.json") 479 | runner = CliRunner() 480 | result = runner.invoke( 481 | cjio.cli, args=[sample_input_path, "triangulate", "--sloppy", "save", p_out] 482 | ) 483 | 484 | assert result.exit_code == 0 485 | assert os.path.exists(p_out) 486 | 487 | os.remove(p_out) 488 | 489 | def test_upgrade_cli(self, sample_input_path, data_output_dir): 490 | p_out = os.path.join(data_output_dir, "upgrade.city.json") 491 | runner = CliRunner() 492 | result = runner.invoke( 493 | cjio.cli, args=[sample_input_path, "upgrade", "save", p_out] 494 | ) 495 | 496 | assert result.exit_code == 0 497 | assert os.path.exists(p_out) 498 | 499 | os.remove(p_out) 500 | 501 | def test_validate_cli(self, sample_input_path): 502 | if not cityjson.MODULE_CJVAL_AVAILABLE: # pragma: no cover 503 | pytest.skip("cjvalpy module not available") 504 | runner = CliRunner() 505 | result = runner.invoke(cjio.cli, args=[sample_input_path, "validate"]) 506 | 507 | assert result.exit_code == 0 508 | 509 | def test_vertices_clean_cli(self, sample_input_path, data_output_dir): 510 | p_out = os.path.join(data_output_dir, "clean.city.json") 511 | runner = CliRunner() 512 | result = runner.invoke( 513 | cjio.cli, args=[sample_input_path, "vertices_clean", "save", p_out] 514 | ) 515 | 516 | assert result.exit_code == 0 517 | assert os.path.exists(p_out) 518 | 519 | os.remove(p_out) 520 | 521 | def test_chained_commands_cli(self, rotterdam_subset_path, data_output_dir): 522 | """ 523 | Test chaining multiple commands to ensure process_pipeline is invoked correctly. 524 | """ 525 | p_out = os.path.join(data_output_dir, "pipeline_output.city.json") 526 | runner = CliRunner() 527 | 528 | result = runner.invoke( 529 | cjio.cli, 530 | args=[ 531 | rotterdam_subset_path, 532 | "subset", 533 | "--id", 534 | "{23D8CA22-0C82-4453-A11E-B3F2B3116DB4}", 535 | "vertices_clean", 536 | "save", 537 | p_out, 538 | ], 539 | ) 540 | 541 | assert result.exit_code == 0 542 | assert os.path.exists(p_out) 543 | 544 | os.remove(p_out) 545 | 546 | def test_off_input_cli(self, sample_off_file, data_output_dir): 547 | """ 548 | Test that the CLI works with a .off input file.""" 549 | p_out = os.path.join(data_output_dir, "box.city.json") 550 | runner = CliRunner() 551 | 552 | result = runner.invoke( 553 | cjio.cli, 554 | args=[ 555 | sample_off_file, 556 | "save", 557 | p_out, 558 | ], 559 | ) 560 | 561 | assert result.exit_code == 0 562 | assert os.path.exists(p_out) 563 | 564 | os.remove(p_out) 565 | 566 | def test_poly_input_cli_0_index(self, sample_poly_0_index_file, data_output_dir): 567 | """ 568 | Test that the CLI works with a .poly input file.""" 569 | p_out = os.path.join(data_output_dir, "cube.city.json") 570 | runner = CliRunner() 571 | 572 | result = runner.invoke( 573 | cjio.cli, 574 | args=[ 575 | sample_poly_0_index_file, 576 | "save", 577 | p_out, 578 | ], 579 | ) 580 | 581 | assert result.exit_code == 0 582 | assert os.path.exists(p_out) 583 | 584 | os.remove(p_out) 585 | 586 | def test_poly_input_cli_1_index(self, sample_poly_1_index_file, data_output_dir): 587 | """ 588 | Test that the CLI works with a .poly input file.""" 589 | p_out = os.path.join(data_output_dir, "cube.city.json") 590 | runner = CliRunner() 591 | 592 | result = runner.invoke( 593 | cjio.cli, 594 | args=[ 595 | sample_poly_1_index_file, 596 | "save", 597 | p_out, 598 | ], 599 | ) 600 | 601 | assert result.exit_code == 0 602 | assert os.path.exists(p_out) 603 | 604 | os.remove(p_out) 605 | 606 | def test_wrong_extension_cli(self, sample_wrong_suffix): 607 | """ 608 | Test that the CLI gives a warning when the file extension is not supported. 609 | """ 610 | runner = CliRunner() 611 | 612 | result = runner.invoke( 613 | cjio.cli, 614 | args=[ 615 | sample_wrong_suffix, 616 | "subset", 617 | "--id", 618 | "{23D8CA22-0C82-4453-A11E-B3F2B3116DB4}", 619 | "vertices_clean", 620 | "save", 621 | "output.city.json", 622 | ], 623 | ) 624 | assert result.exit_code != 0 625 | assert "File type not supported" in result.output 626 | -------------------------------------------------------------------------------- /tests/test_convert.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 4 | from cjio import convert, cityjson 5 | 6 | 7 | @pytest.fixture( 8 | scope="function", params=[("rotterdam", "rotterdam_subset.json"), ("delft.json",)] 9 | ) 10 | def several_cms(data_dir, request): 11 | p = os.path.join(data_dir, *request.param) 12 | with open(p, "r") as f: 13 | yield cityjson.CityJSON(file=f) 14 | 15 | 16 | class TestGltf: 17 | @pytest.mark.slow 18 | def test_convert_to_glb(self, several_cms): 19 | _ = several_cms.export2glb() 20 | 21 | def test_debug_den_haag_glb(self, data_dir): 22 | p = os.path.join(data_dir, "DH_01_subs.city.json") 23 | with open(p, "r") as f: 24 | cm = cityjson.CityJSON(file=f) 25 | _ = cm.export2glb() 26 | 27 | def test_debug_delft_glb(self, data_dir, data_output_dir): 28 | # CityJSON v1.1 29 | p = os.path.join(data_dir, "DH_01_subs.city.json") 30 | with open(p, "r") as f: 31 | cm = cityjson.CityJSON(file=f) 32 | glb = cm.export2glb(do_triangulate=False) 33 | glb.seek(0) 34 | with open(f"{data_output_dir}/DH_01_subs.glb", mode="wb") as bo: 35 | bo.write(glb.getvalue()) 36 | 37 | 38 | class TestB3dm: 39 | @pytest.mark.slow 40 | def test_convert_to_b3dm(self, delft): 41 | glb = delft.export2glb() 42 | _ = convert.to_b3dm(delft, glb) 43 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import pytest 3 | from click import ClickException 4 | 5 | from cjio import utils 6 | 7 | 8 | @pytest.fixture( 9 | scope="session", 10 | params=[ 11 | ("doesnt_exist.json", False), 12 | ("tests/data/rotterdam", True), 13 | ("tests/data/rotterdam/", True), 14 | ("tests/data/delft.json", False), 15 | ("tests/data/doesnt_exist", True), 16 | ], 17 | ) 18 | def valid_path(request): 19 | package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 20 | yield (os.path.join(package_dir, request.param[0]), request.param[1]) 21 | 22 | 23 | @pytest.fixture( 24 | scope="session", 25 | params=[ 26 | ("tests/data/doesnt_exist/doesnt_exist.json", False), 27 | ("tests/data/doesnt_exist/other_dir", True), 28 | ], 29 | ) 30 | def invalid_path(request): 31 | package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 32 | yield (os.path.join(package_dir, request.param[0]), request.param[1]) 33 | 34 | 35 | class TestFileNamesPaths: 36 | def test_verify_filenames_valid(self, valid_path): 37 | res = utils.verify_filename(valid_path[0]) 38 | assert res["dir"] == valid_path[1] 39 | 40 | def test_verify_filenames_invalid(self, invalid_path): 41 | with pytest.raises(ClickException): 42 | utils.verify_filename(invalid_path[0]) 43 | -------------------------------------------------------------------------------- /uid_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Create passwd entry for arbitrary user ID 6 | if [[ -z "$(awk -F ':' "\$3 == $(id -u)" /etc/passwd)" ]]; then 7 | echo "Adding arbitrary user" 8 | echo "${USER_NAME:-default}:x:$(id -u):0:${USER_NAME:-default} user:${HOME}:/sbin/nologin" >> /etc/passwd 9 | echo "$(awk -F ':' "\$3 == $(id -u)" /etc/passwd)" 10 | fi 11 | 12 | echo "Running as $(whoami)" 13 | 14 | exec "$@" 15 | --------------------------------------------------------------------------------