├── tianditu_tools
├── widgets
│ ├── __init__.py
│ ├── AddMap
│ │ ├── __init__.py
│ │ ├── utils.py
│ │ ├── extra_map.py
│ │ ├── main.py
│ │ └── sd.py
│ ├── FitZoom
│ │ ├── __init__.py
│ │ └── main.py
│ ├── Search
│ │ ├── __init__.py
│ │ └── main.py
│ ├── Setting
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── mapmanager.py
│ │ └── dialog.py
│ ├── icons.py
│ └── toolbar.py
├── images
│ ├── icon.png
│ ├── map_icons
│ │ ├── ESRI.webp
│ │ ├── osm.webp
│ │ ├── stamen_toner.webp
│ │ ├── stamen_terrain.webp
│ │ ├── googlemap_default.png
│ │ ├── googlemap_terrain.png
│ │ ├── stamen_watercolor.webp
│ │ ├── googlemap_satellite.png
│ │ ├── amap.svg
│ │ └── default.svg
│ ├── fitzoom.svg
│ ├── search.svg
│ ├── add_map.svg
│ ├── setting.svg
│ ├── earth.svg
│ └── map_tianditu.svg
├── __init__.py
├── maps
│ ├── summary.yml
│ └── extra.yml
├── plugin.py
├── qgis_utils.py
├── Styles
│ ├── terrain.qml
│ └── PointStyle_316.qml
├── metadata.txt
├── compat.py
├── utils.py
└── ui
│ ├── sd.ui
│ ├── sd.py
│ ├── search.ui
│ ├── sd_6.py
│ ├── search.py
│ ├── search_6.py
│ ├── setting.py
│ ├── setting.ui
│ └── setting_6.py
├── docs
├── notes.md
├── images
│ ├── 菜单.png
│ ├── 设置.png
│ ├── 地名搜索.webp
│ ├── 地点标记.jpg
│ ├── 地理编码查询.webp
│ ├── 逆地理编码查询.webp
│ └── PixPin_2024-01-10_16-13-41.png
├── public
│ ├── logo.png
│ ├── favicon.ico
│ └── logo.svg
├── .vitepress
│ ├── theme
│ │ ├── index.js
│ │ └── custom.css
│ └── config.mjs
├── 天地图Web服务API.md
├── 通过 XYZ Tiles 添加天地图.md
├── index.md
├── intro.md
└── 天地图省级节点加载.md
├── .pylintrc
├── pyproject.toml
├── package.json
├── .github
├── ISSUE_TEMPLATE
│ ├── 02_feature_request.yml
│ └── 01_report_bugs.yml
└── workflows
│ ├── commit.yml
│ ├── pylint.yml
│ └── release.yml
├── update_ui.py
├── README.md
├── pack.py
├── .gitignore
└── uv.lock
/tianditu_tools/widgets/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/notes.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # Notes
6 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/AddMap/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import AddMapBtn
2 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/FitZoom/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import FitZoomAction
2 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/Search/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import SearchAction
2 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/Setting/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import SettingAction
2 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MESSAGES CONTROL]
2 | disable = E0401,C0103,C0114,C0115,C0116,R0903,R0914
3 |
--------------------------------------------------------------------------------
/docs/images/菜单.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/images/菜单.png
--------------------------------------------------------------------------------
/docs/images/设置.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/images/设置.png
--------------------------------------------------------------------------------
/docs/images/地名搜索.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/images/地名搜索.webp
--------------------------------------------------------------------------------
/docs/images/地点标记.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/images/地点标记.jpg
--------------------------------------------------------------------------------
/docs/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/public/logo.png
--------------------------------------------------------------------------------
/docs/images/地理编码查询.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/images/地理编码查询.webp
--------------------------------------------------------------------------------
/docs/images/逆地理编码查询.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/images/逆地理编码查询.webp
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/public/favicon.ico
--------------------------------------------------------------------------------
/tianditu_tools/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/icon.png
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.js:
--------------------------------------------------------------------------------
1 | import DefaultTheme from "vitepress/theme";
2 | import "./custom.css";
3 |
4 | export default DefaultTheme;
5 |
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/ESRI.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/map_icons/ESRI.webp
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/osm.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/map_icons/osm.webp
--------------------------------------------------------------------------------
/docs/images/PixPin_2024-01-10_16-13-41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/docs/images/PixPin_2024-01-10_16-13-41.png
--------------------------------------------------------------------------------
/tianditu_tools/__init__.py:
--------------------------------------------------------------------------------
1 | from .plugin import TianDiTu
2 |
3 |
4 | def classFactory(iface):
5 | """QGIS Plugin"""
6 | return TianDiTu(iface)
7 |
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/stamen_toner.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/map_icons/stamen_toner.webp
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/stamen_terrain.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/map_icons/stamen_terrain.webp
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/googlemap_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/map_icons/googlemap_default.png
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/googlemap_terrain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/map_icons/googlemap_terrain.png
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/stamen_watercolor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/map_icons/stamen_watercolor.webp
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/googlemap_satellite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuxspro/qgis-plugin-tianditu/HEAD/tianditu_tools/images/map_icons/googlemap_satellite.png
--------------------------------------------------------------------------------
/tianditu_tools/maps/summary.yml:
--------------------------------------------------------------------------------
1 | extra:
2 | id: extra
3 | name: 其他地图
4 | lastUpdated: '2025-06-07 10:14:11'
5 | tianditu_province:
6 | id: tianditu_province
7 | name: 天地图省级节点
8 | lastUpdated: '2025-06-09 21:49:24'
9 |
--------------------------------------------------------------------------------
/docs/天地图Web服务API.md:
--------------------------------------------------------------------------------
1 | # 天地图 Web 服务 API
2 |
3 |
4 |
5 |
6 | ## 地名搜索
7 |
8 |
9 |
10 | ### 例外
11 |
12 | 搜索`eqwewq12321`
13 | 返回
14 | ```json
15 | {"count": "0", "resultType": 1, "status": {"cndesc": "服务正常", "infocode": 1000}, "keyWord": "eqwewq12321"}
16 | ```
--------------------------------------------------------------------------------
/docs/.vitepress/theme/custom.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --vp-home-hero-name-color: transparent;
3 | --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #bd34fe, #41d1ff);
4 | --vp-home-hero-image-background-image: linear-gradient(-45deg, #dd95ffc9 50%, #47caff 50%);
5 | --vp-home-hero-image-filter: blur(78px);
6 | }
7 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "qgis-plugin-tianditu"
3 | version = "0.5.4"
4 | description = "QGIS 天地图工具,方便进行天地图瓦片底图的添加以及简单实现了部分天地图 Web 服务 API(地名搜索、地理编码查询、逆地理编码查询) "
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = ["pyqt5>=5.15.11", "pyqt6>=6.8.1"]
8 |
9 |
10 | [tool.uv]
11 | # 为项目的依赖项添加额外的约束条件
12 | constraint-dependencies = ["pyqt5-qt5 <=5.15.2; sys_platform == 'win32'"]
13 | # PyQt5-Qt5 这个包只有 5.15.2 还支持 Windows
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qgis-plugin-tianditu-docs",
3 | "version": "0.5.5",
4 | "description": "documents of tianditu-tools plugin",
5 | "main": "index.js",
6 | "scripts": {
7 | "docs:dev": "vitepress dev docs",
8 | "docs:build": "vitepress build docs",
9 | "docs:preview": "vitepress preview docs"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "GPL-3.0",
14 | "devDependencies": {
15 | "vitepress": "1.0.0-rc.36"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tianditu_tools/plugin.py:
--------------------------------------------------------------------------------
1 | from .widgets.toolbar import TiandituToolbar
2 |
3 |
4 | class TianDiTu:
5 | def __init__(self, iface):
6 | self.iface = iface
7 | self.toolbar = TiandituToolbar(self.iface)
8 |
9 | def initGui(self):
10 | self.iface.addToolBar(self.toolbar)
11 |
12 | def unload(self):
13 | """Unload from the QGIS interface"""
14 | self.toolbar.remove_dock()
15 | mw = self.iface.mainWindow()
16 | mw.removeToolBar(self.toolbar)
17 | self.toolbar.deleteLater()
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/02_feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 功能请求🙋♂️
2 | description: 功能请求
3 | labels:
4 | - 'Feature Request'
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 感谢您抽出时间正确填写这份功能请求报告。
10 |
11 | - type: textarea
12 | id: what
13 | attributes:
14 | label: 功能描述
15 | description: |
16 | 描述所要实现的功能
17 | validations:
18 | required: true
19 |
20 | - type: textarea
21 | id: additional-context
22 | attributes:
23 | label: 其他补充描述
24 | description: |
25 | 其他补充描述
--------------------------------------------------------------------------------
/.github/workflows/commit.yml:
--------------------------------------------------------------------------------
1 | name: 'Commit CI'
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'dev'
7 |
8 | jobs:
9 | build:
10 | name: 'build'
11 | runs-on: 'ubuntu-latest'
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Install uv
16 | uses: astral-sh/setup-uv@v5
17 | with:
18 | version: "0.6.0"
19 |
20 | - name: Set up Python
21 | run: uv python install
22 |
23 | - name: 'Build and Pack'
24 | run: |
25 | chmod +x ./build.sh
26 | ./build.sh
27 |
28 | - name: Upload Artifact
29 | uses: actions/upload-artifact@v4
30 | with:
31 | path: ./dist/*.zip
--------------------------------------------------------------------------------
/.github/workflows/pylint.yml:
--------------------------------------------------------------------------------
1 | name: Pylint
2 |
3 | on: [ push ]
4 |
5 | jobs:
6 | Pylint:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ['3.9', '3.12' ]
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v5
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | - name: Install dependencies
18 | run: |
19 | python -m pip install --upgrade pip
20 | pip install pylint
21 | - name: Analysing the code with pylint
22 | run: |
23 | pylint $(git ls-files '*.py' ':(exclude)tianditu_tools/ui/')
24 |
--------------------------------------------------------------------------------
/docs/通过 XYZ Tiles 添加天地图.md:
--------------------------------------------------------------------------------
1 | # 通过 XYZ Tiles 添加天地图
2 |
3 | 天地图的 XYZ 链接为:
4 |
5 | `http://t0.tianditu.gov.cn/img_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=w&TileMatrix={z}&TileRow={y}&TileCol={x}&style=default&format=tiles&tk=<自己申请的key>`
6 |
7 | 以往, 直接填入这一串 URL 就可以正常访问天地图了. 但不知从什么时候开始,加载底图就不行了,原因天地图是启用了防盗链, 需要验证 Referer 后才能正常返回瓦片, 否则就是返回疑似攻击.
8 |
9 | 所以, 在填天地图 XYZ Tiles 的时候, 需要将 Referer 栏(被翻译成“引用”)填入天地图的官网 `https://www.tianditu.gov.cn` 即可正常加载天地图.
10 |
11 | 
12 |
13 | ---
14 |
15 | 参考资料:
16 |
17 | 1. [通过QGIS XYZ Tiles访问国内四大图商地图服务](https://mp.weixin.qq.com/s/V4yI1yqzGSR1M8oqEn0wbA)
18 | 2. [QGIS文章一 —— 实现天地图加载](https://mp.weixin.qq.com/s/6ZUnTNYftIkPDGpV7UxXgA)
19 |
--------------------------------------------------------------------------------
/tianditu_tools/images/fitzoom.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/Setting/main.py:
--------------------------------------------------------------------------------
1 | from qgis.PyQt.QtWidgets import QAction
2 |
3 | from .dialog import SettingDialog
4 | from ..icons import icons
5 | from ...compat import IS_QT5, IS_QT6
6 |
7 |
8 | class SettingAction(QAction):
9 | def __init__(
10 | self,
11 | toolbar,
12 | parent=None,
13 | ):
14 | super().__init__(parent)
15 | self.setIcon(icons["setting"])
16 | self.setText("设置")
17 | self.triggered.connect(self.show_setting_dialog)
18 | self.toolbar = toolbar
19 |
20 | def show_setting_dialog(self):
21 | dlg = SettingDialog(toolbar=self.toolbar)
22 | if IS_QT5:
23 | dlg.exec_()
24 | if IS_QT6:
25 | dlg.exec()
26 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/icons.py:
--------------------------------------------------------------------------------
1 | from qgis.PyQt.QtGui import QIcon
2 |
3 | from ..utils import PluginDir
4 |
5 | icon_dict = {
6 | "setting": "./images/setting.svg",
7 | "add": "./images/add_map.svg",
8 | "map": "./images/map_tianditu.svg",
9 | "other": "./images/earth.svg",
10 | "search": "./images/search.svg",
11 | "fitzoom": "./images/fitzoom.svg",
12 | }
13 |
14 | icons = {key: QIcon(str(PluginDir.joinpath(value))) for key, value in icon_dict.items()}
15 |
16 | map_icon_folder = PluginDir.joinpath("./images/map_icons")
17 |
18 |
19 | def get_extra_map_icon(name: str):
20 | if map_icon_folder.joinpath(name).exists():
21 | return QIcon(str(map_icon_folder.joinpath(name)))
22 | return QIcon(str(map_icon_folder.joinpath("default.svg")))
23 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: 'Release'
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release:
10 | name: 'Release'
11 | runs-on: 'ubuntu-latest'
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Install uv
16 | uses: astral-sh/setup-uv@v5
17 | with:
18 | version: "0.6.0"
19 |
20 | - name: Set up Python
21 | run: uv python install
22 |
23 | - name: 'Build and Pack'
24 | run: |
25 | chmod +x ./build.sh
26 | ./build.sh
27 |
28 | - name: Create GitHub release
29 | id: release
30 | env:
31 | GH_TOKEN: ${{ github.token }}
32 | run: gh release create ${{ github.ref_name }} ./dist/*.zip -t "Release ${{ github.ref_name }}" --generate-notes -d
--------------------------------------------------------------------------------
/tianditu_tools/images/search.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/update_ui.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | cwd = Path.cwd()
4 | ui_dir = cwd.joinpath("tianditu_tools/ui")
5 |
6 |
7 | def update_ui_genate_py(file_path: Path):
8 | assert file_path.suffix == ".py"
9 | content = file_path.read_text(encoding="utf-8")
10 | content = content.replace("from PyQt5", "from qgis.PyQt")
11 | content = content.replace("from PyQt6", "from qgis.PyQt")
12 | file_path.write_text(content, encoding="utf-8")
13 |
14 |
15 | def update_file():
16 | # 遍历目录下的所有 .py 文件
17 | for file_path in ui_dir.glob("**/*.py"):
18 | print(f"正在处理文件: {file_path}")
19 | try:
20 | update_ui_genate_py(file_path)
21 | except Exception as e: # pylint: disable=broad-exception-caught
22 | print(f"处理文件 {file_path} 时出错: {e}")
23 |
24 |
25 | update_file()
26 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitepress";
2 |
3 | // https://vitepress.dev/reference/site-config
4 | export default defineConfig({
5 | title: "Tianditu Tools",
6 | description: "QGIS Plugin",
7 | head: [["link", { rel: "icon", href: "/favicon.ico" }]],
8 | themeConfig: {
9 | // https://vitepress.dev/reference/default-theme-config
10 | logo: "/logo.svg",
11 | nav: [
12 | { text: "主页", link: "/" },
13 | // { text: "开发笔记", link: "https://liuxs.pro" },
14 | ],
15 |
16 | sidebar: [
17 | {
18 | text: "使用说明",
19 | items: [
20 | { text: "简介", link: "/intro" },
21 | { text: "通过 XYZ Tiles 添加天地图", link: "/通过 XYZ Tiles 添加天地图" },
22 | { text: "天地图省级节点加载", link: "/天地图省级节点加载" },
23 | ],
24 | },
25 | ],
26 |
27 | socialLinks: [{ icon: "github", link: "https://github.com/liuxsdev/qgis-plugin-tianditu" }],
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/amap.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "Tianditu Tools"
7 | text: "QGIS Plugin"
8 | tagline: 快速添加天地图地图至 QGIS
9 | image:
10 | src: /logo.svg
11 | width: 100%
12 | alt: Tianditu Tools
13 | actions:
14 | - theme: brand
15 | text: 使用说明
16 | link: /intro
17 | - theme: alt
18 | text: View on GitHub
19 | link: https://github.com/liuxsdev/qgis-plugin-tianditu
20 | - theme: alt
21 | text: View on QGIS Plugins Repository
22 | link: https://plugins.qgis.org/plugins/tianditu-tools/
23 |
24 | features:
25 | - title: 天地图底图
26 | icon: 🗺️
27 | details: 只需要填入天地图 Key,无需自己拼接天地图 URL
28 | - title: 天地图 API
29 | icon: 🔍
30 | details: 在QGIS中进行天地图地名搜索、地理编码查询、逆地理编码查询
31 | - title: 其他图源
32 | icon: 🌍️
33 | details: 收集了部分 QGIS 能加载的天地图省级节点图源以及常见的第三方图源(谷歌、高德、ESRI等)
34 | - title: 完全开源
35 | icon: ☕
36 | details: 代码已经在 Github 开源,喜欢的话,可以点个 Star ✨
37 | ---
38 |
39 |
--------------------------------------------------------------------------------
/docs/intro.md:
--------------------------------------------------------------------------------
1 | # TianDiTu Tools
2 |
3 | QGIS 天地图工具,方便进行天地图瓦片底图的添加以及简单实现了部分[天地图 Web 服务 API](http://lbs.tianditu.gov.cn/server/guide.html)(地名搜索、地理编码查询、逆地理编码查询)
4 |
5 | ## 安装
6 |
7 | 从 QGIS 插件中,搜索 “天地图” 或者 “tianditu” , 找到插件安装即可.
8 |
9 | 安装插件后,可在 QGIS 工具栏中看到工具按钮,按钮功能分别为添加底图、搜索、以及设置.
10 |
11 | ## 设置天地图 Key
12 |
13 | 使用前需要设置天地图 Key,点击设置按钮,输入 key,保存并检查
14 |
15 | 
16 |
17 | ::: tip
18 | 天地图 key 需要到 [天地图控制台](https://console.tianditu.gov.cn/api/key) 去申请,申请的类型为 “浏览器端” 或者 “Android平台”
19 | :::
20 |
21 | ## 天地图底图添加
22 |
23 | 在工具栏下拉菜单中选择底图,点击即可添加到当前工程中。
24 |
25 | 添加底图时随机选择子域名,可减轻服务器压力,提高可用性。
26 |
27 | ## 天地图 Web 服务 API
28 |
29 | | 地名搜索 | 地理编码 | 逆地理编码 |
30 | | :----------------------------: | :------------------------------------: | :----------------------------------------: |
31 | |  |  |  |
32 |
33 | 双击结果项(或点击链接)可添加至当前地图中:
34 |
35 | 
36 |
--------------------------------------------------------------------------------
/tianditu_tools/images/add_map.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/01_report_bugs.yml:
--------------------------------------------------------------------------------
1 | name: 报告插件错误❌
2 | description: 报告你使用插件过程中遇到的异常
3 | labels:
4 | - 'Bug'
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 感谢您抽出时间正确填写这份错误报告。
10 |
11 | - type: textarea
12 | id: what
13 | attributes:
14 | label: 请描述您遇到的问题?
15 | validations:
16 | required: true
17 |
18 | - type: textarea
19 | id: steps
20 | attributes:
21 | label: 如何重现您的问题
22 | description: |
23 | 重现问题的步骤
24 | 1.
25 | 2.
26 | validations:
27 | required: true
28 |
29 | - type: textarea
30 | id: version-info
31 | attributes:
32 | label: 您使用的QGIS版本以及电脑系统版本
33 | description: |
34 | 您使用的QGIS版本以及电脑系统版本, 如 Windows 11 23H2, QGIS 3.34.2
35 | validations:
36 | required: true
37 |
38 | - type: checkboxes
39 | id: self-check
40 | attributes:
41 | label: 自查清单
42 | description: |
43 | 请您自查以下问题 (是则勾选)
44 | options:
45 | - label: 通过XYZ Tiles的方式能否能正常加载天地图.
46 | - label: 访问[天地图官网地图](https://www.tianditu.gov.cn/)是否正常
47 |
48 | - type: textarea
49 | id: additional-context
50 | attributes:
51 | label: 其他补充描述
52 | description: |
53 | 其他补充描述
--------------------------------------------------------------------------------
/tianditu_tools/qgis_utils.py:
--------------------------------------------------------------------------------
1 | from qgis.core import QgsProject, QgsRasterLayer, QgsMessageLog, Qgis
2 |
3 |
4 | def push_message(iface, title: str, message: str):
5 | """
6 | 将具有默认超时时间的信息推送到消息栏。
7 | https://qgis.org/pyqgis/3.44/gui/QgsMessageBar.html#qgis.gui.QgsMessageBar.pushInfo
8 | Args:
9 | iface : iface
10 | title (str): 消息标题
11 | message (str): 消息
12 | """
13 | iface.messageBar().pushInfo(
14 | title,
15 | message,
16 | )
17 |
18 |
19 | def push_warning(iface, title: str, message: str):
20 | iface.messageBar().pushWarning(
21 | title,
22 | message,
23 | )
24 |
25 |
26 | def add_raster_layer(uri: str, name: str, provider_type: str = "wms"):
27 | """QGIS 添加栅格图层
28 |
29 | Args:
30 | uri (str): 栅格图层 uri
31 | name (str): 栅格图层名称
32 | provider_type(str): 栅格图层类型(wms,arcgismapserver)
33 | Reference: https://qgis.org/pyqgis/3.32/core/QgsRasterLayer.html
34 | """
35 | raster_layer = QgsRasterLayer(uri, name, provider_type)
36 | if raster_layer.isValid():
37 | QgsProject.instance().addMapLayer(raster_layer)
38 | return raster_layer
39 | log_message(f"无效的图层 Invalid Layer: \n{uri}")
40 | return None
41 |
42 |
43 | def log_message(message: str):
44 | QgsMessageLog.logMessage(message, "Tianditu-Tools", Qgis.Info)
45 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/toolbar.py:
--------------------------------------------------------------------------------
1 | from qgis.PyQt.QtWidgets import QToolBar
2 |
3 | from .AddMap import AddMapBtn
4 | from .FitZoom import FitZoomAction
5 | from .Search import SearchAction
6 | from .Setting import SettingAction
7 | from .icons import icons
8 | from ..utils import PluginConfig
9 |
10 |
11 | class TiandituToolbar(QToolBar):
12 | def __init__(self, iface, parent=None) -> None:
13 | super().__init__("Tianditu Tools 工具栏", parent)
14 | self.iface = iface
15 | self.icons = icons
16 | self.conf = PluginConfig()
17 | self.setToolTip("天地图 Tools 工具栏")
18 | self.add_button = None
19 | self.actions = []
20 |
21 | self.init_config()
22 | self.setup_action()
23 |
24 | def setup_action(self):
25 | self.add_button = AddMapBtn(iface=self.iface, parent=self)
26 | self.addWidget(self.add_button)
27 | # 添加 Action
28 | action_setting = SettingAction(self)
29 | action_search = SearchAction(iface=self.iface, parent=self)
30 | action_fitzoom = FitZoomAction(iface=self.iface, parent=self)
31 |
32 | self.actions.extend([action_setting, action_search, action_fitzoom])
33 | self.addActions(self.actions)
34 |
35 | def init_config(self):
36 | self.conf.init_config()
37 |
38 | def remove_dock(self):
39 | dock = self.actions[1]
40 | dock.unload()
41 |
--------------------------------------------------------------------------------
/tianditu_tools/Styles/terrain.qml:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 | ### 天地图 Web 服务 API
30 |
31 | | 地名搜索 | 地理编码 | 逆地理编码 |
32 | | :----------------------------: | :------------------------------------: | :----------------------------------------: |
33 | |  |  |  |
34 |
35 | 双击结果项(或点击链接)可添加至当前地图中:
36 |
37 | 
38 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/Search/main.py:
--------------------------------------------------------------------------------
1 | from qgis.PyQt.QtWidgets import QAction, QMessageBox
2 |
3 | from .searchDock import SearchDockWidget
4 | from ..icons import icons
5 | from ...compat import LeftDockWidgetArea
6 | from ...utils import PluginConfig
7 |
8 | conf = PluginConfig()
9 |
10 |
11 | class SearchAction(QAction):
12 | def __init__(
13 | self,
14 | iface,
15 | parent=None,
16 | ):
17 | super().__init__(parent)
18 | self.parent = parent
19 | self.iface = iface
20 | self.setIcon(icons["search"])
21 | self.setText("搜索")
22 | self.searchdockwidget = SearchDockWidget(self.iface)
23 | self.searchdockwidget.visibilityChanged.connect(self.onDockVisibilityChanged)
24 | self.iface.addDockWidget(LeftDockWidgetArea, self.searchdockwidget)
25 | self.searchdockwidget.hide()
26 | self.setCheckable(True)
27 | self.triggered.connect(self.openSearch)
28 |
29 | def openSearch(self):
30 | key = conf.get_key()
31 | if key == "":
32 | QMessageBox.warning(
33 | self.parent,
34 | "错误",
35 | "天地图Key未设置或Key无效",
36 | QMessageBox.Yes,
37 | QMessageBox.Yes,
38 | )
39 | self.setChecked(False)
40 | else:
41 | if self.searchdockwidget.isHidden():
42 | self.searchdockwidget.show()
43 | else:
44 | self.searchdockwidget.hide()
45 |
46 | def onDockVisibilityChanged(self, is_visible):
47 | if not is_visible:
48 | self.setChecked(False)
49 | else:
50 | self.setChecked(True)
51 |
52 | def unload(self):
53 | self.iface.removeDockWidget(self.searchdockwidget)
54 |
--------------------------------------------------------------------------------
/pack.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import shutil
3 | import zipfile
4 | from pathlib import Path
5 |
6 | # Define the source and destination directories
7 | cwd = Path.cwd()
8 | source_dir = cwd.joinpath("tianditu_tools")
9 | dist_dir = cwd.joinpath("dist")
10 | # same as original plugin package name (tianditu-tools).
11 | dist_source_dir = dist_dir.joinpath("tianditu-tools")
12 |
13 | # Other necessary files
14 | other_files = ["README.md", "LICENSE"]
15 |
16 |
17 | def delete_pycache(directory):
18 | pycache_dir = directory.joinpath("__pycache__")
19 |
20 | if pycache_dir.exists() and pycache_dir.is_dir():
21 | shutil.rmtree(pycache_dir)
22 |
23 | for subdirectory in directory.iterdir():
24 | if subdirectory.is_dir():
25 | delete_pycache(subdirectory)
26 |
27 |
28 | # 删除dist文件夹
29 | if dist_dir.exists():
30 | shutil.rmtree(dist_dir)
31 | # 递归删除 __pycache__ 文件夹
32 | delete_pycache(source_dir)
33 | # 复制源文件夹到 dist 目录下的 ianditu-tools 文件夹
34 | shutil.copytree(source_dir, dist_source_dir)
35 | # 删除ui文件夹的 ui 文件,只保留 python 文件
36 | for file_path in dist_source_dir.joinpath("ui").glob("*.ui"):
37 | file_path.unlink()
38 | # 复制 README 和 LICENSE 文件
39 | for file in other_files:
40 | shutil.copy(cwd.joinpath(file), dist_source_dir)
41 | # Read the metadata.txt file using configparser
42 | config = configparser.ConfigParser()
43 | config.read(source_dir.joinpath("metadata.txt"), encoding="utf-8")
44 | # Get the version under the [general] section
45 | version = config.get("general", "version")
46 | filename = f"tianditu_tools-{version}.zip"
47 | # Zip the destination directory
48 | with zipfile.ZipFile(dist_dir.joinpath(filename), "w", zipfile.ZIP_DEFLATED) as zipf:
49 | for file in dist_source_dir.glob("**/*"):
50 | zipf.write(file, file.relative_to(dist_source_dir.parent))
51 | # 删除dist下的打包目录 tianditu_tools
52 | shutil.rmtree(dist_source_dir)
53 | print(f"完成打包 {filename}")
54 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/FitZoom/main.py:
--------------------------------------------------------------------------------
1 | from qgis.PyQt.QtWidgets import QAction
2 | from qgis.core import QgsCoordinateReferenceSystem
3 |
4 | from ..icons import icons
5 |
6 |
7 | class FitZoomAction(QAction):
8 | def __init__(self, iface, parent=None):
9 | self.iface = iface
10 | super().__init__(parent)
11 | self.setIcon(icons["fitzoom"])
12 | self.setText("调整缩放比例")
13 | self.triggered.connect(self.fit_zoom_level)
14 | self.iface.mapCanvas().destinationCrsChanged.connect(self.check_crs)
15 | self.iface.mapCanvas().layersChanged.connect(self.check_crs)
16 | self.check_crs()
17 |
18 | def fit_zoom_level(self):
19 | crs = self.iface.mapCanvas().mapSettings().destinationCrs()
20 | if crs == QgsCoordinateReferenceSystem("EPSG:3857"):
21 | max_zoom_level = 23
22 | mpp_3857 = [40075016.685 / (2**i * 256) for i in range(max_zoom_level)]
23 | current_mpp = self.iface.mapCanvas().mapUnitsPerPixel()
24 | nearest_level = self.find_nearest_number_index(mpp_3857, current_mpp)
25 | zoom_factor = mpp_3857[nearest_level] / current_mpp
26 | if not abs(1 - zoom_factor) < 1e-5:
27 | self.iface.mapCanvas().zoomByFactor(zoom_factor)
28 |
29 | @staticmethod
30 | def find_nearest_number_index(numbers_list, target):
31 | min_difference = float("inf")
32 | nearest_index = None
33 | for i, number in enumerate(numbers_list):
34 | difference = abs(number - target)
35 | if difference < min_difference:
36 | min_difference = difference
37 | nearest_index = i
38 | return nearest_index
39 |
40 | def check_crs(self):
41 | crs = self.iface.mapCanvas().mapSettings().destinationCrs()
42 | layers_number = self.iface.mapCanvas().layerCount()
43 | if crs == QgsCoordinateReferenceSystem("EPSG:3857") and layers_number > 0:
44 | self.setText("调整缩放比例")
45 | self.setEnabled(True)
46 | else:
47 | self.setText("调整缩放比例不可用")
48 | self.setEnabled(False)
49 |
--------------------------------------------------------------------------------
/tianditu_tools/compat.py:
--------------------------------------------------------------------------------
1 | from qgis.PyQt.QtCore import Qt, QT_VERSION_STR
2 | from qgis.PyQt.QtGui import QClipboard
3 | from qgis.PyQt.QtNetwork import QNetworkReply, QNetworkRequest
4 | from qgis.PyQt.QtWidgets import QToolButton
5 |
6 | QT_MAJOR_VERSION = int(QT_VERSION_STR.split(".")[0])
7 | IS_QT5 = QT_MAJOR_VERSION == 5
8 | IS_QT6 = QT_MAJOR_VERSION == 6
9 |
10 | if IS_QT5:
11 | # Qt 5 版本
12 | from .ui.sd import Ui_SdDockWidget # noqa # pylint: disable=unused-import
13 | from .ui.search import Ui_SearchDockWidget # noqa # pylint: disable=unused-import
14 | from .ui.setting import Ui_SettingDialog # noqa # pylint: disable=unused-import
15 |
16 | # 枚举值改变
17 | LeftDockWidgetArea = Qt.LeftDockWidgetArea
18 | RightDockWidgetArea = Qt.RightDockWidgetArea
19 | AlignCenter = Qt.AlignCenter
20 | DescendingOrder = Qt.DescendingOrder
21 | Checked = Qt.Checked
22 | Unchecked = Qt.Unchecked
23 | MatchExactly = Qt.MatchExactly
24 | MenuButtonPopup = QToolButton.MenuButtonPopup
25 | NoError = QNetworkReply.NoError
26 | HttpStatusCodeAttribute = QNetworkRequest.HttpStatusCodeAttribute
27 | ModeClipboard = QClipboard.Clipboard
28 |
29 | if IS_QT6:
30 | # QT6 版本
31 |
32 | from .ui.sd_6 import Ui_SdDockWidget # noqa # pylint: disable=unused-import
33 | from .ui.setting_6 import Ui_SettingDialog # noqa # pylint: disable=unused-import
34 | from .ui.search_6 import ( # noqa # pylint: disable=unused-import
35 | Ui_SearchDockWidget, # noqa # pylint: disable=unused-import
36 | ) # noqa # pylint: disable=unused-import
37 |
38 | # 枚举值改变
39 | LeftDockWidgetArea = Qt.DockWidgetArea.LeftDockWidgetArea
40 | RightDockWidgetArea = Qt.DockWidgetArea.RightDockWidgetArea
41 | AlignCenter = Qt.AlignmentFlag.AlignCenter
42 | DescendingOrder = Qt.SortOrder.DescendingOrder
43 | Checked = Qt.CheckState.Checked
44 | Unchecked = Qt.CheckState.Unchecked
45 | MatchExactly = Qt.MatchFlag.MatchExactly
46 | MenuButtonPopup = QToolButton.ToolButtonPopupMode.MenuButtonPopup
47 | NoError = QNetworkReply.NetworkError.NoError
48 | HttpStatusCodeAttribute = QNetworkRequest.Attribute.HttpStatusCodeAttribute
49 | ModeClipboard = QClipboard.Mode.Clipboard
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | .idea/
6 | # C extensions
7 | *.so
8 | config.ini
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 | pip-wheel-metadata/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | .python-version
87 |
88 | # pipenv
89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
92 | # install all needed dependencies.
93 | #Pipfile.lock
94 |
95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
96 | __pypackages__/
97 |
98 | # Celery stuff
99 | celerybeat-schedule
100 | celerybeat.pid
101 |
102 | # SageMath parsed files
103 | *.sage.py
104 |
105 | # Environments
106 | .env
107 | .venv
108 | env/
109 | venv/
110 | ENV/
111 | env.bak/
112 | venv.bak/
113 |
114 | # Spyder project settings
115 | .spyderproject
116 | .spyproject
117 |
118 | # Rope project settings
119 | .ropeproject
120 |
121 | # mkdocs documentation
122 | /site
123 |
124 | # mypy
125 | .mypy_cache/
126 | .dmypy.json
127 | dmypy.json
128 |
129 | # Pyre type checker
130 | .pyre/
131 |
132 | # node modules
133 | node_modules/
134 | cache/
--------------------------------------------------------------------------------
/tianditu_tools/images/setting.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/AddMap/extra_map.py:
--------------------------------------------------------------------------------
1 | from qgis.PyQt.QtWidgets import QMenu
2 |
3 | from .sd import SdAction
4 | from .utils import get_xyz_uri
5 | from ..icons import icons, get_extra_map_icon
6 | from ...qgis_utils import add_raster_layer
7 | from ...utils import PluginDir, load_yaml, PluginConfig
8 |
9 | conf = PluginConfig()
10 |
11 |
12 | def add_map(data):
13 | name = data["name"]
14 | map_type = data.get("type")
15 | add_map_type = "wms"
16 | uri = data.get("uri", "")
17 | referer = data.get("referer", "")
18 | if uri == "":
19 | uri = get_xyz_uri(data["url"], data["zmin"], data["zmax"], referer)
20 |
21 | if map_type == "arcgismapserver":
22 | add_map_type = "arcgismapserver"
23 |
24 | add_raster_layer(uri, name, add_map_type)
25 |
26 |
27 | def add_tianditu_province_menu(parent_menu: QMenu, iface):
28 | # 增加山东天地图
29 | sd = SdAction(iface, parent=parent_menu)
30 | parent_menu.addAction(sd)
31 | parent_menu.addSeparator()
32 | # 其他省份
33 | tianditu_province_path = PluginDir.joinpath("maps/tianditu_province.yml")
34 | tianditu_province = load_yaml(tianditu_province_path)["maps"]
35 | maps = tianditu_province.keys()
36 | extra_maps_status = conf.get_extra_maps_status()
37 | for map_name in maps:
38 | # 一级菜单 省份名称
39 | if map_name in extra_maps_status["tianditu_province"]:
40 | add_map_action = parent_menu.addAction(icons["map"], map_name)
41 | map_data = tianditu_province[map_name]
42 | sub_menu = QMenu(parent_menu)
43 | for m in map_data:
44 | sub_menu.addAction(
45 | icons["map"],
46 | m["name"],
47 | lambda m_=m: add_map(m_),
48 | )
49 | add_map_action.setMenu(sub_menu)
50 | parent_menu.addSeparator()
51 |
52 |
53 | def add_extra_map_menu(parent_menu: QMenu):
54 | extra = load_yaml(PluginDir.joinpath("maps/extra.yml"))
55 | extra_maps = extra["maps"]
56 | extra_root = parent_menu.addAction(icons["other"], "其他地图")
57 | extra_root_menu = QMenu(parent_menu)
58 | maps = extra_maps.keys()
59 | extra_maps_status = conf.get_extra_maps_status()
60 | for map_name in maps:
61 | if (
62 | map_name in extra_maps_status["tianditu_province"]
63 | or map_name in extra_maps_status["extra"]
64 | ):
65 | map_data = extra_maps[map_name]
66 | sub_menu = extra_root_menu.addAction(icons["other"], map_name)
67 | sub_sub_menu = QMenu(parent_menu)
68 | for sub_map in map_data:
69 | sub_sub_menu.addAction(
70 | get_extra_map_icon(sub_map.get("icon", "default.svg")),
71 | sub_map["name"],
72 | lambda m_=sub_map: add_map(m_),
73 | )
74 | sub_menu.setMenu(sub_sub_menu)
75 | extra_root.setMenu(extra_root_menu)
76 |
--------------------------------------------------------------------------------
/tianditu_tools/widgets/AddMap/main.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from qgis.PyQt.QtWidgets import QToolButton, QMenu, QMessageBox
4 |
5 | from .extra_map import add_tianditu_province_menu, add_extra_map_menu
6 | from .utils import get_xyz_uri
7 | from ..icons import icons
8 | from ...compat import MenuButtonPopup
9 | from ...qgis_utils import add_raster_layer
10 | from ...utils import TIANDITU_HOME_URL, PluginConfig, tianditu_map_url, PluginDir
11 |
12 | tianditu_map_info = {
13 | "vec": "天地图-矢量地图",
14 | "cva": "天地图-矢量注记",
15 | "img": "天地图-影像地图",
16 | "cia": "天地图-影像注记",
17 | "ter": "天地图-地形晕染",
18 | "cta": "天地图-地形注记",
19 | "ibo": "天地图-全球境界",
20 | }
21 |
22 | conf = PluginConfig()
23 |
24 |
25 | class AddMapBtn(QToolButton):
26 | def __init__(self, iface, parent=None):
27 | super().__init__(parent)
28 | self.iface = iface
29 | self.icons = icons
30 | self.setToolTip("添加地图")
31 | self.setup_action()
32 |
33 | def setup_action(self):
34 | menu = QMenu(self)
35 | menu.setObjectName("TianDiTuAddMap")
36 | for map_type, map_name in tianditu_map_info.items():
37 | menu.addAction(
38 | self.icons["map"],
39 | map_name,
40 | lambda maptype_=map_type: self.add_tianditu_basemap(maptype_),
41 | )
42 | # 山体阴影
43 | menu.addAction(self.icons["map"], "天地图-山体阴影", self.add_terrain_rgb)
44 | menu.addSeparator()
45 | # 天地图省级节点
46 | add_tianditu_province_menu(menu, self.iface)
47 | # 其他图源
48 | add_extra_map_menu(menu)
49 | self.setMenu(menu)
50 | self.setPopupMode(MenuButtonPopup)
51 | self.setIcon(self.icons["add"])
52 |
53 | def add_terrain_rgb(self):
54 | key = conf.get_key()
55 | if key == "":
56 | QMessageBox.warning(
57 | self,
58 | "错误",
59 | "天地图Key未设置或Key无效",
60 | QMessageBox.Yes,
61 | QMessageBox.Yes,
62 | )
63 | return
64 | map_url = tianditu_map_url("terrain-rgb", key, "")
65 | uri = get_xyz_uri(map_url, 1, 12, TIANDITU_HOME_URL)
66 | terrain_uri = "interpretation=maptilerterrain&" + uri
67 | terrain_layer = add_raster_layer(terrain_uri, "天地图-山体阴影")
68 | terrain_layer.loadNamedStyle(str(PluginDir.joinpath("./Styles/terrain.qml")))
69 |
70 | def add_tianditu_basemap(self, maptype):
71 | key = conf.get_key()
72 | if key == "":
73 | QMessageBox.warning(
74 | self,
75 | "错误",
76 | "天地图Key未设置或Key无效",
77 | QMessageBox.Yes,
78 | QMessageBox.Yes,
79 | )
80 | else:
81 | random_enabled = conf.get_bool_value("Tianditu/random")
82 | key_random_enabled = conf.get_bool_value("Tianditu/random_key")
83 | if random_enabled:
84 | subdomain = f"t{random.randint(0, 7)}"
85 | else:
86 | subdomain = conf.get_value("Tianditu/subdomain")
87 | if key_random_enabled:
88 | key = conf.get_random_key()
89 | map_url = tianditu_map_url(maptype, key, subdomain)
90 | uri = get_xyz_uri(map_url, 1, 18, TIANDITU_HOME_URL)
91 | add_raster_layer(uri, tianditu_map_info[maptype])
92 |
--------------------------------------------------------------------------------
/tianditu_tools/images/earth.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/tianditu_tools/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 | from random import choice
4 |
5 | import yaml
6 | from qgis.PyQt.QtCore import QUrl, QUrlQuery
7 | from qgis.PyQt.QtNetwork import QNetworkRequest
8 | from qgis.core import QgsSettings
9 |
10 | TIANDITU_HOME_URL = "https://www.tianditu.gov.cn/"
11 | PLUGIN_NAME = "tianditu-tools"
12 |
13 | PluginDir = Path(__file__).parent
14 |
15 | HEADER = {
16 | "User-Agent": "Mozilla/5.0 QGIS/32400/Windows 10 Version 2009",
17 | "Referer": "https://www.tianditu.gov.cn/",
18 | }
19 |
20 | APP_FONT = QgsSettings().value("app/fontFamily")
21 |
22 |
23 | def get_extramap_status():
24 | summary = load_yaml(PluginDir.joinpath("maps/summary.yml"))
25 | default_status = {}
26 | for section in summary:
27 | section_data = load_yaml(PluginDir.joinpath(f"maps/{section}.yml"))
28 | default_status[section] = list(section_data["maps"].keys())
29 | return default_status
30 |
31 |
32 | class PluginConfig:
33 | def __init__(self):
34 | self.conf = QgsSettings()
35 | self.conf_name = "tianditu-tools"
36 | self.section_tianditu = f"{self.conf_name}/Tianditu"
37 |
38 | def init_config(self):
39 | # 初始化配置文件
40 | if not self.conf.contains("tianditu-tools/Tianditu/key"):
41 | print("初始化配置文件")
42 | # 初始化
43 | self.conf.setValue(f"{self.section_tianditu}/key", "")
44 | self.conf.setValue(f"{self.section_tianditu}/keyList", "")
45 | self.conf.setValue(f"{self.section_tianditu}/random", True)
46 | self.conf.setValue(f"{self.section_tianditu}/random_key", False)
47 | self.conf.setValue(f"{self.section_tianditu}/subdomain", "t0")
48 | if not self.conf.contains("tianditu-tools/Other/extramap_status"):
49 | print("初始化 extra map 文件")
50 | self.conf.setValue(
51 | f"{self.conf_name}/Other/extramap_status", str(get_extramap_status())
52 | )
53 | # 升级到保存多个key的版本
54 | if not self.conf.contains(f"{self.section_tianditu}/keyList"):
55 | self.conf.setValue(f"{self.section_tianditu}/random_key", False)
56 | # 保存原来的key
57 | if self.get_key() != "":
58 | self.conf.setValue(f"{self.section_tianditu}/keyList", self.get_key())
59 | else:
60 | self.conf.setValue(f"{self.section_tianditu}/keyList", "")
61 |
62 | def get_key_list(self):
63 | data_str = self.get_value("/Tianditu/keyList")
64 | if data_str == "":
65 | return []
66 | return data_str.split(",")
67 |
68 | def save_key_list(self, data_list):
69 | self.conf.setValue(f"{self.section_tianditu}/keyList", ",".join(data_list))
70 |
71 | def get_extra_maps_status(self):
72 | data = self.get_value("Other/extramap_status")
73 | return json.loads(data.replace("'", '"'))
74 |
75 | def set_extra_maps_status(self, data):
76 | self.conf.setValue(f"{self.conf_name}/Other/extramap_status", str(data))
77 |
78 | def get_value(self, name):
79 | return self.conf.value(f"{self.conf_name}/{name}")
80 |
81 | def get_bool_value(self, name):
82 | return self.conf.value(f"{self.conf_name}/{name}", type=bool)
83 |
84 | def set_value(self, name, value):
85 | self.conf.setValue(f"{self.conf_name}/{name}", value)
86 |
87 | def get_key(self):
88 | return self.get_value("Tianditu/key")
89 |
90 | def get_random_key(self):
91 | key_list = self.get_key_list()
92 | return choice(key_list)
93 |
94 | def set_key(self, key):
95 | key_to_set = ""
96 | if key is not None:
97 | key_to_set = key
98 | self.conf.setValue(f"{self.section_tianditu}/key", key_to_set)
99 |
100 |
101 | def make_request(url: str, referer: str = None, params=None):
102 | u = QUrl(url)
103 | query = QUrlQuery()
104 | if params:
105 | for key, value in params.items():
106 | query.addQueryItem(key, value)
107 | u.setQuery(query)
108 | request = QNetworkRequest(u)
109 | # request.setHeader(QNetworkRequest.UserAgentHeader, headers["User-Agent"]) # 设置 User-Agent 无效
110 | if referer:
111 | request.setRawHeader(b"Referer", referer.encode("utf-8"))
112 |
113 | return request
114 |
115 |
116 | def tianditu_map_url(maptype: str, token: str, subdomain: str) -> str:
117 | """
118 | 返回天地图url
119 |
120 | Args:
121 | maptype (str): 类型
122 | token (str): 天地图key
123 | subdomain (str): 使用的子域名
124 |
125 | Returns:
126 | str: 返回天地图XYZ瓦片地址
127 | """
128 | domain = f"https://{subdomain}.tianditu.gov.cn"
129 | if maptype == "terrain-rgb":
130 | domain = "https://lcdata.tianditu.gov.cn"
131 | url = f"{domain}/{maptype}_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={maptype}"
132 | url += "&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TileCol={x}&TileRow={y}&TileMatrix={z}"
133 | url += f"&tk={token}"
134 | return url
135 |
136 |
137 | def load_yaml(file_path: Path):
138 | """
139 | 读取YAML文件
140 | """
141 | with open(file_path, "r", encoding="utf-8") as f:
142 | return yaml.safe_load(f)
143 |
--------------------------------------------------------------------------------
/tianditu_tools/images/map_icons/default.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tianditu_tools/ui/sd.ui:
--------------------------------------------------------------------------------
1 |
2 |
Key 类型建议选择为“浏览器端” 或者 “Android 平台”
如果工程同步到 QField 中使用,选择“Android 平台”
")) 166 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_keyset), _translate("SettingDialog", "天地图设置")) 167 | self.btn_checkupdate.setText(_translate("SettingDialog", "检查更新")) 168 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_map), _translate("SettingDialog", "地图管理")) 169 | from qgspasswordlineedit import QgsPasswordLineEdit 170 | -------------------------------------------------------------------------------- /tianditu_tools/ui/setting.ui: -------------------------------------------------------------------------------- 1 | 2 |添加底图时随机选择子域名,可减轻服务器压力,提高可用性。
")) 161 | self.checkBox_domain_rand.setText(_translate("SettingDialog", "随机")) 162 | self.label_4.setText(_translate("SettingDialog", "点击此处前往天地图申请 Key\n" 163 | "Key 类型建议选择为“浏览器端” 或者 “Android 平台”
如果工程同步到 QField 中使用,选择“Android 平台”
")) 164 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_keyset), _translate("SettingDialog", "天地图设置")) 165 | self.btn_checkupdate.setText(_translate("SettingDialog", "检查更新")) 166 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_map), _translate("SettingDialog", "地图管理")) 167 | from qgspasswordlineedit import QgsPasswordLineEdit 168 | -------------------------------------------------------------------------------- /tianditu_tools/widgets/Setting/dialog.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from qgis.PyQt import QtWidgets 4 | from qgis.PyQt.QtCore import QTimer 5 | from qgis.PyQt.QtWidgets import QApplication 6 | from qgis.core import QgsNetworkAccessManager 7 | 8 | from .mapmanager import MapManager 9 | from ...compat import Ui_SettingDialog, NoError, HttpStatusCodeAttribute, ModeClipboard 10 | from ...utils import ( 11 | tianditu_map_url, 12 | PluginConfig, 13 | PluginDir, 14 | make_request, 15 | HEADER, 16 | ) 17 | 18 | 19 | def check_key_format(key: str) -> object: 20 | """检查key格式 21 | 22 | Args: 23 | key (str): 天地图key 24 | 25 | Returns: 26 | object: 27 | "key_length_error": key的长度有误, 28 | "has_special_character": 含有除字母数字外的其他字符 29 | """ 30 | correct_length = 32 31 | key_length = len(key) 32 | key_length_error = False 33 | if key_length != correct_length: 34 | key_length_error = True 35 | return { 36 | "key_length_error": key_length_error, 37 | "has_special_character": not key.isalnum(), 38 | } 39 | 40 | 41 | class SettingDialog(QtWidgets.QDialog, Ui_SettingDialog): 42 | def __init__(self, toolbar): 43 | super().__init__() 44 | self.check_thread = None 45 | self.mapm = None 46 | self.toolbar = toolbar 47 | # 读取配置 48 | self.conf = PluginConfig() 49 | self.subdomain_list = [f"t{i}" for i in range(8)] 50 | # 设置 status label 的定时器 51 | self.timer = QTimer(self) 52 | self.timer.timeout.connect(self.clear_status_label) 53 | # 设置界面 54 | self.setupUi(self) 55 | self.initUI() 56 | 57 | def initUI(self): 58 | self.init_keyCombo() 59 | # 如果key输入框是空白的,则禁用保存按钮 60 | if len(self.mLineEdit_key.text()) == 0: 61 | self.saveButton.setEnabled(False) 62 | # 给控件添加事件 63 | self.mLineEdit_key.textChanged.connect(self.on_key_LineEdit_changed) 64 | self.saveButton.clicked.connect(self.save_key) 65 | self.pushButton_copy.clicked.connect(self.copy_key) 66 | self.pushButton_delete.clicked.connect(self.del_key) 67 | self.keyComboBox.currentIndexChanged.connect(self.select_key) 68 | # 是否启用key随机 69 | if self.conf.get_bool_value("Tianditu/random_key"): 70 | self.checkBox_key_rand.setChecked(True) 71 | self.keyComboBox.setEnabled(False) 72 | self.pushButton_copy.setEnabled(False) 73 | self.pushButton_delete.setEnabled(False) 74 | self.checkBox_key_rand.stateChanged.connect(self.enable_key_random) 75 | # subdomain 选择 76 | self.checkBox_domain_rand.setChecked( 77 | self.conf.get_bool_value("Tianditu/random") 78 | ) 79 | self.checkBox_domain_rand.stateChanged.connect(self.enable_random) 80 | # subdomian 设置 81 | self.subdomainComboBox.addItems(self.subdomain_list) 82 | self.subdomainComboBox.setCurrentIndex( 83 | self.subdomain_list.index(self.conf.get_value("Tianditu/subdomain")) 84 | ) 85 | self.subdomainComboBox.setEnabled(not self.conf.get_value("Tianditu/random")) 86 | self.subdomainComboBox.currentIndexChanged.connect(self.select_subdomain) 87 | # init map manager 88 | map_folder = PluginDir.joinpath("maps") 89 | self.mapm = MapManager( 90 | map_folder=map_folder, 91 | parent=self.tab_map, 92 | update_btn=self.btn_checkupdate, 93 | status_label=self.label_checkstatus, 94 | ) 95 | self.verticalLayout_6.addWidget(self.mapm) 96 | self.btn_checkupdate.clicked.connect(self.mapm.check_update) 97 | # 98 | self.tabWidget.currentChanged.connect(self.adjust_tab_height) 99 | 100 | def adjust_tab_height(self): 101 | current_index = self.tabWidget.currentIndex() 102 | if current_index == 0: 103 | self.setFixedHeight(312) 104 | else: 105 | self.setFixedHeight(500) # 根据内容计算高度 106 | 107 | def set_status_label(self, text: str): 108 | """ 109 | 创建提示 110 | """ 111 | self.info_status.setText(text) 112 | self.timer.start(2000) 113 | 114 | def clear_status_label(self): 115 | self.info_status.clear() 116 | self.timer.stop() 117 | 118 | def get_key_by_masked(self, masked): 119 | key_list = self.conf.get_key_list() 120 | filtered_items = [key for key in key_list if key.startswith(masked[:8])] 121 | if len(filtered_items) > 0: 122 | return filtered_items[0] 123 | return "" 124 | 125 | def init_keyCombo(self): 126 | self.keyComboBox.clear() # 先清除 127 | # 如果 key 列表中没有值, keyComboBox 添加提示 128 | key_list = self.conf.get_key_list() 129 | if len(key_list) == 0: 130 | self.keyComboBox.addItem("请添加 key") 131 | self.pushButton_delete.setEnabled(False) 132 | self.pushButton_copy.setEnabled(False) 133 | else: 134 | self.keyComboBox.addItems([f"{key[:8]}****" for key in key_list]) 135 | # 将 keyComboBox 的当前值改为当前选用的 key 136 | current_key = self.conf.get_key() 137 | index = key_list.index(current_key) 138 | self.keyComboBox.setCurrentIndex(index) 139 | self.pushButton_delete.setEnabled(True) 140 | self.pushButton_copy.setEnabled(True) 141 | 142 | def copy_key(self): 143 | current_key = self.keyComboBox.currentText() 144 | full_key = self.get_key_by_masked(current_key) 145 | clipboard = QApplication.clipboard() 146 | clipboard.setText(full_key, ModeClipboard) 147 | self.set_status_label(f"已复制{current_key}到剪贴板") 148 | 149 | def handle_key_check(self, reply, key): 150 | key_list = self.conf.get_key_list() 151 | # 获取状态码 152 | status_code = reply.attribute(HttpStatusCodeAttribute) 153 | if reply.error() == NoError: 154 | response_data = reply.readAll() 155 | png_signature = b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" 156 | if response_data[:8] == png_signature: 157 | if self.keyComboBox.itemText(0) == "请添加 key": 158 | self.keyComboBox.removeItem(0) 159 | self.mLineEdit_key.setText("") 160 | key_list.append(key) 161 | self.conf.save_key_list(key_list) 162 | self.saveButton.setEnabled(False) 163 | self.set_status_label("保存成功") 164 | self.init_keyCombo() 165 | else: 166 | if status_code == 403: 167 | # 当期 2024-06-09 似乎不需要设置 Referer 也能正常访问 168 | # {'msg': '非法Key', 'resolve': '请到API控制台重新申请Key', 'code': 1} 169 | # {'msg': '域名不匹配', 'resolve': '请到API控制台查看域名配置并修改', 'code': 7} 170 | # {'msg': '权限类型错误', 'resolve': 'Key权限类型为:服务端,请使用服务端访问!', 'code': 12} 171 | # {'msg': '权限类型错误', 'resolve': '不支持的key类型!', 'code': 18} 172 | response_data = str(reply.readAll(), "utf-8", "ignore") 173 | json_data = json.loads(response_data) 174 | self.set_status_label( 175 | f"错误: {json_data.get('msg')} {json_data.get('resolve')}" 176 | ) 177 | elif status_code == 418: 178 | self.set_status_label("错误: 疑似攻击") 179 | else: 180 | self.set_status_label("未知错误") 181 | 182 | def save_key(self): 183 | key = self.mLineEdit_key.text() 184 | key_list = self.conf.get_key_list() 185 | if key in key_list: 186 | self.set_status_label("key 已存在") 187 | return 188 | url = tianditu_map_url("vec", key, "t0") 189 | tile_url = url.format(x=0, y=0, z=0) 190 | network_manager = QgsNetworkAccessManager.instance() 191 | request = make_request(tile_url, HEADER.get("Referer")) 192 | reply = network_manager.get(request) 193 | # reply.finished.connect(partial(self.handle_key_check, reply, key)) 194 | reply.finished.connect(lambda: self.handle_key_check(reply, key)) 195 | 196 | def select_key(self): 197 | if self.keyComboBox.count() > 0: 198 | masked_key = self.keyComboBox.currentText() 199 | key = self.get_key_by_masked(masked_key) 200 | self.conf.set_key(key) 201 | self.set_status_label(f"设置当前 key 为{masked_key}") 202 | 203 | def del_key(self): 204 | current_index = self.keyComboBox.currentIndex() 205 | key_list = self.conf.get_key_list() 206 | del key_list[current_index] 207 | self.conf.save_key_list(key_list) 208 | self.set_status_label("已删除") 209 | self.init_keyCombo() 210 | 211 | def handle_ping_finished(self, status): 212 | min_time = min(status) 213 | min_index = status.index(min_time) 214 | for i in range(8): 215 | self.comboBox.setItemText(i, f"t{i} {status[i]}") 216 | self.comboBox.setItemText(min_index, f"t{min_index} {status[min_index]}*") 217 | 218 | def on_key_LineEdit_changed(self): 219 | current_text = self.mLineEdit_key.text() 220 | # 删除key中的空格以及非打印字符 221 | filtered_text = "".join( 222 | [c for c in current_text if c.isprintable() and not c.isspace()] 223 | ) 224 | if filtered_text != current_text: 225 | self.mLineEdit_key.setText(filtered_text) 226 | # 检查key格式 227 | if len(self.mLineEdit_key.text()) > 0: 228 | key_format = check_key_format(self.mLineEdit_key.text()) 229 | if key_format["key_length_error"]: 230 | self.info_status.setText("无效key: 格式错误(长度不对)") 231 | self.saveButton.setEnabled(False) 232 | elif key_format["has_special_character"]: 233 | self.info_status.setText("无效key: 含非常规字符") 234 | self.saveButton.setEnabled(False) 235 | else: 236 | self.info_status.setText("点击保存") 237 | self.saveButton.setEnabled(True) 238 | else: 239 | self.info_status.clear() 240 | 241 | def select_subdomain(self): 242 | selected_index = self.subdomainComboBox.currentIndex() 243 | selected_domain = self.subdomain_list[selected_index] 244 | self.conf.set_value("Tianditu/subdomain", selected_domain) 245 | self.set_status_label(f"设置 subdomain 为 {selected_domain}") 246 | 247 | def enable_random(self): 248 | if self.checkBox_domain_rand.isChecked(): 249 | self.conf.set_value("Tianditu/random", True) 250 | self.subdomainComboBox.setEnabled(False) 251 | self.set_status_label("设置 subdomain 为 随机") 252 | else: 253 | self.conf.set_value("Tianditu/random", False) 254 | self.subdomainComboBox.setEnabled(True) 255 | 256 | def enable_key_random(self): 257 | if self.checkBox_key_rand.isChecked(): 258 | self.conf.set_value("Tianditu/random_key", True) 259 | self.keyComboBox.setEnabled(False) 260 | self.pushButton_copy.setEnabled(False) 261 | self.pushButton_delete.setEnabled(False) 262 | self.set_status_label("设置 key 为 随机") 263 | else: 264 | self.conf.set_value("Tianditu/random_key", False) 265 | self.keyComboBox.setEnabled(True) 266 | self.pushButton_copy.setEnabled(True) 267 | self.pushButton_delete.setEnabled(True) 268 | 269 | def closeEvent(self, event): 270 | # 在对话框关闭时触发的事件 271 | self.mapm.update_map_enable_state() 272 | self.toolbar.add_button.setup_action() 273 | event.accept() # 接受关闭事件,关闭对话框 274 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.12" 4 | resolution-markers = [ 5 | "sys_platform == 'win32'", 6 | "sys_platform != 'win32'", 7 | ] 8 | 9 | [manifest] 10 | constraints = [{ name = "pyqt5-qt5", marker = "sys_platform == 'win32'", specifier = "<=5.15.2" }] 11 | 12 | [[package]] 13 | name = "pyqt5" 14 | version = "5.15.11" 15 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 16 | dependencies = [ 17 | { name = "pyqt5-qt5" }, 18 | { name = "pyqt5-sip" }, 19 | ] 20 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52" } 21 | wheels = [ 22 | { url = "https://mirrors.aliyun.com/pypi/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2" }, 23 | { url = "https://mirrors.aliyun.com/pypi/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6" }, 24 | { url = "https://mirrors.aliyun.com/pypi/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1" }, 25 | { url = "https://mirrors.aliyun.com/pypi/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd" }, 26 | { url = "https://mirrors.aliyun.com/pypi/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517" }, 27 | ] 28 | 29 | [[package]] 30 | name = "pyqt5-qt5" 31 | version = "5.15.2" 32 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 33 | wheels = [ 34 | { url = "https://mirrors.aliyun.com/pypi/packages/62/09/99a222b0360616250fb2e6003a54e43a2a06b0774f0f8d5daafb86a2c375/PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154" }, 35 | { url = "https://mirrors.aliyun.com/pypi/packages/83/d4/241a6a518d0bcf0a9fcdcbad5edfed18d43e884317eab8d5230a2b27e206/PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a" }, 36 | { url = "https://mirrors.aliyun.com/pypi/packages/1c/7e/ce7c66a541a105fa98b41d6405fe84940564695e29fc7dccf6d9e8c5f898/PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327" }, 37 | { url = "https://mirrors.aliyun.com/pypi/packages/37/97/5d3b222b924fa2ed4c2488925155cd0b03fd5d09ee1cfcf7c553c11c9f66/PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962" }, 38 | ] 39 | 40 | [[package]] 41 | name = "pyqt5-sip" 42 | version = "12.17.0" 43 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 44 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/01/79/086b50414bafa71df494398ad277d72e58229a3d1c1b1c766d12b14c2e6d/pyqt5_sip-12.17.0.tar.gz", hash = "sha256:682dadcdbd2239af9fdc0c0628e2776b820e128bec88b49b8d692fe682f90b4f" } 45 | wheels = [ 46 | { url = "https://mirrors.aliyun.com/pypi/packages/a3/e6/e51367c28d69b5a462f38987f6024e766fd8205f121fe2f4d8ba2a6886b9/PyQt5_sip-12.17.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ea08341c8a5da00c81df0d689ecd4ee47a95e1ecad9e362581c92513f2068005" }, 47 | { url = "https://mirrors.aliyun.com/pypi/packages/64/3b/e6d1f772b41d8445d6faf86cc9da65910484ebd9f7df83abc5d4955437d0/PyQt5_sip-12.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a92478d6808040fbe614bb61500fbb3f19f72714b99369ec28d26a7e3494115" }, 48 | { url = "https://mirrors.aliyun.com/pypi/packages/ed/c5/d17fc2ddb9156a593710c88afd98abcf4055a2224b772f8bec2c6eea879c/PyQt5_sip-12.17.0-cp312-cp312-win32.whl", hash = "sha256:b0ff280b28813e9bfd3a4de99490739fc29b776dc48f1c849caca7239a10fc8b" }, 49 | { url = "https://mirrors.aliyun.com/pypi/packages/fe/c5/1174988d52c732d07033cf9a5067142b01d76be7731c6394a64d5c3ef65c/PyQt5_sip-12.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:54c31de7706d8a9a8c0fc3ea2c70468aba54b027d4974803f8eace9c22aad41c" }, 50 | { url = "https://mirrors.aliyun.com/pypi/packages/fd/5d/f234e505af1a85189310521447ebc6052ebb697efded850d0f2b2555f7aa/PyQt5_sip-12.17.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c7a7ff355e369616b6bcb41d45b742327c104b2bf1674ec79b8d67f8f2fa9543" }, 51 | { url = "https://mirrors.aliyun.com/pypi/packages/cd/cb/3b2050e9644d0021bdf25ddf7e4c3526e1edd0198879e76ba308e5d44faf/PyQt5_sip-12.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:419b9027e92b0b707632c370cfc6dc1f3b43c6313242fc4db57a537029bd179c" }, 52 | { url = "https://mirrors.aliyun.com/pypi/packages/51/61/b8ebde7e0b32d0de44c521a0ace31439885b0423d7d45d010a2f7d92808c/PyQt5_sip-12.17.0-cp313-cp313-win32.whl", hash = "sha256:351beab964a19f5671b2a3e816ecf4d3543a99a7e0650f88a947fea251a7589f" }, 53 | { url = "https://mirrors.aliyun.com/pypi/packages/15/ed/ff94d6b2910e7627380cb1fc9a518ff966e6d78285c8e54c9422b68305db/PyQt5_sip-12.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:672c209d05661fab8e17607c193bf43991d268a1eefbc2c4551fbf30fd8bb2ca" }, 54 | ] 55 | 56 | [[package]] 57 | name = "pyqt6" 58 | version = "6.8.1" 59 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 60 | dependencies = [ 61 | { name = "pyqt6-qt6" }, 62 | { name = "pyqt6-sip" }, 63 | ] 64 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ce/bf/ff284a136b39cb1873c18e4fca4a40a8847c84a1910c5fb38c6a77868968/pyqt6-6.8.1.tar.gz", hash = "sha256:91d937d6166274fafd70f4dee11a8da6dbfdb0da53de05f5d62361ddf775e256" } 65 | wheels = [ 66 | { url = "https://mirrors.aliyun.com/pypi/packages/09/da/70971b3d7f53a68644ea32544d3786dfbbb162d18572ac1defcf5a6481d5/PyQt6-6.8.1-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:0425f9eebdd5d4e57ab36424c9382f2ea06670c3c550fa0028c2b19bd0a1d7bd" }, 67 | { url = "https://mirrors.aliyun.com/pypi/packages/be/25/a4392c323a0fb97eb5f449b7594f37e93d9794b900756b43cd65772def77/PyQt6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:36bf48e3df3a6ff536e703315d155480ef4e260396eb5469eb7a875bc5bb7ab4" }, 68 | { url = "https://mirrors.aliyun.com/pypi/packages/de/a3/e528b4cc3394f2ae15b531c17f27b53de756a8c0404dfa9c184502367c48/PyQt6-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:2eac2267a34828b8db7660dd3cc3b3b5fd76a92e61ad45471565b01221cb558b" }, 69 | { url = "https://mirrors.aliyun.com/pypi/packages/f2/69/11404cfcb916bd7207805c21432ecab0401779361d48b67f28ae9337f70d/PyQt6-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:70bad7b890a8f9e9e5fb9598c544b832d9d9d99a9519e0009cb29c1e15e96632" }, 70 | { url = "https://mirrors.aliyun.com/pypi/packages/00/2a/21a555aea9bc8abc4f09017b922dbdf509c421f70506d4c83d2e8f4315b2/PyQt6-6.8.1-cp39-abi3-win_arm64.whl", hash = "sha256:a40f878e8e5eeeb0bba995152d07eeef9375ea0116df0f4aad0a6b97c8ad1175" }, 71 | ] 72 | 73 | [[package]] 74 | name = "pyqt6-qt6" 75 | version = "6.8.2" 76 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 77 | wheels = [ 78 | { url = "https://mirrors.aliyun.com/pypi/packages/1e/a4/3d764e05955382b3dc7227cbfde090700edd63431147f1c66d428ccac45c/PyQt6_Qt6-6.8.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:470dd4211fe5a67b0565e0202e7aa67816e5dcf7d713528b88327adaebd0934e" }, 79 | { url = "https://mirrors.aliyun.com/pypi/packages/d6/b3/6d4f8257b46554fb2c89b33a6773a3f05ed961b3cd83828caee5dc79899f/PyQt6_Qt6-6.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40cda901a3e1617e79225c354fe9d89b80249f0a6c6aaa18b40938e05bbf7d1f" }, 80 | { url = "https://mirrors.aliyun.com/pypi/packages/92/95/0036435b9e2cbd22e08f14eec2362c32fc641660c6e4aea6f59d165cb5fc/PyQt6_Qt6-6.8.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:fb6d0acdd7d43c33fb8b9d2dd7922d381cdedd00da316049fbe01fc1973e6f05" }, 81 | { url = "https://mirrors.aliyun.com/pypi/packages/6e/fb/c01dde044eca1542d88cac72fc99369af76a981cc2f52790236efa566e01/PyQt6_Qt6-6.8.2-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:5970c85d22cbe5c476418994549161b23ed938e25b04fc4ca8fabf6dcac7b03f" }, 82 | { url = "https://mirrors.aliyun.com/pypi/packages/1a/f7/31f03a9f5e6c7cc23ceb2bd0d9c2df0518837f7af0e693e15b6e0881b8b0/PyQt6_Qt6-6.8.2-py3-none-win_amd64.whl", hash = "sha256:28e2bb641f05b01e498503c3ef01c8a919d6e0e96b50230301c0baac2b7d1433" }, 83 | { url = "https://mirrors.aliyun.com/pypi/packages/00/c9/102c9537795ca11c12120ec9d5f554d9437787f52d8e23fbc8269e6a2699/PyQt6_Qt6-6.8.2-py3-none-win_arm64.whl", hash = "sha256:912afdddd0dfc666ce1c16bc4695e2acd680db72343e4f7a2b7c053a0146b4bc" }, 84 | ] 85 | 86 | [[package]] 87 | name = "pyqt6-sip" 88 | version = "13.10.0" 89 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 90 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/90/18/0405c54acba0c8e276dd6f0601890e6e735198218d031a6646104870fe22/pyqt6_sip-13.10.0.tar.gz", hash = "sha256:d6daa95a0bd315d9ec523b549e0ce97455f61ded65d5eafecd83ed2aa4ae5350" } 91 | wheels = [ 92 | { url = "https://mirrors.aliyun.com/pypi/packages/69/81/66d9bdacb790592a0641378749a047f12e3b254cdc2cb51f7ed636cf01d2/PyQt6_sip-13.10.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:48791db2914fc39c3218519a02d2a5fd3fcd354a1be3141a57bf2880701486f2" }, 93 | { url = "https://mirrors.aliyun.com/pypi/packages/26/2c/4796c209009a018e0d4a5c406d5a519234c5a378f370dc679d0ad5f455b2/PyQt6_sip-13.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:466d6b4791973c9fcbdc2e0087ed194b9ea802a8c3948867a849498f0841c70c" }, 94 | { url = "https://mirrors.aliyun.com/pypi/packages/99/34/2ec54bd475f0a811df1d32be485f2344cf9e8b388ce7adb26b46ce5552d4/PyQt6_sip-13.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ae15358941f127cd3d1ab09c1ebd45c4dabb0b2e91587b9eebde0279d0039c54" }, 95 | { url = "https://mirrors.aliyun.com/pypi/packages/0c/e4/82099bb4ab8bc152b5718541e93c0b3adf7566c0f307c9e58e2368b8c517/PyQt6_sip-13.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad573184fa8b00041944e5a17d150ab0d08db2d2189e39c9373574ebab3f2e58" }, 96 | { url = "https://mirrors.aliyun.com/pypi/packages/e3/09/90e0378887a3cb9664da77061229cf8e97e6ec25a5611b7dbc9cc3e02c78/PyQt6_sip-13.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:2d579d810d0047d40bde9c6aef281d6ed218db93c9496ebc9e55b9e6f27a229d" }, 97 | { url = "https://mirrors.aliyun.com/pypi/packages/6b/0c/8d1de48b45b565a46bf4757341f13f9b1853a7d2e6b023700f0af2c213ab/PyQt6_sip-13.10.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7b6e250c2e7c14702a623f2cc1479d7fb8db2b6eee9697cac10d06fe79c281bb" }, 98 | { url = "https://mirrors.aliyun.com/pypi/packages/af/13/e2cc2b667a9f5d44c2d0e18fa6e1066fca3f4521dcb301f4b5374caeb33e/PyQt6_sip-13.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fcb30756568f8cd59290f9ef2ae5ee3e72ff9cdd61a6f80c9e3d3b95ae676be" }, 99 | { url = "https://mirrors.aliyun.com/pypi/packages/20/1a/5c6fcae85edb65cf236c9dc6d23b279b5316e94cdca1abdee6d0a217ddbb/PyQt6_sip-13.10.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:757ac52c92b2ef0b56ecc7cd763b55a62d3c14271d7ea8d03315af85a70090ff" }, 100 | { url = "https://mirrors.aliyun.com/pypi/packages/b9/db/6924ec985be7d746772806b96ab81d24263ef72f0249f0573a82adaed75e/PyQt6_sip-13.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:571900c44a3e38738d696234d94fe2043972b9de0633505451c99e2922cb6a34" }, 101 | { url = "https://mirrors.aliyun.com/pypi/packages/77/c3/9e44729b582ee7f1d45160e8c292723156889f3e38ce6574f88d5ab8fa02/PyQt6_sip-13.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:39cba2cc71cf80a99b4dc8147b43508d4716e128f9fb99f5eb5860a37f082282" }, 102 | ] 103 | 104 | [[package]] 105 | name = "qgis-plugin-tianditu" 106 | version = "0.5.2" 107 | source = { virtual = "." } 108 | dependencies = [ 109 | { name = "pyqt5" }, 110 | { name = "pyqt6" }, 111 | ] 112 | 113 | [package.metadata] 114 | requires-dist = [ 115 | { name = "pyqt5", specifier = ">=5.15.11" }, 116 | { name = "pyqt6", specifier = ">=6.8.1" }, 117 | ] 118 | --------------------------------------------------------------------------------