6. استخدام الطبقات المتجهية

تلميح

تحتاج مقتطفات الشيفرة في هذه الصفحة إلى عمليات الاستيراد التالية إذا كنت خارج وحدة تحكم pyqgis:

 1from qgis.core import (
 2  QgsApplication,
 3  QgsDataSourceUri,
 4  QgsCategorizedSymbolRenderer,
 5  QgsClassificationRange,
 6  QgsPointXY,
 7  QgsProject,
 8  QgsExpression,
 9  QgsField,
10  QgsFields,
11  QgsFeature,
12  QgsFeatureRequest,
13  QgsFeatureRenderer,
14  QgsGeometry,
15  QgsGraduatedSymbolRenderer,
16  QgsMarkerSymbol,
17  QgsMessageLog,
18  QgsRectangle,
19  QgsRendererCategory,
20  QgsRendererRange,
21  QgsSymbol,
22  QgsVectorDataProvider,
23  QgsVectorLayer,
24  QgsVectorFileWriter,
25  QgsWkbTypes,
26  QgsSpatialIndex,
27  QgsVectorLayerUtils
28)
29
30from qgis.core.additions.edit import edit
31
32from qgis.PyQt.QtGui import (
33    QColor,
34)

يلخّص هذا القسم إجراءات مختلفة يمكن تنفيذها على الطبقات المتجهية.

يعتمد معظم العمل هنا على دوال الصنف QgsVectorLayer.

6.1. استرجاع المعلومات عن البيانات الوصفية

يمكنك استرجاع معلومات عن الحقول المرتبطة بطبقة متجهية باستدعاء fields() على كائن من نوع QgsVectorLayer:

vlayer = QgsVectorLayer("testdata/data/data.gpkg|layername=airports", "Airports layer", "ogr")
for field in vlayer.fields():
    print(field.name(), field.typeName())
 1fid Integer64
 2id Integer64
 3scalerank Integer64
 4featurecla String
 5type String
 6name String
 7abbrev String
 8location String
 9gps_code String
10iata_code String
11wikipedia String
12natlscale Real

توفر الدالتان displayField() و mapTipTemplate() معلومات عن الحقل والقالب المستخدمين في التبويب خصائص العرض.

عند تحميل طبقة متجهية، يختار QGIS دائمًا حقلًا ليكون Display Name، بينما يكون HTML Map Tip فارغًا افتراضيًا. وباستخدام هاتين الدالتين يمكنك الحصول على كليهما بسهولة:

vlayer = QgsVectorLayer("testdata/data/data.gpkg|layername=airports", "Airports layer", "ogr")
print(vlayer.displayField())
name

ملاحظة

إذا غيّرت Display Name من حقل إلى تعبير، فعليك استخدام displayExpression() بدلًا من displayField().

6.2. التكرار على الطبقة المتجهية

التكرار على معالم طبقة متجهية من أكثر المهام شيوعًا. فيما يلي مثال على شيفرة أساسية بسيطة لتنفيذ هذه المهمة وعرض بعض المعلومات عن كل معلم. ويُفترض أن المتغير layer يحتوي على كائن من نوع QgsVectorLayer.

 1# "layer" is a QgsVectorLayer instance
 2layer = iface.activeLayer()
 3features = layer.getFeatures()
 4
 5for feature in features:
 6    # retrieve every feature with its geometry and attributes
 7    print("Feature ID: ", feature.id())
 8    # fetch geometry
 9    # show some information about the feature geometry
10    geom = feature.geometry()
11    geomSingleType = QgsWkbTypes.isSingleType(geom.wkbType())
12    if geom.type() == QgsWkbTypes.PointGeometry:
13        # the geometry type can be of single or multi type
14        if geomSingleType:
15            x = geom.asPoint()
16            print("Point: ", x)
17        else:
18            x = geom.asMultiPoint()
19            print("MultiPoint: ", x)
20    elif geom.type() == QgsWkbTypes.LineGeometry:
21        if geomSingleType:
22            x = geom.asPolyline()
23            print("Line: ", x, "length: ", geom.length())
24        else:
25            x = geom.asMultiPolyline()
26            print("MultiLine: ", x, "length: ", geom.length())
27    elif geom.type() == QgsWkbTypes.PolygonGeometry:
28        if geomSingleType:
29            x = geom.asPolygon()
30            print("Polygon: ", x, "Area: ", geom.area())
31        else:
32            x = geom.asMultiPolygon()
33            print("MultiPolygon: ", x, "Area: ", geom.area())
34    else:
35        print("Unknown or invalid geometry")
36    # fetch attributes
37    attrs = feature.attributes()
38    # attrs is a list. It contains all the attribute values of this feature
39    print(attrs)
40    # for this test only print the first feature
41    break
Feature ID:  1
Point:  <QgsPointXY: POINT(7 45)>
[1, 'First feature']

6.3. تحديد المعالم

في QGIS لسطح المكتب، يمكن تحديد المعالم بطرق مختلفة: إذ يمكن للمستخدم النقر على معلم، أو رسم مستطيل على لوحة الخريطة، أو استخدام مرشِّح تعبير. وعادةً ما تُبرَز المعالم المحددة بلون مختلف (الأصفر افتراضيًا) للفت انتباه المستخدم إلى التحديد.

قد يكون من المفيد أحيانًا تحديد المعالم برمجيًا أو تغيير اللون الافتراضي.

لتحديد جميع المعالم، يمكن استخدام الدالة selectAll():

# Get the active layer (must be a vector layer)
layer = iface.activeLayer()
layer.selectAll()

للتحديد باستخدام تعبير، استخدم الدالة selectByExpression():

# Assumes that the active layer is points.shp file from the QGIS test suite
# (Class (string) and Heading (number) are attributes in points.shp)
layer = iface.activeLayer()
layer.selectByExpression('"Class"=\'B52\' and "Heading" > 10 and "Heading" <70', QgsVectorLayer.SetSelection)

لتغيير لون التحديد يمكنك استخدام الدالة setSelectionColor() من QgsMapCanvas كما هو موضح في المثال التالي:

iface.mapCanvas().setSelectionColor( QColor("red") )

لإضافة معالم إلى قائمة المعالم المحددة لطبقة معينة، يمكنك استدعاء select() وتمرير قائمة معرّفات المعالم إليها:

1selected_fid = []
2
3# Get the first feature id from the layer
4feature = next(layer.getFeatures())
5if feature:
6    selected_fid.append(feature.id())
7
8# Add that features to the selected list
9layer.select(selected_fid)

لمسح التحديد:

layer.removeSelection()

6.3.1. الوصول إلى البيانات الوصفية

يمكن الإشارة إلى البيانات الوصفية بأسمائها:

print(feature['name'])
First feature

بدلًا من ذلك، يمكن الإشارة إلى البيانات الوصفية بفهرسها. وهذا أسرع قليلًا من استخدام الاسم. على سبيل المثال، للحصول على السمة الثانية:

print(feature[1])
First feature

6.3.2. التكرار على المعالم المحددة

إذا كنت تحتاج إلى المعالم المحددة فقط، فيمكنك استخدام الدالة selectedFeatures() من الطبقة المتجهية:

selection = layer.selectedFeatures()
for feature in selection:
    # do whatever you need with the feature
    pass

6.3.3. التكرار على مجموعة فرعية من المعالم

إذا أردت التكرار على مجموعة فرعية معينة من المعالم في طبقة، مثل تلك الواقعة داخل منطقة معينة، فعليك إضافة كائن من نوع QgsFeatureRequest إلى استدعاء getFeatures(). إليك مثالًا:

1areaOfInterest = QgsRectangle(450290,400520, 450750,400780)
2
3request = QgsFeatureRequest().setFilterRect(areaOfInterest)
4
5for feature in layer.getFeatures(request):
6    # do whatever you need with the feature
7    pass

لأجل السرعة، كثيرًا ما يُجرى التقاطع باستخدام المستطيل المحيط بالمعلم فقط. لكن هناك علامة ExactIntersect تضمن ألّا تُعاد إلا المعالم المتقاطعة فعلًا:

