`——EPSG组织分配的ID——使用 [`createFromOgcWms()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromOgcWmsCrs)
30 | - `POSTGIS:`——PostGIS数据库使用的ID——使用[`createFromSrid()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromSrid)
31 | - `INTERNAL:`——QGIS内部数据库中使用的ID,使用[`createFromSrsId()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromSrsId)
32 | - `PROJ:`——使用[`createFromProj()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromProj)
33 | - `WKT:`——使用[`createFromWkt()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromWkt)
34 |
35 | 如果未指定前缀,则默认使用WKT定义。
36 |
37 | - 通过WKT指定CRS
38 |
39 | ```python
40 | wkt = 'GEOGCS["WGS84", DATUM["WGS84", SPHEROID["WGS84", 6378137.0, 298.257223563]],' \
41 | 'PRIMEM["Greenwich", 0.0], UNIT["degree",0.017453292519943295],' \
42 | 'AXIS["Longitude",EAST], AXIS["Latitude",NORTH]]'
43 | crs = QgsCoordinateReferenceSystem(wkt)
44 | print(crs.isValid())
45 | # True
46 | ```
47 |
48 | - 创建一个无效的CRS,然后使用其中一个`create*`函数进行初始化。在下面的示例中,我们使用Proj字符串初始化投影。
49 |
50 | ```python
51 | crs = QgsCoordinateReferenceSystem()
52 | crs.createFromProj("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
53 | print(crs.isValid())
54 | # True
55 | ```
56 |
57 | 检查CRS的创建(即在数据库中查找)是否成功是明智的:[`isValid()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.isValid)必须返回`True`。
58 |
59 | 请注意,对于空间参考系统的初始化,QGIS需要在其内部数据库`srs.db`中查找适当的值。因此,如果你创建一个独立的应用程序,你需要使用[`QgsApplication.setPrefixPath()`](https://qgis.org/pyqgis/master/core/QgsApplication.html#qgis.core.QgsApplication.setPrefixPath)正确设置路径 ,否则将无法找到数据库。如果你在QGIS Python控制台中运行命令或开发插件,则无需关注:一切都已经为你准备好了。
60 |
61 | 访问空间参考系统信息:
62 |
63 | ```python
64 | crs = QgsCoordinateReferenceSystem(4326)
65 |
66 | print("QGIS CRS ID:", crs.srsid())
67 | print("PostGIS SRID:", crs.postgisSrid())
68 | print("Description:", crs.description())
69 | print("Projection Acronym:", crs.projectionAcronym())
70 | print("Ellipsoid Acronym:", crs.ellipsoidAcronym())
71 | print("Proj4 String:", crs.toProj4())
72 | # 检查是地理坐标系统还是投影坐标系统
73 | print("Is geographic:", crs.isGeographic())
74 | # 检查CRS的地图单位类型(在QGis::units枚举中定义)
75 | print("Map units:", crs.mapUnits())
76 | ```
77 |
78 | 输出:
79 |
80 | ```python
81 | QGIS CRS ID: 3452
82 | PostGIS SRID: 4326
83 | Description: WGS 84
84 | Projection Acronym: longlat
85 | Ellipsoid Acronym: WGS84
86 | Proj4 String: +proj=longlat +datum=WGS84 +no_defs
87 | Is geographic: True
88 | Map units: DistanceUnit.Degrees
89 | ```
90 |
91 | ## 8.2 坐标参考系统转换
92 |
93 | 你可以使用[`QgsCoordinateTransform`](https://qgis.org/pyqgis/master/core/QgsCoordinateTransform.html#qgis.core.QgsCoordinateTransform)类在不同的空间参考系之间进行转换。使用它的最简单方法是创建原始和目标CRS,并在当前项目中构造[`QgsCoordinateTransform`](https://qgis.org/pyqgis/master/core/QgsCoordinateTransform.html#qgis.core.QgsCoordinateTransform)实例。然后只需反复调用 [`transform()`](https://qgis.org/pyqgis/master/core/QgsCoordinateTransform.html#qgis.core.QgsCoordinateTransform.transform)函数进行转换。默认情况下,它会正向转换,但也可以逆向转换。
94 |
95 | ```python
96 | crsSrc = QgsCoordinateReferenceSystem(4326) # WGS 84
97 | crsDest = QgsCoordinateReferenceSystem(32633) # WGS 84 / UTM zone 33N
98 | xform = QgsCoordinateTransform(crsSrc, crsDest, QgsProject.instance())
99 |
100 | # 正向转换: src -> dest
101 | pt1 = xform.transform(QgsPointXY(18,5))
102 | print("Transformed point:", pt1)
103 |
104 | # 逆向转换: dest -> src
105 | pt2 = xform.transform(pt1, QgsCoordinateTransform.ReverseTransform)
106 | print("Transformed back:", pt2)
107 | ```
108 |
109 | 输出:
110 |
111 | ```python
112 | Transformed point:
113 | Transformed back:
114 | ```
115 |
--------------------------------------------------------------------------------
/docs/11-表达式,过滤和计算值.md:
--------------------------------------------------------------------------------
1 | # 11 表达式,过滤和计算值
2 |
3 | 此页面上的代码段需要导入以下模块:
4 |
5 | ```python
6 | from qgis.core import (
7 | edit,
8 | QgsExpression,
9 | QgsExpressionContext,
10 | QgsFeature,
11 | QgsFeatureRequest,
12 | QgsField,
13 | QgsFields,
14 | QgsVectorLayer,
15 | QgsPointXY,
16 | QgsGeometry,
17 | QgsProject,
18 | QgsExpressionContextUtils
19 | )
20 | ```
21 |
22 | QGIS支持解析类似SQL的表达式。仅支持一小部分SQL语法。表达式可以作为布尔值(返回True或False)或作为函数(返回标量值)来计算。有关可用功能的完整列表,请参阅“用户手册”中的“ [表达式”](https://docs.qgis.org/latest/en/docs/user_manual/working_with_vector/expression.html#vector-expressions)。
23 |
24 | 支持三种基本类型:
25 |
26 | - 数字——整数和小数,例如`123`,`3.14`
27 | - 字符串——它们必须用单引号括起来: `'hello world'`
28 | - 列引用——在评估时,引用将替换为字段的实际值。名称不会被转义。
29 |
30 | 可以使用以下操作:
31 |
32 | - 算术运算符:`+`,`-`,`*`,`/`,`^`
33 | - 括号:用于强制运算符优先级: `(1 + 1) * 3`
34 | - 一元加减:`-12`,`+5`
35 | - 数学函数:`sqrt`,`sin`,`cos`,`tan`,`asin`, `acos`,`atan`
36 | - 转换函数:`to_int`,`to_real`,`to_string`,`to_date`
37 | - 几何函数:`$area`,`$length`
38 | - 几何处理函数:`$x`,`$y`,`$geometry`,`num_geometries`,`centroid`
39 |
40 | 支持以下运算:
41 |
42 | - 比较:`=`,`!=`,`>`,`>=`,`<`,`<=`
43 | - 模式匹配:`LIKE`(使用%和_),`~`(正则表达式)
44 | - 逻辑谓词:`AND`,`OR`,`NOT`
45 | - NULL值检查:,`IS NULL`,`IS NOT NULL`
46 |
47 | 示例:
48 |
49 | - `1 + 2 = 3`
50 | - `sin(angle) > 0`
51 | - `'Hello' LIKE 'He%'`
52 | - `(x > 10 AND y > 10) OR z = 0`
53 |
54 | 标量表达式的示例:
55 |
56 | - `2 ^ 10`
57 | - `sqrt(val)`
58 | - `$length + 1`
59 |
60 | ## 11.1 解析表达式
61 |
62 | 下面的例子展示了如何检查一个表达式是否能被正确解析:
63 |
64 | ```python
65 | exp = QgsExpression('1 + 1 = 2')
66 | assert(not exp.hasParserError())
67 |
68 | exp = QgsExpression('1 + 1 = ')
69 | assert(exp.hasParserError())
70 |
71 | assert(exp.parserErrorString() == '\nsyntax error, unexpected end of file')
72 | ```
73 |
74 | ## 11.2 评估表达式
75 |
76 | 表达式可以在不同的情况下使用,例如过滤要素或计算新的字段值。在任何情况下,表达式都必须被评估。这意味着它的值是通过执行指定的计算步骤计算出来的,计算步骤可以从简单的算术到集合表达式。
77 |
78 | ### 11.2.1 基本表达式
79 |
80 | 此基本表达式代表一个简单的算术运算:
81 |
82 | ```python
83 | exp = QgsExpression('2 * 3')
84 | print(exp)
85 | print(exp.evaluate())
86 |
87 | #
88 | # 6
89 | ```
90 |
91 | 表达式也可用于比较,1为真,0为假
92 |
93 | ```python
94 | exp = QgsExpression('1 + 1 = 2')
95 | exp.evaluate()
96 |
97 | # 1
98 | ```
99 |
100 | ### 11.2.2 要素表达式
101 |
102 | 要对一个要素进行表达式评估,必须创建一个[`QgsExpressionContext`](https://qgis.org/pyqgis/master/core/QgsExpressionContext.html#qgis.core.QgsExpressionContext)对象,并将其传递给`evaluation`函数,以允许表达式访问该要素的字段值。
103 |
104 | 下面的示例展示了如何创建一个名为 "Column "字段的要素,以及如何将该要素添加到表达式上下文中。
105 |
106 | ```python
107 | fields = QgsFields()
108 | field = QgsField('Column')
109 | fields.append(field)
110 | feature = QgsFeature()
111 | feature.setFields(fields)
112 | feature.setAttribute(0, 99)
113 |
114 | exp = QgsExpression('"Column"')
115 | context = QgsExpressionContext()
116 | context.setFeature(feature)
117 | exp.evaluate(context)
118 | # 99
119 | ```
120 |
121 | 下面是一个比较完整的例子,说明如何在矢量图层的上下文中使用表达式,以计算新的字段值:
122 |
123 | ```python
124 | from qgis.PyQt.QtCore import QMetaType
125 |
126 | # 创建矢量图层
127 | vl = QgsVectorLayer("Point", "Companies", "memory")
128 | pr = vl.dataProvider()
129 | pr.addAttributes([QgsField("Name", QMetaType.Type.QString),
130 | QgsField("Employees", QMetaType.Type.Int),
131 | QgsField("Revenue", QMetaType.Type.Double),
132 | QgsField("Rev. per employee", QMetaType.Type.Double),
133 | QgsField("Sum", QMetaType.Type.Double),
134 | QgsField("Fun", QMetaType.Type.Double)])
135 | vl.updateFields()
136 |
137 | # 将数据添加到前三个字段
138 | my_data = [
139 | {'x': 0, 'y': 0, 'name': 'ABC', 'emp': 10, 'rev': 100.1},
140 | {'x': 1, 'y': 1, 'name': 'DEF', 'emp': 2, 'rev': 50.5},
141 | {'x': 5, 'y': 5, 'name': 'GHI', 'emp': 100, 'rev': 725.9}]
142 |
143 | for rec in my_data:
144 | f = QgsFeature()
145 | pt = QgsPointXY(rec['x'], rec['y'])
146 | f.setGeometry(QgsGeometry.fromPointXY(pt))
147 | f.setAttributes([rec['name'], rec['emp'], rec['rev']])
148 | pr.addFeature(f)
149 |
150 | vl.updateExtents()
151 | QgsProject.instance().addMapLayer(vl)
152 |
153 | # 第一个表达式计算每个员工的收入
154 | # 第二个计算图层中所有收入值的总和。
155 | # 第三个表达式并没有什么意义,但是说明了我们可以在表达式中使用各种表达式函数(例如area和buffer):
156 | expression1 = QgsExpression('"Revenue"/"Employees"')
157 | expression2 = QgsExpression('sum("Revenue")')
158 | expression3 = QgsExpression('area(buffer($geometry,"Employees"))')
159 |
160 | # QgsExpressionContextUtils.globalProjectLayerScopes()是一个方便函数,可一次添加全局,项目和图层范围。另外,这些范围也可以手动添加。无论如何,重要的是始终从“最通用”到“最具体”的范围,即从全局到项目再到图层
161 | context = QgsExpressionContext()
162 | context.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(vl))
163 |
164 | with edit(vl):
165 | for f in vl.getFeatures():
166 | context.setFeature(f)
167 | f['Rev. per employee'] = expression1.evaluate(context)
168 | f['Sum'] = expression2.evaluate(context)
169 | f['Fun'] = expression3.evaluate(context)
170 | vl.updateFeature(f)
171 |
172 | print( f['Sum'])
173 |
174 | # 876.5
175 | ```
176 |
177 | ### 11.2.3 表达式过滤图层
178 |
179 | 以下示例可用于过滤图层并返回与谓词匹配的所有要素:
180 |
181 | ```python
182 | layer = QgsVectorLayer("Point?field=Test:integer",
183 | "addfeat", "memory")
184 |
185 | layer.startEditing()
186 |
187 | for i in range(10):
188 | feature = QgsFeature()
189 | feature.setAttributes([i])
190 | assert(layer.addFeature(feature))
191 | layer.commitChanges()
192 |
193 | expression = 'Test >= 3'
194 | request = QgsFeatureRequest().setFilterExpression(expression)
195 |
196 | matches = 0
197 | for f in layer.getFeatures(request):
198 | matches += 1
199 |
200 | print(matches)
201 |
202 | # 7
203 | ```
204 |
205 | ## 11.3 处理异常错误
206 |
207 | 在表达式解析或评估期间,可能发生表达相关的错误:
208 |
209 | ```python
210 | exp = QgsExpression("1 + 1 = 2 ")
211 | if exp.hasParserError():
212 | raise Exception(exp.parserErrorString())
213 |
214 | value = exp.evaluate()
215 | if exp.hasEvalError():
216 | raise ValueError(exp.evalErrorString())
217 | ```
218 |
--------------------------------------------------------------------------------
/docs/4-访问图层目录树.md:
--------------------------------------------------------------------------------
1 | # 4 访问图层目录树
2 |
3 | 此页面上的代码片段需要导入以下模块:
4 |
5 | ```python
6 | from qgis.core import (
7 | QgsProject,
8 | QgsVectorLayer,
9 | )
10 | ```
11 |
12 | ## 4.1 QgsProject类
13 |
14 | 可以使用[QgsProject](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)检索有关目录和所有已加载图层的信息。
15 |
16 | 必须创建[QgsProject](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)的实例`instance`,并使用其方法(函数)来获取已加载的图层。
17 |
18 | [mapLayers](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.mapLayers)方法返回已加载图层的字典:
19 |
20 | ```python
21 | layers = QgsProject.instance().mapLayers()
22 | print(layers)
23 |
24 | # {'countries_89ae1b0f_f41b_4f42_bca4_caf55ddbe4b6': }
25 | ```
26 |
27 | 字典的`keys`是图层的唯一id,而`values`是图层的对象。
28 |
29 | 现在直接去获得有关图层的任何其他信息:
30 |
31 | ```python
32 | # 使用列表表达式获得图层名称
33 | l = [layer.name() for layer in QgsProject.instance().mapLayers().values()]
34 | # 键值对存储图层名称和图层对象
35 | layers_list = {}
36 | for l in QgsProject.instance().mapLayers().values():
37 | layers_list[l.name()] = l
38 |
39 | print(layers_list)
40 |
41 | # {'countries': }
42 | ```
43 |
44 | 还可以使用图层名称查询目录:
45 |
46 | ```python
47 | country_layer = QgsProject.instance().mapLayersByName("countries")[0]
48 | ```
49 |
50 | !!! 提示
51 |
52 | 返回所有匹配图层的列表,因此我们使用索引[0]获取第一个图层。
53 |
54 |
55 | ## 4.2 QgsLayerTreeGroup类
56 |
57 | 图层树是由节点构建的经典树结构。当前有两种类型的节点:图层组节点([QgsLayerTreeGroup](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup))和图层节点([QgsLayerTreeLayer](https://qgis.org/pyqgis/master/core/QgsLayerTreeLayer.html#qgis.core.QgsLayerTreeLayer))。
58 |
59 | !!! 提示
60 |
61 | 更多信息请访问Martin Dobias的[博客](https://www.lutraconsulting.co.uk/blog/2014/07/06/qgis-layer-tree-api-part-1/)。
62 |
63 |
64 | 可以使用[QgsProject](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)类的[layerLayerRoot()](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.layerTreeRoot)方法轻松访问项目图层树:
65 |
66 | ```python
67 | root = QgsProject.instance().layerTreeRoot()
68 | ```
69 |
70 | `root`是一个图层组节点,具有子节点:
71 |
72 | ```python
73 | root.children()
74 | ```
75 |
76 | 返回直接子节点列表。子组节点可以从他们自己的直接父级访问。
77 |
78 | 我们可以检索其中一个子节点:
79 |
80 | ```python
81 | child0 = root.children()[0]
82 | print(child0)
83 |
84 | #
85 | ```
86 |
87 | 可以使用它们的唯一`id`来检索:
88 |
89 | ```python
90 | ids = root.findLayerIds()
91 | # 访问第一个图层
92 | root.findLayer(ids[0])
93 | ```
94 |
95 | 可以使用图层组名称检索:
96 |
97 | ```python
98 | root.findGroup('Group Name')
99 | ```
100 |
101 | [QgsLayerTreeGroup](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup)还有许多其他有用的方法,可用于获取有关目录树的更多信息:
102 |
103 | ```python
104 | # 获得所有已选图层
105 | checked_layers = root.checkedLayers()
106 | print(checked_layers)
107 |
108 | # []
109 | ```
110 |
111 | 现在,我们把一些图层添加到项目的图层树中,有两种方法可以做到:
112 |
113 | 1. **显式添加:** 使用[addLayer()](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup.addLayer)或者[insertLayer()](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup.insertLayer)方法:
114 |
115 | ```python
116 | # 创建临时图层
117 | layer1 = QgsVectorLayer("path_to_layer", "Layer 1", "memory")
118 | # 添加图层到图层树末尾
119 | root.addLayer(layer1)
120 | # 插入图层到指定位置
121 | root.insertLayer(5, layer1)
122 | ```
123 |
124 | 2. **隐式添加:** 由于项目的图层树已连接到图层注册表,因此可以在图层注册表中添加图层:
125 |
126 | ```python
127 | QgsProject.instance().addMapLayer(layer1)
128 | ```
129 |
130 | 你可以轻松地在[QgsVectorLayer](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer)和[QgsLayerTreeLayer](https://qgis.org/pyqgis/master/core/QgsLayerTreeLayer.html#qgis.core.QgsLayerTreeLayer)之间切换:
131 |
132 | ```python
133 | node_layer = root.findLayer(country_layer.id())
134 | print("Layer node:", node_layer)
135 | print("Map layer:", node_layer.layer())
136 |
137 | # Layer node:
138 | # Map layer:
139 | ```
140 |
141 | 可以使用[addGroup()](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup.addGroup)方法添加组。在下面的示例中,前者将在目录树的末尾添加一个组,而后者则可以在现有的组中添加另一个组:
142 |
143 | ```python
144 | node_group1 = root.addGroup('Simple Group')
145 | # 在图层组中添加子组
146 | node_subgroup1 = node_group1.addGroup("I'm a sub group")
147 | ```
148 |
149 | 移动节点和组有许多有用的方法。
150 |
151 | 移动现有节点分三个步骤:
152 |
153 | 1. 克隆已存在的节点
154 | 2. 移动克隆的节点到想要的位置
155 | 3. 删除原始节点
156 |
157 | ```python
158 | # 克隆图层组
159 | cloned_group1 = node_group1.clone()
160 | # 移动图层组(包括子组和图层)到顶层
161 | root.insertChildNode(0, cloned_group1)
162 | # 删除原始图层组节点
163 | root.removeChildNode(node_group1)
164 | ```
165 |
166 | 移动一个图层要稍微复杂一点:
167 |
168 | ```python
169 | # 获得一个图层
170 | vl = QgsProject.instance().mapLayersByName("countries")[0]
171 | # 从图层树中获取
172 | myvl = root.findLayer(vl.id())
173 | # 克隆
174 | myvlclone = myvl.clone()
175 | # 获取父级,如果图层不在图层组中返回空字符串
176 | parent = myvl.parent()
177 | # 移动图层节点到顶层
178 | parent.insertChildNode(0, myvlclone)
179 | # 删除原有节点
180 | root.removeChildNode(myvl)
181 | ```
182 |
183 | 或者移动到已存在的图层组中:
184 |
185 | ```python
186 | # 获得一个图层
187 | vl = QgsProject.instance().mapLayersByName("countries")[0]
188 | # 从图层树中获取
189 | myvl = root.findLayer(vl.id())
190 | # 克隆
191 | myvlclone = myvl.clone()
192 | # 创建一个新组
193 | group1 = root.addGroup("Group1")
194 | # 获取父级,如果图层不在图层组中返回空字符串
195 | parent = myvl.parent()
196 | # 移动图层节点到顶层
197 | group1.insertChildNode(0, myvlclone)
198 | # 从原有组中删除
199 | parent.removeChildNode(myvl)
200 | ```
201 |
202 | 可用于修改组和图层的其他一些方法:
203 |
204 | ```python
205 | node_group1 = root.findGroup("Group1")
206 | # 修改图层组名称
207 | node_group1.setName("Group X")
208 | node_layer2 = root.findLayer(country_layer.id())
209 | # 修改图层名称
210 | node_layer2.setName("Layer X")
211 | # 改变图层可见状态
212 | node_group1.setItemVisibilityChecked(True)
213 | node_layer2.setItemVisibilityChecked(False)
214 | # 折叠/展开图层组
215 | node_group1.setExpanded(True)
216 | node_group1.setExpanded(False)
217 | ```
--------------------------------------------------------------------------------
/docs/13-与用户通信.md:
--------------------------------------------------------------------------------
1 | # 13 与用户通信
2 |
3 | 本节代码片段需导入以下模块:
4 |
5 | ```python
6 | from qgis.core import (
7 | QgsMessageLog,
8 | QgsGeometry,
9 | )
10 |
11 | from qgis.gui import (
12 | QgsMessageBar,
13 | )
14 |
15 | from qgis.PyQt.QtWidgets import (
16 | QSizePolicy,
17 | QPushButton,
18 | QDialog,
19 | QGridLayout,
20 | QDialogButtonBox,
21 | )
22 | ```
23 |
24 | 本节介绍用于与用户通信的一些方法和元素,以保持用户接口的一致性。
25 |
26 | ## 13.1 显示消息——QgsMessageBar
27 |
28 | 从用户体验的角度来看,使用消息框可能是个坏主意。为了显示一小行信息或警告/错误消息,QGIS消息栏通常是更好的选择。
29 |
30 | 使用QGIS接口对象的引用,你可以使用以下代码在消息栏中显示消息
31 |
32 | ```python
33 | from qgis.core import Qgis
34 | iface.messageBar().pushMessage("Error", "I'm sorry Dave, I'm afraid I can't do that", level=Qgis.Critical)
35 | ```
36 |
37 | 
38 |
39 |
40 | QGIS消息栏
41 |
42 | 你可以设置持续时间,在有限时间内显示它
43 |
44 | ```python
45 | iface.messageBar().pushMessage("Ooops", "The plugin is not working as it should", level=Qgis.Critical, duration=3)
46 | ```
47 |
48 | 
49 |
50 | 带定时器的QGIS消息栏
51 |
52 | 上面的示例显示了错误栏,但`level`参数可用于创建警告消息或正常消息——使用[`Qgis.MessageLevel`](https://qgis.org/pyqgis/master/core/Qgis.html#qgis.core.Qgis.MessageLevel)枚举,你最多可以使用4个不同级别:
53 |
54 | 0. Info
55 |
56 | 2. Warning
57 | 3. Critical
58 | 4. Success
59 |
60 | 
61 |
62 | QGIS消息栏(info)
63 |
64 | 控件可以添加到消息栏中,例如用于显示更多信息的按钮
65 |
66 | ```python
67 | def showError():
68 | pass
69 |
70 | widget = iface.messageBar().createMessage("Missing Layers", "Show Me")
71 | button = QPushButton(widget)
72 | button.setText("Show Me")
73 | button.pressed.connect(showError)
74 | widget.layout().addWidget(button)
75 | iface.messageBar().pushWidget(widget, Qgis.Warning)
76 | ```
77 |
78 | 
79 |
80 | 带有按钮的QGIS消息栏
81 |
82 | 你甚至可以在自己的对话框中使用消息栏,这样就不必显示消息框,或者在主QGIS窗口中显示消息时没有意义
83 |
84 | ```python
85 | class MyDialog(QDialog):
86 | def __init__(self):
87 | QDialog.__init__(self)
88 | self.bar = QgsMessageBar()
89 | self.bar.setSizePolicy( QSizePolicy.Minimum, QSizePolicy.Fixed )
90 | self.setLayout(QGridLayout())
91 | self.layout().setContentsMargins(0, 0, 0, 0)
92 | self.buttonbox = QDialogButtonBox(QDialogButtonBox.Ok)
93 | self.buttonbox.accepted.connect(self.run)
94 | self.layout().addWidget(self.buttonbox, 0, 0, 2, 1)
95 | self.layout().addWidget(self.bar, 0, 0, 1, 1)
96 | def run(self):
97 | self.bar.pushMessage("Hello", "World", level=Qgis.Info)
98 |
99 | myDlg = MyDialog()
100 | myDlg.show()
101 | ```
102 |
103 | 
104 |
105 | 自定义对话框中的QGIS消息栏
106 |
107 | ## 13.2 显示进度条
108 |
109 | 进度条也可以放在QGIS消息栏中,正如我们所见,它接受控件。以下是你可以在控制台中尝试的示例:
110 |
111 | ```python
112 | import time
113 | from qgis.PyQt.QtWidgets import QProgressBar
114 | from qgis.PyQt.QtCore import *
115 | progressMessageBar = iface.messageBar().createMessage("Doing something boring...")
116 | progress = QProgressBar()
117 | progress.setMaximum(10)
118 | progress.setAlignment(Qt.AlignLeft|Qt.AlignVCenter)
119 | progressMessageBar.layout().addWidget(progress)
120 | iface.messageBar().pushWidget(progressMessageBar, Qgis.Info)
121 |
122 | for i in range(10):
123 | time.sleep(1)
124 | progress.setValue(i + 1)
125 |
126 | iface.messageBar().clearWidgets()
127 | ```
128 |
129 | 此外,你可以使用内置状态栏报告进度,如下示例所示:
130 |
131 | ```python
132 | vlayer = iface.activeLayer()
133 |
134 | count = vlayer.featureCount()
135 | features = vlayer.getFeatures()
136 |
137 | for i, feature in enumerate(features):
138 | # 做一些耗时任务
139 | print('') # 给予足够的时间来打印进度
140 |
141 | percent = i / float(count) * 100
142 | # iface.mainWindow().statusBar().showMessage("Processed {} %".format(int(percent)))
143 | iface.statusBarIface().showMessage("Processed {} %".format(int(percent)))
144 |
145 | iface.statusBarIface().clearMessage()
146 | ```
147 |
148 | ## 13.3 日志
149 |
150 | QGIS有三种不同类型的日志记录,记录和保存有关代码执行的所有信息。每种类型都有特定的输出位置。请考虑使用正确的日志记录方式:
151 |
152 | - [`QgsMessageLog`](https://qgis.org/pyqgis/master/core/QgsMessageLog.html#qgis.core.QgsMessageLog) 用于向用户传达问题。QgsMessageLog的输出显示在日志消息面板中。
153 | - Python的 **logging** 模块用于调试QGIS Python API(PyQGIS)。建议Python开发人员使用其调试代码,例如,要素的id或者几何。
154 | - [`QgsLogger`](https://qgis.org/pyqgis/master/core/QgsLogger.html#qgis.core.QgsLogger) 用于QGIS内部调试/开发(例如:当你怀疑某些内容引起了崩溃)。只有QGIS的开发者版本可用。
155 |
156 | 不同日志记录类型的示例如下所示。
157 |
158 | !!! 警告 warning
159 |
160 | 在多线程的代码中使用Python `print`语句是不安全的,并且很大程度降低算法的速度。包括函数表达式,渲染器,符号层和处理算法(等等)。在这些情况下,你应该始终使用Python **logging** 模块或线程安全类([`QgsLogger`](https://qgis.org/pyqgis/master/core/QgsLogger.html#qgis.core.QgsLogger)或[`QgsMessageLog`](https://qgis.org/pyqgis/master/core/QgsMessageLog.html#qgis.core.QgsMessageLog))。
161 |
162 |
163 | ### 13.3.1 QgsMessageLog
164 |
165 | ```python
166 | # 你可以选择传递'tag'和'level'参数
167 | QgsMessageLog.logMessage("Your plugin code has been executed correctly", 'MyPlugin', level=Qgis.Info)
168 | QgsMessageLog.logMessage("Your plugin code might have some problems", level=Qgis.Warning)
169 | QgsMessageLog.logMessage("Your plugin code has crashed!", level=Qgis.Critical)
170 | ```
171 |
172 | !!! 提示
173 |
174 | 你可以在[日志消息面板](https://docs.qgis.org/testing/en/docs/user_manual/introduction/general_tools.html#log-message-panel)中查看[`QgsMessageLog`](https://qgis.org/pyqgis/master/core/QgsMessageLog.html#qgis.core.QgsMessageLog)输出的日志。
175 |
176 | ### 13.3.2 python logging模块
177 |
178 | ```python
179 | import logging
180 | formatter = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
181 | logfilename=r'c:\temp\example.log'
182 | logging.basicConfig(filename=logfilename, level=logging.DEBUG, format=formatter)
183 | logging.info("This logging info text goes into the file")
184 | logging.debug("This logging debug text goes into the file as well")
185 |
186 | # 2020-10-08 13:14:42,998 - root - INFO - This logging text goes into the file
187 | # 2020-10-08 13:14:42,998 - root - DEBUG - This logging debug text goes into the file as well
188 | ```
189 |
190 | basicConfig方法配置日志记录的基本设置。在上面的代码中,定义了文件名,日志记录级别和格式。文件名指的是将日志文件写入的位置,日志记录级别定义输出的级别,格式定义输出每个消息的格式。
191 |
192 | 如果你想要每次在执行脚本时删除日志文件,你可以执行以下操作:
193 |
194 | ```python
195 | if os.path.isfile(logfilename):
196 | with open(logfilename, 'w') as file:
197 | pass
198 | ```
199 |
200 | 如何使用如何使用Python日志记录工具的更多资源:
201 |
202 | - https://docs.python.org/3/library/logging.html
203 | - https://docs.python.org/3/howto/logging.html
204 | - https://docs.python.org/3/howto/logging-cookbook.html
205 |
206 | !!! 警告 warning
207 |
208 | 请注意,如果不设置日志文件,日志可能是多线程的,这会严重减慢输出速度
209 |
--------------------------------------------------------------------------------
/docs/5-使用栅格图层.md:
--------------------------------------------------------------------------------
1 | # 5 使用栅格图层
2 |
3 | 此页面上的代码段需要导入以下模块:
4 |
5 | ```python
6 | from qgis.core import (
7 | Qgis,
8 | QgsRasterLayer,
9 | QgsProject,
10 | QgsPointXY,
11 | QgsRaster,
12 | QgsRasterBlock,
13 | QgsRasterShader,
14 | QgsColorRampShader,
15 | QgsSingleBandPseudoColorRenderer,
16 | QgsSingleBandColorDataRenderer,
17 | QgsSingleBandGrayRenderer,
18 | )
19 |
20 | from qgis.PyQt.QtGui import (
21 | QColor,
22 | )
23 | ```
24 |
25 | ## 5.1 图层细节
26 |
27 | 栅格图层由一个或多个栅格波段组成——称为单波段和多波段栅格。一个波段代表一个矩阵。彩色图像(例如航拍照片)是由红色,蓝色和绿色波段组成。单波段栅格通常表示连续变量(例如高程)或离散变量(例如土地使用)。在某些情况下,栅格图层带有调色板,栅格值指的是调色板中存储的颜色。
28 |
29 | 以下代码假定`rlayer`是一个 [`QgsRasterLayer`](https://qgis.org/pyqgis/master/core/QgsRasterLayer.html#qgis.core.QgsRasterLayer)对象。
30 |
31 | ```python
32 | rlayer = QgsProject.instance().mapLayersByName('srtm')[0]
33 | # 获取图层分辨率
34 | rlayer.width(), rlayer.height()
35 | (919, 619)
36 | # 获取图层范围,返回QgsRectangle对象
37 | rlayer.extent()
38 | #
39 | # 获取图层范围并转换为字符串
40 | rlayer.extent().toString()
41 | # '20.0685680819999988,-34.2700107699999990 : 20.8394528430000001,-33.7507750070000014'
42 | # 获取栅格类型: 0 = 灰度值(单波段), 1 = 调色板(单波段), 2 = 多波段
43 | rlayer.rasterType()
44 | # 0
45 | # 获取波段个数
46 | rlayer.bandCount()
47 | # 1
48 | # 获取第一个波段名称
49 | print(rlayer.bandName(1))
50 | # Band 1: Height
51 | # 获取所有可用的元数据,返回QgsLayerMetadata对象
52 | rlayer.metadata()
53 | # ''
54 | ```
55 |
56 | ## 5.2 渲染
57 |
58 | 加载栅格图层时,它会根据其类型获取默认渲染器。它可以在图层属性中更改,也可以通过编程方式更改。
59 |
60 | 查询当前渲染器:
61 |
62 | ```python
63 | rlayer.renderer()
64 | #
65 | rlayer.renderer().type()
66 | # 'singlebandgray'
67 | ```
68 |
69 | 设置渲染器,使用[`QgsRasterLayer`](https://qgis.org/pyqgis/master/core/QgsRasterLayer.html#qgis.core.QgsRasterLayer)的[`setRenderer`](https://qgis.org/pyqgis/master/core/QgsRasterLayer.html#qgis.core.QgsRasterLayer.setRenderer) 方法。下面有许多渲染器类(派生自[`QgsRasterRenderer`](https://qgis.org/pyqgis/master/core/QgsRasterRenderer.html#qgis.core.QgsRasterRenderer)):
70 |
71 | - [`QgsHillshadeRenderer`](https://qgis.org/pyqgis/master/core/QgsHillshadeRenderer.html#qgis.core.QgsHillshadeRenderer)
72 | - [`QgsMultiBandColorRenderer`](https://qgis.org/pyqgis/master/core/QgsMultiBandColorRenderer.html#qgis.core.QgsMultiBandColorRenderer)
73 | - [`QgsPalettedRasterRenderer`](https://qgis.org/pyqgis/master/core/QgsPalettedRasterRenderer.html#qgis.core.QgsPalettedRasterRenderer)
74 | - [`QgsRasterContourRenderer`](https://qgis.org/pyqgis/master/core/QgsRasterContourRenderer.html#qgis.core.QgsRasterContourRenderer)
75 | - [`QgsSingleBandColorDataRenderer`](https://qgis.org/pyqgis/master/core/QgsSingleBandColorDataRenderer.html#qgis.core.QgsSingleBandColorDataRenderer)
76 | - [`QgsSingleBandGrayRenderer`](https://qgis.org/pyqgis/master/core/QgsSingleBandGrayRenderer.html#qgis.core.QgsSingleBandGrayRenderer)
77 | - [`QgsSingleBandPseudoColorRenderer`](https://qgis.org/pyqgis/master/core/QgsSingleBandPseudoColorRenderer.html#qgis.core.QgsSingleBandPseudoColorRenderer)
78 |
79 | 单波段栅格图层可以以灰色(低值=黑色,高值=白色)或使用伪彩色算法绘制,该算法为值指定颜色。也可以使用调色板绘制带调色板的单波段栅格。多波段图层通常通过将波段映射到RGB来绘制颜色。另一种可能是仅使用一个波段来绘图。
80 |
81 | ### 5.2.1 单波段栅格
82 |
83 | 假设我们想要渲染一个单波段栅格图层,颜色范围从绿色到黄色(对应于0到255之间的像素值)。在第一阶段,我们将准备一个:[QgsRasterShader](https://qgis.org/pyqgis/master/core/QgsRasterShader.html#qgis.core.QgsRasterShader)对象并配置其着色器函数:
84 |
85 | ```python
86 | fcn = QgsColorRampShader()
87 | fcn.setColorRampType(QgsColorRampShader.Interpolated)
88 | lst = [ QgsColorRampShader.ColorRampItem(0, QColor(0,255,0)),QgsColorRampShader.ColorRampItem(255, QColor(255,255,0)) ]
89 | fcn.setColorRampItemList(lst)
90 | shader = QgsRasterShader()
91 | shader.setRasterShaderFunction(fcn)
92 | ```
93 |
94 | 着色器按其颜色映射指定的颜色。彩色地图被提供为具有相关颜色的像素值列表。插值有三种模式:
95 |
96 | - 线性(`Interpolated`):从像素值上方和下方的颜色表条带中线性插值颜色
97 | - 离散(`Discrete`):颜色取自具有相同或更高值的最接近的颜色表条带
98 | - 精确(`Exact`):不插值颜色,只绘制值等于颜色表条带的像素
99 |
100 | 在第二步中,我们将此着色器与栅格图层相关联:
101 |
102 | ```python
103 | renderer = QgsSingleBandPseudoColorRenderer(rlayer.dataProvider(), 1, shader)
104 | rlayer.setRenderer(renderer)
105 | ```
106 |
107 | 上面代码中数值`1`是波段号(栅格波段的一个索引)。
108 |
109 | 最后我们必须使用 [`triggerRepaint`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.triggerRepaint)方法来查看结果:
110 |
111 | ```python
112 | rlayer.triggerRepaint()
113 | ```
114 |
115 | ### 5.2.2 多波段栅格
116 |
117 | 默认情况下,QGIS将前三个波段映射为红色,绿色和蓝色来创建彩色图像(这是`MultiBandColor`绘图样式。在某些情况下,你可能希望覆盖这些设置。以下代码互换红色波段(1)和绿色波段(2):
118 |
119 | ```python
120 | rlayer_multi = QgsProject.instance().mapLayersByName('multiband')[0]
121 | rlayer_multi.renderer().setGreenBand(1)
122 | rlayer_multi.renderer().setRedBand(2)
123 | ```
124 |
125 | 如果只需要一个波段来实现光栅的可视化,则可以选择单波段绘制,灰度级或伪彩色。
126 |
127 | 我们必须使用[`triggerRepaint`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.triggerRepaint) 更新地图并查看结果:
128 |
129 | ```python
130 | rlayer.triggerRepaint()
131 | ```
132 |
133 | ## 5.3 查询值
134 |
135 | 查询栅格值的第一种方法是使用 [`QgsRasterDataProvider`](https://qgis.org/pyqgis/master/core/QgsRasterDataProvider.html#qgis.core.QgsRasterDataProvider)的[`sample()`](https://qgis.org/pyqgis/master/core/QgsRasterDataProvider.html#qgis.core.QgsRasterDataProvider.sample)方法查询。你必须指定栅格图层的[`QgsPointXY`](https://qgis.org/pyqgis/master/core/QgsPointXY.html#qgis.core.QgsPointXY)的和波段号。该方法返回一个value和result(true或false):
136 |
137 | ```python
138 | val, res = rlayer.dataProvider().sample(QgsPointXY(20.50, -34), 1)
139 | ```
140 |
141 | 查询栅格值的另一种方法是使用[`identify()`](https://qgis.org/pyqgis/master/core/QgsRasterDataProvider.html#qgis.core.QgsRasterDataProvider.identify)方法,返回[`QgsRasterIdentifyResult`](https://qgis.org/pyqgis/master/core/QgsRasterIdentifyResult.html#qgis.core.QgsRasterIdentifyResult)对象 。
142 |
143 | ```python
144 | ident = rlayer.dataProvider().identify(QgsPointXY(20.5, -34), QgsRaster.IdentifyFormatValue)
145 | if ident.isValid():
146 | print(ident.results())
147 |
148 | # {1: 323.0}
149 | ```
150 |
151 | 在这种情况下,[`results()`](https://qgis.org/pyqgis/master/core/QgsRasterIdentifyResult.html#qgis.core.QgsRasterIdentifyResult.results) 方法返回一个字典,波段索引作为字典键,波段值作为字典值。例如`{1: 323.0}`。
152 |
153 | ## 5.4 编辑栅格数据
154 |
155 | 可以使用[`QgsRasterBlock`](https://qgis.org/pyqgis/master/core/QgsRasterBlock.html#qgis.core.QgsRasterBlock)类创建栅格图层。例如,创建每像素一字节的2x2栅格块:
156 |
157 | ```python
158 | block = QgsRasterBlock(Qgis.Byte, 2, 2)
159 | block.setData(b'\xaa\xbb\xcc\xdd')
160 | ```
161 |
162 | 使用[`writeBlock()`](https://qgis.org/pyqgis/master/core/QgsRasterDataProvider.html#qgis.core.QgsRasterDataProvider.writeBlock)方法可以覆盖栅格像素。用2x2块覆盖位置0,0处的现有栅格数据:
163 |
164 | ```python
165 | provider = rlayer.dataProvider()
166 | provider.setEditable(True)
167 | provider.writeBlock(block, 1, 0, 0)
168 | provider.setEditable(False)
169 | ```
--------------------------------------------------------------------------------
/docs/1-引言.md:
--------------------------------------------------------------------------------
1 | # 1 引言
2 |
3 | 本文档既可作为教程,也可作为参考指南。虽然没有列举所有可能的案例,但是对主要功能有一个很好的概述。
4 |
5 | 根据GNU免费文档许可证、版本1.3或自由软件基金会发布的任何后续版本的条款,允许复制、分发和/或修改该文档;没有固定章节,没有封面文本,也没有封底文本。
6 |
7 | [GNU免费文档许可证](https://docs.qgis.org/testing/en/docs/gentle_gis_introduction/gnu_free_documentation_license.html#gnu-fdl)一节中包含了该许可证的副本。
8 |
9 | 本许可证也适用于本文档中的所有代码段。
10 |
11 | 对于Python的支持最初是在QGIS 0.9中引入的。 目前,在QGIS桌面版中有几种方法可以使用Python,如下:
12 |
13 | - 在QGIS的Python控制台中
14 | - 创建并使用插件
15 | - QGIS启动时自动运行Python代码
16 | - 创建处理算法
17 | - 在QGIS中为表达式创建函数
18 | - 基于QGIS API创建自定义应用程序
19 |
20 | Python支持在QGIS 0.9中首次引入。这里有几种方法可以在QGIS桌面软件中使用Python(涵盖一下部分):
21 |
22 | - 在QGIS中的Python控制台中使用命令
23 | - 创建并使用插件
24 | - 当QGIS启动时自动运行Python代码
25 | - 创建处理算法
26 | - 在QGIS中创建函数表达式
27 | - 基于QGIS API创建自定义应用
28 |
29 | Python绑定也可用于QGIS服务,包括Python插件(请参阅[20-QGIS服务器和Python](20-QGIS服务器和Python.md))和Python绑定,可用于将QGIS服务嵌入到Python应用程序中。
30 | 这里有一个[完整的QGIS C++ API]( )参考——用于记录QGIS库中的类。 [Pythonic QGIS API(pyqgis)](https://qgis.org/pyqgis)几乎与C ++ API相同。
31 | 学习执行常见任务的另一个好办法是从[插件仓库](https://plugins.qgis.org/)下载现有插件并学习它们的代码。
32 |
33 | ## 1.1 在Python控制台中编写脚本
34 |
35 | QGIS为脚本编写提供了一个集成的python控制台。可以从插件→python控制台菜单中打开:
36 |
37 | 
38 |
39 | 上面的截图说明了如何获取图层列表中当前选定的图层,并显示其ID,如果是矢量图层,还可以显示要素个数。对于与QGIS交互环境,有一个`iface`变量,它是[QgisInterface](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)的一个实例。此接口允许访问地图画布、菜单、工具栏和QGIS应用程序的其他部分。
40 |
41 | 为了方便用户,在启动控制台时将会执行以下语句(将来可以设置更多的初始命令)
42 |
43 | ```python
44 | from qgis.core import *
45 | import qgis.utils
46 | ```
47 |
48 | 对于经常使用控制台的用户,设置打开控制台的快捷键可能很有用(在设置→键盘快捷键中...)
49 |
50 | ## 1.2 Python插件
51 |
52 | 可以使用插件来扩展QGIS的功能,可以使用Python编写插件。与C ++插件相比,主要优点是分发简单(不对每个平台进行编译)、更容易的开发。
53 |
54 | 自从引入对Python的支持以来,已经编写了许多涵盖各种功能的插件。插件安装程序允许用户很容易获取、升级和删除Python插件。有关插件和插件开发的更多信息,请参阅[Python插件](https://plugins.qgis.org/)页面。
55 |
56 | 使用Python创建插件很简单,详细说明请参阅[16-开发Python插件](16-开发Python插件.md)。
57 |
58 | !!! 提示
59 |
60 | Python插件也可用于QGIS服务,更多信息,请参阅[20-QGIS服务器和Python](20-QGIS服务器和Python.md)。
61 |
62 | ### 1.2.1 处理插件
63 |
64 | 处理插件可被用于处理数据。它们比Python插件更容易开发、更具体、更轻量。[17-编写处理插件](17-编写处理插件.md)阐述了什么时候该使用处理算法,并且怎么开发它们。
65 |
66 |
67 | ## 1.3 QGIS启动时运行Python代码
68 |
69 | 每次QGIS启动时,都有不同的方法来运行Python代码。
70 |
71 | 1. 创建startup.py脚本
72 | 2. 将`PYQGIS_STARTUP`环境变量设置为现有Python文件
73 | 2. 使用 `--code init_qgis.py` 参数指定启动脚本
74 |
75 | ### 1.3.1 startup.py文件
76 |
77 | 每次QGIS启动时,都会在用户的Python主目录和系统路径列表中搜索名为`startup.py`的文件。 如果该文件存在,则由嵌入式Python解释器执行。
78 |
79 | 用户主目录中的路径通常位于:
80 |
81 | - Linux: `.local/share/QGIS/QGIS3`
82 | - Windows: `AppData\Roaming\QGIS\QGIS3`
83 | - macOS: `Library/Application Support/QGIS/QGIS3`
84 |
85 | 默认系统路径取决于操作系统。要查找适合您的路径,请打开 Python 控制台并运行 `QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)`以查看默认目录列表。
86 |
87 | `startup.py`脚本在QGIS中初始化python时立即执行,在应用程序启动的早期。
88 |
89 | ### 1.3.2 PYQGIS_STARTUP环境变量
90 |
91 | 你可以在QGIS初始化完成之前将`PYQGIS_STARTUP`环境变量设置为现有Python文件的路径来运行Python代码。
92 |
93 | 此代码将在QGIS初始化完成之前运行。此方法对于清理sys.path非常有用——可能存在不需要的路径,或用于隔离/加载初始环境——无需虚拟环境,例如在Mac上使用homebrew或MacPorts。
94 |
95 | ### 1.3.3 `--code`参数
96 |
97 | 你可以提供自定义代码作为 QGIS 的启动参数执行。为此,请创建一个 python 文件,例如 `qgis_init.py`,使用 `qgis --code qgis_init.py `从命令行执行和启动 QGIS。
98 |
99 | 通过 `--code` 提供的代码在 QGIS 初始化阶段的后期,在加载应用程序组件之后执行。
100 |
101 | ### 1.3.4 Python 的其他参数
102 |
103 | 要为 `--code` 脚本或执行的其他 python 代码提供其他参数,可以使用 `--py-args` 参数。在 `--py-args` 之后和 `-- arg`(如果存在)之前的任何参数都将传递给 Python,但被 QGIS 应用程序本身忽略。
104 |
105 | 在下面的例子中,`myfile.tif` 将通过 Python 中的 `sys.argv` 提供,但不会由 QGIS 加载。而`otherfile.tif`将由QGIS加载,但不存在于`sys.argv`中。
106 |
107 | ```bash
108 | qgis --code qgis_init.py --py-args myfile.tif -- otherfile.tif
109 | ```
110 |
111 | 如果你想从Python中访问每个命令行参数,你可以使用`QCoreApplication.arguments()`
112 |
113 | ```python
114 | QgsApplication.instance().arguments()
115 | ```
116 |
117 | ## 1.4 Python应用程序
118 |
119 | 为自动化流程创建脚本通常很方便。使用PyQGIS,这是完全可能的——导入`qgis.core`模块,初始化它,你就可以进行处理了。
120 |
121 | 或者你可能想要创建一个使用GIS功能的交互式应用程序——执行测量、将地图导出为PDF或任何其他功能。`qgis.gui`模块提供了各种GUI部件,最值得注意的是可以合并到应用程序中的地图画布控件——支持缩放,平移和任何其他自定义地图工具。
122 |
123 | 必须配置PyQGIS自定义应用程序或独立脚本以定位QGIS资源,例如投影信息,用于读取矢量和栅格图层的数据提供者等。QGIS资源通过在应用程序或脚本的开头添加几行(代码)来初始化。自定义应用程序和独立脚本初始化QGIS的代码类似。以下提供各自的实例。
124 |
125 | !!! 提示
126 |
127 | 千万不能使用`qgis.py`作为你的测试脚本的名称,否则Python将无法导入绑定。
128 |
129 | ### 1.4.1 在独立脚本中使用PyQGIS
130 |
131 | 启动独立脚本,在脚本开头初始化QGIS资源,类似于以下代码:
132 |
133 | ```python
134 | from qgis.core import *
135 | # 提供qgis安装位置的路径(windows默认:C:\Program Files\QGIS 3.x\apps\qgis-ltr)
136 | QgsApplication.setPrefixPath("/path/to/qgis/installation", True)
137 | # 创建对QgsApplication的引用,第二个参数设置为False将禁用GUI
138 | qgs = QgsApplication([], False)
139 | # 加载提供者
140 | qgs.initQgis()
141 | # 在这里编写代码,加载一些图层,使用处理算法等
142 | # 脚本完成后,调用exitQgis()从内存中删除提供者和图层注册
143 | qgs.exitQgis()
144 | ```
145 |
146 | 我们首先导入`qgis.core`模块,然后配置前缀路径。前缀路径是安装QGIS的路径。它通过调用`setPrefixPath`方法在脚本中配置。第二个参数设置为`True`,它控制是否使用默认路径。
147 |
148 | QGIS安装路径因平台而异,在系统中找到它的最简单方法是在QGIS中使用[1.1 在Python控制台中编写脚本](#11-python)运行 `QgsApplication.prefixPath()`并查看输出。
149 |
150 | 配置前缀路径后,我们在变量`qgs`中保存了一个对`QgsApplication`的引用。第二个参数设置为`False`,表示我们不打算使用GUI,因为我们正在编写一个独立的脚本。配置`QgsApplication`后 ,我们通过调用`qgs.initQgis()`方法加载QGIS数据提供者和图层注册。在QGIS初始化后,我们准备编写脚本的其余部分。最后,我们通过调用`qgs.exitQgis()`从内存中删除数据提供者和图层注册。
151 |
152 | ### 1.4.2 在自定义应用程序中使用PyQGIS
153 |
154 | [1.4.1 在独立脚本中使用PyQGIS](#141-pyqgis)和自定义PyQGIS应用程序之间的唯一的区别是在实例化`QgsApplication`时的第二个参数。传递`True`而不是`False`,表示我们计划使用GUI。
155 |
156 | ```python
157 | from qgis.core import *
158 | # 提供qgis安装位置的路径(windows默认:C:\Program Files\QGIS 3.x\apps\qgis-ltr)
159 | QgsApplication.setPrefixPath("/path/to/qgis/installation", True)
160 | # 创建对QgsApplication设置的引用第二个参数为True启用GUI,我们需要这样做,因为这是一个自定义应用程序
161 | qgs = QgsApplication([], True)
162 | # 加载提供者
163 | qgs.initQgis()
164 | # 在这里编写代码,加载一些图层,使用处理算法等
165 | # 脚本完成后,调用exitQgis()从内存中删除提供者和图层注册
166 | qgs.exitQgis()
167 | ```
168 |
169 | 现在,你可以使用QGIS API——加载图层并执行一些处理或使用地图画布启动GUI。可能性是无止境的:-)
170 |
171 | ### 1.4.3 运行自定义应用程序
172 |
173 | 如果它们不在一个众所周知的位置,你需要告诉系统在哪里搜索QGIS库和合适的Python模块——否则Python会抛出异常:
174 |
175 | ```python
176 | >>> import qgis.core
177 | ImportError: No module named qgis.core
178 | ```
179 |
180 | 可以通过设置`PYTHONPATH`环境变量来修复。在以下命令中,``应替换为你的实际QGIS安装路径:
181 |
182 | - 在Linux上:**export PYTHONPATH=//share/qgis/python**
183 | - 在Windows上:**set PYTHONPATH=c:\\python**
184 | - 在macOS上:**export PYTHONPATH=//Contents/Resources/python**
185 |
186 | 现在,PyQGIS模块的路径设置完成,但它们依赖于`qgis_core`和`qgis_gui`库(仅仅作为封装的Python模块)。这些库的路径通常是操作系统未知的,因此再次出现导入错误(错误消息可能因系统而异):
187 |
188 | ```python
189 | >>> import qgis.core
190 | ImportError: libqgis_core.so.3.2.0: cannot open shared object file:No such file or directory
191 | ```
192 |
193 | 通过将QGIS库所在的目录添加到动态链接器的搜索路径来解决此问题:
194 |
195 | - 在Linux上:**export LD_LIBRARY_PATH=/qgispath/lib**
196 | - 在Windows上:**set PATH=C:\qgispath\BIN; C:\qgispath\APPS\qgisrelease\BIN;PATH%** ,其中`qgisrelease`应替换成你的发布类型(例如,`qgis-ltr`,`qgis`,`qgis-dev`)
197 |
198 | 这些命令可以放入一个引导脚本,负责启动。使用PyQGIS部署自定义应用程序时,通常有两种可能:
199 |
200 | - 要求用户在安装应用程序之前在其平台上安装QGIS。应用程序安装程序应查找QGIS库的默认位置,并允许用户设置路径(如果未找到)。该方法更简单,但是它需要用户执行更多步骤。
201 | - 将QGIS与你的应用程序一起打包。发布应用程序可能更具挑战性,并且程序包将更大,但用户将免于下载和安装其他软件的负担。
202 |
203 | 这两种部署方式可以混合使用——在Windows和macOS上部署独立应用程序,但是对于Linux,将QGIS的安装留给用户和他的包管理器。
204 |
205 | ## 1.5 关于PyQt和SIP的技术说明
206 |
207 | 我们决定使用Python,因为它是最受欢迎的脚本语言之一。QGIS3中的PyQGIS绑定依赖于SIP和PyQt5。使用SIP而不是使用更广泛使用的SWIG的原因是QGIS代码依赖于Qt库。Qt(PyQt)的Python绑定使用SIP完成,这允许PyQGIS与PyQt无缝集成。
--------------------------------------------------------------------------------
/docs/10-地图渲染和打印.md:
--------------------------------------------------------------------------------
1 | 本节代码片段需要导入以下模块:
2 |
3 | ```python
4 | import os
5 |
6 | from qgis.core import (
7 | QgsGeometry,
8 | QgsMapSettings,
9 | QgsPrintLayout,
10 | QgsMapSettings,
11 | QgsMapRendererParallelJob,
12 | QgsLayoutItemLabel,
13 | QgsLayoutItemLegend,
14 | QgsLayoutItemMap,
15 | QgsLayoutItemPolygon,
16 | QgsLayoutItemScaleBar,
17 | QgsLayoutExporter,
18 | QgsLayoutItem,
19 | QgsLayoutPoint,
20 | QgsLayoutSize,
21 | QgsUnitTypes,
22 | QgsProject,
23 | QgsFillSymbol,
24 | QgsAbstractValidityCheck,
25 | check,
26 | )
27 |
28 | from qgis.PyQt.QtGui import (
29 | QPolygonF,
30 | QColor,
31 | )
32 |
33 | from qgis.PyQt.QtCore import (
34 | QPointF,
35 | QRectF,
36 | QSize,
37 | )
38 | ```
39 |
40 | # 10 地图渲染和打印
41 |
42 | 当输入数据作为地图呈现时,通常有两种方法:使用*QgsMapRendererJob*快速进行,或者使用[`QgsLayout`](https://qgis.org/pyqgis/master/core/QgsLayout.html#qgis.core.QgsLayout)类组合地图来生成更精细的输出。
43 |
44 | ## 10.1 简单的渲染
45 |
46 | 渲染完成后,创建一个[`QgsMapSettings`](https://qgis.org/pyqgis/master/core/QgsMapSettings.html#qgis.core.QgsMapSettings)对象来定义渲染选项,然后使用这些选项构建一个[`QgsMapRendererJob`](https://qgis.org/pyqgis/master/core/QgsMapRendererJob.html#qgis.core.QgsMapRendererJob)类。然后使用后者来创建图像。
47 |
48 | 这是一个例子:
49 |
50 | ```python
51 | image_location = os.path.join(QgsProject.instance().homePath(), "render.png")
52 |
53 | vlayer = iface.activeLayer()
54 | settings = QgsMapSettings()
55 | settings.setLayers([vlayer])
56 | settings.setBackgroundColor(QColor(255, 255, 255))
57 | settings.setOutputSize(QSize(800, 600))
58 | settings.setExtent(vlayer.extent())
59 |
60 | render = QgsMapRendererParallelJob(options)
61 |
62 | def finished():
63 | img = render.renderedImage()
64 | # 保存图像; e.g. img.save("/Users/myuser/render.png","png")
65 | img.save(image_location, "png")
66 |
67 | render.finished.connect(finished)
68 |
69 | # 开始渲染
70 | render.start()
71 |
72 | # 通常不需要循环,因为这里是单独使用
73 | from qgis.PyQt.QtCore import QEventLoop
74 | loop = QEventLoop()
75 | render.finished.connect(loop.quit)
76 | loop.exec_()
77 | ```
78 |
79 | ## 10.2 使用不同的CRS渲染图层
80 |
81 | 如果你有多个图层并且它们具有不同的CRS,上面的简单示例可能不起作用:从范围计算中获取正确的值,你必须显式设置目标CRS
82 |
83 | ```python
84 | layers = [iface.activeLayer()]
85 | settings = QgsMapSettings()
86 | settings.setLayers(layers)
87 | render.setDestinationCrs(layers[0].crs())
88 | ```
89 |
90 | ## 10.3 使用打印布局输出
91 |
92 | 如果你想要比上面显示的简单渲染更复杂的输出,打印布局是一个非常方便的工具。可以创建复杂的地图布局,包括地图视图,标签,图例,表格以及通常出现在纸质地图上的其他元素。然后可以将布局导出为PDF,SVG,栅格图像或直接打印在打印机上。
93 |
94 | 布局由一堆类组成。它们都属于核心库。QGIS应用程序有一个方便的GUI布局元素,虽然它在GUI库中不可用。如果你不熟悉 [Qt Graphics View框架](http://doc.qt.io/qt-5/graphicsview.html),那么建议你查看文档,因为布局是基于它的。
95 |
96 | 布局的中心类是[`QgsLayout`](https://qgis.org/pyqgis/master/core/QgsLayout.html#qgis.core.QgsLayout) 类,它是从Qt [QGraphicsScene](https://doc.qt.io/qt-5/qgraphicsscene.html) 类派生的。让我们创建一个它的实例:
97 |
98 | ```python
99 | project = QgsProject.instance()
100 | layout = QgsPrintLayout(project)
101 | layout.initializeDefaults()
102 | ```
103 |
104 | 这将使用一些默认设置初始化布局,将空的A4页添加到布局中。你可以在不调用[`initializeDefaults()`](https://qgis.org/pyqgis/master/core/QgsLayout.html#qgis.core.QgsLayout.initializeDefaults)方法的情况下创建布局,但你需要自己向布局中添加页面。
105 |
106 | 之前的代码创建了在GUI中不可见的“临时”布局。它可以方便快速地添加某些项并导出,而不修改项本身,也不会向户公开这些更改。如果你希望将布局与项目一起保存或恢复,并使其在布局管理器中可用,添加:
107 |
108 | ```python
109 | layout.setName("MyLayout")
110 | project.layoutManager().addLayout(layout)
111 | ```
112 |
113 | 现在我们可以在布局中添加各种元素(map,label,...)。所有这些对象都继承自基类[`QgsLayoutItem`](https://qgis.org/pyqgis/master/core/QgsLayoutItem.html#qgis.core.QgsLayoutItem)。
114 |
115 | 以下是可以添加到布局的一些主要布局项的说明。
116 |
117 | - 地图—— 在这里,我们创建一个地图并将其拉伸到整个纸张大小
118 |
119 | ```python
120 | map = QgsLayoutItemMap(layout)
121 | # 设置地图项的位置和大小(默认是宽高都是0,位置在0,0)
122 | map.attemptMove(QgsLayoutPoint(5,5, QgsUnitTypes.LayoutMillimeters))
123 | map.attemptResize(QgsLayoutSize(200,200, QgsUnitTypes.LayoutMillimeters))
124 | # 提供渲染范围
125 | map.zoomToExtent(iface.mapCanvas().extent())
126 | layout.addItem(map)
127 | ```
128 |
129 | - 标签——允许显示标签。可以修改其字体,颜色,对齐和边距
130 |
131 | ```python
132 | label = QgsLayoutItemLabel(layout)
133 | label.setText("Hello world")
134 | label.adjustSizeToText()
135 | layout.addItem(label)
136 | ```
137 |
138 | - 图例
139 |
140 | ```python
141 | legend = QgsLayoutItemLegend(layout)
142 | legend.setLinkedMap(map) # map是一个QgsLayoutItemMap实例
143 | layout.addItem(legend)
144 | ```
145 |
146 | - 比例尺
147 |
148 | ```python
149 | item = QgsLayoutItemScaleBar(layout)
150 | item.setStyle('Numeric') # 可选择修改样式
151 | item.setLinkedMap(map) # map是一个QgsLayoutItemMap实例
152 | item.applyDefaultSize()
153 | layout.addItem(item)
154 | ```
155 |
156 | - 箭头
157 |
158 | - 图片
159 |
160 | - 基本形状
161 |
162 | - 基于节点的形状
163 |
164 | ```python
165 | polygon = QPolygonF()
166 | polygon.append(QPointF(0.0, 0.0))
167 | polygon.append(QPointF(100.0, 0.0))
168 | polygon.append(QPointF(200.0, 100.0))
169 | polygon.append(QPointF(100.0, 200.0))
170 |
171 | polygonItem = QgsLayoutItemPolygon(polygon, layout)
172 | layout.addItem(polygonItem)
173 |
174 | props = {}
175 | props["color"] = "green"
176 | props["style"] = "solid"
177 | props["style_border"] = "solid"
178 | props["color_border"] = "black"
179 | props["width_border"] = "10.0"
180 | props["joinstyle"] = "miter"
181 |
182 | symbol = QgsFillSymbol.createSimple(props)
183 | polygonItem.setSymbol(symbol)
184 | ```
185 |
186 | - 表格
187 |
188 | 将项添加到布局后,可以移动并调整其大小:
189 |
190 | ```python
191 | item.attemptMove(QgsLayoutPoint(1.4, 1.8, QgsUnitTypes.LayoutCentimeters))
192 | item.attemptResize(QgsLayoutSize(2.8, 2.2, QgsUnitTypes.LayoutCentimeters))
193 | ```
194 |
195 | 默认情况下,每个项目周围都会绘制一个框架,你可以按如下方式删除它:
196 |
197 | ```python
198 | label.setFrameEnabled(False)
199 | ```
200 |
201 | 除了手动创建布局项外,QGIS还支持布局模板,这些布局模板本质上是将所有项保存到.qpt文件中(使用XML语法)。
202 |
203 | 一旦组合准备就绪(布局项已经创建并添加到组合中),我们就可以继续生成栅格或者矢量输出。
204 |
205 | ### 10.3.1 检查布局有效性
206 |
207 | 布局是由一组互连的项组成的,在修改过程中可能会发生这些连接断开的情况(图例连接到已删除的地图、缺少源文件的图像项等),或者你可能希望对布局项应用自定义约束。[QgsAbstractValidatyCheck](https://qgis.org/pyqgis/master/core/QgsAbstractValidityCheck.html#qgis.core.QgsAbstractValidityCheck)可帮助你实现这一点。
208 |
209 | 基本检查如下:
210 |
211 | ```python
212 | @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
213 | def my_layout_check(context, feedback):
214 | results = ...
215 | return results
216 | ```
217 |
218 | 这里有一个检查,每当布局地图项目设置为WEB墨卡托投影时,就会发出警告:
219 |
220 | ```python
221 | @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
222 | def layout_map_crs_choice_check(context, feedback):
223 | layout = context.layout
224 | results = []
225 | for i in layout.items():
226 | if isinstance(i, QgsLayoutItemMap) and i.crs().authid() == 'EPSG:3857':
227 | res = QgsValidityCheckResult()
228 | res.type = QgsValidityCheckResult.Warning
229 | res.title = 'Map projection is misleading'
230 | res.detailedDescription = 'The projection for the map item {} is set to Web Mercator (EPSG:3857) which misrepresents areas and shapes. Consider using an appropriate local projection instead.'.format(i.displayName())
231 | results.append(res)
232 |
233 | return results
234 | ```
235 |
236 | 这里有一个更复杂的例子,如果任何布局地图项被设置为CRS,则会发出警告,该CRS仅在该地图项中显示的范围之外有效:
237 |
238 | ```python
239 | @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
240 | def layout_map_crs_area_check(context, feedback):
241 | layout = context.layout
242 | results = []
243 | for i in layout.items():
244 | if isinstance(i, QgsLayoutItemMap):
245 | bounds = i.crs().bounds()
246 | ct = QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), i.crs(), QgsProject.instance())
247 | bounds_crs = ct.transformBoundingBox(bounds)
248 |
249 | if not bounds_crs.contains(i.extent()):
250 | res = QgsValidityCheckResult()
251 | res.type = QgsValidityCheckResult.Warning
252 | res.title = 'Map projection is incorrect'
253 | res.detailedDescription = 'The projection for the map item {} is set to \'{}\', which is not valid for the area displayed within the map.'.format(i.displayName(), i.crs().authid())
254 | results.append(res)
255 |
256 | return results
257 | ```
258 |
259 | ### 10.3.2 导出布局
260 |
261 | 导出布局,必须使用[`QgsLayoutExporter`](https://qgis.org/pyqgis/master/core/QgsLayoutExporter.html#qgis.core.QgsLayoutExporter)类。
262 |
263 | ```python
264 | pdf_path = os.path.join(QgsProject.instance().homePath(), "output.pdf")
265 |
266 | exporter = QgsLayoutExporter(layout)
267 | exporter.exportToPdf(pdf_path, QgsLayoutExporter.PdfExportSettings())
268 | ```
269 |
270 | 如果要分别导出到SVG或图像文件,请使用[`exportToSvg()`](https://qgis.org/pyqgis/master/core/QgsLayoutExporter.html#qgis.core.QgsLayoutExporter.exportToSvg)或者[`exportToImage()`](https://qgis.org/pyqgis/master/core/QgsLayoutExporter.html#qgis.core.QgsLayoutExporter.exportToImage)导出图像。
271 |
272 | ### 10.3.3 导出布局图集
273 |
274 | 如果要从布局中导出所有页面(在配置中启用了图集选项),则需要在导出器([QgsLayoutExporter](https://qgis.org/pyqgis/master/core/QgsLayoutExporter.html#qgis.core.QgsLayoutExporter))中使用[altas()](https://qgis.org/pyqgis/master/core/QgsPrintLayout.html#qgis.core.QgsPrintLayout.atlas)方法,并进行少量调整。在以下示例中,页面导出为PNG图像:
275 |
276 | ```python
277 | exporter.exportToImage(layout.atlas(), base_path, 'png', QgsLayoutExporter.ImageExportSettings())
278 | ```
279 |
280 | 请注意,输出保存在基本路径文件夹中,使用图集上配置的输出文件名表达式。
281 |
282 |
--------------------------------------------------------------------------------
/docs/7-几何处理.md:
--------------------------------------------------------------------------------
1 | # 7 几何处理
2 |
3 | 此页面上的代码片段需要导入以下模块:
4 |
5 | ```python
6 | from qgis.core import (
7 | QgsGeometry,
8 | QgsGeometryCollection
9 | QgsPoint,
10 | QgsPointXY,
11 | QgsWkbTypes,
12 | QgsProject,
13 | QgsFeatureRequest,
14 | QgsVectorLayer,
15 | QgsDistanceArea,
16 | QgsUnitTypes,
17 | QgsCoordinateTransform,
18 | QgsCoordinateReferenceSystem
19 | )
20 | ```
21 |
22 | 表示空间要素的点、线和多边形通常称为几何。在QGIS中,它们用[`QgsGeometry`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry)类来表示 。
23 |
24 | 有时,一种几何实际上是简单(单部件)几何的集合。另一种几何形状称为多部件几何。如果它只包含一种类型的简单几何,我们称之为多点、多线或多多边形。例如,由多个岛组成的国家可以表示为多多边形。
25 |
26 | 几何的坐标可以在任何坐标参考系统(CRS)中。从图层中提取要素时,关联的几何图形将在图层的CRS中具有坐标。
27 |
28 | 有关所有可访问的几何结构和关系说明,请参阅[OGC简单要素访问标准](https://www.ogc.org/standards/sfa),以获取更详细的信息。
29 |
30 | ## 7.1 几何构造
31 |
32 | PyQGIS提供了几种创建几何的选项:
33 |
34 | - 坐标
35 |
36 | ```python
37 | gPnt = QgsGeometry.fromPointXY(QgsPointXY(1,1))
38 | print(gPnt)
39 | gLine = QgsGeometry.fromPolyline([QgsPoint(1, 1), QgsPoint(2, 2)])
40 | print(gLine)
41 | gPolygon = QgsGeometry.fromPolygonXY([[QgsPointXY(1, 1),
42 | QgsPointXY(2, 2), QgsPointXY(2, 1)]])
43 | print(gPolygon)
44 | ```
45 |
46 | 使用[`QgsPoint`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint)类或[`QgsPointXY`](https://qgis.org/pyqgis/master/core/QgsPointXY.html#qgis.core.QgsPointXY) 类创建坐标。这些类之间的区别在于[`QgsPoint`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint)支持M和Z维度。
47 |
48 | 折线(Linestring)由一系列点表示。
49 |
50 | 多边形由线环列表(即闭合的线段)表示。第一个环是外环(边界),第二个可选项线环是多边形中的孔。请注意,与某些程序不同,QGIS会为你闭合环,因此无需将第一个点复制为最后一个。
51 |
52 | 多部件几何图形更进一步:多点是一个点列表,多线是一个线列表,多多边形是一个多边形列表。
53 |
54 | - WKT
55 |
56 | ```python
57 | gem = QgsGeometry.fromWkt("POINT(3 4)")
58 | print(geom)
59 | ```
60 |
61 | - WKB
62 |
63 | ```python
64 | g = QgsGeometry()
65 | wkb = bytes.fromhex("010100000000000000000045400000000000001440")
66 | g.fromWkb(wkb)
67 |
68 | #使用WKT打印几何
69 | print(g.asWkt())
70 | ```
71 |
72 | ## 7.2 访问几何
73 |
74 | 首先,你应该找出几何类型。[`wkbType()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.wkbType) 方法是其中一种方法。它从[`QgsWkbTypes.Type`](https://qgis.org/pyqgis/master/core/QgsWkbTypes.html#qgis.core.QgsWkbTypes) 枚举中返回一个值。
75 |
76 | ```python
77 | if gPnt.wkbType() == QgsWkbTypes.Point:
78 | print(gPnt.wkbType())
79 | # output: 1 for Point
80 | if gLine.wkbType() == QgsWkbTypes.LineString:
81 | print(gLine.wkbType())
82 | # output: 2 for LineString
83 | if gPolygon.wkbType() == QgsWkbTypes.Polygon:
84 | print(gPolygon.wkbType())
85 | # output: 3 for Polygon
86 | ```
87 |
88 | 作为替代方案,可以使用[`type()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.type) ,从[`QgsWkbTypes.GeometryType`](https://qgis.org/pyqgis/master/core/QgsWkbTypes.html#qgis.core.QgsWkbTypes) 枚举中返回值。
89 |
90 | 你可以使用[`displayString()`](https://qgis.org/pyqgis/master/core/QgsWkbTypes.html#qgis.core.QgsWkbTypes.displayString) 函数来获取人类可读的几何类型。
91 |
92 | ```python
93 | print(QgsWkbTypes.displayString(gPnt.wkbType()))
94 | # output: 'Point'
95 | print(QgsWkbTypes.displayString(gLine.wkbType()))
96 | # output: 'LineString'
97 | print(QgsWkbTypes.displayString(gPolygon.wkbType()))
98 | # output: 'Polygon'
99 | ```
100 |
101 | 还有一个辅助函数 [`isMultipart()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.isMultipart)可以确定几何是否是多部件 。
102 |
103 | 从几何中提取信息,每种矢量类型都有访问器函数。以下是如何使用这些访问器的示例:
104 |
105 | ```python
106 | gPnt.asPoint()
107 | # output:
108 | gLine.asPolyline()
109 | # output: [, ]
110 | gPolygon.asPolygon()
111 | # output: [[, , , ]]
112 | ```
113 |
114 | !!! 提示
115 |
116 | 元组(x,y)不是真正的元组,它们是[`QgsPoint`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint) 对象,可以使用[`x()`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint.x) 和[`y()`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint.y)方法访问这些值。
117 |
118 | 对于多部件几何也有类似的访问函数: [`asMultiPoint()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.asMultiPoint),[`asMultiPolyline()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.asMultiPolyline)和[`asMultiPolygon()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.asMultiPolygon)。
119 |
120 | 不用管几何的类型,直接遍历所有几何是可以的,例如:
121 |
122 | ```python
123 | geom = QgsGeometry.fromWkt( 'MultiPoint( 0 0, 1 1, 2 2)' )
124 | for part in geom.parts():
125 | print(part.asWkt())
126 |
127 | # Point (0 0)
128 | # Point (1 1)
129 | # Point (2 2)
130 | ```
131 |
132 | ```python
133 | geom = QgsGeometry.fromWkt( 'LineString( 0 0, 10 10 )' )
134 | for part in geom.parts():
135 | print(part.asWkt())
136 |
137 | # LineString (0 0, 10 10)
138 | ```
139 |
140 | ```python
141 | gc = QgsGeometryCollection()
142 | gc.fromWkt('GeometryCollection( Point(1 2), Point(11 12), LineString(33 34, 44 45))')
143 | print(gc[1].asWkt())
144 |
145 | # Point (11 12)
146 | ```
147 |
148 | 使用 [`QgsGeometry.parts()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.parts) 方法修改所有几何。
149 |
150 | ```python
151 | geom = QgsGeometry.fromWkt( 'MultiPoint( 0 0, 1 1, 2 2)' )
152 | for part in geom.parts():
153 | part.transform(QgsCoordinateTransform(
154 | QgsCoordinateReferenceSystem("EPSG:4326"),
155 | QgsCoordinateReferenceSystem("EPSG:3111"),
156 | QgsProject.instance())
157 | )
158 |
159 | print(geom.asWkt())
160 |
161 | # MultiPoint ((-10334728.12541878595948219 -5360106.25905461423099041),(-10462135.16126426123082638 -5217485.4735023295506835),(-10589399.84444035589694977 -5072021.45942386891692877))
162 | ```
163 |
164 | ## 7.3 几何谓词与操作
165 |
166 | QGIS使用GEOS库进行高级几何操作,如几何谓词([`contains()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.contains),[`intersects()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.intersects),...),操作([`combine()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.combine),[`difference()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.difference),...)。它还可以计算几何的几何属性,例如面积(多边形)或长度(多边形和线)。
167 |
168 | 让我们看一个结合遍历指定图层要素,并基于它们的几何执行一些几何计算的示例。下面的代码计算并打印`countries` 图层中每个国家的面积和周长。
169 |
170 | 以下代码假定`layer`是具有多边形要素类型的[`QgsVectorLayer`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer)对象。
171 |
172 | ```python
173 | # 访问'countries'图层
174 | layer = QgsProject.instance().mapLayersByName('countries')[0]
175 |
176 | # 过滤以Z开头的国家,然后获取其要素
177 | query = '"name" LIKE \'Z%\''
178 | features = layer.getFeatures(QgsFeatureRequest().setFilterExpression(query))
179 |
180 |
181 | # 现在遍历要素,执行几何计算并打印结果
182 | for f in features:
183 | geom = f.geometry()
184 | name = f.attribute('NAME')
185 | print(name)
186 | print('Area: ', geom.area())
187 | print('Perimeter: ', geom.length())
188 |
189 |
190 | # Zambia
191 | # Area: 62.822790653431205
192 | # Perimeter: 50.65232014052552
193 | # Zimbabwe
194 | # Area: 33.41113559136521
195 | # Perimeter: 26.608288555013935
196 | ```
197 |
198 | 现在,你已经计算并打印了几何图形的面积和周长。但是,你可能会很快注意到这些值很奇怪。这是因为当使用 [`QgsGeometry`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry)类中的[`area()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.area)和[`length()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.length) 方法计算时,面积和周长不会考虑CRS。可以使用更强大的[`QgsDistanceArea`](https://qgis.org/pyqgis/master/core/QgsDistanceArea.html#qgis.core.QgsDistanceArea) 类计算面积和周长,它可以执行基于椭球的计算:
199 |
200 | 以下代码假定`layer`是具有多边形要素类型的[`QgsVectorLayer`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer)对象。
201 |
202 | ```python
203 | d = QgsDistanceArea()
204 | d.setEllipsoid('WGS84')
205 |
206 | layer = QgsProject.instance().mapLayersByName('countries')[0]
207 |
208 | # 过滤以Z开头的国家,然后获取其要素
209 | query = '"name" LIKE \'Z%\''
210 | features = layer.getFeatures(QgsFeatureRequest().setFilterExpression(query))
211 |
212 | for f in features:
213 | geom = f.geometry()
214 | name = f.attribute('NAME')
215 | print(name)
216 | print("Perimeter (m):", d.measurePerimeter(geom))
217 | print("Area (m2):", d.measureArea(geom)) ))
218 | # 打印(“面积(m2):” , d 。measureArea (GEOM ))
219 |
220 | # 计算并重新打印面积,单位为平方公里
221 | print("Area (km2):", d.convertAreaMeasurement(d.measureArea(geom), QgsUnitTypes.AreaSquareKilometers))
222 |
223 | # Zambia
224 | # Perimeter (m): 5539361.250294601
225 | # Area (m2): 751989035032.9031
226 | # Area (km2): 751989.0350329031
227 | # Zimbabwe
228 | # Perimeter (m): 2865021.3325076113
229 | # Area (m2): 389267821381.6008
230 | # Area (km2): 389267.8213816008
231 | ```
232 |
233 | 或者,你可能想知道两点之间的距离。
234 |
235 | ```python
236 | d = QgsDistanceArea()
237 | d.setEllipsoid('WGS84')
238 |
239 | # 让我们创造两个点
240 | # 圣诞老人是一个工作狂,他需要放个暑假,让我们看一下他家离特内里费有多远
241 | santa = QgsPointXY(25.847899, 66.543456)
242 | tenerife = QgsPointXY(-16.5735, 28.0443)
243 |
244 | print("Distance in meters: ", d.measureLine(santa, tenerife))
245 | ```
246 |
247 | 你可以在QGIS中找到许多算法示例,并使用这些方法来分析和转换矢量数据。以下是一些代码的链接。
248 |
249 | - 使用[`QgsDistanceArea`](https://qgis.org/pyqgis/master/core/QgsDistanceArea.html#qgis.core.QgsDistanceArea)类的距离和面积: [距离矩阵算法](https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/qgis/PointDistance.py)
250 | - [线到多边形算法](https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/qgis/LinesToPolygons.py)
251 |
--------------------------------------------------------------------------------
/docs/14-认证基础.md:
--------------------------------------------------------------------------------
1 | # 14 认证基础
2 |
3 | 本节的代码片段需要导入以下模块:
4 |
5 | ```python
6 | from qgis.core import (
7 | QgsApplication,
8 | QgsRasterLayer,
9 | QgsAuthMethodConfig,
10 | QgsDataSourceUri,
11 | QgsPkiBundle,
12 | QgsMessageLog,
13 | )
14 |
15 | from qgis.gui import (
16 | QgsAuthAuthoritiesEditor,
17 | QgsAuthConfigEditor,
18 | QgsAuthConfigSelect,
19 | QgsAuthSettingsWidget,
20 | )
21 |
22 | from qgis.PyQt.QtWidgets import (
23 | QWidget,
24 | QTabWidget,
25 | )
26 |
27 | from qgis.PyQt.QtNetwork import QSslCertificate
28 | ```
29 |
30 | ## 14.1 介绍
31 |
32 | 认证基础的用户参考可以在用户手册的[“认证系统概述”](https://docs.qgis.org/3.4/en/docs/user_manual/auth_system/auth_overview.html#authentication-overview)中阅读。
33 |
34 | 本章是描述从开发人员角度使用认证系统的最佳实践。
35 |
36 | 在QGIS桌面中,当需要凭证来访问特定资源时,例如当一个图层建立在Postgres数据库连接时,数据提供者就广泛地使用了认证系统。
37 |
38 | QGIS gui库中还有一些部件,插件开发人员可以使用它们轻松地将认证基础集成到代码中:
39 |
40 | - [`QgsAuthConfigEditor`](https://qgis.org/pyqgis/master/gui/QgsAuthConfigEditor.html#qgis.gui.QgsAuthConfigEditor)
41 | - [`QgsAuthConfigSelect`](https://qgis.org/pyqgis/master/gui/QgsAuthConfigSelect.html#qgis.gui.QgsAuthConfigSelect)
42 | - [`QgsAuthSettingsWidget`](https://qgis.org/pyqgis/master/gui/QgsAuthSettingsWidget.html#qgis.gui.QgsAuthSettingsWidget)
43 |
44 | 可以从认证基础[测试代码](https://github.com/qgis/QGIS/blob/master/tests/src/python/test_qgsauthsystem.py)中学习良好的代码引用。
45 |
46 | !!! 警告 warning
47 |
48 | 由于认证基础设计过程中考虑到安全约束,只有选定的内部方法集可以暴露给Python。
49 |
50 | ## 14.2 词汇表
51 |
52 | 以下是本章中最常见对象的一些定义。
53 |
54 | - 主密码
55 |
56 | 允许访问和解密存储在QGIS认证中凭据的密码
57 |
58 | - 认证数据库
59 |
60 | 一个[主密码](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Master-Password)加密后的SQLite数据库`qgis-auth.db` ,其中[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Configuration)存储在这里。例如用户名/密码,个人证书和密钥,证书颁发机构
61 |
62 | - 认证数据库
63 |
64 | [认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Database)
65 |
66 | - 认证配置
67 |
68 | 一组认证数据,取决于[认证方法](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Method)。例如,基本认证方法存储一对用户名/密码。
69 |
70 | - 认证方法
71 |
72 | 用于获取认证的特定方法。每个方法都有自己的协议,用于获取认证级别。每个方法都是在QGIS认证基础初始化期间动态加载的共享库。
73 |
74 | ## 14.3 QgsAuthManager入口
75 |
76 | 单例类[`QgsAuthManager`](https://qgis.org/pyqgis/master/core/QgsAuthManager.html#qgis.core.QgsAuthManager)是使用存储在QGIS[认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-DB)加密证书的入口,即,在当前[用户资料](https://docs.qgis.org/testing/en/docs/user_manual/introduction/qgis_configuration.html#user-profiles)文件夹下的`qgis-auth.db`文件。
77 |
78 | 此类负责用户交互:通过设置主密码或透明地使用它来访问加密的存储信息。
79 |
80 | ### 14.3.1 初始化管理器并设置主密码
81 |
82 | 以下代码段提供了一个示例,设置主密码来打开对认证设置的访问权限。代码注释对于理解代码非常重要。
83 |
84 | ```python
85 | authMgr = QgsApplication.authManager()
86 | # 检查QgsAuthManager是否已经初始化... QgsAuthManager.init()的额外作用是设置了AuthDbPath。
87 | # QgsAuthManager.init()在QGIS应用程序初始化期间执行,因此通常不需要直接调用它。
88 | if authMgr.authenticationDatabasePath():
89 | if authMgr.masterPasswordIsSet():
90 | msg = 'Authentication master password not recognized'
91 | assert authMgr.masterPasswordSame( "your master password" ), msg
92 | else:
93 | msg = 'Master password could not be set'
94 | # 验证verify参数密码的哈希值是否已保存在认证数据库中
95 | assert authMgr.setMasterPassword( "your master password",verify=True), msg
96 | else:
97 | # 在qgis环境之外,例如在测试环境中=>在数据库初始化之前设置环境变量
98 | os.environ['QGIS_AUTH_DB_DIR_PATH'] = "/path/where/located/qgis-auth.db"
99 | msg = 'Master password could not be set'
100 | assert authMgr.setMasterPassword("your master password", True), msg
101 | authMgr.init( "/path/where/located/qgis-auth.db" )
102 | ```
103 |
104 | ### 14.3.2 使用新的认证配置项填充认证数据库
105 |
106 | 任何存储的凭证都是[`QgsAuthMethodConfig`](https://qgis.org/pyqgis/master/core/QgsAuthMethodConfig.html#qgis.core.QgsAuthMethodConfig)类的[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Configuration)实例——使用唯一字符串访问:
107 |
108 | ```python
109 | authcfg = 'fm1s770'
110 | ```
111 |
112 | 该字符串是在使用QGIS API或GUI创建条目时自动生成的,但如果配置必须在一个组织内的多个用户之间共享(不同的凭证),将其手动设置为一个已知值可能是有用的。
113 |
114 | [`QgsAuthMethodConfig`](https://qgis.org/pyqgis/master/core/QgsAuthMethodConfig.html#qgis.core.QgsAuthMethodConfig)是任何[认证方法](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Method)的基类。任何认证方法集都会配置哈希映射,其中存储认证信息。下面是一个有用的代码片段,用于存储alice用户的PKI路径凭证:
115 |
116 | ```python
117 | authMgr = QgsApplication.authManager()
118 | # 设置 alice PKI 数据
119 | p_config = QgsAuthMethodConfig()
120 | p_config.setName("alice")
121 | p_config.setMethod("PKI-Paths")
122 | p_config.setUri("https://example.com")
123 | p_config.setConfig("certpath", "path/to/alice-cert.pem" )
124 | p_config.setConfig("keypath", "path/to/alice-key.pem" )
125 | # 检查方法参数是否正确设置
126 | assert p_config.isValid()
127 |
128 | # 在认证数据库中注册alice数据,返回存储的‘authcfg’配置
129 | authMgr.storeAuthenticationConfig(p_config)
130 | newAuthCfgId = p_config.id()
131 | assert (newAuthCfgId)
132 | ```
133 |
134 | #### 14.3.2.1 可用的认证方法
135 |
136 | [认证方法](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Method)在认证管理器初始化时动态加载。可用的认证方法列表如下:
137 |
138 | 1. `Basic` 用户和密码验证
139 | 2. `EsriToken` ESRI token 基础认证
140 | 3. `Identity-Cert` 身份证书认证
141 | 4. `PKI-Paths` PKI路径认证
142 | 5. `PKI-PKCS#12` PKI PKCS#12认证
143 |
144 | #### 14.3.2.2 填充权限
145 |
146 | ```python
147 | authMgr = QgsApplication.authManager()
148 | # 添加权限
149 | cacerts = QSslCertificate.fromPath( "/path/to/ca_chains.pem" )
150 | assert cacerts is not None
151 | # 存储 CA
152 | authMgr.storeCertAuthorities(cacerts)
153 | # 重建CA缓存
154 | authMgr.rebuildCaCertsCache()
155 | authMgr.rebuildTrustedCaCertsCache()
156 | ```
157 |
158 | #### 14.3.2.3 使用QgsPkiBundle管理PKI
159 |
160 | [QgsPkiBundle](https://qgis.org/pyqgis/master/core/QgsPkiBundle.html#qgis.core.QgsPkiBundle)类是一个方便的类,用于打包由SslCert、SslKey和CA链组成的PKI包。下面是一个获得密码保护的片段:
161 |
162 | ```python
163 | # 密钥与密码一起添加alice证书
164 | caBundlesList = []
165 | bundle = QgsPkiBundle.fromPemPaths( "/path/to/alice-cert.pem",
166 | "/path/to/alice-key_w-pass.pem",
167 | "unlock_pwd",
168 | caBundlesList )
169 | assert bundle is not None
170 | # 你可以检查它是否正确
171 | # bundle.isValid()
172 | ```
173 |
174 | 请参阅[`QgsPkiBundle`](https://qgis.org/pyqgis/master/core/QgsPkiBundle.html#qgis.core.QgsPkiBundle)类文档,从包中提取证书/密钥/ CA.
175 |
176 | ### 14.3.3 从认证数据库中删除条目
177 |
178 | 我们可以使用`authcfg`标识符从[认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Database)中删除条目:
179 |
180 | ```python
181 | authMgr = QgsApplication.authManager()
182 | authMgr.removeAuthenticationConfig( "authCfg_Id_to_remove" )
183 | ```
184 |
185 | ### 14.3.4 使用QgsAuthManager扩展authcfg
186 |
187 | 使用存储在[认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-DB)中的[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Config)的最好方法是用唯一的标识符`authcfg`来引用它。扩展,意味着把它从一个标识符转换成一套完整的凭证。使用存储的[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Config)的最佳做法,是让它由认证管理器自动管理。存储配置的常见用途是连接到一个启用了认证的服务,如WMS、WFS或数据库连接。
188 |
189 | !!! 提示
190 |
191 | 并非所有的QGIS数据提供者都与认证基础集成。每个认证方法都是从基类[QgsAuthMethod](https://qgis.org/pyqgis/master/core/QgsAuthMethod.html#qgis.core.QgsAuthMethod)派生出来的,并支持一组不同的提供者。例如,[certIdentity()](https://qgis.org/pyqgis/master/core/QgsAuthManager.html#qgis.core.QgsAuthManager.certIdentity)方法支持以下的提供者列表:
192 | ```python
193 | authM = QgsApplication.authManager()
194 | print(authM.authMethod("Identity-Cert").supportedDataProviders())
195 |
196 | # ['ows', 'wfs', 'wcs', 'wms', 'postgres']
197 | ```
198 | 例如,要使用`authcfg = 'fm1s770'`标识的存储凭证访问WMS服务,我们只需在数据源URL中使用`authcfg`,如以下代码:
199 | ```python
200 | authCfg = 'fm1s770'
201 | quri = QgsDataSourceUri()
202 | quri.setParam("layers", 'usa:states')
203 | quri.setParam("styles", '')
204 | quri.setParam("format", 'image/png')
205 | quri.setParam("crs", 'EPSG:4326')
206 | quri.setParam("dpiMode", '7')
207 | quri.setParam("featureCount", '10')
208 | quri.setParam("authcfg", authCfg) # <---- authCfg url 配置
209 | quri.setParam("contextualWMSLegend", '0')
210 | quri.setParam("url", 'https://my_auth_enabled_server_ip/wms')
211 | rlayer = QgsRasterLayer(str(quri.encodedUri(), "utf-8"), 'states', 'wms')
212 | ```
213 | 以上案例,`wms`提供者在设置HTTP连接之前,会将`authcfg` URI参数与凭证展开。
214 |
215 | !!! 警告 warning
216 |
217 | 开发者必须将`authcfg`扩展留给[QgsAuthManager](https://qgis.org/pyqgis/master/core/QgsAuthManager.html#qgis.core.QgsAuthManager),这样,它就能确保扩展不会太早完成。
218 |
219 | 通常情况下,使用[QgsDataSourceURI](https://qgis.org/pyqgis/master/core/QgsDataSourceUri.html#qgis.core.QgsDataSourceUri)类构建的URI字符串被用来设置数据源,其方式如下:
220 |
221 | ```python
222 | authCfg = 'fm1s770'
223 | quri = QgsDataSourceUri("my WMS uri here")
224 | quri.setParam("authcfg", authCfg)
225 | rlayer = QgsRasterLayer( quri.uri(False), 'states', 'wms')
226 | ```
227 |
228 | !!! 提示
229 |
230 | `False`参数很重要,可以避免URL中已存在的`authcfg`
231 |
232 | #### 14.3.4.1 使用其它数据提供者的PKI例子
233 |
234 | 其它例子可以在QGIS测试文件夹中直接读取,如[test_authmanager_pki_ows](https://github.com/qgis/QGIS/blob/master/tests/src/python/test_authmanager_pki_ows.py)或[test_authmanager_pki_postgres](https://github.com/qgis/QGIS/blob/master/tests/src/python/test_authmanager_pki_postgres.py).
235 |
236 | ## 14.4 调整插件使用认证基础
237 |
238 | 许多第三方插件使用httplib2或其他Python网络库来管理HTTP连接,而不是与[`QgsNetworkAccessManager`](https://qgis.org/pyqgis/master/core/QgsNetworkAccessManager.html#qgis.core.QgsNetworkAccessManager)及其相关认证基础集成。
239 |
240 | 为了便于集成,我们创建了一个名为`NetworkAccessManager`的Python帮助函数。它的代码可以在[这里](https://github.com/rduivenvoorde/pdokservicesplugin/blob/master/networkaccessmanager.py)找到。
241 |
242 | 此帮助程序类可以在以下代码段中使用:
243 |
244 | ```python
245 | http = NetworkAccessManager(authid="my_authCfg", exception_class=My_FailedRequestError)
246 | try:
247 | response, content = http.request( "my_rest_url" )
248 | except My_FailedRequestError, e:
249 | # 处理异常
250 | pass
251 | ```
252 |
253 | ## 14.5 认证GUI
254 |
255 | 本节列出了可用于在自定义接口中集成认证基础的可用GUI。
256 |
257 | ### 14.5.1 GUI选择证书
258 |
259 | 如果需要从存储在[认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-DB)中选择[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Configuration),则可以在GUI类`QgsAuthConfigSelect`中使用。
260 |
261 | 
262 |
263 | 可在以下代码段中使用:
264 |
265 | ```python
266 | # 创建一个QgsAuthConfigSelect GUI实例,该实例与`parent`有父子关系
267 | parent = QWidget() # GUI父级控件
268 | gui = QgsAuthConfigSelect( parent, "postgres" )
269 | # 在一个新标签中添加上述创建的GUI控件。
270 | tabGui = QTabWidget()
271 | tabGui.insertTab( 1, gui, "Configurations" )
272 | ```
273 |
274 | 以上示例摘自QGIS[源代码](https://github.com/qgis/QGIS/blob/master/src/providers/postgres/qgspgnewconnection.cpp#L42)。GUI构造函数的第二个参数引用数据提供者类型。参数用于限制与指定提供者兼容的[认证方法](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Method)。
275 |
276 | ### 14.5.2 认证编辑器
277 |
278 | 用于管理凭据、权限和访问认证实用程序的完整GUI由[`QgsAuthEditorWidgets`](https://qgis.org/pyqgis/master/gui/QgsAuthEditorWidgets.html#qgis.gui.QgsAuthEditorWidgets)类管理。
279 |
280 | 
281 |
282 | 可在以下代码段中使用:
283 |
284 | ```python
285 | # 创建一个QgsAuthConfigSelect GUI实例,该实例与`parent`有父子关系
286 | parent = QWidget() # GUI父级控件
287 | gui = QgsAuthConfigSelect( parent )
288 | gui.show()
289 | ```
290 |
291 | 可以在相关的[测试](https://github.com/qgis/QGIS/blob/master/tests/src/python/test_qgsauthsystem.py#L80)代码中找到一个综合的例子。
292 |
293 | ### 14.5.3 机构(证书颁发)编辑器
294 |
295 | 仅用于管理机构的GUI由[`QGSAuthoritiesEditor`](https://qgis.org/pyqgis/master/gui/QgsAuthAuthoritiesEditor.html#qgis.gui.QgsAuthAuthoritiesEditor)类管理。
296 |
297 | 
298 |
299 | 可在以下代码段中使用:
300 |
301 | ```python
302 | # 创建一个QgsAuthConfigSelect GUI实例,该实例与`parent`有父子关系
303 | parent = QWidget() # GUI父级控件
304 | gui = QgsAuthAuthoritiesEditor( parent )
305 | gui.show()
306 | ```
307 |
--------------------------------------------------------------------------------
/docs/3-加载图层.md:
--------------------------------------------------------------------------------
1 | # 3 加载图层
2 |
3 | 此页面上的代码片段需要导入以下模块:
4 |
5 | ```python
6 | import os # pyqgis控制台同样需要
7 | from qgis.core import (
8 | QgsVectorLayer
9 | )
10 | ```
11 |
12 | 让我们使用数据打开一些图层。QGIS可识别矢量和栅格图层。此外,自定义图层类型也可以使用,但我们不打算在此讨论。
13 |
14 | ## 3.1 矢量图层
15 |
16 | 创建一个矢量图层实例,指定图层的数据源标识、图层名称和提供者名称:
17 |
18 | ```python
19 | # 获取shapefile的路径,例如:/home/project/data/ports.shp
20 | path_to_airports_layer = "testdata/airports.shp"
21 |
22 | # 格式为:
23 | # vlayer = QgsVectorLayer(data_source, layer_name, provider_name)
24 |
25 | vlayer = QgsVectorLayer(path_to_ports_layer, "Airports layer", "ogr")
26 |
27 | if not vlayer.isValid():
28 | print("图层加载失败!")
29 | else:
30 | QgsProject.instance().addMapLayer(vlayer)
31 | ```
32 |
33 | 数据源标识是一个字符串,它特定于每个矢量数据提供者。图层名称用于图层列表部件。检查图层是否已成功加载非常重要。如果不是,则返回无效的图层实例。
34 |
35 | `geopackage`矢量图层:
36 |
37 | ```python
38 | # 获取geopackage的路径,例如:/home/project/data/data.gpkg
39 | path_to_gpkg = os.path.join(QgsProject.instance().homePath(), "data", "data.gpkg")
40 | # 追加图层名称
41 | gpkg_places_layer = path_to_gpkg + "|layername=places"
42 | # 例如:gpkg_places_layer = "/home/project/data/data.gpkg|layername=places"
43 | vlayer = QgsVectorLayer(gpkg_places_layer, "Places layer", "ogr")
44 | if not vlayer.isValid():
45 | print("图层加载失败!")
46 | else:
47 | QgsProject.instance().addMapLayer(vlayer)
48 | ```
49 |
50 | 在QGIS中打开并显示矢量图层的最快方式是使用[`QgisInterface`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)类的 [`addVectorLayer()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addVectorLayer) 方法:
51 |
52 | ```python
53 | vlayer = iface.addVectorLayer(path_to_ports_layer, "Ports layer", "ogr")
54 | if not vlayer:
55 | print("图层加载失败!")
56 | ```
57 |
58 | 这将创建一个新图层,并将其添加到当前QGIS项目中(使其显示在图层列表中)。该函数返回图层实例,如果无法加载图层则返回`None` 。
59 |
60 | 以下列表展示了如何使用矢量数据提供者访问各种数据源:
61 |
62 | - GDAL 库(Shapefile和许多其他文件格式)——数据源是文件的路径:
63 |
64 | - Shapefile:
65 |
66 | ```python
67 | vlayer = QgsVectorLayer("/path/to/shapefile/file.shp", "layer_name_you_like", "ogr")
68 | QgsProject.instance().addMapLayer(vlayer)
69 | ```
70 |
71 | - dxf(注意数据源uri中的内部选项):
72 |
73 | ```python
74 | uri = "/path/to/dxffile/file.dxf|layername=entities|geometrytype=Point"
75 | vlayer = QgsVectorLayer(uri, "layer_name_you_like", "ogr")
76 | QgsProject.instance().addMapLayer(vlayer)
77 | ```
78 |
79 | - PostGIS 数据库——数据源是一个字符串,其中包含与PostgreSQL数据库创建连接所需的所有信息。
80 |
81 | [`QgsDataSourceUri`](https://qgis.org/pyqgis/master/core/QgsDataSourceUri.html#qgis.core.QgsDataSourceUri)类可以为你生成这个字符串。请注意,必须在编译QGIS时支持Postgres,否则此提供者不可用:
82 |
83 | ```python
84 | uri = QgsDataSourceUri()
85 | # 设置主机,端口,数据库名称,用户名和密码
86 | uri.setConnection("localhost", "5432", "dbname", "johny", "xxx")
87 | # 设置数据库架构,表名,几何列和可选项(WHERE 语句)
88 | uri.setDataSource("public", "roads", "the_geom", "cityid = 2643", "primary_key_field")
89 | vlayer = QgsVectorLayer(uri.uri(False), "layer name you like", "postgres")
90 | ```
91 |
92 | : !!! 提示
93 |
94 | `uri.uri(False)`中的`False`参数可以防止扩展认证配置参数,如果你没有使用任何身份验证配置,则此参数不会产生任何差异。
95 |
96 | - CSV或其他分隔的文本文件——打开一个用分号作为分隔符的文件,X坐标使用字段“x”,Y坐标使用字段“y”:
97 |
98 | ```python
99 | uri = "file://{}/testdata/delimited_xy.csv?delimiter={}&xField={}&yField={}".format(os.getcwd(), ";", "x", "y")
100 | vlayer = QgsVectorLayer(uri, "layer name you like", "delimitedtext")
101 | QgsProject.instance().addMapLayer(vlayer)
102 | ```
103 |
104 | : !!! 提示
105 |
106 | 提供者的字符串作为一个URL,因此路径必须以file://为前缀。它还允许WKT((well known text)格式的几何作为`x`和`y`的替代字段,并允许指定坐标参考系统。例如:
107 |
108 | ```python
109 | uri = "file:///some/path/file.csv?delimiter={}&crs=epsg:4723&wktField={}".format(";", "shape")
110 | ```
111 |
112 | - GPX文件——“gpx”数据提供者从gpx文件中读取轨迹,路线和路点。要打开文件,需要在url中指定类型(track / route / waypoint):
113 |
114 | ```python
115 | uri = "path/to/gpx/file.gpx?type=track"
116 | vlayer = QgsVectorLayer(uri, "layer name you like", "gpx")
117 | QgsProject.instance().addMapLayer(vlayer)
118 | ```
119 |
120 | - SpatiaLite数据库——与PostGIS数据库类似, [`QgsDataSourceUri`](https://qgis.org/pyqgis/master/core/QgsDataSourceUri.html#qgis.core.QgsDataSourceUri)可用于生成数据源标识:
121 |
122 | ```python
123 | uri = QgsDataSourceUri()
124 | uri.setDatabase('/home/martin/test-2.3.sqlite')
125 | schema = ''
126 | table = 'Towns'
127 | geom_column = 'Geometry'
128 | uri.setDataSource(schema, table, geom_column)
129 | display_name = 'Towns'
130 | vlayer = QgsVectorLayer(uri.uri(), display_name, 'spatialite')
131 | QgsProject.instance().addMapLayer(vlayer)
132 | ```
133 |
134 | - 基于MySQL WKB的几何,通过GDAL——数据源是连接表的字符串:
135 |
136 | ```python
137 | uri = "MySQL:dbname,host=localhost,port=3306,user=root,password=xxx|layername=my_table"
138 | vlayer = QgsVectorLayer( uri, "my table", "ogr" )
139 | QgsProject.instance().addMapLayer(vlayer)
140 | ```
141 |
142 | - WFS连接:连接使用URI定义并使用`WFS`提供者:
143 |
144 | ```python
145 | uri = "https://demo.geo-solutions.it/geoserver/ows?service=WFS&version=1.1.0&request=GetFeature&typename=geosolutions:regioni"
146 | vlayer = QgsVectorLayer(uri, "my wfs layer", "WFS")
147 | ```
148 | 可以使用标准库`urllib`创建uri:
149 | ```python
150 | import urllib
151 |
152 | params = {
153 | 'service': 'WFS',
154 | 'version': '1.1.0',
155 | 'request': 'GetFeature',
156 | 'typename': 'geosolutions:regioni',
157 | 'srsname': "EPSG:4326"
158 | }
159 | uri2 = 'https://demo.geo-solutions.it/geoserver/ows?' + urllib.parse.unquote(urllib.parse.urlencode(params))
160 | ```
161 |
162 | : !!! 提示
163 |
164 | 可以通过调用[`QgsVectorLayer`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer)的 [`setDataSource()`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.setDataSource) 方法更改现有图层的数据源,如下面例子:
165 |
166 | ```python
167 | uri = "https://demo.geo-solutions.it/geoserver/ows?service=WFS&version=1.1.0&request=GetFeature&typename=geosolutions:regioni"
168 | provider_options = QgsDataProvider.ProviderOptions()
169 | # 使用项目的转换上下文
170 | provider_options.transformContext = QgsProject.instance().transformContext()
171 | vlayer.setDataSource(uri, "layer name you like", "WFS", provider_options)
172 |
173 | del(vlayer)
174 | ```
175 |
176 |
177 | ## 3.2 栅格图层
178 |
179 | 访问栅格文件,用到了GDAL库。它支持多种文件格式。如果你在打开某些文件时遇到问题,请检查你的GDAL是否支持(默认情况下并非所有格式都可用)。从文件加载栅格,需要指定其文件名和显示名称:
180 |
181 | ```python
182 | # 获取tif文件的路径,例如:/home/project/data/srtm.tif
183 | path_to_tif = "qgis-projects/python_cookbook/data/srtm.tif"
184 | rlayer = QgsRasterLayer(path_to_tif, "SRTM layer name")
185 | if not rlayer.isValid():
186 | print("图层加载失败!")
187 | ```
188 |
189 | 从`geopackage`中加载栅格:
190 |
191 | ```python
192 | # 获取geopackage的路径,例如:/home/project/data/data.gpkg
193 | path_to_gpkg = os.path.join(os.getcwd(), "testdata", "sublayers.gpkg")
194 | # gpkg_raster_layer = "GPKG:/home/project/data/data.gpkg:srtm"
195 | gpkg_raster_layer = "GPKG:" + path_to_gpkg + ":srtm"
196 | rlayer = QgsRasterLayer(gpkg_raster_layer, "layer name you like", "gdal")
197 | if not rlayer.isValid():
198 | print("图层加载失败!")
199 | ```
200 |
201 | 与矢量图层类似,可以使用[`QgisInterface`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)对象的`addRasterLayer`函数加载栅格图层:
202 |
203 | ```python
204 | iface.addRasterLayer("/path/to/raster/file.tif", "layer name you like")
205 | ```
206 |
207 | 这将创建一个新图层,并将其添加到当前项目中(使其显示在图层列表中)。
208 |
209 | ### 3.2.1 加载PostGIS栅格图层
210 |
211 | PostGIS栅格图层,类似于PostGIS矢量图层,可以使用URI字符串添加到项目中。为数据库连接参数保留可复用字典是有效的。这样可以轻松编辑适用连接的字典。使用`postgresraster`提供者的元数据对象将字典编码为URI。之后,可以将栅格图层添加到项目中。
212 |
213 | ```python
214 | uri_config = {
215 | # 数据库参数
216 | 'dbname':'gis_db', # 数据库名称
217 | 'host':'localhost', # IP地址或者localhost
218 | 'port':'5432', # 端口
219 | 'sslmode':QgsDataSourceUri.SslDisable, # SslAllow, SslPrefer, SslRequire, SslVerifyCa, SslVerifyFull
220 | # 如果存储在authcfg或服务中,则不需要用户名和密码
221 | 'authcfg':'QconfigId', # QGIS认证数据库ID包含连接信息
222 | 'service': None, # 连接数据库的服务
223 | 'username':None, # 用户名
224 | 'password':None, # 密码
225 | # 数据库表和栅格字段信息
226 | 'schema':'public', # 数据库架构
227 | 'table':'my_rasters', # 数据库表名称
228 | 'geometrycolumn':'rast',# 栅格字段名称
229 | 'sql':None, # WHERE查询语句,应该放在字符串末尾。
230 | 'key':None, # 数据库表主键
231 | 'srid':None, # 字符串,指定坐标参考系
232 | 'estimatedmetadata':'False', # A boolean value telling if the metadata is estimated.
233 | 'type':None, # WKT字符串,指定WKB类型
234 | 'selectatid':None, # 设置为True可禁用按要素ID进行的选择。
235 | 'options':None, # 不在此列表中的其他PostgreSQL连接选项。
236 | 'enableTime': None,
237 | 'temporalDefaultTime': None,
238 | 'temporalFieldIndex': None,
239 | 'mode':'2', # GDAL参数, 2 unions raster tiles, 1 adds tiles separately (may require user input)
240 | }
241 | # 删除空参数
242 | uri_config = {key:val for key, val in uri_config.items() if val is not None}
243 | # 获取数据提供者和配置的元数据
244 | md = QgsProviderRegistry.instance().providerMetadata('postgresraster')
245 | uri = QgsDataSourceUri(md.encodeUri(uri_config))
246 |
247 | # 加载栅格图层到项目
248 | rlayer = iface.addRasterLayer(uri.uri(False), "raster layer name", "postgresraster")
249 | ```
250 |
251 | ### 3.2.2 加载WCS服务栅格图层:
252 |
253 | ```python
254 | layer_name = 'nurc:mosaic'
255 | uri = "https://demo.geo-solutions.it/geoserver/ows?identifier={}".format(layer_name)
256 | rlayer = QgsRasterLayer(uri, 'my wcs layer', 'wcs')
257 | ```
258 |
259 | 以下是WCS URI可以包含的参数说明:
260 |
261 | WCS URI由 **键=值** 对组成,分隔符:`&`。它与URL中的查询字符串格式相同,编码方式相同。[`QgsDataSourceUri`](https://qgis.org/pyqgis/master/core/QgsDataSourceUri.html#qgis.core.QgsDataSourceUri) 可以用于构造URI以确保正确编码特殊字符。
262 |
263 | - **url**(必填):WCS服务器URL。不要在URL中使用VERSION,因为每个版本的WCS对 **GetCapabilities** 版本使用不同的参数名称,请参阅param版本。
264 | - **identifier**(必填):覆盖范围名称
265 | - **time**(可选):时间位置或时间段(beginPosition / endPosition / timeResolution)
266 | - **format**(可选):支持的格式名称。默认是第一个支持的格式,其名称为tif或第一个支持的格式。
267 | - **crs**(可选):CRS格式为AUTHORITY:ID,例如,EPSG:4326。默认为EPSG:4326(如果支持)或第一个支持的CRS。
268 | - **username**(可选):基本身份验证的用户名。
269 | - **password**(可选):基本身份验证的密码。
270 | - **IgnoreGetMapUrl**(可选,hack):如果指定(设置为1),则忽略GetCapabilities公布的GetCoverage URL。如果未正确配置服务器,则可能需要。
271 | - **InvertAxisOrientation**(可选,hack):如果指定(设置为1),则在GetCoverage请求中切换轴。如果服务器使用错误的轴顺序,则可能需要地理CRS。
272 | - **IgnoreAxisOrientation**(可选,hack):如果指定(设置为1),则不要根据地理CRS的WCS标准反转轴方向。
273 | - **cache**(可选):缓存加载控制,如QNetworkRequest :: CacheLoadControl中所述,但如果使用AlwaysCache失败,请求将重新发送为PreferCache。允许的值:AlwaysCache,PreferCache,PreferNetwork,AlwaysNetwork。默认为AlwaysCache。
274 |
275 | 另外,你可以从WMS服务器加载栅格图层。但是目前无法从API访问GetCapabilities响应——你必须知道所需的图层:
276 |
277 | ```python
278 | urlWithParams = 'url=http://irs.gis-lab.info/?layers=landsat&styles=&format=image/jpeg&crs=EPSG:4326'
279 | rlayer = QgsRasterLayer(urlWithParams, 'some layer name', 'wms')
280 | if not rlayer.isValid():
281 | print("图层加载失败!")
282 | ```
283 |
284 | ## 3.3 QgsProject 实例
285 |
286 | 如果你想使用打开的图层进行渲染,请不要忘记将它们添加到[`QgsProject`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)实例中。[`QgsProject`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)实例拥有图层的所有权,可以通过其唯一ID从应用程序的任何部分访问它们。从项目中删除图层时,它也会被删除。用户可以在QGIS接口中删除图层,也可以通过Python使用[`removeMapLayer()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.removeMapLayer)方法删除图层。
287 |
288 | 使用[`addMapLayer()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.addMapLayer)方法将图层添加到当前项目:
289 |
290 | ```python
291 | QgsProject.instance().addMapLayer(rlayer)
292 | ```
293 |
294 | 在绝对位置添加图层:
295 |
296 | ```python
297 | # 首先添加图层但不显示
298 | QgsProject.instance().addMapLayer(rlayer, False)
299 | # 在项目中获取图层树的根图层组
300 | layerTree = iface.layerTreeCanvasBridge().rootGroup()
301 | # 第一个参数是一个从0开始的数字,-1表示结束
302 | layerTree.insertChildNode(-1, QgsLayerTreeLayer(rlayer))
303 | ```
304 |
305 | 如果要删除图层,使用[`removeMapLayer()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.removeMapLayer)方法:
306 |
307 | ```python
308 | QgsProject.instance().removeMapLayer(rlayer.id())
309 | ```
310 |
311 | 在上面的代码中,传递了图层ID(你可以调用图层的[`id()`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.id)方法),但是你也可以传递图层对象本身。
312 |
313 | 获取已加载图层和图层ID的列表,使用[`mapLayers()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.mapLayers)方法:
314 |
315 | ```python
316 | QgsProject.instance().mapLayers()
317 | ```
318 |
319 |
--------------------------------------------------------------------------------
/docs/9-使用地图画布.md:
--------------------------------------------------------------------------------
1 | 本节代码片段需要导入以下模块:
2 |
3 | ```python
4 | from qgis.PyQt.QtGui import (
5 | QColor,
6 | )
7 |
8 | from qgis.PyQt.QtCore import Qt, QRectF
9 |
10 | from qgis.PyQt.QtWidgets import QMenu
11 |
12 | from qgis.core import (
13 | QgsVectorLayer,
14 | QgsPoint,
15 | QgsPointXY,
16 | QgsProject,
17 | QgsGeometry,
18 | QgsMapRendererJob,
19 | QgsWkbTypes,
20 | )
21 |
22 | from qgis.gui import (
23 | QgsMapCanvas,
24 | QgsVertexMarker,
25 | QgsMapCanvasItem,
26 | QgsMapMouseEvent,
27 | QgsRubberBand,
28 | )
29 | ```
30 |
31 | # 9 使用地图画布
32 |
33 | 地图画布控件可能是QGIS中最重要的控件,因为它显示了由重叠地图图层组成的地图,并允许与地图和图层进行交互。画布始终显示由当前画布范围定义地图的一部分。通过使用 **地图工具** 完成交互:有平移,缩放,识别图层,测量,矢量编辑等工具。与其他图形程序类似,总有一个工具处于活动状态,用户可以在可用工具之间切换。
34 |
35 | 地图画布由`qgis.gui`模块中的[`QgsMapCanvas`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas)类实现。该类基于Qt Graphics View框架。该框架通常提供场景和视图,其中放置自定义图形项,并且用户可以与它们交互。我们假设你对Qt足够熟悉,了解图形场景,视图和项的概念。如果没有,请阅读[框架概述](https://doc.qt.io/qt-5/graphicsview.html)。
36 |
37 | 无论何时平移,放大/缩小(或触发刷新的其他操作)地图,地图都会在当前范围内再次渲染。图层将渲染为图像(使用[`QgsMapRendererJob`](https://qgis.org/pyqgis/master/core/QgsMapRendererJob.html#qgis.core.QgsMapRendererJob)类),并显示在画布上。[`QgsMapCanvas`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas)类还控制渲染图的刷新。除了作为背景的项,可能还有更多的 **地图画布项** 。
38 |
39 | 典型的地图画布项是橡皮条(用于测量,矢量编辑等)或顶点标记。画布项通常用于给地图工具提供视觉反馈,例如,在创建新多边形时,地图工具会创建一个橡皮条画布项,显示多边形的当前形状。所有地图画布项都是[`QgsMapCanvasItem`](https://qgis.org/pyqgis/master/gui/QgsMapCanvasItem.html#qgis.gui.QgsMapCanvasItem) 的子类,它为基类`QGraphicsItem`对象添加了更多功能。
40 |
41 | 总而言之,地图画布架构包含三个概念:
42 |
43 | - 地图画布——用于浏览地图
44 | - 地图画布项——可以在地图画布上显示的其他项
45 | - 地图工具——用于与地图画布交互
46 |
47 | ## 9.1 嵌入地图画布
48 |
49 | 地图画布是一个控件,就像任何其他Qt控件一样,因此使用它就像创建和显示它一样简单
50 |
51 | ```python
52 | canvas = QgsMapCanvas()
53 | canvas.show()
54 | ```
55 |
56 | 这将生成一个带有地图画布的独立窗口。它也可以嵌入到现有的控件或窗口中。使用`.ui`文件和Qt设计师时,在表单上放置一个`QWidget`并将其提升为新类:设置`QgsMapCanvas`为类名并设置`qgis.gui`为头文件。`pyuic5`工具将搞定它(译者注:编译为py脚本文件)。这是嵌入画布的一种非常方便的方法。另一种可能性是手动编写代码构造地图画布和其他控件(作为主窗口或对话框的子窗口)并创建布局。
57 |
58 | 默认情况下,地图画布具有黑色背景,不使用消除锯齿。设置白色背景并启用抗锯齿来实现平滑渲染
59 |
60 | ```python
61 | canvas.setCanvasColor(Qt.white)
62 | canvas.enableAntiAliasing(True)
63 | ```
64 |
65 | (如果你想知道,`Qt`来自`PyQt.QtCore`模块,并且 `Qt.white`是预定义的`QColor`实例之一。)
66 |
67 | 现在是时候添加一些地图图层了。我们首先打开一个图层并将其添加到当前项目中。然后我们将设置画布范围并设置画布的图层列表
68 |
69 | ```python
70 | vlayer = QgsVectorLayer("testdata/airports.shp", "Airports layer", "ogr")
71 | if not vlayer.isValid():
72 | print("图层加载失败!")
73 |
74 | # 将图层添加到注册表
75 | QgsProject.instance().addMapLayer(vlayer)
76 |
77 | # 缩放到图层
78 | canvas.setExtent(vlayer.extent())
79 |
80 | # 设置地图画布的图层集
81 | canvas.setLayers([vlayer])
82 | ```
83 |
84 | 执行这些命令后,画布将显示已加载的图层。
85 |
86 | ## 9.2 橡皮条和顶点标记
87 |
88 | 在画布上的地图顶部显示一些其他数据,使用地图画布项。可以创建自定义画布项类(如下所述),但为方便起见,有两个有用的画布项类:[`QgsRubberBand`](https://qgis.org/pyqgis/master/gui/QgsRubberBand.html#qgis.gui.QgsRubberBand)用于绘制折线或多边形,[`QgsVertexMarker`](https://qgis.org/pyqgis/master/gui/QgsVertexMarker.html#qgis.gui.QgsVertexMarker)绘制点。它们都使用地图坐标,因此在平移或缩放画布时会自动移动/缩放形状。
89 |
90 | 显示折线
91 |
92 | ```python
93 | r = QgsRubberBand(canvas, QgsWkbTypes.LineGeometry) # 线
94 | points = [QgsPointXY(-100, 45), QgsPointXY(10, 60), QgsPointXY(120, 45)]
95 | r.setToGeometry(QgsGeometry.fromPolyline(points), None)
96 | ```
97 |
98 | 显示多边形
99 |
100 | ```python
101 | r = QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry) # 多边形
102 | points = [[QgsPointXY(-100, 35), QgsPointXY(10, 50), QgsPointXY(120, 35)]]
103 | r.setToGeometry(QgsGeometry.fromPolygonXY(points), None)
104 | ```
105 |
106 | 请注意,多边形的点不是普通列表:实际上,它是包含多边形线环的环列表:第一个环是外边框,第二个(可选)环对应于多边形中的孔。
107 |
108 | 橡皮条允许一些定制,即改变它们的颜色和线宽
109 |
110 | ```python
111 | r.setColor(QColor(0, 0, 255))
112 | r.setWidth(3)
113 | ```
114 |
115 | 画布项绑定到画布场景。要暂时隐藏它们(并再次显示它们),请使用`hide()`和`show()`组合。完全删除该项,你必须将其从画布的场景中删除
116 |
117 | ```python
118 | canvas.scene().removeItem(r)
119 | ```
120 |
121 | (在C ++中,可以只删除该项,但是在Python`del r`中 只删除引用,并且该对象仍然存在,因为它由画布拥有)
122 |
123 | 橡皮条也可用于绘制点,但 [`QgsVertexMarker`](https://qgis.org/pyqgis/master/gui/QgsVertexMarker.html#qgis.gui.QgsVertexMarker)类更适合于此([`QgsRubberBand`](https://qgis.org/pyqgis/master/gui/QgsRubberBand.html#qgis.gui.QgsRubberBand)仅在所需点周围绘制一个矩形)。
124 |
125 | 你可以像这样使用顶点标记:
126 |
127 | ```python
128 | m = QgsVertexMarker(canvas)
129 | m.setCenter(QgsPointXY(10,40))
130 | ```
131 |
132 | 这将在位置 **[10,45]** 上绘制一个红十字。可以自定义图标类型,大小,颜色和宽度
133 |
134 | ```python
135 | m.setColor(QColor(0, 255, 0))
136 | m.setIconSize(5)
137 | m.setIconType(QgsVertexMarker.ICON_BOX) # or ICON_CROSS, ICON_X
138 | m.setPenWidth(3)
139 | ```
140 |
141 | 临时隐藏顶点标记并从画布中删除它们,使用与橡皮条相同的方法。
142 |
143 | ## 9.3 在画布中使用地图工具
144 |
145 | 以下示例构造一个窗口,其中包含用于地图平移和缩放的地图画布和基本地图工具。激活每个工具:平移工具[`QgsMapToolPan`](https://qgis.org/pyqgis/master/gui/QgsMapToolPan.html#qgis.gui.QgsMapToolPan),放大缩小工具[`QgsMapToolZoom`](https://qgis.org/pyqgis/master/gui/QgsMapToolZoom.html#qgis.gui.QgsMapToolZoom)。设置为可被选中,允许自动处理选中/未选中的操作状态——当激活地图工具时,一个工具被选中时,取消选中上一个工具。使用[`setMapTool()`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas.setMapTool)方法激活地图工具。
146 |
147 | ```python
148 | from qgis.gui import *
149 | from qgis.PyQt.QtWidgets import QAction, QMainWindow
150 | from qgis.PyQt.QtCore import Qt
151 |
152 |
153 | class MyWnd(QMainWindow):
154 | def __init__(self, layer):
155 | QMainWindow.__init__(self)
156 |
157 | self.canvas = QgsMapCanvas()
158 | self.canvas.setCanvasColor(Qt.white)
159 |
160 | self.canvas.setExtent(layer.extent())
161 | self.canvas.setLayers([layer])
162 |
163 | self.setCentralWidget(self.canvas)
164 |
165 | self.actionZoomIn = QAction("Zoom in", self)
166 | self.actionZoomOut = QAction("Zoom out", self)
167 | self.actionPan = QAction("Pan", self)
168 |
169 | self.actionZoomIn.setCheckable(True)
170 | self.actionZoomOut.setCheckable(True)
171 | self.actionPan.setCheckable(True)
172 |
173 | self.actionZoomIn.triggered.connect(self.zoomIn)
174 | self.actionZoomOut.triggered.connect(self.zoomOut)
175 | self.actionPan.triggered.connect(self.pan)
176 |
177 | self.toolbar = self.addToolBar("Canvas actions")
178 | self.toolbar.addAction(self.actionZoomIn)
179 | self.toolbar.addAction(self.actionZoomOut)
180 | self.toolbar.addAction(self.actionPan)
181 |
182 | # 创建地图工具
183 | self.toolPan = QgsMapToolPan(self.canvas)
184 | self.toolPan.setAction(self.actionPan)
185 | self.toolZoomIn = QgsMapToolZoom(self.canvas, False) # false = in
186 | self.toolZoomIn.setAction(self.actionZoomIn)
187 | self.toolZoomOut = QgsMapToolZoom(self.canvas, True) # true = out
188 | self.toolZoomOut.setAction(self.actionZoomOut)
189 |
190 | self.pan()
191 |
192 | def zoomIn(self):
193 | self.canvas.setMapTool(self.toolZoomIn)
194 |
195 | def zoomOut(self):
196 | self.canvas.setMapTool(self.toolZoomOut)
197 |
198 | def pan(self):
199 | self.canvas.setMapTool(self.toolPan)
200 |
201 | ```
202 |
203 | 你可以在Python控制台编辑器中尝试上述代码。调用画布窗口,添加以下代码以实例化`MyWnd`类。它们将在新创建的画布上渲染当前选定的图层
204 |
205 | ```python
206 | w = MyWnd(iface.activeLayer())
207 | w.show()
208 | ```
209 |
210 | ### 9.3.1 使用QgsMapToolIdentifyFeature选择要素
211 |
212 | 你可以使用地图工具[`QgsMapToolIdentifyFeature`](https://qgis.org/pyqgis/master/gui/QgsMapToolIdentifyFeature.html#qgis.gui.QgsMapToolIdentifyFeature)为用户选择一个要素,这个要素将被传递给回调函数。
213 |
214 | ```python
215 | def callback(feature):
216 | """当用户选择要素后被调用"""
217 | print("You clicked on feature {}".format(feature.id()))
218 |
219 |
220 | canvas = iface.mapCanvas()
221 | feature_identifier = QgsMapToolIdentifyFeature(canvas)
222 |
223 | # 表示被选择的图层
224 | feature_identifier.setLayer(vlayer)
225 |
226 | # 当用户识别要素时触发槽函数,使用回调函数
227 | feature_identifier.featureIdentified.connect(callback)
228 |
229 | # 激活这个地图工具
230 | canvas.setMapTool(feature_identifier)
231 | ```
232 |
233 | ### 9.3.2 将项目添加到地图画布上下文菜单
234 |
235 | 使用地图画布也可以通过使用[`contextMenuAboutToShow`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas.contextMenuAboutToShow)信号添加到其上下文菜单中的条目来完成。
236 |
237 | 当你右键单击地图画布时,以下代码会在默认条目旁边添加 **My Menu** ► **My Action** 操作。
238 |
239 | ```python
240 | # 用于填充上下文菜单的插槽
241 | def populateContextMenu(menu: QMenu, event: QgsMapMouseEvent):
242 | subMenu = menu.addMenu('My Menu')
243 | action = subMenu.addAction('My Action')
244 | action.triggered.connect(lambda *args:
245 | print(f'Action triggered at {event.x()},{event.y()}'))
246 |
247 | canvas.contextMenuAboutToShow.connect(populateContextMenu)
248 | canvas.show()
249 | ```
250 |
251 | ## 9.4 编写自定义地图工具
252 |
253 | 你可以编写自定义工具,来实现用户在画布上执行自定义行为的操作。
254 |
255 | 地图工具应继承自[`QgsMapTool`](https://qgis.org/pyqgis/master/gui/QgsMapTool.html#qgis.gui.QgsMapTool)类或任何派生类,并使用[`setMapTool()`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas.setMapTool) 在画布中选择为激活工具。
256 |
257 | 下面是一个地图工具示例,它允许通过在画布上单击并拖动来定义矩形范围。定义矩形后,它会在控制台中打印其边界坐标。它使用前面描述的橡皮条元素来显示所定义的矩形。
258 |
259 | ```python
260 | class RectangleMapTool(QgsMapToolEmitPoint):
261 | def __init__(self, canvas):
262 | self.canvas = canvas
263 | QgsMapToolEmitPoint.__init__(self, self.canvas)
264 | self.rubberBand = QgsRubberBand(self.canvas, QgsWkbTypes.PolygonGeometry)
265 | self.rubberBand.setColor(Qt.red)
266 | self.rubberBand.setWidth(1)
267 | self.reset()
268 |
269 | def reset(self):
270 | self.startPoint = self.endPoint = None
271 | self.isEmittingPoint = False
272 | self.rubberBand.reset(QgsWkbTypes.PolygonGeometry)
273 |
274 | def canvasPressEvent(self, e):
275 | self.startPoint = self.toMapCoordinates(e.pos())
276 | self.endPoint = self.startPoint
277 | self.isEmittingPoint = True
278 | self.showRect(self.startPoint, self.endPoint)
279 |
280 | def canvasReleaseEvent(self, e):
281 | self.isEmittingPoint = False
282 | r = self.rectangle()
283 | if r is not None:
284 | print("Rectangle:", r.xMinimum(),
285 | r.yMinimum(), r.xMaximum(), r.yMaximum()
286 | )
287 |
288 | def canvasMoveEvent(self, e):
289 | if not self.isEmittingPoint:
290 | return
291 |
292 | self.endPoint = self.toMapCoordinates(e.pos())
293 | self.showRect(self.startPoint, self.endPoint)
294 |
295 | def showRect(self, startPoint, endPoint):
296 | self.rubberBand.reset(QgsWkbTypes.PolygonGeometry)
297 | if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
298 | return
299 |
300 | point1 = QgsPointXY(startPoint.x(), startPoint.y())
301 | point2 = QgsPointXY(startPoint.x(), endPoint.y())
302 | point3 = QgsPointXY(endPoint.x(), endPoint.y())
303 | point4 = QgsPointXY(endPoint.x(), startPoint.y())
304 |
305 | self.rubberBand.addPoint(point1, False)
306 | self.rubberBand.addPoint(point2, False)
307 | self.rubberBand.addPoint(point3, False)
308 | self.rubberBand.addPoint(point4, True) # true to update canvas
309 | self.rubberBand.show()
310 |
311 | def rectangle(self):
312 | if self.startPoint is None or self.endPoint is None:
313 | return None
314 | elif (self.startPoint.x() == self.endPoint.x() or \
315 | self.startPoint.y() == self.endPoint.y()):
316 | return None
317 |
318 | return QgsRectangle(self.startPoint, self.endPoint)
319 |
320 | def deactivate(self):
321 | QgsMapTool.deactivate(self)
322 | self.deactivated.emit()
323 |
324 | ```
325 |
326 | ## 9.5 编写自定义地图画布项
327 |
328 | 这是一个自定义画布项的示例,该画布项绘制了一个圆:
329 |
330 | ```python
331 | class CircleCanvasItem(QgsMapCanvasItem):
332 | def __init__(self, canvas):
333 | super().__init__(canvas)
334 | self.center = QgsPoint(0, 0)
335 | self.size = 100
336 |
337 | def setCenter(self, center):
338 | self.center = center
339 |
340 | def center(self):
341 | return self.center
342 |
343 | def setSize(self, size):
344 | self.size = size
345 |
346 | def size(self):
347 | return self.size
348 |
349 | def boundingRect(self):
350 | return QRectF(self.center.x() - self.size / 2,
351 | self.center.y() - self.size / 2,
352 | self.center.x() + self.size / 2,
353 | self.center.y() + self.size / 2)
354 |
355 | def paint(self, painter, option, widget):
356 | path = QPainterPath()
357 | path.moveTo(self.center.x(), self.center.y());
358 | path.arcTo(self.boundingRect(), 0.0, 360.0)
359 | painter.fillPath(path, QColor("red"))
360 |
361 |
362 | # 使用自定义项:
363 | item = CircleCanvasItem(iface.mapCanvas())
364 | item.setCenter(QgsPointXY(200, 200))
365 | item.setSize(80)
366 |
367 | ```
368 |
--------------------------------------------------------------------------------
/docs/15-任务——在后台做繁重的工作.md:
--------------------------------------------------------------------------------
1 | 本节代码片段需导入以下模块:
2 |
3 | ```python
4 | from qgis.core import (
5 | Qgis,
6 | QgsApplication,
7 | QgsMessageLog,
8 | QgsProcessingAlgRunnerTask,
9 | QgsProcessingContext,
10 | QgsProcessingFeedback,
11 | QgsProject,
12 | QgsTask,
13 | QgsTaskManager,
14 | )
15 | ```
16 |
17 | # 15 任务 - 在后台做繁重的工作
18 |
19 | ## 15.1 引言
20 |
21 | 使用线程的后台处理,是在进行繁重处理时保持用户界面响应的一种方式。任务可用于在QGIS中实现线程。
22 |
23 | 任务([`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask))是在后台执行代码的容器,任务管理([`QgsTaskManager`](https://qgis.org/pyqgis/master/core/QgsTaskManager.html#qgis.core.QgsTaskManager))用于控制任务的运行。这些类通过提供信号传递机制、进度报告和后台进程状态访问机制,简化了QGIS中的后台处理。可以使用子任务对任务进行分组。
24 |
25 | 全局任务管理器([`QgsApplication.taskManager()`](https://qgis.org/pyqgis/master/core/QgsApplication.html#qgis.core.QgsApplication.taskManager))通常被使用。这意味着你的任务可能不是由任务管理器控制的唯一任务。
26 |
27 | 有几种方法可以创建QGIS任务:
28 |
29 | - 通过扩展[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask)创建自己的任务
30 |
31 | ```python
32 | class SpecialisedTask(QgsTask):
33 | pass
34 | ```
35 |
36 | - 从函数创建任务
37 |
38 | ```python
39 | def heavyFunction():
40 | # 一些CPU密集型处理 ...
41 | pass
42 |
43 | def workdone():
44 | # ... 使用结果做一些有用的事情
45 | pass
46 |
47 | task = QgsTask.fromFunction('heavy function', heavyFunction,
48 | on_finished=workdone)
49 | ```
50 |
51 | - 从处理算法创建任务
52 |
53 | ```python
54 | params = dict()
55 | context = QgsProcessingContext()
56 | context.setProject(QgsProject.instance())
57 | feedback = QgsProcessingFeedback()
58 |
59 | buffer_alg = QgsApplication.instance().processingRegistry().algorithmById('native:buffer')
60 | task = QgsProcessingAlgRunnerTask(buffer_alg, params, context,feedback)
61 | ```
62 |
63 | !!! warning 警告
64 |
65 | 任何后台任务(无论如何创建)决不能使用任何主线程上的QObject,比如访问QgsVectorLayer, QgsProject或者执行任何GUI操作——比如创建新的部件或者与现有部件交互。只能从主线程访问或修改Qt控件。在任务启动之前,必须复制任务中使用的数据。试图从后台线程使用它们将导致崩溃。
66 |
67 | 此外,请始终确保并 [context](https://qgis.org/pyqgis/master/core/QgsProcessingContext.html#qgis.core.QgsProcessingContext) 和 [feedback](https://qgis.org/pyqgis/3.34/core/QgsProcessingFeedback.html#qgis.core.QgsProcessingFeedback) 至少与使用它们的任务一样长。如果在完成任务后,QGSTaskManager无法访问计划任务的上下文和反馈,则QGIS将崩溃。
68 |
69 | !!! info
70 |
71 | 在调用 `QgsProcessingContext` 后不久调用 [setProject()](https://qgis.org/pyqgis/master/core/QgsProcessingContext.html#qgis.core.QgsProcessingContext.setProject) 是一种常见的模式。这允许任务及其回调函数使用大多数项目范围的设置。这在回调函数中使用空间图层时特别有价值。
72 |
73 | 可以使用[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask)中的[`addSubTask()`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask.addSubTask) 函数来描述任务之间的依赖关系。当声明依赖关系时,任务管理器将自动确定如何执行这些依赖关系。只要有可能,依赖项将并行执行,以便尽快满足它们。如果取消了一个任务所依赖的任务,则相关任务也将被取消。循环依赖可能造成死锁,所以要小心。
74 |
75 | 如果任务依赖于可用的图层,则可以使用[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask)中的[`setDependentLayers`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask.setDependentLayers) 函数来声明。如果任务所依赖的图层不可用,则该任务将被取消。
76 |
77 | 创建任务后,可以使用任务管理器的[`addTask()`](https://qgis.org/pyqgis/master/core/QgsTaskManager.html#qgis.core.QgsTaskManager.addTask)函数调度任务运行。向管理器添加任务会自动将该任务的所有权转移给管理员,管理员将在执行完后清理和删除任务。任务的调度受任务优先级的影响,任务优先级在[`addTask()`](https://qgis.org/pyqgis/master/core/QgsTaskManager.html#qgis.core.QgsTaskManager.addTask)中设置。
78 |
79 | 任务的状态可以使用[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask)、[`QgsTaskManager`](https://qgis.org/pyqgis/master/core/QgsTaskManager.html#qgis.core.QgsTaskManager)的信号和函数进行监控。
80 |
81 | ## 15.2 示例
82 |
83 | ### 15.2.1 扩展QgsTask
84 |
85 | 在此示例中,`RandomIntegerSumTask`扩展了[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask),它将在指定的时间段内生成0到500之间的100个随机整数。如果随机数为42,则中止任务并引发异常。`RandomIntegerSumTask`(带子任务)生成了几个实例并将其添加到任务管理器,展示两种类型的依赖项:
86 |
87 | ```python
88 | import random
89 | from time import sleep
90 |
91 | from qgis.core import (
92 | QgsApplication, QgsTask, QgsMessageLog, Qgis
93 | )
94 |
95 | MESSAGE_CATEGORY = 'RandomIntegerSumTask'
96 |
97 | class RandomIntegerSumTask(QgsTask):
98 | """展示如何子类化QgsTask"""
99 | def __init__(self, description, duration):
100 | super().__init__(description, QgsTask.CanCancel)
101 | self.duration = duration
102 | self.total = 0
103 | self.iterations = 0
104 | self.exception = None
105 |
106 | def run(self):
107 | """在这里你要实现你的任务。
108 | 应该定期测试isCanceled(),以便优雅地终止。
109 | 此方法必须返回True或False。
110 | 引发异常将使QGIS崩溃,因此我们在内部处理这些异常,并在self.finished中抛出。
111 | """
112 | QgsMessageLog.logMessage('Started task "{}"'.format(
113 | self.description()),
114 | MESSAGE_CATEGORY, Qgis.Info)
115 | wait_time = self.duration / 100
116 | for i in range(100):
117 | sleep(wait_time)
118 | # 使用setProgress报告进度
119 | self.setProgress(i)
120 | arandominteger = random.randint(0, 500)
121 | self.total += arandominteger
122 | self.iterations += 1
123 | # 检查isCanceled()处理取消
124 | if self.isCanceled():
125 | return False
126 | # 模拟异常情况
127 | if arandominteger == 42:
128 | # 不要raise Exception('bad value!'),否则将使QGIS崩溃
129 | self.exception = Exception('bad value!')
130 | return False
131 | return True
132 |
133 | def finished(self, result):
134 | """
135 | 当任务完成(无论成功与否)时,这个函数会被自动调用。
136 | 你可以通过实现 finished() 来执行任务完成后的后续事情。
137 | finished总是从主线程中调用的,因此在这里进行GUI操作和引发 Python 异常是安全的。
138 | result是self.run的返回值。
139 | """
140 | if result:
141 | QgsMessageLog.logMessage(
142 | 'Task "{name}" completed\n' \
143 | 'Total: {total} (with {iterations} '\
144 | 'iterations)'.format(
145 | name=self.description(),
146 | total=self.total,
147 | iterations=self.iterations),
148 | MESSAGE_CATEGORY, Qgis.Success)
149 | else:
150 | if self.exception is None:
151 | QgsMessageLog.logMessage(
152 | 'Task "{name}" not successful but without '\
153 | 'exception (probably the task was manually '\
154 | 'canceled by the user)'.format(
155 | name=self.description()),
156 | MESSAGE_CATEGORY, Qgis.Warning)
157 | else:
158 | QgsMessageLog.logMessage(
159 | 'Task "{name}" Exception: {exception}'.format(
160 | name=self.description(),
161 | exception=self.exception),
162 | MESSAGE_CATEGORY, Qgis.Critical)
163 | raise self.exception
164 |
165 | def cancel(self):
166 | QgsMessageLog.logMessage(
167 | 'Task "{name}" was canceled'.format(
168 | name=self.description()),
169 | MESSAGE_CATEGORY, Qgis.Info)
170 | super().cancel()
171 |
172 |
173 | longtask = RandomIntegerSumTask('waste cpu long', 20)
174 | shorttask = RandomIntegerSumTask('waste cpu short', 10)
175 | minitask = RandomIntegerSumTask('waste cpu mini', 5)
176 | shortsubtask = RandomIntegerSumTask('waste cpu subtask short', 5)
177 | longsubtask = RandomIntegerSumTask('waste cpu subtask long', 10)
178 | shortestsubtask = RandomIntegerSumTask('waste cpu subtask shortest', 4)
179 |
180 | # 添加子任务(shortsubtask)到shorttask——必须在minitask和longtask完成后执行
181 | shorttask.addSubTask(shortsubtask, [minitask, longtask])
182 | # 添加子任务(longsubtask)到longtask——必须父级任务之前运行
183 | longtask.addSubTask(longsubtask, [], QgsTask.ParentDependsOnSubTask)
184 | # 添加子任务(shortestsubtask)到longtask
185 | longtask.addSubTask(shortestsubtask)
186 |
187 | QgsApplication.taskManager().addTask(longtask)
188 | QgsApplication.taskManager().addTask(shorttask)
189 | QgsApplication.taskManager().addTask(minitask)
190 |
191 |
192 |
193 |
194 | # RandomIntegerSumTask(0): Started task "waste cpu subtask shortest"
195 | # RandomIntegerSumTask(0): Started task "waste cpu short"
196 | # RandomIntegerSumTask(0): Started task "waste cpu mini"
197 | # RandomIntegerSumTask(0): Started task "waste cpu subtask long"
198 | # RandomIntegerSumTask(3): Task "waste cpu subtask shortest" completed
199 | # RandomTotal: 25452 (with 100 iterations)
200 | # RandomIntegerSumTask(3): Task "waste cpu mini" completed
201 | # RandomTotal: 23810 (with 100 iterations)
202 | # RandomIntegerSumTask(3): Task "waste cpu subtask long" completed
203 | # RandomTotal: 26308 (with 100 iterations)
204 | # RandomIntegerSumTask(0): Started task "waste cpu long"
205 | # RandomIntegerSumTask(3): Task "waste cpu long" completed
206 | # RandomTotal: 22534 (with 100 iterations)
207 | ```
208 |
209 | ### 15.2.2 从函数创建任务
210 |
211 | 从函数创建任务(本示例中的`doSomething`)。该函数的第一个参数为[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask) 。一个重要的参数是`on_finished`,它是在任务完成时被调用的函数。示例中的`doSomething`函数有另一个参数`wait_time`。
212 |
213 | ```python
214 | import random
215 | from time import sleep
216 |
217 | MESSAGE_CATEGORY = 'TaskFromFunction'
218 |
219 | def doSomething(task, wait_time):
220 | """
221 | 抛出一个异常终止任务
222 | 成功则返回结果
223 | 结果将和异常一起传递给 (成功则为空)on_finished函数.
224 | 如果存在异常,结果为空
225 | """
226 | QgsMessageLog.logMessage('Started task {}'.format(task.description()),
227 | MESSAGE_CATEGORY, Qgis.Info)
228 | wait_time = wait_time / 100
229 | total = 0
230 | iterations = 0
231 | for i in range(100):
232 | sleep(wait_time)
233 | # 使用task.setProgress报告进度
234 | task.setProgress(i)
235 | arandominteger = random.randint(0, 500)
236 | total += arandominteger
237 | iterations += 1
238 | # 检查task.isCanceled()处理取消
239 | if task.isCanceled():
240 | stopped(task)
241 | return None
242 | # 抛出异常终止任务
243 | if arandominteger == 42:
244 | raise Exception('bad value!')
245 | return {'total': total, 'iterations': iterations,
246 | 'task': task.description()}
247 |
248 | def stopped(task):
249 | QgsMessageLog.logMessage(
250 | 'Task "{name}" was canceled'.format(
251 | name=task.description()),
252 | MESSAGE_CATEGORY, Qgis.Info)
253 |
254 | def completed(exception, result=None):
255 | """当doSomething完成时呗调佣
256 | 如果抛出异常则异常信息不是空
257 | 结果是doSomething返回的结果"""
258 | if exception is None:
259 | if result is None:
260 | QgsMessageLog.logMessage(
261 | 'Completed with no exception and no result '\
262 | '(probably manually canceled by the user)',
263 | MESSAGE_CATEGORY, Qgis.Warning)
264 | else:
265 | QgsMessageLog.logMessage(
266 | 'Task {name} completed\n'
267 | 'Total: {total} ( with {iterations} '
268 | 'iterations)'.format(
269 | name=result['task'],
270 | total=result['total'],
271 | iterations=result['iterations']),
272 | MESSAGE_CATEGORY, Qgis.Info)
273 | else:
274 | QgsMessageLog.logMessage("Exception: {}".format(exception),
275 | MESSAGE_CATEGORY, Qgis.Critical)
276 | raise exception
277 |
278 | # 创建一些任务
279 | task1 = QgsTask.fromFunction(u'Waste cpu 1', doSomething,
280 | on_finished=completed, wait_time=4)
281 | task2 = QgsTask.fromFunction(u'Waste cpu 2', dosomething,
282 | on_finished=completed, wait_time=3)
283 | QgsApplication.taskManager().addTask(task1)
284 | QgsApplication.taskManager().addTask(task2)
285 |
286 |
287 | # RandomIntegerSumTask(0): Started task "waste cpu subtask short"
288 | # RandomTaskFromFunction(0): Started task Waste cpu 1
289 | # RandomTaskFromFunction(0): Started task Waste cpu 2
290 | # RandomTaskFromFunction(0): Task Waste cpu 2 completed
291 | # RandomTotal: 23263 ( with 100 iterations)
292 | # RandomTaskFromFunction(0): Task Waste cpu 1 completed
293 | # RandomTotal: 25044 ( with 100 iterations)
294 | ```
295 |
296 | ### 14.2.3 处理算法任务
297 |
298 | 创建一个使用算法[`qgis:randompointsinextent`](https://docs.qgis.org/testing/en/docs/user_manual/processing_algs/qgis/vectorcreation.html#qgisrandompointsinextent)的任务,在指定范围内生成50000个随机点。结果以安全的方式添加到项目中。
299 |
300 | ```python
301 | from functools import partial
302 | from qgis.core import (QgsTaskManager, QgsMessageLog,
303 | QgsProcessingAlgRunnerTask, QgsApplication,
304 | QgsProcessingContext, QgsProcessingFeedback,
305 | QgsProject)
306 |
307 | MESSAGE_CATEGORY = 'AlgRunnerTask'
308 |
309 | def task_finished(context, successful, results):
310 | if not successful:
311 | QgsMessageLog.logMessage('Task finished unsucessfully',
312 | MESSAGE_CATEGORY, Qgis.Warning)
313 | output_layer = context.getMapLayer(results['OUTPUT'])
314 | # 因为getMapLayer没有移交所有权, 当上下文超出范围时图层将被删除,你会遇到崩溃
315 | # takeMapLayer移交所有权,因此它将安全地添加到QgsProject中,并赋予QgsProject所有权
316 | if output_layer and output_layer.isValid():
317 | QgsProject.instance().addMapLayer(
318 | context.takeResultLayer(output_layer.id()))
319 |
320 | alg = QgsApplication.processingRegistry().algorithmById(
321 | u'qgis:randompointsinextent')
322 | context = QgsProcessingContext()
323 | feedback = QgsProcessingFeedback()
324 | params = {
325 | 'EXTENT': '0.0,10.0,40,50 [EPSG:4326]',
326 | 'MIN_DISTANCE': 0.0,
327 | 'POINTS_NUMBER': 50000,
328 | 'TARGET_CRS': 'EPSG:4326',
329 | 'OUTPUT': 'memory:My random points'
330 | }
331 | task = QgsProcessingAlgRunnerTask(alg, params, context, feedback)
332 | task.executed.connect(partial(task_finished, context))
333 | QgsApplication.taskManager().addTask(task)
334 | ```
335 |
336 | 也可以查看博客:https://www.opengis.ch/2018/06/22/threads-in-pyqgis3/
--------------------------------------------------------------------------------
/docs/19-网络分析库.md:
--------------------------------------------------------------------------------
1 | 本节代码片段需导入以下模块:
2 |
3 | ```python
4 | from qgis.core import (
5 | QgsVectorLayer,
6 | QgsPointXY,
7 | )
8 | ```
9 |
10 | # 19 网络分析库
11 |
12 | 网络分析库可用于:
13 |
14 | - 从地理数据(矢量线折线图层)创建图
15 | - 实现图论中的基本算法(目前只有Dijkstra的算法)
16 |
17 | 网络分析库是通过从`RoadGraph`核心插件导出基本函数创建的,现在你可以在插件中使用它的方法,也可以直接从Python控制台使用它。
18 |
19 | ## 19.1 一般信息
20 |
21 | 简而言之,一个典型用例可以描述为:
22 |
23 | - 从地理数据创建图(矢量折线图层)
24 | - 运行图算法
25 | - 使用分析结果
26 |
27 | ## 19.2 构建一个图
28 |
29 | 你需要做的第一件事是准备输入数据,也就是将矢量图层转换为图。所有进一步的操作都将使用这个图,而不是图层。
30 |
31 | 作为数据源,我们可以使用任何折线矢量图层。折线的节点成为图的顶点,折线的线段成为图的边。如果几个节点具有相同的坐标,那么它们就是相同的图顶点。因此,具有公共节点的两条线就会相互连接。
32 |
33 | 此外,在图创建过程中,可以将任意数量的附加点“固定”(“绑”)到输入矢量图层。对于每个附加点,将找到一个匹配——最近的顶点或最近的边。在后一种情况下,边将被分割并添加一个新顶点。
34 |
35 | 矢量图层属性和边的长度可以用作边的属性。
36 |
37 | 使用[Builder](https://en.wikipedia.org/wiki/Builder_pattern)编程模式完成从矢量图层到图的转换。图是使用所谓的控制器构造的。目前只有一个控制器:[`QgsVectorLayerDirector`](https://qgis.org/pyqgis/master/analysis/QgsVectorLayerDirector.html#qgis.analysis.QgsVectorLayerDirector)。控制器设置了基本的设置——这些设置将用于从线矢量图层构造图,构建器用来创建图。目前,控制器一样,只有一个构建器存在:[`QgsGraphBuilder`](https://qgis.org/pyqgis/master/analysis/QgsGraphBuilder.html#qgis.analysis.QgsGraphBuilder),它可以创建[`QgsGraph`](https://qgis.org/pyqgis/master/analysis/QgsGraph.html#qgis.analysis.QgsGraph)对象。你可能希望实现自己的构建器,以建立一个与[BGL](https://www.boost.org/doc/libs/1_48_0/libs/graph/doc/index.html)或[NetworkX](https://networkx.org/)等库兼容的图。
38 |
39 | 为了计算边属性,使用编程模式[策略](https://en.wikipedia.org/wiki/Strategy_pattern)。目前只有[`QGSNetworkDistanceTreatgy`](https://qgis.org/pyqgis/master/analysis/QgsNetworkDistanceStrategy.html#qgis.analysis.QgsNetworkDistanceStrategy)策略(考虑到路线的长度)和[`QgsNetworkSpeedStrategy`](https://qgis.org/pyqgis/master/analysis/QgsNetworkSpeedStrategy.html#qgis.analysis.QgsNetworkSpeedStrategy)(也考虑到速度)可用。你可以实现自己的策略,使用所有必要的参数。例如,RoadGraph插件使用的策略是使用边长度和属性中的速度值来计算行程时间。
40 |
41 | 是时候深入研究这个过程了。
42 |
43 | 首先,要使用这个库,我们应该导入分析模块
44 |
45 | ```python
46 | from qgis.analysis import *
47 | ```
48 |
49 | 然后是一些创建控制器的示例:
50 |
51 | ```python
52 | # 不要使用图层属性中有关道路方向的信息,所有道路都是双向的
53 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
54 |
55 | # 使用第5个字段作为道路的方向信息.
56 | # 正向单方向道路使用 "yes",
57 | # 反向单方向道路使用 "1"
58 | # 因此,双向道路使用 “no”。默认情况道路是双向路。
59 | # 此方案可用于OpenStreetMap数据
60 | director = QgsVectorLayerDirector(vectorLayer, 5, 'yes', '1', 'no', QgsVectorLayerDirector.DirectionBoth)
61 | ```
62 |
63 | 为了构造一个控制器,我们应该传递一个矢量图层,该矢量图层作为图结构的源,以及关于每个路段上允许移动的信息(单向或双向移动、直接或反向),像这样:
64 |
65 | ```python
66 | director = QgsVectorLayerDirector(vectorLayer,
67 | directionFieldId,
68 | directDirectionValue,
69 | reverseDirectionValue,
70 | bothDirectionValue,
71 | defaultDirection)
72 | ```
73 |
74 | 以下是这些参数的全部含义:
75 |
76 | - `vectorLayer`——用于构建图的矢量图层
77 | - `directionFieldId`——字段的索引值,用于存储道路方向的信息。如果是`-1`,表示不使用这些信息。整型。
78 | - `directDirectionValue`——正向的字段值(从第一个直线点移动到最后一个直线点)。字符串。
79 | - `reverseDirectionValue`——反向道路的字段值(从最后一个直线点移动到第一个直线点)。字符串。
80 | - `bothDirectionValue`——双向道路的字段值(对于这样的道路,我们可以从第一点移动到最后一点,也可以从最后一点移动到第一点)。字符串。
81 | - `defaultDirectio`——默认道路方向。该值将用于这些道路当字段`directionFieldId`未设置时或具有与上面指定的三个值中的任何一个不同的值。可用的值是:
82 | - [`QgsVectorLayerDirector.DirectionForward`](https://qgis.org/pyqgis/master/analysis/QgsVectorLayerDirector.html#qgis.analysis.QgsVectorLayerDirector.DirectionForward)——正向单向道路
83 | - [`QgsVectorLayerDirector.DirectionBackward`](https://qgis.org/pyqgis/master/analysis/QgsVectorLayerDirector.html#qgis.analysis.QgsVectorLayerDirector.DirectionBackward)——反向单向道路
84 | - [`QgsVectorLayerDirector.DirectionBoth`](https://qgis.org/pyqgis/master/analysis/QgsVectorLayerDirector.html#qgis.analysis.QgsVectorLayerDirector.DirectionBoth)双向道路
85 |
86 | 然后有必要创建用于计算边属性的策略:
87 |
88 | ```python
89 | # 包含速度信息的字段索引值
90 | attributeId = 1
91 | # 默认速度
92 | defaultValue = 50
93 | # 转化速度到米制单位 ('1' 表示不转化)
94 | toMetricFactor = 1
95 | strategy = QgsNetworkSpeedStrategy(attributeId, defaultValue, toMetricFactor)
96 | ```
97 |
98 | 告诉控制器这个策略
99 |
100 | ```python
101 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', 3)
102 | director.addStrategy(strategy)
103 | ```
104 |
105 | 现在我们可以使用构造器来创建图。[`QgsGraphBuilder`](https://qgis.org/pyqgis/master/analysis/QgsGraphBuilder.html#qgis.analysis.QgsGraphBuilder)类构造函数接受几个参数:
106 |
107 | - `crs`——坐标参考系统。必需。
108 | - `otfEnabled`——使用“on the fly” 重投影. 默认为`True` (use OTF).
109 | - `topologyTolerance`——拓扑容差。默认为0。
110 | - `ellipsoidID`——参考椭球。默认为“WGS84”。
111 |
112 | ```python
113 | # 只设置CRS,,其它值默认
114 | builder = QgsGraphBuilder(vectorLayer.crs())
115 | ```
116 |
117 | 我们还可以定义几个点,这些点将用于分析。例如:
118 |
119 | ```python
120 | startPoint = QgsPointXY(1179720.1871, 5419067.3507)
121 | endPoint = QgsPointXY(1180616.0205, 5419745.7839)
122 | ```
123 |
124 | 现在一切就绪,我们可以构建图表并将这些点“连接”到它
125 |
126 | ```python
127 | tiedPoints = director.makeGraph(builder, [startPoint, endPoint])
128 | ```
129 |
130 | 构建图可能需要一些时间(这取决于图层中的要素数量和图层大小)。`tiedPoints`是一个包含“绑定”点坐标的列表。当构建操作完成时,我们可以得到图并将其用于分析
131 |
132 | ```python
133 | graph = builder.graph()
134 | ```
135 |
136 | 通过下面的代码,我们可以得到点的顶点索引
137 |
138 | ```python
139 | startId = graph.findVertex(tiedPoints[0])
140 | endId = graph.findVertex(tiedPoints[1])
141 | ```
142 |
143 | ## 19.3 图分析
144 |
145 | 网络分析用于找到两个问题的答案:哪些顶点是相连的,以及如何找到最短路径。为了解决这些问题,网络分析库提供了Dijkstra算法。
146 |
147 | Dijkstra算法找到从图的一个顶点到所有其他顶点的最短路径以及优化参数的值。结果可以表示为最短路径树。
148 |
149 | 最短路径树是一个有向加权图(或更准确地说是一棵树),具有以下特性:
150 |
151 | - 只有一个顶点没有入射边——树的根
152 | - 所有其他顶点只有一条入射边
153 | - 如果顶点B可以从顶点A到达,那么从A到B的路径是唯一可用的路径,并且它在该图上是最优的(最短的)路径
154 |
155 | 要获得最短路径树,可以使用[`QgsGraphAnalyzer`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer)类的[`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree) 和[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法。建议使用[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法,因为它更快,而且更有效地使用内存。
156 |
157 | [`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree)方法在你想在最短路径树上行走时很有用。它总是创建一个新的图对象(QgsGraph)并接受三个变量:
158 |
159 | - `source`——输入的图
160 | - `startVertexIdx`——树上的点的索引(树的根)
161 | - `criterionNum`——使用的边属性的数量(从0开始)
162 |
163 | ```python
164 | tree = QgsGraphAnalyzer.shortestTree(graph, startId, 0)
165 | ```
166 |
167 | [`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法有相同的参数,但返回两个数组。在第一个数组中,n元素包含传入边的索引,如果没有传入边则为-1。在第二个数组中,n元素包含从树的根到顶点n的距离,如果顶点n从根部无法到达,则为DOUBLE_MAX。
168 |
169 | ```python
170 | (tree, cost) = QgsGraphAnalyzer.dijkstra(graph, startId, 0)
171 | ```
172 |
173 | 下面是一些非常简单的代码,使用[`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree)方法创建的图显示最短路径树(在`Layers`面板中选择`linestring`图层,用你自己的坐标替换)。
174 |
175 | !!! 警告 warning
176 |
177 | 这段代码仅作为一个例子,它创建了大量的[`QgsRubberBand`](https://qgis.org/pyqgis/master/gui/QgsRubberBand.html#qgis.gui.QgsRubberBand)对象,在大数据集上可能会很慢。
178 |
179 | ```python
180 | from qgis.core import *
181 | from qgis.gui import *
182 | from qgis.analysis import *
183 | from qgis.PyQt.QtCore import *
184 | from qgis.PyQt.QtGui import *
185 |
186 | vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines')
187 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
188 | strategy = QgsNetworkDistanceStrategy()
189 | director.addStrategy(strategy)
190 | builder = QgsGraphBuilder(vectorLayer.crs())
191 |
192 | pStart = QgsPointXY(1179661.925139,5419188.074362)
193 | tiedPoint = director.makeGraph(builder, [pStart])
194 | pStart = tiedPoint[0]
195 |
196 | graph = builder.graph()
197 |
198 | idStart = graph.findVertex(pStart)
199 |
200 | tree = QgsGraphAnalyzer.shortestTree(graph, idStart, 0)
201 |
202 | i = 0
203 | while (i < tree.edgeCount()):
204 | rb = QgsRubberBand(iface.mapCanvas())
205 | rb.setColor (Qt.red)
206 | rb.addPoint (tree.vertex(tree.edge(i).fromVertex()).point())
207 | rb.addPoint (tree.vertex(tree.edge(i).toVertex()).point())
208 | i = i + 1
209 | ```
210 |
211 | 使用[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法
212 |
213 | ```python
214 | from qgis.core import *
215 | from qgis.gui import *
216 | from qgis.analysis import *
217 | from qgis.PyQt.QtCore import *
218 | from qgis.PyQt.QtGui import *
219 |
220 | vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines')
221 |
222 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
223 | strategy = QgsNetworkDistanceStrategy()
224 | director.addStrategy(strategy)
225 | builder = QgsGraphBuilder(vectorLayer.crs())
226 |
227 | pStart = QgsPointXY(1179661.925139,5419188.074362)
228 | tiedPoint = director.makeGraph(builder, [pStart])
229 | pStart = tiedPoint[0]
230 |
231 | graph = builder.graph()
232 |
233 | idStart = graph.findVertex(pStart)
234 |
235 | (tree, costs) = QgsGraphAnalyzer.dijkstra(graph, idStart, 0)
236 |
237 | for edgeId in tree:
238 | if edgeId == -1:
239 | continue
240 | rb = QgsRubberBand(iface.mapCanvas())
241 | rb.setColor (Qt.red)
242 | rb.addPoint (graph.vertex(graph.edge(edgeId).fromVertex()).point())
243 | rb.addPoint (graph.vertex(graph.edge(edgeId).toVertex()).point())
244 | ```
245 |
246 | ### 19.3.1 查找最短路径
247 |
248 | 为了找到两点之间的最佳路径,采用了以下方法。两点(起点A和终点B)在图建立时都被 "捆绑 "在一起。然后使用[`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree)或[`dijkstra()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法,我们建立以起点A为根的最短路径树。在同一棵树上,我们也找到了终点B,并开始从B点走到A点,整个算法可以写成这样:
249 |
250 | ```c
251 | assign T = B
252 | while T != B
253 | add point T to path
254 | get incoming edge for point T
255 | look for point TT, that is start point of this edge
256 | assign T = TT
257 | add point A to path
258 | ```
259 |
260 | 在这一点上,我们有一个路径,其形式是顶点的倒置列表(顶点是按照从终点到起点的相反顺序排列的),这些顶点将在这条路径的行程中被访问。
261 |
262 | 以下是使用[`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree)方法的QGIS Python控制台示例代码(你可能需要在图层目录树中中加载并选择一个线图层,并将代码中的坐标替换为你的坐标)。
263 |
264 | ```python
265 | from qgis.core import *
266 | from qgis.gui import *
267 | from qgis.analysis import *
268 |
269 | from qgis.PyQt.QtCore import *
270 | from qgis.PyQt.QtGui import *
271 |
272 | vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines')
273 | builder = QgsGraphBuilder(vectorLayer.sourceCrs())
274 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
275 | strategy = QgsNetworkDistanceStrategy()
276 | director.addStrategy(strategy)
277 |
278 | startPoint = QgsPointXY(1179661.925139,5419188.074362)
279 | endPoint = QgsPointXY(1180942.970617,5420040.097560)
280 |
281 | tiedPoints = director.makeGraph(builder, [startPoint, endPoint])
282 | tStart, tStop = tiedPoints
283 |
284 | graph = builder.graph()
285 | idxStart = graph.findVertex(tStart)
286 |
287 | tree = QgsGraphAnalyzer.shortestTree(graph, idxStart, 0)
288 |
289 | idxStart = tree.findVertex(tStart)
290 | idxEnd = tree.findVertex(tStop)
291 |
292 | if idxEnd == -1:
293 | raise Exception('No route!')
294 |
295 | # 添加最后一个点
296 | route = [tree.vertex(idxEnd).point()]
297 |
298 | # 遍历图
299 | while idxEnd != idxStart:
300 | edgeIds = tree.vertex(idxEnd).incomingEdges()
301 | if len(edgeIds) == 0:
302 | break
303 | edge = tree.edge(edgeIds[0])
304 | route.insert(0, tree.vertex(edge.fromVertex()).point())
305 | idxEnd = edge.fromVertex()
306 |
307 | # 显示
308 | rb = QgsRubberBand(iface.mapCanvas())
309 | rb.setColor(Qt.green)
310 |
311 | # 如果项目的坐标系和图层的坐标系不一样,则需要坐标转换
312 | for p in route:
313 | rb.addPoint(p)
314 | ```
315 |
316 | 使用[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法
317 |
318 | ```python
319 | from qgis.core import *
320 | from qgis.gui import *
321 | from qgis.analysis import *
322 |
323 | from qgis.PyQt.QtCore import *
324 | from qgis.PyQt.QtGui import *
325 |
326 | vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines')
327 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
328 | strategy = QgsNetworkDistanceStrategy()
329 | director.addStrategy(strategy)
330 |
331 | builder = QgsGraphBuilder(vectorLayer.sourceCrs())
332 |
333 | startPoint = QgsPointXY(1179661.925139,5419188.074362)
334 | endPoint = QgsPointXY(1180942.970617,5420040.097560)
335 |
336 | tiedPoints = director.makeGraph(builder, [startPoint, endPoint])
337 | tStart, tStop = tiedPoints
338 |
339 | graph = builder.graph()
340 | idxStart = graph.findVertex(tStart)
341 | idxEnd = graph.findVertex(tStop)
342 |
343 | (tree, costs) = QgsGraphAnalyzer.dijkstra(graph, idxStart, 0)
344 |
345 | if tree[idxEnd] == -1:
346 | raise Exception('No route!')
347 |
348 | # Total cost
349 | cost = costs[idxEnd]
350 |
351 | # 添加最后一个点
352 | route = [graph.vertex(idxEnd).point()]
353 |
354 | # 遍历图
355 | while idxEnd != idxStart:
356 | idxEnd = graph.edge(tree[idxEnd]).fromVertex()
357 | route.insert(0, graph.vertex(idxEnd).point())
358 |
359 | # 显示
360 | rb = QgsRubberBand(iface.mapCanvas())
361 | rb.setColor(Qt.red)
362 |
363 | # 如果项目的坐标系和图层的坐标系不一样,则需要坐标转换
364 | for p in route:
365 | rb.addPoint(p)
366 | ```
367 |
368 | ### 19.3.2 可达区域
369 |
370 | 顶点A的可达区域是指可以从顶点A进入的图形顶点的子集,并且从A到这些顶点的路径成本不超过某个值。
371 |
372 | 这一点可以通过以下例子更清楚地表明。"有一个消防站,消防车可以在5分钟内到达城市的哪些地方?10分钟?15分钟?"。这些问题的答案就是消防站的可达区域。
373 |
374 | 为了找到可达的区域,我们可以使用[`QgsGraphAnalyzer`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer)类的[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法。只需将cost数组的元素与预定义的值进行比较。如果cost[i]小于或等于一个预定义的值,那么顶点i就在可达区域内,否则它就在外面。
375 |
376 | 一个更困难的问题是要得到可达区域的边界。底部边界是仍可访问的顶点集合,而顶部边界是不可访问的顶点集合。事实上这很简单:它是基于最短路径树的边的可达性边界,对于这些边的源顶点是可访问的,而边的目标顶点则不是。
377 |
378 | 下面是一个例子:
379 |
380 | ```python
381 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
382 | strategy = QgsNetworkDistanceStrategy()
383 | director.addStrategy(strategy)
384 | builder = QgsGraphBuilder(vectorLayer.crs())
385 |
386 |
387 | pStart = QgsPointXY(1179661.925139, 5419188.074362)
388 | delta = iface.mapCanvas().getCoordinateTransform().mapUnitsPerPixel() * 1
389 |
390 | rb = QgsRubberBand(iface.mapCanvas())
391 | rb.setColor(Qt.green)
392 | rb.addPoint(QgsPointXY(pStart.x() - delta, pStart.y() - delta))
393 | rb.addPoint(QgsPointXY(pStart.x() + delta, pStart.y() - delta))
394 | rb.addPoint(QgsPointXY(pStart.x() + delta, pStart.y() + delta))
395 | rb.addPoint(QgsPointXY(pStart.x() - delta, pStart.y() + delta))
396 |
397 | tiedPoints = director.makeGraph(builder, [pStart])
398 | graph = builder.graph()
399 | tStart = tiedPoints[0]
400 |
401 | idStart = graph.findVertex(tStart)
402 |
403 | (tree, cost) = QgsGraphAnalyzer.dijkstra(graph, idStart, 0)
404 |
405 | upperBound = []
406 | r = 1500.0
407 | i = 0
408 | tree.reverse()
409 |
410 | while i < len(cost):
411 | if cost[i] > r and tree[i] != -1:
412 | outVertexId = graph.edge(tree [i]).toVertex()
413 | if cost[outVertexId] < r:
414 | upperBound.append(i)
415 | i = i + 1
416 |
417 | for i in upperBound:
418 | centerPoint = graph.vertex(i).point()
419 | rb = QgsRubberBand(iface.mapCanvas())
420 | rb.setColor(Qt.red)
421 | rb.addPoint(QgsPointXY(centerPoint.x() - delta, centerPoint.y() - delta))
422 | rb.addPoint(QgsPointXY(centerPoint.x() + delta, centerPoint.y() - delta))
423 | rb.addPoint(QgsPointXY(centerPoint.x() + delta, centerPoint.y() + delta))
424 | rb.addPoint(QgsPointXY(centerPoint.x() - delta, centerPoint.y() + delta))
425 | ```
426 |
427 |
--------------------------------------------------------------------------------
/docs/21-PyQGIS速查表.md:
--------------------------------------------------------------------------------
1 | # 21 PyQGIS速查表
2 |
3 | ## 21.1 用户接口
4 |
5 | **改变外观**
6 |
7 | ```python
8 | from qgis.PyQt.QtWidgets import QApplication
9 |
10 | app = QApplication.instance()
11 | app.setStyleSheet(".QWidget {color: blue; background-color: yellow;}")
12 | # 你可以从文件读取样式
13 | with open("testdata/file.qss") as qss_file_content:
14 | app.setStyleSheet(qss_file_content.read())
15 | ```
16 | **改变图标和标题**
17 |
18 | ```python
19 | from qgis.PyQt.QtGui import QIcon
20 |
21 | icon = QIcon(r"/path/to/logo/file.png")
22 | iface.mainWindow().setWindowIcon(icon)
23 | iface.mainWindow().setWindowTitle("My QGIS")
24 | ```
25 | ## 21.2 设置
26 |
27 | **获得设置列表**
28 |
29 | ```python
30 | from qgis.PyQt.QtCore import QSettings
31 |
32 | qs = QSettings()
33 |
34 | for k in sorted(qs.allKeys()):
35 | print (k)
36 | ```
37 | ## 21.3 工具栏
38 | **移除工具栏**
39 |
40 | ```python
41 | from qgis.utils import iface
42 |
43 | toolbar = iface.helpToolBar()
44 | parent = toolbar.parentWidget()
45 | parent.removeToolBar(toolbar)
46 |
47 | # 添加
48 | parent.addToolBar(toolbar)
49 | ```
50 | **移除操作**
51 |
52 | ```python
53 | actions = iface.attributesToolBar().actions()
54 | iface.attributesToolBar().clear()
55 | iface.attributesToolBar().addAction(actions[4])
56 | iface.attributesToolBar().addAction(actions[3])
57 | ```
58 | ## 21.4 菜单
59 |
60 | **移除菜单**
61 |
62 | ```python
63 | from qgis.utils import iface
64 |
65 | # 帮助菜单
66 | menu = iface.helpMenu()
67 | menubar = menu.parentWidget()
68 | menubar.removeAction(menu.menuAction())
69 |
70 | # 添加
71 | menubar.addAction(menu.menuAction())
72 | ```
73 | ## 21.5 画布
74 | **访问画布**
75 |
76 | ```python
77 | from qgis.utils import iface
78 |
79 | canvas = iface.mapCanvas()
80 | ```
81 | **改变画布颜色**
82 |
83 | ```python
84 | from qgis.PyQt.QtCore import Qt
85 |
86 | iface.mapCanvas().setCanvasColor(Qt.black)
87 | iface.mapCanvas().refresh()
88 | ```
89 | **画布刷新间隔**
90 |
91 | ```python
92 | from qgis.PyQt.QtCore import QSettings
93 | # 150毫秒
94 | QSettings().setValue("/qgis/map_update_interval", 150)
95 | ```
96 | ## 21.6 图层
97 | **添加图层**
98 |
99 | ```python
100 | from qgis.utils import iface
101 |
102 | layer = iface.addVectorLayer("/path/to/shapefile/file.shp", "layer name you like", "ogr")
103 | if not layer:
104 | print("Layer failed to load!")
105 | ```
106 | **获取当前图层**
107 |
108 | ```python
109 | layer = iface.activeLayer()
110 | ```
111 | **图层列表**
112 |
113 | ```python
114 | from qgis.core import QgsProject
115 |
116 | QgsProject.instance().mapLayers().values()
117 | ```
118 |
119 | **获得图层名称**
120 |
121 | ```python
122 | from qgis.core import QgsVectorLayer
123 | layer = QgsVectorLayer("Point?crs=EPSG:4326", "layer name you like", "memory")
124 | QgsProject.instance().addMapLayer(layer)
125 |
126 | layers_names = []
127 | for layer in QgsProject.instance().mapLayers().values():
128 | layers_names.append(layer.name())
129 |
130 | print("layers TOC = {}".format(layers_names))
131 |
132 | layers_names = [layer.name() for layer in QgsProject.instance().mapLayers().values()]
133 | print("layers TOC = {}".format(layers_names))
134 | ```
135 | **通过名称查找图层**
136 |
137 | ```python
138 | from qgis.core import QgsProject
139 |
140 | layer = QgsProject.instance().mapLayersByName("layer name you like")[0]
141 | print(layer.name())
142 | ```
143 | **设置当前图层**
144 |
145 | ```python
146 | from qgis.core import QgsProject
147 |
148 | layer = QgsProject.instance().mapLayersByName("layer name you like")[0]
149 | iface.setActiveLayer(layer)
150 | ```
151 | **刷新图层**
152 |
153 | ```python
154 | from qgis.core import QgsProject
155 |
156 | layer = QgsProject.instance().mapLayersByName("layer name you like")[0]
157 | # 5秒
158 | layer.setAutoRefreshInterval(5000)
159 | # 自动刷新
160 | layer.setAutoRefreshMode(Qgis.AutoRefreshMode.ReloadData)
161 | ```
162 | **添加表单要素**
163 |
164 | ```python
165 | from qgis.core import QgsFeature, QgsGeometry
166 |
167 | feat = QgsFeature()
168 | geom = QgsGeometry()
169 | feat.setGeometry(geom)
170 | feat.setFields(layer.fields())
171 |
172 | iface.openFeatureForm(layer, feat, False)
173 | ```
174 | **添加无表单要素**
175 |
176 | ```python
177 | from qgis.core import QgsPointXY
178 |
179 | pr = layer.dataProvider()
180 | feat = QgsFeature()
181 | feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10,10)))
182 | pr.addFeatures([feat])
183 | ```
184 | **获得要素**
185 |
186 | ```python
187 | for f in layer.getFeatures():
188 | print (f)
189 | ```
190 | **获得已选要素**
191 |
192 | ```python
193 | for f in layer.selectedFeatures():
194 | print (f)
195 | ```
196 | **获得要素id**
197 |
198 | ```python
199 | selected_ids = layer.selectedFeatureIds()
200 | print(selected_ids)
201 | ```
202 | **从已选要素id创建内存图层**
203 |
204 | ```python
205 | from qgis.core import QgsFeatureRequest
206 |
207 | memory_layer = layer.materialize(QgsFeatureRequest().setFilterFids(layer.selectedFeatureIds()))
208 | QgsProject.instance().addMapLayer(memory_layer)
209 | ```
210 | **获得几何**
211 |
212 | ```python
213 | # 点图层
214 | for f in layer.getFeatures():
215 | geom = f.geometry()
216 | print ('%f, %f' % (geom.asPoint().y(), geom.asPoint().x()))
217 | ```
218 | **移动几何**
219 |
220 | ```python
221 | from qgis.core import QgsFeature, QgsGeometry
222 | poly = QgsFeature()
223 | geom = QgsGeometry.fromWkt("POINT(7 45)")
224 | geom.translate(1, 1)
225 | poly.setGeometry(geom)
226 | print(poly.geometry())
227 | ```
228 | **设置坐标参考系统**
229 |
230 | ```python
231 | from qgis.core import QgsProject, QgsCoordinateReferenceSystem
232 |
233 | for layer in QgsProject.instance().mapLayers().values():
234 | layer.setCrs(QgsCoordinateReferenceSystem(4326, QgsCoordinateReferenceSystem.EpsgCrsId))
235 | ```
236 | **查看坐标参考系统**
237 |
238 | ```python
239 | from qgis.core import QgsProject
240 |
241 | for layer in QgsProject.instance().mapLayers().values():
242 | crs = layer.crs().authid()
243 | layer.setName('{} ({})'.format(layer.name(), crs))
244 | ```
245 | **隐藏字段**
246 |
247 | ```python
248 | from qgis.core import QgsEditorWidgetSetup
249 |
250 | def fieldVisibility (layer,fname):
251 | setup = QgsEditorWidgetSetup('Hidden', {})
252 | for i, column in enumerate(layer.fields()):
253 | if column.name()==fname:
254 | layer.setEditorWidgetSetup(idx, setup)
255 | break
256 | else:
257 | continue
258 | ```
259 | **图层添加WKT要素**
260 |
261 | ```python
262 | from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry, QgsProject
263 |
264 | layer = QgsVectorLayer('Polygon?crs=epsg:4326', 'Mississippi', 'memory')
265 | pr = layer.dataProvider()
266 | poly = QgsFeature()
267 | geom = QgsGeometry.fromWkt("POLYGON ((-88.82 34.99,-88.09 34.89,-88.39 30.34,-89.57 30.18,-89.73 31,-91.63 30.99,-90.87 32.37,-91.23 33.44,-90.93 34.23,-90.30 34.99,-88.82 34.99))")
268 | poly.setGeometry(geom)
269 | pr.addFeatures([poly])
270 | layer.updateExtents()
271 | QgsProject.instance().addMapLayers([layer])
272 | ```
273 | **从GeoPackage加载所有图层**
274 |
275 | ```python
276 | from qgis.core import QgsVectorLayer, QgsProject
277 |
278 | fileName = "/path/to/gpkg/file.gpkg"
279 | layer = QgsVectorLayer(fileName,"test","ogr")
280 | subLayers =layer.dataProvider().subLayers()
281 |
282 | for subLayer in subLayers:
283 | name = subLayer.split('!!::!!')[1]
284 | uri = "%s|layername=%s" % (fileName, name,)
285 | # 创建图层
286 | sub_vlayer = QgsVectorLayer(uri, name, 'ogr')
287 | # Add layer to map
288 | QgsProject.instance().addMapLayer(sub_vlayer)
289 | ```
290 | **加载瓦片图层(xyz)**
291 |
292 | ```python
293 | from qgis.core import QgsRasterLayer, QgsProject
294 |
295 | def loadXYZ(url, name):
296 | rasterLyr = QgsRasterLayer("type=xyz&url=" + url, name, "wms")
297 | QgsProject.instance().addMapLayer(rasterLyr)
298 |
299 | urlWithParams = 'type=xyz&url=https://a.tile.openstreetmap.org/%7Bz%7D/%7Bx%7D/%7By%7D.png&zmax=19&zmin=0&crs=EPSG3857'
300 | loadXYZ(urlWithParams, 'OpenStreetMap')
301 | ```
302 | **移除所有图层**
303 |
304 | ```python
305 | QgsProject.instance().removeAllMapLayers()
306 | ```
307 | **移除全部**
308 |
309 | ```python
310 | QgsProject.instance().clear()
311 | ```
312 | ## 21.7 目录
313 | **访问选中的图层**
314 |
315 | ```python
316 | from qgis.utils import iface
317 |
318 | iface.mapCanvas().layers()
319 | ```
320 | **移除上下文菜单**
321 |
322 | ```python
323 | ltv = iface.layerTreeView()
324 | mp = ltv.menuProvider()
325 | ltv.setMenuProvider(None)
326 | # 恢复
327 | ltv.setMenuProvider(mp)
328 | ```
329 | ## 21.8 高级目录
330 | **根节点**
331 |
332 | ```python
333 | from qgis.core import QgsVectorLayer, QgsProject, QgsLayerTreeLayer
334 |
335 | root = QgsProject.instance().layerTreeRoot()
336 | node_group = root.addGroup("My Group")
337 |
338 | layer = QgsVectorLayer("Point?crs=EPSG:4326", "layer name you like", "memory")
339 | QgsProject.instance().addMapLayer(layer, False)
340 |
341 | node_group.addLayer(layer)
342 |
343 | print(root)
344 | print(root.children())
345 | ```
346 | **访问第一个子节点**
347 |
348 | ```python
349 | from qgis.core import QgsLayerTreeGroup, QgsLayerTreeLayer, QgsLayerTree
350 |
351 | child0 = root.children()[0]
352 | print (child0.name())
353 | print (type(child0))
354 | print (isinstance(child0, QgsLayerTreeLayer))
355 | print (isinstance(child0.parent(), QgsLayerTree))
356 | ```
357 | **查找图层组和所有节点**
358 |
359 | ```python
360 | from qgis.core import QgsLayerTreeGroup, QgsLayerTreeLayer
361 |
362 | def get_group_layers(group):
363 | print('- group: ' + group.name())
364 | for child in group.children():
365 | if isinstance(child, QgsLayerTreeGroup):
366 | # 遍历嵌套图层组
367 | get_group_layers(child)
368 | else:
369 | print(' - layer: ' + child.name())
370 |
371 |
372 | root = QgsProject.instance().layerTreeRoot()
373 | for child in root.children():
374 | if isinstance(child, QgsLayerTreeGroup):
375 | get_group_layers(child)
376 | elif isinstance(child, QgsLayerTreeLayer):
377 | print ('- layer: ' + child.name())
378 | ```
379 | **通过名称查找图层组**
380 |
381 | ```python
382 | print (root.findGroup("My Group"))
383 | ```
384 | **通过id查找图层组**
385 |
386 | ```python
387 | print (root.findLayer(layer.layerId()))
388 | ```
389 | **添加图层**
390 |
391 | ```python
392 | from qgis.core import QgsVectorLayer, QgsProject
393 |
394 | layer1 = QgsVectorLayer("Point?crs=EPSG:4326", "layer name you like", "memory")
395 | QgsProject.instance().addMapLayer(layer1, False)
396 | node_layer1 = root.addLayer(layer1)
397 | ```
398 | **添加图层组**
399 |
400 | ```python
401 | from qgis.core import QgsLayerTreeGroup
402 |
403 | node_group2 = QgsLayerTreeGroup("Group 2")
404 | root.addChildNode(node_group2)
405 | ```
406 | **移除加载的图层**
407 |
408 | ```python
409 | layer = QgsProject.instance().mapLayersByName("layer name you like")[0]
410 | root = QgsProject.instance().layerTreeRoot()
411 |
412 | myLayer = root.findLayer(layer.id())
413 | myClone = myLayer.clone()
414 | parent = myLayer.parent()
415 |
416 | myGroup = root.findGroup("My Group")
417 | # 插入到第一个位置
418 | myGroup.insertChildNode(0, myClone)
419 |
420 | parent.removeChildNode(myLayer)
421 | ```
422 | **移除指定图层组**
423 |
424 | ```python
425 | QgsProject.instance().addMapLayer(layer, False)
426 |
427 | root = QgsProject.instance().layerTreeRoot()
428 | myGroup = root.findGroup("My Group")
429 | myOriginalLayer = root.findLayer(layer.id())
430 | myLayer = myOriginalLayer.clone()
431 | myGroup.insertChildNode(0, myLayer)
432 | parent.removeChildNode(myOriginalLayer)
433 | ```
434 | **改变可见性**
435 |
436 | ```python
437 | root = QgsProject.instance().layerTreeRoot()
438 | node = root.findLayer(layer.id())
439 | new_state = Qt.Checked if node.isVisible() == Qt.Unchecked else Qt.Unchecked
440 | node.setItemVisibilityChecked(new_state)
441 | ```
442 |
443 | **图层组是否被选择**
444 |
445 | ```python
446 | def isMyGroupSelected( groupName ):
447 | myGroup = QgsProject.instance().layerTreeRoot().findGroup( groupName )
448 | return myGroup in iface.layerTreeView().selectedNodes()
449 |
450 | print(isMyGroupSelected( 'my group name' ))
451 | ```
452 |
453 | **移动节点**
454 |
455 | ```python
456 | cloned_group1 = node_group.clone()
457 | root.insertChildNode(0, cloned_group1)
458 | root.removeChildNode(node_group)
459 | ```
460 | **展开节点**
461 |
462 | ```python
463 | print(myGroup.isExpanded())
464 | myGroup.setExpanded(False)
465 | ```
466 |
467 | **隐藏节点**
468 |
469 | ```python
470 | from qgis.core import QgsProject
471 |
472 | model = iface.layerTreeView().layerTreeModel()
473 | ltv = iface.layerTreeView()
474 | root = QgsProject.instance().layerTreeRoot()
475 |
476 | layer = QgsProject.instance().mapLayersByName('layer name you like')[0]
477 | node = root.findLayer(layer.id())
478 |
479 | index = model.node2index( node )
480 | ltv.setRowHidden( index.row(), index.parent(), True )
481 | node.setCustomProperty( 'nodeHidden', 'true')
482 | ltv.setCurrentIndex(model.node2index(root))
483 | ```
484 |
485 | **节点信号**
486 |
487 | ```python
488 | def onWillAddChildren(node, indexFrom, indexTo):
489 | print ("WILL ADD", node, indexFrom, indexTo)
490 |
491 | def onAddedChildren(node, indexFrom, indexTo):
492 | print ("ADDED", node, indexFrom, indexTo)
493 |
494 | root.willAddChildren.connect(onWillAddChildren)
495 | root.addedChildren.connect(onAddedChildren)
496 | ```
497 |
498 | **移除图层**
499 |
500 | ```python
501 | root.removeLayer(layer)
502 | ```
503 |
504 | **移除图层组**
505 |
506 | ```python
507 | root.removeChildNode(node_group2)
508 | ```
509 |
510 | **创建新的目录树**
511 |
512 | ```python
513 | root = QgsProject.instance().layerTreeRoot()
514 | model = QgsLayerTreeModel(root)
515 | view = QgsLayerTreeView()
516 | view.setModel(model)
517 | view.show()
518 | ```
519 |
520 | **移动节点**
521 |
522 | ```python
523 | cloned_group1 = node_group.clone()
524 | root.insertChildNode(0, cloned_group1)
525 | root.removeChildNode(node_group)
526 | ```
527 |
528 | **重命名节点**
529 |
530 | ```python
531 | cloned_group1.setName("Group X")
532 | node_layer1.setName("Layer X")
533 | ```
534 | ## 21.9 处理算法
535 | **获得算法列表**
536 |
537 | ```python
538 | from qgis.core import QgsApplication
539 |
540 | for alg in QgsApplication.processingRegistry().algorithms():
541 | if 'buffer' == alg.name():
542 | print("{}:{} --> {}".format(alg.provider().name(), alg.name(), alg.displayName()))
543 | ```
544 | **获得算法帮助**
545 |
546 | ```python
547 | from qgis import processing
548 |
549 | processing.algorithmHelp("qgis:randomselection")
550 | ```
551 | **运行算法**
552 |
553 | 本示例,结果存储在添加到项目的临时内存层中。
554 |
555 | ```python
556 | from qgis import processing
557 | result = processing.run("native:buffer", {'INPUT': layer, 'OUTPUT': 'memory:'})
558 | QgsProject.instance().addMapLayer(result['OUTPUT'])
559 | ```
560 | **算法统计**
561 |
562 | ```python
563 | from qgis.core import QgsApplication
564 |
565 | len(QgsApplication.processingRegistry().algorithms())
566 | ```
567 | **数据提供者统计**
568 |
569 | ```python
570 | from qgis.core import QgsApplication
571 |
572 | len(QgsApplication.processingRegistry().providers())
573 | ```
574 | **表达式统计**
575 |
576 | ```python
577 | from qgis.core import QgsExpression
578 |
579 | len(QgsExpression.Functions())
580 | ```
581 | ## 21.10 装饰器
582 | **版权**
583 |
584 | ```python
585 | from qgis.PyQt.Qt import QTextDocument
586 | from qgis.PyQt.QtGui import QFont
587 |
588 | mQFont = "Sans Serif"
589 | mQFontsize = 9
590 | mLabelQString = "© QGIS 2019"
591 | mMarginHorizontal = 0
592 | mMarginVertical = 0
593 | mLabelQColor = "#FF0000"
594 |
595 | INCHES_TO_MM = 0.0393700787402 # 1 毫米 = 0.0393700787402 英寸
596 | case = 2
597 |
598 | def add_copyright(p, text, xOffset, yOffset):
599 | p.translate( xOffset , yOffset )
600 | text.drawContents(p)
601 | p.setWorldTransform( p.worldTransform() )
602 |
603 | def _on_render_complete(p):
604 | deviceHeight = p.device().height() # 获取绘制设备高度
605 | deviceWidth = p.device().width() # 获取绘制设备宽度
606 | # 创建一个新的富文本容器
607 | text = QTextDocument()
608 | font = QFont()
609 | font.setFamily(mQFont)
610 | font.setPointSize(int(mQFontsize))
611 | text.setDefaultFont(font)
612 | style = ""
613 | text.setHtml( style + "" + mLabelQString + "
" )
614 | # 文本大小
615 | size = text.size()
616 |
617 | # 渲染
618 | pixelsInchX = p.device().logicalDpiX()
619 | pixelsInchY = p.device().logicalDpiY()
620 | xOffset = pixelsInchX * INCHES_TO_MM * int(mMarginHorizontal)
621 | yOffset = pixelsInchY * INCHES_TO_MM * int(mMarginVertical)
622 |
623 | # 计算点位
624 | if case == 0:
625 | # 左上
626 | add_copyright(p, text, xOffset, yOffset)
627 |
628 | elif case == 1:
629 | # 左下
630 | yOffset = deviceHeight - yOffset - size.height()
631 | add_copyright(p, text, xOffset, yOffset)
632 |
633 | elif case == 2:
634 | # 右上
635 | xOffset = deviceWidth - xOffset - size.width()
636 | add_copyright(p, text, xOffset, yOffset)
637 |
638 | elif case == 3:
639 | # 右下
640 | yOffset = deviceHeight - yOffset - size.height()
641 | xOffset = deviceWidth - xOffset - size.width()
642 | add_copyright(p, text, xOffset, yOffset)
643 |
644 | elif case == 4:
645 | # 上中心点
646 | xOffset = deviceWidth / 2
647 | add_copyright(p, text, xOffset, yOffset)
648 |
649 | else:
650 | # 下中心点
651 | yOffset = deviceHeight - yOffset - size.height()
652 | xOffset = deviceWidth / 2
653 | add_copyright(p, text, xOffset, yOffset)
654 |
655 | # 当画布渲染完成后发送信号
656 | iface.mapCanvas().renderComplete.connect(_on_render_complete)
657 | # 重绘画布
658 | iface.mapCanvas().refresh()
659 | ```
660 |
661 | ## 21.11 创作者
662 |
663 | **按名称获取打印布局**
664 |
665 | ```python
666 | composerTitle = 'MyComposer' # 创作者名称
667 |
668 | project = QgsProject.instance()
669 | projectLayoutManager = project.layoutManager()
670 | layout = projectLayoutManager.layoutByName(composerTitle)
671 | ```
672 |
673 | ## 21.12 来源
674 |
675 | - [QGIS Python (PyQGIS) API](https://qgis.org/pyqgis/master/)
676 | - [QGIS C++ API](https://qgis.org/api/)
677 | - [StackOverFlow QGIS questions](https://stackoverflow.com/questions/tagged/qgis)
678 | - [Script by Klas Karlsson](https://raw.githubusercontent.com/klakar/QGIS_resources/master/collections/Geosupportsystem/python/qgis_basemaps.py)
679 |
680 |
--------------------------------------------------------------------------------
/docs/16-开发Python插件.md:
--------------------------------------------------------------------------------
1 | # 16 开发Python插件
2 |
3 | 可以用Python编程语言创建插件。与用C ++编写插件相比,由于Python语言的动态特性,这些插件应该更容易编写,理解,维护和分发。
4 |
5 | Python插件与QGIS插件管理器中的C ++插件一起列出。他们在`~/(UserProfile)/python/plugins`和以下路径中搜索:
6 |
7 | - UNIX / Mac上: `(qgis_prefix)/share/qgis/python/plugins`
8 | - Windows: `(qgis_prefix)/python/plugins`
9 |
10 | 有关`~`和`(UserProfile)`的定义,请查看[Core和External插件](https://docs.qgis.org/latest/en/docs/user_manual/plugins/plugins.html#core-and-external-plugins)。
11 |
12 | !!! 提示
13 |
14 | 将QGIS_PLUGINPATH设置为一个存在的目录路径,可以将此路径添加到插件的搜索路径列表中。
15 |
16 |
17 | ## 16.1 构建Python插件
18 |
19 | 创建插件,需要以下步骤:
20 |
21 | 1. *想法*:你想要使用新的QGIS插件做什么。你为什么要这么做?你想解决什么问题?这个问题已经有另一个插件吗?
22 | 2. *创建文件*:一些必要文件(查看[插件文件](#16111))
23 | 3. *编写代码*:在恰当的文件中写代码
24 | 3. 文档:编写插件文档
25 | 3. 可选:翻译:将插件翻译成不同的语言
26 | 4. *测试*:如果准备就绪,[重新加载插件](#1615)
27 | 5. *发布*:在QGIS仓库中发布你的插件或将你自己的仓库作为个人“GIS武器”的“武器库”。
28 |
29 | ### 16.1.1 准备开始
30 |
31 | 在开始编写新的插件之前,先看看[Python官方插件库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/releasing.html#official-pyqgis-repository)。现有插件的源代码可以帮助你了解更多编程知识。你也可能会发现类似的插件已经存在,你可以扩展它,或者至少在它的基础上开发自己的插件。
32 |
33 | #### 16.1.1.1 插件文件结构
34 |
35 | 开始使用一个新的插件,我们需要设置必要的插件文件。
36 |
37 | 有两种插件模板资源可以帮助你入门:
38 |
39 | - 为了教育目的或者在需要采用极简主义方法时,[最小化插件模板](https://github.com/wonder-sk/qgis-minimal-plugin)提供了创建有效的QGIS Python插件所需的基本文件(框架)。
40 | - 更加完整的插件模板,[插件构建器](https://plugins.qgis.org/plugins/pluginbuilder3/)可以创建多种不同类型的插件模板,包括本地化(翻译)和测试等功能。
41 |
42 | 典型的插件目录包括以下文件:
43 |
44 | - `metadata.txt` - *必须* - 包含插件网站和插件基础结构使用的常规信息,版本、名称和一些其他元数据。
45 | - `__init__.py` - *必须* - 插件的入口。它必须具有 `classFactory()`方法,还可以具有任何其他初始化代码。
46 | - `mainPlugin.py` - *核心代码* - 插件的主要工作代码。包含有关插件操作和主要代码的所有信息。
47 | - `form.ui` - *插件自定义UI* - Qt设计师创建的GUI。
48 | - `form.py` - *编译的GUI* - 将上面描述的form.ui转换为Python代码。
49 | - `resources.qrc` - *可选* - Qt设计师创建的xml文档。包含表单资源的相对路径。
50 | - `resources.py` - *编译的资源文件,可选* - 将上述.qrc文件转换为Python代码。
51 |
52 | !!! warning 警告
53 |
54 | 如果您打算将插件上传到[Python官方插件库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/releasing.html#official-pyqgis-repository),则必须检查插件是否遵循插件[验证](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/releasing.html#official-pyqgis-repository-validation)所必需的一些附加规则
55 |
56 | ### 16.1.2 编写插件代码
57 |
58 | 以下部分显示了应该在上面介绍的每个文件中添加哪些内容。
59 |
60 | #### 16.1.2.1 metadata.txt
61 |
62 | 首先,插件管理器需要检索有关插件的一些基本信息,例如其名称、描述等。文件`metadata.txt`存储此信息。
63 |
64 | !!! 提示
65 |
66 | 所有元数据必须采用UTF-8编码。
67 |
68 | | 元数据名称 | 是否必需 | 描述 |
69 | | :-------------------- | :------: | :----------------------------------------------------------- |
70 | | name | 是 | 插件名称,短字符串 |
71 | | qgisMinimumVersion | 是 | 最小QGIS版本 |
72 | | qgisMaximumVersion | 否 | 最大QGIS版本 |
73 | | description | 是 | 描述插件的简短文本,不支持HTML |
74 | | about | 是 | 较长的文本,详细描述插件,不支持HTML |
75 | | version | 是 | 版本 |
76 | | author | 是 | 作者姓名 |
77 | | email | 是 | 作者的电子邮件,在网站上仅显示给登录的用户,但在插件安装后可在插件管理器中看到 |
78 | | changelog | 否 | 字符串,可以是多行,不支持HTML |
79 | | experimental | 否 | 实验性,布尔值,True或False |
80 | | deprecated | 否 | 弃用,boolean值,True或False,适用于整个插件,而不仅仅适用于上传的版本 |
81 | | tags | 否 | 以逗号分隔的列表,允许在单个标记内使用空格 |
82 | | homepage | 否 | 指向插件主页的有效网址 |
83 | | repository | 是 | 源代码存储库的有效URL |
84 | | tracker | 否 | 故障和错误报告的有效URL |
85 | | icon | 否 | 对于web友好的图像(PNG,JPEG)文件名或相对路径(相对于插件压缩包的文件夹) |
86 | | category | 否 | `Raster`, `Vector`, `Database`, `Mesh` and `Web`(栅格、矢量、数据库和网络) |
87 | | plugin_dependencies | 否 | 类似于PIP的逗号分隔的其他插件列表,使用来自元数据名称字段的插件名称 |
88 | | server | 否 | 布尔值,True或False,确定插件是否具有服务器接口 |
89 | | hasProcessingProvider | 否 | 布尔值,True或False,确定插件是否提供处理算法 |
90 |
91 | 默认情况下,插件放在 **Plugins** 菜单中(我们将在下一节中看到如何为插件添加菜单项),但也可以将它们放入 **Raster** , **Vector** , **Database** ,**Mesh** 和 **Web** 菜单中。
92 |
93 | 输入指定的“category”元数据,可以相应地对插件进行分类。此元数据用于提示用户,并告诉他们可以在哪里(在哪个菜单中)找到该插件。“category”的允许值为:Vector, Raster, Database或者Web。例如,如果你的插件可以从Raster菜单中找到,请将其添加到`metadata.txt`中
94 |
95 | ```ini
96 | category=Raster
97 | ```
98 |
99 | !!! 提示
100 |
101 | 如果qgisMaximumVersion为空,则在上传到[官方Python插件库](#1643-python)时,它将自动设置为主要版本加上.99(例如:3.99)。
102 |
103 |
104 | metadata.txt示例:
105 |
106 | ```ini
107 | ; 以下是强制性的
108 |
109 | [general]
110 | name=HelloWorld
111 | email=me@example.com
112 | author=Just Me
113 | qgisMinimumVersion=3.0
114 | description=This is an example plugin for greeting the world.\
115 | Multiline is allowed:\
116 | lines starting with spaces belong to the same\
117 | field, in this case to the "description" field.\
118 | HTML formatting is not allowed.
119 | about=This paragraph can contain a detailed description\
120 | of the plugin. Multiline is allowed, HTML is not.
121 | version=version 1.2
122 | tracker=http://bugs.itopen.it
123 | repository=http://www.itopen.it/repo
124 | ; 结束强制
125 |
126 | ; 以下是可选的
127 | category=Raster
128 | changelog=The changelog lists the plugin versions\
129 | and their changes as in the example below:\
130 | 1.0 - First stable release\
131 | 0.9 - All features implemented\
132 | 0.8 - First testing release
133 |
134 | ; 标签采用逗号分隔,标签名称允许使用空格
135 | ; 标签应该是英文的,在创建之前请检查现有标签和同义词
136 | tags=wkt,raster,hello world
137 |
138 | ; 这些元数据可以为空,最终将成为强制性的。
139 | homepage=https://www.itopen.it
140 | icon=icon.png
141 |
142 | ; 实验标志(适用于单一版本)
143 | experimental=True
144 |
145 | ; 弃用标志 (适用于整个插件,不仅适用于上传的版本)
146 | deprecated=False
147 |
148 | ; 如果为空,它将自动设置为主要版本+.99
149 | qgisMaximumVersion=3.99
150 |
151 | ; 从 QGIS 3.8开始,可以指定以逗号分隔指定要安装(或更新)的插件列表
152 | ; 下面示例安装或更新版本1.12的“MyOtherPlugin”和任何版本的“YetAnotherPlugin”
153 | plugin_dependencies=MyOtherPlugin==1.12,YetAnotherPlugin
154 | ```
155 |
156 | #### 16.1.2.2 \_\_init\_\_.py
157 |
158 | Python包需要此文件。此外,QGIS要求此文件包含一个`classFactory()`函数,该函数在插件被加载到QGIS时调用。它接收[`QgisInterface`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)实例, 并且必须返回`mainplugin.py`中插件类的对象——在我们的例子中它被命名为`TestPlugin`(见下文)。`__init__.py`应该是这样的:
159 |
160 | ```python
161 | def classFactory(iface):
162 | from .mainPlugin import TestPlugin
163 | return TestPlugin(iface)
164 |
165 | # 任何其他初始化
166 | ```
167 |
168 | #### 16.1.2.3 mainPlugin.py
169 |
170 | 这就是魔法发生的地方,下面就是魔法的例子:
171 |
172 | ```python
173 | from qgis.PyQt.QtGui import *
174 | from qgis.PyQt.QtWidgets import *
175 |
176 | # 从文件resources.py初始化Qt的资源
177 | from . import resources
178 |
179 |
180 | class TestPlugin:
181 |
182 | def __init__(self, iface):
183 | # 保存QGIS interface引用
184 | self.iface = iface
185 |
186 | def initGui(self):
187 | # 创建操作,它将启动插件配置
188 | self.action = QAction(QIcon(":/plugins/testplug/icon.png"), "Test plugin", self.iface.mainWindow())
189 | self.action.setObjectName("testAction")
190 | self.action.setWhatsThis("Configuration for test plugin")
191 | self.action.setStatusTip("This is status tip")
192 | self.action.triggered.connect(self.run)
193 |
194 | # 添加工具栏按钮和菜单项
195 | self.iface.addToolBarIcon(self.action)
196 | self.iface.addPluginToMenu("&Test plugins", self.action)
197 |
198 | # 连接信号renderComplete——画布渲染完成后发送的信号
199 | self.iface.mapCanvas().renderComplete.connect(self.renderTest)
200 |
201 | def unload(self):
202 | # 删除插件菜单项和图标
203 | self.iface.removePluginMenu("&Test plugins", self.action)
204 | self.iface.removeToolBarIcon(self.action)
205 |
206 | # 断开信号
207 | self.iface.mapCanvas().renderComplete.disconnect(self.renderTest)
208 |
209 | def run(self):
210 | # 创建并显示一个配置对话框或类似的事情
211 | print("TestPlugin: run called!")
212 |
213 | def renderTest(self, painter):
214 |
215 | # 使用painter绘制地图画布
216 | print("TestPlugin: renderTest called!")
217 | ```
218 |
219 | 主插件源文件中(例如 `mainPlugin.py`)必须存在的插件函数是:
220 |
221 | - `__init__` - >可以访问QGIS界面
222 | - `initGui()` - >加载插件时调用
223 | - `unload()` - >卸载插件时调用
224 |
225 | 在上面的例子中,[`addPluginToMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToMenu)被使用。这会将相应的菜单操作添加到 **Plugins** 菜单中。存在额外的方法将操作(action)添加到不同菜单。以下是这些方法的列表:
226 |
227 | - [`addPluginToRasterMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToRasterMenu)
228 | - [`addPluginToVectorMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToVectorMenu)
229 | - [`addPluginToDatabaseMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToDatabaseMenu)
230 | - [`addPluginToWebMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToWebMenu)
231 |
232 | 它们都具有与[`addPluginToMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToMenu)方法相同的语法 。
233 |
234 | 建议将插件菜单添加到其中一个预定义方法,以保持插件条目组织方式的一致性。但是,你可以将自定义菜单组直接添加到菜单栏,如下面示例所示:
235 |
236 | ```python
237 | def initGui(self):
238 | self.menu = QMenu(self.iface.mainWindow())
239 | self.menu.setObjectName("testMenu")
240 | self.menu.setTitle("MyMenu")
241 |
242 | self.action = QAction(QIcon(":/plugins/testplug/icon.png"), "Test plugin", self.iface.mainWindow())
243 | self.action.setObjectName("testAction")
244 | self.action.setWhatsThis("Configuration for test plugin")
245 | self.action.setStatusTip("This is status tip")
246 | self.action.triggered.connect(self.run)
247 | self.menu.addAction(self.action)
248 |
249 | menuBar = self.iface.mainWindow().menuBar()
250 | menuBar.insertMenu(self.iface.firstRightStandardMenu().menuAction(), self.menu)
251 |
252 | def unload(self):
253 | self.menu.deleteLater()
254 | ```
255 |
256 | 不要忘记设置`QAction`和`QMenu` `objectName`插件的特定名称,以便可以自定义。
257 |
258 | 虽然帮助和关于操作也可以添加到你的自定义菜单中,但QGIS主 **帮助** ► **插件** 菜单中有一个简便的地方。这是使用[`pluginHelpMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.pluginHelpMenu)方法完成的。
259 |
260 | ```python
261 | def initGui(self):
262 |
263 | self.help_action = QAction(
264 | QIcon(":/plugins/testplug/icon.png"),
265 | self.tr("Test Plugin..."),
266 | self.iface.mainWindow()
267 | )
268 | # 添加操作到帮助菜单
269 | self.iface.pluginHelpMenu().addAction(self.help_action)
270 |
271 | self.help_action.triggered.connect(self.show_help)
272 |
273 | @staticmethod
274 | def show_help():
275 | """打开在线帮助"""
276 | QDesktopServices.openUrl(QUrl('https://docs.qgis.org'))
277 |
278 | def unload(self):
279 |
280 | self.iface.pluginHelpMenu().removeAction(self.help_action)
281 | del self.help_action
282 | ```
283 |
284 | ### 16.1.3 文档
285 |
286 | 该插件的文档可以编写为HTML帮助文档。`qgis.utils`模块提供了一个函数,`showPluginHelp()`将打开帮助文档浏览器,与其他QGIS帮助文档的方式相同。
287 |
288 | `showPluginHelp()`函数在与调用模块相同的目录中查找帮助文档。它会寻找`index-ll_cc.html`,`index-ll.html`,`index-en.html`,`index-en_us.html`和`index.html`,显示它找到的第一个文档。这`ll_cc`是QGIS语言环境,这允许文档有多个翻译。
289 |
290 | `showPluginHelp()`函数还可以使用参数`packageName`——它显示指定插件的帮助文档,`filename`——可以替换被搜索的文件名中的“index”,section——它是html锚标记的名称,浏览器将定位到该位置。
291 |
292 | ### 16.1.4 翻译
293 |
294 | 通过几个步骤,你可以设置插件本地化的环境,以便根据计算机的区域,插件将以不同语言加载。
295 |
296 | #### 16.1.4.1 软件要求
297 |
298 | 创建和管理所有翻译文件的最简单方法是安装 [Qt Linguist](https://doc.qt.io/qt-5/qtlinguist-index.html)。在基于Debian的GNU / Linux环境中,你可以这样安装它:
299 |
300 | ```shell
301 | sudo apt-get install qttools5-dev-tools
302 | ```
303 |
304 | #### 16.1.4.2 文件和目录
305 |
306 | 创建插件时,你将在主插件目录中找到该文件夹`i18n`。
307 |
308 | **所有翻译文件都必须放在此目录中。**
309 |
310 | ##### 16.1.4.2.1 .pro文件
311 |
312 | 首先,你应该创建一个`.pro`文件,这是一个可以由 **Qt Linguist** 管理的 *项目* 文件。
313 |
314 | 在此`.pro`文件中,你必须指定要翻译的所有文件和窗体(.ui文件)。此文件用于设置本地化文件和变量。下面是一个项目文件,匹配我们的[示例插件](#16111)的结构 :
315 |
316 | ```ini
317 | FORMS = ../form.ui
318 | SOURCES = ../your_plugin.py
319 | TRANSLATIONS = your_plugin_it.ts
320 | ```
321 |
322 | 你的插件可能有更复杂的结构,并且可能分布在多个文件中。如果是这种情况,请记住,使用`pylupdate5`读取`.pro`文件并更新可翻译字符串,这不会扩展通配符,因此你需要将每个文件显式放在`.pro`文件中。你的项目文件可能看起来像这样:
323 |
324 | ```ini
325 | FORMS = ../ui/about.ui ../ui/feedback.ui \
326 | ../ui/main_dialog.ui
327 | SOURCES = ../your_plugin.py ../computation.py \
328 | ../utils.py
329 | ```
330 |
331 | 此外,`your_plugin.py`文件是*调用* QGIS工具栏中插件的所有菜单和子菜单的文件,你希望将它们全部翻译。
332 |
333 | 最后,使用*TRANSLATIONS*变量,你可以指定所需的翻译语言。
334 |
335 | !!! 警告 warning
336 |
337 | 确保`ts`文件名称为`your_plugin_` + `language` + `.ts`,否则语言将加载失败。使用两个字母的语言缩写(it对应Italian,de对应German,等等)
338 |
339 | ##### 16.1.4.2.2 .ts文件
340 |
341 | 创建`.pro`完成后,你就可以为插件的语言生成`.ts`文件了。
342 |
343 | 打开终端,转到`your_plugin/i18n`目录并输入:
344 |
345 | ```shell
346 | pylupdate5 your_plugin.pro
347 | ```
348 |
349 | 你应该可以看到`your_plugin_language.ts`文件。
350 |
351 | 用 **Qt Linguist** 打开`.ts`文件并开始翻译。
352 |
353 | ##### 16.1.4.2.3 .qm文件
354 |
355 | 当你完成插件翻译时(如果某些字符串未完成,将使用这些字符串的源语言),你必须创建`.qm` 文件(将被QGIS使用的`.ts`编译文件)。
356 |
357 | 只需在`your_plugin/i18n`目录中打开终端cd 并输入:
358 |
359 | ```shell
360 | lrelease your_plugin.ts
361 | ```
362 |
363 | 现在,在`i18n`目录中你将看到`your_plugin.qm`文件。
364 |
365 | #### 16.1.4.3 使用MakeFile进行翻译
366 |
367 | 或者,如果你使用Plugin Builder创建了插件,则可以使用makefile从python代码和Qt对话框中提取消息。在Makefile的开头有一个LOCALES变量:
368 |
369 | ```ini
370 | LOCALES = en
371 | ```
372 |
373 | 将该语言的缩写添加到此变量中,例如匈牙利语:
374 |
375 | ```ini
376 | LOCALES = en hu
377 | ```
378 |
379 | 现在,你可以通过以下方式从源生成或更新`hu.ts`文件(以及其中的`en.ts`):
380 |
381 | ```shell
382 | make transup
383 | ```
384 |
385 | 在此之后,你已在LOCALES变量中更新`.ts`了所有语言的文件。使用 **Qt Linguist** 翻译程序消息。完成翻译后,`.qm`可以通过`transcompile`创建:
386 |
387 | ```shell
388 | make transcompile
389 | ```
390 |
391 | 你必须在你的插件中分发`.ts`文件。
392 |
393 | #### 16.1.4.4 加载插件
394 |
395 | 要查看插件的翻译,只需打开QGIS,更改语言( **设置** ‣ **选项** ‣ **通用** )并重新启动QGIS。
396 |
397 | 你应该看到你的插件使用正确的语言。
398 |
399 | !!! 警告 warning
400 |
401 | 如果你改变了一些东西(新的UI,新的菜单,等等),你必须重新生成`.ts`和`.qm`文件,因此需要再一次执行以上命令。
402 |
403 | ### 16.1.5 提示和技巧
404 |
405 | #### 16.1.5.1 插件重载
406 |
407 | 在开发插件期间,你经常需要在QGIS中重新加载它以进行测试。使用 **Plugin Reloader** 插件非常容易。你可以在[插件管理器](https://docs.qgis.org/testing/en/docs/user_manual/plugins/plugins.html#plugins)中找到它。
408 |
409 | #### 16.1.5.2 使用 qgis-plugin-ci自动打包、发布和翻译
410 |
411 | [qgis-plugin-ci](https://opengisch.github.io/qgis-plugin-ci/)提供了一个命令行界面,用于在你的计算机上执行 QGIS 插件的自动打包和部署,或使用持续集成(如 [GitHub 工作流](https://docs.github.com/en/actions/using-workflows)或 [Gitlab-CI](https://docs.gitlab.com/ee/ci/) 以及 [Transifex](https://www.transifex.com/) 进行翻译)。
412 |
413 | #### 16.1.5.3 访问插件
414 |
415 | 你可以使用python从QGIS中访问所有已安装插件类,这可以方便调试:
416 |
417 | ```python
418 | my_plugin = qgis.utils.plugins['My Plugin']
419 | ```
420 |
421 | #### 16.1.5.4 日志消息
422 |
423 | 插件在[日志消息面板中](https://docs.qgis.org/latest/en/docs/user_manual/introduction/general_tools.html#log-message-panel)有自己的选项卡。
424 |
425 | #### 16.1.5.5 分享你的插件
426 |
427 | QGIS在插件仓库中托管了数百个插件。考虑分享你的插件!它将扩展QGIS,人们将能够从你的代码中学习。可以使用插件管理器在QGIS中找到并安装所有托管的插件。
428 |
429 | 信息和要求:[plugins.qgis.org](https://plugins.qgis.org/)。
430 |
431 | ### 16.1.6 提示和技巧
432 |
433 | #### 16.1.6.1 插件重新加载程序
434 |
435 | 在开发插件过程中,你经常需要重新加载它以进行测试。使用**Plugin Reloader**插件非常容易。你可以在[插件管理器](https://docs.qgis.org/testing/en/docs/user_manual/plugins/plugins.html#plugins)中找到它。
436 |
437 | #### 16.1.6.2 使用qgis插件ci自动打包、发布和翻译
438 |
439 | [qgis-plugin-ci](https://opengisch.github.io/qgis-plugin-ci/) 提供了一个命令行界面,可以在你的计算机上或使用持续集成工具(如 [GitHub 工作流](https://docs.github.com/en/actions/using-workflows)或 [Gitlab-CI](https://docs.gitlab.com/ee/ci/))以及 [Transifex](https://www.transifex.com/) 进行自动打包和部署 QGIS 插件。它允许通过 CLI 或在 CI 操作中发布、翻译、发布或生成 XML 插件存储库文件。
440 |
441 | #### 16.1.6.3 访问插件
442 |
443 | 你可以使用Python在QGIS中访问所有已安装插件的类,这对于调试很有用。
444 |
445 | ```python
446 | my_plugin = qgis.utils.plugins['My Plugin']
447 | ```
448 |
449 | #### 16.1.6.4 日志信息
450 |
451 | 插件在[日志消息面板](https://docs.qgis.org/testing/en/docs/user_manual/introduction/general_tools.html#log-message-panel)中有自己的选项卡。
452 |
453 | #### 16.1.6.5 资源文件
454 |
455 | 你可以看到在`initGui()`中我们使用了资源文件中的图标(在我们的案例中是`resources.qrc`)
456 |
457 | ```xml
458 |
459 |
460 | icon.png
461 |
462 |
463 | ```
464 |
465 | 最好使用不会与其他插件或QGIS的任何部分发生冲突的前缀,否则你可能会得到你不想要的资源。现在你只需要生成一个包含资源的Python文件。它是用 **pyrcc5** 命令完成的:
466 |
467 | ```shell
468 | pyrcc5 -o resources.py resources.qrc
469 | ```
470 |
471 | !!! 提示
472 |
473 | 在Windows环境中,尝试从CMD或Powershell运行pyrcc5可能会导致错误“Windows无法访问指定的设备,路径,或文件[...]”。最简单的解决方案可能是使用osgeo4wshell,但如果你愿意修改PATH环境变量或显式指定可执行文件的路径,你应该可以在`\bin\pyrcc5.exe`找到它
474 |
475 | 就这些……没什么复杂的:)
476 |
477 | 如果你已正确完成所有操作,则应该能够在插件管理器中查找并加载插件,在点击工具栏图标或相应的菜单项时,可以在控制台中查看到消息。
478 |
479 | 在处理真正的插件时,最好将插件写入另一个(工作)目录并创建一个makefile,它将生成UI和资源文件并将插件安装到QGIS安装中。
480 |
481 |
482 |
483 | ## 16.2 代码片段
484 |
485 | 本节以代码片段为例,讲解插件开发
486 |
487 | ### 16.2.1 如何通过快捷键调用方法
488 |
489 | 在`initGui()`中添加:
490 |
491 | ```python
492 | self.key_action = QAction("Test Plugin", self.iface.mainWindow())
493 | self.iface.registerMainWindowAction(self.key_action, "Ctrl+I") # 操作被Ctrl+I触发
494 | self.iface.addPluginToMenu("&Test plugins", self.key_action)
495 | self.key_action.triggered.connect(self.key_action_triggered)
496 | ```
497 |
498 | 在`unload()`中添加:
499 |
500 | ```python
501 | self.iface.unregisterMainWindowAction(self.keyAction)
502 | ```
503 |
504 | 按下`CTRL+I`时调用方法:
505 |
506 | ```python
507 | def key_action_triggered(self):
508 | QMessageBox.information(self.iface.mainWindow(),"Ok", "You pressed Ctrl+I")
509 | ```
510 |
511 | 还可以允许用户为提供的操作自定义快捷键。这是通过添加以下内容来完成的:
512 |
513 | ```python
514 | # 在 initGui() 方法中
515 | QgsGui.shortcutsManager().registerAction(self.key_action)
516 |
517 | # 在 unload() 方法中
518 | QgsGui.shortcutsManager().unregisterAction(self.key_action)
519 | ```
520 |
521 | ### 16.2.2 如何重用QGIS图标
522 |
523 | 因为它们是众所周知的,并且向用户传达了明确的信息,所以有时你可能希望在插件中重用QGIS图标,而不是绘制和设置新图标。使用[`getThemeIcon()`](https://qgis.org/pyqgis/master/core/QgsApplication.html#qgis.core.QgsApplication.getThemeIcon)方法。
524 |
525 | 例如,重用 QGIS 代码存储库中可用的 [mActionFileOpen.svg](https://github.com/qgis/QGIS/blob/master/images/themes/default/mActionFileOpen.svg) 图标:
526 |
527 | ```
528 | # 例如:在初始化GUI时
529 | self.file_open_action = QAction(
530 | QgsApplication.getThemeIcon("/mActionFileOpen.svg"),
531 | self.tr("Select a File..."),
532 | self.iface.mainWindow()
533 | )
534 | self.iface.addPluginToMenu("MyPlugin", self.file_open_action)
535 | ```
536 |
537 | [`iconPath()`](https://qgis.org/pyqgis/master/core/QgsApplication.html#qgis.core.QgsApplication.iconPath)是调用 QGIS 图标的另一种方法。有关调用主题图标的示例,请访问[QGIS嵌入式图像—速查表](https://static.geotribu.fr/toc_nav_ignored/qgis_resources_preview_table/)。
538 |
539 | ### 16.2.3 选项对话框中的插件接口
540 |
541 | 你可以在 **设置** ‣ **选项** 中添加一个自定义插件选项标签。这比为你的插件选项添加一个特定的主菜单条目更可取,因为它将所有的QGIS应用程序设置和插件设置保存在一个单一的地方,便于用户发现和导航。
542 |
543 | 下面的代码片段将为插件的设置添加一个新的空白选项卡,为你填充所有选项和你的插件特定设置做好准备。你可以将下面的类拆分成不同的文件。在这个例子中,我们在mainPlugin.py文件中添加了两个类。
544 |
545 | ```python
546 | class MyPluginOptionsFactory(QgsOptionsWidgetFactory):
547 |
548 | def __init__(self):
549 | super().__init__()
550 |
551 | def icon(self):
552 | return QIcon('icons/my_plugin_icon.svg')
553 |
554 | def createWidget(self, parent):
555 | return ConfigOptionsPage(parent)
556 |
557 |
558 | class ConfigOptionsPage(QgsOptionsPageWidget):
559 |
560 | def __init__(self, parent):
561 | super().__init__(parent)
562 | layout = QHBoxLayout()
563 | layout.setContentsMargins(0, 0, 0, 0)
564 | self.setLayout(layout)
565 | ```
566 |
567 | 最后我们添加导入和修改`__init__`函数:
568 |
569 | ```python
570 | from qgis.gui import QgsOptionsWidgetFactory, QgsOptionsPageWidget
571 |
572 |
573 | class MyPlugin:
574 | def __init__(self, iface):
575 | """构造函数.
576 |
577 | :param iface: 将传递给该类的接口实例它提供了一个钩子,你可以通过它来操作QGIS运行时应用程序。
578 | :type iface: QgsInterface
579 | """
580 | # 保存引用
581 | self.iface = iface
582 |
583 |
584 | def initGui(self):
585 | self.options_factory = MyPluginOptionsFactory()
586 | self.options_factory.setTitle(self.tr('My Plugin'))
587 | iface.registerOptionsWidgetFactory(self.options_factory)
588 |
589 | def unload(self):
590 | iface.unregisterOptionsWidgetFactory(self.options_factory)
591 | ```
592 |
593 | !!! 提示
594 |
595 | **将自定义选项卡添加到图层属性对话框**
596 |
597 | 你可以应用类似的逻辑,使用[QgsMapLayerConfigWidgetFactory](https://qgis.org/pyqgis/master/gui/QgsMapLayerConfigWidgetFactory.html#qgis.gui.QgsMapLayerConfigWidgetFactory)和[QgsMapLayerConfigWidget](https://qgis.org/pyqgis/master/gui/QgsMapLayerConfigWidget.html#qgis.gui.QgsMapLayerConfigWidget)类将插件自定义选项添加到图层属性对话框中。
598 |
599 | ### 16.2.4 在图层树中嵌入图层的自定义小组件
600 |
601 | 除了在图层面板中常见的图层符号元素之外,你还可以添加自己的小部件,以便快速访问一些经常与图层一起使用的操作(设置过滤、选择、样式、使用按钮小部件刷新图层、创建基于时间轴的图层或仅在标签中显示额外的图层信息等)。这些所谓的图层树嵌入式小部件通过图层的属性图例选项卡为各个图层提供。
602 |
603 | 以下代码片段在图例中创建一个下拉菜单,显示图层可用的图层样式,允许快速在不同的图层样式之间切换。
604 |
605 | ```python
606 | class LayerStyleComboBox(QComboBox):
607 | def __init__(self, layer):
608 | QComboBox.__init__(self)
609 | self.layer = layer
610 | for style_name in layer.styleManager().styles():
611 | self.addItem(style_name)
612 |
613 | idx = self.findText(layer.styleManager().currentStyle())
614 | if idx != -1:
615 | self.setCurrentIndex(idx)
616 |
617 | self.currentIndexChanged.connect(self.on_current_changed)
618 |
619 | def on_current_changed(self, index):
620 | self.layer.styleManager().setCurrentStyle(self.itemText(index))
621 |
622 | class LayerStyleWidgetProvider(QgsLayerTreeEmbeddedWidgetProvider):
623 | def __init__(self):
624 | QgsLayerTreeEmbeddedWidgetProvider.__init__(self)
625 |
626 | def id(self):
627 | return "style"
628 |
629 | def name(self):
630 | return "Layer style chooser"
631 |
632 | def createWidget(self, layer, widgetIndex):
633 | return LayerStyleComboBox(layer)
634 |
635 | def supportsLayer(self, layer):
636 | return True # any layer is fine
637 |
638 | provider = LayerStyleWidgetProvider()
639 | QgsGui.layerTreeEmbeddedWidgetRegistry().addProvider(provider)
640 | ```
641 |
642 | 从给定图层的图例属性选项卡中,将“图层样式选择器”从“可用小部件”拖动到“已用小部件”,以在图层树中启用小部件。嵌入式小部件始终显示在其相关图层节点子项的顶部。
643 |
644 | 如果你想从插件中使用小部件,可以按以下方法添加它们:
645 |
646 | ```python
647 | layer = iface.activeLayer()
648 | counter = int(layer.customProperty("embeddedWidgets/count", 0))
649 | layer.setCustomProperty("embeddedWidgets/count", counter+1)
650 | layer.setCustomProperty("embeddedWidgets/{}/id".format(counter), "style")
651 | view = self.iface.layerTreeView()
652 | view.layerTreeModel().refreshLayerLegend(view.currentLegendNode())
653 | view.currentNode().setExpanded(True)
654 | ```
655 |
656 | ## 16.3 编写和调试插件的IDE设置
657 |
658 | **TODO**
659 |
660 | ## 16.4 发布你的插件
661 |
662 | 一旦你的插件准备好了,并且你认为这个插件可能对某些人有帮助,不要犹豫,把它上传到[官方Python插件仓库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/releasing.html#official-pyqgis-repository)。在该页面上,你还可以找到关于如何准备插件以与插件安装程序良好配合的打包指南。或者,如果你想建立自己的插件库,可以创建一个简单的XML文件,列出插件及其元数据。
663 |
664 | 请特别注意一下建议:
665 |
666 | ### 16.4.1 元数据和名称
667 |
668 | - 避免使用与现有插件过于相似的名称
669 | - 如果你的插件与现有的插件有类似的功能,请在 "关于 "一栏中解释其区别,这样用户就会知道使用哪一个,而不需要安装和测试。
670 | - 避免在插件本身的名称中重复使用"plugin"。
671 | - 使用元数据中的描述字段进行单行描述,使用关于字段进行更详细的说明
672 | - 包括一个代码库、一个错误跟踪器和一个主页;这将极大地提高合作的可能性,并且可以通过现有的网络基础设施(GitHub、GitLab、Bitbucket等)非常容易地完成。
673 | - 谨慎选择标签:避免不具参考价值的标签(如vector),最好选择已经被他人使用的标签(见插件网站)。
674 | - 添加一个合适的图标,不要使用默认的图标;参见QGIS界面,了解要使用的风格建议
675 |
676 | ### 16.4.2 代码和帮助
677 |
678 | - 不要把生成的文件(ui_*.py, resources_rc.py, 生成的帮助文件...)和无用的东西(如.gitignore)包括在版本库中
679 |
680 | - 将插件添加到适当的菜单中(Vector, Raster, Web, Database)。
681 |
682 | - 在适当的时候(执行分析的插件),考虑将插件添加为Processing框架的子插件:这将允许用户批量运行它,将它集成到更复杂的工作流中,并将你从设计界面的负担中解放出来
683 |
684 | - 至少包括最基本的文档,如果对测试和理解有用的话,还包括样本数据。
685 |
686 | ### 16.4.3 官方python插件仓库
687 |
688 | 你可以找到官方python插件仓库:https://plugins.qgis.org/。
689 |
690 | 为了使用官方python插件仓库,你必须从[OSGEO web portal](https://www.osgeo.org/community/getting-started-osgeo/osgeo_userid/)获得OSGEO ID。
691 |
692 | 一旦你上传了你的插件,它将得到一个工作人员的批准,你会得到通知。
693 |
694 | #### 16.4.3.1 权限
695 |
696 | 这些规则已经在官方插件库中实现:
697 |
698 | - 每个注册用户都可以添加一个新的插件
699 |
700 | - 员工用户可以批准或不批准所有的插件版本
701 |
702 | - 拥有特殊权限`plugins.can_approve`的用户可以自动批准他们上传的版本
703 |
704 | - 拥有特殊权限`plugins.can_approve`的用户可以批准其他人上传的版本,只要他们在插件所有者的列表中。
705 |
706 | - 一个特定的插件只能由员工用户和插件所有者删除和编辑。
707 |
708 | - 如果一个没有`plugins.can_approve`权限的用户上传了一个新的版本,该插件的版本会自动取消审批。
709 |
710 | #### 16.4.3.2 信任管理
711 |
712 | 工作人员可以通过前端应用程序设置`plugins.can_approve`权限,向选定的插件创建者授予信任。
713 |
714 | 插件详情视图提供了直接链接,以授予对插件创建者或插件所有者的信任。
715 |
716 | #### 16.4.3.3 验证
717 |
718 | 在上传插件时,插件的元数据会自动从压缩包中导入并进行验证。
719 |
720 | 这里有一些验证规则,当你想在官方仓库上传一个插件时,你应该注意:
721 |
722 | 1. 包含插件的主文件夹的名称必须只包含ASCII字符(A-Z和a-z)、数字和下划线(_)和减号(-),而且不能以数字开头。
723 | 2. `metadata.txt`是必需的
724 | 3. 元数据表中列出的所有必需的元数据都必须存在
725 | 4. 元数据`version`字段必须是唯一的
726 |
727 | #### 16.4.3.4 插件结构
728 |
729 | 按照验证规则,你的插件的压缩包(.zip)必须有一个特定的结构,才能作为一个功能性插件进行验证。由于该插件将被解压在用户的plugins文件夹内,它必须在.zip文件内有自己的目录,以不干扰其他插件。必需的文件有:`metadata.txt` 和 `__init__.py`。但如果能有一个README,当然还有一个代表该插件的图标(resources.qrc),那就更好了。以下是一个plugin.zip的例子,它应该是这样的。
730 |
731 | ```
732 | plugin.zip
733 | pluginfolder/
734 | |-- i18n
735 | | |-- translation_file_de.ts
736 | |-- img
737 | | |-- icon.png
738 | | `-- iconsource.svg
739 | |-- __init__.py
740 | |-- Makefile
741 | |-- metadata.txt
742 | |-- more_code.py
743 | |-- main_code.py
744 | |-- README
745 | |-- resources.qrc
746 | |-- resources_rc.py
747 | `-- ui_Qt_user_interface_file.ui
748 | ```
749 |
750 |
--------------------------------------------------------------------------------
/docs/20-QGIS服务器和Python.md:
--------------------------------------------------------------------------------
1 | # 20-QGIS服务器和Python
2 |
3 | ## 20.1 介绍
4 |
5 | 要了解有关QGIS服务器的更多信息,请阅读[QGIS服务器指南/手册](https://docs.qgis.org/testing/en/docs/server_manual/index.html#qgis-server-manual)。
6 |
7 | QGIS服务器是三个不同的东西:
8 |
9 | 1. QGIS服务器库:一个为创建OGC网络服务提供API的库
10 | 2. QGIS服务器FCGI:一个FCGI二进制应用程序`qgis_mapserv.fcgi`,与网络服务器一起实现一套OGC服务(WMS、WFS、WCS等)和OGC APIs(WFS3/OAPIF)。
11 | 3. QGIS开发服务器:一个开发服务器二进制应用程序`qgis_mapserver`,实现了一套OGC服务(WMS、WFS、WCS等)和OGC APIs(WFS3/OAPIF)。
12 |
13 | 本章的重点是第一个话题,通过解释QGIS服务器API的用法来说明如何使用Python来扩展、增强或定制服务器行为,或如何使用QGIS服务器API将QGIS服务器嵌入到另一个应用程序。
14 |
15 | 你可以通过一些不同的方式来改变QGIS服务器的行为或扩展其功能,以提供新的定制服务或API,一下是你可能面临的主要情况:
16 |
17 | - 融合 → 从另一个Python应用程序中使用QGIS服务器API
18 | - 独立 → 以独立的WSGI/HTTP服务方式运行QGIS服务器
19 | - 过滤 → 使用过滤插件增强/定制QGIS服务器
20 | - 服务 → 添加一个新的*服务*
21 | - OGC APIs → 添加一个新的*OGC API*
22 |
23 | 嵌入和独立的应用程序需要直接从另一个Python脚本或应用程序中使用QGIS服务器的Python API。其余的选项更适合于当你想在标准的QGIS服务器二进制应用程序(FCGI或开发服务器)中添加自定义功能时:在这种情况下,你需要为服务器应用程序编写一个Python插件,并注册你的自定义过滤器、服务或API。
24 |
25 | ## 20.2 服务器API基础
26 |
27 | 一个典型的QGIS服务器应用程序所涉及的基本类是:
28 |
29 | - [`QgsServer`](https://qgis.org/pyqgis/master/server/QgsServer.html#qgis.server.QgsServer) 服务实例 (通常在整个应用程序的生命周期中只有一个实例)
30 | - [`QgsServerRequest`](https://qgis.org/pyqgis/master/server/QgsServerRequest.html#qgis.server.QgsServerRequest) 请求对象(通常在每次请求时重新创建)
31 | - [`QgsServer.handleRequest(request, response)`](https://qgis.org/pyqgis/master/server/QgsServer.html#qgis.server.QgsServer.handleRequest) 处理请求并响应
32 |
33 | QGIS服务器FCGI或开发服务器的工作流程可以概括为以下几点:
34 |
35 | ```
36 | initialize the QgsApplication
37 | create the QgsServer
38 | the main server loop waits forever for client requests:
39 | for each incoming request:
40 | create a QgsServerRequest request
41 | create a QgsServerResponse response
42 | call QgsServer.handleRequest(request, response)
43 | filter plugins may be executed
44 | send the output to the client
45 | ```
46 |
47 | 在[`QgsServer.handleRequest(request, response)`](https://qgis.org/pyqgis/master/server/QgsServer.html#qgis.server.QgsServer.handleRequest)方法中,过滤器插件的回调函数被调用,[`QgsServerRequest`](https://qgis.org/pyqgis/master/server/QgsServerRequest.html#qgis.server.QgsServerRequest)和[`QgsServerResponse`](https://qgis.org/pyqgis/master/server/QgsServerResponse.html#qgis.server.QgsServerResponse)通过[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)类被提供给插件。
48 |
49 | !!! 警告 warning
50 |
51 | QGIS服务器类不是线程安全的,在构建基于QGIS服务器API的可扩展应用程序时,你应该始终使用多进程模型或容器。
52 |
53 | ## 20.3 独立或嵌入
54 |
55 | 对于独立的服务器应用或嵌入,你需要直接使用上述的服务器类,将它们包装成一个Web服务器实现,管理所有与客户端的HTTP协议交互。
56 |
57 | 这里有一个关于QGIS服务器API应用的最小例子(没有HTTP部分):
58 |
59 | ```python
60 | from qgis.core import QgsApplication
61 | from qgis.server import *
62 | app = QgsApplication([], False)
63 |
64 | # 创建服务器实例,它可能是一个单一的实例,在多个请求中重复使用
65 | server = QgsServer()
66 |
67 | # 通过指定完整的URL和一个可选的主体来创建请求(例如POST请求)
68 | request = QgsBufferServerRequest(
69 | 'http://localhost:8081/?MAP=/qgis-server/projects/helloworld.qgs' +
70 | '&SERVICE=WMS&REQUEST=GetCapabilities')
71 |
72 | # 创建响应对象
73 | response = QgsBufferServerResponse()
74 |
75 | # 处理请求
76 | server.handleRequest(request, response)
77 |
78 | print(response.headers())
79 | print(response.body().data().decode('utf8'))
80 |
81 | app.exitQgis()
82 | ```
83 |
84 | 这里有一个完整的独立应用实例,它是为QGIS源代码库的持续集成测试而开发的,它展示了一系列不同的插件过滤器和认证方案(不意味着可用于生产环境,因为它们只是为测试目的而开发的,但对于学习来说仍然很有趣):https://github.com/qgis/QGIS/blob/master/tests/src/python/qgis_wrapped_server.py
85 |
86 | ## 20.4 服务器插件
87 |
88 | 服务器python插件在QGIS服务器应用程序启动时被加载一次,可用于注册过滤器、服务或API。
89 |
90 | 服务器插件的结构与桌面版的插件非常相似,一个[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)对象被提供给插件,插件可以通过使用服务器接口暴露的方法将一个或多个自定义过滤器、服务或API注册到相应的注册表。
91 |
92 | ### 20.4.1 服务器过滤插件
93 |
94 | 过滤器有三种不同的类型,它们可以通过子类化下面的一个类并调用[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)的相应方法来实例化。
95 |
96 | | 过滤器类型 | 基类 | QgsServerInterface 注册 |
97 | | -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
98 | | I/O | [`QgsServerFilter`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter) | [`registerFilter()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.registerFilter) |
99 | | Access Control | [`QgsAccessControlFilter`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter) | [`registerAccessControl()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.registerAccessControl) |
100 | | Cache | [`QgsServerCacheFilter`](https://qgis.org/pyqgis/master/server/QgsServerCacheFilter.html#qgis.server.QgsServerCacheFilter) | [`registerServerCache()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.registerServerCache) |
101 |
102 | #### 20.4.1.1 I/O过滤器
103 |
104 | I/O过滤器可以修改核心服务(WMS、WFS等)的服务器输入和输出(请求和响应),允许对服务工作流进行任何形式的操作。例如,可以限制对选定图层的访问,向XML响应注入XSL样式表,向生成的WMS图像添加水印等等。
105 |
106 | 从这一点来看,你可能会发现快速浏览一下[服务器插件的API文档](https://qgis.org/pyqgis/master/server)很有用。
107 |
108 | 每一个插件应该至少实现以下三个回调函数:
109 |
110 | - [`onRequestReady()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onRequestReady)
111 | - [`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete)
112 | - [`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse)
113 |
114 | 所有的过滤器都可以访问请求/响应对象([`QgsRequestHandler`](https://qgis.org/pyqgis/master/server/QgsRequestHandler.html#qgis.server.QgsRequestHandler)),并且可以操作它的所有属性(输入/输出)和引发异常(同时以一种相当特别的方式,我们将在下面看到)。
115 |
116 | 所有这些方法都返回一个布尔值,指示调用是否应传播到后续过滤器。如果其中一个方法返回False,则传播链停止,否则调用将传播到下一个过滤器。
117 |
118 | 下面是显示服务器如何处理一个典型请求以及何时调用过滤器回调函数的伪代码。
119 |
120 | ```
121 | for each incoming request:
122 | create GET/POST request handler
123 | pass request to an instance of QgsServerInterface
124 | call onRequestReady filters
125 |
126 | if there is not a response:
127 | if SERVICE is WMS/WFS/WCS:
128 | create WMS/WFS/WCS service
129 | call service’s executeRequest
130 | possibly call onSendResponse for each chunk of bytes
131 | sent to the client by a streaming services (WFS)
132 | call onResponseComplete
133 | request handler sends the response to the client
134 | ```
135 |
136 | 下面几个段落详细描述了可用的回调函数。
137 |
138 | ##### 20.4.1.1.1 请求就绪
139 |
140 | 当请求准备就绪时,将被调用:传入的URL和数据已经被解析,在进入核心服务(WMS,WFS等)开关之前,这是你可以操作输入和执行动作的地方:
141 |
142 | - 认证授权
143 | - 重定向
144 | - 添加删除某些参数 (例如类型名称)
145 | - 抛出异常
146 |
147 | 你甚至可以通过改变 **SERVICE** 参数来完全替代一个核心服务,从而完全绕过核心服务(不过这并没有什么意义)。
148 |
149 | ##### 20.4.1.1.2 发送响应
150 |
151 | 无论何时从响应缓冲区刷新任何部分输出,都会调用该函数(例如 **FCGI** 标准输出被使用),并从缓冲区刷新到客户端。当大量内容被流式传输(比如WFS GetFeature)时,就会发生这种情况。在这种情况下,[`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse)可能会被多次调用。
152 |
153 | 请注意,如果响应没有流式传输,则根本不会调用[`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse)。
154 |
155 | 在所有情况下,调用[`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete)后,最后一个(或唯一的)块将被发送到客户端。
156 |
157 | 返回`False`将防止向客户端刷新数据。当插件希望从响应中收集所有块,并在[`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete)中检查或更改响应时,这是可取的。
158 |
159 | ##### 20.4.1.1.3 响应完成
160 |
161 | 当核心服务(如果被击中的话)完成它们的过程,并且请求准备好被发送到客户端时,将被调用一次。如上所述,通常在[`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse)之前调用,除了流媒体服务(或其他插件过滤器)可能在之前调用[`sendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.sendResponse)。
162 |
163 | [`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete)是提供新服务实现(WPS或自定义服务)和对来自核心服务的输出进行直接操作的理想场所(例如,在WMS图像上添加水印)。
164 |
165 | 请注意,返回`False`将阻止下一个插件执行[`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete),但在任何情况下,都会阻止将响应发送到客户端。
166 |
167 | #### 20.4.1.2 从插件引发异常
168 |
169 | 在这个问题上还有一些工作要做:目前的实现可以通过将[`QgsRequestHandler`](https://qgis.org/pyqgis/master/server/QgsRequestHandler.html#qgis.server.QgsRequestHandler)属性设置为QgsMapServiceException的一个实例来区分已处理和未处理的异常,这样,主要的C++代码可以捕获已处理的Python异常而忽略未处理的异常(或者更好的是:记录日志)。
170 |
171 | 这种方法基本上是可行的,但它不是很 "pythonic":一个更好的方法是在python代码中引发异常,并看到它们进入到C++循环中被处理。
172 |
173 | #### 20.4.1.3 编写一个服务器插件
174 |
175 | 服务器插件是一个标准的 QGIS Python 插件,如[16-开发Python插件](16-开发Python插件.md)中所述,它只是提供了一个额外的(或替代的)接口:典型的 QGIS 桌面插件通过 [`QgisInterface `](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)实例访问 QGIS 应用程序,而服务器插件只有在 QGIS Server 应用程序上下文中执行时才能访问。
176 |
177 | 为了让QGIS Server知道一个插件有一个服务器接口,需要一个特殊的元数据条目(在`metadata.txt`中)。
178 |
179 | ```ini
180 | server=True
181 | ```
182 |
183 | !!! 重要的 important
184 |
185 | 只有设置了`server=True`元数据的插件才能被QGIS Server加载和执行。
186 |
187 | 这里讨论的 [qgis3-server-vagrant](https://github.com/elpaso/qgis3-server-vagrant/tree/master/resources/web/plugins) 示例插件(以及更多插件)可在 Github 上找到,一些服务器插件也发布在官方的 [QGIS 插件仓库](https://plugins.qgis.org/plugins/server)中。
188 |
189 | #### 20.4.1.4 插件文件
190 |
191 | 下面是我们的示例服务器插件的目录结构。
192 |
193 | ```
194 | PYTHON_PLUGINS_PATH/
195 | HelloServer/
196 | __init__.py --> *required*
197 | HelloServer.py --> *required*
198 | metadata.txt --> *required*
199 | ```
200 |
201 | ##### 20.4.1.4.1 _\_init\_\_.py
202 |
203 | 这个文件是Python的导入系统所要求的。此外,QGIS Server要求该文件包含一个`serverClassFactory()`函数,当服务器启动时,插件被加载到QGIS Server中时,该函数将被调用。它接收对[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)实例的引用,并必须返回你的插件类的实例。以下是插件示例 `__init__.py `的样子:
204 |
205 | ```python
206 | def serverClassFactory(serverIface):
207 | from .HelloServer import HelloServerServer
208 | return HelloServerServer(serverIface)
209 | ```
210 |
211 | ##### 20.4.1.4.2 HelloServer.py
212 |
213 | 这就是魔法发生的地方,这就是魔法的模样。(例如:`HelloServer.py`)
214 |
215 | 一个服务器插件通常由一个或多个回调函数组成,被打包到[`QgsServerFilter`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter)的实例中。
216 |
217 | 每个[`QgsServerFilter`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter)都实现了一个或多个以下的回调函数:
218 |
219 | - [`onRequestReady()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onRequestReady)
220 | - [`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete)
221 | - [`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse)
222 |
223 | 下面的例子实现了一个最小的过滤器,当 **SERVICE** 参数等于 " **HELLO** "时,打印出*HelloServer*!
224 |
225 | ```python
226 | class HelloFilter(QgsServerFilter):
227 |
228 | def __init__(self, serverIface):
229 | super().__init__(serverIface)
230 |
231 | def onRequestReady(self) -> bool:
232 | QgsMessageLog.logMessage("HelloFilter.onRequestReady")
233 | return True
234 |
235 | def onSendResponse(self) -> bool:
236 | QgsMessageLog.logMessage("HelloFilter.onSendResponse")
237 | return True
238 |
239 | def onResponseComplete(self) -> bool:
240 | QgsMessageLog.logMessage("HelloFilter.onResponseComplete")
241 | request = self.serverInterface().requestHandler()
242 | params = request.parameterMap()
243 | if params.get('SERVICE', '').upper() == 'HELLO':
244 | request.clear()
245 | request.setResponseHeader('Content-type', 'text/plain')
246 | # 注意内容类型是"bytes"
247 | request.appendBody(b'HelloServer!')
248 | return True
249 | ```
250 |
251 | 过滤器必须被注册到 **serverIface** 中,如下例所示:
252 |
253 | ```python
254 | class HelloServerServer:
255 | def __init__(self, serverIface):
256 | serverIface.registerFilter(HelloFilter(serverIface), 100)
257 | ```
258 |
259 | [`registerFilter()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.registerFilter)的第二个参数设置了一个优先级,定义了同名回调函数的顺序(优先级低的回调先被调用)。
260 |
261 | 通过使用这三个回调函数,插件可以以许多不同的方式操纵服务器的输入输出。在每个时刻,插件实例都可以通过[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)访问[`QgsRequestHandler`](https://qgis.org/pyqgis/master/server/QgsRequestHandler.html#qgis.server.QgsRequestHandler)。[`QgsRequestHandler`](https://qgis.org/pyqgis/master/server/QgsRequestHandler.html#qgis.server.QgsRequestHandler)类有很多方法,可以用来在进入服务器的核心处理之前(通过使用`requestReady()`)或在请求被核心服务处理之后(通过使用`sendResponse()`)改变输入参数。
262 |
263 | 下面的例子涵盖了一些常见的使用案例。
264 |
265 | ##### 20.4.1.4.3 修改输入
266 |
267 | 示例插件包含一个改变来自查询字符串的输入参数的测试例子,在这个例子中,一个新的参数被注入到(已经解析过的)`parameterMap`中,然后这个参数被核心服务(WMS等)看到,在核心服务处理结束时,我们检查这个参数是否仍然存在:
268 |
269 | ```python
270 | class ParamsFilter(QgsServerFilter):
271 |
272 | def __init__(self, serverIface):
273 | super(ParamsFilter, self).__init__(serverIface)
274 |
275 | def onRequestReady(self) -> bool:
276 | request = self.serverInterface().requestHandler()
277 | params = request.parameterMap( )
278 | request.setParameter('TEST_NEW_PARAM', 'ParamsFilter')
279 | return True
280 |
281 | def onResponseComplete(self) -> bool:
282 | request = self.serverInterface().requestHandler()
283 | params = request.parameterMap( )
284 | if params.get('TEST_NEW_PARAM') == 'ParamsFilter':
285 | QgsMessageLog.logMessage("SUCCESS - ParamsFilter.onResponseComplete")
286 | else:
287 | QgsMessageLog.logMessage("FAIL - ParamsFilter.onResponseComplete")
288 | return True
289 | ```
290 |
291 | 这是日志文件中内容的摘录:
292 |
293 | ```text hl_lines="6"
294 | src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloServerServer - loading filter ParamsFilter
295 | src/core/qgsmessagelog.cpp: 45: (logMessage) [1ms] 2014-12-12T12:39:29 Server[0] Server plugin HelloServer loaded!
296 | src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 Server[0] Server python plugins loaded
297 | src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO into the parameter map
298 | src/mapserver/qgsserverfilter.cpp: 42: (onRequestReady) [0ms] QgsServerFilter plugin default onRequestReady called
299 | src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.onResponseComplete
300 | ```
301 |
302 | 在突出显示的一行,"SUCCESS "字符串表示该插件通过了测试。
303 |
304 | 同样的技术可以被利用来代替核心服务:例如,你可以跳过 **WFS SERVICE** 请求或任何其他核心请求,只需将 **SERVICE** 参数改为不同的参数,核心服务就会被跳过。然后,你可以将你的自定义结果注入到输出中,并将其发送给客户端(这将在下面解释)。
305 |
306 | !!! 提示 infomation
307 |
308 | 如果你真的想实现一个自定义的服务,建议将[`QgsService`](https://qgis.org/pyqgis/master/server/QgsService.html#qgis.server.QgsService)子类化,并通过调用其[`registerService(service)`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.serviceRegistry)方法在[`registerFilter()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.serviceRegistry)方法上注册你的服务。
309 |
310 | ##### 20.4.1.4.4 修改或替换输出
311 |
312 | 水印过滤器的例子显示了如何用一个新的图像替换WMS的输出,该图像是通过在WMS核心服务生成的WMS图像上添加一个水印图像而获得:
313 |
314 | ```python
315 | from qgis.server import *
316 | from qgis.PyQt.QtCore import *
317 | from qgis.PyQt.QtGui import *
318 |
319 | class WatermarkFilter(QgsServerFilter):
320 |
321 | def __init__(self, serverIface):
322 | super().__init__(serverIface)
323 |
324 | def responseComplete(self):
325 | request = self.serverInterface().requestHandler()
326 | params = request.parameterMap( )
327 | # 一些检查
328 | if (params.get('SERVICE').upper() == 'WMS' \
329 | and params.get('REQUEST').upper() == 'GETMAP' \
330 | and not request.exceptionRaised() ):
331 | QgsMessageLog.logMessage("WatermarkFilter.responseComplete: image ready %s" % request.parameter("FORMAT"))
332 | # 获取图像
333 | img = QImage()
334 | img.loadFromData(request.body())
335 | # 添加水印
336 | watermark = QImage(os.path.join(os.path.dirname(__file__), 'media/watermark.png'))
337 | p = QPainter(img)
338 | p.drawImage(QRect( 20, 20, 40, 40), watermark)
339 | p.end()
340 | ba = QByteArray()
341 | buffer = QBuffer(ba)
342 | buffer.open(QIODevice.WriteOnly)
343 | img.save(buffer, "PNG" if "png" in request.parameter("FORMAT") else "JPG")
344 | # 设置数据体
345 | request.clearBody()
346 | request.appendBody(ba)
347 | ```
348 |
349 | 在这个例子中,**SERVICE** 参数值被检查,如果传入的请求是一个 **WMS GETMAP** ,并且没有被先前执行的插件或核心服务(在这个例子中是WMS)设置过异常,那么WMS生成的图像就会从输出缓冲区中被检索出来,并且添加水印图像。最后一步是清除输出缓冲区,用新生成的图像替换它。请注意,在现实世界中,我们还应该检查所要求的图像类型,而不是只支持PNG或JPG。
350 |
351 | #### 20.4.1.5 访问控制过滤器
352 |
353 | 访问控制过滤器为开发者提供了对哪些层、要素和属性可以被访问的细粒度控制,以下回调函数可以在访问控制过滤器中实现:
354 |
355 | - [`layerFilterExpression(layer)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.layerFilterExpression)
356 | - [`layerFilterSubsetString(layer)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.layerFilterSubsetString)
357 | - [`layerPermissions(layer)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.layerPermissions)
358 | - [`authorizedLayerAttributes(layer, attributes)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.authorizedLayerAttributes)
359 | - [`allowToEdit(layer, feature)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.allowToEdit)
360 | - [`cacheKey()`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.cacheKey)
361 |
362 | ##### 20.4.1.5.1. 插件文件
363 |
364 | 下面是我们的示例服务器插件的目录结构:
365 |
366 | ```
367 | PYTHON_PLUGINS_PATH/
368 | MyAccessControl/
369 | __init__.py --> *required*
370 | AccessControl.py --> *required*
371 | metadata.txt --> *required*
372 | ```
373 |
374 | ##### 20.4.1.5.2 _\_init\_\_.py
375 |
376 | 这个文件是Python的导入系统所要求的。对于所有的QGIS服务器插件来说,这个文件包含一个`serverClassFactory()`函数,当插件在启动时被加载到QGIS服务器中时,它将被调用。它接收一个对[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)实例的引用,并必须返回一个你的插件类的实例。以下是插件实例 `__init__.py`的样子:
377 |
378 | ```python
379 | def serverClassFactory(serverIface):
380 | from MyAccessControl.AccessControl import AccessControlServer
381 | return AccessControlServer(serverIface)
382 | ```
383 |
384 | ##### 20.4.1.5.3. AccessControl.py
385 |
386 | ```python
387 | class AccessControlFilter(QgsAccessControlFilter):
388 |
389 | def __init__(self, server_iface):
390 | super().__init__(server_iface)
391 |
392 | def layerFilterExpression(self, layer):
393 | """ 返回一个额外的表达式过滤器 """
394 | return super().layerFilterExpression(layer)
395 |
396 | def layerFilterSubsetString(self, layer):
397 | """ 返回一个额外的子集字符串(通常是SQL)过滤器 """
398 | return super().layerFilterSubsetString(layer)
399 |
400 | def layerPermissions(self, layer):
401 | """ 返回该层的权限 """
402 | return super().layerPermissions(layer)
403 |
404 | def authorizedLayerAttributes(self, layer, attributes):
405 | """ 返回授权的图层属性 """
406 | return super().authorizedLayerAttributes(layer, attributes)
407 |
408 | def allowToEdit(self, layer, feature):
409 | """ 我们是否被授权修改以下几何图形 """
410 | return super().allowToEdit(layer, feature)
411 |
412 | def cacheKey(self):
413 | return super().cacheKey()
414 |
415 | class AccessControlServer:
416 |
417 | def __init__(self, serverIface):
418 | """ 注册访问控制过滤器 """
419 | serverIface.registerAccessControl(AccessControlFilter(serverIface), 100)
420 | ```
421 |
422 | 这个例子为每个人提供了一个完整的访问权限。
423 |
424 | 插件的作用是知道谁在登录。
425 |
426 | 在所有这些方法中,我们都有一个图层的参数,以便能够定制每个图层的限制。
427 |
428 | ##### 20.4.1.5.4. layerFilterExpression
429 |
430 | 用于添加一个表达式来限制结果。
431 |
432 | 例如:限制要素的属性`role`等于`user`。
433 |
434 | ```python
435 | def layerFilterExpression(self, layer):
436 | return "$role = 'user'"
437 | ```
438 |
439 | ##### 20.4.1.5.5. layerFilterSubsetString
440 |
441 | 与前者相同,但使用`SubsetString`(在数据库中执行)。
442 |
443 | ```python
444 | def layerFilterSubsetString(self, layer):
445 | return "role = 'user'"
446 | ```
447 |
448 | 限制要素的属性`role`等于`“user”`。
449 |
450 | ##### 20.4.1.5.6. layerPermissions
451 |
452 | 限制访问图层。
453 |
454 | 返回一个[`LayerPermissions()`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.layerPermissions)对象,它有以下属性:
455 |
456 | - [`canRead`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.LayerPermissions.canRead)可以在`GetCapabilities`中看到它并有读取权限。
457 | - [`canInsert`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.LayerPermissions.canInsert)能够插入一个新的要素。
458 | - [`canUpdate`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.LayerPermissions.canUpdate)能够更新一个要素。
459 | - [`canDelete`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.LayerPermissions.canDelete)能够删除一个要素。
460 |
461 | 例如:
462 |
463 | ```python
464 | def layerPermissions(self, layer):
465 | rights = QgsAccessControlFilter.LayerPermissions()
466 | rights.canRead = True
467 | rights.canInsert = rights.canUpdate = rights.canDelete = False
468 | return rights
469 | ```
470 |
471 | 限制每一个人只读访问。
472 |
473 | ##### 20.4.1.5.7. authorizedLayerAttributes
474 |
475 | 用于限制一个特定的属性子集的可见性。
476 |
477 | 参数 attribute 返回当前的可见属性集。
478 |
479 | 例如:
480 |
481 | ```python
482 | def authorizedLayerAttributes(self, layer, attributes):
483 | return [a for a in attributes if a != "role"]
484 | ```
485 |
486 | 隐藏‘role’属性。
487 |
488 | ##### 20.4.1.5.8. allowToEdit
489 |
490 | 这被用来限制对一个属性子集的编辑。
491 |
492 | 它在`WFS-Transaction`协议中使用。
493 |
494 | 例如:
495 |
496 | ```python
497 | def allowToEdit(self, layer, feature):
498 | return feature.attribute('role') == 'user'
499 | ```
500 |
501 | 只能够编辑具有‘role’属性的要素。
502 |
503 | ##### 20.4.1.5.9. cacheKey
504 |
505 | QGIS服务器会对能力进行缓存,如果要对每个角色进行缓存,你可以在此方法中返回角色。或者返回`None`以完全禁用缓存。
506 |
507 | ### 20.4.2 自定义服务
508 |
509 | 在QGIS服务器中,WMS、WFS和WCS等核心服务是作为[`QgsService`](https://qgis.org/pyqgis/master/server/QgsService.html#qgis.server.QgsService)的子类实现的。
510 |
511 | 为了实现一个新的服务,当查询字符串参数`SERVICE`与服务名称相匹配时将被执行,你可以实现你自己的[`QgsService`](https://qgis.org/pyqgis/master/server/QgsService.html#qgis.server.QgsService),并通过调用[`registerService(service)`](https://qgis.org/pyqgis/master/server/QgsServiceRegistry.html#qgis.server.QgsServiceRegistry.registerService)在[`serviceRegistry()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.serviceRegistry)上注册你的服务。
512 |
513 | 下面是一个名为CUSTOM的自定义服务的例子:
514 |
515 | ```python
516 | from qgis.server import QgsService
517 | from qgis.core import QgsMessageLog
518 |
519 | class CustomServiceService(QgsService):
520 |
521 | def __init__(self):
522 | QgsService.__init__(self)
523 |
524 | def name(self):
525 | return "CUSTOM"
526 |
527 | def version(self):
528 | return "1.0.0"
529 |
530 | def executeRequest(self, request, response, project):
531 | response.setStatusCode(200)
532 | QgsMessageLog.logMessage('Custom service executeRequest')
533 | response.write("Custom service executeRequest")
534 |
535 |
536 | class CustomService():
537 |
538 | def __init__(self, serverIface):
539 | serverIface.serviceRegistry().registerService(CustomServiceService())
540 | ```
541 |
542 | ### 20.4.3. 自定义APIs
543 |
544 | 在QGIS Server中,诸如OAPIF(又称WFS3)等核心OGC APIs被实现为[`QgsServerOgcApiHandler`](https://qgis.org/pyqgis/master/server/QgsServerOgcApiHandler.html#qgis.server.QgsServerOgcApiHandler)子类的集合,这些子类被注册到[`QgsServerOgcApi`](https://qgis.org/pyqgis/master/server/QgsServerOgcApi.html#qgis.server.QgsServerOgcApi)(或其父类[`QgsServerApi`](https://qgis.org/pyqgis/master/server/QgsServerApi.html#qgis.server.QgsServerApi))的实例中。
545 |
546 | 要实现一个新的API,当url路径与某个URL相匹配时就会被执行,你可以实现你自己的[`QgsServerOgcApiHandler`](https://qgis.org/pyqgis/master/server/QgsServerOgcApiHandler.html#qgis.server.QgsServerOgcApiHandler)实例,将它们添加到[`QgsServerOgcApi`](https://qgis.org/pyqgis/master/server/QgsServerOgcApi.html#qgis.server.QgsServerOgcApi)中,并通过调用其[`registerApi(api)`](https://qgis.org/pyqgis/master/server/QgsServiceRegistry.html#qgis.server.QgsServiceRegistry.registerApi)在[`serviceRegistry()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.serviceRegistry)上注册该API。
547 |
548 | 下面是一个自定义API的例子,当URL包含`/customapi`时将被执行:
549 |
550 | ```python
551 | import json
552 | import os
553 |
554 | from qgis.PyQt.QtCore import QBuffer, QIODevice, QTextStream, QRegularExpression
555 | from qgis.server import (
556 | QgsServiceRegistry,
557 | QgsService,
558 | QgsServerFilter,
559 | QgsServerOgcApi,
560 | QgsServerQueryStringParameter,
561 | QgsServerOgcApiHandler,
562 | )
563 |
564 | from qgis.core import (
565 | QgsMessageLog,
566 | QgsJsonExporter,
567 | QgsCircle,
568 | QgsFeature,
569 | QgsPoint,
570 | QgsGeometry,
571 | )
572 |
573 |
574 | class CustomApiHandler(QgsServerOgcApiHandler):
575 |
576 | def __init__(self):
577 | super(CustomApiHandler, self).__init__()
578 | self.setContentTypes([QgsServerOgcApi.HTML, QgsServerOgcApi.JSON])
579 |
580 | def path(self):
581 | return QRegularExpression("/customapi")
582 |
583 | def operationId(self):
584 | return "CustomApiXYCircle"
585 |
586 | def summary(self):
587 | return "Creates a circle around a point"
588 |
589 | def description(self):
590 | return "Creates a circle around a point"
591 |
592 | def linkTitle(self):
593 | return "Custom Api XY Circle"
594 |
595 | def linkType(self):
596 | return QgsServerOgcApi.data
597 |
598 | def handleRequest(self, context):
599 | """Simple Circle"""
600 |
601 | values = self.values(context)
602 | x = values['x']
603 | y = values['y']
604 | r = values['r']
605 | f = QgsFeature()
606 | f.setAttributes([x, y, r])
607 | f.setGeometry(QgsCircle(QgsPoint(x, y), r).toCircularString())
608 | exporter = QgsJsonExporter()
609 | self.write(json.loads(exporter.exportFeature(f)), context)
610 |
611 | def templatePath(self, context):
612 | # 模板路径用于提供HTML内容
613 | return os.path.join(os.path.dirname(__file__), 'circle.html')
614 |
615 | def parameters(self, context):
616 | return [QgsServerQueryStringParameter('x', True, QgsServerQueryStringParameter.Type.Double, 'X coordinate'),
617 | QgsServerQueryStringParameter(
618 | 'y', True, QgsServerQueryStringParameter.Type.Double, 'Y coordinate'),
619 | QgsServerQueryStringParameter('r', True, QgsServerQueryStringParameter.Type.Double, 'radius')]
620 |
621 |
622 | class CustomApi():
623 |
624 | def __init__(self, serverIface):
625 | api = QgsServerOgcApi(serverIface, '/customapi',
626 | 'custom api', 'a custom api', '1.1')
627 | handler = CustomApiHandler()
628 | api.registerHandler(handler)
629 | serverIface.serviceRegistry().registerApi(api)
630 | ```
631 |
632 |
--------------------------------------------------------------------------------