ymgis 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. ymgis-0.1.0/.gitignore +30 -0
  2. ymgis-0.1.0/LICENSE +21 -0
  3. ymgis-0.1.0/PKG-INFO +259 -0
  4. ymgis-0.1.0/README.md +229 -0
  5. ymgis-0.1.0/examples/basic_viewer.py +124 -0
  6. ymgis-0.1.0/examples/online_basemap.py +187 -0
  7. ymgis-0.1.0/pyproject.toml +67 -0
  8. ymgis-0.1.0/src/ymgis/__about__.py +1 -0
  9. ymgis-0.1.0/src/ymgis/__init__.py +85 -0
  10. ymgis-0.1.0/src/ymgis/canvas/__init__.py +5 -0
  11. ymgis-0.1.0/src/ymgis/canvas/map_canvas.py +218 -0
  12. ymgis-0.1.0/src/ymgis/crs.py +142 -0
  13. ymgis-0.1.0/src/ymgis/extent.py +92 -0
  14. ymgis-0.1.0/src/ymgis/layers/__init__.py +41 -0
  15. ymgis-0.1.0/src/ymgis/layers/base.py +96 -0
  16. ymgis-0.1.0/src/ymgis/layers/manager.py +92 -0
  17. ymgis-0.1.0/src/ymgis/layers/raster.py +173 -0
  18. ymgis-0.1.0/src/ymgis/layers/services/__init__.py +29 -0
  19. ymgis-0.1.0/src/ymgis/layers/services/arcgis.py +83 -0
  20. ymgis-0.1.0/src/ymgis/layers/services/baidu.py +179 -0
  21. ymgis-0.1.0/src/ymgis/layers/services/baidu_math.py +91 -0
  22. ymgis-0.1.0/src/ymgis/layers/services/http_image.py +60 -0
  23. ymgis-0.1.0/src/ymgis/layers/services/providers.py +125 -0
  24. ymgis-0.1.0/src/ymgis/layers/services/supermap.py +67 -0
  25. ymgis-0.1.0/src/ymgis/layers/services/viewport.py +135 -0
  26. ymgis-0.1.0/src/ymgis/layers/services/webmercator.py +74 -0
  27. ymgis-0.1.0/src/ymgis/layers/services/wms.py +66 -0
  28. ymgis-0.1.0/src/ymgis/layers/services/xyz.py +164 -0
  29. ymgis-0.1.0/src/ymgis/layers/vector.py +173 -0
  30. ymgis-0.1.0/src/ymgis/py.typed +1 -0
  31. ymgis-0.1.0/src/ymgis/tools/__init__.py +15 -0
  32. ymgis-0.1.0/src/ymgis/tools/base.py +49 -0
  33. ymgis-0.1.0/src/ymgis/tools/draw_point.py +31 -0
  34. ymgis-0.1.0/src/ymgis/tools/draw_polygon.py +108 -0
  35. ymgis-0.1.0/src/ymgis/tools/draw_rect.py +75 -0
  36. ymgis-0.1.0/src/ymgis/tools/pan.py +55 -0
  37. ymgis-0.1.0/src/ymgis/transform.py +45 -0
  38. ymgis-0.1.0/src/ymgis/widgets/__init__.py +5 -0
  39. ymgis-0.1.0/src/ymgis/widgets/layer_tree.py +73 -0
  40. ymgis-0.1.0/tests/test_crs.py +67 -0
  41. ymgis-0.1.0/tests/test_extent.py +21 -0
  42. ymgis-0.1.0/tests/test_services.py +55 -0