request = QgsFeatureRequest().setFilterRect(areaOfInterest) \
                             .setFlags(QgsFeatureRequest.ExactIntersect)

باستخدام setLimit() يمكنك تحديد عدد المعالم المطلوبة. إليك مثالًا:

request = QgsFeatureRequest()
request.setLimit(2)
for feature in layer.getFeatures(request):
    print(feature)
<qgis._core.QgsFeature object at 0x7f9b78590948>
<qgis._core.QgsFeature object at 0x7faef5881670>

إذا كنت تحتاج إلى مرشِّح قائم على البيانات الوصفية بدلًا من المرشِّح المكاني (أو بالإضافة إليه) كما في الأمثلة أعلاه، فيمكنك بناء كائن من نوع QgsExpression وتمريره إلى مُنشئ QgsFeatureRequest. إليك مثالًا:

# The expression will filter the features where the field "location_name"
# contains the word "Lake" (case insensitive)
exp = QgsExpression('location_name ILIKE \'%Lake%\'')
request = QgsFeatureRequest(exp)

راجع التعبيرات والتصفية وحساب القيم للاطلاع على تفاصيل الصياغة التي يدعمها QgsExpression.

يمكن استخدام الطلب لتحديد البيانات المسترجعة لكل معلم، بحيث يُعيد المُكرِّر جميع المعالم لكنه يُعيد بيانات جزئية لكل منها.

 1# Only return selected fields to increase the "speed" of the request
 2request.setSubsetOfAttributes([0,2])
 3
 4# More user friendly version
 5request.setSubsetOfAttributes(['name','id'],layer.fields())
 6
 7# Don't return geometry objects to increase the "speed" of the request
 8request.setFlags(QgsFeatureRequest.NoGeometry)
 9
10# Fetch only the feature with id 45
11request.setFilterFid(45)
12
13# The options may be chained
14request.setFilterRect(areaOfInterest).setFlags(QgsFeatureRequest.NoGeometry).setFilterFid(45).setSubsetOfAttributes([0,2])

6.4. تعديل الطبقات المتجهية

تدعم معظم مزوّدات البيانات المتجهية تحرير بيانات الطبقة. وأحيانًا لا تدعم إلا مجموعة فرعية من إجراءات التحرير الممكنة. استخدم الدالة capabilities() لمعرفة مجموعة الوظائف المدعومة.

caps = layer.dataProvider().capabilities()
# Check if a particular capability is supported:
if caps & QgsVectorDataProvider.DeleteFeatures:
    print('The layer supports DeleteFeatures')
The layer supports DeleteFeatures

للاطلاع على قائمة بجميع القدرات المتاحة، يُرجى الرجوع إلى API Documentation of QgsVectorDataProvider.

لطباعة الوصف النصي لقدرات الطبقة في قائمة مفصولة بفواصل، يمكنك استخدام capabilitiesString() كما في المثال التالي:

1caps_string = layer.dataProvider().capabilitiesString()
2# Print:
3# 'Add Features, Delete Features, Change Attribute Values, Add Attributes,
4# Delete Attributes, Rename Attributes, Fast Access to Features at ID,
5# Presimplify Geometries, Presimplify Geometries with Validity Check,
6# Transactions, Curved Geometries'

باستخدام أي من الدوال التالية لتحرير الطبقة المتجهية، تُحفظ التغييرات مباشرةً في مخزن البيانات الأساسي (ملف، قاعدة بيانات، إلخ). وإذا كنت تريد إجراء تغييرات مؤقتة فقط، فانتقل إلى القسم التالي الذي يشرح كيفية إجراء التعديلات باستخدام مخزن التحرير المؤقت.

ملاحظة

إذا كنت تعمل داخل QGIS (سواء من وحدة التحكم أو من إضافة)، فقد يكون من الضروري فرض إعادة رسم لوحة الخريطة لرؤية التغييرات التي أجريتها على الشكل الهندسي أو النمط أو البيانات الوصفية:

1# If caching is enabled, a simple canvas refresh might not be sufficient
2# to trigger a redraw and you must clear the cached image for the layer
3if iface.mapCanvas().isCachingEnabled():
4    layer.triggerRepaint()
5else:
6    iface.mapCanvas().refresh()

6.4.1. إضافة معالم

أنشئ بعض نسخ QgsFeature ومرّر قائمة بها إلى الدالة addFeatures() الخاصة بالمزوّد QgsVectorDataProvider. وستُعيد قيمتين: النتيجة (True أو False) وقائمة المعالم المضافة (يُعيِّن مخزن البيانات معرّفاتها).

لإعداد البيانات الوصفية للمعلم، يمكنك إما تهيئة المعلم بتمرير كائن من نوع QgsFields (يمكنك الحصول عليه من الدالة fields() الخاصة بالطبقة المتجهية) أو استدعاء initAttributes() مع تمرير عدد الحقول التي تريد إضافتها.

1if caps & QgsVectorDataProvider.AddFeatures:
2    feat = QgsFeature(layer.fields())
3    feat.setAttributes([0, 'hello'])
4    # Or set a single attribute by key or by index:
5    feat.setAttribute('name', 'hello')
6    feat.setAttribute(0, 'hello')
7    feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(123, 456)))
8    (res, outFeats) = layer.dataProvider().addFeatures([feat])

6.4.2. حذف معالم

لحذف بعض المعالم، يكفي تقديم قائمة بمعرّفاتها.

if caps & QgsVectorDataProvider.DeleteFeatures:
    res = layer.dataProvider().deleteFeatures([5, 10])

6.4.3. تعديل معالم

يمكن إما تغيير الشكل الهندسي للمعلم أو تغيير بعض بياناته الوصفية. يغيّر المثال التالي أولًا قيم البيانات الوصفية ذات الفهرسين 0 و1، ثم يغيّر الشكل الهندسي للمعلم.

1fid = 100   # ID of the feature we will modify
2
3if caps & QgsVectorDataProvider.ChangeAttributeValues:
4    attrs = { 0 : "hello", 1 : 123 }
5    layer.dataProvider().changeAttributeValues({ fid : attrs })
6
7if caps & QgsVectorDataProvider.ChangeGeometries:
8    geom = QgsGeometry.fromPointXY(QgsPointXY(111,222))
9    layer.dataProvider().changeGeometryValues({ fid : geom })

نصيحة

فضِّل الصنف QgsVectorLayerEditUtils لتحرير الأشكال الهندسية فقط

إذا كنت تحتاج إلى تغيير الأشكال الهندسية فقط، فقد ترغب في استخدام QgsVectorLayerEditUtils الذي يوفر بعض الدوال المفيدة لتحرير الأشكال الهندسية (إزاحة رأس أو إدراجه أو نقله، إلخ).

6.4.4. تعديل الطبقات المتجهية باستخدام مخزن التحرير المؤقت

عند تحرير البيانات المتجهية داخل تطبيق QGIS، عليك أولًا بدء وضع التحرير لطبقة معينة، ثم إجراء بعض التعديلات، وأخيرًا حفظ التغييرات (أو التراجع عنها). ولا تُكتب أي تغييرات تجريها حتى تحفظها --- بل تبقى في مخزن التحرير المؤقت للطبقة في الذاكرة. ويمكن استخدام هذه الوظيفة برمجيًا أيضًا --- فهي مجرد طريقة أخرى لتحرير الطبقة المتجهية تكمّل الاستخدام المباشر لمزوّدات البيانات. استخدم هذا الخيار عند توفير أدوات واجهة رسومية لتحرير الطبقة المتجهية، إذ يتيح ذلك للمستخدم أن يقرر الحفظ أو التراجع، ويتيح استخدام التراجع/الإعادة. وعند حفظ التغييرات، تُحفظ جميع التغييرات الموجودة في مخزن التحرير المؤقت في مزوّد البيانات.

الدوال مشابهة لتلك التي رأيناها في المزوّد، لكنها تُستدعى على كائن QgsVectorLayer بدلًا منه.

