2

I'm having an issue where when I try to run my script through user scripts in processing tools, I am unable to properly join two attribute tables from two separate layers when the layer that I want to join has the same name in subsequent runs of the same script.

When the script is run the first time, the tables are joined properly. However, unless the layer name of the first layer that I want the second table to be joined to is changed before the script is run the second time, the join does not work despite getting no errors.

  • I did not include all of my code, only the parts that are relevant to the join (The blocks that actually control the layers in the layer panel, removal of the layer of the same name, and the join itself). I can provide more code if required.
  • I have not refactored the code or focused on efficiency as of yet, I simply want to get the functionality down first. That being said, I welcome any constructive suggestions (within the scope of my question).
  • The idea is that a user can use the same name for the layer as many times as they want with a successful join. This is currently not happening. If a user uses a different name for the layer, there are no problems.

EDIT:

  • I included my entire code, however for convenience I broke it into three distinct sections so that someone read and understand where the join is happening without searching. I tried my best with the comments to be as clear as I can for what the code is doing, please let me know if something is unclear.
  • I know the code is fairly ugly and needs improving. I'm still fairly new to QGIS, so I'm really just focused on functionality right now.

CODE

Creating the layer

##Huff_Model_Layer=vector
##Mall=field Huff_Model_Layer
##Census_Layer=vector
##Mall_Layer_Name= string test
##PDF_file=output file pdf

from qgis.core import *
from qgis.gui import *
from qgis.utils import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import os

# Load the huff model layer
ori_huff_model = processing.getObject(Huff_Model_Layer)
# Load the census layer
census_layer = processing.getObject(Census_Layer)
# Choose the mall (field in table) to find the primary and secondary market 
# areas for
mall = ori_huff_model.fieldNameIndex(Mall)

# Make a copy of the huff model layer in memory to work with so that all 
# changes made to copy only keeping original data intact
huff_model = QgsVectorLayer("Polygon?crs=epsg:4326", Mall_Layer_Name, "memory")
huff_model_data = huff_model.dataProvider()

huff_model.startEditing()

attr = ori_huff_model.dataProvider().fields().toList()
huff_model_data.addAttributes(attr)
huff_model.updateFields()

feat = QgsFeature()
for elem in ori_huff_model.getFeatures():
    feat.setGeometry(elem.geometry())
    feat.setAttributes(elem.attributes())
    huff_model.addFeatures([feat])
    huff_model.updateExtents()

# Find the index of the mall(field) that will be used to find the correct 
# probabilities within that field
for field in huff_model.fields():
    field_id = huff_model.fieldNameIndex(field.name())
    if mall == field.name():
        mall = huff_model.fieldNameIndex(field.name())
        break

col_num = int(mall)

# Create two new fields that will only show the primary and secondary market 
# areas based on what is chosen as primary and secondary marketer 
# probabilities
primary = QgsField('Primary', QVariant.Double)
huff_model.addAttribute(primary)
index_pri = huff_model.fieldNameIndex('Primary')

secondary = QgsField('Secondary', QVariant.Double)
huff_model.addAttribute(secondary)
index_sec = huff_model.fieldNameIndex('Secondary')

# Find the relevant features for the chosen mall
for feature in huff_model.getFeatures():
    probability = feature.attributes()
    ctuid = feature["CTUID"]
    if not probability[col_num]:
        print ("This mall does not exist")
    else:
        # Primary market values will go into the "Primary" field
        if probability[col_num] >= .6:
            huff_model.changeAttributeValue(feature.id(), index_pri, probability[col_num])
        # Secondary market values will go into the "Secondary" field
        if .4 <= probability[col_num] < .6:
            huff_model.changeAttributeValue(feature.id(), index_sec, probability[col_num])

# Delete all other fields except for the CTUID, Primary, Secondary columns
fields = []
fieldnames = {'CTUID', 'Primary', 'Secondary'}
for field in huff_model.fields():
    if field.name() not in fieldnames:
        fields.append(huff_model.fieldNameIndex(field.name()))
        #print(fields.append(huff_model.fieldNameIndex(field.name())))
