8

I have QGIS 3.22.15 and I have a point layer with three fields: "FID", "UNIT", "PARENT". Each "UNIT" is associated with the "PARENT" field, thus creating an organization tree.

make_line($geometry, geometry(get_feature('ORGANISATION', 'fid', Parent )))

feature table

How is it possible to create an organization tree from the point layer similar to the Layer Panel Grouping in such a way that when clicking on the points of different levels, they also appear/disappear from the map?

The hierarchy must be generated automatically when I add or change points on the layer.

enter image description here

The sample database is here organization tree database

9
  • 5
    The main reason why you have not received an answer so far may be that your question is unclear. I personally don't understand what you are trying to do. How is each UNIT associated with PARENT? How does the attribute table look like? How did you get those red lines (What is the criteria for drawing the lines)? What do you mean by "different levels"? I have more questions about your question. Feb 20 at 10:32
  • I want to create organisation tree, where can I hide or make visible the elements of each sub -organization. Each UNIT is associated with expression to Parent 'make_line($geometry , geometry( get_feature( 'ORGANISATION', 'fid' , Parent ) ) )' "Different level" means that each unit has sub-units and sub-sub-units ... and they are associated vits "make_line" expression. The hierarhy of symbols are like "nested symbol" described here gis.stackexchange.com/questions/191146/…
    – Rii Pii
    Feb 20 at 13:50
  • Are you open to python solutions?
    – Mayo
    Feb 22 at 0:47
  • 2
    If you are open to a Python script within QGIS, you could follow this approach to build your Rule based hierarchy: gis.stackexchange.com/questions/445269/…
    – Kasper
    Feb 22 at 7:25
  • this code generates only one level, but each item can have more than one level ob sub-units
    – Rii Pii
    Feb 22 at 13:49

2 Answers 2

6

A very interesting questions, that no doubt is beyond built-in functionality. I would split this problem in to 3 parts. To go from your data to a structure that is appropriate for a rule-based tree you need some additional information in your data. If you don't apply more information for each feature, the first rule will have to hold information for all features belonging to that main-branch, and QGIS can't handle that. A suggestion would be to go from your structure with one-level information to tree-depth information on all nodes, such as this: enter image description here Now all features can be identified without searching neighboring features. I kept the code a iterative and modular as possible since that (possibly) makes it easier to reuse.

layer = iface.activeLayer()
#first section finds all the super-parents. Features that are root.
#By having super-parents we can iterate down the featurelist with a starting-point
#get all features in layer
features = layer.getFeatures()
#placeholder list
itemList = []
#Add all features to our placeholderlist
for ele in features:
    itemList.append(ele.attributes())

fidList=[e[0] for e in itemList]
super_parent_features = [e for e in itemList if e[1] not in fidList]
super_parent_features_fid = [e[0] for e in super_parent_features]

for e in itemList:
    if e[0] in super_parent_features_fid:
        e.append(0)
#itemlist now contains all superparents, which is our point of comparison

#Function that adds tree information to unassigned items by comparing with the level above
#Intentionally readability over performance
def returnParentLvl(candidates, parents, lvl):
    parents_fid = [e[0] for e in parents if len(e) == 4 and e[3] == lvl-1]
    for idx,e in enumerate(candidates):
        if e[1] in parents_fid:
            e.append(lvl)
    return(candidates, parents, lvl)
#the call below will loop the featurelist
lvl=1
while len([e for e in itemList if len(e) == 3])>0:
    #list that holds all items that hasn't been assigned a parent
    unassignedItems = [e for e in itemList if len(e) == 3]
    #adds parental information - the number of iterations must equal the level in the hierachy
    returnParentLvl(unassignedItems, itemList,lvl)
    lvl+=1
#adds information to table
layer.dataProvider().addAttributes([QgsField(f"lvl_in_hiercahy", QVariant.Int)])
layer.updateFields()

itemList.sort(key = lambda itemList: itemList[3])
lowestLvl=max([e[3] for e in itemList])

#adds grandparents to features and add column to hold ref.value
#Insert a column called lvl_in_hiercahy
for i in range(0,lowestLvl,1):
    layer.dataProvider().addAttributes([QgsField(f"lvl{i}", QVariant.String, '', 3)])
    layer.updateFields()
    for parent in itemList:
        if parent[3] == i:
            for child in itemList:
                if child[3] == i+1:
                    if parent[0]==child[1]:
                        try:
                            for grandparents in parent[4:]:
                                child.append(grandparents)
                        except:
                            pass
                        child.append(parent[1])

#Fill out table
for item in itemList:
    iterator = layer.getFeatures(QgsFeatureRequest().setFilterExpression(f""""fid"={item[0]}"""))
    attr_value={3:int(item[3])}
    feat = list(iterator)[0]
    id=feat.id()
    layer.dataProvider().changeAttributeValues({id:attr_value})
    #the 4 is a mess - it means that the iterator ignores the first 4 columns
    for i in range(4,lowestLvl+4,1):
        try:
            attr_lvl_value={i:item[i]}
            layer.dataProvider().changeAttributeValues({id:attr_lvl_value})
        except:
            pass
    layer.updateFields()

layer.triggerRepaint()