لكي تعمل هذه الدوال، يجب أن تكون الطبقة في وضع التحرير. لبدء وضع التحرير، استخدم الدالة startEditing(). ولإيقاف التحرير، استخدم الدالة commitChanges() أو rollBack(). فالأولى ستحفظ جميع تغييراتك في مصدر البيانات، بينما ستتجاهلها الثانية ولن تعدّل مصدر البيانات إطلاقًا.

لمعرفة ما إذا كانت الطبقة في وضع التحرير، استخدم الدالة isEditable().

إليك بعض الأمثلة التي توضح كيفية استخدام دوال التحرير هذه.

 1from qgis.PyQt.QtCore import QMetaType
 2
 3feat1 = feat2 = QgsFeature(layer.fields())
 4fid = 99
 5feat1.setId(fid)
 6
 7# add two features (QgsFeature instances)
 8layer.addFeatures([feat1,feat2])
 9# delete a feature with specified ID
10layer.deleteFeature(fid)
11
12# set new geometry (QgsGeometry instance) for a feature
13geometry = QgsGeometry.fromWkt("POINT(7 45)")
14layer.changeGeometry(fid, geometry)
15# update an attribute with given field index (int) to a given value
16fieldIndex =1
17value ='My new name'
18layer.changeAttributeValue(fid, fieldIndex, value)
19
20# add new field
21layer.addAttribute(QgsField("mytext", QMetaType.Type.QString))
22# remove a field
23layer.deleteAttribute(fieldIndex)

لكي يعمل التراجع/الإعادة بشكل صحيح، يجب تغليف الاستدعاءات المذكورة أعلاه في أوامر تراجع. (إذا لم يكن التراجع/الإعادة يهمك وتريد تخزين التغييرات فورًا، فسيكون عملك أسهل عبر التحرير باستخدام مزوّد البيانات.)

إليك كيفية استخدام وظيفة التراجع:

 1layer.beginEditCommand("Feature triangulation")
 2
 3# ... call layer's editing methods ...
 4
 5if problem_occurred:
 6  layer.destroyEditCommand()
 7  # ... tell the user that there was a problem
 8  # and return
 9
10# ... more editing ...
11
12layer.endEditCommand()

ستُنشئ الدالة beginEditCommand() أمرًا داخليًا "نشطًا" وستسجّل التغييرات اللاحقة في الطبقة المتجهية. ومع استدعاء endEditCommand() يُدفع الأمر إلى مكدّس التراجع، وسيتمكن المستخدم من التراجع عنه أو إعادته من الواجهة الرسومية. وفي حال حدوث خطأ أثناء إجراء التغييرات، ستزيل الدالة destroyEditCommand() الأمر وتتراجع عن جميع التغييرات التي أُجريت أثناء نشاط هذا الأمر.

يمكنك أيضًا استخدام العبارة with edit(layer) لتغليف الحفظ والتراجع في كتلة شيفرة أوضح دلالةً كما هو موضح في المثال أدناه:

with edit(layer):
  feat = next(layer.getFeatures())
  feat[0] = 5
  layer.updateFeature(feat)

سيستدعي هذا تلقائيًا commitChanges() في النهاية. وإذا وقع أي استثناء، فسيُنفّذ rollBack() للتراجع عن جميع التغييرات. وفي حال مواجهة مشكلة داخل commitChanges() (عندما تُعيد الدالة False) سيُطلق استثناء QgsEditError.

6.4.5. إضافة الحقول وإزالتها

لإضافة حقول (بيانات وصفية)، عليك تحديد قائمة بتعريفات الحقول. ولحذف الحقول يكفي تقديم قائمة بفهارس الحقول.

1from qgis.PyQt.QtCore import QMetaType
2
3if caps & QgsVectorDataProvider.AddAttributes:
4    res = layer.dataProvider().addAttributes(
5        [QgsField("mytext", QMetaType.Type.QString),
6        QgsField("myint", QMetaType.Type.Int)])
7
8if caps & QgsVectorDataProvider.DeleteAttributes:
9    res = layer.dataProvider().deleteAttributes([0])
 1# Alternate methods for removing fields
 2# first create temporary fields to be removed (f1-3)
 3layer.dataProvider().addAttributes([QgsField("f1", QMetaType.Type.Int),
 4                                    QgsField("f2", QMetaType.Type.Int),
 5                                    QgsField("f3", QMetaType.Type.Int)])
 6layer.updateFields()
 7count=layer.fields().count() # count of layer fields
 8ind_list=list((count-3, count-2)) # create list
 9
10# remove a single field with an index
11layer.dataProvider().deleteAttributes([count-1])
12
13# remove multiple fields with a list of indices
14layer.dataProvider().deleteAttributes(ind_list)

بعد إضافة الحقول أو إزالتها في مزوّد البيانات، يجب تحديث حقول الطبقة لأن التغييرات لا تنتقل تلقائيًا.

layer.updateFields()

نصيحة

احفظ التغييرات مباشرةً باستخدام with أمر قائم عليها

باستخدام with edit(layer): ستُحفظ التغييرات تلقائيًا باستدعاء commitChanges() في النهاية. وإذا وقع أي استثناء، فسيُنفّذ rollBack() للتراجع عن جميع التغييرات. راجع تعديل الطبقات المتجهية باستخدام مخزن التحرير المؤقت.

6.5. استخدام الفهرس المكاني

يمكن للفهارس المكانية أن تحسّن أداء شيفرتك بشكل كبير إذا كنت تحتاج إلى إجراء استعلامات متكررة على طبقة متجهية. تخيّل مثلًا أنك تكتب خوارزمية استيفاء، وأنك تحتاج لموقع معين إلى معرفة أقرب 10 نقاط من طبقة نقاط، لاستخدام تلك النقاط في حساب القيمة المستوفاة. فمن دون فهرس مكاني، الطريقة الوحيدة أمام QGIS للعثور على تلك النقاط العشر هي حساب المسافة من كل نقطة على حدة إلى الموقع المحدد ثم مقارنة تلك المسافات. وقد تستغرق هذه المهمة وقتًا طويلًا جدًا، خاصة إذا كان يجب تكرارها لعدة مواقع. أما إذا وُجد فهرس مكاني للطبقة، فإن العملية تصبح أكثر فعالية بكثير.

تخيّل الطبقة التي لا تملك فهرسًا مكانيًا كدليل هاتف أرقامه غير مرتبة وغير مفهرسة. فالطريقة الوحيدة للعثور على رقم هاتف شخص معين هي القراءة من البداية حتى تعثر عليه.

لا تُنشأ الفهارس المكانية افتراضيًا لطبقة QGIS المتجهية، لكن يمكنك إنشاؤها بسهولة. وهذا ما عليك فعله:

  • أنشئ فهرسًا مكانيًا باستخدام الصنف QgsSpatialIndex:

    index = QgsSpatialIndex()
    
  • أضف المعالم إلى الفهرس --- يأخذ الفهرس كائن QgsFeature ويضيفه إلى بنية البيانات الداخلية. ويمكنك إنشاء الكائن يدويًا أو استخدام كائن ناتج عن استدعاء سابق للدالة getFeatures() الخاصة بالمزوّد.

    index.addFeature(feat)
    
  • بدلًا من ذلك، يمكنك تحميل جميع معالم الطبقة دفعة واحدة باستخدام التحميل المجمَّع

    index = QgsSpatialIndex(layer.getFeatures())
    
  • بعد ملء الفهرس المكاني ببعض القيم، يمكنك إجراء بعض الاستعلامات

    1# returns array of feature IDs of five nearest features
    2nearest = index.nearestNeighbor(QgsPointXY(25.4, 12.7), 5)
    3
    4# returns array of IDs of features which intersect the rectangle
    5intersect = index.intersects(QgsRectangle(22.5, 15.3, 23.1, 17.2))
    