huff_model.deleteAttributes(fields)

# Delete all other features (rows) that do not have a probability in the  
# primary and the secondary fields
expr = QgsExpression("\"Primary\" is NULL and \"Secondary\" is NULL")
for f in huff_model.getFeatures(QgsFeatureRequest(expr)):
    huff_model.deleteFeature(f.id())

huff_model.commitChanges()

Setting up the Join

# Remove the previously created layer from when the script is run. I only 
# want one joined layer to show in the panel at any given time. That is, if 
# someone ran the script multiple times, I want to avoid stacking new  
# layers as a PDF output is created each time for the attribute table of the 
# joined layer.

root = QgsProject.instance().layerTreeRoot()
ids = root.findLayerIds()
last_layer = root.findLayer(ids[0])
print last_layer.name()
root.removeChildNode(last_layer)

# Add newly created layer to map. A layer I have created (using code not 
# included here) is added to layer panel so that a join is possible. 

reg = QgsMapLayerRegistry.instance()
reg.addMapLayer(huff_model)

# Set the appropriate layers for the join. Mall_Layer_Name is the name for 
# the huff_model layer
targetLyr = QgsMapLayerRegistry.instance()
targetLyr.mapLayersByName(Mall_Layer_Name)[0] # Mall_Layer_Name is a string 
censusLyr = QgsMapLayerRegistry.instance()
censusLyr.mapLayersByName(census_layer.name())[0]

# Set properties for the join
targetField = 'CTUID'
inField = 'CTUID'
joinObject = QgsVectorJoinInfo()
joinObject.joinLayerId = censusLyr.id()
joinObject.joinFieldName = inField
joinObject.targetFieldName = targetField
joinObject.memoryCache = True
print(targetLyr.addJoin(joinObject))  # You should get True as response.
targetLyr.addJoin(joinObject)

Creating the PDF output

# Make sure that the newly created and joined layer is the active layer.
huff_model = iface.activeLayer()

# Create the attribute table using the map composer in QGIS and show the 
# output in PDF format.
mapRenderer = iface.mapCanvas().mapRenderer()
c = QgsComposition(mapRenderer)
c.setPlotStyle(QgsComposition.Print)
x, y = 0, 0
w, h = c.paperWidth(), c.paperHeight()
composerMap = QgsComposerMap(c, x, y, w, h)

table = QgsComposerAttributeTable(c)
table.setComposerMap(composerMap)
table.setVectorLayer(huff_model)
table.setMaximumNumberOfFeatures(huff_model.featureCount())
c.addItem(table)

# Checks for the existence of a file with the same name and removes it. 
if os.path.isfile(PDF_file):
    os.remove(PDF_file)

# Set printer properties
printer = QPrinter()
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setOutputFileName(PDF_file)
printer.setPaperSize(QSizeF(c.paperWidth(), c.paperHeight()), QPrinter.Millimeter)
printer.setFullPage(True)

# Create the PDF output and close printer
pdfPainter = QPainter(printer)
paperRectMM = printer.pageRect(QPrinter.Millimeter)
paperRectPixel = printer.pageRect(QPrinter.DevicePixel)
c.render(pdfPainter, paperRectPixel, paperRectMM)
pdfPainter.end()

The image below illustrates the final joined tabled, the script parameters, and layer panel. This image is for the first run and is successful.

First Run: Successful join

enter image description here

The image below is exactly the same as the first image except for the fact that the seen test layer is a new layer despite having the same name. Every other parameter is exactly the same. Also notice that the join did not work.Subsequent runs after the second run have the same result if the layer name is not changed.

Second Run: Unsuccessful join

enter image description here

  • Could you please edit your post to include how you are creating the output? – Joseph Jan 8 '18 at 11:00
  • 1
    Done. I wasn't sure if you meant the layer output or the PDF, so I included all the code. It's broken up into three section for your convenience. Thanks! – ShockDoctor Jan 8 '18 at 18:21
