├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── push_translations.yml │ ├── release.yml │ └── test-and-pre-release.yml ├── .gitignore ├── .gitmodules ├── .qgis-plugin-ci ├── CHANGELOG.md ├── GlobeBuilder ├── __init__.py ├── build.py ├── core │ ├── __init__.py │ ├── globe.py │ ├── graticules.py │ ├── halo.py │ └── utils │ │ ├── __init__.py │ │ ├── geocoder.py │ │ └── utils.py ├── definitions │ ├── __init__.py │ ├── projections.py │ └── settings.py ├── logs │ └── .gitignore ├── metadata.txt ├── plugin.py ├── pylintrc ├── resources │ ├── data │ │ ├── ne_110m_admin_0_countries.geojson │ │ └── ne_50m_admin_0_countries.geojson │ ├── icon.png │ └── ui │ │ └── globe_builder_dockwidget_base.ui └── ui │ ├── __init__.py │ └── globe_builder_dockwidget.py ├── LICENSE ├── README.md ├── docs └── development.md ├── images ├── icon.xcf └── screenshots │ ├── geocoding1.gif │ ├── globe_view_gist.gif │ └── layout1.gif └── requirements-dev.txt /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 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 | - Plugin: [e.g. 1.0] 28 | - QGIS [e.g. 3.14] 29 | - Python: [e.g. 3.8] 30 | - OS: [e.g. Windows 10, Fedora 32] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Expected behaviour** 11 | A clear and concise description of what you'd like to happen if you do x. 12 | 13 | **Current behaviour** 14 | A clear and concise description of the current behaviour when you do x. If completely new feature, leave empty. 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. If relevant please also provide version of the plugin and information on the system you are running it on. 21 | -------------------------------------------------------------------------------- /.github/workflows/push_translations.yml: -------------------------------------------------------------------------------- 1 | name: Translations 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | push_translations: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | submodules: true 16 | 17 | - name: Set up Python 3.8 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: 3.8 21 | 22 | - name: Install qgis-plugin-ci 23 | run: pip3 install qgis-plugin-ci 24 | 25 | - name: Push translations 26 | run: qgis-plugin-ci push-translation ${{ secrets.TRANSIFEX_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: published 6 | 7 | jobs: 8 | plugin_dst: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | submodules: true 15 | 16 | - name: Set up Python 3.8 17 | uses: actions/setup-python@v1 18 | with: 19 | python-version: 3.8 20 | 21 | - name: Install Qt lrelease 22 | run: sudo apt-get install qt5-default qttools5-dev-tools 23 | 24 | - name: Install qgis-plugin-ci 25 | run: pip3 install qgis-plugin-ci 26 | 27 | - name: Deploy plugin 28 | run: qgis-plugin-ci release ${GITHUB_REF/refs\/tags\//} --github-token ${{ secrets.GITHUB_TOKEN }} --osgeo-username Joonala --osgeo-password ${{ secrets.OSGEO_PASSWORD }} --transifex-token ${{ secrets.TRANSIFEX_TOKEN }} --disable-submodule-update 29 | -------------------------------------------------------------------------------- /.github/workflows/test-and-pre-release.yml: -------------------------------------------------------------------------------- 1 | # workflow name 2 | name: Tests 3 | 4 | # Controls when the action will run. Triggers the workflow on push or pull request 5 | # events but only for the wanted branches 6 | on: 7 | pull_request: 8 | push: 9 | branches: [master, main] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | linux_tests: 14 | # The type of runner that the job will run on 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | # Remove unsupported versions and add more versions. Use LTR version in the cov_tests job 19 | docker_tags: [release-3_10, release-3_16, latest] 20 | fail-fast: false 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - uses: actions/checkout@v2 26 | with: 27 | submodules: true 28 | 29 | - name: Pull qgis 30 | run: docker pull qgis/qgis:${{ matrix.docker_tags }} 31 | 32 | # Runs all tests 33 | - name: Run tests 34 | run: > 35 | docker run --rm --net=host --volume `pwd`:/app -w=/app -e QGIS_PLUGIN_IN_CI=1 qgis/qgis:${{ matrix.docker_tags }} sh -c 36 | "pip3 install -qr requirements-dev.txt && xvfb-run -s '+extension GLX -screen 0 1024x768x24' 37 | pytest -v --cov=GlobeBuilder --cov-report=xml" 38 | 39 | # Upload coverage report. Will not work if the repo is private 40 | - name: Upload coverage to Codecov 41 | if: ${{ matrix.docker_tags == 'latest' && !github.event.repository.private }} 42 | uses: codecov/codecov-action@v1 43 | with: 44 | file: ./coverage.xml 45 | flags: unittests 46 | fail_ci_if_error: false # set to true when upload is working 47 | verbose: false 48 | 49 | windows_tests: 50 | runs-on: windows-latest 51 | 52 | steps: 53 | - uses: actions/checkout@v2 54 | with: 55 | submodules: true 56 | 57 | - name: Choco install qgis 58 | uses: crazy-max/ghaction-chocolatey@v1 59 | with: 60 | args: install qgis-ltr -y 61 | 62 | - name: Run tests 63 | shell: pwsh 64 | run: | 65 | $env:PATH="C:\Program Files\QGIS 3.16\bin;$env:PATH" 66 | $env:QGIS_PLUGIN_IN_CI=1 67 | python-qgis-ltr.bat -m pip install -qr requirements-dev.txt 68 | python-qgis-ltr.bat -m pytest -v 69 | 70 | pre-release: 71 | name: "Pre Release" 72 | runs-on: "ubuntu-latest" 73 | needs: [linux_tests, windows_tests] 74 | 75 | steps: 76 | - uses: hmarr/debug-action@v2 77 | 78 | - uses: "marvinpinto/action-automatic-releases@latest" 79 | if: ${{ github.event.pull_request }} 80 | with: 81 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 82 | automatic_release_tag: "dev-pr" 83 | prerelease: true 84 | title: "Development Build made for PR #${{ github.event.number }}" 85 | 86 | - uses: "marvinpinto/action-automatic-releases@latest" 87 | if: ${{ github.event.after != github.event.before }} 88 | with: 89 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 90 | automatic_release_tag: "dev" 91 | prerelease: true 92 | title: "Development Build made for master branch" 93 | 94 | - uses: actions/checkout@v2 95 | with: 96 | submodules: true 97 | 98 | - name: Set up Python 3.8 99 | uses: actions/setup-python@v1 100 | with: 101 | python-version: 3.8 102 | 103 | # Needed if the plugin is using Transifex, to have the lrelease command 104 | # - name: Install Qt lrelease 105 | # run: sudo apt-get update && sudo apt-get install qt5-default qttools5-dev-tools 106 | 107 | - name: Install qgis-plugin-ci 108 | run: pip3 install qgis-plugin-ci 109 | 110 | # When Transifex is wanted: --transifex-token ${{ secrets.TRANSIFEX_TOKEN }} 111 | - name: Deploy plugin 112 | if: ${{ github.event.pull_request }} 113 | run: qgis-plugin-ci release dev-pr --github-token ${{ secrets.GITHUB_TOKEN }} --disable-submodule-update 114 | 115 | # When Transifex is wanted: --transifex-token ${{ secrets.TRANSIFEX_TOKEN }} 116 | - name: Deploy plugin 117 | if: ${{ github.event.after != github.event.before }} 118 | run: qgis-plugin-ci release dev --github-token ${{ secrets.GITHUB_TOKEN }} --disable-submodule-update 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | GlobeBuilder/.coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | *~ 104 | 105 | # Projects specifig 106 | .idea 107 | GlobeBuilder.iml 108 | /icon_large.xcf 109 | /GlobeBuilder.zip 110 | /GlobeBuilder/i18n/ 111 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "GlobeBuilder/qgis_plugin_tools"] 2 | path = GlobeBuilder/qgis_plugin_tools 3 | url = https://github.com/GispoCoding/qgis_plugin_tools.git 4 | -------------------------------------------------------------------------------- /.qgis-plugin-ci: -------------------------------------------------------------------------------- 1 | plugin_path: GlobeBuilder 2 | github_organization_slug: GispoCoding 3 | project_slug: GlobeBuilder 4 | transifex_coordinator: Joonalai 5 | transifex_organization: gispo 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ### 0.2.3 - 15/01/2020 4 | 5 | * Initial release 6 | 7 | ### 0.3.0 - 17/01/2020 8 | 9 | * Rounder halo with default of 64 segments. 10 | * New data sources: Sentinel-2 cloudless and Natural Earth 30 degree graticules 11 | 12 | ### 0.4.0 - 24/01/2020 13 | 14 | * Dockable window 15 | * Visualization customization 16 | * Option to center based on a layer 17 | * Bug fixes 18 | 19 | ### 0.5.0 - 11/02/2020 20 | 21 | * Possibility to add Globe to any layout 22 | * More accurate countries and graticules 23 | * Improved UI and customization 24 | * Simple tests 25 | 26 | ### 0.6.0 - 23/09/2020 27 | 28 | * More projections (might work depending on PROJ version) 29 | * Fixed bug with changing origin of the globe 30 | * Fixed rendering artefacts caused by graticules by generating those via processing algorithms 31 | * Various bug fixes 32 | * Use pytest and CI tests 33 | * Automatic release process with qgis-plugin-ci 34 | 35 | ## 36 | -------------------------------------------------------------------------------- /GlobeBuilder/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 5 | # 6 | # 7 | # This file is part of GlobeBuilder. 8 | # 9 | # GlobeBuilder is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # GlobeBuilder is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with GlobeBuilder. If not, see . 21 | 22 | import os 23 | 24 | from .qgis_plugin_tools.infrastructure.debugging import setup_pydevd 25 | 26 | if os.environ.get('QGIS_PLUGIN_USE_DEBUGGER') == 'pydevd': 27 | if os.environ.get('IN_TESTS', "0") != "1" and os.environ.get('QGIS_PLUGIN_IN_CI', "0") != "1": 28 | setup_pydevd() 29 | 30 | 31 | # noinspection PyPep8Naming 32 | def classFactory(iface): # pylint: disable=invalid-name 33 | """Load GlobeBuilder class from file GlobeBuilder. 34 | 35 | :param iface: A QGIS interface instance. 36 | :type iface: QgsInterface 37 | """ 38 | # 39 | from .plugin import GlobeBuilder 40 | return GlobeBuilder(iface) 41 | -------------------------------------------------------------------------------- /GlobeBuilder/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 6 | # 7 | # 8 | # This file is part of GlobeBuilder. 9 | # 10 | # GlobeBuilder is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 2 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # GlobeBuilder is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with GlobeBuilder. If not, see . 22 | 23 | import glob 24 | from typing import List 25 | 26 | from qgis_plugin_tools.infrastructure.plugin_maker import PluginMaker 27 | 28 | """ 29 | ################################################# 30 | # Edit the following to match the plugin 31 | ################################################# 32 | """ 33 | 34 | py_files = [ 35 | fil 36 | for fil in glob.glob("**/*.py", recursive=True) 37 | if "test/" not in fil and "test\\" not in fil 38 | ] 39 | locales = ["fi"] 40 | profile = "dev" 41 | ui_files = list(glob.glob("**/*.ui", recursive=True)) 42 | resources = list(glob.glob("**/*.qrc", recursive=True)) 43 | extra_dirs = ["resources"] 44 | compiled_resources: List[str] = [] 45 | 46 | PluginMaker( 47 | py_files=py_files, 48 | ui_files=ui_files, 49 | resources=resources, 50 | extra_dirs=extra_dirs, 51 | compiled_resources=compiled_resources, 52 | locales=locales, 53 | profile=profile, 54 | ) 55 | -------------------------------------------------------------------------------- /GlobeBuilder/core/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 2 | # 3 | # 4 | # This file is part of GlobeBuilder. 5 | # 6 | # GlobeBuilder is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GlobeBuilder is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GlobeBuilder. If not, see . 18 | -------------------------------------------------------------------------------- /GlobeBuilder/core/globe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 5 | # 6 | # 7 | # This file is part of GlobeBuilder. 8 | # 9 | # GlobeBuilder is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # GlobeBuilder is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with GlobeBuilder. If not, see . 21 | 22 | import logging 23 | import os 24 | 25 | from PyQt5.QtGui import QColor 26 | from qgis.core import (QgsProject, QgsCoordinateReferenceSystem, Qgis, QgsRasterLayer, QgsVectorLayer, 27 | QgsMapThemeCollection, QgsLayoutItemMap, 28 | QgsMapSettings, QgsRectangle, QgsLayoutPoint, QgsUnitTypes, QgsLayoutSize) 29 | 30 | from .graticules import Graticules 31 | from .halo import Halo 32 | from .utils.utils import set_selection_based_style, get_feature_ids_that_intersect_bbox 33 | from ..definitions.projections import Projections 34 | from ..definitions.settings import (LayerConnectionType, S2CLOUDLESS_WMTS_URL, LOCAL_DATA_DIR, 35 | DEFAULT_LAYER_CONNECTION_TYPE, NATURAL_EARTH_BASE_URL, 36 | DEFAULT_ORIGIN, 37 | WGS84) 38 | from ..qgis_plugin_tools.tools.custom_logging import bar_msg 39 | from ..qgis_plugin_tools.tools.i18n import tr 40 | from ..qgis_plugin_tools.tools.resources import plugin_name 41 | from ..qgis_plugin_tools.tools.settings import get_setting 42 | 43 | LOGGER = logging.getLogger(plugin_name()) 44 | 45 | 46 | class Globe: 47 | THEME_NAME = tr('Globe') 48 | GROUP_NAME = tr('Globe') 49 | 50 | def __init__(self, iface, origin=DEFAULT_ORIGIN, projection=Projections.AZIMUTHAL_ORTHOGRAPHIC): 51 | self.iface = iface 52 | self.origin = origin 53 | self.projection = projection 54 | # noinspection PyArgumentList 55 | self.qgis_instance = QgsProject.instance() 56 | 57 | @property 58 | def group(self): 59 | root = self.qgis_instance.layerTreeRoot() 60 | groups = tuple(filter(lambda g: g.name() == Globe.GROUP_NAME, 61 | filter(root.isGroup, root.children()))) 62 | if len(groups) == 1: 63 | return groups[0] 64 | else: 65 | return root.addGroup(Globe.GROUP_NAME) 66 | 67 | def delete_group(self): 68 | group = self.group 69 | root = self.qgis_instance.layerTreeRoot() 70 | if group is not None: 71 | for child in group.children(): 72 | dump = child.dump() 73 | self.qgis_instance.removeMapLayer(dump.split("=")[-1].strip()) 74 | root.removeChildNode(group) 75 | 76 | def set_origin(self, coordinates): 77 | if coordinates is not None: 78 | self.origin = coordinates 79 | 80 | def set_projection(self, projection): 81 | self.projection = projection 82 | 83 | def set_group_visibility(self, is_visible): 84 | self.group.setItemVisibilityCheckedRecursive(is_visible) 85 | 86 | def load_data(self, load_s2, load_countries, load_graticules, countries_color, graticules_color, 87 | intersecting_countries_color, counties_res, graticules_res): 88 | existing_layer_names = self.get_existing_layer_names() 89 | s2_cloudless_layer_name = tr(u'S2 Cloudless 2018') 90 | if load_s2 and s2_cloudless_layer_name not in existing_layer_names: 91 | s2_layer = QgsRasterLayer(S2CLOUDLESS_WMTS_URL, s2_cloudless_layer_name, "wms") 92 | if s2_layer.isValid(): 93 | self.insert_layer_to_group(s2_layer) 94 | else: 95 | LOGGER.warning(tr(u"Could not add Sentinel 2 Cloudless layer"), extra=bar_msg()) 96 | 97 | ne_data = {} 98 | if load_countries: 99 | def style_countries(layer): 100 | if intersecting_countries_color is None: 101 | layer.renderer().symbol().setColor(countries_color) 102 | else: 103 | set_selection_based_style(layer, intersecting_countries_color, countries_color) 104 | ids = get_feature_ids_that_intersect_bbox(layer, self.iface.mapCanvas().extent(), 105 | self.qgis_instance.crs()) 106 | layer.select(ids) 107 | 108 | ne_data[tr(u'Countries')] = ( 109 | 'ne_{}_admin_0_countries.geojson'.format(counties_res), lambda l: style_countries(l)) 110 | 111 | if len(ne_data): 112 | self.load_natural_earth_data(ne_data) 113 | 114 | gra = Graticules(graticules_res) 115 | if load_graticules and gra.LAYER_NAME not in existing_layer_names: 116 | graticule_layer = gra.create_graticules(graticules_color) 117 | if graticule_layer.isValid(): 118 | self.insert_layer_to_group(graticule_layer) 119 | else: 120 | LOGGER.warning(tr('Could not add graticules'), extra=bar_msg()) 121 | 122 | self.iface.mapCanvas().refresh() 123 | 124 | def load_natural_earth_data(self, ne_data): 125 | # TODO: resolution 126 | existing_layer_names = self.get_existing_layer_names() 127 | 128 | connection_type = LayerConnectionType( 129 | get_setting("layerConnectionType", DEFAULT_LAYER_CONNECTION_TYPE.value, int)) 130 | 131 | if connection_type == LayerConnectionType.local: 132 | root = LOCAL_DATA_DIR 133 | else: 134 | root = NATURAL_EARTH_BASE_URL 135 | 136 | for name, data in ne_data.items(): 137 | source, styling_method = data 138 | if name not in existing_layer_names: 139 | layer = QgsVectorLayer(os.path.join(root, source), name, "ogr") 140 | if layer is None: 141 | layer = QgsVectorLayer(os.path.join(LOCAL_DATA_DIR, source), name, "ogr") 142 | if layer is None: 143 | self.iface.messageBar().pushMessage( 144 | tr(u"Could not load Natural Earth layer '{}'", name), 145 | level=Qgis.Warning, duration=3) 146 | else: 147 | layer.setName(name) 148 | self.insert_layer_to_group(layer) 149 | 150 | else: 151 | layer = self.qgis_instance.mapLayersByName(name)[0] 152 | 153 | if styling_method is not None and layer is not None: 154 | styling_method(layer) 155 | layer.triggerRepaint() 156 | 157 | def insert_layer_to_group(self, layer, index=0): 158 | self.qgis_instance.addMapLayer(layer, False) 159 | self.group.insertLayer(index, layer) 160 | 161 | def change_project_projection(self): 162 | # Change to wgs84 to activate the changes in origin 163 | self.qgis_instance.setCrs(WGS84) 164 | proj_string = self.projection.value.proj_str(self.origin) 165 | crs = QgsCoordinateReferenceSystem() 166 | crs.createFromProj(proj_string) 167 | self.qgis_instance.setCrs(crs) 168 | 169 | def change_temporarily_to_globe_projection(self): 170 | crs = self.qgis_instance.crs() 171 | self.change_project_projection() 172 | self.qgis_instance.setCrs(crs) 173 | 174 | def change_background_color(self, new_background_color): 175 | # Write it to the project (will still need to be saved!) 176 | self.qgis_instance.writeEntry("Gui", "/CanvasColorRedPart", new_background_color.red()) 177 | self.qgis_instance.writeEntry("Gui", "/CanvasColorGreenPart", new_background_color.green()) 178 | self.qgis_instance.writeEntry("Gui", "/CanvasColorBluePart", new_background_color.blue()) 179 | 180 | # And apply for the current session 181 | self.iface.mapCanvas().setCanvasColor(new_background_color) 182 | self.iface.mapCanvas().refresh() 183 | 184 | # noinspection PyArgumentList 185 | @staticmethod 186 | def get_existing_layer_names(): 187 | return [layer.name() for layer in QgsProject.instance().mapLayers().values()] 188 | 189 | # noinspection PyArgumentList 190 | def add_halo(self, use_effects, stroke_color, fill_color=None, halo_with_fill=False): 191 | halo = Halo(self.iface, self.origin, self.projection) 192 | 193 | if halo_with_fill: 194 | self.add_halo(True, stroke_color) 195 | else: 196 | [self.qgis_instance.removeMapLayer(lyr.id()) for lyr in 197 | self.qgis_instance.mapLayersByName(halo.LAYER_NAME)] 198 | 199 | layer, index = halo.create_halo_layer(use_effects, stroke_color, fill_color) 200 | self.insert_layer_to_group(layer, index) 201 | 202 | def refresh_theme(self): 203 | theme_collection = self.qgis_instance.mapThemeCollection() 204 | layers = [layer.layer() for layer in self.group.findLayers()] 205 | if Globe.THEME_NAME in theme_collection.mapThemes(): 206 | theme_collection.removeMapTheme(Globe.THEME_NAME) 207 | if len(layers): 208 | map_theme_record = QgsMapThemeCollection.MapThemeRecord() 209 | map_theme_record.setLayerRecords([QgsMapThemeCollection.MapThemeLayerRecord(layer) for layer in layers]) 210 | theme_collection.insert(Globe.THEME_NAME, map_theme_record) 211 | 212 | def add_to_layout(self, layout, background_color=QColor(255, 255, 255, 0), size=80): 213 | """ 214 | Inspired by https://opensourceoptions.com/blog/pyqgis-create-and-print-a-map-layout-with-python/ 215 | """ 216 | layers = [layer.layer() for layer in self.group.findLayers()] 217 | # create map item in the layout 218 | map = QgsLayoutItemMap(layout) 219 | map.setRect(20, 20, 20, 20) 220 | # set the map extent 221 | ms = QgsMapSettings() 222 | ms.setLayers(layers) # set layers to be mapped 223 | crs = QgsCoordinateReferenceSystem() 224 | crs.createFromProj(self.projection.value.proj_str(self.origin)) 225 | map.setCrs(crs) 226 | map.setFollowVisibilityPreset(True) 227 | map.setFollowVisibilityPresetName(Globe.THEME_NAME) 228 | rect = QgsRectangle(ms.fullExtent()) 229 | 230 | ms.setExtent(rect) 231 | map.setExtent(rect) 232 | map.setBackgroundColor(background_color) 233 | layout.addLayoutItem(map) 234 | map.attemptMove(QgsLayoutPoint(5, 20, QgsUnitTypes.LayoutMillimeters)) 235 | map.attemptResize(QgsLayoutSize(size, size, QgsUnitTypes.LayoutMillimeters)) 236 | -------------------------------------------------------------------------------- /GlobeBuilder/core/graticules.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 2 | # 3 | # 4 | # This file is part of GlobeBuilder. 5 | # 6 | # GlobeBuilder is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GlobeBuilder is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GlobeBuilder. If not, see . 18 | 19 | from PyQt5.QtGui import QColor 20 | from qgis.core import (QgsProcessingFeedback, QgsVectorLayer, QgsFillSymbol, 21 | QgsSymbolLayer, QgsFeatureRenderer) 22 | 23 | from ..definitions.settings import WGS84 24 | from ..qgis_plugin_tools.tools.i18n import tr 25 | 26 | ''' 27 | alg: 28 | create grid! 29 | densify grid! 30 | ''' 31 | 32 | 33 | class Graticules: 34 | LAYER_NAME = tr('Graticules') 35 | 36 | def __init__(self, spacing=30, num_vertices=100) -> None: 37 | self.spacing = spacing 38 | self.num_vertices = num_vertices 39 | self.feedback = QgsProcessingFeedback() 40 | 41 | def create_graticules(self, stroke_color): 42 | tmp_grid_layer = self._create_grid_layer() 43 | layer = self._create_graticule_layer(tmp_grid_layer) 44 | self._set_styles(layer, stroke_color) 45 | return layer 46 | 47 | @staticmethod 48 | def _set_styles(layer, stroke_color): 49 | # Set transparent fill 50 | 51 | renderer: QgsFeatureRenderer = layer.renderer() 52 | props = {'color': 'white'} 53 | # noinspection PyArgumentList 54 | fill_symbol = QgsFillSymbol.createSimple(props) 55 | fill_symbol_layer: QgsSymbolLayer = fill_symbol.symbolLayers()[0] 56 | fill_color: QColor = fill_symbol_layer.fillColor() 57 | fill_color.setAlpha(0) 58 | fill_symbol_layer.setFillColor(fill_color) 59 | fill_symbol_layer.setStrokeColor(stroke_color) 60 | # noinspection PyUnresolvedReferences 61 | renderer.setSymbol(fill_symbol) 62 | layer.triggerRepaint() 63 | 64 | def _create_graticule_layer(self, tmp_grid_layer) -> QgsVectorLayer: 65 | # noinspection PyUnresolvedReferences 66 | import processing 67 | params = { 68 | 'INPUT': tmp_grid_layer, 69 | 'OUTPUT': f'memory:{self.LAYER_NAME}', 'VERTICES': self.num_vertices} 70 | res = processing.run('native:densifygeometries', params, feedback=self.feedback) 71 | layer: QgsVectorLayer = res['OUTPUT'] 72 | 73 | return layer 74 | 75 | def _create_grid_layer(self) -> QgsVectorLayer: 76 | # noinspection PyUnresolvedReferences 77 | import processing 78 | params = {'CRS': WGS84, 79 | 'EXTENT': '-180,180,-90,90 [EPSG:4326]', 'HOVERLAY': 0, 'HSPACING': self.spacing, 80 | 'OUTPUT': 'memory:tmp_grid', 'TYPE': 2, 'VOVERLAY': 0, 'VSPACING': self.spacing} 81 | res = processing.run('native:creategrid', params, feedback=self.feedback) 82 | tmp_grid_layer = res['OUTPUT'] 83 | return tmp_grid_layer 84 | -------------------------------------------------------------------------------- /GlobeBuilder/core/halo.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 2 | # 3 | # 4 | # This file is part of GlobeBuilder. 5 | # 6 | # GlobeBuilder is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GlobeBuilder is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GlobeBuilder. If not, see . 18 | 19 | from qgis.core import (QgsFillSymbol, QgsEffectStack, QgsDropShadowEffect, QgsInnerShadowEffect, 20 | QgsGeometryGeneratorSymbolLayer, QgsVectorLayer, QgsFeature, QgsGeometry, QgsPointXY, 21 | QgsCoordinateTransform, QgsProject) 22 | from qgis.gui import QgisInterface 23 | 24 | from ..definitions.projections import Projections 25 | from ..definitions.settings import (TRANSPARENT_COLOR, HaloDrawMethod, EARTH_RADIUS, DEFAULT_HALO_DRAW_METHOD, 26 | DEFAULT_NUMBER_OF_SEGMENTS, WGS84) 27 | from ..qgis_plugin_tools.tools.i18n import tr 28 | from ..qgis_plugin_tools.tools.settings import get_setting 29 | 30 | 31 | class Halo: 32 | LAYER_NAME = tr('Halo') 33 | 34 | def __init__(self, iface: QgisInterface, origin, projection) -> None: 35 | self.iface = iface 36 | self.origin = origin 37 | self.projection = projection 38 | 39 | def create_halo_layer(self, use_effects, stroke_color, fill_color=None): 40 | # noinspection PyArgumentList 41 | qgis_instance: QgsProject = QgsProject.instance() 42 | 43 | draw_method = HaloDrawMethod( 44 | get_setting("haloDrawMethod", DEFAULT_HALO_DRAW_METHOD.value, str)) 45 | proj_string = self.projection.value.proj_str(self.origin) 46 | # Block signals required to prevent the pop up asking about the crs change 47 | self.iface.mainWindow().blockSignals(True) 48 | layer = QgsVectorLayer(draw_method.value, self.LAYER_NAME, "memory") 49 | crs = layer.crs() 50 | crs.createFromProj(proj_string) 51 | layer.setCrs(crs) 52 | self.iface.mainWindow().blockSignals(False) 53 | 54 | feature = QgsFeature() 55 | if self.projection == Projections.AZIMUTHAL_ORTHOGRAPHIC: 56 | # noinspection PyArgumentList 57 | geom = QgsGeometry.fromPointXY(QgsPointXY(self.origin['lat'], self.origin['lon'])) 58 | if draw_method == HaloDrawMethod.buffered_point: 59 | geom = geom.buffer(EARTH_RADIUS, DEFAULT_NUMBER_OF_SEGMENTS) 60 | else: 61 | geom = self.create_grid_halo(layer.crs()) 62 | 63 | feature.setGeometry(geom) 64 | provider = layer.dataProvider() 65 | layer.startEditing() 66 | provider.addFeatures([feature]) 67 | layer.commitChanges() 68 | 69 | # Assign styles and to map (but not toc yet) 70 | self.set_halo_styles(layer, draw_method, stroke_color, use_effects, fill_color) 71 | qgis_instance.addMapLayer(layer, False) 72 | 73 | index = 0 if use_effects else -1 74 | return layer, index 75 | 76 | # noinspection PyCallByClass 77 | @staticmethod 78 | def set_halo_styles(layer, draw_method, stroke_color, use_effects, fill_color=None): 79 | renderer = layer.renderer() 80 | sym = renderer.symbol() 81 | 82 | props = {'color': 'blue'} 83 | # noinspection PyArgumentList 84 | fill_symbol = QgsFillSymbol.createSimple(props) 85 | fill_symbol_layer = fill_symbol.symbolLayers()[0] 86 | fill_symbol_layer.setStrokeColor(stroke_color) 87 | if fill_color is not None: 88 | fill_symbol_layer.setColor(fill_color) 89 | elif not use_effects: 90 | fill_symbol_layer.setColor(TRANSPARENT_COLOR) 91 | 92 | if use_effects: 93 | # Assign effects 94 | effect_stack = QgsEffectStack() 95 | drop_shdw = QgsDropShadowEffect() 96 | drop_shdw.setColor(stroke_color) 97 | inner_shdw = QgsInnerShadowEffect() 98 | inner_shdw.setColor(stroke_color) 99 | effect_stack.appendEffect(drop_shdw) 100 | effect_stack.appendEffect(inner_shdw) 101 | 102 | fill_symbol_layer.setPaintEffect(effect_stack) 103 | # noinspection PyArgumentList 104 | if draw_method == HaloDrawMethod.buffered_point: 105 | renderer.setSymbol(fill_symbol) 106 | else: 107 | # noinspection PyCallByClass, PyArgumentList 108 | geom_generator_sl = QgsGeometryGeneratorSymbolLayer.create({ 109 | 'SymbolType': 'Fill', 110 | 'geometryModifier': 'buffer($geometry, {:d})'.format(EARTH_RADIUS) 111 | }) 112 | geom_generator_sl.setSubSymbol(fill_symbol) 113 | sym.changeSymbolLayer(0, geom_generator_sl) 114 | 115 | layer.triggerRepaint() 116 | return layer 117 | 118 | @staticmethod 119 | def create_grid_halo(crs): 120 | min_x = -180 121 | min_y = -90 122 | max_x = 180 123 | max_y = 90 124 | step = 2 125 | coords = [] 126 | for y in range(min_y, max_y + step, step): 127 | coords.append((min_x, y)) 128 | for x in range(min_x + step, max_x + step, step): 129 | coords.append((x, max_y)) 130 | for y in reversed(range(min_y, max_y + step, step)): 131 | coords.append((max_x, y)) 132 | for x in reversed(range(min_x + step, max_x + step, step)): 133 | coords.append((x, min_y)) 134 | coords.append(coords[0]) 135 | # noinspection PyCallByClass,PyArgumentList 136 | geom = QgsGeometry.fromPolygonXY([[QgsPointXY(pair[0], pair[1]) for pair in coords]]).asQPolygonF() 137 | # noinspection PyArgumentList 138 | transformer = QgsCoordinateTransform(WGS84, crs, QgsProject.instance()) 139 | transformer.transformPolygon(geom) 140 | # noinspection PyArgumentList 141 | return QgsGeometry.fromQPolygonF(geom) 142 | -------------------------------------------------------------------------------- /GlobeBuilder/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 2 | # 3 | # 4 | # This file is part of GlobeBuilder. 5 | # 6 | # GlobeBuilder is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GlobeBuilder is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GlobeBuilder. If not, see . 18 | -------------------------------------------------------------------------------- /GlobeBuilder/core/utils/geocoder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 5 | # 6 | # 7 | # This file is part of GlobeBuilder. 8 | # 9 | # GlobeBuilder is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # GlobeBuilder is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with GlobeBuilder. If not, see . 21 | 22 | import json 23 | import logging 24 | 25 | from PyQt5.QtCore import QSettings, QUrl 26 | from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager 27 | from qgis.core import Qgis, QgsMessageLog 28 | 29 | from ...definitions.settings import NOMINATIM_URL, MAX_NAME_PARTS 30 | from ...qgis_plugin_tools.tools.resources import plugin_name 31 | 32 | LOGGER = logging.getLogger(plugin_name()) 33 | 34 | 35 | class Geocoder: 36 | # noinspection PyUnresolvedReferences 37 | def __init__(self, callback): 38 | self.network_access_manager = QNetworkAccessManager() 39 | self.network_access_manager.finished.connect(self.on_search_response) 40 | self.callback = callback 41 | 42 | def geocode(self, query, max_results): 43 | params = { 44 | 'query': query, 45 | 'limit': max_results 46 | } 47 | url = NOMINATIM_URL.format(**params) 48 | 49 | # http://osgeo-org.1560.x6.nabble.com/QGIS-Developer-Do-we-have-a-User-Agent-string-for-QGIS-td5360740.html 50 | user_agent = QSettings().value("/qgis/networkAndProxy/userAgent", "Mozilla/5.0") 51 | user_agent += " " if len(user_agent) else "" 52 | # noinspection PyUnresolvedReferences 53 | user_agent += "QGIS/{}".format(Qgis.QGIS_VERSION_INT) 54 | user_agent += " GlobeBuilder-plugin" 55 | 56 | QgsMessageLog.logMessage(url, "GlobeBuilder", Qgis.Info) 57 | geocoding_request = QNetworkRequest(QUrl(url)) 58 | # https://www.riverbankcomputing.com/pipermail/pyqt/2016-May/037514.html 59 | geocoding_request.setRawHeader(b"User-Agent", bytes(user_agent, "utf-8")) 60 | self.network_access_manager.get(geocoding_request) 61 | 62 | def on_search_response(self, search_result): 63 | error = search_result.error() 64 | result_dict = {} 65 | if error == QNetworkReply.NoError: 66 | bytes_string = search_result.readAll() 67 | data_string = str(bytes_string, 'utf-8') 68 | 69 | result = json.loads(data_string) 70 | 71 | for f in result['features']: 72 | name_parts = f['properties']['display_name'].split(",") 73 | name = "{} ({})".format(",".join( 74 | name_parts[0:min(MAX_NAME_PARTS, len(name_parts) - 1)]), 75 | name_parts[-1].strip()) if len(name_parts) > 1 else name_parts[0] 76 | coordinates = f['geometry']['coordinates'] 77 | result_dict[name] = coordinates 78 | else: 79 | LOGGER.warning(str(error)) 80 | LOGGER.warning(search_result.errorString()) 81 | 82 | self.callback(result_dict) 83 | -------------------------------------------------------------------------------- /GlobeBuilder/core/utils/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 5 | # 6 | # 7 | # This file is part of GlobeBuilder. 8 | # 9 | # GlobeBuilder is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # GlobeBuilder is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with GlobeBuilder. If not, see . 21 | 22 | from qgis.core import QgsPrintLayout, QgsFillSymbol, QgsRuleBasedRenderer, QgsFeatureRequest, \ 23 | QgsCoordinateTransformContext, QgsCoordinateTransform 24 | 25 | from ...definitions.settings import WGS84 26 | from ...qgis_plugin_tools.tools.i18n import tr 27 | 28 | 29 | def create_layout(layout_name, qgis_instance): 30 | manager = qgis_instance.layoutManager() 31 | layouts_list = manager.printLayouts() 32 | # remove any duplicate layouts 33 | for layout in layouts_list: 34 | if layout.name() == layout_name: 35 | manager.removeLayout(layout) 36 | layout = QgsPrintLayout(qgis_instance) 37 | layout.initializeDefaults() 38 | layout.setName(layout_name) 39 | manager.addLayout(layout) 40 | return layout 41 | 42 | 43 | def set_selection_based_style(layer, s_color, else_color): 44 | # noinspection PyCallByClass,PyArgumentList 45 | fill_for_selected = QgsFillSymbol.createSimple({'color': 'blue'}) 46 | fill_for_selected.setColor(s_color) 47 | rule_s = QgsRuleBasedRenderer.Rule(fill_for_selected, label=tr(u"Selected"), 48 | filterExp="is_selected()") 49 | 50 | fill_for_else = fill_for_selected.clone() 51 | fill_for_else.setColor(else_color) 52 | rule_else = QgsRuleBasedRenderer.Rule(fill_for_else, label=tr(u"Not Selected"), 53 | elseRule=True) 54 | 55 | renderer = QgsRuleBasedRenderer(QgsRuleBasedRenderer.Rule(None)) 56 | root_rule = renderer.rootRule() 57 | root_rule.appendChild(rule_s) 58 | root_rule.appendChild(rule_else) 59 | 60 | layer.setRenderer(renderer) 61 | return layer 62 | 63 | 64 | def transform_to_wgs84(geom, crs, qgis_instance): 65 | transformer = QgsCoordinateTransform(crs, WGS84, qgis_instance) 66 | return transformer.transform(geom) 67 | 68 | 69 | def get_feature_ids_that_intersect_bbox(layer, rect, crs): 70 | request = (QgsFeatureRequest() 71 | .setFilterRect(rect) 72 | .setDestinationCrs(crs=crs, context=QgsCoordinateTransformContext()) 73 | .setNoAttributes().setFlags(QgsFeatureRequest.NoGeometry)) 74 | return [f.id() for f in layer.getFeatures(request)] 75 | 76 | 77 | def get_map_center_coordinates(iface, qgis_instance, frmt="{:.0f}"): 78 | center_point = iface.mapCanvas().extent().center() 79 | center_point = transform_to_wgs84(center_point, qgis_instance.crs(), 80 | qgis_instance) 81 | center = {'lon': float(frmt.format(center_point.x())), 'lat': float(frmt.format(center_point.y()))} 82 | return center 83 | -------------------------------------------------------------------------------- /GlobeBuilder/definitions/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 2 | # 3 | # 4 | # This file is part of GlobeBuilder. 5 | # 6 | # GlobeBuilder is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GlobeBuilder is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GlobeBuilder. If not, see . 18 | -------------------------------------------------------------------------------- /GlobeBuilder/definitions/projections.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 2 | # 3 | # 4 | # This file is part of GlobeBuilder. 5 | # 6 | # GlobeBuilder is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GlobeBuilder is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GlobeBuilder. If not, see . 18 | 19 | from enum import Enum 20 | from typing import Tuple 21 | 22 | from ..qgis_plugin_tools.tools.i18n import tr 23 | 24 | 25 | class Projection: 26 | 27 | def __init__(self, name: str, proj_str: str, min_proj_version: Tuple[int, int] = (0, 0)): 28 | self.name = name 29 | self.proj_str_raw = proj_str 30 | self.min_proj = min_proj_version 31 | 32 | def proj_str(self, origin: {str: float}) -> str: 33 | return self.proj_str_raw.format(**origin) 34 | 35 | 36 | class Projections(Enum): 37 | AZIMUTHAL_ORTHOGRAPHIC = Projection(tr(u'Azimuthal Orthographic'), 38 | '+proj=ortho +lat_0={lat} +lon_0={lon} +x_0=0 +y_0=0 +a=6370997 +b=6370997 +units=m +no_defs') 39 | 40 | # Unfortunately +lon_0={lon} causes nasty rendering artifacts for all projections below (tested with QGIS 3.14.0 and 3.20.3) 41 | 42 | # https://www.gislounge.com/how-to-use-the-equal-earth-projection-using-qgis-on-the-mac/ 43 | EQUAL_EARTH = Projection(tr(u'Equal Earth'), '+proj=eqearth +datum=WGS84 +units=m +no_defs', (5, 2)) 44 | 45 | # 'https://proj.org/operations/projections/hammer.html' 46 | HAMMER_ECKERT = Projection(tr(u'Hammer & Eckert-Greifendorff'), '+proj=hammer') 47 | 48 | # 'https://proj.org/operations/projections/aitoff.html' 49 | AITOFF = Projection(tr(u'Aitoff'), '+proj=aitoff') 50 | 51 | # 'https://proj.org/operations/projections/eck1.html' 52 | ECKERT_I = Projection(tr(u'Eckert I'), '+proj=eck1') 53 | 54 | @staticmethod 55 | def proj_from_id(tr_name: str): 56 | for projection in Projections: 57 | if projection.value.name == tr_name: 58 | return projection 59 | -------------------------------------------------------------------------------- /GlobeBuilder/definitions/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 5 | # 6 | # 7 | # This file is part of GlobeBuilder. 8 | # 9 | # GlobeBuilder is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # GlobeBuilder is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with GlobeBuilder. If not, see . 21 | 22 | import enum 23 | 24 | from PyQt5.QtCore import Qt 25 | from PyQt5.QtGui import QColor 26 | from qgis.core import QgsCoordinateReferenceSystem 27 | 28 | from ..qgis_plugin_tools.tools.resources import resources_path 29 | 30 | 31 | class LayerConnectionType(enum.Enum): 32 | local = 1 33 | url = 2 34 | 35 | 36 | class HaloDrawMethod(enum.Enum): 37 | geometry_generator = "Point" 38 | buffered_point = "Polygon" 39 | 40 | 41 | NATURAL_EARTH_BASE_URL = "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson" 42 | S2CLOUDLESS_WMTS_URL = "url=https://tiles.maps.eox.at/wmts?SERVICE%3DWMTS%26REQUEST%3DGetCapabilities&contextualWMSLegend=0&crs=EPSG:4326&dpiMode=7&featureCount=10&format=image/jpeg&layers=s2cloudless-2018&styles=default&tileMatrixSet=WGS84" 43 | LOCAL_DATA_DIR = resources_path("data") 44 | DEFAULT_ORIGIN = {'lat': 42.5, 'lon': 0.5} 45 | 46 | EARTH_RADIUS = 6370997 47 | DEFAULT_NUMBER_OF_SEGMENTS = 64 48 | DEFAULT_LAYER_CONNECTION_TYPE = LayerConnectionType.local 49 | DEFAULT_HALO_DRAW_METHOD = HaloDrawMethod.buffered_point 50 | 51 | _crs = QgsCoordinateReferenceSystem() 52 | _crs.createFromId(4326) 53 | WGS84 = _crs 54 | 55 | # Colors 56 | DEFAULT_BACKGROUND_COLOR = QColor(Qt.black) 57 | DEFAULT_LAYOUT_BACKGROUND_COLOR = QColor(Qt.white) 58 | DEFAULT_COUNTRIES_COLOR = QColor(Qt.darkGreen) 59 | DEFAULT_GRATICULES_COLOR = QColor(Qt.gray) 60 | DEFAULT_HALO_COLOR = QColor(Qt.lightGray) 61 | DEFAULT_HALO_LAYOUT_COLOR = QColor(Qt.black) 62 | DEFAULT_HALO_FILL_COLOR = QColor(Qt.blue) 63 | DEFAULT_INTERSECTING_COUNTRIES_COLOR = QColor(Qt.red) 64 | _transparent = QColor(Qt.black) 65 | _transparent.setAlpha(0) 66 | TRANSPARENT_COLOR = _transparent 67 | 68 | # UI 69 | NOMINATIM_URL = "https://nominatim.openstreetmap.org/search/{query}?limit={limit}&format=geojson" 70 | DEFAULT_MAX_NUMBER_OF_RESULTS = 5 71 | MAX_NAME_PARTS = 3 72 | DEFAULT_USE_NE_COUNTRIES = True 73 | DEFAULT_USE_NE_GRATICULES = False 74 | DEFAULT_USE_S2_CLOUDLESS = False 75 | -------------------------------------------------------------------------------- /GlobeBuilder/logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log* 2 | -------------------------------------------------------------------------------- /GlobeBuilder/metadata.txt: -------------------------------------------------------------------------------- 1 | # This file contains metadata for your plugin. 2 | 3 | # This file should be included when you package your plugin. 4 | # Mandatory items: 5 | 6 | [general] 7 | name=Globe Builder 8 | qgisMinimumVersion=3.2 9 | description=This plugin is meant for adding globe visualization to the current map or layout. 10 | version=0.5.0 11 | author=Gispo Ltd. 12 | email=joona@gispo.fi 13 | 14 | about=This plugin builds a globe from Natural Earth countries GeoJSON and an Azimuthal orthographic projection ("world from space") projection described here. This plugin uses OpenStreetMap Nominatim geocoding API. The OpenStreetMap data is licensed under ODbL license. Made with Natural Earth. Free vectior and raster map data naturalearthdata.com. This plugin lists Sentinel-2 cloudless as an optional data source. Sentinel-2 cloudless data https://s2maps.eu/ by EOX IT Services GmbH (Contains modified Copernicus Sentinel data 2017 & 2018) released under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. For commercial usage of Sentinel-2 cloudless please see https://cloudless.eox.at. 15 | 16 | tracker=https://github.com/GispoCoding/GlobeBuilder/issues 17 | repository=https://github.com/GispoCoding/GlobeBuilder 18 | # End of mandatory metadata 19 | 20 | # Recommended items: 21 | 22 | hasProcessingProvider=no 23 | # Uncomment the following line and add your changelog: 24 | changelog= 25 | 26 | # Tags are comma separated with spaces allowed 27 | tags=python, globe, world 28 | 29 | homepage=https://github.com/GispoCoding/GlobeBuilder 30 | category=Plugins 31 | icon=icon.png 32 | # experimental flag 33 | experimental=False 34 | 35 | # deprecated flag (applies to the whole plugin, not just a single version) 36 | deprecated=False 37 | 38 | # Since QGIS 3.8, a comma separated list of plugins to be installed 39 | # (or upgraded) can be specified. 40 | # Check the documentation for more information. 41 | # plugin_dependencies= 42 | 43 | # If the plugin can run on QGIS Server. 44 | server=False 45 | 46 | -------------------------------------------------------------------------------- /GlobeBuilder/plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 5 | # 6 | # 7 | # This file is part of GlobeBuilder. 8 | # 9 | # GlobeBuilder is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # GlobeBuilder is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with GlobeBuilder. If not, see . 21 | 22 | from qgis.PyQt.QtCore import QTranslator, QCoreApplication, Qt 23 | from qgis.PyQt.QtGui import QIcon 24 | from qgis.PyQt.QtWidgets import QAction 25 | 26 | from .qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger 27 | from .qgis_plugin_tools.tools.i18n import setup_translation, tr 28 | from .qgis_plugin_tools.tools.resources import plugin_name, resources_path 29 | from .ui.globe_builder_dockwidget import GlobeBuilderDockWidget 30 | 31 | 32 | class GlobeBuilder: 33 | """QGIS Plugin Implementation.""" 34 | 35 | def __init__(self, iface): 36 | """Constructor. 37 | 38 | :param iface: An interface instance that will be passed to this class 39 | which provides the hook by which you can manipulate the QGIS 40 | application at run time. 41 | :type iface: QgsInterface 42 | """ 43 | # Save reference to the QGIS interface 44 | self.iface = iface 45 | 46 | setup_logger(plugin_name()) 47 | 48 | # initialize locale 49 | locale, file_path = setup_translation() 50 | if file_path: 51 | translator = QTranslator() 52 | translator.load(file_path) 53 | # noinspection PyCallByClass,PyArgumentList 54 | QCoreApplication.installTranslator(translator) 55 | else: 56 | pass 57 | 58 | # Declare instance attributes 59 | self.actions = [] 60 | self.menu = tr(u'&Globe Builder') 61 | 62 | # Check if plugin was started the first time in current QGIS session 63 | # Must be set in initGui() to survive plugin reloads 64 | self.pluginIsActive = False 65 | self.dockwidget = None 66 | 67 | # noinspection PyMethodMayBeStatic 68 | def tr(self, message): 69 | """Get the translation for a string using Qt translation API. 70 | 71 | We implement this ourselves since we do not inherit QObject. 72 | 73 | :param message: String for translation. 74 | :type message: str, QString 75 | 76 | :returns: Translated version of message. 77 | :rtype: QString 78 | """ 79 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass 80 | return QCoreApplication.translate('GlobeBuilder', message) 81 | 82 | def add_action( 83 | self, 84 | icon_path, 85 | text, 86 | callback, 87 | enabled_flag=True, 88 | add_to_menu=True, 89 | add_to_toolbar=True, 90 | status_tip=None, 91 | whats_this=None, 92 | parent=None): 93 | """Add a toolbar icon to the toolbar. 94 | 95 | :param icon_path: Path to the icon for this action. Can be a resource 96 | path (e.g. ':/plugins/foo/bar.png') or a normal file system path. 97 | :type icon_path: str 98 | 99 | :param text: Text that should be shown in menu items for this action. 100 | :type text: str 101 | 102 | :param callback: Function to be called when the action is triggered. 103 | :type callback: function 104 | 105 | :param enabled_flag: A flag indicating if the action should be enabled 106 | by default. Defaults to True. 107 | :type enabled_flag: bool 108 | 109 | :param add_to_menu: Flag indicating whether the action should also 110 | be added to the menu. Defaults to True. 111 | :type add_to_menu: bool 112 | 113 | :param add_to_toolbar: Flag indicating whether the action should also 114 | be added to the toolbar. Defaults to True. 115 | :type add_to_toolbar: bool 116 | 117 | :param status_tip: Optional text to show in a popup when mouse pointer 118 | hovers over the action. 119 | :type status_tip: str 120 | 121 | :param parent: Parent widget for the new action. Defaults None. 122 | :type parent: QWidget 123 | 124 | :param whats_this: Optional text to show in the status bar when the 125 | mouse pointer hovers over the action. 126 | 127 | :returns: The action that was created. Note that the action is also 128 | added to self.actions list. 129 | :rtype: QAction 130 | """ 131 | 132 | icon = QIcon(icon_path) 133 | action = QAction(icon, text, parent) 134 | # noinspection PyUnresolvedReferences 135 | action.triggered.connect(callback) 136 | action.setEnabled(enabled_flag) 137 | 138 | if status_tip is not None: 139 | action.setStatusTip(status_tip) 140 | 141 | if whats_this is not None: 142 | action.setWhatsThis(whats_this) 143 | 144 | if add_to_toolbar: 145 | # Adds plugin icon to Plugins toolbar 146 | self.iface.addToolBarIcon(action) 147 | 148 | if add_to_menu: 149 | self.iface.addPluginToMenu( 150 | self.menu, 151 | action) 152 | 153 | self.actions.append(action) 154 | 155 | return action 156 | 157 | def initGui(self): 158 | """Create the menu entries and toolbar icons inside the QGIS GUI.""" 159 | 160 | # noinspection PyTypeChecker 161 | self.add_action( 162 | resources_path('icon.png'), 163 | text=tr(u'Build Globe view'), 164 | callback=self.run, 165 | parent=self.iface.mainWindow()) 166 | 167 | # will be set False in run() 168 | self.first_start = True 169 | 170 | def onClosePlugin(self): 171 | """Cleanup necessary items here when plugin dockwidget is closed""" 172 | 173 | # disconnects 174 | self.dockwidget.closingPlugin.disconnect(self.onClosePlugin) 175 | 176 | # remove this statement if dockwidget is to remain 177 | # for reuse if plugin is reopened 178 | # Commented next statement since it causes QGIS crashe 179 | # when closing the docked window: 180 | # self.dockwidget = None 181 | 182 | self.pluginIsActive = False 183 | 184 | def unload(self): 185 | """Removes the plugin menu item and icon from QGIS GUI.""" 186 | for action in self.actions: 187 | self.iface.removePluginMenu( 188 | tr(u'&Globe Builder'), 189 | action) 190 | self.iface.removeToolBarIcon(action) 191 | teardown_logger(plugin_name()) 192 | 193 | def run(self): 194 | """Run method that performs all the real work""" 195 | if not self.pluginIsActive: 196 | self.pluginIsActive = True 197 | 198 | # dockwidget may not exist if: 199 | # first run of plugin 200 | # removed on close (see self.onClosePlugin method) 201 | if self.dockwidget == None: 202 | # Create the dockwidget (after translation) and keep reference 203 | self.dockwidget = GlobeBuilderDockWidget(self.iface) 204 | 205 | # connect to provide cleanup on closing of dockwidget 206 | self.dockwidget.closingPlugin.connect(self.onClosePlugin) 207 | 208 | # show the dockwidget 209 | self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dockwidget) 210 | self.dockwidget.show() 211 | -------------------------------------------------------------------------------- /GlobeBuilder/pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. See also the "--disable" option for examples. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifiers separated by comma (,) or put this 34 | # option multiple times (only on the command line, not in the configuration 35 | # file where it should appear only once).You can also use "--disable=all" to 36 | # disable everything first and then reenable specific checks. For example, if 37 | # you want to run only the similarities checker, you can use "--disable=all 38 | # --enable=similarities". If you want to run only the classes checker, but have 39 | # no Warning level messages displayed, use"--disable=all --enable=classes 40 | # --disable=W" 41 | # see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them 42 | disable=locally-disabled,C0103 43 | 44 | 45 | [REPORTS] 46 | 47 | # Set the output format. Available formats are text, parseable, colorized, msvs 48 | # (visual studio) and html. You can also give a reporter class, eg 49 | # mypackage.mymodule.MyReporterClass. 50 | output-format=text 51 | 52 | # Put messages in a separate file for each module / package specified on the 53 | # command line instead of printing them on stdout. Reports (if any) will be 54 | # written in a file name "pylint_global.[txt|html]". 55 | files-output=no 56 | 57 | # Tells whether to display a full report or only the messages 58 | reports=yes 59 | 60 | # Python expression which should return a note less than 10 (10 is the highest 61 | # note). You have access to the variables errors warning, statement which 62 | # respectively contain the number of errors / warnings messages and the total 63 | # number of statements analyzed. This is used by the global evaluation report 64 | # (RP0004). 65 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 66 | 67 | # Add a comment according to your evaluation note. This is used by the global 68 | # evaluation report (RP0004). 69 | comment=no 70 | 71 | # Template used to display messages. This is a python new-style format string 72 | # used to format the message information. See doc for all details 73 | #msg-template= 74 | 75 | 76 | [BASIC] 77 | 78 | # Required attributes for module, separated by a comma 79 | required-attributes= 80 | 81 | # List of builtins function names that should not be used, separated by a comma 82 | bad-functions=map,filter,apply,input 83 | 84 | # Regular expression which should only match correct module names 85 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 86 | 87 | # Regular expression which should only match correct module level names 88 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 89 | 90 | # Regular expression which should only match correct class names 91 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 92 | 93 | # Regular expression which should only match correct function names 94 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 95 | 96 | # Regular expression which should only match correct method names 97 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 98 | 99 | # Regular expression which should only match correct instance attribute names 100 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 101 | 102 | # Regular expression which should only match correct argument names 103 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 104 | 105 | # Regular expression which should only match correct variable names 106 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 107 | 108 | # Regular expression which should only match correct attribute names in class 109 | # bodies 110 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 111 | 112 | # Regular expression which should only match correct list comprehension / 113 | # generator expression variable names 114 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 115 | 116 | # Good variable names which should always be accepted, separated by a comma 117 | good-names=i,j,k,ex,Run,_ 118 | 119 | # Bad variable names which should always be refused, separated by a comma 120 | bad-names=foo,bar,baz,toto,tutu,tata 121 | 122 | # Regular expression which should only match function or class names that do 123 | # not require a docstring. 124 | no-docstring-rgx=__.*__ 125 | 126 | # Minimum line length for functions/classes that require docstrings, shorter 127 | # ones are exempt. 128 | docstring-min-length=-1 129 | 130 | 131 | [MISCELLANEOUS] 132 | 133 | # List of note tags to take in consideration, separated by a comma. 134 | notes=FIXME,XXX,TODO 135 | 136 | 137 | [TYPECHECK] 138 | 139 | # Tells whether missing members accessed in mixin class should be ignored. A 140 | # mixin class is detected if its name ends with "mixin" (case insensitive). 141 | ignore-mixin-members=yes 142 | 143 | # List of classes names for which member attributes should not be checked 144 | # (useful for classes with attributes dynamically set). 145 | ignored-classes=SQLObject 146 | 147 | # When zope mode is activated, add a predefined set of Zope acquired attributes 148 | # to generated-members. 149 | zope=no 150 | 151 | # List of members which are set dynamically and missed by pylint inference 152 | # system, and so shouldn't trigger E0201 when accessed. Python regular 153 | # expressions are accepted. 154 | generated-members=REQUEST,acl_users,aq_parent 155 | 156 | 157 | [VARIABLES] 158 | 159 | # Tells whether we should check for unused import in __init__ files. 160 | init-import=no 161 | 162 | # A regular expression matching the beginning of the name of dummy variables 163 | # (i.e. not used). 164 | dummy-variables-rgx=_$|dummy 165 | 166 | # List of additional names supposed to be defined in builtins. Remember that 167 | # you should avoid to define new builtins when possible. 168 | additional-builtins= 169 | 170 | 171 | [FORMAT] 172 | 173 | # Maximum number of characters on a single line. 174 | max-line-length=80 175 | 176 | # Regexp for a line that is allowed to be longer than the limit. 177 | ignore-long-lines=^\s*(# )??$ 178 | 179 | # Allow the body of an if to be on the same line as the test if there is no 180 | # else. 181 | single-line-if-stmt=no 182 | 183 | # List of optional constructs for which whitespace checking is disabled 184 | no-space-check=trailing-comma,dict-separator 185 | 186 | # Maximum number of lines in a module 187 | max-module-lines=1000 188 | 189 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 190 | # tab). 191 | indent-string=' ' 192 | 193 | 194 | [SIMILARITIES] 195 | 196 | # Minimum lines number of a similarity. 197 | min-similarity-lines=4 198 | 199 | # Ignore comments when computing similarities. 200 | ignore-comments=yes 201 | 202 | # Ignore docstrings when computing similarities. 203 | ignore-docstrings=yes 204 | 205 | # Ignore imports when computing similarities. 206 | ignore-imports=no 207 | 208 | 209 | [IMPORTS] 210 | 211 | # Deprecated modules which should not be used, separated by a comma 212 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 213 | 214 | # Create a graph of every (i.e. internal and external) dependencies in the 215 | # given file (report RP0402 must not be disabled) 216 | import-graph= 217 | 218 | # Create a graph of external dependencies in the given file (report RP0402 must 219 | # not be disabled) 220 | ext-import-graph= 221 | 222 | # Create a graph of internal dependencies in the given file (report RP0402 must 223 | # not be disabled) 224 | int-import-graph= 225 | 226 | 227 | [DESIGN] 228 | 229 | # Maximum number of arguments for function / method 230 | max-args=5 231 | 232 | # Argument names that match this expression will be ignored. Default to name 233 | # with leading underscore 234 | ignored-argument-names=_.* 235 | 236 | # Maximum number of locals for function / method body 237 | max-locals=15 238 | 239 | # Maximum number of return / yield for function / method body 240 | max-returns=6 241 | 242 | # Maximum number of branch for function / method body 243 | max-branches=12 244 | 245 | # Maximum number of statements in function / method body 246 | max-statements=50 247 | 248 | # Maximum number of parents for a class (see R0901). 249 | max-parents=7 250 | 251 | # Maximum number of attributes for a class (see R0902). 252 | max-attributes=7 253 | 254 | # Minimum number of public methods for a class (see R0903). 255 | min-public-methods=2 256 | 257 | # Maximum number of public methods for a class (see R0904). 258 | max-public-methods=20 259 | 260 | 261 | [CLASSES] 262 | 263 | # List of interface methods to ignore, separated by a comma. This is used for 264 | # instance to not check methods defines in Zope's Interface base class. 265 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 266 | 267 | # List of method names used to declare (i.e. assign) instance attributes. 268 | defining-attr-methods=__init__,__new__,setUp 269 | 270 | # List of valid names for the first argument in a class method. 271 | valid-classmethod-first-arg=cls 272 | 273 | # List of valid names for the first argument in a metaclass class method. 274 | valid-metaclass-classmethod-first-arg=mcs 275 | 276 | 277 | [EXCEPTIONS] 278 | 279 | # Exceptions that will emit a warning when being caught. Defaults to 280 | # "Exception" 281 | overgeneral-exceptions=Exception 282 | -------------------------------------------------------------------------------- /GlobeBuilder/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GispoCoding/GlobeBuilder/3a9ec05ef91e39fb67e52962c3bf24007b2d3046/GlobeBuilder/resources/icon.png -------------------------------------------------------------------------------- /GlobeBuilder/resources/ui/globe_builder_dockwidget_base.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | GlobeBuilderDockWidgetBase 4 | 5 | 6 | 7 | 0 8 | 0 9 | 546 10 | 888 11 | 12 | 13 | 14 | false 15 | 16 | 17 | Globe Builder 18 | 19 | 20 | 21 | 22 | 23 | 24 | QLayout::SetNoConstraint 25 | 26 | 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 0 35 | -143 36 | 510 37 | 886 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Data sources (optional) 47 | 48 | 49 | false 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | true 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Countries 50m 71 | 72 | 73 | 74 | 75 | Countries 110m 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | Qt::Horizontal 84 | 85 | 86 | 87 | 40 88 | 20 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | true 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | Graticules 10 108 | 109 | 110 | 111 | 112 | Graticules 2 113 | 114 | 115 | 116 | 117 | Graticules 5 118 | 119 | 120 | 121 | 122 | Graticules 20 123 | 124 | 125 | 126 | 127 | Graticules 30 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Qt::Horizontal 136 | 137 | 138 | 139 | 40 140 | 20 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Sentinel-2 cloudless 149 | 150 | 151 | true 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | Projection 168 | 169 | 170 | 171 | 172 | 173 | Projection 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | true 187 | 188 | 189 | Center the Globe based on: 190 | 191 | 192 | false 193 | 194 | 195 | false 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | Coordinates 206 | 207 | 208 | true 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 'Longitude, Latitude' in decimal degrees 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | Qt::Horizontal 232 | 233 | 234 | 235 | 236 | 237 | 238 | Center to the current view 239 | 240 | 241 | 242 | 243 | 244 | 245 | Qt::Horizontal 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | Layer 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | Qt::Horizontal 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | Geocoding 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | Address for the geolocation of interest: 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | Search 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | Qt::Horizontal 311 | 312 | 313 | 314 | 40 315 | 20 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 0 325 | 0 326 | 327 | 328 | 329 | Maximum number of addresses to list: 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 0 338 | 0 339 | 340 | 341 | 342 | 343 | 50 344 | 16777215 345 | 346 | 347 | 348 | 1 349 | 350 | 351 | 10 352 | 353 | 354 | 5 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | Visualization 373 | 374 | 375 | false 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | Fill 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 50 396 | false 397 | 398 | 399 | 400 | Halo 401 | 402 | 403 | 404 | 405 | 406 | 407 | Line 408 | 409 | 410 | 411 | 412 | 413 | 414 | Background 415 | 416 | 417 | 418 | 419 | 420 | 421 | true 422 | 423 | 424 | 425 | 426 | 427 | 428 | Graticules 429 | 430 | 431 | 432 | 433 | 434 | 435 | true 436 | 437 | 438 | 439 | 440 | 441 | 442 | Countries 443 | 444 | 445 | 446 | 447 | 448 | 449 | true 450 | 451 | 452 | 453 | 454 | 455 | 456 | Optional visualizations 457 | 458 | 459 | 460 | 461 | 462 | 463 | true 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | Qt::Vertical 476 | 477 | 478 | 479 | 20 480 | 40 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | Qt::Vertical 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | Halo type 500 | 501 | 502 | 503 | 504 | 505 | 506 | Qt::Vertical 507 | 508 | 509 | 510 | 20 511 | 40 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | Halo, no fill 520 | 521 | 522 | true 523 | 524 | 525 | 526 | 527 | 528 | 529 | Halo, fill 530 | 531 | 532 | 533 | 534 | 535 | 536 | Outline, no fill 537 | 538 | 539 | 540 | 541 | 542 | 543 | Outline, fill 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | Qt::Horizontal 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | Qt::Horizontal 564 | 565 | 566 | 567 | 40 568 | 20 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | Layout 582 | 583 | 584 | false 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | Map composer: 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | Globe Layout background 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 50 619 | 16 620 | 621 | 622 | 623 | true 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | Color for Countries intersecting the current view 635 | 636 | 637 | 638 | 639 | 640 | 641 | true 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | Qt::Horizontal 653 | 654 | 655 | 656 | 40 657 | 20 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 0 667 | 0 668 | 669 | 670 | 671 | Globe size in mm 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 0 680 | 0 681 | 682 | 683 | 684 | 685 | 50 686 | 16777215 687 | 688 | 689 | 690 | 1 691 | 692 | 693 | 10000 694 | 695 | 696 | 80 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | Qt::Vertical 709 | 710 | 711 | 712 | 20 713 | 40 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | Qt::Horizontal 728 | 729 | 730 | 731 | 40 732 | 20 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | Apply changes 741 | 742 | 743 | 744 | 745 | 746 | 747 | Add the Globe to a Map 748 | 749 | 750 | 751 | 752 | 753 | 754 | Add the Globe to a Layout 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | Attributions 764 | 765 | 766 | 767 | 768 | 769 | <html><head/><body><p><span style=" font-size:10pt;">Please, 770 | note that the plugin utilizes the </span><a href="https://wiki.openstreetmap.org/wiki/Nominatim"><span 771 | style=" font-size:10pt; text-decoration: underline; color:#0000ff;">OpenStreetMap Nominatim</span></a><span 772 | style=" font-size:10pt;"> geocoding API. Please, </span><span style=" 773 | font-size:10pt; font-weight:600;">use maximum of 1 geocoding request per second</span><span 774 | style=" font-size:10pt;">. The OpenStreetMap data is licensed under </span><a href="https://opendatacommons.org/licenses/odbl/"><span 775 | style=" font-size:10pt; text-decoration: underline; color:#0000ff;">ODbL license</span></a><span 776 | style=" font-size:10pt;">. Made with Natural Earth. Free vectior and raster map data @ </span><a 777 | href="naturalearthdata.com"><span style=" font-size:10pt; text-decoration: underline; 778 | color:#0000ff;">naturalearthdata.com</span></a><span style=" font-size:10pt;">. 779 | Sentinel-2 cloudless data- </span><a href="https://s2maps.eu/"><span style=" 780 | font-size:10pt; text-decoration: underline; color:#0000ff;">https://s2maps.eu</span></a><span 781 | style=" font-size:10pt;"> by EOX IT Services GmbH (Contains modified Copernicus Sentinel data 782 | 2017 &amp; 2018) released under </span><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><span 783 | style=" font-size:10pt; text-decoration: underline; color:#0000ff;">Creative Commons 784 | Attribution-NonCommercial-ShareAlike 4.0 International License</span></a><span style=" 785 | font-size:10pt;">. For commercial usage of Sentinel-2 cloudless please see </span><a href="https://cloudless.eox.at"><span 786 | style=" font-size:10pt; text-decoration: underline; color:#0000ff;">https://cloudless.eox.at</span></a><span 787 | style=" font-size:10pt;">. </span></p></body></html> 788 | 789 | 790 | 791 | true 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | QgsCollapsibleGroupBox 806 | QGroupBox 807 |
qgscollapsiblegroupbox.h
808 | 1 809 |
810 | 811 | QgsColorButton 812 | QToolButton 813 |
qgscolorbutton.h
814 |
815 | 816 | QgsMapLayerComboBox 817 | QComboBox 818 |
qgsmaplayercombobox.h
819 |
820 |
821 | 822 | 823 |
824 | -------------------------------------------------------------------------------- /GlobeBuilder/ui/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 2 | # 3 | # 4 | # This file is part of GlobeBuilder. 5 | # 6 | # GlobeBuilder is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GlobeBuilder is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GlobeBuilder. If not, see . 18 | -------------------------------------------------------------------------------- /GlobeBuilder/ui/globe_builder_dockwidget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # Copyright (C) 2020-2021 GlobeBuilder contributors. 5 | # 6 | # 7 | # This file is part of GlobeBuilder. 8 | # 9 | # GlobeBuilder is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # GlobeBuilder is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with GlobeBuilder. If not, see . 21 | 22 | import logging 23 | 24 | from PyQt5.QtCore import pyqtSignal, pyqtSlot 25 | from qgis.PyQt import QtWidgets 26 | from qgis.core import QgsProject 27 | 28 | from ..core.globe import Globe 29 | from ..core.utils.geocoder import Geocoder 30 | from ..core.utils.utils import create_layout, transform_to_wgs84, get_map_center_coordinates 31 | from ..definitions.projections import Projections 32 | from ..definitions.settings import (DEFAULT_MAX_NUMBER_OF_RESULTS, DEFAULT_USE_NE_COUNTRIES, 33 | DEFAULT_USE_NE_GRATICULES, DEFAULT_USE_S2_CLOUDLESS, DEFAULT_ORIGIN, 34 | DEFAULT_BACKGROUND_COLOR, DEFAULT_HALO_COLOR, DEFAULT_HALO_FILL_COLOR, 35 | DEFAULT_LAYOUT_BACKGROUND_COLOR, DEFAULT_COUNTRIES_COLOR, 36 | DEFAULT_GRATICULES_COLOR, DEFAULT_INTERSECTING_COUNTRIES_COLOR) 37 | from ..qgis_plugin_tools.tools.custom_logging import bar_msg 38 | from ..qgis_plugin_tools.tools.i18n import tr 39 | from ..qgis_plugin_tools.tools.resources import load_ui, plugin_name 40 | from ..qgis_plugin_tools.tools.settings import get_setting, set_setting 41 | from ..qgis_plugin_tools.tools.version import proj_version 42 | 43 | FORM_CLASS = load_ui('globe_builder_dockwidget_base.ui') 44 | LOGGER = logging.getLogger(plugin_name()) 45 | 46 | 47 | class GlobeBuilderDockWidget(QtWidgets.QDockWidget, FORM_CLASS): 48 | closingPlugin = pyqtSignal() 49 | 50 | def __init__(self, iface, parent=None): 51 | """Constructor.""" 52 | self.is_initializing = True 53 | 54 | super(GlobeBuilderDockWidget, self).__init__(parent) 55 | 56 | # Set up the user interface from Designer. 57 | # After setupUI you can access any designer object by doing 58 | # self., and you can use autoconnect slots - see 59 | # http://doc.qt.io/qt-5/designer-using-a-ui-file.html 60 | # #widgets-and-dialogs-with-auto-connect 61 | self.setupUi(self) 62 | 63 | self.iface = iface 64 | # noinspection PyArgumentList 65 | self.qgis_instance = QgsProject.instance() 66 | self.layout_mngr = self.qgis_instance.layoutManager() 67 | self.globe = Globe(iface) 68 | self.geocoder = Geocoder(lambda results: self.on_geocoding_finished(results)) 69 | 70 | # Set default values 71 | self.spinBoxMaxResults.setValue(get_setting("maxNumberOfResults", DEFAULT_MAX_NUMBER_OF_RESULTS, int)) 72 | self.checkBoxCountries.setChecked(get_setting("useNE-countries", DEFAULT_USE_NE_COUNTRIES, bool)) 73 | self.checkBoxGraticules.setChecked(get_setting("useNE-graticules", DEFAULT_USE_NE_GRATICULES, bool)) 74 | self.checkBoxS2cloudless.setChecked(get_setting("useS2cloudless", DEFAULT_USE_S2_CLOUDLESS, bool)) 75 | 76 | self.lineEditLonLat.setText("{lon}, {lat}".format(**DEFAULT_ORIGIN)) 77 | self.on_radioButtonCoordinates_toggled(self.radioButtonCoordinates.isChecked()) 78 | self.on_radioButtonLayer_toggled(self.radioButtonLayer.isChecked()) 79 | self.on_radioButtonGeocoding_toggled(self.radioButtonGeocoding.isChecked()) 80 | self.on_radioButtonHFill_toggled(self.radioButtonHFill.isChecked()) 81 | self.on_radioButtonHFillWithHalo_toggled(self.radioButtonHFillWithHalo.isChecked()) 82 | self.on_checkBoxIntCountries_stateChanged() 83 | 84 | self.populate_comboBoxLayouts() 85 | self.populate_comboBoxProjections() 86 | 87 | self.mColorButtonBackground.setColor(DEFAULT_BACKGROUND_COLOR) 88 | self.mColorButtonHalo.setColor(DEFAULT_HALO_COLOR) 89 | self.mColorButtonHFill.setColor(DEFAULT_HALO_FILL_COLOR) 90 | self.mColorButtonLayoutBackground.setColor(DEFAULT_LAYOUT_BACKGROUND_COLOR) 91 | self.mColorButtonCountries.setColor(DEFAULT_COUNTRIES_COLOR) 92 | self.mColorButtonGraticules.setColor(DEFAULT_GRATICULES_COLOR) 93 | self.mColorButtonIntCountries.setColor(DEFAULT_INTERSECTING_COUNTRIES_COLOR) 94 | 95 | self.geolocations = {} 96 | self.old_coordinates = DEFAULT_ORIGIN 97 | self.old_projection = Projections.proj_from_id(self.comboBoxProjections.currentText()) 98 | 99 | # connections 100 | self.layout_mngr.layoutAdded.connect(self.populate_comboBoxLayouts) 101 | self.layout_mngr.layoutRemoved.connect(self.populate_comboBoxLayouts) 102 | self.layout_mngr.layoutRenamed.connect(self.populate_comboBoxLayouts) 103 | self.comboBoxProjections.currentTextChanged.connect(self.projection_changed) 104 | 105 | self.is_initializing = False 106 | 107 | def closeEvent(self, event): 108 | # noinspection PyUnresolvedReferences 109 | self.closingPlugin.emit() 110 | event.accept() 111 | 112 | # Without this annotation the signal is handled twice, 113 | # see https://stackoverflow.com/questions/14311578/event-signal-is-emmitted-twice-every-time 114 | @pyqtSlot(bool) 115 | def on_radioButtonCoordinates_toggled(self, is_checked): 116 | self.lineEditLonLat.setEnabled(is_checked) 117 | 118 | @pyqtSlot(bool) 119 | def on_radioButtonLayer_toggled(self, is_checked): 120 | self.mMapLayerComboBox.setEnabled(is_checked) 121 | 122 | @pyqtSlot(bool) 123 | def on_radioButtonGeocoding_toggled(self, is_checked): 124 | self.lineEditGeocoding.setEnabled(is_checked) 125 | self.pushButtonSearch.setEnabled(is_checked) 126 | self.listWidgetGeocodingResults.setEnabled(is_checked) 127 | self.spinBoxMaxResults.setEnabled(is_checked) 128 | 129 | 130 | def on_radioButtonHFillWithHalo_toggled(self, is_checked): 131 | self.mColorButtonHFill.setEnabled(is_checked or self.radioButtonHFill.isChecked()) 132 | 133 | @pyqtSlot(bool) 134 | def on_radioButtonHFill_toggled(self, is_checked): 135 | self.mColorButtonHFill.setEnabled(is_checked or self.radioButtonHFillWithHalo.isChecked()) 136 | 137 | @pyqtSlot(int) 138 | def on_spinBoxMaxResults_valueChanged(self, value): 139 | set_setting("maxNumberOfResults", value) 140 | 141 | def on_checkBoxCountries_stateChanged(self): 142 | set_setting("useNE-countries", self.checkBoxCountries.isChecked()) 143 | 144 | def on_checkBoxGraticules_stateChanged(self): 145 | set_setting("useNE-graticules", self.checkBoxGraticules.isChecked()) 146 | 147 | def on_checkBoxS2cloudless_stateChanged(self): 148 | set_setting("useS2cloudless", self.checkBoxS2cloudless.isChecked()) 149 | 150 | def on_checkBoxIntCountries_stateChanged(self): 151 | set_setting("intCountries", self.checkBoxIntCountries.isChecked()) 152 | self.mColorButtonIntCountries.setEnabled(self.checkBoxIntCountries.isChecked()) 153 | 154 | @pyqtSlot() 155 | def on_pushButtonSearch_clicked(self): 156 | text = self.lineEditGeocoding.text() 157 | if len(text.strip()): 158 | self.listWidgetGeocodingResults.clear() 159 | self.geolocations.clear() 160 | self.geocoder.geocode(text, self.spinBoxMaxResults.value()) 161 | 162 | @pyqtSlot() 163 | def on_pushButtonApplyVisualizations_clicked(self): 164 | if not self.is_initializing: 165 | coordinates = self.calculate_origin_coordinates() 166 | if coordinates != self.old_coordinates: 167 | self.old_coordinates = coordinates 168 | self.globe.set_origin(coordinates) 169 | projection = Projections.proj_from_id(self.comboBoxProjections.currentText()) 170 | if projection != self.old_projection: 171 | self.old_projection = projection 172 | self.globe.set_projection(projection) 173 | self.globe.change_project_projection() 174 | self.load_data_to_globe() 175 | self.globe.change_background_color(self.mColorButtonBackground.color()) 176 | self.mColorButtonBackground.setColor(self.iface.mapCanvas().canvasColor()) 177 | self.globe.set_group_visibility(True) 178 | self.add_halo_to_globe() 179 | 180 | @pyqtSlot() 181 | def on_pushButtonRun_clicked(self): 182 | self.load_data_to_globe(False) 183 | self.old_coordinates = self.calculate_origin_coordinates() 184 | self.globe.set_origin(self.old_coordinates) 185 | self.old_projection = Projections.proj_from_id(self.comboBoxProjections.currentText()) 186 | self.globe.set_projection(self.old_projection) 187 | self.globe.change_background_color(self.mColorButtonBackground.color()) 188 | self.mColorButtonBackground.setColor(self.iface.mapCanvas().canvasColor()) 189 | self.globe.change_project_projection() 190 | self.globe.set_group_visibility(True) 191 | self.add_halo_to_globe() 192 | 193 | @pyqtSlot() 194 | def on_pushButtonAddToLayout_clicked(self): 195 | selected_layouts = tuple( 196 | filter(lambda l: l.name() == self.comboBoxLayouts.currentText(), self.layout_mngr.layouts())) 197 | layout = selected_layouts[0] if len(selected_layouts) == 1 else create_layout("LayoutGlobe", self.qgis_instance) 198 | crs = self.qgis_instance.crs() 199 | 200 | # For some reason layout mode can't handle azimuthal ortographic projection with any decimals and the 201 | # projection needs to be set to project level before attempting to use it in a layout 202 | self.globe.set_origin( 203 | {key: float("{:.0f}".format(val)) for key, val in self.calculate_origin_coordinates().items()}) 204 | self.globe.set_projection(Projections.proj_from_id(self.comboBoxProjections.currentText())) 205 | self.globe.change_temporarily_to_globe_projection() 206 | self.globe.delete_group() 207 | self.load_data_to_globe() 208 | 209 | self.add_halo_to_globe() 210 | self.globe.set_group_visibility(False) 211 | self.globe.refresh_theme() 212 | self.globe.add_to_layout(layout, background_color=self.mColorButtonLayoutBackground.color(), 213 | size=self.spinBoxGlobeSize.value()) 214 | 215 | self.qgis_instance.setCrs(crs) 216 | 217 | def populate_comboBoxLayouts(self, *args): 218 | self.comboBoxLayouts.clear() 219 | self.comboBoxLayouts.addItem(tr(u"Create new layout (LayoutGlobe)")) 220 | for layout in self.layout_mngr.layouts(): 221 | self.comboBoxLayouts.addItem(layout.name()) 222 | 223 | def populate_comboBoxProjections(self, *args): 224 | try: 225 | proj_v = proj_version() 226 | except AttributeError: 227 | proj_v = (0, 0) 228 | self.comboBoxProjections.clear() 229 | for projection in Projections: 230 | if proj_v >= projection.value.min_proj: 231 | self.comboBoxProjections.addItem(projection.value.name) 232 | 233 | def add_halo_to_globe(self): 234 | self.globe.add_halo(self.radioButtonHHalo.isChecked(), self.mColorButtonHalo.color(), 235 | self.get_halo_fill_color(), self.radioButtonHFillWithHalo.isChecked()) 236 | self.globe.refresh_theme() 237 | 238 | def get_halo_fill_color(self): 239 | return self.mColorButtonHFill.color() if ( 240 | self.radioButtonHFill.isChecked() or self.radioButtonHFillWithHalo.isChecked()) else None 241 | 242 | def get_intersecting_countries_color(self): 243 | return self.mColorButtonIntCountries.color() if self.checkBoxIntCountries.isChecked() else None 244 | 245 | def on_geocoding_finished(self, geolocations): 246 | self.geolocations = geolocations.copy() 247 | [self.listWidgetGeocodingResults.addItem(name) for name in self.geolocations.keys()] 248 | 249 | def load_data_to_globe(self, possibly_use_intersecting_colors=True): 250 | self.globe.load_data(self.checkBoxS2cloudless.isChecked(), self.checkBoxCountries.isChecked(), 251 | self.checkBoxGraticules.isChecked(), self.mColorButtonCountries.color(), 252 | self.mColorButtonGraticules.color(), 253 | self.get_intersecting_countries_color() if possibly_use_intersecting_colors else None, 254 | self.comboBoxCountries.currentText().split(" ")[-1], 255 | self.comboBoxGraticules.currentText().split(" ")[-1]) 256 | 257 | def get_geocoded_coordinates(self): 258 | coordinates = None 259 | if (len(self.geolocations) and 260 | self.listWidgetGeocodingResults.count() > 0 and 261 | self.listWidgetGeocodingResults.currentItem() is not None): 262 | geolocation = self.listWidgetGeocodingResults.currentItem().text() 263 | coordinates = self.geolocations.get(geolocation, None) 264 | coordinates = {'lon': coordinates[0], 'lat': coordinates[1]} 265 | 266 | return coordinates 267 | 268 | def calculate_origin_coordinates(self): 269 | coordinates = None 270 | try: 271 | if self.radioButtonCoordinates.isChecked(): 272 | coordinates = tuple(map(lambda c: float(c.strip()), self.lineEditLonLat.text().split(','))) 273 | coordinates = {'lon': coordinates[0], 'lat': coordinates[1]} 274 | 275 | elif self.radioButtonGeocoding.isChecked(): 276 | coordinates = self.get_geocoded_coordinates() 277 | if not coordinates: 278 | coordinates = coordinates 279 | raise ValueError(tr(u"Make sure to select an item from the Geolocation list")) 280 | 281 | elif self.radioButtonLayer.isChecked(): 282 | layer = self.mMapLayerComboBox.currentLayer() 283 | if layer is None: 284 | raise ValueError(tr(u"Make sure to have at least one layer in the project")) 285 | center_point = layer.extent().center() 286 | 287 | center_point = transform_to_wgs84(center_point, layer.crs(), self.qgis_instance) 288 | coordinates = {'lon': center_point.x(), 'lat': center_point.y()} 289 | 290 | elif self.radioButtonCenter.isChecked(): 291 | coordinates = get_map_center_coordinates(self.iface, self.qgis_instance) 292 | 293 | except ValueError as e: 294 | LOGGER.warning(tr(u"Error occurred while parsing center of the globe"), 295 | bar_msg(f"{tr(u'Traceback')}: {e}", duration=6)) 296 | return coordinates 297 | 298 | def projection_changed(self, projection_id: str): 299 | # TODO: check if this causes problems in newer QGIS versions 300 | projection = Projections.proj_from_id(projection_id) 301 | # Centering is disabled due to rendering artifacts 302 | self.centeringGroupBox.setEnabled(projection == Projections.AZIMUTHAL_ORTHOGRAPHIC) 303 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Globe Builder 2 | ============= 3 | ![](https://github.com/GispoCoding/GlobeBuilder/workflows/Tests/badge.svg) 4 | [![codecov.io](https://codecov.io/github/GispoCoding/GlobeBuilder/coverage.svg?branch=master)](https://codecov.io/github/GispoCoding/GlobeBuilder?branch=master) 5 | ![](https://github.com/GispoCoding/GlobeBuilder/workflows/Release/badge.svg) 6 | ![](https://github.com/GispoCoding/GlobeBuilder/workflows/Translations/badge.svg) 7 | [![GPLv2 license](https://img.shields.io/badge/License-GPLv2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) 8 | 9 | QGIS 3 plugin that is meant for adding globe visualization to the current map using. 10 | 11 | ![Plugin in action](/images/screenshots/geocoding1.gif?raw=true "Plugin in action") 12 | 13 | Inspired by these blog posts by 14 | * [statsmapsnpix](http://www.statsmapsnpix.com/2019/09/globe-projections-and-insets-in-qgis.html) 15 | * [Gispo](https://www.gispo.fi/en/blog/the-power-of-community-30daymapchallenge/) 16 | * [gislounge](https://www.gislounge.com/how-to-use-the-equal-earth-projection-using-qgis-on-the-mac/) 17 | 18 | 19 | The plugin is still in beta-development. Please report issues preferably to Issues. 20 | 21 | **Developed by [Gispo Ltd.](https://www.gispo.fi/en/home/)** 22 | 23 | ## Installation instructions 24 | ### QGIS Plugin 25 | The plugin can be installed trough QGIS Plugins Repository. It can also be installed by downloading a release from this 26 | repository: 27 | 28 | 1. Download the latest release zip from GitHub releases (above). 29 | 30 | 2. Launch QGIS and the plugins menu by selecting Plugins - Manage and Install Plugins from the top menu. 31 | 32 | 3. Select the Install from ZIP tab, browse to the zip file you just downloaded, and click Install Plugin! 33 | 34 | 35 | ## Usage 36 | 37 | The Globe could be added either to the current map or to a layout. 38 | 39 | ![Plugin in layout mode](/images/screenshots/layout1.gif?raw=true "Plugin in layout mode") 40 | 41 | ## Gist 42 | There is also a Github [Gist](https://gist.github.com/Joonalai/7b8693ef904df75cb15cb9af0e82c032) that 43 | provides the core functionality of the plugin. The usage can be seen in the following gif. 44 | 45 | ![Gist in action](/images/screenshots/globe_view_gist.gif?raw=true "Gist in action") 46 | 47 | 48 | ## Development 49 | 50 | Refer to [development instructions](docs/development.md). 51 | 52 | ## Licence 53 | 54 | This plugin is licenced with [GNU Genereal Public License, version 2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). 55 | This plugin uses OpenStreetMap Nominatim geocoding API. 56 | The OpenStreetMap data is licensed under ODbL license. 57 | This plugin lists Sentinel-2 cloudless as an optional data source. Sentinel-2 cloudless data by 58 | by EOX IT Services GmbH (Contains modified Copernicus Sentinel data 2017 & 2018) released under 59 | [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/). 60 | For commercial usage of Sentinel-2 cloudless please see https://cloudless.eox.at. 61 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | Development of GlobeBuilder plugin 2 | =========================== 3 | 4 | The code for the plugin is in the [Qaava](../GlobeBuilder) folder. Make sure you have required tools, such as 5 | Qt with Qt Editor and Qt Linquist installed by following this 6 | [tutorial](https://www.qgistutorials.com/en/docs/3/building_a_python_plugin.html#get-the-tools). 7 | 8 | For building the plugin use platform independent [build.py](../GlobeBuilder/build.py) script. 9 | 10 | ### Adding or editing source files 11 | If you create or edit source files make sure that: 12 | * they contain relative imports 13 | ```python 14 | '''file GlobeBuilder/database_tools/db_initializer.py''' 15 | 16 | from ..utils.exceptions import GlobeBuilderAuthConfigException # Good 17 | 18 | from GlobeBuilder.utils.exceptions import GlobeBuilderAuthConfigException # Bad 19 | ``` 20 | * they will be found by [build.py](../GlobeBuilder/build.py) script (`py_files` and `ui_files` values) 21 | * you consider adding test files for the new functionality 22 | 23 | ### Deployment 24 | 25 | Edit [build.py](../GlobeBuilder/build.py) to contain working values for *profile*, *lrelease* and *pyrcc*. 26 | If you are running on Windows, make sure the value *QGIS_INSTALLATION_DIR* points to right folder 27 | 28 | Run the deployment with: 29 | ```shell script 30 | python build.py deploy 31 | ``` 32 | 33 | After deploying and restarting QGIS you should see the plugin in the QGIS installed plugins 34 | where you have to activate it. 35 | 36 | #### Testing 37 | Install Docker, docker-compose and python packages listed in [requirements.txt](requirements.txt) 38 | to run tests with: 39 | 40 | ```shell script 41 | python build.py test 42 | ``` 43 | 44 | #### Translating 45 | 46 | The translation files are in [i18n](../GlobeBuilder/resources/i18n) folder. 47 | Translatable content in python files is code such as `tr(u"Hello World")`. 48 | 49 | To update language *.ts* files to contain newest lines to translate, run 50 | ```shell script 51 | python build.py transup 52 | ``` 53 | 54 | You can then open the *.ts* files you wish to translate with Qt Linguist and make the changes. 55 | 56 | Compile the translations to *.qm* files with: 57 | ```shell script 58 | python build.py transcompile 59 | ``` 60 | 61 | ### Creating a release 62 | Follow these steps to create a release 63 | * Add changelog information to [CHANGELOG.md](../CHANGELOG.md) using this 64 | [format](https://raw.githubusercontent.com/opengisch/qgis-plugin-ci/master/CHANGELOG.md) 65 | * Make a new commit. (`git add -A && git commit -m "Release v0.1.0"`) 66 | * Create new tag for it (`git tag -a v0.1.0 -m "Version 0.1.0"`) 67 | * Push tag to Github using `git push --follow-tags` 68 | * Create Github release 69 | * [qgis-plugin-ci](https://github.com/opengisch/qgis-plugin-ci) adds release zip automatically as an asset 70 | -------------------------------------------------------------------------------- /images/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GispoCoding/GlobeBuilder/3a9ec05ef91e39fb67e52962c3bf24007b2d3046/images/icon.xcf -------------------------------------------------------------------------------- /images/screenshots/geocoding1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GispoCoding/GlobeBuilder/3a9ec05ef91e39fb67e52962c3bf24007b2d3046/images/screenshots/geocoding1.gif -------------------------------------------------------------------------------- /images/screenshots/globe_view_gist.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GispoCoding/GlobeBuilder/3a9ec05ef91e39fb67e52962c3bf24007b2d3046/images/screenshots/globe_view_gist.gif -------------------------------------------------------------------------------- /images/screenshots/layout1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GispoCoding/GlobeBuilder/3a9ec05ef91e39fb67e52962c3bf24007b2d3046/images/screenshots/layout1.gif -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Testing 2 | pytest~=6.2.3 3 | pytest-cov~=2.12.1 4 | pytest-qgis~=1.0.1 --------------------------------------------------------------------------------