يمكنك أيضًا استخدام الفهرس المكاني QgsSpatialIndexKDBush. وهذا الفهرس مشابه لـ QgsSpatialIndex القياسي لكنه:

  • يدعم فقط المعالم النقطية المفردة

  • ثابت (لا يمكن إضافة معالم أخرى إلى الفهرس بعد إنشائه)

  • أسرع بكثير!

  • يتيح الاسترجاع المباشر لنقاط المعالم الأصلية، دون الحاجة إلى طلبات معالم إضافية

  • يدعم عمليات بحث حقيقية قائمة على المسافة، أي إعادة جميع النقاط الواقعة ضمن نصف قطر معين من نقطة البحث

6.6. الصنف QgsVectorLayerUtils

يحتوي الصنف QgsVectorLayerUtils على بعض الدوال المفيدة جدًا التي يمكنك استخدامها مع الطبقات المتجهية.

على سبيل المثال، تُعِدّ الدالة createFeature() كائن QgsFeature لإضافته إلى طبقة متجهية مع الحفاظ على جميع القيود المحتملة والقيم الافتراضية لكل حقل:

vlayer = QgsVectorLayer("testdata/data/data.gpkg|layername=airports", "Airports layer", "ogr")
feat = QgsVectorLayerUtils.createFeature(vlayer)

تتيح لك الدالة getValues() الحصول بسرعة على قيم حقل أو تعبير:

1vlayer = QgsVectorLayer("testdata/data/data.gpkg|layername=airports", "Airports layer", "ogr")
2# select only the first feature to make the output shorter
3vlayer.selectByIds([1])
4val = QgsVectorLayerUtils.getValues(vlayer, "NAME", selectedOnly=True)
5print(val)
(['Sahnewal'], True)

6.7. إنشاء الطبقات المتجهية

هناك عدة طرق لتوليد مجموعة بيانات لطبقة متجهية:

  • الصنف QgsVectorFileWriter: صنف ملائم لكتابة الملفات المتجهية على القرص، إما باستدعاء ساكن للدالة writeAsVectorFormatV3() التي تحفظ الطبقة المتجهية كاملة، أو بإنشاء نسخة من الصنف وإجراء استدعاءات للدالة الموروثة addFeature(). ويدعم هذا الصنف جميع الصيغ المتجهية التي يدعمها GDAL (مثل GeoPackage وShapefile وGeoJSON وKML وغيرها).

  • الصنف QgsVectorLayer: يُنشئ نسخة من مزوّد بيانات يفسّر المسار (url) المقدَّم لمصدر البيانات للاتصال بالبيانات والوصول إليها. ويمكن استخدامه لإنشاء طبقات مؤقتة قائمة على الذاكرة (memory) والاتصال بمجموعات بيانات GDAL المتجهية (ogr) وقواعد البيانات (postgres و spatialite و mysql و mssql) وغيرها (wfs و gpx و delimitedtext...).

6.7.1. من نسخة من QgsVectorFileWriter

 1# SaveVectorOptions contains many settings for the writer process
 2save_options = QgsVectorFileWriter.SaveVectorOptions()
 3transform_context = QgsProject.instance().transformContext()
 4# Write to a GeoPackage (default)
 5error = QgsVectorFileWriter.writeAsVectorFormatV3(layer,
 6                                                  "testdata/my_new_file.gpkg",
 7                                                  transform_context,
 8                                                  save_options)
 9if error[0] == QgsVectorFileWriter.NoError:
10    print("success!")
11else:
12  print(error)
 1# Write to an ESRI Shapefile format dataset using UTF-8 text encoding
 2save_options = QgsVectorFileWriter.SaveVectorOptions()
 3save_options.driverName = "ESRI Shapefile"
 4save_options.fileEncoding = "UTF-8"
 5transform_context = QgsProject.instance().transformContext()
 6error = QgsVectorFileWriter.writeAsVectorFormatV3(layer,
 7                                                  "testdata/my_new_shapefile",
 8                                                  transform_context,
 9                                                  save_options)
10if error[0] == QgsVectorFileWriter.NoError:
11    print("success again!")
12else:
13  print(error)
 1# Write to an ESRI GDB file
 2save_options = QgsVectorFileWriter.SaveVectorOptions()
 3save_options.driverName = "FileGDB"
 4# if no geometry
 5save_options.overrideGeometryType = QgsWkbTypes.Unknown
 6save_options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
 7save_options.layerName = 'my_new_layer_name'
 8transform_context = QgsProject.instance().transformContext()
 9gdb_path = "testdata/my_example.gdb"
10error = QgsVectorFileWriter.writeAsVectorFormatV3(layer,
11                                                gdb_path,
12                                                transform_context,
13                                                save_options)
14if error[0] == QgsVectorFileWriter.NoError:
15  print("success!")
16else:
17  print(error)

يمكنك أيضًا تحويل الحقول لجعلها متوافقة مع صيغ مختلفة باستخدام FieldValueConverter. على سبيل المثال، لتحويل أنواع المتغيرات المصفوفية (كما في Postgres) إلى نوع نصي، يمكنك القيام بما يلي:

 1LIST_FIELD_NAME = 'xxxx'
 2
 3class ESRIValueConverter(QgsVectorFileWriter.FieldValueConverter):
 4
 5  def __init__(self, layer, list_field):
 6    QgsVectorFileWriter.FieldValueConverter.__init__(self)
 7    self.layer = layer
 8    self.list_field_idx = self.layer.fields().indexFromName(list_field)
 9
10  def convert(self, fieldIdxInLayer, value):
11    if fieldIdxInLayer == self.list_field_idx:
12      return QgsListFieldFormatter().representValue(layer=vlayer,
13                                                    fieldIndex=self.list_field_idx,
14                                                    config={},
15                                                    cache=None,
16                                                    value=value)
17    else:
18      return value
19
20  def fieldDefinition(self, field):
21    idx = self.layer.fields().indexFromName(field.name())
22    if idx == self.list_field_idx:
23      return QgsField(LIST_FIELD_NAME, QMetaType.Type.QString)
24    else:
25      return self.layer.fields()[idx]
26
27converter = ESRIValueConverter(vlayer, LIST_FIELD_NAME)
28opts = QgsVectorFileWriter.SaveVectorOptions()
29opts.fieldValueConverter = converter

يمكن أيضًا تحديد نظام إحداثيات مرجعي وجهة --- فإذا مُرِّرت نسخة صالحة من QgsCoordinateReferenceSystem كمعامل رابع، تُحوَّل الطبقة إلى ذلك النظام.

لمعرفة أسماء برامج التشغيل (drivers) الصالحة، يُرجى استدعاء الدالة supportedFiltersAndFormats() أو الرجوع إلى supported formats by OGR --- ويجب عليك تمرير القيمة الموجودة في عمود "Code" كاسم لبرنامج التشغيل.

يمكنك اختياريًا تحديد ما إذا كنت تريد تصدير المعالم المحددة فقط، أو تمرير خيارات إنشاء إضافية خاصة ببرنامج التشغيل، أو إخبار أداة الكتابة بعدم إنشاء البيانات الوصفية... وهناك عدد من المعاملات (الاختيارية) الأخرى؛ راجع توثيق QgsVectorFileWriter للتفاصيل.

6.7.2. مباشرةً من المعالم

 1from qgis.PyQt.QtCore import QMetaType
 2
 3# define fields for feature attributes. A QgsFields object is needed
 4fields = QgsFields()
 5fields.append(QgsField("first", QMetaType.Type.Int))
 6fields.append(QgsField("second", QMetaType.Type.QString))
 7
 8""" create an instance of vector file writer, which will create the vector file.
 9Arguments:
101. path to new file (will fail if exists already)
112. field map
123. geometry type - from WKBTYPE enum
134. layer's spatial reference (instance of
14   QgsCoordinateReferenceSystem)
155. coordinate transform context
166. save options (driver name for the output file, encoding etc.)
17"""
18
19crs = QgsProject.instance().crs()
20transform_context = QgsProject.instance().transformContext()
21save_options = QgsVectorFileWriter.SaveVectorOptions()
22save_options.driverName = "ESRI Shapefile"
23save_options.fileEncoding = "UTF-8"
24
25writer = QgsVectorFileWriter.create(
26  "testdata/my_new_shapefile.shp",
27  fields,
28  QgsWkbTypes.Point,
29  crs,
30  transform_context,
31  save_options
32)
33
34if writer.hasError() != QgsVectorFileWriter.NoError:
35    print("Error when creating shapefile: ",  writer.errorMessage())
36
37# add a feature
38fet = QgsFeature()
39
40fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10,10)))
41fet.setAttributes([1, "text"])
42writer.addFeature(fet)
43
44# delete the writer to flush features to disk
45del writer