There are many simpler approaches, but I find that level could be suitable. This is sort of a flat tree. You can know create a rule-set that is simple enough for QGIS, where each level of rules is just a look-up in a single column. During testing I tried to create rule-set that would encapsulate all 'fid's' for each level, and it crashed the system. To apply a nested ruleset you can do 2 processes. Firstly create a tree by making a tree-class. Then iterate the tree and apply a nesting-level suitable for the lvl_in_hierachy. enter image description here To create the rules, with the right nesting and as few refinements as possible (to move the lifting from the renderer to the script), we can now look only two places. The variable indicating the place in the hiercahy, and the nearest parent. The code could look something like this . which should result in the image :

from qgis.PyQt.QtGui import QColor
from qgis.core import QgsRuleBasedRenderer, QgsSymbol, QgsFillSymbol, QgsSimpleMarkerSymbolLayer
from qgis.utils import iface

layer = iface.activeLayer()

# Define a class called "TreeNode"
class TreeNode:
    # Initialize the class with  attributes: "data" and "children" and "lvl"
    def __init__(self, data, lvlno):
        self.data = data
        self.children = []
        self.rule = None
        self.lvlno = 0

# Create a new instance of the TreeNode class and assign it to the variable "root"
root = TreeNode(None,0)

# Define a function to recursively create rules from a given node and add them to a parent rule
def create_rules(node, parent_rule):
   # Create a new rule for this branchlevel
    symbol = QgsMarkerSymbol.createSimple({})
    symbol.setColor(QColor.fromHsl((node.lvlno * 60) % 360, 255, 128))
    symbol.setSize(2)
    rule = QgsRuleBasedRenderer.Rule(symbol)

    # Set the label and filter expression for the rule
    label = f'{node.data}'
    rule.setLabel(label)
    if node.data is None:
        rule.setFilterExpression(f""""lvl0" is not NULL""")
    else:
        rule.setFilterExpression(f""""lvl{node.lvlno}"='{node.data}'""")

    # Add the rule to the parent rule and set it as the node's rule
    parent_rule.appendChild(rule)
    node.rule = rule

    # Recursively create rules for each child node and add them to this rule
    for child in node.children:
        create_rules(child, rule)

features = layer.getFeatures()
#Add all features to our placeholderlist

itemHold=[]
for feature in features:
    itemTemp=[]
    for c,element in enumerate(feature):
        if element != NULL and c > 3:
            itemTemp.append(element)
    itemHold.append(itemTemp)

itemSet = itemHold

# Populate the tree with the items in itemSet
for item in itemSet:
    current_node = root
    lvlno=0
    for element in item:
        found_child = False
        for child in current_node.children:
            if child.data == element:
                current_node = child
                found_child = True
                break
        if not found_child:
            new_child = TreeNode(element, lvlno)
            new_child.lvlno = lvlno
            current_node.children.append(new_child)
            current_node = new_child
        #lvl is determined by the iteration in which the node is set
        lvlno+=1

# Create the root rule and recursively create rules for the tree
symbol = QgsFillSymbol.createSimple({})
symbol.setColor(QColor.fromHsl(0, 0, 255))
root_rule = QgsRuleBasedRenderer.Rule(symbol)
root_rule.setLabel('Root Rule')
rb_renderer = QgsRuleBasedRenderer(root_rule)
create_rules(root, root_rule)

# Assign the created renderer to the layer
if rb_renderer is not None:
    layer.setRenderer(rb_renderer)

layer.triggerRepaint()

If you want the process to run by itself when you add/alter the layer you can put it in an action, or put the whole script (and a part that empties the tables) and run that manually on edits.

2
  • thank you Mathias, your pictures show exactly what I want, but I'm not a Python expert and when I run the code in QGIS Python module I get an error. exec(Path('C:/Users/ ... /org.py').read_text()) Traceback (most recent call last): File "C:\OSGeo4W\apps\Python39\lib\code.py", line 90, in runcode exec(code, self.locals) File "<input>", line 1, in <module> File "<string>", line 45, in <module> NameError: name 'lowestLvl' is not defined what is my problem?
    – Rii Pii
    Mar 18 at 14:11
  • I think I made a mistake while copy/pasting. Try the code now, I moved the line that sets lowestLvl some lines up. Feel free to ask for all the help you need, I know the code is a bit messy. Also - I can wrap it into piece of code if you'd like, but I'm afraid it'll be impossible to maintain (although it'll be much shorter).
    – Mathias
    Mar 18 at 22:56
6

Sort of workaround solution:

  1. Create a Virtual layer through Layer > Add Layer > Add/Edit Virtual Layer with the following SQL code :

    WITH RECURSIVE org(fid, unit, parent, level, geom) AS (
        SELECT
            "FID", "UNIT", "PARENT", 0, geometry
        FROM organization
        WHERE "PARENT" IS NULL
        UNION ALL
        SELECT
            o."FID", o."UNIT", o."PARENT", org.level + 1, o.geometry
        FROM organization o, org
        WHERE o."PARENT" = org.fid
    )
    
    SELECT *
    FROM org
    
  2. Load the virtual layer

  3. Style this layer with Categorized renderer on the new level field :

    hierarchical style

  4. You can check, and uncheck the different levels to show them on the map.

1
  • this answer was helpful but it's still not what I needed... I still wanted the org tree, not the view of the different levels of the org
    – Rii Pii
    Mar 4 at 8:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.