1

Too long for a comment but my guess is when joining, it is still referencing the old layer which was removed. What you could try is to use a try except method which:

  • Searches for an existing layer with the name given in the Mall_Layer_Name parameter.
    • If found then it will be removed and the reference to it deleted. Otherwise;
  • It will remove the first layer.

Put the following at the beginning of your script (and after your imports):

try:
    last_layer = QgsMapLayerRegistry.instance().mapLayersByName(Mall_Layer_Name)[0]
    QgsMapLayerRegistry.instance().removeMapLayers([last_layer])
    del last_layer
except IndexError:
    root = QgsProject.instance().layerTreeRoot()
    ids = root.findLayerIds()
    last_layer = root.findLayer(ids[0])
    print last_layer.name()
    root.removeChildNode(last_layer)

And remove the same code you used when you're setting up the join (so that the first layer is not removed twice).

  • 1
    Works like a charm! If you wouldn't mind, do you think you could explain to me why my way wasn't working? I thought that removing by the root class also deletes it (but I guess it doesn't?). Is your way the best way to delete layers so that you don't accidentally reference an old layer? Thanks again! – ShockDoctor Jan 9 '18 at 22:45
  • @OskiTuranoglu - Most welcome, glad it helped! Can't say for certain but I assume that using QgsLayerTreeGroup::removeChildNode() only removes the node from the panel; whereas QgsMapLayerRegistry::removeMapLayers() removes it entirely from the registry. I think this method is the best way to remove layers effectively :) – Joseph Jan 10 '18 at 10:05
  • 1
    That makes a lot of sense! Thank you for the explanation as well :) – ShockDoctor Jan 10 '18 at 21:26
1

I wanted to build on the answer Joseph gave (which was totally correct!) because his code deleted the first layer on first run no matter what layer it was. This is the following code I used to ensure that no other layers but the newly created layer were deleted.

try:
    last_layer = QgsMapLayerRegistry.instance().mapLayersByName(Mall_Layer_Name)[0]
    QgsMapLayerRegistry.instance().removeMapLayers([last_layer])
    del last_layer
except IndexError:
    child1 = root.children()[1]  # Adds a condition so that the first layer 
                                 # isn't automatically deleted on the first 
                                 # run as the huff model layer isn't added yet.
    if child1.name() == census_layer.name():
        ids = root.findLayerIds()
        last_layer = root.findLayer(ids[0])
        print last_layer.name()
        root.removeChildNode(last_layer)

The if statement ensures that the first layer is deleted as long as the census layer is in the second position. The census layer was added into that position after the newly created layer was added to the registry because QGIS automatically adds layer from the top of the layers panel. On a subsequent run, the above code would find the census layer in position two and the old huff model layer in position one and proceed to remove it. Below is code I used to insert the census layer into the correct position to perform this task.

# Add newly created layer to map. Make sure you add layer first.
reg = QgsMapLayerRegistry.instance()
reg.addMapLayer(huff_model)

# Join the census layer to the newly created layer to show specific demographic data. Currently the full table is
# joined but specific columns can be chose if an entire table join is unnecessary.

# Necessary for join later 
targetLyr = QgsMapLayerRegistry.instance().mapLayersByName( Mall_Layer_Name)[0] 
# Necessary for join later and for proper positional insertion into layer panel
censusLyr = QgsMapLayerRegistry.instance().mapLayersByName(census_layer.name())[0]

# Use this block of code to insert the layer (in my case my census layer) 
# into the correct position. Make sure it's **after** the layer you added to 
# the registry
mycensusLyr = root.findLayer(censusLyr.id())
censusClone = mycensusLyr.clone()
parent = mycensusLyr.parent()
parent.insertChildNode(1, censusClone)
parent.removeChildNode(mycensusLyr)

# Join comes after this block. See original question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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