6.7.3. من نسخة من QgsVectorLayer

من بين جميع مزوّدات البيانات التي يدعمها الصنف QgsVectorLayer، لنركّز على الطبقات القائمة على الذاكرة. فمزوّد الذاكرة مخصص للاستخدام أساسًا من قِبل مطوّري الإضافات أو تطبيقات الجهات الخارجية. وهو لا يخزّن البيانات على القرص، مما يتيح للمطورين استخدامه كواجهة خلفية سريعة لبعض الطبقات المؤقتة.

يدعم المزوّد الحقول من الأنواع string وint وdouble.

يدعم مزوّد الذاكرة أيضًا الفهرسة المكانية، التي تُفعَّل باستدعاء الدالة createSpatialIndex() الخاصة بالمزوّد. وبعد إنشاء الفهرس المكاني ستتمكن من التكرار على المعالم داخل مناطق أصغر بشكل أسرع (إذ لا يلزم المرور على جميع المعالم، بل على تلك الواقعة في المستطيل المحدد فقط).

يُنشأ مزوّد الذاكرة بتمرير "memory" كسلسلة المزوّد إلى مُنشئ QgsVectorLayer.

يأخذ المُنشئ أيضًا عنوان URI يحدد نوع الشكل الهندسي للطبقة، وهو أحد ما يلي: "Point" أو "LineString" أو "Polygon" أو "MultiPoint" أو "MultiLineString" أو "MultiPolygon" أو "None".

يمكن لعنوان URI أيضًا أن يحدد نظام الإحداثيات المرجعي والحقول وفهرسة مزوّد الذاكرة. والصيغة هي:

crs=definition

يحدد نظام الإحداثيات المرجعي، حيث يمكن أن يكون definition بأي من الصيغ التي تقبلها QgsCoordinateReferenceSystem.createFromString()

index=yes

يحدد أن المزوّد سيستخدم فهرسًا مكانيًا

field=name:type(length,precision) (أي: الاسم:النوع(الطول،الدقة))

يحدد سمة من سمات الطبقة. للسمة اسم، واختياريًا نوع (integer أو double أو string) وطول ودقة. ويمكن أن تكون هناك تعريفات متعددة للحقول.

يتضمن المثال التالي لعنوان URI جميع هذه الخيارات

"Point?crs=epsg:4326&field=id:integer&field=name:string(20)&index=yes"

توضّح الشيفرة المثال التالية إنشاء مزوّد ذاكرة وملأه بالبيانات

 1from qgis.PyQt.QtCore import QDateTime, QMetaType, Qt
 2
 3# create layer
 4vl = QgsVectorLayer("Point", "temporary_points", "memory")
 5pr = vl.dataProvider()
 6
 7# add fields
 8pr.addAttributes([QgsField("name", QMetaType.Type.QString),
 9                    QgsField("age",  QMetaType.Type.Int),
10                    QgsField("size", QMetaType.Type.Double),
11                    QgsField("birthday", QMetaType.Type.QDateTime)])
12vl.updateFields() # tell the vector layer to fetch changes from the provider
13
14# add a feature
15fet = QgsFeature()
16fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10,10)))
17fmt = Qt.DateFormat.ISODate
18t = QDateTime.fromString("2000-01-01T12:00:00", fmt)
19fet.setAttributes(["Johnny", 2, 0.3, t])
20pr.addFeatures([fet])
21
22# update layer's extent when new features have been added
23# because change of extent in provider is not propagated to the layer
24vl.updateExtents()

أخيرًا، لنتحقق مما إذا كان كل شيء قد سار على ما يرام

 1# show some stats
 2print("fields:", len(pr.fields()))
 3print("features:", pr.featureCount())
 4e = vl.extent()
 5print("extent:", e.xMinimum(), e.yMinimum(), e.xMaximum(), e.yMaximum())
 6
 7# iterate over features
 8features = vl.getFeatures()
 9for fet in features:
10    print("F:", fet.id(), fet.attributes(), fet.geometry().asPoint())
fields: 4
features: 1
extent: 10.0 10.0 10.0 10.0
F: 1 ['Johnny', 2, 0.3, PyQt5.QtCore.QDateTime(2000, 1, 1, 12, 0)] <QgsPointXY: POINT(10 10)>

6.8. مظهر الطبقات المتجهية (الترميز)

عند رسم طبقة متجهية، يتحدد مظهر البيانات من خلال أداة الرسم و الرموز المرتبطة بالطبقة. والرموز أصناف تتولى رسم التمثيل المرئي للمعالم، بينما تحدد أدوات الرسم الرمز الذي سيُستخدم لمعلم معين.

يمكن الحصول على أداة الرسم لطبقة معينة كما هو موضح أدناه:

renderer = layer.renderer()

وباستخدام هذا المرجع، دعونا نستكشفها قليلًا

print("Type:", renderer.type())
Type: singleSymbol

هناك عدة أنواع معروفة من أدوات الرسم متاحة في مكتبة QGIS الأساسية:

النوع

الصنف

وصف

singleSymbol

QgsSingleSymbolRenderer

يرسم جميع المعالم بالرمز نفسه

categorizedSymbol

QgsCategorizedSymbolRenderer

يرسم المعالم باستخدام رمز مختلف لكل فئة

graduatedSymbol

QgsGraduatedSymbolRenderer

يرسم المعالم باستخدام رمز مختلف لكل نطاق من القيم

قد توجد أيضًا بعض أنواع أدوات الرسم المخصصة، لذا لا تفترض أبدًا أن هذه الأنواع هي الوحيدة. يمكنك الاستعلام من QgsRendererRegistry الخاص بالتطبيق لمعرفة أدوات الرسم المتاحة حاليًا:

print(QgsApplication.rendererRegistry().renderersList())
['nullSymbol', 'singleSymbol', 'categorizedSymbol', 'graduatedSymbol', 'RuleRenderer', 'pointDisplacement', 'pointCluster', 'mergedFeatureRenderer', 'invertedPolygonRenderer', 'heatmapRenderer', '25dRenderer', 'embeddedSymbol']

يمكن الحصول على تفريغ لمحتويات أداة الرسم في صورة نصية --- وقد يكون ذلك مفيدًا لتصحيح الأخطاء

renderer.dump()
SINGLE: MARKER SYMBOL (1 layers) color 190,207,80,255

6.8.1. أداة رسم الرمز المفرد

يمكنك الحصول على الرمز المستخدم في الرسم باستدعاء الدالة symbol() وتغييره باستخدام الدالة setSymbol() (ملاحظة لمطوّري C++: تتولى أداة الرسم ملكية الرمز.)

يمكنك تغيير الرمز المستخدم في طبقة متجهية معينة باستدعاء setSymbol() مع تمرير نسخة من الرمز المناسب. ويمكن إنشاء رموز طبقات النقاط و الخطوط و المضلعات باستدعاء الدالة createSimple() من الأصناف المقابلة QgsMarkerSymbol و QgsLineSymbol و QgsFillSymbol.

يحدد القاموس المُمرَّر إلى createSimple() خصائص نمط الرمز.

على سبيل المثال، يمكنك استبدال الرمز المستخدم في طبقة نقاط معينة باستدعاء setSymbol() مع تمرير نسخة من QgsMarkerSymbol، كما في مثال الشيفرة التالي:

