├── .gitattributes ├── .github └── workflows │ ├── build.yml │ ├── lint.yaml │ ├── release.yml │ └── test_plugin.yaml ├── .gitignore ├── CHANGELOG.md ├── Makefile ├── README.md ├── cesium_ion ├── __init__.py ├── core │ ├── __init__.py │ ├── api_client.py │ ├── asset.py │ ├── enums.py │ ├── meta.py │ └── token.py ├── gui │ ├── __init__.py │ ├── add_asset_dialog.py │ ├── asset_by_id_widget.py │ ├── data_items.py │ ├── gui_utils.py │ └── select_token_widget.py ├── i18n │ ├── af.qm │ └── af.ts ├── icon.svg ├── icons │ ├── browser_root.svg │ └── cesium_3d_tile.svg ├── metadata.txt ├── plugin.py ├── resources │ └── auth_cfg.json └── ui │ ├── asset_by_id.ui │ └── select_token.ui ├── pylintrc ├── requirements ├── packaging.txt └── testing.txt ├── scripts ├── compile-strings.sh ├── run-env-linux.sh └── update-strings.sh └── setup.cfg /.gitattributes: -------------------------------------------------------------------------------- 1 | cesium_ion/test export-ignore 2 | cesium_ion/test_suite.py export-ignore 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: "Build" 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Get source code 12 | uses: actions/checkout@v4 13 | with: 14 | # To fetch tags 15 | fetch-depth: 0 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.10" 21 | cache: "pip" 22 | cache-dependency-path: "requirements/packaging.txt" 23 | 24 | # - name: Install Qt lrelease 25 | # run: | 26 | # sudo apt-get update 27 | # sudo apt-get install qttools5-dev-tools 28 | 29 | - name: Install Python requirements 30 | run: pip install -r requirements/packaging.txt 31 | 32 | - name: Set env 33 | run: | 34 | TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) 35 | echo "VERSION=$(echo ${TAG} | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}')-alpha" >> $GITHUB_ENV 36 | echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 37 | 38 | - name: Build package 39 | run: | 40 | qgis-plugin-ci --no-validation package ${{ env.VERSION }} 41 | mkdir tmp 42 | unzip cesium_ion.${{ env.VERSION }}.zip -d tmp 43 | 44 | - uses: actions/upload-artifact@v2 45 | with: 46 | name: cesium_ion_plugin.${{ env.VERSION }}.${{ env.SHA_SHORT }} 47 | path: tmp 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | paths: 6 | - "cesium_ion/**" 7 | - ".github/workflows/lint.yaml" 8 | pull_request: 9 | types: [opened, synchronize, edited] 10 | 11 | 12 | jobs: 13 | 14 | Check-code-quality: 15 | runs-on: ubuntu-latest 16 | steps: 17 | 18 | - name: Install Python 19 | uses: actions/setup-python@v1 20 | with: 21 | python-version: '3.9' 22 | 23 | - name: Check out source repository 24 | uses: actions/checkout@v2 25 | 26 | - name: Install packages 27 | run: | 28 | pip install -r requirements/testing.txt 29 | pip install pylint pycodestyle 30 | 31 | - name: flake8 Lint 32 | uses: py-actions/flake8@v1 33 | 34 | - name: Pycodestyle 35 | run: make pycodestyle 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: published 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | if: github.repository_owner == 'north-road' && contains(github.ref, 'refs/tags/') 9 | 10 | steps: 11 | - name: Set env 12 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 13 | 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.10" 20 | cache: "pip" 21 | cache-dependency-path: "requirements/packaging.txt" 22 | 23 | # Needed if the plugin is using Transifex, to have the lrelease command 24 | # - name: Install Qt lrelease 25 | # run: | 26 | # sudo apt-get update 27 | # sudo apt-get install qt5-make qttools5-dev-tools 28 | 29 | - name: Install Python requirements 30 | run: pip install -r requirements/packaging.txt 31 | 32 | - name : Fetch current changelog 33 | run: qgis-plugin-ci changelog ${{ env.RELEASE_VERSION }} >> release.md 34 | 35 | - name: Deploy plugin 36 | run: >- 37 | qgis-plugin-ci 38 | release ${{ env.RELEASE_VERSION }} 39 | --github-token ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/test_plugin.yaml: -------------------------------------------------------------------------------- 1 | name: Test plugin 2 | 3 | on: 4 | push: 5 | paths: 6 | - "cesium_ion/**" 7 | - ".github/workflows/test_plugin.yaml" 8 | pull_request: 9 | types: [opened, synchronize, edited] 10 | 11 | env: 12 | # plugin name/directory where the code for the plugin is stored 13 | PLUGIN_NAME: cesium_ion 14 | # python notation to test running inside plugin 15 | TESTS_RUN_FUNCTION: cesium_ion.test_suite.test_package 16 | # Docker settings 17 | DOCKER_IMAGE: qgis/qgis 18 | 19 | 20 | jobs: 21 | 22 | Test-plugin-cesium_ion: 23 | 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | matrix: 28 | docker_tags: [latest] 29 | 30 | steps: 31 | 32 | - name: Checkout 33 | uses: actions/checkout@v2 34 | 35 | - name: Docker pull and create qgis-testing-environment 36 | run: | 37 | docker pull "$DOCKER_IMAGE":${{ matrix.docker_tags }} 38 | docker run -d --name qgis-testing-environment -v "$GITHUB_WORKSPACE":/tests_directory -e DISPLAY=:99 "$DOCKER_IMAGE":${{ matrix.docker_tags }} 39 | 40 | - name: Docker set up QGIS 41 | run: | 42 | docker exec qgis-testing-environment sh -c "qgis_setup.sh $PLUGIN_NAME" 43 | docker exec qgis-testing-environment sh -c "rm -f /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/$PLUGIN_NAME" 44 | docker exec qgis-testing-environment sh -c "ln -s /tests_directory/$PLUGIN_NAME /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/$PLUGIN_NAME" 45 | docker exec qgis-testing-environment sh -c "pip3 install -r /tests_directory/requirements/testing.txt" 46 | 47 | - name: Docker run plugin tests 48 | run: | 49 | docker exec qgis-testing-environment sh -c "qgis_testrunner.sh $TESTS_RUN_FUNCTION" 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .noseids 9 | 10 | # Distribution / packaging 11 | .Python 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 | MANIFEST 28 | .idea 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | .Rproj.user 108 | *.Rproj 109 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [1.0.0] - 2023-08-28 6 | 7 | - Initial release 8 | 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # QGIS3 default 2 | QGISDIR=.local/share/QGIS/QGIS3/profiles/default 3 | 4 | PLUGIN_NAME = cesium_ion 5 | 6 | EXTRAS = metadata.txt icon.png 7 | 8 | EXTRA_DIRS = 9 | 10 | default: 11 | 12 | 13 | default: 14 | 15 | %.qm : %.ts 16 | $(LRELEASE) $< 17 | 18 | test: transcompile 19 | @echo 20 | @echo "----------------------" 21 | @echo "Regression Test Suite" 22 | @echo "----------------------" 23 | 24 | @# Preceding dash means that make will continue in case of errors 25 | @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \ 26 | export QGIS_DEBUG=0; \ 27 | export QGIS_LOG_FILE=/dev/null; \ 28 | nosetests3 -v -s --with-id --with-coverage --cover-package=. $(PLUGIN_NAME).test \ 29 | 3>&1 1>&2 2>&3 3>&- || true 30 | @echo "----------------------" 31 | @echo "If you get a 'no module named qgis.core error, try sourcing" 32 | @echo "the helper script we have provided first then run make test." 33 | @echo "e.g. source run-env-linux.sh ; make test" 34 | @echo "----------------------" 35 | 36 | 37 | deploy: 38 | @echo 39 | @echo "------------------------------------------" 40 | @echo "Deploying (symlinking) plugin to your qgis3 directory." 41 | @echo "------------------------------------------" 42 | # The deploy target only works on unix like operating system where 43 | # the Python plugin directory is located at: 44 | # $HOME/$(QGISDIR)/python/plugins 45 | ln -s `pwd`/$(PLUGIN_NAME) $(HOME)/$(QGISDIR)/python/plugins/${PWD##*/} 46 | 47 | 48 | transup: 49 | @echo 50 | @echo "------------------------------------------------" 51 | @echo "Updating translation files with any new strings." 52 | @echo "------------------------------------------------" 53 | @chmod +x scripts/update-strings.sh 54 | @scripts/update-strings.sh $(LOCALES) 55 | 56 | transcompile: 57 | @echo 58 | @echo "----------------------------------------" 59 | @echo "Compiled translation files to .qm files." 60 | @echo "----------------------------------------" 61 | @chmod +x scripts/compile-strings.sh 62 | @scripts/compile-strings.sh $(LRELEASE) $(LOCALES) 63 | 64 | transclean: 65 | @echo 66 | @echo "------------------------------------" 67 | @echo "Removing compiled translation files." 68 | @echo "------------------------------------" 69 | rm -f i18n/*.qm 70 | 71 | pylint: 72 | @echo 73 | @echo "-----------------" 74 | @echo "Pylint violations" 75 | @echo "-----------------" 76 | @pylint --reports=n --rcfile=pylintrc $(PLUGIN_NAME) 77 | @echo 78 | @echo "----------------------" 79 | @echo "If you get a 'no module named qgis.core' error, try sourcing" 80 | @echo "the helper script we have provided first then run make pylint." 81 | @echo "e.g. source run-env-linux.sh ; make pylint" 82 | @echo "----------------------" 83 | 84 | 85 | # Run pep8/pycodestyle style checking 86 | #http://pypi.python.org/pypi/pep8 87 | pycodestyle: 88 | @echo 89 | @echo "-----------" 90 | @echo "pycodestyle PEP8 issues" 91 | @echo "-----------" 92 | @pycodestyle --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128,E402,E501,W504 $(PLUGIN_NAME) 93 | @echo "-----------" 94 | 95 | 96 | # The dclean target removes compiled python files from plugin directory 97 | # also deletes any .git entry 98 | dclean: 99 | @echo 100 | @echo "-----------------------------------" 101 | @echo "Removing any compiled python files." 102 | @echo "-----------------------------------" 103 | find $(PLUGIN_NAME) -iname "*.pyc" -delete 104 | find $(PLUGIN_NAME) -iname ".git" -prune -exec rm -Rf {} \; 105 | 106 | zip: dclean 107 | @echo 108 | @echo "---------------------------" 109 | @echo "Creating plugin zip bundle." 110 | @echo "---------------------------" 111 | # The zip target deploys the plugin and creates a zip file with the deployed 112 | # content. You can then upload the zip file on http://plugins.qgis.org 113 | rm -f $(PLUGIN_NAME).zip 114 | zip -9r $(PLUGIN_NAME).zip $(PLUGIN_NAME) -x *.git* -x *__pycache__* -x *test* 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QGIS Cesium ion plugin 2 | 3 | A QGIS plugin which allows users to explore and easily add their datasets from 4 | Cesium ion. 5 | 6 | -------------------------------------------------------------------------------- /cesium_ion/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cesium ion QGIS plugin 3 | """ 4 | 5 | 6 | def classFactory(iface): 7 | """ 8 | Creates the plugin 9 | """ 10 | # pylint: disable=import-outside-toplevel 11 | from cesium_ion.plugin import CesiumIonPlugin 12 | 13 | return CesiumIonPlugin(iface) 14 | -------------------------------------------------------------------------------- /cesium_ion/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core module 3 | """ 4 | 5 | from .enums import AssetType, Status # NOQA 6 | from .api_client import CesiumIonApiClient, API_CLIENT # NOQA 7 | from .asset import Asset # NOQA 8 | from .token import Token # NOQA 9 | -------------------------------------------------------------------------------- /cesium_ion/core/api_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cesium ion API client 3 | """ 4 | 5 | import json 6 | from typing import ( 7 | Dict, 8 | Optional, 9 | List 10 | ) 11 | 12 | from qgis.PyQt.QtCore import ( 13 | QUrl, 14 | QUrlQuery, 15 | QObject, 16 | pyqtSignal 17 | ) 18 | from qgis.PyQt.QtNetwork import ( 19 | QNetworkRequest, 20 | QNetworkReply 21 | ) 22 | from qgis.core import ( 23 | QgsApplication, 24 | QgsBlockingNetworkRequest 25 | ) 26 | 27 | from .asset import Asset 28 | from .meta import PLUGIN_METADATA_PARSER 29 | from .token import Token 30 | 31 | 32 | class CesiumIonApiClient(QObject): 33 | """ 34 | Client for the Cesium ion REST API 35 | """ 36 | 37 | URL = 'https://api.cesium.com' 38 | LIST_ASSETS_ENDPOINT = '/v1/assets' 39 | LIST_TOKENS_ENDPOINT = '/v2/tokens' 40 | CREATE_TOKEN_ENDPOINT = '/v2/tokens' 41 | OAUTH_ID = 'cesiion' 42 | 43 | error_occurred = pyqtSignal(str) 44 | 45 | def __init__(self, parent=None): 46 | super().__init__(parent) 47 | # default headers to add to all requests 48 | self.headers = { 49 | 'accept': 'application/json', 50 | 'x-qgis-plugin-version': PLUGIN_METADATA_PARSER.get_version() 51 | } 52 | 53 | @staticmethod 54 | def build_url(endpoint: str) -> QUrl: 55 | """ 56 | Returns the full url of the specified endpoint 57 | """ 58 | return QUrl(CesiumIonApiClient.URL + endpoint) 59 | 60 | @staticmethod 61 | def _to_url_query(parameters: Dict[str, object]) -> QUrlQuery: 62 | """ 63 | Converts query parameters as a dictionary to a URL query 64 | """ 65 | query = QUrlQuery() 66 | for name, value in parameters.items(): 67 | if isinstance(value, (list, tuple)): 68 | for v in value: 69 | query.addQueryItem(name, str(v)) 70 | else: 71 | query.addQueryItem(name, str(value)) 72 | return query 73 | 74 | def _build_request(self, endpoint: str, headers=None, params=None) \ 75 | -> QNetworkRequest: 76 | """ 77 | Builds a network request 78 | """ 79 | url = self.build_url(endpoint) 80 | 81 | if params: 82 | url.setQuery(CesiumIonApiClient._to_url_query(params)) 83 | 84 | network_request = QNetworkRequest(url) 85 | 86 | combined_headers = self.headers 87 | if headers: 88 | combined_headers.update(headers) 89 | 90 | for header, value in combined_headers.items(): 91 | network_request.setRawHeader(header.encode(), value.encode()) 92 | 93 | return network_request 94 | 95 | def list_assets_request(self, 96 | page: Optional[int] = None, 97 | filter_string: Optional[str] = None) \ 98 | -> QNetworkRequest: 99 | """ 100 | List assets asynchronously 101 | """ 102 | params = {} 103 | if page is not None: 104 | params['page'] = page 105 | if filter_string: 106 | params['search'] = filter_string 107 | 108 | params['status'] = 'COMPLETE' 109 | params['type'] = '3DTILES' 110 | 111 | request = self._build_request( 112 | self.LIST_ASSETS_ENDPOINT, 113 | params=params 114 | ) 115 | return request 116 | 117 | def list_assets_blocking(self, 118 | page: Optional[int] = None, 119 | filter_string: Optional[str] = None 120 | ) -> List[Asset]: 121 | """ 122 | Parse a list assets reply and return as a list of Asset objects 123 | """ 124 | req = self.list_assets_request(page, filter_string) 125 | blocking_request = QgsBlockingNetworkRequest() 126 | blocking_request.setAuthCfg(API_CLIENT.OAUTH_ID) 127 | 128 | res = blocking_request.get(req) 129 | if res != QgsBlockingNetworkRequest.NoError: 130 | self.error_occurred.emit(blocking_request.errorMessage()) 131 | return [] 132 | 133 | reply = blocking_request.reply() 134 | if reply.error() == QNetworkReply.OperationCanceledError: 135 | return [] 136 | 137 | if reply.error() != QNetworkReply.NoError: 138 | self.error_occurred.emit(reply.errorString()) 139 | return [] 140 | 141 | assets_json = json.loads(reply.content().data().decode())['items'] 142 | return [Asset.from_json(asset) for asset in assets_json] 143 | 144 | def list_tokens_request(self, 145 | page: Optional[int] = None, 146 | filter_string: Optional[str] = None) \ 147 | -> QNetworkRequest: 148 | """ 149 | Creates a list tokens request 150 | """ 151 | params = {} 152 | if page is not None: 153 | params['page'] = page 154 | if filter_string: 155 | params['search'] = filter_string 156 | 157 | request = self._build_request( 158 | self.LIST_TOKENS_ENDPOINT, 159 | params=params 160 | ) 161 | QgsApplication.authManager().updateNetworkRequest( 162 | request, API_CLIENT.OAUTH_ID 163 | ) 164 | return request 165 | 166 | def parse_list_tokens_reply(self, 167 | reply: QNetworkReply 168 | ) -> List[Token]: 169 | """ 170 | Parses a list tokens reply and returns a list of tokens 171 | """ 172 | if reply.error() != QNetworkReply.NoError: 173 | self.error_occurred.emit(reply.errorString()) 174 | return [] 175 | 176 | if reply.error() == QNetworkReply.OperationCanceledError: 177 | return [] 178 | 179 | reply_data = reply.readAll() 180 | tokens_json = json.loads(reply_data.data().decode())['items'] 181 | return [Token.from_json(token) for token in tokens_json] 182 | 183 | def create_token(self, token_name: str, 184 | scopes: List[str], 185 | asset_ids: Optional[List[int]] = None) -> Optional[Token]: 186 | """ 187 | Creates a new token 188 | """ 189 | params = {'name': token_name, 190 | 'scopes': scopes} 191 | if asset_ids: 192 | params['assetIds'] = asset_ids 193 | 194 | request = self._build_request( 195 | self.CREATE_TOKEN_ENDPOINT, 196 | {'Content-Type': 'application/json'} 197 | ) 198 | 199 | blocking_request = QgsBlockingNetworkRequest() 200 | blocking_request.setAuthCfg(API_CLIENT.OAUTH_ID) 201 | 202 | res = blocking_request.post(request, json.dumps(params).encode()) 203 | if res != QgsBlockingNetworkRequest.NoError: 204 | self.error_occurred.emit(blocking_request.errorMessage()) 205 | return None 206 | 207 | reply = blocking_request.reply() 208 | if reply.error() == QNetworkReply.OperationCanceledError: 209 | return None 210 | 211 | if reply.error() != QNetworkReply.NoError: 212 | self.error_occurred.emit(reply.errorString()) 213 | return None 214 | 215 | token_json = json.loads(reply.content().data().decode()) 216 | return Token.from_json(token_json) 217 | 218 | 219 | API_CLIENT = CesiumIonApiClient() 220 | -------------------------------------------------------------------------------- /cesium_ion/core/asset.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cesium ion asset class 3 | """ 4 | 5 | from dataclasses import dataclass 6 | from typing import Optional, Dict 7 | 8 | from qgis.PyQt.QtCore import ( 9 | Qt, 10 | QDateTime 11 | ) 12 | 13 | from .enums import ( 14 | AssetType, 15 | Status 16 | ) 17 | 18 | 19 | @dataclass 20 | class Asset: 21 | """ 22 | Represents an ion asset 23 | """ 24 | id: str 25 | name: str 26 | type: AssetType 27 | status: Status 28 | description: Optional[str] = None 29 | attribution: Optional[str] = None 30 | bytes: Optional[int] = None 31 | date_added: Optional[QDateTime] = None 32 | percent_complete: Optional[int] = None 33 | archivable: Optional[bool] = None 34 | exportable: Optional[bool] = None 35 | 36 | @staticmethod 37 | def from_json(json: Dict) -> 'Asset': 38 | """ 39 | Creates an asset from a JSON object 40 | """ 41 | return Asset( 42 | id=json['id'], 43 | name=json['name'], 44 | description=json.get('description'), 45 | attribution=json.get('attribution'), 46 | type=AssetType.from_string(json['type']), 47 | bytes=json.get('bytes'), 48 | date_added=QDateTime.fromString( 49 | json['dateAdded'], Qt.DateFormat.ISODate 50 | ) if 'dateAdded' in json else None, 51 | status=Status.from_string(json['status']), 52 | percent_complete=json.get('percentComplete'), 53 | archivable=json.get('archivable'), 54 | exportable=json.get('exportable') 55 | ) 56 | 57 | def as_qgis_data_source(self, access_token: Optional[str] = None) -> str: 58 | """ 59 | Returns a QGIS data source string representing a connection 60 | to the asset 61 | """ 62 | if access_token: 63 | return 'ion://?assetId={}&accessToken={}'.format( 64 | self.id, access_token 65 | ) 66 | 67 | # pylint: disable=import-outside-toplevel 68 | from .api_client import CesiumIonApiClient 69 | # pylint: enable=import-outside-toplevel 70 | return 'ion://?assetId={}&authcfg={}'.format( 71 | self.id, CesiumIonApiClient.OAUTH_ID 72 | ) 73 | -------------------------------------------------------------------------------- /cesium_ion/core/enums.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cesium ion plugin enums 3 | """ 4 | 5 | from enum import Enum, auto 6 | 7 | 8 | class AssetType(Enum): 9 | """ 10 | Asset types 11 | """ 12 | Tiles3D = auto() 13 | GLTF = auto() 14 | Imagery = auto() 15 | Terrain = auto() 16 | KML = auto() 17 | CZML = auto() 18 | GeoJSON = auto() 19 | 20 | @staticmethod 21 | def from_string(string: str) -> 'AssetType': 22 | """ 23 | Returns an asset type from a string value 24 | """ 25 | return { 26 | '3DTILES': AssetType.Tiles3D, 27 | 'GLTF': AssetType.GLTF, 28 | 'IMAGERY': AssetType.Imagery, 29 | 'TERRAIN': AssetType.Terrain, 30 | 'KML': AssetType.KML, 31 | 'CZML': AssetType.CZML, 32 | 'GEOJSON': AssetType.GeoJSON 33 | }[string.upper()] 34 | 35 | 36 | class Status(Enum): 37 | """ 38 | Asset status 39 | """ 40 | AwaitingFiles = auto() 41 | NotStarted = auto() 42 | InProgress = auto() 43 | Complete = auto() 44 | DataError = auto() 45 | Error = auto() 46 | 47 | @staticmethod 48 | def from_string(string: str) -> 'Status': 49 | """ 50 | Returns a status from a string value 51 | """ 52 | return { 53 | 'AWAITING_FILES': Status.AwaitingFiles, 54 | 'NOT_STARTED': Status.NotStarted, 55 | 'IN_PROGRESS': Status.InProgress, 56 | 'COMPLETE': Status.Complete, 57 | 'DATA_ERROR': Status.DataError, 58 | 'ERROR': Status.Error 59 | }[string.upper()] 60 | -------------------------------------------------------------------------------- /cesium_ion/core/meta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Felt API client 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import configparser 11 | import re 12 | from pathlib import Path 13 | from typing import Tuple 14 | 15 | 16 | class PluginMetadataParser: 17 | """ 18 | Plugin metadata parser 19 | """ 20 | 21 | #: Semantic version regex 22 | VERSION_REGEX = re.compile(r'^(\d+)\.(\d+).*') 23 | 24 | def __init__(self): 25 | self._meta_parser = configparser.ConfigParser() 26 | self._meta_parser.read( 27 | str(Path(__file__).parent.parent / 'metadata.txt') 28 | ) 29 | self._prop_cache = {} 30 | 31 | @staticmethod 32 | def semantic_version(version: str) -> Tuple[int, int]: 33 | """ 34 | Converts a version string to a (major, minor) version tuple. 35 | """ 36 | m = PluginMetadataParser.VERSION_REGEX.match(version) 37 | if not m or len(m.groups()) != 2: 38 | return 0, 0 39 | return tuple(int(v) for v in m.groups()) # noqa 40 | 41 | def get_property(self, name, section='general'): 42 | """ 43 | Reads the property with the given name from the local plugin metadata. 44 | """ 45 | key = f'{section}.{name}' 46 | try: 47 | value = self._prop_cache.get( 48 | key, 49 | self._meta_parser.get(section, name) 50 | ) 51 | except (configparser.NoOptionError, configparser.NoSectionError): 52 | value = None 53 | self._prop_cache[key] = value 54 | return value 55 | 56 | def get_app_name(self) -> str: 57 | """ 58 | Returns the name of the QGIS plugin. 59 | """ 60 | return self.get_property("name") 61 | 62 | def get_long_app_name(self) -> str: 63 | """ 64 | Returns the full name of the QGIS plugin. 65 | Depending on the settings, this may return the same as calling 66 | get_app_name(). 67 | """ 68 | long_name = self.get_property("longName") 69 | if long_name: 70 | return long_name 71 | return self.get_app_name() 72 | 73 | def get_short_app_name(self) -> str: 74 | """ Returns the short name of the QGIS plugin. 75 | Depending on the settings, this may return the same as 76 | calling get_app_name(). 77 | """ 78 | short_name = self.get_property("shortName") 79 | if short_name: 80 | return short_name 81 | return self.get_app_name() 82 | 83 | def get_tracker_url(self) -> str: 84 | """ 85 | Returns the issue tracker URL for the plugin. 86 | """ 87 | return self.get_property("tracker") 88 | 89 | def get_version(self) -> str: 90 | """ 91 | Returns the plugin version string. 92 | """ 93 | return self.get_property("version").strip() 94 | 95 | def get_docs_url(self) -> str: 96 | """ 97 | Returns the plugin documentation URL. 98 | """ 99 | return self.get_property("docs", "bridge").rstrip('/') 100 | 101 | 102 | PLUGIN_METADATA_PARSER = PluginMetadataParser() 103 | -------------------------------------------------------------------------------- /cesium_ion/core/token.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cesium ion token class 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import Optional, Dict, List 7 | 8 | from qgis.PyQt.QtCore import ( 9 | Qt, 10 | QDateTime 11 | ) 12 | 13 | 14 | @dataclass 15 | class Token: 16 | """ 17 | Represents an ion asset 18 | """ 19 | id: str 20 | name: str 21 | token: str 22 | scopes: str 23 | date_added: Optional[QDateTime] = None 24 | date_modified: Optional[QDateTime] = None 25 | date_last_used: Optional[QDateTime] = None 26 | asset_ids: List[int] = field(default_factory=list) 27 | is_default: Optional[bool] = None 28 | 29 | @staticmethod 30 | def from_json(json: Dict) -> 'Token': 31 | """ 32 | Creates an asset from a JSON object 33 | """ 34 | return Token( 35 | id=json['id'], 36 | name=json['name'], 37 | token=json.get('token'), 38 | date_added=QDateTime.fromString( 39 | json['dateAdded'], Qt.DateFormat.ISODate 40 | ) if 'dateAdded' in json else None, 41 | date_modified=QDateTime.fromString( 42 | json['dateModified'], Qt.DateFormat.ISODate 43 | ) if 'dateModified' in json else None, 44 | date_last_used=QDateTime.fromString( 45 | json['dateLastUsed'], Qt.DateFormat.ISODate 46 | ) if 'dateLastUsed' in json else None, 47 | asset_ids=json.get('assetIds', []), 48 | is_default=json.get('isDefault'), 49 | scopes=json['scopes'] 50 | ) 51 | -------------------------------------------------------------------------------- /cesium_ion/gui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | GUI module 3 | """ 4 | from .data_items import ( 5 | CesiumIonDataItemProvider, # NOQA 6 | CesiumIonDataItemGuiProvider, # NOQA 7 | CesiumIonDropHandler # NOQA 8 | ) 9 | from .select_token_widget import SelectTokenWidget # NOQA 10 | from .add_asset_dialog import ( 11 | AddAssetDialog, # NOQA 12 | AddAssetByIdDialog # NOQA 13 | ) 14 | from .asset_by_id_widget import AssetByIdWidget # NOQA 15 | -------------------------------------------------------------------------------- /cesium_ion/gui/add_asset_dialog.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add asset dialog 3 | """ 4 | from typing import Optional 5 | 6 | from qgis.PyQt.QtWidgets import ( 7 | QDialog, 8 | QWidget, 9 | QVBoxLayout, 10 | QDialogButtonBox 11 | ) 12 | from qgis.gui import ( 13 | QgsGui 14 | ) 15 | 16 | from .select_token_widget import SelectTokenWidget 17 | from .asset_by_id_widget import AssetByIdWidget 18 | 19 | 20 | class AddAssetDialog(QDialog): 21 | """ 22 | A custom dialog for adding an asset to a project 23 | """ 24 | 25 | def __init__(self, parent: Optional[QWidget] = None): 26 | super().__init__(parent) 27 | 28 | self.setObjectName('AddAssetDialog') 29 | QgsGui.enableAutoGeometryRestore(self) 30 | 31 | self.setWindowTitle(self.tr('Select Cesium ion Token')) 32 | 33 | vl = QVBoxLayout() 34 | self.select_token_widget = SelectTokenWidget() 35 | vl.addWidget(self.select_token_widget) 36 | 37 | self.button_box = QDialogButtonBox( 38 | QDialogButtonBox.Ok | QDialogButtonBox.Cancel 39 | ) 40 | self.button_box.accepted.connect(self.accept) 41 | self.button_box.rejected.connect(self.reject) 42 | vl.addWidget(self.button_box) 43 | 44 | self.setLayout(vl) 45 | 46 | self.select_token_widget.is_valid_changed.connect(self._set_valid) 47 | self.button_box.button(QDialogButtonBox.Ok).setEnabled( 48 | self.select_token_widget.is_valid() 49 | ) 50 | 51 | def _set_valid(self, is_valid: bool): 52 | """ 53 | Sets whether the dialog state is valid 54 | """ 55 | self.button_box.button(QDialogButtonBox.Ok).setEnabled( 56 | is_valid 57 | ) 58 | 59 | def existing_token(self) -> Optional[str]: 60 | """ 61 | Returns the selected existing token 62 | """ 63 | return self.select_token_widget.existing_token() 64 | 65 | def new_token_name(self) -> Optional[str]: 66 | """ 67 | Returns the name for the new token 68 | """ 69 | return self.select_token_widget.new_token_name() 70 | 71 | 72 | class AddAssetByIdDialog(QDialog): 73 | """ 74 | A custom dialog for adding an asset by ID 75 | """ 76 | 77 | def __init__(self, parent: Optional[QWidget] = None): 78 | super().__init__(parent) 79 | 80 | self.setObjectName('AddAssetByIdDialog') 81 | QgsGui.enableAutoGeometryRestore(self) 82 | 83 | self.setWindowTitle(self.tr('Add Cesium ion Asset')) 84 | 85 | vl = QVBoxLayout() 86 | self.asset_widget = AssetByIdWidget() 87 | vl.addWidget(self.asset_widget) 88 | 89 | self.button_box = QDialogButtonBox( 90 | QDialogButtonBox.Ok | QDialogButtonBox.Cancel 91 | ) 92 | self.button_box.accepted.connect(self.accept) 93 | self.button_box.rejected.connect(self.reject) 94 | vl.addWidget(self.button_box) 95 | 96 | self.setLayout(vl) 97 | 98 | self.asset_widget.is_valid_changed.connect(self._set_valid) 99 | self.button_box.button(QDialogButtonBox.Ok).setEnabled( 100 | self.asset_widget.is_valid() 101 | ) 102 | 103 | def _set_valid(self, is_valid: bool): 104 | """ 105 | Sets whether the dialog state is valid 106 | """ 107 | self.button_box.button(QDialogButtonBox.Ok).setEnabled( 108 | is_valid 109 | ) 110 | 111 | def asset_id(self) -> str: 112 | """ 113 | Returns the selected asset ID 114 | """ 115 | return self.asset_widget.asset_id() 116 | 117 | def token(self) -> str: 118 | """ 119 | Returns the selected token 120 | """ 121 | return self.asset_widget.token() 122 | -------------------------------------------------------------------------------- /cesium_ion/gui/asset_by_id_widget.py: -------------------------------------------------------------------------------- 1 | """ 2 | Asset by ID widget 3 | """ 4 | from typing import Optional 5 | 6 | from qgis.PyQt import uic 7 | from qgis.PyQt.QtCore import ( 8 | pyqtSignal 9 | ) 10 | from qgis.PyQt.QtWidgets import ( 11 | QWidget 12 | ) 13 | 14 | from .gui_utils import GuiUtils 15 | 16 | WIDGET, _ = uic.loadUiType(GuiUtils.get_ui_file_path('asset_by_id.ui')) 17 | 18 | 19 | class AssetByIdWidget(QWidget, WIDGET): 20 | """ 21 | Custom widget for adding assets by ID 22 | """ 23 | 24 | is_valid_changed = pyqtSignal(bool) 25 | 26 | def __init__(self, # pylint: disable=too-many-statements 27 | parent: Optional[QWidget] = None): 28 | super().__init__(parent) 29 | self.setupUi(self) 30 | 31 | self.edit_asset_id.textChanged.connect(self._validate) 32 | self.edit_access_token.textChanged.connect(self._validate) 33 | 34 | def _validate(self): 35 | """ 36 | Validates the current settings 37 | """ 38 | self.is_valid_changed.emit(self.is_valid()) 39 | 40 | def is_valid(self) -> bool: 41 | """ 42 | Returns True if the settings are valid 43 | """ 44 | if not self.edit_asset_id.text().strip(): 45 | return False 46 | 47 | if not self.edit_access_token.text().strip(): 48 | return False 49 | 50 | return True 51 | 52 | def asset_id(self) -> str: 53 | """ 54 | Returns the selected asset ID 55 | """ 56 | return self.edit_asset_id.text().strip() 57 | 58 | def token(self) -> str: 59 | """ 60 | Returns the selected token 61 | """ 62 | return self.edit_access_token.text().strip() 63 | -------------------------------------------------------------------------------- /cesium_ion/gui/data_items.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cesium ion data browser items 3 | """ 4 | from functools import partial 5 | 6 | from qgis.PyQt.QtCore import ( 7 | QCoreApplication 8 | ) 9 | from qgis.PyQt.QtWidgets import ( 10 | QAction 11 | ) 12 | from qgis.core import ( 13 | Qgis, 14 | QgsDataProvider, 15 | QgsDataItemProvider, 16 | QgsDataCollectionItem, 17 | QgsDataItem, 18 | QgsMimeDataUtils 19 | ) 20 | from qgis.gui import ( 21 | QgsDataItemGuiProvider, 22 | QgsCustomDropHandler 23 | ) 24 | from qgis.utils import iface 25 | 26 | from .gui_utils import GuiUtils 27 | from ..core import ( 28 | Asset, 29 | AssetType, 30 | Status, 31 | API_CLIENT 32 | ) 33 | 34 | 35 | class IonAssetItem(QgsDataItem): 36 | """ 37 | Represents an individual asset.py on Cesium ion 38 | """ 39 | 40 | def __init__(self, 41 | parent: QgsDataItem, 42 | asset: Asset): # NOQA 43 | super().__init__( 44 | Qgis.BrowserItemType.Custom, 45 | parent, 46 | asset.name, 47 | 'ion{}'.format(asset.id), 48 | 'cesium_ion') 49 | self.asset = asset 50 | self.setState( 51 | Qgis.BrowserItemState.Populated 52 | ) 53 | self.setIcon(GuiUtils.get_icon('cesium_3d_tile.svg')) 54 | 55 | # QgsDataItem interface: 56 | 57 | # pylint: disable=missing-docstring 58 | def hasDragEnabled(self): 59 | return True 60 | 61 | def mimeUri(self): 62 | u = QgsMimeDataUtils.Uri() 63 | u.layerType = "custom" 64 | u.providerKey = "cesium_ion" 65 | u.name = self.asset.name 66 | u.uri = str(self.asset.id) 67 | return u 68 | 69 | def mimeUris(self): # pylint: disable=missing-docstring 70 | return [self.mimeUri()] 71 | 72 | # pylint: enable=missing-docstring 73 | 74 | 75 | class IonRootItem(QgsDataCollectionItem): 76 | """ 77 | Root item for Cesium ion browser entries 78 | """ 79 | 80 | def __init__(self): 81 | super().__init__(None, 'Cesium ion', 'cesium_ion', 'cesium_ion') 82 | self.setCapabilitiesV2( 83 | Qgis.BrowserItemCapabilities( 84 | Qgis.BrowserItemCapability.Fertile 85 | ) 86 | ) 87 | 88 | self.setIcon(GuiUtils.get_icon('browser_root.svg')) 89 | 90 | # QgsDataCollectionItem interface 91 | 92 | # pylint: disable=missing-function-docstring 93 | def createChildren(self): 94 | assets = API_CLIENT.list_assets_blocking() 95 | res = [] 96 | for asset in assets: 97 | res.append(IonAssetItem(self, asset)) 98 | return res 99 | # pylint: enable=missing-function-docstring 100 | 101 | 102 | class CesiumIonDataItemProvider(QgsDataItemProvider): 103 | """ 104 | Data item provider for Cesium ion items in the QGIS browser 105 | """ 106 | 107 | # QgsDataItemProvider interface 108 | # pylint: disable=missing-function-docstring,unused-argument 109 | def name(self): 110 | return 'cesium_ion' 111 | 112 | def dataProviderKey(self): 113 | return 'cesium_ion' 114 | 115 | def capabilities(self): 116 | return QgsDataProvider.Dir 117 | 118 | def createDataItem(self, path, parentItem): 119 | if not path: 120 | return IonRootItem() 121 | 122 | return None 123 | # pylint: enable=missing-function-docstring,unused-argument 124 | 125 | 126 | class CesiumIonLayerUtils: 127 | """ 128 | Utilities for working with cesium ion QGIS map layers 129 | """ 130 | 131 | @staticmethod 132 | def add_asset_interactive(asset: Asset): 133 | """ 134 | Interactively allows users to add an asset to a project 135 | """ 136 | # pylint: disable=import-outside-toplevel 137 | from .add_asset_dialog import AddAssetDialog 138 | # pylint: enable=import-outside-toplevel 139 | 140 | dialog = AddAssetDialog() 141 | if not dialog.exec_(): 142 | return 143 | 144 | if dialog.existing_token(): 145 | CesiumIonLayerUtils.add_asset_with_token( 146 | asset, dialog.existing_token() 147 | ) 148 | else: 149 | new_token = API_CLIENT.create_token( 150 | dialog.new_token_name(), 151 | scopes=['assets:list', 'assets:read'], 152 | asset_ids=[int(asset.id)] 153 | ) 154 | if new_token: 155 | CesiumIonLayerUtils.add_asset_with_token( 156 | asset, new_token.token 157 | ) 158 | 159 | @staticmethod 160 | def add_asset_by_id_interactive(): 161 | """ 162 | Interactively allows users to add an asset by ID to a project 163 | """ 164 | # pylint: disable=import-outside-toplevel 165 | from .add_asset_dialog import AddAssetByIdDialog 166 | # pylint: enable=import-outside-toplevel 167 | 168 | dialog = AddAssetByIdDialog() 169 | if not dialog.exec_(): 170 | return 171 | 172 | asset = Asset( 173 | id=dialog.asset_id(), 174 | name='Unknown', 175 | type=AssetType.Tiles3D, 176 | status=Status.Complete 177 | ) 178 | CesiumIonLayerUtils.add_asset_with_token( 179 | asset, dialog.token() 180 | ) 181 | 182 | @staticmethod 183 | def add_asset_with_token(asset: Asset, token: str): 184 | """ 185 | Adds an asset with the specified token 186 | """ 187 | ds = asset.as_qgis_data_source(token) 188 | iface.addTiledSceneLayer( 189 | ds, asset.name, 'cesiumtiles' 190 | ) 191 | 192 | 193 | class CesiumIonDataItemGuiProvider(QgsDataItemGuiProvider): 194 | """ 195 | Data item GUI provider for Cesium ion items 196 | """ 197 | 198 | # QgsDataItemGuiProvider interface: 199 | 200 | # pylint: disable=missing-docstring,unused-argument 201 | def name(self): 202 | return 'cesium_ion' 203 | 204 | def handleDoubleClick(self, item, context): 205 | if not isinstance(item, IonAssetItem): 206 | return False 207 | 208 | CesiumIonLayerUtils.add_asset_interactive(item.asset) 209 | return True 210 | 211 | def tr(self, string, context=''): 212 | if context == '': 213 | context = self.__class__.__name__ 214 | return QCoreApplication.translate(context, string) 215 | 216 | def populateContextMenu(self, item, menu, selectedItems, context): 217 | if isinstance(item, IonAssetItem): 218 | add_to_project_action = QAction(self.tr('Add Asset to Project…'), 219 | menu) 220 | add_to_project_action.triggered.connect( 221 | partial(self._add_asset, item.asset)) 222 | menu.addAction(add_to_project_action) 223 | elif isinstance(item, IonRootItem): 224 | add_by_id_action = QAction(self.tr('Add Asset by ID…'), 225 | menu) 226 | add_by_id_action.triggered.connect(self._add_asset_by_id) 227 | menu.addAction(add_by_id_action) 228 | 229 | # pylint: enable=missing-docstring,unused-argument 230 | 231 | def _add_asset(self, asset: Asset): 232 | """ 233 | Adds an asset to the project 234 | """ 235 | CesiumIonLayerUtils.add_asset_interactive(asset) 236 | 237 | def _add_asset_by_id(self): 238 | """ 239 | Interactively adds an asset by ID 240 | """ 241 | CesiumIonLayerUtils.add_asset_by_id_interactive() 242 | 243 | 244 | class CesiumIonDropHandler(QgsCustomDropHandler): 245 | """ 246 | Custom drop handler for Cesium ion assets 247 | """ 248 | 249 | # QgsCustomDropHandler interface: 250 | 251 | # pylint: disable=missing-docstring 252 | def customUriProviderKey(self): 253 | return 'cesium_ion' 254 | 255 | def handleCustomUriDrop(self, uri): 256 | asset = Asset( 257 | id=uri.uri, 258 | name=uri.name, 259 | type=AssetType.Tiles3D, 260 | status=Status.Complete 261 | ) 262 | 263 | CesiumIonLayerUtils.add_asset_interactive(asset) 264 | 265 | # pylint: enable=missing-docstring 266 | -------------------------------------------------------------------------------- /cesium_ion/gui/gui_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | GUI Utilities 3 | """ 4 | 5 | import math 6 | import os 7 | import re 8 | from typing import Optional 9 | 10 | from qgis.PyQt.QtCore import Qt 11 | from qgis.PyQt.QtGui import ( 12 | QIcon, 13 | QFont, 14 | QFontMetrics, 15 | QImage, 16 | QPixmap, 17 | QFontDatabase, 18 | QColor, 19 | QPainter 20 | ) 21 | from qgis.PyQt.QtSvg import QSvgRenderer 22 | from qgis.PyQt.QtWidgets import ( 23 | QMenu 24 | ) 25 | from qgis.core import ( 26 | Qgis 27 | ) 28 | from qgis.utils import iface 29 | 30 | FONT_FAMILIES = "" 31 | 32 | FELT_STYLESHEET = """ 33 | QDialog { 34 | background-color: #ececec; 35 | color: black; 36 | } 37 | QLabel { 38 | color: black !important; 39 | } 40 | 41 | QPushButton { 42 | background: solid #3d521e; 43 | color: white; 44 | } 45 | 46 | QLineEdit { 47 | background: solid white; 48 | color: black; 49 | } 50 | 51 | QProgressBar { 52 | background: solid white; 53 | } 54 | 55 | """ 56 | 57 | 58 | class GuiUtils: 59 | """ 60 | Utilities for GUI plugin components 61 | """ 62 | 63 | APPLICATION_FONT_MAP = {} 64 | 65 | @staticmethod 66 | def set_link_color(html: str, 67 | wrap_color=True, 68 | color: Optional[QColor] = None) -> str: 69 | """ 70 | Adds style tags to links in a HTML string for the standard link color 71 | """ 72 | if color: 73 | color_string = color.name() 74 | else: 75 | color_string = 'rgba(0,0,0,.3)' 76 | res = re.sub(r'(', 77 | r'\1 style="color: {};">'.format(color_string), 78 | html) 79 | if wrap_color: 80 | res = '{}'.format(color_string, 81 | res) 82 | return res 83 | 84 | @staticmethod 85 | def get_icon(icon: str) -> QIcon: 86 | """ 87 | Returns a plugin icon 88 | :param icon: icon name (svg file name) 89 | :return: QIcon 90 | """ 91 | path = GuiUtils.get_icon_svg(icon) 92 | if not path: 93 | return QIcon() 94 | 95 | return QIcon(path) 96 | 97 | @staticmethod 98 | def get_icon_svg(icon: str) -> str: 99 | """ 100 | Returns a plugin icon's SVG file path 101 | :param icon: icon name (svg file name) 102 | :return: icon svg path 103 | """ 104 | path = os.path.join( 105 | os.path.dirname(__file__), 106 | '..', 107 | 'icons', 108 | icon) 109 | if not os.path.exists(path): 110 | return '' 111 | 112 | return path 113 | 114 | @staticmethod 115 | def get_icon_pixmap(icon: str) -> QPixmap: 116 | """ 117 | Returns a plugin icon's PNG file path 118 | :param icon: icon name (png file name) 119 | :return: icon png path 120 | """ 121 | path = os.path.join( 122 | os.path.dirname(__file__), 123 | '..', 124 | 'icons', 125 | icon) 126 | if not os.path.exists(path): 127 | return QPixmap() 128 | 129 | im = QImage(path) 130 | return QPixmap.fromImage(im) 131 | 132 | @staticmethod 133 | def get_svg_as_image(icon: str, width: int, height: int, 134 | background_color: Optional[QColor] = None, 135 | device_pixel_ratio: float = 1) -> QImage: 136 | """ 137 | Returns an SVG returned as an image 138 | """ 139 | path = GuiUtils.get_icon_svg(icon) 140 | if not os.path.exists(path): 141 | return QImage() 142 | 143 | renderer = QSvgRenderer(path) 144 | image = QImage(int(width * device_pixel_ratio), 145 | int(height * device_pixel_ratio), 146 | QImage.Format_ARGB32) 147 | image.setDevicePixelRatio(device_pixel_ratio) 148 | if not background_color: 149 | image.fill(Qt.transparent) 150 | else: 151 | image.fill(background_color) 152 | 153 | painter = QPainter(image) 154 | painter.scale(1 / device_pixel_ratio, 155 | 1 / device_pixel_ratio) 156 | renderer.render(painter) 157 | painter.end() 158 | 159 | return image 160 | 161 | @staticmethod 162 | def get_ui_file_path(file: str) -> str: 163 | """ 164 | Returns a UI file's path 165 | :param file: file name (uifile name) 166 | :return: ui file path 167 | """ 168 | path = os.path.join( 169 | os.path.dirname(__file__), 170 | '..', 171 | 'ui', 172 | file) 173 | if not os.path.exists(path): 174 | return '' 175 | 176 | return path 177 | 178 | @staticmethod 179 | def scale_icon_size(standard_size: int) -> int: 180 | """ 181 | Scales an icon size accounting for device DPI 182 | """ 183 | fm = QFontMetrics((QFont())) 184 | scale = 1.1 * standard_size / 24.0 185 | return int(math.floor(max(Qgis.UI_SCALE_FACTOR * fm.height() * scale, 186 | float(standard_size)))) 187 | 188 | @staticmethod 189 | def get_default_font() -> QFont: 190 | """ 191 | Returns the best font match for the Koordinates default font 192 | families which is available on the system 193 | """ 194 | for family in FONT_FAMILIES.split(','): 195 | family_cleaned = re.match(r'^\s*\'?(.*?)\'?\s*$', family).group(1) 196 | font = QFont(family_cleaned) 197 | if font.exactMatch(): 198 | return font 199 | 200 | return QFont() 201 | 202 | @staticmethod 203 | def get_font_path(font: str) -> str: 204 | """ 205 | Returns the path to an included font file 206 | :param font: font name 207 | :return: font file path 208 | """ 209 | path = os.path.join( 210 | os.path.dirname(__file__), 211 | '..', 212 | 'fonts', 213 | font) 214 | if not os.path.exists(path): 215 | return '' 216 | 217 | return path 218 | 219 | @staticmethod 220 | def get_embedded_font(font: str) -> QFont: 221 | """ 222 | Returns a font created from an embedded font file 223 | """ 224 | if font in GuiUtils.APPLICATION_FONT_MAP: 225 | return GuiUtils.APPLICATION_FONT_MAP[font] 226 | 227 | path = GuiUtils.get_font_path(font) 228 | if not path: 229 | return QFont() 230 | 231 | res = QFontDatabase.addApplicationFont(path) 232 | families = QFontDatabase.applicationFontFamilies(res) 233 | installed_font = QFont(families[0]) 234 | GuiUtils.APPLICATION_FONT_MAP[font] = installed_font 235 | return installed_font 236 | 237 | @staticmethod 238 | def get_project_import_export_menu() -> Optional[QMenu]: 239 | """ 240 | Returns the application Project - Import/Export sub menu 241 | """ 242 | try: 243 | # requires QGIS 3.30+ 244 | return iface.projectImportExportMenu() 245 | except AttributeError: 246 | pass 247 | 248 | project_menu = iface.projectMenu() 249 | matches = [m for m in project_menu.children() 250 | if m.objectName() == 'menuImport_Export'] 251 | if matches: 252 | return matches[0] 253 | 254 | return None 255 | -------------------------------------------------------------------------------- /cesium_ion/gui/select_token_widget.py: -------------------------------------------------------------------------------- 1 | """ 2 | Select token widget 3 | """ 4 | from typing import Optional 5 | 6 | from qgis.PyQt import uic 7 | from qgis.PyQt.QtCore import ( 8 | pyqtSignal 9 | ) 10 | from qgis.PyQt.QtWidgets import ( 11 | QWidget 12 | ) 13 | 14 | from qgis.core import ( 15 | QgsNetworkAccessManager 16 | ) 17 | 18 | from .gui_utils import GuiUtils 19 | from ..core import API_CLIENT 20 | 21 | WIDGET, _ = uic.loadUiType(GuiUtils.get_ui_file_path('select_token.ui')) 22 | 23 | 24 | class SelectTokenWidget(QWidget, WIDGET): 25 | """ 26 | Custom widget for selecting access tokens 27 | """ 28 | 29 | is_valid_changed = pyqtSignal(bool) 30 | 31 | def __init__(self, # pylint: disable=too-many-statements 32 | parent: Optional[QWidget] = None): 33 | super().__init__(parent) 34 | self.setupUi(self) 35 | 36 | self.radio_existing.setChecked(True) 37 | self.widget_new.setEnabled(False) 38 | self.widget_existing.setEnabled(True) 39 | self.widget_manual.setEnabled(False) 40 | 41 | self.radio_new.toggled.connect(self.widget_new.setEnabled) 42 | self.radio_existing.toggled.connect(self.widget_existing.setEnabled) 43 | self.radio_manual.toggled.connect(self.widget_manual.setEnabled) 44 | 45 | self.radio_new.toggled.connect(self._validate) 46 | self.radio_existing.toggled.connect(self._validate) 47 | self.radio_manual.toggled.connect(self._validate) 48 | self.edit_new_token_name.textChanged.connect(self._validate) 49 | self.combo_existing.currentIndexChanged.connect(self._validate) 50 | self.edit_manual.textChanged.connect(self._validate) 51 | 52 | req = API_CLIENT.list_tokens_request() 53 | self._list_tokens_reply = QgsNetworkAccessManager.instance().get(req) 54 | self._list_tokens_reply.finished.connect(self._reply_finished) 55 | 56 | def _reply_finished(self): 57 | """ 58 | Called when the list tokens reply is finished 59 | """ 60 | tokens = API_CLIENT.parse_list_tokens_reply(self._list_tokens_reply) 61 | for token in tokens: 62 | self.combo_existing.addItem(token.name, token.token) 63 | 64 | if not tokens: 65 | self.radio_existing.setEnabled(False) 66 | if self.radio_existing.isChecked(): 67 | self.radio_new.setChecked(True) 68 | 69 | def _validate(self): 70 | """ 71 | Validates the current settings 72 | """ 73 | self.is_valid_changed.emit(self.is_valid()) 74 | 75 | def is_valid(self) -> bool: 76 | """ 77 | Returns True if the settings are valid 78 | """ 79 | if self.radio_new.isChecked(): 80 | return bool(self.edit_new_token_name.text()) 81 | 82 | if self.radio_existing.isChecked(): 83 | return bool(self.combo_existing.currentData()) 84 | 85 | if self.radio_manual.isChecked(): 86 | return bool(self.edit_manual.text()) 87 | 88 | return False 89 | 90 | def existing_token(self) -> Optional[str]: 91 | """ 92 | Returns the selected existing token 93 | """ 94 | if self.radio_existing.isChecked(): 95 | return self.combo_existing.currentData() 96 | 97 | if self.radio_manual.isChecked(): 98 | return self.edit_manual.text() 99 | 100 | return None 101 | 102 | def new_token_name(self) -> Optional[str]: 103 | """ 104 | Returns the name for the new token 105 | """ 106 | if self.radio_new.isChecked(): 107 | return self.edit_new_token_name.text() 108 | 109 | return None 110 | -------------------------------------------------------------------------------- /cesium_ion/i18n/af.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/north-road/cesium-ion-plugin/d0f5b7596066708ce4ad79fc4baa8a490b8970cb/cesium_ion/i18n/af.qm -------------------------------------------------------------------------------- /cesium_ion/i18n/af.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @default 5 | 6 | 7 | Good morning 8 | Goeie more 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /cesium_ion/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 52 | 53 | 55 | 59 | 66 | 69 | 71 | 77 | 78 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /cesium_ion/icons/browser_root.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 52 | 53 | 55 | 59 | 66 | 75 | 76 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /cesium_ion/icons/cesium_3d_tile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cesium_ion/metadata.txt: -------------------------------------------------------------------------------- 1 | # This file contains metadata for your plugin. Since 2 | # version 2.0 of QGIS this is the proper way to supply 3 | # information about a plugin. The old method of 4 | # embedding metadata in __init__.py will 5 | # is no longer supported since version 2.0. 6 | 7 | # This file should be included when you package your plugin. 8 | # Mandatory items: 9 | 10 | [general] 11 | name=Cesium ion 12 | qgisMinimumVersion=3.33 13 | description=Browse and add datasets from Cesium ion 14 | version=1.0.2 15 | author=North Road 16 | email=info@north-road.com 17 | 18 | about=Browse and add your assets from the Cesium ion platform. 19 | 20 | tracker=https://github.com/north-road/cesium-ion-plugin/issues 21 | repository=https://github.com/north-road/cesium-ion-plugin 22 | # End of mandatory metadata 23 | 24 | # Recommended items: 25 | 26 | # Uncomment the following line and add your changelog: 27 | changelog=1.0.2 Fix compatibility with QGIS 3.36 28 | 29 | # Tags are comma separated with spaces allowed 30 | tags=tiled,3d,tiles,mesh,cesium,ion,b3dm,gltf,scenes 31 | 32 | homepage=https://github.com/north-road/cesium-ion-plugin 33 | category=Plugins 34 | icon=icon.svg 35 | # experimental flag 36 | experimental=False 37 | 38 | # deprecated flag (applies to the whole plugin, not just a single version) 39 | deprecated=False 40 | -------------------------------------------------------------------------------- /cesium_ion/plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cesium ion QGIS plugin 3 | """ 4 | import os 5 | from typing import Optional 6 | 7 | from qgis.PyQt import sip 8 | from qgis.PyQt.QtCore import ( 9 | QObject, 10 | QCoreApplication 11 | ) 12 | from qgis.PyQt.QtWidgets import ( 13 | QPushButton, 14 | QMessageBox 15 | ) 16 | from qgis.core import ( 17 | Qgis, 18 | QgsApplication, 19 | QgsAuthMethodConfig 20 | ) 21 | from qgis.gui import ( 22 | QgsGui, 23 | QgisInterface, 24 | QgsMessageBarItem 25 | ) 26 | 27 | from .core import API_CLIENT 28 | from .gui import ( 29 | CesiumIonDataItemProvider, 30 | CesiumIonDataItemGuiProvider, 31 | CesiumIonDropHandler 32 | ) 33 | 34 | 35 | class CesiumIonPlugin(QObject): 36 | """ 37 | Felt QGIS plugin 38 | """ 39 | 40 | def __init__(self, iface: QgisInterface): 41 | super().__init__() 42 | self.iface: QgisInterface = iface 43 | 44 | self.data_item_provider: Optional[CesiumIonDataItemProvider] = None 45 | self.data_item_gui_provider: Optional[CesiumIonDataItemGuiProvider] = \ 46 | None 47 | self.drop_handler: Optional[CesiumIonDropHandler] = None 48 | 49 | self._current_message_bar_item: Optional[QgsMessageBarItem] = None 50 | 51 | # qgis plugin interface 52 | # pylint: disable=missing-function-docstring 53 | 54 | def initGui(self): 55 | if not self._create_oauth_config(): 56 | return 57 | 58 | self.data_item_provider = CesiumIonDataItemProvider() 59 | QgsApplication.dataItemProviderRegistry().addProvider( 60 | self.data_item_provider 61 | ) 62 | 63 | self.data_item_gui_provider = CesiumIonDataItemGuiProvider() 64 | QgsGui.dataItemGuiProviderRegistry().addProvider( 65 | self.data_item_gui_provider 66 | ) 67 | 68 | self.drop_handler = CesiumIonDropHandler() 69 | self.iface.registerCustomDropHandler(self.drop_handler) 70 | 71 | def unload(self): 72 | if self.data_item_gui_provider and \ 73 | not sip.isdeleted(self.data_item_gui_provider): 74 | QgsGui.dataItemGuiProviderRegistry().removeProvider( 75 | self.data_item_gui_provider 76 | ) 77 | self.data_item_gui_provider = None 78 | 79 | if self.data_item_provider and \ 80 | not sip.isdeleted(self.data_item_provider): 81 | QgsApplication.dataItemProviderRegistry().removeProvider( 82 | self.data_item_provider 83 | ) 84 | self.data_item_provider = None 85 | 86 | self.iface.unregisterCustomDropHandler(self.drop_handler) 87 | self.drop_handler = None 88 | 89 | # pylint: enable=missing-function-docstring 90 | 91 | @staticmethod 92 | def tr(message): 93 | """Get the translation for a string using Qt translation API. 94 | 95 | We implement this ourselves since we do not inherit QObject. 96 | 97 | :param message: String for translation. 98 | :type message: str, QString 99 | 100 | :returns: Translated version of message. 101 | :rtype: QString 102 | """ 103 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass 104 | return QCoreApplication.translate('Cesium ION', message) 105 | 106 | def _configure_auth(self): 107 | """ 108 | Walks user through configuring QGIS authentication system 109 | """ 110 | if (self._current_message_bar_item and 111 | not sip.isdeleted(self._current_message_bar_item)): 112 | self.iface.messageBar().popWidget(self._current_message_bar_item) 113 | self._current_message_bar_item = None 114 | 115 | QMessageBox.information( 116 | self.iface.mainWindow(), 117 | self.tr('Configure QGIS Authentication'), 118 | self.tr('In order to securely store access credentials, ' 119 | 'the Cesium ion plugin requires use of the QGIS ' 120 | 'Authentication database. This has not been setup for ' 121 | 'this QGIS user profile.\n\n' 122 | 'On the next screen you\'ll be prompted for a master ' 123 | 'password which will be used to secure the QGIS ' 124 | 'Authentication system. Please enter a secure password ' 125 | 'and store this safely!\n\n' 126 | '(This password will not be accessible to the Cesium ion ' 127 | 'plugin, and will never be shared with the Cesium ion ' 128 | 'service. Don\'t use the same password as you use for ' 129 | 'ion!)'), 130 | QMessageBox.Ok 131 | ) 132 | 133 | QgsApplication.authManager().setMasterPassword() 134 | # remove unwanted message 135 | for item in self.iface.messageBar().items(): 136 | if item.text().startswith('Retrieving password from your ' 137 | 'Wallet/KeyRing failed'): 138 | self.iface.messageBar().popWidget(item) 139 | break 140 | 141 | self.initGui() 142 | 143 | def _create_oauth_config(self) -> bool: 144 | """ 145 | Creates the Cesium ion oauth config, if it doesn't already exist. 146 | 147 | Returns True if the oauth config is ready to use 148 | """ 149 | if not QgsApplication.authManager().masterPasswordHashInDatabase() or \ 150 | not QgsApplication.authManager().setMasterPassword(True): 151 | 152 | message_widget = self.iface.messageBar().createMessage( 153 | self.tr('Cesium ion'), 154 | self.tr( 155 | 'QGIS authentication system not available -- ' 156 | 'please configure') 157 | ) 158 | details_button = QPushButton(self.tr("Configure")) 159 | details_button.clicked.connect(self._configure_auth) 160 | message_widget.layout().addWidget(details_button) 161 | self._current_message_bar_item = ( 162 | self.iface.messageBar().pushWidget(message_widget, 163 | Qgis.MessageLevel.Warning, 164 | 0)) 165 | return False 166 | 167 | config = QgsAuthMethodConfig(method='OAuth2') 168 | config.setName('Cesium ion') 169 | config.setId(API_CLIENT.OAUTH_ID) 170 | 171 | config_json_path = os.path.join( 172 | os.path.dirname(__file__), 173 | 'resources', 174 | 'auth_cfg.json') 175 | 176 | with open(config_json_path, 'rt', encoding='utf8') as config_json_file: 177 | config_json = config_json_file.read() 178 | 179 | config.setConfig('oauth2config', config_json) 180 | 181 | if API_CLIENT.OAUTH_ID in QgsApplication.authManager().configIds(): 182 | QgsApplication.authManager().updateAuthenticationConfig(config) 183 | else: 184 | QgsApplication.authManager().storeAuthenticationConfig(config) 185 | 186 | return True 187 | -------------------------------------------------------------------------------- /cesium_ion/resources/auth_cfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessMethod": 0, 3 | "apiKey": "", 4 | "clientId": "510", 5 | "clientSecret": "", 6 | "configType": 1, 7 | "customHeader": "", 8 | "description": "", 9 | "grantFlow": 3, 10 | "id": "cesiion", 11 | "name": "Cesium ion", 12 | "objectName": "", 13 | "password": "", 14 | "persistToken": false, 15 | "queryPairs": { 16 | }, 17 | "redirectPort": 7070, 18 | "redirectUrl": "qgis/oauth2/callback", 19 | "refreshTokenUrl": "", 20 | "requestTimeout": 30, 21 | "requestUrl": "https://ion.cesium.com/oauth", 22 | "scope": "assets:read assets:list tokens:write tokens:read", 23 | "tokenUrl": "https://api.cesium.com/oauth/token", 24 | "username": "", 25 | "version": 1 26 | } 27 | -------------------------------------------------------------------------------- /cesium_ion/ui/asset_by_id.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AssetByIdWidgetBase 4 | 5 | 6 | 7 | 0 8 | 0 9 | 321 10 | 301 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | Asset ID 33 | 34 | 35 | 36 | 37 | 38 | 39 | Qt::Vertical 40 | 41 | 42 | 43 | 20 44 | 40 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Access token 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Enter the details for the Cesium ion asset to add 66 | 67 | 68 | false 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 0 77 | 78 | 79 | 0 80 | 81 | 82 | 0 83 | 84 | 85 | 0 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /cesium_ion/ui/select_token.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SelectTokenWidgetBase 4 | 5 | 6 | 7 | 0 8 | 0 9 | 321 10 | 301 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 0 40 | 41 | 42 | 0 43 | 44 | 45 | 46 | 47 | Qt::Horizontal 48 | 49 | 50 | QSizePolicy::Fixed 51 | 52 | 53 | 54 | 10 55 | 20 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Token 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | Specify a token 77 | 78 | 79 | 80 | 81 | 82 | 83 | Select the Cesium ion token to use for this asset: 84 | 85 | 86 | false 87 | 88 | 89 | 90 | 91 | 92 | 93 | Use an existing token 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 0 102 | 103 | 104 | 0 105 | 106 | 107 | 0 108 | 109 | 110 | 0 111 | 112 | 113 | 114 | 115 | Qt::Horizontal 116 | 117 | 118 | QSizePolicy::Fixed 119 | 120 | 121 | 122 | 10 123 | 20 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 0 139 | 140 | 141 | 0 142 | 143 | 144 | 0 145 | 146 | 147 | 0 148 | 149 | 150 | 151 | 152 | Qt::Horizontal 153 | 154 | 155 | QSizePolicy::Fixed 156 | 157 | 158 | 159 | 13 160 | 20 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | Name 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | Create a new token 182 | 183 | 184 | 185 | 186 | 187 | 188 | Qt::Vertical 189 | 190 | 191 | 192 | 20 193 | 40 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | Cesium for QGIS embeds a Cesium ion token in your project in order to allow it to access the assets you add to your maps. 202 | 203 | 204 | true 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /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 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS,script_editor 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins= 20 | 21 | 22 | [MESSAGES CONTROL] 23 | 24 | # Enable the message, report, category or checker with the given id(s). You can 25 | # either give multiple identifier separated by comma (,) or put this option 26 | # multiple time. See also the "--disable" option for examples. 27 | #enable= 28 | 29 | # Disable the message, report, category or checker with the given id(s). You 30 | # can either give multiple identifiers separated by comma (,) or put this 31 | # option multiple times (only on the command line, not in the configuration 32 | # file where it should appear only once).You can also use "--disable=all" to 33 | # disable everything first and then reenable specific checks. For example, if 34 | # you want to run only the similarities checker, you can use "--disable=all 35 | # --enable=similarities". If you want to run only the classes checker, but have 36 | # no Warning level messages displayed, use"--disable=all --enable=classes 37 | # --disable=W" 38 | # see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them 39 | disable=locally-disabled,C0103,no-name-in-module,duplicate-code,import-error,line-too-long,too-many-arguments,too-many-instance-attributes,no-self-use,too-few-public-methods,bad-option-value,fixme,consider-using-f-string,cyclic-import 40 | 41 | 42 | [REPORTS] 43 | 44 | # Set the output format. Available formats are text, parseable, colorized, msvs 45 | # (visual studio) and html. You can also give a reporter class, eg 46 | # mypackage.mymodule.MyReporterClass. 47 | output-format=text 48 | 49 | # Tells whether to display a full report or only the messages 50 | reports=yes 51 | 52 | # Python expression which should return a note less than 10 (10 is the highest 53 | # note). You have access to the variables errors warning, statement which 54 | # respectively contain the number of errors / warnings messages and the total 55 | # number of statements analyzed. This is used by the global evaluation report 56 | # (RP0004). 57 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 58 | 59 | # Template used to display messages. This is a python new-style format string 60 | # used to format the message information. See doc for all details 61 | #msg-template= 62 | 63 | 64 | [BASIC] 65 | 66 | # Regular expression which should only match correct module names 67 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 68 | 69 | # Regular expression which should only match correct module level names 70 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 71 | 72 | # Regular expression which should only match correct class names 73 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 74 | 75 | # Regular expression which should only match correct function names 76 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 77 | 78 | # Regular expression which should only match correct method names 79 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 80 | 81 | # Regular expression which should only match correct instance attribute names 82 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 83 | 84 | # Regular expression which should only match correct argument names 85 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 86 | 87 | # Regular expression which should only match correct variable names 88 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 89 | 90 | # Regular expression which should only match correct attribute names in class 91 | # bodies 92 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 93 | 94 | # Regular expression which should only match correct list comprehension / 95 | # generator expression variable names 96 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 97 | 98 | # Good variable names which should always be accepted, separated by a comma 99 | good-names=i,j,k,ex,Run,_ 100 | 101 | # Bad variable names which should always be refused, separated by a comma 102 | bad-names=foo,bar,baz,toto,tutu,tata 103 | 104 | # Regular expression which should only match function or class names that do 105 | # not require a docstring. 106 | no-docstring-rgx=__.*__ 107 | 108 | # Minimum line length for functions/classes that require docstrings, shorter 109 | # ones are exempt. 110 | docstring-min-length=-1 111 | 112 | 113 | [MISCELLANEOUS] 114 | 115 | # List of note tags to take in consideration, separated by a comma. 116 | notes=FIXME,XXX,TODO 117 | 118 | 119 | [TYPECHECK] 120 | 121 | # Tells whether missing members accessed in mixin class should be ignored. A 122 | # mixin class is detected if its name ends with "mixin" (case insensitive). 123 | ignore-mixin-members=yes 124 | 125 | # List of classes names for which member attributes should not be checked 126 | # (useful for classes with attributes dynamically set). 127 | ignored-classes=SQLObject 128 | 129 | # List of members which are set dynamically and missed by pylint inference 130 | # system, and so shouldn't trigger E0201 when accessed. Python regular 131 | # expressions are accepted. 132 | generated-members=REQUEST,acl_users,aq_parent 133 | 134 | 135 | [VARIABLES] 136 | 137 | # Tells whether we should check for unused import in __init__ files. 138 | init-import=no 139 | 140 | # A regular expression matching the beginning of the name of dummy variables 141 | # (i.e. not used). 142 | dummy-variables-rgx=_$|dummy 143 | 144 | # List of additional names supposed to be defined in builtins. Remember that 145 | # you should avoid to define new builtins when possible. 146 | additional-builtins= 147 | 148 | 149 | [FORMAT] 150 | 151 | # Maximum number of characters on a single line. 152 | max-line-length=80 153 | 154 | # Regexp for a line that is allowed to be longer than the limit. 155 | ignore-long-lines=^\s*(# )??$ 156 | 157 | # Allow the body of an if to be on the same line as the test if there is no 158 | # else. 159 | single-line-if-stmt=no 160 | 161 | # Maximum number of lines in a module 162 | max-module-lines=1000 163 | 164 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 165 | # tab). 166 | indent-string=' ' 167 | 168 | 169 | [SIMILARITIES] 170 | 171 | # Minimum lines number of a similarity. 172 | min-similarity-lines=4 173 | 174 | # Ignore comments when computing similarities. 175 | ignore-comments=yes 176 | 177 | # Ignore docstrings when computing similarities. 178 | ignore-docstrings=yes 179 | 180 | # Ignore imports when computing similarities. 181 | ignore-imports=no 182 | 183 | 184 | [IMPORTS] 185 | 186 | # Deprecated modules which should not be used, separated by a comma 187 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 188 | 189 | # Create a graph of every (i.e. internal and external) dependencies in the 190 | # given file (report RP0402 must not be disabled) 191 | import-graph= 192 | 193 | # Create a graph of external dependencies in the given file (report RP0402 must 194 | # not be disabled) 195 | ext-import-graph= 196 | 197 | # Create a graph of internal dependencies in the given file (report RP0402 must 198 | # not be disabled) 199 | int-import-graph= 200 | 201 | 202 | [DESIGN] 203 | 204 | # Maximum number of arguments for function / method 205 | max-args=5 206 | 207 | # Argument names that match this expression will be ignored. Default to name 208 | # with leading underscore 209 | ignored-argument-names=_.* 210 | 211 | # Maximum number of locals for function / method body 212 | max-locals=15 213 | 214 | # Maximum number of return / yield for function / method body 215 | max-returns=6 216 | 217 | # Maximum number of branch for function / method body 218 | max-branches=12 219 | 220 | # Maximum number of statements in function / method body 221 | max-statements=50 222 | 223 | # Maximum number of parents for a class (see R0901). 224 | max-parents=7 225 | 226 | # Maximum number of attributes for a class (see R0902). 227 | max-attributes=7 228 | 229 | # Minimum number of public methods for a class (see R0903). 230 | min-public-methods=2 231 | 232 | # Maximum number of public methods for a class (see R0904). 233 | max-public-methods=21 234 | 235 | 236 | [CLASSES] 237 | 238 | # List of method names used to declare (i.e. assign) instance attributes. 239 | defining-attr-methods=__init__,__new__,setUp 240 | 241 | # List of valid names for the first argument in a class method. 242 | valid-classmethod-first-arg=cls 243 | 244 | # List of valid names for the first argument in a metaclass class method. 245 | valid-metaclass-classmethod-first-arg=mcs 246 | 247 | -------------------------------------------------------------------------------- /requirements/packaging.txt: -------------------------------------------------------------------------------- 1 | qgis-plugin-ci>=2.7.0 2 | -------------------------------------------------------------------------------- /requirements/testing.txt: -------------------------------------------------------------------------------- 1 | # For tests execution: 2 | deepdiff 3 | mock 4 | flake8 5 | pep257 6 | -------------------------------------------------------------------------------- /scripts/compile-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LRELEASE=$1 3 | LOCALES=$2 4 | 5 | 6 | for LOCALE in ${LOCALES} 7 | do 8 | echo "Processing: ${LOCALE}.ts" 9 | # Note we don't use pylupdate with qt .pro file approach as it is flakey 10 | # about what is made available. 11 | $LRELEASE cesium_ion/i18n/${LOCALE}.ts 12 | done 13 | -------------------------------------------------------------------------------- /scripts/run-env-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | QGIS_PREFIX_PATH=/usr/local/qgis-3.0 4 | if [ -n "$1" ]; then 5 | QGIS_PREFIX_PATH=$1 6 | fi 7 | 8 | echo ${QGIS_PREFIX_PATH} 9 | 10 | 11 | export QGIS_PREFIX_PATH=${QGIS_PREFIX_PATH} 12 | export QGIS_PATH=${QGIS_PREFIX_PATH} 13 | export LD_LIBRARY_PATH=${QGIS_PREFIX_PATH}/lib 14 | export PYTHONPATH=${QGIS_PREFIX_PATH}/share/qgis/python:${QGIS_PREFIX_PATH}/python:${QGIS_PREFIX_PATH}/python/plugins:${PYTHONPATH} 15 | 16 | echo "QGIS PATH: $QGIS_PREFIX_PATH" 17 | export QGIS_DEBUG=0 18 | export QGIS_LOG_FILE=/tmp/redistrict/realtime/logs/qgis.log 19 | 20 | export PATH=${QGIS_PREFIX_PATH}/bin:$PATH 21 | 22 | echo "This script is intended to be sourced to set up your shell to" 23 | echo "use a QGIS 3.0 built in $QGIS_PREFIX_PATH" 24 | echo 25 | echo "To use it do:" 26 | echo "source $BASH_SOURCE /your/optional/install/path" 27 | echo 28 | echo "Then use the make file supplied here e.g. make guitest" 29 | -------------------------------------------------------------------------------- /scripts/update-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LOCALES=$* 3 | 4 | PYLUPDATE=${PYLUPDATE:-pylupdate5} 5 | 6 | # Get newest .py files so we don't update strings unnecessarily 7 | 8 | CHANGED_FILES=0 9 | PYTHON_FILES=`find . -regex ".*\(ui\|py\)$" -type f` 10 | for PYTHON_FILE in $PYTHON_FILES 11 | do 12 | CHANGED=$(stat -c %Y $PYTHON_FILE) 13 | if [ ${CHANGED} -gt ${CHANGED_FILES} ] 14 | then 15 | CHANGED_FILES=${CHANGED} 16 | fi 17 | done 18 | 19 | # Qt translation stuff 20 | # for .ts file 21 | UPDATE=false 22 | for LOCALE in ${LOCALES} 23 | do 24 | TRANSLATION_FILE="cesium_ion/i18n/$LOCALE.ts" 25 | if [ ! -f ${TRANSLATION_FILE} ] 26 | then 27 | # Force translation string collection as we have a new language file 28 | touch ${TRANSLATION_FILE} 29 | UPDATE=true 30 | break 31 | fi 32 | 33 | MODIFICATION_TIME=$(stat -c %Y ${TRANSLATION_FILE}) 34 | if [ ${CHANGED_FILES} -gt ${MODIFICATION_TIME} ] 35 | then 36 | # Force translation string collection as a .py file has been updated 37 | UPDATE=true 38 | break 39 | fi 40 | done 41 | 42 | if [ ${UPDATE} == true ] 43 | # retrieve all python files 44 | then 45 | print ${PYTHON_FILES} 46 | # update .ts 47 | echo "Please provide translations by editing the translation files below:" 48 | for LOCALE in ${LOCALES} 49 | do 50 | echo "cesium_ion/i18n/"${LOCALE}".ts" 51 | # Note we don't use pylupdate with qt .pro file approach as it is flakey 52 | # about what is made available. 53 | ${PYLUPDATE} -noobsolete ${PYTHON_FILES} -ts cesium_ion/i18n/${LOCALE}.ts 54 | done 55 | else 56 | echo "No need to edit any translation files (.ts) because no python files" 57 | echo "has been updated since the last update translation. " 58 | fi 59 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [qgis-plugin-ci] 2 | plugin_path = cesium_ion 3 | github_organization_slug = north-road 4 | project_slug = cesium-ion-plugin 5 | --------------------------------------------------------------------------------