9

Essentially I am wanting to produce an atlas based on a categorical field in a point layer.

i.e. I have a point layer of childcare providers with the categorical field "Provision". I've categorized each feature in this field with "After School Club" , "Breakfast Club" etc, and I now want to produce a set of maps that iterate through each category and show only the points for each. One map of after school clubs, one map of breakfast clubs etc. The extents might be subtly different.

I could do it one by one but it seems like there should be a way to produce an atlas based on the extents of each category? (I do feel i'm missing something obvious :) )

Or alternatively is there a way of automating the creation of a polygon layer and using that as a hidden coverage for the atlas?

EDIT: I've made a little progress with this - you can use rule based styling to switch on and off features relevant to the current atlas coverage feature. it actually works fine if all you want to do is show a different set of points. I'm now looking at tying that back to a color scheme and reactive legend.

  • 1
    This is basically a duplicate of gis.stackexchange.com/questions/155143 – Chris W Jul 21 '15 at 18:27
  • Thanks Chris - but not sure if it is. That one looks to be asking if you can do a sub-atlas for each area in an original atlas? e.g. 4 areas each with 4 pages? (Though I did struggle to follow what was being asked for) – JonoPatterson Jul 21 '15 at 18:54
  • 1
    No, basically both of you want to create a map series. The series shows the same map extent and base information, but different features in each one. My comment there talks about and links to doing it in ArcGIS via what are called page definition queries - that is, each page in the atlas/mapbook has a definition query that determines which layers/features are shown on that page. He wants a series of series, where you just want a single series. However I don't know if QGIS yet offers such functionality (I thought I'd read an answer/comment that it didn't, but I can't find that now). – Chris W Jul 21 '15 at 18:57
  • Also, in your case you could generate bounding boxes based on the extents of each point sharing the same attributes and then use those as your index features, but you're still left with the problem of automatically turning on and off the different groups of points. Even if you split them to separate layers, without some sort of definition query there's no way to turn those points off on any given page. – Chris W Jul 21 '15 at 19:02
  • Yes your dead on. Its also a repeat of this one gis.stackexchange.com/questions/121802/… - so i might have to resort to doing it manually. – JonoPatterson Jul 21 '15 at 19:11
8
+200

I've finally solved this for my purposes so here's the solution I came up with if it helps anyone:

Write a python script (mine at end of this) which essentially does this:

  1. identify the unique categories in the point layer field of interest
  2. for each category, select all matching points and establish the extent of this set
  3. for each extent generate a new polygon in a blank atlas coverage layer with a key attribute "CategoryName"

This gave me the atlas coverage layer with one polygon for each category of interest looking like this: Atlas coverage layer

Configure the atlas and print composer as per normal - leaving only the issue of turning off and on features.

For this it's a little bit of trial and error to work out the exact set of options:

  1. The below expression lets you get the value currently held in the CategoryName field for the current atlas feature

    attribute ($atlasfeature, 'CategoryName') 
    
  2. Use this to create rule based styling for the point layer along the lines of

    attribute ($atlasfeature, 'CategoryName') = PointCategory AND PointCategory = "RedDots"
    
  3. I also had a rule to guarantee all others became transparent

    attribute ($atlasfeature, 'CategoryName') IS NOT PointCategory
    

Rules shown

Testing this with the atlas works really well. Finally just use the same approach to manipulate labels shown, make labels dynamic and filter tables appropriately. Ticking the 'filter legend by map content' is also very effective if you don't want all legend items on all maps.

Final atlas set:

Feature Based Atlas

Edit - as it was asked for, here's my script:

    from PyQt4.QtCore import *

#main script----------------------------------------------
    #set up the layer references - you will need to change this
targetlayer=QgsMapLayerRegistry.instance().mapLayer("AtlasExtents20150727154732521")
eylayer = QgsMapLayerRegistry.instance().mapLayer("Early_Years_Providers20150727152919862")

#establish the unique categories 
names = getUniqueAttributes(eylayer, 'Mapping_La')

#get a set of boxes
boxset = getBoundings(eylayer, names)

#ensure layer is emptied, then add bounding boxes
deleteBoxes(targetlayer)
createBoxes(targetlayer, boxset)
 #end main script----------------------------------------------   


 #------functions-------#
#gets unique set of attributes - returns a set()
def getUniqueAttributes(layer, fieldname):
    values = set()
    for feature in layer.getFeatures():
        values.add(feature[fieldname])
    return values

#quickly selects all points on a layer, given a query 
def selectionQuick(layer, queryitem):
    layer.removeSelection ()

    #hardcoded field name
    expr = QgsExpression( "\"Mapping_La\" = '" + queryitem +"'")
    it = layer.getFeatures( QgsFeatureRequest( expr ) )
    ids = [i.id() for i in it]
    layer.setSelectedFeatures( ids )

#for a set of unique items, get bounding boxes 
def getBoundings(layer, itemset):
    bboxes = {}
    for itemname in itemset:
        selectionQuick(layer,itemname)
        box = layer.boundingBoxOfSelected()
        bboxes[itemname] = box
    return bboxes

#for a layer create a bunch of boxes
def createBoxes(layer, boxes):
    id=0
    for boxkey in boxes:
        id = id +1
        box=boxes[boxkey]
        feat = QgsFeature(layer.pendingFields())
        geom = QgsGeometry.fromRect(box)
        feat.setAttribute('id', id)
        #hardcoded field name
        feat.setAttribute('CareType', boxkey)
        feat.setGeometry(geom)
        (res, outFeats) = layer.dataProvider().addFeatures([feat])

def deleteBoxes(layer):
        ids = [f.id() for f in layer.getFeatures()]
        layer.dataProvider().deleteFeatures( ids )
  • 2
    @JonoPatterson if you would now also share your python script mentioned in thebeginning, this would be the best answer ever ;) – Bernd V. Jul 28 '15 at 17:46
  • Ok will do this - though its rough n ready so it will need some tweaks (haven't done any coding for years!). What's the best way to do it - just paste in a codebox? – JonoPatterson Jul 28 '15 at 20:10
  • @JonoPatterson Thank you a lot for the script. For me as a beginner, this looks very good already :). I'm sure I will need this soon. – Bernd V. Jul 29 '15 at 23:39
  • Your example expressions are a bit wrong - it should be "$atlasfeature", not " $atlasfeatureid" – ndawson Jul 30 '15 at 20:16

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.