symbol = QgsMarkerSymbol.createSimple({'name': 'square', 'color': 'red'})
layer.renderer().setSymbol(symbol)
# show the change
layer.triggerRepaint()

يشير name إلى شكل العلامة، ويمكن أن يكون أيًّا مما يلي:

  • circle

  • square

  • cross

  • rectangle

  • diamond

  • pentagon

  • triangle

  • equilateral_triangle

  • star

  • regular_star

  • arrow

  • filled_arrowhead

  • x

للحصول على القائمة الكاملة لخصائص طبقة الرمز الأولى لنسخة رمز ما، يمكنك اتباع الشيفرة المثال:

print(layer.renderer().symbol().symbolLayers()[0].properties())
{'angle': '0', 'cap_style': 'square', 'color': '255,0,0,255,rgb:1,0,0,1', 'horizontal_anchor_point': '1', 'joinstyle': 'bevel', 'name': 'square', 'offset': '0,0', 'offset_map_unit_scale': '3x:0,0,0,0,0,0', 'offset_unit': 'MM', 'outline_color': '35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1', 'outline_style': 'solid', 'outline_width': '0', 'outline_width_map_unit_scale': '3x:0,0,0,0,0,0', 'outline_width_unit': 'MM', 'scale_method': 'diameter', 'size': '2', 'size_map_unit_scale': '3x:0,0,0,0,0,0', 'size_unit': 'MM', 'vertical_anchor_point': '1'}

قد يكون هذا مفيدًا إذا أردت تعديل بعض الخصائص:

 1# You can alter a single property...
 2layer.renderer().symbol().symbolLayer(0).setSize(3)
 3# ... but not all properties are accessible from methods,
 4# you can also replace the symbol completely:
 5props = layer.renderer().symbol().symbolLayer(0).properties()
 6props['color'] = 'yellow'
 7props['name'] = 'square'
 8layer.renderer().setSymbol(QgsMarkerSymbol.createSimple(props))
 9# show the changes
10layer.triggerRepaint()

6.8.2. أداة رسم الرموز المصنَّفة

عند استخدام أداة رسم مصنَّفة، يمكنك الاستعلام عن السمة المستخدمة في التصنيف وتعيينها: استخدم الدالتين classAttribute() و setClassAttribute().

للحصول على قائمة بالفئات

1categorized_renderer = QgsCategorizedSymbolRenderer()
2# Add a few categories
3cat1 = QgsRendererCategory('1', QgsMarkerSymbol(), 'category 1')
4cat2 = QgsRendererCategory('2', QgsMarkerSymbol(), 'category 2')
5categorized_renderer.addCategory(cat1)
6categorized_renderer.addCategory(cat2)
7
8for cat in categorized_renderer.categories():
9    print("{}: {} :: {}".format(cat.value(), cat.label(), cat.symbol()))
1: category 1 :: <qgis._core.QgsMarkerSymbol object at 0x7f378ffcd9d8>
2: category 2 :: <qgis._core.QgsMarkerSymbol object at 0x7f378ffcd9d8>

حيث value() هي القيمة المستخدمة للتمييز بين الفئات، و label() نص يُستخدم لوصف الفئة، وتُعيد الدالة symbol() الرمز المُسنَد.

تخزّن أداة الرسم عادةً أيضًا الرمز الأصلي وتدرّج الألوان اللذين استُخدما في التصنيف: الدالتان sourceColorRamp() و sourceSymbol().

6.8.3. أداة رسم الرموز المتدرجة

تشبه أداة الرسم هذه كثيرًا أداة رسم الرموز المصنَّفة الموضحة أعلاه، لكنها بدلًا من قيمة سمة واحدة لكل فئة تعمل مع نطاقات من القيم، ولذلك لا يمكن استخدامها إلا مع البيانات الوصفية العددية.

لمعرفة المزيد عن النطاقات المستخدمة في أداة الرسم

 1graduated_renderer = QgsGraduatedSymbolRenderer()
 2# Add a few categories
 3graduated_renderer.addClassRange(QgsRendererRange(QgsClassificationRange('class 0-100', 0, 100), QgsMarkerSymbol()))
 4graduated_renderer.addClassRange(QgsRendererRange(QgsClassificationRange('class 101-200', 101, 200), QgsMarkerSymbol()))
 5
 6for ran in graduated_renderer.ranges():
 7    print("{} - {}: {} {}".format(
 8        ran.lowerValue(),
 9        ran.upperValue(),
10        ran.label(),
11        ran.symbol()
12      ))
0.0 - 100.0: class 0-100 <qgis._core.QgsMarkerSymbol object at 0x7f8bad281b88>
101.0 - 200.0: class 101-200 <qgis._core.QgsMarkerSymbol object at 0x7f8bad281b88>

يمكنك مرة أخرى استخدام الدوال classAttribute() (لمعرفة اسم سمة التصنيف) و sourceSymbol() و sourceColorRamp(). وإضافة إلى ذلك، هناك الدالة mode() التي تحدد كيفية إنشاء النطاقات: باستخدام فترات متساوية أو مُكمّمات (quantiles) أو طريقة أخرى.

إذا أردت إنشاء أداة رسم رموز متدرجة خاصة بك، فيمكنك فعل ذلك كما هو موضح في المقتطف المثال أدناه (الذي يُنشئ ترتيبًا بسيطًا من فئتين)

 1from qgis.PyQt import QtGui
 2
 3myVectorLayer = QgsVectorLayer("testdata/data/data.gpkg|layername=airports", "Airports layer", "ogr")
 4myTargetField = 'scalerank'
 5myRangeList = []
 6myOpacity = 1
 7# Make our first symbol and range...
 8myMin = 0.0
 9myMax = 50.0
10myLabel = 'Group 1'
11myColour = QtGui.QColor('#ffee00')
12mySymbol1 = QgsSymbol.defaultSymbol(myVectorLayer.geometryType())
13mySymbol1.setColor(myColour)
14mySymbol1.setOpacity(myOpacity)
15myRange1 = QgsRendererRange(myMin, myMax, mySymbol1, myLabel)
16myRangeList.append(myRange1)
17#now make another symbol and range...
18myMin = 50.1
19myMax = 100
20myLabel = 'Group 2'
21myColour = QtGui.QColor('#00eeff')
22mySymbol2 = QgsSymbol.defaultSymbol(
23     myVectorLayer.geometryType())
24mySymbol2.setColor(myColour)
25mySymbol2.setOpacity(myOpacity)
26myRange2 = QgsRendererRange(myMin, myMax, mySymbol2, myLabel)
27myRangeList.append(myRange2)
28myRenderer = QgsGraduatedSymbolRenderer('', myRangeList)
29myClassificationMethod = QgsApplication.classificationMethodRegistry().method("EqualInterval")
30myRenderer.setClassificationMethod(myClassificationMethod)
31myRenderer.setClassAttribute(myTargetField)
32
33myVectorLayer.setRenderer(myRenderer)

6.8.4. العمل مع الرموز

لتمثيل الرموز، هناك الصنف الأساسي QgsSymbol مع ثلاثة أصناف مشتقة:

  • QgsMarkerSymbol --- للمعالم النقطية

  • QgsLineSymbol --- للمعالم الخطية

  • QgsFillSymbol --- للمعالم المضلعة

يتكون كل رمز من طبقة رمز واحدة أو أكثر (أصناف مشتقة من QgsSymbolLayer). وطبقات الرمز هي التي تتولى الرسم الفعلي، أما صنف الرمز نفسه فيعمل فقط كحاوية لطبقات الرمز.

عندما تكون لديك نسخة من رمز (من أداة رسم مثلًا)، يمكنك استكشافها: إذ تخبرك الدالة type() ما إذا كان رمز علامة أو خط أو تعبئة. وهناك الدالة dump() التي تُعيد وصفًا موجزًا للرمز. وللحصول على قائمة بطبقات الرمز:

marker_symbol = QgsMarkerSymbol()
for i in range(marker_symbol.symbolLayerCount()):
    lyr = marker_symbol.symbolLayer(i)
    print("{}: {}".format(i, lyr.layerType()))
