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

Taras
  • 32,823
  • 4
  • 66
  • 137
Rii Pii
  • 425
  • 2
  • 12
  • 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. – Kadir Şahbaz Feb 20 '23 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 https://gis.stackexchange.com/questions/191146/how-to-use-nested-styles-in-qgis – Rii Pii Feb 20 '23 at 13:50
  • Are you open to python solutions? – Mayo Feb 22 '23 at 00:47
  • 2
    If you are open to a Python script within QGIS, you could follow this approach to build your Rule based hierarchy: https://gis.stackexchange.com/questions/445269/update-rule-based-symbology-qgis/445391#445391 – Kasper Feb 22 '23 at 07:25
  • this code generates only one level, but each item can have more than one level ob sub-units – Rii Pii Feb 22 '23 at 13:49
  • If I understood correctly what you ask, I want to say that QGIS and other GIS softwares are not suitable and not designed for making such a design as you mention. To make a hierarchy like that, you need either to make many copies of the layer (within the Layers panel) and use a separate expression for each copy, or save the points in separate data files according to their levels, load them into QGIS. Even in this case, I'm not sure if you can do what you need. – Kadir Şahbaz Feb 25 '23 at 12:17
  • I have done something similar, but had more structured data. Can you upload a stylefile? – Mathias Mar 03 '23 at 17:14
  • Geopackage with style file inside can download here https://drive.google.com/file/d/1IPyO3oDY2V5czTpUvDmmYU8DSxaPwzLI/view – Rii Pii Mar 04 '23 at 08:18
  • On a sidenhen. Whats with the lines in you screenshot? Do you generate them? – Mathias Mar 17 '23 at 16:46

2 Answers2

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.

Taras
  • 32,823
  • 4
  • 66
  • 137
J. Monticolo
  • 15,695
  • 1
  • 29
  • 64
  • 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 04 '23 at 08:28
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.

Mathias
  • 247
  • 1
  • 7
  • 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 "", line 1, in File "", line 45, in NameError: name 'lowestLvl' is not defined

    what is my problem?

    – Rii Pii Mar 18 '23 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 '23 at 22:56