ymgis-0.1.0/.gitignore ADDED
@@ -0,0 +1,30 @@
1
+ # Build / publish artifacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ .eggs/
6
+ upload_log.txt
7
+
8
+ # Local secrets — never commit
9
+ scripts/publish.local.ps1
10
+ .env
11
+ .pypirc
12
+ PyPI-Recovery-Codes*.txt
13
+ *Recovery-Codes*
14
+ *API*token*
15
+ *.pem
16
+
17
+ # Python / tooling
18
+ __pycache__/
19
+ .pytest_cache/
20
+ .ruff_cache/
21
+ .mypy_cache/
22
+ .venv/
23
+ venv/
24
+ *.pyc
25
+
26
+ # OS / IDE
27
+ .DS_Store
28
+ Thumbs.db
29
+ .idea/
30
+ .vscode/
ymgis-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ymgis-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.5
2
+ Name: ymgis
3
+ Version: 0.1.0
4
+ Summary: Lightweight PyQt GIS canvas with dynamic CRS reprojection, online basemaps and drawing tools
5
+ Project-URL: Homepage, https://github.com/yima/ymgis
6
+ Project-URL: Issues, https://github.com/yima/ymgis/issues
7
+ Author: Yima
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: arcgis,canvas,geotiff,gis,pyqt,raster,shapely,tianditu,wms,xyz
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: GIS
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: numpy>=1.24
20
+ Requires-Dist: pyproj>=3.6
21
+ Requires-Dist: pyqt6>=6.6
22
+ Requires-Dist: rasterio>=1.3
23
+ Requires-Dist: shapely>=2.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1.2; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.6; extra == 'dev'
28
+ Requires-Dist: twine>=5.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # ymgis
32
+
33
+ 面向桌面 GIS 小工具的轻量组件库:**画布 + 图层管理 + 图层树 + 基础绘制 + 在线地图服务预览 + 动态重投影**。
34
+
35
+ 不依赖 QGIS。基于 **PyQt6 + rasterio + Shapely + pyproj**,可发布到 PyPI,供 QuickAnno、下载器、质检工具等复用。
36
+
37
+ ## 能力概览
38
+
39
+ | 能力 | 说明 |
40
+ |------|------|
41
+ | 地图画布 | 平移、滚轮缩放、图层栈渲染、地图工具切换 |
42
+ | 动态重投影 | 可叠加不同坐标系数据;**视图 CRS 由第一个带 CRS 的图层决定** |
43
+ | 本地栅格 | GeoTIFF(rasterio)按视口窗口读取;跨 CRS 时 WarpedVRT 重投影 |
44
+ | 矢量标注 | 内存 Shapely 点/线/面 + 简单样式;渲染时按需重投影 |
45
+ | 图层树 | 勾选显隐、图层列表 |
46
+ | 绘制工具 | 平移 / 点 / 矩形 / 多边形 |
47
+ | 切片底图 | 通用 XYZ/TMS;预设天地图、高德、百度、谷歌 |
48
+ | OGC WMS | `GetMap` 视口图像预览 |
49
+ | ArcGIS REST | MapServer / ImageServer `export`;亦可 XYZ `tile/{z}/{y}/{x}` |
50
+ | 超图 iServer | 地图 `image.png` 视口预览;亦可 `zxyTileImage` 瓦片 |
51
+
52
+ 在线服务图层自身为 **EPSG:3857**,叠到其它视图 CRS 时会动态重投影(AABB 近似)。高德/百度存在国内加密坐标偏移,适合底图预览,不宜与精密测绘数据直接叠合量测。
53
+
54
+ ## 安装
55
+
56
+ ```bash
57
+ # 开发态(本仓库)
58
+ pip install -e .
59
+
60
+ # 发布后
61
+ pip install ymgis
62
+ ```
63
+
64
+ ## 快速开始
65
+
66
+ ```python
67
+ from PyQt6.QtWidgets import QApplication, QMainWindow, QSplitter
68
+ from ymgis import MapCanvas, RasterLayer, VectorLayer, LayerTreeWidget, PanTool, DrawPolygonTool
69
+
70
+ app = QApplication([])
71
+ win = QMainWindow()
72
+ canvas = MapCanvas()
73
+ tree = LayerTreeWidget(canvas.layer_manager)
74
+
75
+ # 第一个带 CRS 的图层决定视图坐标系(无需手动 set_crs)
76
+ raster = RasterLayer(r"D:\data\demo.tif")
77
+ canvas.layer_manager.add_layer(raster)
78
+ canvas.zoom_to_full_extent()
79
+
80
+ # 标注层自动继承视图 CRS;绘制坐标为画布 CRS
81
+ annos = VectorLayer("标注")
82
+ canvas.layer_manager.add_layer(annos)
83
+
84
+ tool = DrawPolygonTool()
85
+ tool.polygon_drawn.connect(
86
+ lambda g: annos.add_feature(g, {"class": "demo"}, source_crs=canvas.crs)
87
+ )
88
+ canvas.set_map_tool(tool)
89
+
90
+ split = QSplitter()
91
+ split.addWidget(tree)
92
+ split.addWidget(canvas)
93
+ win.setCentralWidget(split)
94
+ win.resize(1200, 800)
95
+ win.show()
96
+ app.exec()
97
+ ```
98
+
99
+ ### 多坐标系叠加
100
+
101
+ ```python
102
+ from ymgis import MapCanvas, RasterLayer, VectorLayer, create_amap
103
+ from shapely.geometry import Point
104
+
105
+ canvas = MapCanvas()
106
+
107
+ # 先加载 CGCS2000 / 任意投影影像 → 视图 CRS = 该影像 CRS
108
+ canvas.layer_manager.add_layer(RasterLayer(r"D:\data\cgcs2000.tif"))
109
+
110
+ # 再叠加 WGS84 矢量 → 渲染时自动重投影到视图 CRS
111
+ pts = VectorLayer("点", crs="EPSG:4326")
112
+ pts.add_feature(Point(116.4, 39.9))
113
+ canvas.layer_manager.add_layer(pts)
114
+
115
+ # 在线底图(EPSG:3857)也会重投影到当前视图
116
+ canvas.layer_manager.add_layer(create_amap(), to_top=False)
117
+
118
+ canvas.zoom_to_full_extent()
119
+ # 如需强制指定视图 CRS(并锁定,不再被首层覆盖):
120
+ # canvas.set_crs("EPSG:3857")
121
+ ```
122
+
123
+ ### 在线底图 / 地图服务预览
124
+
125
+ ```python
126
+ from ymgis import (
127
+ MapCanvas,
128
+ create_amap,
129
+ create_baidu,
130
+ create_google,
131
+ create_tianditu,
132
+ WmsLayer,
133
+ ArcGisRestLayer,
134
+ SuperMapRestLayer,
135
+ )
136
+
137
+ canvas = MapCanvas()
138
+
139
+ canvas.layer_manager.add_layer(create_amap(), to_top=False)
140
+ # canvas.layer_manager.add_layer(create_baidu(), to_top=False)
141
+ # canvas.layer_manager.add_layer(create_google(), to_top=False)
142
+ # canvas.layer_manager.add_layer(create_tianditu("你的天地图tk"), to_top=False)
143
+
144
+ # WMS / ArcGIS / 超图
145
+ # canvas.layer_manager.add_layer(WmsLayer("https://host/wms", "workspace:layer"))
146
+ # canvas.layer_manager.add_layer(
147
+ # ArcGisRestLayer("https://host/arcgis/rest/services/Demo/MapServer")
148
+ # )
149
+ # canvas.layer_manager.add_layer(
150
+ # SuperMapRestLayer("http://host:8090/iserver/services/map-demo/rest/maps/Demo")
151
+ # )
152
+
153
+ canvas.zoom_to_full_extent()
154
+ ```
155
+
156
+ 自定义 XYZ:
157
+
158
+ ```python
159
+ from ymgis import XyzTileLayer
160
+
161
+ layer = XyzTileLayer(
162
+ "https://tile.example.com/{z}/{x}/{y}.png",
163
+ name="自定义瓦片",
164
+ subdomains=["a", "b", "c"], # 对应模板 {s}
165
+ )
166
+ ```
167
+
168
+ 运行示例:
169
+
170
+ ```bash
171
+ pip install -e .
172
+ python examples/basic_viewer.py
173
+ # 可选传入 GeoTIFF:
174
+ python examples/basic_viewer.py D:\data\demo.tif
175
+
176
+ # 在线底图 / WMS / ArcGIS / 超图预览:
177
+ python examples/online_basemap.py
178
+ # 天地图也可:set TIANDITU_TOKEN=你的tk
179
+ ```
180
+
181
+ ## 模块
182
+
183
+ | 模块 | 说明 |
184
+ |------|------|
185
+ | `MapCanvas` | 地图画布;首层定 CRS、动态重投影渲染 |
186
+ | `LayerManager` | 图层栈;`combined_extent(target_crs)` |
187
+ | `RasterLayer` | GeoTIFF 窗口读取 + WarpedVRT 重投影 |
188
+ | `VectorLayer` | 内存 Shapely 要素;可指定 `crs=` |
189
+ | `ymgis.crs` | `transform_xy` / `transform_extent` / `transform_geometry` |
190
+ | `XyzTileLayer` | 通用 XYZ/TMS 异步瓦片底图 |
191
+ | `BaiduTileLayer` / `create_*` | 百度及天地图/高德/谷歌预设 |
192
+ | `WmsLayer` | OGC WMS GetMap 视口预览 |
193
+ | `ArcGisRestLayer` | ArcGIS REST export 预览 |
194
+ | `SuperMapRestLayer` | 超图 iServer 地图预览 |
195
+ | `LayerTreeWidget` | 勾选显隐、图层列表 |
196
+ | `PanTool` / `DrawPointTool` / `DrawRectTool` / `DrawPolygonTool` | 交互工具 |
197
+
198
+ ## 设计约定
199
+
200
+ - **视图 CRS**:由**第一个加载且带有 CRS 的图层**自动设定;`canvas.set_crs(...)` 可显式指定并锁定。
201
+ - **动态重投影**:后续图层 CRS 不同时,在渲染/`combined_extent` 时转换到视图 CRS(依赖 pyproj;栅格用 WarpedVRT)。
202
+ - 无 CRS 的图层(如空标注层)加入后自动继承当前视图 CRS;绘制工具产出的几何为画布 CRS。
203
+ - 在线切片/WMS 图层原生多为 **EPSG:3857**;瓦片异步加载并带内存缓存。
204
+ - 栅格按视口读取并缩放到控件尺寸,避免整图进内存。
205
+
206
+ ## 与 QGIS / GeoD 的关系
207
+
208
+ - **不是** QGIS 替代品:无工程文件、无复杂符号、无拓扑规则。
209
+ - 可预览常见在线底图与 OGC/REST 地图服务,但**不是** GeoD 那种批量网图下载器。
210
+ - 目标:多个内部小工具共用同一套画布/图层树,安装包体积远小于捆绑 `qgis_runtime`。
211
+
212
+ ## 发布到 PyPI
213
+
214
+ 推荐使用脚本(Windows PowerShell):
215
+
216
+ ```powershell
217
+ # 首次:复制并填写 token(不要提交 publish.local.ps1)
218
+ copy scripts\publish.local.ps1.example scripts\publish.local.ps1
219
+ # 编辑 scripts\publish.local.ps1,将 $PyPiToken 设为 pypi-... 开头的 API token
220
+
221
+ .\scripts\publish.ps1
222
+ # 或:.\scripts\publish.ps1 -Token "pypi-你的完整token"
223
+ # 测试库:.\scripts\publish.ps1 -TestPyPI
224
+ ```
225
+
226
+ 也可手动:
227
+
228
+ ```bash
229
+ pip install -e ".[dev]"
230
+ python -m build
231
+ # Username=__token__ Password=pypi-...
232
+ twine upload dist/*
233
+ ```
234
+
235
+ 请先在 [PyPI](https://pypi.org) 确认包名 `ymgis` 可用;若冲突可改为 `yima-gis` 并同步修改 `pyproject.toml` 的 `name`。若已上传过同一版本,需先升高 `pyproject.toml` 的 `version`。
236
+
237
+ ## 目录
238
+
239
+ ```
240
+ ymgis/
241
+ ├── pyproject.toml
242
+ ├── README.md
243
+ ├── LICENSE
244
+ ├── examples/
245
+ │ ├── basic_viewer.py
246
+ │ └── online_basemap.py
247
+ ├── src/ymgis/
248
+ │ ├── crs.py # 动态重投影
249
+ │ ├── canvas/
250
+ │ ├── layers/
251
+ │ │ └── services/ # XYZ / WMS / ArcGIS / 超图 / 厂商预设
252
+ │ ├── tools/
253
+ │ └── widgets/
254
+ └── tests/
255
+ ```
256
+
257
+ ## License
258
+
259
+ MIT
ymgis-0.1.0/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # ymgis
2
+
3
+ 面向桌面 GIS 小工具的轻量组件库:**画布 + 图层管理 + 图层树 + 基础绘制 + 在线地图服务预览 + 动态重投影**。
4
+
5
+ 不依赖 QGIS。基于 **PyQt6 + rasterio + Shapely + pyproj**,可发布到 PyPI,供 QuickAnno、下载器、质检工具等复用。
6
+
7
+ ## 能力概览
8
+
9
+ | 能力 | 说明 |
10
+ |------|------|
11
+ | 地图画布 | 平移、滚轮缩放、图层栈渲染、地图工具切换 |
12
+ | 动态重投影 | 可叠加不同坐标系数据;**视图 CRS 由第一个带 CRS 的图层决定** |
13
+ | 本地栅格 | GeoTIFF(rasterio)按视口窗口读取;跨 CRS 时 WarpedVRT 重投影 |
14
+ | 矢量标注 | 内存 Shapely 点/线/面 + 简单样式;渲染时按需重投影 |
15
+ | 图层树 | 勾选显隐、图层列表 |
16
+ | 绘制工具 | 平移 / 点 / 矩形 / 多边形 |
17
+ | 切片底图 | 通用 XYZ/TMS;预设天地图、高德、百度、谷歌 |
18
+ | OGC WMS | `GetMap` 视口图像预览 |
19
+ | ArcGIS REST | MapServer / ImageServer `export`;亦可 XYZ `tile/{z}/{y}/{x}` |
20
+ | 超图 iServer | 地图 `image.png` 视口预览;亦可 `zxyTileImage` 瓦片 |
21
+
22
+ 在线服务图层自身为 **EPSG:3857**,叠到其它视图 CRS 时会动态重投影(AABB 近似)。高德/百度存在国内加密坐标偏移,适合底图预览,不宜与精密测绘数据直接叠合量测。
23
+
24
+ ## 安装
25
+
26
+ ```bash
27
+ # 开发态(本仓库)
28
+ pip install -e .
29
+
30
+ # 发布后
31
+ pip install ymgis
32
+ ```
33
+
34
+ ## 快速开始
35
+
36
+ ```python
37
+ from PyQt6.QtWidgets import QApplication, QMainWindow, QSplitter
38
+ from ymgis import MapCanvas, RasterLayer, VectorLayer, LayerTreeWidget, PanTool, DrawPolygonTool
39
+
40
+ app = QApplication([])
41
+ win = QMainWindow()
42
+ canvas = MapCanvas()
43
+ tree = LayerTreeWidget(canvas.layer_manager)
44
+
45
+ # 第一个带 CRS 的图层决定视图坐标系(无需手动 set_crs)
46
+ raster = RasterLayer(r"D:\data\demo.tif")
47
+ canvas.layer_manager.add_layer(raster)
48
+ canvas.zoom_to_full_extent()
49
+
50
+ # 标注层自动继承视图 CRS;绘制坐标为画布 CRS
51
+ annos = VectorLayer("标注")
52
+ canvas.layer_manager.add_layer(annos)
53
+
54
+ tool = DrawPolygonTool()
55
+ tool.polygon_drawn.connect(
56
+ lambda g: annos.add_feature(g, {"class": "demo"}, source_crs=canvas.crs)
57
+ )
58
+ canvas.set_map_tool(tool)
59
+
60
+ split = QSplitter()
61
+ split.addWidget(tree)
62
+ split.addWidget(canvas)
63
+ win.setCentralWidget(split)
64
+ win.resize(1200, 800)
65
+ win.show()
66
+ app.exec()
67
+ ```
68
+
69
+ ### 多坐标系叠加
70
+
71
+ ```python
72
+ from ymgis import MapCanvas, RasterLayer, VectorLayer, create_amap
73
+ from shapely.geometry import Point
74
+
75
+ canvas = MapCanvas()
76
+
77
+ # 先加载 CGCS2000 / 任意投影影像 → 视图 CRS = 该影像 CRS
78
+ canvas.layer_manager.add_layer(RasterLayer(r"D:\data\cgcs2000.tif"))
79
+
80
+ # 再叠加 WGS84 矢量 → 渲染时自动重投影到视图 CRS
81
+ pts = VectorLayer("点", crs="EPSG:4326")
82
+ pts.add_feature(Point(116.4, 39.9))
83
+ canvas.layer_manager.add_layer(pts)
84
+
85
+ # 在线底图(EPSG:3857)也会重投影到当前视图
86
+ canvas.layer_manager.add_layer(create_amap(), to_top=False)
87
+
88
+ canvas.zoom_to_full_extent()
89
+ # 如需强制指定视图 CRS(并锁定,不再被首层覆盖):
90
+ # canvas.set_crs("EPSG:3857")
91
+ ```
92
+
93
+ ### 在线底图 / 地图服务预览
94
+
95
+ ```python
96
+ from ymgis import (
97
+ MapCanvas,
98
+ create_amap,
99
+ create_baidu,
100
+ create_google,
101
+ create_tianditu,
102
+ WmsLayer,
103
+ ArcGisRestLayer,
104
+ SuperMapRestLayer,
105
+ )
106
+
107
+ canvas = MapCanvas()
108
+
109
+ canvas.layer_manager.add_layer(create_amap(), to_top=False)
110
+ # canvas.layer_manager.add_layer(create_baidu(), to_top=False)
111
+ # canvas.layer_manager.add_layer(create_google(), to_top=False)
112
+ # canvas.layer_manager.add_layer(create_tianditu("你的天地图tk"), to_top=False)
113
+
114
+ # WMS / ArcGIS / 超图
115
+ # canvas.layer_manager.add_layer(WmsLayer("https://host/wms", "workspace:layer"))
116
+ # canvas.layer_manager.add_layer(
117
+ # ArcGisRestLayer("https://host/arcgis/rest/services/Demo/MapServer")
118
+ # )
119
+ # canvas.layer_manager.add_layer(
120
+ # SuperMapRestLayer("http://host:8090/iserver/services/map-demo/rest/maps/Demo")
121
+ # )
122
+
123
+ canvas.zoom_to_full_extent()
124
+ ```
125
+
126
+ 自定义 XYZ:
127
+
128
+ ```python
129
+ from ymgis import XyzTileLayer
130
+
131
+ layer = XyzTileLayer(
132
+ "https://tile.example.com/{z}/{x}/{y}.png",
133
+ name="自定义瓦片",
134
+ subdomains=["a", "b", "c"], # 对应模板 {s}
135
+ )
136
+ ```
137
+
138
+ 运行示例:
139
+
140
+ ```bash
141
+ pip install -e .
142
+ python examples/basic_viewer.py
143
+ # 可选传入 GeoTIFF:
144
+ python examples/basic_viewer.py D:\data\demo.tif
145
+
146
+ # 在线底图 / WMS / ArcGIS / 超图预览:
147
+ python examples/online_basemap.py
148
+ # 天地图也可:set TIANDITU_TOKEN=你的tk
149
+ ```
150
+
151
+ ## 模块
152
+
153
+ | 模块 | 说明 |
154
+ |------|------|
155
+ | `MapCanvas` | 地图画布;首层定 CRS、动态重投影渲染 |
156
+ | `LayerManager` | 图层栈;`combined_extent(target_crs)` |
157
+ | `RasterLayer` | GeoTIFF 窗口读取 + WarpedVRT 重投影 |
158
+ | `VectorLayer` | 内存 Shapely 要素;可指定 `crs=` |
159
+ | `ymgis.crs` | `transform_xy` / `transform_extent` / `transform_geometry` |
160
+ | `XyzTileLayer` | 通用 XYZ/TMS 异步瓦片底图 |
161
+ | `BaiduTileLayer` / `create_*` | 百度及天地图/高德/谷歌预设 |
162
+ | `WmsLayer` | OGC WMS GetMap 视口预览 |
163
+ | `ArcGisRestLayer` | ArcGIS REST export 预览 |
164
+ | `SuperMapRestLayer` | 超图 iServer 地图预览 |
165
+ | `LayerTreeWidget` | 勾选显隐、图层列表 |
166
+ | `PanTool` / `DrawPointTool` / `DrawRectTool` / `DrawPolygonTool` | 交互工具 |
167
+
168
+ ## 设计约定
169
+
170
+ - **视图 CRS**:由**第一个加载且带有 CRS 的图层**自动设定;`canvas.set_crs(...)` 可显式指定并锁定。
171
+ - **动态重投影**:后续图层 CRS 不同时,在渲染/`combined_extent` 时转换到视图 CRS(依赖 pyproj;栅格用 WarpedVRT)。
172
+ - 无 CRS 的图层(如空标注层)加入后自动继承当前视图 CRS;绘制工具产出的几何为画布 CRS。
173
+ - 在线切片/WMS 图层原生多为 **EPSG:3857**;瓦片异步加载并带内存缓存。
174
+ - 栅格按视口读取并缩放到控件尺寸,避免整图进内存。
175
+
176
+ ## 与 QGIS / GeoD 的关系
177
+
178
+ - **不是** QGIS 替代品:无工程文件、无复杂符号、无拓扑规则。
179
+ - 可预览常见在线底图与 OGC/REST 地图服务,但**不是** GeoD 那种批量网图下载器。
180
+ - 目标:多个内部小工具共用同一套画布/图层树,安装包体积远小于捆绑 `qgis_runtime`。
181
+
182
+ ## 发布到 PyPI
183
+
184
+ 推荐使用脚本(Windows PowerShell):
185
+
186
+ ```powershell
187
+ # 首次:复制并填写 token(不要提交 publish.local.ps1)
188
+ copy scripts\publish.local.ps1.example scripts\publish.local.ps1
189
+ # 编辑 scripts\publish.local.ps1,将 $PyPiToken 设为 pypi-... 开头的 API token
190
+
191
+ .\scripts\publish.ps1
192
+ # 或:.\scripts\publish.ps1 -Token "pypi-你的完整token"
193
+ # 测试库:.\scripts\publish.ps1 -TestPyPI
194
+ ```
195
+
196
+ 也可手动:
197
+
198
+ ```bash
199
+ pip install -e ".[dev]"
200
+ python -m build
201
+ # Username=__token__ Password=pypi-...
202
+ twine upload dist/*
203
+ ```
204
+
205
+ 请先在 [PyPI](https://pypi.org) 确认包名 `ymgis` 可用;若冲突可改为 `yima-gis` 并同步修改 `pyproject.toml` 的 `name`。若已上传过同一版本,需先升高 `pyproject.toml` 的 `version`。
206
+
207
+ ## 目录
208
+
209
+ ```
210
+ ymgis/
211
+ ├── pyproject.toml
212
+ ├── README.md
213
+ ├── LICENSE
214
+ ├── examples/
215
+ │ ├── basic_viewer.py
216
+ │ └── online_basemap.py
217
+ ├── src/ymgis/
218
+ │ ├── crs.py # 动态重投影
219
+ │ ├── canvas/
220
+ │ ├── layers/
221
+ │ │ └── services/ # XYZ / WMS / ArcGIS / 超图 / 厂商预设
222
+ │ ├── tools/
223
+ │ └── widgets/
224
+ └── tests/
225
+ ```
226
+
227
+ ## License
228
+
229
+ MIT
@@ -0,0 +1,124 @@
1
+ """Minimal ymgis viewer: layer tree + canvas + pan / draw polygon."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from PyQt6.QtGui import QAction, QActionGroup
9
+ from PyQt6.QtWidgets import (
10
+ QApplication,
11
+ QLabel,
12
+ QMainWindow,
13
+ QMessageBox,
14
+ QSplitter,
15
+ QStatusBar,
16
+ QToolBar,
17
+ QVBoxLayout,
18
+ QWidget,
19
+ )
20
+
21
+ # Allow running without install: add ../src
22
+ ROOT = Path(__file__).resolve().parents[1]
23
+ sys.path.insert(0, str(ROOT / "src"))
24
+
25
+ from ymgis import ( # noqa: E402
26
+ DrawPolygonTool,
27
+ DrawRectTool,
28
+ LayerTreeWidget,
29
+ MapCanvas,
30
+ PanTool,
31
+ RasterLayer,
32
+ VectorLayer,
33
+ )
34
+
35
+
36
+ class MainWindow(QMainWindow):
37
+ def __init__(self, raster_path: str | None = None) -> None:
38
+ super().__init__()
39
+ self.setWindowTitle("ymgis basic viewer")
40
+ self.resize(1280, 800)
41
+
42
+ self.canvas = MapCanvas()
43
+ self.tree = LayerTreeWidget(self.canvas.layer_manager)
44
+ self.annos = VectorLayer("标注")
45
+ self.canvas.layer_manager.add_layer(self.annos)
46
+
47
+ split = QSplitter()
48
+ left = QWidget()
49
+ left_layout = QVBoxLayout(left)
50
+ left_layout.setContentsMargins(4, 4, 4, 4)
51
+ left_layout.addWidget(QLabel("图层"))
52
+ left_layout.addWidget(self.tree)
53
+ split.addWidget(left)
54
+ split.addWidget(self.canvas)
55
+ split.setStretchFactor(1, 1)
56
+ self.setCentralWidget(split)
57
+
58
+ self._init_toolbar()
59
+ self._status = QStatusBar()
60
+ self.setStatusBar(self._status)
61
+ self.canvas.cursor_map_moved.connect(
62
+ lambda x, y: self._status.showMessage(f"X={x:.3f} Y={y:.3f}")
63
+ )
64
+
65
+ if raster_path:
66
+ self._load_raster(raster_path)
67
+
68
+ def _init_toolbar(self) -> None:
69
+ tb = QToolBar("工具")
70
+ self.addToolBar(tb)
71
+ group = QActionGroup(self)
72
+ group.setExclusive(True)
73
+
74
+ self._pan = PanTool(self)
75
+ self._rect = DrawRectTool(self)
76
+ self._poly = DrawPolygonTool(self)
77
+ self._rect.rect_drawn.connect(self._on_geom)
78
+ self._poly.polygon_drawn.connect(self._on_geom)
79
+
80
+ def add_tool(text: str, tool, checked: bool = False) -> None:
81
+ act = QAction(text, self)
82
+ act.setCheckable(True)
83
+ act.setChecked(checked)
84
+ group.addAction(act)
85
+ tb.addAction(act)
86
+ act.triggered.connect(lambda _=False, t=tool: self.canvas.set_map_tool(t))
87
+
88
+ add_tool("平移", self._pan, True)
89
+ add_tool("矩形", self._rect)
90
+ add_tool("多边形", self._poly)
91
+ self.canvas.set_map_tool(self._pan)
92
+
93
+ act_full = QAction("全图", self)
94
+ act_full.triggered.connect(self.canvas.zoom_to_full_extent)
95
+ tb.addAction(act_full)
96
+
97
+ def _on_geom(self, geom) -> None:
98
+ self.annos.add_feature(geom, {"source": "draw"})
99
+ self._status.showMessage(f"已添加要素,共 {self.annos.feature_count()} 个", 3000)
100
+
101
+ def _load_raster(self, path: str) -> None:
102
+ try:
103
+ layer = RasterLayer(path)
104
+ except Exception as ex:
105
+ QMessageBox.warning(self, "打开失败", str(ex))
106
+ return
107
+ self.canvas.layer_manager.add_layer(layer, to_top=False)
108
+ # 视图 CRS 由第一个带坐标系的图层自动确定;也可 canvas.set_crs(...) 锁定
109
+ self.canvas.zoom_to_full_extent()
110
+ self._status.showMessage(f"已加载: {path}")
111
+
112
+
113
+ def main() -> None:
114
+ app = QApplication(sys.argv)
115
+ path = sys.argv[1] if len(sys.argv) > 1 else None
116
+ win = MainWindow(path)
117
+ win.show()
118
+ if path is None:
119
+ win.statusBar().showMessage("未指定影像:可传入 GeoTIFF 路径,或仅试用矢量绘制")
120
+ sys.exit(app.exec())
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()