0: SimpleMarker

لمعرفة لون الرمز استخدم الدالة color()، واستخدم setColor() لتغيير لونه. ومع رموز العلامات يمكنك أيضًا الاستعلام عن حجم الرمز ودورانه باستخدام الدالتين size() و angle(). أما رموز الخطوط فتُعيد فيها الدالة width() عرض الخط.

الحجم والعرض بالمليمترات افتراضيًا، والزوايا بالدرجات.

6.8.4.1. العمل مع طبقات الرموز

كما ذُكر سابقًا، تحدد طبقات الرموز (الأصناف الفرعية من QgsSymbolLayer) مظهر المعالم. وهناك عدة أصناف أساسية لطبقات الرموز للاستخدام العام. ومن الممكن تنفيذ أنواع جديدة من طبقات الرموز وبالتالي تخصيص طريقة رسم المعالم كما تشاء. وتحدد الدالة layerType() صنف طبقة الرمز تحديدًا فريدًا --- والأنواع الأساسية والافتراضية هي أنواع طبقات الرموز SimpleMarker و SimpleLine و SimpleFill.

يمكنك الحصول على قائمة كاملة بأنواع طبقات الرموز التي يمكنك إنشاؤها لصنف معين من طبقات الرموز باستخدام الشيفرة التالية:

1from qgis.core import QgsSymbolLayerRegistry
2myRegistry = QgsApplication.symbolLayerRegistry()
3myMetadata = myRegistry.symbolLayerMetadata("SimpleFill")
4for item in myRegistry.symbolLayersForType(QgsSymbol.Marker):
5    print(item)
 1AnimatedMarker
 2EllipseMarker
 3FilledMarker
 4FontMarker
 5GeometryGenerator
 6MaskMarker
 7RasterMarker
 8SimpleMarker
 9SvgMarker
10VectorField

يدير الصنف QgsSymbolLayerRegistry قاعدة بيانات لجميع أنواع طبقات الرموز المتاحة.

للوصول إلى بيانات طبقة الرمز، استخدم دالتها properties() التي تُعيد قاموسًا من أزواج المفتاح والقيمة للخصائص التي تحدد المظهر. ولكل نوع من طبقات الرموز مجموعة محددة من الخصائص التي يستخدمها. وإضافة إلى ذلك، هناك الدوال العامة color() و size() و angle() و width()، مع نظيراتها الخاصة بالتعيين. وبالطبع لا يتوفر الحجم والزاوية إلا لطبقات رموز العلامات، ولا يتوفر العرض إلا لطبقات رموز الخطوط.

6.8.4.2. إنشاء أنواع مخصصة من طبقات الرموز

تخيّل أنك تريد تخصيص طريقة رسم البيانات. يمكنك إنشاء صنف طبقة رمز خاص بك يرسم المعالم تمامًا كما تريد. إليك مثالًا على علامة ترسم دوائر حمراء بنصف قطر محدد

 1from qgis.core import QgsMarkerSymbolLayer
 2from qgis.PyQt.QtGui import QColor
 3
 4class FooSymbolLayer(QgsMarkerSymbolLayer):
 5
 6  def __init__(self, radius=4.0):
 7      QgsMarkerSymbolLayer.__init__(self)
 8      self.radius = radius
 9      self.color = QColor(255,0,0)
10
11  def layerType(self):
12     return "FooMarker"
13
14  def properties(self):
15      return { "radius" : str(self.radius) }
16
17  def startRender(self, context):
18    pass
19
20  def stopRender(self, context):
21      pass
22
23  def renderPoint(self, point, context):
24      # Rendering depends on whether the symbol is selected (QGIS >= 1.5)
25      color = context.selectionColor() if context.selected() else self.color
26      p = context.renderContext().painter()
27      p.setPen(color)
28      p.drawEllipse(point, self.radius, self.radius)
29
30  def clone(self):
31      return FooSymbolLayer(self.radius)

تحدد الدالة layerType() اسم طبقة الرمز؛ ويجب أن يكون فريدًا بين جميع طبقات الرموز. وتُستخدم الدالة properties() لحفظ الخصائص. ويجب أن تُعيد الدالة clone() نسخة من طبقة الرمز بجميع الخصائص مطابقة تمامًا. وأخيرًا هناك دوال الرسم: تُستدعى startRender() قبل رسم المعلم الأول، و stopRender() عند انتهاء الرسم، وتُستدعى renderPoint() لتنفيذ الرسم. وتكون إحداثيات النقطة (أو النقاط) قد حُوِّلت مسبقًا إلى إحداثيات المخرجات.

بالنسبة للخطوط المتعددة والمضلعات، يكمن الفرق الوحيد في دالة الرسم: إذ ستستخدم renderPolyline() التي تتلقى قائمة بالخطوط، بينما تتلقى renderPolygon() قائمة بنقاط الحلقة الخارجية كمعامل أول وقائمة بالحلقات الداخلية (أو None) كمعامل ثانٍ.

عادةً ما يكون من الملائم إضافة واجهة رسومية لتعيين خصائص نوع طبقة الرمز لتمكين المستخدمين من تخصيص المظهر: ففي مثالنا أعلاه يمكننا السماح للمستخدم بتعيين نصف قطر الدائرة. تنفّذ الشيفرة التالية عنصر واجهة كهذا

 1from qgis.gui import QgsSymbolLayerWidget
 2
 3class FooSymbolLayerWidget(QgsSymbolLayerWidget):
 4    def __init__(self, parent=None):
 5        QgsSymbolLayerWidget.__init__(self, parent)
 6
 7        self.layer = None
 8
 9        # setup a simple UI
10        self.label = QLabel("Radius:")
11        self.spinRadius = QDoubleSpinBox()
12        self.hbox = QHBoxLayout()
13        self.hbox.addWidget(self.label)
14        self.hbox.addWidget(self.spinRadius)
15        self.setLayout(self.hbox)
16        self.connect(self.spinRadius, SIGNAL("valueChanged(double)"), \
17            self.radiusChanged)
18
19    def setSymbolLayer(self, layer):
20        if layer.layerType() != "FooMarker":
21            return
22        self.layer = layer
23        self.spinRadius.setValue(layer.radius)
24
25    def symbolLayer(self):
26        return self.layer
27
28    def radiusChanged(self, value):
29        self.layer.radius = value
30        self.emit(SIGNAL("changed()"))

يمكن تضمين عنصر الواجهة هذا في نافذة حوار خصائص الرمز. فعند تحديد نوع طبقة الرمز في نافذة حوار خصائص الرمز، تُنشئ النافذة نسخة من طبقة الرمز ونسخة من عنصر واجهة طبقة الرمز. ثم تستدعي الدالة setSymbolLayer() لإسناد طبقة الرمز إلى عنصر الواجهة. وفي تلك الدالة يجب أن يحدّث عنصر الواجهة واجهة المستخدم لتعكس خصائص طبقة الرمز. وتستخدم نافذة الخصائص الدالة symbolLayer() لاسترجاع طبقة الرمز مجددًا لاستخدامها في الرمز.

مع كل تغيير في الخصائص، يجب أن يُطلق عنصر الواجهة الإشارة changed() لتتمكن نافذة الخصائص من تحديث معاينة الرمز.

لا ينقصنا الآن إلا الرابط الأخير: جعل QGIS يتعرف على هذه الأصناف الجديدة. ويتم ذلك بإضافة طبقة الرمز إلى السجل. ومن الممكن استخدام طبقة الرمز دون إضافتها إلى السجل، لكن بعض الوظائف لن تعمل: مثل تحميل ملفات المشاريع التي تحتوي على طبقات الرموز المخصصة، أو عدم القدرة على تحرير خصائص الطبقة في الواجهة الرسومية.

