├── .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`的测试结果在[](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 | 
4 |
5 | [](https://pybdshadow.readthedocs.io/en/latest/?badge=latest) [](https://pepy.tech/project/pybdshadow) [](https://codecov.io/gh/ni1o1/pybdshadow) [](https://github.com/ni1o1/pybdshadow/actions/workflows/tests.yml) [](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 [](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 | 
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 | 
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 | 
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 | 
112 |
113 | Result of sunshine time on the ground:
114 |
115 | 
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 [](https://github.com/ni1o1/pybdshadow/graphs/contributors) 
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 |
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 | Id |
55 | Floor |
56 | height |
57 | x |
58 | y |
59 | geometry |
60 |
61 |
62 |
63 |
64 | 0 |
65 | 0 |
66 | 2 |
67 | 6.0 |
68 | 120.597313 |
69 | 31.309152 |
70 | POLYGON ((120.59739 31.30921, 120.59740 31.309... |
71 |
72 |
73 | 1 |
74 | 0 |
75 | 2 |
76 | 6.0 |
77 | 120.597276 |
78 | 31.309312 |
79 | POLYGON ((120.59737 31.30938, 120.59738 31.309... |
80 |
81 |
82 | 2 |
83 | 0 |
84 | 2 |
85 | 6.0 |
86 | 120.597313 |
87 | 31.308982 |
88 | POLYGON ((120.59741 31.30905, 120.59742 31.308... |
89 |
90 |
91 | 3 |
92 | 0 |
93 | 2 |
94 | 6.0 |
95 | 120.597272 |
96 | 31.309489 |
97 | POLYGON ((120.59735 31.30955, 120.59736 31.309... |
98 |
99 |
100 | 4 |
101 | 0 |
102 | 2 |
103 | 6.0 |
104 | 120.597128 |
105 | 31.309778 |
106 | POLYGON ((120.59729 31.30986, 120.59730 31.309... |
107 |
108 |
109 |
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 | geometry |
162 | Id |
163 | Floor |
164 | height |
165 | x |
166 | y |
167 | building_id |
168 |
169 |
170 |
171 |
172 | 0 |
173 | POLYGON ((120.60496 31.29717, 120.60521 31.297... |
174 | 0 |
175 | 2 |
176 | 6.0 |
177 | 120.604951 |
178 | 31.297207 |
179 | 0 |
180 |
181 |
182 | 1 |
183 | POLYGON ((120.60494 31.29728, 120.60496 31.297... |
184 | 0 |
185 | 2 |
186 | 6.0 |
187 | 120.604951 |
188 | 31.297207 |
189 | 1 |
190 |
191 |
192 | 0 |
193 | POLYGON ((120.59739 31.30921, 120.59740 31.309... |
194 | 0 |
195 | 2 |
196 | 6.0 |
197 | 120.597313 |
198 | 31.309152 |
199 | 2 |
200 |
201 |
202 | 1 |
203 | POLYGON ((120.59737 31.30938, 120.59738 31.309... |
204 | 0 |
205 | 2 |
206 | 6.0 |
207 | 120.597276 |
208 | 31.309312 |
209 | 3 |
210 |
211 |
212 | 2 |
213 | POLYGON ((120.59741 31.30905, 120.59742 31.308... |
214 | 0 |
215 | 2 |
216 | 6.0 |
217 | 120.597313 |
218 | 31.308982 |
219 | 4 |
220 |
221 |
222 |
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 | height |
271 | building_id |
272 | geometry |
273 | type |
274 |
275 |
276 |
277 |
278 | 0 |
279 | 6.0 |
280 | 186 |
281 | POLYGON ((120.60080 31.30858, 120.60080 31.308... |
282 | roof |
283 |
284 |
285 | 1 |
286 | 6.0 |
287 | 524 |
288 | POLYGON EMPTY |
289 | roof |
290 |
291 |
292 | 2 |
293 | 6.0 |
294 | 1009 |
295 | POLYGON ((120.60394 31.30111, 120.60394 31.301... |
296 | roof |
297 |
298 |
299 | 3 |
300 | 6.0 |
301 | 2229 |
302 | MULTIPOLYGON (((120.61384 31.29957, 120.61384 ... |
303 | roof |
304 |
305 |
306 | 4 |
307 | 6.0 |
308 | 2297 |
309 | POLYGON ((120.61328 31.29770, 120.61330 31.297... |
310 | roof |
311 |
312 |
313 | ... |
314 | ... |
315 | ... |
316 | ... |
317 | ... |
318 |
319 |
320 | 3072 |
321 | 0.0 |
322 | 3072 |
323 | POLYGON ((120.61484 31.29058, 120.61484 31.290... |
324 | ground |
325 |
326 |
327 | 3073 |
328 | 0.0 |
329 | 3073 |
330 | POLYGON ((120.61532 31.29039, 120.61532 31.290... |
331 | ground |
332 |
333 |
334 | 3074 |
335 | 0.0 |
336 | 3074 |
337 | MULTIPOLYGON (((120.61499 31.29096, 120.61499 ... |
338 | ground |
339 |
340 |
341 | 3075 |
342 | 0.0 |
343 | 3075 |
344 | POLYGON ((120.61472 31.29091, 120.61472 31.290... |
345 | ground |
346 |
347 |
348 | 3076 |
349 | 0.0 |
350 | 3076 |
351 | POLYGON ((120.61491 31.29122, 120.61491 31.291... |
352 | ground |
353 |
354 |
355 |
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 | { width=100% }
62 |
63 | { 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 |
--------------------------------------------------------------------------------