├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── python-publish.yml │ └── tests.yml ├── .gitignore ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── Visualization.rst │ ├── _static │ ├── default.css │ ├── jupytersettings.png │ ├── logo-wordmark-dark.png │ ├── logo-wordmark-light.png │ ├── logo.ico │ └── visualize.png │ ├── _templates │ └── layout.html │ ├── analysis.rst │ ├── bdshadow.rst │ ├── conf.py │ ├── example │ ├── example.rst │ ├── output_14_0.png │ ├── output_24_1.png │ ├── output_27_0.png │ ├── output_29_0.png │ ├── output_31_0.png │ └── output_6_1.png │ ├── index.rst │ ├── install.rst │ └── preprocess.rst ├── example ├── Example1-building_shadow_analysis.ipynb └── data │ ├── Suzhoubuildings │ ├── 苏州(1).dbf │ ├── 苏州(1).prj │ ├── 苏州(1).sbn │ ├── 苏州(1).sbx │ ├── 苏州(1).shp │ └── 苏州(1).shx │ ├── bd_demo.json │ └── bd_demo_2.json ├── image ├── README │ ├── 1649074615552.png │ ├── 1649161376291_1.png │ ├── 1649405838683_1.png │ ├── 1651490411329.png │ ├── 1651490416315.png │ ├── 1651506285290.png │ ├── 1651645524782.png │ ├── 1651645530892.png │ ├── 1651741110878.png │ ├── 1651975815798.png │ └── 1651975824187.png └── paper │ ├── 1651656639873.png │ └── 1651656857394.png ├── paper.bib ├── paper.md ├── requirements.txt ├── setup.py └── src └── pybdshadow ├── __init__.py ├── analysis.py ├── get_buildings.py ├── preprocess.py ├── pybdshadow.py ├── tests ├── __init__.py ├── test_analysis.py ├── test_pointlightshadow.py └── test_sunlightshadow.py ├── utils.py ├── visiblearea.py └── visualization.py /.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 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.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/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | # schedule: 4 | # - cron: '59 23 * * *' 5 | 6 | name: Tests 7 | 8 | on: 9 | push: 10 | branches: 11 | - '*' 12 | pull_request: 13 | branches: 14 | - '*' 15 | workflow_dispatch: 16 | inputs: 17 | version: 18 | description: Manual Test Trigger 19 | default: test 20 | required: false 21 | 22 | jobs: 23 | build: 24 | name: ${{ matrix.os }}, ${{ matrix.python-version }} 25 | runs-on: ${{ matrix.os }} 26 | timeout-minutes: 30 27 | strategy: 28 | matrix: 29 | os: [ubuntu-latest] 30 | python-version: ["3.8", "3.9"] 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v2 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install --upgrade pip 42 | pip install flake8 pytest 43 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 44 | 45 | - name: Lint with flake8 46 | run: | 47 | # stop the build if there are Python syntax errors or undefined names 48 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 49 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 50 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 51 | 52 | - name: Test 53 | run: | 54 | pip install pytest-cov 55 | pytest -v -r s --color=yes --cov=src --cov-append --cov-report term-missing --cov-report xml 56 | 57 | - name: Upload coverage to Codecov 58 | uses: codecov/codecov-action@v2 59 | with: 60 | fail_ci_if_error: true 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,python 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Python ### 35 | # Byte-compiled / optimized / DLL files 36 | __pycache__/ 37 | *.py[cod] 38 | *$py.class 39 | 40 | # C extensions 41 | *.so 42 | 43 | # Distribution / packaging 44 | .Python 45 | build/ 46 | develop-eggs/ 47 | dist/ 48 | downloads/ 49 | eggs/ 50 | .eggs/ 51 | lib/ 52 | lib64/ 53 | parts/ 54 | sdist/ 55 | var/ 56 | wheels/ 57 | share/python-wheels/ 58 | *.egg-info/ 59 | .installed.cfg 60 | *.egg 61 | MANIFEST 62 | 63 | # PyInstaller 64 | # Usually these files are written by a python script from a template 65 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 66 | *.manifest 67 | *.spec 68 | 69 | # Installer logs 70 | pip-log.txt 71 | pip-delete-this-directory.txt 72 | 73 | # Unit test / coverage reports 74 | htmlcov/ 75 | .tox/ 76 | .nox/ 77 | .coverage 78 | .coverage.* 79 | .cache 80 | nosetests.xml 81 | coverage.xml 82 | *.cover 83 | *.py,cover 84 | .hypothesis/ 85 | .pytest_cache/ 86 | cover/ 87 | 88 | # Translations 89 | *.mo 90 | *.pot 91 | 92 | # Django stuff: 93 | *.log 94 | local_settings.py 95 | db.sqlite3 96 | db.sqlite3-journal 97 | 98 | # Flask stuff: 99 | instance/ 100 | .webassets-cache 101 | 102 | # Scrapy stuff: 103 | .scrapy 104 | 105 | # Sphinx documentation 106 | docs/_build/ 107 | 108 | # PyBuilder 109 | .pybuilder/ 110 | target/ 111 | 112 | # Jupyter Notebook 113 | .ipynb_checkpoints 114 | 115 | # IPython 116 | profile_default/ 117 | ipython_config.py 118 | 119 | # pyenv 120 | # For a library or package, you might want to ignore these files since the code is 121 | # intended to run in multiple environments; otherwise, check them in: 122 | # .python-version 123 | 124 | # pipenv 125 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 126 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 127 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 128 | # install all needed dependencies. 129 | #Pipfile.lock 130 | 131 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 132 | __pypackages__/ 133 | 134 | # Celery stuff 135 | celerybeat-schedule 136 | celerybeat.pid 137 | 138 | # SageMath parsed files 139 | *.sage.py 140 | 141 | # Environments 142 | .env 143 | .venv 144 | env/ 145 | venv/ 146 | ENV/ 147 | env.bak/ 148 | venv.bak/ 149 | 150 | # Spyder project settings 151 | .spyderproject 152 | .spyproject 153 | 154 | # Rope project settings 155 | .ropeproject 156 | 157 | # mkdocs documentation 158 | /site 159 | 160 | # mypy 161 | .mypy_cache/ 162 | .dmypy.json 163 | dmypy.json 164 | 165 | # Pyre type checker 166 | .pyre/ 167 | 168 | # pytype static type analyzer 169 | .pytype/ 170 | 171 | # Cython debug symbols 172 | cython_debug/ 173 | 174 | # End of https://www.toptal.com/developers/gitignore/api/macos,python 175 | 176 | 177 | .vscode -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: "Global Estimation of Building-Integrated Facade and Rooftop Photovoltaic Potential by Integrating 3D Building Footprint and Spatio-Temporal Datasets" 3 | authors: 4 | - family-names: Yu 5 | given-names: Qing 6 | affiliation: "School of Urban Planning and Design, Peking University Shenzhen Graduate School" 7 | - family-names: Dong 8 | given-names: Kechuan 9 | affiliation: "Center for Spatial Information Science, University of Tokyo" 10 | - family-names: Guo 11 | given-names: Zhiling 12 | affiliation: "Department of Building Environment and Energy Engineering, The Hong Kong Polytechnic University" 13 | - family-names: Xu 14 | given-names: Jian 15 | affiliation: "Department of Building Environment and Energy Engineering, The Hong Kong Polytechnic University" 16 | - family-names: Li 17 | given-names: Jiaxing 18 | affiliation: "School of Urban Planning and Design, Peking University Shenzhen Graduate School" 19 | - family-names: Tan 20 | given-names: Hongjun 21 | affiliation: "Department of Building Environment and Energy Engineering, The Hong Kong Polytechnic University" 22 | - family-names: Jin 23 | given-names: Yanxiu 24 | affiliation: "Center for Spatial Information Science, University of Tokyo" 25 | - family-names: Yuan 26 | given-names: Jian 27 | affiliation: "School of Urban Planning and Design, Peking University Shenzhen Graduate School" 28 | - family-names: Zhang 29 | given-names: Haoran 30 | affiliation: "Department of Building Environment and Energy Engineering, The Hong Kong Polytechnic University" 31 | - family-names: Liu 32 | given-names: Junwei 33 | affiliation: "Department of Building Environment and Energy Engineering, The Hong Kong Polytechnic University" 34 | - family-names: Chen 35 | given-names: Qi 36 | affiliation: "School of Geography and Information Engineering, China University of Geosciences (Wuhan)" 37 | - family-names: Yan 38 | given-names: Jinyue 39 | affiliation: "Department of Building Environment and Energy Engineering, The Hong Kong Polytechnic University" 40 | repository-code: "https://github.com/ni1o1/pybdshadow" 41 | date-published: "2025" 42 | doi: "10.1016/j.ynexs.2025.100060." 43 | year: "2025" 44 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | qingyu0815@foxmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to pybdshadow 2 | 3 | Whether you are a novice or experienced software developer, all contributions and suggestions are welcome! 4 | 5 | ## Getting Started 6 | 7 | If you are looking to contribute to the *pybdshadow* codebase, the best place to start is the [GitHub "issues" tab](https://github.com/ni1o1/pybdshadow/issues). This is also a great place for filing bug reports and making suggestions for ways in which we can improve the code and documentation. 8 | 9 | ## Step-by-step Instructions of Contribute 10 | 11 | The code is hosted on [GitHub](https://github.com/ni1o1/pybdshadow), 12 | so you will need to use [Git](http://git-scm.com/) to clone the project and make 13 | changes to the codebase. 14 | 15 | 1. Fork the [Pybdshadow repository](https://github.com/ni1o1/pybdshadow). 16 | 2. Create a new branch from the `Pybdshadow` master branch. 17 | 3. Within your forked copy, the source code of `Pybdshadow` is located at the [src](https://github.com/ni1o1/pybdshadow/tree/main/src) folder, you can make and test changes in the source code. 18 | 4. Before submitting your changes for review, make sure to check that your changes do not break any tests by running: ``pytest``. The tests are located in the [tests](https://github.com/ni1o1/pybdshadow/tree/main/src/pybdshadow/tests) folder. 19 | 5. When you are ready to submit your contribution, raise the Pull Request(PR). Once you finished your PR, the github [testing workflow](https://github.com/ni1o1/pybdshadow/actions/workflows/tests.yml) will test your code. We will review your changes, and might ask you to make additional changes before it is finally ready to merge. However, once it's ready, we will merge it, and you will have successfully contributed to the codebase! 20 | 21 | # 为pybdshadow项目贡献代码 22 | 23 | 无论您是新手还是经验丰富的软件开发人员,欢迎您提供所有意见和建议! 24 | 25 | ## 开始 26 | 27 | 如果你想为*pybdshadow*代码库做贡献,最好从[GitHub issues](https://github.com/ni1o1/pybdshadow/issues)开始。你可以在这里提交BUG报告,并提出改进代码和文档的方法和建议。 28 | 29 | ## 如何贡献代码 30 | 31 | 代码托管在[GitHub](https://github.com/ni1o1/pybdshadow),所以你需要使用[Git](http://git-scm.com/)克隆项目并对代码做出更改。具体方法如下: 32 | 1. Fork [`Pybdshadow`仓库](https://github.com/ni1o1/pybdshadow). 33 | 2. 以`Pybdshadow`的`main`分支为基础创建新分支。 34 | 3. 在您的分支仓库中,`Pybdshadow`的源代码位于[src](https://github.com/ni1o1/pybdshadow/tree/main/src)文件夹,您可以在源代码中进行和测试更改,如果你使用的是jupyter notebook,可以在src文件夹下建立ipynb文件进行调试,这样修改pybdshadow的源码时可以直接读取到。 35 | 4. 在提交更改以供审阅之前,请运行`pytest`来测试代码,确保您对代码的更改不会破坏任何测试结果。测试代码位于[tests](https://github.com/ni1o1/pybdshadow/tree/main/src/pybdshadow/tests)文件夹中 36 | 5. 当你准备好提交你的贡献时,提交Pull Request(PR)。完成PR后,github提供的[测试工作流](https://github.com/ni1o1/pybdshadow/actions/workflows/tests.yml)将测试您的代码,并将测试结果做出分析。 37 | 6. test分两部分,一部分是旧的代码会test保证输出一致,另一部分是你增加的方法需要自己写个test文件,增加test,这样后面贡献的人要改你代码时也会test,确保不会更变你的程序功能。`Pybdshadow`的测试结果在[![codecov](https://codecov.io/gh/ni1o1/pybdshadow/branch/main/graph/badge.svg?token=GLAVYYCD9L)](https://codecov.io/gh/ni1o1/pybdshadow)这里可以看到,其中的百分比表示单元测试覆盖率,表明有多少比例的代码通过了测试。 38 | 7. 测试成功后,我们将检查您的更改,并可能要求您在最终准备合并之前进行其他更改。如果成功,我们将merge到`main`分支中,贡献就成功啦。 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Qing Yu 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune docs 2 | prune example 3 | prune image 4 | prune src/pybdshadow/tests -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pybdshadow 2 | 3 | ![1649074615552.png](https://github.com/ni1o1/pybdshadow/raw/main/image/README/1649074615552.png) 4 | 5 | [![Documentation Status](https://readthedocs.org/projects/pybdshadow/badge/?version=latest)](https://pybdshadow.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://pepy.tech/badge/pybdshadow)](https://pepy.tech/project/pybdshadow) [![codecov](https://codecov.io/gh/ni1o1/pybdshadow/branch/main/graph/badge.svg?token=GLAVYYCD9L)](https://codecov.io/gh/ni1o1/pybdshadow) [![Tests](https://github.com/ni1o1/pybdshadow/actions/workflows/tests.yml/badge.svg)](https://github.com/ni1o1/pybdshadow/actions/workflows/tests.yml) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/ni1o1/pybdshadow/3d7f14d9db7fe2060e18e12935021ee9df4e1d5d?urlpath=lab%2Ftree%2Fexample%2FExample1-building_shadow_analysis.ipynb) 6 | 7 | ## Introduction 8 | 9 | `pybdshadow` is a python package for generating, analyzing and visualizing building shadows from large scale building geographic data. `pybdshadow` support generate building shadows from both sun light and point light. `pybdshadow` provides an efficient and easy-to-use method to generate a new source of geospatial data with great application potential in urban study. 10 | 11 | The latest stable release of the software can be installed via pip and full documentation can be found [here](https://pybdshadow.readthedocs.io/en/latest/). 12 | 13 | ## Functionality 14 | 15 | Currently, `pybdshadow` mainly provides the following methods: 16 | 17 | - *Generating building shadow from sun light*: With given location and time, the function in `pybdshadow` uses the properties of sun position obtained from [`suncalc-py`](https://github.com/kylebarron/suncalc-py) and the building height to generate shadow geometry data. 18 | - *Generating building shadow from point light*: `pybdshadow` can generate the building shadow with given location and height of the point light, which can be potentially useful for visual area analysis in urban environment. 19 | - *Analysis*: `pybdshadow` integrated the analysing method based on the properties of sun movement to track the changing position of shadows within a fixed time interval. Based on the grid processing framework provided by [`TransBigData`](https://github.com/ni1o1/transbigdata), `pybdshadow` is capable of calculating sunshine time on the ground and on the roof. 20 | - *Visualization*: Built-in visualization capabilities leverage the visualization package `keplergl` to interactively visualize building and shadow data in Jupyter notebooks with simple code. 21 | 22 | The target audience of `pybdshadow` includes data science researchers and data engineers in the field of BIM, GIS, energy, environment, and urban computing. 23 | 24 | ## Installation 25 | 26 | It is recommended to use `Python 3.7, 3.8, 3.9` 27 | 28 | ### Using pypi [![PyPI version](https://badge.fury.io/py/pybdshadow.svg)](https://badge.fury.io/py/pybdshadow) 29 | 30 | `pybdshadow` can be installed by using `pip install`. Before installing `pybdshadow`, make sure that you have installed the available [geopandas package](https://geopandas.org/en/stable/getting_started/install.html). If you already have geopandas installed, run the following code directly from the command prompt to install `pybdshadow`: 31 | 32 | ```python 33 | pip install pybdshadow 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### Shadow generated by Sun light 39 | 40 | Detail usage can be found in [this example](https://github.com/ni1o1/pybdshadow/blob/main/example/Example1-building_shadow_analysis.ipynb). 41 | `pybdshadow` is capable of generating shadows from building geographic data. 42 | The buildings are usually store in the data as the form of Polygon object with `height` information (usually Shapefile or GeoJSON file). 43 | 44 | ```python 45 | import pandas as pd 46 | import geopandas as gpd 47 | #Read building GeoJSON data 48 | buildings = gpd.read_file(r'data/bd_demo_2.json') 49 | ``` 50 | 51 | Given a building GeoDataFrame and UTC datetime, `pybdshadow` can calculate the building shadow based on the sun position obtained by `suncalc-py`. 52 | 53 | ```python 54 | import pybdshadow 55 | #Given UTC datetime 56 | date = pd.to_datetime('2022-01-01 12:45:33.959797119')\ 57 | .tz_localize('Asia/Shanghai')\ 58 | .tz_convert('UTC') 59 | #Calculate building shadow for sun light 60 | shadows = pybdshadow.bdshadow_sunlight(buildings,date) 61 | ``` 62 | 63 | Visualize buildings and shadows using matplotlib. 64 | 65 | ```python 66 | import matplotlib.pyplot as plt 67 | fig = plt.figure(1, (12, 12)) 68 | ax = plt.subplot(111) 69 | # plot buildings 70 | buildings.plot(ax=ax) 71 | # plot shadows 72 | shadows['type'] += ' shadow' 73 | shadows.plot(ax=ax, alpha=0.7, 74 | column='type', 75 | categorical=True, 76 | cmap='Set1_r', 77 | legend=True) 78 | plt.show() 79 | ``` 80 | 81 | ![1651741110878.png](image/README/1651741110878.png) 82 | 83 | `pybdshadow` also provide visualization method supported by keplergl. 84 | 85 | ```python 86 | # visualize buildings and shadows 87 | pybdshadow.show_bdshadow(buildings = buildings,shadows = shadows) 88 | ``` 89 | 90 | ![1649161376291.png](https://github.com/ni1o1/pybdshadow/raw/main/image/README/1649161376291_1.png) 91 | 92 | ### Shadow generated by Point light 93 | 94 | `pybdshadow` can also calculate the building shadow generated by point light. Given coordinates and height of the point light: 95 | 96 | ```python 97 | #Calculate building shadow for point light 98 | shadows = pybdshadow.bdshadow_pointlight(buildings,139.713319,35.552040,200) 99 | #Visualize buildings and shadows 100 | pybdshadow.show_bdshadow(buildings = buildings,shadows = shadows) 101 | ``` 102 | 103 | ![1649405838683.png](https://github.com/ni1o1/pybdshadow/raw/main/image/README/1649405838683_1.png) 104 | 105 | ### Shadow coverage analysis 106 | 107 | `pybdshadow` provides the functionality to analysis sunshine time on the roof and on the ground. 108 | 109 | Result of shadow coverage on the roof: 110 | 111 | ![1651645524782.png](image/README/1651645524782.png)![1651975815798.png](image/README/1651975815798.png) 112 | 113 | Result of sunshine time on the ground: 114 | 115 | ![1651645530892.png](image/README/1651645530892.png)![1651975824187.png](image/README/1651975824187.png) 116 | 117 | ## Dependency 118 | 119 | `pybdshadow` depends on the following packages 120 | 121 | * `numpy` 122 | * `pandas` 123 | * `shapely` 124 | * `rtree` 125 | * `geopandas` 126 | * `matplotlib` 127 | * [`suncalc`](https://github.com/kylebarron/suncalc-py) 128 | * [`keplergl`](https://kepler.gl/) 129 | * [`TransBigData`](https://github.com/ni1o1/transbigdata) 130 | 131 | ## Citation information 132 | 133 | Citation information can be found at [CITATION.cff](https://github.com/ni1o1/pybdshadow/blob/main/CITATION.cff). 134 | 135 | ## Contributing to pybdshadow [![GitHub contributors](https://img.shields.io/github/contributors/ni1o1/pybdshadow.svg)](https://github.com/ni1o1/pybdshadow/graphs/contributors) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ni1o1/pybdshadow) 136 | 137 | All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. A detailed overview on how to contribute can be found in the [contributing guide](https://github.com/ni1o1/pybdshadow/blob/master/CONTRIBUTING.md) on GitHub. 138 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 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 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /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% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/Visualization.rst: -------------------------------------------------------------------------------- 1 | .. _Visualization: 2 | 3 | .. currentmodule:: pybdshadow 4 | 5 | ***************************** 6 | Visualization 7 | ***************************** 8 | 9 | Visualization Settings in Jupyter 10 | -------------------------------------- 11 | 12 | | The `pybdshadow`` package provide visualization methods based on the visualization plugin provided by `kepler.gl`. 13 | 14 | If you want to display the visualization results in jupyter notebook, you need to check the jupyter-js-widgets (which may need to be installed separately) and keplergl-jupyter plugins 15 | 16 | .. image:: _static/jupytersettings.png 17 | 18 | Visualization 19 | -------------------------------------- 20 | 21 | .. autofunction:: show_bdshadow -------------------------------------------------------------------------------- /docs/source/_static/default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Alternate Sphinx design 3 | * Originally created by Armin Ronacher for Werkzeug, adapted by Georg Brandl. 4 | */ 5 | 6 | body { 7 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; 8 | font-size: 14px; 9 | letter-spacing: -0.01em; 10 | line-height: 150%; 11 | text-align: center; 12 | /*background-color: #AFC1C4; */ 13 | background-color: #BFD1D4; 14 | color: black; 15 | padding: 0; 16 | border: 1px solid #aaa; 17 | 18 | margin: 0px 80px 0px 80px; 19 | min-width: 740px; 20 | } 21 | 22 | a { 23 | color: #CA7900; 24 | text-decoration: none; 25 | } 26 | 27 | a:hover { 28 | color: #2491CF; 29 | } 30 | 31 | pre { 32 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 33 | font-size: 0.95em; 34 | letter-spacing: 0.015em; 35 | padding: 0.5em; 36 | border: 1px solid #ccc; 37 | background-color: #f8f8f8; 38 | } 39 | 40 | td.linenos pre { 41 | padding: 0.5em 0; 42 | border: 0; 43 | background-color: transparent; 44 | color: #aaa; 45 | } 46 | 47 | table.highlighttable { 48 | margin-left: 0.5em; 49 | } 50 | 51 | table.highlighttable td { 52 | padding: 0 0.5em 0 0.5em; 53 | } 54 | 55 | cite, code, tt { 56 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 57 | font-size: 0.95em; 58 | letter-spacing: 0.01em; 59 | } 60 | 61 | hr { 62 | border: 1px solid #abc; 63 | margin: 2em; 64 | } 65 | 66 | tt { 67 | background-color: #f2f2f2; 68 | border-bottom: 1px solid #ddd; 69 | color: #333; 70 | } 71 | 72 | tt.descname { 73 | background-color: transparent; 74 | font-weight: bold; 75 | font-size: 1.2em; 76 | border: 0; 77 | } 78 | 79 | tt.descclassname { 80 | background-color: transparent; 81 | border: 0; 82 | } 83 | 84 | tt.xref { 85 | background-color: transparent; 86 | font-weight: bold; 87 | border: 0; 88 | } 89 | 90 | a tt { 91 | background-color: transparent; 92 | font-weight: bold; 93 | border: 0; 94 | color: #CA7900; 95 | } 96 | 97 | a tt:hover { 98 | color: #2491CF; 99 | } 100 | 101 | dl { 102 | margin-bottom: 15px; 103 | } 104 | 105 | dd p { 106 | margin-top: 0px; 107 | } 108 | 109 | dd ul, dd table { 110 | margin-bottom: 10px; 111 | } 112 | 113 | dd { 114 | margin-top: 3px; 115 | margin-bottom: 10px; 116 | margin-left: 30px; 117 | } 118 | 119 | .refcount { 120 | color: #060; 121 | } 122 | 123 | dt:target, 124 | .highlight { 125 | background-color: #fbe54e; 126 | } 127 | 128 | dl.class, dl.function { 129 | border-top: 2px solid #888; 130 | } 131 | 132 | dl.method, dl.attribute { 133 | border-top: 1px solid #aaa; 134 | } 135 | 136 | dl.glossary dt { 137 | font-weight: bold; 138 | font-size: 1.1em; 139 | } 140 | 141 | pre { 142 | line-height: 120%; 143 | } 144 | 145 | pre a { 146 | color: inherit; 147 | text-decoration: underline; 148 | } 149 | 150 | .first { 151 | margin-top: 0 !important; 152 | } 153 | 154 | div.document { 155 | background-color: white; 156 | text-align: left; 157 | background-image: url(contents.png); 158 | background-repeat: repeat-x; 159 | } 160 | 161 | /* 162 | div.documentwrapper { 163 | width: 100%; 164 | } 165 | */ 166 | 167 | div.clearer { 168 | clear: both; 169 | } 170 | 171 | div.related h3 { 172 | display: none; 173 | } 174 | 175 | div.related ul { 176 | background-image: url(navigation.png); 177 | height: 2em; 178 | list-style: none; 179 | border-top: 1px solid #ddd; 180 | border-bottom: 1px solid #ddd; 181 | margin: 0; 182 | padding-left: 10px; 183 | } 184 | 185 | div.related ul li { 186 | margin: 0; 187 | padding: 0; 188 | height: 2em; 189 | float: left; 190 | } 191 | 192 | div.related ul li.right { 193 | float: right; 194 | margin-right: 5px; 195 | } 196 | 197 | div.related ul li a { 198 | margin: 0; 199 | padding: 0 5px 0 5px; 200 | line-height: 1.75em; 201 | color: #EE9816; 202 | } 203 | 204 | div.related ul li a:hover { 205 | color: #3CA8E7; 206 | } 207 | 208 | div.body { 209 | margin: 0; 210 | padding: 0.5em 20px 20px 20px; 211 | } 212 | 213 | div.bodywrapper { 214 | margin: 0 240px 0 0; 215 | border-right: 1px solid #ccc; 216 | } 217 | 218 | div.body a { 219 | text-decoration: underline; 220 | } 221 | 222 | div.sphinxsidebar { 223 | margin: 0; 224 | padding: 0.5em 15px 15px 0; 225 | width: 210px; 226 | float: right; 227 | text-align: left; 228 | /* margin-left: -100%; */ 229 | } 230 | 231 | div.sphinxsidebar h4, div.sphinxsidebar h3 { 232 | margin: 1em 0 0.5em 0; 233 | font-size: 0.9em; 234 | padding: 0.1em 0 0.1em 0.5em; 235 | color: white; 236 | border: 1px solid #86989B; 237 | background-color: #AFC1C4; 238 | } 239 | 240 | div.sphinxsidebar ul { 241 | padding-left: 1.5em; 242 | margin-top: 7px; 243 | list-style: none; 244 | padding: 0; 245 | line-height: 130%; 246 | } 247 | 248 | div.sphinxsidebar ul ul { 249 | list-style: square; 250 | margin-left: 20px; 251 | } 252 | 253 | p { 254 | margin: 0.8em 0 0.5em 0; 255 | } 256 | 257 | p.rubric { 258 | font-weight: bold; 259 | } 260 | 261 | h1 { 262 | margin: 0; 263 | padding: 0.7em 0 0.3em 0; 264 | font-size: 1.5em; 265 | color: #11557C; 266 | } 267 | 268 | h2 { 269 | margin: 1.3em 0 0.2em 0; 270 | font-size: 1.35em; 271 | padding: 0; 272 | } 273 | 274 | h3 { 275 | margin: 1em 0 -0.3em 0; 276 | font-size: 1.2em; 277 | } 278 | 279 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { 280 | color: black!important; 281 | } 282 | 283 | h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { 284 | display: none; 285 | margin: 0 0 0 0.3em; 286 | padding: 0 0.2em 0 0.2em; 287 | color: #aaa!important; 288 | } 289 | 290 | h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, 291 | h5:hover a.anchor, h6:hover a.anchor { 292 | display: inline; 293 | } 294 | 295 | h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, 296 | h5 a.anchor:hover, h6 a.anchor:hover { 297 | color: #777; 298 | background-color: #eee; 299 | } 300 | 301 | table { 302 | border-collapse: collapse; 303 | margin: 0 -0.5em 0 -0.5em; 304 | } 305 | 306 | table td, table th { 307 | padding: 0.2em 0.5em 0.2em 0.5em; 308 | } 309 | 310 | div.footer { 311 | background-color: #E3EFF1; 312 | color: #86989B; 313 | padding: 3px 8px 3px 0; 314 | clear: both; 315 | font-size: 0.8em; 316 | text-align: right; 317 | } 318 | 319 | div.footer a { 320 | color: #86989B; 321 | text-decoration: underline; 322 | } 323 | 324 | div.pagination { 325 | margin-top: 2em; 326 | padding-top: 0.5em; 327 | border-top: 1px solid black; 328 | text-align: center; 329 | } 330 | 331 | div.sphinxsidebar ul.toc { 332 | margin: 1em 0 1em 0; 333 | padding: 0 0 0 0.5em; 334 | list-style: none; 335 | } 336 | 337 | div.sphinxsidebar ul.toc li { 338 | margin: 0.5em 0 0.5em 0; 339 | font-size: 0.9em; 340 | line-height: 130%; 341 | } 342 | 343 | div.sphinxsidebar ul.toc li p { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | div.sphinxsidebar ul.toc ul { 349 | margin: 0.2em 0 0.2em 0; 350 | padding: 0 0 0 1.8em; 351 | } 352 | 353 | div.sphinxsidebar ul.toc ul li { 354 | padding: 0; 355 | } 356 | 357 | div.admonition, div.warning { 358 | font-size: 0.9em; 359 | margin: 1em 0 0 0; 360 | border: 1px solid #86989B; 361 | background-color: #f7f7f7; 362 | } 363 | 364 | div.admonition p, div.warning p { 365 | margin: 0.5em 1em 0.5em 1em; 366 | padding: 0; 367 | } 368 | 369 | div.admonition pre, div.warning pre { 370 | margin: 0.4em 1em 0.4em 1em; 371 | } 372 | 373 | div.admonition p.admonition-title, 374 | div.warning p.admonition-title { 375 | margin: 0; 376 | padding: 0.1em 0 0.1em 0.5em; 377 | color: white; 378 | border-bottom: 1px solid #86989B; 379 | font-weight: bold; 380 | background-color: #AFC1C4; 381 | } 382 | 383 | div.warning { 384 | border: 1px solid #940000; 385 | } 386 | 387 | div.warning p.admonition-title { 388 | background-color: #CF0000; 389 | border-bottom-color: #940000; 390 | } 391 | 392 | div.admonition ul, div.admonition ol, 393 | div.warning ul, div.warning ol { 394 | margin: 0.1em 0.5em 0.5em 3em; 395 | padding: 0; 396 | } 397 | 398 | div.versioninfo { 399 | margin: 1em 0 0 0; 400 | border: 1px solid #ccc; 401 | background-color: #DDEAF0; 402 | padding: 8px; 403 | line-height: 1.3em; 404 | font-size: 0.9em; 405 | } 406 | 407 | 408 | a.headerlink { 409 | color: #c60f0f!important; 410 | font-size: 1em; 411 | margin-left: 6px; 412 | padding: 0 4px 0 4px; 413 | text-decoration: none!important; 414 | visibility: hidden; 415 | } 416 | 417 | h1:hover > a.headerlink, 418 | h2:hover > a.headerlink, 419 | h3:hover > a.headerlink, 420 | h4:hover > a.headerlink, 421 | h5:hover > a.headerlink, 422 | h6:hover > a.headerlink, 423 | dt:hover > a.headerlink { 424 | visibility: visible; 425 | } 426 | 427 | a.headerlink:hover { 428 | background-color: #ccc; 429 | color: white!important; 430 | } 431 | 432 | table.indextable td { 433 | text-align: left; 434 | vertical-align: top; 435 | } 436 | 437 | table.indextable dl, table.indextable dd { 438 | margin-top: 0; 439 | margin-bottom: 0; 440 | } 441 | 442 | table.indextable tr.pcap { 443 | height: 10px; 444 | } 445 | 446 | table.indextable tr.cap { 447 | margin-top: 10px; 448 | background-color: #f2f2f2; 449 | } 450 | 451 | img.toggler { 452 | margin-right: 3px; 453 | margin-top: 3px; 454 | cursor: pointer; 455 | } 456 | 457 | img.inheritance { 458 | border: 0px 459 | } 460 | 461 | form.pfform { 462 | margin: 10px 0 20px 0; 463 | } 464 | 465 | table.contentstable { 466 | width: 90%; 467 | } 468 | 469 | table.contentstable p.biglink { 470 | line-height: 150%; 471 | } 472 | 473 | a.biglink { 474 | font-size: 1.3em; 475 | } 476 | 477 | span.linkdescr { 478 | font-style: italic; 479 | padding-top: 5px; 480 | font-size: 90%; 481 | } 482 | 483 | ul.search { 484 | margin: 10px 0 0 20px; 485 | padding: 0; 486 | } 487 | 488 | ul.search li { 489 | padding: 5px 0 5px 20px; 490 | background-image: url(file.png); 491 | background-repeat: no-repeat; 492 | background-position: 0 7px; 493 | } 494 | 495 | ul.search li a { 496 | font-weight: bold; 497 | } 498 | 499 | ul.search li div.context { 500 | color: #888; 501 | margin: 2px 0 0 30px; 502 | text-align: left; 503 | } 504 | 505 | ul.keywordmatches li.goodmatch a { 506 | font-weight: bold; 507 | } 508 | -------------------------------------------------------------------------------- /docs/source/_static/jupytersettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/_static/jupytersettings.png -------------------------------------------------------------------------------- /docs/source/_static/logo-wordmark-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/_static/logo-wordmark-dark.png -------------------------------------------------------------------------------- /docs/source/_static/logo-wordmark-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/_static/logo-wordmark-light.png -------------------------------------------------------------------------------- /docs/source/_static/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/_static/logo.ico -------------------------------------------------------------------------------- /docs/source/_static/visualize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/_static/visualize.png -------------------------------------------------------------------------------- /docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | 4 | {% block rootrellink %} 5 |
  • 主页
  • 6 |
  • 搜索
  • 7 | {% endblock %} 8 | 9 | 10 | {% block relbar1 %} 11 | 12 |
    13 |

    TransBigData

    14 |
    15 | {{ super() }} 16 | {% endblock %} 17 | 18 | {# put the sidebar before the body #} 19 | {% block sidebar1 %}{{ sidebar() }}{% endblock %} 20 | {% block sidebar2 %}{% endblock %} 21 | {% block sidebar3 %}{% endblock %} -------------------------------------------------------------------------------- /docs/source/analysis.rst: -------------------------------------------------------------------------------- 1 | .. _analysis: 2 | 3 | 4 | ***************************** 5 | Shadow coverage 6 | ***************************** 7 | 8 | .. currentmodule:: pybdshadow 9 | 10 | Shadow coverage 11 | -------------------------------------- 12 | 13 | .. autofunction:: cal_sunshine 14 | 15 | .. autofunction:: cal_sunshadows 16 | 17 | .. autofunction:: cal_shadowcoverage 18 | -------------------------------------------------------------------------------- /docs/source/bdshadow.rst: -------------------------------------------------------------------------------- 1 | .. _bdshadow: 2 | 3 | 4 | ********************* 5 | Building shadow 6 | ********************* 7 | 8 | .. currentmodule:: pybdshadow 9 | 10 | 11 | Shadow from sunlight 12 | -------------------------------------- 13 | 14 | .. autofunction:: bdshadow_sunlight 15 | 16 | Shadow from pointlight 17 | -------------------------------------- 18 | 19 | .. autofunction:: bdshadow_pointlight -------------------------------------------------------------------------------- /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 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 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 | import sys 21 | import os 22 | project = 'pybdshadow' 23 | copyright = '2022, Qing Yu' 24 | author = 'Qing Yu' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = '0.3.4' 28 | version = '0.3.4' 29 | html_logo = "_static/logo-wordmark-light.png" 30 | html_favicon = '_static/logo.ico' 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = ['sphinx.ext.autodoc', 37 | 'sphinx.ext.napoleon' ] 38 | napoleon_google_docstring = False 39 | napoleon_numpy_docstring = True 40 | 41 | sys.path.insert(0, os.path.abspath('../../src')) 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The language for content autogenerated by Sphinx. Refer to documentation 47 | # for a list of supported languages. 48 | # 49 | # This is also used if you do content translation via gettext catalogs. 50 | # Usually you set "language" from the command line for these cases. 51 | language = 'zh_CN' 52 | locale_dirs = ['../locale/'] # path is example but recommended. 53 | gettext_compact = False # optional. 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 | 61 | # -- Options for HTML output ------------------------------------------------- 62 | 63 | # The theme to use for HTML and HTML Help pages. See the documentation for 64 | # a list of builtin themes. 65 | # 66 | html_theme = 'sphinx_rtd_theme' 67 | html_sidebars = { 68 | '**': [ 69 | 'localtoc.html', 70 | 'relations.html', 71 | 'searchbox.html', 72 | 'sourcelink.html', 73 | ] 74 | } 75 | html_theme_options = { 76 | 'logo_only': True, 77 | 'display_version': True, 78 | } 79 | latex_logo = '_static/logo-wordmark-dark.png' 80 | # Add any paths that contain custom static files (such as style sheets) here, 81 | # relative to this directory. They are copied after the builtin static files, 82 | # so a file named "default.css" will overwrite the builtin "default.css". 83 | html_static_path = ['_static'] 84 | -------------------------------------------------------------------------------- /docs/source/example/example.rst: -------------------------------------------------------------------------------- 1 | Building shadow analysis 2 | ============================== 3 | 4 | Notebook for this example: `here `__. 5 | 6 | In this example, we will introduce how to use ``pybdshadow`` to 7 | generate, analyze and visualize the building shadow data 8 | 9 | 10 | 11 | Building data preprocessing 12 | ----------------------------- 13 | 14 | Building data can be obtain by Python package 15 | `OSMnx `__ from OpenStreetMap 16 | (Some of the buildings do not contain the height information). 17 | 18 | The buildings are usually store in the data as the form of Polygon 19 | object with ``height`` column. Here, we provide a demo building data 20 | store as GeoJSON file to demonstrate the functionality of ``pybdshadow`` 21 | 22 | :: 23 | 24 | import pandas as pd 25 | import geopandas as gpd 26 | import pybdshadow 27 | #Read building data 28 | buildings = gpd.read_file(r'../example/data/bd_demo_2.json') 29 | buildings.head(5) 30 | 31 | 32 | 33 | 34 | .. raw:: html 35 | 36 |
    37 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
    IdFloorheightxygeometry
    0026.0120.59731331.309152POLYGON ((120.59739 31.30921, 120.59740 31.309...
    1026.0120.59727631.309312POLYGON ((120.59737 31.30938, 120.59738 31.309...
    2026.0120.59731331.308982POLYGON ((120.59741 31.30905, 120.59742 31.308...
    3026.0120.59727231.309489POLYGON ((120.59735 31.30955, 120.59736 31.309...
    4026.0120.59712831.309778POLYGON ((120.59729 31.30986, 120.59730 31.309...
    110 |
    111 | 112 | 113 | 114 | The input building data must be a ``GeoDataFrame`` with the ``height`` 115 | column storing the building height information and the ``geometry`` 116 | column storing the geometry polygon information of building outline. 117 | 118 | :: 119 | 120 | #Plot the buildings 121 | buildings.plot(figsize=(12,12)) 122 | 123 | 124 | 125 | .. image:: output_6_1.png 126 | 127 | 128 | Before analysing buildings, make sure to preprocess building data using 129 | :func:`pybdshadow.bd_preprocess` before calculate shadow. It will remove 130 | empty polygons, convert multipolygons into polygons and generate 131 | ``building_id`` for each building. 132 | 133 | :: 134 | 135 | buildings = pybdshadow.bd_preprocess(buildings) 136 | buildings.head(5) 137 | 138 | 139 | 140 | 141 | .. raw:: html 142 | 143 |
    144 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
    geometryIdFloorheightxybuilding_id
    0POLYGON ((120.60496 31.29717, 120.60521 31.297...026.0120.60495131.2972070
    1POLYGON ((120.60494 31.29728, 120.60496 31.297...026.0120.60495131.2972071
    0POLYGON ((120.59739 31.30921, 120.59740 31.309...026.0120.59731331.3091522
    1POLYGON ((120.59737 31.30938, 120.59738 31.309...026.0120.59727631.3093123
    2POLYGON ((120.59741 31.30905, 120.59742 31.308...026.0120.59731331.3089824
    223 |
    224 | 225 | 226 | 227 | Generate building shadows 228 | ----------------------------- 229 | 230 | Shadow generated by Sun light 231 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 232 | 233 | Given a building GeoDataFrame and UTC datetime, ``pybdshadow`` can 234 | calculate the building shadow based on the sun position obtained by 235 | ``suncalc`` 236 | 237 | :: 238 | 239 | #Given UTC time 240 | date = pd.to_datetime('2022-01-01 12:45:33.959797119')\ 241 | .tz_localize('Asia/Shanghai')\ 242 | .tz_convert('UTC') 243 | #Calculate shadows 244 | shadows = pybdshadow.bdshadow_sunlight(buildings,date,roof=True,include_building = False) 245 | shadows 246 | 247 | 248 | 249 | 250 | .. raw:: html 251 | 252 |
    253 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 |
    heightbuilding_idgeometrytype
    06.0186POLYGON ((120.60080 31.30858, 120.60080 31.308...roof
    16.0524POLYGON EMPTYroof
    26.01009POLYGON ((120.60394 31.30111, 120.60394 31.301...roof
    36.02229MULTIPOLYGON (((120.61384 31.29957, 120.61384 ...roof
    46.02297POLYGON ((120.61328 31.29770, 120.61330 31.297...roof
    ...............
    30720.03072POLYGON ((120.61484 31.29058, 120.61484 31.290...ground
    30730.03073POLYGON ((120.61532 31.29039, 120.61532 31.290...ground
    30740.03074MULTIPOLYGON (((120.61499 31.29096, 120.61499 ...ground
    30750.03075POLYGON ((120.61472 31.29091, 120.61472 31.290...ground
    30760.03076POLYGON ((120.61491 31.29122, 120.61491 31.291...ground
    356 |

    3374 rows × 4 columns

    357 |
    358 | 359 | 360 | 361 | The generated shadow data is store as another ``GeoDataFrame``. It 362 | contains both rooftop shadow(with height over 0) and ground shadow(with 363 | height equal to 0). 364 | 365 | :: 366 | 367 | # Visualize buildings and shadows using matplotlib 368 | import matplotlib.pyplot as plt 369 | fig = plt.figure(1, (12, 12)) 370 | ax = plt.subplot(111) 371 | 372 | # plot buildings 373 | buildings.plot(ax=ax) 374 | 375 | # plot shadows 376 | shadows.plot(ax=ax, alpha=0.7, 377 | column='type', 378 | categorical=True, 379 | cmap='Set1_r', 380 | legend=True) 381 | 382 | plt.show() 383 | 384 | 385 | 386 | 387 | .. image:: output_14_0.png 388 | 389 | 390 | ``pybdshadow`` also provide 3D visualization method supported by 391 | keplergl. 392 | 393 | :: 394 | 395 | #Visualize using keplergl 396 | pybdshadow.show_bdshadow(buildings = buildings,shadows = shadows) 397 | 398 | 399 | .. figure:: https://github.com/ni1o1/pybdshadow/raw/main/image/README/1649161376291_1.png 400 | :alt: 1649161376291.png 401 | 402 | 1649161376291.png 403 | 404 | Shadow generated by Point light 405 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 406 | 407 | ``pybdshadow`` can generate the building shadow generated by point 408 | light, which can be potentially useful for visual area analysis in urban 409 | environment. Given coordinates and height of the point light: 410 | 411 | :: 412 | 413 | #Define the position and the height of the point light 414 | pointlon,pointlat,pointheight = [120.60820619503946,31.300141884245672,100] 415 | #Calculate building shadow for point light 416 | shadows = pybdshadow.bdshadow_pointlight(buildings,pointlon,pointlat,pointheight) 417 | #Visualize buildings and shadows 418 | pybdshadow.show_bdshadow(buildings = buildings,shadows = shadows) 419 | 420 | 421 | .. figure:: https://github.com/ni1o1/pybdshadow/raw/main/image/README/1649405838683_1.png 422 | :alt: 1649405838683.png 423 | 424 | 1649405838683.png 425 | 426 | Shadow coverage analysis 427 | ----------------------------- 428 | 429 | To demonstrate the analysis function of ``pybdshadow``, here we select a 430 | smaller area for detail analysis of shadow coverage. 431 | 432 | :: 433 | 434 | #define analysis area 435 | bounds = [120.603,31.303,120.605,31.305] 436 | #filter the buildings 437 | buildings['x'] = buildings.centroid.x 438 | buildings['y'] = buildings.centroid.y 439 | buildings_analysis = buildings[(buildings['x'] > bounds[0]) & 440 | (buildings['x'] < bounds[2]) & 441 | (buildings['y'] > bounds[1]) & 442 | (buildings['y'] < bounds[3])] 443 | buildings_analysis.plot() 444 | 445 | 446 | 447 | 448 | .. image:: output_24_1.png 449 | 450 | 451 | Use :func:`pybdshadow.cal_sunshine` to analyse shadow coverage and sunshine 452 | time. Here, we select ``2022-01-01`` as the date, set the spatial 453 | resolution of 1 meter*1 meter grids, and 900 s as the time interval. 454 | 455 | :: 456 | 457 | #calculate sunshine time on the building roof 458 | sunshine = pybdshadow.cal_sunshine(buildings_analysis, 459 | day='2022-01-01', 460 | roof=True, 461 | accuracy=1, 462 | precision=900) 463 | 464 | :: 465 | 466 | #Visualize buildings and sunshine time using matplotlib 467 | import matplotlib.pyplot as plt 468 | fig = plt.figure(1,(10,5)) 469 | ax = plt.subplot(111) 470 | #define colorbar 471 | cax = plt.axes([0.15, 0.33, 0.02, 0.3]) 472 | plt.title('Hour') 473 | #plot the sunshine time 474 | sunshine.plot(ax = ax,cmap = 'plasma',column ='Hour',alpha = 1,legend = True,cax = cax,) 475 | #Buildings 476 | buildings_analysis.plot(ax = ax,edgecolor='k',facecolor=(0,0,0,0)) 477 | plt.sca(ax) 478 | plt.title('Sunshine time') 479 | plt.show() 480 | 481 | 482 | 483 | .. image:: output_27_0.png 484 | 485 | 486 | :: 487 | 488 | #calculate sunshine time on the ground (set the roof to False) 489 | sunshine = pybdshadow.cal_sunshine(buildings_analysis, 490 | day='2022-01-01', 491 | roof=False, 492 | accuracy=1, 493 | precision=900) 494 | 495 | :: 496 | 497 | #Visualize buildings and sunshine time using matplotlib 498 | import matplotlib.pyplot as plt 499 | fig = plt.figure(1,(10,5)) 500 | ax = plt.subplot(111) 501 | #define colorbar 502 | cax = plt.axes([0.15, 0.33, 0.02, 0.3]) 503 | plt.title('Hour') 504 | #plot the sunshine time 505 | sunshine.plot(ax = ax,cmap = 'plasma',column ='Hour',alpha = 1,legend = True,cax = cax,) 506 | #Buildings 507 | buildings_analysis.plot(ax = ax,edgecolor='k',facecolor=(0,0,0,0)) 508 | plt.sca(ax) 509 | plt.title('Sunshine time') 510 | plt.show() 511 | 512 | 513 | 514 | .. image:: output_29_0.png 515 | 516 | 517 | We can change the date to see if it has different result: 518 | 519 | :: 520 | 521 | #calculate sunshine time on the ground (set the roof to False) 522 | sunshine = pybdshadow.cal_sunshine(buildings_analysis, 523 | day='2022-07-15', 524 | roof=False, 525 | accuracy=1, 526 | precision=900) 527 | #Visualize buildings and sunshine time using matplotlib 528 | import matplotlib.pyplot as plt 529 | fig = plt.figure(1,(10,5)) 530 | ax = plt.subplot(111) 531 | #define colorbar 532 | cax = plt.axes([0.15, 0.33, 0.02, 0.3]) 533 | plt.title('Hour') 534 | #plot the sunshine time 535 | sunshine.plot(ax = ax,cmap = 'plasma',column ='Hour',alpha = 1,legend = True,cax = cax,) 536 | #Buildings 537 | buildings_analysis.plot(ax = ax,edgecolor='k',facecolor=(0,0,0,0)) 538 | plt.sca(ax) 539 | plt.title('Sunshine time') 540 | plt.show() 541 | 542 | 543 | 544 | .. image:: output_31_0.png 545 | 546 | -------------------------------------------------------------------------------- /docs/source/example/output_14_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/example/output_14_0.png -------------------------------------------------------------------------------- /docs/source/example/output_24_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/example/output_24_1.png -------------------------------------------------------------------------------- /docs/source/example/output_27_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/example/output_27_0.png -------------------------------------------------------------------------------- /docs/source/example/output_29_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/example/output_29_0.png -------------------------------------------------------------------------------- /docs/source/example/output_31_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/example/output_31_0.png -------------------------------------------------------------------------------- /docs/source/example/output_6_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/docs/source/example/output_6_1.png -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pybdshadow documentation master file, created by 2 | sphinx-quickstart on Thu Oct 21 14:41:25 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | pybdshadow 7 | ======================================== 8 | 9 | .. image:: _static/logo-wordmark-dark.png 10 | 11 | Introduction 12 | --------------------------------- 13 | 14 | | `pybdshadow` is a python package for generating, analyzing and visualizing building shadows from large scale building geographic data. `pybdshadow` support generate building shadows from both sun light and point light. `pybdshadow` provides an efficient and easy-to-use method to generate a new source of geospatial data with great application potential in urban study. 15 | 16 | | The latest stable release of the software can be installed via pip and full documentation can be found [here](https://pybdshadow.readthedocs.io/en/latest/). 17 | 18 | 19 | Functionality 20 | --------------------------------- 21 | 22 | Currently, `pybdshadow` mainly provides the following methods: 23 | 24 | - **Generating building shadow from sun light**: With given location and time, the function in `pybdshadow` uses the properties of sun position obtained from `suncalc-py` and the building height to generate shadow geometry data. 25 | - **Generating building shadow from point light**: `pybdshadow` can generate the building shadow with given location and height of the point light, which can be potentially useful for visual area analysis in urban environment. 26 | - **Analysis**: `pybdshadow` integrated the analysing method based on the properties of sun movement to track the changing position of shadows within a fixed time interval. Based on the grid processing framework provided by `TransBigData`, `pybdshadow` is capable of calculating sunshine time on the ground and on the roof. 27 | - **Visualization**: Built-in visualization capabilities leverage the visualization package `keplergl` to interactively visualize building and shadow data in Jupyter notebooks with simple code. 28 | 29 | The target audience of `pybdshadow` includes data science researchers and data engineers in the field of BIM, GIS, energy, environment, and urban computing. 30 | 31 | 32 | Example 33 | --------------------------------- 34 | 35 | Given a building GeoDataFrame and UTC datetime, `pybdshadow` can calculate the building shadow based on the sun position obtained by `suncalc` 36 | 37 | :: 38 | 39 | import pybdshadow 40 | #Given UTC datetime 41 | date = pd.to_datetime('2022-01-01 12:45:33.959797119')\ 42 | .tz_localize('Asia/Shanghai')\ 43 | .tz_convert('UTC') 44 | #Calculate building shadow 45 | shadows = pybdshadow.bdshadow_sunlight(buildings,date) 46 | 47 | `pybdshadow` also provide visualization method supported by keplergl. 48 | 49 | :: 50 | 51 | # visualize buildings and shadows 52 | pybdshadow.show_bdshadow(buildings = buildings,shadows = shadows) 53 | 54 | .. image:: _static/visualize.png 55 | 56 | 57 | .. toctree:: 58 | :caption: Installation and dependencies 59 | :maxdepth: 2 60 | 61 | install.rst 62 | 63 | .. toctree:: 64 | :caption: Example 65 | :maxdepth: 2 66 | 67 | example/example.rst 68 | 69 | .. toctree:: 70 | :caption: Method 71 | :maxdepth: 2 72 | 73 | preprocess.rst 74 | bdshadow.rst 75 | analysis.rst 76 | Visualization.rst -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _install: 3 | 4 | 5 | ****************************** 6 | Installation and dependencies 7 | ****************************** 8 | 9 | 10 | Installation 11 | -------------------------------------- 12 | 13 | 14 | | It is recommended to use `Python 3.7, 3.8, 3.9`. 15 | | `pybdshadow` can be installed by using `pip install`. Before installing `pybdshadow`, make sure that you have installed the available `geopandas` package: https://geopandas.org/en/stable/getting_started/install.html. 16 | | If you already have geopandas installed, run the following code directly from the command prompt to install `pybdshadow`: 17 | 18 | :: 19 | 20 | pip install pybdshadow 21 | 22 | Dependency 23 | -------------------------------------- 24 | `pybdshadow` depends on the following packages 25 | 26 | * `numpy` 27 | * `pandas` 28 | * `shapely` 29 | * `rtree` 30 | * `geopandas` 31 | * `matplotlib` 32 | * `suncalc` 33 | * `keplergl` 34 | * `TransBigData` 35 | -------------------------------------------------------------------------------- /docs/source/preprocess.rst: -------------------------------------------------------------------------------- 1 | .. _preprocess: 2 | 3 | .. currentmodule:: pybdshadow 4 | 5 | ********************* 6 | Building Preprocess 7 | ********************* 8 | 9 | Building preprocess 10 | -------------------------------------- 11 | 12 | .. autofunction:: bd_preprocess -------------------------------------------------------------------------------- /example/data/Suzhoubuildings/苏州(1).dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/example/data/Suzhoubuildings/苏州(1).dbf -------------------------------------------------------------------------------- /example/data/Suzhoubuildings/苏州(1).prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] -------------------------------------------------------------------------------- /example/data/Suzhoubuildings/苏州(1).sbn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/example/data/Suzhoubuildings/苏州(1).sbn -------------------------------------------------------------------------------- /example/data/Suzhoubuildings/苏州(1).sbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/example/data/Suzhoubuildings/苏州(1).sbx -------------------------------------------------------------------------------- /example/data/Suzhoubuildings/苏州(1).shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/example/data/Suzhoubuildings/苏州(1).shp -------------------------------------------------------------------------------- /example/data/Suzhoubuildings/苏州(1).shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/example/data/Suzhoubuildings/苏州(1).shx -------------------------------------------------------------------------------- /image/README/1649074615552.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1649074615552.png -------------------------------------------------------------------------------- /image/README/1649161376291_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1649161376291_1.png -------------------------------------------------------------------------------- /image/README/1649405838683_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1649405838683_1.png -------------------------------------------------------------------------------- /image/README/1651490411329.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1651490411329.png -------------------------------------------------------------------------------- /image/README/1651490416315.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1651490416315.png -------------------------------------------------------------------------------- /image/README/1651506285290.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1651506285290.png -------------------------------------------------------------------------------- /image/README/1651645524782.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1651645524782.png -------------------------------------------------------------------------------- /image/README/1651645530892.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1651645530892.png -------------------------------------------------------------------------------- /image/README/1651741110878.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1651741110878.png -------------------------------------------------------------------------------- /image/README/1651975815798.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1651975815798.png -------------------------------------------------------------------------------- /image/README/1651975824187.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/README/1651975824187.png -------------------------------------------------------------------------------- /image/paper/1651656639873.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/paper/1651656639873.png -------------------------------------------------------------------------------- /image/paper/1651656857394.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni1o1/pybdshadow/e2665ee7a6527b9fb0d581bbf7d6dc2914015b02/image/paper/1651656857394.png -------------------------------------------------------------------------------- /paper.bib: -------------------------------------------------------------------------------- 1 | @INPROCEEDINGS{9267879-1, 2 | author={Ivanov, Sergey and Nikolskaya, Ksenia and Radchenko, Gleb and Sokolinsky, Leonid and Zymbler, Mikhail}, 3 | booktitle={2020 Global Smart Industry Conference (GloSIC)}, 4 | title={Digital Twin of City: Concept Overview}, 5 | year={2020}, 6 | volume={}, 7 | number={}, 8 | pages={178-186}, 9 | doi={10.1109/GloSIC50886.2020.9267879}} 10 | 11 | @INPROCEEDINGS{9254288-2, 12 | author={Erol, Tolga and Mendi, Arif Furkan and Doğan, Dilara}, 13 | booktitle={2020 4th International Symposium on Multidisciplinary Studies and Innovative Technologies (ISMSIT)}, 14 | title={Digital Transformation Revolution with Digital Twin Technology}, 15 | year={2020}, 16 | volume={}, 17 | number={}, 18 | pages={1-7}, 19 | doi={10.1109/ISMSIT50672.2020.9254288}} 20 | 21 | @article{DAI201977-3, 22 | title = {Thermal impacts of greenery, water, and impervious structures in Beijing’s Olympic area: A spatial regression approach}, 23 | journal = {Ecological Indicators}, 24 | volume = {97}, 25 | pages = {77-88}, 26 | year = {2019}, 27 | issn = {1470-160X}, 28 | doi = {10.1016/j.ecolind.2018.09.041}, 29 | url = {https://www.sciencedirect.com/science/article/pii/S1470160X18307386}, 30 | author = {Zhaoxin Dai and Jean-Michel Guldmann and Yunfeng Hu}, 31 | keywords = {Urban heat island, Land uses, Trees, Grass, Spatial autocorrelation}, 32 | abstract = {This paper explores the urban land-use determinants of the urban heat island (UHI) in Beijing’s Olympic Area, using different statistical models, land surface temperatures (LST) derived from Landsat 8 remote sensing, and land-use data derived from 1-m high-resolution imagery. Data are captured over grids of different sizes. Spatial regressions are necessary to capture neighboring effects, particularly when the grid unit is small. Grass, trees, water bodies, and shades have all significant and negative effects on LST, whereas buildings, roads and other impervious surfaces have all significant and positive effects. The results also point to significant nonlinear and interaction effects of grass, trees and water, particularly when the grid cell size is small (60 m-90 m). Trees are found to be the most important predictor of LST. When the grids are smaller than 180 m, the indirect impacts are larger than the direct ones, whereas, the opposite takes place for larger grids. Because of their strong performance (R2 ranging from 0.839 to 0.970), the models can be used for predicting the impacts of land-use changes on the UHI and as tools for urban planning. Finally, extensive uncertainty and sensitivity analyses show that the models are very reliable in terms of both input data accuracy and estimated coefficients precision.} 33 | } 34 | 35 | @article{PARK2021101655-4, 36 | title = {Impacts of tree and building shades on the urban heat island: Combining remote sensing, 3D digital city and spatial regression approaches}, 37 | journal = {Computers, Environment and Urban Systems}, 38 | volume = {88}, 39 | pages = {101655}, 40 | year = {2021}, 41 | issn = {0198-9715}, 42 | doi = {10.1016/j.compenvurbsys.2021.101655}, 43 | url = {https://www.sciencedirect.com/science/article/pii/S0198971521000624}, 44 | author = {Yujin Park and Jean-Michel Guldmann and Desheng Liu}, 45 | keywords = {3D city model, Tree shade, Shade location, Urban heat mitigation, Greening scenario, Spatial regression}, 46 | abstract = {The continued increase in average and extreme temperatures around the globe is expected to strike urban communities more harshly because of the urban heat island (UHI). Devising natural and design-based solutions to stem the rising heat has become an important urban planning issue. Recent studies have examined the impacts of 2D/3D urban land-use structures on land surface temperature (LST), but with little attention to the shades cast by 3D objects, such as buildings and trees. It is, however, known that shades are particularly relevant for controlling summertime temperatures. This study examines the role of urban shades created by trees and buildings, focusing on the effects of shade extent and location on LST mitigation. A realistic 3D digital representation of urban and suburban landscapes, combined with detailed 2D land cover information, is developed. Shadows projected on horizontal and vertical surfaces are obtained through GIS analysis, and then quantified as independent variables explaining LST variations over grids of varying sizes with spatial regression models. The estimation results show that the shades on different 3D surfaces, including building rooftops, sun-facing façades, not-sun-facing façades, and on 2D surfaces including roadways, other paved covers, and grass, have cooling effects of varying impact, showing that shades clearly modify the thermal effects of urban built-up surfaces. Tree canopy volume has distinct effects on LST via evapotranspiration. One of the estimated models is used, after validation, to simulate the LST impacts of neighborhood scenarios involving additional greening. The findings illustrate how urban planners can use the proposed methodology to design 3D land-use solutions for effective heat mitigation.} 47 | } 48 | 49 | @article{WU2021116884-5, 50 | title = {Coupled optical-electrical-thermal analysis of a semi-transparent photovoltaic glazing façade under building shadow}, 51 | journal = {Applied Energy}, 52 | volume = {292}, 53 | pages = {116884}, 54 | year = {2021}, 55 | issn = {0306-2619}, 56 | doi = {10.1016/j.apenergy.2021.116884}, 57 | url = {https://www.sciencedirect.com/science/article/pii/S030626192100372X}, 58 | author = {Jing Wu and Ling Zhang and Zhongbing Liu and Zhenghong Wu}, 59 | keywords = {Semi-transparent glazing façade, Building shadow, Three-dimensional heat transfer, Implicit finite difference, Optical-electrical-thermal simulation}, 60 | abstract = {The semi-transparent photovoltaic glazing (STPVG) façade can introduce comfortable daylight into the indoor space and achieve energy efficiency, which is a promising PV glazing façade system. However, it is susceptible to building shadow, reducing power generation efficiency. This paper established a coupled optical-electrical-thermal model under dynamic changing building eave shadow of the STPVG façade and built a full-scale experiment platform to test and verify the coupled model. The model was then used to simulate and analyze the electrical performance and the temperature distribution of the STPVG under different eave shadow. The results show that the I/V curve appears multi-knee shape and the P/V curve appears multi-peak shape due to the different shadow coefficient in each PV string. Furthermore, the annual overall energy performance of STPVG in Changsha, China was compared with different eave width. The transmitted solar radiation, the energy generation and energy conversion efficiency, and the total heat gain decrease with the eave width increases in the months when the shadow appears. When the eave width is 0.29 m, the monthly largest transmission loss rate is in May at 3.86%; the largest energy generation loss rate is in April at 15.3%; and the largest indoor heat gain reduction rate is in August at 3.28%. This study can provide theoretical guidance for the system optimization and engineering application of the STPVG in building energy conservation.} 61 | } 62 | 63 | @article{YADAV201811-6, 64 | title = {Performance of building integrated photovoltaic thermal system with PV module installed at optimum tilt angle and influenced by shadow}, 65 | journal = {Renewable Energy}, 66 | volume = {127}, 67 | pages = {11-23}, 68 | year = {2018}, 69 | issn = {0960-1481}, 70 | doi = {10.1016/j.renene.2018.04.030}, 71 | url = {https://www.sciencedirect.com/science/article/pii/S0960148118304373}, 72 | author = {S. Yadav and S.K. Panda and M. Tripathy}, 73 | keywords = {BIPV thermal system, Optimum tilt angle, HDKR model, Energy equilibrium equation, Sky view factor, Shading coefficient}, 74 | abstract = {Building integrated photovoltaic (BIPV) thermal system is an efficient system for urban applications to convert a building to net zero energy buildings by utilizing solar insolation. In this study, HDKR/S (Hay, Davies, Klucher, Reindl/shadow) model is developed which is a modified HDKR model where influence of shadow is incorporated in the mathematical model. Four discrete rectangular buildings situated in four directions (North, South, East and West) around a BIPV thermal system are considered for creating adverse effect of shadow. Variation of width (B), storey height (H) and horizontal distance (D) of these surrounded buildings are taken into account for evaluating optimum tilt angle, insolation and performance of BIPV thermal system by introducing corresponding shadow effects. The performance of the system is adversely affected because of the presence of surrounded building located at close proximity i.e., due to higher influence of shading and sky view blocking effects.} 75 | } 76 | 77 | 78 | @Article{rs13152862-7, 79 | AUTHOR = {Xie, Yakun and Feng, Dejun and Xiong, Sifan and Zhu, Jun and Liu, Yangge}, 80 | TITLE = {Multi-Scene Building Height Estimation Method Based on Shadow in High Resolution Imagery}, 81 | JOURNAL = {Remote Sensing}, 82 | VOLUME = {13}, 83 | YEAR = {2021}, 84 | NUMBER = {15}, 85 | ARTICLE-NUMBER = {2862}, 86 | URL = {https://www.mdpi.com/2072-4292/13/15/2862}, 87 | ISSN = {2072-4292}, 88 | ABSTRACT = {Accurately building height estimation from remote sensing imagery is an important and challenging task. However, the existing shadow-based building height estimation methods have large errors due to the complex environment in remote sensing imagery. In this paper, we propose a multi-scene building height estimation method based on shadow in high resolution imagery. First, the shadow of building is classified and described by analyzing the features of building shadow in remote sensing imagery. Second, a variety of shadow-based building height estimation models is established in different scenes. In addition, a method of shadow regularization extraction is proposed, which can solve the problem of mutual adhesion shadows in dense building areas effectively. Finally, we propose a method for shadow length calculation combines with the fish net and the pauta criterion, which means that the large error caused by the complex shape of building shadow can be avoided. Multi-scene areas are selected for experimental analysis to prove the validity of our method. The experiment results show that the accuracy rate is as high as 96% within 2 m of absolute error of our method. In addition, we compared our proposed approach with the existing methods, and the results show that the absolute error of our method are reduced by 1.24 m–3.76 m, which can achieve high-precision estimation of building height.}, 89 | DOI = {10.3390/rs13152862} 90 | } 91 | 92 | 93 | @article{CHEN2020114-8, 94 | title = {An end-to-end shape modeling framework for vectorized building outline generation from aerial images}, 95 | journal = {ISPRS Journal of Photogrammetry and Remote Sensing}, 96 | volume = {170}, 97 | pages = {114-126}, 98 | year = {2020}, 99 | issn = {0924-2716}, 100 | doi = {10.1016/j.isprsjprs.2020.10.008}, 101 | url = {https://www.sciencedirect.com/science/article/pii/S092427162030280X}, 102 | author = {Qi Chen and Lei Wang and Steven L. Waslander and Xiuguo Liu}, 103 | keywords = {Building segmentation, Boundary optimization, Automatic mapping, Deep learning, Shape modeling}, 104 | abstract = {The identification and annotation of buildings has long been a tedious and expensive part of high-precision vector map production. The deep learning techniques such as fully convolution network (FCN) have largely promoted the accuracy of automatic building segmentation from remote sensing images. However, compared with the deep-learning-based building segmentation methods that greatly benefit from data-driven feature learning, the building boundary vector representation generation techniques mainly rely on handcrafted features and high human intervention. These techniques continue to employ manual design and ignore the opportunity of using the rich feature information that can be learned from training data to directly generate vectorized boundary descriptions. Aiming to address this problem, we introduce PolygonCNN, a learnable end-to-end vector shape modeling framework for generating building outlines from aerial images. The framework first performs an FCN-like segmentation to extract initial building contours. Then, by encoding the vertices of the building polygons along with the pooled image features extracted from segmentation step, a modified PointNet is proposed to learn shape priors and predict a polygon vertex deformation to generate refined building vector results. Additionally, we propose 1) a simplify-and-densify sampling strategy to generate homogeneously sampled polygon with well-kept geometric signals for shape prior learning; and 2) a novel loss function for estimating shape similarity between building polygons with vastly different vertex numbers. The experiments on over 10,000 building samples verify that PolygonCNN can generate building vectors with higher vertex-based F1-score than the state-of-the-art method, and simultaneously well maintains the building segmentation accuracy achieved by the FCN-like model.} 105 | } 106 | 107 | 108 | @article{bolin2020investigation-9, 109 | title={An investigation of the influence of the refractive shadow zone on wind turbine noise}, 110 | author={Bolin, Karl and Conrady, Kristina and Karasalo, Ilkka and Sj{\"o}blom, Anna}, 111 | journal={The Journal of the Acoustical Society of America}, 112 | volume={148}, 113 | number={2}, 114 | pages={EL166--EL171}, 115 | year={2020}, 116 | publisher={Acoustical Society of America}, 117 | doi={10.1121/10.0001589} 118 | } 119 | 120 | @inproceedings{zhou2015integrated-10, 121 | title={An integrated approach for shadow detection of the building in urban areas}, 122 | author={Zhou, Guoqing and Han, Caiyun and Ye, Siqi and Wang, Yuefeng and Wang, Chenxi}, 123 | booktitle={International Conference on Intelligent Earth Observing and Applications 2015}, 124 | volume={9808}, 125 | pages={98082W}, 126 | year={2015}, 127 | doi = {10.1117/12.2207632}, 128 | organization={International Society for Optics and Photonics} 129 | } 130 | 131 | 132 | @Article{rs12040679-11, 133 | AUTHOR = {Zhou, Guoqing and Sha, Hongjun}, 134 | TITLE = {Building Shadow Detection on Ghost Images}, 135 | JOURNAL = {Remote Sensing}, 136 | VOLUME = {12}, 137 | YEAR = {2020}, 138 | NUMBER = {4}, 139 | ARTICLE-NUMBER = {679}, 140 | URL = {https://www.mdpi.com/2072-4292/12/4/679}, 141 | ISSN = {2072-4292}, 142 | ABSTRACT = {Although many efforts have been made on building shadow detection from aerial images, little research on simultaneous shadows detection on both building roofs and grounds has been presented. Hence, this paper proposes a new method for simultaneous shadow detection on ghost image. In the proposed method, a corner point on shadow boundary is selected and its 3D approximate coordinate is calculated through photogrammetric collinear equation on the basis of assumption of average elevation within the aerial image. The 3D coordinates of the shadow corner point on shadow boundary is used to calculate the solar zenith angle and the solar altitude angle. The shadow areas on the ground, at the moment of aerial photograph shooting are determined by the solar zenith angle and the solar altitude angle with the prior information of the digital building model (DBM). Using the relationship between the shadows of each building and the height difference of buildings, whether there exists a shadow on the building roof is determined, and the shadow area on the building roof on the ghost image is detected on the basis of the DBM. High-resolution aerial images located in the City of Denver, Colorado, USA are used to verify the proposed method. The experimental results demonstrate that the shadows of the 120 buildings in the study area are completely detected, and the success rate is 15% higher than the traditional shadow detection method based on shadow features. Especially, when the shadows occur on the ground and on the buildings roofs, the successful rate of shadow detection can be improved by 9.42% and 33.33% respectively.}, 143 | DOI = {10.3390/rs12040679} 144 | } 145 | 146 | @article{RAFIEE2014397-12, 147 | title = {From BIM to Geo-analysis: View Coverage and Shadow Analysis by BIM/GIS Integration}, 148 | journal = {Procedia Environmental Sciences}, 149 | volume = {22}, 150 | pages = {397-402}, 151 | year = {2014}, 152 | note = {12th International Conference on Design and Decision Support Systems in Architecture and Urban Planning, DDSS 2014}, 153 | issn = {1878-0296}, 154 | doi = {10.1016/j.proenv.2014.11.037}, 155 | url = {https://www.sciencedirect.com/science/article/pii/S1878029614001844}, 156 | author = {Azarakhsh Rafiee and Eduardo Dias and Steven Fruijtier and Henk Scholten}, 157 | keywords = {BIM, GIS, Shadow, Analysis}, 158 | abstract = {Data collection is moving towards more details and larger scales and efficient ways of interpreting the data and analysing it is of great importance. A Building Information Model (BIM) includes very detailed and accurate information of a construction. However, this information model is not necessarily geo located but uses a local coordinate system hampering environmental analysis. Transforming the BIM to its corresponding geo-located model helps answering many environmental questions efficiently. In this research, we have applied methods to automatically transform the geometric and semantic information of a BIM model to a geo-referenced model. Two analyses, namely view and shadow analysis, have been performed using the geometric and semantic information within the geo-referenced BIM model and other existing geospatial elements. These analyses demonstrate the value of integrating BIM and spatial data for e.g. spatial planning.} 159 | } 160 | 161 | @article{HONG2016408-13, 162 | title = {Estimation of the Available Rooftop Area for Installing the Rooftop Solar Photovoltaic (PV) System by Analyzing the Building Shadow Using Hillshade Analysis}, 163 | journal = {Energy Procedia}, 164 | volume = {88}, 165 | pages = {408-413}, 166 | year = {2016}, 167 | note = {CUE 2015 - Applied Energy Symposium and Summit 2015: Low carbon cities and urban energy systems}, 168 | issn = {1876-6102}, 169 | doi = {10.1016/j.egypro.2016.06.013}, 170 | url = {https://www.sciencedirect.com/science/article/pii/S1876610216300777}, 171 | author = {Taehoon Hong and Minhyun Lee and Choongwan Koo and Jimin Kim and Kwangbok Jeong}, 172 | keywords = {Rooftop solar photovoltaic (PV) system, Hillshade analysis, Building shadow, Available rooftop area}, 173 | abstract = {For continuous promotion of the solar PV system in buildings, it is crucial to analyze the rooftop solar PV potential. However, the rooftop solar PV potential in urban areas highly varies depending on the available rooftop area due to the building shadow. In order to estimate the available rooftop area accurately by considering the building shadow, this study proposed an estimation method of the available rooftop area for installing the rooftop solar PV system by analyzing the building shadow using Hillshade Analysis. A case study of Gangnam district in Seoul, South Korea was shown by applying the proposed estimation method.} 174 | } 175 | 176 | @Article{ijgi7100413-13, 177 | AUTHOR = {Agius, Tyler and Sabri, Soheil and Kalantari, Mohsen}, 178 | TITLE = {Three-Dimensional Rule-Based City Modelling to Support Urban Redevelopment Process}, 179 | JOURNAL = {ISPRS International Journal of Geo-Information}, 180 | VOLUME = {7}, 181 | YEAR = {2018}, 182 | NUMBER = {10}, 183 | ARTICLE-NUMBER = {413}, 184 | URL = {https://www.mdpi.com/2220-9964/7/10/413}, 185 | ISSN = {2220-9964}, 186 | ABSTRACT = {Multi-dimensional representation of urban settings has received a great deal of attention among urban planners, policy makers, and urban scholars. This is due to the fact that cities grow vertically and new urbanism strategies encourage higher density and compact city development. Advancements in computer technology and multi-dimensional geospatial data integration, analysis and visualisation play a pivotal role in supporting urban planning and design. However, due to the complexity of the models and technical requirements of the multi-dimensional city models, planners are yet to fully exploit such technologies in their activities. This paper proposes a workflow to support non-experts in using three-dimensional city modelling tools to carry out planning control amendments and assess their implications. The paper focuses on using a parametric three-dimensional (3D) city model to enable planners to measure the physical (e.g., building height, shadow, setback) and functional (e.g., mix of land uses) impacts of new planning controls. The workflow is then implemented in an inner suburb of Metropolitan Melbourne, where urban intensification strategies require the planners to carry out radical changes in regulations. This study demonstrates the power of the proposed 3D visualisation tool for urban planners at taking two-dimensional (2D) Geographic Information System (GIS) procedural modelling to construct a 3D model.}, 187 | DOI = {10.3390/ijgi7100413} 188 | } 189 | 190 | @ARTICLE{8283638-14, 191 | author={Miranda, Fabio and Doraiswamy, Harish and Lage, Marcos and Wilson, Luc and Hsieh, Mondrian and Silva, Cláudio T.}, 192 | journal={IEEE Transactions on Visualization and Computer Graphics}, 193 | title={Shadow Accrual Maps: Efficient Accumulation of City-Scale Shadows Over Time}, 194 | year={2019}, 195 | volume={25}, 196 | number={3}, 197 | pages={1559-1574}, 198 | doi={10.1109/TVCG.2018.2802945}} 199 | 200 | 201 | @article{Yu2022, 202 | doi = {10.21105/joss.04021}, 203 | url = {10.21105/joss.04021}, 204 | year = {2022}, 205 | publisher = {The Open Journal}, 206 | volume = {7}, 207 | number = {71}, 208 | pages = {4021}, 209 | author = {Qing Yu and Jian Yuan}, 210 | title = {TransBigData: A Python package for transportation spatio-temporal big data processing, analysis and visualization}, 211 | journal = {Journal of Open Source Software} 212 | } 213 | 214 | @Article{rs13163297, 215 | AUTHOR = {Zhang, Ying and Roffey, Matthew and Leblanc, Sylvain G.}, 216 | TITLE = {A Novel Framework for Rapid Detection of Damaged Buildings Using Pre-Event LiDAR Data and Shadow Change Information}, 217 | JOURNAL = {Remote Sensing}, 218 | VOLUME = {13}, 219 | YEAR = {2021}, 220 | NUMBER = {16}, 221 | ARTICLE-NUMBER = {3297}, 222 | URL = {https://www.mdpi.com/2072-4292/13/16/3297}, 223 | ISSN = {2072-4292}, 224 | ABSTRACT = {After a major earthquake in a dense urban area, the spatial distribution of heavily damaged buildings is indicative of the impact of the event on public safety. Timely assessment of the locations of severely damaged buildings and their damage morphologies using remote sensing approaches is critical for search and rescue actions. Detection of damaged buildings that did not suffer collapse can be highly challenging from aerial or satellite optical imagery, especially those structures with height-reduction or inclination damage and apparently intact roofs. A key information cue can be provided by a comparison of predicted building shadows based on pre-event building models with shadow estimates extracted from post-event imagery. This paper addresses the detection of damaged buildings in dense urban areas using the information of building shadow changes based on shadow simulation, analysis, and image processing in order to improve real-time damage detection and analysis. A novel processing framework for the rapid detection of damaged buildings without collapse is presented, which includes (a) generation of building digital surface models (DSMs) from pre-event LiDAR data, (b) building shadow detection and extraction from imagery, (c) simulation of predicted building shadows utilizing building DSMs, and (d) detection and identification of shadow areas exhibiting significant pre- and post-event differences that can be attributed to building damage. The framework is demonstrated through two simulated case studies. The building damage types considered are those typically observed in earthquake events and include height-reduction, over-turn collapse, and inclination. Total collapse cases are not addressed as these are comparatively easy to be detected using simpler algorithms. Key issues are discussed including the attributes of essential information layers and sources of error influencing the accuracy of building damage detection.}, 225 | DOI = {10.3390/rs13163297} 226 | } 227 | 228 | @article{pysal2007, 229 | author={Sergio Rey and Luc Anselin}, 230 | title={{PySAL: A Python Library of Spatial Analytical Methods}}, 231 | doi = {10.1007/978-3-642-03647-7_11}, 232 | journal={The Review of Regional Studies}, 233 | year=2007, 234 | volume={37}, 235 | number={1}, 236 | pages={5-27}, 237 | keywords={Open Source; Software; Spatial}, 238 | url={https://rrs.scholasticahq.com/article/8285.pdf} 239 | } 240 | 241 | 242 | @software{kelsey_jordahl_2021_5573592, 243 | author = {Kelsey Jordahl and 244 | Joris Van den Bossche and 245 | Martin Fleischmann and 246 | James McBride and 247 | Jacob Wasserman and 248 | Adrian Garcia Badaracco and 249 | Jeffrey Gerard and 250 | Alan D. Snow and 251 | Jeff Tratner and 252 | Matthew Perry and 253 | Carson Farmer and 254 | Geir Arne Hjelle and 255 | Micah Cochran and 256 | Sean Gillies and 257 | Lucas Culbertson and 258 | Matt Bartos and 259 | Brendan Ward and 260 | Giacomo Caria and 261 | Mike Taves and 262 | Nick Eubank and 263 | sangarshanan and 264 | John Flavin and 265 | Matt Richards and 266 | Sergio Rey and 267 | maxalbert and 268 | Aleksey Bilogur and 269 | Christopher Ren and 270 | Dani Arribas-Bel and 271 | Daniel Mesejo-León and 272 | Leah Wasser}, 273 | title = {geopandas/geopandas: v0.10.2}, 274 | month = oct, 275 | year = 2021, 276 | publisher = {Zenodo}, 277 | version = {v0.10.2}, 278 | doi = {10.5281/zenodo.5573592}, 279 | url = {10.5281/zenodo.5573592} 280 | } -------------------------------------------------------------------------------- /paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'pybdshadow: a python package for generating, analyzing and visualizing building shadows' 3 | tags: 4 | - Python 5 | - GIS 6 | - Geospatial data 7 | - Sunlight 8 | - Urban analysis 9 | - Building shadow 10 | - Building outline data 11 | - Billboard visual area 12 | authors: 13 | - name: Qing Yu^[corresponding author] 14 | orcid: 0000-0003-2513-2969 15 | affiliation: 1 16 | - name: Ge Li 17 | orcid: 0000-0002-5761-7269 18 | affiliation: 2 19 | affiliations: 20 | - name: Key Laboratory of Road and Traffic Engineering of the Ministry of Education, Tongji University, 4800 Cao’an Road, Shanghai 201804, People’s Republic of China 21 | index: 1 22 | - name: School of Geography and Information Engineering, China University of Geosciences (Wuhan), Wuhan 430074, People’s Republic of China 23 | index: 2 24 | date: 30 April 2022 25 | bibliography: paper.bib 26 | --- 27 | 28 | # Summary 29 | 30 | Building shadows, as one of the significant elements in urban area, have an impact on a variety of features of the urban environment. Building shadows have been shown to affect local surface temperature in metropolitan environments, which will generate thermal influence to the greenery, water, and impervious structures on the urban heat island[@DAI201977-3; @PARK2021101655-4]. In the field of photovoltaic(PV), building integrated PV systems are expected to disseminate due to effective use of urban space. Researchers also focus on the power output performance affected by the shading of buildings[@WU2021116884-5]. Study of the spatial-temporal distribution of building shadow is conducive in determining the best location for photovoltaic panels to maximize energy generation[@YADAV201811-6]. In addition, building shadows also play a significant role in the field of urban planning[@RAFIEE2014397-12], noise propagation[@bolin2020investigation-9], and post-disaster building rehabilitation[@rs13163297]. 31 | 32 | With the development of remote sensing, photogrammetry and deep learning technology, researchers are able to obtain city-scale building data with high resolution. These newly emerged building data provides an available data source for generating and analyzing building shadows[@CHEN2020114-8]. 33 | 34 | `pybdshadow` is a Python package to generate building shadows from building data and provide corresponding methods to analyze the changing position of shadows. `pybdshadow` can provide brand new and valuable data source for supporting the field of urban studies. 35 | 36 | # State of the art 37 | 38 | Existing methods of generating and detecting building shadows can be devided into two major ways: Remote sensing and BIM/GIS analysis. 39 | 40 | - Remote sensing: In the field of remote sensing and satellite image processing, researchers examine shadow information from remote sensing images by identifying and distinguishing building shadows from other objects[@rs13152862-7]. 41 | Zhou et al. developed a shadow detection method by combining the zero-crossing detection method with the DBM-based geometric method to identify shadow from high-resolution images[@zhou2015integrated-10; @rs12040679-11]. 42 | - BIM/GIS analysis: Another way of obtaining building shadow is to transform Building Information Model(BIM) to its corresponding geo-located model[@RAFIEE2014397-12]. The Hillshade function provided in ArcGIS is capable of producing a grayscale 3D representation of the terrain surface, which can be used as a tool for analysing building shadow. Hong et al. analyze the building shadow using Hillshade Analysis and estimate the available rooftop area for PV System[@HONG2016408-13]. Miranda et al. propose an approach that uses the properties of sun movement to track the changing position of shadows within a fixed time interval[@8283638-14]. 43 | 44 | In Python environment, geospatial analysing package like `geopandas`, `PySAL` provide tools to easily implement the spatial analysis of spatial data[@kelsey_jordahl_2021_5573592; @pysal2007]. Nevertheless, there is a lack of an effective tool for generating and analyzing building shadows that is compatible with the Python geospatial data processing framework. 45 | 46 | # Statement of need 47 | 48 | `pybdshadow` is a python package for generating, analyzing and visualizing building shadows from large scale building geographic data. `pybdshadow` support generate building shadows from both sun light and point light. `pybdshadow` provides an efficient and easy-to-use method to generate a new source of geospatial data with great application potential in urban study. 49 | 50 | Currently, `pybdshadow` mainly provides the following methods: 51 | 52 | - *Generating building shadow from sun light*: With given location and time, the function in `pybdshadow` uses the properties of sun position obtained from `suncalc-py` and the building height to generate shadow geometry data(\autoref{fig:fig1}(a)). 53 | - *Generating building shadow from point light*: `pybdshadow` can generate the building shadow with given location and height of the point light, which can be potentially useful for visual area analysis in urban environment(\autoref{fig:fig1}(b)). 54 | - *Analysis*: `pybdshadow` integrated the analysing method based on the properties of sun movement to track the changing position of shadows within a fixed time interval. Based on the grid processing framework provided by `TransBigData`[@Yu2022], `pybdshadow` is capable of calculating sunshine time on the ground and on the roof(\autoref{fig:fig2}). 55 | - *Visualization*: Built-in visualization capabilities leverage the visualization package `keplergl` to interactively visualize building and shadow data in Jupyter notebooks with simple code. 56 | 57 | The target audience of `pybdshadow` includes data science researchers and data engineers in the field of BIM, GIS, energy, environment, and urban computing. 58 | 59 | The latest stable release of the software can be installed via `pip` and full documentation can be found at https://pybdshadow.readthedocs.io/en/latest/. 60 | 61 | ![pybdshadow generate and visualize building shadows.\label{fig:fig1}](image/paper/1651656857394.png){ width=100% } 62 | 63 | ![pybdshadow analyse sunshine time on the building roof and on the ground.\label{fig:fig2}](image/paper/1651656639873.png){ width=100% } 64 | 65 | # References 66 | 67 | 68 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | rtree 3 | geopandas 4 | matplotlib 5 | suncalc 6 | keplergl 7 | scikit-opt 8 | transbigdata 9 | mapbox_vector_tile 10 | vt2geojson 11 | requests 12 | tqdm 13 | retrying -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="pybdshadow", 8 | version="0.3.5", 9 | author="Qing Yu", 10 | author_email="qingyu0815@foxmail.com", 11 | description="Python package to generate building shadow geometry", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | license="BSD", 15 | url="https://github.com/ni1o1/pybdshadow", 16 | project_urls={ 17 | "Bug Tracker": "https://github.com/ni1o1/pybdshadow/issues", 18 | }, 19 | install_requires=[ 20 | "numpy", "pandas", "shapely", "geopandas", "matplotlib","suncalc","keplergl","transbigdata","mapbox_vector_tile","vt2geojson","requests","tqdm","retrying" 21 | ], 22 | classifiers=[ 23 | "Operating System :: OS Independent", 24 | "Topic :: Text Processing :: Indexing", 25 | "Topic :: Utilities", 26 | "Topic :: Software Development :: Libraries :: Python Modules", 27 | "License :: OSI Approved :: BSD License", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | ], 32 | package_dir={'pybdshadow': 'src/pybdshadow'}, 33 | packages=['pybdshadow'], 34 | python_requires=">=3.8", 35 | ) 36 | -------------------------------------------------------------------------------- /src/pybdshadow/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | `pybdshadow`: Python package to generate building shadow geometry. 3 | 4 | BSD 3-Clause License 5 | 6 | Copyright (c) 2022, Qing Yu 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | 15 | 2. Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | 3. Neither the name of the copyright holder nor the names of its 20 | contributors may be used to endorse or promote products derived from 21 | this software without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | """ 34 | 35 | __version__ = '0.3.5' 36 | __author__ = 'Qing Yu ' 37 | 38 | # module level doc-string 39 | __doc__ = """ 40 | `pybdshadow` - Python package to generate building shadow geometry. 41 | """ 42 | from .pybdshadow import * 43 | from .get_buildings import ( 44 | get_buildings_by_polygon, 45 | get_buildings_by_bounds, 46 | ) 47 | from .pybdshadow import ( 48 | bdshadow_sunlight, 49 | bdshadow_pointlight 50 | ) 51 | from .preprocess import ( 52 | bd_preprocess 53 | ) 54 | from .visualization import ( 55 | show_bdshadow, 56 | show_sunshine, 57 | ) 58 | from .analysis import ( 59 | cal_sunshine, 60 | cal_sunshadows, 61 | cal_shadowcoverage, 62 | get_timetable 63 | ) 64 | 65 | from .utils import ( 66 | extrude_poly 67 | ) 68 | 69 | __all__ = ['bdshadow_sunlight', 70 | 'bdshadow_pointlight', 71 | 'bd_preprocess', 72 | 'show_bdshadow', 73 | 'cal_sunshine', 74 | 'cal_sunshadows', 75 | 'cal_shadowcoverage', 76 | 'get_timetable', 77 | 'get_buildings_by_polygon', 78 | 'get_buildings_by_bounds', 79 | 'cal_sunshine_facade', 80 | 'show_sunshine', 81 | 'extrude_poly' 82 | ] 83 | -------------------------------------------------------------------------------- /src/pybdshadow/analysis.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from suncalc import get_times 3 | from shapely.geometry import MultiPolygon,Polygon 4 | import transbigdata as tbd 5 | import geopandas as gpd 6 | from .pybdshadow import ( 7 | bdshadow_sunlight, 8 | ) 9 | from .preprocess import bd_preprocess 10 | from .utils import count_overlapping_features 11 | 12 | def get_timetable(lon, lat, dates=['2022-01-01'], precision=3600, padding=1800): 13 | # generate timetable with given interval 14 | def get_timeSeries(day, lon, lat, precision=3600, padding=1800): 15 | date = pd.to_datetime(day+' 12:45:33.959797119') 16 | times = get_times(date, lon, lat) 17 | date_sunrise = times['sunrise'] 18 | data_sunset = times['sunset'] 19 | timestamp_sunrise = pd.Series(date_sunrise).astype('int') 20 | timestamp_sunset = pd.Series(data_sunset).astype('int') 21 | times = pd.to_datetime(pd.Series(range( 22 | timestamp_sunrise.iloc[0]+padding*1000000000, 23 | timestamp_sunset.iloc[0]-padding*1000000000, 24 | precision*1000000000))) 25 | return times 26 | dates = pd.DataFrame(pd.concat( 27 | [get_timeSeries(date, lon, lat, precision, padding) for date in dates]), columns=['datetime']) 28 | dates['date'] = dates['datetime'].apply(lambda r: str(r)[:19]) 29 | return dates 30 | 31 | 32 | def cal_sunshine(buildings, day='2022-01-01', roof=False, grids=gpd.GeoDataFrame(), accuracy=1, precision=3600, padding=1800): 33 | ''' 34 | Calculate the sunshine time in given date. 35 | 36 | Parameters 37 | -------------------- 38 | buildings : GeoDataFrame 39 | Buildings. coordinate system should be WGS84 40 | day : str 41 | the day to calculate the sunshine 42 | roof : bool 43 | whether to calculate roof shadow. 44 | grids : GeoDataFrame 45 | grids generated by TransBigData in study area 46 | precision : number 47 | time precision(s) 48 | padding : number 49 | padding time before and after sunrise and sunset 50 | accuracy : number 51 | size of grids. Produce vector polygons if set as `vector` 52 | 53 | Return 54 | ---------- 55 | grids : GeoDataFrame 56 | grids generated by TransBigData in study area, each grids have a `time` column store the sunshine time 57 | 58 | ''' 59 | 60 | 61 | # calculate day time duration 62 | lon, lat = buildings['geometry'].iloc[0].bounds[:2] 63 | date = pd.to_datetime(day+' 12:45:33.959797119') 64 | times = get_times(date, lon, lat) 65 | date_sunrise = times['sunrise'] 66 | data_sunset = times['sunset'] 67 | timestamp_sunrise = pd.Series(date_sunrise).astype('int') 68 | timestamp_sunset = pd.Series(data_sunset).astype('int') 69 | sunlighthour = ( 70 | timestamp_sunset.iloc[0]-timestamp_sunrise.iloc[0])/(1000000000*3600) 71 | 72 | # Generate shadow every time interval 73 | shadows = cal_sunshadows( 74 | buildings, dates=[day], precision=precision, padding=padding) 75 | if accuracy == 'vector': 76 | if roof: 77 | shadows = shadows[shadows['type'] == 'roof'] 78 | if len(shadows)>0: 79 | shadows = bd_preprocess(shadows) 80 | shadows = shadows.groupby(['date', 'type','height'])['geometry'].apply( 81 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 82 | shadows = bd_preprocess(shadows) 83 | 84 | # 额外:增加屋顶面 85 | shadows = pd.concat([shadows, buildings]) 86 | #return shadows 87 | shadows = shadows.groupby('height').apply(count_overlapping_features).reset_index() 88 | shadows['count'] -= 1 89 | else: 90 | shadows = shadows[shadows['type'] == 'ground'] 91 | 92 | shadows = bd_preprocess(shadows) 93 | shadows = shadows.groupby(['date', 'type'])['geometry'].apply( 94 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 95 | shadows = bd_preprocess(shadows) 96 | 97 | # 额外:增加地面面 98 | minpos = shadows.bounds[['minx','miny']].min() 99 | maxpos = shadows.bounds[['maxx','maxy']].max() 100 | 101 | ground = gpd.GeoDataFrame(geometry=[ 102 | Polygon([ 103 | [minpos['minx'],minpos['miny']], 104 | [minpos['minx'],maxpos['maxy']], 105 | [maxpos['maxx'],maxpos['maxy']], 106 | [maxpos['maxx'],minpos['miny']], 107 | ]) 108 | ]) 109 | shadows = pd.concat([shadows, 110 | ground 111 | ]) 112 | shadows = count_overlapping_features(shadows,buffer=False) 113 | shadows['count'] -= 1 114 | 115 | 116 | shadows['time'] = shadows['count']*precision 117 | shadows['Hour'] = sunlighthour-shadows['time']/3600 118 | #shadows.loc[shadows['Hour'] <= 0, 'Hour'] = 0 119 | return shadows 120 | else: 121 | # Grid analysis of shadow cover duration(ground). 122 | grids = cal_shadowcoverage( 123 | shadows, buildings, grids=grids, roof=roof, precision=precision, accuracy=accuracy) 124 | 125 | grids['Hour'] = sunlighthour-grids['time']/3600 126 | return grids 127 | 128 | 129 | def cal_sunshadows(buildings, cityname='somecity', dates=['2022-01-01'], precision=3600, padding=1800, 130 | roof=True, include_building=True, save_shadows=False, printlog=False): 131 | ''' 132 | Calculate the sunlight shadow in different date with given time precision. 133 | 134 | Parameters 135 | -------------------- 136 | buildings : GeoDataFrame 137 | Buildings. coordinate system should be WGS84 138 | cityname : string 139 | Cityname. If save_shadows, this function will create `result/cityname` folder to save the shadows 140 | dates : list 141 | List of dates 142 | precision : number 143 | Time precision(s) 144 | padding : number 145 | Padding time (second) before and after sunrise and sunset. Should be over 1800s to avoid sun altitude under 0 146 | roof : bool 147 | whether to calculate roof shadow. 148 | include_building : bool 149 | whether the shadow include building outline 150 | save_shadows : bool 151 | whether to save calculated shadows 152 | printlog : bool 153 | whether to print log 154 | 155 | Return 156 | ---------- 157 | allshadow : GeoDataFrame 158 | All building shadows calculated 159 | ''' 160 | if (padding < 1800): 161 | raise ValueError( 162 | 'Padding time should be over 1800s to avoid sun altitude under 0') # pragma: no cover 163 | # obtain city location 164 | lon, lat = buildings['geometry'].iloc[0].bounds[:2] 165 | timetable = get_timetable(lon, lat, dates, precision, padding) 166 | import os 167 | if save_shadows: 168 | if not os.path.exists('result'): # pragma: no cover 169 | os.mkdir('result') # pragma: no cover 170 | if not os.path.exists('result/'+cityname): # pragma: no cover 171 | os.mkdir('result/'+cityname) # pragma: no cover 172 | allshadow = [] 173 | for i in range(len(timetable)): 174 | date = timetable['datetime'].iloc[i] 175 | name = timetable['date'].iloc[i] 176 | if not os.path.exists('result/'+cityname+'/roof_'+name+'.json'): 177 | if printlog: 178 | print('Calculating', cityname, ':', name) # pragma: no cover 179 | # Calculate shadows 180 | shadows = bdshadow_sunlight( 181 | buildings, date, roof=roof, include_building=include_building) 182 | shadows['date'] = date 183 | roof_shaodws = shadows[shadows['type'] == 'roof'] 184 | ground_shaodws = shadows[shadows['type'] == 'ground'] 185 | 186 | if save_shadows: 187 | if len(roof_shaodws) > 0: # pragma: no cover 188 | roof_shaodws.to_file( # pragma: no cover 189 | 'result/'+cityname+'/roof_'+name+'.json', driver='GeoJSON') # pragma: no cover 190 | if len(ground_shaodws) > 0: # pragma: no cover 191 | ground_shaodws.to_file( # pragma: no cover 192 | 'result/'+cityname+'/ground_'+name+'.json', driver='GeoJSON') # pragma: no cover 193 | allshadow.append(shadows) 194 | allshadow = pd.concat(allshadow) 195 | return allshadow 196 | 197 | 198 | def cal_shadowcoverage(shadows_input, buildings, grids=gpd.GeoDataFrame(), roof=True, precision=3600, accuracy=1): 199 | ''' 200 | Calculate the sunlight shadow coverage time for given area. 201 | 202 | Parameters 203 | -------------------- 204 | shadows_input : GeoDataFrame 205 | All building shadows calculated 206 | buildings : GeoDataFrame 207 | Buildings. coordinate system should be WGS84 208 | grids : GeoDataFrame 209 | grids generated by TransBigData in study area 210 | roof : bool 211 | If true roof shadow, false then ground shadow 212 | precision : number 213 | time precision(s), which is for calculation of coverage time 214 | accuracy : number 215 | size of grids. 216 | 217 | Return 218 | -------------------- 219 | grids : GeoDataFrame 220 | grids generated by TransBigData in study area, each grids have a `time` column store the shadow coverage time 221 | 222 | ''' 223 | shadows = bd_preprocess(shadows_input) 224 | 225 | # study area 226 | bounds = buildings.unary_union.bounds 227 | if len(grids) == 0: 228 | grids, params = tbd.area_to_grid(bounds, accuracy) 229 | 230 | if roof: 231 | ground_shadows = shadows[shadows['type'] == 'roof'].groupby(['date'])['geometry'].apply( 232 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 233 | 234 | buildings.crs = None 235 | grids = gpd.sjoin(grids, buildings) 236 | else: 237 | ground_shadows = shadows[shadows['type'] == 'ground'].groupby(['date'])['geometry'].apply( 238 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 239 | 240 | buildings.crs = None 241 | grids = gpd.sjoin(grids, buildings, how='left') 242 | grids = grids[grids['index_right'].isnull()] 243 | 244 | gridcount = gpd.sjoin(grids[['LONCOL', 'LATCOL', 'geometry']], ground_shadows[['geometry', 'date']]).\ 245 | drop_duplicates(subset=['LONCOL', 'LATCOL', 'date']).groupby(['LONCOL', 'LATCOL'])['geometry'].\ 246 | count().rename('count').reset_index() 247 | grids = pd.merge(grids, gridcount, how='left') 248 | grids['time'] = grids['count'].fillna(0)*precision 249 | 250 | return grids 251 | 252 | -------------------------------------------------------------------------------- /src/pybdshadow/get_buildings.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from vt2geojson.tools import vt_bytes_to_geojson 3 | import pandas as pd 4 | import geopandas as gpd 5 | import transbigdata as tbd 6 | from .preprocess import bd_preprocess 7 | from tqdm import tqdm 8 | import math 9 | from retrying import retry 10 | from requests.exceptions import RequestException 11 | 12 | def deg2num(lat_deg, lon_deg, zoom): 13 | ''' 14 | Calculate xy tiles from coordinates 15 | 16 | Parameters 17 | ------- 18 | lon_deg : number 19 | Longitude 20 | lat_deg : number 21 | Latitude 22 | zoom : Int 23 | Zoom level of the map 24 | ''' 25 | lat_rad = math.radians(lat_deg) 26 | n = 2.0 ** zoom 27 | xtile = int((lon_deg + 180.0) / 360.0 * n) 28 | ytile = int((1.0 - math.log(math.tan(lat_rad) + 29 | (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n) 30 | return (xtile, ytile) 31 | 32 | 33 | 34 | def is_request_exception(e): 35 | return issubclass(type(e),RequestException) 36 | 37 | @retry(retry_on_exception=is_request_exception,wrap_exception=False, stop_max_attempt_number=300) 38 | def safe_request(url, **kwargs): 39 | return requests.get(url, **kwargs) 40 | 41 | def getbd(x,y,z,MAPBOX_ACCESS_TOKEN): 42 | ''' 43 | Get buildings from mapbox vector tiles 44 | 45 | Parameters 46 | ------- 47 | x : Int 48 | x tile number 49 | y : Int 50 | y tile number 51 | z : Int 52 | zoom level of the map 53 | MAPBOX_ACCESS_TOKEN : str 54 | Mapbox access token 55 | 56 | Return 57 | ---------- 58 | building : GeoDataFrame 59 | buildings in the tile 60 | ''' 61 | try: 62 | url = f"https://api.mapbox.com/v4/mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2,mapbox.mapbox-bathymetry-v2/{z}/{x}/{y}.vector.pbf?sku=101vMyxQx9v3Q&access_token={MAPBOX_ACCESS_TOKEN}" 63 | 64 | r = safe_request(url, timeout=10) 65 | assert r.status_code == 200, r.content 66 | vt_content = r.content 67 | features = vt_bytes_to_geojson(vt_content, x, y, z) 68 | gdf = gpd.GeoDataFrame.from_features(features) 69 | building = gdf[gdf['height']>0][['geometry', 'height','type']] 70 | except: 71 | building = pd.DataFrame() 72 | return building 73 | 74 | 75 | def get_tiles_by_lonlat(lon1,lat1,lon2,lat2,z): 76 | ''' 77 | Get tiles by lonlat 78 | 79 | Parameters 80 | ------- 81 | lon1 : number 82 | Longitude of the first point 83 | lat1 : number 84 | Latitude of the first point 85 | lon2 : number 86 | Longitude of the second point 87 | lat2 : number 88 | Latitude of the second point 89 | z : Int 90 | Zoom level of the map 91 | 92 | Return 93 | ---------- 94 | tiles : DataFrame 95 | Tiles in the area 96 | ''' 97 | x1,y1 = deg2num(lat1, lon1, z) 98 | x2,y2 = deg2num(lat2, lon2, z) 99 | x_min = min(x1,x2) 100 | x_max = max(x1,x2) 101 | y_min = min(y1,y2) 102 | y_max = max(y1,y2) 103 | tiles = pd.DataFrame(range(x_min,x_max+1), columns=['x']).assign(foo=1).merge(pd.DataFrame(range(y_min,y_max+1), columns=['y']).assign(foo=1)).drop('foo', axis=1).assign(z=z) 104 | return tiles 105 | 106 | def get_tiles_by_polygon(polygon,z): 107 | ''' 108 | Get tiles by polygon 109 | 110 | Parameters 111 | ------- 112 | polygon : GeoDataFrame of Polygon or MultiPolygon 113 | Polygon of the area 114 | z : Int 115 | Zoom level of the map 116 | 117 | Return 118 | ---------- 119 | tiles : DataFrame 120 | Tiles in the area 121 | ''' 122 | grid,params = tbd.area_to_grid(polygon,accuracy=400) 123 | grid['lon'] = grid.centroid.x 124 | grid['lat'] = grid.centroid.y 125 | a = grid.apply(lambda x: deg2num(x.lat, x.lon, z), axis=1) 126 | grid['x'] = a.apply(lambda a:a[0]) 127 | grid['y'] = a.apply(lambda a:a[1]) 128 | grid['z'] = z 129 | tiles = grid[['x','y','z']].drop_duplicates() 130 | return tiles 131 | 132 | def get_buildings_threading(tiles,MAPBOX_ACCESS_TOKEN,merge=False,num_threads=100): 133 | ''' 134 | Get buildings by threading 135 | 136 | Parameters 137 | ------- 138 | tiles : DataFrame 139 | Tiles in the area 140 | MAPBOX_ACCESS_TOKEN : str 141 | Mapbox access token 142 | merge : bool 143 | whether to merge buildings in the same grid 144 | num_threads : Int 145 | number of threads 146 | 147 | Return 148 | ---------- 149 | building : GeoDataFrame 150 | buildings in the area 151 | ''' 152 | def merge_building(building): 153 | building = building.groupby(['height','type']).apply(lambda r:r.unary_union).reset_index() 154 | building.columns = ['height','type','geometry'] 155 | building = gpd.GeoDataFrame(building,geometry = 'geometry') 156 | building = bd_preprocess(building) 157 | return building 158 | 159 | # 这是修改后的 getbd_tojson 函数 160 | def getbd_tojson(data, MAPBOX_ACCESS_TOKEN, pbar, results): 161 | for j in range(len(data)): 162 | r = data.iloc[j] 163 | x, y, z = r['x'], r['y'], r['z'] 164 | try: 165 | url = f"https://api.mapbox.com/v4/mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2,mapbox.mapbox-bathymetry-v2/{z}/{x}/{y}.vector.pbf?sku=101vMyxQx9v3Q&access_token={MAPBOX_ACCESS_TOKEN}" 166 | r = safe_request(url, timeout=10) 167 | assert r.status_code == 200, r.content 168 | vt_content = r.content 169 | features = vt_bytes_to_geojson(vt_content, x, y, z) 170 | gdf = gpd.GeoDataFrame.from_features(features) 171 | building = gdf[gdf['height'] > 0][['geometry', 'height', 'type']] 172 | results.append(building) # 将结果添加到全局列表 173 | except: 174 | pass 175 | finally: 176 | pbar.update() 177 | 178 | # 主程序 179 | import threading 180 | import os 181 | # 主程序 182 | # 分割数据 183 | grid = tiles.copy() 184 | bins = num_threads 185 | 186 | grid['tmpid'] = range(len(grid)) 187 | grid['group_num'] = pd.cut(grid['tmpid'], bins, precision=2, labels=range(bins)) 188 | 189 | # 创建进度条 190 | pbar = tqdm(total=len(grid), desc='Downloading Buildings: ') 191 | 192 | # 存储结果的全局列表 193 | results = [] 194 | 195 | # 划分线程 196 | threads = [] 197 | for i in range(bins): 198 | data = grid[grid['group_num'] == i] 199 | threads.append(threading.Thread(target=getbd_tojson, args=(data, MAPBOX_ACCESS_TOKEN, pbar, results))) 200 | 201 | # 线程开始 202 | for t in threads: 203 | t.setDaemon(True) 204 | t.start() 205 | for t in threads: 206 | t.join() 207 | 208 | # 关闭进度条 209 | pbar.close() 210 | threads.clear() 211 | 212 | # 合并数据 213 | building = pd.concat(results) 214 | 215 | if merge: 216 | #再做一次聚合,分栅格聚合建筑 217 | building['x'] = building.centroid.x 218 | building['y'] = building.centroid.y 219 | params = tbd.area_to_params(building['geometry'].iloc[0].bounds) 220 | building['LONCOL'],building['LATCOL'] = tbd.GPS_to_grid(building['x'],building['y'],params) 221 | building['tile'] = building['LONCOL'].astype(str)+'_'+building['LATCOL'].astype(str) 222 | building = building.groupby(['tile','type']).apply(merge_building).reset_index(drop=True) 223 | 224 | building = building[['geometry','height','type']] 225 | building['building_id'] = range(len(building)) 226 | 227 | return building 228 | 229 | def get_buildings_by_bounds(lon1,lat1,lon2,lat2,MAPBOX_ACCESS_TOKEN,merge=False): 230 | ''' 231 | Get buildings by bounds 232 | 233 | Parameters 234 | ------- 235 | lon1 : number 236 | Longitude of the first point 237 | lat1 : number 238 | Latitude of the first point 239 | lon2 : number 240 | Longitude of the second point 241 | lat2 : number 242 | Latitude of the second point 243 | MAPBOX_ACCESS_TOKEN : str 244 | Mapbox access token 245 | merge : bool 246 | whether to merge buildings in the same grid 247 | 248 | Return 249 | ---------- 250 | building : GeoDataFrame 251 | buildings in the area 252 | ''' 253 | tiles = get_tiles_by_lonlat(lon1,lat1,lon2,lat2,16) 254 | building = get_buildings_threading(tiles,MAPBOX_ACCESS_TOKEN,merge) 255 | building = bd_preprocess(building) 256 | return building 257 | 258 | def get_buildings_by_polygon(polygon,MAPBOX_ACCESS_TOKEN,merge=False): 259 | ''' 260 | Get buildings by polygon 261 | 262 | Parameters 263 | ------- 264 | polygon : GeoDataFrame of Polygon or MultiPolygon 265 | Polygon of the area 266 | MAPBOX_ACCESS_TOKEN : str 267 | Mapbox access token 268 | merge : bool 269 | whether to merge buildings in the same grid 270 | 271 | Return 272 | ---------- 273 | building : GeoDataFrame 274 | buildings in the area 275 | ''' 276 | tiles = get_tiles_by_polygon(polygon,16) 277 | building = get_buildings_threading(tiles,MAPBOX_ACCESS_TOKEN,merge) 278 | building = bd_preprocess(building) 279 | return building -------------------------------------------------------------------------------- /src/pybdshadow/preprocess.py: -------------------------------------------------------------------------------- 1 | """ 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Qing Yu 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | """ 32 | import shapely 33 | import pandas as pd 34 | import geopandas as gpd 35 | from shapely.geometry import MultiPolygon 36 | 37 | def bd_preprocess(buildings, height=''): 38 | ''' 39 | Preprocess building data, so that we can perform shadow calculation. 40 | Remove empty polygons and convert multipolygons into polygons. 41 | 42 | Parameters 43 | -------------- 44 | buildings : GeoDataFrame 45 | Buildings. 46 | height : string 47 | Column name of building height(meter). 48 | 49 | Return 50 | ---------- 51 | allbds : GeoDataFrame 52 | Polygon buildings 53 | ''' 54 | buildings['geometry'] = buildings.buffer(0) 55 | buildings = buildings[buildings.is_valid].copy() 56 | if height!='': 57 | # 建筑高度筛选 58 | buildings[height] = pd.to_numeric(buildings[height], errors='coerce') 59 | buildings = buildings[buildings[height]>0].copy() 60 | 61 | polygon_buildings = buildings[buildings['geometry'].apply( 62 | lambda r:type(r) == shapely.geometry.polygon.Polygon)] 63 | multipolygon_buildings = buildings[buildings['geometry'].apply( 64 | lambda r:type(r) == shapely.geometry.multipolygon.MultiPolygon)] 65 | allbds = [] 66 | for j in range(len(multipolygon_buildings)): 67 | r = multipolygon_buildings.iloc[j] 68 | singlebd = gpd.GeoDataFrame() 69 | singlebd['geometry'] = list(r['geometry'].geoms) 70 | for i in r.index: 71 | if i != 'geometry': 72 | singlebd[i] = r[i] 73 | allbds.append(singlebd) 74 | allbds.append(polygon_buildings) 75 | allbds = pd.concat(allbds) 76 | if len(allbds) > 0: 77 | allbds = gpd.GeoDataFrame(allbds) 78 | allbds['building_id'] = range(len(allbds)) 79 | allbds['geometry'] = allbds.buffer(0) 80 | else: 81 | allbds = gpd.GeoDataFrame() 82 | allbds.crs = {'init': 'epsg:4326'} 83 | return allbds 84 | 85 | def gdf_difference(gdf_a,gdf_b,col = 'building_id'): 86 | ''' 87 | difference gdf_b from gdf_a 88 | ''' 89 | gdfa = gdf_a.copy() 90 | gdfb = gdf_b.copy() 91 | gdfb = gdfb[['geometry']] 92 | #判断重叠 93 | 94 | gdfa.crs = gdfb.crs 95 | gdfb = gpd.sjoin(gdfb,gdfa).groupby([col])['geometry'].apply( 96 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 97 | #分割有重叠和无重叠的 98 | gdfb['tmp'] = 1 99 | gdfa_1 = pd.merge(gdfa,gdfb[[col,'tmp']],how = 'left') 100 | gdfa = gdfa_1[gdfa_1['tmp'] == 1].drop('tmp',axis = 1) 101 | gdfa_notintersected = gdfa_1[gdfa_1['tmp'].isnull()].drop('tmp',axis = 1) 102 | #对有重叠的进行裁剪 103 | gdfa = gdfa.sort_values(by = col).set_index(col) 104 | gdfb = gdfb.sort_values(by = col).set_index(col) 105 | gdfa.crs = gdfb.crs 106 | gdfa['geometry'] = gdfa.difference(gdfb).buffer(0) 107 | gdfa = gdfa.reset_index() 108 | #拼合 109 | gdfa = pd.concat([gdfa,gdfa_notintersected]) 110 | return gdfa 111 | 112 | def gdf_intersect(gdf_a,gdf_b,col = 'building_id'): 113 | ''' 114 | intersect gdf_b from gdf_a 115 | ''' 116 | gdfa = gdf_a.copy() 117 | gdfb = gdf_b.copy() 118 | gdfb = gdfb[['geometry']] 119 | #判断重叠 120 | gdfa.crs = gdfb.crs 121 | gdfb = gpd.sjoin(gdfb,gdfa).groupby([col])['geometry'].apply( 122 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 123 | #分割有重叠和无重叠的 124 | gdfb['tmp'] = 1 125 | gdfa_1 = pd.merge(gdfa,gdfb[[col,'tmp']],how = 'left') 126 | gdfa = gdfa_1[gdfa_1['tmp'] == 1].drop('tmp',axis = 1) 127 | #对有重叠的进行裁剪 128 | gdfa = gdfa.sort_values(by = col).set_index(col) 129 | gdfb = gdfb.sort_values(by = col).set_index(col) 130 | gdfa.crs = gdfb.crs 131 | gdfa['geometry'] = gdfa.intersection(gdfb).buffer(0) 132 | gdfa = gdfa.reset_index() 133 | 134 | return gdfa -------------------------------------------------------------------------------- /src/pybdshadow/pybdshadow.py: -------------------------------------------------------------------------------- 1 | """ 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Qing Yu 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | """ 32 | import pandas as pd 33 | import geopandas as gpd 34 | from suncalc import get_position 35 | from shapely.geometry import Polygon, MultiPolygon 36 | import math 37 | import numpy as np 38 | from .utils import ( 39 | lonlat2aeqd, 40 | aeqd2lonlat 41 | ) 42 | from .preprocess import gdf_difference,gdf_intersect 43 | 44 | 45 | def calSunShadow_vector(shape, shapeHeight, sunPosition): 46 | ''' 47 | Calculate the shadow of a building on the ground. 48 | 49 | Parameters 50 | ---------- 51 | shape : numpy.ndarray 52 | The shape of the building. The shape of the array is (n,2,2), where n the number of walls, 2 is that each wall has two points, and the last dimension is for longitude and latitude. 53 | shapeHeight : float 54 | The height of the building. 55 | sunPosition : dict 56 | The position of the sun. The keys are 'azimuth' and 'altitude'. 57 | 58 | Returns 59 | ------- 60 | shadow : numpy.ndarray 61 | The shadow of the building on the ground. shape = [n,5,2] 62 | ''' 63 | # transform coordinate system 64 | meanlon = shape[:,:,0].mean() 65 | meanlat = shape[:,:,1].mean() 66 | shape = lonlat2aeqd(shape,meanlon,meanlat) 67 | 68 | azimuth = sunPosition['azimuth'] 69 | altitude = sunPosition['altitude'] 70 | 71 | n = np.shape(shape)[0] 72 | distance = shapeHeight/math.tan(altitude) 73 | 74 | # calculate the offset of the projection position 75 | lonDistance = distance*math.sin(azimuth) 76 | lonDistance = lonDistance.reshape((n, 1)) 77 | latDistance = distance*math.cos(azimuth) 78 | latDistance = latDistance.reshape((n, 1)) 79 | 80 | shadowShape = np.zeros((n, 5, 2)) # n buildings, each building has 5 points, each point has 2 dimensions 81 | 82 | shadowShape[:, 0:2, :] += shape 83 | shadowShape[:, 2:4, 0] = shape[:, :, 0] + lonDistance 84 | shadowShape[:, 2:4, 1] = shape[:, :, 1] + latDistance 85 | 86 | shadowShape[:, [2, 3], :] = shadowShape[:, [3, 2], :] 87 | shadowShape[:, 4, :] = shadowShape[:, 0, :] 88 | 89 | shadowShape = aeqd2lonlat(shadowShape,meanlon,meanlat) 90 | return shadowShape 91 | 92 | 93 | def bdshadow_sunlight(buildings, date, height='height', roof=False,include_building = True,ground=0): 94 | ''' 95 | Calculate the sunlight shadow of the buildings. 96 | 97 | Parameters 98 | ---------- 99 | buildings : GeoDataFrame 100 | Buildings. coordinate system should be WGS84 101 | date : datetime 102 | Datetime 103 | height : string 104 | Column name of building height(meter). 105 | roof : bool 106 | Whether to calculate the roof shadows. 107 | include_building : bool 108 | Whether the shadow include building outline. 109 | ground : number 110 | Height of the ground(meter). 111 | 112 | Returns 113 | ---------- 114 | shadows : GeoDataFrame 115 | Building shadow 116 | ''' 117 | 118 | building = buildings.copy() 119 | 120 | building[height] -= ground 121 | building = building[building[height] > 0] 122 | 123 | # calculate position 124 | lon1, lat1, lon2, lat2 = list(building.bounds.mean()) 125 | lon = (lon1+lon2)/2 126 | lat = (lat1+lat2)/2 127 | 128 | # obtain sun position 129 | sunPosition = get_position(date, lon, lat) 130 | if ( sunPosition['altitude']<0): 131 | raise ValueError("Given time before sunrise or after sunset") # pragma: no cover 132 | buildingshadow = building.copy() 133 | 134 | a = buildingshadow['geometry'].apply(lambda r: list(r.exterior.coords)) 135 | buildingshadow['wall'] = a 136 | buildingshadow = buildingshadow.set_index(['building_id']) 137 | a = buildingshadow.apply(lambda x: pd.Series(x['wall']), axis=1).unstack() 138 | walls = a[- a.isnull()].reset_index().sort_values( 139 | by=['building_id', 'level_0']) 140 | walls = pd.merge(walls, buildingshadow['height'].reset_index()) 141 | walls['x1'] = walls[0].apply(lambda r: r[0]) 142 | walls['y1'] = walls[0].apply(lambda r: r[1]) 143 | walls['x2'] = walls['x1'].shift(-1) 144 | walls['y2'] = walls['y1'].shift(-1) 145 | walls = walls[walls['building_id'] == walls['building_id'].shift(-1)] 146 | walls = walls[['x1', 'y1', 'x2', 'y2', 'building_id', 'height']] 147 | walls['wall'] = walls.apply(lambda r: [[r['x1'], r['y1']], 148 | [r['x2'], r['y2']]], axis=1) 149 | 150 | ground_shadow = walls.copy() 151 | walls_shape = np.array(list(ground_shadow['wall'])) 152 | 153 | # calculate shadow for walls 154 | shadowShape = calSunShadow_vector( 155 | walls_shape, ground_shadow['height'].values, sunPosition) 156 | 157 | ground_shadow['geometry'] = list(shadowShape) 158 | ground_shadow['geometry'] = ground_shadow['geometry'].apply( 159 | lambda r: Polygon(r)) 160 | ground_shadow = gpd.GeoDataFrame(ground_shadow) 161 | 162 | 163 | 164 | ground_shadow = pd.concat([ground_shadow, building]) 165 | ground_shadow = ground_shadow.groupby(['building_id'])['geometry'].apply( 166 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 167 | 168 | ground_shadow['height'] = 0 169 | ground_shadow['type'] = 'ground' 170 | 171 | if not roof: 172 | if not include_building: 173 | #从地面阴影裁剪建筑轮廓 174 | ground_shadow = gdf_difference(ground_shadow,buildings) 175 | return ground_shadow 176 | else: 177 | def calwall_shadow(walldata, building): 178 | walls = walldata.copy() 179 | walls_shape = np.array(list(walls['wall'])) 180 | # calculate shadow for walls 181 | shadowShape = calSunShadow_vector( 182 | walls_shape, walls['height'].values, sunPosition) 183 | walls['geometry'] = list(shadowShape) 184 | walls['geometry'] = walls['geometry'].apply(lambda r: Polygon(r)) 185 | walls = gpd.GeoDataFrame(walls) 186 | walls = pd.concat([walls, building]) 187 | 188 | walls = walls.groupby(['building_id'])['geometry'].apply( 189 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 190 | return walls 191 | 192 | # 计算屋顶阴影 193 | roof_shadows = [] 194 | for roof_height in walls[height].drop_duplicates(): 195 | # 高于给定高度的墙 196 | walls_high = walls[walls[height] > roof_height].copy() 197 | if len(walls_high) == 0: 198 | continue 199 | walls_high[height] -= roof_height 200 | # 高于给定高度的建筑 201 | building_high = building[building[height] > roof_height].copy() 202 | if len(building_high) == 0: 203 | continue 204 | building_high[height] -= roof_height 205 | # 所有建筑在此高度的阴影 206 | building_shadow_height = calwall_shadow(walls_high, building_high) 207 | # 在此高度的建筑屋顶 208 | building_roof = building[building[height] == roof_height].copy() 209 | building_shadow_height.crs = building_roof.crs 210 | # 取有遮挡的阴影 211 | building_shadow_height = gpd.sjoin( 212 | gpd.GeoDataFrame(building_shadow_height), gpd.GeoDataFrame(building_roof)) 213 | if len(building_shadow_height) == 0: 214 | continue 215 | # 与屋顶做交集 216 | building_roof = gdf_intersect(building_roof,building_shadow_height) 217 | 218 | # 再减去这个高度以上的建筑 219 | building_higher = building[building[height] > roof_height].copy() 220 | building_roof = gdf_difference(building_roof,building_higher) 221 | 222 | #给出高度信息 223 | building_roof['height'] = roof_height 224 | building_roof = building_roof[-building_roof['geometry'].is_empty] 225 | 226 | roof_shadows.append(building_roof) 227 | if len(roof_shadows) == 0: 228 | roof_shadow = gpd.GeoDataFrame() 229 | else: 230 | roof_shadow = pd.concat(roof_shadows)[ 231 | ['height', 'building_id', 'geometry']] 232 | roof_shadow['type'] = 'roof' 233 | 234 | if not include_building: 235 | #从地面阴影裁剪建筑轮廓 236 | ground_shadow = gdf_difference(ground_shadow,buildings) 237 | 238 | shadows = pd.concat([roof_shadow, ground_shadow]) 239 | shadows.crs = None 240 | shadows['geometry'] = shadows.buffer(0.000001).buffer(-0.000001) 241 | return shadows 242 | 243 | 244 | def calPointLightShadow_vector(shape, shapeHeight, pointLight): 245 | ''' 246 | calculate shadow for a point light 247 | 248 | Parameters 249 | ---------- 250 | shape : numpy.array 251 | The shape of the building. The shape of the array is (n,2,2), where n the number of walls, 2 is that each wall has two points, and the last dimension is for longitude and latitude. 252 | shapeHeight : numpy.array 253 | height of building, shape = [n,1], n is the number of buildings 254 | pointLight : dict 255 | point light, pointLight = {'position':[lon,lat,height]} 256 | 257 | Returns 258 | ------- 259 | shadowShape : numpy.array 260 | shape of shadow, shape = [n,5,2] 261 | ''' 262 | # 多维数据类型:numpy 263 | # 输入的shape是一个矩阵(n*2*2) n个建筑物面,每个建筑有2个点,每个点有三个维度 264 | # shapeHeight(n) 每一栋建筑的高度都是一样的 265 | n = np.shape(shape)[0] 266 | pointLightPosition = pointLight['position'] # [lon,lat,height] 267 | 268 | # 高度比 269 | diff = pointLightPosition[2] - shapeHeight 270 | scale = np.zeros(n) 271 | scale[diff != 0] = shapeHeight[diff != 0]/(diff[diff != 0]) 272 | scale[scale <= 0] = 10 # n 273 | scale = scale.reshape((n, 1)) 274 | 275 | shadowShape = np.zeros((n, 5, 2)) 276 | 277 | shadowShape[:, 0:2, :] += shape # 前两个点不变 278 | vertexToLightVector = shape - pointLightPosition[0:2] # n,2,2 279 | 280 | shadowShape[:, 2, :] = shape[:, 1, :] + \ 281 | vertexToLightVector[:, 1, :]*scale # [n,2,2] = [n,2,2]+[n,2,2]*n 282 | shadowShape[:, 3, :] = shape[:, 0, :] + \ 283 | vertexToLightVector[:, 0, :]*scale 284 | 285 | shadowShape[:, 4, :] = shadowShape[:, 0, :] 286 | 287 | return shadowShape 288 | 289 | def bdshadow_pointlight(buildings, 290 | pointlon, 291 | pointlat, 292 | pointheight, 293 | merge=True, 294 | height='height', 295 | ground=0): 296 | ''' 297 | Calculate the sunlight shadow of the buildings. 298 | 299 | Parameters 300 | -------------------- 301 | buildings : GeoDataFrame 302 | Buildings. coordinate system should be WGS84 303 | pointlon,pointlat,pointheight : float 304 | Point light coordinates and height(meter). 305 | date : datetime 306 | Datetime 307 | merge : bool 308 | Whether to merge the wall shadows into the building shadows 309 | height : string 310 | Column name of building height(meter). 311 | ground : number 312 | Height of the ground 313 | 314 | Returns 315 | ---------- 316 | shadows : GeoDataFrame 317 | Building shadow 318 | ''' 319 | 320 | building = buildings.copy() 321 | 322 | building[height] -= ground 323 | building = building[building[height] > 0] 324 | 325 | if len(building) == 0: 326 | walls = gpd.GeoDataFrame() 327 | walls['geometry'] = [] 328 | walls['building_id'] = [] 329 | return walls 330 | # building to walls 331 | buildingshadow = building.copy() 332 | 333 | a = buildingshadow['geometry'].apply(lambda r: list(r.exterior.coords)) #裸格式的几何 334 | buildingshadow['wall'] = a # 335 | #print(a[0]) 336 | buildingshadow = buildingshadow.set_index(['building_id']) #设置阴影所对应的id 337 | a = buildingshadow.apply(lambda x: pd.Series(x['wall']), axis=1).unstack() #压缩为一个数组 338 | walls = a[- a.isnull()].reset_index().sort_values( 339 | by=['building_id', 'level_0']) #重新排序 340 | walls = pd.merge(walls, buildingshadow['height'].reset_index())#与高度融合 341 | 342 | walls['x1'] = walls[0].apply(lambda r: r[0]) # 343 | walls['y1'] = walls[0].apply(lambda r: r[1]) 344 | walls['x2'] = walls['x1'].shift(-1) #向量中的序号全部向前提了一个 345 | walls['y2'] = walls['y1'].shift(-1) 346 | walls = walls[walls['building_id'] == walls['building_id'].shift(-1)] 347 | walls = walls[['x1', 'y1', 'x2', 'y2', 'building_id', 'height']] 348 | walls['wall'] = walls.apply(lambda r: [[r['x1'], r['y1']], 349 | [r['x2'], r['y2']]], axis=1) 350 | walls_shape = np.array(list(walls['wall'])) 351 | 352 | # Create point light 353 | pointLightPosition = {'position': [pointlon, pointlat, pointheight]} 354 | # calculate shadow for walls 355 | shadowShape = calPointLightShadow_vector( 356 | walls_shape, walls['height'].values, pointLightPosition) 357 | 358 | walls['geometry'] = list(shadowShape) #阴影存储 359 | walls['geometry'] = walls['geometry'].apply(lambda r: Polygon(r)) #将numpy转换成polygon形式 360 | walls = gpd.GeoDataFrame(walls) #8列 x1 , y1 ,x2 , y2 , building_id , height , wall ,shadow 361 | wallsBuilding = pd.concat([walls, building]) # 362 | #print(wallsBuilding) 363 | if merge: 364 | wallsBuilding = wallsBuilding.groupby(['building_id'])['geometry'].apply( 365 | lambda df: MultiPolygon(list(df)).buffer(0)).reset_index() 366 | #print(wallsBuilding) 367 | shadows=wallsBuilding 368 | return shadows 369 | -------------------------------------------------------------------------------- /src/pybdshadow/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | __all__ = ['pytest'] 3 | -------------------------------------------------------------------------------- /src/pybdshadow/tests/test_analysis.py: -------------------------------------------------------------------------------- 1 | import pybdshadow 2 | import geopandas as gpd 3 | from shapely.geometry import Polygon 4 | 5 | 6 | class Testanalysis: 7 | def test_analysis(self): 8 | buildings = gpd.GeoDataFrame({ 9 | 'height': [42, 9], 10 | 'geometry': [ 11 | Polygon([(139.698311, 35.533796), 12 | (139.698311, 13 | 35.533642), 14 | (139.699075, 15 | 35.533637), 16 | (139.699079, 17 | 35.53417), 18 | (139.698891, 19 | 35.53417), 20 | (139.698888, 21 | 35.533794), 22 | (139.698311, 35.533796)]), 23 | Polygon([(139.69799, 35.534175), 24 | (139.697988, 35.53389), 25 | (139.698814, 35.533885), 26 | (139.698816, 35.534171), 27 | (139.69799, 35.534175)])]}) 28 | 29 | buildings = pybdshadow.bd_preprocess(buildings) 30 | #分析 31 | date = '2022-01-01' 32 | shadows = pybdshadow.cal_sunshadows(buildings,dates = [date],precision=3600) 33 | bdgrids = pybdshadow.cal_shadowcoverage(shadows,buildings,precision = 3600,accuracy=2) 34 | assert len(bdgrids)==1185 35 | 36 | grids = pybdshadow.cal_sunshine(buildings) 37 | assert len(grids)==1882 38 | 39 | sunshine = pybdshadow.cal_sunshine(buildings,accuracy='vector') 40 | sunshine = pybdshadow.cal_sunshine(buildings,accuracy='vector',roof = True) -------------------------------------------------------------------------------- /src/pybdshadow/tests/test_pointlightshadow.py: -------------------------------------------------------------------------------- 1 | import pybdshadow 2 | import numpy as np 3 | import geopandas as gpd 4 | from shapely.geometry import Polygon 5 | 6 | 7 | class Testpointlightshadow: 8 | def test_bdshadow_pointlight(self): 9 | 10 | buildings = gpd.GeoDataFrame({ 11 | 'height': [42, 9], 12 | 'geometry': [ 13 | Polygon([(139.698311, 35.533796), 14 | (139.698311, 15 | 35.533642), 16 | (139.699075, 17 | 35.533637), 18 | (139.699079, 19 | 35.53417), 20 | (139.698891, 21 | 35.53417), 22 | (139.698888, 23 | 35.533794), 24 | (139.698311, 35.533796)]), 25 | Polygon([(139.69799, 35.534175), 26 | (139.697988, 35.53389), 27 | (139.698814, 35.533885), 28 | (139.698816, 35.534171), 29 | (139.69799, 35.534175)])]}) 30 | 31 | buildings = pybdshadow.bd_preprocess(buildings) 32 | 33 | pointlon,pointlat,pointheight = [139.69799, 35.534175,100] 34 | #Calculate building shadow for point light 35 | shadows = pybdshadow.bdshadow_pointlight(buildings,pointlon,pointlat,pointheight) 36 | result = list(shadows['geometry'].iloc[0].exterior.coords) 37 | truth = [(139.698311, 35.533642), 38 | (139.698311, 35.533796), 39 | (139.698888, 35.533794), 40 | (139.698891, 35.53417), 41 | (139.699079, 35.53417), 42 | (139.69986758620692, 35.53416637931035), 43 | (139.69986068965517, 35.533247413793106), 44 | (139.69854344827584, 35.53325603448276), 45 | (139.698311, 35.533642)] 46 | assert np.allclose(result,truth) -------------------------------------------------------------------------------- /src/pybdshadow/tests/test_sunlightshadow.py: -------------------------------------------------------------------------------- 1 | import pybdshadow 2 | import pandas as pd 3 | import numpy as np 4 | import geopandas as gpd 5 | from shapely.geometry import Polygon 6 | 7 | 8 | class Testsunlightshadow: 9 | def test_bdshadow_sunlight(self): 10 | 11 | buildings = gpd.GeoDataFrame({ 12 | 'height': [42, 9], 13 | 'geometry': [ 14 | Polygon([(139.698311, 35.533796), 15 | (139.698311, 16 | 35.533642), 17 | (139.699075, 18 | 35.533637), 19 | (139.699079, 20 | 35.53417), 21 | (139.698891, 22 | 35.53417), 23 | (139.698888, 24 | 35.533794), 25 | (139.698311, 35.533796)]), 26 | Polygon([(139.69799, 35.534175), 27 | (139.697988, 35.53389), 28 | (139.698814, 35.533885), 29 | (139.698816, 35.534171), 30 | (139.69799, 35.534175)])]}) 31 | date = pd.to_datetime('2015-01-01 03:45:33.959797119') 32 | 33 | buildings = pybdshadow.bd_preprocess(buildings) 34 | 35 | buildingshadow = pybdshadow.bdshadow_sunlight( 36 | buildings, date, roof=True, include_building=False) 37 | 38 | area = buildingshadow['geometry'].iloc[0] 39 | area = np.array(area.exterior.coords) 40 | truth = np.array([(139.6983434498457, 35.53388784954066), 41 | (139.698343456533, 35.533887872006716), 42 | (139.6984440527688, 35.53417277873741), 43 | (139.69844406145836, 35.534172800060766), 44 | (139.69844408446318, 35.534172801043766), 45 | (139.69881597541797, 35.534171000119045), 46 | (139.69881599883948, 35.53417099885312), 47 | (139.6988159998281, 35.53417097541834), 48 | (139.69881400017155, 35.533885024533475), 49 | (139.6988139988598, 35.53388500115515), 50 | (139.69881397546646, 35.53388500014851), 51 | (139.69834347324914, 35.53388784822488), 52 | (139.6983434498457, 35.53388784954066)]) 53 | assert np.allclose(area, truth) 54 | 55 | pybdshadow.show_bdshadow(buildings=buildings, 56 | shadows=buildingshadow) 57 | -------------------------------------------------------------------------------- /src/pybdshadow/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Qing Yu 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | """ 32 | 33 | import numpy as np 34 | import shapely 35 | import geopandas as gpd 36 | import pandas as pd 37 | from pyproj import CRS,Transformer 38 | from shapely.geometry import Polygon 39 | 40 | def extrude_poly(poly,h): 41 | poly_coords = np.array(poly.exterior.coords) 42 | poly_coords = np.c_[poly_coords, np.ones(poly_coords.shape[0])*h] 43 | return Polygon(poly_coords) 44 | 45 | def make_clockwise(polygon): 46 | if polygon.exterior.is_ccw: 47 | return polygon 48 | else: 49 | return Polygon(list(polygon.exterior.coords)[::-1]) 50 | 51 | def lonlat2aeqd(lonlat, center_lon, center_lat): 52 | ''' 53 | Convert longitude and latitude to azimuthal equidistant projection coordinates. 54 | 55 | Parameters 56 | ---------- 57 | lonlat : numpy.ndarray 58 | Longitude and latitude in degrees. The shape of the array is (n,m,2), where n and m are the number of pixels in the first and second dimension, respectively. The last dimension is for longitude and latitude. 59 | 60 | Returns 61 | ------- 62 | proj_coords : numpy.ndarray 63 | Azimuthal equidistant projection coordinates. The shape of the array is (n,m,2), where n and m are the number of pixels in the first and second dimension, respectively. The last dimension is for x and y coordinates. 64 | 65 | example 66 | ----------------- 67 | >>> import numpy as np 68 | >>> from pybdshadow import utils 69 | >>> lonlat = np.array([[[120,30],[121,31]],[[120,30],[121,31]]]) 70 | >>> proj_coords = utils.lonlat2aeqd(lonlat) 71 | >>> proj_coords 72 | array([[[-48243.5939812 , -55322.02388971], 73 | [ 47752.57582735, 55538.86412435]], 74 | [[-48243.5939812 , -55322.02388971], 75 | [ 47752.57582735, 55538.86412435]]]) 76 | ''' 77 | epsg = CRS.from_proj4("+proj=aeqd +lat_0="+str(center_lat) + 78 | " +lon_0="+str(center_lon)+" +datum=WGS84") 79 | transformer = Transformer.from_crs("EPSG:4326", epsg, always_xy=True) 80 | proj_coords = transformer.transform(lonlat[:, :, 0], lonlat[:, :, 1]) 81 | proj_coords = np.array(proj_coords).transpose([1, 2, 0]) 82 | return proj_coords 83 | 84 | def aeqd2lonlat_3d(proj_coords, meanlon, meanlat): 85 | 86 | # 提取 xy 坐标和 z 坐标 87 | xy_coords = proj_coords[:, :, :2] 88 | z_coords = proj_coords[:, :, 2] if proj_coords.shape[2] > 2 else np.zeros( 89 | xy_coords.shape[:2]) 90 | 91 | # 定义转换器 92 | epsg = CRS.from_proj4("+proj=aeqd +lat_0=" + str(meanlat) + 93 | " +lon_0=" + str(meanlon) + " +datum=WGS84") 94 | transformer = Transformer.from_crs(epsg, "EPSG:4326", always_xy=True) 95 | 96 | # 转换 xy 坐标 97 | lon, lat = transformer.transform(xy_coords[:, :, 0], xy_coords[:, :, 1]) 98 | 99 | # 将转换后的坐标和原始 z 坐标组合 100 | lonlat = np.dstack([lon, lat, z_coords]) 101 | return lonlat 102 | 103 | def aeqd2lonlat(proj_coords,meanlon,meanlat): 104 | ''' 105 | Convert azimuthal equidistant projection coordinates to longitude and latitude. 106 | 107 | Parameters 108 | ---------- 109 | proj_coords : numpy.ndarray 110 | Azimuthal equidistant projection coordinates. The shape of the array is (n,m,2), where n and m are the number of pixels in the first and second dimension, respectively. The last dimension is for x and y coordinates. 111 | meanlon : float 112 | Longitude of the center of the azimuthal equidistant projection in degrees. 113 | meanlat : float 114 | Latitude of the center of the azimuthal equidistant projection in degrees. 115 | 116 | Returns 117 | ------- 118 | lonlat : numpy.ndarray 119 | Longitude and latitude in degrees. The shape of the array is (n,m,2), where n and m are the number of pixels in the first and second dimension, respectively. The last dimension is for longitude and latitude. 120 | 121 | Example 122 | ----------------- 123 | >>> import numpy as np 124 | >>> from pybdshadow import utils 125 | >>> proj_coords = proj_coords = np.array( 126 | [[[-48243.5939812 , -55322.02388971], 127 | [ 47752.57582735, 55538.86412435]], 128 | [[-48243.5939812 , -55322.02388971], 129 | [ 47752.57582735, 55538.86412435]]]) 130 | >>> lonlat = utils.aeqd2lonlat(proj_coords,120.5,30.5) 131 | >>> lonlat 132 | array([[[120., 30.], 133 | [121., 31.]], 134 | [[120., 30.], 135 | [121., 31.]]]) 136 | ''' 137 | 138 | epsg = CRS.from_proj4("+proj=aeqd +lat_0="+str(meanlat)+" +lon_0="+str(meanlon)+" +datum=WGS84") 139 | transformer = Transformer.from_crs( epsg,"EPSG:4326",always_xy = True) 140 | lonlat = transformer.transform(proj_coords[:,:,0], proj_coords[:,:,1]) 141 | lonlat = np.array(lonlat).transpose([1,2,0]) 142 | return lonlat 143 | 144 | def calculate_normal(points): 145 | points = np.array(points) 146 | if points.shape[0] < 3: 147 | raise ValueError("墙至少需要三个点。") 148 | 149 | for i in range(points.shape[0]): 150 | for j in range(i + 1, points.shape[0]): 151 | for k in range(j + 1, points.shape[0]): 152 | vector1 = points[j] - points[i] 153 | vector2 = points[k] - points[i] 154 | normal = np.cross(vector1, vector2) 155 | if np.linalg.norm(normal) != 0: 156 | return normal / np.linalg.norm(normal) 157 | 158 | raise ValueError("该墙所有点共线,无法计算法向量。") 159 | 160 | def has_normal(points): 161 | # 将点列表转换为NumPy数组以便处理 162 | points = np.array(points) 163 | 164 | # 需要至少三个点来形成一个平面 165 | if points.shape[0] < 3: 166 | return False 167 | 168 | # 寻找不共线的三个点 169 | for i in range(points.shape[0]): 170 | for j in range(i+1, points.shape[0]): 171 | for k in range(j+1, points.shape[0]): 172 | # 计算两个向量 173 | vector1 = points[j] - points[i] 174 | vector2 = points[k] - points[i] 175 | 176 | # 计算叉乘 177 | normal = np.cross(vector1, vector2) 178 | 179 | # 检查法向量是否非零(即点不共线) 180 | if np.linalg.norm(normal) != 0: 181 | # 返回归一化的法向量 182 | return True 183 | return False 184 | 185 | def count_overlapping_features(gdf,buffer = True): 186 | # 计算多边形的重叠次数 187 | if buffer: 188 | bounds = gdf.geometry.buffer(1e-9).exterior.unary_union 189 | else: 190 | bounds = gdf.geometry.exterior.unary_union 191 | 192 | new_polys = list(shapely.ops.polygonize(bounds)) 193 | new_gdf = gpd.GeoDataFrame(geometry=new_polys) 194 | new_gdf['id'] = range(len(new_gdf)) 195 | new_gdf_centroid = new_gdf.copy() 196 | new_gdf_centroid['geometry'] = new_gdf.geometry.representative_point() 197 | overlapcount = gpd.sjoin(new_gdf_centroid, gdf) 198 | overlapcount = overlapcount.groupby( 199 | ['id'])['index_right'].count().rename('count').reset_index() 200 | out_gdf = pd.merge(new_gdf, overlapcount) 201 | return out_gdf -------------------------------------------------------------------------------- /src/pybdshadow/visiblearea.py: -------------------------------------------------------------------------------- 1 | """ 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Qing Yu 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | """ 32 | 33 | ''' 34 | import pandas as pd 35 | import geopandas as gpd 36 | from suncalc import get_position 37 | from shapely.geometry import Polygon,LineString, MultiPolygon 38 | import math 39 | import numpy as np 40 | #======================================================================================= 41 | 42 | #Calculate building shadow for point light 43 | 44 | #Enter two points to calculate the general equation of the line:Ax+By+C = 0 45 | def calLine(p1, p2): 46 | 47 | A = p2[1] - p1[1] 48 | B = p1[0] - p2[0] 49 | C = p2[0] * p1[1] - p1[0] * p2[1] 50 | 51 | return [A, B, C] 52 | 53 | #Calculate the point of intersection of straight lines 54 | def calCross2DLine_Geo(l1, l2): 55 | 56 | l1A = l1.apply(lambda r: r[0]) 57 | l1B = l1.apply(lambda r: r[1]) 58 | l1C = l1.apply(lambda r: r[2]) 59 | l2A = l2.apply(lambda r: r[0]) 60 | l2B = l2.apply(lambda r: r[1]) 61 | l2C = l2.apply(lambda r: r[2]) 62 | cross = gpd.GeoDataFrame() 63 | 64 | #x = (c2 * b1 - c1 * b2) / (a1 * b2 - a2 * b1)S 65 | cross['lon'] = (l2C * l1B - l1C * l2B) / (l1A * l2B - l2A * l1B) 66 | #corss['lon'] = corss[- corss.isnull()] 67 | 68 | #y = (c1 * a2 - c2 * a1) / (a1 * b2 - a2 * b1) 69 | cross['lat'] = (l1C * l2A - l2C * l1A) / (l1A * l2B - l2A * l1B) 70 | cross['lonlat'] = cross.apply(lambda r: [r['lon'], r['lat']], axis=1) 71 | return cross['lonlat'] 72 | 73 | #Calculate the point of intersection of straight lines:(numpy) 74 | def calCross2DLine1(l1, l2): 75 | 76 | n = np.shape(l1)[0] 77 | cross = np.zeros((n,2)) 78 | #x = (c2 * b1 - c1 * b2) / (a1 * b2 - a2 * b1) 79 | cross[:,0] = (l2[:,2] * l1[:,1] - l1[:,2] * l2[:,1]) / (l1[:,0] * l2[:,1] - l2[:,0] * l1[:,1]) 80 | #y = (c1 * a2 - c2 * a1) / (a1 * b2 - a2 * b1) 81 | cross[:,1] = (l1[:,2] * l2[:,0] - l2[:,2] * l1[:,0]) / (l1[:,0] * l2[:,1] - l2[:,0] * l1[:,1]) 82 | 83 | return cross 84 | 85 | #Calculate the vector formed by two straight lines 86 | def calVector2(p1,p2): 87 | return [p1[0] - p2[0], p1[1] - p2[1]] 88 | 89 | def vecDotMultiply(v1, v2): 90 | return v1[0] * v2[0] + v1[1] * v2[1] 91 | 92 | def vecDotMultiply_Geo(v1,v2): 93 | vDot = gpd.GeoDataFrame() 94 | vDot['v1'] = v1 95 | vDot['v2'] = v2 96 | vDot['vDotRes'] = vDot.apply(lambda r : vecDotMultiply(r['v1'],r['v2']),axis=1) 97 | return vDot['vDotRes'] 98 | 99 | def judgeDotSymbol(r,i): 100 | if r['crossPDot'] < 0: 101 | return r['beShelterWall'][1-i] 102 | else: 103 | return r['crossP'] 104 | 105 | def calDistance(p1,p2): #输入两个点 106 | return math.sqrt((p2[0] - p1[0])*(p2[0] - p1[0])+(p2[1] - p1[1])*(p2[1] - p1[1])) 107 | 108 | def calWallsShadow(pointLight,wallJoinShadow): 109 | #算shelterWall在beShelterWall上的投影 110 | #print(wallJoinShadow) 111 | 112 | shelterWall = wallJoinShadow['wall'] 113 | shelterHeight = wallJoinShadow['height'] 114 | beShelterWall = wallJoinShadow['beShelterWall'] 115 | 116 | pointLightPosition = pointLight['position'] 117 | 118 | #计算被遮挡面所在直线 119 | pBeShelterWall = beShelterWall.apply(lambda r: [r[0],r[1]]) 120 | lBeShelterWall = pBeShelterWall.apply(lambda r: calLine(r[0], r[1])) 121 | shadow = gpd.GeoDataFrame() 122 | shadow['beShelterWall'] = beShelterWall 123 | shadow['beShelterHeight'] = wallJoinShadow['beShelterHeight'] 124 | shadow['beShelterIndex'] = wallJoinShadow['index_right'] 125 | 126 | for i in range(2): 127 | #计算中心点与遮挡面上的点构成的直线 128 | p = shelterWall.apply(lambda r: [r[i],pointLightPosition[0:2]])#exterior. 129 | l = p.apply(lambda r: calLine(r[0], r[1]))# 130 | 131 | shadowPoint = gpd.GeoDataFrame() 132 | shadowPoint['shelterWall'] = shelterWall 133 | shadowPoint['beShelterWall'] = beShelterWall 134 | shadowPoint['shelterHeight'] = shelterHeight 135 | 136 | shadowPoint['crossP'] = calCross2DLine_Geo(l, lBeShelterWall) #射线与投影面的交点 137 | ##另一种形式:使用numpy矩阵求交点,结果相同 138 | #l1Numpy = np.array(list(l1)) 139 | #lBeShelterWallNumpy = np.array(list(lBeShelterWall)) 140 | #cross = calCross2DLine1(l1Numpy, lBeShelterWallNumpy) 141 | 142 | #通过向量点乘结果判断相交位置 143 | v = shelterWall.apply(lambda r: calVector2(r[i],pointLightPosition[0:2])) 144 | vShadow = shadowPoint['crossP'].apply(lambda r: calVector2(r,pointLightPosition[0:2])) 145 | shadowPoint['crossPDot'] = vecDotMultiply_Geo(v,vShadow) 146 | shadowPoint['point'] = shadowPoint.apply(lambda r: judgeDotSymbol(r,i),axis = 1) 147 | #print(shadowPoint['point']) 148 | 149 | #高度的比例 150 | shadowPoint['height'] = shadowPoint.apply(lambda r: r['shelterHeight']/calDistance(r['shelterWall'][i],pointLightPosition[0:2]) 151 | *calDistance(r['crossP'],pointLightPosition[0:2]),axis = 1) 152 | shadow['Point ' + str(i)] = shadowPoint['point'] 153 | shadow['Height ' + str(i)] = shadowPoint['height'] 154 | #print(shadow) 155 | return shadow 156 | 157 | def convert3To2(point,originP,directP): 158 | #X = gpd.GeoDataFrame() 159 | x = calDistance(point,originP) 160 | v1 = [directP[0] - originP[0],directP[1] - originP[1]] 161 | v2 = [point[0] - originP[0],point[1] - originP[1]] 162 | if (vecDotMultiply(v1, v2)<0): 163 | x *= -1 164 | return x 165 | 166 | def getWallShape(shadow): 167 | shadow['beShelPointX'] =shadow.apply(lambda r:convert3To2( 168 | r['beShelterWall'][1],r['beShelterWall'][0],r['beShelterWall'][1]),axis = 1) 169 | shadow['beShelShape2'] = shadow.apply(lambda r:Polygon([[0.0,0.0], 170 | [r['beShelPointX'],0.0], 171 | [r['beShelPointX'],r['beShelterHeight']], 172 | [0.0,r['beShelterHeight']], 173 | [0.0,0.0]]),axis = 1) 174 | 175 | #print(shadow['beShelShape2']) 176 | shadow['shelPointX1'] =shadow.apply(lambda r:convert3To2( 177 | r['Point 0'],r['beShelterWall'][0],r['beShelterWall'][1]),axis = 1) 178 | 179 | shadow['shelPointX2'] =shadow.apply(lambda r:convert3To2( 180 | r['Point 1'],r['beShelterWall'][0],r['beShelterWall'][1]),axis = 1) 181 | temp = shadow[-shadow['shelPointX1'].isnull()].copy() 182 | shadow = temp[-temp['shelPointX2'].isnull()].copy() 183 | shadow['shelShape2'] = shadow.apply(lambda r:Polygon([[r['shelPointX1'],0.0], 184 | [r['shelPointX2'],0.0], 185 | [r['shelPointX2'],r['Height 1']], 186 | [r['shelPointX1'],r['Height 0']], 187 | [r['shelPointX1'],0.0]]),axis = 1) 188 | #print(shadow['shelShape2']) 189 | return shadow 190 | 191 | 192 | def decrease(p1,p2): 193 | return [p1[0] - p2[0],p1[1] - p2[1],calDistance(p1,p2)] 194 | 195 | def calVisibleShape(shadow): 196 | #print(shadow) 197 | beShelShape2 = gpd.GeoSeries(shadow['beShelShape2'])#[-shadow['beShelShape2'].isnull()] 198 | shelShape2 = gpd.GeoSeries(shadow['shelShape2']) 199 | shadow['diff'] = beShelShape2.difference(shelShape2, align=False) 200 | 201 | #area = shadow[shadow['diff'].area != 0] 202 | union = shadow.reset_index().sort_values(by=['beShelterIndex']) 203 | 204 | unionShapes = [] 205 | beShelShapes = [] 206 | origins = [] 207 | directions = [] 208 | for i in range(len(union)): 209 | r = union.iloc[i] # 210 | if (i!= 0) and (r['beShelterIndex'] == union.iloc[i-1]['beShelterIndex']): 211 | unionShapes[-1] = unionShape.union(r['diff']) 212 | else: 213 | #创建一个新的union 214 | unionShape = r['diff'] 215 | unionShapes.append(unionShape) 216 | #被遮挡的shape 217 | #beShelShape = r['beShelShape2'] 218 | beShelShapes.append(r['beShelShape2']) 219 | #原点 220 | origins.append(r['beShelterWall'][0]) 221 | directions.append(decrease(r['beShelterWall'][1],r['beShelterWall'][0])) 222 | 223 | 224 | unionShapes = gpd.GeoSeries(unionShapes)#GeoSeries 225 | 226 | beShelShapes = gpd.GeoSeries(beShelShapes) 227 | visibleShapes = gpd.GeoDataFrame() 228 | visiShapes = beShelShapes.difference(unionShapes) 229 | 230 | visibleShapes['visiShapes'] = visiShapes#.apply(lambda r: list(r.exterior.coords))list 231 | visibleShapes['coordSysOrigins'] = origins 232 | visibleShapes['coordSysDir'] = directions 233 | #print(visibleShapes) 234 | 235 | #visibleShapes = visibleShapes[len(visibleShapes['visiShapes']) != 0]#.area 236 | visibleShapes = visibleShapes[visibleShapes['visiShapes'].area != 0] 237 | #print(visibleShapes) 238 | 239 | return visibleShapes 240 | 241 | def convert2To3(r): 242 | results = [] 243 | visiList = list(r['visiShapes'].exterior.coords) 244 | #print(visiList) 245 | for i in range(len(visiList)): 246 | x = r['coordSysOrigins'][0] + r['coordSysDir'][0] * (visiList[i][0] / r['coordSysDir'][2]) 247 | y = r['coordSysOrigins'][1] + r['coordSysDir'][1] * (visiList[i][0] / r['coordSysDir'][2]) 248 | results.append([x,y,visiList[i][1]]) 249 | return results 250 | 251 | def calVisibleArea(wall,pointLight): 252 | 253 | wallsGeo = gpd.GeoDataFrame() 254 | wallsGeo['geometry'] = wall['wall'].apply(lambda r: LineString(r)) #几何图形对应的阴影 255 | wallsGeo['beShelterWall'] = wall['wall']#几何图形对应的地面坐标 256 | wallsGeo['beShelterHeight'] = wall['height']#几何图形对应的高度 257 | 258 | wallJoinShadow = gpd.sjoin(wall,wallsGeo) #wall遮挡的墙面,被遮挡的墙面 259 | 260 | shadow = calWallsShadow(pointLight,wallJoinShadow) #计算墙面阴影的地面点 261 | shadow = getWallShape(shadow) #组成遮挡阴影以及被遮挡面的shape 262 | visibleShapes = calVisibleShape(shadow) #计算可视面积 263 | visibleShapes['visibleShapes'] = visibleShapes.apply(lambda r: convert2To3(r),axis = 1) 264 | return visibleShapes['visibleShapes'] 265 | ''' -------------------------------------------------------------------------------- /src/pybdshadow/visualization.py: -------------------------------------------------------------------------------- 1 | """ 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2022, Qing Yu 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | """ 32 | 33 | import numpy as np 34 | import geopandas as gpd 35 | from shapely.geometry import Polygon 36 | 37 | def show_bdshadow(buildings=gpd.GeoDataFrame(), 38 | shadows=gpd.GeoDataFrame(), 39 | ad=gpd.GeoDataFrame(), 40 | ad_visualArea=gpd.GeoDataFrame(), 41 | height='height', 42 | zoom='auto', 43 | vis_height = 800): 44 | ''' 45 | Visualize the building and shadow with keplergl. 46 | 47 | Parameters 48 | -------------------- 49 | buildings : GeoDataFrame 50 | Buildings. coordinate system should be WGS84 51 | shadows : GeoDataFrame 52 | Building shadows. coordinate system should be WGS84 53 | ad : GeoDataFrame 54 | Advertisment. coordinate system should be WGS84 55 | ad_visualArea : GeoDataFrame 56 | Visualarea of Advertisment. coordinate system should be WGS84 57 | height : string 58 | Column name of building height 59 | zoom : number 60 | Zoom level of the map 61 | 62 | Return 63 | -------------------- 64 | vmap : keplergl.keplergl.KeplerGl 65 | Visualizations provided by keplergl 66 | ''' 67 | displaybuilding = buildings.copy() 68 | displaybuildingshadow = shadows.copy() 69 | displayad = ad.copy() 70 | displayad_visualArea = ad_visualArea.copy() 71 | vmapdata = {} 72 | layers = [] 73 | if len(displayad_visualArea) == 0: 74 | displayad_visualArea['geometry'] = [] 75 | displayad_visualArea[height] = [] 76 | else: 77 | 78 | bdcentroid = displayad_visualArea['geometry'].bounds[[ 79 | 'minx', 'miny', 'maxx', 'maxy']] 80 | lon_center, lat_center = bdcentroid['minx'].mean( 81 | ), bdcentroid['miny'].mean() 82 | lon_min, lon_max = bdcentroid['minx'].min(), bdcentroid['maxx'].max() 83 | vmapdata['ad_visualArea'] = displayad_visualArea 84 | layers.append( 85 | {'id': 'lz48o1', 86 | 'type': 'geojson', 87 | 'config': { 88 | 'dataId': 'ad_visualArea', 89 | 'label': 'ad_visualArea', 90 | 'color': [255, 255, 0], 91 | 'highlightColor': [252, 242, 26, 255], 92 | 'columns': {'geojson': 'geometry'}, 93 | 'isVisible': True, 94 | 'visConfig': { 95 | 'opacity': 0.32, 96 | 'strokeOpacity': 0.8, 97 | 'thickness': 0.5, 98 | 'strokeColor': [255, 153, 31], 99 | 'colorRange': {'name': 'Global Warming', 100 | 'type': 'sequential', 101 | 'category': 'Uber', 102 | 'colors': ['#5A1846', 103 | '#900C3F', 104 | '#C70039', 105 | '#E3611C', 106 | '#F1920E', 107 | '#FFC300']}, 108 | 'strokeColorRange': {'name': 'Global Warming', 109 | 'type': 'sequential', 110 | 'category': 'Uber', 111 | 'colors': ['#5A1846', 112 | '#900C3F', 113 | '#C70039', 114 | '#E3611C', 115 | '#F1920E', 116 | '#FFC300']}, 117 | 'radius': 10, 118 | 'sizeRange': [0, 10], 119 | 'radiusRange': [0, 50], 120 | 'heightRange': [0, 500], 121 | 'elevationScale': 5, 122 | 'enableElevationZoomFactor': True, 123 | 'stroked': False, 124 | 'filled': True, 125 | 'enable3d': False, 126 | 'wireframe': False}, 127 | 'hidden': False, 128 | 'textLabel': [{ 129 | 'field': None, 130 | 'color': [255, 255, 255], 131 | 'size': 18, 132 | 'offset': [0, 0], 133 | 'anchor': 'start', 134 | 'alignment': 'center'}]}, 135 | 'visualChannels': { 136 | 'colorField': None, 137 | 'colorScale': 'quantile', 138 | 'strokeColorField': None, 139 | 'strokeColorScale': 'quantile', 140 | 'sizeField': None, 141 | 'sizeScale': 'linear', 142 | 'heightField': None, 143 | 'heightScale': 'linear', 144 | 'radiusField': None, 145 | 'radiusScale': 'linear'}}) 146 | if len(displayad) == 0: 147 | displayad['geometry'] = [] 148 | displayad[height] = [] 149 | else: 150 | vmapdata['advertisment'] = displayad 151 | layers.append( 152 | {'id': 'lz48o2', 153 | 'type': 'geojson', 154 | 'config': { 155 | 'dataId': 'advertisment', 156 | 'label': 'advertisment', 157 | 'color': [255, 0, 0], 158 | 'highlightColor': [252, 242, 26, 255], 159 | 'columns': {'geojson': 'geometry'}, 160 | 'isVisible': True, 161 | 'visConfig': { 162 | 'opacity': 0.32, 163 | 'strokeOpacity': 0.8, 164 | 'thickness': 0.5, 165 | 'strokeColor': [255, 153, 31], 166 | 'colorRange': {'name': 'Global Warming', 167 | 'type': 'sequential', 168 | 'category': 'Uber', 169 | 'colors': ['#5A1846', 170 | '#900C3F', 171 | '#C70039', 172 | '#E3611C', 173 | '#F1920E', 174 | '#FFC300']}, 175 | 'strokeColorRange': {'name': 'Global Warming', 176 | 'type': 'sequential', 177 | 'category': 'Uber', 178 | 'colors': ['#5A1846', 179 | '#900C3F', 180 | '#C70039', 181 | '#E3611C', 182 | '#F1920E', 183 | '#FFC300']}, 184 | 'radius': 10, 185 | 'sizeRange': [0, 10], 186 | 'radiusRange': [0, 50], 187 | 'heightRange': [0, 500], 188 | 'elevationScale': 5, 189 | 'enableElevationZoomFactor': True, 190 | 'stroked': False, 191 | 'filled': True, 192 | 'enable3d': False, 193 | 'wireframe': False}, 194 | 'hidden': False, 195 | 'textLabel': [{ 196 | 'field': None, 197 | 'color': [255, 255, 255], 198 | 'size': 18, 199 | 'offset': [0, 0], 200 | 'anchor': 'start', 201 | 'alignment': 'center'}]}, 202 | 'visualChannels': { 203 | 'colorField': None, 204 | 'colorScale': 'quantile', 205 | 'strokeColorField': None, 206 | 'strokeColorScale': 'quantile', 207 | 'sizeField': None, 208 | 'sizeScale': 'linear', 209 | 'heightField': None, 210 | 'heightScale': 'linear', 211 | 'radiusField': None, 212 | 'radiusScale': 'linear'}}) 213 | bdcentroid = displayad['geometry'].bounds[[ 214 | 'minx', 'miny', 'maxx', 'maxy']] 215 | lon_center, lat_center = bdcentroid['minx'].mean( 216 | ), bdcentroid['miny'].mean() 217 | lon_min, lon_max = bdcentroid['minx'].min(), bdcentroid['maxx'].max() 218 | 219 | if len(displaybuilding) == 0: 220 | displaybuilding['geometry'] = [] 221 | displaybuilding[height] = [] 222 | else: 223 | vmapdata['building'] = displaybuilding 224 | 225 | layers.append({ 226 | 'id': 'lz48o3', 227 | 'type': 'geojson', 228 | 'config': { 229 | 'dataId': 'building', 230 | 'label': 'building', 231 | 'color': [169, 203, 237], 232 | 'highlightColor': [252, 242, 26, 255], 233 | 'columns': {'geojson': 'geometry'}, 234 | 'isVisible': True, 235 | 'visConfig': { 236 | 'opacity': 0.8, 237 | 'strokeOpacity': 0.8, 238 | 'thickness': 0.5, 239 | 'strokeColor': [221, 178, 124], 240 | 'colorRange': { 241 | 'name': 'Global Warming', 242 | 'type': 'sequential', 243 | 'category': 'Uber', 244 | 'colors': ['#5A1846', 245 | '#900C3F', 246 | '#C70039', 247 | '#E3611C', 248 | '#F1920E', 249 | '#FFC300']}, 250 | 'strokeColorRange': {'name': 'Global Warming', 251 | 'type': 'sequential', 252 | 'category': 'Uber', 253 | 'colors': ['#5A1846', 254 | '#900C3F', 255 | '#C70039', 256 | '#E3611C', 257 | '#F1920E', 258 | '#FFC300']}, 259 | 'radius': 10, 260 | 'sizeRange': [0, 10], 261 | 'radiusRange': [0, 50], 262 | 'heightRange': [0, 500], 263 | 'elevationScale': 0.3, 264 | 'enableElevationZoomFactor': True, 265 | 'stroked': False, 266 | 'filled': True, 267 | 'enable3d': True, 268 | 'wireframe': False}, 269 | 'hidden': False, 270 | 'textLabel': [{'field': None, 271 | 'color': [255, 255, 255], 272 | 'size': 18, 273 | 'offset': [0, 0], 274 | 'anchor': 'start', 275 | 'alignment': 'center'}]}, 276 | 'visualChannels': {'colorField': None, 277 | 'colorScale': 'quantile', 278 | 'strokeColorField': None, 279 | 'strokeColorScale': 'quantile', 280 | 'sizeField': None, 281 | 'sizeScale': 'linear', 282 | 'heightField': { 283 | 'name': 'height', 284 | 'type': 'integer'}, 285 | 'heightScale': 'linear', 286 | 'radiusField': None, 287 | 'radiusScale': 'linear'}}) 288 | bdcentroid = displaybuilding['geometry'].bounds[[ 289 | 'minx', 'miny', 'maxx', 'maxy']] 290 | lon_center, lat_center = bdcentroid['minx'].mean( 291 | ), bdcentroid['miny'].mean() 292 | lon_min, lon_max = bdcentroid['minx'].min(), bdcentroid['maxx'].max() 293 | if len(displaybuildingshadow) == 0: 294 | displaybuildingshadow['geometry'] = [] 295 | else: 296 | bdcentroid = displaybuildingshadow['geometry'].bounds[[ 297 | 'minx', 'miny', 'maxx', 'maxy']] 298 | lon_center, lat_center = bdcentroid['minx'].mean( 299 | ), bdcentroid['miny'].mean() 300 | lon_min, lon_max = bdcentroid['minx'].min(), bdcentroid['maxx'].max() 301 | vmapdata['shadow'] = displaybuildingshadow 302 | layers.append( 303 | {'id': 'lz48o4', 304 | 'type': 'geojson', 305 | 'config': { 306 | 'dataId': 'shadow', 307 | 'label': 'shadow', 308 | 'color': [73, 73, 73], 309 | 'highlightColor': [252, 242, 26, 255], 310 | 'columns': {'geojson': 'geometry'}, 311 | 'isVisible': True, 312 | 'visConfig': { 313 | 'opacity': 0.32, 314 | 'strokeOpacity': 0.8, 315 | 'thickness': 0.5, 316 | 'strokeColor': [255, 153, 31], 317 | 'colorRange': {'name': 'Global Warming', 318 | 'type': 'sequential', 319 | 'category': 'Uber', 320 | 'colors': ['#5A1846', 321 | '#900C3F', 322 | '#C70039', 323 | '#E3611C', 324 | '#F1920E', 325 | '#FFC300']}, 326 | 'strokeColorRange': {'name': 'Global Warming', 327 | 'type': 'sequential', 328 | 'category': 'Uber', 329 | 'colors': ['#5A1846', 330 | '#900C3F', 331 | '#C70039', 332 | '#E3611C', 333 | '#F1920E', 334 | '#FFC300']}, 335 | 'radius': 10, 336 | 'sizeRange': [0, 10], 337 | 'radiusRange': [0, 50], 338 | 'heightRange': [0, 500], 339 | 'elevationScale': 5, 340 | 'enableElevationZoomFactor': True, 341 | 'stroked': False, 342 | 'filled': True, 343 | 'enable3d': False, 344 | 'wireframe': False}, 345 | 'hidden': False, 346 | 'textLabel': [{ 347 | 'field': None, 348 | 'color': [255, 255, 255], 349 | 'size': 18, 350 | 'offset': [0, 0], 351 | 'anchor': 'start', 352 | 'alignment': 'center'}]}, 353 | 'visualChannels': { 354 | 'colorField': None, 355 | 'colorScale': 'quantile', 356 | 'strokeColorField': None, 357 | 'strokeColorScale': 'quantile', 358 | 'sizeField': None, 359 | 'sizeScale': 'linear', 360 | 'heightField': None, 361 | 'heightScale': 'linear', 362 | 'radiusField': None, 363 | 'radiusScale': 'linear'}}) 364 | try: 365 | from keplergl import KeplerGl 366 | except ImportError: 367 | raise ImportError( 368 | "Please install keplergl, run " 369 | "the following code in cmd: pip install keplergl") 370 | 371 | if zoom == 'auto': 372 | zoom = 8.5-np.log(lon_max-lon_min)/np.log(2) 373 | vmap = KeplerGl(config={ 374 | 'version': 'v1', 375 | 'config': { 376 | 'visState': { 377 | 'filters': [], 378 | 'layers': layers, 379 | 'layerBlending': 'normal', 380 | 'animationConfig': {'currentTime': None, 'speed': 1}}, 381 | 'mapState': {'bearing': -3, 382 | 'dragRotate': True, 383 | 'latitude': lat_center, 384 | 'longitude': lon_center, 385 | 'pitch': 50, 386 | 'zoom': zoom, 387 | 'isSplit': False}, 388 | 'mapStyle': {'styleType': 'light', 389 | 'topLayerGroups': {}, 390 | 'visibleLayerGroups': {'label': True, 391 | 'road': True, 392 | 'border': False, 393 | 'building': True, 394 | 'water': True, 395 | 'land': True}, 396 | 'mapStyles': {}}}}, data=vmapdata, height=vis_height) 397 | return vmap 398 | 399 | 400 | 401 | def show_sunshine(sunshine=gpd.GeoDataFrame(), 402 | zoom='auto',vis_height = 800): 403 | ''' 404 | Visualize the sunshine with keplergl. 405 | 406 | Parameters 407 | -------------------- 408 | sunshine : GeoDataFrame 409 | sunshine. coordinate system should be WGS84 410 | zoom : number 411 | Zoom level of the map 412 | 413 | Return 414 | -------------------- 415 | vmap : keplergl.keplergl.KeplerGl 416 | Visualizations provided by keplergl 417 | ''' 418 | def offset_wall(wall_poly): 419 | wall_coords = np.array(wall_poly.exterior.coords) 420 | wall_coords[:,0]+=wall_coords[:,2]*0.000000001 421 | wall_coords[:,1]+=wall_coords[:,2]*0.000000001 422 | return Polygon(wall_coords) 423 | sunshine = sunshine.copy() 424 | sunshine['geometry'] = sunshine['geometry'].apply(offset_wall) 425 | vmapdata = {} 426 | layers = [] 427 | 428 | bdcentroid = sunshine['geometry'].bounds[[ 429 | 'minx', 'miny', 'maxx', 'maxy']] 430 | lon_center, lat_center = bdcentroid['minx'].mean( 431 | ), bdcentroid['miny'].mean() 432 | lon_min, lon_max = bdcentroid['minx'].min(), bdcentroid['maxx'].max() 433 | vmapdata['sunshine'] = sunshine 434 | 435 | 436 | layers.append( 437 | {'id': 'lz48o4', 438 | 'type': 'geojson', 439 | 'config': { 440 | 'dataId': 'sunshine', 441 | 'label': 'sunshine', 442 | 'color': [73, 73, 73], 443 | 'highlightColor': [252, 242, 26, 255], 444 | 'columns': {'geojson': 'geometry'}, 445 | 'isVisible': True, 446 | 'visConfig': { 447 | 'opacity': 1, 448 | 'strokeOpacity': 1, 449 | 'thickness': 0.5, 450 | 'strokeColor': [255, 153, 31], 451 | 'colorRange': {'name': 'UberPool 9', 452 | 'type': 'sequential', 453 | 'category': 'Uber', 454 | 'colors': ['#2C51BE', 455 | '#482BBD', 456 | '#7A0DA6', 457 | '#AE0E7F', 458 | '#CF1750', 459 | '#E31A1A', 460 | '#FD7900', 461 | '#FAC200', 462 | '#FAE300'], 463 | 'reversed': False}, 464 | 'strokeColorRange': {'name': 'Global Warming', 465 | 'type': 'sequential', 466 | 'category': 'Uber', 467 | 'colors': ['#5A1846', 468 | '#900C3F', 469 | '#C70039', 470 | '#E3611C', 471 | '#F1920E', 472 | '#FFC300']}, 473 | 'radius': 10, 474 | 'sizeRange': [0, 10], 475 | 'radiusRange': [0, 50], 476 | 'heightRange': [0, 500], 477 | 'elevationScale': 5, 478 | 'enableElevationZoomFactor': True, 479 | 'stroked': False, 480 | 'filled': True, 481 | 'enable3d': False, 482 | 'wireframe': False}, 483 | 'hidden': False, 484 | 'textLabel': [{ 485 | 'field': None, 486 | 'color': [255, 255, 255], 487 | 'size': 18, 488 | 'offset': [0, 0], 489 | 'anchor': 'start', 490 | 'alignment': 'center'}]}, 491 | 'visualChannels': { 492 | 'colorField': {'name': 'Hour', 'type': 'real'}, 493 | 'colorScale': 'quantize', 494 | 'strokeColorField': None, 495 | 'strokeColorScale': 'quantize', 496 | 'sizeField': None, 497 | 'sizeScale': 'linear', 498 | 'heightField': None, 499 | 'heightScale': 'linear', 500 | 'radiusField': None, 501 | 'radiusScale': 'linear'}}) 502 | try: 503 | from keplergl import KeplerGl 504 | except ImportError: 505 | raise ImportError( 506 | "Please install keplergl, run " 507 | "the following code in cmd: pip install keplergl") 508 | 509 | if zoom == 'auto': 510 | zoom = 10.5-np.log(lon_max-lon_min)/np.log(2) 511 | vmap = KeplerGl(config={ 512 | 'version': 'v1', 513 | 'config': { 514 | 'visState': { 515 | 'filters': [], 516 | 'layers': layers, 517 | 'layerBlending': 'normal', 518 | 'animationConfig': {'currentTime': None, 'speed': 1}}, 519 | 'mapState': {'bearing': 30, 520 | 'dragRotate': True, 521 | 'latitude': lat_center, 522 | 'longitude': lon_center, 523 | 'pitch': 50, 524 | 'zoom': zoom, 525 | 'isSplit': False}, 526 | 'mapStyle': {'styleType': 'light', 527 | 'topLayerGroups': {}, 528 | 'visibleLayerGroups': {'label': True, 529 | 'road': True, 530 | 'border': False, 531 | 'building': True, 532 | 'water': True, 533 | 'land': True}, 534 | 'mapStyles': {}}}}, data=vmapdata, height=vis_height) 535 | return vmap 536 | 537 | --------------------------------------------------------------------------------