سيتعين علينا إنشاء بيانات وصفية (metadata) لطبقة الرمز

 1from qgis.core import QgsSymbol, QgsSymbolLayerAbstractMetadata, QgsSymbolLayerRegistry
 2
 3class FooSymbolLayerMetadata(QgsSymbolLayerAbstractMetadata):
 4
 5  def __init__(self):
 6    super().__init__("FooMarker", "My new Foo marker", QgsSymbol.Marker)
 7
 8  def createSymbolLayer(self, props):
 9    radius = float(props["radius"]) if "radius" in props else 4.0
10    return FooSymbolLayer(radius)
11
12fslmetadata = FooSymbolLayerMetadata()
QgsApplication.symbolLayerRegistry().addSymbolLayerType(fslmetadata)

يجب عليك تمرير نوع الطبقة (نفسه الذي تُعيده الطبقة) ونوع الرمز (marker/line/fill) إلى مُنشئ الصنف الأب. وتتولى الدالة createSymbolLayer() إنشاء نسخة من طبقة الرمز بالخصائص المحددة في القاموس props. وهناك الدالة createSymbolLayerWidget() التي تُعيد عنصر واجهة الإعدادات لهذا النوع من طبقات الرموز.

الخطوة الأخيرة هي إضافة طبقة الرمز هذه إلى السجل --- وبذلك ننتهي.

6.8.5. إنشاء أدوات رسم مخصصة

قد يكون من المفيد إنشاء تنفيذ جديد لأداة رسم إذا أردت تخصيص قواعد اختيار الرموز لرسم المعالم. ومن حالات الاستخدام التي قد ترغب فيها في ذلك: تحديد الرمز من مجموعة من الحقول، أو تغيّر حجم الرموز بحسب مقياس الرسم الحالي، إلخ.

توضّح الشيفرة التالية أداة رسم مخصصة بسيطة تُنشئ رمزَي علامة وتختار أحدهما عشوائيًا لكل معلم

 1import random
 2from qgis.core import QgsWkbTypes, QgsSymbol, QgsFeatureRenderer
 3
 4
 5class RandomRenderer(QgsFeatureRenderer):
 6  def __init__(self, syms=None):
 7    super().__init__("RandomRenderer")
 8    self.syms = syms if syms else [
 9      QgsSymbol.defaultSymbol(QgsWkbTypes.geometryType(QgsWkbTypes.Point)),
10      QgsSymbol.defaultSymbol(QgsWkbTypes.geometryType(QgsWkbTypes.Point))
11    ]
12
13  def symbolForFeature(self, feature, context):
14    return random.choice(self.syms)
15
16  def startRender(self, context, fields):
17    super().startRender(context, fields)
18    for s in self.syms:
19      s.startRender(context, fields)
20
21  def stopRender(self, context):
22    super().stopRender(context)
23    for s in self.syms:
24      s.stopRender(context)
25
26  def usedAttributes(self, context):
27    return []
28
29  def clone(self):
30    return RandomRenderer(self.syms)

يحتاج مُنشئ الصنف الأب QgsFeatureRenderer إلى اسم لأداة الرسم (يجب أن يكون فريدًا بين أدوات الرسم). والدالة symbolForFeature() هي التي تقرر الرمز الذي سيُستخدم لمعلم معين. وتتولى startRender() و stopRender() تهيئة رسم الرموز وإنهاءه. ويمكن للدالة usedAttributes() أن تُعيد قائمة بأسماء الحقول التي تتوقع أداة الرسم وجودها. وأخيرًا، يجب أن تُعيد الدالة clone() نسخة من أداة الرسم.

كما هو الحال مع طبقات الرموز، يمكن إرفاق واجهة رسومية لضبط أداة الرسم. ويجب أن تكون مشتقة من QgsRendererWidget. تُنشئ الشيفرة المثال التالية زرًا يتيح للمستخدم تعيين الرمز الأول

 1from qgis.gui import QgsRendererWidget, QgsColorButton
 2
 3
 4class RandomRendererWidget(QgsRendererWidget):
 5  def __init__(self, layer, style, renderer):
 6    super().__init__(layer, style)
 7    if renderer is None or renderer.type() != "RandomRenderer":
 8      self.r = RandomRenderer()
 9    else:
10      self.r = renderer
11    # setup UI
12    self.btn1 = QgsColorButton()
13    self.btn1.setColor(self.r.syms[0].color())
14    self.vbox = QVBoxLayout()
15    self.vbox.addWidget(self.btn1)
16    self.setLayout(self.vbox)
17    self.btn1.colorChanged.connect(self.setColor1)
18
19  def setColor1(self):
20    color = self.btn1.color()
21    if not color.isValid(): return
22    self.r.syms[0].setColor(color)
23
24  def renderer(self):
25    return self.r

يتلقى المُنشئ نسخًا من الطبقة النشطة (QgsVectorLayer) والنمط العام (QgsStyle) وأداة الرسم الحالية. فإذا لم تكن هناك أداة رسم أو كانت أداة الرسم من نوع مختلف، فستُستبدل بأداة الرسم الجديدة الخاصة بنا، وإلا فسنستخدم أداة الرسم الحالية (التي هي من النوع الذي نحتاجه أصلًا). ويجب تحديث محتويات عنصر الواجهة لإظهار الحالة الحالية لأداة الرسم. وعند قبول نافذة حوار أداة الرسم، تُستدعى الدالة renderer() الخاصة بعنصر الواجهة للحصول على أداة الرسم الحالية --- وستُسنَد إلى الطبقة.

الجزء الأخير الناقص هو البيانات الوصفية (metadata) لأداة الرسم وتسجيلها في السجل، وإلا فلن يعمل تحميل الطبقات التي تستخدم أداة الرسم ولن يتمكن المستخدم من اختيارها من قائمة أدوات الرسم. لننهِ مثال RandomRenderer الخاص بنا

 1from qgis.core import (
 2  QgsRendererAbstractMetadata,
 3  QgsRendererRegistry,
 4  QgsApplication
 5)
 6
 7class RandomRendererMetadata(QgsRendererAbstractMetadata):
 8
 9  def __init__(self):
10    super().__init__("RandomRenderer", "Random renderer")
11
12  def createRenderer(self, element):
13    return RandomRenderer()
14
15  def createRendererWidget(self, layer, style, renderer):
16    return RandomRendererWidget(layer, style, renderer)
17
18rrmetadata = RandomRendererMetadata()
QgsApplication.rendererRegistry().addRenderer(rrmetadata)

وكما هو الحال مع طبقات الرموز، ينتظر مُنشئ البيانات الوصفية المجردة اسم أداة الرسم، والاسم الظاهر للمستخدمين، واختياريًا اسم أيقونة أداة الرسم. وتمرّر الدالة createRenderer() نسخة QDomElement يمكن استخدامها لاستعادة حالة أداة الرسم من شجرة DOM. وتُنشئ الدالة createRendererWidget() عنصر واجهة الضبط. وليس من الضروري وجودها، أو يمكنها إعادة None إذا لم تكن أداة الرسم مزوّدة بواجهة رسومية.

لربط أيقونة بأداة الرسم، يمكنك إسنادها في مُنشئ QgsRendererAbstractMetadata كوسيط ثالث (اختياري) --- فيصبح مُنشئ الصنف الأساسي في الدالة __init__() الخاصة بـ RandomRendererMetadata

QgsRendererAbstractMetadata.__init__(self,
       "RandomRenderer",
       "Random renderer",
       QIcon(QPixmap("RandomRendererIcon.png", "png")))

يمكن أيضًا ربط الأيقونة في أي وقت لاحق باستخدام الدالة setIcon() من صنف البيانات الوصفية. ويمكن تحميل الأيقونة من ملف (كما هو موضح أعلاه) أو من مورد Qt (تتضمن PyQt5 مترجمًا لملفات .qrc للغة Python).

6.9. موضوعات إضافية

مهام مستقبلية (TODO):

  • إنشاء الرموز وتعديلها

  • العمل مع الأنماط (QgsStyle)

  • العمل مع تدرّجات الألوان (QgsColorRamp)

  • استكشاف سجلات طبقات الرموز وأدوات الرسم